Next.js App Router setup
The minimal wiring to get Trackbridge running in a Next.js App Router project, using the @trackbridge/sdk/next and @trackbridge/sdk/next/server adapters.
You can use Trackbridge without these adapters — createBrowserTracker and createServerTracker work in any framework. The adapters exist to remove the boilerplate every Next.js integration ends up writing: gtag-loading order, the 'use client' provider, the page-view subscription to usePathname, and the module-level singleton for createServerTracker.
Prerequisites
Section titled “Prerequisites”- Next.js 13.4+ (App Router).
- Trackbridge installed:
pnpm add @trackbridge/sdk(orbun add/npm install). - A Google Ads conversion ID and a GA4 measurement ID. For server-side, the five
GOOGLE_*andGA4_API_SECRETvalues from the Google Ads OAuth guide.
1. Wire the env vars
Section titled “1. Wire the env vars”# Public — sent to the browserNEXT_PUBLIC_GOOGLE_ADS_CONVERSION_ID="AW-XXXXXXXXX"NEXT_PUBLIC_GA4_MEASUREMENT_ID="G-XXXXXXXXXX"NEXT_PUBLIC_GOOGLE_ADS_PURCHASE_LABEL="abcDEF1234"
# Server-only — never exposeGA4_API_SECRET=""GOOGLE_ADS_DEVELOPER_TOKEN=""GOOGLE_ADS_CUSTOMER_ID=""GOOGLE_ADS_REFRESH_TOKEN=""GOOGLE_OAUTH_CLIENT_ID=""GOOGLE_OAUTH_CLIENT_SECRET=""GOOGLE_ADS_PURCHASE_RESOURCE_NAME=""The NEXT_PUBLIC_GOOGLE_ADS_PURCHASE_LABEL is your gtag conversion label — same string used for send_to: ${adsConversionId}/${label} on the browser side. The GOOGLE_ADS_PURCHASE_RESOURCE_NAME is the Ads API resource name (customers/{customerId}/conversionActions/{actionId}) — see Mapping conversion actions.
2. Wrap the root layout
Section titled “2. Wrap the root layout”import { TrackbridgeProvider, TrackbridgePageViews } from '@trackbridge/sdk/next';
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <body> <TrackbridgeProvider config={{ adsConversionId: process.env.NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_ID!, ga4MeasurementId: process.env.NEXT_PUBLIC_GA4_MEASUREMENT_ID!, consentMode: 'v2', conversionLabels: { purchase: process.env.NEXT_PUBLIC_GOOGLE_ADS_PURCHASE_LABEL, }, }} > <TrackbridgePageViews /> {children} </TrackbridgeProvider> </body> </html> );}<TrackbridgeProvider> injects gtag('consent', 'default', { all-denied }) beforeInteractive and loads gtag.js afterInteractive. <TrackbridgePageViews /> fires trackPageView on every navigation.
For non-EEA deployments, pass consentDefaults={{ ad_storage: 'granted', /* … */ }} to override the all-denied default.
3. Use the tracker from any client component
Section titled “3. Use the tracker from any client component”'use client';import { useTracker } from '@trackbridge/sdk/next';
export function CheckoutButton({ cart }: { cart: Cart }) { const tracker = useTracker();
return ( <button onClick={async () => { await tracker.trackBeginCheckout({ transactionId: cart.id, value: cart.total, currency: cart.currency, items: cart.items, }); router.push('/checkout'); }} > Checkout </button> );}useTracker throws if used outside <TrackbridgeProvider> — that’s a programming error, not a runtime issue.
For non-component code (e.g., calling the tracker from a Server Action’s resolved value), define a lib/tracker.client.ts module with createBrowserTracker and import it directly. The hook is for client-component composition.
4. Define the server tracker once
Section titled “4. Define the server tracker once”import 'server-only';import { defineServerTracker } from '@trackbridge/sdk/next/server';
export const getServerTracker = defineServerTracker(() => ({ 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: process.env.GOOGLE_ADS_PURCHASE_RESOURCE_NAME!, }, }, conversionLabels: { purchase: process.env.NEXT_PUBLIC_GOOGLE_ADS_PURCHASE_LABEL, },}));defineServerTracker lazy-constructs the tracker on first use and caches it for the lifetime of the module. If construction fails, the error is cached and re-thrown on every subsequent call — fix the config and reload.
5. Fire conversions from a route handler
Section titled “5. Fire conversions from a route handler”import { getServerTracker } from '@/lib/tracker.server';
export async function POST(req: Request) { const event = await verifyStripeSignature(req); if (event.type !== 'checkout.session.completed') return new Response();
const order = await db.orders.findByStripeId(event.data.object.id);
const result = await getServerTracker().trackPurchase({ transactionId: order.id, value: order.total, currency: order.currency, items: order.items, clientId: order.gaClientId, // forwarded from tracker.getClientId() at checkout gclid: order.gclid, userData: { email: order.customer.email }, });
if (!result.ads.ok && !('skipped' in result.ads)) reportError(result.ads.error); if (!result.ga4.ok && !('skipped' in result.ga4)) reportError(result.ga4.error);
return new Response();}6. Optional — read identity from request cookies
Section titled “6. Optional — read identity from request cookies”For routes where the user’s browser is making the request itself (rather than a third-party webhook like Stripe), you can pull the auto-captured _tb_* and _ga cookies straight off the request via readEnvelopeFromRequest:
import { cookies, headers } from 'next/headers';import { readEnvelopeFromRequest } from '@trackbridge/sdk/next/server';import { getServerTracker } from '@/lib/tracker.server';
export async function POST(req: Request) { const order = await createOrder(await req.json());
const envelope = readEnvelopeFromRequest({ cookies: cookies(), headers: headers(), });
if (envelope !== null) { const bound = getServerTracker().fromContext(envelope); const result = await bound.trackPurchase({ transactionId: order.id, value: order.total, currency: order.currency, items: order.items, // clientId / gclid / userData all default from the envelope }); if (!result.ads.ok && !('skipped' in result.ads)) reportError(result.ads.error); }
return Response.json({ order });}For pre-login userData (or cross-origin cases where cookies are blocked), send an x-trackbridge-context header carrying tracker.exportContext({ userData }) from the browser. readEnvelopeFromRequest merges header over cookies per field.
Verification
Section titled “Verification”next devand load the app. DevTools → Network: confirmgtag/js?id=AW-...loads, the consent default snippet ran, and apage_viewcollectrequest fires on navigation.- Click through to checkout. Confirm
begin_checkoutfires (DevTools → Network →collect?...&en=begin_checkout). - Trigger a test purchase via the Stripe CLI (
stripe listen --forward-to localhost:3000/api/webhooks/stripe) and place an order in test mode. Confirm the webhook handler logs{ ads: { ok: true }, ga4: { ok: true } }(orads: { skipped: 'no_label_configured' }if you haven’t setconversionLabels.purchaseyet). - Wait up to 24 hours, then check the conversion in the Ads UI — it should appear once, deduped on
transactionId.
See also
Section titled “See also”<TrackbridgeProvider>/<TrackbridgePageViews />/useTracker()defineServerTracker()/readEnvelopeFromRequest()- Setting up Google Ads OAuth
- Mapping conversion actions
- Delayed conversions with the envelope — when the conversion fires hours later.