Skip to content

Commit 5afccb5

Browse files
feat(onboarding): track dashboard exploration (#3050)
* feat(onboarding): track dashboard exploration * fix(onboarding): scope dashboard exploration event * fix(onboarding): buffer dashboard exploration tracking * fix(onboarding): track demo dashboard exploration * test(onboarding): bound demo exploration assertion * test(onboarding): require demo exploration signal
1 parent f464449 commit 5afccb5

7 files changed

Lines changed: 87 additions & 1 deletion

src/components/Sidebar.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useMainStore } from '~/stores/main'
1818
import {
1919
allowOnboardingDashboardExploration,
2020
getOnboardingResumeAppId,
21+
ONBOARDING_DASHBOARD_EXPLORED_EVENT,
2122
shouldConfirmOnboardingDashboardExploration,
2223
} from '~/utils/onboardingRedirect'
2324
import DropdownProfile from '../components/dashboard/DropdownProfile.vue'
@@ -96,6 +97,7 @@ async function openTab(tab: Tab) {
9697
if (dialogStore.lastButtonRole !== 'primary')
9798
return
9899
100+
window.dispatchEvent(new Event(ONBOARDING_DASHBOARD_EXPLORED_EVENT))
99101
allowOnboardingDashboardExploration(onboardingUserId, onboardingResumeAppId)
100102
}
101103

