thumbgate 1.22.0 → 1.23.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 +1 -0
- package/adapters/chatgpt/openapi.yaml +10 -0
- 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 +194 -30
- package/openapi/openapi.yaml +10 -0
- package/package.json +13 -3
- package/public/agents-cost-savings.html +151 -0
- package/public/ai-malpractice-prevention.html +183 -0
- package/public/codex-plugin.html +1 -1
- package/public/index.html +3 -3
- package/public/numbers.html +2 -2
- package/public/pricing.html +1 -1
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/gates-engine.js +119 -6
- package/scripts/meta-agent-loop.js +32 -0
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/rate-limiter.js +7 -1
- package/scripts/self-healing-check.js +193 -0
- package/scripts/silent-failure-cluster.js +512 -0
- package/scripts/telemetry-analytics.js +38 -0
- package/src/api/server.js +252 -36
package/src/api/server.js
CHANGED
|
@@ -429,6 +429,46 @@ const TRACKED_LINK_TARGETS = Object.freeze({
|
|
|
429
429
|
plan_id: 'team',
|
|
430
430
|
},
|
|
431
431
|
},
|
|
432
|
+
// Aliases: /go/team → same as /go/teams, /go/checkout → same as /go/pro,
|
|
433
|
+
// /go/trial → install guide (trial starts on init)
|
|
434
|
+
team: {
|
|
435
|
+
path: '/#workflow-sprint-intake',
|
|
436
|
+
ctaId: 'go_team',
|
|
437
|
+
ctaPlacement: 'link_router',
|
|
438
|
+
eventType: 'team_intake_started',
|
|
439
|
+
defaults: {
|
|
440
|
+
utm_source: 'website',
|
|
441
|
+
utm_medium: 'link_router',
|
|
442
|
+
utm_campaign: 'team_intake',
|
|
443
|
+
plan_id: 'team',
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
checkout: {
|
|
447
|
+
path: '/checkout/pro',
|
|
448
|
+
ctaId: 'go_checkout',
|
|
449
|
+
ctaPlacement: 'link_router',
|
|
450
|
+
eventType: 'cta_click',
|
|
451
|
+
defaults: {
|
|
452
|
+
utm_source: 'website',
|
|
453
|
+
utm_medium: 'link_router',
|
|
454
|
+
utm_campaign: 'pro_upgrade',
|
|
455
|
+
plan_id: 'pro',
|
|
456
|
+
billing_cycle: 'monthly',
|
|
457
|
+
},
|
|
458
|
+
allowCustomerEmail: true,
|
|
459
|
+
},
|
|
460
|
+
trial: {
|
|
461
|
+
path: '/guide',
|
|
462
|
+
ctaId: 'go_trial',
|
|
463
|
+
ctaPlacement: 'link_router',
|
|
464
|
+
eventType: 'trial_start_click',
|
|
465
|
+
defaults: {
|
|
466
|
+
utm_source: 'website',
|
|
467
|
+
utm_medium: 'link_router',
|
|
468
|
+
utm_campaign: 'trial_start',
|
|
469
|
+
plan_id: 'free_trial',
|
|
470
|
+
},
|
|
471
|
+
},
|
|
432
472
|
install: {
|
|
433
473
|
path: '/guide',
|
|
434
474
|
ctaId: 'go_install',
|
|
@@ -757,7 +797,7 @@ function getMcpSkillManifests(hostedConfig) {
|
|
|
757
797
|
'Inspect prevention_rules after repeats.',
|
|
758
798
|
],
|
|
759
799
|
installCommand: 'npx thumbgate init',
|
|
760
|
-
contextUrl: buildPublicUrl(hostedConfig, '/
|
|
800
|
+
contextUrl: buildPublicUrl(hostedConfig, '/llm-context.md'),
|
|
761
801
|
proofUrl: VERIFICATION_EVIDENCE_URL,
|
|
762
802
|
},
|
|
763
803
|
{
|
|
@@ -784,7 +824,7 @@ function getMcpSkillManifests(hostedConfig) {
|
|
|
784
824
|
'Evaluate NDCG@10 on visual hard negatives.',
|
|
785
825
|
'Require artifact links before using retrieved evidence in claims.',
|
|
786
826
|
],
|
|
787
|
-
contextUrl: buildPublicUrl(hostedConfig, '/
|
|
827
|
+
contextUrl: buildPublicUrl(hostedConfig, '/llm-context.md'),
|
|
788
828
|
proofUrl: VERIFICATION_EVIDENCE_URL,
|
|
789
829
|
},
|
|
790
830
|
{
|
|
@@ -798,7 +838,7 @@ function getMcpSkillManifests(hostedConfig) {
|
|
|
798
838
|
'Compact feedback context with anchors for proof-critical lessons.',
|
|
799
839
|
'Record estimated token savings next to the workflow evidence.',
|
|
800
840
|
],
|
|
801
|
-
contextUrl: buildPublicUrl(hostedConfig, '/
|
|
841
|
+
contextUrl: buildPublicUrl(hostedConfig, '/llm-context.md'),
|
|
802
842
|
footprintUrl: buildPublicUrl(hostedConfig, '/.well-known/mcp/footprint.json'),
|
|
803
843
|
proofUrl: VERIFICATION_EVIDENCE_URL,
|
|
804
844
|
},
|
|
@@ -813,7 +853,7 @@ function getMcpSkillManifests(hostedConfig) {
|
|
|
813
853
|
'Require baseline evals before adding autonomy or subagents.',
|
|
814
854
|
'Classify tool risk before allowing writes, money movement, production changes, or outbound actions.',
|
|
815
855
|
],
|
|
816
|
-
contextUrl: buildPublicUrl(hostedConfig, '/
|
|
856
|
+
contextUrl: buildPublicUrl(hostedConfig, '/llm-context.md'),
|
|
817
857
|
proofUrl: VERIFICATION_EVIDENCE_URL,
|
|
818
858
|
},
|
|
819
859
|
{
|
|
@@ -827,7 +867,7 @@ function getMcpSkillManifests(hostedConfig) {
|
|
|
827
867
|
'Grade goal inference separately from intervention timing.',
|
|
828
868
|
'Block multi-app proactive writes until rollback and orchestration evidence exists.',
|
|
829
869
|
],
|
|
830
|
-
contextUrl: buildPublicUrl(hostedConfig, '/
|
|
870
|
+
contextUrl: buildPublicUrl(hostedConfig, '/llm-context.md'),
|
|
831
871
|
proofUrl: VERIFICATION_EVIDENCE_URL,
|
|
832
872
|
},
|
|
833
873
|
{
|
|
@@ -841,7 +881,7 @@ function getMcpSkillManifests(hostedConfig) {
|
|
|
841
881
|
'Map every proxy metric to the real user objective.',
|
|
842
882
|
'Require holdout or regression proof before treating benchmark gains as product gains.',
|
|
843
883
|
],
|
|
844
|
-
contextUrl: buildPublicUrl(hostedConfig, '/
|
|
884
|
+
contextUrl: buildPublicUrl(hostedConfig, '/llm-context.md'),
|
|
845
885
|
proofUrl: VERIFICATION_EVIDENCE_URL,
|
|
846
886
|
},
|
|
847
887
|
{
|
|
@@ -855,7 +895,7 @@ function getMcpSkillManifests(hostedConfig) {
|
|
|
855
895
|
'Reproduce locally before claiming a fix.',
|
|
856
896
|
'Open one focused PR with tests, proof, and transparent ThumbGate context only when relevant.',
|
|
857
897
|
],
|
|
858
|
-
contextUrl: buildPublicUrl(hostedConfig, '/
|
|
898
|
+
contextUrl: buildPublicUrl(hostedConfig, '/llm-context.md'),
|
|
859
899
|
proofUrl: VERIFICATION_EVIDENCE_URL,
|
|
860
900
|
},
|
|
861
901
|
{
|
|
@@ -869,7 +909,7 @@ function getMcpSkillManifests(hostedConfig) {
|
|
|
869
909
|
'Route self-serve intent to the guide and team pain to Workflow Hardening Sprint intake.',
|
|
870
910
|
'Block unsupported ad and landing-page claims before spend scales.',
|
|
871
911
|
],
|
|
872
|
-
contextUrl: buildPublicUrl(hostedConfig, '/
|
|
912
|
+
contextUrl: buildPublicUrl(hostedConfig, '/llm-context.md'),
|
|
873
913
|
proofUrl: VERIFICATION_EVIDENCE_URL,
|
|
874
914
|
},
|
|
875
915
|
];
|
|
@@ -1003,7 +1043,7 @@ function getMcpDiscoveryManifest(hostedConfig) {
|
|
|
1003
1043
|
footprint: getContextFootprintReport(hostedConfig),
|
|
1004
1044
|
proof: {
|
|
1005
1045
|
verificationEvidenceUrl: VERIFICATION_EVIDENCE_URL,
|
|
1006
|
-
llmContextUrl: buildPublicUrl(hostedConfig, '/
|
|
1046
|
+
llmContextUrl: buildPublicUrl(hostedConfig, '/llm-context.md'),
|
|
1007
1047
|
},
|
|
1008
1048
|
};
|
|
1009
1049
|
}
|
|
@@ -1493,12 +1533,14 @@ function buildCheckoutIntentHref(baseUrl, metadata = {}, overrides = {}) {
|
|
|
1493
1533
|
}
|
|
1494
1534
|
|
|
1495
1535
|
function renderCheckoutIntentPage({
|
|
1496
|
-
confirmHref,
|
|
1497
1536
|
workflowIntakeHref,
|
|
1537
|
+
confirmHiddenParams,
|
|
1498
1538
|
}) {
|
|
1499
|
-
const safeConfirmHref = escapeHtmlAttribute(confirmHref);
|
|
1500
1539
|
const safeWorkflowIntakeHref = escapeHtmlAttribute(workflowIntakeHref);
|
|
1501
|
-
|
|
1540
|
+
const hiddenFields = (confirmHiddenParams || [])
|
|
1541
|
+
.map(([k, v]) => `<input type="hidden" name="${escapeHtmlAttribute(k)}" value="${escapeHtmlAttribute(v)}">`)
|
|
1542
|
+
.join('');
|
|
1543
|
+
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}form{margin:0}input[type=email]{width:100%;box-sizing:border-box;padding:14px 16px;border:1px solid #374151;border-radius:8px;background:#111827;color:#fff;font-size:15px;margin:16px 0 0;outline:none}input[type=email]:focus{border-color:#22d3ee}input[type=email]::placeholder{color:#64748b}button.primary{background:#22d3ee;color:#000;padding:16px;text-align:center;border-radius:8px;font-weight:700;font-size:16px;margin:10px 0;border:none;cursor:pointer;width:100%}a{display:block;text-decoration:none}a.secondary{border:1px solid #374151;color:#cbd5e1;padding:12px;text-align:center;border-radius:8px;margin:8px 0 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}.choice-note{font-size:13px;color:#94a3b8;margin-top:14px}.back{text-align:center;color:#64748b;font-size:12px;margin-top:24px}.back a{color:#64748b;display:inline}.email-note{font-size:12px;color:#64748b;margin:4px 0 0}</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>The npm package runs your gates locally. <strong>Pro</strong> is what keeps them working across every machine, every agent runtime, and every breaking-change week.</p><form action="/checkout/pro" method="GET" data-i="pro_checkout_confirmed">${hiddenFields}<input type="hidden" name="confirm" value="1"><input type="email" name="customer_email" placeholder="you@company.com" required autocomplete="email"><p class="email-note">Pre-fills your Stripe receipt. We only email if you ask.</p><button type="submit" class="primary">Pay $19/mo with Stripe →</button></form><a class="secondary" data-i="workflow_sprint_intake" href="${safeWorkflowIntakeHref}">Not sure yet? Send the workflow first</a><p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages.</p><div class="trust"><div class="trust-item">Lessons synced across all your machines — no local SQLite to babysit</div><div class="trust-item">Adapter matrix kept current for Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode — version drift is our problem, not yours</div><div class="trust-item">Hosted dashboard: gate stats, DPO export, org-wide rule library</div><div class="trust-item">24×7 ops on the rule engine — SonarCloud regressions fixed in <24h</div></div><p class="back"><a href="/">← Back to thumbgate.ai</a></p></main><script>document.querySelector('form').addEventListener('submit',e=>{if(navigator.sendBeacon)navigator.sendBeacon('/v1/telemetry/ping',new Blob([JSON.stringify({eventType:'checkout_interstitial_cta_clicked',clientType:'web',page:'/checkout/pro',ctaId:'pro_checkout_confirmed',ctaPlacement:'checkout_interstitial',customerEmail:document.querySelector('input[name=customer_email]').value})],{type:'application/json'}))})</script></html>`;
|
|
1502
1544
|
}
|
|
1503
1545
|
|
|
1504
1546
|
function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneyState(req, parsed)) {
|
|
@@ -1825,6 +1867,19 @@ function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
|
|
|
1825
1867
|
}
|
|
1826
1868
|
}
|
|
1827
1869
|
|
|
1870
|
+
function inferActionIntegration(body = {}, headers = {}) {
|
|
1871
|
+
const explicit = pickFirstText(body.integration, body.source, body.actionIntegration, body.provider);
|
|
1872
|
+
const userAgent = String(headers['user-agent'] || '').toLowerCase();
|
|
1873
|
+
if (/chatgpt|gpt[_-]?actions?|openai/.test(String(explicit || '').toLowerCase()) || /chatgpt|openai/.test(userAgent)) {
|
|
1874
|
+
return 'chatgpt_gpt';
|
|
1875
|
+
}
|
|
1876
|
+
return explicit || 'api';
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function chatgptActionEventType(integration, suffix) {
|
|
1880
|
+
return integration === 'chatgpt_gpt' ? `chatgpt_action_${suffix}` : `api_action_${suffix}`;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1828
1883
|
function getPublicOrigin(req) {
|
|
1829
1884
|
const proto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim() || 'http';
|
|
1830
1885
|
const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim() || 'localhost';
|
|
@@ -2505,6 +2560,8 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
2505
2560
|
{ path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
|
|
2506
2561
|
{ path: '/codex-plugin', changefreq: 'weekly', priority: '0.75' },
|
|
2507
2562
|
{ path: '/codex-enterprise', changefreq: 'weekly', priority: '0.85' },
|
|
2563
|
+
{ path: '/agents-cost-savings', changefreq: 'weekly', priority: '0.85' },
|
|
2564
|
+
{ path: '/ai-malpractice-prevention', changefreq: 'weekly', priority: '0.9' },
|
|
2508
2565
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
2509
2566
|
];
|
|
2510
2567
|
return [
|
|
@@ -4545,6 +4602,47 @@ async function addContext(){
|
|
|
4545
4602
|
return;
|
|
4546
4603
|
}
|
|
4547
4604
|
|
|
4605
|
+
if (isGetLikeRequest && (pathname === '/agents-cost-savings' || pathname === '/agents-cost-savings.html')) {
|
|
4606
|
+
// FinOps-for-AI positioning page. Pairs with the `thumbgate cost` CLI
|
|
4607
|
+
// shipped in #2281: the CLI prints the dollar amount, this page is
|
|
4608
|
+
// the public-facing explanation of why "prevention" (ThumbGate's
|
|
4609
|
+
// PreToolUse gates) is a distinct category from "reporting" (Finout,
|
|
4610
|
+
// Helicone, Vantage, AgentOps). Reply-to-pitch surface for buyers
|
|
4611
|
+
// who get a FinOps-for-AI marketing email and need a frame.
|
|
4612
|
+
try {
|
|
4613
|
+
servePublicMarketingPage({
|
|
4614
|
+
req,
|
|
4615
|
+
res,
|
|
4616
|
+
parsed,
|
|
4617
|
+
hostedConfig,
|
|
4618
|
+
isHeadRequest,
|
|
4619
|
+
renderHtml: () => fs.readFileSync(path.join(PUBLIC_DIR, 'agents-cost-savings.html'), 'utf-8'),
|
|
4620
|
+
extraTelemetry: { pageType: 'agents_cost_savings' },
|
|
4621
|
+
});
|
|
4622
|
+
} catch {
|
|
4623
|
+
sendJson(res, 404, { error: 'Agents Cost Savings page not found' });
|
|
4624
|
+
}
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4628
|
+
if (isGetLikeRequest && (pathname === '/ai-malpractice-prevention' || pathname === '/ai-malpractice-prevention.html')) {
|
|
4629
|
+
// Legal-vertical landing page (2026-05-21).
|
|
4630
|
+
try {
|
|
4631
|
+
servePublicMarketingPage({
|
|
4632
|
+
req,
|
|
4633
|
+
res,
|
|
4634
|
+
parsed,
|
|
4635
|
+
hostedConfig,
|
|
4636
|
+
isHeadRequest,
|
|
4637
|
+
renderHtml: () => fs.readFileSync(path.join(PUBLIC_DIR, 'ai-malpractice-prevention.html'), 'utf-8'),
|
|
4638
|
+
extraTelemetry: { pageType: 'ai_malpractice_prevention' },
|
|
4639
|
+
});
|
|
4640
|
+
} catch {
|
|
4641
|
+
sendJson(res, 404, { error: 'AI Malpractice Prevention page not found' });
|
|
4642
|
+
}
|
|
4643
|
+
return;
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4548
4646
|
if (isGetLikeRequest && pathname === '/learn/learn.css') {
|
|
4549
4647
|
try {
|
|
4550
4648
|
const cssPath = path.join(LEARN_DIR, 'learn.css');
|
|
@@ -4663,7 +4761,7 @@ async function addContext(){
|
|
|
4663
4761
|
|
|
4664
4762
|
if (isGetLikeRequest && pathname === '/checkout/pro') {
|
|
4665
4763
|
if (isHeadRequest) {
|
|
4666
|
-
|
|
4764
|
+
sendHtml(res, 200, '', {}, {
|
|
4667
4765
|
headOnly: true,
|
|
4668
4766
|
});
|
|
4669
4767
|
return;
|
|
@@ -4761,10 +4859,15 @@ async function addContext(){
|
|
|
4761
4859
|
ctaPlacement: 'checkout_interstitial',
|
|
4762
4860
|
planId: 'team',
|
|
4763
4861
|
});
|
|
4862
|
+
const confirmHiddenParams = [];
|
|
4863
|
+
for (const [key, value] of parsed.searchParams.entries()) {
|
|
4864
|
+
if (key !== 'confirm' && key !== 'customer_email') {
|
|
4865
|
+
confirmHiddenParams.push([key, value]);
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4764
4868
|
const html = renderCheckoutIntentPage({
|
|
4765
|
-
confirmHref: buildCheckoutConfirmHref(parsed),
|
|
4766
4869
|
workflowIntakeHref,
|
|
4767
|
-
|
|
4870
|
+
confirmHiddenParams,
|
|
4768
4871
|
});
|
|
4769
4872
|
sendHtml(res, 200, html, responseHeaders);
|
|
4770
4873
|
return;
|
|
@@ -5165,40 +5268,57 @@ async function addContext(){
|
|
|
5165
5268
|
// was down, when feedback-dir was unwritable, when env was misconfigured.
|
|
5166
5269
|
// The fix is shallow but meaningful: probe each critical subsystem and
|
|
5167
5270
|
// surface failures with HTTP 503 + a per-check breakdown.
|
|
5271
|
+
// Tiered failure classification — not all degradations should kill the
|
|
5272
|
+
// container. A *service-failing* check (feedback dir unwritable,
|
|
5273
|
+
// appOrigin missing) returns 503 → Railway's healthcheck fails → SIGTERM
|
|
5274
|
+
// → restart loop → outage (this exact failure mode took prod down
|
|
5275
|
+
// 2026-05-21 18:21Z → 19:30Z when BUILD_METADATA.buildSha came up empty
|
|
5276
|
+
// after a Railway env-var cleanup). A *telemetry-degraded* check (missing
|
|
5277
|
+
// buildSha) returns 200 with `degraded: true` so observability stays
|
|
5278
|
+
// visible but Railway doesn't kill an otherwise-healthy container over
|
|
5279
|
+
// a SHA gap.
|
|
5168
5280
|
const checks = {};
|
|
5169
|
-
let
|
|
5281
|
+
let failing = false; // any service-failing check → 503
|
|
5282
|
+
let degraded = false; // any telemetry-degraded check → 200 + flag
|
|
5170
5283
|
|
|
5171
5284
|
// Check 1: feedback dir exists and is writable.
|
|
5285
|
+
// SERVICE-FAILING — if we can't write feedback, the API can't function.
|
|
5172
5286
|
try {
|
|
5173
5287
|
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
5174
5288
|
fs.accessSync(FEEDBACK_DIR, fs.constants.W_OK);
|
|
5175
5289
|
checks.feedbackDir = { ok: true };
|
|
5176
5290
|
} catch (err) {
|
|
5177
|
-
checks.feedbackDir = { ok: false, error: err?.code || 'inaccessible' };
|
|
5178
|
-
|
|
5291
|
+
checks.feedbackDir = { ok: false, error: err?.code || 'inaccessible', severity: 'failing' };
|
|
5292
|
+
failing = true;
|
|
5179
5293
|
}
|
|
5180
5294
|
|
|
5181
5295
|
// Check 2: hosted config resolves the canonical app origin.
|
|
5182
|
-
//
|
|
5296
|
+
// SERVICE-FAILING — if appOrigin is missing/empty, redirects + checkout
|
|
5297
|
+
// flow break silently.
|
|
5183
5298
|
if (hostedConfig?.appOrigin) {
|
|
5184
5299
|
checks.hostedConfig = { ok: true };
|
|
5185
5300
|
} else {
|
|
5186
|
-
checks.hostedConfig = { ok: false, error: 'missing_appOrigin' };
|
|
5187
|
-
|
|
5301
|
+
checks.hostedConfig = { ok: false, error: 'missing_appOrigin', severity: 'failing' };
|
|
5302
|
+
failing = true;
|
|
5188
5303
|
}
|
|
5189
5304
|
|
|
5190
|
-
// Check 3: build metadata loaded.
|
|
5191
|
-
//
|
|
5305
|
+
// Check 3: build metadata loaded.
|
|
5306
|
+
// TELEMETRY-DEGRADED — observability gap, not a runtime outage. The
|
|
5307
|
+
// container still serves requests fine; we just can't tag responses with
|
|
5308
|
+
// the deployed SHA. Surfaces the gap to monitors via `degraded: true`
|
|
5309
|
+
// without triggering Railway's SIGTERM-on-503 loop.
|
|
5192
5310
|
if (BUILD_METADATA?.buildSha) {
|
|
5193
5311
|
checks.buildMetadata = { ok: true };
|
|
5194
5312
|
} else {
|
|
5195
|
-
checks.buildMetadata = { ok: false, error: 'missing_buildSha' };
|
|
5196
|
-
|
|
5313
|
+
checks.buildMetadata = { ok: false, error: 'missing_buildSha', severity: 'degraded' };
|
|
5314
|
+
degraded = true;
|
|
5197
5315
|
}
|
|
5198
5316
|
|
|
5199
|
-
const statusCode =
|
|
5317
|
+
const statusCode = failing ? 503 : 200;
|
|
5318
|
+
const status = failing ? 'failing' : (degraded ? 'degraded' : 'ok');
|
|
5200
5319
|
sendJson(res, statusCode, {
|
|
5201
|
-
status
|
|
5320
|
+
status,
|
|
5321
|
+
degraded: degraded || failing,
|
|
5202
5322
|
version: pkg.version,
|
|
5203
5323
|
buildSha: BUILD_METADATA.buildSha,
|
|
5204
5324
|
uptime: process.uptime(),
|
|
@@ -5296,15 +5416,25 @@ async function addContext(){
|
|
|
5296
5416
|
const bd = require('../../scripts/bot-detector');
|
|
5297
5417
|
const { FEEDBACK_DIR: metricsDir } = getFeedbackPaths();
|
|
5298
5418
|
const telemetryPath = path.join(metricsDir, 'telemetry-pings.jsonl');
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5419
|
+
|
|
5420
|
+
// Cache parsed entries for 60s to avoid re-parsing 72K+ JSON lines per request
|
|
5421
|
+
const cacheKey = '_metricsRealCache';
|
|
5422
|
+
const cacheTTL = 60_000;
|
|
5423
|
+
let entries;
|
|
5424
|
+
if (global[cacheKey] && (Date.now() - global[cacheKey].ts) < cacheTTL) {
|
|
5425
|
+
entries = global[cacheKey].entries;
|
|
5426
|
+
} else {
|
|
5427
|
+
entries = [];
|
|
5428
|
+
try {
|
|
5429
|
+
if (fs.existsSync(telemetryPath)) {
|
|
5430
|
+
entries = fs.readFileSync(telemetryPath, 'utf8')
|
|
5431
|
+
.split('\n').filter(Boolean)
|
|
5432
|
+
.map(l => { try { return JSON.parse(l); } catch(_e) { return null; } })
|
|
5433
|
+
.filter(Boolean);
|
|
5434
|
+
}
|
|
5435
|
+
} catch { entries = []; }
|
|
5436
|
+
global[cacheKey] = { entries, ts: Date.now() };
|
|
5437
|
+
}
|
|
5308
5438
|
|
|
5309
5439
|
const now = Date.now();
|
|
5310
5440
|
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
|
|
@@ -5333,6 +5463,52 @@ async function addContext(){
|
|
|
5333
5463
|
byVisitorType[e.visitorType] = (byVisitorType[e.visitorType] || 0) + 1;
|
|
5334
5464
|
});
|
|
5335
5465
|
|
|
5466
|
+
// ---------------------------------------------------------------
|
|
5467
|
+
// Active user analytics: group by installId to distinguish real
|
|
5468
|
+
// users from bots/mirrors. "Active" = performed a meaningful CLI
|
|
5469
|
+
// action (capture, recall, search, gate-check, stats) — not just init.
|
|
5470
|
+
// ---------------------------------------------------------------
|
|
5471
|
+
// Only events that prove a human used the product beyond init.
|
|
5472
|
+
// cli_pro_view excluded: browsing the upgrade page is not "using" the product.
|
|
5473
|
+
// cli_gate_check excluded: runs as subprocess, never fires trackEvent().
|
|
5474
|
+
// cli_search excluded: event name doesn't exist in production telemetry.
|
|
5475
|
+
const ACTIVE_EVENTS = new Set([
|
|
5476
|
+
'cli_capture', 'cli_recall', 'cli_stats',
|
|
5477
|
+
'activation_first_rule_promoted',
|
|
5478
|
+
]);
|
|
5479
|
+
const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
|
|
5480
|
+
const last30 = classified.filter(e => {
|
|
5481
|
+
const ts = e.timestamp || e.receivedAt;
|
|
5482
|
+
return ts && new Date(ts).getTime() > thirtyDaysAgo;
|
|
5483
|
+
});
|
|
5484
|
+
|
|
5485
|
+
// Per-install activity (all-time and recent windows)
|
|
5486
|
+
const activeInstalls7d = new Set();
|
|
5487
|
+
const activeInstalls30d = new Set();
|
|
5488
|
+
const allTimeActiveInstalls = new Set();
|
|
5489
|
+
const uniqueSessions7d = new Set();
|
|
5490
|
+
const uniqueSessions30d = new Set();
|
|
5491
|
+
|
|
5492
|
+
for (const e of classified) {
|
|
5493
|
+
if (!e.installId) continue;
|
|
5494
|
+
const et = e.eventType || '';
|
|
5495
|
+
if (!ACTIVE_EVENTS.has(et)) continue;
|
|
5496
|
+
if (e.visitorType === 'bot' || e.visitorType === 'ci') continue;
|
|
5497
|
+
allTimeActiveInstalls.add(e.installId);
|
|
5498
|
+
}
|
|
5499
|
+
for (const e of recent) {
|
|
5500
|
+
if (!e.installId) continue;
|
|
5501
|
+
if (e.visitorType === 'bot' || e.visitorType === 'ci') continue;
|
|
5502
|
+
if (ACTIVE_EVENTS.has(e.eventType || '')) activeInstalls7d.add(e.installId);
|
|
5503
|
+
if (e.sessionId) uniqueSessions7d.add(e.sessionId);
|
|
5504
|
+
}
|
|
5505
|
+
for (const e of last30) {
|
|
5506
|
+
if (!e.installId) continue;
|
|
5507
|
+
if (e.visitorType === 'bot' || e.visitorType === 'ci') continue;
|
|
5508
|
+
if (ACTIVE_EVENTS.has(e.eventType || '')) activeInstalls30d.add(e.installId);
|
|
5509
|
+
if (e.sessionId) uniqueSessions30d.add(e.sessionId);
|
|
5510
|
+
}
|
|
5511
|
+
|
|
5336
5512
|
sendJson(res, 200, {
|
|
5337
5513
|
allTime: {
|
|
5338
5514
|
total: classified.length,
|
|
@@ -5341,6 +5517,12 @@ async function addContext(){
|
|
|
5341
5517
|
owner: classified.filter(e => e.visitorType === 'owner').length,
|
|
5342
5518
|
ci: classified.filter(e => e.visitorType === 'ci').length,
|
|
5343
5519
|
uniqueInstalls: uniqueInstallIds.size,
|
|
5520
|
+
activeInstalls: allTimeActiveInstalls.size,
|
|
5521
|
+
},
|
|
5522
|
+
last30Days: {
|
|
5523
|
+
uniqueInstalls: new Set(last30.filter(e => e.installId).map(e => e.installId)).size,
|
|
5524
|
+
activeInstalls: activeInstalls30d.size,
|
|
5525
|
+
uniqueSessions: uniqueSessions30d.size,
|
|
5344
5526
|
},
|
|
5345
5527
|
last7Days: {
|
|
5346
5528
|
total: recent.length,
|
|
@@ -5349,6 +5531,8 @@ async function addContext(){
|
|
|
5349
5531
|
owner: recent.filter(e => e.visitorType === 'owner').length,
|
|
5350
5532
|
ci: recent.filter(e => e.visitorType === 'ci').length,
|
|
5351
5533
|
uniqueInstalls: recentInstallIds.size,
|
|
5534
|
+
activeInstalls: activeInstalls7d.size,
|
|
5535
|
+
uniqueSessions: uniqueSessions7d.size,
|
|
5352
5536
|
},
|
|
5353
5537
|
byEventType,
|
|
5354
5538
|
byVisitorType,
|
|
@@ -6259,6 +6443,14 @@ async function addContext(){
|
|
|
6259
6443
|
return;
|
|
6260
6444
|
}
|
|
6261
6445
|
|
|
6446
|
+
// Catch non-API GET requests that didn't match any public page route above.
|
|
6447
|
+
// Without this, /about, /docs, /demo etc. fall through to the API auth gate
|
|
6448
|
+
// and return a raw JSON 401 instead of a user-friendly 404.
|
|
6449
|
+
if (isGetLikeRequest && !pathname.startsWith('/v1/') && !pathname.startsWith('/api/')) {
|
|
6450
|
+
sendHtml(res, 404, `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Page Not Found — ThumbGate</title><style>body{background:#0a0a0a;color:#e2e8f0;font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}main{text-align:center;padding:2rem}h1{font-size:4rem;margin:0;color:#22d3ee}p{font-size:1.1rem;color:#94a3b8;margin:1rem 0}a{color:#22d3ee;text-decoration:underline}</style></head><body><main><h1>404</h1><p>This page doesn't exist.</p><p><a href="/">Back to ThumbGate</a></p></main></body></html>`);
|
|
6451
|
+
return;
|
|
6452
|
+
}
|
|
6453
|
+
|
|
6262
6454
|
// Operator key is allowed to bypass the general admin gate for its dedicated endpoint
|
|
6263
6455
|
const _reqToken = extractApiKey(req);
|
|
6264
6456
|
const isOperatorBillingRequest = Boolean(expectedOperatorKey)
|
|
@@ -6840,6 +7032,18 @@ async function addContext(){
|
|
|
6840
7032
|
tags: extractTags(body.tags),
|
|
6841
7033
|
skill: body.skill,
|
|
6842
7034
|
});
|
|
7035
|
+
const actionIntegration = inferActionIntegration(body, req.headers);
|
|
7036
|
+
appendBestEffortTelemetry(requestFeedbackDir, {
|
|
7037
|
+
eventType: chatgptActionEventType(actionIntegration, 'capture_feedback'),
|
|
7038
|
+
clientType: 'api',
|
|
7039
|
+
source: actionIntegration,
|
|
7040
|
+
integration: actionIntegration,
|
|
7041
|
+
actionOperation: 'captureFeedback',
|
|
7042
|
+
endpoint: '/v1/feedback/capture',
|
|
7043
|
+
actionStatus: result.accepted ? 'accepted' : 'clarification_required',
|
|
7044
|
+
accepted: Boolean(result.accepted),
|
|
7045
|
+
failureCode: result.accepted ? null : 'clarification_required',
|
|
7046
|
+
}, req.headers, 'api_action_capture_feedback');
|
|
6843
7047
|
if (result?.accepted) {
|
|
6844
7048
|
// Fan out to any connected dashboard clients so they re-render
|
|
6845
7049
|
// without polling. Non-sensitive summary only (no chat history,
|
|
@@ -7650,6 +7854,18 @@ async function addContext(){
|
|
|
7650
7854
|
});
|
|
7651
7855
|
report.actionId = evaluation.actionId;
|
|
7652
7856
|
if (report.decisionControl) report.decisionControl.actionId = evaluation.actionId;
|
|
7857
|
+
const actionIntegration = inferActionIntegration(body, req.headers);
|
|
7858
|
+
appendBestEffortTelemetry(requestFeedbackDir, {
|
|
7859
|
+
eventType: chatgptActionEventType(actionIntegration, 'evaluate_decision'),
|
|
7860
|
+
clientType: 'api',
|
|
7861
|
+
source: actionIntegration,
|
|
7862
|
+
integration: actionIntegration,
|
|
7863
|
+
actionOperation: 'evaluateDecision',
|
|
7864
|
+
endpoint: '/v1/decisions/evaluate',
|
|
7865
|
+
actionStatus: 'accepted',
|
|
7866
|
+
accepted: true,
|
|
7867
|
+
decisionMode: report.decisionControl && report.decisionControl.executionMode,
|
|
7868
|
+
}, req.headers, 'api_action_evaluate_decision');
|
|
7653
7869
|
sendJson(res, 200, report);
|
|
7654
7870
|
return;
|
|
7655
7871
|
}
|