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,228 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import logger from "../sensor/logger.js";
|
|
4
|
+
const HIGH_CONFIDENCE = 0.8;
|
|
5
|
+
const LOW_CONFIDENCE = 0.6;
|
|
6
|
+
const ALL_SIGNALS_BONUS = 0.05;
|
|
7
|
+
const TIMING_ONLY_CAP = 0.55;
|
|
8
|
+
function computeBlameAttribution(opts) {
|
|
9
|
+
const { implicatedFile, deployedSha, firedAt, candidates, prUrl } = opts;
|
|
10
|
+
const priorDeploys = candidates.filter((d) => d.timestamp <= firedAt && d.status === "success").sort((a, b) => b.timestamp - a.timestamp);
|
|
11
|
+
if (priorDeploys.length === 0) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const scored = priorDeploys.slice(0, 5).map((deploy) => ({
|
|
15
|
+
deploy,
|
|
16
|
+
attribution: _scoreCandidate({ deploy, implicatedFile, deployedSha, firedAt })
|
|
17
|
+
}));
|
|
18
|
+
scored.sort((a, b) => b.attribution.confidence - a.attribution.confidence);
|
|
19
|
+
const best = scored[0];
|
|
20
|
+
if (!best) return null;
|
|
21
|
+
return {
|
|
22
|
+
...best.attribution,
|
|
23
|
+
topCandidate: {
|
|
24
|
+
sha: best.deploy.sha,
|
|
25
|
+
deployedAt: best.deploy.timestamp,
|
|
26
|
+
environment: best.deploy.environment,
|
|
27
|
+
prUrl: prUrl ?? best.deploy.url ?? null
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function _scoreCandidate(opts) {
|
|
32
|
+
const { deploy, implicatedFile, deployedSha, firedAt } = opts;
|
|
33
|
+
const timing = _timingSignal(deploy.timestamp, firedAt);
|
|
34
|
+
const shaMatch = _shaMatchSignal(deployedSha, deploy.sha);
|
|
35
|
+
const changedFiles = _getChangedFiles(deploy.sha);
|
|
36
|
+
const fileOverlap = _fileOverlapSignal(implicatedFile, changedFiles);
|
|
37
|
+
const timingFired = timing.available && timing.score > 0;
|
|
38
|
+
const shaFired = shaMatch.available && shaMatch.score > 0;
|
|
39
|
+
const overlapFired = fileOverlap.available && fileOverlap.score > 0;
|
|
40
|
+
const signalsFired = [timingFired, shaFired, overlapFired].filter(Boolean).length;
|
|
41
|
+
let confidence = timing.contribution + shaMatch.contribution + fileOverlap.contribution;
|
|
42
|
+
if (signalsFired === 3) confidence = Math.min(1, confidence + ALL_SIGNALS_BONUS);
|
|
43
|
+
if (timingFired && !shaFired && !overlapFired) confidence = Math.min(confidence, TIMING_ONLY_CAP);
|
|
44
|
+
const confidenceLabel = confidence >= HIGH_CONFIDENCE ? "HIGH" : confidence >= LOW_CONFIDENCE ? "MEDIUM" : "LOW";
|
|
45
|
+
const explanation = _buildExplanation({ deploy, timing, shaMatch, fileOverlap, confidence, signalsFired, firedAt });
|
|
46
|
+
return {
|
|
47
|
+
confidence: Math.min(1, confidence),
|
|
48
|
+
confidenceLabel,
|
|
49
|
+
signals: { timing, shaMatch, fileOverlap },
|
|
50
|
+
explanation,
|
|
51
|
+
lowConfidence: confidence < LOW_CONFIDENCE,
|
|
52
|
+
changedFiles
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function _timingSignal(deployedAt, firedAt) {
|
|
56
|
+
const WEIGHT = 0.4;
|
|
57
|
+
const deltaMs = firedAt - deployedAt;
|
|
58
|
+
const deltaSec = deltaMs / 1e3;
|
|
59
|
+
const deltaMin = deltaMs / 6e4;
|
|
60
|
+
let score;
|
|
61
|
+
let detail;
|
|
62
|
+
if (deltaMin < 2) {
|
|
63
|
+
score = 1;
|
|
64
|
+
detail = `deploy merged ${Math.round(deltaSec)}s before first error \u2014 extremely tight window`;
|
|
65
|
+
} else if (deltaMin < 5) {
|
|
66
|
+
score = 0.8;
|
|
67
|
+
detail = `deploy merged ${Math.round(deltaMin)}m before first error \u2014 very tight window`;
|
|
68
|
+
} else if (deltaMin < 15) {
|
|
69
|
+
score = 0.55;
|
|
70
|
+
detail = `deploy merged ${Math.round(deltaMin)}m before first error \u2014 plausible window`;
|
|
71
|
+
} else if (deltaMin < 30) {
|
|
72
|
+
score = 0.3;
|
|
73
|
+
detail = `deploy merged ${Math.round(deltaMin)}m before first error \u2014 possible but loose`;
|
|
74
|
+
} else {
|
|
75
|
+
score = 0.05;
|
|
76
|
+
detail = `deploy merged ${Math.round(deltaMin)}m before first error \u2014 weak temporal link`;
|
|
77
|
+
}
|
|
78
|
+
return { score, weight: WEIGHT, contribution: score * WEIGHT, detail, available: true };
|
|
79
|
+
}
|
|
80
|
+
function _shaMatchSignal(incidentSha, deploySha) {
|
|
81
|
+
const WEIGHT = 0.3;
|
|
82
|
+
if (!incidentSha) {
|
|
83
|
+
return {
|
|
84
|
+
score: 0,
|
|
85
|
+
weight: WEIGHT,
|
|
86
|
+
contribution: 0,
|
|
87
|
+
detail: "no buildSha in incident \u2014 browser events lack git SHA or Datadog span missing git.commit.sha",
|
|
88
|
+
available: false
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const matches = deploySha === incidentSha || deploySha.startsWith(incidentSha) || incidentSha.startsWith(deploySha);
|
|
92
|
+
if (matches) {
|
|
93
|
+
return {
|
|
94
|
+
score: 1,
|
|
95
|
+
weight: WEIGHT,
|
|
96
|
+
contribution: WEIGHT,
|
|
97
|
+
detail: `browser buildSha \`${incidentSha.slice(0, 8)}\` matches deploy SHA \u2014 confirmed same artifact`,
|
|
98
|
+
available: true
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
score: 0,
|
|
103
|
+
weight: WEIGHT,
|
|
104
|
+
contribution: 0,
|
|
105
|
+
detail: `browser buildSha \`${incidentSha.slice(0, 8)}\` does not match deploy SHA \`${deploySha.slice(0, 8)}\` \u2014 different artifact`,
|
|
106
|
+
available: true
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function _fileOverlapSignal(implicatedFile, changedFiles) {
|
|
110
|
+
const WEIGHT = 0.3;
|
|
111
|
+
if (!implicatedFile) {
|
|
112
|
+
return {
|
|
113
|
+
score: 0,
|
|
114
|
+
weight: WEIGHT,
|
|
115
|
+
contribution: 0,
|
|
116
|
+
detail: "no implicated file from stack trace \u2014 sourcemap resolution unavailable",
|
|
117
|
+
available: false
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (changedFiles.length === 0) {
|
|
121
|
+
return {
|
|
122
|
+
score: 0,
|
|
123
|
+
weight: WEIGHT,
|
|
124
|
+
contribution: 0,
|
|
125
|
+
detail: "git diff unavailable \u2014 cannot check file overlap (no git repo or SHA not found)",
|
|
126
|
+
available: false
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const basename = path.basename(implicatedFile);
|
|
130
|
+
const implicatedDir = path.dirname(implicatedFile).split("/").filter(Boolean).pop() ?? "";
|
|
131
|
+
for (const f of changedFiles) {
|
|
132
|
+
if (f === implicatedFile || f.endsWith("/" + implicatedFile) || implicatedFile.endsWith("/" + f)) {
|
|
133
|
+
return {
|
|
134
|
+
score: 1,
|
|
135
|
+
weight: WEIGHT,
|
|
136
|
+
contribution: WEIGHT,
|
|
137
|
+
detail: `\`${f}\` in deploy diff \u2014 exact match with implicated file \`${implicatedFile}\``,
|
|
138
|
+
available: true
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const f of changedFiles) {
|
|
143
|
+
if (path.basename(f) === basename) {
|
|
144
|
+
return {
|
|
145
|
+
score: 0.75,
|
|
146
|
+
weight: WEIGHT,
|
|
147
|
+
contribution: 0.75 * WEIGHT,
|
|
148
|
+
detail: `\`${f}\` in deploy diff \u2014 same filename as implicated \`${basename}\` (path differs, possibly Docker mount)`,
|
|
149
|
+
available: true
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const f of changedFiles) {
|
|
154
|
+
if (implicatedDir && path.dirname(f).split("/").filter(Boolean).pop() === implicatedDir) {
|
|
155
|
+
return {
|
|
156
|
+
score: 0.35,
|
|
157
|
+
weight: WEIGHT,
|
|
158
|
+
contribution: 0.35 * WEIGHT,
|
|
159
|
+
detail: `deploy modified files in \`${implicatedDir}/\` \u2014 same directory as implicated file, weak overlap`,
|
|
160
|
+
available: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
score: 0,
|
|
166
|
+
weight: WEIGHT,
|
|
167
|
+
contribution: 0,
|
|
168
|
+
detail: `implicated file \`${basename}\` not in deploy diff \u2014 different area of codebase changed`,
|
|
169
|
+
available: true
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function _getChangedFiles(sha) {
|
|
173
|
+
try {
|
|
174
|
+
const output = execSync(`git diff --name-only ${sha}^..${sha}`, {
|
|
175
|
+
timeout: 5e3,
|
|
176
|
+
encoding: "utf8",
|
|
177
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
178
|
+
});
|
|
179
|
+
return output.trim().split("\n").filter(Boolean);
|
|
180
|
+
} catch {
|
|
181
|
+
try {
|
|
182
|
+
const output = execSync(`git show --name-only --format="" ${sha}`, {
|
|
183
|
+
timeout: 5e3,
|
|
184
|
+
encoding: "utf8",
|
|
185
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
186
|
+
});
|
|
187
|
+
return output.trim().split("\n").filter(Boolean);
|
|
188
|
+
} catch {
|
|
189
|
+
logger.debug({ sha }, "git diff unavailable for blame attribution");
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function _buildExplanation(opts) {
|
|
195
|
+
const { deploy, timing, shaMatch, fileOverlap, confidence, signalsFired } = opts;
|
|
196
|
+
const sha8 = deploy.sha.slice(0, 8);
|
|
197
|
+
const pct = Math.round(confidence * 100);
|
|
198
|
+
const available = [timing, shaMatch, fileOverlap].filter((s) => s.available);
|
|
199
|
+
const strong = available.filter((s) => s.score >= 0.75);
|
|
200
|
+
let core = `Deploy \`${sha8}\` (${deploy.environment}) \u2014 ${pct}% confidence, ${signalsFired}/3 signals fired.`;
|
|
201
|
+
if (signalsFired === 3) {
|
|
202
|
+
core += " All three signals corroborate: " + available.map((s) => s.detail).join("; ") + ".";
|
|
203
|
+
} else if (signalsFired === 1 && timing.score > 0 && !shaMatch.score && !fileOverlap.score) {
|
|
204
|
+
core += " Only timing fired \u2014 high-traffic services produce errors near every deploy; treat as weak hypothesis. " + timing.detail + ".";
|
|
205
|
+
} else if (strong.length > 0) {
|
|
206
|
+
core += " Strong signals: " + strong.map((s) => s.detail).join("; ") + ".";
|
|
207
|
+
const weak = available.filter((s) => s.score < 0.75 && s.score > 0);
|
|
208
|
+
if (weak.length) core += " Weaker signals: " + weak.map((s) => s.detail).join("; ") + ".";
|
|
209
|
+
} else {
|
|
210
|
+
core += " Signals: " + available.map((s) => s.detail).join("; ") + ".";
|
|
211
|
+
}
|
|
212
|
+
const unavailable = [timing, shaMatch, fileOverlap].filter((s) => !s.available);
|
|
213
|
+
if (unavailable.length > 0) {
|
|
214
|
+
core += ` Missing data for: ${unavailable.map((s, i) => ["timing", "shaMatch", "fileOverlap"][i]).join(", ")}.`;
|
|
215
|
+
}
|
|
216
|
+
if (confidence < LOW_CONFIDENCE) {
|
|
217
|
+
core += " Confidence below threshold \u2014 recommend manual investigation before acting.";
|
|
218
|
+
}
|
|
219
|
+
return core;
|
|
220
|
+
}
|
|
221
|
+
function validateAttribution(attributedSha, fixSha) {
|
|
222
|
+
if (!attributedSha || !fixSha) return null;
|
|
223
|
+
return attributedSha.startsWith(fixSha) || fixSha.startsWith(attributedSha);
|
|
224
|
+
}
|
|
225
|
+
export {
|
|
226
|
+
computeBlameAttribution,
|
|
227
|
+
validateAttribution
|
|
228
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { DATA_DIR } from "../sensor/paths.js";
|
|
4
|
+
import { startClientSpan } from "./otel-trace.js";
|
|
5
|
+
function loadConfig() {
|
|
6
|
+
if (process.env.DD_API_KEY && process.env.DD_APP_KEY) {
|
|
7
|
+
return {
|
|
8
|
+
apiKey: process.env.DD_API_KEY,
|
|
9
|
+
appKey: process.env.DD_APP_KEY,
|
|
10
|
+
site: process.env.DATADOG_SITE ?? process.env.DD_SITE ?? "datadoghq.com"
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const configPath = path.join(DATA_DIR, "config.json");
|
|
14
|
+
if (existsSync(configPath)) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
17
|
+
if (raw.datadog?.apiKey && raw.datadog?.appKey) {
|
|
18
|
+
return {
|
|
19
|
+
apiKey: raw.datadog.apiKey,
|
|
20
|
+
appKey: raw.datadog.appKey,
|
|
21
|
+
site: raw.datadog.site ?? "datadoghq.com"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
async function ddPost(config, endpoint, body) {
|
|
30
|
+
const url = `https://api.${config.site}${endpoint}`;
|
|
31
|
+
const span = startClientSpan();
|
|
32
|
+
const res = await fetch(url, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
"DD-API-KEY": config.apiKey,
|
|
37
|
+
"DD-APPLICATION-KEY": config.appKey,
|
|
38
|
+
"traceparent": span.traceparent
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
signal: AbortSignal.timeout(15e3)
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
throw new Error(`Datadog API ${res.status}: ${text.slice(0, 200)}`);
|
|
46
|
+
}
|
|
47
|
+
return res.json();
|
|
48
|
+
}
|
|
49
|
+
async function fetchSpans(opts) {
|
|
50
|
+
const config = loadConfig();
|
|
51
|
+
if (!config) throw new Error("Datadog not configured. Run: mergen-server init");
|
|
52
|
+
const queryParts = [];
|
|
53
|
+
if (opts.service) queryParts.push(`service:${opts.service}`);
|
|
54
|
+
if (opts.errorsOnly) queryParts.push("status:error");
|
|
55
|
+
const raw = await ddPost(config, "/api/v2/spans", {
|
|
56
|
+
filter: {
|
|
57
|
+
from: opts.from.toISOString(),
|
|
58
|
+
to: opts.to.toISOString(),
|
|
59
|
+
query: queryParts.join(" ") || "*"
|
|
60
|
+
},
|
|
61
|
+
page: { limit: opts.limit ?? 100 },
|
|
62
|
+
sort: "-timestamp"
|
|
63
|
+
});
|
|
64
|
+
return (raw.data ?? []).map((d) => ({
|
|
65
|
+
id: d.id,
|
|
66
|
+
traceId: d.attributes.trace_id,
|
|
67
|
+
service: d.attributes.service,
|
|
68
|
+
resourceName: d.attributes.resource_name,
|
|
69
|
+
durationNs: d.attributes.duration,
|
|
70
|
+
status: d.attributes.status,
|
|
71
|
+
startNs: d.attributes.start,
|
|
72
|
+
tags: d.attributes.tags ?? {}
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
async function fetchLogsByTraceId(opts) {
|
|
76
|
+
const config = loadConfig();
|
|
77
|
+
if (!config) throw new Error("Datadog not configured. Run: mergen-server init");
|
|
78
|
+
const queryParts = [`trace_id:${opts.traceId}`];
|
|
79
|
+
if (opts.errorsOnly) queryParts.push("status:error");
|
|
80
|
+
const raw = await ddPost(config, "/api/v2/logs/events/search", {
|
|
81
|
+
filter: {
|
|
82
|
+
from: opts.from.toISOString(),
|
|
83
|
+
to: opts.to.toISOString(),
|
|
84
|
+
query: queryParts.join(" ")
|
|
85
|
+
},
|
|
86
|
+
page: { limit: 50 }
|
|
87
|
+
});
|
|
88
|
+
return (raw.data ?? []).map((d) => ({
|
|
89
|
+
id: d.id,
|
|
90
|
+
timestamp: d.attributes.timestamp,
|
|
91
|
+
message: d.attributes.message,
|
|
92
|
+
status: d.attributes.status,
|
|
93
|
+
service: d.attributes.service,
|
|
94
|
+
tags: Object.fromEntries(
|
|
95
|
+
(d.attributes.tags ?? []).map((t) => {
|
|
96
|
+
const idx = t.indexOf(":");
|
|
97
|
+
return idx === -1 ? [t, ""] : [t.slice(0, idx), t.slice(idx + 1)];
|
|
98
|
+
})
|
|
99
|
+
),
|
|
100
|
+
attributes: d.attributes.attributes ?? {}
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
async function fetchLatestErrorTrace(service, windowMinutes = 10) {
|
|
104
|
+
const to = /* @__PURE__ */ new Date();
|
|
105
|
+
const from = new Date(to.getTime() - windowMinutes * 60 * 1e3);
|
|
106
|
+
const errorSpans = await fetchSpans({ service, from, to, errorsOnly: true, limit: 50 });
|
|
107
|
+
if (errorSpans.length === 0) return null;
|
|
108
|
+
const byTrace = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const s of errorSpans) {
|
|
110
|
+
byTrace.set(s.traceId, (byTrace.get(s.traceId) ?? 0) + 1);
|
|
111
|
+
}
|
|
112
|
+
const bestTraceId = [...byTrace.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
113
|
+
const allSpans = await fetchSpans({ service, from, to, limit: 200 });
|
|
114
|
+
const traceSpans = allSpans.filter((s) => s.traceId === bestTraceId);
|
|
115
|
+
return { traceId: bestTraceId, spans: traceSpans };
|
|
116
|
+
}
|
|
117
|
+
async function fetchErrorCountSince(service, windowMinutes) {
|
|
118
|
+
const config = loadConfig();
|
|
119
|
+
if (!config) return null;
|
|
120
|
+
const to = /* @__PURE__ */ new Date();
|
|
121
|
+
const from = new Date(to.getTime() - windowMinutes * 6e4);
|
|
122
|
+
try {
|
|
123
|
+
const raw = await ddPost(config, "/api/v2/spans", {
|
|
124
|
+
filter: {
|
|
125
|
+
from: from.toISOString(),
|
|
126
|
+
to: to.toISOString(),
|
|
127
|
+
query: `service:${service} status:error`
|
|
128
|
+
},
|
|
129
|
+
page: { limit: 1 },
|
|
130
|
+
sort: "-timestamp"
|
|
131
|
+
});
|
|
132
|
+
return raw.data?.length ?? 0;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function testConnection() {
|
|
138
|
+
const config = loadConfig();
|
|
139
|
+
if (!config) throw new Error("No credentials found");
|
|
140
|
+
const res = await fetch(`https://api.${config.site}/api/v1/validate`, {
|
|
141
|
+
headers: { "DD-API-KEY": config.apiKey, "DD-APPLICATION-KEY": config.appKey },
|
|
142
|
+
signal: AbortSignal.timeout(8e3)
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
const body = await res.text();
|
|
146
|
+
throw new Error(`Auth failed (${res.status}): ${body.slice(0, 100)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function isConfigured() {
|
|
150
|
+
return loadConfig() !== null;
|
|
151
|
+
}
|
|
152
|
+
export {
|
|
153
|
+
fetchErrorCountSince,
|
|
154
|
+
fetchLatestErrorTrace,
|
|
155
|
+
fetchLogsByTraceId,
|
|
156
|
+
fetchSpans,
|
|
157
|
+
isConfigured,
|
|
158
|
+
testConnection
|
|
159
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { fetchLogsByTraceId } from "./client.js";
|
|
2
|
+
import { matchLines } from "./line-matcher.js";
|
|
3
|
+
function pruneTrace(spans) {
|
|
4
|
+
if (spans.length === 0) return [];
|
|
5
|
+
const maxDuration = Math.max(...spans.map((s) => s.durationNs));
|
|
6
|
+
const threshold = maxDuration * 0.05;
|
|
7
|
+
return spans.filter((s) => s.status === "error" || s.durationNs >= threshold);
|
|
8
|
+
}
|
|
9
|
+
const KEEP_PREFIXES = [
|
|
10
|
+
"error.",
|
|
11
|
+
"exception.",
|
|
12
|
+
"db.",
|
|
13
|
+
"http.status_code",
|
|
14
|
+
"http.url",
|
|
15
|
+
"http.method",
|
|
16
|
+
"http.route",
|
|
17
|
+
"git.commit",
|
|
18
|
+
"net.peer.name"
|
|
19
|
+
];
|
|
20
|
+
const STRIP_PREFIXES = [
|
|
21
|
+
"container_",
|
|
22
|
+
"k8s.",
|
|
23
|
+
"os.",
|
|
24
|
+
"process.runtime",
|
|
25
|
+
"net.transport",
|
|
26
|
+
"network.",
|
|
27
|
+
"peer.",
|
|
28
|
+
"otel.",
|
|
29
|
+
"http.scheme",
|
|
30
|
+
"http.flavor",
|
|
31
|
+
"messaging."
|
|
32
|
+
];
|
|
33
|
+
function stripAttributes(tags) {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [k, v] of Object.entries(tags)) {
|
|
36
|
+
if (STRIP_PREFIXES.some((p) => k.startsWith(p))) continue;
|
|
37
|
+
if (KEEP_PREFIXES.some((p) => k.startsWith(p))) out[k] = v;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
async function inlineLogs(spans, from, to) {
|
|
42
|
+
const traceIds = [...new Set(spans.map((s) => s.traceId))];
|
|
43
|
+
const logsByTrace = /* @__PURE__ */ new Map();
|
|
44
|
+
await Promise.all(
|
|
45
|
+
traceIds.map(async (traceId) => {
|
|
46
|
+
try {
|
|
47
|
+
const logs = await fetchLogsByTraceId({ traceId, from, to, errorsOnly: true });
|
|
48
|
+
if (logs.length > 0) logsByTrace.set(traceId, logs);
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
return spans.map((s) => ({ ...s, inlinedLogs: logsByTrace.get(s.traceId) }));
|
|
54
|
+
}
|
|
55
|
+
function parseTopFrame(stack) {
|
|
56
|
+
const goMatch = stack.match(/\t([^:\s]+\.go):(\d+)/);
|
|
57
|
+
if (goMatch) return { file: goMatch[1], line: parseInt(goMatch[2], 10) };
|
|
58
|
+
const nodeMatch = stack.match(/at\s+\S+\s+\(([^:)]+):(\d+):\d+\)/);
|
|
59
|
+
if (nodeMatch) return { file: nodeMatch[1], line: parseInt(nodeMatch[2], 10) };
|
|
60
|
+
const pyMatch = stack.match(/File "([^"]+)", line (\d+)/);
|
|
61
|
+
if (pyMatch) return { file: pyMatch[1], line: parseInt(pyMatch[2], 10) };
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
async function serializeToFact(spans, traceId) {
|
|
65
|
+
const errorSpans = spans.filter((s) => s.status === "error");
|
|
66
|
+
const root = errorSpans[0] ?? spans[0];
|
|
67
|
+
if (!root) throw new Error("No spans to serialize");
|
|
68
|
+
const service = root.service;
|
|
69
|
+
const endpoint = root.resourceName;
|
|
70
|
+
const errorMsg = root.tags["error.message"] ?? root.tags["error.msg"] ?? "Unknown error";
|
|
71
|
+
const errorType = root.tags["error.type"] ?? root.tags["exception.type"] ?? "";
|
|
72
|
+
const errorStack = root.tags["error.stack"] ?? root.tags["exception.stacktrace"] ?? "";
|
|
73
|
+
const deployedSha = root.tags["git.commit.sha"] ?? root.tags["git.commit"];
|
|
74
|
+
const durationMs = Math.round(root.durationNs / 1e6);
|
|
75
|
+
let failingFile;
|
|
76
|
+
let failingLine;
|
|
77
|
+
let codeContext;
|
|
78
|
+
const frame = errorStack ? parseTopFrame(errorStack) : null;
|
|
79
|
+
if (frame) {
|
|
80
|
+
failingFile = frame.file;
|
|
81
|
+
failingLine = frame.line;
|
|
82
|
+
try {
|
|
83
|
+
codeContext = await matchLines(failingFile, failingLine, deployedSha);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const seen = /* @__PURE__ */ new Set();
|
|
88
|
+
const logLines = [];
|
|
89
|
+
for (const s of spans) {
|
|
90
|
+
for (const l of s.inlinedLogs ?? []) {
|
|
91
|
+
const key = l.message.slice(0, 80);
|
|
92
|
+
if (!seen.has(key)) {
|
|
93
|
+
seen.add(key);
|
|
94
|
+
logLines.push(l.message.slice(0, 200));
|
|
95
|
+
}
|
|
96
|
+
if (logLines.length >= 5) break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const lines = [
|
|
100
|
+
"## MERGEN RUNTIME FACT",
|
|
101
|
+
"",
|
|
102
|
+
`**Failure Endpoint:** \`${endpoint}\` (Service: \`${service}\`)`,
|
|
103
|
+
`**Trace ID:** \`${traceId}\``,
|
|
104
|
+
`**Duration:** ${durationMs}ms`,
|
|
105
|
+
"",
|
|
106
|
+
errorType ? `**Exception:** \`${errorType}: ${errorMsg}\`` : `**Error:** ${errorMsg}`
|
|
107
|
+
];
|
|
108
|
+
const dbStatement = root.tags["db.statement"];
|
|
109
|
+
if (dbStatement) {
|
|
110
|
+
lines.push("", "**Failing Query:**", "```sql", dbStatement.slice(0, 500), "```");
|
|
111
|
+
}
|
|
112
|
+
const httpStatus = root.tags["http.status_code"];
|
|
113
|
+
if (httpStatus) lines.push("", `**HTTP Status:** ${httpStatus}`);
|
|
114
|
+
if (errorStack) {
|
|
115
|
+
const topFrames = errorStack.split("\n").slice(0, 8).join("\n");
|
|
116
|
+
lines.push("", "**Stack Trace (top frames):**", "```", topFrames, "```");
|
|
117
|
+
}
|
|
118
|
+
if (codeContext && failingFile && failingLine) {
|
|
119
|
+
lines.push(
|
|
120
|
+
"",
|
|
121
|
+
`**Local Code Context** (\`${failingFile}:${failingLine}\`):`,
|
|
122
|
+
"```",
|
|
123
|
+
codeContext,
|
|
124
|
+
"```"
|
|
125
|
+
);
|
|
126
|
+
} else if (failingFile && failingLine) {
|
|
127
|
+
lines.push("", `**Failing Location:** \`${failingFile}:${failingLine}\` *(local workspace not matched)*`);
|
|
128
|
+
}
|
|
129
|
+
if (logLines.length > 0) {
|
|
130
|
+
lines.push("", "**Correlated Error Logs:**");
|
|
131
|
+
for (const l of logLines) lines.push(`- ${l}`);
|
|
132
|
+
}
|
|
133
|
+
if (deployedSha) {
|
|
134
|
+
lines.push(
|
|
135
|
+
"",
|
|
136
|
+
`**Deployed Commit:** \`${deployedSha.slice(0, 7)}\``,
|
|
137
|
+
`*Run \`git show ${deployedSha.slice(0, 7)}\` to see what changed.*`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const rawKb = Math.round(JSON.stringify(spans).length / 1024);
|
|
141
|
+
const factKb = Math.max(1, Math.round(lines.join("\n").length / 1024));
|
|
142
|
+
lines.push(
|
|
143
|
+
"",
|
|
144
|
+
"---",
|
|
145
|
+
`*Compacted ${rawKb}KB raw trace (${spans.length} spans) \u2192 ${factKb}KB Runtime Fact*`
|
|
146
|
+
);
|
|
147
|
+
return {
|
|
148
|
+
markdown: lines.join("\n"),
|
|
149
|
+
service,
|
|
150
|
+
traceId,
|
|
151
|
+
endpoint,
|
|
152
|
+
errorMessage: errorMsg,
|
|
153
|
+
failingFile,
|
|
154
|
+
failingLine,
|
|
155
|
+
deployedSha,
|
|
156
|
+
rawSpanCount: spans.length,
|
|
157
|
+
compactedSpanCount: errorSpans.length
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function compact(input) {
|
|
161
|
+
const pruned = pruneTrace(input.spans);
|
|
162
|
+
const stripped = pruned.map((s) => ({ ...s, tags: stripAttributes(s.tags) }));
|
|
163
|
+
const withLogs = await inlineLogs(stripped, input.timeWindow.from, input.timeWindow.to);
|
|
164
|
+
const fact = await serializeToFact(withLogs, input.traceId);
|
|
165
|
+
return { fact };
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
compact
|
|
169
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
function normalizeEndpoint(raw) {
|
|
3
|
+
return raw.toLowerCase().replace(/\/\d+/g, "/{id}").replace(/\/[0-9a-f]{8}-[0-9a-f-]{23,35}/g, "/{uuid}").replace(/[^a-z0-9:/_{}]/g, "");
|
|
4
|
+
}
|
|
5
|
+
function errorClass(msg) {
|
|
6
|
+
return msg.trim().split(/[\s:,.(]/)[0].toLowerCase().slice(0, 24);
|
|
7
|
+
}
|
|
8
|
+
function extractTable(stmt) {
|
|
9
|
+
const m = stmt.match(/(?:FROM|INTO|UPDATE|JOIN)\s+["'`]?(\w+)["'`]?/i);
|
|
10
|
+
return m ? m[1].toLowerCase() : "";
|
|
11
|
+
}
|
|
12
|
+
function computeFingerprint(input) {
|
|
13
|
+
const et = (input.errorType ?? "").toLowerCase().replace(/[^a-z0-9.]/g, "");
|
|
14
|
+
const svc = input.service.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
15
|
+
const ep = normalizeEndpoint(input.endpoint);
|
|
16
|
+
const tbl = (input.dbTable ?? "").toLowerCase().replace(/[^a-z0-9_]/g, "");
|
|
17
|
+
const ec = input.errorMessage ? errorClass(input.errorMessage) : "";
|
|
18
|
+
const key = [et, svc, ep, tbl, ec].join("|");
|
|
19
|
+
return crypto.createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
20
|
+
}
|
|
21
|
+
function fingerprintFromFact(fact) {
|
|
22
|
+
const dbTable = extractTable(fact.errorMessage);
|
|
23
|
+
return computeFingerprint({
|
|
24
|
+
errorType: fact.errorMessage.split(":")[0],
|
|
25
|
+
service: fact.service,
|
|
26
|
+
endpoint: fact.endpoint,
|
|
27
|
+
dbTable,
|
|
28
|
+
errorMessage: fact.errorMessage
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
computeFingerprint,
|
|
33
|
+
fingerprintFromFact
|
|
34
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const INCIDENT_TTL_MS = 2 * 60 * 60 * 1e3;
|
|
2
|
+
let activeIncident = null;
|
|
3
|
+
function setActiveIncident(incident) {
|
|
4
|
+
activeIncident = incident;
|
|
5
|
+
}
|
|
6
|
+
function getActiveIncident() {
|
|
7
|
+
if (!activeIncident) return null;
|
|
8
|
+
if (Date.now() - activeIncident.firedAt > INCIDENT_TTL_MS) {
|
|
9
|
+
activeIncident = null;
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return activeIncident;
|
|
13
|
+
}
|
|
14
|
+
function clearActiveIncident() {
|
|
15
|
+
activeIncident = null;
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
clearActiveIncident,
|
|
19
|
+
getActiveIncident,
|
|
20
|
+
setActiveIncident
|
|
21
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
const CONTEXT_LINES = 15;
|
|
5
|
+
function findLocalFile(filePath, cwd = process.cwd()) {
|
|
6
|
+
if (path.isAbsolute(filePath) && existsSync(filePath)) return filePath;
|
|
7
|
+
const rel = path.resolve(cwd, filePath);
|
|
8
|
+
if (existsSync(rel)) return rel;
|
|
9
|
+
const parts = filePath.split("/");
|
|
10
|
+
for (let i = 1; i < parts.length; i++) {
|
|
11
|
+
const candidate = path.resolve(cwd, parts.slice(i).join("/"));
|
|
12
|
+
if (existsSync(candidate)) return candidate;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
function localHeadSha() {
|
|
17
|
+
try {
|
|
18
|
+
return execSync("git rev-parse HEAD", {
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
21
|
+
}).trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function matchLines(filePath, lineNumber, deployedSha) {
|
|
27
|
+
const localFile = findLocalFile(filePath);
|
|
28
|
+
if (!localFile) {
|
|
29
|
+
return `[file not found in local workspace: ${filePath}]`;
|
|
30
|
+
}
|
|
31
|
+
let shaWarning = "";
|
|
32
|
+
if (deployedSha) {
|
|
33
|
+
const localSha = localHeadSha();
|
|
34
|
+
if (localSha && !localSha.startsWith(deployedSha.slice(0, 7)) && !deployedSha.startsWith(localSha.slice(0, 7))) {
|
|
35
|
+
shaWarning = `
|
|
36
|
+
\u26A0 SHA mismatch: deployed=${deployedSha.slice(0, 7)}, local=${localSha.slice(0, 7)} \u2014 code below may differ from what's running in production`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const content = readFileSync(localFile, "utf8");
|
|
41
|
+
const allLines = content.split("\n");
|
|
42
|
+
const start = Math.max(0, lineNumber - CONTEXT_LINES - 1);
|
|
43
|
+
const end = Math.min(allLines.length, lineNumber + CONTEXT_LINES);
|
|
44
|
+
const excerpt = allLines.slice(start, end).map((line, i) => {
|
|
45
|
+
const ln = start + i + 1;
|
|
46
|
+
const marker = ln === lineNumber ? ">>>" : " ";
|
|
47
|
+
return `${marker} ${String(ln).padStart(4)}: ${line}`;
|
|
48
|
+
}).join("\n");
|
|
49
|
+
return excerpt + shaWarning;
|
|
50
|
+
} catch {
|
|
51
|
+
return `[could not read ${localFile}]${shaWarning}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
matchLines
|
|
56
|
+
};
|