Name the event once.
Every tool gets its own.
One interface between your application and every analytics and advertising tool, in the browser and through their conversion APIs.
track('purchase', { transaction_id: 'T-1024', value: 49.9, currency: 'BRL' })- GA4shipped
gtag('event', 'purchase', { transaction_id: 'T-1024', value: 49.9, currency: 'BRL' }) - Metashipped
fbq('track', 'Purchase', { value: 49.9, currency: 'BRL' }, { eventID: 'T-1024' }) - LinkedInnot shipped yet
lintrk('track', { conversion_id: 8421066, conversion_value: 49.9, currency: 'BRL', event_id: 'T-1024' }) - PostHogshipped
posthog.capture('purchase', { transaction_id: 'T-1024', value: 49.9, currency: 'BRL' }) - Xnot shipped yet
twq('event', 'tw-o1abc-oq2de', { value: 49.9, currency: 'BRL', conversion_id: 'T-1024' })
the vocabulary
The same event, both sides.
Two independent libraries that share only the vocabulary, which is what makes an event mean the same thing in the browser and in your backend. The server half speaks each vendor's own conversion API, Measurement Protocol, Conversions API, and Capture API, from a call that looks like the browser one. No shared runtime, no shared state, and nothing to keep in sync but the name.
import { createTracking, ga4 } from 'tracklane/browser';
const { track, identify, consent } = createTracking({
providers: [ga4('G-XXXXXXX')],
onError: (error) => report(error),
});
track('purchase', { transaction_id: 'T-1024', value: 49.9, currency: 'BRL' });
identify({ userId: 'user_42', email: 'ana@example.com' }, { plan: 'pro' });
consent('update', { ad_storage: 'granted', analytics_storage: 'granted' });import { createTracking, ga4 } from 'tracklane/server';
const { track } = createTracking({
providers: [ga4({ measurementId: 'G-XXXXXXX', apiSecret })],
});
await track('purchase', order, {
cookies: request.headers.get('cookie'),
user: { userId: order.userId, email: order.email },
dedupId: order.id,
timestamp: order.paidAt,
});the rule
It standardises and organises.
It never changes behaviour.
Every decision is settled by one question: if you did not have this library, how would you write this line by hand? It replicates exactly that.
One event, every tool
Name what happened once, in GA4 vocabulary. Each provider translates it into its own. Adding a tool is a line of configuration, not a diff across your application.
Pixels and conversion APIs
The server-side APIs are the expensive half: hashed identifiers, deduplication against the browser event, a different payload shape per vendor. Every provider ships both halves, and neither is an add-on.
No opinions
It never withholds a send on policy grounds, holds no consent state, and installs no vendor tags. What reaches a tool is what a hand-written call would have sent, including the errors.
Nothing you did not ask for
No queue, no retry, no delivery guarantee, no runtime dependencies. One vendor failing never affects another, and never reaches your call site.
destinations
Five in v1, and anything else you write.
Any tool that receives events about what your users do can be a destination, not just ad pixels. Writing your own uses the same contract the built-in ones use: no registry entry, no allow-list.
- GA4, Measurement Protocol, shipped
- Meta, Conversions API, shipped
- LinkedIn, Conversions API, not shipped yet
- PostHog, Capture API, shipped
- X, Conversions API, not shipped yet
Start with one tool.
GA4, Meta, and PostHog ship today, in the browser and on the server. LinkedIn and X are next, one at a time, verified against a real account before it ships.
pnpm add tracklane