track, identify via the Node SDK’s identify pattern, and alignment with session-backed anonymousAnalyticsId and login flows.
Why
Amplitude gives you a production-grade analytics pipeline without any infrastructure to manage. You instrument events on the backend, and Amplitude handles ingestion, storage, and visualisation. It’s the right choice when you need funnel analysis, retention metrics, or cohort-based product insights.How it works
Dual identity ontrack: Every track call receives { anonymousId: string; userId?: string }. The adapter maps these to Amplitude’s device_id (anonymous id) and optional user_id when authenticated, consistent with Node SDK usage.
identify: identify({ anonymousId, userId }) links device and user in Amplitude via the SDK’s identify API (see implementation in apps/tools/analytics/amplitude/src/index.ts).
When identify runs (server-only): Same as other providers — after successful login via identifyUserInAnalytics, and via syncAnalyticsIdentityIfNeeded when a new anonymous id is created for an already-authenticated session (legacy repair).
No browser Amplitude SDK is wired in this stack yet; instrumentation is backend-only through tools.analytics and GraphQL trackFrontendEvent.
Setup
- In
apps/backend/src/configuration/production.ts(andstaging.tsif you have one), settools.analytics: - In your production environment, set:
ANALYTICS_APPis required whenever analytics is enabled. The backend attaches it as theappproperty on every outboundtrackevent. - Keep
development.tsandtest.tsusing thelocalclient — Amplitude should never receive dev or test traffic. - Run
make test module=backend path=apps/backend/src/tools/analytics/__tests__/loader.spec.ts.
Tracking events
Calltools.analytics.track() with an event name, a properties object, and identification attributes:
snake_case. Keep property keys bounded and low-cardinality where used for filtering.
Frontend tracking endpoint
Frontend analytics events should flow throughtrackFrontendEvent on GraphQL. This avoids direct provider SDK usage in the browser and keeps rate limiting plus payload constraints on the backend.
anonymous:session:<anonymousAnalyticsId> when the session id is available (apps/backend/src/gql/middlewares/rateLimitFrontendTracking.ts).
Future: browser SDK and one canonical id
When you add a browser Amplitude SDK, the intended pattern is to surface the sameanonymousAnalyticsId the backend stores in session — for example a first-party cookie on responses — so the client uses the same device id, not a second generator. HttpOnly vs JS-readable and SameSite are decisions for that phase.
Extending this system
- Change track/identify mapping in
apps/tools/analytics/amplitude/src/index.ts. - If you change the shared contract, update
apps/shared/src/analytics/types.tsand other adapters. - Run
make test module=tooling-analytics-amplitudeandmake test module=backend path=apps/backend/src/tools/analytics/__tests__/loader.spec.ts.
What not to do
❌ Never import Amplitude directly in a GraphQL resolver or React component. Usetools.analyticsfrom the backend. ❌ Never send Amplitude traffic fromdevelopment.tsortest.ts— it pollutes your production data and your event quota. ❌ Never use unbounded strings as metric tags; follow.cursor/rules/security.mdc.
File conventions
Gotchas
amplitude.init()is called once at startup insideapps/tools/analytics/amplitude/src/index.ts. Do not call it elsewhere.- Use
trackingPolicy.allowedFieldsfor every allowed property key. Unknown keys are dropped before emission. - The node SDK batches and flushes events asynchronously. A process crash before the flush window may drop the last few events — this is expected behaviour for fire-and-forget analytics.
- Analytics providers expose
shutdown(), and backend startup wires graceful shutdown hooks to reduce event loss during process termination.
What’s next?
- Local — the dev stub to use in
development.tsandtest.ts. - PostHog — alternate production analytics provider.
- Configuration — switching pluggable tool implementations.
