thumbgate 1.17.0 → 1.19.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/public/pro.html CHANGED
@@ -718,7 +718,7 @@ __GA_BOOTSTRAP__
718
718
  <a href="#pricing">Pricing</a>
719
719
  <a href="#faq">FAQ</a>
720
720
  <a href="/dashboard">Demo</a>
721
- <a class="nav-cta" data-quick-read-link href="https://buy.stripe.com/aFa8wPgH29Lo4lH35V3sI0w" onclick="sendFirstPartyTelemetry('quick_read_checkout_started',{ctaId:'pro_page_nav_quick_read_checkout',price:19});sendGa4Event('begin_checkout',{currency:'USD',value:19,items:[{item_id:'quick_read',item_name:'AI Agent Failure Quick Read'}]});">Pay $19 quick read</a>
721
+ <a rel="nofollow noopener noreferrer" target="_blank" class="nav-cta" data-quick-read-link href="https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H" onclick="sendFirstPartyTelemetry('quick_read_checkout_started',{ctaId:'pro_page_nav_quick_read_checkout',price:19});sendGa4Event('begin_checkout',{currency:'USD',value:19,items:[{item_id:'quick_read',item_name:'AI Agent Failure Quick Read'}]});">Pay $19 quick read</a>
722
722
  </div>
723
723
  </div>
724
724
  </nav>
@@ -738,7 +738,7 @@ __GA_BOOTSTRAP__
738
738
  <div class="proof-pill">Founder support on risky flows</div>
739
739
  </div>
740
740
  <div class="hero-actions">
741
- <a class="btn-primary" data-quick-read-link href="https://buy.stripe.com/aFa8wPgH29Lo4lH35V3sI0w" onclick="sendFirstPartyTelemetry('quick_read_checkout_started',{ctaId:'pro_page_hero_quick_read_checkout',price:19});sendGa4Event('begin_checkout',{currency:'USD',value:19,items:[{item_id:'quick_read',item_name:'AI Agent Failure Quick Read'}]});">Pay $19 quick read</a>
741
+ <a rel="nofollow noopener noreferrer" target="_blank" class="btn-primary" data-quick-read-link href="https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H" onclick="sendFirstPartyTelemetry('quick_read_checkout_started',{ctaId:'pro_page_hero_quick_read_checkout',price:19});sendGa4Event('begin_checkout',{currency:'USD',value:19,items:[{item_id:'quick_read',item_name:'AI Agent Failure Quick Read'}]});">Pay $19 quick read</a>
742
742
  <a class="btn-secondary btn-pro-checkout" href="/checkout/pro?utm_source=website&utm_medium=pro_page_hero&utm_campaign=pro_pack&cta_id=pro_page_primary&cta_placement=hero&plan_id=pro&landing_path=%2Fpro">Start Pro dashboard</a>
743
743
  <a class="btn-secondary btn-demo" href="/dashboard?utm_source=website&utm_medium=pro_page&utm_campaign=pro_pack">Open dashboard demo</a>
744
744
  <a class="btn-ghost btn-free-path" href="/guide?utm_source=website&utm_medium=pro_page&utm_campaign=free_install">Stay on Free and install locally</a>
@@ -780,8 +780,8 @@ __GA_BOOTSTRAP__
780
780
  <h3>Buy the paid diagnostic</h3>
781
781
  <p>Skip self-serve Pro and pay for the smallest useful review now.</p>
782
782
  <div class="price-stack">
783
- <a class="btn-secondary" data-first-rule-link href="https://buy.stripe.com/4gM6oHgH2bTw4lH6i73sI0z" onclick="sendFirstPartyTelemetry('first_failure_rule_checkout_started',{ctaId:'pro_page_first_failure_rule_checkout',price:1});sendGa4Event('begin_checkout',{currency:'USD',value:1,items:[{item_id:'first_failure_rule',item_name:'First AI Agent Failure Rule'}]});">Pay $1 first rule</a>
784
- <a class="btn-secondary" data-quick-read-link href="https://buy.stripe.com/aFa8wPgH29Lo4lH35V3sI0w" onclick="sendFirstPartyTelemetry('quick_read_checkout_started',{ctaId:'pro_page_quick_read_checkout',price:19});">Pay $19 quick read</a>
783
+ <a rel="nofollow noopener noreferrer" target="_blank" class="btn-secondary" data-first-rule-link href="https://buy.stripe.com/fZu28rfCY6zcbO99uj3sI2G" onclick="sendFirstPartyTelemetry('first_failure_rule_checkout_started',{ctaId:'pro_page_first_failure_rule_checkout',price:1});sendGa4Event('begin_checkout',{currency:'USD',value:1,items:[{item_id:'first_failure_rule',item_name:'First AI Agent Failure Rule'}]});">Pay $1 first rule</a>
784
+ <a rel="nofollow noopener noreferrer" target="_blank" class="btn-secondary" data-quick-read-link href="https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H" onclick="sendFirstPartyTelemetry('quick_read_checkout_started',{ctaId:'pro_page_quick_read_checkout',price:19});">Pay $19 quick read</a>
785
785
  <a class="btn-primary" data-sprint-diagnostic-link href="__SPRINT_DIAGNOSTIC_CHECKOUT_URL__" onclick="sendFirstPartyTelemetry('workflow_sprint_diagnostic_checkout_started',{ctaId:'pro_page_sprint_diagnostic_checkout'});sendGa4Event('begin_checkout',{currency:'USD',value:__SPRINT_DIAGNOSTIC_PRICE_DOLLARS__});">Pay $__SPRINT_DIAGNOSTIC_PRICE_DOLLARS__ diagnostic</a>
