thumbgate 1.23.0 → 1.23.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.claude-plugin/marketplace.json +5 -5
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.well-known/llms.txt +26 -11
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +69 -34
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/mcp/server-stdio.js +1 -1
  8. package/adapters/opencode/opencode.json +1 -1
  9. package/bin/cli.js +57 -16
  10. package/bin/postinstall.js +11 -22
  11. package/config/gate-templates.json +72 -0
  12. package/config/github-about.json +1 -1
  13. package/config/post-deploy-marketing-pages.json +10 -0
  14. package/package.json +6 -6
  15. package/public/agent-manager.html +3 -3
  16. package/public/agents-cost-savings.html +3 -3
  17. package/public/ai-malpractice-prevention.html +726 -149
  18. package/public/blog.html +3 -3
  19. package/public/codex-enterprise.html +3 -3
  20. package/public/codex-plugin.html +4 -4
  21. package/public/compare.html +6 -6
  22. package/public/dashboard.html +211 -126
  23. package/public/guide.html +5 -5
  24. package/public/index.html +187 -47
  25. package/public/learn.html +24 -10
  26. package/public/lessons.html +2 -2
  27. package/public/numbers.html +6 -6
  28. package/public/pricing.html +6 -5
  29. package/public/pro.html +23 -0
  30. package/scripts/billing.js +17 -0
  31. package/scripts/commercial-offer.js +75 -0
  32. package/scripts/dashboard.js +53 -1
  33. package/scripts/gates-engine.js +3 -3
  34. package/scripts/plausible-server-events.js +2 -1
  35. package/scripts/rate-limiter.js +16 -12
  36. package/scripts/seo-gsd.js +167 -1
  37. package/scripts/telemetry-analytics.js +310 -0
  38. package/scripts/visitor-journey.js +172 -0
  39. package/src/api/server.js +65 -29
  40. package/adapters/chatgpt/openapi.yaml +0 -1705
@@ -155,6 +155,21 @@ const HIGH_ROI_QUERY_SEEDS = [
155
155
  91,
156
156
  'Captures research-led interest in how AI systems decide which brands and tools to recommend.',
157
157
  ),
