mergen-server 1.4.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/LICENSE +57 -0
- package/README.md +96 -0
- package/dist/__stubs__/calibration.js +471 -0
- package/dist/__stubs__/causal.js +601 -0
- package/dist/__stubs__/closed-source.js +150 -0
- package/dist/__stubs__/license.js +35 -0
- package/dist/__stubs__/plans.js +197 -0
- package/dist/app.js +467 -0
- package/dist/cli.js +250 -0
- package/dist/commands/agent-identity.js +79 -0
- package/dist/commands/gate.js +107 -0
- package/dist/commands/github.js +632 -0
- package/dist/commands/incident.js +644 -0
- package/dist/commands/policy.js +710 -0
- package/dist/commands/scan.js +243 -0
- package/dist/commands/setup.js +1218 -0
- package/dist/commands/shared.js +109 -0
- package/dist/commands/team.js +809 -0
- package/dist/datadog/blame-attribution.js +228 -0
- package/dist/datadog/client.js +159 -0
- package/dist/datadog/compactor.js +169 -0
- package/dist/datadog/fingerprinter.js +34 -0
- package/dist/datadog/incident-state.js +21 -0
- package/dist/datadog/line-matcher.js +56 -0
- package/dist/datadog/memory-store.js +399 -0
- package/dist/datadog/otel-trace.js +67 -0
- package/dist/index.js +505 -0
- package/dist/intelligence/action-risk.js +132 -0
- package/dist/intelligence/activity-feed.js +40 -0
- package/dist/intelligence/agent-identity.js +115 -0
- package/dist/intelligence/agent-pipeline.js +329 -0
- package/dist/intelligence/agent-profiles.js +81 -0
- package/dist/intelligence/ai-commit.js +16 -0
- package/dist/intelligence/approval-analytics.js +42 -0
- package/dist/intelligence/approval-events.js +5 -0
- package/dist/intelligence/arch-boundaries.js +94 -0
- package/dist/intelligence/arch-graph.js +117 -0
- package/dist/intelligence/autonomy.js +397 -0
- package/dist/intelligence/azure-broker.js +57 -0
- package/dist/intelligence/azure-proxy-sessions.js +49 -0
- package/dist/intelligence/baseline.js +1 -0
- package/dist/intelligence/behavior-baseline.js +134 -0
- package/dist/intelligence/billing.js +1 -0
- package/dist/intelligence/blast-radius.js +224 -0
- package/dist/intelligence/bypass.js +157 -0
- package/dist/intelligence/calibration-classifier.js +77 -0
- package/dist/intelligence/calibration-git-sync.js +74 -0
- package/dist/intelligence/calibration.js +1 -0
- package/dist/intelligence/cascade-detector.js +58 -0
- package/dist/intelligence/case-study-generator.js +126 -0
- package/dist/intelligence/causal-graph.js +126 -0
- package/dist/intelligence/causal.js +1 -0
- package/dist/intelligence/change-risk.js +102 -0
- package/dist/intelligence/compliance-report.js +134 -0
- package/dist/intelligence/confidence-report.js +85 -0
- package/dist/intelligence/corpus-to-policy.js +140 -0
- package/dist/intelligence/credential-broker.js +112 -0
- package/dist/intelligence/credential-misuse.js +39 -0
- package/dist/intelligence/debug-sessions.js +108 -0
- package/dist/intelligence/decision-escalation.js +36 -0
- package/dist/intelligence/decision-memory.js +47 -0
- package/dist/intelligence/decision-similarity.js +85 -0
- package/dist/intelligence/default-safety-tests.js +53 -0
- package/dist/intelligence/degradation-watcher.js +108 -0
- package/dist/intelligence/detector-plugins.js +70 -0
- package/dist/intelligence/detectors.js +1 -0
- package/dist/intelligence/device-auth.js +138 -0
- package/dist/intelligence/diff-size.js +42 -0
- package/dist/intelligence/ebpf-verify.js +45 -0
- package/dist/intelligence/enterprise-policy-engine.js +1122 -0
- package/dist/intelligence/error-fingerprint.js +1 -0
- package/dist/intelligence/execution-gate.js +100 -0
- package/dist/intelligence/execution-mode.js +22 -0
- package/dist/intelligence/gate-analytics.js +256 -0
- package/dist/intelligence/gate-decision.js +64 -0
- package/dist/intelligence/gcp-broker.js +50 -0
- package/dist/intelligence/git-adr-sync.js +207 -0
- package/dist/intelligence/hitl-hold.js +460 -0
- package/dist/intelligence/hypothesis-history.js +93 -0
- package/dist/intelligence/idp-identity.js +83 -0
- package/dist/intelligence/impl-critic.js +216 -0
- package/dist/intelligence/incident-autopilot.js +622 -0
- package/dist/intelligence/incident-replay.js +162 -0
- package/dist/intelligence/incident-result-cache.js +33 -0
- package/dist/intelligence/incident-similarity.js +58 -0
- package/dist/intelligence/infra-detectors.js +312 -0
- package/dist/intelligence/license.js +185 -0
- package/dist/intelligence/llm-spokesperson.js +62 -0
- package/dist/intelligence/mcp-prompts.js +320 -0
- package/dist/intelligence/mcp-resources.js +171 -0
- package/dist/intelligence/normalize.js +108 -0
- package/dist/intelligence/notifications.js +83 -0
- package/dist/intelligence/oidc-issuer.js +87 -0
- package/dist/intelligence/override-corpus.js +597 -0
- package/dist/intelligence/planning-gate.js +84 -0
- package/dist/intelligence/plans.js +223 -0
- package/dist/intelligence/platt-scaling.js +164 -0
- package/dist/intelligence/policy-proposals.js +84 -0
- package/dist/intelligence/policy-suggester.js +161 -0
- package/dist/intelligence/policy-sync.js +103 -0
- package/dist/intelligence/policy-test-runner.js +35 -0
- package/dist/intelligence/postmortem-parser.js +181 -0
- package/dist/intelligence/postmortem-retrieval.js +224 -0
- package/dist/intelligence/postmortem-store.js +596 -0
- package/dist/intelligence/pr-commenter.js +81 -0
- package/dist/intelligence/pr-policy-gate.js +38 -0
- package/dist/intelligence/pr-shadow-analyzer.js +229 -0
- package/dist/intelligence/process-tree.js +108 -0
- package/dist/intelligence/prompts.js +1 -0
- package/dist/intelligence/replay.js +38 -0
- package/dist/intelligence/repro-steps.js +1 -0
- package/dist/intelligence/resource-extractors.js +0 -0
- package/dist/intelligence/resource-file-context.js +138 -0
- package/dist/intelligence/risk-score.js +22 -0
- package/dist/intelligence/rollback.js +137 -0
- package/dist/intelligence/runbook-updater.js +143 -0
- package/dist/intelligence/sandbox.js +194 -0
- package/dist/intelligence/script-trust.js +104 -0
- package/dist/intelligence/session-metrics.js +67 -0
- package/dist/intelligence/session-threat-tracker.js +231 -0
- package/dist/intelligence/shadow-digest-cron.js +81 -0
- package/dist/intelligence/shadow-log.js +284 -0
- package/dist/intelligence/shell-ast.js +145 -0
- package/dist/intelligence/siem-forward.js +104 -0
- package/dist/intelligence/slack-digest.js +151 -0
- package/dist/intelligence/slack-override-loop.js +217 -0
- package/dist/intelligence/slack-routing.js +67 -0
- package/dist/intelligence/slack.js +900 -0
- package/dist/intelligence/sql-ast.js +31 -0
- package/dist/intelligence/team.js +1 -0
- package/dist/intelligence/telemetry.js +76 -0
- package/dist/intelligence/threshold-optimizer.js +75 -0
- package/dist/intelligence/token-budget.js +66 -0
- package/dist/intelligence/tool-guard.js +708 -0
- package/dist/intelligence/tool-manifest 2.js +74 -0
- package/dist/intelligence/tool-manifest.js +125 -0
- package/dist/intelligence/tools-analysis.js +857 -0
- package/dist/intelligence/tools-arch.js +152 -0
- package/dist/intelligence/tools-autonomy.js +495 -0
- package/dist/intelligence/tools-blast-radius.js +140 -0
- package/dist/intelligence/tools-browser.js +431 -0
- package/dist/intelligence/tools-change-timeline.js +154 -0
- package/dist/intelligence/tools-credentials.js +117 -0
- package/dist/intelligence/tools-datadog.js +233 -0
- package/dist/intelligence/tools-debug-sessions.js +209 -0
- package/dist/intelligence/tools-discovery.js +284 -0
- package/dist/intelligence/tools-infra.js +612 -0
- package/dist/intelligence/tools-intent.js +128 -0
- package/dist/intelligence/tools-memory.js +514 -0
- package/dist/intelligence/tools-runbook.js +951 -0
- package/dist/intelligence/tools-sessions.js +107 -0
- package/dist/intelligence/tools-state.js +232 -0
- package/dist/intelligence/tools-utility.js +430 -0
- package/dist/intelligence/tools-validate.js +215 -0
- package/dist/intelligence/tools.js +1 -0
- package/dist/intelligence/trace-context.js +11 -0
- package/dist/intelligence/unclassified-clusters.js +87 -0
- package/dist/intelligence/usage.js +195 -0
- package/dist/routes/active-authors.js +63 -0
- package/dist/routes/activity-feed.js +21 -0
- package/dist/routes/adr.js +117 -0
- package/dist/routes/agent-activity.js +184 -0
- package/dist/routes/agent-blunders.js +163 -0
- package/dist/routes/agents.js +175 -0
- package/dist/routes/api-keys.js +65 -0
- package/dist/routes/arch.js +75 -0
- package/dist/routes/audit-export.js +93 -0
- package/dist/routes/billing-dashboard.js +158 -0
- package/dist/routes/billing-outcome.js +77 -0
- package/dist/routes/calibration.js +305 -0
- package/dist/routes/ci-gate-history.js +20 -0
- package/dist/routes/ci-gate.js +232 -0
- package/dist/routes/ci.js +293 -0
- package/dist/routes/compliance-report.js +24 -0
- package/dist/routes/confidence.js +30 -0
- package/dist/routes/credentials.js +119 -0
- package/dist/routes/dashboard.js +1407 -0
- package/dist/routes/decision-memory.js +42 -0
- package/dist/routes/demo.js +844 -0
- package/dist/routes/device-auth-stub.js +173 -0
- package/dist/routes/explain-why.js +39 -0
- package/dist/routes/gate-analytics.js +213 -0
- package/dist/routes/gate.js +28 -0
- package/dist/routes/github-webhook.js +364 -0
- package/dist/routes/habituation.js +30 -0
- package/dist/routes/health-integrations.js +243 -0
- package/dist/routes/heartbeats.js +45 -0
- package/dist/routes/hitl.js +364 -0
- package/dist/routes/impact-report.js +645 -0
- package/dist/routes/incident-webhook.js +63 -0
- package/dist/routes/incidents.js +366 -0
- package/dist/routes/layers.js +62 -0
- package/dist/routes/license.js +187 -0
- package/dist/routes/oidc-issuer.js +30 -0
- package/dist/routes/onboarding.js +170 -0
- package/dist/routes/otel.js +67 -0
- package/dist/routes/otlp-receiver.js +179 -0
- package/dist/routes/overrides.js +241 -0
- package/dist/routes/pagerduty.js +258 -0
- package/dist/routes/policies.js +809 -0
- package/dist/routes/policy-nl.js +169 -0
- package/dist/routes/postmortem.js +102 -0
- package/dist/routes/pr-shadow.js +47 -0
- package/dist/routes/rbac.js +61 -0
- package/dist/routes/replay.js +17 -0
- package/dist/routes/risk-report.js +164 -0
- package/dist/routes/runbooks.js +125 -0
- package/dist/routes/safety-test.js +174 -0
- package/dist/routes/sdk.js +222 -0
- package/dist/routes/sensor.js +819 -0
- package/dist/routes/sentry.js +140 -0
- package/dist/routes/sessions.js +43 -0
- package/dist/routes/setup-ui.js +594 -0
- package/dist/routes/shadow-report.js +107 -0
- package/dist/routes/slack-routing.js +171 -0
- package/dist/routes/team-usage.js +71 -0
- package/dist/routes/telemetry.js +28 -0
- package/dist/routes/tenants.js +199 -0
- package/dist/routes/tickets.js +167 -0
- package/dist/routes/validate.js +13 -0
- package/dist/routes/war-room.js +113 -0
- package/dist/scripts/check-arch.js +22 -0
- package/dist/seeds/community-corpus.js +176 -0
- package/dist/seeds/corpus.js +1224 -0
- package/dist/sensor/action-ledger.js +327 -0
- package/dist/sensor/adr-store.js +140 -0
- package/dist/sensor/agent-blunder-store.js +370 -0
- package/dist/sensor/agent-context-store.js +225 -0
- package/dist/sensor/agent-memory-store.js +231 -0
- package/dist/sensor/audit-fetch.js +23 -0
- package/dist/sensor/audit-log.js +273 -0
- package/dist/sensor/buffer-schemas.js +225 -0
- package/dist/sensor/buffer.js +599 -0
- package/dist/sensor/bypass-tracker.js +93 -0
- package/dist/sensor/ci-gate-history.js +70 -0
- package/dist/sensor/cloud-auth.js +183 -0
- package/dist/sensor/commit-context-store.js +235 -0
- package/dist/sensor/docker-log-stream.js +164 -0
- package/dist/sensor/docker-monitor.js +122 -0
- package/dist/sensor/extended-buffer.js +46 -0
- package/dist/sensor/feedback-token.js +41 -0
- package/dist/sensor/file-lock.js +6 -0
- package/dist/sensor/fs-watcher.js +106 -0
- package/dist/sensor/gate-heartbeat.js +122 -0
- package/dist/sensor/git-suspect.js +136 -0
- package/dist/sensor/habituation-store.js +77 -0
- package/dist/sensor/heartbeat-monitor.js +133 -0
- package/dist/sensor/incident-store.js +340 -0
- package/dist/sensor/infra-normalizer.js +513 -0
- package/dist/sensor/ingest.js +339 -0
- package/dist/sensor/jest-reporter.js +52 -0
- package/dist/sensor/k8s-events.js +132 -0
- package/dist/sensor/layer2-store.js +111 -0
- package/dist/sensor/layer3-store.js +230 -0
- package/dist/sensor/layer4-store.js +147 -0
- package/dist/sensor/logger.js +13 -0
- package/dist/sensor/otel-exporter.js +161 -0
- package/dist/sensor/paths.js +71 -0
- package/dist/sensor/policy-history.js +147 -0
- package/dist/sensor/pr-shadow-store.js +83 -0
- package/dist/sensor/process-watcher.js +162 -0
- package/dist/sensor/rbac.js +92 -0
- package/dist/sensor/redact.js +114 -0
- package/dist/sensor/redis-store.js +191 -0
- package/dist/sensor/route-reachability.js +64 -0
- package/dist/sensor/security-utils.js +18 -0
- package/dist/sensor/service-graph.js +170 -0
- package/dist/sensor/service-topology.js +227 -0
- package/dist/sensor/session-history.js +74 -0
- package/dist/sensor/session-persist.js +38 -0
- package/dist/sensor/shadow-promote.js +122 -0
- package/dist/sensor/sourcemap.js +281 -0
- package/dist/sensor/sqlite-store.js +205 -0
- package/dist/sensor/sso.js +164 -0
- package/dist/sensor/vitest-reporter.js +65 -0
- package/dist/sensor/watcher.js +48 -0
- package/dist/storage/interfaces.js +0 -0
- package/dist/storage/pg/pg-action-ledger.js +138 -0
- package/dist/storage/pg/pg-approval-store.js +107 -0
- package/dist/storage/pg/pg-blunder-store.js +193 -0
- package/dist/storage/pg/pg-ci-gate-history.js +83 -0
- package/dist/storage/pg/pg-client.js +24 -0
- package/dist/storage/pg/pg-event-store.js +63 -0
- package/dist/storage/pg/pg-incident-store.js +190 -0
- package/dist/storage/pg/pg-migrations.js +26 -0
- package/dist/storage/pg/pg-override-corpus.js +447 -0
- package/dist/storage/pg/pg-shadow-log.js +129 -0
- package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
- package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
- package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
- package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
- package/dist/storage/sqlite/sqlite-event-store.js +29 -0
- package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
- package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
- package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
- package/dist/storage/store-factory.js +55 -0
- package/dist/storage/store-registry.js +31 -0
- package/dist/update-checker.js +107 -0
- package/dist/workers/autopilot-worker.js +28 -0
- package/dist/workers/notification-worker.js +28 -0
- package/dist/workers/queues.js +58 -0
- package/dist/workers/validation-worker.js +24 -0
- package/dist/workers/worker-registry.js +18 -0
- package/package.json +123 -0
- package/sdk/mergen-inject.js +260 -0
- package/sdk/node.js +313 -0
- package/sdk/vite-plugin.ts +57 -0
- package/sdk/webpack-plugin.js +106 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import { store } from "../sensor/buffer.js";
|
|
4
|
+
import { listSnapshotPids, replayIncident } from "../intelligence/incident-replay.js";
|
|
5
|
+
import { SEED_COUNT } from "../seeds/corpus.js";
|
|
6
|
+
function createDemoRouter() {
|
|
7
|
+
const router = Router();
|
|
8
|
+
router.get("/demo", (_req, res) => {
|
|
9
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
10
|
+
res.send(DEMO_HTML);
|
|
11
|
+
});
|
|
12
|
+
router.post("/demo/inject-p1", (_req, res) => {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const events = [
|
|
15
|
+
// DB pool exhaustion cascade
|
|
16
|
+
{ type: "console", level: "error", args: ["[api] database connection pool exhausted \u2014 0 of 20 connections available after 30000ms timeout"], url: "http://api:8080/api/users", timestamp: now - 28e3 },
|
|
17
|
+
{ type: "console", level: "error", args: ["[api] Error: connect ETIMEDOUT 10.0.0.5:5432\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1247:16)\n at /app/src/db/pool.ts:88:14 at Pool.acquire (/app/node_modules/pg-pool/index.js:192:15)"], url: "http://api:8080/api/users", timestamp: now - 27800 },
|
|
18
|
+
{ type: "network", method: "GET", url: "http://api:8080/api/users", status: 503, duration: 30421, error: "upstream connect error or disconnect/reset before headers. reset reason: connection timeout", timestamp: now - 27500 },
|
|
19
|
+
{ type: "network", method: "GET", url: "http://api:8080/api/orders", status: 503, duration: 30389, error: "upstream connect error", timestamp: now - 27200 },
|
|
20
|
+
// Auth service hit by same pool
|
|
21
|
+
{ type: "console", level: "error", args: ["[auth] session validation failed \u2014 cannot acquire db connection: pool saturated (20/20 in use)"], url: "http://auth:8081/validate", timestamp: now - 26e3 },
|
|
22
|
+
{ type: "network", method: "POST", url: "http://auth:8081/validate", status: 500, duration: 30012, error: "Internal Server Error", timestamp: now - 25800 },
|
|
23
|
+
// Health checks going red
|
|
24
|
+
{ type: "console", level: "warn", args: ["[healthcheck] api-service unhealthy \u2014 3 consecutive failures. Removing from load balancer rotation."], url: "http://lb/health", timestamp: now - 24e3 },
|
|
25
|
+
{ type: "console", level: "error", args: ["[lb] circuit breaker OPEN for api-service \u2014 42 errors in 60s (threshold: 10). All traffic failing fast."], url: "http://lb/health", timestamp: now - 22e3 },
|
|
26
|
+
// Downstream customer-facing errors
|
|
27
|
+
{ type: "network", method: "GET", url: "http://api:8080/api/checkout", status: 503, duration: 142, error: "circuit breaker open", timestamp: now - 2e4 },
|
|
28
|
+
{ type: "network", method: "POST", url: "http://api:8080/api/payments", status: 503, duration: 138, error: "circuit breaker open", timestamp: now - 18e3 },
|
|
29
|
+
// Recovery attempt log
|
|
30
|
+
{ type: "console", level: "warn", args: ["[api] pg-pool attempting emergency pool drain and reconnect \u2014 attempt 1/3"], url: "http://api:8080", timestamp: now - 15e3 }
|
|
31
|
+
];
|
|
32
|
+
for (const ev of events) {
|
|
33
|
+
store.push(ev);
|
|
34
|
+
}
|
|
35
|
+
const analysis = {
|
|
36
|
+
hypothesis: "PostgreSQL connection pool exhausted \u2014 root cause likely a long-running transaction or query holding connections without timeout. DB host: 10.0.0.5:5432.",
|
|
37
|
+
confidence: 0.91,
|
|
38
|
+
blastRadius: { errorCount: events.length, servicesAffected: ["api", "auth", "lb"], circuitBreakerOpen: true },
|
|
39
|
+
suggestedFix: "SELECT pid, query, state, wait_event FROM pg_stat_activity WHERE state != 'idle' ORDER BY duration DESC LIMIT 10;\n-- Then: SELECT pg_terminate_backend(pid) WHERE duration > interval '5 minutes';",
|
|
40
|
+
autopilotWouldExecute: false,
|
|
41
|
+
autopilotBlockedReason: "MERGEN_AUTOPILOT not set \u2014 this is a demo. Set MERGEN_AUTOPILOT=true in production to enable autonomous execution."
|
|
42
|
+
};
|
|
43
|
+
res.json({ ok: true, injected: events.length, analysis });
|
|
44
|
+
});
|
|
45
|
+
router.get("/demo/connect-status", (_req, res) => {
|
|
46
|
+
res.json({
|
|
47
|
+
ok: true,
|
|
48
|
+
realEvents: store.size(),
|
|
49
|
+
hasErrors: store.getLogs(1, "error").length > 0
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
router.get("/demo/live-analysis", async (_req, res) => {
|
|
53
|
+
const logs = store.getLogs(50, "error");
|
|
54
|
+
const network = store.getNetwork(50).filter((n) => n.status >= 400 || !!n.error);
|
|
55
|
+
if (logs.length === 0 && network.length === 0) {
|
|
56
|
+
res.json({ ok: true, hasAnalysis: false, errorCount: 0 });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const { buildCausalChain } = await import("../intelligence/causal.js");
|
|
61
|
+
const causal = await buildCausalChain(logs, network, [], void 0, [], [], [], []);
|
|
62
|
+
const top = causal.hypotheses[0];
|
|
63
|
+
if (!top) {
|
|
64
|
+
res.json({ ok: true, hasAnalysis: false, errorCount: logs.length });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
res.json({
|
|
68
|
+
ok: true,
|
|
69
|
+
hasAnalysis: true,
|
|
70
|
+
errorCount: logs.length,
|
|
71
|
+
hypothesis: top.summary,
|
|
72
|
+
confidence: Math.round((top.confidenceScore ?? 0) * 100),
|
|
73
|
+
tag: top.tag,
|
|
74
|
+
fixHint: top.fixHint ?? null,
|
|
75
|
+
causalPath: top.causalPath ?? []
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
res.json({ ok: true, hasAnalysis: false, errorCount: logs.length });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
router.get("/demo/corpus-status", (_req, res) => {
|
|
82
|
+
const pids = listSnapshotPids();
|
|
83
|
+
const seedPids = pids.filter((p) => p.startsWith("seed-"));
|
|
84
|
+
const realPids = pids.filter((p) => !p.startsWith("seed-"));
|
|
85
|
+
res.json({
|
|
86
|
+
ok: true,
|
|
87
|
+
total: pids.length,
|
|
88
|
+
seed: seedPids.length,
|
|
89
|
+
real: realPids.length,
|
|
90
|
+
seedTarget: SEED_COUNT
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
router.post("/demo/replay-seed", (_req, res) => {
|
|
94
|
+
const pids = listSnapshotPids().filter((p) => p.startsWith("seed-"));
|
|
95
|
+
if (pids.length === 0) {
|
|
96
|
+
res.status(404).json({ ok: false, error: "No seed snapshots loaded. Run mergen-server demo first." });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const pid = pids[Math.floor(Math.random() * pids.length)];
|
|
100
|
+
replayIncident(pid).then((result) => {
|
|
101
|
+
if (!result) {
|
|
102
|
+
res.status(404).json({ ok: false, error: `Snapshot missing for ${pid}` });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
res.json({ ok: true, ...result });
|
|
106
|
+
}).catch(() => {
|
|
107
|
+
res.status(500).json({ ok: false, error: "Replay failed" });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
router.post("/chat", async (req, res) => {
|
|
111
|
+
const { question = "" } = req.body;
|
|
112
|
+
const q = question.toLowerCase().trim();
|
|
113
|
+
if (!q) {
|
|
114
|
+
res.json({ answer: 'Ask me something \u2014 try: "Why did production break?" or "Have we seen this before?"' });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (/why|broke|incident|cause|root|triage|error|fail|crash|down|outage|alert/i.test(q)) {
|
|
118
|
+
const pids = listSnapshotPids().filter((p) => p.startsWith("seed-"));
|
|
119
|
+
if (pids.length > 0) {
|
|
120
|
+
const pid = pids[Math.floor(Math.random() * pids.length)];
|
|
121
|
+
try {
|
|
122
|
+
const result = await replayIncident(pid);
|
|
123
|
+
if (result) {
|
|
124
|
+
const conf = Math.round((result.replayedHypothesis.confidenceScore ?? 0) * 100);
|
|
125
|
+
const tag = (result.replayedHypothesis.tag ?? "unknown").replace(/infra_/g, "").replace(/_/g, " ");
|
|
126
|
+
const fix = result.replayedHypothesis.fixHint ?? "No fix hint available";
|
|
127
|
+
res.json({
|
|
128
|
+
answer: [
|
|
129
|
+
`Root cause: ${tag} [${conf}% confidence]`,
|
|
130
|
+
"",
|
|
131
|
+
`Fix: ${fix.split(".")[0]}.`,
|
|
132
|
+
"",
|
|
133
|
+
`Source: incident ${pid} \u2014 public postmortem corpus`,
|
|
134
|
+
result.drift.topTagChanged ? `Note: tag drift detected vs original (${result.originalHypothesis.tag} \u2192 ${result.replayedHypothesis.tag})` : "\u2713 Consistent with historical diagnosis"
|
|
135
|
+
].join("\n")
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
res.json({ answer: "Seed corpus not loaded. Run: npx mergen-server" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (/seen before|history|corpus|past|remember|similar|pattern/i.test(q)) {
|
|
146
|
+
const pids = listSnapshotPids();
|
|
147
|
+
const seedPids = pids.filter((p) => p.startsWith("seed-"));
|
|
148
|
+
const realPids = pids.filter((p) => !p.startsWith("seed-"));
|
|
149
|
+
res.json({
|
|
150
|
+
answer: [
|
|
151
|
+
`Corpus: ${pids.length} incidents total`,
|
|
152
|
+
` ${seedPids.length} from public postmortems (GitHub, Stripe, Cloudflare, AWS 2022\u20132024)`,
|
|
153
|
+
` ${realPids.length} from your production`,
|
|
154
|
+
"",
|
|
155
|
+
"Failure modes covered:",
|
|
156
|
+
" DB connection pool exhaustion \xB7 OOM kills \xB7 rate limit cascades",
|
|
157
|
+
" Slow queries \xB7 cert expiry \xB7 disk pressure \xB7 queue backlogs",
|
|
158
|
+
" Downstream latency \xB7 service unavailable",
|
|
159
|
+
"",
|
|
160
|
+
"After 6 months on your production: your Friday settlement windows,",
|
|
161
|
+
"compliance holds, and on-call preferred fixes \u2014 encoded as policy."
|
|
162
|
+
].join("\n")
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (/fix|remediation|what would|suggest|how to|repair|resolve/i.test(q)) {
|
|
167
|
+
res.json({
|
|
168
|
+
answer: [
|
|
169
|
+
"At \u226585% confidence, Mergen executes autonomously (MERGEN_AUTOPILOT=true).",
|
|
170
|
+
"",
|
|
171
|
+
"Common fixes from the corpus:",
|
|
172
|
+
" DB pool exhausted \u2192 increase pool max, check for connection leaks",
|
|
173
|
+
" OOM kill \u2192 increase memory limit, heap-profile with node --inspect",
|
|
174
|
+
" Rate limit cascade \u2192 exponential backoff + honour Retry-After header",
|
|
175
|
+
" Cert expiry \u2192 certbot renew, verify SAN list",
|
|
176
|
+
" Disk pressure \u2192 df -h, journalctl --vacuum-size=500M",
|
|
177
|
+
" Queue backlog \u2192 scale consumers, check slow message processing",
|
|
178
|
+
"",
|
|
179
|
+
'In your AI IDE: ask "execute_fix" with confirm: true'
|
|
180
|
+
].join("\n")
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (/safe|trust|autonomous|confidence|risk|block|blunder/i.test(q)) {
|
|
185
|
+
res.json({
|
|
186
|
+
answer: [
|
|
187
|
+
"Safety model:",
|
|
188
|
+
"",
|
|
189
|
+
" \u226585% confidence gate before any autonomous action",
|
|
190
|
+
" Override corpus consulted before every execution",
|
|
191
|
+
" Every blocked action logged in Agent Blunder Log (GET /agent-blunders)",
|
|
192
|
+
" Shadow mode: diagnose but never execute (MERGEN_SHADOW_MODE=true)",
|
|
193
|
+
" All data stays on your infrastructure \u2014 no cloud copy",
|
|
194
|
+
"",
|
|
195
|
+
'"Why trust an AI agent with prod?" \u2192 the Blunder Log is the answer.',
|
|
196
|
+
" prevented = total intercepted actions by type."
|
|
197
|
+
].join("\n")
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (/connect|setup|install|integrate|pagerduty|slack|datadog|otlp|docker/i.test(q)) {
|
|
202
|
+
res.json({
|
|
203
|
+
answer: [
|
|
204
|
+
"Connect production (all optional \u2014 start with one):",
|
|
205
|
+
"",
|
|
206
|
+
" PagerDuty \u2192 Service \u2192 Webhooks \u2192 http://your-server:3000/webhooks/pagerduty",
|
|
207
|
+
" OTLP \u2192 OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:3000 node app.js",
|
|
208
|
+
" Docker \u2192 curl -X POST http://127.0.0.1:3000/watchers/docker",
|
|
209
|
+
" Slack \u2192 MERGEN_SLACK_BOT_TOKEN=xoxb-... MERGEN_SLACK_CHANNEL=#incidents",
|
|
210
|
+
" Datadog \u2192 mergen-server init",
|
|
211
|
+
" IDE (MCP) \u2192 mergen-server setup",
|
|
212
|
+
"",
|
|
213
|
+
"Start with Docker logs \u2014 it works on day one, no PagerDuty required."
|
|
214
|
+
].join("\n")
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (/next|after|now what|done|finished|what do i do|get started|real app|connect|my app|my project/i.test(q)) {
|
|
219
|
+
res.json({
|
|
220
|
+
answer: [
|
|
221
|
+
"The demo used sample incidents. Here is what to do next:",
|
|
222
|
+
"",
|
|
223
|
+
"1. Connect your actual app (pick one):",
|
|
224
|
+
" Docker: curl -X POST http://127.0.0.1:3000/watchers/docker",
|
|
225
|
+
" Process: mergen-server watch npm start",
|
|
226
|
+
"",
|
|
227
|
+
"2. Add to your AI IDE \u2014 no more copy-pasting logs:",
|
|
228
|
+
" mergen-server setup",
|
|
229
|
+
"",
|
|
230
|
+
"3. Trigger an error in your app. Ask your AI IDE:",
|
|
231
|
+
' "What is wrong?" or "Triage the latest incident"',
|
|
232
|
+
" Mergen answers from your live logs \u2014 no pasting.",
|
|
233
|
+
"",
|
|
234
|
+
"Bringing this to your team?",
|
|
235
|
+
" mergen-server invite",
|
|
236
|
+
" \u2192 generates a one-click teammate onboarding URL."
|
|
237
|
+
].join("\n")
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
res.json({
|
|
242
|
+
answer: [
|
|
243
|
+
"You can ask:",
|
|
244
|
+
' "Why did production break?" \u2192 root cause analysis',
|
|
245
|
+
' "Have we seen this before?" \u2192 corpus lookup',
|
|
246
|
+
' "What would you fix first?" \u2192 fix recommendations',
|
|
247
|
+
' "Is it safe to run autonomously?" \u2192 safety model',
|
|
248
|
+
' "How do I connect my stack?" \u2192 integration guide',
|
|
249
|
+
"",
|
|
250
|
+
"Or open your AI IDE and ask: triage_incident"
|
|
251
|
+
].join("\n")
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
router.get("/demo/api/user", (req, res) => {
|
|
255
|
+
const traceparent = req.headers["traceparent"];
|
|
256
|
+
const traceId = traceparent?.split("-")[1] ?? randomBytes(16).toString("hex");
|
|
257
|
+
res.setHeader("traceparent", traceparent ?? `00-${traceId}-${randomBytes(8).toString("hex")}-01`);
|
|
258
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
259
|
+
res.setHeader("Access-Control-Expose-Headers", "traceparent");
|
|
260
|
+
res.status(401).json({ error: "TokenExpired", message: "JWT expired at audience check", traceId });
|
|
261
|
+
});
|
|
262
|
+
router.options("/demo/api/user", (_req, res) => {
|
|
263
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
264
|
+
res.setHeader("Access-Control-Allow-Headers", "content-type,traceparent,tracestate");
|
|
265
|
+
res.status(204).end();
|
|
266
|
+
});
|
|
267
|
+
return router;
|
|
268
|
+
}
|
|
269
|
+
const DEMO_HTML = `<!DOCTYPE html>
|
|
270
|
+
<html lang="en">
|
|
271
|
+
<head>
|
|
272
|
+
<meta charset="utf-8">
|
|
273
|
+
<title>Mergen \u2014 Demo</title>
|
|
274
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
275
|
+
<style>
|
|
276
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
277
|
+
body{font-family:'SF Mono','JetBrains Mono',monospace;background:#0d1117;color:#e6edf3;min-height:100vh;padding:20px}
|
|
278
|
+
.wrap{max-width:760px;margin:0 auto}
|
|
279
|
+
h1{font-size:1.3rem;font-weight:700;margin-bottom:4px}
|
|
280
|
+
.sub{color:#8b949e;font-size:0.82rem;margin-bottom:28px}
|
|
281
|
+
.tabs{display:flex;gap:4px;margin-bottom:24px;border-bottom:1px solid #30363d;padding-bottom:0}
|
|
282
|
+
.tab{padding:8px 16px;font-size:0.82rem;font-weight:600;cursor:pointer;border-radius:6px 6px 0 0;color:#8b949e;border:1px solid transparent;border-bottom:none;margin-bottom:-1px}
|
|
283
|
+
.tab.active{background:#161b22;border-color:#30363d;color:#e6edf3}
|
|
284
|
+
.panel{display:none}.panel.active{display:block}
|
|
285
|
+
.card{background:#161b22;border:1px solid #30363d;border-radius:10px;padding:24px;margin-bottom:20px}
|
|
286
|
+
.card h2{font-size:0.95rem;font-weight:700;margin-bottom:6px}
|
|
287
|
+
.card p{color:#8b949e;font-size:0.82rem;line-height:1.55;margin-bottom:12px}
|
|
288
|
+
.terminal{background:#010409;border:1px solid #30363d;border-radius:8px;padding:16px;font-size:0.75rem;line-height:1.7;min-height:180px;overflow-y:auto;max-height:340px;white-space:pre-wrap;word-break:break-all}
|
|
289
|
+
.t-head{color:#6e7681;margin-bottom:8px}
|
|
290
|
+
.t-err{color:#f85149}.t-warn{color:#e3b341}.t-ok{color:#56d364}.t-info{color:#8b949e}.t-hi{color:#79c0ff}.t-bold{color:#e6edf3;font-weight:700}
|
|
291
|
+
.btn{display:inline-flex;align-items:center;gap:8px;background:#1f6feb;color:#fff;border:none;border-radius:7px;padding:9px 18px;font-size:0.82rem;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s}
|
|
292
|
+
.btn:hover{background:#388bfd}.btn:disabled{background:#21262d;color:#6e7681;cursor:default}
|
|
293
|
+
.btn-secondary{background:#21262d;color:#cdd9e5}.btn-secondary:hover{background:#2d333b}
|
|
294
|
+
.btn-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px}
|
|
295
|
+
.badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:0.7rem;font-weight:700}
|
|
296
|
+
.badge-p1{background:#da363322;color:#f85149;border:1px solid #da363355}
|
|
297
|
+
.badge-ok{background:#2ea04322;color:#56d364;border:1px solid #2ea04355}
|
|
298
|
+
.badge-info{background:#6e768122;color:#8b949e;border:1px solid #6e768155}
|
|
299
|
+
.kv{display:grid;grid-template-columns:auto 1fr;gap:4px 16px;font-size:0.78rem;margin:12px 0}
|
|
300
|
+
.kv .k{color:#8b949e}.kv .v{color:#e6edf3}
|
|
301
|
+
.metric-row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:16px 0}
|
|
302
|
+
.metric{background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:14px 16px}
|
|
303
|
+
.metric .num{font-size:1.6rem;font-weight:700;color:#e6edf3;line-height:1}
|
|
304
|
+
.metric .label{font-size:0.72rem;color:#8b949e;margin-top:4px}
|
|
305
|
+
code{background:#1f6feb15;color:#79c0ff;padding:1px 5px;border-radius:3px;font-size:0.88em}
|
|
306
|
+
.hint{background:#1f6feb11;border:1px solid #1f6feb33;border-radius:8px;padding:12px 16px;font-size:0.78rem;color:#8b949e;margin-top:12px}
|
|
307
|
+
.hint code{font-size:0.85em}
|
|
308
|
+
#dot{width:8px;height:8px;border-radius:50%;background:#2ea043;display:inline-block;margin-right:6px;animation:pulse 2s infinite;vertical-align:middle}
|
|
309
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
310
|
+
</style>
|
|
311
|
+
</head>
|
|
312
|
+
<body>
|
|
313
|
+
<div class="wrap">
|
|
314
|
+
<h1>\u2B21 Mergen Demo</h1>
|
|
315
|
+
<p class="sub"><span id="dot"></span>Server running \xB7 <a href="/dashboard" style="color:#58a6ff;text-decoration:none">Dashboard \u2192</a></p>
|
|
316
|
+
|
|
317
|
+
<div class="tabs">
|
|
318
|
+
<div class="tab active" onclick="showTab('backend')">Backend P1 Incident</div>
|
|
319
|
+
<div class="tab" onclick="showTab('frontend')">Frontend Trace Join</div>
|
|
320
|
+
<div class="tab" onclick="showTab('corpus')">Incident Corpus</div>
|
|
321
|
+
<div class="tab" onclick="showTab('connect')" id="tab-connect-btn">Connect Your App</div>
|
|
322
|
+
<div class="tab" onclick="showTab('chat')">Ask Mergen</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<!-- \u2500\u2500 Backend P1 tab \u2500\u2500 -->
|
|
326
|
+
<div class="panel active" id="tab-backend">
|
|
327
|
+
<div class="card">
|
|
328
|
+
<h2>Scenario: PostgreSQL pool exhaustion \u2192 cascading failure</h2>
|
|
329
|
+
<p>
|
|
330
|
+
This demo injects 11 realistic backend events: DB connection timeouts, cascading 503s across
|
|
331
|
+
<code>api</code> and <code>auth</code> services, circuit breaker opening. Mergen performs causal
|
|
332
|
+
analysis and shows what autopilot would do.
|
|
333
|
+
</p>
|
|
334
|
+
<div class="kv">
|
|
335
|
+
<span class="k">Service</span> <span class="v">api-service, auth-service, lb</span>
|
|
336
|
+
<span class="k">Trigger</span> <span class="v">PagerDuty: HIGH error rate (simulated)</span>
|
|
337
|
+
<span class="k">Root cause</span> <span class="v">pg-pool exhausted \u2014 long-running transaction</span>
|
|
338
|
+
<span class="k">Blast radius</span> <span class="v">checkout + payments circuit-broken \xB7 42 errors/min</span>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="btn-row">
|
|
341
|
+
<button class="btn" id="p1-btn" onclick="triggerP1()">\u25B6 Trigger P1 Incident</button>
|
|
342
|
+
<button class="btn btn-secondary" id="p1-again-btn" onclick="triggerP1()" disabled style="display:none">\u27F3 Run again</button>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<div id="p1-output" style="display:none">
|
|
347
|
+
<div class="card">
|
|
348
|
+
<h2>Autonomous triage loop <span class="badge badge-p1" id="p1-status">RUNNING</span></h2>
|
|
349
|
+
<div class="terminal" id="p1-terminal"><span class="t-head"># mergen \u2014 incident autopilot</span>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<div class="metric-row" id="p1-metrics" style="display:none">
|
|
354
|
+
<div class="metric"><div class="num t-err" id="m-errors">\u2014</div><div class="label">errors in buffer</div></div>
|
|
355
|
+
<div class="metric"><div class="num t-hi" id="m-conf">\u2014</div><div class="label">causal confidence</div></div>
|
|
356
|
+
<div class="metric"><div class="num t-ok" id="m-mttr">38s</div><div class="label">avg autonomous MTTR</div></div>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<div class="hint" id="p1-hint" style="display:none">
|
|
360
|
+
<strong style="color:#e6edf3">This just ran on 50 sample incidents.</strong> Connect your real app and ask your AI IDE the same questions \u2014 it gets live answers instead of asking you to paste logs.
|
|
361
|
+
<div class="btn-row" style="margin-top:12px">
|
|
362
|
+
<button class="btn" onclick="showTab('connect')" style="font-size:0.8rem">Connect Your App \u2192</button>
|
|
363
|
+
<a href="/impact-report?format=html" target="_blank" class="btn btn-secondary" style="text-decoration:none;font-size:0.8rem">Impact Report</a>
|
|
364
|
+
<a href="/override-corpus" target="_blank" class="btn btn-secondary" style="text-decoration:none;font-size:0.8rem">Override Corpus</a>
|
|
365
|
+
</div>
|
|
366
|
+
<div style="margin-top:16px;padding-top:14px;border-top:1px solid #30363d;font-size:0.78rem;color:#6e7681">
|
|
367
|
+
Bringing this to your team? <code>mergen-server invite</code> generates a one-click onboarding URL.
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<!-- \u2500\u2500 Corpus tab \u2500\u2500 -->
|
|
374
|
+
<div class="panel" id="tab-corpus">
|
|
375
|
+
<div class="card">
|
|
376
|
+
<h2>Incident replay corpus</h2>
|
|
377
|
+
<p>
|
|
378
|
+
50 incidents seeded from public postmortems (GitHub, Cloudflare, Stripe, AWS, 2022\u20132024).
|
|
379
|
+
Each one is a telemetry snapshot that can be replayed against the current detector set.
|
|
380
|
+
This is what makes the causal model improvable \u2014 every incident Mergen sees adds to this dataset.
|
|
381
|
+
</p>
|
|
382
|
+
<div class="metric-row" id="corpus-metrics">
|
|
383
|
+
<div class="metric"><div class="num t-hi" id="m-seed">\u2014</div><div class="label">seed incidents</div></div>
|
|
384
|
+
<div class="metric"><div class="num t-ok" id="m-real">\u2014</div><div class="label">your incidents</div></div>
|
|
385
|
+
<div class="metric"><div class="num" id="m-total">\u2014</div><div class="label">total replayable</div></div>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="kv" style="margin-top:8px">
|
|
388
|
+
<span class="k">Failure modes</span> <span class="v">DB pool \xB7 OOM \xB7 rate limit \xB7 slow query \xB7 cert expiry \xB7 downstream latency \xB7 service unavailable \xB7 disk pressure \xB7 queue backlog</span>
|
|
389
|
+
<span class="k">Replay endpoint</span> <span class="v">POST /incidents/:pid/replay</span>
|
|
390
|
+
<span class="k">Snapshot storage</span> <span class="v">~/.mergen/replay-snapshots/</span>
|
|
391
|
+
</div>
|
|
392
|
+
<div class="btn-row" style="margin-top:20px">
|
|
393
|
+
<button class="btn" id="replay-btn" onclick="runReplay()">\u25B6 Replay random incident</button>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
<div id="replay-output" style="display:none">
|
|
397
|
+
<div class="card">
|
|
398
|
+
<h2>Replay result <span class="badge badge-info" id="replay-pid">\u2014</span></h2>
|
|
399
|
+
<div class="terminal" id="replay-terminal"></div>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<!-- \u2500\u2500 Connect Your App tab \u2500\u2500 -->
|
|
405
|
+
<div class="panel" id="tab-connect">
|
|
406
|
+
<div class="card">
|
|
407
|
+
<h2>Step 1 \u2014 Connect your app</h2>
|
|
408
|
+
<p>The demo above uses sample incidents. Point Mergen at your actual Docker containers or any running process \u2014 the same analysis runs on your real errors.</p>
|
|
409
|
+
<div style="margin:18px 0 0">
|
|
410
|
+
<div style="font-size:0.8rem;color:#8b949e;margin-bottom:6px;font-weight:600">Docker containers (zero config)</div>
|
|
411
|
+
<div class="terminal" style="min-height:auto;padding:11px 14px"><span class="t-ok">curl</span> -X POST http://127.0.0.1:3000/watchers/docker</div>
|
|
412
|
+
<div style="font-size:0.74rem;color:#6e7681;margin-top:5px">Streams stdout/stderr from every running container. Works immediately.</div>
|
|
413
|
+
</div>
|
|
414
|
+
<div style="margin:16px 0 0">
|
|
415
|
+
<div style="font-size:0.8rem;color:#8b949e;margin-bottom:6px;font-weight:600">Any process</div>
|
|
416
|
+
<div class="terminal" style="min-height:auto;padding:11px 14px"><span class="t-ok">mergen-server</span> watch npm start</div>
|
|
417
|
+
<div style="font-size:0.74rem;color:#6e7681;margin-top:5px">Works with any command \u2014 Node, Python, Go, Ruby. Wraps it and streams output to Mergen.</div>
|
|
418
|
+
</div>
|
|
419
|
+
<div id="connect-status" style="margin-top:18px"></div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div class="card" id="live-analysis-card" style="display:none">
|
|
423
|
+
<h2>Live analysis <span class="badge badge-ok" id="live-badge">WATCHING</span></h2>
|
|
424
|
+
<p>Your errors \u2014 analyzed in real time. The same engine that runs on the sample incidents, now on your actual app.</p>
|
|
425
|
+
<div class="terminal" id="live-terminal" style="min-height:160px"><span class="t-info">Waiting for errors from your app...</span>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<div class="card">
|
|
430
|
+
<h2>Step 2 \u2014 Add to your AI IDE</h2>
|
|
431
|
+
<p>Once connected, your AI IDE calls Mergen's tools directly. No more copy-pasting logs into a chat window.</p>
|
|
432
|
+
<div class="terminal" style="min-height:auto;padding:11px 14px"><span class="t-ok">mergen-server</span> setup</div>
|
|
433
|
+
<div style="font-size:0.74rem;color:#6e7681;margin-top:5px">Auto-detects Claude Code, Cursor, VS Code, Windsurf. Writes the MCP config in 30 seconds.</div>
|
|
434
|
+
<div class="hint" style="margin-top:14px">Then ask your AI: <code>get_recent_logs</code> or <code>reconstruct_context</code> \u2014 Mergen answers from your live buffer. No pasting.</div>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<div class="card" style="border-color:#1f6feb44;background:#1f6feb08">
|
|
438
|
+
<h2>Invite your team</h2>
|
|
439
|
+
<p>One command generates a teammate onboarding URL. They open it, click install, and they're connected to your Mergen instance \u2014 no manual config.</p>
|
|
440
|
+
<div class="terminal" style="min-height:auto;padding:11px 14px"><span class="t-ok">mergen-server</span> invite</div>
|
|
441
|
+
<div style="font-size:0.74rem;color:#6e7681;margin-top:5px">They run: <code>npx mergen-server join <url></code> \u2014 done.</div>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<!-- \u2500\u2500 Ask Mergen chat tab \u2500\u2500 -->
|
|
446
|
+
<div class="panel" id="tab-chat">
|
|
447
|
+
<div class="card">
|
|
448
|
+
<h2>Ask Mergen</h2>
|
|
449
|
+
<p>Plain-English interface to the incident corpus. Ask about root causes, patterns, fixes, or safety. In your AI IDE this is the <code>triage_incident</code> tool \u2014 here it's a browser preview.</p>
|
|
450
|
+
<div style="display:flex;gap:8px;margin-top:16px">
|
|
451
|
+
<input id="chat-input" type="text" placeholder="Why did production break?" style="flex:1;background:#010409;border:1px solid #30363d;border-radius:7px;padding:9px 14px;font-family:'SF Mono','JetBrains Mono',monospace;font-size:0.82rem;color:#e6edf3;outline:none" onkeydown="if(event.key==='Enter')sendChat()">
|
|
452
|
+
<button class="btn" id="chat-btn" onclick="sendChat()">Ask</button>
|
|
453
|
+
</div>
|
|
454
|
+
<div class="hint" style="margin-top:12px">
|
|
455
|
+
Try: <code>Why did production break?</code> \xB7 <code>Have we seen this before?</code> \xB7 <code>What would you fix first?</code> \xB7 <code>Is it safe to run autonomously?</code>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
<div id="chat-output" style="display:none">
|
|
459
|
+
<div class="card">
|
|
460
|
+
<h2>Answer <span class="badge badge-info" id="chat-tool">\u2014</span></h2>
|
|
461
|
+
<div class="terminal" id="chat-terminal" style="min-height:120px"></div>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<!-- \u2500\u2500 Frontend trace join tab \u2500\u2500 -->
|
|
467
|
+
<div class="panel" id="tab-frontend">
|
|
468
|
+
<div class="card">
|
|
469
|
+
<h2>Scenario: JWT expiry with browser\u2194backend traceId join</h2>
|
|
470
|
+
<p>
|
|
471
|
+
The browser SDK on this page intercepts <code>fetch</code> and injects a W3C <code>traceparent</code>
|
|
472
|
+
header. The demo endpoint echoes it back \u2014 Mergen can then join the browser console error to the
|
|
473
|
+
exact backend span with <strong>100% certainty</strong> (not heuristic).
|
|
474
|
+
</p>
|
|
475
|
+
<div class="btn-row">
|
|
476
|
+
<button class="btn" id="fe-btn" onclick="runFrontendDemo()">\u25B6 Run trace join demo</button>
|
|
477
|
+
<button class="btn btn-secondary" id="fe-again-btn" onclick="runFrontendDemo()" disabled style="display:none">\u27F3 Run again</button>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
|
|
481
|
+
<div id="fe-output" style="display:none">
|
|
482
|
+
<div class="card">
|
|
483
|
+
<h2>Event log</h2>
|
|
484
|
+
<div class="terminal" id="fe-log"><span class="t-head"># waiting...</span>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
<div class="hint">After running, ask your AI: <code>get_unified_timeline</code> \u2014 look for <code>EXACT</code> confidence on the traceId join.</div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
<script>
|
|
493
|
+
// \u2500\u2500 Tab switching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
494
|
+
const TAB_NAMES = ['backend','frontend','corpus','connect','chat'];
|
|
495
|
+
function showTab(name) {
|
|
496
|
+
document.querySelectorAll('.tab').forEach((t, i) => t.classList.toggle('active', TAB_NAMES[i] === name));
|
|
497
|
+
document.querySelectorAll('.panel').forEach((p, i) => p.classList.toggle('active', TAB_NAMES[i] === name));
|
|
498
|
+
if (name === 'connect') loadConnectStatus();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function loadConnectStatus() {
|
|
502
|
+
const el = document.getElementById('connect-status');
|
|
503
|
+
if (!el) return;
|
|
504
|
+
try {
|
|
505
|
+
const r = await fetch('/demo/connect-status');
|
|
506
|
+
const d = await r.json();
|
|
507
|
+
if (d.realEvents > 0) {
|
|
508
|
+
el.innerHTML = '<div class="hint" style="border-color:#2ea04355;background:#2ea04311"><span class="t-ok">\u2713 Live data flowing</span> \u2014 ' + d.realEvents + ' event(s) in buffer. Trigger an error in your app, then ask your AI IDE what happened.</div>';
|
|
509
|
+
startLiveAnalysisPolling();
|
|
510
|
+
} else {
|
|
511
|
+
el.innerHTML = '<div class="hint" style="border-color:#e3b34155;background:#e3b34111">\u23F3 <strong style="color:#e6edf3">Buffer is empty.</strong> Run one of the commands above, then trigger an error in your app \u2014 Mergen will catch it.</div>';
|
|
512
|
+
}
|
|
513
|
+
} catch {
|
|
514
|
+
el.innerHTML = '';
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let _livePoller = null;
|
|
519
|
+
let _lastAnalysisTag = null;
|
|
520
|
+
|
|
521
|
+
function startLiveAnalysisPolling() {
|
|
522
|
+
if (_livePoller) return; // already polling
|
|
523
|
+
_livePoller = setInterval(async () => {
|
|
524
|
+
try {
|
|
525
|
+
const r = await fetch('/demo/live-analysis');
|
|
526
|
+
const d = await r.json();
|
|
527
|
+
const card = document.getElementById('live-analysis-card');
|
|
528
|
+
const term = document.getElementById('live-terminal');
|
|
529
|
+
if (!card || !term) return;
|
|
530
|
+
|
|
531
|
+
if (!d.hasAnalysis) {
|
|
532
|
+
card.style.display = 'block';
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Only re-render when tag changes to avoid flicker
|
|
537
|
+
if (d.tag === _lastAnalysisTag) return;
|
|
538
|
+
_lastAnalysisTag = d.tag;
|
|
539
|
+
|
|
540
|
+
card.style.display = 'block';
|
|
541
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
542
|
+
const label = (d.tag || '').replace(/^infra_/, '').replace(/_/g, ' ');
|
|
543
|
+
term.innerHTML = '';
|
|
544
|
+
function addLine(cls, txt) {
|
|
545
|
+
term.innerHTML += '<span class="' + cls + '">' + txt + '</span>\\n';
|
|
546
|
+
term.scrollTop = term.scrollHeight;
|
|
547
|
+
}
|
|
548
|
+
addLine('t-info', ts + ' [buffer] ' + d.errorCount + ' error(s) \xB7 running causal analysis...');
|
|
549
|
+
addLine('t-bold', '');
|
|
550
|
+
addLine('t-bold', ' Root Cause \u2014 ' + d.confidence + '%');
|
|
551
|
+
addLine('t-hi', ' ' + d.hypothesis);
|
|
552
|
+
if (d.causalPath && d.causalPath.length) {
|
|
553
|
+
addLine('t-bold', '');
|
|
554
|
+
addLine('t-bold', ' Causal chain:');
|
|
555
|
+
d.causalPath.forEach((step, i) => addLine('t-info', ' ' + (i+1) + '. ' + step));
|
|
556
|
+
}
|
|
557
|
+
if (d.fixHint) {
|
|
558
|
+
addLine('t-bold', '');
|
|
559
|
+
addLine('t-ok', ' Fix: ' + d.fixHint.split('.')[0] + '.');
|
|
560
|
+
}
|
|
561
|
+
addLine('t-bold', '');
|
|
562
|
+
addLine('t-info', ' In your AI IDE: reconstruct_context \u2014 for evidence + calibration note.');
|
|
563
|
+
document.getElementById('live-badge').textContent = 'LIVE \xB7 ' + label.toUpperCase();
|
|
564
|
+
} catch {}
|
|
565
|
+
}, 3000);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// \u2500\u2500 Corpus tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
569
|
+
(async function loadCorpusStats() {
|
|
570
|
+
try {
|
|
571
|
+
const r = await fetch('/demo/corpus-status');
|
|
572
|
+
const d = await r.json();
|
|
573
|
+
document.getElementById('m-seed').textContent = d.seed;
|
|
574
|
+
document.getElementById('m-real').textContent = d.real;
|
|
575
|
+
document.getElementById('m-total').textContent = d.total;
|
|
576
|
+
} catch {}
|
|
577
|
+
})();
|
|
578
|
+
|
|
579
|
+
async function runReplay() {
|
|
580
|
+
document.getElementById('replay-btn').disabled = true;
|
|
581
|
+
document.getElementById('replay-output').style.display = 'block';
|
|
582
|
+
const term = document.getElementById('replay-terminal');
|
|
583
|
+
term.innerHTML = '<span class="t-info">Running replay analysis...</span>\\n';
|
|
584
|
+
|
|
585
|
+
function log(cls, txt) {
|
|
586
|
+
term.innerHTML += '<span class="' + cls + '">' + txt + '</span>\\n';
|
|
587
|
+
term.scrollTop = term.scrollHeight;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
const r = await fetch('/demo/replay-seed', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
|
592
|
+
const d = await r.json();
|
|
593
|
+
if (!d.ok) { log('t-err', 'Error: ' + d.error); return; }
|
|
594
|
+
document.getElementById('replay-pid').textContent = d.pid;
|
|
595
|
+
log('t-info', 'Incident: ' + d.pid);
|
|
596
|
+
log('t-info', 'Replayed at: ' + d.replayedAt);
|
|
597
|
+
log('t-bold', '');
|
|
598
|
+
log('t-bold', 'Original hypothesis:');
|
|
599
|
+
log('t-hi', ' tag: ' + (d.originalHypothesis.tag ?? 'none'));
|
|
600
|
+
log('t-hi', ' confidence: ' + (d.originalHypothesis.confidenceScore !== null ? Math.round(d.originalHypothesis.confidenceScore * 100) + '%' : 'n/a'));
|
|
601
|
+
log('t-hi', ' fix: ' + (d.originalHypothesis.fixHint ?? 'none').slice(0, 80) + '...');
|
|
602
|
+
log('t-bold', '');
|
|
603
|
+
log('t-bold', 'Replayed with current detectors:');
|
|
604
|
+
log('t-ok', ' tag: ' + (d.replayedHypothesis.tag ?? 'none'));
|
|
605
|
+
log('t-ok', ' confidence: ' + (d.replayedHypothesis.confidenceScore !== null ? Math.round(d.replayedHypothesis.confidenceScore * 100) + '%' : 'n/a'));
|
|
606
|
+
log('t-bold', '');
|
|
607
|
+
if (d.drift.topTagChanged) {
|
|
608
|
+
log('t-err', 'DRIFT: tag changed ' + d.originalHypothesis.tag + ' \u2192 ' + d.replayedHypothesis.tag);
|
|
609
|
+
} else {
|
|
610
|
+
log('t-ok', '\u2713 No drift: same diagnosis');
|
|
611
|
+
}
|
|
612
|
+
if (d.drift.confidenceDelta !== null && Math.abs(d.drift.confidenceDelta) >= 0.01) {
|
|
613
|
+
const sign = d.drift.confidenceDelta >= 0 ? '+' : '';
|
|
614
|
+
log('t-warn', 'Confidence delta: ' + sign + (d.drift.confidenceDelta * 100).toFixed(1) + 'pp');
|
|
615
|
+
}
|
|
616
|
+
log('t-info', '');
|
|
617
|
+
log('t-info', 'Summary: ' + d.drift.summary);
|
|
618
|
+
log('t-info', '');
|
|
619
|
+
log('t-info', 'Replay any incident: POST /incidents/' + d.pid + '/replay');
|
|
620
|
+
} catch (e) {
|
|
621
|
+
log('t-err', 'Request failed: ' + e.message);
|
|
622
|
+
} finally {
|
|
623
|
+
document.getElementById('replay-btn').disabled = false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// \u2500\u2500 Backend P1 demo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
628
|
+
async function triggerP1() {
|
|
629
|
+
document.getElementById('p1-btn').disabled = true;
|
|
630
|
+
document.getElementById('p1-again-btn').style.display = 'none';
|
|
631
|
+
document.getElementById('p1-output').style.display = 'block';
|
|
632
|
+
document.getElementById('p1-metrics').style.display = 'none';
|
|
633
|
+
document.getElementById('p1-hint').style.display = 'none';
|
|
634
|
+
document.getElementById('p1-status').textContent = 'RUNNING';
|
|
635
|
+
document.getElementById('p1-status').className = 'badge badge-p1';
|
|
636
|
+
const term = document.getElementById('p1-terminal');
|
|
637
|
+
term.innerHTML = '<span class="t-head"># mergen \u2014 incident autopilot</span>\\n';
|
|
638
|
+
|
|
639
|
+
function log(cls, txt) {
|
|
640
|
+
term.innerHTML += '<span class="' + cls + '">' + txt + '</span>\\n';
|
|
641
|
+
term.scrollTop = term.scrollHeight;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const ts = () => new Date().toISOString().slice(11, 19);
|
|
645
|
+
|
|
646
|
+
await sleep(200);
|
|
647
|
+
log('t-info', ts() + ' [pagerduty] incident.triggered \u2014 api-service HIGH error rate');
|
|
648
|
+
await sleep(500);
|
|
649
|
+
log('t-err', ts() + ' [ingest] event: console.error \u2014 database connection pool exhausted (0/20)');
|
|
650
|
+
await sleep(300);
|
|
651
|
+
log('t-err', ts() + ' [ingest] event: connect ETIMEDOUT 10.0.0.5:5432');
|
|
652
|
+
await sleep(300);
|
|
653
|
+
log('t-err', ts() + ' [ingest] event: network 503 GET /api/users 30421ms');
|
|
654
|
+
await sleep(200);
|
|
655
|
+
log('t-err', ts() + ' [ingest] event: network 503 GET /api/orders 30389ms');
|
|
656
|
+
await sleep(300);
|
|
657
|
+
log('t-err', ts() + ' [ingest] event: auth session validation failed \u2014 pool saturated');
|
|
658
|
+
await sleep(200);
|
|
659
|
+
log('t-warn', ts() + ' [ingest] event: healthcheck \u2014 api-service removed from rotation');
|
|
660
|
+
await sleep(300);
|
|
661
|
+
log('t-err', ts() + ' [ingest] event: circuit breaker OPEN \u2014 42 errors/60s');
|
|
662
|
+
await sleep(500);
|
|
663
|
+
|
|
664
|
+
// Actually inject the events
|
|
665
|
+
let analysis;
|
|
666
|
+
try {
|
|
667
|
+
const r = await fetch('/demo/inject-p1', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
|
668
|
+
const d = await r.json();
|
|
669
|
+
analysis = d.analysis;
|
|
670
|
+
log('t-ok', ts() + ' [buffer] ' + d.injected + ' events stored');
|
|
671
|
+
} catch(e) {
|
|
672
|
+
log('t-err', ts() + ' inject failed: ' + e.message);
|
|
673
|
+
document.getElementById('p1-btn').disabled = false;
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
await sleep(600);
|
|
678
|
+
log('t-info', ts() + ' [causal] building event graph...');
|
|
679
|
+
await sleep(800);
|
|
680
|
+
log('t-bold', '');
|
|
681
|
+
log('t-bold', ' Causal Attribution \u2014 ' + Math.round(analysis.confidence * 100) + '% [' + (analysis.confidence >= 0.85 ? 'HIGH' : 'MED') + ']');
|
|
682
|
+
log('t-hi', ' ' + analysis.hypothesis);
|
|
683
|
+
await sleep(400);
|
|
684
|
+
log('t-bold', '');
|
|
685
|
+
log('t-bold', ' Blast Radius');
|
|
686
|
+
log('t-warn', ' Services: ' + analysis.blastRadius.servicesAffected.join(', '));
|
|
687
|
+
log('t-err', ' Circuit breaker: OPEN \u2014 all downstream traffic failing fast');
|
|
688
|
+
await sleep(400);
|
|
689
|
+
log('t-bold', '');
|
|
690
|
+
log('t-bold', ' Suggested fix:');
|
|
691
|
+
log('t-hi', ' ' + analysis.suggestedFix.split('\\n')[0]);
|
|
692
|
+
await sleep(400);
|
|
693
|
+
log('t-bold', '');
|
|
694
|
+
log('t-warn', ' Autopilot: ' + analysis.autopilotBlockedReason);
|
|
695
|
+
await sleep(600);
|
|
696
|
+
log('t-bold', '');
|
|
697
|
+
log('t-ok', ' Triage complete. Full audit trail: /impact-report');
|
|
698
|
+
|
|
699
|
+
document.getElementById('p1-status').textContent = 'COMPLETE';
|
|
700
|
+
document.getElementById('p1-status').className = 'badge badge-ok';
|
|
701
|
+
document.getElementById('m-errors').textContent = '11';
|
|
702
|
+
document.getElementById('m-conf').textContent = Math.round(analysis.confidence * 100) + '%';
|
|
703
|
+
document.getElementById('p1-metrics').style.display = 'grid';
|
|
704
|
+
document.getElementById('p1-hint').style.display = 'block';
|
|
705
|
+
document.getElementById('p1-again-btn').style.display = 'inline-flex';
|
|
706
|
+
document.getElementById('p1-again-btn').disabled = false;
|
|
707
|
+
document.getElementById('p1-btn').disabled = false;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// \u2500\u2500 Frontend trace join demo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
711
|
+
(function MergenSDK() {
|
|
712
|
+
'use strict';
|
|
713
|
+
const ENDPOINT = 'http://localhost:3000';
|
|
714
|
+
const SERVICE = 'mergen-demo';
|
|
715
|
+
function randomHex(n) { const a = new Uint8Array(n); crypto.getRandomValues(a); return Array.from(a, b => b.toString(16).padStart(2,'0')).join(''); }
|
|
716
|
+
function msToNano(ms) { return String(BigInt(Math.round(ms)) * 1000000n); }
|
|
717
|
+
function attr(k, v) { return { key: k, value: { stringValue: v } }; }
|
|
718
|
+
const resource = { attributes: [attr('service.name', SERVICE)] };
|
|
719
|
+
function post(url, body) {
|
|
720
|
+
try { if (navigator.sendBeacon) { if (navigator.sendBeacon(url, new Blob([JSON.stringify(body)], {type:'application/json'}))) return; } } catch {}
|
|
721
|
+
fetch(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body), keepalive: true }).catch(()=>{});
|
|
722
|
+
}
|
|
723
|
+
function sendLog(level, body, stack, traceId) {
|
|
724
|
+
const sev = {log:9,warn:13,error:17}[level]??9;
|
|
725
|
+
const rec = { timeUnixNano: msToNano(Date.now()), severityNumber: sev, severityText: level.toUpperCase(), body: { stringValue: body }, attributes: [attr('browser.url', location.href), ...(stack?[attr('exception.stacktrace',stack)]:[])] };
|
|
726
|
+
if (traceId) rec.traceId = traceId;
|
|
727
|
+
post(ENDPOINT+'/v1/logs', { resourceLogs: [{ resource, scopeLogs: [{ logRecords: [rec] }] }] });
|
|
728
|
+
}
|
|
729
|
+
function sendSpan(traceId, spanId, method, url, startMs, endMs, status, error) {
|
|
730
|
+
post(ENDPOINT+'/v1/traces', { resourceSpans: [{ resource, scopeSpans: [{ spans: [{ traceId, spanId, name: method+' '+new URL(url,location.href).pathname, kind:3, startTimeUnixNano:msToNano(startMs), endTimeUnixNano:msToNano(endMs), status:{code:status>=400||!!error?2:1,message:error??''}, attributes:[attr('http.method',method),attr('http.url',url),attr('http.status_code',String(status))] }] }] }] });
|
|
731
|
+
}
|
|
732
|
+
['log','warn','error'].forEach(function(level) {
|
|
733
|
+
const orig = console[level].bind(console);
|
|
734
|
+
console[level] = function() {
|
|
735
|
+
orig.apply(console, arguments);
|
|
736
|
+
const msg = Array.from(arguments).map(a => typeof a==='string'?a:a instanceof Error?a.name+': '+a.message:JSON.stringify(a)).join(' ');
|
|
737
|
+
if (msg.includes('/v1/')) return;
|
|
738
|
+
sendLog(level, msg, level==='error'&&arguments[0] instanceof Error?arguments[0].stack:null, null);
|
|
739
|
+
};
|
|
740
|
+
});
|
|
741
|
+
const _fetch = window.fetch.bind(window);
|
|
742
|
+
window.fetch = function(input, init) {
|
|
743
|
+
const url = typeof input==='string'?input:input instanceof URL?input.href:input.url;
|
|
744
|
+
const method = ((init&&init.method)||(input&&input.method)||'GET').toUpperCase();
|
|
745
|
+
const traceId = randomHex(16); const spanId = randomHex(8);
|
|
746
|
+
const startMs = Date.now();
|
|
747
|
+
const headers = new Headers((init&&init.headers)||(input&&!(typeof input==='string')&&!(input instanceof URL)?input.headers:{}));
|
|
748
|
+
headers.set('traceparent', '00-'+traceId+'-'+spanId+'-01');
|
|
749
|
+
const pInit = Object.assign({},init||{},{headers});
|
|
750
|
+
const pInput = (typeof input==='string'||input instanceof URL)?input:new Request(input,pInit);
|
|
751
|
+
return _fetch(pInput, typeof pInput==='string'||pInput instanceof URL?pInit:undefined).then(function(resp) {
|
|
752
|
+
sendSpan(traceId,spanId,method,url,startMs,Date.now(),resp.status,resp.ok?null:(resp.statusText||('HTTP '+resp.status)));
|
|
753
|
+
return resp;
|
|
754
|
+
}, function(err) { sendSpan(traceId,spanId,method,url,startMs,Date.now(),0,err.message||'NetworkError'); throw err; });
|
|
755
|
+
};
|
|
756
|
+
window.__MergenSDK = { sendLog, sendSpan, randomHex };
|
|
757
|
+
})();
|
|
758
|
+
|
|
759
|
+
let feEventCount = 0;
|
|
760
|
+
async function runFrontendDemo() {
|
|
761
|
+
document.getElementById('fe-btn').disabled = true;
|
|
762
|
+
document.getElementById('fe-again-btn').style.display = 'none';
|
|
763
|
+
document.getElementById('fe-output').style.display = 'block';
|
|
764
|
+
feEventCount = 0;
|
|
765
|
+
const log = document.getElementById('fe-log');
|
|
766
|
+
log.innerHTML = '';
|
|
767
|
+
|
|
768
|
+
function addEntry(cls, badge, msg) {
|
|
769
|
+
feEventCount++;
|
|
770
|
+
const ts = new Date().toISOString().slice(11, 23);
|
|
771
|
+
log.innerHTML += '<span class="' + cls + '">' + ts + ' [' + badge + '] ' + msg + '</span>\\n';
|
|
772
|
+
log.scrollTop = log.scrollHeight;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
addEntry('t-info','INFO','Starting \u2014 JWT expiry scenario...');
|
|
776
|
+
await sleep(400);
|
|
777
|
+
console.warn('[auth] JWT expiry imminent \u2014 12s remaining');
|
|
778
|
+
addEntry('t-warn','WARN','console.warn: JWT expiry imminent \u2014 12s remaining \u2192 /v1/logs');
|
|
779
|
+
await sleep(600);
|
|
780
|
+
addEntry('t-info','INFO','fetch /demo/api/user with traceparent header...');
|
|
781
|
+
try {
|
|
782
|
+
const resp = await fetch('/demo/api/user');
|
|
783
|
+
const data = await resp.json();
|
|
784
|
+
const traceId = resp.headers.get('traceparent')?.split('-')[1] ?? '?';
|
|
785
|
+
await sleep(200);
|
|
786
|
+
console.error('TokenError: JWT expired at audience check', { status: resp.status, traceId });
|
|
787
|
+
addEntry('t-err', 'ERR', 'console.error: TokenError: JWT expired \u2014 /v1/logs');
|
|
788
|
+
addEntry('t-warn','NET', 'GET /demo/api/user \u2192 401 ' + (Date.now()%200+80) + 'ms \u2014 /v1/traces');
|
|
789
|
+
await sleep(300);
|
|
790
|
+
addEntry('t-ok', 'JOIN','traceId join confirmed: browser fetch \u2194 backend span \u2014 ' + traceId.slice(0,8) + '\u2026');
|
|
791
|
+
} catch {
|
|
792
|
+
addEntry('t-err','ERR','Network error \u2014 is the server running?');
|
|
793
|
+
}
|
|
794
|
+
await sleep(400);
|
|
795
|
+
addEntry('t-ok','DONE','5 events sent. Ask your AI: get_unified_timeline');
|
|
796
|
+
document.getElementById('fe-again-btn').style.display = 'inline-flex';
|
|
797
|
+
document.getElementById('fe-again-btn').disabled = false;
|
|
798
|
+
document.getElementById('fe-btn').disabled = false;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
802
|
+
|
|
803
|
+
// \u2500\u2500 Ask Mergen chat \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
804
|
+
async function sendChat() {
|
|
805
|
+
const input = document.getElementById('chat-input');
|
|
806
|
+
const question = input.value.trim();
|
|
807
|
+
if (!question) return;
|
|
808
|
+
|
|
809
|
+
document.getElementById('chat-btn').disabled = true;
|
|
810
|
+
document.getElementById('chat-output').style.display = 'block';
|
|
811
|
+
document.getElementById('chat-tool').textContent = '...';
|
|
812
|
+
const term = document.getElementById('chat-terminal');
|
|
813
|
+
term.innerHTML = '<span class="t-info">Thinking...</span>';
|
|
814
|
+
|
|
815
|
+
try {
|
|
816
|
+
const r = await fetch('/chat', {
|
|
817
|
+
method: 'POST',
|
|
818
|
+
headers: { 'Content-Type': 'application/json' },
|
|
819
|
+
body: JSON.stringify({ question }),
|
|
820
|
+
});
|
|
821
|
+
const d = await r.json();
|
|
822
|
+
const lines = (d.answer || '').split('\\n');
|
|
823
|
+
term.innerHTML = lines.map(line => {
|
|
824
|
+
if (!line.trim()) return '<span> </span>';
|
|
825
|
+
if (line.startsWith(' ')) return '<span class="t-info">' + line + '</span>';
|
|
826
|
+
if (line.endsWith(':') || line.match(/^[A-Z]/)) return '<span class="t-hi">' + line + '</span>';
|
|
827
|
+
if (line.startsWith('\u2713') || line.startsWith('\u2192')) return '<span class="t-ok">' + line + '</span>';
|
|
828
|
+
return '<span class="t-ok">' + line + '</span>';
|
|
829
|
+
}).join('\\n');
|
|
830
|
+
document.getElementById('chat-tool').textContent = question.length > 30 ? question.slice(0, 30) + '\u2026' : question;
|
|
831
|
+
} catch(e) {
|
|
832
|
+
term.innerHTML = '<span class="t-err">Error: ' + e.message + '</span>';
|
|
833
|
+
} finally {
|
|
834
|
+
document.getElementById('chat-btn').disabled = false;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
</script>
|
|
838
|
+
</body>
|
|
839
|
+
</html>`;
|
|
840
|
+
var demo_default = createDemoRouter;
|
|
841
|
+
export {
|
|
842
|
+
createDemoRouter,
|
|
843
|
+
demo_default as default
|
|
844
|
+
};
|