Developer

Server-side capture worker

Deploy the optional Cloudflare Worker that recovers the ad click at the edge, before the browser can throw it away. For what server-side capture is and why, see the product page.

The worker code

This is the whole worker. Copy and paste it, no coding needed. It is written to be safe: it forwards the visitor first, wraps everything in a guard, and only sends when there is a real ad click to recover.

JavaScript
export default {
  async fetch(request, env, ctx) {
    // 1. NEVER BLOCK THE VISITOR. Forward to your site immediately.
    //    Everything below runs in the background; the page never waits.
    const response = fetch(request);

    try {
      const url = new URL(request.url);

      // Skip static assets. Only real page views carry ad attribution.
      if (/\.(js|css|png|jpg|jpeg|svg|ico|woff|woff2|gif|webp)$/.test(url.pathname)) {
        return response;
      }

      // 2. COLLECT click IDs + UTMs from the URL, at the edge, before
      //    iOS / Safari can strip them. This is the data your pixel loses.
      //    Every major ad platform's click ID, so nothing is missed no
      //    matter where the traffic comes from.
      const clickIds = {
        fbclid:    url.searchParams.get('fbclid'),    // Meta (Facebook/Instagram)
        gclid:     url.searchParams.get('gclid'),     // Google Ads
        wbraid:    url.searchParams.get('wbraid'),    // Google Ads (web-to-app)
        gbraid:    url.searchParams.get('gbraid'),    // Google Ads (app-to-web)
        dclid:     url.searchParams.get('dclid'),     // Google Display / Campaign Manager
        msclkid:   url.searchParams.get('msclkid'),   // Microsoft/Bing Ads
        ttclid:    url.searchParams.get('ttclid'),    // TikTok Ads
        li_fat_id: url.searchParams.get('li_fat_id'), // LinkedIn Ads
        twclid:    url.searchParams.get('twclid'),    // Twitter/X Ads
        epik:      url.searchParams.get('epik'),      // Pinterest Ads
        rdt_cid:   url.searchParams.get('rdt_cid'),   // Reddit Ads
        sccid:     url.searchParams.get('sccid'),     // Snapchat Ads
        ScCid:     url.searchParams.get('ScCid'),     // Snapchat Ads (alt casing)
        obclid:    url.searchParams.get('obclid'),    // Outbrain
        ttp:       url.searchParams.get('ttp'),       // TikTok pixel cookie param
        irclickid: url.searchParams.get('irclickid'), // Impact / affiliate
      };
      const utms = {
        source:   url.searchParams.get('utm_source'),
        medium:   url.searchParams.get('utm_medium'),
        campaign: url.searchParams.get('utm_campaign'),
      };

      // Only send when there is real attribution to recover. A plain
      // visit with no click ID / UTM is left alone. No data collected.
      const hasAttribution = Object.values(clickIds).some(Boolean)
                          || Object.values(utms).some(Boolean);
      if (!hasAttribution) return response;

      // 3. SEND to DataCops, after the response, never blocking the page.
      //    Authenticated with your account key. The isEU flag lets
      //    DataCops apply the consent gate instantly, with no IP lookup:
      //    EU visitors' marketing events are held until consent is given.
      ctx.waitUntil(fetch(env.DATACOPS_COLLECT_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-cop-key': env.DATACOPS_COP_KEY,
        },
        body: JSON.stringify({
          eventType: 'network_capture',
          source: 'cloudflare_worker',
          ip: request.headers.get('cf-connecting-ip'),
          ua: request.headers.get('user-agent'),
          data: {
            clickIds,
            utms,
            cf: {
              country:     request.cf?.country ?? null,
              asOrg:       request.cf?.asOrganization ?? null,
              isEU:        request.cf?.isEUCountry ?? false, // consent signal
              botScore:    request.cf?.botManagement?.score ?? null,
              verifiedBot: request.cf?.botManagement?.verifiedBot ?? false,
            },
            referrer: request.headers.get('referer'),
            pageUrl:  url.href,
            ts: Date.now(),
          },
        }),
      }).catch(() => {}));
    } catch (_) {}

    return response;
  },
};

Set it up

Your domain needs to be on Cloudflare DNS. It is free and takes about five minutes if you are not on Cloudflare yet.

  1. Create a Cloudflare Worker

    In Cloudflare, go to Workers and Pages, Create, pick the Hello World starter, Deploy. Then Edit code, delete the sample, paste the code above, and Deploy again.

  2. Set two variables

    In your Worker, go to Settings, Variables and Secrets, Add. Add DATACOPS_COP_KEY and DATACOPS_COLLECT_URL, using the values from your DataCops dashboard. They stay private and are never visible to visitors.

  3. Add a Worker route

    In your Worker, go to the Domains tab, Routes, Add a route. Pick your domain and use a pattern that matches all pages, like yourdomain.com followed by /*.

  4. Verify

    Visit your own site once with an ad click in the link, then click Verify in DataCops. It confirms in under a minute.

Optional extras

  • Bot Fight Mode. If your Cloudflare plan has it, turn it on under Security, Bots. The worker already reads its signal, no code change needed.
  • Push flagged addresses back to Cloudflare. Connect a scoped token and DataCops can send the addresses it catches back to Cloudflare to block at the edge, from the second flagged visit on.
Was this page helpful?