From b2c1bd8340fd8cf311a9f59683eacfbf5f9ad9e2 Mon Sep 17 00:00:00 2001 From: Luis <114909465+luiskcc@users.noreply.github.com> Date: Sat, 6 Dec 2025 19:33:11 +0000 Subject: [PATCH 1/3] 1. Added Analytics Event Type (src/lib/analytics.ts:139) - New event: 'apps_and_websites_blocked' 2. Added Analytics Properties (src/lib/analytics.ts:164-170) - blocked_apps_count: Number of apps being blocked - blocked_websites_count: Number of websites being blocked - total_blocked_count: Total count of blocked items - is_block_list: Whether it's a block list or allow list - blocked_app_names: Array of app names being blocked - blocked_website_names: Array of website names being blocked 3. Created Analytics Data Function (src/api/ebbApi/blockingPreferenceApi.ts:150-187) - New function getWorkflowBlockedAppsAnalytics() that: - Reuses the same logic as getWorkflowBlockedApps() to get all blocked apps - Separates apps from websites based on the is_browser flag - Returns detailed analytics data including counts and names 4. Added Tracking Call (src/pages/FlowPage/FlowPage.tsx:70-79) - Tracks the event when blocking starts - Includes all the analytics data along with workflow info - Only visible on PostHog dashboard for internal analysis Now when users start a focus session, you'll be able to see in PostHog: - How many apps/websites are being blocked per session - Which specific apps/websites are most commonly blocked - Whether users prefer block lists vs allow lists - Patterns across different workflows --- README.md | 1 - src/api/ebbApi/blockingPreferenceApi.ts | 54 +++++++++++++++++++++++-- src/hooks/usePermissions.ts | 22 +++++++++- src/lib/analytics.ts | 11 +++++ src/pages/FlowPage/FlowPage.tsx | 12 ++++++ 5 files changed, 95 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1022236b..dc99806c 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,5 @@ Ebb uses your window, mouse and keyboard activity to track your workflow habits. The monitoring service is a Rust application that runs on your computer and is responsible for recording your activities. It is designed to be run as a background service and will not interfere with your workflow. Makes use of the os-monitor crate to monitor your activities and then record them to the database on your device. -# [Monitor](https://github.com/CodeClimbersIO/os-monitor) The monitor is a Rust application that runs on your computer and is responsible for monitoring your activities. It is specifically responsible for monitoring (but not recording) your window, mouse and keyboard activity. diff --git a/src/api/ebbApi/blockingPreferenceApi.ts b/src/api/ebbApi/blockingPreferenceApi.ts index 839712d3..24778b6d 100644 --- a/src/api/ebbApi/blockingPreferenceApi.ts +++ b/src/api/ebbApi/blockingPreferenceApi.ts @@ -121,17 +121,17 @@ const getWorkflowBlockedApps = async (workflowId: string): Promise => { const defaultCategoryNames = ['social media', 'entertainment'] const allCategoryTags = await TagRepo.getTagsByType('category') - const defaultTags = allCategoryTags.filter((tag: Tag) => + const defaultTags = allCategoryTags.filter((tag: Tag) => defaultCategoryNames.includes(tag.name) ) const defaultTagIds = defaultTags.map((tag: Tag) => tag.id).filter((id): id is string => !!id) - + let defaultSearchOptions: SearchOption[] = [] if (defaultTagIds.length > 0) { const categoriesWithCounts = await TagRepo.getCategoriesWithAppCounts(defaultTagIds) defaultSearchOptions = categoriesWithCounts.map((catInfo: TagWithAppCount): SearchOption => ({ type: 'category', - tag: catInfo, + tag: catInfo, category: catInfo.name as AppCategory, count: catInfo.count })) @@ -139,10 +139,58 @@ const getDefaultSearchOptions = async (): Promise => { return defaultSearchOptions } +interface BlockingAnalyticsData { + blocked_apps_count: number + blocked_websites_count: number + total_blocked_count: number + blocked_app_names: string[] + blocked_website_names: string[] +} + +const getWorkflowBlockedAppsAnalytics = async (workflowId: string): Promise => { + const selectedApps = await getWorkflowBlockingPreferencesAsSearchOptions(workflowId) + + const directAppSelections: App[] = selectedApps + .filter((option): option is Extract => option.type === 'app' && !!option.app) + .map(option => option.app) + + const categorySelections: Tag[] = selectedApps + .filter((option): option is Extract => option.type === 'category' && !!option.tag) + .map(option => option.tag) + + const categoryTagIds = categorySelections.map(tag => tag.id).filter(id => !!id) as string[] + + const appsFromCategories = categoryTagIds.length > 0 + ? await AppRepo.getAppsByCategoryTags(categoryTagIds) + : [] + + const allAppsToConsider = [...directAppSelections, ...appsFromCategories] + + const uniqueAppsMap = new Map() + allAppsToConsider.forEach(app => { + if (app && app.id) { + uniqueAppsMap.set(app.id, app) + } + }) + const uniqueApps = Array.from(uniqueAppsMap.values()) + + const apps = uniqueApps.filter(app => !app.is_browser) + const websites = uniqueApps.filter(app => app.is_browser) + + return { + blocked_apps_count: apps.length, + blocked_websites_count: websites.length, + total_blocked_count: uniqueApps.length, + blocked_app_names: apps.map(app => app.name), + blocked_website_names: websites.map(app => app.name), + } +} + export const BlockingPreferenceApi = { getWorkflowBlockingPreferencesAsSearchOptions, saveWorkflowBlockingPreferences, getWorkflowBlockedApps, getDefaultSearchOptions, + getWorkflowBlockedAppsAnalytics, } diff --git a/src/hooks/usePermissions.ts b/src/hooks/usePermissions.ts index 79074708..5c5ac3a4 100644 --- a/src/hooks/usePermissions.ts +++ b/src/hooks/usePermissions.ts @@ -3,13 +3,33 @@ import { useAuth } from './useAuth' import { defaultPermissions } from '@/api/ebbApi/licenseApi' import { usePermissionsStore } from '@/lib/stores/permissionsStore' import { useEffect } from 'react' +import { isDev } from '@/lib/utils/environment.util' + +const devPermissions = { + canUseHardDifficulty: true, + canUseAllowList: true, + canUseMultipleProfiles: true, + canUseMultipleDevices: true, + canUseSmartFocus: true, + canUseSlackIntegration: true, + canScheduleSessions: true, + hasProAccess: true, +} export const usePermissions = () => { const { user } = useAuth() const { data: licenseData } = useLicenseWithDevices(user?.id || null) const setPermissions = usePermissionsStore((state) => state.setPermissions) - const permissions = licenseData?.permissions || defaultPermissions + // In dev mode, grant all pro permissions + const isDevMode = isDev() + console.log('[usePermissions] isDev:', isDevMode, 'import.meta.env.DEV:', import.meta.env.DEV) + + const permissions = isDevMode + ? devPermissions + : (licenseData?.permissions || defaultPermissions) + + console.log('[usePermissions] permissions:', permissions) // Sync to store whenever permissions change useEffect(() => { diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index c813b3ed..c10bce0f 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -135,6 +135,9 @@ export type AnalyticsEvent = | 'activity_history_delete_all_clicked' | 'activity_history_page_changed' + // Blocking Events + | 'apps_and_websites_blocked' + export interface AnalyticsEventProperties { // Focus session properties difficulty?: 'easy' | 'medium' | 'hard' @@ -157,6 +160,14 @@ export interface AnalyticsEventProperties { // Billing properties days_remaining?: number + + // Blocking properties + blocked_apps_count?: number + blocked_websites_count?: number + total_blocked_count?: number + is_block_list?: boolean + blocked_app_names?: string[] + blocked_website_names?: string[] } const trackEvent = ( diff --git a/src/pages/FlowPage/FlowPage.tsx b/src/pages/FlowPage/FlowPage.tsx index 92a6d51d..2a4f82ec 100644 --- a/src/pages/FlowPage/FlowPage.tsx +++ b/src/pages/FlowPage/FlowPage.tsx @@ -32,6 +32,7 @@ import { EbbWorker } from '@/lib/ebbWorker' import { Timer } from './Timer' import { MonitorApi } from '@/api/monitorApi/monitorApi' import { StorageUtils } from '../../lib/utils/storage.util' +import { AnalyticsService } from '@/lib/analytics' type CurrentTrack = { song: { @@ -65,6 +66,17 @@ const startBlocking = async (workflow: Workflow) => { // start blocking const isBlockList = !workflow.settings.isAllowList await invoke('start_blocking', { blockingApps, isBlockList }) + + // Track analytics for PostHog + if (workflow.id) { + const analyticsData = await BlockingPreferenceApi.getWorkflowBlockedAppsAnalytics(workflow.id) + AnalyticsService.trackEvent('apps_and_websites_blocked', { + ...analyticsData, + is_block_list: isBlockList, + workflow_id: workflow.id, + workflow_name: workflow.name, + }) + } } export const FlowPage = () => { From f0a8c21a429123b6c9fae1ecf302b41896e96e24 Mon Sep 17 00:00:00 2001 From: Luis <114909465+luiskcc@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:23:09 +0000 Subject: [PATCH 2/3] Track individual app/website block attempts in PostHog Added PostHog event tracking to monitor when users attempt to access blocked apps/websites during focus sessions. Changes: - Added 'app_or_website_block_attempt' event type in analytics.ts - Added 'blocked_item_name' property to track specific blocked items - Listen to 'on-app-blocked' event in useNotificationListener.ts - Extract trackBlockedApps() helper function for cleaner code - Use app_external_id for websites (URLs) and app_name for apps - Each block attempt fires a separate PostHog event --- src/api/ebbApi/blockingPreferenceApi.ts | 48 ------------------------- src/hooks/useNotificationListener.ts | 30 +++++++++++++++- src/lib/analytics.ts | 11 ++---- src/pages/FlowPage/FlowPage.tsx | 12 ------- 4 files changed, 32 insertions(+), 69 deletions(-) diff --git a/src/api/ebbApi/blockingPreferenceApi.ts b/src/api/ebbApi/blockingPreferenceApi.ts index 24778b6d..b262d13e 100644 --- a/src/api/ebbApi/blockingPreferenceApi.ts +++ b/src/api/ebbApi/blockingPreferenceApi.ts @@ -139,58 +139,10 @@ const getDefaultSearchOptions = async (): Promise => { return defaultSearchOptions } -interface BlockingAnalyticsData { - blocked_apps_count: number - blocked_websites_count: number - total_blocked_count: number - blocked_app_names: string[] - blocked_website_names: string[] -} - -const getWorkflowBlockedAppsAnalytics = async (workflowId: string): Promise => { - const selectedApps = await getWorkflowBlockingPreferencesAsSearchOptions(workflowId) - - const directAppSelections: App[] = selectedApps - .filter((option): option is Extract => option.type === 'app' && !!option.app) - .map(option => option.app) - - const categorySelections: Tag[] = selectedApps - .filter((option): option is Extract => option.type === 'category' && !!option.tag) - .map(option => option.tag) - - const categoryTagIds = categorySelections.map(tag => tag.id).filter(id => !!id) as string[] - - const appsFromCategories = categoryTagIds.length > 0 - ? await AppRepo.getAppsByCategoryTags(categoryTagIds) - : [] - - const allAppsToConsider = [...directAppSelections, ...appsFromCategories] - - const uniqueAppsMap = new Map() - allAppsToConsider.forEach(app => { - if (app && app.id) { - uniqueAppsMap.set(app.id, app) - } - }) - const uniqueApps = Array.from(uniqueAppsMap.values()) - - const apps = uniqueApps.filter(app => !app.is_browser) - const websites = uniqueApps.filter(app => app.is_browser) - - return { - blocked_apps_count: apps.length, - blocked_websites_count: websites.length, - total_blocked_count: uniqueApps.length, - blocked_app_names: apps.map(app => app.name), - blocked_website_names: websites.map(app => app.name), - } -} - export const BlockingPreferenceApi = { getWorkflowBlockingPreferencesAsSearchOptions, saveWorkflowBlockingPreferences, getWorkflowBlockedApps, getDefaultSearchOptions, - getWorkflowBlockedAppsAnalytics, } diff --git a/src/hooks/useNotificationListener.ts b/src/hooks/useNotificationListener.ts index c0c72d59..c0f0c953 100644 --- a/src/hooks/useNotificationListener.ts +++ b/src/hooks/useNotificationListener.ts @@ -7,6 +7,25 @@ import { useNavigate } from 'react-router-dom' import { SmartSessionApi } from '@/api/ebbApi/smartSessionApi' import { useFlowTimer } from '@/lib/stores/flowTimer' import { FlowSessionApi } from '@/api/ebbApi/flowSessionApi' +import { AnalyticsService } from '@/lib/analytics' + +interface BlockedApp { + app_name: string + app_external_id: string +} + +const trackBlockedApps = (blockedApps: BlockedApp[], workflowId?: string, workflowName?: string) => { + blockedApps.forEach((app) => { + // For websites, use app_external_id (the URL), for apps use app_name + const blockedName = app.app_external_id || app.app_name + + AnalyticsService.trackEvent('app_or_website_block_attempt', { + blocked_item_name: blockedName, + workflow_id: workflowId, + workflow_name: workflowName, + }) + }) +} export const useNotificationListener = () => { const navigate = useNavigate() @@ -55,17 +74,26 @@ export const useNotificationListener = () => { window.dispatchEvent(new Event('end-session')) }) }) - unlistenAppBlocked = await listen('on-app-blocked', async () => { + unlistenAppBlocked = await listen('on-app-blocked', async (event: { payload: { blocked_apps: BlockedApp[] } }) => { info('App: app blocked') EbbWorker.debounceWork(async () => { try { const session = await FlowSessionApi.getInProgressFlowSessionWithWorkflow() const isHardMode = session?.workflow_json?.settings.difficulty === 'hard' + if (isHardMode) { await invoke('show_notification', { notificationType: 'blocked-app-hard' }) } else { await invoke('show_notification', { notificationType: 'blocked-app' }) } + + if (event.payload?.blocked_apps) { + trackBlockedApps( + event.payload.blocked_apps, + session?.workflow_id, + session?.workflow_json?.name + ) + } } catch (error) { console.error(`Error getting in progress flow session with workflow: ${error}`, error) } diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index c10bce0f..a18a9d45 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -136,7 +136,7 @@ export type AnalyticsEvent = | 'activity_history_page_changed' // Blocking Events - | 'apps_and_websites_blocked' + | 'app_or_website_block_attempt' export interface AnalyticsEventProperties { // Focus session properties @@ -161,13 +161,8 @@ export interface AnalyticsEventProperties { // Billing properties days_remaining?: number - // Blocking properties - blocked_apps_count?: number - blocked_websites_count?: number - total_blocked_count?: number - is_block_list?: boolean - blocked_app_names?: string[] - blocked_website_names?: string[] + // Block attempt properties + blocked_item_name?: string } const trackEvent = ( diff --git a/src/pages/FlowPage/FlowPage.tsx b/src/pages/FlowPage/FlowPage.tsx index 2a4f82ec..92a6d51d 100644 --- a/src/pages/FlowPage/FlowPage.tsx +++ b/src/pages/FlowPage/FlowPage.tsx @@ -32,7 +32,6 @@ import { EbbWorker } from '@/lib/ebbWorker' import { Timer } from './Timer' import { MonitorApi } from '@/api/monitorApi/monitorApi' import { StorageUtils } from '../../lib/utils/storage.util' -import { AnalyticsService } from '@/lib/analytics' type CurrentTrack = { song: { @@ -66,17 +65,6 @@ const startBlocking = async (workflow: Workflow) => { // start blocking const isBlockList = !workflow.settings.isAllowList await invoke('start_blocking', { blockingApps, isBlockList }) - - // Track analytics for PostHog - if (workflow.id) { - const analyticsData = await BlockingPreferenceApi.getWorkflowBlockedAppsAnalytics(workflow.id) - AnalyticsService.trackEvent('apps_and_websites_blocked', { - ...analyticsData, - is_block_list: isBlockList, - workflow_id: workflow.id, - workflow_name: workflow.name, - }) - } } export const FlowPage = () => { From d8f4f31e9e2c16245c5cdc131b44810f648aba5d Mon Sep 17 00:00:00 2001 From: Luis <114909465+luiskcc@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:35:13 +0000 Subject: [PATCH 3/3] refactored code and rm console.logs --- README.md | 2 +- src/hooks/usePermissions.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index dc99806c..28005792 100644 --- a/README.md +++ b/README.md @@ -30,5 +30,5 @@ Ebb uses your window, mouse and keyboard activity to track your workflow habits. The monitoring service is a Rust application that runs on your computer and is responsible for recording your activities. It is designed to be run as a background service and will not interfere with your workflow. Makes use of the os-monitor crate to monitor your activities and then record them to the database on your device. - +# [Monitor](https://github.com/CodeClimbersIO/os-monitor) The monitor is a Rust application that runs on your computer and is responsible for monitoring your activities. It is specifically responsible for monitoring (but not recording) your window, mouse and keyboard activity. diff --git a/src/hooks/usePermissions.ts b/src/hooks/usePermissions.ts index 5c5ac3a4..cf33e98d 100644 --- a/src/hooks/usePermissions.ts +++ b/src/hooks/usePermissions.ts @@ -23,14 +23,10 @@ export const usePermissions = () => { // In dev mode, grant all pro permissions const isDevMode = isDev() - console.log('[usePermissions] isDev:', isDevMode, 'import.meta.env.DEV:', import.meta.env.DEV) - const permissions = isDevMode ? devPermissions : (licenseData?.permissions || defaultPermissions) - console.log('[usePermissions] permissions:', permissions) - // Sync to store whenever permissions change useEffect(() => { setPermissions(permissions)