158
+ querySeed(
159
+ 'ai mode ads agent governance',
160
+ 94,
161
+ 'Conversational Google AI Mode ad demand where ThumbGate can become the cited answer for teams asking how to govern AI-agent actions, not just buy clicks.',
162
+ ),
163
+ querySeed(
164
+ 'mcp tool governance',
165
+ 95,
166
+ 'High-intent MCP buyer query where teams need approval boundaries and audit proof before exposing tools to agents.',
167
+ ),
168
+ querySeed(
169
+ 'ai agent pre action approval gates',
170
+ 95,
171
+ 'Bottom-of-funnel query for teams ready to add human approval and evidence requirements before AI agents touch risky tools.',
172
+ ),
158
173
  {
159
174
  query: 'thumbs up thumbs down feedback for ai coding agents',
160
175
  businessValue: 95,
@@ -1362,6 +1377,51 @@ const AI_RECOMMENDATION_VISIBILITY_GUIDE_SPECS = Object.freeze([
1362
1377
  ],
1363
1378
  relatedPaths: ['/guides/ai-search-topical-presence', '/compare/mem0'],
1364
1379
  },
1380
+ {
1381
+ slug: 'ai-mode-ads-agent-governance',
1382
+ meta: {
1383
+ query: 'ai mode ads agent governance',
1384
+ title: 'AI Mode Ads for Agent Governance | Turn Buyer Prompts Into ThumbGate Demand',
1385
+ heroTitle: 'AI Mode ads make agent-governance promotion conversational',
1386
+ heroSummary: 'Google AI Mode and conversational ad formats shift promotion from click-chasing to answer ownership. ThumbGate should show up when buyers ask how to govern Claude Code, Cursor, Codex, Gemini, MCP tools, and risky agent actions.',
1387
+ },
1388
+ takeaways: [
1389
+ 'Conversational ads reward brands that answer the buyer question directly before the click.',
1390
+ 'ThumbGate should own prompts about pre-action gates, MCP tool governance, repeated AI-agent mistakes, and audit proof.',
1391
+ 'The best paid-search asset is an answer page with schema, proof links, and a checkout or workflow-intake path.',
1392
+ ],
1393
+ sections: [
1394
+ ['paragraphs', 'Why conversational ads change the promotion plan', [
1395
+ 'Search ads are moving toward generated answers and product recommendations inside the search conversation. A buyer may not click ten blue links before forming an opinion; the assistant-like surface may summarize the category, compare options, and mention a vendor in one flow.',
1396
+ 'That means ThumbGate promotion has to be answer-shaped. The page must say exactly what ThumbGate is, which agent-risk problem it solves, which tools it supports, and what proof a buyer can verify before starting checkout or a Workflow Hardening Sprint.',
1397
+ ]],
1398
+ ['bullets', 'Buyer prompts ThumbGate should target', [
1399
+ 'How do I stop Claude Code or Cursor from repeating risky mistakes?',
1400
+ 'What is the pre-action approval layer for AI coding agents?',
1401
+ 'How do teams govern MCP tools before agents call them?',
1402
+ 'How do I audit AI-agent actions before production deploys, payments, or database writes?',
1403
+ 'What tool turns thumbs-down feedback into enforceable rules?',
1404
+ ]],
1405
+ ['paragraphs', 'How this page should be used', [
1406
+ 'Use this page as the landing asset for AI Mode, Gemini, and high-intent search experiments. Do not send conversational-ad traffic to a generic homepage first. Send it to the answer that mirrors the buyer prompt, then route warm readers to Pro checkout, the install guide, or workflow intake depending on intent.',
1407
+ ]],
1408
+ ],
1409
+ faq: [
1410
+ [
1411
+ 'How do AI Mode ads help ThumbGate?',
1412
+ 'They favor direct, conversational answers. ThumbGate benefits when it has pages that answer specific buyer prompts about AI-agent governance, MCP tool risk, pre-action approval gates, and repeated failure prevention.',
1413
+ ],
1414
+ [
1415
+ 'Should ThumbGate run broad Google Ads immediately?',
1416
+ 'No. The high-ROI path is to build answer assets first, then test narrow paid search or AI Mode campaigns against exact buyer prompts where the page, schema, proof links, and checkout route all match the question.',
1417
+ ],
1418
+ [
1419
+ 'What is the one-sentence ad answer?',
1420
+ 'ThumbGate is the pre-action execution gate for AI agents: it checks risky tool calls before they run, turns feedback into rules, and gives teams audit proof.',
1421
+ ],
1422
+ ],
1423
+ relatedPaths: ['/guides/ai-search-topical-presence', '/guides/pre-action-checks', '/guides/mcp-tool-governance'],
1424
+ },
1365
1425
  ]);
1366
1426
 
