Analytics
One analytics package with 11 providers. Track auth, billing, organisation, and credit events automatically.
Almost Shipped ships with a single analytics package
(@repo/analytics) that auto-tracks the events that matter for SaaS —
auth, billing, organisations, credits, page views — and routes them
through whichever provider you pick.
Default provider: ActuallyTracking (cloud only, no self-hosting). Eleven providers total. Switching is a two-line change.
Enable in 3 steps
Step 1 — Sign up with an analytics provider
Create an account at your chosen provider's dashboard. The default, ActuallyTracking, gives you a client ID, client secret, and an endpoint URL.
Step 2 — Add environment variables
In .env.local:
# ActuallyTracking (default)
NEXT_PUBLIC_ANALYTICS_CLIENT_ID=at_xxxxxxxxxxxx
NEXT_PUBLIC_ANALYTICS_URL=https://your-endpoint.actuallytracking.com
ANALYTICS_CLIENT_ID=at_xxxxxxxxxxxx
ANALYTICS_CLIENT_SECRET=your_client_secret
ANALYTICS_URL=https://your-endpoint.actuallytracking.comSee .env.example at the repo root for every other provider's
env vars.
Step 3 — Enable the feature flag
In apps/saas/modules/shared/lib/feature-flags.ts:
export const FEATURES = {
analytics: true, // ← change to true
credits: true,
} as const;That's it. Auto-tracking fires immediately.
Provider capability matrix
| Provider | Pages | Events | Identify | Group | Server-side |
|---|---|---|---|---|---|
| ActuallyTracking | ✓ | ✓ | ✓ | ✓ | ✓ |
| PostHog | ✓ | ✓ | ✓ | ✓ | ✓ |
| Mixpanel | ✓ | ✓ | ✓ | ✓ | ✓ |
| Google Analytics | ✓ | ✓ | partial | ✗ | ✗ |
| Plausible | ✓ | ✓ | ✗ | ✗ | HTTP |
| Umami | ✓ | ✓ | ✗ | ✗ | HTTP |
| Vercel | ✓ | ✓ | ✗ | ✗ | HTTP |
| Pirsch | ✓ | ✓ | ✗ | ✗ | HTTP |
| Custom | stub | stub | stub | stub | stub |
| Console | ✓ | ✓ | ✓ | ✓ | ✓ |
| None | ✗ | ✗ | ✗ | ✗ | ✗ |
- Pages — automatic page-view tracking on every route change.
- Events — custom event tracking via
track(). - Identify — associate events with a known user.
- Group — associate events with an organisation / workspace.
- Server-side — backend events (auth, billing webhooks, credits)
fire through the provider too.
HTTPmeans the provider has no Node SDK but we send events via their HTTP API.
Auto-tracked events
packages/analytics/src/events.ts defines the canonical event catalog.
Every event fires automatically from the source-of-truth call site —
no per-call wiring required.
auth.signed_up auth.signed_in
auth.email_verified auth.2fa_enabled
auth.passkey_added auth.oauth_connected
org.created org.member_invited
org.member_joined org.member_removed
org.switched
billing.subscription_created billing.subscription_cancelled
billing.subscription_updated billing.payment_succeeded
billing.payment_failed billing.trial_started
billing.trial_converted billing.plan_upgraded
billing.plan_downgraded billing.seat_added
billing.seat_removed billing.credit_purchased
billing.credit_used
onboarding.started onboarding.step_completed
onboarding.completed onboarding.skipped
feature.used page.viewedSwitch provider
Two files. Both must reference the same provider:
packages/analytics/src/provider/index.ts — client swap:
export * from "./posthog"; // ← change thispackages/analytics/src/provider/server.ts — server swap:
import "server-only";
export { serverAdapter } from "./posthog/server"; // ← keep in syncThen restart the dev server. Available targets:
actuallytracking, posthog, mixpanel, google, plausible,
umami, vercel, pirsch, custom, console, none.
Track custom events
Client-side, from any "use client" component:
"use client";
import { useAnalytics } from "@repo/analytics/client";
export function ExportButton() {
const { track } = useAnalytics();
return (
<button onClick={() => track("feature.used", { feature: "export", format: "csv" })}>
Export
</button>
);
}Server-side, from any server component / route handler / webhook:
import { AS_EVENTS } from "@repo/analytics/events";
import { track } from "@repo/analytics/server";
await track(AS_EVENTS.BILLING_SUBSCRIPTION_CREATED, {
userId,
orgId,
planId: "pro",
});GDPR & consent
The analytics script now mounts in both apps (saas + marketing) via
<AnalyticsProvider />. Anonymous users on /login and /signup also
load it. Behaviour by provider:
- ActuallyTracking — cookieless by default. No consent banner needed in most jurisdictions.
- Plausible / Umami / Pirsch / Vercel — cookieless by design. No banner needed.
- PostHog / Mixpanel / Google Analytics — use cookies. You must add your own consent banner for these providers in regulated jurisdictions (GDPR, CCPA, etc.).
The useAnalytics() hook exposes opt-out helpers:
const { optOut, optIn, hasOptedOut } = useAnalytics();
if (!hasUserConsented) optOut(); // localStorage flag, instant no-opAdd a custom provider
Edit packages/analytics/src/provider/custom/client.tsx and
custom/server.ts — both are template files with stub console.info
implementations. Replace each stub with a call into your own SDK or
HTTP API. Then swap the two files in provider/index.ts and
provider/server.ts to ./custom / ./custom/server.
The AnalyticsAdapter interface is defined in
packages/analytics/src/types.ts. Method signatures every provider
must match:
interface AnalyticsAdapter {
track(event, properties?): void;
identify(userId, traits?): void;
group(groupId, groupType, traits?): void;
page(path, properties?): void;
reset(): void;
}No-op any method your tool doesn't support — never throw.