PageSourceSearch

https://discovery-traffic-tracker.lovable.app/assets/sites._id.install-CAIq-0fK.js

js discovery-traffic-tracker.lovable.app collected 2026-10-03 03:26:24 UTC 71,595 bytes, 784 lines download raw bytes

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.