1367
1427
  function buildAiRecommendationVisibilityGuide(spec) {
@@ -1952,6 +2012,112 @@ const PAGE_BLUEPRINTS = [
1952
2012
  },
1953
2013
  ...BROWSER_BRIDGE_GUIDE_SPECS.map(buildBrowserBridgeGuide),
1954
2014
  ...AI_RECOMMENDATION_VISIBILITY_GUIDE_SPECS.map(buildAiRecommendationVisibilityGuide),
2015
+ {
2016
+ query: 'mcp tool governance',
2017
+ path: '/guides/mcp-tool-governance',
2018
+ pageType: 'guide',
2019
+ pillar: 'pre-action-checks',
2020
+ title: 'MCP Tool Governance | Pre-Action Gates Before Agents Call Tools',
2021
+ heroTitle: 'MCP tool governance before agents call real systems',
2022
+ heroSummary: 'MCP makes tools easy for agents to discover and call. ThumbGate adds the missing governance layer: approval boundaries, evidence requirements, and audit logs before high-risk MCP tool calls execute.',
2023
+ takeaways: [
2024
+ 'MCP adoption expands what agents can touch, so teams need a tool-call control plane.',
2025
+ 'Governance belongs before execution, not only in post-run logs or prompt rules.',
2026
+ 'ThumbGate turns feedback, policies, and evidence requirements into enforceable pre-action gates for MCP-compatible agent workflows.',
2027
+ ],
2028
+ sections: [
2029
+ {
2030
+ heading: 'Why MCP changes the risk model',
2031
+ paragraphs: [
2032
+ 'MCP turns databases, file systems, browsers, ticketing systems, cloud APIs, and internal tools into surfaces an agent can call. That is useful, but it also means a bad plan can become a real action faster than a human reviewer can notice.',
2033
+ 'The governance question is no longer only which tools exist. It is which agent, workflow, branch, file path, command, customer record, or environment is allowed to use each tool under which proof requirements.',
2034
+ ],
2035
+ },
2036
+ {
2037
+ heading: 'What MCP tool governance needs',
2038
+ bullets: [
2039
+ 'Tool inventory: know which tools are exposed to which agents and runtimes.',
2040
+ 'Risk tiers: classify destructive, customer-facing, production, payment, and data-export tools differently from read-only tools.',
2041
+ 'Pre-action checks: require evidence or approval before risky calls execute.',
2042
+ 'Feedback loops: turn thumbs-down reviews and incidents into reusable prevention rules.',
2043
+ 'Audit proof: log allowed, blocked, and approved tool calls with enough context for review.',
2044
+ ],
2045
+ },
2046
+ {
2047
+ heading: 'Where ThumbGate fits',
2048
+ paragraphs: [
2049
+ 'ThumbGate sits between generated intent and executed action. The agent can still plan and propose MCP tool calls, but ThumbGate checks the call against learned lessons, policy boundaries, evidence requirements, and workflow risk before the tool runs.',
2050
+ ],
2051
+ },
2052
+ ],
2053
+ faq: [
2054
+ {
2055
+ question: 'What is MCP tool governance?',
2056
+ answer: 'MCP tool governance is the policy, approval, evidence, and audit layer around tools exposed through Model Context Protocol so agents do not call high-risk systems without the right checks.',
2057
+ },
2058
+ {
2059
+ question: 'How is this different from an MCP server allowlist?',
2060
+ answer: 'An allowlist says a tool exists or is available. ThumbGate adds runtime context: tool arguments, branch, path, environment, prior feedback, evidence requirements, and whether this exact action should be allowed now.',
2061
+ },
2062
+ {
2063
+ question: 'Can ThumbGate work across multiple MCP-compatible agents?',
2064
+ answer: 'Yes. The same local-first lesson and pre-action gate pattern is designed for Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode, and MCP-compatible workflows.',
2065
+ },
2066
+ ],
2067
+ relatedPaths: ['/guides/pre-action-checks', '/guides/ai-mode-ads-agent-governance', '/guides/background-agent-governance'],
2068
+ },
2069
+ {
2070
+ query: 'ai agent pre action approval gates',
2071
+ path: '/guides/ai-agent-pre-action-approval-gates',
2072
+ pageType: 'guide',
2073
+ pillar: 'pre-action-checks',
2074
+ title: 'AI Agent Pre-Action Approval Gates | Human Review Before Risky Tool Calls',
2075
+ heroTitle: 'AI agent pre-action approval gates for risky tool calls',
2076
+ heroSummary: 'Human review after the damage lands is too late. ThumbGate adds pre-action approval gates so risky AI-agent commands, deploys, file edits, API calls, and MCP tool calls can require evidence or explicit approval before execution.',
2077
+ takeaways: [
2078
+ 'Approval gates matter most at the action boundary, where the agent is about to touch files, terminals, APIs, CI, payments, or production systems.',
2079
+ 'The right gate can block, pause for approval, or log-and-continue depending on risk.',
2080
+ 'ThumbGate converts prior thumbs-downs, workflow policies, and verification expectations into reusable approval rules.',
2081
+ ],
2082
+ sections: [
2083
+ {
2084
+ heading: 'Why approval must happen before execution',
2085
+ paragraphs: [
2086
+ 'Many agent failures are irreversible or expensive by the time a post-run reviewer sees them: force-pushes, destructive SQL, unsafe deploys, leaked secrets, customer-facing messages, and runaway API calls.',
2087
+ 'A pre-action approval gate pauses the action while there is still something to decide. The agent keeps its speed on safe work, but risky work requires proof, policy match, or a human yes.',
2088
+ ],
2089
+ },
2090
+ {
2091
+ heading: 'Three practical gate outcomes',
2092
+ bullets: [
2093
+ 'Block: deny known-bad actions such as force-pushing protected branches or touching secret files.',
2094
+ 'Approve: pause production deploys, schema migrations, payment actions, or customer-facing sends until a human approves.',
2095
+ 'Log: allow lower-risk actions while preserving audit evidence for review and future lessons.',
2096
+ ],
2097
+ },
2098
+ {
2099
+ heading: 'How ThumbGate turns approvals into learning',
2100
+ paragraphs: [
2101
+ 'Every approval, block, and thumbs-down gives the system better operating context. Repeated failures become prevention rules, accepted safe paths become reinforced lessons, and the audit trail gives teams evidence that the boundary fired before execution.',
2102
+ ],
2103
+ },
2104
+ ],
2105
+ faq: [
2106
+ {
2107
+ question: 'What should require a pre-action approval gate?',
2108
+ answer: 'Production deploys, destructive database actions, protected-branch writes, payment or refund actions, customer-facing sends, secret or PII access, high-cost API calls, and any repeated failure pattern the team has already corrected once.',
2109
+ },
2110
+ {
2111
+ question: 'Do approval gates slow every agent action down?',
2112
+ answer: 'No. Good gates are risk-tiered. Safe actions can continue, uncertain actions can be logged, risky actions can pause for approval, and known-bad actions can be blocked.',
2113
+ },
2114
+ {
2115
+ question: 'How does ThumbGate know what to block?',
2116
+ answer: 'ThumbGate uses explicit feedback, learned lessons, policy templates, command and path context, evidence requirements, and prior gate outcomes to decide whether the proposed action should proceed.',
2117
+ },
2118
+ ],
2119
+ relatedPaths: ['/guides/pre-action-checks', '/guides/mcp-tool-governance', '/guides/ai-agent-governance-sprint'],
2120
+ },
1955
2121
  guideBlueprint({
1956
2122
  query: 'autoresearch agent safety',
1957
2123
  path: '/guides/autoresearch-agent-safety',
@@ -2756,7 +2922,7 @@ function renderSeoPageHtml(page, runtimeConfig = {}) {
2756
2922
  <meta property="og:type" content="article" />
2757
2923
  <meta property="og:url" content="${escapeHtml(canonicalUrl)}" />
2758
2924
  <link rel="canonical" href="${escapeHtml(canonicalUrl)}" />
2759
- <link rel="llm-context" href="/public/llm-context.md" type="text/markdown" />
2925
+ <link rel="llm-context" href="/llm-context.md" type="text/markdown" />
2760
2926
  <link rel="icon" type="image/svg+xml" href="/thumbgate-icon.png" />
2761
2927
  <link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg" />
2762
2928
  <meta property="og:image" content="/og.png" />
@@ -255,6 +255,149 @@ function inferTrafficChannel(raw = {}, referrerHost = null) {
255
255
  return 'referral';
256
256
  }
257
257
 
258
+ function normalizeBoolean(value) {
259
+ if (value === true || value === false) return value;
260
+ const text = normalizeText(value, 32);
261
+ if (!text) return false;
262
+ return ['1', 'true', 'yes', 'y', 'bot'].includes(text.toLowerCase());
263
+ }
264
+
265
+ function includesAnyToken(values, tokens) {
266
+ const haystack = values
267
+ .map((value) => normalizeText(value, 512))
268
+ .filter(Boolean)
269
+ .join(' ')
270
+ .toLowerCase();
271
+ if (!haystack) return false;
272
+ return tokens.some((token) => haystack.includes(token));
273
+ }
274
+
275
+ function addAudienceReason(reasons, code) {
276
+ if (!reasons.includes(code)) reasons.push(code);
277
+ }
278
+
279
+ function classifyTelemetryAudience(entry = {}, raw = {}) {
280
+ const reasons = [];
281
+ const identityValues = [
282
+ raw.email,
283
+ raw.customerEmail,
284
+ raw.buyerEmail,
285
+ raw.userEmail,
286
+ entry.visitorId,
287
+ entry.sessionId,
288
+ entry.traceId,
289
+ entry.acquisitionId,
290
+ entry.installId,
291
+ ];
292
+ const attributionValues = [
293
+ entry.source,
294
+ entry.utmSource,
295
+ entry.utmMedium,
296
+ entry.utmCampaign,
297
+ entry.utmContent,
298
+ entry.creator,
299
+ entry.offerCode,
300
+ entry.campaignVariant,
301
+ entry.referrerHost,
302
+ ];
303
+ const eventValues = [
304
+ entry.eventType,
305
+ entry.event,
306
+ entry.page,
307
+ entry.landingPath,
308
+ entry.ctaId,
309
+ entry.reasonCode,
310
+ entry.reasonDetail,
311
+ ];
312
+ const userAgent = normalizeText(entry.userAgent || raw.userAgent, 512) || '';
313
+ const hostValues = [raw.host, raw.hostname, raw.origin, raw.url, raw.pageUrl, entry.referrerHost];
314
+
315
+ if (normalizeBoolean(entry.isBot || raw.isBot) || includesAnyToken([userAgent], [
316
+ 'bot',
317
+ 'crawler',
318
+ 'spider',
319
+ 'headless',
320
+ 'playwright',
321
+ 'puppeteer',
322
+ 'curl/',
323
+ 'wget/',
324
+ 'lighthouse',
325
+ ])) {
326
+ addAudienceReason(reasons, 'bot_or_automation_user_agent');
327
+ }
328
+
329
+ if (includesAnyToken(identityValues, [
330
+ '@example.com',
331
+ 'buyer@example.com',
332
+ 'test@example.com',
333
+ 'codex-verification',
334
+ 'audit@thumbgate.ai',
335
+ ])) {
336
+ addAudienceReason(reasons, 'test_identity');
337
+ }
338
+
339
+ if (includesAnyToken([...attributionValues, ...eventValues], [
340
+ 'codex',
341
+ 'audit',
342
+ 'verification',
343
+ 'synthetic',
344
+ 'smoke',
345
+ 'probe',
346
+ 'test',
347
+ ])) {
348
+ addAudienceReason(reasons, 'test_or_audit_attribution');
349
+ }
350
+
351
+ if (includesAnyToken(hostValues, [
352
+ 'localhost',
353
+ '127.0.0.1',
354
+ '::1',
355
+ 'thumbgate-production.up.railway.app',
356
+ ])) {
357
+ addAudienceReason(reasons, 'internal_host');
358
+ }
359
+
360
+ if (includesAnyToken([userAgent, ...identityValues], [
361
+ 'codex',
362
+ 'github-actions',
363
+ 'node-fetch',
364
+ 'thumbgate-analytics',
365
+ 'thumbgate-verification',
366
+ ])) {
367
+ addAudienceReason(reasons, 'internal_operator_or_ci');
368
+ }
369
+
370
+ if (
371
+ (entry.clientType || entry.client) === 'unknown' &&
372
+ !pickFirstText(entry.visitorId, entry.sessionId, entry.acquisitionId, entry.installId, entry.traceId) &&
373
+ !userAgent &&
374
+ includesAnyToken([entry.source, entry.utmSource], ['direct', 'website'])
375
+ ) {
376
+ addAudienceReason(reasons, 'unknown_direct_no_identity');
377
+ }
378
+
379
+ const audience = reasons.includes('test_identity') || reasons.includes('test_or_audit_attribution')
380
+ ? 'test'
381
+ : (reasons.includes('bot_or_automation_user_agent')
382
+ ? 'bot'
383
+ : (
384
+ reasons.includes('internal_host') ||
385
+ reasons.includes('internal_operator_or_ci') ||
386
+ reasons.includes('unknown_direct_no_identity')
387
+ ? 'internal'
388
+ : 'external'
389
+ ));
390
+
391
+ return {
392
+ audience,
393
+ reasons,
394
+ isExternal: audience === 'external',
395
+ isInternal: audience === 'internal',
396
+ isTest: audience === 'test',
397
+ isBot: audience === 'bot',
398
+ };
399
+ }
400
+
258
401
  function sanitizeTelemetryPayload(payload = {}, headers = {}) {
259
402
  const raw = payload && typeof payload === 'object' ? payload : {};
260
403
  const clientType = inferClientType(raw);
@@ -342,6 +485,15 @@ function sanitizeTelemetryPayload(payload = {}, headers = {}) {
342
485
  ),
343
486
  };
344
487
 
488
+ const audience = classifyTelemetryAudience(entry, raw);
489
+ entry.audience = audience.audience;
490
+ entry.trafficAudience = audience.audience;
491
+ entry.audienceReasons = audience.reasons;
492
+ entry.isExternal = audience.isExternal;
493
+ entry.isInternal = audience.isInternal;
494
+ entry.isTest = audience.isTest;
495
+ entry.isBotTraffic = audience.isBot;
496
+
345
497
  return entry;
346
498
  }
347
499
 
@@ -432,9 +584,162 @@ function summarizeRecentEvents(events) {
432
584
  community: entry.community || null,
433
585
  offerCode: entry.offerCode || null,
434
586
  campaignVariant: entry.campaignVariant || null,
587
+ audience: entry.audience || 'external',
588
+ audienceReasons: entry.audienceReasons || [],
435
589
  }));
436
590
  }
