Skip to content

Commit 4cb44e9

Browse files
feat(telemetry): split nudge events by how the tour ended (#15107)
@MaanilVerma — **your call entirely; close it if you'd rather not.** I owe you context on why it's arriving as a PR eight days late rather than as a question. ## The gap `OnboardingTourNudgeMetadata` is `{ tour }` only, so `nudge_shown` and `explore_templates_clicked` cannot be split by how the tour ended. That matters more here than it would elsewhere, because **every ending arms the nudge** — deliberately, per your call in #14144: *"a user who saw no tour is the one who most needs somewhere to go next."* So the nudge's audience is deliberately a mix of *finished the tour* and *never saw one*, and right now the funnel cannot tell those two apart. A conversion from someone who completed the walkthrough and one from someone the tour never started for land in the same bucket. ## The change `tourWasCompleted` is already in scope in the component — it picks the copy one line above the telemetry call. This carries the same value onto both events: ```ts telemetry?.trackOnboardingTour('nudge_shown', { tour: 'firstRun', tour_completed: tourWasCompleted.value }) ``` Optional field, no new event, **no visibility rule changed**, nothing added to any other event. ## Verification `FirstRunTourNudge.test.ts` **12/12**. The two existing assertions pinned the exact metadata shape, so they failed until updated — which is the suite working. Added one case asserting both events carry `false` when the tour did not complete. Mutation-checked: hard-coding `tour_completed: true` fails exactly that new case, 11 others still pass. `typecheck` and `format:check` clean. ## Why it's late I drafted this as part of a Slack reply to you on **2026-08-04** and never sent it, so you have never actually seen the offer. That's the failure, not the telemetry. It was the one item in that draft still worth acting on — the rest of it has since been overtaken (the nudge copy landed in #14677, and you've since settled the OG image on #14957). If you'd rather have the *full* ending instead of a boolean — `completed` vs `skipped` with its `skipReason` — say so and I'll rework it; the controller has `engine.lastEnding` and only exposes the boolean today. I went with the boolean because it needed no change to the controller's surface. Co-authored-by: t <t@t.t>
1 parent 2026348 commit 4cb44e9

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

src/platform/telemetry/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ export interface OnboardingTourStepMetadata {
156156
/** The nudge is post-tour, so it reports no step and no count. */
157157
export interface OnboardingTourNudgeMetadata {
158158
tour: string
159+
/**
160+
* Whether the tour was walked to the end. Without it `nudge_shown` and
161+
* `explore_templates_clicked` cannot be split by how the tour ended, so a
162+
* conversion from a completed tour and one from a tour that never started
163+
* land in the same bucket.
164+
*/
165+
tour_completed?: boolean
159166
}
160167

161168
export type OnboardingTourMetadata =

src/renderer/extensions/firstRunTour/nudge/FirstRunTourNudge.test.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ describe('FirstRunTourNudge', () => {
8787
'a nudge armed before this mounted still has to appear'
8888
).not.toBeNull()
8989
expect(mocks.trackOnboardingTour).toHaveBeenCalledWith('nudge_shown', {
90-
tour: 'firstRun'
90+
tour: 'firstRun',
91+
tour_completed: true
9192
})
9293
})
9394

@@ -223,7 +224,29 @@ describe('FirstRunTourNudge', () => {
223224
expect(mocks.dismissNudge).toHaveBeenCalled()
224225
expect(mocks.trackOnboardingTour).toHaveBeenCalledWith(
225226
'explore_templates_clicked',
226-
{ tour: 'firstRun' }
227+
{ tour: 'firstRun', tour_completed: true }
228+
)
229+
})
230+
231+
it('separates a conversion from a completed tour from one that never ran', async () => {
232+
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
233+
mocks.tourWasCompleted.value = false
234+
mocks.nudgeArmed.value = true
235+
renderNudge()
236+
await vi.advanceTimersByTimeAsync(APPEAR_DELAY_MS)
237+
238+
await user.click(screen.getByTestId('first-run-nudge-explore'))
239+
240+
// Both events carry it, so the funnel can be read end to end: without it
241+
// a conversion from a finished tour and one from a tour that never
242+
// started are indistinguishable.
243+
expect(mocks.trackOnboardingTour).toHaveBeenCalledWith('nudge_shown', {
244+
tour: 'firstRun',
245+
tour_completed: false
246+
})
247+
expect(mocks.trackOnboardingTour).toHaveBeenCalledWith(
248+
'explore_templates_clicked',
249+
{ tour: 'firstRun', tour_completed: false }
227250
)
228251
})
229252
})

src/renderer/extensions/firstRunTour/nudge/FirstRunTourNudge.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ const { start: scheduleAppearance, stop: cancelAppearance } = useTimeoutFn(
9090
onScreen.value = true
9191
if (reported) return
9292
reported = true
93-
telemetry?.trackOnboardingTour('nudge_shown', { tour: 'firstRun' })
93+
telemetry?.trackOnboardingTour('nudge_shown', {
94+
tour: 'firstRun',
95+
tour_completed: tourWasCompleted.value
96+
})
9497
},
9598
APPEAR_DELAY_MS,
9699
{ immediate: false }
@@ -117,7 +120,8 @@ watch(
117120
function onExplore() {
118121
useWorkflowTemplateSelectorDialog().show('first_run_nudge')
119122
telemetry?.trackOnboardingTour('explore_templates_clicked', {
120-
tour: 'firstRun'
123+
tour: 'firstRun',
124+
tour_completed: tourWasCompleted.value
121125
})
122126
dismissNudge()
123127
}

0 commit comments

Comments
 (0)