Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions docs/guides/consent-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ import {
### 1. Explicit banner (GDPR-style)

```ts
const analytics = ({ consent: { initial: 'pending', requireExplicit: true } });
const analytics = createAnalytics({
consent: { initialStatus: 'pending', requireExplicit: true },
});

if (getConsent()?.status === 'pending') showBanner();

Expand All @@ -154,8 +156,8 @@ reject.onclick = () => denyConsent();
### 2. Implicit grant on first interaction (opt-out)

```ts
const analytics = ({
consent: { initial: 'pending', requireExplicit: false },
const analytics = createAnalytics({
consent: { initialStatus: 'pending', requireExplicit: false },
});
// First analytics event after user interacts can auto-promote to 'granted'.
// (Never auto-promotes if requireExplicit is true.)
Expand All @@ -164,8 +166,8 @@ const analytics = ({
### 3. Policy version bumps (force re-consent)

```ts
const analytics = ({
consent: { policyVersion: '2024-10-01', initial: 'pending' },
const analytics = createAnalytics({
consent: { policyVersion: '2024-10-01', initialStatus: 'pending' },
});
// If stored policyVersion differs, consent resets to 'pending'.
```
Expand Down Expand Up @@ -238,15 +240,15 @@ If your CMP provides granular categories, gate calls accordingly and keep Trackk

* **Unit/integration tests:** simulate each initial state and verify queue/flush/drop:

* `initial: 'pending'` → queue, then `grantConsent()` → flush
* `initial: 'denied'` with/without `allowEssentialOnDenied` → drop/allow essential
* `initialStatus: 'pending'` → queue, then `grantConsent()` → flush
* `initialStatus: 'denied'` with/without `allowEssentialOnDenied` → drop/allow essential
* `policyVersion` bump → stored consent invalidated → back to `pending`
* Use small `await Promise.resolve()` (or a short `setTimeout(0)`) to let async flushes complete.


## Best Practices

1. **Default to explicit consent** unless your legal basis differs (`requireExplicit: true`, `initial: 'pending'`).
1. **Default to explicit consent** unless your legal basis differs (`requireExplicit: true`, `initialStatus: 'pending'`).
2. **Version your policy** and rotate `policyVersion` when language meaningfully changes.
3. **Don’t rely on auto-promotion** if you need an auditable explicit signal.
4. **Load extras after grant** (pixels, replayers) via `onConsentChange`.
Expand Down
20 changes: 9 additions & 11 deletions docs/guides/custom-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export interface MyProviderOptions {
}
```

These options will later be merged into the main `InitOptions` via `providerOptions` or a dedicated `myProvider` block (depending on how you design it). Keep them minimal and explicit.
These options will later be passed through the `provider` field in `AnalyticsOptions` when you configure your analytics instance. Keep them minimal and explicit.


## Step 2 – Implement the adapter
Expand Down Expand Up @@ -251,19 +251,17 @@ Once the provider is wired into the registry and the type union:
import { createAnalytics } from 'trackkit';

const analytics = createAnalytics({
provider: 'myprovider',
site: '…', // if you need it
// or preferably a dedicated options block based on your types:
providerOptions: {
myprovider: {
apiKey: '…',
endpoint: 'https://api.example-analytics.com',
}
}
provider: {
name: 'myprovider',
site: '…', // if you need it
// Provider-specific options are passed through the provider object:
apiKey: '…',
endpoint: 'https://api.example-analytics.com',
},
});
```

Match this to however youve wired provider-specific options into your `InitOptions`; use the existing Umami / Plausible / GA4 implementations as authoritative examples.
Match this to however you've wired provider-specific options into your `AnalyticsOptions`; use the existing Umami / Plausible / GA4 implementations as authoritative examples.


## Diagnostics & Snapshots
Expand Down
51 changes: 36 additions & 15 deletions docs/guides/queue-management.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Queue Management

Trackkit buffers events when its *not safe* to send yet, then replays them in order once conditions are met.
Trackkit buffers events when it's *not safe* to send yet, then replays them in order once conditions are met.

> **Want to see queue management in action?**
>
Expand Down Expand Up @@ -138,24 +138,45 @@ const analytics = createAnalytics({ debug: true });
const diagnostics = analytics.getDiagnostics();
/*
{
id: 'AF_xxx',
hasProvider: true,
providerReady: true,
queueState: { ... },
facadeQueueSize: 0,
ssrQueueSize: 0,
totalQueueSize: 0,
initializing: false,
provider: 'umami',
consent: 'granted',
debug: true,
lastSentUrl: '/current',
lastPlannedUrl: '/current'
timestamp: 1709553600000,
instanceId: 'AF_xxx',
config: {
autoTrack: true,
debug: true,
queueSize: 50,
trackLocalhost: true,
// ... other resolved config flags
},
consent: {
status: 'granted',
},
dispatcher: {
transportMode: 'smart',
batching: { enabled: false, ... },
resilience: { detectBlockers: false, fallbackStrategy: 'proxy', ... },
connection: { monitor: false, offlineStorage: false, ... },
},
provider: {
key: 'umami',
state: 'ready',
events: 5,
history: [ <state transitions> ],
},
queue: {
totalBuffered: 0,
ssrQueueBuffered: 0,
facadeQueueBuffered: 0,
capacity: 50,
},
urls: {
lastPlanned: '/current',
lastSent: '/current',
},
}
*/
```

You can safely read `facadeQueueSize`, `ssrQueueSize`, and `totalQueueSize` to understand whats currently buffered.
You can safely read `queue.facadeQueueBuffered`, `queue.ssrQueueBuffered`, and `queue.totalBuffered` to understand what's currently buffered.


## SSR Flow
Expand Down
111 changes: 59 additions & 52 deletions docs/guides/resilience-and-transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,25 @@ Other HTTP statuses are treated as permanent failures and are **not** retried.

### Customising retries

You can override retry options in your config (see your `AnalyticsOptions`):
You can override retry options via `dispatcher.resilience.retry` in your `AnalyticsOptions`:

```ts
const analytics = createAnalytics({
provider: {
name: 'umami',
site: '…',
},
retry: {
maxAttempts: 5,
initialDelay: 500,
maxDelay: 60000,
multiplier: 2,
jitter: true,
retryableStatuses: [408, 429, 500, 502, 503, 504],
dispatcher: {
resilience: {
retry: {
maxAttempts: 5,
initialDelay: 500,
maxDelay: 60000,
multiplier: 2,
jitter: true,
retryableStatuses: [408, 429, 500, 502, 503, 504],
},
},
},
});
```
Expand All @@ -87,8 +91,9 @@ Resilience defaults (`RESILIENCE_DEFAULTS`) look like:
```ts
export const RESILIENCE_DEFAULTS = {
detectBlockers: false,
fallbackStrategy: 'smart' as const, // 'smart' | 'proxy' | 'beacon' | 'none'
fallbackStrategy: 'proxy' as const, // 'proxy' | 'beacon'
proxy: undefined,
retry: { /* see RETRY_DEFAULTS above */ },
} as const;
```

Expand All @@ -112,28 +117,32 @@ Transport resolution (in resolve.ts) determines which low-level mechanism (`fetc

### Fallback Strategies

If a blocker is detected, Trackkit chooses a fallback based on `resilience.fallbackStrategy`:
If a blocker is detected, Trackkit chooses a fallback based on `dispatcher.resilience.fallbackStrategy`:

* `'smart'` (default):
* If `proxy.proxyUrl` is configured → uses `ProxiedTransport`.
* If no proxy is configured → uses `BeaconTransport` (often bypasses blockers).
* `'proxy'`: forces usage of `ProxiedTransport`. **Throws a configuration error** if `proxyUrl` is missing. Use this to ensure you don't accidentally send direct requests if proxying fails.
* `'proxy'` (default): forces usage of `ProxiedTransport`. **Throws a configuration error** if `proxyUrl` is missing. Use this to ensure you don't accidentally send direct requests if proxying fails.
* `'beacon'`: forces usage of `BeaconTransport`.
* `'none'`: keeps using `FetchTransport`. The request will likely fail, but it adheres to strict compliance policies if you absolutely cannot use other methods.

If you want no fallback at all, simply leave `detectBlockers: false` (the default). When blocker detection is disabled, Trackkit always uses the base transport (`FetchTransport`) regardless of `fallbackStrategy`.

> **Note:** The default `transportMode` is `'smart'`, which handles the overall transport selection logic. `fallbackStrategy` only applies when `detectBlockers: true` *and* a blocker is detected.

### Configuring `resilience`

Resilience options live under `dispatcher.resilience` in your `AnalyticsOptions`:

```ts
const analytics = createAnalytics({
provider: { name: 'plausible', site: 'yourdomain.com' },
resilience: {
detectBlockers: true,
fallbackStrategy: 'smart',
proxy: {
proxyUrl: '/api/trackkit-proxy',
token: process.env.TRACKKIT_PROXY_TOKEN,
headers: {
'X-Trackkit-Source': 'web',
dispatcher: {
resilience: {
detectBlockers: true,
fallbackStrategy: 'proxy', // 'proxy' | 'beacon'
proxy: {
proxyUrl: '/api/trackkit-proxy',
token: process.env.TRACKKIT_PROXY_TOKEN,
headers: {
'X-Trackkit-Source': 'web',
},
},
},
},
Expand Down Expand Up @@ -162,58 +171,56 @@ Benefits:

## Example strategies

All `resilience` options live under `dispatcher.resilience`:

**Baseline / early stage:**

```ts
resilience: {
detectBlockers: false, // off
dispatcher: {
resilience: {
detectBlockers: false, // off (default)
},
}
```

**Blocker-aware, no proxy:**

```ts
resilience: {
detectBlockers: true,
fallbackStrategy: 'beacon',
}
```

**Production with proxy (Safety First):**

```ts
resilience: {
detectBlockers: true,
fallbackStrategy: 'proxy', // Throws if proxy config missing
proxy: {
proxyUrl: '/api/trackkit',
token: '…',
dispatcher: {
resilience: {
detectBlockers: true,
fallbackStrategy: 'beacon',
},
}
```

**Production with proxy (Auto-fallback):**
**Production with proxy:**

```ts
// Uses proxy if available, but allows falling back to beacon if proxy config is somehow invalid/missing at runtime
resilience: {
detectBlockers: true,
fallbackStrategy: 'smart',
proxy: { ... }
dispatcher: {
resilience: {
detectBlockers: true,
fallbackStrategy: 'proxy', // Throws if proxy config missing
proxy: {
proxyUrl: '/api/trackkit',
token: '…',
},
},
}
```

**Explicitly no fallback:**
**No fallback (default transport only):**

```ts
resilience: {
detectBlockers: true,
fallbackStrategy: 'none',
// Simply leave detectBlockers: false (the default).
// Trackkit always uses FetchTransport; no blocker detection runs.
dispatcher: {
resilience: {
detectBlockers: false,
},
}
```

(events simply fail when blocked – rarely desirable, but available).

## Interaction with queues and consent

Resilience concerns **how** events are sent, not whether they *should* be sent.
Expand Down
4 changes: 2 additions & 2 deletions docs/overview/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ The facade is the only layer that users call. It owns:

---

### Configuration (`src/config/schema.ts`, `src/util/env.ts`)
### Configuration (`src/facade/config.ts`, `src/util/env.ts`)

Configuration is schema-driven:

* `config/schema.ts` defines the shape of `InitOptions` and how defaults are applied.
* `facade/config.ts` defines how `AnalyticsOptions` are merged and how defaults are applied.
* `util/env.ts` reads build-time env (`TRACKKIT_*`, `VITE_TRACKKIT_*`, etc.) and runtime overrides (`window.__TRACKKIT_ENV__`, meta tags).

The merge order (simplified):
Expand Down
30 changes: 30 additions & 0 deletions docs/overview/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,35 @@ analytics.track('signup_submitted', {
});
```

### Typed events (optional)

For compile-time checking of event names and properties, define an event map
and pass it as a type parameter:

```ts
// analytics.ts
import { createAnalytics } from 'trackkit';

type MyEvents = {
signup_submitted: { plan: 'free' | 'pro'; source: string };
purchase_completed: { amount: number; currency: string };
};

export const analytics = createAnalytics<MyEvents>({
provider: { name: 'umami', site: '...' },
});

analytics.track('signup_submitted', { plan: 'pro', source: 'hero' }); // ✅
analytics.track('signup_submitted', { plan: 'gold' }); // ❌ type error
analytics.track('unknown_event'); // ❌ not in MyEvents
```

When no type parameter is supplied, `track()` accepts any string name and any
props — identical to untyped usage.

> The singleton API does not support typed events. Use `createAnalytics<E>()`
> for type-checked tracking.

### Identify (where supported)

```ts
Expand Down Expand Up @@ -204,3 +233,4 @@ const analytics = createAnalytics({
- [Plausible](/providers/plausible)
- [Google Analytics 4](/providers/ga4)
- See full SSR semantics: [SSR Guide](/guides/ssr)
- Typed events API details: [API Reference](/reference/api)
Loading
Loading