437
591
 
592
+ function summarizeExternalTrafficQuality(events) {
593
+ const byAudience = {};
594
+ const byExclusionReason = {};
595
+ const externalVisitors = new Set();
596
+ const externalSessions = new Set();
597
+ const externalCheckoutStarters = new Set();
598
+ const pageViewsBySource = {};
599
+ const pageViewsByPath = {};
600
+ const pageViewsByTrafficChannel = {};
601
+ const checkoutStartsBySource = {};
602
+ const checkoutStartsByTrafficChannel = {};
603
+ const buyerLossReasons = {};
604
+ const visitorPaths = new Map();
605
+ let externalEvents = 0;
606
+ let excludedEvents = 0;
607
+ let externalWebEvents = 0;
608
+ let externalPageViews = 0;
609
+ let externalCtaClicks = 0;
610
+ let externalCheckoutStarts = 0;
611
+ let externalBuyerLossSignals = 0;
612
+
613
+ for (const entry of events) {
614
+ const audience = entry.audience || 'external';
615
+ incrementCounter(byAudience, audience);
616
+ if (audience !== 'external') {
617
+ excludedEvents += 1;
618
+ const reasons = Array.isArray(entry.audienceReasons) && entry.audienceReasons.length > 0
619
+ ? entry.audienceReasons
620
+ : [`${audience}_traffic`];
621
+ for (const reason of reasons) incrementCounter(byExclusionReason, reason);
622
+ continue;
623
+ }
624
+
625
+ externalEvents += 1;
626
+ if ((entry.clientType || entry.client) !== 'web') continue;
627
+
628
+ externalWebEvents += 1;
629
+ const visitorKey = pickFirstText(entry.visitorId, entry.installId, entry.sessionId);
630
+ if (visitorKey) externalVisitors.add(visitorKey);
631
+ if (entry.sessionId) externalSessions.add(entry.sessionId);
632
+
633
+ const eventType = entry.eventType || entry.event;
634
+ if (visitorKey) {
635
+ const pathRow = visitorPaths.get(visitorKey) || {
636
+ visitorKey,
637
+ firstSeenAt: entry.receivedAt || null,
638
+ lastSeenAt: entry.receivedAt || null,
639
+ source: entry.source || null,
640
+ trafficChannel: entry.trafficChannel || null,
641
+ events: [],
642
+ pages: [],
643
+ checkoutStarts: 0,
644
+ };
645
+ if (!pathRow.firstSeenAt || String(entry.receivedAt || '') < pathRow.firstSeenAt) {
646
+ pathRow.firstSeenAt = entry.receivedAt || null;
647
+ }
648
+ if (!pathRow.lastSeenAt || String(entry.receivedAt || '') > pathRow.lastSeenAt) {
649
+ pathRow.lastSeenAt = entry.receivedAt || null;
650
+ }
651
+ pathRow.events.push({
652
+ at: entry.receivedAt || null,
653
+ eventType,
654
+ page: entry.page || entry.landingPath || null,
655
+ ctaId: entry.ctaId || null,
656
+ });
657
+ if ((entry.page || entry.landingPath) && !pathRow.pages.includes(entry.page || entry.landingPath)) {
658
+ pathRow.pages.push(entry.page || entry.landingPath);
659
+ }
660
+ if (eventType === 'checkout_start' || eventType === 'checkout_bootstrap') {
661
+ pathRow.checkoutStarts += 1;
662
+ }
663
+ visitorPaths.set(visitorKey, pathRow);
664
+ }
665
+
666
+ if (eventType === 'landing_page_view') {
667
+ externalPageViews += 1;
668
+ incrementCounter(pageViewsBySource, entry.source);
669
+ incrementCounter(pageViewsByPath, entry.page);
670
+ incrementCounter(pageViewsByTrafficChannel, entry.trafficChannel);
671
+ }
672
+
673
+ if (isMarketingClickEvent(eventType)) {
674
+ externalCtaClicks += 1;
675
+ }
676
+
677
+ if (eventType === 'checkout_start' || eventType === 'checkout_bootstrap') {
678
+ externalCheckoutStarts += 1;
679
+ incrementCounter(checkoutStartsBySource, entry.source);
680
+ incrementCounter(checkoutStartsByTrafficChannel, entry.trafficChannel);
681
+ const starterKey = pickFirstText(
682
+ entry.acquisitionId,
683
+ entry.visitorId,
684
+ entry.sessionId,
685
+ entry.installId,
686
+ entry.traceId
687
+ );
688
+ if (starterKey) externalCheckoutStarters.add(starterKey);
689
+ }
690
+
691
+ if (eventType === 'checkout_cancelled' || eventType === 'checkout_abandoned' || eventType === 'reason_not_buying') {
692
+ externalBuyerLossSignals += 1;
693
+ incrementCounter(buyerLossReasons, entry.reasonCode);
694
+ }
695
+ }
696
+
697
+ const topPath = getTopCounterEntry(pageViewsByPath);
698
+ const topSource = getTopCounterEntry(pageViewsBySource);
699
+ const topTrafficChannel = getTopCounterEntry(pageViewsByTrafficChannel);
700
+ const topExclusionReason = getTopCounterEntry(byExclusionReason);
701
+
702
+ return {
703
+ rawEvents: events.length,
704
+ externalEvents,
705
+ excludedEvents,
706
+ internalEvents: byAudience.internal || 0,
707
+ testEvents: byAudience.test || 0,
708
+ botEvents: byAudience.bot || 0,
709
+ exclusionRate: safeRate(excludedEvents, events.length),
710
+ byAudience,
711
+ byExclusionReason,
712
+ topExclusionReason: topExclusionReason ? { key: topExclusionReason[0], count: topExclusionReason[1] } : null,
713
+ external: {
714
+ totalEvents: externalWebEvents,
715
+ uniqueVisitors: externalVisitors.size,
716
+ uniqueSessions: externalSessions.size,
717
+ uniqueCheckoutStarters: externalCheckoutStarters.size,
718
+ pageViews: externalPageViews,
719
+ ctaClicks: externalCtaClicks,
720
+ checkoutStarts: externalCheckoutStarts,
721
+ buyerLossSignals: externalBuyerLossSignals,
722
+ pageViewToCheckoutRate: safeRate(externalCheckoutStarts, externalPageViews),
723
+ visitorToCheckoutRate: safeRate(externalCheckoutStarts, externalVisitors.size),
724
+ bySource: pageViewsBySource,
725
+ byPath: pageViewsByPath,
726
+ byTrafficChannel: pageViewsByTrafficChannel,
727
+ checkoutStartsBySource,
728
+ checkoutStartsByTrafficChannel,
729
+ buyerLossReasons,
730
+ topSource: topSource ? { key: topSource[0], count: topSource[1] } : null,
731
+ topPath: topPath ? { key: topPath[0], count: topPath[1] } : null,
732
+ topTrafficChannel: topTrafficChannel ? { key: topTrafficChannel[0], count: topTrafficChannel[1] } : null,
733
+ visitorPaths: Array.from(visitorPaths.values())
734
+ .sort((a, b) => String(a.firstSeenAt || '').localeCompare(String(b.firstSeenAt || '')))
735
+ .slice(0, 50),
736
+ },
737
+ verdict: externalEvents > 0
738
+ ? (excludedEvents > externalEvents ? 'polluted_but_usable' : 'usable')
739
+ : (events.length > 0 ? 'polluted_no_external_signal' : 'missing'),
740
+ };
741
+ }
742
+
438
743
  function getTelemetrySummary(feedbackDir, options = {}) {
439
744
  const analyticsWindow = resolveAnalyticsWindow(options);
440
745
  const telemetryLoadOptions = analyticsWindow.bounded
@@ -445,6 +750,7 @@ function getTelemetrySummary(feedbackDir, options = {}) {
445
750
  analyticsWindow,
446
751
  (entry) => entry && (entry.receivedAt || entry.timestamp)
447
752
  );
753
+ const trafficQuality = summarizeExternalTrafficQuality(events);
448
754
  const byClientType = {};
449
755
  const byEventType = {};
450
756
  const webVisitors = new Set();
@@ -788,6 +1094,7 @@ function getTelemetrySummary(feedbackDir, options = {}) {
788
1094
  window: serializeAnalyticsWindow(analyticsWindow),
789
1095
  totalEvents: events.length,
790
1096
  latestSeenAt,
1097
+ trafficQuality,
791
1098
  byClientType,
792
1099
  byEventType,
793
1100
  conversionFunnel: {
@@ -963,6 +1270,8 @@ function getTelemetryAnalytics(feedbackDir, options = {}) {
963
1270
  window: summary.window,
964
1271
  totalEvents: summary.totalEvents,
965
1272
  latestSeenAt: summary.latestSeenAt,
1273
+ trafficQuality: summary.trafficQuality,
1274
+ qualified: summary.trafficQuality.external,
966
1275
  byClientType: summary.byClientType,
967
1276
  byEventType: summary.byEventType,
968
1277
  conversionFunnel: summary.conversionFunnel,
@@ -1142,6 +1451,7 @@ const appendTelemetryPing = appendTelemetryEvent;
1142
1451
  module.exports = {
1143
1452
  TELEMETRY_FILE_NAME,
1144
1453
  sanitizeTelemetryPayload,
1454
+ classifyTelemetryAudience,
1145
1455
  appendTelemetryPing,
1146
1456
  appendTelemetryEvent,
1147
1457
  getTelemetrySourceDiagnostics,