Skip to content
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ vite.config.js.timestamp-*
vite.config.ts.timestamp-*

# Misc
.audit
*.DS_Store
deadcode-report.md
docs/reference/sdk/
55 changes: 21 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,30 @@
[![bundle size](https://img.shields.io/bundlephobia/minzip/trackkit)](https://bundlephobia.com/package/trackkit)
![types](https://img.shields.io/npm/types/trackkit)

Trackkit is a lightweight, provider-agnostic analytics SDK with a single facade for Umami, Plausible, GA4 (Measurement Protocol only), and custom adapters.
Trackkit is a lightweight, provider-agnostic analytics SDK with a single facade for Umami, Plausible, and GA4 (Measurement Protocol).
This repository hosts the full SDK source, development tooling, documentation, tests, and release pipeline.

**SSR-aware • MV3-safe • No remote scripts • <20 kB core (brotli)**
**SSR-aware • CSP-friendly • No remote scripts • Tree-shakeable**

- **Zero remote scripts** — CSP-friendly and safe for MV3 and strict environments.
- **Consent-aware** — EU-style consent flows with event gating + queueing.
- **SSR hydration** — collect server events and replay once on the client.
- **Multi-provider** — Umami/Plausible baseline with optional GA4 layer.
- **Typed DX** — clean facade API, strong TypeScript types, great developer tooling.
### What happens when you call `analytics.track()`

## Why Trackkit?
Every call flows through the same runtime pipeline — you never need to think about timing, readiness, or consent yourself:

* **Privacy-first**: cookieless by default for Umami & Plausible; consent-aware for GA4.
* **No remote scripts**: CSP-friendly, safe for MV3 extensions, workers, and strict sites.
* **Small & fast**: tree-shakeable core; adapters load lazily.
* **Multi-provider**: run Umami/Plausible for everyone and layer GA4 for consented users.
* **SSR aware**: queue on the server, hydrate and replay on the client.
* **DX matters**: typed API, debug logs, queue inspection, provider state machine.
1. **Lazy-load** the provider adapter (only the one you configure is bundled).
2. **Queue events** while the provider or consent isn't ready yet.
3. **Respect consent**, domain rules, DNT, and localhost settings before anything is sent.
4. **Flush** the queue through PolicyGate → Consent → Provider → Transport.

This means you can call `track()` at any point — during SSR, before the provider script has loaded, or before a user has responded to a consent banner — and Trackkit will do the right thing.

### Why Trackkit?

* **Privacy-first** — cookieless by default for Umami & Plausible; consent-aware for GA4.
* **No remote scripts** — CSP-friendly, no injected `<script>` tags; safe for strict environments.
* **Small & fast** — tree-shakeable core; only the adapter you use is bundled.
* **Multi-provider** — run Umami/Plausible for everyone and layer GA4 for consented users.
* **SSR aware** — queue on the server, hydrate and replay on the client.
* **Typed DX** — conditional tuple types, debug logs, queue inspection, provider state machine.

## Documentation

Expand All @@ -34,16 +39,6 @@ Visit Trackkit's **[full documentation site](https://enkosiventures.github.io/tr

To run the documentation site locally, run `pnpm docs:dev` and open [`http://localhost:5173`](http://localhost:5173).

## Packages in this monorepo

| Package | Path | Status | Purpose |
| ------------------------- | ------------------------------------ | ----------- | ------------------------------------------------------------- |
| **Core SDK** | `packages/trackkit` | available | Provider-agnostic runtime + built-ins (Umami, Plausible, GA4) |
| React wrapper | `packages/trackkit-react` | planned | `<AnalyticsProvider />`, hooks |
| Vue wrapper | `packages/trackkit-vue` | planned | Plugin + composables |
| Plugin API | `packages/trackkit-plugin-api` | planned | Adapter interface & dev helpers |
| Example plugin | `packages/trackkit-plugin-amplitude` | planned | Amplitude adapter (opt-in) |

This repo uses **pnpm workspaces**. All commands below are run from the repository root.

## Core SDK
Expand Down Expand Up @@ -95,14 +90,6 @@ track('signup_submitted', { plan: 'starter' });

Internally, both forms hit the same core sdk.

### Gating & Flow

Trackkit sends events only after passing:

**PolicyGate → Consent → Provider readiness → Queue/Offline → Transport**

Every mechanism (SSR, offline, resilience, performance) is downstream of these gates.

### Consent (EU-friendly defaults)

```ts
Expand Down Expand Up @@ -159,9 +146,9 @@ Trackkit queues events during server rendering and hydrates them on the client.

See the **[Server-Side Rendering](https://enkosiventures.github.io/trackkit/guides/ssr)** guide for full semantics.

### CSP / MV3
### CSP

Add the providers you use to `connect-src`. Example:
Trackkit sends all data via `fetch` — no injected `<script>` tags. Add the providers you use to `connect-src`:

```jsonc
"connect-src": [
Expand Down
3 changes: 3 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ hero:
- theme: brand
text: Get Started
link: /overview/what-is-trackkit
- theme: alt
text: Download Now
link: https://www.npmjs.com/package/trackkit
- theme: alt
text: View on GitHub
link: https://github.com/enkosiventures/trackkit
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import type {

### Initialization

#### `createAnalytics<E extends EventMap = AnyEventMap>(opts?: AnalyticsOptions): AnalyticsInstance<E>`
#### `createAnalytics<E extends EventMap = AnyEventMap>(opts?: AnalyticsOptions): AnalyticsFacade<E>`

Factory API. Creates a **new analytics instance**.

Expand Down
58 changes: 35 additions & 23 deletions examples/next-ssr-ga4/README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,48 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Next.js SSR + GA4 via Trackkit

## Getting Started
Demonstrates server-side rendering with Trackkit's SSR API and GA4 (Measurement Protocol).

First, run the development server:
## What this example shows

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
- **SSR event collection** — `ssrPageview()` and `ssrTrack()` run inside `getServerSideProps` and enqueue events into `globalThis.__TRACKKIT_SSR_QUEUE__` without initialising a provider or making network calls.
- **HTML injection** — a custom `_document.tsx` captures the SSR queue in `getInitialProps` and injects it as a `<script>` tag so the client SDK can hydrate it.
- **Client hydration** — the client-side `createAnalytics()` instance picks up the injected queue automatically and replays server events through GA4 alongside any client events.
- **Client tracking** — a "Track Event" button fires `analytics.track()` on the client to demonstrate client-side events merging with the hydrated SSR queue.

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Key files

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
| File | Role |
|------|------|
| `lib/analytics.ts` | Creates the GA4 analytics instance (client-only, guarded by `typeof window`) |
| `pages/_app.tsx` | Wraps all pages; exposes the analytics instance on `window` for debugging |
| `pages/index.tsx` | Calls `ssrPageview()` and `ssrTrack()` in `getServerSideProps`; renders a split-view showing server vs client events |
| `pages/_document.tsx` | Captures and injects `__TRACKKIT_SSR_QUEUE__` into the HTML |

## Setup

```bash
cd examples/next-ssr-ga4
pnpm install
```

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
Create a `.env.local`:

## Learn More
```bash
NEXT_PUBLIC_GA4_MEASUREMENT_ID=G-XXXXXXXXXX
```

To learn more about Next.js, take a look at the following resources:
## Run

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
```bash
pnpm dev
```

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
Open [http://localhost:3000](http://localhost:3000). The page renders server-side events on the left panel and lets you fire client events on the right. Check the Network tab to see events sent to GA4.

## Deploy on Vercel
## SSR guarantees

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
The SSR API (`trackkit/ssr`) is designed to be safe in server contexts:

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
- **Does NOT initialise providers** — `ssrTrack` / `ssrPageview` / `ssrIdentify` only push to an in-memory queue.
- **Does NOT manipulate consent** — consent state is a client concern; server calls accept a category but never read or write consent.
- **Does NOT flush or send** — `flushSSRAll()` is a no-op on the server (`if (!hasDOM()) return []`). Flushing only happens client-side during hydration.
57 changes: 4 additions & 53 deletions examples/next-ssr-ga4/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,6 @@
// import Document, { Html, Head, Main, NextScript, DocumentProps, DocumentContext } from 'next/document';
// // We simply capture the queue state; no special helper function needed if we inject manually
// import { getSSRQueue } from 'trackkit/ssr';

// type Props = DocumentProps & {
// // Pass the serialized queue as a prop
// initialQueueState?: any[];
// };

// class MyDocument extends Document<Props> {
// static async getInitialProps(ctx: DocumentContext) {
// const initialProps = await Document.getInitialProps(ctx);

// // Capture events tracked during getServerSideProps
// // We clone the queue to safely serialize it
// const initialQueueState = [...(getSSRQueue() || [])];

// return { ...initialProps, initialQueueState };
// }

// render() {
// const { initialQueueState } = this.props;

// return (
// <Html lang="en">
// <Head />
// <body>
// <Main />

// {/* Inject the queue state for the client SDK to hydrate */}
// {initialQueueState && initialQueueState.length > 0 && (
// <script
// id="trackkit-ssr"
// dangerouslySetInnerHTML={{
// __html: `window.__tk_ssr_queue=${JSON.stringify(initialQueueState)}`,
// }}
// />
// )}

// <NextScript />
// </body>
// </Html>
// );
// }
// }

// export default MyDocument;


import Document, { Html, Head, Main, NextScript } from 'next/document';
import type { DocumentProps, DocumentContext } from 'next/document';
import { getSSRQueue } from 'trackkit/ssr';
import { getSSRQueue, serializeSSRQueue } from 'trackkit/ssr';

type Props = DocumentProps & {
initialQueueState?: any[];
Expand All @@ -76,11 +27,11 @@ class MyDocument extends Document<Props> {

{/* 2. Inject using the CORRECT global variable expected by Trackkit */}
{initialQueueState && initialQueueState.length > 0 && (
<script
<div
id="trackkit-ssr"
dangerouslySetInnerHTML={{
// MUST be __TRACKKIT_SSR_QUEUE__ to match the SDK hydration logic
__html: `window.__TRACKKIT_SSR_QUEUE__=${JSON.stringify(initialQueueState)}`,
// serializeSSRQueue escapes </script> breakouts and other XSS vectors
__html: serializeSSRQueue(initialQueueState),
}}
/>
)}
Expand Down
63 changes: 0 additions & 63 deletions examples/next-ssr-ga4/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,3 @@
// import type { GetServerSideProps } from 'next';
// import { ssrPageview } from 'trackkit/ssr';
// import { analytics } from '../lib/analytics';

// type HomeProps = {
// url: string;
// };

// export const getServerSideProps: GetServerSideProps<HomeProps> = async (ctx) => {
// const url = ctx.resolvedUrl || '/';

// // Record a server-side pageview. This only enqueues into the SSR queue;
// // no provider is initialised on the server.
// ssrPageview(url);

// return {
// props: {
// url,
// },
// };
// };

// export default function Home({ url }: HomeProps) {
// const handleClick = () => {
// analytics?.track('signup_clicked', {
// plan: 'pro',
// source: 'next-ssr-ga4-example',
// });
// };

// return (
// <main style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
// <h1>Trackkit + Next.js (SSR) + GA4</h1>
// <p>
// This page was rendered for <code>{url}</code> with a server-side
// <code>pageview</code> via <code>trackkit/ssr</code>.
// </p>

// <button
// onClick={handleClick}
// style={{
// marginTop: '1rem',
// padding: '0.75rem 1.5rem',
// fontSize: '1rem',
// cursor: 'pointer',
// }}
// >
// Simulate “Sign up” event
// </button>

// <section style={{ marginTop: '2rem' }}>
// <h2>Diagnostics</h2>
// <p>
// In the browser console, run{' '}
// <code>window.__analytics?.getDiagnostics()</code> to inspect the
// hydrated queue, provider state, and consent snapshot.
// </p>
// </section>
// </main>
// );
// }


import { useState } from 'react';
import type { GetServerSideProps } from 'next';
import { ssrPageview, ssrTrack, getSSRQueue } from 'trackkit/ssr';
Expand Down
30 changes: 22 additions & 8 deletions examples/react-spa-umami/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
# React SPA + Umami via Trackkit

Minimal example showing how to use Trackkit in a Vite + React SPA with Umami.
Demonstrates a client-side React app using Trackkit with Umami as the analytics provider, including consent management and automatic page tracking.

## What this example shows

- **Auto-tracking** — `autoTrack: true` listens for `pushState` / `popstate` and sends pageviews automatically as you navigate between routes. No manual `pageview()` calls needed.
- **Consent flow** — analytics starts with `initialStatus: 'pending'` and `requireExplicit: true`. Events queue locally until the user interacts with the consent banner (`ConsentBanner.tsx`).
- **Custom events** — the Pricing page fires `analytics.track('signup_clicked', { plan, source })` on button click.
- **Umami adapter** — cookieless by default, no remote `<script>` tag; all data is sent via `fetch`.

## Key files

| File | Role |
|------|------|
| `src/analytics.ts` | Creates the Umami analytics instance with consent pending |
| `src/App.tsx` | Simple SPA router with three pages; demonstrates auto-tracking and custom events |
| `src/ConsentBanner.tsx` | Subscribes to consent state and calls `grantConsent()` / `denyConsent()` |
| `src/main.tsx` | React entry point; exposes analytics on `window` for debugging |

## Setup

```bash
cd examples/react-spa-umami

# if you haven't created the app yet:
# pnpm create vite . --template react-ts

pnpm install
````
```

Create a `.env.local`:

Expand All @@ -26,5 +38,7 @@ VITE_UMAMI_HOST=https://cloud.umami.is # or your self-hosted domain
pnpm dev
```

Then visit [http://localhost:5173](http://localhost:5173) and click around.
Trackkit will send pageviews (via `autoTrack`) and a custom event on button click.
Open [http://localhost:5173](http://localhost:5173) and click around. A consent banner appears at the bottom — events queue locally until you accept. Navigate between Home, Features, and Pricing to see auto-tracked pageviews. Click the signup button on the Pricing page to fire a custom event.

Inspect `window.__analytics.getDiagnostics()` in the console to see queued events, provider state, and consent status.

Loading
Loading