Skip to content

feat(onboarding): route analytics through backend - #3131

Open
WcaleNieWolny wants to merge 4 commits into
mainfrom
wolny/onboarding-backend-events
Open

feat(onboarding): route analytics through backend#3131
WcaleNieWolny wants to merge 4 commits into
mainfrom
wolny/onboarding-backend-events

Conversation

@WcaleNieWolny

@WcaleNieWolny WcaleNieWolny commented Aug 19, 2026

Copy link
Copy Markdown
Member

Summary

  • route frontend onboarding analytics through the authenticated /private/events endpoint while preserving the previous non-production suppression
  • preserve onboarding properties plus PostHog session, window, device, sanitized URL/path/referrer, and original event timestamp as event-only context
  • derive the PostHog actor from backend authentication, retain optional verified organization grouping, and send onboarding_ai_instructions_copied once to both PostHog and the existing Bento mapping

Validation

  • bun lint (zero errors; existing warnings only)
  • bun lint:backend
  • bun typecheck
  • bun test:unit (260 files, 2,156 tests)

Summary by CodeRabbit

  • Enhancements

    • Improved onboarding analytics across setup steps, organization intent selection, invitations, demo creation, and AI instruction copying.
    • Added richer, sanitized session and browser context to onboarding events.
    • Improved event metadata, including organization details, application context, and consistent timestamps.
    • Onboarding tracking is now limited to appropriate production environments.
  • Bug Fixes

    • Reduced duplicate or inconsistent tracking and improved handling of tracking failures.
  • Tests

    • Added coverage for onboarding event routing, metadata, environment handling, and timestamp formatting.

@codspeed-hq

codspeed-hq Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing wolny/onboarding-backend-events (c4075a0) with main (9e06dc3)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d5191354-9740-4154-bcdc-a43b7598d3bb

📥 Commits

Reviewing files that changed from the base of the PR and between 9e06dc3 and c4075a0.

📒 Files selected for processing (16)
  • src/components/dashboard/AppOnboardingFlow.vue
  • src/components/dashboard/DemoOnboardingModal.vue
  • src/components/dashboard/StepsApp.vue
  • src/components/dashboard/StepsBuild.vue
  • src/components/dashboard/StepsBundle.vue
  • src/pages/onboarding/organization.vue
  • src/services/onboardingTracking.ts
  • src/utils/onboardingProgressAnalytics.ts
  • supabase/functions/_backend/private/events.ts
  • supabase/functions/_backend/utils/onboarding_copy_tracking.ts
  • supabase/functions/_backend/utils/tracking.ts
  • tests/app-onboarding-progress-integration.unit.test.ts
  • tests/onboarding-backend-tracking.unit.test.ts
  • tests/onboarding-copy-tracking.unit.test.ts
  • tests/organization-onboarding-logo-upload.unit.test.ts
  • tests/tracking.unit.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)
💤 Files with no reviewable changes (1)
  • tests/onboarding-copy-tracking.unit.test.ts

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.


📝 Walkthrough

Walkthrough

Changes

The PR adds shared onboarding tracking with browser context, local-host gating, enriched event properties, and failure suppression. Frontend onboarding flows use the service. Backend tracking normalizes timestamps and updates AI-copy event handling.

Onboarding analytics service

