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.
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|map|json|txt|xml)$/.test(url.pathname)) {
return response;
}
const H = request.headers;
// Only measure top-level page navigations. A non-GET request or a
// sub-resource fetch (image, script, xhr) is not a visit and would
// inflate the numbers. Sec-Fetch-Dest 'document' == a real page load.
const secDest = H.get('sec-fetch-dest');
if (request.method !== 'GET') return response;
if (secDest && secDest !== 'document') return response;
// PREFETCH / PRELOAD DETECTION. The ground-truth signal.
// Facebook preloads your page before anyone taps and stamps
// 'X-Purpose: preview'. Browsers speculative-prefetch with
// Purpose / Sec-Purpose / X-Moz: prefetch|prerender.
// A preload runs NO JavaScript, so it is never a human visit. We tag
// it at the edge so the funnel counts real taps only.
const purposeHdr = (H.get('sec-purpose') || H.get('purpose') || H.get('x-purpose') || H.get('x-moz') || '').toLowerCase();
const isPrefetch = /prefetch|prerender|preview/.test(purposeHdr);
const prefetchSignal = isPrefetch ? purposeHdr.slice(0, 40) : null;
// In-app browser the request came from. Context, and a fallback preload hint.
const uaStr = H.get('user-agent') || '';
const inApp = /FBAN|FBAV|FB_IAB|FBIOS|Instagram/i.test(uaStr) ? 'meta'
: /TikTok|musical_ly|BytedanceWebview/i.test(uaStr) ? 'tiktok'
: null;
// 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'),
};
// First-party durable id for an exact session join.
const cookieHeader = request.headers.get('cookie') || '';
const dcMatch = cookieHeader.match(/(?:^|;\s*)_dc_uid=([^;]+)/);
const dcuid = dcMatch ? decodeURIComponent(dcMatch[1]) : null;
// 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: H.get('cf-connecting-ip'),
ua: uaStr,
data: {
clickIds,
utms,
dcuid, // exact session join key (_dc_uid cookie)
prefetch: isPrefetch, // TRUE = preload, not a human tap
prefetchSignal, // the header + value that proved it
inApp, // 'meta' | 'tiktok' | null
nav: { // navigation context (real load vs sub-resource)
dest: secDest || null,
mode: H.get('sec-fetch-mode') || null,
site: H.get('sec-fetch-site') || null,
},
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.
-
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.
-
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.
-
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 /*.
-
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.