Skip to main content

Analytics Events

FocusFlow analytics event names use result-style names such as Started, Completed, Viewed, Created, Copied, and Tapped. Add or update rows here in the same change that adds or renames an AnalyticsEvent constant.

Describe every property a row sends — what it means and its type — rather than only naming it. For a property with a fixed set of values (an enum), list all the values. Use None when an event sends no properties.

Desktop Events

Source: apps/clients/src/renderer/core/telemetry/events.ts

EventTriggerProperties
Accessibility Permission CompletedAccessibility permission request resolves or fails during onboarding.permissionStatus
Account Restricted ShownThe account-restricted surface first becomes visible (operator kill-switch active). Fires once per transition to restricted — covers both the reactive 403 IPC broadcast and the proactive entitlements snapshot path.None
App OpenThe desktop app finishes launching and analytics initializes — fires once per app launch, anonymous before sign-in. Mirrors the iOS App Open lifecycle event.None
Calendar Link Screen ViewedThe connect-Google-Calendar onboarding step is shown (once per mount) — the gate between the onboarding questions and the permission screen. Makes drop-off at the calendar step visible in the onboarding funnel.None
Calendar Connect SkippedThe user takes the skip exit on the Google-connect onboarding step after a failed or declined connect attempt. Shown only for the intents that have the step (email triage, meeting notes).onboardingId, path
Calendar Sign In CompletedGoogle Calendar connection completes and the calendar link state is confirmed as linked.None
Desktop App CTA ClickedUser clicks a download or installed-app action on a desktop-only CTA in the web build.source (string) — which feature surfaced the CTA, one of recording, dictation, meetingDetection, template, or desktopNudge.
placement (optional string) — where the CTA rendered, one of meetingBanner, templateLanding, desktopDownloadPopup, or desktopDownloadSidebar.
platform (optional string) — targeted desktop OS for a desktop nudge, one of mac or windows.
followUp (optional string) — nudge state, one of download or finishSetup.
action (optional string) — desktop nudge action, one of downloadInstaller or openDesktopApp.
Desktop App CTA ViewedA web feature surface renders the download-desktop CTA in the web build (once per visible surface).source (string) — which feature surfaced the CTA, one of recording, dictation, meetingDetection, template, or desktopNudge.
placement (optional string) — where the CTA rendered, one of meetingBanner, templateLanding, desktopDownloadPopup, or desktopDownloadSidebar.
platform (optional string) — targeted desktop OS for a desktop nudge, one of mac or windows.
followUp (optional string) — nudge state, one of download or finishSetup.
Email nudge also uses placement = templatePostPaywall, onboardingPostPaywall, reminderPopup, or sidebar; surface = reminder or email; downloadMethod = email; browser = mobile or desktop; additional platform values mobileWeb or otherDesktop.
Desktop Download Email RequestedSigned-in web user explicitly requests a desktop link and the backend accepts the durable request. Emitted by the web client once per accepted reference in the screen session, keyed on the ref the request endpoint returns.source = desktopNudge; placement = templatePostPaywall, onboardingPostPaywall, reminderPopup, or sidebar; browser = mobile or desktop; opaque UUID ref; downloadMethod = email.
Desktop Download Email SentEmitted by the backend worker when it observes a completed CRM transactional send task with a provider message ID. This means provider acceptance, not inbox delivery.source = desktopNudge; placement = templatePostPaywall, onboardingPostPaywall, reminderPopup, or sidebar; browser = mobile or desktop; opaque UUID ref; downloadMethod = email. Firebase user ID; stable insert ID per reference.
Desktop App ActivatedEmitted by the backend when it first observes an authenticated desktop session for the account. Existing activated accounts do not replay.desktopPlatform; for an owned email reference authenticated within 30 days: source = desktopNudge, original placement, opaque ref, downloadMethod = email. Email forwarding alone never attributes activation.
Desktop Download Nudge SnoozedUser closes a web-to-desktop popup or sidebar reminder, successfully requesting the server-owned 15-day snooze.placement (string) — one of desktopDownloadPopup or desktopDownloadSidebar.
platform (string) — one of mac or windows.
followUp (string) — one of download or finishSetup.
Dictation FailedA dictation attempt ends in an error of any kind (connect, provider, finalize, or paste failure).cause (string) — failure classification, one of engine_dead, internal_error, websocket_close, websocket_error, auth, rate_limit, timeout, parse_error, or unknown (fallback for anything unclassified).
Dictation Limit ReachedThe rolling dictation-seconds cap is hit — fired once per cap event from the main-process dictation pipeline, covering the admission 429s, the finalize-time 429, and the mid-session budget stop.stage (string) — where the cap surfaced, one of start (session blocked at the credential mint), retry (blocked re-attempt), finalize (blocked while transcribing after key-up), or cutoff (stopped mid-dictation by the budget signal).
Dictation StartedDictation recording begins after the trigger shortcut is pressed and the microphone is authorized.None
Dictation SucceededA dictation attempt finishes with text pasted (auto-paste, manual paste, or recovered after retry).durationSeconds (number) — whole seconds from recording start to paste, including processing time.
pasteResult (string) — how the text landed, one of autoPasted (inserted into the focused field) or manualPaste (left on the clipboard for the user to paste).
Document CreatedA workspace document is created.None
Document DuplicatedA workspace document is duplicated from the sidebar row menu.None
HabitatZero Desktop App InstalledFirst time the desktop app opens after install (once per installation). Anonymous at fire time; stitches to the user via the desktop device_id at sign-in. Top of the install → onboarding funnel.acquisition_source (string) — where the installation originated, one of microsoft-store or website. Default is website.
HabitatZero First Note CreatedA user's first desktop note is created with active cross-promo attribution still inside the activation window.placement, ref, source, trialDays, optional UTM properties
Install via ReferrerFires once per installation, the first time a referrer resolves. Web/desktop: the website-cookie cross-promo attribution, available once entitlements resolve after sign-in. Mobile: the native Adjust attribution, polled from app boot before sign-in — there is no website cookie on a native install. Also persists the resolved acquisition UTMs as user properties (referrerUtmSource, referrerUtmCampaign, referrerUtmMedium, referrerUtmTerm, referrerUtmContent) so cohorts can filter by acquisition source, not just this one event. Mirrors the iOS Install via Referrer acquisition event.Web/desktop: placement, ref, source, trialDays, optional UTM properties. Mobile: optional UTM properties only (utmSource required for the event to fire; utmCampaign, utmMedium, utmTerm optional).
Meeting Duration Auto EndedThe desktop meeting soft cap stops an active recording after the warning was not postponed.None
Meeting Duration PostponedA user taps Postpone on the meeting duration warning.postponeCount
Meeting Duration Warning ShownThe desktop meeting soft-cap warning is shown on the notification overlay.postponeCount
Meeting Max Length ReachedThe server hard backstop ends an active meeting through meeting_max_duration or meeting.terminate.None
Meeting Transcription LostThe desktop app ended a recording because transcription could not be restored, and told the user it stopped. cause distinguishes reconnectBudgetExhausted (retried for two minutes and gave up) from unrecoverableStatus (an immediate 403/404/410 that retrying cannot fix).cause
Meeting Note CreatedA meeting note is created from the tray, deeplink, or note creation flow.None
Meeting Recording EndedA recording session finalizes (manual stop or its note is deleted mid-recording).durationSeconds (number) — whole seconds of wall-clock from recording start to finalize, including any paused time.
hasTranscript (boolean) — whether any transcript content was captured during the session.
transcriptWordCount (number) — words captured during the session (0 when none).
Meeting Recording StartedRecording starts for a note.source
Meeting Summary CompletedA transcript summary websocket event completes for a note.None
Meeting TranscribedA recording session first receives transcript content.None
Microphone Permission CompletedMicrophone permission request resolves during onboarding.permissionStatus
Notification OpenedA user taps an accepted mobile calendar reminder notification.notificationId (string) — the stable id that joins this event to the server-side Notification Sent event.
Notification SentFCM accepted a mobile calendar reminder notification for delivery.notificationId (string), appId (string), deviceCount (number), platform (string) — always mobile, sent as a top-level Amplitude event property rather than an event property
On App BackgroundThe desktop main window is minimized or hidden (closing the window hides it in this tray app). Fired from the main process on window minimize/hide — deliberately not on focus loss/occlusion while the window is still visible. Mirrors the iOS On App Background lifecycle event.None
Onboarding CompletedThe user reaches the real end of the onboarding flow (after permissions and any dictation opt-in).onboardingId, path (the chosen intent, when known), fastPath (boolean) — true when onboarding was fast-pathed for a template arrival (survey and calendar skipped).
Onboarding Question AnsweredA user answers one onboarding question.questionId, optionId, optionLabel, optionPosition (1-based displayed position; the intent question's option order is randomized), step, onboardingId
Onboarding Questions CompletedThe onboarding questions flow completes.Onboarding answer user-property payload (usage_context, starting_point, onboardingAnswers), onboardingId
Onboarding Questions StartedThe onboarding questions flow first mounts.source, onboardingId
Onboarding StartedA signed-in user begins onboarding for the first tracked app session.source
Paywall ViewedThe offer sheet is shown, whatever raised it: the welcome ask, a template arrival, a limit hit, the launch popup, print, the desktop sidebar CTA, or the always-visible mobile plan indicator. For a trial experiment, the first event for each trialAssignmentId is the exposure; later openings remain product events but do not create another experiment exposure. Analysis joins that ID to the stored server-side arm.trigger, source (onboarding_welcome, template_web_signin, app_open_popup, sidebar_cta, mobile_plan_indicator, print, limit_dictation, limit_meetings, …), featureKey, templateSlug, offerKind, offerDays, trialAssignmentId (opt-in offers only). Template arrivals also carry the template purchase attribution properties.
Permission Screen ViewedThe permission screen is shown during onboarding.None
Purchase SuccessA subscription first becomes active or trialing after checkout/webhook processing, from either payment provider. Nova emits web purchase events; FocusFlow emits store purchase events.ownerId, workspaceId, planCode, priceLookupKey, currency, amount, interval, stripeSubscriptionId, source (the product/paywall origin captured when purchase started, such as sidebar_cta, mobile_plan_indicator, template_web_signin, plans_and_billing, or trial_banner; unknown for legacy or invalid metadata). HZ-template purchases also carry the template purchase attribution properties.
See provider properties for the fields an App Store purchase adds.
Quarter Goal CreatedA quarterly planning goal is created.None
Sales SuccessA Stripe invoice with a positive paid amount succeeds, including the first paid invoice and recurring invoices.ownerId (string) — ID of the subscription owner.
workspaceId (string) — ID of the workspace that owns the subscription.
planCode (optional string) — billing plan code from the subscription snapshot.
currency (optional string) — ISO 4217 currency code from the subscription snapshot.
amount (number) — paid invoice amount in Stripe minor currency units, not the subscription price.
interval (optional string) — subscription billing interval: month or year.
stripeSubscriptionId (string) — Stripe subscription ID.
stripeInvoiceId (string) — Stripe invoice ID.
currentPeriodEnd (optional ISO 8601 date-time string) — end of the current subscription billing period.
source (string) — event origin; always webhook for this event.
Share Link CopiedA share URL is copied from sharing UI.entityType, source
Share Link TappedThe share toggle/action is tapped from the sharing popover.entityType, source
Subscription Manage OpenedA user opens the Stripe billing portal from Plans and Billing.None
Subscription Purchase FailedA checkout start/create call fails before Stripe checkout can be completed.planCode, currency, errorMessage
Subscription Purchase StartedA user starts hosted or embedded checkout from Plans and Billing.planCode, currency, source. HZ-template purchases also carry the template purchase attribution properties.
Trial Offer AcceptedFocusFlow proves from a signed Stripe webhook that the exact subscription accepted an assigned trial offer. This non-revenue event bridges the FocusFlow assignment to Nova-owned purchase and invoice events. Join billing events by stripeSubscriptionId, also exposed as Name and Reference. Join the experiment assignment by the optional remoteConfigInstallationId.ownerId, workspaceId, stripeSubscriptionId, trialAssignmentId, offerDays, optional remoteConfigInstallationId, source (always stripe_webhook), Origin (always stripe).
Subscription Purchase SucceededClient-side constant reserved for checkout success UI; authoritative server-side first activation is tracked to Amplitude as Purchase Success.None
System Audio Permission CompletedSystem audio permission request resolves, fails, or times out during onboarding.permissionStatus
Template Open In App TappedA visitor on the web templates landing (/templates/<slug>) taps "Already have the app? Open it" — the only action that fires the desktop deeplink now that boot no longer auto-fires it.placement (string) — always templateLanding.
source (string) — always template.
templateSlug (string) — the resolved template slug.
Template ClonedA template clone succeeded and the document opened, from any path.templateSlug, templateName, source (string) — gallery, webHandoff, deeplink, or coldInstall.
Template Personalize TappedThe user taps the personalize/use button on a template detail page (primary pill or worksheet preview open).templateName, templateSlug
Template Sign In ViewedThe web build shows the Canva-style sign-in preview for a resolved template after a signed-out visitor lands on /templates/<slug>.templateName, templateSlug, plus the template purchase attribution properties.
Todo CreatedA non-goal todo is created.None
Top Goal CreatedA daily top goal is created or promoted.goalPosition, source
Web App ViewedThe web build finishes loading and analytics initializes — the web analogue of App Open; fires once per page load.None
Web Sign In CompletedThe web OAuth redirect returns and the code exchange plus browser-session creation succeed.source (string) — always template — and the template purchase attribution properties, present only when Google sign-in was actually started from a /templates/<slug> arrival (the direct-Google handoff, or the landing screen's own Google button). A sign-in started anywhere else is not attributed to the template, even when a template visit is still pending in the same browser session. Absent otherwise.
Web Sign In StartedGoogle web sign-in is invoked, by a user action or the direct-Google template handoff, before the OAuth redirect.The template purchase attribution properties, when sign-in came from an HZ template.
Weekly Goal CreatedA weekly planning card is created.source
Weekly Goal PlannedAn unplanned weekly card is scheduled into a day or next week.placement

Template purchase attribution

The HZ template funnel uses the analytics properties that already describe cross-product traffic: utmSource is the sending app (fabulous, clarify, and so on), placement is its surface or row, ref is the deployment, experiment, or campaign reference, and utmMedium, utmCampaign, utmContent, and templateSlug preserve the rest of the handoff context.

These fields are additive. They never replace source: a purchase that began at an HZ web-template paywall continues to report source = template_web_signin. This makes the paywall path stable while utmSource and placement answer which app and surface produced it.

Billing provider properties

Server-side billing events are emitted for every payment provider under the same event names, so no dashboard forks by provider. A store purchase reports the same planCode a web purchase does.

PropertyMeaning
providerstripe, app_store or play_store. Absent means Stripe, for compatibility with events emitted before store support existed. Sent to Amplitude as Origin.
environmentProduction, Sandbox or Xcode, on App Store events only. Play has no equivalent field.
appStoreOriginalTransactionIdApple's stable subscription identifier. Stands in for stripeSubscriptionId in Name and Reference.
appStoreTransactionIdApple's identifier for the individual transaction.
playStoreOrderIdGoogle's order id for a Play subscription. Stands in for stripeSubscriptionId in Name and Reference, and is the dedup key. Never the purchase token.

Three behaviours follow from these and are easy to misread as bugs:

  • platform follows the provider: ios for App Store, android for Play Store, desktop for Stripe and for an absent provider. Changing the desktop default would break every historical series that splits by platform.
  • A non-Production App Store event carries no revenue fields. price, revenue, quantity, productId and revenueType are omitted for Sandbox and Xcode purchases, and so are Value, Currency, and amount in the event properties — an amount that is not real money is reported nowhere, so a dashboard reading Value cannot count it either. A TestFlight purchase is real access but not real money, and counting it would inflate every revenue series with internal testing. The event itself still fires, so the conversion funnel stays complete.
  • A Play Store event carries no revenue fields at all, top-level or in the event properties. Google's verification response supplies no trusted price, so the only amount available came from the device — and a device must not be able to write our revenue series. The conversion is recorded and the money is omitted everywhere.

Who emits which purchase

PurchaseEmitted byWhere from
App Store (iOS)this backendPOST /api/billing/store/verify, after Nova confirms the purchase
Play Store (Android)this backendthe same route
Stripe (web, desktop)Novaits own Stripe webhook handling

A native client cannot be trusted to report its own conversion, so a store purchase's event waits for provider-confirmed verification and is then emitted server-side. That is also why the mobile builds having no Amplitude client key does not affect it — this event carries its user_id explicitly and never passes through the device.

The store event is emitted at most once per subscription, and that is enforced in our own database rather than by Amplitude. The backend claims the provider reference on the workspace (storePurchaseConversions) before publishing, and only the caller that wins the claim sends the event. Amplitude's own deduplication is a backstop for the seconds-apart race only: it matches a repeated insert_id just "within the past 7 days", and only alongside a matching device_id, which a server-side event does not send — so a customer restoring a year later would otherwise be counted twice.

The event is still keyed on the provider's stable identifier — appStoreOriginalTransactionId for Apple, playStoreOrderId for Google — and nothing renewable is put on it: currentPeriodEnd in particular is deliberately absent, because eventInsertId would use it as its state marker and a renewal would then mint a new id. Purchase tokens never reach analytics — a token is a credential, and it rotates across renewals, which would make the key unstable in both directions.

Frontend Public Share Events

Source: apps/frontend/src/lib/analytics/events.ts

EventTriggerProperties
HabitatZero Download Page VisitedA visitor views the Habitat Zero download page.Optional placement, ref, source, trialDays, and UTM properties
HabitatZero Download StartedA visitor starts a platform download via the /download/mac or /download/win link.platform (mac or windows), optional placement, ref, source, trialDays, and UTM properties
Open With Habitat Zero TappedA visitor taps an "Open with Habitat Zero" call to action on a public share page.placement
Public Share ViewedA public share page is viewed.entityType