thumbgate 1.4.5 → 1.5.0
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +14 -3
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/package.json +6 -2
- package/public/compare.html +310 -0
- package/public/guide.html +317 -0
- package/public/index.html +1331 -0
- package/public/lessons.html +989 -0
- package/scripts/billing.js +8 -2
- package/scripts/commercial-offer.js +1 -1
- package/scripts/telemetry-analytics.js +41 -1
- package/src/api/server.js +282 -0
package/scripts/billing.js
CHANGED
|
@@ -2292,8 +2292,14 @@ async function handleWebhook(rawBody, signature) {
|
|
|
2292
2292
|
if (LOCAL_MODE()) return { handled: false, reason: 'local_mode' };
|
|
2293
2293
|
let event;
|
|
2294
2294
|
try {
|
|
2295
|
-
|
|
2296
|
-
|
|
2295
|
+
if (CONFIG.STRIPE_WEBHOOK_SECRET) {
|
|
2296
|
+
const stripe = getStripeClient();
|
|
2297
|
+
event = stripe.webhooks.constructEvent(rawBody, signature, CONFIG.STRIPE_WEBHOOK_SECRET);
|
|
2298
|
+
} else {
|
|
2299
|
+
// No webhook secret configured — signature was already checked by verifyWebhookSignature
|
|
2300
|
+
// (which is also lenient when no secret). Parse the raw body directly.
|
|
2301
|
+
event = JSON.parse(rawBody.toString('utf-8'));
|
|
2302
|
+
}
|
|
2297
2303
|
} catch (err) {
|
|
2298
2304
|
return { handled: false, reason: 'invalid_signature', error: err.message };
|
|
2299
2305
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const PRO_MONTHLY_PAYMENT_LINK = 'https://buy.stripe.com/
|
|
3
|
+
const PRO_MONTHLY_PAYMENT_LINK = 'https://buy.stripe.com/7sYcN5bmIf5IcSd8qf3sI0a';
|
|
4
4
|
const PRO_ANNUAL_PAYMENT_LINK = 'https://buy.stripe.com/3cI8wPfCYaPs2dzdKz3sI07';
|
|
5
5
|
|
|
6
6
|
const PRO_MONTHLY_PRICE_ID = 'price_1THQY7GGBpd520QYHoS7RG0J';
|
|
@@ -15,6 +15,21 @@ const {
|
|
|
15
15
|
} = require('./feedback-paths');
|
|
16
16
|
|
|
17
17
|
const TELEMETRY_FILE_NAME = 'telemetry-pings.jsonl';
|
|
18
|
+
const MARKETING_CLICK_EVENT_TYPES = new Set([
|
|
19
|
+
'cta_click',
|
|
20
|
+
'checkout_start',
|
|
21
|
+
'checkout_bootstrap',
|
|
22
|
+
'chatgpt_gpt_open',
|
|
23
|
+
'chatgpt_gpt_click',
|
|
24
|
+
'install_guide_click',
|
|
25
|
+
'install_click',
|
|
26
|
+
'pro_page_click',
|
|
27
|
+
'pro_checkout_start',
|
|
28
|
+
'workflow_sprint_intake_click',
|
|
29
|
+
'demo_click',
|
|
30
|
+
'github_repo_click',
|
|
31
|
+
'community_landing_redirect',
|
|
32
|
+
]);
|
|
18
33
|
|
|
19
34
|
function shouldIncludeLegacyTelemetry() {
|
|
20
35
|
if (
|
|
@@ -64,6 +79,10 @@ function safeRate(num, den) {
|
|
|
64
79
|
return Number((num / den).toFixed(4));
|
|
65
80
|
}
|
|
66
81
|
|
|
82
|
+
function isMarketingClickEvent(eventType) {
|
|
83
|
+
return MARKETING_CLICK_EVENT_TYPES.has(String(eventType || '').toLowerCase());
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
function getTelemetryPath(feedbackDir) {
|
|
68
87
|
return path.join(feedbackDir, TELEMETRY_FILE_NAME);
|
|
69
88
|
}
|
|
@@ -288,6 +307,8 @@ function sanitizeTelemetryPayload(payload = {}, headers = {}) {
|
|
|
288
307
|
ctaId: pickFirstText(raw.ctaId),
|
|
289
308
|
ctaPlacement: pickFirstText(raw.ctaPlacement),
|
|
290
309
|
planId: pickFirstText(raw.planId),
|
|
310
|
+
linkSlug: pickFirstText(raw.linkSlug, raw.destinationSlug),
|
|
311
|
+
destinationPath: pickFirstText(raw.destinationPath),
|
|
291
312
|
pipelineStatus: pickFirstText(raw.pipelineStatus, raw.workflowSprintStatus, raw.status),
|
|
292
313
|
reasonCode,
|
|
293
314
|
reasonDetail: pickFirstText(raw.reasonDetail, raw.reasonText, raw.otherReason, raw.notes),
|
|
@@ -365,6 +386,7 @@ function summarizeRecentEvents(events) {
|
|
|
365
386
|
utmCampaign: entry.utmCampaign || null,
|
|
366
387
|
ctaId: entry.ctaId || null,
|
|
367
388
|
page: entry.page || null,
|
|
389
|
+
linkSlug: entry.linkSlug || null,
|
|
368
390
|
reasonCode: entry.reasonCode || null,
|
|
369
391
|
creator: entry.creator || null,
|
|
370
392
|
community: entry.community || null,
|
|
@@ -474,7 +496,7 @@ function getTelemetrySummary(feedbackDir, options = {}) {
|
|
|
474
496
|
if (entry.attributionTagged) attributedPageViews += 1;
|
|
475
497
|
}
|
|
476
498
|
|
|
477
|
-
if ((entry.eventType || entry.event)
|
|
499
|
+
if (isMarketingClickEvent(entry.eventType || entry.event)) {
|
|
478
500
|
ctaClicks += 1;
|
|
479
501
|
incrementCounter(ctaClicksBySource, entry.source);
|
|
480
502
|
incrementCounter(ctaClicksByCampaign, entry.utmCampaign);
|
|
@@ -601,6 +623,10 @@ function getTelemetrySummary(feedbackDir, options = {}) {
|
|
|
601
623
|
pageViewsByTrafficChannel[channelKey] || 0
|
|
602
624
|
);
|
|
603
625
|
}
|
|
626
|
+
const installCopies = byEventType.install_copy || 0;
|
|
627
|
+
const gptOpens = (byEventType.chatgpt_gpt_open || 0) + (byEventType.chatgpt_gpt_click || 0);
|
|
628
|
+
const trialEmails = byEventType.trial_email_captured || 0;
|
|
629
|
+
const proConversions = checkoutPaidConfirmations;
|
|
604
630
|
|
|
605
631
|
return {
|
|
606
632
|
window: serializeAnalyticsWindow(analyticsWindow),
|
|
@@ -608,6 +634,19 @@ function getTelemetrySummary(feedbackDir, options = {}) {
|
|
|
608
634
|
latestSeenAt,
|
|
609
635
|
byClientType,
|
|
610
636
|
byEventType,
|
|
637
|
+
conversionFunnel: {
|
|
638
|
+
landingViews: pageViews,
|
|
639
|
+
installCopies,
|
|
640
|
+
gptOpens,
|
|
641
|
+
checkoutStarts,
|
|
642
|
+
trialEmails,
|
|
643
|
+
proConversions,
|
|
644
|
+
landingToInstallCopyRate: safeRate(installCopies, pageViews),
|
|
645
|
+
landingToGptOpenRate: safeRate(gptOpens, pageViews),
|
|
646
|
+
landingToCheckoutRate: safeRate(checkoutStarts, pageViews),
|
|
647
|
+
checkoutToTrialEmailRate: safeRate(trialEmails, checkoutStarts),
|
|
648
|
+
checkoutToProConversionRate: safeRate(proConversions, checkoutStarts),
|
|
649
|
+
},
|
|
611
650
|
web: {
|
|
612
651
|
totalEvents: webEvents,
|
|
613
652
|
uniqueVisitors: webVisitors.size,
|
|
@@ -712,6 +751,7 @@ function getTelemetryAnalytics(feedbackDir, options = {}) {
|
|
|
712
751
|
latestSeenAt: summary.latestSeenAt,
|
|
713
752
|
byClientType: summary.byClientType,
|
|
714
753
|
byEventType: summary.byEventType,
|
|
754
|
+
conversionFunnel: summary.conversionFunnel,
|
|
715
755
|
visitors: {
|
|
716
756
|
totalEvents: summary.web.totalEvents,
|
|
717
757
|
uniqueVisitors: summary.web.uniqueVisitors,
|
package/src/api/server.js
CHANGED
|
@@ -227,6 +227,123 @@ const BUILD_METADATA = resolveBuildMetadata();
|
|
|
227
227
|
const TERMINAL_JOB_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
228
228
|
const IDLE_JOB_STATUSES = new Set(['queued', 'paused', 'resume_requested']);
|
|
229
229
|
const JOB_CONTROL_ACTIONS = new Set(['pause', 'cancel', 'resume']);
|
|
230
|
+
const TRACKED_LINK_QUERY_KEYS = [
|
|
231
|
+
'utm_source',
|
|
232
|
+
'utm_medium',
|
|
233
|
+
'utm_campaign',
|
|
234
|
+
'utm_content',
|
|
235
|
+
'utm_term',
|
|
236
|
+
'source',
|
|
237
|
+
'creator',
|
|
238
|
+
'creator_handle',
|
|
239
|
+
'community',
|
|
240
|
+
'subreddit',
|
|
241
|
+
'post_id',
|
|
242
|
+
'comment_id',
|
|
243
|
+
'campaign_variant',
|
|
244
|
+
'offer_code',
|
|
245
|
+
'acquisition_id',
|
|
246
|
+
'visitor_id',
|
|
247
|
+
'session_id',
|
|
248
|
+
'visitor_session_id',
|
|
249
|
+
'install_id',
|
|
250
|
+
'trace_id',
|
|
251
|
+
'cta_id',
|
|
252
|
+
'cta_placement',
|
|
253
|
+
'plan_id',
|
|
254
|
+
'billing_cycle',
|
|
255
|
+
'seat_count',
|
|
256
|
+
'landing_path',
|
|
257
|
+
'referrer_host',
|
|
258
|
+
];
|
|
259
|
+
const TRACKED_LINK_TARGETS = Object.freeze({
|
|
260
|
+
gpt: {
|
|
261
|
+
href: 'https://chatgpt.com/g/g-69dcfd1cd5f881918ae31874631d6f08-thumbgate',
|
|
262
|
+
external: true,
|
|
263
|
+
ctaId: 'go_gpt',
|
|
264
|
+
ctaPlacement: 'link_router',
|
|
265
|
+
eventType: 'chatgpt_gpt_open',
|
|
266
|
+
defaults: {
|
|
267
|
+
utm_source: 'website',
|
|
268
|
+
utm_medium: 'link_router',
|
|
269
|
+
utm_campaign: 'chatgpt_gpt',
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
pro: {
|
|
273
|
+
path: '/checkout/pro',
|
|
274
|
+
ctaId: 'go_pro',
|
|
275
|
+
ctaPlacement: 'link_router',
|
|
276
|
+
eventType: 'cta_click',
|
|
277
|
+
defaults: {
|
|
278
|
+
utm_source: 'website',
|
|
279
|
+
utm_medium: 'link_router',
|
|
280
|
+
utm_campaign: 'pro_upgrade',
|
|
281
|
+
plan_id: 'pro',
|
|
282
|
+
billing_cycle: 'monthly',
|
|
283
|
+
},
|
|
284
|
+
allowCustomerEmail: true,
|
|
285
|
+
},
|
|
286
|
+
install: {
|
|
287
|
+
path: '/guide',
|
|
288
|
+
ctaId: 'go_install',
|
|
289
|
+
ctaPlacement: 'link_router',
|
|
290
|
+
eventType: 'install_guide_click',
|
|
291
|
+
defaults: {
|
|
292
|
+
utm_source: 'website',
|
|
293
|
+
utm_medium: 'link_router',
|
|
294
|
+
utm_campaign: 'install_free',
|
|
295
|
+
plan_id: 'free',
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
reddit: {
|
|
299
|
+
path: '/',
|
|
300
|
+
ctaId: 'go_reddit',
|
|
301
|
+
ctaPlacement: 'link_router',
|
|
302
|
+
eventType: 'community_landing_redirect',
|
|
303
|
+
defaults: {
|
|
304
|
+
utm_source: 'reddit',
|
|
305
|
+
utm_medium: 'organic_social',
|
|
306
|
+
utm_campaign: 'first_party_redirect',
|
|
307
|
+
campaign_variant: 'reddit_shortlink',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
linkedin: {
|
|
311
|
+
path: '/',
|
|
312
|
+
ctaId: 'go_linkedin',
|
|
313
|
+
ctaPlacement: 'link_router',
|
|
314
|
+
eventType: 'community_landing_redirect',
|
|
315
|
+
defaults: {
|
|
316
|
+
utm_source: 'linkedin',
|
|
317
|
+
utm_medium: 'organic_social',
|
|
318
|
+
utm_campaign: 'first_party_redirect',
|
|
319
|
+
campaign_variant: 'linkedin_shortlink',
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
x: {
|
|
323
|
+
path: '/',
|
|
324
|
+
ctaId: 'go_x',
|
|
325
|
+
ctaPlacement: 'link_router',
|
|
326
|
+
eventType: 'community_landing_redirect',
|
|
327
|
+
defaults: {
|
|
328
|
+
utm_source: 'x',
|
|
329
|
+
utm_medium: 'organic_social',
|
|
330
|
+
utm_campaign: 'first_party_redirect',
|
|
331
|
+
campaign_variant: 'x_shortlink',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
github: {
|
|
335
|
+
href: 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
336
|
+
external: true,
|
|
337
|
+
ctaId: 'go_github',
|
|
338
|
+
ctaPlacement: 'link_router',
|
|
339
|
+
eventType: 'github_repo_click',
|
|
340
|
+
defaults: {
|
|
341
|
+
utm_source: 'website',
|
|
342
|
+
utm_medium: 'link_router',
|
|
343
|
+
utm_campaign: 'github_repo',
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
});
|
|
230
347
|
|
|
231
348
|
// ---------------------------------------------------------------------------
|
|
232
349
|
// Stripe event tracking helpers
|
|
@@ -841,6 +958,130 @@ function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneySt
|
|
|
841
958
|
};
|
|
842
959
|
}
|
|
843
960
|
|
|
961
|
+
function normalizeTrackedLinkSlug(value) {
|
|
962
|
+
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function getTrackedLinkTarget(slug) {
|
|
966
|
+
const normalizedSlug = normalizeTrackedLinkSlug(slug);
|
|
967
|
+
return TRACKED_LINK_TARGETS[normalizedSlug]
|
|
968
|
+
? { slug: normalizedSlug, ...TRACKED_LINK_TARGETS[normalizedSlug] }
|
|
969
|
+
: null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
|
|
973
|
+
const params = parsed.searchParams;
|
|
974
|
+
for (const [key, value] of Object.entries(target.defaults || {})) {
|
|
975
|
+
if (!destinationUrl.searchParams.has(key)) {
|
|
976
|
+
appendQueryParam(destinationUrl, key, value);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
for (const key of TRACKED_LINK_QUERY_KEYS) {
|
|
980
|
+
const value = params.get(key);
|
|
981
|
+
if (value && value.trim()) {
|
|
982
|
+
destinationUrl.searchParams.set(key, value.trim());
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (target.allowCustomerEmail) {
|
|
986
|
+
appendQueryParam(destinationUrl, 'customer_email', params.get('customer_email'));
|
|
987
|
+
}
|
|
988
|
+
if (!destinationUrl.searchParams.has('cta_id')) {
|
|
989
|
+
appendQueryParam(destinationUrl, 'cta_id', target.ctaId);
|
|
990
|
+
}
|
|
991
|
+
if (!destinationUrl.searchParams.has('cta_placement')) {
|
|
992
|
+
appendQueryParam(destinationUrl, 'cta_placement', target.ctaPlacement);
|
|
993
|
+
}
|
|
994
|
+
if (!destinationUrl.searchParams.has('landing_path')) {
|
|
995
|
+
appendQueryParam(destinationUrl, 'landing_path', `/go/${target.slug}`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function buildTrackedLinkDestination(target, hostedConfig, parsed) {
|
|
1000
|
+
const destinationUrl = target.href
|
|
1001
|
+
? new URL(target.href)
|
|
1002
|
+
: new URL(target.path || '/', hostedConfig.appOrigin);
|
|
1003
|
+
appendTrackedLinkQueryParams(destinationUrl, parsed, target);
|
|
1004
|
+
return destinationUrl;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function buildTrackedLinkAttribution(target, parsed, req, journeyState, destinationUrl) {
|
|
1008
|
+
const params = parsed.searchParams;
|
|
1009
|
+
const referrer = pickFirstText(params.get('referrer'), req.headers.referer, req.headers.referrer);
|
|
1010
|
+
const referrerHost = pickFirstText(params.get('referrer_host'), parseReferrerHost(referrer));
|
|
1011
|
+
const source = pickFirstText(
|
|
1012
|
+
params.get('source'),
|
|
1013
|
+
params.get('utm_source'),
|
|
1014
|
+
target.defaults && target.defaults.utm_source,
|
|
1015
|
+
inferSource(referrerHost)
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
return {
|
|
1019
|
+
eventType: target.eventType || 'cta_click',
|
|
1020
|
+
clientType: 'web',
|
|
1021
|
+
acquisitionId: journeyState.acquisitionId,
|
|
1022
|
+
visitorId: journeyState.visitorId,
|
|
1023
|
+
sessionId: journeyState.sessionId,
|
|
1024
|
+
installId: pickFirstText(params.get('install_id')),
|
|
1025
|
+
traceId: pickFirstText(params.get('trace_id')),
|
|
1026
|
+
source,
|
|
1027
|
+
utmSource: pickFirstText(params.get('utm_source'), source),
|
|
1028
|
+
utmMedium: pickFirstText(params.get('utm_medium'), target.defaults && target.defaults.utm_medium, 'link_router'),
|
|
1029
|
+
utmCampaign: pickFirstText(params.get('utm_campaign'), target.defaults && target.defaults.utm_campaign, 'first_party_redirect'),
|
|
1030
|
+
utmContent: pickFirstText(params.get('utm_content')),
|
|
1031
|
+
utmTerm: pickFirstText(params.get('utm_term')),
|
|
1032
|
+
creator: pickFirstText(params.get('creator'), params.get('creator_handle')),
|
|
1033
|
+
community: pickFirstText(params.get('community'), params.get('subreddit')),
|
|
1034
|
+
postId: pickFirstText(params.get('post_id')),
|
|
1035
|
+
commentId: pickFirstText(params.get('comment_id')),
|
|
1036
|
+
campaignVariant: pickFirstText(params.get('campaign_variant'), target.defaults && target.defaults.campaign_variant),
|
|
1037
|
+
offerCode: pickFirstText(params.get('offer_code')),
|
|
1038
|
+
ctaId: pickFirstText(params.get('cta_id'), target.ctaId),
|
|
1039
|
+
ctaPlacement: pickFirstText(params.get('cta_placement'), target.ctaPlacement),
|
|
1040
|
+
planId: pickFirstText(params.get('plan_id'), target.defaults && target.defaults.plan_id),
|
|
1041
|
+
landingPath: pickFirstText(params.get('landing_path'), `/go/${target.slug}`),
|
|
1042
|
+
page: `/go/${target.slug}`,
|
|
1043
|
+
referrer,
|
|
1044
|
+
referrerHost,
|
|
1045
|
+
destinationSlug: target.slug,
|
|
1046
|
+
destinationPath: target.external ? destinationUrl.host : destinationUrl.pathname,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadRequest, slug }) {
|
|
1051
|
+
const target = getTrackedLinkTarget(slug);
|
|
1052
|
+
if (!target) {
|
|
1053
|
+
sendJson(res, 404, {
|
|
1054
|
+
error: 'Tracked link not found',
|
|
1055
|
+
allowed: Object.keys(TRACKED_LINK_TARGETS),
|
|
1056
|
+
}, {}, {
|
|
1057
|
+
headOnly: isHeadRequest,
|
|
1058
|
+
});
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
1063
|
+
const journeyState = resolveJourneyState(req, parsed);
|
|
1064
|
+
const destinationUrl = buildTrackedLinkDestination(target, hostedConfig, parsed);
|
|
1065
|
+
if (!isHeadRequest) {
|
|
1066
|
+
appendBestEffortTelemetry(
|
|
1067
|
+
FEEDBACK_DIR,
|
|
1068
|
+
buildTrackedLinkAttribution(target, parsed, req, journeyState, destinationUrl),
|
|
1069
|
+
req.headers,
|
|
1070
|
+
`tracked_link_redirect:${target.slug}`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
res.writeHead(302, {
|
|
1075
|
+
...(journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}),
|
|
1076
|
+
'Cache-Control': 'no-store',
|
|
1077
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
1078
|
+
'X-Robots-Tag': 'noindex,nofollow',
|
|
1079
|
+
'X-ThumbGate-Link-Slug': target.slug,
|
|
1080
|
+
Location: destinationUrl.toString(),
|
|
1081
|
+
});
|
|
1082
|
+
res.end();
|
|
1083
|
+
}
|
|
1084
|
+
|
|
844
1085
|
function resolveCheckoutOfferSummary(metadata = {}) {
|
|
845
1086
|
const planId = normalizePlanId(metadata.planId);
|
|
846
1087
|
const billingCycle = normalizeBillingCycle(metadata.billingCycle);
|
|
@@ -2697,6 +2938,34 @@ function createApiServer() {
|
|
|
2697
2938
|
attribution,
|
|
2698
2939
|
}) + '\n');
|
|
2699
2940
|
}
|
|
2941
|
+
const journeyState = resolveJourneyState(req, parsed);
|
|
2942
|
+
appendBestEffortTelemetry(getFeedbackPaths().FEEDBACK_DIR, {
|
|
2943
|
+
eventType: 'trial_email_captured',
|
|
2944
|
+
clientType: 'web',
|
|
2945
|
+
acquisitionId: journeyState.acquisitionId,
|
|
2946
|
+
visitorId: journeyState.visitorId,
|
|
2947
|
+
sessionId: journeyState.sessionId,
|
|
2948
|
+
source: attribution.source || 'landing-page',
|
|
2949
|
+
utmSource: attribution.source || null,
|
|
2950
|
+
utmMedium: attribution.medium || 'newsletter',
|
|
2951
|
+
utmCampaign: attribution.campaign || 'trial_email_capture',
|
|
2952
|
+
utmContent: attribution.content || null,
|
|
2953
|
+
utmTerm: attribution.term || null,
|
|
2954
|
+
creator: attribution.creator || null,
|
|
2955
|
+
community: attribution.community || null,
|
|
2956
|
+
postId: attribution.postId || null,
|
|
2957
|
+
commentId: attribution.commentId || null,
|
|
2958
|
+
campaignVariant: attribution.campaignVariant || null,
|
|
2959
|
+
offerCode: attribution.offerCode || null,
|
|
2960
|
+
ctaId: 'trial_email',
|
|
2961
|
+
ctaPlacement: landingPath === '/pro' ? 'pro_email_form' : 'homepage_email_form',
|
|
2962
|
+
planId: 'pro',
|
|
2963
|
+
pipelineStatus: duplicate ? 'duplicate' : 'accepted',
|
|
2964
|
+
page: landingPath,
|
|
2965
|
+
landingPath,
|
|
2966
|
+
referrer: referrer || null,
|
|
2967
|
+
referrerHost,
|
|
2968
|
+
}, req.headers, 'trial_email_captured');
|
|
2700
2969
|
if (wantsJson) {
|
|
2701
2970
|
sendJson(res, 200, {
|
|
2702
2971
|
accepted: true,
|
|
@@ -2750,6 +3019,19 @@ function createApiServer() {
|
|
|
2750
3019
|
}
|
|
2751
3020
|
|
|
2752
3021
|
// Public endpoints — no auth required
|
|
3022
|
+
const trackedLinkMatch = pathname.match(/^\/go\/([^/]+)$/);
|
|
3023
|
+
if (isGetLikeRequest && trackedLinkMatch) {
|
|
3024
|
+
serveTrackedLinkRedirect({
|
|
3025
|
+
req,
|
|
3026
|
+
res,
|
|
3027
|
+
parsed,
|
|
3028
|
+
hostedConfig,
|
|
3029
|
+
isHeadRequest,
|
|
3030
|
+
slug: trackedLinkMatch[1],
|
|
3031
|
+
});
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
2753
3035
|
if (isGetLikeRequest && pathname === '/robots.txt') {
|
|
2754
3036
|
sendText(res, 200, renderRobotsTxt(hostedConfig), {
|
|
2755
3037
|
'Content-Type': 'text/plain; charset=utf-8',
|