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/sdk

One package, two entry points. The browser code is in @trackbridge/sdk/browser, the server code in @trackbridge/sdk/server. Bundlers tree-shake away whichever side you don’t import.

lib/tracker.client.ts
import { createBrowserTracker } from '@trackbridge/sdk/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/sdk/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.

6. Optional — use a typed helper instead

Section titled “6. Optional — use a typed helper instead”

If your conversion is one of the canonical five (purchase, begin_checkout, add_to_cart, sign_up, refund), the typed helpers replace Step 4’s trackConversion + a separate trackEvent for GA4 with one call:

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

To opt the helper into Ads dual-send, set conversionLabels.purchase on both createBrowserTracker and createServerTracker. With the label set, trackPurchase fires both Ads + GA4; without it, GA4 only. See Semantic helpers.