Layer / File(s) Summary
Shared tracking context and dispatch
src/services/onboardingTracking.ts, tests/onboarding-backend-tracking.unit.test.ts
Adds sanitized browser context collection and sendOnboardingEvent. Tests cover host gating, payload metadata, fire-and-forget behavior, and AI-copy context.
Frontend onboarding migration
src/components/dashboard/*, src/pages/onboarding/organization.vue, src/utils/onboardingProgressAnalytics.ts, tests/app-onboarding-progress-integration.unit.test.ts, tests/organization-onboarding-logo-upload.unit.test.ts
Replaces onboarding pushEvent calls with sendOnboardingEvent. Progress analytics uses a default capture adapter. Related mocks and compatibility assertions are updated.
Backend tracking normalization
supabase/functions/_backend/private/events.ts, supabase/functions/_backend/utils/*, tests/onboarding-copy-tracking.unit.test.ts, tests/tracking.unit.test.ts
Removes frontend-capture suppression, narrows AI-copy detection, and converts valid tracking timestamps to ISO strings. Tests cover the updated helper usage and timestamp conversion.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to c4075

This PR routes onboarding analytics through the authenticated backend while preserving existing event context and suppression behavior. No actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant OnboardingComponent
  participant sendOnboardingEvent
  participant PostHog
  OnboardingComponent->>sendOnboardingEvent: send onboarding event and properties
  sendOnboardingEvent->>sendOnboardingEvent: collect sanitized browser context
  sendOnboardingEvent->>PostHog: dispatch enriched event
``

</details>

<!-- walkthrough_end -->
<!-- pre_merge_checks_walkthrough_start -->

<details>
<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary>

### ❌ Failed checks (1 warning)

|     Check name     | Status     | Explanation                                                                          | Resolution                                                                         |
| :----------------: | :--------- | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- |
| Docstring Coverage | ⚠️ Warning | Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |

<details>
<summary>✅ Passed checks (4 passed)</summary>

|         Check name         | Status   | Explanation                                                                                                                                                        |
| :------------------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|     Linked Issues check    | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                           |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                           |
|         Title check        | ✅ Passed | The title clearly identifies the main change: routing onboarding analytics through the backend.                                                                    |
|      Description check     | ✅ Passed | The description explains the backend routing, preserved context, event behavior, and validation results, but omits the template checklist and detailed test steps. |

</details>

</details>

<!-- pre_merge_checks_walkthrough_end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches 💡 1</summary>

<!-- finishing_touch_suggestion:docstrings -->
<details>
<summary>📝 Generate docstrings 💡</summary>

- [ ] <!-- {"checkboxId":"7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create stacked PR
- [ ] <!-- {"checkboxId":"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit on current branch

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---




<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>

<!-- tips_end -->
Loading

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 17 files

Confidence score: 3/5

  • captureOnboardingEvent in src/utils/onboardingProgressAnalytics.ts and the sendOnboardingEvent path in src/components/dashboard/StepsApp.vue no longer honor the local-host guard, so local and preview sessions may send authenticated analytics to /private/events and pollute production data; restore the environment check before dispatch.
  • src/services/onboardingTracking.ts only promotes app_id for AI_INSTRUCTIONS_COPIED_EVENT, so other onboarding events can reach the backend without their app association; preserve app_id tagging for every event that provides it.
  • The fire-and-forget tracking in src/components/dashboard/AppOnboardingFlow.vue drops the prior rejection handling, allowing analytics failures to become unhandled promise rejections; retain explicit error handling around the tracking call.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/components/dashboard/StepsApp.vue">

<violation number="1" location="src/components/dashboard/StepsApp.vue:97">
P3: The old pushEvent skipped tracking when the API host was local (isLocal(config.supaHost)); sendOnboardingEvent routes through sendEvent, which sends to /private/events for any authenticated session with no local-env suppression. If a developer runs the console against the production/real backend (or VITE_API_HOST points to a non-local host), onboarding steps now emit real PostHog/analytics events from a dev environment where they were previously suppressed. Confirm this is intended or keep a local guard in sendOnboardingEvent.</violation>
</file>

<file name="src/utils/onboardingProgressAnalytics.ts">

<violation number="1" location="src/utils/onboardingProgressAnalytics.ts:53">
P3: The new `captureOnboardingEvent` drops the `isLocal(supaHost)` guard that `pushEvent` previously applied. Onboarding analytics are now always dispatched through `sendOnboardingEvent`, so events from local/preview environments that were suppressed before now reach the analytics backend (and its downstream PostHog). If dev-noise suppression was intentional to keep, thread the locality check through `sendOnboardingEvent`/`sendEvent` or gate `captureOnboardingEvent` on `isLocal(supaHost)` instead of discarding it.</violation>

<violation number="2" location="src/utils/onboardingProgressAnalytics.ts:53">
P2: The previous default capture (`pushEvent`) suppressed analytics outside the production host with `if (isLocal(supaHost)) return`. The new `captureOnboardingEvent` discards `supaHost` and always calls `sendEvent`, which POSTs to `defaultApiHost/private/events` whenever an auth session exists. This silently re-enables onboarding event delivery in local/dev builds, sending test-session events (or rejected tokens) to the configured API host. Gate the new path with the same `isLocal(supaHost)` guard before calling `sendOnboardingEvent`.</violation>
</file>

<file name="src/services/onboardingTracking.ts">

<violation number="1" location="src/services/onboardingTracking.ts:20">
P3: The app association is sent to the backend only for the AI-copy event. `app_id` is promoted to `tags.app_id` solely when `event === AI_INSTRUCTIONS_COPIED_EVENT`; for all other onboarding events that carry `app_id`, it stays inside `nonPersonTags`, which the backend's `getAppId` in `private/events.ts` ignores. As a result those events reach the endpoint without app attribution or app-level access control, silently dropping the app association the properties intend to carry. Promote `app_id` to `tags` consistently (or document why only the copy event should carry it).</violation>
</file>

<file name="src/components/dashboard/AppOnboardingFlow.vue">

<violation number="1" location="src/components/dashboard/AppOnboardingFlow.vue:1513">
P3: The new fire-and-forget analytics call drops the rejection handling the replaced code had. The old `onboarding_intent_selected` path wrapped `pushEvent` in try/catch, and `trackSuccessfulCopy` called `sendEvent(...).catch(() => {})`. The new `void sendOnboardingEvent('onboarding_intent_selected', {...})` leaves the returned promise without a rejection handler. `sendOnboardingEvent` returns the `sendEvent` promise and is invoked as fire-and-forget at the component boundary, so preserve the convention of `void` plus an explicit rejection handler (`.catch(() => {})`). Currently `sendEvent` swallows its own errors and resolves `null`, so this is defensive, but it keeps the changed call consistent with the team convention for frontend telemetry.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/services/posthog.ts Outdated
Comment thread src/components/dashboard/DemoOnboardingModal.vue Outdated
type AnalyticsProperties = Record<string, AnalyticsPrimitive>
type CaptureEvent = (name: string, supaHost: string, properties?: AnalyticsProperties) => void

const captureOnboardingEvent: CaptureEvent = (name, _supaHost, properties) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The previous default capture (pushEvent) suppressed analytics outside the production host with if (isLocal(supaHost)) return. The new captureOnboardingEvent discards supaHost and always calls sendEvent, which POSTs to defaultApiHost/private/events whenever an auth session exists. This silently re-enables onboarding event delivery in local/dev builds, sending test-session events (or rejected tokens) to the configured API host. Gate the new path with the same isLocal(supaHost) guard before calling sendOnboardingEvent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/utils/onboardingProgressAnalytics.ts, line 53:

<comment>The previous default capture (`pushEvent`) suppressed analytics outside the production host with `if (isLocal(supaHost)) return`. The new `captureOnboardingEvent` discards `supaHost` and always calls `sendEvent`, which POSTs to `defaultApiHost/private/events` whenever an auth session exists. This silently re-enables onboarding event delivery in local/dev builds, sending test-session events (or rejected tokens) to the configured API host. Gate the new path with the same `isLocal(supaHost)` guard before calling `sendOnboardingEvent`.</comment>

<file context>
@@ -50,6 +50,10 @@ type AnalyticsPrimitive = string | number | boolean
 type AnalyticsProperties = Record<string, AnalyticsPrimitive>
 type CaptureEvent = (name: string, supaHost: string, properties?: AnalyticsProperties) => void
 
+const captureOnboardingEvent: CaptureEvent = (name, _supaHost, properties) => {
+  void sendOnboardingEvent(name, properties)
+}
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed centrally in 2e03d16. sendOnboardingEvent checks the configured Supabase host with isLocal before collecting browser context or dispatching.

Comment thread src/components/dashboard/StepsApp.vue Outdated
notify: false,
}).catch()
pushEvent(`user:onboarding-step-${stepToName(step.value)}`, config.supaHost, { org_id: orgId })
void sendOnboardingEvent(`user:onboarding-step-${stepToName(step.value)}`, { org_id: orgId })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The old pushEvent skipped tracking when the API host was local (isLocal(config.supaHost)); sendOnboardingEvent routes through sendEvent, which sends to /private/events for any authenticated session with no local-env suppression. If a developer runs the console against the production/real backend (or VITE_API_HOST points to a non-local host), onboarding steps now emit real PostHog/analytics events from a dev environment where they were previously suppressed. Confirm this is intended or keep a local guard in sendOnboardingEvent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/dashboard/StepsApp.vue, line 97:

<comment>The old pushEvent skipped tracking when the API host was local (isLocal(config.supaHost)); sendOnboardingEvent routes through sendEvent, which sends to /private/events for any authenticated session with no local-env suppression. If a developer runs the console against the production/real backend (or VITE_API_HOST points to a non-local host), onboarding steps now emit real PostHog/analytics events from a dev environment where they were previously suppressed. Confirm this is intended or keep a local guard in sendOnboardingEvent.</comment>

<file context>
@@ -94,7 +94,7 @@ function setLog() {
         notify: false,
       }).catch()
-      pushEvent(`user:onboarding-step-${stepToName(step.value)}`, config.supaHost, { org_id: orgId })
+      void sendOnboardingEvent(`user:onboarding-step-${stepToName(step.value)}`, { org_id: orgId })
     }
   }
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed centrally in 2e03d16. All onboarding calls now retain the previous non-production suppression through sendOnboardingEvent.

type AnalyticsProperties = Record<string, AnalyticsPrimitive>
type CaptureEvent = (name: string, supaHost: string, properties?: AnalyticsProperties) => void

const captureOnboardingEvent: CaptureEvent = (name, _supaHost, properties) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new captureOnboardingEvent drops the isLocal(supaHost) guard that pushEvent previously applied. Onboarding analytics are now always dispatched through sendOnboardingEvent, so events from local/preview environments that were suppressed before now reach the analytics backend (and its downstream PostHog). If dev-noise suppression was intentional to keep, thread the locality check through sendOnboardingEvent/sendEvent or gate captureOnboardingEvent on isLocal(supaHost) instead of discarding it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/utils/onboardingProgressAnalytics.ts, line 53:

<comment>The new `captureOnboardingEvent` drops the `isLocal(supaHost)` guard that `pushEvent` previously applied. Onboarding analytics are now always dispatched through `sendOnboardingEvent`, so events from local/preview environments that were suppressed before now reach the analytics backend (and its downstream PostHog). If dev-noise suppression was intentional to keep, thread the locality check through `sendOnboardingEvent`/`sendEvent` or gate `captureOnboardingEvent` on `isLocal(supaHost)` instead of discarding it.</comment>

<file context>
@@ -50,6 +50,10 @@ type AnalyticsPrimitive = string | number | boolean
 type AnalyticsProperties = Record<string, AnalyticsPrimitive>
 type CaptureEvent = (name: string, supaHost: string, properties?: AnalyticsProperties) => void
 
+const captureOnboardingEvent: CaptureEvent = (name, _supaHost, properties) => {
+  void sendOnboardingEvent(name, properties)
+}
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed centrally in 2e03d16. The helper retains the old isLocal suppression for the configured Supabase host.

Comment on lines +1513 to +1517
void sendOnboardingEvent('onboarding_intent_selected', {
intent: selectedIntent.value,
estimated_mau: estimatedMau,
org_id: data.id,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new fire-and-forget analytics call drops the rejection handling the replaced code had. The old onboarding_intent_selected path wrapped pushEvent in try/catch, and trackSuccessfulCopy called sendEvent(...).catch(() => {}). The new void sendOnboardingEvent('onboarding_intent_selected', {...}) leaves the returned promise without a rejection handler. sendOnboardingEvent returns the sendEvent promise and is invoked as fire-and-forget at the component boundary, so preserve the convention of void plus an explicit rejection handler (.catch(() => {})). Currently sendEvent swallows its own errors and resolves null, so this is defensive, but it keeps the changed call consistent with the team convention for frontend telemetry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/dashboard/AppOnboardingFlow.vue, line 1513:

<comment>The new fire-and-forget analytics call drops the rejection handling the replaced code had. The old `onboarding_intent_selected` path wrapped `pushEvent` in try/catch, and `trackSuccessfulCopy` called `sendEvent(...).catch(() => {})`. The new `void sendOnboardingEvent('onboarding_intent_selected', {...})` leaves the returned promise without a rejection handler. `sendOnboardingEvent` returns the `sendEvent` promise and is invoked as fire-and-forget at the component boundary, so preserve the convention of `void` plus an explicit rejection handler (`.catch(() => {})`). Currently `sendEvent` swallows its own errors and resolves `null`, so this is defensive, but it keeps the changed call consistent with the team convention for frontend telemetry.</comment>

<file context>
@@ -1511,16 +1510,11 @@ async function createOrganizationAndApp() {
-    catch (eventError) {
-      console.error('Failed to track onboarding intent', eventError)
-    }
+    void sendOnboardingEvent('onboarding_intent_selected', {
+      intent: selectedIntent.value,
+      estimated_mau: estimatedMau,
</file context>
Suggested change
void sendOnboardingEvent('onboarding_intent_selected', {
intent: selectedIntent.value,
estimated_mau: estimatedMau,
org_id: data.id,
})
void sendOnboardingEvent('onboarding_intent_selected', {
intent: selectedIntent.value,
estimated_mau: estimatedMau,
org_id: data.id,
}).catch(() => {})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed centrally in 2e03d16. sendOnboardingEvent now returns void and owns the sendEvent promise with a rejection handler, so component callers cannot leak an unhandled rejection.

...getPostHogBrowserContext(),
},
...(orgId ? { org_id: orgId } : {}),
...(event === AI_INSTRUCTIONS_COPIED_EVENT && appId ? { tags: { app_id: appId } } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The app association is sent to the backend only for the AI-copy event. app_id is promoted to tags.app_id solely when event === AI_INSTRUCTIONS_COPIED_EVENT; for all other onboarding events that carry app_id, it stays inside nonPersonTags, which the backend's getAppId in private/events.ts ignores. As a result those events reach the endpoint without app attribution or app-level access control, silently dropping the app association the properties intend to carry. Promote app_id to tags consistently (or document why only the copy event should carry it).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/services/onboardingTracking.ts, line 20:

<comment>The app association is sent to the backend only for the AI-copy event. `app_id` is promoted to `tags.app_id` solely when `event === AI_INSTRUCTIONS_COPIED_EVENT`; for all other onboarding events that carry `app_id`, it stays inside `nonPersonTags`, which the backend's `getAppId` in `private/events.ts` ignores. As a result those events reach the endpoint without app attribution or app-level access control, silently dropping the app association the properties intend to carry. Promote `app_id` to `tags` consistently (or document why only the copy event should carry it).</comment>

<file context>
@@ -0,0 +1,24 @@
+      ...getPostHogBrowserContext(),
+    },
+    ...(orgId ? { org_id: orgId } : {}),
+    ...(event === AI_INSTRUCTIONS_COPIED_EVENT && appId ? { tags: { app_id: appId } } : {}),
+    timestamp: Date.now(),
+    tracking_version: 2,
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept intentionally. app_id already remains in nonPersonTags for event attribution. Promoting every app_id to tags would make the backend PostHog adapter include it in the $set person properties, contrary to the event-only design. Only onboarding_ai_instructions_copied duplicates app_id into tags because the existing Bento mapper requires verified app context; that exception is now documented in code.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 11 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread tests/onboarding-backend-tracking.unit.test.ts Outdated
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant