@mneme-ai/correlator 0.8.3
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/README.md +42 -0
- package/dist/adapters/datadog.d.ts +41 -0
- package/dist/adapters/datadog.d.ts.map +1 -0
- package/dist/adapters/datadog.js +136 -0
- package/dist/adapters/datadog.js.map +1 -0
- package/dist/adapters/github-actions.d.ts +44 -0
- package/dist/adapters/github-actions.d.ts.map +1 -0
- package/dist/adapters/github-actions.js +114 -0
- package/dist/adapters/github-actions.js.map +1 -0
- package/dist/adapters/manual.d.ts +20 -0
- package/dist/adapters/manual.d.ts.map +1 -0
- package/dist/adapters/manual.js +47 -0
- package/dist/adapters/manual.js.map +1 -0
- package/dist/adapters/manual.test.d.ts +2 -0
- package/dist/adapters/manual.test.d.ts.map +1 -0
- package/dist/adapters/manual.test.js +54 -0
- package/dist/adapters/manual.test.js.map +1 -0
- package/dist/adapters/sentry.d.ts +51 -0
- package/dist/adapters/sentry.d.ts.map +1 -0
- package/dist/adapters/sentry.js +206 -0
- package/dist/adapters/sentry.js.map +1 -0
- package/dist/adapters/sentry.test.d.ts +2 -0
- package/dist/adapters/sentry.test.d.ts.map +1 -0
- package/dist/adapters/sentry.test.js +241 -0
- package/dist/adapters/sentry.test.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/temporal.d.ts +24 -0
- package/dist/temporal.d.ts.map +1 -0
- package/dist/temporal.js +121 -0
- package/dist/temporal.js.map +1 -0
- package/dist/temporal.test.d.ts +2 -0
- package/dist/temporal.test.d.ts.map +1 -0
- package/dist/temporal.test.js +132 -0
- package/dist/temporal.test.js.map +1 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @mneme-ai/correlator
|
|
2
|
+
|
|
3
|
+
Phase 3 — error/incident correlation engine for [Mneme](https://github.com/patsa2561-art/mneme-ai).
|
|
4
|
+
|
|
5
|
+
The differentiator. Joins commits + incidents + time into one graph, so you can ask:
|
|
6
|
+
|
|
7
|
+
> *"Every time PaymentService.charge is touched, a Stripe webhook 500 spikes within 48h."*
|
|
8
|
+
>
|
|
9
|
+
> *"This PR touches code that has caused 3 of the last 5 incidents in OrderQueue."*
|
|
10
|
+
>
|
|
11
|
+
> *"Incident INC-1287 was likely introduced by commit a1b2c3d (87% confidence — same file, 14h before the spike)."*
|
|
12
|
+
|
|
13
|
+
## What's in here
|
|
14
|
+
|
|
15
|
+
- `temporal.ts` — `TemporalCorrelationEngine` (temporal proximity + file overlap)
|
|
16
|
+
- `adapters/sentry.ts` — first pager adapter (REST API; live)
|
|
17
|
+
- `adapters/datadog.ts` — Datadog adapter (stub, in progress)
|
|
18
|
+
- `adapters/manual.ts` — JSON file input (works today, useful for testing)
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { TemporalCorrelationEngine, SentryAdapter } from "@mneme-ai/correlator";
|
|
24
|
+
|
|
25
|
+
const sentry = new SentryAdapter({
|
|
26
|
+
orgSlug: "my-org",
|
|
27
|
+
projectSlug: "web",
|
|
28
|
+
apiToken: process.env.SENTRY_TOKEN!,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const incidents = await sentry.fetch({ since: "2025-01-01T00:00:00Z" });
|
|
32
|
+
const engine = new TemporalCorrelationEngine();
|
|
33
|
+
const correlations = await engine.correlate({
|
|
34
|
+
commits,
|
|
35
|
+
incidents,
|
|
36
|
+
windowMs: 7 * 24 * 60 * 60 * 1000,
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datadog Events / Incidents adapter — pulls error-class events from the
|
|
3
|
+
* Datadog Events API v2 and maps them onto the Mneme `Incident` shape so
|
|
4
|
+
* the temporal correlation engine can rank them against commits.
|
|
5
|
+
*
|
|
6
|
+
* Endpoint: POST https://api.{site}/api/v2/events/search
|
|
7
|
+
* Auth: DD-API-KEY + DD-APPLICATION-KEY headers
|
|
8
|
+
*
|
|
9
|
+
* "Site" is one of: datadoghq.com (US1), us3.datadoghq.com, us5.datadoghq.com,
|
|
10
|
+
* datadoghq.eu, ap1.datadoghq.com, ddog-gov.com — passed via the `site` option.
|
|
11
|
+
*
|
|
12
|
+
* Mneme stays brand-neutral in user-facing copy ("the pager"); the adapter
|
|
13
|
+
* class name is technically accurate because that's the API it speaks.
|
|
14
|
+
*/
|
|
15
|
+
import type { Incident, correlate as CorrelateNS } from "@mneme-ai/core";
|
|
16
|
+
export interface DatadogAdapterOptions {
|
|
17
|
+
apiKey: string;
|
|
18
|
+
appKey: string;
|
|
19
|
+
/** "datadoghq.com" (default), "us5.datadoghq.com", "datadoghq.eu", etc. */
|
|
20
|
+
site?: string;
|
|
21
|
+
/** Filter expression, e.g. `source:error,exception` (default) */
|
|
22
|
+
filterQuery?: string;
|
|
23
|
+
/** Cap on events fetched. Default 1000. */
|
|
24
|
+
maxEvents?: number;
|
|
25
|
+
/** Per-request timeout (ms). */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare class DatadogAdapter implements CorrelateNS.IncidentAdapter {
|
|
29
|
+
readonly source: "datadog";
|
|
30
|
+
private readonly apiKey;
|
|
31
|
+
private readonly appKey;
|
|
32
|
+
private readonly site;
|
|
33
|
+
private readonly filterQuery;
|
|
34
|
+
private readonly maxEvents;
|
|
35
|
+
private readonly timeoutMs;
|
|
36
|
+
constructor(opts: DatadogAdapterOptions);
|
|
37
|
+
fetch(opts: CorrelateNS.FetchIncidentOptions): Promise<Incident[]>;
|
|
38
|
+
private fetchPage;
|
|
39
|
+
private mapEvent;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=datadog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"datadog.d.ts","sourceRoot":"","sources":["../../src/adapters/datadog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,IAAI,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEzE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID,qBAAa,cAAe,YAAW,WAAW,CAAC,eAAe;IAChE,QAAQ,CAAC,MAAM,EAAG,SAAS,CAAU;IAErC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,IAAI,EAAE,qBAAqB;IAWjC,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC,oBAAoB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;YAgB1D,SAAS;IA0DvB,OAAO,CAAC,QAAQ;CAwBjB"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const DEFAULT_SITE = "datadoghq.com";
|
|
2
|
+
export class DatadogAdapter {
|
|
3
|
+
source = "datadog";
|
|
4
|
+
apiKey;
|
|
5
|
+
appKey;
|
|
6
|
+
site;
|
|
7
|
+
filterQuery;
|
|
8
|
+
maxEvents;
|
|
9
|
+
timeoutMs;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
if (!opts.apiKey)
|
|
12
|
+
throw new Error("DatadogAdapter requires apiKey");
|
|
13
|
+
if (!opts.appKey)
|
|
14
|
+
throw new Error("DatadogAdapter requires appKey");
|
|
15
|
+
this.apiKey = opts.apiKey;
|
|
16
|
+
this.appKey = opts.appKey;
|
|
17
|
+
this.site = opts.site ?? DEFAULT_SITE;
|
|
18
|
+
this.filterQuery = opts.filterQuery ?? "source:error,exception status:error";
|
|
19
|
+
this.maxEvents = Math.max(1, opts.maxEvents ?? 1000);
|
|
20
|
+
this.timeoutMs = opts.timeoutMs ?? 15_000;
|
|
21
|
+
}
|
|
22
|
+
async fetch(opts) {
|
|
23
|
+
const out = [];
|
|
24
|
+
let cursor = null;
|
|
25
|
+
while (out.length < this.maxEvents) {
|
|
26
|
+
const remaining = this.maxEvents - out.length;
|
|
27
|
+
const pageSize = Math.min(100, remaining);
|
|
28
|
+
const result = await this.fetchPage(opts, cursor, pageSize);
|
|
29
|
+
if (!result || result.events.length === 0)
|
|
30
|
+
break;
|
|
31
|
+
out.push(...result.events.map((e) => this.mapEvent(e)));
|
|
32
|
+
if (!result.nextCursor)
|
|
33
|
+
break;
|
|
34
|
+
cursor = result.nextCursor;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
async fetchPage(opts, cursor, pageSize, attempt = 0) {
|
|
39
|
+
const url = `https://api.${this.site}/api/v2/events/search`;
|
|
40
|
+
const body = {
|
|
41
|
+
filter: {
|
|
42
|
+
query: this.filterQuery,
|
|
43
|
+
...(opts.since ? { from: opts.since } : { from: "now-30d" }),
|
|
44
|
+
...(opts.until ? { to: opts.until } : { to: "now" }),
|
|
45
|
+
},
|
|
46
|
+
sort: "timestamp",
|
|
47
|
+
page: { limit: pageSize, ...(cursor ? { cursor } : {}) },
|
|
48
|
+
};
|
|
49
|
+
const ctrl = new AbortController();
|
|
50
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: {
|
|
55
|
+
"DD-API-KEY": this.apiKey,
|
|
56
|
+
"DD-APPLICATION-KEY": this.appKey,
|
|
57
|
+
"content-type": "application/json",
|
|
58
|
+
accept: "application/json",
|
|
59
|
+
"user-agent": "mneme/0.1",
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
signal: ctrl.signal,
|
|
63
|
+
});
|
|
64
|
+
if (res.status === 404)
|
|
65
|
+
return null;
|
|
66
|
+
if (res.status === 429 || res.status >= 500) {
|
|
67
|
+
if (attempt >= 4) {
|
|
68
|
+
throw new Error(`Datadog API ${res.status} after ${attempt} retries: ${url}`);
|
|
69
|
+
}
|
|
70
|
+
const retryAfter = Number(res.headers.get("retry-after") ?? 0);
|
|
71
|
+
const backoff = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500;
|
|
72
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
73
|
+
return this.fetchPage(opts, cursor, pageSize, attempt + 1);
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const txt = await res.text();
|
|
77
|
+
throw new Error(`Datadog API ${res.status}: ${txt.slice(0, 200)}`);
|
|
78
|
+
}
|
|
79
|
+
const json = (await res.json());
|
|
80
|
+
return {
|
|
81
|
+
events: json.data ?? [],
|
|
82
|
+
nextCursor: json.meta?.page?.after ?? null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
mapEvent(e) {
|
|
90
|
+
const id = `datadog:${e.id ?? e.attributes?.attributes?.evt?.id ?? "unknown"}`;
|
|
91
|
+
const attrs = e.attributes?.attributes ?? {};
|
|
92
|
+
const title = String(attrs.message ?? attrs.title ?? "Datadog event");
|
|
93
|
+
const occurredAt = String(attrs.timestamp ?? new Date().toISOString());
|
|
94
|
+
const severity = mapStatus(String(attrs.status ?? "error"));
|
|
95
|
+
const service = attrs.service ? String(attrs.service) : undefined;
|
|
96
|
+
const host = attrs.host ? String(attrs.host) : undefined;
|
|
97
|
+
const affectedFiles = [];
|
|
98
|
+
if (typeof attrs.error?.stack === "string") {
|
|
99
|
+
affectedFiles.push(...extractFiles(attrs.error.stack));
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
id,
|
|
103
|
+
source: "datadog",
|
|
104
|
+
externalId: e.id,
|
|
105
|
+
title: title.slice(0, 240),
|
|
106
|
+
occurredAt,
|
|
107
|
+
severity,
|
|
108
|
+
affectedFiles: affectedFiles.length ? Array.from(new Set(affectedFiles)) : undefined,
|
|
109
|
+
url: attrs.url ? String(attrs.url) : undefined,
|
|
110
|
+
metadata: { service, host, tags: attrs.tags ?? [] },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function mapStatus(status) {
|
|
115
|
+
switch (status.toLowerCase()) {
|
|
116
|
+
case "critical":
|
|
117
|
+
case "fatal":
|
|
118
|
+
return "critical";
|
|
119
|
+
case "error":
|
|
120
|
+
return "error";
|
|
121
|
+
case "warn":
|
|
122
|
+
case "warning":
|
|
123
|
+
return "warning";
|
|
124
|
+
case "info":
|
|
125
|
+
case "debug":
|
|
126
|
+
case "ok":
|
|
127
|
+
return "info";
|
|
128
|
+
default:
|
|
129
|
+
return "error";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function extractFiles(stack) {
|
|
133
|
+
const matches = stack.match(/[\w./\\-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|kt|cs|rb|php)/g);
|
|
134
|
+
return matches ?? [];
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=datadog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"datadog.js","sourceRoot":"","sources":["../../src/adapters/datadog.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions failure adapter.
|
|
3
|
+
*
|
|
4
|
+
* GET /repos/{owner}/{repo}/actions/runs?status=failure&per_page=100
|
|
5
|
+
*
|
|
6
|
+
* Maps each failed workflow run to an `Incident`. Useful for repos that don't
|
|
7
|
+
* have a paid observability stack but DO have CI: a failed deploy or test run
|
|
8
|
+
* IS an incident.
|
|
9
|
+
*
|
|
10
|
+
* • title: workflow name + run number + head commit subject
|
|
11
|
+
* • occurredAt: run.run_started_at
|
|
12
|
+
* • severity: "error" (always — these are red CI runs)
|
|
13
|
+
* • affectedFiles: pulled from the run's commit if available
|
|
14
|
+
* • url: run.html_url
|
|
15
|
+
*/
|
|
16
|
+
import type { Incident, correlate as CorrelateNS } from "@mneme-ai/core";
|
|
17
|
+
export interface GitHubActionsAdapterOptions {
|
|
18
|
+
owner: string;
|
|
19
|
+
repo: string;
|
|
20
|
+
/** Token with `actions:read` and `contents:read`. Use a fine-grained PAT or installation token. */
|
|
21
|
+
token: string;
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
/** Cap on runs fetched. Default 1000. */
|
|
24
|
+
maxRuns?: number;
|
|
25
|
+
/** Per-request timeout (ms). */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
/** Filter to a single workflow file (e.g. "deploy.yml"). */
|
|
28
|
+
workflow?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare class GitHubActionsAdapter implements CorrelateNS.IncidentAdapter {
|
|
31
|
+
readonly source: "github";
|
|
32
|
+
private readonly base;
|
|
33
|
+
private readonly token;
|
|
34
|
+
private readonly owner;
|
|
35
|
+
private readonly repo;
|
|
36
|
+
private readonly maxRuns;
|
|
37
|
+
private readonly timeoutMs;
|
|
38
|
+
private readonly workflow?;
|
|
39
|
+
constructor(opts: GitHubActionsAdapterOptions);
|
|
40
|
+
fetch(opts: CorrelateNS.FetchIncidentOptions): Promise<Incident[]>;
|
|
41
|
+
private request;
|
|
42
|
+
private mapRun;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=github-actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-actions.d.ts","sourceRoot":"","sources":["../../src/adapters/github-actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,IAAI,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEzE,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,mGAAmG;IACnG,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAID,qBAAa,oBAAqB,YAAW,WAAW,CAAC,eAAe;IACtE,QAAQ,CAAC,MAAM,EAAG,QAAQ,CAAU;IAEpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;gBAEvB,IAAI,EAAE,2BAA2B;IAavC,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC,oBAAoB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;YA2B1D,OAAO;IA6BrB,OAAO,CAAC,MAAM;CA2Bf"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const DEFAULT_BASE = "https://api.github.com";
|
|
2
|
+
export class GitHubActionsAdapter {
|
|
3
|
+
source = "github";
|
|
4
|
+
base;
|
|
5
|
+
token;
|
|
6
|
+
owner;
|
|
7
|
+
repo;
|
|
8
|
+
maxRuns;
|
|
9
|
+
timeoutMs;
|
|
10
|
+
workflow;
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
if (!opts.owner)
|
|
13
|
+
throw new Error("GitHubActionsAdapter requires owner");
|
|
14
|
+
if (!opts.repo)
|
|
15
|
+
throw new Error("GitHubActionsAdapter requires repo");
|
|
16
|
+
if (!opts.token)
|
|
17
|
+
throw new Error("GitHubActionsAdapter requires token");
|
|
18
|
+
this.base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
19
|
+
this.token = opts.token;
|
|
20
|
+
this.owner = opts.owner;
|
|
21
|
+
this.repo = opts.repo;
|
|
22
|
+
this.maxRuns = Math.max(1, opts.maxRuns ?? 1000);
|
|
23
|
+
this.timeoutMs = opts.timeoutMs ?? 15_000;
|
|
24
|
+
this.workflow = opts.workflow;
|
|
25
|
+
}
|
|
26
|
+
async fetch(opts) {
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
status: "failure",
|
|
29
|
+
per_page: "100",
|
|
30
|
+
});
|
|
31
|
+
if (opts.since)
|
|
32
|
+
params.set("created", `>=${opts.since}`);
|
|
33
|
+
const path = this.workflow
|
|
34
|
+
? `/repos/${this.owner}/${this.repo}/actions/workflows/${encodeURIComponent(this.workflow)}/runs`
|
|
35
|
+
: `/repos/${this.owner}/${this.repo}/actions/runs`;
|
|
36
|
+
const out = [];
|
|
37
|
+
let page = 1;
|
|
38
|
+
while (out.length < this.maxRuns) {
|
|
39
|
+
const url = `${this.base}${path}?${params.toString()}&page=${page}`;
|
|
40
|
+
const json = await this.request(url);
|
|
41
|
+
if (!json || !json.workflow_runs?.length)
|
|
42
|
+
break;
|
|
43
|
+
for (const run of json.workflow_runs) {
|
|
44
|
+
out.push(this.mapRun(run));
|
|
45
|
+
if (out.length >= this.maxRuns)
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (json.workflow_runs.length < 100)
|
|
49
|
+
break;
|
|
50
|
+
page++;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
async request(url, attempt = 0) {
|
|
55
|
+
const headers = {
|
|
56
|
+
authorization: `Bearer ${this.token}`,
|
|
57
|
+
accept: "application/vnd.github+json",
|
|
58
|
+
"x-github-api-version": "2022-11-28",
|
|
59
|
+
"user-agent": "mneme/0.1",
|
|
60
|
+
};
|
|
61
|
+
const ctrl = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(url, { headers, signal: ctrl.signal });
|
|
65
|
+
if (res.status === 404)
|
|
66
|
+
return null;
|
|
67
|
+
if (res.status === 429 || res.status >= 500) {
|
|
68
|
+
if (attempt >= 4)
|
|
69
|
+
throw new Error(`GitHub API ${res.status} after ${attempt} retries`);
|
|
70
|
+
const retryAfter = Number(res.headers.get("retry-after") ?? 0);
|
|
71
|
+
const backoff = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500;
|
|
72
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
73
|
+
return this.request(url, attempt + 1);
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const txt = await res.text();
|
|
77
|
+
throw new Error(`GitHub API ${res.status}: ${txt.slice(0, 200)}`);
|
|
78
|
+
}
|
|
79
|
+
return (await res.json());
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
mapRun(run) {
|
|
86
|
+
const id = `github-actions:${run.id}`;
|
|
87
|
+
const title = `${run.name} #${run.run_number}: ${run.display_title ?? "(no title)"}`;
|
|
88
|
+
const affectedFiles = [];
|
|
89
|
+
if (run.head_commit?.message) {
|
|
90
|
+
// Heuristic: if the commit message references files, pull them out.
|
|
91
|
+
const m = run.head_commit.message.match(/[\w./\\-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|kt|cs|rb|php)/g);
|
|
92
|
+
if (m)
|
|
93
|
+
affectedFiles.push(...m);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
source: "github",
|
|
98
|
+
externalId: String(run.id),
|
|
99
|
+
title: title.slice(0, 240),
|
|
100
|
+
occurredAt: run.run_started_at ?? run.created_at,
|
|
101
|
+
severity: "error",
|
|
102
|
+
affectedFiles: affectedFiles.length ? Array.from(new Set(affectedFiles)) : undefined,
|
|
103
|
+
url: run.html_url,
|
|
104
|
+
metadata: {
|
|
105
|
+
workflow: run.name,
|
|
106
|
+
runNumber: run.run_number,
|
|
107
|
+
headBranch: run.head_branch,
|
|
108
|
+
headSha: run.head_sha,
|
|
109
|
+
event: run.event,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=github-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-actions.js","sourceRoot":"","sources":["../../src/adapters/github-actions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Incident, correlate as CorrelateNS } from "@mneme-ai/core";
|
|
2
|
+
/**
|
|
3
|
+
* Manual JSON incident adapter — works today.
|
|
4
|
+
*
|
|
5
|
+
* Reads a JSON file shaped like:
|
|
6
|
+
* [
|
|
7
|
+
* { "id": "INC-1", "title": "Stripe webhook 500s", "occurredAt": "2025-09-01T10:00:00Z",
|
|
8
|
+
* "severity": "error", "affectedFiles": ["src/payment.ts"], "url": "https://..." }
|
|
9
|
+
* ]
|
|
10
|
+
*
|
|
11
|
+
* Useful for: bootstrapping a customer that doesn't have an observability platform yet, or
|
|
12
|
+
* importing from CSV/spreadsheet exports.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ManualJsonAdapter implements CorrelateNS.IncidentAdapter {
|
|
15
|
+
private readonly path;
|
|
16
|
+
readonly source: "manual";
|
|
17
|
+
constructor(path: string);
|
|
18
|
+
fetch(_opts: CorrelateNS.FetchIncidentOptions): Promise<Incident[]>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=manual.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manual.d.ts","sourceRoot":"","sources":["../../src/adapters/manual.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,IAAI,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEzE;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAkB,YAAW,WAAW,CAAC,eAAe;IAGvD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,QAAQ,CAAC,MAAM,EAAG,QAAQ,CAAU;gBAEP,IAAI,EAAE,MAAM;IAEnC,KAAK,CAAC,KAAK,EAAE,WAAW,CAAC,oBAAoB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;CAM1E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Manual JSON incident adapter — works today.
|
|
4
|
+
*
|
|
5
|
+
* Reads a JSON file shaped like:
|
|
6
|
+
* [
|
|
7
|
+
* { "id": "INC-1", "title": "Stripe webhook 500s", "occurredAt": "2025-09-01T10:00:00Z",
|
|
8
|
+
* "severity": "error", "affectedFiles": ["src/payment.ts"], "url": "https://..." }
|
|
9
|
+
* ]
|
|
10
|
+
*
|
|
11
|
+
* Useful for: bootstrapping a customer that doesn't have an observability platform yet, or
|
|
12
|
+
* importing from CSV/spreadsheet exports.
|
|
13
|
+
*/
|
|
14
|
+
export class ManualJsonAdapter {
|
|
15
|
+
path;
|
|
16
|
+
source = "manual";
|
|
17
|
+
constructor(path) {
|
|
18
|
+
this.path = path;
|
|
19
|
+
}
|
|
20
|
+
async fetch(_opts) {
|
|
21
|
+
const raw = readFileSync(this.path, "utf8");
|
|
22
|
+
const arr = JSON.parse(raw);
|
|
23
|
+
if (!Array.isArray(arr))
|
|
24
|
+
throw new Error(`${this.path} did not contain a JSON array`);
|
|
25
|
+
return arr.map((row, i) => normalize(row, i));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function normalize(row, idx) {
|
|
29
|
+
if (!row.title)
|
|
30
|
+
throw new Error(`incident at index ${idx} is missing 'title'`);
|
|
31
|
+
if (!row.occurredAt)
|
|
32
|
+
throw new Error(`incident at index ${idx} is missing 'occurredAt'`);
|
|
33
|
+
return {
|
|
34
|
+
id: String(row.id ?? `manual-${idx}`),
|
|
35
|
+
source: "manual",
|
|
36
|
+
externalId: row.externalId,
|
|
37
|
+
title: String(row.title),
|
|
38
|
+
occurredAt: String(row.occurredAt),
|
|
39
|
+
resolvedAt: row.resolvedAt,
|
|
40
|
+
severity: (row.severity ?? "error"),
|
|
41
|
+
affectedFiles: row.affectedFiles,
|
|
42
|
+
stackFrames: row.stackFrames,
|
|
43
|
+
url: row.url,
|
|
44
|
+
metadata: row.metadata,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=manual.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manual.js","sourceRoot":"","sources":["../../src/adapters/manual.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvC;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,iBAAiB;IAGC;IAFpB,MAAM,GAAG,QAAiB,CAAC;IAEpC,YAA6B,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;IAAG,CAAC;IAE7C,KAAK,CAAC,KAAK,CAAC,KAAuC;QACjD,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,+BAA+B,CAAC,CAAC;QACtF,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC;CACF;AAED,SAAS,SAAS,CAAC,GAAQ,EAAE,GAAW;IACtC,IAAI,CAAC,GAAG,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,qBAAqB,CAAC,CAAC;IAC/E,IAAI,CAAC,GAAG,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,0BAA0B,CAAC,CAAC;IACzF,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,UAAU,GAAG,EAAE,CAAC;QACrC,MAAM,EAAE,QAAQ;QAChB,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;QAClC,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,QAAQ,EAAE,CAAC,GAAG,CAAC,QAAQ,IAAI,OAAO,CAAyB;QAC3D,aAAa,EAAE,GAAG,CAAC,aAAa;QAChC,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,QAAQ,EAAE,GAAG,CAAC,QAAQ;KACvB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manual.test.d.ts","sourceRoot":"","sources":["../../src/adapters/manual.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { ManualJsonAdapter } from "./manual.js";
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let path;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mneme-manual-"));
|
|
10
|
+
path = join(tmpDir, "incidents.json");
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
describe("ManualJsonAdapter", () => {
|
|
16
|
+
it("loads valid incidents", async () => {
|
|
17
|
+
writeFileSync(path, JSON.stringify([
|
|
18
|
+
{
|
|
19
|
+
id: "INC-1",
|
|
20
|
+
title: "Stripe webhook 500",
|
|
21
|
+
occurredAt: "2025-01-15T10:00:00Z",
|
|
22
|
+
severity: "error",
|
|
23
|
+
},
|
|
24
|
+
]));
|
|
25
|
+
const adapter = new ManualJsonAdapter(path);
|
|
26
|
+
const incidents = await adapter.fetch({});
|
|
27
|
+
expect(incidents).toHaveLength(1);
|
|
28
|
+
expect(incidents[0].title).toBe("Stripe webhook 500");
|
|
29
|
+
expect(incidents[0].source).toBe("manual");
|
|
30
|
+
});
|
|
31
|
+
it("auto-fills id when missing", async () => {
|
|
32
|
+
writeFileSync(path, JSON.stringify([{ title: "x", occurredAt: "2025-01-01T00:00:00Z" }]));
|
|
33
|
+
const adapter = new ManualJsonAdapter(path);
|
|
34
|
+
const incidents = await adapter.fetch({});
|
|
35
|
+
expect(incidents[0].id).toBe("manual-0");
|
|
36
|
+
});
|
|
37
|
+
it("defaults severity to 'error'", async () => {
|
|
38
|
+
writeFileSync(path, JSON.stringify([{ title: "x", occurredAt: "2025-01-01T00:00:00Z" }]));
|
|
39
|
+
const adapter = new ManualJsonAdapter(path);
|
|
40
|
+
const [inc] = await adapter.fetch({});
|
|
41
|
+
expect(inc.severity).toBe("error");
|
|
42
|
+
});
|
|
43
|
+
it("throws if file is not a JSON array", async () => {
|
|
44
|
+
writeFileSync(path, JSON.stringify({ not: "array" }));
|
|
45
|
+
const adapter = new ManualJsonAdapter(path);
|
|
46
|
+
await expect(adapter.fetch({})).rejects.toThrow(/JSON array/);
|
|
47
|
+
});
|
|
48
|
+
it("throws on missing required fields", async () => {
|
|
49
|
+
writeFileSync(path, JSON.stringify([{ id: "x" }]));
|
|
50
|
+
const adapter = new ManualJsonAdapter(path);
|
|
51
|
+
await expect(adapter.fetch({})).rejects.toThrow(/title/);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
//# sourceMappingURL=manual.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manual.test.js","sourceRoot":"","sources":["../../src/adapters/manual.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry incident adapter — real implementation.
|
|
3
|
+
*
|
|
4
|
+
* The Sentry REST API gives us issues with first/last seen timestamps and
|
|
5
|
+
* culprit/frames. We fetch the issue list, optionally fetch the latest event
|
|
6
|
+
* for each to get stack frames, and map every result onto the Mneme `Incident`
|
|
7
|
+
* shape so the temporal correlation engine can rank against commits.
|
|
8
|
+
*
|
|
9
|
+
* Endpoint reference:
|
|
10
|
+
* GET /api/0/projects/{org}/{project}/issues/?statsPeriod=...
|
|
11
|
+
* GET /api/0/issues/{id}/events/latest/
|
|
12
|
+
*
|
|
13
|
+
* Auth: Bearer <token>. Token must have `event:read` and `project:read`.
|
|
14
|
+
*
|
|
15
|
+
* This adapter is opt-in. The token never touches disk. Rate-limit handling
|
|
16
|
+
* matches the GitHub adapter (Retry-After + exponential backoff on 429/5xx).
|
|
17
|
+
*/
|
|
18
|
+
import type { Incident, correlate as CorrelateNS } from "@mneme-ai/core";
|
|
19
|
+
export interface SentryAdapterOptions {
|
|
20
|
+
orgSlug: string;
|
|
21
|
+
projectSlug: string;
|
|
22
|
+
apiToken: string;
|
|
23
|
+
/** https://sentry.io for SaaS, https://sentry.your-co.com for self-hosted. */
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
/** Concurrent event-detail requests. Default 4. */
|
|
26
|
+
concurrency?: number;
|
|
27
|
+
/** Per-request timeout (ms). */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
/** Pull stack frames from the most recent event of each issue. Default true. */
|
|
30
|
+
fetchStackFrames?: boolean;
|
|
31
|
+
/** Cap on issues fetched. Default 1000. */
|
|
32
|
+
maxIssues?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class SentryAdapter implements CorrelateNS.IncidentAdapter {
|
|
35
|
+
readonly source: "sentry";
|
|
36
|
+
private readonly base;
|
|
37
|
+
private readonly token;
|
|
38
|
+
private readonly orgSlug;
|
|
39
|
+
private readonly projectSlug;
|
|
40
|
+
private readonly concurrency;
|
|
41
|
+
private readonly timeoutMs;
|
|
42
|
+
private readonly fetchStackFrames;
|
|
43
|
+
private readonly maxIssues;
|
|
44
|
+
constructor(opts: SentryAdapterOptions);
|
|
45
|
+
fetch(opts: CorrelateNS.FetchIncidentOptions): Promise<Incident[]>;
|
|
46
|
+
private listIssues;
|
|
47
|
+
private mapIssue;
|
|
48
|
+
private fetchLatestEventFrames;
|
|
49
|
+
private requestWithLink;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=sentry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentry.d.ts","sourceRoot":"","sources":["../../src/adapters/sentry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAc,SAAS,IAAI,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAErF,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID,qBAAa,aAAc,YAAW,WAAW,CAAC,eAAe;IAC/D,QAAQ,CAAC,MAAM,EAAG,QAAQ,CAAU;IAEpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,IAAI,EAAE,oBAAoB;IAchC,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC,oBAAoB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;YA2B1D,UAAU;IA0BxB,OAAO,CAAC,QAAQ,CAoBb;YAEW,sBAAsB;YA0BtB,eAAe;CAkC9B"}
|