thumbgate 1.23.1 → 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.
- package/.claude-plugin/marketplace.json +5 -5
- package/.claude-plugin/plugin.json +2 -2
- package/.well-known/llms.txt +26 -11
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +69 -34
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +39 -16
- package/bin/postinstall.js +11 -22
- package/config/gate-templates.json +72 -0
- package/config/github-about.json +1 -1
- package/config/post-deploy-marketing-pages.json +6 -1
- package/package.json +5 -5
- package/public/agent-manager.html +3 -3
- package/public/agents-cost-savings.html +3 -3
- package/public/ai-malpractice-prevention.html +278 -7
- package/public/blog.html +3 -3
- package/public/codex-enterprise.html +3 -3
- package/public/codex-plugin.html +4 -4
- package/public/compare.html +6 -6
- package/public/dashboard.html +211 -126
- package/public/guide.html +5 -5
- package/public/index.html +156 -47
- package/public/learn.html +24 -10
- package/public/lessons.html +2 -2
- package/public/numbers.html +6 -6
- package/public/pricing.html +6 -5
- package/public/pro.html +1 -0
- package/scripts/billing.js +17 -0
- package/scripts/commercial-offer.js +4 -1
- package/scripts/dashboard.js +53 -1
- package/scripts/gates-engine.js +3 -3
- package/scripts/plausible-server-events.js +2 -1
- package/scripts/rate-limiter.js +16 -12
- package/scripts/seo-gsd.js +167 -1
- package/scripts/telemetry-analytics.js +310 -0
- package/scripts/visitor-journey.js +172 -0
- package/src/api/server.js +65 -29
- package/adapters/chatgpt/openapi.yaml +0 -1705
|
@@ -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,
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const STAGE_ORDER = [
|
|
4
|
+
'landing',
|
|
5
|
+
'pricing',
|
|
6
|
+
'checkout_viewed',
|
|
7
|
+
'email_submitted',
|
|
8
|
+
'stripe_redirect',
|
|
9
|
+
'purchase',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function parseTimestamp(row) {
|
|
13
|
+
const raw = row && (row.timestamp || row.receivedAt || row.ts);
|
|
14
|
+
const ms = raw ? Date.parse(raw) : NaN;
|
|
15
|
+
return Number.isFinite(ms) ? ms : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pickText(...values) {
|
|
19
|
+
for (const value of values) {
|
|
20
|
+
const text = String(value || '').trim();
|
|
21
|
+
if (text) return text;
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractSessionId(row) {
|
|
27
|
+
return pickText(
|
|
28
|
+
row.sessionId,
|
|
29
|
+
row.visitorSessionId,
|
|
30
|
+
row.visitor_session_id,
|
|
31
|
+
row.attribution && row.attribution.sessionId,
|
|
32
|
+
row.metadata && row.metadata.sessionId,
|
|
33
|
+
row.stripeSessionId,
|
|
34
|
+
row.traceId,
|
|
35
|
+
row.acquisitionId,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractVisitorId(row) {
|
|
40
|
+
return pickText(
|
|
41
|
+
row.visitorId,
|
|
42
|
+
row.visitor_id,
|
|
43
|
+
row.installId,
|
|
44
|
+
row.attribution && row.attribution.visitorId,
|
|
45
|
+
row.metadata && row.metadata.visitorId,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractPath(row) {
|
|
50
|
+
return pickText(
|
|
51
|
+
row.path,
|
|
52
|
+
row.page,
|
|
53
|
+
row.landingPath,
|
|
54
|
+
row.landing_path,
|
|
55
|
+
row.attribution && row.attribution.landingPath,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function classifyStage(row) {
|
|
60
|
+
const event = String(row.eventType || row.event || '').toLowerCase();
|
|
61
|
+
const stage = String(row.stage || '').toLowerCase();
|
|
62
|
+
const path = extractPath(row).toLowerCase();
|
|
63
|
+
const cta = String(row.ctaId || row.cta_id || '').toLowerCase();
|
|
64
|
+
|
|
65
|
+
if (/purchase|paid|checkout\.session\.completed/.test(event) || stage === 'purchase') return 'purchase';
|
|
66
|
+
if (/stripe_redirect|stripe redirect|redirect started/.test(event) || stage === 'stripe_redirect') return 'stripe_redirect';
|
|
67
|
+
if (/email_submitted|email submitted|checkout_interstitial_cta_clicked/.test(event)) return 'email_submitted';
|
|
68
|
+
if (/checkout/.test(path) || /checkout.*view|checkout pro viewed/.test(event)) return 'checkout_viewed';
|
|
69
|
+
if (/pricing/.test(path) || /pricing/.test(cta) || /pricing_cta/.test(event)) return 'pricing';
|
|
70
|
+
if (/landing|page_view|landing_page_view|discovery/.test(event) || stage === 'discovery') return 'landing';
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stageRank(stage) {
|
|
75
|
+
const index = STAGE_ORDER.indexOf(stage);
|
|
76
|
+
return index === -1 ? -1 : index;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function nextMissingStage(maxStage) {
|
|
80
|
+
const index = stageRank(maxStage);
|
|
81
|
+
if (index < 0) return 'unknown';
|
|
82
|
+
if (maxStage === 'purchase') return 'converted';
|
|
83
|
+
return STAGE_ORDER[index + 1] || 'unknown';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildVisitorJourneySummary({ telemetryRows = [], funnelRows = [], limit = 100 } = {}) {
|
|
87
|
+
const sessions = new Map();
|
|
88
|
+
const stageCounts = Object.fromEntries(STAGE_ORDER.map((stage) => [stage, 0]));
|
|
89
|
+
|
|
90
|
+
function ingest(row, source) {
|
|
91
|
+
const ts = parseTimestamp(row);
|
|
92
|
+
if (ts == null) return;
|
|
93
|
+
const sessionId = extractSessionId(row) || `anonymous:${extractVisitorId(row) || ts}`;
|
|
94
|
+
const visitorId = extractVisitorId(row) || null;
|
|
95
|
+
const stage = classifyStage(row);
|
|
96
|
+
const path = extractPath(row);
|
|
97
|
+
const existing = sessions.get(sessionId) || {
|
|
98
|
+
sessionId,
|
|
99
|
+
visitorId,
|
|
100
|
+
firstSeen: new Date(ts).toISOString(),
|
|
101
|
+
lastSeen: new Date(ts).toISOString(),
|
|
102
|
+
maxStage: null,
|
|
103
|
+
dropoffStage: 'unknown',
|
|
104
|
+
events: [],
|
|
105
|
+
paths: [],
|
|
106
|
+
ctas: [],
|
|
107
|
+
sources: [],
|
|
108
|
+
visitorType: row.visitorType || null,
|
|
109
|
+
utm: {},
|
|
110
|
+
referrerHost: pickText(row.referrerHost, row.referrer_host, row.referrer),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
existing.visitorId = existing.visitorId || visitorId;
|
|
114
|
+
existing.firstSeen = new Date(Math.min(Date.parse(existing.firstSeen), ts)).toISOString();
|
|
115
|
+
existing.lastSeen = new Date(Math.max(Date.parse(existing.lastSeen), ts)).toISOString();
|
|
116
|
+
existing.sources.push(source);
|
|
117
|
+
if (stage) {
|
|
118
|
+
stageCounts[stage] += 1;
|
|
119
|
+
if (stageRank(stage) > stageRank(existing.maxStage)) existing.maxStage = stage;
|
|
120
|
+
}
|
|
121
|
+
if (path && !existing.paths.includes(path)) existing.paths.push(path);
|
|
122
|
+
const cta = pickText(row.ctaId, row.cta_id);
|
|
123
|
+
if (cta && !existing.ctas.includes(cta)) existing.ctas.push(cta);
|
|
124
|
+
for (const key of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']) {
|
|
125
|
+
const value = pickText(row[key], row.attribution && row.attribution[key]);
|
|
126
|
+
if (value && !existing.utm[key]) existing.utm[key] = value;
|
|
127
|
+
}
|
|
128
|
+
existing.events.push({
|
|
129
|
+
timestamp: new Date(ts).toISOString(),
|
|
130
|
+
source,
|
|
131
|
+
eventType: pickText(row.eventType, row.event, row.stage, 'unknown'),
|
|
132
|
+
stage,
|
|
133
|
+
path: path || null,
|
|
134
|
+
ctaId: cta || null,
|
|
135
|
+
});
|
|
136
|
+
sessions.set(sessionId, existing);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const row of telemetryRows) ingest(row, 'telemetry');
|
|
140
|
+
for (const row of funnelRows) ingest(row, 'funnel');
|
|
141
|
+
|
|
142
|
+
const journeys = [...sessions.values()].map((session) => {
|
|
143
|
+
session.sources = [...new Set(session.sources)];
|
|
144
|
+
session.events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
145
|
+
session.maxStage = session.maxStage || 'unknown';
|
|
146
|
+
session.dropoffStage = nextMissingStage(session.maxStage);
|
|
147
|
+
session.eventCount = session.events.length;
|
|
148
|
+
return session;
|
|
149
|
+
}).sort((a, b) => Date.parse(b.lastSeen) - Date.parse(a.lastSeen));
|
|
150
|
+
|
|
151
|
+
const dropoffCounts = {};
|
|
152
|
+
for (const journey of journeys) {
|
|
153
|
+
dropoffCounts[journey.dropoffStage] = (dropoffCounts[journey.dropoffStage] || 0) + 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
generatedAt: new Date().toISOString(),
|
|
158
|
+
sessionCount: journeys.length,
|
|
159
|
+
stageCounts,
|
|
160
|
+
dropoffCounts,
|
|
161
|
+
journeys: journeys.slice(0, Math.max(1, Math.min(Number(limit) || 100, 500))),
|
|
162
|
+
truncated: journeys.length > Math.max(1, Math.min(Number(limit) || 100, 500)),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
STAGE_ORDER,
|
|
168
|
+
buildVisitorJourneySummary,
|
|
169
|
+
classifyStage,
|
|
170
|
+
extractSessionId,
|
|
171
|
+
extractVisitorId,
|
|
172
|
+
};
|