src/components/dashboard/AppOnboardingFlow.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import {
4848
loadOnboardingAppDraft,
4949
} from '~/utils/onboardingAppDraft'
5050
import { createOnboardingDetailsFieldDebouncer, createOnboardingProgressTracker } from '~/utils/onboardingProgressAnalytics'
51-
import { allowOnboardingDashboardExploration } from '~/utils/onboardingRedirect'
51+
import { allowOnboardingDashboardExploration, ONBOARDING_DASHBOARD_EXPLORED_EVENT } from '~/utils/onboardingRedirect'
5252
import { slugifyOnboardingSegment } from '~/utils/onboardingSlug'
5353
import AppOnboardingIconInput from './AppOnboardingIconInput.vue'
5454
@@ -286,6 +286,7 @@ const setupTitle = computed(() => usesBuilderSetupCommand.value ? t('unified-onb
286286
const setupSubtitle = computed(() => usesBuilderSetupCommand.value ? t('unified-onboarding-setup-builder-subtitle') : t('unified-onboarding-setup-ota-subtitle'))
287287
288288
let progressTracker: ReturnType<typeof createOnboardingProgressTracker> | null = null
289+
let pendingDashboardExplored = false
289290
290291
function trackV2DetailsEvent(name: OnboardingDetailsEvent, details: OnboardingDetailsEventProperties = {}) {
291292
if (props.preOrg)
@@ -306,6 +307,8 @@ function initializeProgressTracking(resumed: boolean) {
306307
supaHost: config.supaHost,
307308
})
308309
progressTracker.viewStep(flowStep.value)
310+
if (pendingDashboardExplored)
311+
trackDashboardExplored()
309312
}
310313
311314
function completeAndViewStep(nextStep: OnboardingFlowStep, completionProperties: OnboardingStepCompletionProperties = {}) {
@@ -1010,6 +1013,7 @@ async function seedDemoData() {
10101013
throw error
10111014
}
10121015
1016+
window.dispatchEvent(new Event(ONBOARDING_DASHBOARD_EXPLORED_EVENT))
10131017
allowOnboardingDashboardExploration(onboardingUserId.value, createdApp.value.app_id)
10141018
dashboardAppsStore.upsertApp({
10151019
app_id: createdApp.value.app_id,
@@ -1124,11 +1128,22 @@ function openDashboard() {
11241128
appId: createdApp.value.app_id,
11251129
})
11261130
}
1131+
window.dispatchEvent(new Event(ONBOARDING_DASHBOARD_EXPLORED_EVENT))
11271132
allowOnboardingDashboardExploration(onboardingUserId.value, createdApp.value.app_id)
11281133
router.push(`/app/${encodeURIComponent(createdApp.value.app_id)}/getting-started`)
11291134
}
11301135
1136+
function trackDashboardExplored() {
1137+
if (!progressTracker) {
1138+
pendingDashboardExplored = true
1139+
return
1140+
}
1141+
pendingDashboardExplored = false
1142+
progressTracker?.trackDashboardExplored(createdApp.value?.app_id)
1143+
}
1144+
11311145
onMounted(async () => {
1146+
window.addEventListener(ONBOARDING_DASHBOARD_EXPLORED_EVENT, trackDashboardExplored)
11321147
let resumedFlow = false
11331148
isLoading.value = true
11341149
try {
@@ -1162,6 +1177,7 @@ onMounted(async () => {
11621177
})
11631178
11641179
onBeforeUnmount(() => {
1180+
window.removeEventListener(ONBOARDING_DASHBOARD_EXPLORED_EVENT, trackDashboardExplored)
11651181
detailsFieldTracker.dispose()
11661182
11671183
if (localIconPreview.value.startsWith('blob:'))

src/utils/onboardingProgressAnalytics.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,23 @@ export function createOnboardingProgressTracker(options: CreateOnboardingProgres
206206
return eventProperties
207207
}
208208

209+
function trackDashboardExplored(appId?: string) {
210+
if (!activeStep)
211+
return
212+
213+
const properties = sharedProperties(activeStep)
214+
if (!properties)
215+
return
216+
if (appId)
217+
properties.app_id = appId
218+
219+
safelyCapture('onboarding_dashboard_explored', properties)
220+
}
221+
209222
return {
210223
completeStep,
211224
trackCopyEvent,
225+
trackDashboardExplored,
212226
trackDetailsEvent,
213227
viewStep,
214228
}

src/utils/onboardingRedirect.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// August uses Central European Summer Time (UTC+2).
22
export const ONBOARDING_REDIRECT_CUTOFF = Date.parse('2026-08-04T01:00:00+02:00')
3+
export const ONBOARDING_DASHBOARD_EXPLORED_EVENT = 'capgo:onboarding-dashboard-explored'
34

45
const DASHBOARD_EXPLORATION_STORAGE_KEY = 'capgo:onboarding-dashboard-exploration'
56

tests/app-onboarding-progress-integration.unit.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs'
22
import { describe, expect, it } from 'vitest'
33

44
const onboardingSource = readFileSync(new URL('../src/components/dashboard/AppOnboardingFlow.vue', import.meta.url), 'utf8')
5+
const sidebarSource = readFileSync(new URL('../src/components/Sidebar.vue', import.meta.url), 'utf8')
56

67
function sourceBetween(start: string, end: string) {
78
return onboardingSource.slice(onboardingSource.indexOf(start), onboardingSource.indexOf(end))
@@ -92,5 +93,19 @@ describe('app onboarding progress analytics integration', () => {
9293
expect(dashboardExit).toContain('appId: createdApp.value.app_id')
9394
expect(dashboardExit).toContain('/getting-started')
9495
expect(dashboardExit.indexOf('completeStep')).toBeLessThan(dashboardExit.indexOf('router.push'))
96+
expect(dashboardExit).toContain('window.dispatchEvent(new Event(ONBOARDING_DASHBOARD_EXPLORED_EVENT))')
97+
expect(onboardingSource).toContain('progressTracker?.trackDashboardExplored(createdApp.value?.app_id)')
98+
expect(onboardingSource).toContain('window.addEventListener(ONBOARDING_DASHBOARD_EXPLORED_EVENT, trackDashboardExplored)')
99+
expect(onboardingSource).toContain('window.removeEventListener(ONBOARDING_DASHBOARD_EXPLORED_EVENT, trackDashboardExplored)')
100+
expect(onboardingSource).toContain('pendingDashboardExplored = true')
101+
expect(onboardingSource).toContain('if (pendingDashboardExplored)')
102+
103+
const demoExit = sourceBetween('async function seedDemoData()', 'async function copyText(')
104+
expect(demoExit).toContain('window.dispatchEvent')
105+
expect(demoExit).toContain('allowOnboardingDashboardExploration')
106+
expect(demoExit.indexOf('window.dispatchEvent')).toBeLessThan(demoExit.indexOf('allowOnboardingDashboardExploration'))
107+
108+
const confirmedSidebarExit = sidebarSource.slice(sidebarSource.indexOf('if (requiresOnboardingExplorationConfirmation)'), sidebarSource.indexOf('if (tab.onClick)'))
109+
expect(confirmedSidebarExit.indexOf("lastButtonRole !== 'primary'")).toBeLessThan(confirmedSidebarExit.indexOf('window.dispatchEvent(new Event(ONBOARDING_DASHBOARD_EXPLORED_EVENT))'))
95110
})
96111
})

tests/onboarding-progress-analytics.unit.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,33 @@ describe('onboarding progress analytics', () => {
176176
expect(properties).toEqual(capture.mock.calls[0]?.[2])
177177
})
178178

179+
it.concurrent('captures dashboard exploration with the active onboarding attempt context', () => {
180+
const capture = vi.fn()
181+
const tracker = createOnboardingProgressTracker({
182+
capture,
183+
flow: 'pre_org',
184+
resumed: true,
185+
steps,
186+
supaHost: 'https://supabase.capgo.test',
187+
})
188+
tracker.viewStep('setup')
189+
capture.mockClear()
190+
191+
tracker.trackDashboardExplored('com.example.app')
192+
193+
expect(capture).toHaveBeenCalledWith(
194+
'onboarding_dashboard_explored',
195+
'https://supabase.capgo.test',
196+
expect.objectContaining({
197+
app_id: 'com.example.app',
198+
onboarding_attempt_id: expect.any(String),
199+
onboarding_version: ONBOARDING_ANALYTICS_VERSION,
200+
resumed: true,
201+
step: 'setup',
202+
}),
203+
)
204+
})
205+
179206
it.concurrent('deduplicates completion for one visit and resets timing after back navigation', () => {
180207
let now = 10
181208
const capture = vi.fn()

tests/onboarding-redirect.unit.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ describe('onboarding dashboard redirect', () => {
7777
expect(refreshedModule.getOnboardingResumeRedirect({ appId: 'com.example.app', appCount: 1, createdAt: eligibleUser, organizationCount: 1, path: '/apps', resumeAppId: null, userId: 'user-1' })).toBeNull()
7878
})
7979

80+
it('keeps the shared exploration grant free of analytics signals', async () => {
81+
const module = await import('../src/utils/onboardingRedirect.ts')
82+
const listener = vi.fn()
83+
window.addEventListener(module.ONBOARDING_DASHBOARD_EXPLORED_EVENT, listener)
84+
85+
module.allowOnboardingDashboardExploration('user-1', 'com.example.app')
86+
87+
expect(listener).not.toHaveBeenCalled()
88+
window.removeEventListener(module.ONBOARDING_DASHBOARD_EXPLORED_EVENT, listener)
89+
})
90+
8091
it('ignores a null grant so a missing user id cannot clear exploration', async () => {
8192
const module = await import('../src/utils/onboardingRedirect.ts')
8293
module.allowOnboardingDashboardExploration('user-1', 'com.example.app')

0 commit comments

Comments
 (0)