Skip to content

Add RudderStack event tracking alongside Amplitude#196

Open
sonyccd wants to merge 7 commits intomainfrom
feat/rudderstack-integration
Open

Add RudderStack event tracking alongside Amplitude#196
sonyccd wants to merge 7 commits intomainfrom
feat/rudderstack-integration

Conversation

@sonyccd
Copy link
Owner

@sonyccd sonyccd commented Mar 3, 2026

Summary

  • Adds RudderStack (@rudderstack/analytics-js v3) as a secondary event tracking layer alongside the existing direct Amplitude integration
  • Mirrors all 16 tracked events (auth, study, navigation, content) through RudderStack using a dual-call pattern
  • Includes RudderStackProvider for user identity sync and dynamic SDK initialization for minimal bundle impact

Details

  • New files: src/lib/rudderstack.ts (event tracking), src/hooks/useRudderStack.tsx (identity provider), plus tests for both
  • Modified files: 10 call-site files updated to fire events to both Amplitude and RudderStack
  • Configuration: Two new env vars (VITE_RUDDERSTACK_WRITE_KEY, VITE_RUDDERSTACK_DATA_PLANE_URL) — gracefully disabled when not set
  • 27 new tests (18 for tracking module, 9 for identity provider), all 3502 existing tests pass

Test plan

  • All 3502 tests pass (npx vitest run)
  • Production build succeeds (npm run build)
  • Verify RudderStack events appear in dashboard after setting env vars in deployment
  • Confirm Amplitude tracking is unaffected (no regressions in Amplitude dashboard)

🤖 Generated with Claude Code

sonyccd and others added 7 commits March 2, 2026 19:50
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@supabase
Copy link

supabase bot commented Mar 3, 2026

Updates to Preview Branch (feat/rudderstack-integration) ↗︎

Deployments Status Updated
Database Tue, 03 Mar 2026 18:29:41 UTC
Services Tue, 03 Mar 2026 18:29:41 UTC
APIs Tue, 03 Mar 2026 18:29:41 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Tue, 03 Mar 2026 18:29:49 UTC
Migrations Tue, 03 Mar 2026 18:29:58 UTC
Seeding Tue, 03 Mar 2026 18:30:07 UTC
Edge Functions Tue, 03 Mar 2026 18:30:18 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@vercel
Copy link
Contributor

vercel bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openhamprep Ready Ready Preview, Comment Mar 3, 2026 6:47pm

@sentry
Copy link

sentry bot commented Mar 3, 2026

⚠️ JUnit XML file not found

The CLI was unable to find any JUnit XML files to upload.
For more help, visit our troubleshooting guide.

@claude
Copy link

claude bot commented Mar 3, 2026

PR Review: Add RudderStack event tracking alongside Amplitude

Overall this is a clean, well-tested implementation. Good job on graceful degradation (safe no-ops when env vars are not set) and thorough test coverage. A few issues worth addressing before merging.


Bug: try/catch does not cover async errors in main.tsx

The initialization code wraps a dynamic import() in a try/catch, but this does not work as intended:

try {
  import('@rudderstack/analytics-js').then(({ RudderAnalytics }) => {
    const rudderAnalytics = new RudderAnalytics();
    rudderAnalytics.load(rudderWriteKey, rudderDataPlaneUrl); // errors here are unhandled
    (window as any).rudderanalytics = rudderAnalytics;
  });
} catch (error) {
  console.warn('Failed to initialize RudderStack:', error); // never reached for async errors
}

import() always returns a Promise, so the catch block only guards synchronous errors (impossible here). Any failure in the .then() callback becomes a silently dropped unhandled rejection. Fix:

import('@rudderstack/analytics-js')
  .then(({ RudderAnalytics }) => {
    const rudderAnalytics = new RudderAnalytics();
    rudderAnalytics.load(rudderWriteKey, rudderDataPlaneUrl);
    (window as any).rudderanalytics = rudderAnalytics;
  })
  .catch((error) => {
    console.warn('Failed to initialize RudderStack:', error);
  });

Race condition: early events are silently dropped

The SDK loads asynchronously. Any event fired between page load and SDK initialization (e.g., an OAuth redirect completing immediately) will hit safeTrack, find window.rudderanalytics undefined, and silently no-op. This is consistent behavior with when env vars are absent, but worth documenting. Consider an event queue pattern if early-session events matter.


Architectural concern: check for duplicate events in Amplitude

The typical RudderStack CDP pattern routes events through RudderStack to downstream destinations (including Amplitude), not in parallel. With the dual-call approach here:

User action --> trackX()    --> Amplitude
            --> rsTrackX()  --> RudderStack --> (downstream destinations)

If RudderStack is configured to forward to Amplitude, every event will appear twice in Amplitude. Worth verifying the RudderStack destination config before going live, as this would inflate all metrics.

The dual-call pattern also means every future event must be added in two places. A long-term simplification would be to route everything through RudderStack and remove direct Amplitude SDK calls.


Type coupling: importing prop types from amplitude.ts

// src/lib/rudderstack.ts
import type {
  PracticeTestStartedProps,
  PracticeTestCompletedProps,
  // ...
} from './amplitude';

This creates a dependency from rudderstack.ts on amplitude.ts. If Amplitude is ever swapped out, these types need to move first. A src/lib/analyticsTypes.ts shared module would decouple the two, making each independently removable.


Minor: inconsistent indentation in App.tsx

The RudderStackProvider tags are indented at 6 spaces (same level as AmplitudeProvider) but the provider tree uses 2-space nesting. Small cosmetic issue but breaks the visual hierarchy.


What is done well

  • Graceful degradation: safeTrack returns early cleanly when SDK is not ready -- no crashes in dev or without env vars
  • Test coverage: 27 tests covering edge cases (no user, user switch, SDK errors, missing window.rudderanalytics)
  • Consistent event naming: Same event names and property schemas as Amplitude -- makes cross-tool analysis straightforward
  • Test isolation: vi.unmock pattern correctly bypasses the global mock in setup.ts for the dedicated unit tests
  • Dynamic import: Loading the SDK lazily is good for initial bundle size

The main.tsx async error handling is the only thing I would consider blocking. The architectural concern about duplicate events is worth verifying before the first production deployment.

🤖 Generated with Claude Code

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