# Product analytics > **Prerequisite:** This guide assumes `@repo/core-analytics` is installed. If > your project started from the slim template, run > `pnpm turbo gen core-package analytics` first, then follow the wiring steps > below. `@repo/core-analytics` is the fourth capture channel after tracing, error capture, and audit. It provides a typed, vendor-neutral `IAnalytics` interface that feature use cases call at the same call sites where they call `auditLog.record()` or `bus.publish()`. The conformance system applies the same five-latency drift detection (tsc → ESLint → boot assertion → conformance CI → fallow) that already protects every other channel. For design rationale and alternatives considered see `docs/decisions/adr-024-product-analytics-channel.md`. --- ## Interface ```ts // @repo/core-analytics export type AnalyticsAttributeValue = string | number | boolean; export type AnalyticsUser = { id: string }; export interface IAnalytics { /** Emit a named event. Conforms to manifest's analyticsEvents. */ track( event: string, attributes?: Record, ): void; /** Associate attributes with a user. Not gated by the manifest. */ identify( user: AnalyticsUser, attributes?: Record, ): void; /** Client-side route change. Server impls no-op by convention. */ pageView( path: string, attributes?: Record, ): void; /** Drain the in-memory batch. Wire into graceful-shutdown and serverless-response-finish hooks. */ flush(): Promise; } ``` The template ships two implementations: | Implementation | Package | Use | | -------------------- | ---------------------- | -------------------------------- | | `NoopAnalytics` | `@repo/core-analytics` | Dev-seed and test fallback | | `RecordingAnalytics` | `@repo/core-testing` | Unit and integration test double | Your production backend (PostHog, Segment, Mixpanel, …) is a third implementation you add after the library evaluation step (§ Vendor evaluation below). --- ## Server-side wiring ### 1. Declare events in the manifest Every analytics event a use case emits must be declared in `src/feature.manifest.ts`: ```ts // packages/auth/src/feature.manifest.ts export const authManifest = defineFeature({ name: "auth", requiredCores: ["analytics"], useCases: { signUp: { mutates: true, audits: ["user.created"], publishes: ["auth.user-signed-up"], consumes: [], analyticsEvents: ["user.signed-up"], // declare here }, }, realtimeChannels: [], jobs: [], } as const); ``` `requiredCores: ["analytics"]` tells the `required-cores-installed` ESLint rule to verify that `@repo/core-analytics` is present in the workspace. ### 2. Call `analytics.track()` in the use-case factory The use case receives `analytics` as a dep, guarded with `?.` so the feature compiles when no analytics instance is provided (dev seed default): ```ts // packages/auth/src/application/use-cases/sign-up.use-case.ts import type { AnalyticsProtocol } from "@repo/core-shared/di"; export function signUpUseCase(deps: { usersRepo: IUsersRepository; analytics?: AnalyticsProtocol; }) { return async (input: SignUpInput): Promise => { const user = await deps.usersRepo.create(input); deps.analytics?.track("user.signed-up", { plan: input.plan }); return signUpOutputSchema.parse(user); }; } ``` Use `AnalyticsProtocol` (from `@repo/core-shared/di`) as the dep type — it exposes only the `track` method, which is all a use case needs. Reserve `IAnalytics` for call sites that need `identify`, `pageView`, or `flush`. ### 3. Wrap with `withAnalytics` in `bind-production.ts` Apply `withAnalytics` inside the existing wrapper chain. Composition order (innermost → outermost): `factory(deps)` → `withAnalytics` → `withAudit` → `withCapture` → `withSpan`. ```ts // packages/auth/src/di/bind-production.ts import { withAnalytics } from "@repo/core-analytics"; export function bindProductionAuth(ctx: BindProductionContext): void { const analytics = ctx.analytics; authContainer .bind(AUTH_SYMBOLS.ISignUpUseCase) .toDynamicValue(() => withSpan( ctx.tracer, { name: "auth.signUp" }, withCapture( ctx.logger, { feature: "auth" }, withAudit( ctx.auditLog, authManifest.useCases.signUp, withAnalytics( analytics, signUpUseCase({ usersRepo: new UsersRepository( ctx.config, ctx.tracer, ctx.logger, ), analytics, }), ), ), ), ), ); assertFeatureConformance( authContainer, authManifest, { signUp: AUTH_SYMBOLS.ISignUpUseCase, }, ctx, ); } ``` `assertFeatureConformance` will throw at boot if a use case declares `analyticsEvents` but its binding is missing the `__analyzed` brand — i.e. if `withAnalytics` was omitted. ### 4. Pass `analytics` from the app aggregator In `apps/web-next/src/server/bind-production.ts`, construct your vendor impl (or `NoopAnalytics` for dev) and thread it through `ctx`: ```ts import { NoopAnalytics } from "@repo/core-analytics"; // import { MyVendorAnalytics } from "@repo/core-analytics-posthog"; // after library eval const analytics = process.env.ANALYTICS_WRITE_KEY ? new MyVendorAnalytics(process.env.ANALYTICS_WRITE_KEY) : new NoopAnalytics(); const ctx: BindProductionContext = { tracer, logger, config, analytics, // ... other optional cores }; bindProductionAuth(ctx); // ... other features ``` Wire `analytics.flush()` into your graceful-shutdown handler so in-flight batches are drained before process exit: ```ts process.on("SIGTERM", async () => { await analytics.flush(); process.exit(0); }); ``` --- ## Client-side wiring The `@repo/core-analytics/react` subpath exports `` and `useAnalytics()`. Both sides share the same `IAnalytics` contract the server uses. ### 1. Mount the provider at the app boundary ```tsx // apps/web-next/src/app/layout.tsx import { AnalyticsProvider } from "@repo/core-analytics/react"; import { clientAnalytics } from "@/lib/analytics"; // your vendor SDK wrapper export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` `clientAnalytics` is an `IAnalytics` instance backed by your vendor SDK. It lives at the app boundary — feature components never import the vendor SDK directly. ### 2. Call `useAnalytics()` in feature components ```tsx // packages/auth/src/ui/sign-up-button.tsx import { useAnalytics } from "@repo/core-analytics/react"; export function SignUpButton() { const analytics = useAnalytics(); return ( ); } ``` `useAnalytics()` throws `AnalyticsContextError` if called outside a provider — the error message tells you where to add the provider. ### 3. Wire route-change events The provider does **not** auto-wire framework route events. Wire your framework's route-changed hook to `pageView()` yourself: ```ts // apps/web-next/src/app/layout.tsx (inside a client component) const analytics = useAnalytics(); const pathname = usePathname(); useEffect(() => { analytics.pageView(pathname); }, [pathname]); ``` ### 4. Call `identify()` after login ```ts const analytics = useAnalytics(); analytics.identify({ id: session.user.id }); ``` Call `identify` once per session or when the user changes. The provider keeps the same instance across renders so subsequent `track` calls share the same context in SDKs that maintain internal user state. --- ## Vendor evaluation The template ships only `NoopAnalytics` (and `RecordingAnalytics` for tests). Before wiring a real backend, run the library evaluation gate: ``` /evaluate-library ``` This invokes the 9-filter + 3-prompt evaluation protocol (ADR-022) and writes a decision trace to `docs/library-decisions/-.md`. The trace covers EU residency, license compatibility, Socket.dev supply-chain flags, and weekly revalidation requirements. Merge the trace alongside the implementation PR. Common vendor packages to evaluate: `posthog-node` / `posthog-js`, `@segment/analytics-node` / `@segment/analytics-next`, `mixpanel` / `mixpanel-browser`. The vendor implementation wraps the SDK and implements `IAnalytics` — it lives in a dedicated package (e.g. `@repo/core-analytics-posthog`) so the rest of the codebase stays vendor-neutral. --- ## PII boundary The analytics surface has a **structurally different PII policy** from the observability surface (ADR-017 §7). **Observability** (`ILogger`, `ITracer`) is id-only: `setUser({ id })` only, `sendDefaultPii: false` everywhere, CI grep gate, server-side scrubbing at the OTel processor layer. This policy is deliberate and remains untouched. **Analytics** is structurally permissive. `AnalyticsUser.id` is the only required field; `attributes` accepts arbitrary values in `identify()`, `track()`, and `pageView()`. The template makes no claim about what's allowed in those attributes. **You are responsible for:** - Cookie consent and legal basis (e.g. GDPR Art. 6) - Retention policy in your analytics backend - Trait allowlist enforcement if you need stricter than permissive - DSAR / right-to-erasure plumbing in the vendor backend There is no CI guardrail enforcing these — the actionable surface is covered by ADR-022 (library evaluation covering the backend) and ADR-023 (supply-chain scan). The separation of analytics from observability is explicit so that neither surface's policy silently relaxes the other's. --- ## Conformance reference | What you changed | Gate that catches drift | | ----------------------------------------------------------- | -------------------------------------- | | Added `analyticsEvents` but no `withAnalytics` | tsc TS2322 + boot assertion | | Called `analytics.track("X")` but `X` not in manifest | `no-undeclared-analytics-event` (warn) | | Declared `requiredCores: ["analytics"]` but package missing | `required-cores-installed` (error) | | Manifest has use case, binder doesn't call `wireUseCase` | `usecase-must-be-wired` (error) | See `docs/guides/conformance-quickref.md` for the full rule table and drift pattern catalogue.