thumbgate 1.16.22 → 1.18.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.
@@ -12,28 +12,28 @@ const {
12
12
  const USAGE_FILE = path.join(process.env.HOME || '/tmp', '.thumbgate', 'usage-limits.json');
13
13
 
14
14
  // ──────────────────────────────────────────────────────────
15
- // NEW: Lifetime caps on free tier users hit the wall fast
16
- // and must upgrade to keep using core features.
15
+ // Free tier: generous on captures (habit formation) and rules
16
+ // (5 active gates), gated on Pro-only features (recall, search,
17
+ // exports). Dashboard, exports, and unlimited rules drive Pro.
17
18
  // ──────────────────────────────────────────────────────────
18
19
  const FREE_TIER_LIMITS = {
19
- capture_feedback: { daily: Infinity, lifetime: 3, label: 'feedback captures' },
20
- prevention_rules: { daily: Infinity, lifetime: 1, label: 'prevention rules generated' },
21
- recall: { daily: 0, lifetime: 0, label: 'recall queries (Pro only)' },
22
- search_lessons: { daily: 0, lifetime: 0, label: 'lesson searches (Pro only)' },
23
- search_thumbgate: { daily: 0, lifetime: 0, label: 'ThumbGate searches (Pro only)' },
24
- commerce_recall: { daily: 0, lifetime: 0, label: 'commerce recalls (Pro only)' },
25
- export_dpo: { daily: 0, lifetime: 0, label: 'DPO exports (Pro only)' },
26
- export_databricks: { daily: 0, lifetime: 0, label: 'Databricks exports (Pro only)' },
27
- construct_context_pack: { daily: Infinity, lifetime: 3, label: 'context packs' },
20
+ capture_feedback: { daily: Infinity, lifetime: Infinity, label: 'feedback captures' },
21
+ prevention_rules: { daily: Infinity, lifetime: Infinity, label: 'prevention rules generated' },
22
+ recall: { daily: 0, lifetime: 0, label: 'recall queries (Pro only)' },
23
+ search_lessons: { daily: 0, lifetime: 0, label: 'lesson searches (Pro only)' },
24
+ search_thumbgate: { daily: 0, lifetime: 0, label: 'ThumbGate searches (Pro only)' },
25
+ commerce_recall: { daily: 0, lifetime: 0, label: 'commerce recalls (Pro only)' },
26
+ export_dpo: { daily: 0, lifetime: 0, label: 'DPO exports (Pro only)' },
27
+ export_databricks: { daily: 0, lifetime: 0, label: 'Databricks exports (Pro only)' },
28
+ construct_context_pack: { daily: Infinity, lifetime: Infinity, label: 'context packs' },
28
29
  };
29
30
 
30
- const FREE_TIER_MAX_GATES = 1; // Down from 5 one auto-promoted gate, then paywall
31
+ const FREE_TIER_MAX_GATES = 5; // 5 active prevention rules on free; Pro is unlimited
31
32
 
32
- const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited captures, recall, prevention rules, and dashboard: ${PRO_MONTHLY_PAYMENT_LINK}\n Team: ${TEAM_PRICE_LABEL} after workflow qualification.`;
33
+ const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Team: ${TEAM_PRICE_LABEL} after workflow qualification.`;
33
34
 
34
35
  const PAYWALL_MESSAGES = {
35
- capture_feedback: 'You\'ve used all 3 free feedback captures. Your agent is still making mistakes upgrade to Pro to capture every one and build real prevention rules.',
36
- prevention_rules: 'Free tier includes 1 prevention rule. Your agents need more protection — upgrade to Pro for unlimited rules.',
36
+ prevention_rules: 'Free tier includes 5 active prevention rules. Promote more or unlock unlimited rules with Pro.',
37
37
  recall: 'Recall is a Pro feature. Your past feedback is stored locally — upgrade to search and reuse it.',
38
38
  search_lessons: 'Lesson search is a Pro feature. Upgrade to find patterns in your agent\'s mistakes.',
39
39
  default: 'This feature requires Pro. Start Pro — card required; billed today.',
package/src/api/server.js CHANGED
@@ -16,6 +16,8 @@ const POSTHOG_STATIC_PATH_PREFIX = '/static/';
16
16
  const FIRST_FAILURE_RULE_CHECKOUT_URL = 'https://buy.stripe.com/4gM6oHgH2bTw4lH6i73sI0z';
17
17
  const QUICK_READ_CHECKOUT_URL = 'https://buy.stripe.com/aFa8wPgH29Lo4lH35V3sI0w';
18
18
  const WORKFLOW_TEARDOWN_CHECKOUT_URL = 'https://buy.stripe.com/7sYfZhgH29LodWhdKz3sI0v';
19
+ const SPRINT_DIAGNOSTIC_CHECKOUT_URL = 'https://buy.stripe.com/3cI7sLgH25v8dWh5e33sI0o';
20
+ const WORKFLOW_SPRINT_CHECKOUT_URL = 'https://buy.stripe.com/8x25kDcqMaPs9G15e33sI0p';
19
21
 
20
22
  function getPosthogProxyPath(pathname) {
21
23
  return pathname.slice('/ingest'.length) || '/';
@@ -210,6 +212,7 @@ const NUMBERS_PAGE_PATH = path.resolve(__dirname, '../../public/numbers.html');
210
212
  const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
211
213
  const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
212
214
  const COMPARE_DIR = path.resolve(__dirname, '../../public/compare');
215
+ const USE_CASES_DIR = path.resolve(__dirname, '../../public/use-cases');
213
216
  const PUBLIC_DIR = path.resolve(__dirname, '../../public');
214
217
  const PUBLIC_ASSETS_DIR = path.resolve(__dirname, '../../public/assets');
215
218
  const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
@@ -400,6 +403,27 @@ const TRACKED_LINK_TARGETS = Object.freeze({
400
403
  },
401
404
  allowCustomerEmail: true,
402
405
  },
406
+ // 2026-05-12: Aiventyx marketplace listing routes its Teams clicks through
407
+ // /go/teams (best-performing listing at ~62% CTR). Without this slug the
408
+ // server returned 404 + "Tracked link not found". Every Aiventyx Teams
409
+ // click between the URL swap and this deploy landed on that error page.
410
+ // Destination: 3-seat Team self-serve Stripe checkout (the path I shipped
411
+ // in PR #1877 — plan_id=team + seat_count=3 = $147/mo entry).
412
+ teams: {
413
+ path: '/checkout/pro',
414
+ ctaId: 'go_teams',
415
+ ctaPlacement: 'link_router',
416
+ eventType: 'cta_click',
417
+ defaults: {
418
+ utm_source: 'website',
419
+ utm_medium: 'link_router',
420
+ utm_campaign: 'team_self_serve',
421
+ plan_id: 'team',
422
+ seat_count: '3',
423
+ billing_cycle: 'monthly',
424
+ },
425
+ allowCustomerEmail: true,
426
+ },
403
427
  install: {
404
428
  path: '/guide',
405
429
  ctaId: 'go_install',
@@ -1508,7 +1532,7 @@ function renderCheckoutIntentPage({
1508
1532
  const teardownAction = safeWorkflowTeardownCheckoutHref
1509
1533
  ? `<a data-i="workflow_teardown_checkout" href="${safeWorkflowTeardownCheckoutHref}">Pay $99 teardown</a>`
1510
1534
  : '';
1511
- return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{background:#0a0a0a;color:#eee;font-family:system-ui,sans-serif}div{max-width:560px;margin:12vh auto}a{display:block;margin:10px 0;padding:12px;border:1px solid #374151;color:inherit;text-align:center}.primary{background:#22d3ee;color:#000}.high-ticket{border-color:#4ade80;color:#4ade80}</style><div><h1>Choose the right paid path.</h1><p>For one repeated workflow failure, start with the diagnostic or sprint. Use Pro only when you need the local self-serve dashboard.</p>${diagnosticAction.replace('<a ', '<a class="high-ticket" ')}${sprintAction.replace('<a ', '<a class="high-ticket" ')}<a class="primary" data-i="pro_checkout_confirmed" href="${safeConfirmHref}">Pay in Stripe</a>${teardownAction}${quickReadAction}${firstRuleAction}<a data-i="workflow_sprint_intake" href="${safeWorkflowIntakeHref}">Send workflow first</a><a data-i="team_paid_path" href="${safeTeamOptionsHref}">See options</a><p>Stripe checkout.</p><a href="/">Back</a></div><script>addEventListener('click',e=>{let a=e.target.closest('[data-i]');if(a&&navigator.sendBeacon)navigator.sendBeacon('/v1/telemetry/ping',new Blob([JSON.stringify({eventType:'checkout_interstitial_cta_clicked',clientType:'web',page:'/checkout/pro',ctaId:a.dataset.i,ctaPlacement:'checkout_interstitial'})],{type:'application/json'}))})</script>`;
1535
+ return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Confirm — ThumbGate Pro</title><style>body{background:#0a0a0a;color:#eee;font-family:system-ui,-apple-system,sans-serif;line-height:1.5}main{max-width:520px;margin:8vh auto;padding:0 20px}.brand{display:flex;align-items:center;gap:10px;margin-bottom:24px;font-size:14px;color:#94a3b8}.brand-mark{width:24px;height:24px;background:#22d3ee;border-radius:6px;display:inline-block}h1{font-size:24px;margin:0 0 8px;color:#fff}.price{font-size:32px;font-weight:700;color:#22d3ee;margin:8px 0 4px}.price small{font-size:14px;color:#94a3b8;font-weight:400}p{color:#cbd5e1;margin:8px 0}a{display:block;text-decoration:none}a.primary{background:#22d3ee;color:#000;padding:16px;text-align:center;border-radius:8px;font-weight:700;font-size:16px;margin:20px 0 10px}a.secondary{border:1px solid #374151;color:#cbd5e1;padding:12px;text-align:center;border-radius:8px;margin:8px 0;font-size:14px}.trust{margin:24px 0;padding:16px;border:1px solid #1f2937;border-radius:8px;background:#0f172a}.trust-item{font-size:13px;color:#cbd5e1;padding:4px 0;display:flex;gap:8px}.trust-item::before{content:"✓";color:#22d3ee;font-weight:700}details{margin-top:32px;font-size:13px;color:#94a3b8}details summary{cursor:pointer;padding:8px 0}details a{border:1px solid #374151;color:#94a3b8;padding:10px;text-align:center;border-radius:6px;margin:6px 0;font-size:13px}.back{text-align:center;color:#64748b;font-size:12px;margin-top:24px}.back a{color:#64748b;display:inline}</style><main><div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div><h1>Start ThumbGate Pro</h1><div class="price">$19<small>/mo</small></div><p>Block every repeat AI-agent mistake. Local-first. MIT-licensed CLI included. Cancel anytime.</p><a class="primary" data-i="pro_checkout_confirmed" href="${safeConfirmHref}">Pay $19/mo with Stripe →</a><div class="trust"><div class="trust-item">6 paying customers, 18,000+ installs verified on npm</div><div class="trust-item">Cancel anytime — instant refund within 7 days</div><div class="trust-item">MIT open source · no vendor lock-in</div><div class="trust-item">Works with Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode</div></div><details><summary>Other paid paths (diagnostic, sprint, teardown, single-rule)</summary>${diagnosticAction.replace('<a ', '<a class="secondary" ')}${sprintAction.replace('<a ', '<a class="secondary" ')}${teardownAction.replace('<a ', '<a class="secondary" ')}${quickReadAction.replace('<a ', '<a class="secondary" ')}${firstRuleAction.replace('<a ', '<a class="secondary" ')}<a class="secondary" data-i="workflow_sprint_intake" href="${safeWorkflowIntakeHref}">Send workflow first (intake)</a><a class="secondary" data-i="team_paid_path" href="${safeTeamOptionsHref}">See all options</a></details><p class="back"><a href="/">← Back to thumbgate.ai</a></p></main><script>addEventListener('click',e=>{let a=e.target.closest('[data-i]');if(a&&navigator.sendBeacon)navigator.sendBeacon('/v1/telemetry/ping',new Blob([JSON.stringify({eventType:'checkout_interstitial_cta_clicked',clientType:'web',page:'/checkout/pro',ctaId:a.dataset.i,ctaPlacement:'checkout_interstitial'})],{type:'application/json'}))})</script></html>`;
1512
1536
  }
1513
1537
 
1514
1538
  function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneyState(req, parsed)) {
@@ -1587,6 +1611,12 @@ function normalizeTrackedLinkSlug(value) {
1587
1611
  return String(value || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
1588
1612
  }
1589
1613
 
1614
+ function normalizePublicPageSlug(value) {
1615
+ return String(value || '')
1616
+ .replace(/\.html$/i, '')
1617
+ .replace(/[^a-z0-9-]/g, '');
1618
+ }
1619
+
1590
1620
  function getTrackedLinkTarget(slug) {
1591
1621
  const normalizedSlug = normalizeTrackedLinkSlug(slug);
1592
1622
  return TRACKED_LINK_TARGETS[normalizedSlug]
@@ -1939,8 +1969,8 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
1939
1969
  '__CHECKOUT_FALLBACK_URL__': runtimeConfig.checkoutFallbackUrl,
1940
1970
  '__PRO_PRICE_DOLLARS__': runtimeConfig.proPriceDollars,
1941
1971
  '__PRO_PRICE_LABEL__': runtimeConfig.proPriceLabel,
1942
- '__SPRINT_DIAGNOSTIC_CHECKOUT_URL__': runtimeConfig.sprintDiagnosticCheckoutUrl || '',
1943
- '__WORKFLOW_SPRINT_CHECKOUT_URL__': runtimeConfig.workflowSprintCheckoutUrl || '',
1972
+ '__SPRINT_DIAGNOSTIC_CHECKOUT_URL__': runtimeConfig.sprintDiagnosticCheckoutUrl || SPRINT_DIAGNOSTIC_CHECKOUT_URL,
1973
+ '__WORKFLOW_SPRINT_CHECKOUT_URL__': runtimeConfig.workflowSprintCheckoutUrl || WORKFLOW_SPRINT_CHECKOUT_URL,
1944
1974
  '__SPRINT_DIAGNOSTIC_PRICE_DOLLARS__': runtimeConfig.sprintDiagnosticPriceDollars || 499,
1945
1975
  '__WORKFLOW_SPRINT_PRICE_DOLLARS__': runtimeConfig.workflowSprintPriceDollars || 1500,
1946
1976
  '__GA_MEASUREMENT_ID__': runtimeConfig.gaMeasurementId || '',
@@ -4193,6 +4223,15 @@ async function addContext(){
4193
4223
  return;
4194
4224
  }
4195
4225
 
4226
+ if (isGetLikeRequest && pathname === '/lessons/') {
4227
+ res.writeHead(302, {
4228
+ Location: '/lessons',
4229
+ 'Cache-Control': 'no-store',
4230
+ });
4231
+ res.end();
4232
+ return;
4233
+ }
4234
+
4196
4235
  if (isGetLikeRequest && pathname === '/lessons') {
4197
4236
  try {
4198
4237
  const html = loadLessonsPageHtml(req, expectedApiKey);
@@ -4245,7 +4284,7 @@ async function addContext(){
4245
4284
  return;
4246
4285
  }
4247
4286
 
4248
- if (isGetLikeRequest && pathname === '/guide') {
4287
+ if (isGetLikeRequest && (pathname === '/guide' || pathname === '/guide.html')) {
4249
4288
  try {
4250
4289
  const html = fs.readFileSync(GUIDE_PAGE_PATH, 'utf-8');
4251
4290
  sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
@@ -4255,7 +4294,7 @@ async function addContext(){
4255
4294
  return;
4256
4295
  }
4257
4296
 
4258
- if (isGetLikeRequest && pathname === '/codex-plugin') {
4297
+ if (isGetLikeRequest && (pathname === '/codex-plugin' || pathname === '/codex-plugin.html')) {
4259
4298
  try {
4260
4299
  const html = fs.readFileSync(CODEX_PLUGIN_PAGE_PATH, 'utf-8');
4261
4300
  sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
@@ -4265,7 +4304,7 @@ async function addContext(){
4265
4304
  return;
4266
4305
  }
4267
4306
 
4268
- if (isGetLikeRequest && pathname === '/compare') {
4307
+ if (isGetLikeRequest && (pathname === '/compare' || pathname === '/compare.html')) {
4269
4308
  try {
4270
4309
  const html = fs.readFileSync(COMPARE_PAGE_PATH, 'utf-8');
4271
4310
  sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
@@ -4275,7 +4314,7 @@ async function addContext(){
4275
4314
  return;
4276
4315
  }
4277
4316
 
4278
- if (isGetLikeRequest && pathname === '/blog') {
4317
+ if (isGetLikeRequest && (pathname === '/blog' || pathname === '/blog.html')) {
4279
4318
  try {
4280
4319
  const blogPath = path.resolve(__dirname, '../../public/blog.html');
4281
4320
  const html = fs.readFileSync(blogPath, 'utf-8');
@@ -4286,7 +4325,7 @@ async function addContext(){
4286
4325
  return;
4287
4326
  }
4288
4327
 
4289
- if (isGetLikeRequest && pathname === '/learn') {
4328
+ if (isGetLikeRequest && (pathname === '/learn' || pathname === '/learn.html')) {
4290
4329
  try {
4291
4330
  const html = fs.readFileSync(LEARN_PAGE_PATH, 'utf-8');
4292
4331
  sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
@@ -4296,6 +4335,24 @@ async function addContext(){
4296
4335
  return;
4297
4336
  }
4298
4337
 
4338
+ if (isGetLikeRequest && (pathname === '/guides' || pathname === '/guides/' || pathname === '/guides.html')) {
4339
+ res.writeHead(302, {
4340
+ Location: '/learn',
4341
+ 'Cache-Control': 'no-store',
4342
+ });
4343
+ res.end();
4344
+ return;
4345
+ }
4346
+
4347
+ if (isGetLikeRequest && (pathname === '/services' || pathname === '/services.html')) {
4348
+ res.writeHead(302, {
4349
+ Location: '/#workflow-sprint-intake',
4350
+ 'Cache-Control': 'no-store',
4351
+ });
4352
+ res.end();
4353
+ return;
4354
+ }
4355
+
4299
4356
  if (isGetLikeRequest && (pathname === '/numbers' || pathname === '/numbers.html')) {
4300
4357
  // Route through servePublicMarketingPage so landing_page_view telemetry
4301
4358
  // + funnel-events.jsonl `discovery/landing_view` get captured with UTM
@@ -4331,7 +4388,7 @@ async function addContext(){
4331
4388
 
4332
4389
  if (isGetLikeRequest && pathname.startsWith('/learn/')) {
4333
4390
  try {
4334
- const slug = pathname.replace('/learn/', '').replace(/[^a-z0-9-]/g, '');
4391
+ const slug = normalizePublicPageSlug(pathname.replace('/learn/', ''));
4335
4392
  const articlePath = path.join(LEARN_DIR, `${slug}.html`);
4336
4393
  if (!articlePath.startsWith(LEARN_DIR)) {
4337
4394
  sendJson(res, 403, { error: 'Forbidden' });
@@ -4347,7 +4404,7 @@ async function addContext(){
4347
4404
 
4348
4405
  if (isGetLikeRequest && pathname.startsWith('/guides/')) {
4349
4406
  try {
4350
- const slug = pathname.replace('/guides/', '').replace(/[^a-z0-9-]/g, '');
4407
+ const slug = normalizePublicPageSlug(pathname.replace('/guides/', ''));
4351
4408
  const guidePath = path.join(GUIDES_DIR, `${slug}.html`);
4352
4409
  if (!guidePath.startsWith(GUIDES_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
4353
4410
  const html = fs.readFileSync(guidePath, 'utf-8');
@@ -4358,7 +4415,7 @@ async function addContext(){
4358
4415
 
4359
4416
  if (isGetLikeRequest && pathname.startsWith('/compare/') && pathname !== '/compare') {
4360
4417
  try {
4361
- const slug = pathname.replace('/compare/', '').replace(/[^a-z0-9-]/g, '');
4418
+ const slug = normalizePublicPageSlug(pathname.replace('/compare/', ''));
4362
4419
  const comparePath = path.join(COMPARE_DIR, `${slug}.html`);
4363
4420
  if (!comparePath.startsWith(COMPARE_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
4364
4421
  const html = fs.readFileSync(comparePath, 'utf-8');
@@ -4367,6 +4424,17 @@ async function addContext(){
4367
4424
  return;
4368
4425
  }
4369
4426
 
4427
+ if (isGetLikeRequest && pathname.startsWith('/use-cases/')) {
4428
+ try {
4429
+ const slug = normalizePublicPageSlug(pathname.replace('/use-cases/', ''));
4430
+ const useCasePath = path.join(USE_CASES_DIR, `${slug}.html`);
4431
+ if (!useCasePath.startsWith(USE_CASES_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
4432
+ const html = fs.readFileSync(useCasePath, 'utf-8');
4433
+ sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
4434
+ } catch { sendJson(res, 404, { error: 'Use case not found' }); }
4435
+ return;
4436
+ }
4437
+
4370
4438
  if (isGetLikeRequest && pathname.startsWith('/assets/')) {
4371
4439
  const rel = pathname.slice('/assets/'.length);
4372
4440
  const resolved = path.resolve(PUBLIC_ASSETS_DIR, rel);
@@ -4500,24 +4568,28 @@ async function addContext(){
4500
4568
  ctaPlacement: 'checkout_interstitial',
4501
4569
  planId: 'workflow_teardown',
4502
4570
  });
4503
- const diagnosticCheckoutHref = hostedConfig.sprintDiagnosticCheckoutUrl
4504
- ? buildCheckoutIntentHref(hostedConfig.sprintDiagnosticCheckoutUrl, analyticsMetadata, {
4571
+ const diagnosticCheckoutHref = buildCheckoutIntentHref(
4572
+ hostedConfig.sprintDiagnosticCheckoutUrl || SPRINT_DIAGNOSTIC_CHECKOUT_URL,
4573
+ analyticsMetadata,
4574
+ {
4505
4575
  utmMedium: 'checkout_interstitial_paid_path',
4506
4576
  utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_diagnostic',
4507
4577
  ctaId: 'checkout_interstitial_sprint_diagnostic_checkout',
4508
4578
  ctaPlacement: 'checkout_interstitial',
4509
4579
  planId: 'sprint_diagnostic',
4510
- })
4511
- : '';
4512
- const sprintCheckoutHref = hostedConfig.workflowSprintCheckoutUrl
4513
- ? buildCheckoutIntentHref(hostedConfig.workflowSprintCheckoutUrl, analyticsMetadata, {
4580
+ }
4581
+ );
4582
+ const sprintCheckoutHref = buildCheckoutIntentHref(
4583
+ hostedConfig.workflowSprintCheckoutUrl || WORKFLOW_SPRINT_CHECKOUT_URL,
4584
+ analyticsMetadata,
4585
+ {
4514
4586
  utmMedium: 'checkout_interstitial_paid_path',
4515
4587
  utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_sprint',
4516
4588
  ctaId: 'checkout_interstitial_workflow_sprint_checkout',
4517
4589
  ctaPlacement: 'checkout_interstitial',
4518
4590
  planId: 'workflow_sprint',
4519
- })
4520
- : '';
4591
+ }
4592
+ );
4521
4593
  const html = renderCheckoutIntentPage({
4522
4594
  confirmHref: buildCheckoutConfirmHref(parsed),
4523
4595
  firstRuleCheckoutHref,