Testing dual-send locally
The SDK’s tests cover the unit-level invariants. To verify your wiring against real Google infrastructure, the SDK ships an end-to-end demo at examples/nextjs-demo/. It’s a one-button Next.js page that fires the same conversion from browser and server, with the same transactionId, against your sandbox Ads account. Within ~24 hours, the conversion shows up in the Ads UI with browser + server attribution merged.
This is the test you run before going to production with a real conversion path.
Prerequisites
Section titled “Prerequisites”- A clone of github.com/trackbridge/sdk.
- A Google Ads test account (created via the manager → Tools & Settings → Sub-account settings → Create test account). Test accounts work with a developer token in test mode, no upgrade required.
- A GA4 property with a Measurement Protocol API secret minted.
- A Google Cloud OAuth client + refresh token, per Setting up Google Ads OAuth. The refresh token can be against the test account; the OAuth flow is the same.
Run it
Section titled “Run it”git clone https://github.com/trackbridge/sdk.gitcd sdkpnpm installpnpm --filter @trackbridge-examples/nextjs-demo build
cd examples/nextjs-democp .env.local.example .env.local# Edit .env.local — fill in real valuespnpm devThen open http://localhost:3000?gclid=demo-click-id.
The ?gclid=demo-click-id is what proves click-identifier capture: with clickIdentifierStorage: 'cookie', the SDK writes _tb_gclid=demo-click-id immediately (the demo defaults to consentMode: 'off').
What to check
Section titled “What to check”After clicking Simulate purchase:
-
DevTools → Console. With
NODE_ENV !== 'production', the SDK logs debug warnings on both sides. The auto-transactionIdwarning is silent (the demo passes one). Any gtag exception or Ads API failure shows up here. -
DevTools → Application → Cookies. Confirm
_tb_gclid=demo-click-idis present after landing. -
DevTools → Network →
/api/conversion. This is the demo’s server-side endpoint. The request body has the order ID; the server log (in your terminal) shows the correspondingserverTracker.trackConversioncall going to Google. -
window.dataLayerin the console. After the conversion, you’ll see two new entries:['set', 'user_data', { ... }]— the hashed identity fields.['event', 'conversion', { send_to, transaction_id, gclid, ... }]— the actual conversion event.
-
Google Ads UI → Conversions → diagnostic table. Within ~24 hours, the conversion appears with both browser and server attribution. The dedup means it counts as one conversion, not two.
Adapting the demo for your own integration
Section titled “Adapting the demo for your own integration”The demo is intentionally minimal — one button, one fixed order shape, one conversion type. To use it as a smoke test for your own setup:
- Replace the
conversionActionresource name in the server’screateServerTrackerconfig with one from your own (test) account. - Use your own
gclidby visiting with?gclid=<your-real-test-gclid>— Ads sandbox accounts give you test gclids you can use that flow through dedup correctly. - Trigger conversions repeatedly by reloading and clicking, varying the
transactionId(the demo usesDate.now()per click). Each uniquetransactionIdshows up as a separate conversion in the UI.
Verifying dedup specifically
Section titled “Verifying dedup specifically”Dedup verification needs a controlled experiment:
- Set the demo to fire only the browser side (comment out the server fetch in
app/page.tsx). Click. Note thetransactionId. - Re-enable the server side. Reload. Click again with the same
transactionId(modify the demo to use a fixed value). - Check the Ads UI within 24 hours. You should see the conversion once, not twice.
If you see two conversions, dedup didn’t fire. The most common cause: the transactionId in the browser call differs from the one in the server call. Verify by logging both before they fire.
Verifying the failure mode
Section titled “Verifying the failure mode”To prove the server side actually saves the day when the browser side fails:
- Install uBlock Origin or enable Brave Shields. The browser-side gtag conversion will be blocked — visible as a missing/failed request to
googleadservices.comin DevTools → Network. - Click the button anyway.
- The server-side fetch (
/api/conversion) still goes through. The server tracker fires. - Within 24 hours, the conversion appears in the Ads UI — credited to the server-side call alone.
That’s the floor: ad blocker on, conversion still counts.
Common stumbling blocks
Section titled “Common stumbling blocks”developer-token: <token>rejected withDEVELOPER_TOKEN_NOT_APPROVED. You’re using a production developer token (test mode) against a real customer account. Either upgrade to basic access (per the OAuth guide), or switch to a test account.- OAuth refresh fails with
invalid_grant. The refresh token has been revoked. Reissue from OAuth Playground. If you used a personal Google account, switch to a Workspace account. /api/conversionsucceeds but the conversion never appears in the Ads UI. Most often theconversionActionresource name belongs to a different customer thancustomerId. Verify in the Ads UI: the conversion action should be in the same customer account as the credentials.
See also
Section titled “See also”- The SDK’s
examples/nextjs-demoREADME — full env-var reference. - Setting up Google Ads OAuth — getting the credentials.
- Mapping conversion actions —
conversionActionresource names.