1import{k as O,r as T,s as A,j as e,L as j,t as E}from"./index-Bu8Ndfyn.js";import{T as P,a as N,b as m,c as _}from"./tabs-DkNiEBri.js";import{C as n,a as c,b as d,c as l}from"./card-bnER_ro1.js";import{B as v}from"./button-B_3xGooe.js";import{B as S}from"./badge-D00WBcmh.js";import{I as L}from"./input-C9bdYoLJ.js";import"./index-USkQNPkL.js";import"./index-C-sK8-3O.js";import"./index-C047FIor.js";import"./index-B3uQs-9P.js";function w(t){return`<!-- Discovery Conversion Tracker (JS-only fallback) --> 2<script 3 src="${t.collectorOrigin}/api/public/tracker.js" 4 data-site-id="${t.siteId}" 5 data-mode="js" 6 data-collector="${t.collectorOrigin}" 7 data-debug="false" 8 async><\/script>`}function I(t){const s=JSON.stringify([t.primaryDomain,...t.allowedDomains]);return`// Discovery Conversion Tracker - Cloudflare Worker 9// Recommended deployment. Inspects each request server-side, classifies the 10// source, sets attribution cookies before the page loads, and forwards to origin. 11// JS tracker is still required on the page for CTA click capture. 12 13const SITE_ID = "${t.siteId}"; 14const COLLECTOR_URL = "${t.collectorOrigin}"; 15// Accepts bare hostnames or full origins; normalised to hostnames either way. 16const ALLOWED_HOSTS = ${s} 17 .map(v => v.replace(/^https?:\\/\\//, "").replace(/[/:].*$/, "").toLowerCase()) 18 .filter(Boolean); 19const ATTR_DAYS = ${t.attributionWindowDays}; 20const OWNER_TAG_PARAM = ${JSON.stringify(t.ownerTaggedParam||"dct_llm")}; 21const OWNER_TAG_ENABLED = ${t.ownerTaggedEnabled?"true":"false"}; 22const DEBUG = false; 23 24const PAID_PARAMS = ["gclid","gbraid","wbraid","msclkid","dclid","fbclid","ttclid","li_fat_id"]; 25const BLOCKED_UTM = ["cpc","ppc","paid","paid_search","paid_social","display","social","influencer","affiliate","email","newsletter","referral","sponsored"]; 26const LLM_HOSTS = ["chatgpt.com","chat.openai.com","perplexity.ai","claude.ai","gemini.google.com","copilot.microsoft.com","you.com","phind.com","poe.com","mistral.ai"]; 27const SEARCH_HOSTS = ["bing.com","duckduckgo.com","yahoo.com","search.yahoo.com"]; 28const BLOCKED_HOSTS = ["facebook.com","instagram.com","linkedin.com","t.co","twitter.com","x.com","youtube.com","tiktok.com","pinterest.com","reddit.com","threads.net","medium.com","substack.com","quora.com","producthunt.com","linktr.ee","beacons.ai","bit.ly","l.instagram.com","lm.facebook.com"]; 29const BOT_RE = /(GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|anthropic-ai|PerplexityBot|Perplexity-User|Google-Extended|Applebot-Extended|Meta-ExternalAgent|Meta-ExternalFetcher|Bytespider|cohere-ai|CCBot|AI2Bot|Omgilibot|Diffbot|Googlebot|AdsBot-Google|Bingbot|Slurp|DuckDuckBot|YandexBot|Baiduspider|facebookexternalhit|MJ12bot|SemrushBot|AhrefsBot|DotBot|Petalbot|bingpreview|HeadlessChrome|PhantomJS|Puppeteer|Playwright|python-requests|Go-http-client|okhttp|axios\\/|curl\\/|Wget\\/|crawler|spider|bot\\b)/i; 30function searchName(h){ if (/(^|\\.)google\\./.test(h)) return "google"; if (h.indexOf("bing")>=0) return "bing"; if (h.indexOf("duckduckgo")>=0) return "duckduckgo"; if (h.indexOf("yahoo")>=0) return "yahoo"; return h; } 31 32function host(u) { try { return new URL(u).hostname.toLowerCase(); } catch { return ""; } } 33function matchHost(h, list) { return list.some(d => h === d || h.endsWith("." + d)); } 34function isGoogle(h) { return /(^|\\.)google\\.[a-z.]{2,}$/.test(h); } 35function uuid() { return crypto.randomUUID(); } 36 37function classify(req) { 38 const url = new URL(req.url); 39 const params = url.searchParams; 40 const ref = req.headers.get("referer") || ""; 41 const ua = req.headers.get("user-agent") || ""; 42 const cfBot = req.cf && req.cf.botManagement && req.cf.botManagement.score < 30; 43 if (cfBot || BOT_RE.test(ua)) return { ok:false, c:"blocked", reason:"bot" }; 44 45 for (const p of PAID_PARAMS) if (params.has(p)) return { ok:false, c:"blocked", reason:"paid_ad_param" }; 46 const um = (params.get("utm_medium") || "").toLowerCase(); 47 if (um && BLOCKED_UTM.includes(um)) return { ok:false, c:"blocked", reason:"blocked_utm:"+um }; 48 49 const rh = host(ref); 50 if (rh && matchHost(rh, BLOCKED_HOSTS)) return { ok:false, c:"blocked", reason:"blocked_source:"+rh }; 51 52 if (OWNER_TAG_ENABLED && params.has(OWNER_TAG_PARAM)) { 53 const v = (params.get(OWNER_TAG_PARAM) || "").toLowerCase(); 54 return { ok:true, c:"confirmed_owner_tagged_llm", source: v, type:"owner_tagged_llm" }; 55 } 56 if (rh && matchHost(rh, LLM_HOSTS)) return { ok:true, c:"confirmed_llm_referral", source: rh, type:"llm" }; 57 if (rh && isGoogle(rh)) return { ok:true, c:"confirmed_google_organic", source:"google", type:"search" }; 58 if (rh && matchHost(rh, SEARCH_HOSTS)) return { ok:true, c:"confirmed_search_organic", source: searchName(rh), type:"search" }; 59 if (!rh) return { ok:false, c:"unknown", reason:"empty_referrer" }; 60 return { ok:false, c:"unknown", reason:"unknown_referrer" }; 61} 62 63function rootDomain(host) { 64 const parts = host.split("."); if (parts.length < 2) return host; 65 return parts.slice(-2).join("."); 66} 67 68async function postJson(url, body) { 69 try { 70 return await fetch(url, { method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify(body) }); 71 } catch(e) { if (DEBUG) console.log("post err", e); } 72} 73 74export default { 75 async fetch(request) { 76 const url = new URL(request.url); 77 if (!ALLOWED_HOSTS.some(h => url.hostname === h || url.hostname.endsWith("." + h))) { 78 return fetch(request); 79 } 80 // Skip non-document requests 81 const accept = request.headers.get("accept") || ""; 82 if (!accept.includes("text/html") && request.method !== "GET") return fetch(request); 83 84 const cookies = Object.fromEntries((request.headers.get("cookie") || "") 85 .split(";").map(s =>
85 s.trim()).filter(Boolean).map(s => { 86 const i = s.indexOf("="); return [s.slice(0,i), decodeURIComponent(s.slice(i+1))]; 87 })); 88 let vid = cookies["dct_vid"] || uuid(); 89 let sid = cookies["dct_sid_"+SITE_ID] || null; 90 91 const cls = classify(request); 92 let setSidCookie = null; 93 let setSiteCookie = null; 94 let setVidCookie = !cookies["dct_vid"]; 95 96 // Only create or refresh session for confirmed sources, never extend on every request 97 if (cls.ok && !sid) { 98 const r = await postJson(COLLECTOR_URL + "/api/public/session/start", { 99 site_id: SITE_ID, 100 url: request.url, 101 referrer: request.headers.get("referer") || null, 102 visitor_id: vid, 103 user_agent: request.headers.get("user-agent") || "", 104 tracker_mode: "cloudflare_worker" 105 }); 106 try { 107 const j = r ? await r.json() : null; 108 if (j && j.ok && j.session_id) { sid = j.session_id; setSidCookie = sid; setSiteCookie = SITE_ID; } 109 } catch(_) {} 110 } else if (!cls.ok && cls.c === "blocked") { 111 // log blocked diagnostic out-of-band 112 postJson(COLLECTOR_URL + "/api/public/diagnostic", { 113 site_id: SITE_ID, classification: "blocked", reason: cls.reason, 114 event_type: "request", tracker_mode: "cloudflare_worker", 115 url: request.url, referrer: request.headers.get("referer") || "" 116 }); 117 } 118 119 const resp = await fetch(request); 120 const out = new Response(resp.body, resp); 121 const root = "." + rootDomain(url.hostname); 122 const exp = ATTR_DAYS * 86400; 123 const sec = url.protocol === "https:" ? "; Secure" : ""; 124 if (setVidCookie) out.headers.append("set-cookie", 125 \`dct_vid=\${encodeURIComponent(vid)}; Max-Age=31536000; Path=/; Domain=\${root}; SameSite=Lax\${sec}\`); 126 if (setSidCookie) out.headers.append("set-cookie", 127 \`dct_sid_\${SITE_ID}=\${encodeURIComponent(setSidCookie)}; Max-Age=\${exp}; Path=/; Domain=\${root}; SameSite=Lax\${sec}\`); 128 if (setSiteCookie) out.headers.append("set-cookie", 129 \`dct_site=\${encodeURIComponent(setSiteCookie)}; Max-Age=\${exp}; Path=/; Domain=\${root}; SameSite=Lax\${sec}\`); 130 return out; 131 } 132}`}function B(t){const s=JSON.stringify([t.primaryDomain,...t.allowedDomains]);return`<?php 133/** 134 * Discovery Conversion Tracker â Raw PHP (no WordPress required) 135 * MUST be included at the VERY TOP of every page, BEFORE any HTML output 136 * (otherwise setcookie() will fail with "headers already sent"). 137 * 138 * Easiest install: 139 * 1. Save this file as: /path/to/your/site/dct-tracker.php 140 * 2. Add to .htaccess (Apache): php_value auto_prepend_file "/full/path/to/dct-tracker.php" 141 * OR add to php.ini: auto_prepend_file = /full/path/to/dct-tracker.php 142 * OR put require_once __DIR__ . '/dct-tracker.php'; as the FIRST line 143 * of every entry file (index.php, header.php BEFORE <!DOCTYPE>, etc.) 144 */ 145if (defined('DCT_LOADED')) return; define('DCT_LOADED', true); 146 147define('DCT_SITE_ID', '${t.siteId}'); 148define('DCT_COLLECTOR', '${t.collectorOrigin}'); 149define('DCT_ATTR_DAYS', ${t.attributionWindowDays}); 150define('DCT_OWNER_PARAM', '${t.ownerTaggedParam||"dct_llm"}'); 151define('DCT_OWNER_ENABLED', ${t.ownerTaggedEnabled?"true":"false"}); 152 153$DCT_ALLOWED_HOSTS = ${s}; 154$DCT_PAID_PARAMS = array('gclid','gbraid','wbraid','msclkid','dclid','fbclid','ttclid','li_fat_id'); 155$DCT_BLOCKED_UTM = array('cpc','ppc','paid','paid_search','paid_social','display','social','influencer','affiliate','email','newsletter','referral','sponsored'); 156$DCT_LLM_HOSTS = array('chatgpt.com','chat.openai.com','perplexity.ai','claude.ai','gemini.google.com','copilot.microsoft.com','you.com','phind.com','poe.com','mistral.ai'); 157$DCT_SEARCH_HOSTS = array('bing.com','duckduckgo.com','yahoo.com','search.yahoo.com'); 158$DCT_BLOCKED_HOSTS = array('facebook.com','instagram.com','linkedin.com','t.co','twitter.com','x.com','youtube.com','tiktok.com','pinterest.com','reddit.com','threads.net','medium.com','substack.com','quora.com','producthunt.com','linktr.ee','beacons.ai','bit.ly','l.instagram.com','lm.facebook.com'); 159$DCT_BOT_RE = '/(GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|anthropic-ai|PerplexityBot|Perplexity-User|Google-Extended|Applebot-Extended|Meta-ExternalAgent|Meta-ExternalFetcher|Bytespider|cohere-ai|CCBot|AI2Bot|Omgilibot|Diffbot|Googlebot|AdsBot-Google|Bingbot|Slurp|DuckDuckBot|YandexBot|Baiduspider|facebookexternalhit|MJ12bot|SemrushBot|AhrefsBot|DotBot|Petalbot|bingpreview|HeadlessChrome|PhantomJS|Puppeteer|Playwright|python-requests|Go-http-client|okhttp|axios\\\\/|curl\\\\/|Wget\\\\/|crawler|spider|bot\\\\b)/i'; 160 161if (!function_exists('dct_uuid4')) { 162 function dct_uuid4(){ $d = random_bytes(16); $d[6] = chr((ord($d[6]) & 0x0f) | 0x40); $d[8] = chr((ord($d[8]) & 0x3f) | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($d), 4)); } 163} 164function dct_host($u){ $h = parse_url($u, PHP_URL_HOST); return $h ? strtolower($h) : ''; } 165function dct_match_host($h,$list){ foreach($list as $d){ if($h===$d || (strlen($h) > strlen($d) && substr($h, -strlen('.'.$d)) === '.'.$d)) return true; } return false; } 166function dct_is_google($h){ return (bool) preg_match('/(^|\\.)google\\.[a-z.]{2,}$/i', $h); } 167function dct_root_domain($h){ $parts = explode('.', $h); return count($parts) < 2 ? $h : implode('.', array_slice($parts, -2)); } 168 169function dct_classify(){ 170 global $DCT_PAID_PARAMS,$DCT_BLOCKED_UTM,$DCT_LLM_HOSTS,$DCT_SEARCH_HOSTS,$DCT_BLOCKED_HOSTS,$DCT_BOT_RE; 171 $ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; 172 if (preg_match($DCT_BOT_RE, $ua)) return array('ok'=>false,'c'=>'blocked','reason'=>'bot'); 173 foreach($DCT_PAID_PARAMS as $p){ if(isset($_GET[$p])) return array('ok'=>false,'c'=>'blocked','reason'=>'paid_ad_param'); } 174 $um = isset($_GET['utm_medium']) ? strtolower($_GET['utm_medium']) : ''; 175 if ($um && in_array($um, $DCT_BLOCKED_UTM, true)) return array('ok'=>false,'c'=>'blocked','reason'=>'blocked_utm:'.$um); 176 $ref = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; 177 $rh = dct_host($ref); 178 if ($rh && dct_match_host($rh, $DCT_BLOCKED_HOSTS)) return array('ok'=>false,'c'=>'blocked','reason'=>'blocked_source:'.$rh); 179 if (DCT_OWNER_ENABLED && isset($_GET[DCT_OWNER_PARAM])) return array('ok'=>true,'c'=>'confirmed_owner_tagged_llm','source'=>strtolower($_GET[DCT_OWNER_PARAM])); 180 if ($rh && dct_match_host($rh, $DCT_LLM_HOSTS)) return array('ok'=>true,'c'=>'confirmed_llm_referral','source'=>$rh); 181 if ($rh && dct_is_google($rh)) return array('ok'=>true,'c'=>'confirmed_google_organic','source'=>'google'); 182 if ($rh && dct_match_host($rh, $DCT_SEARCH_HOSTS)) {
183 $sn = (strpos($rh,'bing')!==false)?'bing':((strpos($rh,'duckduckgo')!==false)?'duckduckgo':((strpos($rh,'yahoo')!==false)?'yahoo':$rh)); 184 return array('ok'=>true,'c'=>'confirmed_search_organic','source'=>$sn); 185 } 186 return array('ok'=>false,'c'=>'unknown','reason'=>$rh ? 'unknown_referrer' : 'empty_referrer'); 187} 188 189function dct_post_json($url, $body){ 190 $payload = json_encode($body); 191 if (function_exists('curl_init')) { 192 $ch = curl_init($url); 193 curl_setopt_array($ch, array( 194 CURLOPT_POST=>true, CURLOPT_POSTFIELDS=>$payload, 195 CURLOPT_HTTPHEADER=>array('Content-Type: application/json'), 196 CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>4, CURLOPT_CONNECTTIMEOUT=>3, 197 )); 198 $resp = curl_exec($ch); $err = curl_error($ch); curl_close($ch); 199 return $err ? array('error'=>$err) : array('body'=>$resp); 200 } 201 $ctx = stream_context_create(array('http'=>array( 202 'method'=>'POST','header'=>"Content-Type: application/json\\r\\n", 203 'content'=>$payload,'timeout'=>4,'ignore_errors'=>true, 204 ))); 205 $resp = @file_get_contents($url, false, $ctx); 206 return $resp === false ? array('error'=>'http_failed') : array('body'=>$resp); 207} 208 209function dct_set_cookie($name, $val, $days){ 210 $h = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; 211 if (headers_sent()) return; 212 setcookie($name, $val, array( 213 'expires' => time() + ($days * 86400), 214 'path' => '/', 215 'domain' => '.' . dct_root_domain($h), 216 'samesite' => 'Lax', 217 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', 218 )); 219} 220 221(function(){ 222 $dct_debug = isset($_GET['dct_debug']); 223 $vid = isset($_COOKIE['dct_vid']) ? $_COOKIE['dct_vid'] : ''; 224 if (!$vid) { $vid = dct_uuid4(); dct_set_cookie('dct_vid', $vid, 365); $_COOKIE['dct_vid'] = $vid; } 225 226 $sid_cookie = 'dct_sid_' . DCT_SITE_ID; 227 $sid = isset($_COOKIE[$sid_cookie]) ? $_COOKIE[$sid_cookie] : ''; 228 229 $cls = dct_classify(); 230 if (($cls['ok'] || $dct_debug) && !$sid) { 231 $url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']==='on' ? 'https' : 'http') 232 . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; 233 $resp = dct_post_json(DCT_COLLECTOR . '/api/public/session/start', array( 234 'site_id'=>DCT_SITE_ID, 'url'=>$url, 235 'referrer'=>isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null, 236 'visitor_id'=>$vid, 'user_agent'=>isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 237 'tracker_mode'=>'raw_php', 238 )); 239 if (empty($resp['error'])) { 240 $j = json_decode($resp['body'], true); 241 if (!empty($j['ok']) && !empty($j['session_id'])) { 242 dct_set_cookie($sid_cookie, $j['session_id'], DCT_ATTR_DAYS); 243 dct_set_cookie('dct_site', DCT_SITE_ID, DCT_ATTR_DAYS); 244 $_COOKIE[$sid_cookie] = $j['session_id']; 245 $_COOKIE['dct_site'] = DCT_SITE_ID; 246 } 247 if ($dct_debug && !headers_sent()) { 248 header('X-DCT-Debug-Classification: ' . $cls['c']); 249 header('X-DCT-Debug-Reason: ' . (isset($cls['reason']) ? $cls['reason'] : '')); 250 header('X-DCT-Debug-Backend: ' . substr($resp['body'], 0, 200)); 251 } 252 } elseif ($dct_debug && !headers_sent()) { 253 header('X-DCT-Debug-Error: ' . $resp['error']); 254 } 255 } elseif ($dct_debug && !headers_sent()) { 256 header('X-DCT-Debug-Classification: ' . $cls['c'] . ' (no_post:' . ($sid ? 'has_session' : 'not_confirmed') . ')'); 257 header('X-DCT-Debug-Reason: ' . (isset($cls['reason']) ? $cls['reason'] : '')); 258 } 259 260 // Auto-inject the JS tracker just before </body> (or appended to output if no </body>). 261 if (!headers_sent()) { ob_start(function($html){ 262 $sid_attr = isset($_COOKIE['dct_sid_' . DCT_SITE_ID]) ? ' data-session-id="' . htmlspecialchars($_COOKIE['dct_sid_' . DCT_SITE_ID], ENT_QUOTES, 'UTF-8') . '"' : ''; 263 $tag = '<script src="' . DCT_COLLECTOR . '/api/public/tracker.js" data-site-id="' . DCT_SITE_ID . '" data-mode="raw_php" data-collector="' . DCT_COLLECTOR . '"' . $sid_attr . ' async><\/script>'; 264 if (stripos($html, '</body>') !== false) return preg_replace('/<\\/body>/i', $tag . '</body>', $html, 1); 265 return $html . $tag; 266 }); } 267})(); 268`}function H(t){const s=JSON.stringify([t.primaryDomain,...t.allowedDomains]);return`<?php 269/** 270 * Plugin Name: Discovery Conversion Tracker 271 * Description: Server-side discovery source classification + JS for CTA tracking. 272 * Version: 1.0.0 273 */ 274if (!defined('ABSPATH')) exit; 275 276define('DCT_SITE_ID', '${t.siteId}'); 277define('DCT_COLLECTOR', '${t.collectorOrigin}'); 278define('DCT_ATTR_DAYS', ${t.attributionWindowDays}); 279define('DCT_OWNER_PARAM', '${t.ownerTaggedParam||"dct_llm"}'); 280define('DCT_OWNER_ENABLED', ${t.ownerTaggedEnabled?"true":"false"}); 281 282$DCT_ALLOWED_HOSTS = ${s}; 283$DCT_PAID_PARAMS = array('gclid','gbraid','wbraid','msclkid','dclid','fbclid','ttclid','li_fat_id'); 284$DCT_BLOCKED_UTM = array('cpc','ppc','paid','paid_search','paid_social','display','social','influencer','affiliate','email','newsletter','referral','sponsored'); 285$DCT_LLM_HOSTS = array('chatgpt.com','chat.openai.com','perplexity.ai','claude.ai','gemini.google.com','copilot.microsoft.com','you.com','phind.com','poe.com','mistral.ai'); 286$DCT_SEARCH_HOSTS = array('bing.com','duckduckgo.com','yahoo.com','search.yahoo.com'); 287$DCT_BLOCKED_HOSTS = array('facebook.com','instagram.com','linkedin.com','t.co','twitter.com','x.
287com','youtube.com','tiktok.com','pinterest.com','reddit.com','threads.net','medium.com','substack.com','quora.com','producthunt.com','linktr.ee','beacons.ai','bit.ly','l.instagram.com','lm.facebook.com'); 288$DCT_BOT_RE = '/(GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|anthropic-ai|PerplexityBot|Perplexity-User|Google-Extended|Applebot-Extended|Meta-ExternalAgent|Meta-ExternalFetcher|Bytespider|cohere-ai|CCBot|AI2Bot|Omgilibot|Diffbot|Googlebot|AdsBot-Google|Bingbot|Slurp|DuckDuckBot|YandexBot|Baiduspider|facebookexternalhit|MJ12bot|SemrushBot|AhrefsBot|DotBot|Petalbot|bingpreview|HeadlessChrome|PhantomJS|Puppeteer|Playwright|python-requests|Go-http-client|okhttp|axios\\\\/|curl\\\\/|Wget\\\\/|crawler|spider|bot\\\\b)/i'; 289 290function dct_host($u){ $h = parse_url($u, PHP_URL_HOST); return $h ? strtolower($h) : ''; } 291function dct_match_host($h,$list){ foreach($list as $d){ if($h===$d || (strlen($h) > strlen($d) && substr($h, -strlen('.'.$d)) === '.'.$d)) return true; } return false; } 292function dct_is_google($h){ return (bool) preg_match('/(^|\\.)google\\.[a-z.]{2,}$/i', $h); } 293 294function dct_classify(){ 295 global $DCT_PAID_PARAMS,$DCT_BLOCKED_UTM,$DCT_LLM_HOSTS,$DCT_SEARCH_HOSTS,$DCT_BLOCKED_HOSTS,$DCT_BOT_RE; 296 $ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; 297 if (preg_match($DCT_BOT_RE, $ua)) return array('ok'=>false,'c'=>'blocked','reason'=>'bot'); 298 foreach($DCT_PAID_PARAMS as $p){ if(isset($_GET[$p])) return array('ok'=>false,'c'=>'blocked','reason'=>'paid_ad_param'); } 299 $um = isset($_GET['utm_medium']) ? strtolower($_GET['utm_medium']) : ''; 300 if ($um && in_array($um, $DCT_BLOCKED_UTM, true)) return array('ok'=>false,'c'=>'blocked','reason'=>'blocked_utm:'.$um); 301 $ref = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; 302 $rh = dct_host($ref); 303 if ($rh && dct_match_host($rh, $DCT_BLOCKED_HOSTS)) return array('ok'=>false,'c'=>'blocked','reason'=>'blocked_source:'.$rh); 304 if (DCT_OWNER_ENABLED && isset($_GET[DCT_OWNER_PARAM])) return array('ok'=>true,'c'=>'confirmed_owner_tagged_llm','source'=>strtolower($_GET[DCT_OWNER_PARAM])); 305 if ($rh && dct_match_host($rh, $DCT_LLM_HOSTS)) return array('ok'=>true,'c'=>'confirmed_llm_referral','source'=>$rh); 306 if ($rh && dct_is_google($rh)) return array('ok'=>true,'c'=>'confirmed_google_organic','source'=>'google'); 307 if ($rh && dct_match_host($rh, $DCT_SEARCH_HOSTS)) { 308 $sn = (strpos($rh,'bing')!==false)?'bing':((strpos($rh,'duckduckgo')!==false)?'duckduckgo':((strpos($rh,'yahoo')!==false)?'yahoo':$rh)); 309 return array('ok'=>true,'c'=>'confirmed_search_organic','source'=>$sn); 310 } 311 return array('ok'=>false,'c'=>'unknown','reason'=>$rh ? 'unknown_referrer' : 'empty_referrer'); 312} 313 314function dct_post_json($url, $body){ 315 $args = array( 316 'body' => wp_json_encode($body), 317 'headers' => array('Content-Type'=>'application/json'), 318 'timeout' => 4, 'blocking' => true, 319 ); 320 return wp_remote_post($url, $args); 321} 322 323function dct_root_domain($h){ $parts = explode('.', $h); return count($parts) < 2 ? $h : implode('.', array_slice($parts, -2)); } 324 325function dct_set_cookie($name, $val, $days){ 326 $h = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; 327 setcookie($name, $val, array( 328 'expires' => time() + ($days * 86400), 329 'path' => '/', 330 'domain' => '.' . dct_root_domain($h), 331 'samesite' => 'Lax', 332 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', 333 )); 334} 335 336add_action('init', function(){ 337 if (is_admin() || (defined('DOING_CRON') && DOING_CRON) || (defined('REST_REQUEST') && REST_REQUEST)) return; 338 // Admin bypass â but allow ?dct_debug=1 to override for testing 339 $dct_debug = isset($_GET['dct_debug']); 340 if (!$dct_debug && is_user_logged_in() && current_user_can('manage_options')) return; 341 342 $vid = isset($_COOKIE['dct_vid']) ? $_COOKIE['dct_vid'] : ''; 343 if (!$vid) { $vid = wp_generate_uuid4(); dct_set_cookie('dct_vid', $vid, 365); } 344 345 $sid_cookie = 'dct_sid_' . DCT_SITE_ID; 346 $sid = isset($_COOKIE[$sid_cookie]) ? $_COOKIE[$sid_cookie] : ''; 347 348 $cls = dct_classify(); 349 // In debug mode, force a session_start call so you can see backend logs even 350 // when the visit would not normally be confirmed. 351 if (($cls['ok'] || $dct_debug) && !$sid) { 352 $url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']==='on' ? 'https' : 'http') 353 . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; 354 $resp = dct_post_json(DCT_COLLECTOR . '/api/public/session/start', array( 355 'site_id'=>DCT_SITE_ID, 'url'=>$url, 356 'referrer'=>isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null, 357 'visitor_id'=>$vid, 'user_agent'=>isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 358 'tracker_mode'=>'wordpress_php', 359 )); 360 if (!is_wp_error($resp)) { 361 $j = json_decode(wp_remote_retrieve_body($resp), true); 362 if (!empty($j['ok']) && !empty($j['session_id'])) { 363 dct_set_cookie($sid_cookie, $j['session_id'], DCT_ATTR_DAYS); 364 dct_set_cookie('dct_site', DCT_SITE_ID, DCT_ATTR_DAYS); 365 $_COOKIE[$sid_cookie] = $j['session_id']; 366 $_COOKIE['dct_site'] = DCT_SITE_ID; 367 } 368 if ($dct_debug) { 369 header('X-DCT-Debug-Classification: ' . $cls['c']); 370 header('X-DCT-Debug-Reason: ' . (isset($cls['reason']) ? $cls['reason'] : '')); 371 header('X-DCT-Debug-Backend: ' . substr(wp_remote_retrieve_body($resp), 0, 200)); 372 } 373 } elseif ($dct_debug) { 374 header('X-DCT-Debug-Error: ' . $resp->get_error_message()); 375 } 376 } elseif ($dct_debug) { 377 header('X-DCT-Debug-Classification: ' . $cls['c'] . ' (no_post:' . ($sid ? 'has_session' : 'not_confirmed') . ')'); 378 header('X-DCT-Debug-Reason: ' . (isset($cls['reason']) ? $cls['reason'] : '')); 379 } 380}); 381 382// Inject JS tracker for CTA clicks + success page detection 383add_action('wp_footer', function(){ 384 if (is_user_logged_in() && current_user_can('manage_options')) return; 385 $sid_cookie = 'dct_sid_' . DCT_SITE_ID;
386 $sid_attr = isset($_COOKIE[$sid_cookie]) ? ' data-session-id="' . esc_attr($_COOKIE[$sid_cookie]) . '"' : ''; 387 echo '<script src="' . esc_url(DCT_COLLECTOR . '/api/public/tracker.js') 388 . '" data-site-id="' . esc_attr(DCT_SITE_ID) 389 . '" data-mode="wordpress_php"' 390 . ' data-collector="' . esc_url(DCT_COLLECTOR) . '"' 391 . $sid_attr 392 . ' async><\/script>'; 393}, 99); 394`}function U(t){const s=JSON.stringify([t.primaryDomain,...t.allowedDomains]);return`// Discovery Conversion Tracker â Next.js Middleware 395// Save as: middleware.ts at the project root (next to package.json / app/ / pages/). 396// Runs at the edge BEFORE the page renders. Classifies the request server-side, 397// sets attribution cookies, then forwards. The browser tracker.js is still 398// required on the page for CTA-click and success-page tracking. 399 400import { NextRequest, NextResponse } from "next/server"; 401 402const SITE_ID = ${JSON.stringify(t.siteId)}; 403const COLLECTOR_URL = ${JSON.stringify(t.collectorOrigin)}; 404// Accepts bare hostnames or full origins; normalised to hostnames either way. 405const ALLOWED_HOSTS: string[] = (${s} as string[]) 406 .map(v => v.replace(/^https?:\\/\\//, "").replace(/[/:].*$/, "").toLowerCase()) 407 .filter(Boolean); 408const ATTR_DAYS = ${t.attributionWindowDays}; 409const OWNER_TAG_PARAM = ${JSON.stringify(t.ownerTaggedParam||"dct_llm")}; 410const OWNER_TAG_ENABLED = ${t.ownerTaggedEnabled?"true":"false"}; 411const DEBUG = false; 412 413const PAID_PARAMS = ["gclid","gbraid","wbraid","msclkid","dclid","fbclid","ttclid","li_fat_id"]; 414const BLOCKED_UTM = ["cpc","ppc","paid","paid_search","paid_social","display","social","influencer","affiliate","email","newsletter","referral","sponsored"]; 415const LLM_HOSTS = ["chatgpt.com","chat.openai.com","perplexity.ai","claude.ai","gemini.google.com","copilot.microsoft.com","you.com","phind.com","poe.com","mistral.ai"]; 416const SEARCH_HOSTS = ["bing.com","duckduckgo.com","yahoo.com","search.yahoo.com"]; 417const BLOCKED_HOSTS = ["facebook.com","instagram.com","linkedin.com","t.co","twitter.com","x.com","youtube.com","tiktok.com","pinterest.com","reddit.com","threads.net","medium.com","substack.com","quora.com","producthunt.com","linktr.ee","beacons.ai","bit.ly","l.instagram.com","lm.facebook.com"]; 418const BOT_RE = /(GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|anthropic-ai|PerplexityBot|Perplexity-User|Google-Extended|Applebot-Extended|Meta-ExternalAgent|Meta-ExternalFetcher|Bytespider|cohere-ai|CCBot|AI2Bot|Omgilibot|Diffbot|Googlebot|AdsBot-Google|Bingbot|Slurp|DuckDuckBot|YandexBot|Baiduspider|facebookexternalhit|MJ12bot|SemrushBot|AhrefsBot|DotBot|Petalbot|bingpreview|HeadlessChrome|PhantomJS|Puppeteer|Playwright|python-requests|Go-http-client|okhttp|axios\\/|curl\\/|Wget\\/|crawler|spider|bot\\b)/i; 419const STATIC_EXT_RE = /\\.(?:js|mjs|css|map|png|jpe?g|gif|svg|ico|webp|avif|woff2?|ttf|otf|eot|json|xml|txt|pdf|mp4|webm|mp3|wasm)$/i; 420 421function host(u: string){ try { return new URL(u).hostname.toLowerCase(); } catch { return ""; } } 422function matchHost(h: string, list: string[]){ return list.some(d => h === d || h.endsWith("." + d)); } 423function isGoogle(h: string){ return /(^|\\.)google\\.[a-z.]{2,}$/.test(h); } 424function searchName(h: string){ if (/(^|\\.)google\\./.test(h)) return "google"; if (h.indexOf("bing")>=0) return "bing"; if (h.indexOf("duckduckgo")>=0) return "duckduckgo"; if (h.indexOf("yahoo")>=0) return "yahoo"; return h; } 425function rootDomain(h: string){ const p = h.split("."); return p.length < 2 ? h : p.slice(-2).join("."); } 426function uuid(){ return (globalThis.crypto && "randomUUID" in globalThis.crypto) ? globalThis.crypto.randomUUID() : "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => { const r = Math.random()*16|0; const v = c==="x"?r:(r&0x3|0x8); return v.toString(16); }); } 427 428type Cls = { ok: true; c: string; source: string; type: string } | { ok: false; c: string; reason: string }; 429 430function classify(req: NextRequest): Cls { 431 const url = req.nextUrl; 432 const params = url.searchParams; 433 const ref = req.headers.get("referer") || ""; 434 const ua = req.headers.get("user-agent") || ""; 435 if (BOT_RE.test(ua)) return { ok: false, c: "blocked", reason: "bot" }; 436 for (const p of PAID_PARAMS) if (params.has(p)) return { ok: false, c: "blocked", reason: "paid_ad_param" }; 437 const um = (params.get("utm_medium") || "").toLowerCase(); 438 if (um && BLOCKED_UTM.includes(um)) return { ok: false, c: "blocked", reason: "blocked_utm:" + um }; 439 const rh = host(ref); 440 if (rh && matchHost(rh, BLOCKED_HOSTS)) return { ok: false, c: "blocked", reason: "blocked_source:" + rh }; 441 if (OWNER_TAG_ENABLED && params.has(OWNER_TAG_PARAM)) { 442 const v = (params.get(OWNER_TAG_PARAM) || "").toLowerCase(); 443 return { ok: true, c: "confirmed_owner_tagged_llm", source: v, type: "owner_tagged_llm" }; 444 } 445 if (rh && matchHost(rh, LLM_HOSTS)) return { ok: true, c: "confirmed_llm_referral", source: rh, type: "llm" }; 446 if (rh && isGoogle(rh)) return { ok: true, c: "confirmed_google_organic", source: "google", type: "search" }; 447 if (rh && matchHost(rh, SEARCH_HOSTS)) return { ok: true, c: "confirmed_search_organic", source: searchName(rh), type: "search" }; 448 if (!rh) return { ok: false, c: "unknown", reason: "empty_referrer" }; 449 return { ok: false, c: "unknown", reason: "unknown_referrer" }; 450} 451 452async function postJson(url: string, body: unknown){ 453 try { 454 return await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); 455 } catch (e) { if (DEBUG) console.log("[dct] post err", e); return null; } 456} 457 458export async function middleware(request: NextRequest) { 459 const url = request.nextUrl; 460 const pathname = url.pathname; 461 462 // Skip framework internals, APIs, and static files 463 if ( 464 pathname.startsWith("/_next/") || 465 pathname.startsWith("/api/") || 466 pathname === "/favicon.ico" || 467 STATIC_EXT_RE.test(pathname) 468 ) return NextResponse.next(); 469 470 // Skip Next/Vercel prefetch requests 471 if ( 472 request.headers.get("next-router-prefetch") || 473 request.headers.get("purpose") === "prefetch" || 474 request.headers.get("x-purpose") === "prefetch" || 475 request.headers.get("sec-purpose")?.includes("prefetch") 476 ) return NextResponse.next(); 477 478 // Only run for document-style navigations (or plain GETs with no Accept). 479 const accept = request.headers.get("accept") || ""; 480 if (request.method !== "GET") return NextResponse.next(); 481 if (accept && !accept.includes("text/html") && !accept.includes("*/*")) return NextResponse.next(); 482 483 // Host allow-list 484 const hostname = url.hostname.toLowerCase(); 485 if (!ALLOWED_HOSTS.some(h => hostname === h || hostname.endsWith("." + h))) { 486 return NextResponse.next(); 487 } 488 489 const vidCookie = request.cookies.get("dct_vid")?.value; 490 const sidCookie = request.cookies.get("dct_sid_" + SITE_ID)?.value; 491 const vid = vidCookie || uuid(); 492 493 const cls = classify(request); 494 const res = NextResponse.next(); 495 const root = "." + rootDomain(hostname); 496 const secure = url.protocol === "https:"; 497 498 if (!vidCookie) { 499 res.cookies.set("dct_vid", vid, { 500 maxAge: 60 * 60 * 24 * 365, path: "/", domain: root, sameSite: "lax", secure, 501 }); 502 } 503 504 if (cls.ok && !sidCookie) { 505 const r = await postJson(COLLECTOR_URL + "/api/public/session/start", { 506 site_id: SITE_ID, 507 url: request.url, 508 referrer: request.headers.get("referer") || null, 509 visitor_id: vid, 510 user_agent: request.headers.get("user-agent") || "", 511 tracker_mode: "nextjs_middleware", 512 }); 513 try { 514 const j = r ? await r.json() : null; 515 if (j && j.ok && j.session_id) { 516 const exp = ATTR_DAYS * 86400; 517 res.cookies.set("dct_sid_" + SITE_ID, j.session_id, { 518 maxAge: exp, path: "/", domain: root, sameSite: "lax", secure, 519 }); 520 res.cookies.set("dct_site", SITE_ID, { 521 maxAge: exp, path: "/", domain: root, sameSite: "lax", secure, 522 }); 523 } 524 } catch (_) { /* ignore */ } 525 } else if (!cls.ok && cls.c === "blocked") { 526 // Fire-and-forget diagnostic â never blocks rendering 527 postJson(COLLECTOR_URL + "/api/public/diagnostic", { 528 site_id: SITE_ID, 529 classification: "blocked", 530 reason: cls.reason, 531 event_type: "request", 532 tracker_mode: "nextjs_middleware", 533 url: request.url, 534 referrer: request.headers.get("referer") || "", 535 }); 536 } 537 538 return res; 539} 540 541export const config = { 542 // Run on real page routes; skip Next internals, API routes, and common static assets. 543 matcher: [ 544 "/((?!_next/static|_next/image|api/|favicon.ico|.*\\\\.(?:js|mjs|css|map|png|jpe?g|gif|svg|ico|webp|avif|woff2?|ttf|otf|eot|json|xml|txt|pdf|mp4|webm|mp3|wasm)$).*)", 545 ], 546}; 547`}function M(t){return`<!-- Discovery Funnel Guard Shopify Tracker: site_id=${t.siteId} mode=shopify_theme --> 548<!-- Paste this entire block immediately before </body> in layout/theme.liquid (PUBLISHED theme). --> 549<!-- Purchase conversions are tracked separately via the Shopify Custom Pixel. --> 550<script> 551 window.DCT_SHOPIFY_CONTEXT = { 552 page_type: "{{ request.page_type | escape }}", 553 template: "{{ template.name | escape }}", 554 shop_domain: "{{ shop.permanent_domain | escape }}" 555 }; 556<\/script> 557<script 558 src="${t.collectorOrigin}/api/public/tracker.js" 559 data-site-id="${t.siteId}" 560 data-mode="shopify_theme" 561 data-collector="${t.collectorOrigin}" 562 data-debug="false" 563 async> 564<\/script>`}function q(t){return`// Discovery Funnel Guard â Shopify Custom Pixel
565// Settings > Customer events > Add custom pixel > paste this code. 566// Permission: "Not required" (we send no PII / no customer / no order data). 567// Subscribes ONLY to checkout_completed and maps it to a DCT 568// success_page_view, reusing the qualified DCT session created by the 569// theme snippet so Search / LLM attribution is preserved through purchase. 570// 571// Privacy: NO customer email/name/phone/address, NO order id, NO payment, 572// NO line items, NO cart contents, NO product ids/names, NO value, NO 573// checkout token â only DCT session/visitor cookies + page URL + title. 574const SITE_ID = ${JSON.stringify(t.siteId)}; 575const COLLECTOR = ${JSON.stringify(t.collectorOrigin)}; 576const MODE = "shopify_pixel"; 577 578async function getCookie(name) { 579 try { 580 if (browser && browser.cookie && browser.cookie.get) { 581 return await browser.cookie.get(name); 582 } 583 } catch (e) {} 584 return null; 585} 586 587async function postJson(path, body) { 588 try { 589 await fetch(COLLECTOR + path, { 590 method: "POST", 591 headers: { "Content-Type": "application/json" }, 592 body: JSON.stringify(body), 593 keepalive: true 594 }); 595 } catch (e) {} 596} 597 598analytics.subscribe("checkout_completed", async (event) => { 599 const sid = await getCookie("dct_sid_" + SITE_ID); 600 const vid = await getCookie("dct_vid"); 601 if (!sid && !vid) return; 602 603 const url = 604 (event && event.context && event.context.document && event.context.document.location && event.context.document.location.href) || 605 (event && event.context && event.context.window && event.context.window.location && event.context.window.location.href) || 606 ("https://" + ${JSON.stringify(t.primaryDomain)} + "/checkout/thank-you"); 607 608 await postJson("/api/public/collect", { 609 site_id: SITE_ID, 610 session_id: sid || "", 611 visitor_id: vid || "", 612 event_type: "success_page_view", 613 url: url, 614 page_title: "Shopify purchase completed", 615 tracker_mode: MODE 616 }); 617});`}function G(){return`import { sequence } from "astro:middleware"; 618import { onRequest as dct } from "./dct-middleware"; 619 620// If the site already has its own middleware, add it after dct: 621// export const onRequest = sequence(dct, existingMiddleware); 622export const onRequest = sequence(dct); 623`}function W(t){const s=JSON.stringify([t.primaryDomain,...t.allowedDomains]);return`// Discovery Conversion Tracker â Astro + EmDash Middleware 624// Save as: src/dct-middleware.ts 625import { defineMiddleware } from "astro:middleware"; 626 627const SITE_ID = "${t.siteId}"; 628const COLLECTOR_URL = "${t.collectorOrigin}"; 629const ATTR_DAYS = ${t.attributionWindowDays}; 630const OWNER_TAG_PARAM = ${JSON.stringify(t.ownerTaggedParam||"dct_llm")}; 631const OWNER_TAG_ENABLED = ${t.ownerTaggedEnabled?"true":"false"}; 632const DEBUG = false; 633 634// Accepts bare hostnames or full origins; normalised to hostnames either way. 635const ALLOWED_HOSTS: string[] = (${s} as string[]) 636 .map((v) => v.replace(/^https?:\\/\\//, "").replace(/[/:].*$/, "").toLowerCase()) 637 .filter(Boolean); 638 639const PAID_PARAMS = ["gclid","gbraid","wbraid","msclkid","dclid","fbclid","ttclid","li_fat_id"]; 640const BLOCKED_UTM = ["cpc","ppc","paid","paid_search","paid_social","display","social","influencer","affiliate","email","newsletter","referral","sponsored"]; 641const LLM_HOSTS = ["chatgpt.com","chat.openai.com","perplexity.ai","claude.ai","gemini.google.com","copilot.microsoft.com","you.com","phind.com","poe.com","mistral.ai"]; 642const SEARCH_HOSTS = ["bing.com","duckduckgo.com","yahoo.com","search.yahoo.com"]; 643const BLOCKED_HOSTS = ["facebook.com","instagram.com","linkedin.com","t.co","twitter.com","x.com","youtube.com","tiktok.com","pinterest.com","reddit.com","threads.net","medium.com","substack.com","quora.com","producthunt.com","linktr.ee","beacons.ai","bit.ly","l.instagram.com","lm.facebook.com"]; 644const BOT_RE = /(GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|anthropic-ai|PerplexityBot|Perplexity-User|Google-Extended|Applebot-Extended|Meta-ExternalAgent|Meta-ExternalFetcher|Bytespider|cohere-ai|CCBot|AI2Bot|Omgilibot|Diffbot|Googlebot|AdsBot-Google|Bingbot|Slurp|DuckDuckBot|YandexBot|Baiduspider|facebookexternalhit|MJ12bot|SemrushBot|AhrefsBot|DotBot|Petalbot|bingpreview|HeadlessChrome|PhantomJS|Puppeteer|Playwright|python-requests|Go-http-client|okhttp|axios\\/|curl\\/|Wget\\/|crawler|spider|bot\\b)/i; 645const STATIC_EXT_RE = /\\.(?:js|mjs|css|map|png|jpe?g|gif|svg|ico|webp|avif|woff2?|ttf|otf|eot|json|xml|txt|pdf|mp4|webm|mp3|wasm)$/i; 646 647// Astro has no config.matcher, so exclusions live inline. 648// /_emdash/ MUST be first: it covers the EmDash admin panel, REST API, media 649// library and passkey auth routes. Never classify, never set cookies and never 650// call the collector on those paths. 651const SKIP_PREFIXES = [ 652 "/_emdash/", 653 "/_astro/", 654 "/_image", 655 "/_server-islands/", 656 "/_actions/", 657 "/api/", 658]; 659 660function host(u: string) { try { return new URL(u).hostname.toLowerCase(); } catch { return ""; } } 661function matchHost(h: string, list: string[]) { return list.some((d) => h === d || h.endsWith("." + d)); } 662function isGoogle(h: string) { return /(^|\\.)google\\.[a-z.]{2,}$/.test(h); } 663function searchName(h: string) { if (/(^|\\.)google\\./.test(h)) return "google"; if (h.indexOf("bing") >= 0) return "bing"; if (h.indexOf("duckduckgo") >= 0) return "duckduckgo"; if (h.indexOf("yahoo") >= 0) return "yahoo"; return h; } 664function rootDomain(h: string) { const p = h.split("."); return p.length < 2 ? h : p.slice(-2).join("."); } 665function uuid() { return (globalThis.crypto && "randomUUID" in globalThis.crypto) ? globalThis.crypto.randomUUID() : "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === "x" ? r : (r & 0x3 | 0x8); return v.toString(16); }); } 666 667type Cls = 668 | { ok: true; c: string; source: string; type: string } 669 | { ok: false; c: string; reason: string }; 670 671function classify(url: URL, headers: Headers): Cls { 672 const params = url.searchParams; 673 const ref = headers.get("referer") || ""; 674 const ua = headers.get("user-agent") || ""; 675 if (BOT_RE.test(ua)) return { ok: false, c: "blocked", reason: "bot" }; 676 for (const p of PAID_PARAMS) if (params.has(p)) return { ok: false, c: "blocked", reason: "paid_ad_param" }; 677 const um = (params.get("utm_medium") || "").toLowerCase(); 678 if (um && BLOCKED_UTM.includes(um)) return { ok: false, c: "blocked", reason: "blocked_utm:" + um }; 679 const rh = host(ref); 680 if (rh && matchHost(rh, BLOCKED_HOSTS)) return { ok: false, c: "blocked", reason: "blocked_source:" + rh }; 681 if (OWNER_TAG_ENABLED && params.has(OWNER_TAG_PARAM)) { 682 const v = (params.get(OWNER_TAG_PARAM) || "").toLowerCase(); 683 return { ok: true, c: "confirmed_owner_tagged_llm", source: v, type: "owner_tagged_llm" }; 684 } 685 if (rh && matchHost(rh, LLM_HOSTS)) return { ok: true, c: "confirmed_llm_referral", source: rh, type: "llm" }; 686 if (rh && isGoogle(rh)) return { ok: true, c: "confirmed_google_organic", source: "google", type: "search" }; 687 if (rh && matchHost(rh, SEARCH_HOSTS)) return { ok: true, c: "confirmed_search_organic", source: searchName(rh), type: "search" }; 688 if (!rh) return { ok: false, c: "unknown", reason: "empty_referrer" }; 689 return { ok: false, c: "unknown", reason: "unknown_referrer" }; 690} 691 692async function postJson(url: string, body: unknown) { 693 try { 694 return await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); 695 } catch (e) { if (DEBUG) console.log("[dct] post err", e); return null; } 696} 697 698export const onRequest = defineMiddleware(async (context, next) => { 699 const { url, request, cookies, locals } = context;
700 // Undefined in astro dev and on the Node.js + SQLite self-hosted deployment. 701 const runtime = (locals as any)?.runtime; 702 const cf = runtime?.cf; 703 const waitUntil = (p: Promise<unknown> | null | undefined) => { 704 try { if (p) runtime?.ctx?.waitUntil?.(p); } catch { /* noop */ } 705 }; 706 707 const pathname = url.pathname; 708 const h = request.headers; 709 710 if (SKIP_PREFIXES.some((p) => pathname.startsWith(p))) return next(); 711 if (pathname === "/favicon.ico" || STATIC_EXT_RE.test(pathname)) return next(); 712 if ( 713 h.get("purpose") === "prefetch" || 714 h.get("x-purpose") === "prefetch" || 715 (h.get("sec-purpose") || "").includes("prefetch") 716 ) return next(); 717 if (request.method !== "GET") return next(); 718 const accept = h.get("accept") || ""; 719 if (accept && !accept.includes("text/html") && !accept.includes("*/*")) return next(); 720 721 const hostname = url.hostname.toLowerCase(); 722 if (!ALLOWED_HOSTS.some((x) => hostname === x || hostname.endsWith("." + x))) return next(); 723 724 const vidCookie = cookies.get("dct_vid")?.value; 725 const sidCookie = cookies.get("dct_sid_" + SITE_ID)?.value; 726 const vid = vidCookie || uuid(); 727 728 const cls = classify(url, h); 729 730 // Raw Set-Cookie strings, appended to the outgoing response. Deliberate: on 731 // Cloudflare the response from next() can come off the assets binding, where 732 // Astro's cookie layer is not guaranteed to attach anything. 733 const domain = "." + rootDomain(hostname); 734 const secure = url.protocol === "https:"; 735 const attrs = (maxAge: number) => 736 \`; Max-Age=\${maxAge}; Path=/; Domain=\${domain}; SameSite=Lax\` + (secure ? "; Secure" : ""); 737
738 const setCookies: string[] = []; 739 if (!vidCookie) setCookies.push(\`dct_vid=\${encodeURIComponent(vid)}\${attrs(60 * 60 * 24 * 365)}\`); 740 741 if (cls.ok && !sidCookie) { 742 // Awaited on purpose: the collector returns the session_id that becomes a 743 // cookie on this same response, so it cannot be fire-and-forget. 744 const r = await postJson(COLLECTOR_URL + "/api/public/session/start", { 745 site_id: SITE_ID, 746 url: request.url, 747 referrer: h.get("referer") || null, 748 visitor_id: vid, 749 user_agent: h.get("user-agent") || "", 750 country: cf?.country ?? null, 751 tracker_mode: "astro_emdash_middleware", 752 }); 753 try { 754 const j: any = r ? await r.json() : null; 755 if (j && j.ok && j.session_id) { 756 const exp = ATTR_DAYS * 86400; 757 setCookies.push(\`dct_sid_\${SITE_ID}=\${encodeURIComponent(j.session_id)}\${attrs(exp)}\`); 758 setCookies.push(\`dct_site=\${encodeURIComponent(SITE_ID)}\${attrs(exp)}\`); 759 } 760 } catch { /* ignore */ } 761 } else if (!cls.ok && cls.c === "blocked") { 762 waitUntil(postJson(COLLECTOR_URL + "/api/public/diagnostic", { 763 site_id: SITE_ID, 764 classification: "blocked", 765 reason: cls.reason, 766 event_type: "request", 767 tracker_mode: "astro_emdash_middleware", 768 url: request.url, 769 referrer: h.get("referer") || "", 770 })); 771 } 772 773 const response = await next(); 774 if (!setCookies.length) return response; 775 776 // Rebuild so headers are mutable. Bodyless statuses must stay bodyless. 777 const bodyless = [101, 204, 205, 304].includes(response.status); 778 const out = new Response(bodyless ? null : response.body, response); 779 for (const c of setCookies) out.headers.append("Set-Cookie", c); 780 return out; 781}); 782`}const y=typeof window<"u"?window.location.origin:"";function se(){const{id:t}=O.useParams(),[s,f]=T.useState(null);if(T.useEffect(()=>{A.from("sites").select("*").eq("id",t).maybeSingle().then(({data:b})=>f(b))},[t]),!s)return e.jsx("div",{className:"p-8 text-muted-foreground",children:"Loadingâ¦"});const o=String(s.primary_domain||"").trim().toLowerCase().replace(/^https?:\/\//,"").split("/")[0].split(":")[0],a={collectorOrigin:y,siteId:s.id,primaryDomain:o,allowedDomains:(s.allowed_domains||[]).filter(b=>b!==s.primary_domain),attributionWindowDays:s.attribution_window_days||7,ownerTaggedParam:s.owner_tagged_llm_param_name,ownerTaggedEnabled:s.allow_owner_tagged_llm},k=I(a),$=W(a),u=G(),x=U(a),C=H(a),g=B(a),r=w(a),p=w({...a}),D=M(a),R=q(a);return e.jsxs("div",{className:"mx-auto max-w-5xl px-4 py-8 space-y-6",children:[e.jsxs("div",{children:[e.jsx(j,{to:"/sites/$id",params:{id:t},className:"text-sm text-muted-foreground hover:text-foreground",children:"â Back"}),e.jsxs("h1",{className:"text-2xl font-bold mt-2",children:["Install trackers â ",s.client_name]}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Three deployment modes. Backend re-classifies every event."})]}),e.jsxs(P,{defaultValue:"cf",children:[e.jsxs(N,{children:[e.jsxs(m,{value:"cf",children:["Cloudflare Worker ",e.jsx(S,{className:"ml-2",variant:"default",children:"Recommended"})]}),e.jsxs(m,{value:"astro",children:["Astro + EmDash (Cloudflare) ",e.jsx(S,{className:"ml-2",variant:"default",children:"Recommended"})]}),e.jsxs(m,{value:"nextjs",children:["Next.js Middleware ",e.jsx(S,{className:"ml-2",variant:"default",children:"Recommended"})]}),e.jsx(m,{value:"wp",children:"WordPress PHP"}),e.jsx(m,{value:"rawphp",children:"Raw PHP"}),e.jsx(m,{value:"js",children:"JS Only"}),e.jsx(m,{value:"shopify",children:"Shopify"})]}),e.jsx(_,{value:"cf",children:e.jsxs(n,{children:[e.jsx(c,{children:e.jsx(d,{children:"Cloudflare Worker"})}),e.jsxs(l,{className:"space-y-4",children:[e.jsx("p",{className:"text-sm text-muted-foreground",children:"Strongest mode. Inspects requests server-side, sets attribution cookies before page load, sees Cloudflare bot/country signals. Pair with the JS tracker for CTA clicks."}),e.jsx(h,{items:["Add your domain to Cloudflare.","Create a Worker.","Paste the generated code below and Deploy.","Add routes: "+[s.primary_domain,...s.allowed_domains||[]].map(b=>b+"/*").join(", "),"Add the JS tracker snippet below before </body> for CTA click tracking.","Use the Test Lab to verify classification."]}),e.jsx(i,{title:"worker.js",code:k}),e.jsx(i,{title:"JS for CTA clicks",code:p})]})]})}),e.jsx(_,{value:"astro",children:e.jsxs(n,{children:[e.jsx(c,{children:e.jsx(d,{children:"Astro + EmDash (Cloudflare)"})}),e.jsxs(l,{className:"space-y-4",children:[e.jsxs("p",{className:"text-sm text-muted-foreground",children:["For Astro sites deployed to Cloudflare Workers, including sites running the EmDash CMS. On these setups the site itself IS the Cloudflare Worker, bound to ",e.jsx("code",{className:"text-xs",children:"yourdomain.com/*"}),". Cloudflare runs only one Worker per route, so a second tracking Worker cannot be added on top of it. This mode runs the tracker inside the app instead. Pair with the JS tracker for CTA clicks."]}),e.jsxs("div",{className:"text-sm bg-amber-500/10 border border-amber-500/40 rounded p-3 space-y-2",children:[e.jsx("p",{className:"font-semibold",children:"Read before deploying"}),e.jsxs("ul",{className:"list-disc list-inside space-y-1 text-xs",children:[e.jsxs("li",{children:["Astro middleware does not run for prerendered pages, and Cloudflare serves matching static assets before invoking the Worker. If the Test Lab shows nothing after deploy, set ",e.jsx("code",{children:"assets.run_worker_first"})," in ",e.jsx("code",{children:"wrangler.jsonc"}),". Note this bills asset requests as Worker invocations; an array of path patterns can limit it to HTML routes."]}),e.jsxs("li",{children:["Admin traffic under ",e.jsx("code",{children:"/_emdash/"})," is never tracked by design. Editor sessions and CMS preview URLs will not appear in reports."]}),e.jsx("li",{children:"EmDash's Dynamic Worker Loaders requirement, which needs a paid Cloudflare account, applies to EmDash plugins only. This tracker is Astro middleware and has no such requirement."}),e.jsxs("li",{children:["EmDash is in beta preview. If a future release changes the ",e.jsx("code",{children:"/_emdash/"})," route prefix, update ",e.jsx("code",{children:"SKIP_PREFIXES"}),"."]})]})]}),e.jsx(h,{items:["Confirm astro.config.mjs uses the Cloudflare adapter and output: 'server'. EmDash sites already meet this.","Create src/dct-middleware.ts and paste File 1 below.","Create or update src/middleware.ts with File 2 below. Do not overwrite an existing middleware file with a bare export; compose it with sequence() instead.",'Check the "assets" block in wrangler.jsonc. If any pages are prerendered, add "run_worker_first": true. EmDash serves content live from the database, so most pages are server-rendered and this is usually unnecessary. Verify, do not assume.',"Deploy with wrangler deploy.","Add the JS tracker snippet before </body> in the theme layout for CTA click tracking.","Use the Test Lab to verify classification on a live content page."]}),e.jsx(i,{title:"src/dct-middleware.ts",code:$}),e.jsx(i,{title:"src/middleware.ts",code:u}),e.jsx(i,{title:"JS for CTA clicks",code:p})]})]})}),e.jsx(_,{value:"nextjs",children:e.jsxs(n,{children:[e.jsx(c,{children:e.jsx(d,{children:"Next.js Middleware"})}),e.jsxs(l,{className:"space-y-4",children:[e.jsxs("p",{className:"text-sm text-muted-foreground",children:["Edge middleware that runs ",e.jsx("strong",{children:"before"})," the page renders. Same quality as Cloudflare Worker: server-side classification of referrer / user-agent / URL, attribution cookies set before HTML is sent, bots blocked from creating sessions. Pair with the JS tracker for CTA clicks and conversions."]}),e.jsxs("div",{className:"text-sm bg-secondary/60 border border-border rounded p-3 space-y-1",children:[e.jsx("p",{className:"font-semibold",children:"Where this file goes"}),e.jsxs("p",{children:["Save as ",e.jsx("code",{className:"text-xs",children:"middleware.ts"})," at your Next.js project root (same folder as ",e.jsx("code",{className:"text-xs",children:"package.json"}),", next to ",e.jsx("code",{className:"text-xs",children:"app/"})," or ",e.jsx("code",{className:"text-xs",children:"pages/"}),"). Works with both App Router and Pages Router. Deploys automatically on Vercel, Netlify, or self-hosted Next.js â no extra config needed."]}),e.jsxs("p",{children:["The exported ",e.jsx("code",{className:"text-xs",children:"config.matcher"})," skips ",e.jsx("code",{className:"text-xs",children:"/_next/*"}),", ",e.jsx("code",{className:"text-xs",children:"/api/*"}),", static assets (.js/.css/.png/.svg/.woff/â¦), and Next/Vercel prefetch requests, so only real page navigations are classified."]})]}),e.jsx(h,{items:["Create middleware.ts at your Next.js project root.","Paste the generated code below.","Commit and deploy (Vercel / Netlify / self-hosted â all supported).","Add the JS tracker snippet to your root layout (app/layout.tsx or pages/_app.tsx) for CTA click + conversion tracking.","Use the Test Lab to verify classification."]}),e.jsx(i,{title:"middleware.ts",code:x}),e.jsx(i,{title:"JS for CTA clicks (add to app/layout.tsx or _app)",code:p})]})]})}),e.jsx(_,{value:"wp",children:e.jsxs(n,{children:[e.jsx(c,{children:e.jsx(d,{children:"WordPress plugin"})}),e.jsxs(l,{className:"space-y-4",children:[e.jsx("p",{className:"text-sm text-muted-foreground",children:"Server-side referrer/UA classification + cookies + injected JS. Excludes admins, cron, and REST."}),e.jsxs("div",{className:"text-sm bg-destructive/10 border border-destructive/30 rounded p-3 space-y-1",children:[e.jsx("p",{className:"font-semibold",children:"Important fix: reinstall this updated plugin code."}),e.jsxs("p",{children:["Your previous copy used the preview app URL as the collector, which can return a login HTML page instead of tracker JSON. The code below now posts to the public collector: ",e.jsx("code",{className:"text-xs",children:y}),"."]})]}),e.jsxs("div",{className:"text-sm bg-secondary/60 border border-border rounded p-3 space-y-1",children:[e.jsx("p",{className:"font-semibold",children:"Where does this PHP code go?"}),e.jsxs("p",{children:[e.jsx("strong",{children:"Recommended:"})," save it as a plugin file at ",e.jsx("code",{className:"text-xs",children:"wp-content/plugins/discovery-conversion-tracker/discovery-conversion-tracker.php"})," and activate it from ",e.jsx("em",{children:"WP Admin â Plugins"}),". No theme edits, no header/body/footer paste â the plugin auto-injects the tracker via the ",e.jsx("code",{className:"text-xs",children:"wp_footer"})," hook on every public page."]}),e.jsxs("p",{children:[e.jsx("strong",{children:"Alternative:"})," paste the body (everything after ",e.jsx("code",{className:"text-xs",children:"<?php"}),") into the ",e.jsx("em",{children:"Code Snippets"}),' plugin and set it to "Run snippet everywhere". Do ',e.jsx("strong",{children:"not"})," paste this into ",e.jsx("em",{children:"header.php"}),", ",e.jsx("em",{children:"footer.php"}),", or a Custom HTML widget â it must run as PHP, not as raw HTML."]})]}),e.jsx(h,{items:["Save the code below as wp-content/plugins/discovery-conversion-tracker/discovery-conversion-tracker.php","In WP Admin â Plugins, activate Discovery Conversion Tracker.","Or, alternatively, paste the body into the Code Snippets plugin (run everywhere).","Visit any public page of your site once to fire the tracker.","Run the Verifier below, then use the Test Lab for scenario coverage."]}),e.jsx(i,{title:"discovery-conversion-tracker.php",code:C})]})]})}),e.jsx(_,{value:"rawphp",children:e.jsxs(n,{children:[e.jsx(c,{children:e.jsx(d,{children:"Raw PHP (no WordPress / no plugin)"})}),e.jsxs(l,{className:"space-y-4",children:[e.jsx("p",{className:"text-sm text-muted-foreground",children:"Drop-in PHP for any custom site, framework, or hand-built theme. Same server-side classification as the WP plugin, but uses pure PHP (cURL / file_get_contents) â no WordPress functions required."}),e.jsxs("div",{className:"text-sm bg-amber-500/10 border border-amber-500/40 rounded p-3 space-y-2",children:[e.jsx("p",{className:"font-semibold",children:"Where exactly to put this code"}),e.jsxs("p",{children:["It ",e.jsx("strong",{children:"must run before any HTML output"})," (the script sets cookies and headers). Pick ONE of the three options below â do not paste it into the <head>, <body>, or footer of a rendered HTML page; it will fail silently because headers are already sent."]}),e.jsxs("ol",{className:"list-decimal list-inside space-y-1",children:[e.jsxs("li",{children:[e.jsx("strong",{children:"Option A â Auto-prepend (best, zero edits to your pages):"})," save the snippet as ",e.jsx("code",{className:"text-xs",children:"/full/path/to/dct-tracker.php"}),", then add to your ",e.jsx("code",{className:"text-xs",children:".htaccess"})," (Apache):",e.jsx("br",{}),e.jsx("code",{className:"text-xs block bg-background/60 p-2 mt-1 rounded",children:'php_value auto_prepe
782nd_file "/full/path/to/dct-tracker.php"'}),"or in ",e.jsx("code",{className:"text-xs",children:"php.ini"})," / ",e.jsx("code",{className:"text-xs",children:".user.ini"}),":",e.jsx("br",{}),e.jsx("code",{className:"text-xs block bg-background/60 p-2 mt-1 rounded",children:"auto_prepend_file = /full/path/to/dct-tracker.php"})]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Option B â Manual include:"})," add this as the ",e.jsx("strong",{children:"very first line"})," of every PHP entry file (",e.jsx("code",{children:"index.php"}),", ",e.jsx("code",{children:"header.php"})," if it runs ",e.jsx("em",{children:"before"})," ",e.jsx("code",{children:"<!DOCTYPE>"}),", etc.), with no whitespace before ",e.jsx("code",{children:"<?php"}),":",e.jsx("br",{}),e.jsx("code",{className:"text-xs block bg-background/60 p-2 mt-1 rounded",children:"<?php require_once __DIR__ . '/dct-tracker.php'; ?>"})]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Option C â Paste inline:"})," paste the entire snippet as the very first thing in your front controller (e.g. top of ",e.jsx("code",{children:"index.php"}),"), before any ",e.jsx("code",{children:"echo"}),", HTML, or whitespace."]})]}),e.jsxs("p",{className:"text-xs",children:["The JS tracker tag for CTA clicks is auto-injected before ",e.jsx("code",{children:"</body>"})," by an output buffer â you do not need to add a script tag yourself."]})]}),e.jsx(h,{items:["Save the code below as dct-tracker.php on your server.","Wire it up using Option A, B, or C above.","Visit any public page once (incognito or with ?dct_debug=1).","Run the Verifier below to confirm the tracker is reporting."]}),e.jsx(i,{title:"dct-tracker.php",code:g})]})]})}),e.jsx(_,{value:"js",children:e.jsxs(n,{children:[e.jsx(c,{children:e.jsx(d,{children:"JavaScript only"})}),e.jsxs(l,{className:"space-y-4",children:[e.jsx("p",{className:"text-sm text-amber-700 dark:text-amber-400 bg-secondary p-3 rounded",children:"â JavaScript-only mode may miss LLM clicks when the LLM platform strips referrer data. Use Cloudflare Worker mode or WordPress PHP mode when possible."}),e.jsx(h,{items:["Paste the snippet below before </body> on every page.","Use the Test Lab to verify classification."]}),e.jsx(i,{title:"snippet.html",code:r})]})]})}),e.jsx(_,{value:"shopify",children:e.jsxs(n,{children:[e.jsx(c,{children:e.jsx(d,{children:"Shopify"})}),e.jsxs(l,{className:"space-y-6",children:[e.jsxs("p",{className:"text-sm text-muted-foreground",children:["Two pieces. The ",e.jsx("strong",{children:"theme snippet"})," tracks qualified visits, page views, and CTA clicks on storefront pages. The ",e.jsx("strong",{children:"Custom Pixel"})," tracks the completed purchase from Shopify's checkout (which the theme can't reach) and attaches it to the existing DCT session â preserving Search / LLM attribution all the way to conversion. We deliberately send no customer, order, product, or value data."]}),e.jsxs("div",{className:"text-sm bg-amber-500/10 border border-amber-500/40 rounded p-3 space-y-2",children:[e.jsx("p",{className:"font-semibold",children:"Use the latest snippet â collector URL must match this app"}),e.jsxs("p",{children:["Your tracker code must use this app's current public URL. If your existing snippet contains a different host (e.g. ",e.jsx("code",{className:"text-xs",children:"project--â¦lovable.app"})," or any old preview URL), ",e.jsx("strong",{children:"reinstall the latest snippet below"}),"."]}),e.jsxs("ul",{className:"list-disc list-inside text-xs space-y-0.5",children:[e.jsxs("li",{children:["Current collector URL: ",e.jsx("code",{className:"text-xs",children:y})]}),e.jsxs("li",{children:["Site ID: ",e.jsx("code",{className:"text-xs",children:s.id})]})]})]}),e.jsxs("div",{className:"text-sm bg-amber-500/10 border border-amber-500/40 rounded p-3 space-y-2",children:[e.jsx("p",{className:"font-semibold",children:"Theme editor warning"}),e.jsxs("p",{children:["The Shopify code editor can show ",e.jsx("code",{className:"text-xs",children:"theme.liquid"})," for a theme that is ",e.jsx("strong",{children:"not"})," the published/live theme. The verifier checks the public live URL, not the editor preview. Always confirm you are editing the ",e.jsx("strong",{children:"currently published"})," theme."]})]}),e.jsxs("div",{className:"text-sm bg-secondary/60 border border-border rounded p-3 space-y-1",children:[e.jsx("p",{className:"font-semibold",children:"After saving â verify on the live page source"}),e.jsxs("ol",{className:"list-decimal list-inside text-xs space-y-0.5",children:[e.jsxs("li",{children:["Open the live storefront in an ",e.jsx("strong",{children:"incognito"})," window."]}),e.jsxs("li",{children:["Right-click â ",e.jsx("strong",{children:"View page source"}),"."]}),e.jsxs("li",{children:["Search for ",e.jsx("code",{children:"Discovery Funnel Guard Shopify Tracker"}),"."]}),e.jsxs("li",{children:["Confirm it contains ",e.jsxs("code",{children:["site_id=",s.id]}),", ",e.jsxs("code",{children:['src="',y,'/api/public/tracker.js"']}),", and ",e.jsxs("code",{children:['data-site-id="',s.id,'"']}),"."]}
782),e.jsx("li",{children:"If not found, you edited the wrong/unpublished theme or did not save."})]})]}),e.jsxs("div",{children:[e.jsx("h3",{className:"font-semibold text-sm mb-2",children:"1. Storefront tracker (theme snippet)"}),e.jsx(h,{items:["Shopify Admin â Online Store â Themes â ⯠â Edit code (on the PUBLISHED theme).","Open layout/theme.liquid.","Paste the snippet below immediately before </body> and Save.","Visit any storefront page from a qualified source (Google / ChatGPT / Perplexity) to verify a qualified session is created."]}),e.jsx(i,{title:"theme.liquid (paste before </body>)",code:D})]}),e.jsxs("div",{children:[e.jsx("h3",{className:"font-semibold text-sm mb-2",children:"2. Purchase pixel (Shopify Custom Pixel)"}),e.jsxs("div",{className:"text-sm bg-secondary/60 border border-border rounded p-3 space-y-1 mb-3",children:[e.jsx("p",{className:"font-semibold",children:"Why a Custom Pixel?"}),e.jsxs("p",{children:[`Shopify's checkout pages no longer run theme JavaScript (checkout.liquid and "Additional Scripts" are deprecated). The Custom Pixel API is the supported replacement. This pixel subscribes only to `,e.jsx("code",{className:"text-xs",children:"checkout_completed"})," and maps it to a DCT ",e.jsx("code",{className:"text-xs",children:"success_page_view"})," with ",e.jsx("code",{className:"text-xs",children:"tracker_mode: shopify_pixel"}),". It reads the existing ",e.jsx("code",{className:"text-xs",children:"dct_sid_*"})," / ",e.jsx("code",{className:"text-xs",children:"dct_vid"})," cookies set by the theme snippet â if neither exists, no conversion is sent (we never invent attribution). The backend treats ",e.jsx("code",{className:"text-xs",children:"shopify_pixel"})," conversions as authoritative and bypasses normal success-URL rule matching."]})]}),e.jsx(h,{items:["Shopify Admin â Settings â Customer events â Add custom pixel.",'Name it "Discovery Funnel Guard â Purchase" and set Permission to "Not required" (we send no PII).',"Paste the code below into the Code box, Save, then Connect the pixel.","Place a test order from a qualified-source session to verify the conversion appears in the DCT dashboard."]}),e.jsx(i,{title:"Shopify Custom Pixel â code",code:R})]}),e.jsxs("div",{children:[e.jsx("h3",{className:"font-semibold text-sm mb-2",children:"3. End-to-end test flow"}),e.jsx(h,{items:["Save storefront snippet in the published theme.",`Open storefront in incognito with: https://${o}/?dct_debug=1&dct_cta_debug=1`,"Open DevTools console â confirm [DCT] session or [DCT] no session logs.",`If owner-tagged LLM is enabled, test with: https://${o}/?dct_llm=chatgpt`,"Click a CTA on the storefront.","Complete a Shopify test checkout.","Confirm the dashboard shows: qualified visit, page view, CTA click, Shopify purchase conversion, with source preserved as Search or LLM."]})]})]})]})})]}),e.jsxs(n,{className:"border-primary/40 bg-primary/5",children:[e.jsx(c,{children:e.jsx(d,{className:"text-base",children:"How CTA clicks are tracked"})}),e.jsxs(l,{className:"text-sm space-y-3",children:[e.jsxs("p",{children:["The tracker only counts a click as a CTA if (a) the visitor has a ",e.jsx("strong",{children:"qualified session"})," (landed from Google or an LLM), and (b) the clicked element is recognised as a CTA. There are three ways to recognise a CTA â pick whichever fits your site:"]}),e.jsxs("ol",{className:"list-decimal list-inside space-y-2",children:[e.jsxs("li",{children:[e.jsx("strong",{children:"Auto-detect (default, no setup):"})," any ",e.jsx("code",{children:"<button>"}),", ",e.jsx("code",{children:"<a>"}),", ",e.jsx("code",{children:'<input type="button|submit">'}),", or ",e.jsx("code",{children:'[role="button"]'})," click is tracked. The visible label is used as the CTA name in reports. ",e.jsxs("em",{children:["If your site uses custom ",e.jsx("code",{children:"<div>"}),"-based buttons, this will miss them â use option 2 or 3."]})]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Tag elements directly in your HTML:"})," add ",e.jsx("code",{children:'data-discovery-cta="Free trial"'})," to any element. The element becomes a tracked CTA and the attribute value is used as its label. Works on divs, spans, icons â anything.",e.jsx("pre",{className:"bg-background/60 text-xs p-2 rounded mt-1",children:'<div class="hero-btn" data-discovery-cta="Hero â Get started">Get started â</div>'})]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Declare custom CTA rules in Settings (no code on your site):"})," open ",e.jsx(j,{to:"/sites/$id/settings",params:{id:s.id},className:"underline font-medium",children:"Site settings â Custom CTA rules"})," and add CSS selectors, roles, or data-attribute rules. Useful when you can't edit the site's HTML. Examples:",e.jsx("pre",{className:"bg-background/60 text-xs p-2 rounded mt-1",children:`selector:.btn-primary|Primary CTA 783selector:#pricing button|Pricing CTA 784selector:a[href*="/signup"]|Signup link`}),"Toggle ",e.jsx("strong",{children:"Strict mode"})," in the same panel to disable auto-detect entirely and only count what you've declared (kills nav/cookie-banner noise)."]})]}),e.jsxs("div",{className:"border-t border-border pt-3 mt-3",children:[e.jsx("p",{className:"font-semibold",children:"Debug what's being tracked on your live site"}),e.jsxs("p",{children:["Open your site with ",e.jsx("code",{children:"?dct_cta_debug=1"})," in the URL (e.g. ",e.jsxs("code",{children:["https://",o,"/?dct_cta_debug=1"]}),") and open DevTools â Console. The tracker will:"]}),e.jsxs("ul",{className:"list-disc list-inside text-xs mt-1 space-y-0.5",children:[e.jsxs("li",{children:["Outline every detected CTA (",e.jsx("span",{className:"text-green-600",children:"green dashed"})," = auto-detect, ",e.jsx("span",{className:"text-blue-600",children:"blue solid"})," = custom rule)."]}),e.jsxs("li",{children:["Log ",e.jsx("code",{children:"[DCT] sending cta_click"})," when a tracked click fires."]}),e.jsxs("li",{children:["Log ",e.jsx("code",{children:"[DCT] click ignored â â¦"})," with the actual element when an untracked click is dropped, so you can see exactly what selector you'd need."]}),e.jsxs("li",{children:["If you see ",e.jsx("em",{children:'"CTA matched but no qualified session"'}),", the click matched a CTA but the visitor didn't arrive via Google/LLM â that's expected (only qualified visits count)."]})]})]})]})]}),e.jsxs(n,{className:"border-amber-500/40 bg-amber-500/5",children:[e.jsx(c,{children:e.jsx(d,{className:"text-base",children:"If verification passes but no visits are tracked"})}),e.jsxs(l,{className:"text-sm space-y-2",children:[e.jsxs("p",{children:[e.jsx("strong",{children:"1. Log out of WP Admin (or open incognito)."})," The plugin intentionally skips logged-in administrators so your own visits don't pollute analytics. Test from a private/incognito window or a different browser."]}
784),e.jsxs("p",{children:[e.jsx("strong",{children:"2. Force-test as admin:"})," append ",e.jsx("code",{children:"?dct_debug=1"})," to any page URL (e.g. ",e.jsxs("code",{children:["https://",o,"/?dct_debug=1"]}),"). This bypasses the admin skip and adds ",e.jsx("code",{children:"X-DCT-Debug-*"})," response headers you can inspect in DevTools â Network."]}),e.jsxs("p",{children:[e.jsx("strong",{children:"3. Check your domain settings."})," Primary domain and allowed domains should be bare hostnames like ",e.jsx("code",{children:o}),", not ",e.jsxs("code",{children:["https://",o,"/"]}),". The backend now normalizes these, but cleaner is better."]}),e.jsxs("p",{children:[e.jsx("strong",{children:"4. Google referrer policy:"})," some Google result pages send ",e.jsx("code",{children:"origin"}),"-only referrer (",e.jsx("code",{children:"https://www.google.com/"}),") which we accept. If your site sends ",e.jsx("code",{children:"Referrer-Policy: no-referrer"}),", organic Google visits will be classified as ",e.jsx("code",{children:"unknown"})," and not counted."]})]})]}),e.jsx(J,{siteId:s.id,defaultUrl:`https://${o}/`,expectedCollectorOrigin:y})]})}function J({siteId:t,defaultUrl:s,expectedCollectorOrigin:f}){const[o,a]=T.useState(s),[k,$]=T.useState(!1),[u,x]=T.useState(null);async function C(){$(!0),x(null);try{const p=await(await fetch("/api/public/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({site_id:t,url:o,expected_collector_origin:f})})).json();p?.error?(E.error(p.error),x(null)):x(p)}catch(r){E.error(r?.message||"Verifier failed")}finally{$(!1)}}const g={tracker_installed_and_active:{text:"Tracker installed and actively reporting",tone:"ok"},tracker_installed_no_activity_yet:{text:"Tracker installed â no backend activity yet. Visit the page once.",tone:"warn"},tracker_not_detected:{text:"Tracker not detected on this URL",tone:"bad"},tracker_wrong_collector:{text:"Tracker found but points to an old/wrong collector URL â recopy the latest snippet",tone
784:"bad"},unreachable:{text:"Page is unreachable from the verifier",tone:"bad"}};return e.jsxs(n,{children:[e.jsx(c,{children:e.jsx(d,{children:"Installation Verifier"})}),e.jsxs(l,{className:"space-y-4",children:[e.jsx("p",{className:"text-sm text-muted-foreground",children:"Enter a public URL on your site. The verifier fetches the page, looks for the tracker snippet, and checks for recent backend activity."}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(L,{value:o,onChange:r=>a(r.target.value),placeholder:"https://example.com/"}),e.jsx(v,{onClick:C,disabled:k||!o,children:k?"Checkingâ¦":"Verify"})]}),u&&e.jsxs("div",{className:"space-y-3",children:[e.jsx("div",{className:"text-sm font-medium p-3 rounded border "+(g[u.summary]?.tone==="ok"?"border-green-500/40 bg-green-500/10 text-green-700 dark:text-green-400":g[u.summary]?.tone==="warn"?"border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-400":"border-red-500/40 bg-red-500/10 text-red-700 dark:text-red-400"),children:g[u.summary]?.text||u.summary}),e.jsx("ul",{className:"space-y-1.5",children:u.checks.map(r=>e.jsxs("li",{className:"flex items-start gap-2 text-sm",children:[e.jsx("span",{className:r.pass?"text-green-600":"text-red-600",children:r.pass?"â":"â"}),e.jsxs("div",{children:[e.jsx("div",{children:r.label}),r.detail&&e.jsx("div",{className:"text-xs text-muted-foreground font-mono",children:r.detail})]})]},r.id))})]})]})]})}function h({items:t}){return e.jsx("ol",{className:"text-sm space-y-1 list-decimal list-inside",children:t.map((s,f)=>e.jsx("li",{children:s},f))})}function i({title:t,code:s}){return e.jsxs("div",{className:"rounded-md border border-border bg-secondary/40",children:[e.jsxs("div",{className:"flex items-center justify-between px-3 py-2 border-b border-border",children:[e.jsx("span",{className:"text-xs font-mono text-muted-foreground",children:t}),e.jsx(v,{size:"sm",variant:"ghost",onClick:()=>{navigator.clipboard.writeText(s),E.success("Copied")},children:"Copy"})]}),e.jsx("pre",{className:"text-xs p-3 overflow-auto max-h-96",children:e.jsx("code",{children:s})})]})}export{se as component};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.