AlmostShipped
Integrations

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.com

See .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

ProviderPagesEventsIdentifyGroupServer-side
ActuallyTracking✓✓✓✓✓
PostHog✓✓✓✓✓
Mixpanel✓✓✓✓✓
Google Analytics✓✓partial✗✗
Plausible✓✓✗✗HTTP
Umami✓✓✗✗HTTP
Vercel✓✓✗✗HTTP
Pirsch✓✓✗✗HTTP
Customstubstubstubstubstub
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. HTTP means 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.viewed

Switch provider

Two files. Both must reference the same provider:

packages/analytics/src/provider/index.ts — client swap:

export * from "./posthog"; // ← change this

packages/analytics/src/provider/server.ts — server swap:

import "server-only";
export { serverAdapter } from "./posthog/server"; // ← keep in sync

Then 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",
});

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-op

Add 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.

On this page