Skip to content

Quick start

Five minutes from bun add to a deduplicated conversion. This page assumes you already have a Google Ads conversion ID and a GA4 measurement ID. If you don’t, the Google Ads OAuth guide covers obtaining the secrets you need on the server side.

Terminal window
bun add @trackbridge/browser @trackbridge/server

The two packages are versioned together. Install both even if you think you only need one — the dual-send pattern is the point of the SDK.

lib/tracker.client.ts
import { createBrowserTracker } from '@trackbridge/browser';
export const tracker = createBrowserTracker({
adsConversionId: process.env.NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_ID!,
ga4MeasurementId: process.env.NEXT_PUBLIC_GA4_MEASUREMENT_ID!,
consentMode: 'v2',
debug: process.env.NODE_ENV !== 'production',
});

The example uses process.env.NEXT_PUBLIC_*. For Vite use import.meta.env.VITE_*; for SvelteKit and Astro use import.meta.env.PUBLIC_*. Whatever the framework, the token must be inlined into the client bundle — these two values are public.

consentMode: 'v2' is the right default for any site that operates under GDPR or CCPA. Set it to 'off' only if you have an explicit reason. With v2, click-identifier cookies are gated on ad_storage consent — see Consent Mode v2.

lib/tracker.server.ts
import { createServerTracker } from '@trackbridge/server';
export const serverTracker = createServerTracker({
ga4MeasurementId: process.env.NEXT_PUBLIC_GA4_MEASUREMENT_ID!,
ga4ApiSecret: process.env.GA4_API_SECRET!,
ads: {
developerToken: process.env.GOOGLE_ADS_DEVELOPER_TOKEN!,
customerId: process.env.GOOGLE_ADS_CUSTOMER_ID!,
refreshToken: process.env.GOOGLE_ADS_REFRESH_TOKEN!,
clientId: process.env.GOOGLE_OAUTH_CLIENT_ID!,
clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET!,
conversionActions: {
purchase: 'customers/1234567890/conversionActions/9876543210',
},
},
debug: process.env.NODE_ENV !== 'production',
});

Import this only from server-side code: API routes, server actions, webhook handlers. The five GOOGLE_* and GA4_API_SECRET values must never reach the browser bundle.

The keys in conversionActions are your own labels. The values are full Ads API resource names of the form customers/{customerId}/conversionActions/{actionId} — see Mapping conversion actions.

The same call shape on the browser and on the server, with the same transactionId. Google sees both, deduplicates them on the transaction ID, and counts the conversion once.

app/checkout/success/page.tsx
'use client';
import { useEffect } from 'react';
import { tracker } from '@/lib/tracker.client';
export function FireConversion({ order }: { order: Order }) {
useEffect(() => {
tracker.trackConversion({
label: 'purchase',
value: order.total,
currency: 'USD',
transactionId: order.id,
userData: {
email: order.customer.email,
},
});
}, [order]);
return null;
}

The transactionId is the dedup key. Pass order.id (or any stable string you own) on both sides. Do not generate a UUID per call — different IDs cause Google to count the conversion twice. If you omit transactionId, the SDK auto-generates one and prints a warning, because dual-send is disabled for that call.

The userData is passed raw. The SDK normalizes (lowercase, NFC, trim) and SHA-256 hashes the fields Google requires hashed, identically on both sides. Never hash in your own code.

In debug: true mode, the SDK is loud about both successes and failures.

Browser side. Open DevTools → Network, filter for googleadservices.com and google-analytics.com. After the conversion, you should see a collect request to GA4 and a gtag conversion request to Ads with the transaction_id you passed.

Server side. The Ads API returns 200 OK on accepted batches. With debug: true, any non-2xx response is logged with the response body so you can see what Google rejected.

End-to-end attribution into the Ads UI takes up to 24 hours. During development, watch the network requests — that’s the contract.