Skip to content

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.

  • 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.
Terminal window
git clone https://github.com/trackbridge/sdk.git
cd sdk
pnpm install
pnpm --filter @trackbridge-examples/nextjs-demo build
cd examples/nextjs-demo
cp .env.local.example .env.local
# Edit .env.local — fill in real values
pnpm dev

Then 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').

After clicking Simulate purchase:

  1. DevTools → Console. With NODE_ENV !== 'production', the SDK logs debug warnings on both sides. The auto-transactionId warning is silent (the demo passes one). Any gtag exception or Ads API failure shows up here.

  2. DevTools → Application → Cookies. Confirm _tb_gclid=demo-click-id is present after landing.

  3. 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 corresponding serverTracker.trackConversion call going to Google.

  4. window.dataLayer in 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.
  5. 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 conversionAction resource name in the server’s createServerTracker config with one from your own (test) account.
  • Use your own gclid by 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 uses Date.now() per click). Each unique transactionId shows up as a separate conversion in the UI.

Dedup verification needs a controlled experiment:

  1. Set the demo to fire only the browser side (comment out the server fetch in app/page.tsx). Click. Note the transactionId.
  2. Re-enable the server side. Reload. Click again with the same transactionId (modify the demo to use a fixed value).
  3. 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.

To prove the server side actually saves the day when the browser side fails:

  1. Install uBlock Origin or enable Brave Shields. The browser-side gtag conversion will be blocked — visible as a missing/failed request to googleadservices.com in DevTools → Network.
  2. Click the button anyway.
  3. The server-side fetch (/api/conversion) still goes through. The server tracker fires.
  4. 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.

  • developer-token: <token> rejected with DEVELOPER_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/conversion succeeds but the conversion never appears in the Ads UI. Most often the conversionAction resource name belongs to a different customer than customerId. Verify in the Ads UI: the conversion action should be in the same customer account as the credentials.