786
786
  <a class="btn-secondary" data-workflow-sprint-link href="__WORKFLOW_SPRINT_CHECKOUT_URL__" onclick="sendFirstPartyTelemetry('workflow_sprint_checkout_started',{ctaId:'pro_page_workflow_sprint_checkout'});sendGa4Event('begin_checkout',{currency:'USD',value:__WORKFLOW_SPRINT_PRICE_DOLLARS__});">Pay $__WORKFLOW_SPRINT_PRICE_DOLLARS__ sprint</a>
787
787
  </div>
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Activation tracker — fires `activation_first_rule_promoted` exactly once
5
+ * per install, the first time a prevention rule auto-promotes from feedback.
6
+ *
7
+ * Why: v1.17.0 opened the free tier (5 active rules, unlimited captures).
8
+ * The metric that decides whether that worked is the % of `npx thumbgate init`
9
+ * runs that produce a first auto-promoted prevention rule within 24h. Without
10
+ * this telemetry, every funnel decision is guessing. This is improvement #1
11
+ * from the 2026-05-13 revenue-ROI critique.
12
+ *
13
+ * Payload (anonymous):
14
+ * - eventType: 'activation_first_rule_promoted'
15
+ * - installId: stable random UUID (from cli-telemetry.getInstallId)
16
+ * - daysToFirstRule: days between INSTALL_ID_PATH creation and now
17
+ * - visitorType: ci | owner | real_user (from cli-telemetry.classifyInstall)
18
+ *
19
+ * No personal data, no rule content, no feedback text. Just the activation
20
+ * signal. Respects THUMBGATE_NO_TELEMETRY=1 / DO_NOT_TRACK=1 opt-out via
21
+ * the underlying trackEvent helper.
22
+ *
23
+ * Idempotency: writes a marker file. After the first firing the function
24
+ * is a no-op for the lifetime of that install.
25
+ */
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+
30
+ const { trackEvent, getInstallId, classifyInstall, INSTALL_ID_PATH } = require('./cli-telemetry');
31
+
32
+ const MARKER_DIR = path.join(path.dirname(INSTALL_ID_PATH), 'activation');
33
+ const MARKER_PATH = path.join(MARKER_DIR, 'first-rule-promoted.json');
34
+
35
+ function readMarker() {
36
+ try {
37
+ if (!fs.existsSync(MARKER_PATH)) return null;
38
+ return JSON.parse(fs.readFileSync(MARKER_PATH, 'utf8'));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function writeMarker(record) {
45
+ try {
46
+ if (!fs.existsSync(MARKER_DIR)) fs.mkdirSync(MARKER_DIR, { recursive: true });
47
+ fs.writeFileSync(MARKER_PATH, JSON.stringify(record, null, 2));
48
+ } catch {
49
+ // non-fatal: worst case we double-fire on a future run; the telemetry
50
+ // backend can dedup by installId. Better to be silent than to throw.
51
+ }
52
+ }
53
+
54
+ function computeDaysSinceInstall() {
55
+ try {
56
+ if (!fs.existsSync(INSTALL_ID_PATH)) return null;
57
+ const installed = fs.statSync(INSTALL_ID_PATH).mtimeMs;
58
+ if (!Number.isFinite(installed) || installed <= 0) return null;
59
+ const diffMs = Date.now() - installed;
60
+ if (diffMs < 0) return 0;
61
+ // 1 decimal of precision; the metric uses 24h buckets but a finer-grained
62
+ // number lets analytics chart same-day vs. 1d / 2d / week-of activation.
63
+ return Math.round((diffMs / 86_400_000) * 10) / 10;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Fire the activation event if this is the first rule promotion for this
71
+ * install. Returns true if an event was actually emitted, false if skipped
72
+ * (already fired before, or environment opted out of telemetry).
73
+ *
74
+ * Always synchronous and never throws — safe to call from any side-effect
75
+ * site without try/catch wrapping. The underlying telemetry call is itself
76
+ * fire-and-forget over HTTPS with a 3s timeout.
77
+ */
78
+ function recordFirstRulePromotion(metadata = {}) {
79
+ if (readMarker()) return false; // already fired
80
+
81
+ if (process.env.THUMBGATE_NO_TELEMETRY === '1' || process.env.DO_NOT_TRACK === '1') {
82
+ // Still write the marker so a later run with telemetry re-enabled does not
83
+ // fire stale activation data weeks after the actual first promotion.
84
+ writeMarker({
85
+ installId: getInstallId(),
86
+ promotedAt: new Date().toISOString(),
87
+ telemetryOptOut: true,
88
+ });
89
+ return false;
90
+ }
91
+
92
+ const installId = getInstallId();
93
+ const daysToFirstRule = computeDaysSinceInstall();
94
+ const visitorType = classifyInstall();
95
+
96
+ writeMarker({
97
+ installId,
98
+ promotedAt: new Date().toISOString(),
99
+ daysToFirstRule,
100
+ visitorType,
101
+ ...metadata,
102
+ });
103
+
104
+ trackEvent('activation_first_rule_promoted', {
105
+ daysToFirstRule,
106
+ visitorType,
107
+ ...metadata,
108
+ });
109
+ return true;
110
+ }
111
+
112
+ function resetForTesting() {
113
+ // Test-only helper. Removes the marker so a subsequent recordFirstRulePromotion
114
+ // call re-fires. Never used in production code paths.
115
+ try {
116
+ if (fs.existsSync(MARKER_PATH)) fs.unlinkSync(MARKER_PATH);
117
+ } catch {}
118
+ }
119
+
120
+ module.exports = {
121
+ recordFirstRulePromotion,
122
+ computeDaysSinceInstall,
123
+ readMarker,
124
+ writeMarker,
125
+ MARKER_PATH,
126
+ resetForTesting,
127
+ };
@@ -6,7 +6,10 @@ const path = require('path');
6
6
  const { resolveFeedbackDir } = require('./feedback-paths');
7
7
 
8
8
  const MAX_AUTO_GATES = 10;
9
- const WARN_THRESHOLD = 2; // 2+ repeated failures surface a warning gate
9
+ // 1+ failure auto-promotes to a warning gate. Cold buyers expect "one 👎 → blocked next time"
10
+ // — a 2-capture threshold made first-capture invisible and broke the activation loop. Block
11
+ // escalation still requires 3 captures (BLOCK_THRESHOLD) so noise doesn't auto-hard-block.
12
+ const WARN_THRESHOLD = 1;
10
13
  const BLOCK_THRESHOLD = 3; // 3+ repeated failures hard-block the action
11
14
  const WINDOW_DAYS = 30;
12
15
 
@@ -1398,7 +1398,20 @@ function captureFeedback(params) {
1398
1398
  if (feedbackEvent.signal === 'negative') {
1399
1399
  try {
1400
1400
  const autoPromote = require('./auto-promote-gates');
1401
- autoPromote.promote(FEEDBACK_LOG_PATH);
1401
+ const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH);
1402
+ // First-rule activation telemetry: anonymous ping the first time
1403
+ // a prevention rule auto-promotes for this install. Idempotent —
1404
+ // see scripts/activation-tracker.js. Critical for activation funnel
1405
+ // analytics; no rule content, just install_id + days_to_first_rule.
1406
+ if (promoteResult && Array.isArray(promoteResult.promotions) && promoteResult.promotions.length > 0) {
1407
+ try {
1408
+ const { recordFirstRulePromotion } = require('./activation-tracker');
1409
+ recordFirstRulePromotion({
1410
+ promotionCount: promoteResult.promotions.length,
1411
+ totalGates: promoteResult.totalGates,
1412
+ });
1413
+ } catch { /* activation telemetry is non-critical */ }
1414
+ }
1402
1415
  } catch { /* Gate promotion is non-critical */ }
1403
1416
  }
1404
1417
 
@@ -32,7 +32,17 @@ function normalize(ctx) {
32
32
  return (ctx || '').replace(/\/Users\/[^\s/]+/g, '~').replace(/:[0-9]+/g, '').toLowerCase().trim();
33
33
  }
34
34
 
35
- const HIGH_RISK_TAGS = new Set(['git-workflow', 'scope-control', 'trust-breach', 'execution-gap', 'regression', 'security']);
35
+ // HIGH_RISK_TAGS triggers single-capture promotion (count >= 1 && hasHighRisk).
36
+ // Tags here MUST overlap with what inferSemanticTags() actually emits (see scripts/feedback-loop.js)
37
+ // — otherwise cold buyers' first 👎 stays a lesson and never becomes a gate.
38
+ const HIGH_RISK_TAGS = new Set([
39
+ // Original semantic-category labels
40
+ 'git-workflow', 'scope-control', 'trust-breach', 'execution-gap', 'regression', 'security',
41
+ // Tags inferSemanticTags() emits for destructive / irreversible operations
42
+ 'destructive', 'force-push', 'delete', 'drop', 'force-overwrite',
43
+ 'production', 'database', 'payment', 'credentials', 'secrets',
44
+ 'rm-rf', 'reset-hard', 'truncate', 'data-loss',
45
+ ]);
36
46
  function analyze(entries) {
37
47
  let positiveCount = 0, negativeCount = 0;
38
48
  const categories = {};