Resilience & Transports
Trackkit is designed to survive real-world conditions:
- flaky or slow networks
- ad blockers and privacy extensions
- strict CSP and corporate proxies
This guide explains how retries, adblocker detection, and transports work together, and how to configure them.
Overview
When Trackkit dispatches an event:
- It builds a provider-specific payload.
- It resolves the transport based on your configuration and environment:
- Checks
transportMode(fetch vs beacon). - If enabled, runs adblocker detection and switches to a fallback strategy (e.g. proxy) if needed.
- Checks
- It hands the request to the transport, which applies retry logic (backoff/jitter) if the request fails.
You control this via:
retryoptions (see dispatcher types)resilienceoptionsRETRY_DEFAULTSandRESILIENCE_DEFAULTSinconstants.ts
Retry behaviour
Trackkit’s default retry settings (RETRY_DEFAULTS) are:
export const RETRY_DEFAULTS = {
maxAttempts: 3,
initialDelay: 1000, // ms
maxDelay: 30000, // ms
multiplier: 2,
jitter: true,
retryableStatuses: [408, 429, 500, 502, 503, 504],
} as const;What this means
Up to 3 attempts (initial try + up to 2 retries).
Exponential backoff starting at 1s, capped at 30s.
Jitter randomises delays slightly to avoid thundering herd.
Retries only on:
408(timeout)429(rate limited)500,502,503,504(transient server errors)
Other HTTP statuses are treated as permanent failures and are not retried.
Customising retries
You can override retry options via dispatcher.resilience.retry in your AnalyticsOptions:
const analytics = createAnalytics({
provider: {
name: 'umami',
site: '…',
},
dispatcher: {
resilience: {
retry: {
maxAttempts: 5,
initialDelay: 500,
maxDelay: 60000,
multiplier: 2,
jitter: true,
retryableStatuses: [408, 429, 500, 502, 503, 504],
},
},
},
});Recommended:
- Use higher
maxAttempts/maxDelayfor low-volume, high-value events. - Use lower values when latency matters more than “event eventually lands”.
Adblocker detection
Adblockers often block requests to known analytics endpoints. Trackkit can probe for this and adjust the transport.
Resilience defaults (RESILIENCE_DEFAULTS) look like:
export const RESILIENCE_DEFAULTS = {
detectBlockers: false,
fallbackStrategy: 'proxy' as const, // 'proxy' | 'beacon'
proxy: undefined,
retry: { /* see RETRY_DEFAULTS above */ },
} as const;detectBlockers
false(default) → no detection, always use the base transport (fetch).true→ run an adblocker check at runtime, and if a blocker is detected, choose a fallback transport.
Detection is handled by dispatcher/adblocker.ts and typically cached for the session.
Transports & fallback strategy
Transport resolution (in resolve.ts) determines which low-level mechanism (fetch vs beacon) or endpoint (direct vs proxy) to use.
- Check Base Transport: Uses
FetchTransportby default. - Check Blockers:
- If
detectBlockersis disabled, proceed with base transport. - If
detectBlockersis enabled, run a probe.- If no blocker detected → proceed with base transport.
- If a blocker is detected → resolve the fallback strategy.
- If
Fallback Strategies
If a blocker is detected, Trackkit chooses a fallback based on dispatcher.resilience.fallbackStrategy:
'proxy'(default): forces usage ofProxiedTransport. Throws a configuration error ifproxyUrlis missing. Use this to ensure you don't accidentally send direct requests if proxying fails.'beacon': forces usage ofBeaconTransport.
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
transportModeis'smart', which handles the overall transport selection logic.fallbackStrategyonly applies whendetectBlockers: trueand a blocker is detected.
Configuring resilience
Resilience options live under dispatcher.resilience in your AnalyticsOptions:
const analytics = createAnalytics({
provider: { name: 'plausible', site: 'yourdomain.com' },
dispatcher: {
resilience: {
detectBlockers: true,
fallbackStrategy: 'proxy', // 'proxy' | 'beacon'
proxy: {
proxyUrl: '/api/trackkit-proxy',
token: process.env.TRACKKIT_PROXY_TOKEN,
headers: {
'X-Trackkit-Source': 'web',
},
},
},
},
});Proxy transport
ProxiedTransport sends events to your own backend instead of directly to the analytics host:
- Adds an
X-Trackkit-Targetheader with the original URL. - Can add a bearer token and arbitrary headers.
- Forwards selected
fetchoptions (e.g.credentials,mode,cache) where supported.
On your backend, you implement the proxy endpoint:
- Validate request (token, headers).
- Forward payload to Umami/Plausible/GA4/etc.
- Apply any additional privacy logic (IP truncation, sampling, etc.).
Benefits:
- Simplified CSP: only need to allow your own domain in
connect-src. - Better resilience against adblockers targeting common analytics domains.
- Centralised logging and control over outbound analytics traffic.
Example strategies
All resilience options live under dispatcher.resilience:
Baseline / early stage:
dispatcher: {
resilience: {
detectBlockers: false, // off (default)
},
}Blocker-aware, no proxy:
dispatcher: {
resilience: {
detectBlockers: true,
fallbackStrategy: 'beacon',
},
}Production with proxy:
dispatcher: {
resilience: {
detectBlockers: true,
fallbackStrategy: 'proxy', // Throws if proxy config missing
proxy: {
proxyUrl: '/api/trackkit',
token: '…',
},
},
}No fallback (default transport only):
// Simply leave detectBlockers: false (the default).
// Trackkit always uses FetchTransport; no blocker detection runs.
dispatcher: {
resilience: {
detectBlockers: false,
},
}Interaction with queues and consent
Resilience concerns how events are sent, not whether they should be sent.
Rough order of operations:
PolicyGate decides if sending is allowed:
- consent (pending / granted / denied)
doNotTrack- localhost policy
- domain / exclude rules
Queues buffer events if needed:
- pre-init / SSR queues
- runtime facade queue
- overflow drops oldest events and emits
QUEUE_OVERFLOW
Resilience / transport controls actual delivery:
- which transport (fetch / beacon / proxy)
- retry strategy
- behaviour under adblockers
If consent is denied or policy gates block, no transport or retry setting will send anything. If a queue is full, no transport choice can save the events you’ve chosen to drop.
Use resilience to handle network and environment issues, not to bypass policy.