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.

Get startedView on GitHubpnpm add tracklane
your application
track('purchase', { transaction_id: 'T-1024', value: 49.9, currency: 'BRL' })
what each tool receives
  • 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.

in the browser
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' });
on your server
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.

Get startedpnpm add tracklane