crumbtrail-node 0.1.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 +21 -0
- package/README.md +242 -0
- package/dist/bug-widget-LGIRLS76-HL62OAMR.js +201 -0
- package/dist/chunk-EASSXKUJ.js +18920 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/cli.cjs +233486 -0
- package/dist/cli.d.cts +21 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +214747 -0
- package/dist/index.cjs +21337 -0
- package/dist/index.d.cts +2798 -0
- package/dist/index.d.ts +2798 -0
- package/dist/index.js +2519 -0
- package/package.json +62 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,2798 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { Symptom, EvidenceSourceDescriptor, EvidenceQuery, EvidenceSourceResult, DbEngine, TargetDescriptor, BugEvent, EvidenceItem, IntentSignal, EvidenceGap, EvidenceJoinKey, GitHostClient, DbDiffOp } from 'crumbtrail-core';
|
|
3
|
+
export { EvidenceJoinKey, EvidenceSourceDescriptor } from 'crumbtrail-core';
|
|
4
|
+
|
|
5
|
+
type TicketProvider = "jira" | "zendesk" | "trello";
|
|
6
|
+
declare function jiraToSymptom(payload: unknown): Symptom;
|
|
7
|
+
|
|
8
|
+
interface TicketConnector {
|
|
9
|
+
fetchSymptom(id: string): Promise<Symptom>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extension of TicketConnector for providers that can write an advisory
|
|
13
|
+
* comment back to the ticket. Declared as a separate interface (not an
|
|
14
|
+
* optional method on TicketConnector) so read-only connectors stay honest and
|
|
15
|
+
* callers that need commenting — the cloud webhook writer — type against the
|
|
16
|
+
* capability explicitly (contract decision #10).
|
|
17
|
+
*/
|
|
18
|
+
interface CommentingTicketConnector extends TicketConnector {
|
|
19
|
+
postComment(ticketKey: string, adfBody: unknown, retry?: BoundedRetryOptions): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** Thrown when a ticket provider REST call fails, or required env config is missing (status 0). */
|
|
22
|
+
declare class TicketError extends Error {
|
|
23
|
+
status: number;
|
|
24
|
+
constructor(status: number, message: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Source-identification User-Agent sent on EVERY outbound integration request.
|
|
28
|
+
* Atlassian's third-party app guidance requires apps to "identify their source
|
|
29
|
+
* transparently"; a stable, descriptive UA is how a Jira/Confluence admin (and
|
|
30
|
+
* Atlassian's own abuse tooling) can attribute traffic to Crumbtrail. Applied to
|
|
31
|
+
* Zendesk/Trello too for consistency. Kept version-light on purpose so it does
|
|
32
|
+
* not churn every release — it identifies the source, not an exact build.
|
|
33
|
+
*/
|
|
34
|
+
declare const CRUMBTRAIL_USER_AGENT = "Crumbtrail/0.1 (+https://crumbtrail.dev; integrations@crumbtrail.dev)";
|
|
35
|
+
/** Options for {@link withBoundedRetry}. All fields optional; production callers
|
|
36
|
+
* rely on the defaults, tests inject `baseDelayMs: 0`/a fake `sleep` to run fast. */
|
|
37
|
+
interface BoundedRetryOptions {
|
|
38
|
+
/** Total attempts (not extra retries). Default 3. */
|
|
39
|
+
attempts?: number;
|
|
40
|
+
/** Linear backoff base; the wait before attempt N is `baseDelayMs * (N-1)`. */
|
|
41
|
+
baseDelayMs?: number;
|
|
42
|
+
/** Decide whether a thrown error is worth retrying. Default: transient only. */
|
|
43
|
+
isRetryable?: (error: unknown) => boolean;
|
|
44
|
+
/** Sleep hook (overridable in tests). Default: real setTimeout. */
|
|
45
|
+
sleep?: (ms: number) => Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Run `fn` with a small bounded retry (default 3 attempts, short linear
|
|
49
|
+
* backoff). Re-throws the last error once attempts are exhausted or the error is
|
|
50
|
+
* non-retryable. Deliberately tiny — no queue, no jitter; comment posting is the
|
|
51
|
+
* one caller and a v1 best-effort write.
|
|
52
|
+
*/
|
|
53
|
+
declare function withBoundedRetry<T>(fn: () => Promise<T>, options?: BoundedRetryOptions): Promise<T>;
|
|
54
|
+
/** HTTP Basic auth: Atlassian API token bound to an account email
|
|
55
|
+
* (`email:token`, base64). This is the self-host / API-token path. */
|
|
56
|
+
interface JiraBasicAuth {
|
|
57
|
+
type: "basic";
|
|
58
|
+
email: string;
|
|
59
|
+
apiToken: string;
|
|
60
|
+
}
|
|
61
|
+
/** OAuth 3LO bearer auth: a short-lived access token. Per Atlassian, OAuth API
|
|
62
|
+
* calls go to `https://api.atlassian.com/ex/jira/{cloudId}` (NOT the
|
|
63
|
+
* `*.atlassian.net` site base) with `Authorization: Bearer <accessToken>`; the
|
|
64
|
+
* caller sets `baseUrl` accordingly. The client never refreshes — the cloud
|
|
65
|
+
* connector boundary hands in an already-valid access token. */
|
|
66
|
+
interface JiraBearerAuth {
|
|
67
|
+
type: "bearer";
|
|
68
|
+
accessToken: string;
|
|
69
|
+
}
|
|
70
|
+
type JiraAuth = JiraBasicAuth | JiraBearerAuth;
|
|
71
|
+
/** Legacy positional shape — kept byte-compatible so existing callers
|
|
72
|
+
* (ticketClientFromEnv, self-host) construct exactly as before. */
|
|
73
|
+
interface JiraTicketClientConfigLegacy {
|
|
74
|
+
baseUrl: string;
|
|
75
|
+
email: string;
|
|
76
|
+
apiToken: string;
|
|
77
|
+
fetchImpl?: typeof fetch;
|
|
78
|
+
}
|
|
79
|
+
/** Discriminated-auth shape (basic OR bearer) with an explicit baseUrl. */
|
|
80
|
+
interface JiraTicketClientConfigWithAuth {
|
|
81
|
+
baseUrl: string;
|
|
82
|
+
auth: JiraAuth;
|
|
83
|
+
fetchImpl?: typeof fetch;
|
|
84
|
+
}
|
|
85
|
+
type JiraTicketClientConfig = JiraTicketClientConfigLegacy | JiraTicketClientConfigWithAuth;
|
|
86
|
+
declare class JiraTicketClient implements CommentingTicketConnector {
|
|
87
|
+
private baseUrl;
|
|
88
|
+
private auth;
|
|
89
|
+
private fetchImpl;
|
|
90
|
+
constructor(config: JiraTicketClientConfig);
|
|
91
|
+
/** The Authorization header value for the configured auth mode. The secret
|
|
92
|
+
* (token/access token) only ever lives inside this header string — it is
|
|
93
|
+
* never surfaced in an error message (only sanitized URLs are). */
|
|
94
|
+
private authHeader;
|
|
95
|
+
fetchSymptom(id: string): Promise<Symptom>;
|
|
96
|
+
/**
|
|
97
|
+
* POST an advisory comment to a Jira issue. `adfBody` is an Atlassian Document
|
|
98
|
+
* Format doc (see buildAdvisoryComment); it is sent as `{ body: <adf> }`.
|
|
99
|
+
* Wrapped in a small bounded retry so a transient 5xx/429/network blip does not
|
|
100
|
+
* drop the comment. Throws TicketError on a non-2xx that survives the retries —
|
|
101
|
+
* the API token never appears in the message (only the sanitized URL does).
|
|
102
|
+
*/
|
|
103
|
+
postComment(issueIdOrKey: string, adfBody: unknown, retry?: BoundedRetryOptions): Promise<void>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Evidence-source framework (runtime half). Mirrors the ticket-connector
|
|
108
|
+
* pattern in `ticket/clients.ts`: env-var credentials for self-host, a provider
|
|
109
|
+
* is present ⇔ its required vars are all set, and every outbound request carries
|
|
110
|
+
* {@link CRUMBTRAIL_USER_AGENT}. Adapters (CP3+) register here; this file is the
|
|
111
|
+
* contract + presence logic only.
|
|
112
|
+
*/
|
|
113
|
+
/**
|
|
114
|
+
* Health of one configured source. Emitted by `EvidenceSource.health()` (a
|
|
115
|
+
* cheap authenticated no-op) and surfaced by `crumbtrail-server doctor`.
|
|
116
|
+
*/
|
|
117
|
+
interface SourceHealth {
|
|
118
|
+
ok: boolean;
|
|
119
|
+
provider: string;
|
|
120
|
+
/** ms epoch when the check ran. */
|
|
121
|
+
checkedAt: number;
|
|
122
|
+
/** Sanitized failure reason when `ok` is false (never a raw secret). */
|
|
123
|
+
error?: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* One provider's runtime surface. `fetchEvidence` returns neutral evidence.v1
|
|
127
|
+
* items (no ranking); it never gates and — through the framework — degrades to
|
|
128
|
+
* a gap rather than throwing. The optional `signal` lets the framework's
|
|
129
|
+
* per-source timeout abort an in-flight outbound fetch; adapters should thread
|
|
130
|
+
* it into their `fetch` calls when present.
|
|
131
|
+
*/
|
|
132
|
+
interface EvidenceSource {
|
|
133
|
+
readonly descriptor: EvidenceSourceDescriptor;
|
|
134
|
+
health(signal?: AbortSignal): Promise<SourceHealth>;
|
|
135
|
+
fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A registered provider: how to detect its env credentials and construct a
|
|
139
|
+
* source from them. Adapters append an entry to {@link EVIDENCE_SOURCE_PROVIDERS}.
|
|
140
|
+
*/
|
|
141
|
+
interface EvidenceSourceProvider {
|
|
142
|
+
provider: string;
|
|
143
|
+
/**
|
|
144
|
+
* Required env-var names. The provider is considered configured iff every one
|
|
145
|
+
* is set to a non-empty string — mirroring `ticketClientFromEnv`'s
|
|
146
|
+
* present ⇔ required-vars-set rule.
|
|
147
|
+
*/
|
|
148
|
+
authFields: string[];
|
|
149
|
+
fromEnv(env: Record<string, string | undefined>): EvidenceSource;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Provider registry. Adapters (Sentry in CP3, then CloudWatch/Splunk/…) append
|
|
153
|
+
* their {@link EvidenceSourceProvider} here via {@link registerEvidenceProvider}.
|
|
154
|
+
* `evidenceSourcesFromEnv` walks it to build the configured sources.
|
|
155
|
+
*/
|
|
156
|
+
declare const EVIDENCE_SOURCE_PROVIDERS: EvidenceSourceProvider[];
|
|
157
|
+
/**
|
|
158
|
+
* Register a provider once (idempotent by `provider` id). Adapter modules call
|
|
159
|
+
* this at import time; `../evidence-sources/index.ts` imports every adapter so a
|
|
160
|
+
* single import of the barrel populates the registry. Idempotency keeps repeated
|
|
161
|
+
* imports (test isolation, HMR) from double-registering.
|
|
162
|
+
*/
|
|
163
|
+
declare function registerEvidenceProvider(provider: EvidenceSourceProvider): void;
|
|
164
|
+
/**
|
|
165
|
+
* Build the configured evidence sources from env. A provider is included iff all
|
|
166
|
+
* its `authFields` are set. Never throws for a partially-configured provider —
|
|
167
|
+
* it is simply omitted (adapter absence degrades to a gap downstream, never an
|
|
168
|
+
* error). `providers` is injectable for tests.
|
|
169
|
+
*/
|
|
170
|
+
declare function evidenceSourcesFromEnv(env?: Record<string, string | undefined>, providers?: EvidenceSourceProvider[]): EvidenceSource[];
|
|
171
|
+
/**
|
|
172
|
+
* Base headers every adapter's outbound request must include. Exposes the
|
|
173
|
+
* shared source-identifying User-Agent so adapters do not re-declare it.
|
|
174
|
+
*/
|
|
175
|
+
declare function evidenceRequestHeaders(extra?: Record<string, string>): Record<string, string>;
|
|
176
|
+
|
|
177
|
+
type AudioTranscriptionState = "not-requested" | "transcription-ready" | "transcription-unavailable" | "transcription-error";
|
|
178
|
+
interface PostProcessAudioSummary {
|
|
179
|
+
artifact: "audio.webm";
|
|
180
|
+
bytes: number;
|
|
181
|
+
upload?: {
|
|
182
|
+
metadataFile: "audio.json";
|
|
183
|
+
uploadedAt?: number;
|
|
184
|
+
contentType?: string;
|
|
185
|
+
mimeType?: string;
|
|
186
|
+
durationMs?: number;
|
|
187
|
+
chunkCount?: number;
|
|
188
|
+
transcriptionRequested?: boolean;
|
|
189
|
+
};
|
|
190
|
+
transcription: {
|
|
191
|
+
state: AudioTranscriptionState;
|
|
192
|
+
code?: "transcription_unavailable" | "transcription_failed";
|
|
193
|
+
message?: string;
|
|
194
|
+
transcriptFile?: "transcript.json";
|
|
195
|
+
eventCount?: number;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
type Severity = 'critical' | 'high' | 'medium' | 'low';
|
|
200
|
+
interface SessionSummary {
|
|
201
|
+
id: string;
|
|
202
|
+
release?: string;
|
|
203
|
+
build?: string;
|
|
204
|
+
start: number;
|
|
205
|
+
end?: number;
|
|
206
|
+
dur?: number;
|
|
207
|
+
evts?: number;
|
|
208
|
+
errors: number;
|
|
209
|
+
failedReqs: number;
|
|
210
|
+
topSeverity?: Severity;
|
|
211
|
+
title?: string;
|
|
212
|
+
hasVideo: boolean;
|
|
213
|
+
hasDiagnosis: boolean;
|
|
214
|
+
}
|
|
215
|
+
interface SessionFileFlags {
|
|
216
|
+
hasVideo: boolean;
|
|
217
|
+
hasDiagnosis: boolean;
|
|
218
|
+
topSeverity?: Severity;
|
|
219
|
+
}
|
|
220
|
+
declare function buildSessionSummary(metaJson: unknown, indexJson: unknown, fileFlags: SessionFileFlags): SessionSummary;
|
|
221
|
+
|
|
222
|
+
interface SessionManagerConfig {
|
|
223
|
+
outputDir: string;
|
|
224
|
+
whisperModel?: string;
|
|
225
|
+
postProcess?: (sessionDir: string, whisperModel?: string) => Promise<void>;
|
|
226
|
+
}
|
|
227
|
+
interface SessionListItem {
|
|
228
|
+
id: string;
|
|
229
|
+
release?: string;
|
|
230
|
+
build?: string;
|
|
231
|
+
start: number;
|
|
232
|
+
end?: number;
|
|
233
|
+
}
|
|
234
|
+
interface SessionFinalizationResult {
|
|
235
|
+
ok: true;
|
|
236
|
+
sessionId: string;
|
|
237
|
+
processed: boolean;
|
|
238
|
+
degraded: boolean;
|
|
239
|
+
finalizedAt: number;
|
|
240
|
+
postProcess: {
|
|
241
|
+
ok: boolean;
|
|
242
|
+
error?: string;
|
|
243
|
+
audio?: PostProcessAudioSummary;
|
|
244
|
+
warnings?: Array<{
|
|
245
|
+
capability: "audio" | "video";
|
|
246
|
+
code: string;
|
|
247
|
+
message: string;
|
|
248
|
+
}>;
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
declare class SessionManager {
|
|
252
|
+
private outputDir;
|
|
253
|
+
private whisperModel?;
|
|
254
|
+
private postProcess;
|
|
255
|
+
constructor(config: string | SessionManagerConfig);
|
|
256
|
+
create(sessionId: string, metadata: Record<string, unknown>): void;
|
|
257
|
+
getSessionDir(sessionId: string): string;
|
|
258
|
+
getExistingSessionDir(sessionId: string): string | undefined;
|
|
259
|
+
private validateSessionId;
|
|
260
|
+
private resolveInsideOutput;
|
|
261
|
+
private resolveStagingSessionDir;
|
|
262
|
+
private assertSafeSessionDir;
|
|
263
|
+
private findExistingSessionDir;
|
|
264
|
+
private moveSessionToV2Partition;
|
|
265
|
+
finalize(sessionId: string, options?: {
|
|
266
|
+
refinalize?: boolean;
|
|
267
|
+
}): Promise<SessionFinalizationResult>;
|
|
268
|
+
private eachSessionDir;
|
|
269
|
+
list(filters?: {
|
|
270
|
+
app?: string;
|
|
271
|
+
after?: number;
|
|
272
|
+
before?: number;
|
|
273
|
+
release?: string;
|
|
274
|
+
build?: string;
|
|
275
|
+
}): SessionListItem[];
|
|
276
|
+
listSummaries(): SessionSummary[];
|
|
277
|
+
private isSessionDir;
|
|
278
|
+
private ensurePartitionParent;
|
|
279
|
+
private assertSafeDirectoryInsideOutput;
|
|
280
|
+
private writeSessionArtifact;
|
|
281
|
+
private assertNoSymlinkedGeneratedArtifacts;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
interface AiDiagnosisConfig {
|
|
285
|
+
enabled: boolean;
|
|
286
|
+
apiKey?: string;
|
|
287
|
+
model?: string;
|
|
288
|
+
allowAutoModel?: boolean;
|
|
289
|
+
maxWindows?: number;
|
|
290
|
+
maxPromptBytes?: number;
|
|
291
|
+
fetchImpl?: typeof fetch;
|
|
292
|
+
log?: (message: string) => void;
|
|
293
|
+
/** Diagnose finalized sessions already on disk when the server starts. */
|
|
294
|
+
backfillOnStart?: boolean;
|
|
295
|
+
/** Bounded provider concurrency for startup backfill. Default 2. */
|
|
296
|
+
backfillConcurrency?: number;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interface ServerConfig {
|
|
300
|
+
port: number;
|
|
301
|
+
outputDir: string;
|
|
302
|
+
whisperModel?: string;
|
|
303
|
+
/** Override the session finalization post-processing step (e.g. to skip whisper transcription in tests). */
|
|
304
|
+
postProcess?: SessionManagerConfig["postProcess"];
|
|
305
|
+
staticDir?: string;
|
|
306
|
+
authToken?: string;
|
|
307
|
+
allowedOrigins?: string[];
|
|
308
|
+
maxJsonBodyBytes?: number;
|
|
309
|
+
maxEventBatchSize?: number;
|
|
310
|
+
maxSessionEventBytes?: number;
|
|
311
|
+
maxBlobBytes?: number;
|
|
312
|
+
allowRemoteApi?: boolean;
|
|
313
|
+
ai?: AiDiagnosisConfig;
|
|
314
|
+
otlpAutoSessionWindowMs?: number;
|
|
315
|
+
maxOtlpTraceSessionCache?: number;
|
|
316
|
+
/**
|
|
317
|
+
* Idle-session sweep: server-side finalization for sessions that never call
|
|
318
|
+
* POST /api/session/end (autoCapture backends, OTLP auto-sessions). Enabled
|
|
319
|
+
* by default — without it those sessions never produce index.json and every
|
|
320
|
+
* dashboard detail read 404s. Set `enabled: false` to opt out (tests).
|
|
321
|
+
*/
|
|
322
|
+
sessionSweep?: {
|
|
323
|
+
enabled?: boolean;
|
|
324
|
+
idleMs?: number;
|
|
325
|
+
/** Max age before a never-idle (still-active) session is checkpoint-finalized. */
|
|
326
|
+
checkpointMs?: number;
|
|
327
|
+
intervalMs?: number;
|
|
328
|
+
maxPerSweep?: number;
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* Severity-triggered fast finalize: high-severity ingest events (uncaught
|
|
332
|
+
* errors/rejections, 5xx, backend errors, OTel ERROR spans/logs) schedule a
|
|
333
|
+
* debounced checkpoint-finalize for their session, cutting worst-case
|
|
334
|
+
* detection latency from the idle-sweep cadence (~40 min) to roughly the
|
|
335
|
+
* debounce window. Enabled by default; set `enabled: false` to opt out
|
|
336
|
+
* (tests). Scheduler state is in-memory only — a fast finalize lost to a
|
|
337
|
+
* restart falls back to the normal idle sweep.
|
|
338
|
+
* Defaults: debounceMs 45_000, maxConcurrent 2, cooldownMs 300_000.
|
|
339
|
+
*/
|
|
340
|
+
fastFinalize?: {
|
|
341
|
+
enabled?: boolean;
|
|
342
|
+
/** Delay from the first severe event to the finalize attempt. Default 45_000. */
|
|
343
|
+
debounceMs?: number;
|
|
344
|
+
/** Global cap on concurrently running fast finalizes. Default 2. */
|
|
345
|
+
maxConcurrent?: number;
|
|
346
|
+
/** Minimum spacing between finalize attempts per session. Default 300_000. */
|
|
347
|
+
cooldownMs?: number;
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Fired after every successful session finalization (including
|
|
351
|
+
* re-finalizations), from all three finalize paths: POST /api/session/end,
|
|
352
|
+
* the idle sweeper, and the severity-triggered fast finalizer. Embedders
|
|
353
|
+
* (e.g. crumbtrail-cloud) use it to learn about finalization in-process
|
|
354
|
+
* instead of polling. Must be fast and non-blocking — it runs on the
|
|
355
|
+
* finalize path. A throwing hook is swallowed and logged; it never fails
|
|
356
|
+
* the finalize or suppresses AI-diagnosis scheduling.
|
|
357
|
+
*/
|
|
358
|
+
onSessionFinalized?: (sessionId: string, info: {
|
|
359
|
+
refinalized: boolean;
|
|
360
|
+
}) => void;
|
|
361
|
+
/**
|
|
362
|
+
* Test-only seam: overrides how the client's evidence sources are constructed
|
|
363
|
+
* for the inner /api/solve-context adapter phase (sessionless Mode A +
|
|
364
|
+
* blended). Production leaves this unset; gatherAdapterEvidence then builds
|
|
365
|
+
* them from env via evidenceSourcesFromEnv().
|
|
366
|
+
*/
|
|
367
|
+
evidenceSourcesFactory?: () => EvidenceSource[];
|
|
368
|
+
}
|
|
369
|
+
declare function createServer(config: ServerConfig): http.Server;
|
|
370
|
+
|
|
371
|
+
type CausalConfidence = "high" | "medium" | "low";
|
|
372
|
+
|
|
373
|
+
interface LlmBundleFullStackEventRef {
|
|
374
|
+
t: number;
|
|
375
|
+
iso?: string;
|
|
376
|
+
offsetMs?: number;
|
|
377
|
+
kind?: string;
|
|
378
|
+
}
|
|
379
|
+
interface LlmBundleFrontendRequestEvidenceSummary {
|
|
380
|
+
ref?: LlmBundleFullStackEventRef;
|
|
381
|
+
requestId?: string;
|
|
382
|
+
sessionId?: string;
|
|
383
|
+
method?: string;
|
|
384
|
+
url?: string;
|
|
385
|
+
status?: number;
|
|
386
|
+
durationMs?: number;
|
|
387
|
+
error?: {
|
|
388
|
+
message?: string;
|
|
389
|
+
transport?: string;
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
interface LlmBundleBackendRequestEvidenceSummary {
|
|
393
|
+
requestId?: string;
|
|
394
|
+
sessionId?: string;
|
|
395
|
+
correlation?: {
|
|
396
|
+
status?: string;
|
|
397
|
+
sessionIdSource?: string;
|
|
398
|
+
requestIdSource?: string;
|
|
399
|
+
};
|
|
400
|
+
start?: LlmBundleFullStackEventRef;
|
|
401
|
+
end?: LlmBundleFullStackEventRef;
|
|
402
|
+
errorRef?: LlmBundleFullStackEventRef;
|
|
403
|
+
method?: string;
|
|
404
|
+
url?: string;
|
|
405
|
+
pathname?: string;
|
|
406
|
+
route?: string;
|
|
407
|
+
statusCode?: number;
|
|
408
|
+
durationMs?: number;
|
|
409
|
+
error?: {
|
|
410
|
+
name?: string;
|
|
411
|
+
code?: string;
|
|
412
|
+
message?: string;
|
|
413
|
+
statusCode?: number;
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Redaction-aware summary of one `k:'db.diff'` event (a row that changed during a request),
|
|
418
|
+
* correlated to the request via `requestId`. CP5 DB diffing. Sensitive columns were dropped in the
|
|
419
|
+
* shim; bundle build re-runs the redaction policy as defense-in-depth.
|
|
420
|
+
*/
|
|
421
|
+
interface LlmBundleDbDiff {
|
|
422
|
+
t: number;
|
|
423
|
+
iso?: string;
|
|
424
|
+
offsetMs?: number;
|
|
425
|
+
engine: DbEngine;
|
|
426
|
+
op: "insert" | "update" | "delete";
|
|
427
|
+
table: string;
|
|
428
|
+
pk: Record<string, unknown> | null;
|
|
429
|
+
after?: Record<string, unknown>;
|
|
430
|
+
before?: Record<string, unknown>;
|
|
431
|
+
/**
|
|
432
|
+
* Only set on an image-less statement-level fallback event (`pk: null`, no `after`/`before`)
|
|
433
|
+
* where per-row images were unobtainable — records how many rows the statement changed so the
|
|
434
|
+
* write stays visible to differencing.
|
|
435
|
+
*/
|
|
436
|
+
rowCount?: number;
|
|
437
|
+
requestId?: string;
|
|
438
|
+
}
|
|
439
|
+
interface LlmBundleDbRead {
|
|
440
|
+
t: number;
|
|
441
|
+
iso?: string;
|
|
442
|
+
offsetMs?: number;
|
|
443
|
+
engine: DbEngine;
|
|
444
|
+
table: string;
|
|
445
|
+
pk: Record<string, unknown> | null;
|
|
446
|
+
row: Record<string, unknown>;
|
|
447
|
+
requestId?: string;
|
|
448
|
+
}
|
|
449
|
+
interface LlmBundleDbActivity {
|
|
450
|
+
t: number;
|
|
451
|
+
iso?: string;
|
|
452
|
+
offsetMs?: number;
|
|
453
|
+
evidenceType: "otel_db_activity_statements_not_row_diffs";
|
|
454
|
+
system?: string;
|
|
455
|
+
operation?: string;
|
|
456
|
+
statement?: string;
|
|
457
|
+
spanName?: string;
|
|
458
|
+
serviceName?: string;
|
|
459
|
+
requestId?: string;
|
|
460
|
+
upgradeHint: string;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
declare const CANDIDATE_SCHEMA_VERSION: 1;
|
|
464
|
+
interface EvidenceCandidate {
|
|
465
|
+
schemaVersion: typeof CANDIDATE_SCHEMA_VERSION;
|
|
466
|
+
id: string;
|
|
467
|
+
detector: string;
|
|
468
|
+
title: string;
|
|
469
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
470
|
+
score: number;
|
|
471
|
+
confidence: "high" | "medium" | "low";
|
|
472
|
+
anchor: {
|
|
473
|
+
t: number;
|
|
474
|
+
offsetMs?: number;
|
|
475
|
+
route?: string;
|
|
476
|
+
elementLabel?: string;
|
|
477
|
+
requestId?: string;
|
|
478
|
+
method?: string;
|
|
479
|
+
url?: string;
|
|
480
|
+
status?: number;
|
|
481
|
+
errorCode?: string;
|
|
482
|
+
message?: string;
|
|
483
|
+
source?: string;
|
|
484
|
+
target?: TargetDescriptor;
|
|
485
|
+
};
|
|
486
|
+
/** Causal role assigned by the confidence-gated re-rank (CP3). Additive/optional. */
|
|
487
|
+
causalRole?: "root" | "symptom" | "isolated";
|
|
488
|
+
/** For a symptom, the candidate id of its attributed root cause. */
|
|
489
|
+
rootCauseId?: string;
|
|
490
|
+
/** For a root, the sorted candidate ids of the symptoms attributed to it. */
|
|
491
|
+
causes?: string[];
|
|
492
|
+
/** Weakest edge confidence along the causal path from root to this symptom. */
|
|
493
|
+
attributionConfidence?: CausalConfidence;
|
|
494
|
+
evidenceWindow: {
|
|
495
|
+
start: number;
|
|
496
|
+
end: number;
|
|
497
|
+
windowId: string;
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
declare const DISTINCT_BUGS_SCHEMA_VERSION: 1;
|
|
502
|
+
type DistinctBugSeverity = EvidenceCandidate["severity"];
|
|
503
|
+
/**
|
|
504
|
+
* A reference back to one ranked {@link EvidenceCandidate} that contributed to a distinct bug.
|
|
505
|
+
* Carries only already-redacted, candidate-derived fields so the grouped view re-exposes nothing.
|
|
506
|
+
*/
|
|
507
|
+
interface DistinctBugEvidenceRef {
|
|
508
|
+
candidateId: string;
|
|
509
|
+
detector: string;
|
|
510
|
+
t: number;
|
|
511
|
+
offsetMs?: number;
|
|
512
|
+
requestId?: string;
|
|
513
|
+
method?: string;
|
|
514
|
+
status?: number;
|
|
515
|
+
route?: string;
|
|
516
|
+
target?: TargetDescriptor;
|
|
517
|
+
message?: string;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* One DISTINCT, labeled bug a single session hit, grouped deterministically from the ranked
|
|
521
|
+
* evidence candidates. Front-end and back-end evidence are split so the bug carries its correlated
|
|
522
|
+
* window; correlated requests/traces share one bug via {@link DistinctBug.requestIds}.
|
|
523
|
+
*/
|
|
524
|
+
interface DistinctBug {
|
|
525
|
+
schemaVersion: typeof DISTINCT_BUGS_SCHEMA_VERSION;
|
|
526
|
+
bugId: string;
|
|
527
|
+
title: string;
|
|
528
|
+
severity: DistinctBugSeverity;
|
|
529
|
+
firstSeen: number;
|
|
530
|
+
lastSeen: number;
|
|
531
|
+
window: {
|
|
532
|
+
start: number;
|
|
533
|
+
end: number;
|
|
534
|
+
};
|
|
535
|
+
requestIds: string[];
|
|
536
|
+
representative: {
|
|
537
|
+
title: string;
|
|
538
|
+
detector: string;
|
|
539
|
+
severity: DistinctBugSeverity;
|
|
540
|
+
message?: string;
|
|
541
|
+
route?: string;
|
|
542
|
+
method?: string;
|
|
543
|
+
status?: number;
|
|
544
|
+
target?: TargetDescriptor;
|
|
545
|
+
requestId?: string;
|
|
546
|
+
};
|
|
547
|
+
frontendEvidence: DistinctBugEvidenceRef[];
|
|
548
|
+
backendEvidence: DistinctBugEvidenceRef[];
|
|
549
|
+
dbDiffs?: DistinctBugEvidenceRef[];
|
|
550
|
+
candidateIds: string[];
|
|
551
|
+
}
|
|
552
|
+
interface DistinctBugRecurrenceInput {
|
|
553
|
+
bug: DistinctBug;
|
|
554
|
+
session: {
|
|
555
|
+
sessionId: string;
|
|
556
|
+
dir?: string;
|
|
557
|
+
app?: string;
|
|
558
|
+
tenant?: string;
|
|
559
|
+
release?: string;
|
|
560
|
+
build?: string;
|
|
561
|
+
start?: number;
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
interface DistinctBugRecurrenceOccurrence {
|
|
565
|
+
sessionId: string;
|
|
566
|
+
bugId: string;
|
|
567
|
+
title: string;
|
|
568
|
+
severity: DistinctBugSeverity;
|
|
569
|
+
firstSeen: number;
|
|
570
|
+
lastSeen: number;
|
|
571
|
+
app?: string;
|
|
572
|
+
tenant?: string;
|
|
573
|
+
release?: string;
|
|
574
|
+
build?: string;
|
|
575
|
+
dir?: string;
|
|
576
|
+
}
|
|
577
|
+
interface DistinctBugRecurrence {
|
|
578
|
+
signature: string;
|
|
579
|
+
title: string;
|
|
580
|
+
severity: DistinctBugSeverity;
|
|
581
|
+
first_seen: number;
|
|
582
|
+
last_seen: number;
|
|
583
|
+
session_count: number;
|
|
584
|
+
release_span?: {
|
|
585
|
+
first: string;
|
|
586
|
+
last: string;
|
|
587
|
+
label: string;
|
|
588
|
+
};
|
|
589
|
+
apps: string[];
|
|
590
|
+
tenants: string[];
|
|
591
|
+
occurrences: DistinctBugRecurrenceOccurrence[];
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Groups the ranked candidates into DISTINCT bugs deterministically.
|
|
595
|
+
*
|
|
596
|
+
* Grouping key heuristics (deterministic, order-independent):
|
|
597
|
+
* - Candidates sharing a correlated `anchor.requestId` (Crumbtrail request id or W3C trace id)
|
|
598
|
+
* collapse into ONE bug, so the front-end signal and its back-end span/log land together.
|
|
599
|
+
* - Remaining candidates cluster by `(detector + normalized message/error signature + component
|
|
600
|
+
* signature)` within a {@link CLUSTER_WINDOW_MS} time window. Identical signatures far apart in
|
|
601
|
+
* time become separate bugs (disambiguated with a stable `#n` suffix so their ids never collide).
|
|
602
|
+
*
|
|
603
|
+
* The `bugId` is `bug_<hash>` where the hash is a stable FNV-1a digest of the dedup key — identical
|
|
604
|
+
* input always yields identical ids and ordering. Bugs are sorted by severity desc, then firstSeen
|
|
605
|
+
* asc, then bugId asc.
|
|
606
|
+
*/
|
|
607
|
+
declare function groupDistinctBugs(candidates: EvidenceCandidate[]): DistinctBug[];
|
|
608
|
+
declare function buildDistinctBugSignature(bug: Pick<DistinctBug, "title" | "representative">): string;
|
|
609
|
+
declare function groupDistinctBugRecurrences(inputs: DistinctBugRecurrenceInput[]): DistinctBugRecurrence[];
|
|
610
|
+
|
|
611
|
+
interface AppendEventsOptions {
|
|
612
|
+
maxEventBytes?: number;
|
|
613
|
+
}
|
|
614
|
+
interface AppendEventsResult {
|
|
615
|
+
accepted: number;
|
|
616
|
+
dropped: number;
|
|
617
|
+
truncated: boolean;
|
|
618
|
+
bytesWritten: number;
|
|
619
|
+
}
|
|
620
|
+
interface ArtifactStat {
|
|
621
|
+
bytes: number;
|
|
622
|
+
isDir: boolean;
|
|
623
|
+
}
|
|
624
|
+
interface ResolveSessionScope {
|
|
625
|
+
tenant?: string;
|
|
626
|
+
app?: string;
|
|
627
|
+
date?: string;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Storage boundary for a single session's on-disk artifacts. Every filesystem
|
|
631
|
+
* write/read primitive that the node package performs against a session
|
|
632
|
+
* directory flows through this seam so the layout can later move behind an
|
|
633
|
+
* alternate backend (e.g. R2) without touching call sites.
|
|
634
|
+
*/
|
|
635
|
+
interface SessionStore {
|
|
636
|
+
createSessionDir(sessionId: string): string;
|
|
637
|
+
appendEvents(sessionDir: string, events: BugEvent[], opts?: AppendEventsOptions): AppendEventsResult;
|
|
638
|
+
writeArtifact(sessionDir: string, name: string, data: string | Buffer): void;
|
|
639
|
+
writeBlob(sessionDir: string, name: string, data: Buffer): void;
|
|
640
|
+
writeSessionArtifact(sessionDir: string, name: string, data: string | Buffer): void;
|
|
641
|
+
readArtifact(sessionDir: string, name: string): Buffer | undefined;
|
|
642
|
+
statArtifact(sessionDir: string, name: string): ArtifactStat | undefined;
|
|
643
|
+
listSessions(outputDir: string): Array<{
|
|
644
|
+
id: string;
|
|
645
|
+
dir: string;
|
|
646
|
+
}>;
|
|
647
|
+
listArtifacts(sessionDir: string): string[];
|
|
648
|
+
/**
|
|
649
|
+
* Atomically relocate `sessionDir` to `targetDir` (staging -> finalized partition).
|
|
650
|
+
* This is the load-bearing FS-only "atomic rename" semantic the R2 adapter will have
|
|
651
|
+
* to emulate. Path/containment/symlink policy is enforced by the caller before and
|
|
652
|
+
* after the move (see SessionManager.moveSessionToV2Partition); this primitive owns
|
|
653
|
+
* only the atomic move itself.
|
|
654
|
+
*/
|
|
655
|
+
moveToPartition(sessionDir: string, targetDir: string): string;
|
|
656
|
+
resolveSessionDir(idOrDir: string, outputDir?: string, scope?: ResolveSessionScope): string;
|
|
657
|
+
/**
|
|
658
|
+
* Tenant-scoped lookup with the cloud isolation contract: returns the session dir only
|
|
659
|
+
* when it exists inside {sessionsDir}/{tenant}/{app}/{YYYY-MM-DD}/{sessionId}, else
|
|
660
|
+
* undefined (never a fallback path, never a cross-tenant hit). Backs cloud's findSessionDir.
|
|
661
|
+
*/
|
|
662
|
+
resolveScopedSessionDir(sessionsDir: string, tenant: string, app: string, sessionId: string): string | undefined;
|
|
663
|
+
deleteSessionDir(sessionDir: string): void;
|
|
664
|
+
}
|
|
665
|
+
declare class FilesystemSessionStore implements SessionStore {
|
|
666
|
+
createSessionDir(sessionId: string): string;
|
|
667
|
+
appendEvents(sessionDir: string, events: BugEvent[], opts?: AppendEventsOptions): AppendEventsResult;
|
|
668
|
+
writeArtifact(sessionDir: string, name: string, data: string | Buffer): void;
|
|
669
|
+
writeBlob(sessionDir: string, name: string, data: Buffer): void;
|
|
670
|
+
writeSessionArtifact(sessionDir: string, name: string, data: string | Buffer): void;
|
|
671
|
+
readArtifact(sessionDir: string, name: string): Buffer | undefined;
|
|
672
|
+
statArtifact(sessionDir: string, name: string): ArtifactStat | undefined;
|
|
673
|
+
listSessions(outputDir: string): Array<{
|
|
674
|
+
id: string;
|
|
675
|
+
dir: string;
|
|
676
|
+
}>;
|
|
677
|
+
listArtifacts(sessionDir: string): string[];
|
|
678
|
+
moveToPartition(sessionDir: string, targetDir: string): string;
|
|
679
|
+
resolveSessionDir(idOrDir: string, outputDir?: string, scope?: ResolveSessionScope): string;
|
|
680
|
+
resolveScopedSessionDir(sessionsDir: string, tenant: string, app: string, sessionId: string): string | undefined;
|
|
681
|
+
deleteSessionDir(sessionDir: string): void;
|
|
682
|
+
}
|
|
683
|
+
declare const defaultSessionStore: SessionStore;
|
|
684
|
+
|
|
685
|
+
declare const DEFAULT_SWEEP_IDLE_MS: number;
|
|
686
|
+
declare const DEFAULT_SWEEP_INTERVAL_MS: number;
|
|
687
|
+
declare const DEFAULT_SWEEP_CHECKPOINT_MS: number;
|
|
688
|
+
interface SessionSweepOptions {
|
|
689
|
+
sessions: SessionManager;
|
|
690
|
+
outputDir: string;
|
|
691
|
+
/** Inactivity threshold before an un-finalized session is swept. */
|
|
692
|
+
idleMs?: number;
|
|
693
|
+
/**
|
|
694
|
+
* Checkpoint threshold for sessions that never go idle: an un-finalized
|
|
695
|
+
* session older than this is finalized even while active, and an active
|
|
696
|
+
* finalized session with late events is re-finalized once per window.
|
|
697
|
+
*/
|
|
698
|
+
checkpointMs?: number;
|
|
699
|
+
/** Upper bound on finalizations (incl. re-finalizations) per sweep. */
|
|
700
|
+
maxPerSweep?: number;
|
|
701
|
+
/** Called after each successful finalize (e.g. to schedule AI diagnosis). */
|
|
702
|
+
onFinalized?: (sessionId: string, refinalized: boolean) => void;
|
|
703
|
+
/** Clock seam for tests. */
|
|
704
|
+
now?: () => number;
|
|
705
|
+
}
|
|
706
|
+
interface SessionSweepResult {
|
|
707
|
+
scanned: number;
|
|
708
|
+
finalized: number;
|
|
709
|
+
refinalized: number;
|
|
710
|
+
failed: number;
|
|
711
|
+
/** Candidates left alone because they saw activity within idleMs. */
|
|
712
|
+
active: number;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* One sweep pass: finalize every idle un-finalized session, and re-finalize
|
|
716
|
+
* every finalized session that received events after its last finalization.
|
|
717
|
+
* Serial by design. Returns counts for logging; never throws.
|
|
718
|
+
*/
|
|
719
|
+
declare function sweepIdleSessions(options: SessionSweepOptions): Promise<SessionSweepResult>;
|
|
720
|
+
interface SessionSweeperHandle {
|
|
721
|
+
stop(): void;
|
|
722
|
+
/** Run one sweep immediately (test seam; also used by the interval). */
|
|
723
|
+
sweepNow(): Promise<SessionSweepResult>;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Start the periodic sweeper. The timer is unref'd so it never keeps a
|
|
727
|
+
* process alive, and ticks never overlap (a long postProcess simply delays
|
|
728
|
+
* the next pass).
|
|
729
|
+
*/
|
|
730
|
+
declare function startSessionSweeper(options: SessionSweepOptions & {
|
|
731
|
+
intervalMs?: number;
|
|
732
|
+
/** Called with each sweep's counts whenever a sweep did any work. */
|
|
733
|
+
onSweep?: (result: SessionSweepResult) => void;
|
|
734
|
+
}): SessionSweeperHandle;
|
|
735
|
+
|
|
736
|
+
interface BugReport {
|
|
737
|
+
bugId: string;
|
|
738
|
+
sessionId: string;
|
|
739
|
+
flaggedAt: number;
|
|
740
|
+
windowMs: number;
|
|
741
|
+
note?: string;
|
|
742
|
+
voiceNote?: string;
|
|
743
|
+
url: string;
|
|
744
|
+
userAgent: string;
|
|
745
|
+
tags?: string[];
|
|
746
|
+
status?: "open" | "resolved";
|
|
747
|
+
summary: {
|
|
748
|
+
errorCount: number;
|
|
749
|
+
failedRequestCount: number;
|
|
750
|
+
eventCount: number;
|
|
751
|
+
eventKinds: Record<string, number>;
|
|
752
|
+
durationMs: number;
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
interface BugQueueConfig {
|
|
756
|
+
bugsDir: string;
|
|
757
|
+
whisperModel?: string;
|
|
758
|
+
}
|
|
759
|
+
interface LlmBugContext {
|
|
760
|
+
v: 1;
|
|
761
|
+
id: string;
|
|
762
|
+
sid: string;
|
|
763
|
+
ts: number;
|
|
764
|
+
w: number;
|
|
765
|
+
u: string;
|
|
766
|
+
n?: string;
|
|
767
|
+
g?: string[];
|
|
768
|
+
s: {
|
|
769
|
+
e: number;
|
|
770
|
+
f: number;
|
|
771
|
+
c: number;
|
|
772
|
+
d: number;
|
|
773
|
+
k: Record<string, number>;
|
|
774
|
+
};
|
|
775
|
+
err?: Array<{
|
|
776
|
+
t: number;
|
|
777
|
+
m: string;
|
|
778
|
+
}>;
|
|
779
|
+
req?: Array<{
|
|
780
|
+
t: number;
|
|
781
|
+
m: string;
|
|
782
|
+
u: string;
|
|
783
|
+
s: number;
|
|
784
|
+
}>;
|
|
785
|
+
nav?: Array<{
|
|
786
|
+
t: number;
|
|
787
|
+
to: string;
|
|
788
|
+
}>;
|
|
789
|
+
}
|
|
790
|
+
declare class BugQueueManager {
|
|
791
|
+
private bugsDir;
|
|
792
|
+
private whisperModel?;
|
|
793
|
+
constructor(config: BugQueueConfig);
|
|
794
|
+
create(report: BugReport, events: BugEvent[]): Promise<void>;
|
|
795
|
+
writeVoice(bugId: string, data: Buffer): Promise<boolean>;
|
|
796
|
+
list(filters?: {
|
|
797
|
+
after?: number;
|
|
798
|
+
before?: number;
|
|
799
|
+
status?: string;
|
|
800
|
+
tags?: string[];
|
|
801
|
+
}): BugReport[];
|
|
802
|
+
get(bugId: string): BugReport | null;
|
|
803
|
+
getBugDir(bugId: string): string;
|
|
804
|
+
resolve(bugId: string): void;
|
|
805
|
+
getLlmContext(bugId: string): LlmBugContext | null;
|
|
806
|
+
private resolveBugDir;
|
|
807
|
+
private resolveBugStagingDir;
|
|
808
|
+
private assertBugDirAvailable;
|
|
809
|
+
private assertSafeBugDir;
|
|
810
|
+
private writeLlmContext;
|
|
811
|
+
private writeLlmContextForDir;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Outcome of an on-demand reproduction attempt against a symptom. Reserved
|
|
816
|
+
* for the injected `Reproducer` seam consumed by `solveContext` when
|
|
817
|
+
* historical evidence is thin — see `docs/integrations/reproduction.md`.
|
|
818
|
+
*/
|
|
819
|
+
interface ReproductionResult {
|
|
820
|
+
attempted: boolean;
|
|
821
|
+
/** Where a fresh session was recorded, if any. */
|
|
822
|
+
sessionDir?: string;
|
|
823
|
+
/** Fresh evidence gathered (empty if none). */
|
|
824
|
+
evidence: EvidenceItem[];
|
|
825
|
+
/** Usually empty; reserved. */
|
|
826
|
+
intent: IntentSignal[];
|
|
827
|
+
/** Human/agent-readable outcome. */
|
|
828
|
+
note: string;
|
|
829
|
+
}
|
|
830
|
+
interface Reproducer {
|
|
831
|
+
reproduce(symptom: Symptom): Promise<ReproductionResult>;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Parallel adapter fan-out. Queries every configured {@link EvidenceSource} at
|
|
836
|
+
* once, bounds each by a per-source timeout, redacts every result at the
|
|
837
|
+
* boundary, and enforces a global byte cap across all sources. It NEVER throws:
|
|
838
|
+
* a failed or slow source becomes an {@link EvidenceGap} ("sentry: timeout after
|
|
839
|
+
* 10s"), because evidence is advisory and "inconclusive" is a valid outcome.
|
|
840
|
+
*/
|
|
841
|
+
/** Per-source timeout when neither the query nor options specify one. */
|
|
842
|
+
declare const DEFAULT_SOURCE_TIMEOUT_MS = 10000;
|
|
843
|
+
/** Global cap on normalized (redacted) evidence bytes across all sources. */
|
|
844
|
+
declare const DEFAULT_MAX_TOTAL_BYTES: number;
|
|
845
|
+
interface AdapterSourceStats {
|
|
846
|
+
provider: string;
|
|
847
|
+
/**
|
|
848
|
+
* Health signal for this source. INVARIANT: `ok` is false iff the source
|
|
849
|
+
* could not deliver its PRIMARY evidence — a total failure/timeout that
|
|
850
|
+
* returned zero items — regardless of whether the adapter threw or
|
|
851
|
+
* self-degraded internally. A source that returned any items (even partial,
|
|
852
|
+
* with an enrichment/secondary gap) stays `ok: true`. The framework is the
|
|
853
|
+
* single source of truth: it derives `ok` from the throw path OR a
|
|
854
|
+
* `kind: "source-unavailable"` gap on a zero-item result (see below), never
|
|
855
|
+
* from gap `reason` text.
|
|
856
|
+
*/
|
|
857
|
+
ok: boolean;
|
|
858
|
+
/** Items the source reported fetching (0 on failure). */
|
|
859
|
+
fetched: number;
|
|
860
|
+
/** Items actually included after the global byte cap. */
|
|
861
|
+
returned: number;
|
|
862
|
+
/** True if the source's own maxItems OR the global byte cap dropped items. */
|
|
863
|
+
truncated: boolean;
|
|
864
|
+
/** Framework-measured wall time for this source's fetch. */
|
|
865
|
+
latencyMs: number;
|
|
866
|
+
/** Bytes of this source's included normalized items. */
|
|
867
|
+
bytes: number;
|
|
868
|
+
/** Sanitized failure/timeout reason when `ok` is false. */
|
|
869
|
+
error?: string;
|
|
870
|
+
}
|
|
871
|
+
interface AdapterEvidence {
|
|
872
|
+
items: EvidenceItem[];
|
|
873
|
+
gaps: EvidenceGap[];
|
|
874
|
+
stats: AdapterSourceStats[];
|
|
875
|
+
}
|
|
876
|
+
interface FetchAdapterEvidenceOptions {
|
|
877
|
+
/** Per-source timeout override (else query.limits.timeoutMs, else default). */
|
|
878
|
+
timeoutMs?: number;
|
|
879
|
+
/** Global byte cap across sources. Default 512 KB. */
|
|
880
|
+
maxTotalBytes?: number;
|
|
881
|
+
/** Clock hook (tests). Default Date.now. */
|
|
882
|
+
now?: () => number;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Fan out `query` to every source in parallel, degrade failures to gaps, redact
|
|
886
|
+
* at the boundary, and cap total normalized bytes. Deterministic: outcomes are
|
|
887
|
+
* folded in source order, so the byte cap trims from a stable point.
|
|
888
|
+
*/
|
|
889
|
+
declare function fetchAdapterEvidence(sources: EvidenceSource[], query: EvidenceQuery, options?: FetchAdapterEvidenceOptions): Promise<AdapterEvidence>;
|
|
890
|
+
|
|
891
|
+
declare function redactEvidenceItem(item: EvidenceItem, index?: number): EvidenceItem;
|
|
892
|
+
declare function redactEvidenceGap(gap: EvidenceGap, index?: number): EvidenceGap;
|
|
893
|
+
/**
|
|
894
|
+
* Redact a whole adapter result. Returns a new result (items + gaps redacted);
|
|
895
|
+
* `stats` and `schemaVersion` pass through untouched.
|
|
896
|
+
*/
|
|
897
|
+
declare function redactSourceResult(result: EvidenceSourceResult): EvidenceSourceResult;
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Sentry evidence adapter — the reference implementation every other adapter
|
|
901
|
+
* (CloudWatch, Splunk, Datadog, PostHog, Cloudflare) copies.
|
|
902
|
+
*
|
|
903
|
+
* It is a query-at-incident-time pull: given a located incident window and the
|
|
904
|
+
* correlation keys known for it, ask Sentry's REST API for the issues/events in
|
|
905
|
+
* that window and normalize each into the neutral `evidence.v1` contract. It is
|
|
906
|
+
* zero-copy — nothing Sentry returns is persisted; only the derived bundle is.
|
|
907
|
+
*
|
|
908
|
+
* Load-bearing patterns for CP4–CP7 to mirror:
|
|
909
|
+
* - **Primary-first, enrichment-best-effort**: neutral items are built from the
|
|
910
|
+
* list/query call (the PRIMARY evidence — `brief`, `ref`, `whenObserved`) and
|
|
911
|
+
* emitted regardless of any secondary enrichment. Enrichment (here: per-issue
|
|
912
|
+
* stack heads) fans out with capped concurrency inside a sub-budget carved
|
|
913
|
+
* from `limits.timeoutMs`; if it is slow, fails, or the abort signal fires,
|
|
914
|
+
* the primary items still ship with a less-enriched field (`after: null`) —
|
|
915
|
+
* never nothing. This keeps a timed-out source degrading to "fewer/less-rich
|
|
916
|
+
* items + a gap," never "zero items."
|
|
917
|
+
* - **Injectable transport**: the constructor takes `fetchImpl?: typeof fetch`.
|
|
918
|
+
* Production uses global `fetch`; contract tests inject a stub that replays
|
|
919
|
+
* recorded fixtures, so there is zero network in CI.
|
|
920
|
+
* - **Descriptor-keyed query construction**: the search query is built strictly
|
|
921
|
+
* from `descriptor.joinKeys` ∩ the keys present in `EvidenceQuery.keys`, and a
|
|
922
|
+
* requested key the adapter cannot use becomes an honest {@link EvidenceGap}
|
|
923
|
+
* rather than a silent drop.
|
|
924
|
+
* - **Boundaries**: never throws out of `fetchEvidence` in a way the framework
|
|
925
|
+
* can't turn into a gap; honors `limits.maxItems`/`maxBytes`; no pagination
|
|
926
|
+
* walk beyond `maxItems`; every request carries `CRUMBTRAIL_USER_AGENT`; the
|
|
927
|
+
* auth token lives only in the Authorization header, never in a message.
|
|
928
|
+
* - **Redaction stays at the framework boundary** (`redact.ts` /
|
|
929
|
+
* `fetchAdapterEvidence`); this file only populates the fields that boundary
|
|
930
|
+
* scrubs (`brief`, `after`, `ref.url`).
|
|
931
|
+
*/
|
|
932
|
+
/** Env var carrying the Sentry auth token (Bearer). Required. */
|
|
933
|
+
declare const SENTRY_AUTH_TOKEN_ENV = "CRUMBTRAIL_SENTRY_AUTH_TOKEN";
|
|
934
|
+
/** Env var carrying the Sentry organization slug. Required. */
|
|
935
|
+
declare const SENTRY_ORG_ENV = "CRUMBTRAIL_SENTRY_ORG";
|
|
936
|
+
/** Env var overriding the API host (self-hosted Sentry). Optional. */
|
|
937
|
+
declare const SENTRY_HOST_ENV = "CRUMBTRAIL_SENTRY_HOST";
|
|
938
|
+
/** Default SaaS host when {@link SENTRY_HOST_ENV} is unset. */
|
|
939
|
+
declare const SENTRY_DEFAULT_HOST = "https://sentry.io";
|
|
940
|
+
/** Presence of these two ⇒ the provider is configured (mirrors ticket clients). */
|
|
941
|
+
declare const SENTRY_AUTH_FIELDS: string[];
|
|
942
|
+
declare const SENTRY_DESCRIPTOR: EvidenceSourceDescriptor;
|
|
943
|
+
interface SentrySourceConfig {
|
|
944
|
+
authToken: string;
|
|
945
|
+
org: string;
|
|
946
|
+
/** API host, no trailing slash. Defaults to {@link SENTRY_DEFAULT_HOST}. */
|
|
947
|
+
host?: string;
|
|
948
|
+
/** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
|
|
949
|
+
fetchImpl?: typeof fetch;
|
|
950
|
+
}
|
|
951
|
+
interface SentryQueryPlan {
|
|
952
|
+
/** Sentry search query string (may be empty ⇒ time-window only). */
|
|
953
|
+
search: string;
|
|
954
|
+
/** Join keys actually used to narrow beyond the time window, best-first. */
|
|
955
|
+
usedKeys: EvidenceJoinKey[];
|
|
956
|
+
/** Honest gaps: a requested key Sentry cannot filter by. */
|
|
957
|
+
gaps: EvidenceGap[];
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Build the Sentry search query from `descriptor.joinKeys` ∩ present keys.
|
|
961
|
+
* Precedence: a `trace:{traceId}` token when a trace is present (tightest), else
|
|
962
|
+
* `url:` / `release:` / `user.email:` tokens for whichever of those keys is
|
|
963
|
+
* present. Any requested key Sentry does NOT support (requestId, sessionId,
|
|
964
|
+
* service, and — when no trace is present — nothing to pin on) yields a gap so
|
|
965
|
+
* the bundle states plainly what filtered the result.
|
|
966
|
+
*/
|
|
967
|
+
declare function buildSentryQuery(query: EvidenceQuery): SentryQueryPlan;
|
|
968
|
+
/** Minimal shape of the Sentry issues-list rows we consume. */
|
|
969
|
+
interface SentryIssue {
|
|
970
|
+
id: string;
|
|
971
|
+
title?: string;
|
|
972
|
+
culprit?: string;
|
|
973
|
+
permalink?: string;
|
|
974
|
+
lastSeen?: string;
|
|
975
|
+
firstSeen?: string;
|
|
976
|
+
metadata?: {
|
|
977
|
+
value?: string;
|
|
978
|
+
type?: string;
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
/** Minimal shape of a Sentry event (from `/issues/{id}/events/latest/`). */
|
|
982
|
+
interface SentryEvent {
|
|
983
|
+
id?: string;
|
|
984
|
+
dateCreated?: string;
|
|
985
|
+
entries?: Array<{
|
|
986
|
+
type?: string;
|
|
987
|
+
data?: unknown;
|
|
988
|
+
}>;
|
|
989
|
+
}
|
|
990
|
+
/** Sentry issue + its latest event → neutral evidence.v1 item. Pure. */
|
|
991
|
+
declare function normalizeSentryIssue(issue: SentryIssue, event?: SentryEvent): EvidenceItem;
|
|
992
|
+
declare class SentryEvidenceSource implements EvidenceSource {
|
|
993
|
+
readonly descriptor: EvidenceSourceDescriptor;
|
|
994
|
+
private readonly authToken;
|
|
995
|
+
private readonly org;
|
|
996
|
+
private readonly host;
|
|
997
|
+
private readonly fetchImpl;
|
|
998
|
+
constructor(config: SentrySourceConfig);
|
|
999
|
+
private headers;
|
|
1000
|
+
private getJson;
|
|
1001
|
+
/** Cheap authenticated no-op: GET the org endpoint. */
|
|
1002
|
+
health(signal?: AbortSignal): Promise<SourceHealth>;
|
|
1003
|
+
/** GET the latest event for one issue (for its stack head). A per-issue
|
|
1004
|
+
* enrichment failure is non-fatal: we return undefined and the item is still
|
|
1005
|
+
* emitted with `after: null` rather than sinking the whole fetch. Single
|
|
1006
|
+
* attempt on purpose — this runs once per issue, so retrying it would let the
|
|
1007
|
+
* best-effort enrichment blow the per-source timeout budget; the primary
|
|
1008
|
+
* issues query keeps the bounded retry. */
|
|
1009
|
+
private fetchLatestEvent;
|
|
1010
|
+
/**
|
|
1011
|
+
* OPTIONAL stack-head enrichment — the resilient pattern CP4–CP7 mirror.
|
|
1012
|
+
*
|
|
1013
|
+
* Fans out `/events/latest/` calls with capped concurrency, bounded by BOTH
|
|
1014
|
+
* the caller's abort signal AND a sub-budget carved from the per-source
|
|
1015
|
+
* timeout (`ENRICH_BUDGET_FRACTION` of `limits.timeoutMs`). A slow, failed, or
|
|
1016
|
+
* aborted event fetch simply leaves that issue's event `undefined` (→
|
|
1017
|
+
* `after: null`); it never blocks or drops the primary issues-list evidence.
|
|
1018
|
+
* Returns the per-issue events (index-aligned to `issues`) plus `incomplete`,
|
|
1019
|
+
* set when the signal fired or the sub-budget was exhausted before every
|
|
1020
|
+
* enrichment finished.
|
|
1021
|
+
*/
|
|
1022
|
+
private enrichStackHeads;
|
|
1023
|
+
fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
|
|
1024
|
+
}
|
|
1025
|
+
/** Registry entry: build a Sentry source from env when its auth fields are set. */
|
|
1026
|
+
declare const sentryEvidenceProvider: EvidenceSourceProvider;
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* CloudWatch Logs Insights evidence adapter — the second adapter, built to the
|
|
1030
|
+
* Sentry reference shape (`sentry.ts`). Query-at-incident-time pull: given a
|
|
1031
|
+
* located incident window and the correlation keys known for it, run a Logs
|
|
1032
|
+
* Insights query over the configured log group(s) and normalize each matching
|
|
1033
|
+
* log line into the neutral `evidence.v1` contract. Zero-copy — nothing AWS
|
|
1034
|
+
* returns is persisted; only the derived bundle is.
|
|
1035
|
+
*
|
|
1036
|
+
* Load-bearing patterns mirrored from the Sentry reference:
|
|
1037
|
+
* - **Injectable transport**: constructor takes `fetchImpl?: typeof fetch`
|
|
1038
|
+
* defaulting to global `fetch`; contract tests inject a fixture-routing stub so
|
|
1039
|
+
* there is zero network in CI.
|
|
1040
|
+
* - **Resilience / self-limiting poll budget**: Logs Insights is async —
|
|
1041
|
+
* `StartQuery` then poll `GetQueryResults`. The polling loop is the analog of
|
|
1042
|
+
* Sentry's best-effort enrichment: it self-limits to a sub-budget carved from
|
|
1043
|
+
* `limits.timeoutMs` (`POLL_BUDGET_FRACTION`) so the adapter resolves BEFORE
|
|
1044
|
+
* the framework's per-source race fires, and it keeps the newest partial result
|
|
1045
|
+
* snapshot. A query that out-runs the budget degrades to the rows that
|
|
1046
|
+
* completed + an honest gap ("cloudwatch: query did not complete within Ns"),
|
|
1047
|
+
* never "no items."
|
|
1048
|
+
* - **Descriptor-keyed query construction**: the `filter` term is built strictly
|
|
1049
|
+
* from `descriptor.joinKeys` ∩ present keys; a requested key CloudWatch cannot
|
|
1050
|
+
* use becomes an honest {@link EvidenceGap} rather than a silent drop.
|
|
1051
|
+
* - **Boundaries**: honors `limits.maxItems`/`maxBytes`/`timeoutMs`; no
|
|
1052
|
+
* pagination walk beyond `maxItems`; every request is SigV4-signed and carries
|
|
1053
|
+
* `CRUMBTRAIL_USER_AGENT`; credentials live only in the signing path, never in a
|
|
1054
|
+
* gap, stat, thrown message, or log.
|
|
1055
|
+
* - **Redaction stays at the framework boundary** (`redact.ts`); this file only
|
|
1056
|
+
* populates the fields that boundary scrubs (`brief`, `after`, `ref.url`).
|
|
1057
|
+
*/
|
|
1058
|
+
/** Env var carrying the AWS access key id. Required. */
|
|
1059
|
+
declare const CLOUDWATCH_ACCESS_KEY_ID_ENV = "CRUMBTRAIL_CLOUDWATCH_ACCESS_KEY_ID";
|
|
1060
|
+
/** Env var carrying the AWS secret access key. Required. */
|
|
1061
|
+
declare const CLOUDWATCH_SECRET_ACCESS_KEY_ENV = "CRUMBTRAIL_CLOUDWATCH_SECRET_ACCESS_KEY";
|
|
1062
|
+
/** Env var carrying the AWS region, e.g. "us-east-1". Required. */
|
|
1063
|
+
declare const CLOUDWATCH_REGION_ENV = "CRUMBTRAIL_CLOUDWATCH_REGION";
|
|
1064
|
+
/** Env var carrying a comma-separated list of log group names. Required. */
|
|
1065
|
+
declare const CLOUDWATCH_LOG_GROUPS_ENV = "CRUMBTRAIL_CLOUDWATCH_LOG_GROUPS";
|
|
1066
|
+
/** Env var carrying an STS session token for temporary/role creds. Optional. */
|
|
1067
|
+
declare const CLOUDWATCH_SESSION_TOKEN_ENV = "CRUMBTRAIL_CLOUDWATCH_SESSION_TOKEN";
|
|
1068
|
+
/** Env var overriding the Logs endpoint host (govcloud/localstack). Optional. */
|
|
1069
|
+
declare const CLOUDWATCH_ENDPOINT_ENV = "CRUMBTRAIL_CLOUDWATCH_ENDPOINT";
|
|
1070
|
+
/** Presence of these four ⇒ the provider is configured (mirrors ticket clients). */
|
|
1071
|
+
declare const CLOUDWATCH_AUTH_FIELDS: string[];
|
|
1072
|
+
declare const CLOUDWATCH_DESCRIPTOR: EvidenceSourceDescriptor;
|
|
1073
|
+
interface CloudWatchSourceConfig {
|
|
1074
|
+
accessKeyId: string;
|
|
1075
|
+
secretAccessKey: string;
|
|
1076
|
+
region: string;
|
|
1077
|
+
/** Log group names to query, one Logs Insights query each. */
|
|
1078
|
+
logGroups: string[];
|
|
1079
|
+
/** STS session token for temporary/role credentials. Optional. */
|
|
1080
|
+
sessionToken?: string;
|
|
1081
|
+
/** Logs endpoint host override (no trailing slash). Defaults to the regional
|
|
1082
|
+
* `https://logs.{region}.amazonaws.com`. */
|
|
1083
|
+
endpoint?: string;
|
|
1084
|
+
/** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
|
|
1085
|
+
fetchImpl?: typeof fetch;
|
|
1086
|
+
/** Poll interval override (ms) for the GetQueryResults loop. Tests shrink it. */
|
|
1087
|
+
pollIntervalMs?: number;
|
|
1088
|
+
}
|
|
1089
|
+
interface CloudWatchQueryPlan {
|
|
1090
|
+
/** The Logs Insights query string (fields/filter/sort/limit). */
|
|
1091
|
+
queryString: string;
|
|
1092
|
+
/** Join keys actually used to narrow beyond the time window, best-first. */
|
|
1093
|
+
usedKeys: EvidenceJoinKey[];
|
|
1094
|
+
/** Honest gaps: a requested key CloudWatch cannot filter by. */
|
|
1095
|
+
gaps: EvidenceGap[];
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Build the Logs Insights query from `descriptor.joinKeys` ∩ present keys.
|
|
1099
|
+
* Precedence: a `filter @message like "<id>"` term when a requestId/traceId is
|
|
1100
|
+
* present (the tightest correlation Logs Insights offers on unstructured logs),
|
|
1101
|
+
* else a time-window-only scan (the window is expressed via StartQuery's
|
|
1102
|
+
* start/end, not the query string). Any requested key CloudWatch does NOT support
|
|
1103
|
+
* (sessionId, release, url, user) yields a gap so the bundle states plainly what
|
|
1104
|
+
* filtered the result.
|
|
1105
|
+
*/
|
|
1106
|
+
declare function buildCloudWatchQuery(query: EvidenceQuery): CloudWatchQueryPlan;
|
|
1107
|
+
/** One Logs Insights result row: a list of `{ field, value }` cells. */
|
|
1108
|
+
type CloudWatchResultRow = Array<{
|
|
1109
|
+
field?: string;
|
|
1110
|
+
value?: string;
|
|
1111
|
+
}>;
|
|
1112
|
+
/**
|
|
1113
|
+
* CloudWatch Logs Insights console deep link for a log group in a region, so a
|
|
1114
|
+
* human can open the source. The console double-encodes path separators
|
|
1115
|
+
* (`/` → `$252F`). This links to the log group; a token-bearing URL would be
|
|
1116
|
+
* scrubbed at the redaction boundary anyway (there is none here).
|
|
1117
|
+
*/
|
|
1118
|
+
declare function cloudWatchDeepLink(region: string, logGroup: string): string;
|
|
1119
|
+
/** One Logs Insights row → neutral evidence.v1 item. Pure. */
|
|
1120
|
+
declare function normalizeCloudWatchRow(row: CloudWatchResultRow, region: string, logGroup: string): EvidenceItem;
|
|
1121
|
+
declare class CloudWatchEvidenceSource implements EvidenceSource {
|
|
1122
|
+
readonly descriptor: EvidenceSourceDescriptor;
|
|
1123
|
+
private readonly accessKeyId;
|
|
1124
|
+
private readonly secretAccessKey;
|
|
1125
|
+
private readonly region;
|
|
1126
|
+
private readonly logGroups;
|
|
1127
|
+
private readonly sessionToken?;
|
|
1128
|
+
private readonly endpoint;
|
|
1129
|
+
private readonly fetchImpl;
|
|
1130
|
+
private readonly pollIntervalMs;
|
|
1131
|
+
constructor(config: CloudWatchSourceConfig);
|
|
1132
|
+
/**
|
|
1133
|
+
* One signed POST to the Logs JSON API. `action` is the short target name
|
|
1134
|
+
* (e.g. "StartQuery"). The SigV4 signature and the (non-secret) User-Agent are
|
|
1135
|
+
* the only auth material on the wire; the secret key never leaves the signer.
|
|
1136
|
+
*/
|
|
1137
|
+
private post;
|
|
1138
|
+
private startQuery;
|
|
1139
|
+
private getQueryResults;
|
|
1140
|
+
/** Cheap authenticated no-op: DescribeLogGroups with limit 1. */
|
|
1141
|
+
health(signal?: AbortSignal): Promise<SourceHealth>;
|
|
1142
|
+
/**
|
|
1143
|
+
* Run one log group's Logs Insights query end-to-end: StartQuery, then poll
|
|
1144
|
+
* GetQueryResults until Complete, a terminal non-complete status, or the
|
|
1145
|
+
* shared deadline/abort fires. Keeps the newest partial `results` snapshot so
|
|
1146
|
+
* a query that out-runs the budget still yields the rows that had completed —
|
|
1147
|
+
* never empty. A group whose query throws (bad creds, missing group) degrades
|
|
1148
|
+
* to `error` on the outcome rather than sinking the other groups' rows.
|
|
1149
|
+
*/
|
|
1150
|
+
private runGroupQuery;
|
|
1151
|
+
fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
|
|
1152
|
+
}
|
|
1153
|
+
/** Registry entry: build a CloudWatch source from env when its auth fields are set. */
|
|
1154
|
+
declare const cloudWatchEvidenceProvider: EvidenceSourceProvider;
|
|
1155
|
+
|
|
1156
|
+
interface SigV4Input {
|
|
1157
|
+
method: string;
|
|
1158
|
+
/** Absolute request URL (host + path + optional query). */
|
|
1159
|
+
url: string;
|
|
1160
|
+
region: string;
|
|
1161
|
+
/** AWS service name, e.g. "logs". */
|
|
1162
|
+
service: string;
|
|
1163
|
+
accessKeyId: string;
|
|
1164
|
+
secretAccessKey: string;
|
|
1165
|
+
/** Temporary-credential session token (STS/role). Signed + echoed when set. */
|
|
1166
|
+
sessionToken?: string;
|
|
1167
|
+
/** Request body (already serialized). Hashed into the canonical request. */
|
|
1168
|
+
body: string;
|
|
1169
|
+
/** Non-auth headers to sign (e.g. content-type, x-amz-target). */
|
|
1170
|
+
headers: Record<string, string>;
|
|
1171
|
+
/** Signing instant. Injectable for deterministic tests. Default `new Date()`. */
|
|
1172
|
+
now?: Date;
|
|
1173
|
+
}
|
|
1174
|
+
interface SignedHeaders {
|
|
1175
|
+
[name: string]: string;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Sign `input` and return the request headers to send (the caller's headers plus
|
|
1179
|
+
* `X-Amz-Date`, `Authorization`, and `X-Amz-Security-Token` when a session token
|
|
1180
|
+
* is present). Pure and synchronous.
|
|
1181
|
+
*/
|
|
1182
|
+
declare function signSigV4(input: SigV4Input): SignedHeaders;
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Splunk evidence adapter — built to the Sentry reference shape (`sentry.ts`) and
|
|
1186
|
+
* the CloudWatch async-poll shape (`cloudwatch.ts`). Query-at-incident-time pull:
|
|
1187
|
+
* given a located incident window and the correlation keys known for it, run a
|
|
1188
|
+
* bounded SPL search over the configured index(es) and normalize each matching
|
|
1189
|
+
* event into the neutral `evidence.v1` contract. Zero-copy — nothing Splunk
|
|
1190
|
+
* returns is persisted; only the derived bundle is.
|
|
1191
|
+
*
|
|
1192
|
+
* Load-bearing patterns mirrored from the references:
|
|
1193
|
+
* - **Injectable transport**: constructor takes `fetchImpl?: typeof fetch`
|
|
1194
|
+
* defaulting to global `fetch`; contract tests inject a fixture-routing stub so
|
|
1195
|
+
* there is zero network in CI.
|
|
1196
|
+
* - **Resilience / self-limiting search budget**: a Splunk search is async —
|
|
1197
|
+
* `POST .../jobs` dispatches a job, then we poll `.../results_preview`. The poll
|
|
1198
|
+
* loop is the analog of CloudWatch's poll: it self-limits to a sub-budget carved
|
|
1199
|
+
* from `limits.timeoutMs` (`POLL_BUDGET_FRACTION`) so the adapter resolves BEFORE
|
|
1200
|
+
* the framework's per-source race fires, and it keeps the newest partial preview
|
|
1201
|
+
* snapshot. A search that out-runs the budget degrades to the rows Splunk had
|
|
1202
|
+
* previewed + an honest gap ("splunk: search did not complete within Ns"),
|
|
1203
|
+
* never "no items."
|
|
1204
|
+
* - **Descriptor-keyed query construction**: the SPL terms are built strictly from
|
|
1205
|
+
* `descriptor.joinKeys` ∩ present keys; a requested key Splunk cannot use becomes
|
|
1206
|
+
* an honest {@link EvidenceGap} rather than a silent drop. Every declared join
|
|
1207
|
+
* key is genuinely applied (traceId/requestId as a term, service as a field
|
|
1208
|
+
* filter, time as earliest/latest) — no silent no-op.
|
|
1209
|
+
* - **Boundaries**: honors `limits.maxItems`/`maxBytes`/`timeoutMs`; the `count`
|
|
1210
|
+
* arg caps results server-side so there is no pagination walk beyond `maxItems`;
|
|
1211
|
+
* every request carries `CRUMBTRAIL_USER_AGENT`; the token lives only in the
|
|
1212
|
+
* Authorization header, never in a gap, stat, thrown message, or log.
|
|
1213
|
+
* - **Redaction stays at the framework boundary** (`redact.ts`); this file only
|
|
1214
|
+
* populates the fields that boundary scrubs (`brief`, `after`, `ref.url`).
|
|
1215
|
+
*/
|
|
1216
|
+
/** Env var carrying the Splunk search head base URL (incl. mgmt port, e.g.
|
|
1217
|
+
* `https://splunk.example.com:8089`). Required. */
|
|
1218
|
+
declare const SPLUNK_HOST_ENV = "CRUMBTRAIL_SPLUNK_HOST";
|
|
1219
|
+
/** Env var carrying the Splunk authentication (JWT) token. Required. */
|
|
1220
|
+
declare const SPLUNK_TOKEN_ENV = "CRUMBTRAIL_SPLUNK_TOKEN";
|
|
1221
|
+
/** Env var carrying a comma-separated list of allowed indexes. Required. */
|
|
1222
|
+
declare const SPLUNK_INDEX_ENV = "CRUMBTRAIL_SPLUNK_INDEX";
|
|
1223
|
+
/** Env var overriding the web UI base URL for deep links (e.g.
|
|
1224
|
+
* `https://splunk.example.com:8000`). Optional — derived from the host if unset. */
|
|
1225
|
+
declare const SPLUNK_WEB_URL_ENV = "CRUMBTRAIL_SPLUNK_WEB_URL";
|
|
1226
|
+
/** Presence of these three ⇒ the provider is configured (mirrors ticket clients). */
|
|
1227
|
+
declare const SPLUNK_AUTH_FIELDS: string[];
|
|
1228
|
+
declare const SPLUNK_DESCRIPTOR: EvidenceSourceDescriptor;
|
|
1229
|
+
interface SplunkSourceConfig {
|
|
1230
|
+
/** Search head base URL (no trailing slash), typically the :8089 mgmt port. */
|
|
1231
|
+
host: string;
|
|
1232
|
+
/** Splunk authentication (JWT) token. */
|
|
1233
|
+
token: string;
|
|
1234
|
+
/** Allowed index names; the SPL scans these (OR-joined). */
|
|
1235
|
+
indexes: string[];
|
|
1236
|
+
/** Web UI base URL for deep links. Defaults to a best-effort host derivation. */
|
|
1237
|
+
webUrl?: string;
|
|
1238
|
+
/** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
|
|
1239
|
+
fetchImpl?: typeof fetch;
|
|
1240
|
+
/** Poll interval override (ms) for the results_preview loop. Tests shrink it. */
|
|
1241
|
+
pollIntervalMs?: number;
|
|
1242
|
+
}
|
|
1243
|
+
interface SplunkQueryPlan {
|
|
1244
|
+
/** The full SPL string (starts with `search`, incl. index + earliest/latest). */
|
|
1245
|
+
spl: string;
|
|
1246
|
+
/** Join keys actually used to narrow beyond the time window, best-first. */
|
|
1247
|
+
usedKeys: EvidenceJoinKey[];
|
|
1248
|
+
/** Honest gaps: a requested key Splunk cannot filter by. */
|
|
1249
|
+
gaps: EvidenceGap[];
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Build the SPL from `descriptor.joinKeys` ∩ present keys.
|
|
1253
|
+
* Precedence: a bare quoted correlation-id term (traceId, else requestId) is the
|
|
1254
|
+
* tightest filter over raw events; `service="<svc>"` is applied additively as a
|
|
1255
|
+
* field filter; the time window is always expressed via `earliest`/`latest`. Any
|
|
1256
|
+
* requested key Splunk does NOT support (sessionId, release, url, user) yields a
|
|
1257
|
+
* gap so the bundle states plainly what filtered the result.
|
|
1258
|
+
*/
|
|
1259
|
+
declare function buildSplunkQuery(query: EvidenceQuery, indexes: string[]): SplunkQueryPlan;
|
|
1260
|
+
/** One Splunk search result row (the `results[]` entries in the JSON output). */
|
|
1261
|
+
interface SplunkResultRow {
|
|
1262
|
+
_raw?: string;
|
|
1263
|
+
_time?: string;
|
|
1264
|
+
_cd?: string;
|
|
1265
|
+
_bkt?: string;
|
|
1266
|
+
index?: string;
|
|
1267
|
+
sourcetype?: string;
|
|
1268
|
+
[field: string]: unknown;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Derive a best-effort web UI base for deep links from the mgmt host: the mgmt
|
|
1272
|
+
* port (8089) becomes the default web port (8000); otherwise the origin is used.
|
|
1273
|
+
* A configured {@link SplunkSourceConfig.webUrl} always wins.
|
|
1274
|
+
*/
|
|
1275
|
+
declare function splunkWebBase(host: string, webUrl?: string): string;
|
|
1276
|
+
/**
|
|
1277
|
+
* Splunk search-app deep link that lands on the SPL + time range the event came
|
|
1278
|
+
* from — the practical "actual search where the log lives" link (better than a
|
|
1279
|
+
* bare index link). The mgmt API host carries no UI, so we point at the web base.
|
|
1280
|
+
*/
|
|
1281
|
+
declare function splunkSearchDeepLink(webBase: string, spl: string, window: {
|
|
1282
|
+
start: number;
|
|
1283
|
+
end: number;
|
|
1284
|
+
}): string;
|
|
1285
|
+
/** One Splunk result row → neutral evidence.v1 item. Pure. */
|
|
1286
|
+
declare function normalizeSplunkRow(row: SplunkResultRow, webBase: string, spl: string, window: {
|
|
1287
|
+
start: number;
|
|
1288
|
+
end: number;
|
|
1289
|
+
}): EvidenceItem;
|
|
1290
|
+
declare class SplunkEvidenceSource implements EvidenceSource {
|
|
1291
|
+
readonly descriptor: EvidenceSourceDescriptor;
|
|
1292
|
+
private readonly host;
|
|
1293
|
+
private readonly token;
|
|
1294
|
+
private readonly indexes;
|
|
1295
|
+
private readonly webBase;
|
|
1296
|
+
private readonly fetchImpl;
|
|
1297
|
+
private readonly pollIntervalMs;
|
|
1298
|
+
constructor(config: SplunkSourceConfig);
|
|
1299
|
+
private headers;
|
|
1300
|
+
private getJson;
|
|
1301
|
+
/** Dispatch a search job (form-encoded), returning its sid. */
|
|
1302
|
+
private createJob;
|
|
1303
|
+
/** Fetch the newest preview snapshot for a job (also final rows once done). */
|
|
1304
|
+
private previewResults;
|
|
1305
|
+
/** Cheap authenticated no-op: GET server info. */
|
|
1306
|
+
health(signal?: AbortSignal): Promise<SourceHealth>;
|
|
1307
|
+
fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
|
|
1308
|
+
}
|
|
1309
|
+
/** Registry entry: build a Splunk source from env when its auth fields are set. */
|
|
1310
|
+
declare const splunkEvidenceProvider: EvidenceSourceProvider;
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Datadog evidence adapter — built to the Sentry reference shape (`sentry.ts`).
|
|
1314
|
+
* Query-at-incident-time pull: given a located incident window and the
|
|
1315
|
+
* correlation keys known for it, ask Datadog's Logs Search v2 API (primary) and
|
|
1316
|
+
* Spans Search v2 API (secondary) for matching records in that window, and
|
|
1317
|
+
* normalize each into the neutral `evidence.v1` contract. Zero-copy — nothing
|
|
1318
|
+
* Datadog returns is persisted; only the derived bundle is.
|
|
1319
|
+
*
|
|
1320
|
+
* Load-bearing patterns mirrored from the Sentry reference:
|
|
1321
|
+
* - **Injectable transport**: constructor takes `fetchImpl?: typeof fetch`
|
|
1322
|
+
* defaulting to global `fetch`; contract tests inject a fixture-routing stub so
|
|
1323
|
+
* there is zero network in CI.
|
|
1324
|
+
* - **Resilience / self-limiting secondary budget**: logs are the PRIMARY
|
|
1325
|
+
* evidence (fetched with the bounded retry). Spans are a SECONDARY fetch, run
|
|
1326
|
+
* best-effort inside a sub-budget carved from `limits.timeoutMs`
|
|
1327
|
+
* (`SPAN_BUDGET_FRACTION`) and bounded by the caller's abort signal. If the span
|
|
1328
|
+
* search is slow, fails, or is aborted, the log items still ship + an honest gap
|
|
1329
|
+
* — one API's slowness NEVER drops the other API's items, and the adapter always
|
|
1330
|
+
* resolves before the framework's per-source timeout fires.
|
|
1331
|
+
* - **Descriptor-keyed query construction**: the query string is built strictly
|
|
1332
|
+
* from `descriptor.joinKeys` ∩ present keys; a requested key Datadog cannot use
|
|
1333
|
+
* becomes an honest {@link EvidenceGap}. Every declared join key is genuinely
|
|
1334
|
+
* applied (traceId → `@trace_id`, service → `service:`, url → `@http.url`, time
|
|
1335
|
+
* → filter from/to) — no silent no-op.
|
|
1336
|
+
* - **Boundaries**: honors `limits.maxItems`/`maxBytes`/`timeoutMs`; the page
|
|
1337
|
+
* limit caps results server-side so there is no pagination walk beyond
|
|
1338
|
+
* `maxItems`; every request carries `CRUMBTRAIL_USER_AGENT`; the API/app keys live
|
|
1339
|
+
* only in the request headers, never in a gap, stat, thrown message, or log.
|
|
1340
|
+
* - **Redaction stays at the framework boundary** (`redact.ts`); this file only
|
|
1341
|
+
* populates the fields that boundary scrubs (`brief`, `after`, `ref.url`).
|
|
1342
|
+
*/
|
|
1343
|
+
/** Env var carrying the Datadog API key. Required. */
|
|
1344
|
+
declare const DATADOG_API_KEY_ENV = "CRUMBTRAIL_DATADOG_API_KEY";
|
|
1345
|
+
/** Env var carrying the Datadog application key. Required. */
|
|
1346
|
+
declare const DATADOG_APP_KEY_ENV = "CRUMBTRAIL_DATADOG_APP_KEY";
|
|
1347
|
+
/** Env var carrying the Datadog site (datadoghq.com | datadoghq.eu | us3… ). Optional. */
|
|
1348
|
+
declare const DATADOG_SITE_ENV = "CRUMBTRAIL_DATADOG_SITE";
|
|
1349
|
+
/** Default site when {@link DATADOG_SITE_ENV} is unset. */
|
|
1350
|
+
declare const DATADOG_DEFAULT_SITE = "datadoghq.com";
|
|
1351
|
+
/** Presence of these two ⇒ the provider is configured (mirrors ticket clients).
|
|
1352
|
+
* Site has a default so it is not required. */
|
|
1353
|
+
declare const DATADOG_AUTH_FIELDS: string[];
|
|
1354
|
+
declare const DATADOG_DESCRIPTOR: EvidenceSourceDescriptor;
|
|
1355
|
+
interface DatadogSourceConfig {
|
|
1356
|
+
apiKey: string;
|
|
1357
|
+
appKey: string;
|
|
1358
|
+
/** Datadog site, e.g. "datadoghq.com". Defaults to {@link DATADOG_DEFAULT_SITE}. */
|
|
1359
|
+
site?: string;
|
|
1360
|
+
/** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
|
|
1361
|
+
fetchImpl?: typeof fetch;
|
|
1362
|
+
}
|
|
1363
|
+
interface DatadogQueryPlan {
|
|
1364
|
+
/** The Datadog search query string (may be empty ⇒ time-window only). */
|
|
1365
|
+
queryString: string;
|
|
1366
|
+
/** Join keys actually used to narrow beyond the time window, best-first. */
|
|
1367
|
+
usedKeys: EvidenceJoinKey[];
|
|
1368
|
+
/** Honest gaps: a requested key Datadog cannot filter by. */
|
|
1369
|
+
gaps: EvidenceGap[];
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Build the Datadog query from `descriptor.joinKeys` ∩ present keys. The same
|
|
1373
|
+
* query string filters both the logs and spans search bodies.
|
|
1374
|
+
* Precedence: a `@trace_id:<id>` term when a trace is present (tightest), plus
|
|
1375
|
+
* additive `service:<svc>` and `@http.url:<url>` terms for whichever of those
|
|
1376
|
+
* keys is present. Any requested key Datadog does NOT support (requestId,
|
|
1377
|
+
* sessionId, release, user) yields a gap so the bundle states plainly what
|
|
1378
|
+
* filtered the result.
|
|
1379
|
+
*/
|
|
1380
|
+
declare function buildDatadogQuery(query: EvidenceQuery): DatadogQueryPlan;
|
|
1381
|
+
/** Minimal shape of a Datadog Logs Search v2 result row. */
|
|
1382
|
+
interface DatadogLog {
|
|
1383
|
+
id?: string;
|
|
1384
|
+
attributes?: {
|
|
1385
|
+
timestamp?: string | number;
|
|
1386
|
+
message?: string;
|
|
1387
|
+
service?: string;
|
|
1388
|
+
status?: string;
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
/** Minimal shape of a Datadog Spans Search v2 result row. */
|
|
1392
|
+
interface DatadogSpan {
|
|
1393
|
+
id?: string;
|
|
1394
|
+
attributes?: {
|
|
1395
|
+
start_timestamp?: string | number;
|
|
1396
|
+
resource_name?: string;
|
|
1397
|
+
operation_name?: string;
|
|
1398
|
+
service?: string;
|
|
1399
|
+
duration?: number;
|
|
1400
|
+
trace_id?: string;
|
|
1401
|
+
span_id?: string;
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Derive the Datadog app UI base for deep links from the API site.
|
|
1406
|
+
* `datadoghq.com`/`.eu` use an `app.` subdomain; regional sites (us3/us5/ap1)
|
|
1407
|
+
* already carry their subdomain, so the site is the app host.
|
|
1408
|
+
*/
|
|
1409
|
+
declare function datadogAppBase(site: string): string;
|
|
1410
|
+
/** One Datadog log → neutral evidence.v1 item (lane "logs"). Pure. */
|
|
1411
|
+
declare function normalizeDatadogLog(log: DatadogLog, appBase: string, queryString: string, window: {
|
|
1412
|
+
start: number;
|
|
1413
|
+
end: number;
|
|
1414
|
+
}): EvidenceItem;
|
|
1415
|
+
/** One Datadog span → neutral evidence.v1 item (lane "network"). Pure. */
|
|
1416
|
+
declare function normalizeDatadogSpan(span: DatadogSpan, appBase: string): EvidenceItem;
|
|
1417
|
+
declare class DatadogEvidenceSource implements EvidenceSource {
|
|
1418
|
+
readonly descriptor: EvidenceSourceDescriptor;
|
|
1419
|
+
private readonly apiKey;
|
|
1420
|
+
private readonly appKey;
|
|
1421
|
+
private readonly site;
|
|
1422
|
+
private readonly apiBase;
|
|
1423
|
+
private readonly appBase;
|
|
1424
|
+
private readonly fetchImpl;
|
|
1425
|
+
constructor(config: DatadogSourceConfig);
|
|
1426
|
+
private headers;
|
|
1427
|
+
private post;
|
|
1428
|
+
/** Cheap authenticated no-op: validate the API key. */
|
|
1429
|
+
health(signal?: AbortSignal): Promise<SourceHealth>;
|
|
1430
|
+
/** PRIMARY: Logs Search v2. Bounded retry — this is the evidence we must ship. */
|
|
1431
|
+
private searchLogs;
|
|
1432
|
+
/** SECONDARY: Spans Search v2. Single attempt on purpose — it runs inside a
|
|
1433
|
+
* sub-budget, so retrying would risk blowing the per-source timeout. */
|
|
1434
|
+
private searchSpans;
|
|
1435
|
+
/**
|
|
1436
|
+
* Best-effort span search inside a sub-budget carved from `limits.timeoutMs`
|
|
1437
|
+
* and bounded by the caller's abort signal. A slow/failed/aborted span search
|
|
1438
|
+
* leaves `spans` empty (→ the log items still ship) and flips `incomplete`.
|
|
1439
|
+
* Mirrors Sentry's `enrichStackHeads`.
|
|
1440
|
+
*/
|
|
1441
|
+
private fetchSpansBestEffort;
|
|
1442
|
+
fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
|
|
1443
|
+
}
|
|
1444
|
+
/** Registry entry: build a Datadog source from env when its auth fields are set. */
|
|
1445
|
+
declare const datadogEvidenceProvider: EvidenceSourceProvider;
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* PostHog evidence adapter — built to the Sentry reference shape (`sentry.ts`)
|
|
1449
|
+
* and the Datadog two-API shape (`datadog.ts`). Query-at-incident-time pull:
|
|
1450
|
+
* given a located incident window and the correlation keys known for it, ask
|
|
1451
|
+
* PostHog's Events REST API (primary) and Session-Recordings list API
|
|
1452
|
+
* (secondary) for records in that window, and normalize each into the neutral
|
|
1453
|
+
* `evidence.v1` contract. Zero-copy — nothing PostHog returns is persisted; only
|
|
1454
|
+
* the derived bundle is.
|
|
1455
|
+
*
|
|
1456
|
+
* Load-bearing patterns mirrored from the references:
|
|
1457
|
+
* - **Injectable transport**: constructor takes `fetchImpl?: typeof fetch`
|
|
1458
|
+
* defaulting to global `fetch`; contract tests inject a fixture-routing stub so
|
|
1459
|
+
* there is zero network in CI.
|
|
1460
|
+
* - **Resilience / self-limiting secondary budget**: events are the PRIMARY
|
|
1461
|
+
* evidence (fetched with the bounded retry). Session recordings are a SECONDARY
|
|
1462
|
+
* fetch, run best-effort inside a sub-budget carved from `limits.timeoutMs`
|
|
1463
|
+
* (`RECORDINGS_BUDGET_FRACTION`) and bounded by the caller's abort signal. If
|
|
1464
|
+
* the recordings list is slow, fails, or is aborted, the event items still ship
|
|
1465
|
+
* + an honest gap — one API's slowness NEVER drops the other API's items, and
|
|
1466
|
+
* the adapter always resolves before the framework's per-source timeout fires.
|
|
1467
|
+
* - **Descriptor-keyed query construction**: the filters are built strictly from
|
|
1468
|
+
* `descriptor.joinKeys` ∩ present keys; a requested key PostHog cannot use
|
|
1469
|
+
* becomes an honest {@link EvidenceGap}. Every declared join key is genuinely
|
|
1470
|
+
* applied (user → `distinct_id`, sessionId → `$session_id` event property +
|
|
1471
|
+
* recordings `session_ids`, url → `$current_url` event property, time → the
|
|
1472
|
+
* window) — no silent no-op.
|
|
1473
|
+
* - **Recordings are LINK-ONLY**: the adapter lists recordings and links them by
|
|
1474
|
+
* `ref.url` (a replay player deep link). It NEVER downloads or stores recording
|
|
1475
|
+
* content — `before`/`after` stay null. See the brief's Out of Scope.
|
|
1476
|
+
* - **Boundaries**: honors `limits.maxItems`/`maxBytes`/`timeoutMs`; the `limit`
|
|
1477
|
+
* arg caps results server-side so there is no pagination walk beyond
|
|
1478
|
+
* `maxItems`; every request carries `CRUMBTRAIL_USER_AGENT`; the personal API key
|
|
1479
|
+
* lives only in the Authorization header, never in a gap, stat, thrown message,
|
|
1480
|
+
* or log.
|
|
1481
|
+
* - **Redaction stays at the framework boundary** (`redact.ts`); this file only
|
|
1482
|
+
* populates the fields that boundary scrubs (`brief`, `after`, `ref.url`).
|
|
1483
|
+
*/
|
|
1484
|
+
/** Env var carrying the PostHog personal API key (Bearer). Required. */
|
|
1485
|
+
declare const POSTHOG_API_KEY_ENV = "CRUMBTRAIL_POSTHOG_API_KEY";
|
|
1486
|
+
/** Env var carrying the PostHog project id (numeric or short id). Required. */
|
|
1487
|
+
declare const POSTHOG_PROJECT_ID_ENV = "CRUMBTRAIL_POSTHOG_PROJECT_ID";
|
|
1488
|
+
/** Env var overriding the API/UI host (self-hosted or EU cloud). Optional. */
|
|
1489
|
+
declare const POSTHOG_HOST_ENV = "CRUMBTRAIL_POSTHOG_HOST";
|
|
1490
|
+
/** Default cloud host when {@link POSTHOG_HOST_ENV} is unset. The API and UI
|
|
1491
|
+
* share this origin, so it also anchors the deep links. Use
|
|
1492
|
+
* `https://eu.posthog.com` (EU cloud) or your self-hosted origin otherwise. */
|
|
1493
|
+
declare const POSTHOG_DEFAULT_HOST = "https://us.posthog.com";
|
|
1494
|
+
/** Presence of these two ⇒ the provider is configured (mirrors ticket clients).
|
|
1495
|
+
* Host has a default so it is not required. */
|
|
1496
|
+
declare const POSTHOG_AUTH_FIELDS: string[];
|
|
1497
|
+
declare const POSTHOG_DESCRIPTOR: EvidenceSourceDescriptor;
|
|
1498
|
+
interface PostHogSourceConfig {
|
|
1499
|
+
apiKey: string;
|
|
1500
|
+
projectId: string;
|
|
1501
|
+
/** API/UI host, no trailing slash. Defaults to {@link POSTHOG_DEFAULT_HOST}. */
|
|
1502
|
+
host?: string;
|
|
1503
|
+
/** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
|
|
1504
|
+
fetchImpl?: typeof fetch;
|
|
1505
|
+
}
|
|
1506
|
+
/** One PostHog event-property filter (the `properties` query param is a JSON
|
|
1507
|
+
* array of these). */
|
|
1508
|
+
interface PostHogPropertyFilter {
|
|
1509
|
+
key: string;
|
|
1510
|
+
value: string;
|
|
1511
|
+
operator: "exact";
|
|
1512
|
+
type: "event";
|
|
1513
|
+
}
|
|
1514
|
+
interface PostHogQueryPlan {
|
|
1515
|
+
/** `distinct_id` filter (from the `user` key), applied to events + recordings. */
|
|
1516
|
+
distinctId?: string;
|
|
1517
|
+
/** `$session_id` filter (from the `sessionId` key), applied to recordings too. */
|
|
1518
|
+
sessionId?: string;
|
|
1519
|
+
/** Event-property filters (`$session_id`, `$current_url`) for the events query. */
|
|
1520
|
+
properties: PostHogPropertyFilter[];
|
|
1521
|
+
/** Join keys actually used to narrow beyond the time window, best-first. */
|
|
1522
|
+
usedKeys: EvidenceJoinKey[];
|
|
1523
|
+
/** Honest gaps: a requested key PostHog cannot filter by. */
|
|
1524
|
+
gaps: EvidenceGap[];
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Build the PostHog query from `descriptor.joinKeys` ∩ present keys.
|
|
1528
|
+
* Precedence: `user` → `distinct_id` (the person filter, tightest and applied to
|
|
1529
|
+
* BOTH the events and recordings requests); `sessionId` → a `$session_id` event
|
|
1530
|
+
* property filter AND the recordings `session_ids` filter; `url` → a
|
|
1531
|
+
* `$current_url` event property filter. The time window is always applied via the
|
|
1532
|
+
* request date range. Any requested key PostHog does NOT support (traceId,
|
|
1533
|
+
* requestId, release, service) yields a gap so the bundle states plainly what
|
|
1534
|
+
* filtered the result.
|
|
1535
|
+
*/
|
|
1536
|
+
declare function buildPostHogQuery(query: EvidenceQuery): PostHogQueryPlan;
|
|
1537
|
+
/** Minimal shape of a PostHog event row (from `GET /api/projects/{id}/events/`). */
|
|
1538
|
+
interface PostHogEvent {
|
|
1539
|
+
id?: string;
|
|
1540
|
+
distinct_id?: string;
|
|
1541
|
+
event?: string;
|
|
1542
|
+
timestamp?: string;
|
|
1543
|
+
properties?: Record<string, unknown>;
|
|
1544
|
+
}
|
|
1545
|
+
/** Minimal shape of a PostHog session-recording row (from the list endpoint). */
|
|
1546
|
+
interface PostHogRecording {
|
|
1547
|
+
id?: string;
|
|
1548
|
+
distinct_id?: string;
|
|
1549
|
+
start_time?: string;
|
|
1550
|
+
end_time?: string;
|
|
1551
|
+
/** Recording length in SECONDS (PostHog reports `recording_duration`). */
|
|
1552
|
+
recording_duration?: number;
|
|
1553
|
+
}
|
|
1554
|
+
/** Human-readable recording duration, e.g. "45s" or "3m 20s". */
|
|
1555
|
+
declare function formatDuration(seconds: number | undefined): string;
|
|
1556
|
+
/** A person-scoped deep link when the distinct id is known (the reliable link),
|
|
1557
|
+
* else the project activity explorer. */
|
|
1558
|
+
declare function posthogEventDeepLink(appBase: string, projectId: string, distinctId: string | undefined): string;
|
|
1559
|
+
/** The replay player deep link for a recording (LINK-ONLY — no content fetched). */
|
|
1560
|
+
declare function posthogRecordingDeepLink(appBase: string, projectId: string, recordingId: string): string;
|
|
1561
|
+
/** One PostHog event → neutral evidence.v1 item (lane "browser"/"flow"). Pure. */
|
|
1562
|
+
declare function normalizePostHogEvent(event: PostHogEvent, appBase: string, projectId: string): EvidenceItem;
|
|
1563
|
+
/**
|
|
1564
|
+
* One PostHog session recording → neutral evidence.v1 item (lane "flow").
|
|
1565
|
+
* LINK-ONLY: the value is the replay deep link in `ref.url`; NO recording content
|
|
1566
|
+
* is fetched or stored, so `before`/`after` are null. Pure.
|
|
1567
|
+
*/
|
|
1568
|
+
declare function normalizePostHogRecording(recording: PostHogRecording, appBase: string, projectId: string): EvidenceItem;
|
|
1569
|
+
declare class PostHogEvidenceSource implements EvidenceSource {
|
|
1570
|
+
readonly descriptor: EvidenceSourceDescriptor;
|
|
1571
|
+
private readonly apiKey;
|
|
1572
|
+
private readonly projectId;
|
|
1573
|
+
private readonly appBase;
|
|
1574
|
+
private readonly fetchImpl;
|
|
1575
|
+
constructor(config: PostHogSourceConfig);
|
|
1576
|
+
private headers;
|
|
1577
|
+
private getJson;
|
|
1578
|
+
private projectPath;
|
|
1579
|
+
/** Cheap authenticated no-op: GET the project endpoint. */
|
|
1580
|
+
health(signal?: AbortSignal): Promise<SourceHealth>;
|
|
1581
|
+
/** PRIMARY: events query. Bounded retry — this is the evidence we must ship. */
|
|
1582
|
+
private searchEvents;
|
|
1583
|
+
/** SECONDARY: session-recordings list. Single attempt on purpose — it runs
|
|
1584
|
+
* inside a sub-budget, so retrying would risk blowing the per-source timeout.
|
|
1585
|
+
* LINK-ONLY: this lists recordings; it NEVER reads snapshot/content blobs. */
|
|
1586
|
+
private searchRecordings;
|
|
1587
|
+
/**
|
|
1588
|
+
* Best-effort recordings list inside a sub-budget carved from `limits.timeoutMs`
|
|
1589
|
+
* and bounded by the caller's abort signal. A slow/failed/aborted list leaves
|
|
1590
|
+
* `recordings` empty (→ the event items still ship) and flips `incomplete`.
|
|
1591
|
+
* Mirrors Datadog's `fetchSpansBestEffort`.
|
|
1592
|
+
*/
|
|
1593
|
+
private fetchRecordingsBestEffort;
|
|
1594
|
+
fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
|
|
1595
|
+
}
|
|
1596
|
+
/** Registry entry: build a PostHog source from env when its auth fields are set. */
|
|
1597
|
+
declare const posthogEvidenceProvider: EvidenceSourceProvider;
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Cloudflare evidence adapter — built to the Sentry reference shape
|
|
1601
|
+
* (`sentry.ts`). Cloudflare has no ad-hoc log-query API on most plans, so this
|
|
1602
|
+
* adapter reads a **Logpush-to-R2 sink**: the client configures a Logpush job
|
|
1603
|
+
* that streams HTTP-request logs (or Workers Trace Events) as gzipped NDJSON
|
|
1604
|
+
* objects into an R2 bucket, and this adapter lists+reads the objects whose
|
|
1605
|
+
* time-partitioned keys fall inside the located incident window.
|
|
1606
|
+
*
|
|
1607
|
+
* R2 exposes an **S3-compatible API**, so we reuse the hand-rolled
|
|
1608
|
+
* {@link signSigV4} signer (service `s3`, region `auto`, R2 access key/secret,
|
|
1609
|
+
* endpoint `https://{account}.r2.cloudflarestorage.com`) rather than adding an
|
|
1610
|
+
* S3 SDK — the same zero-dependency decision made for CloudWatch. R2 egress is
|
|
1611
|
+
* FREE, so bulk reads cost nothing (the one provider where this is true), but we
|
|
1612
|
+
* still bound total work by `maxItems`/`maxBytes`/`timeoutMs` — we never walk the
|
|
1613
|
+
* whole bucket; truncation is recorded as a gap. Zero-copy: nothing R2 returns is
|
|
1614
|
+
* persisted; only the derived bundle is.
|
|
1615
|
+
*
|
|
1616
|
+
* Load-bearing patterns mirrored from the reference adapters:
|
|
1617
|
+
* - **Injectable transport**: constructor takes `fetchImpl?: typeof fetch`
|
|
1618
|
+
* defaulting to global `fetch`; contract tests inject a fixture-routing stub so
|
|
1619
|
+
* there is zero network in CI.
|
|
1620
|
+
* - **Descriptor-keyed filtering**: R2 has no server-side query, so `url` and
|
|
1621
|
+
* `requestId` are applied as LINE-LEVEL filters after read; `time` is applied
|
|
1622
|
+
* both by mapping the window to the minimal set of date-partitioned object keys
|
|
1623
|
+
* AND at the line level. Any requested key the dataset cannot use becomes an
|
|
1624
|
+
* honest {@link EvidenceGap} rather than a silent no-op.
|
|
1625
|
+
* - **Self-limiting read budget**: listing is the PRIMARY step (bounded retry);
|
|
1626
|
+
* object reads fan out with capped concurrency inside a sub-budget carved from
|
|
1627
|
+
* `limits.timeoutMs` (`READ_BUDGET_FRACTION`) so the adapter resolves BEFORE the
|
|
1628
|
+
* framework's per-source timeout fires. Objects that could not be read in budget
|
|
1629
|
+
* degrade to whatever was parsed + an honest gap, never "no items."
|
|
1630
|
+
* - **Resilience / source-unavailable marker**: a hard failure (bucket
|
|
1631
|
+
* unreachable / auth fail) that yielded zero items self-degrades to a
|
|
1632
|
+
* `kind: "source-unavailable"` gap → `ok:false`; partial success stays
|
|
1633
|
+
* `ok:true` (see `fetch-all.ts`).
|
|
1634
|
+
* - **Secrets discipline**: the R2 secret lives ONLY inside the signer; it never
|
|
1635
|
+
* appears in a gap, stat, thrown message, or log.
|
|
1636
|
+
* - **Redaction stays at the framework boundary** (`redact.ts`); this file only
|
|
1637
|
+
* populates the fields that boundary scrubs (`brief`, `after`, `ref.url`).
|
|
1638
|
+
*/
|
|
1639
|
+
/** Env var carrying the Cloudflare account id (R2 endpoint host). Required. */
|
|
1640
|
+
declare const CLOUDFLARE_R2_ACCOUNT_ID_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_ACCOUNT_ID";
|
|
1641
|
+
/** Env var carrying the R2 access key id. Required. */
|
|
1642
|
+
declare const CLOUDFLARE_R2_ACCESS_KEY_ID_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_ACCESS_KEY_ID";
|
|
1643
|
+
/** Env var carrying the R2 secret access key. Required. */
|
|
1644
|
+
declare const CLOUDFLARE_R2_SECRET_ACCESS_KEY_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_SECRET_ACCESS_KEY";
|
|
1645
|
+
/** Env var carrying the R2 bucket name Logpush writes into. Required. */
|
|
1646
|
+
declare const CLOUDFLARE_R2_BUCKET_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_BUCKET";
|
|
1647
|
+
/** Env var carrying the key prefix BEFORE the `{DATE}` Logpush partition. Optional. */
|
|
1648
|
+
declare const CLOUDFLARE_R2_PREFIX_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_PREFIX";
|
|
1649
|
+
/** Env var selecting the Logpush dataset. Optional (default http_requests). */
|
|
1650
|
+
declare const CLOUDFLARE_R2_DATASET_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_DATASET";
|
|
1651
|
+
/** Env var overriding the R2 endpoint host (testing/localstack). Optional. */
|
|
1652
|
+
declare const CLOUDFLARE_R2_ENDPOINT_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_ENDPOINT";
|
|
1653
|
+
/** Logpush datasets this adapter knows how to normalize. */
|
|
1654
|
+
type CloudflareDataset = "http_requests" | "workers_trace_events";
|
|
1655
|
+
/** Presence of these four ⇒ the provider is configured (mirrors ticket clients).
|
|
1656
|
+
* Prefix, dataset, and endpoint have defaults so they are not required. */
|
|
1657
|
+
declare const CLOUDFLARE_AUTH_FIELDS: string[];
|
|
1658
|
+
declare const CLOUDFLARE_DESCRIPTOR: EvidenceSourceDescriptor;
|
|
1659
|
+
interface CloudflareSourceConfig {
|
|
1660
|
+
accountId: string;
|
|
1661
|
+
accessKeyId: string;
|
|
1662
|
+
secretAccessKey: string;
|
|
1663
|
+
bucket: string;
|
|
1664
|
+
/** Key prefix BEFORE the `{DATE}` Logpush partition (no leading slash). */
|
|
1665
|
+
prefix?: string;
|
|
1666
|
+
/** Logpush dataset. Defaults to http_requests. */
|
|
1667
|
+
dataset?: CloudflareDataset;
|
|
1668
|
+
/** Endpoint host override (no trailing slash). Defaults to the R2 endpoint. */
|
|
1669
|
+
endpoint?: string;
|
|
1670
|
+
/** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
|
|
1671
|
+
fetchImpl?: typeof fetch;
|
|
1672
|
+
}
|
|
1673
|
+
interface CloudflarePlan {
|
|
1674
|
+
/** Case-insensitive substring the request URL must contain, when url keyed. */
|
|
1675
|
+
urlFilter?: string;
|
|
1676
|
+
/** Exact RayID/request id a line must carry, when requestId keyed. */
|
|
1677
|
+
requestIdFilter?: string;
|
|
1678
|
+
/** Join keys actually used to narrow beyond the time window, best-first. */
|
|
1679
|
+
usedKeys: EvidenceJoinKey[];
|
|
1680
|
+
/** Honest gaps: a requested key this dataset cannot filter by. */
|
|
1681
|
+
gaps: EvidenceGap[];
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Build the line-level filter plan from `descriptor.joinKeys` ∩ present keys.
|
|
1685
|
+
* R2 has no query API, so filters are applied per NDJSON line after read:
|
|
1686
|
+
* `requestId` → exact match on the line's RayID (both datasets); `url` →
|
|
1687
|
+
* case-insensitive substring on the request URL (http_requests only — Workers
|
|
1688
|
+
* Trace Events carry no URL, so a requested `url` becomes a gap). Any requested
|
|
1689
|
+
* key outside the descriptor (traceId, sessionId, release, user, service) yields
|
|
1690
|
+
* a gap so the bundle states plainly what filtered the result.
|
|
1691
|
+
*/
|
|
1692
|
+
declare function buildCloudflarePlan(query: EvidenceQuery, dataset: CloudflareDataset): CloudflarePlan;
|
|
1693
|
+
declare class CloudflareEvidenceSource implements EvidenceSource {
|
|
1694
|
+
readonly descriptor: EvidenceSourceDescriptor;
|
|
1695
|
+
private readonly accessKeyId;
|
|
1696
|
+
private readonly secretAccessKey;
|
|
1697
|
+
private readonly bucket;
|
|
1698
|
+
private readonly prefix;
|
|
1699
|
+
private readonly dataset;
|
|
1700
|
+
private readonly endpoint;
|
|
1701
|
+
private readonly fetchImpl;
|
|
1702
|
+
constructor(config: CloudflareSourceConfig);
|
|
1703
|
+
/** One SigV4-signed GET against R2 (S3 API). The secret lives only in the
|
|
1704
|
+
* signer; `x-amz-content-sha256` (empty-body hash) is signed as S3 requires.
|
|
1705
|
+
* Returns the raw Response so callers can read text() or arrayBuffer(). */
|
|
1706
|
+
private signedGet;
|
|
1707
|
+
/** Build a ListObjectsV2 URL for a key prefix, capped to a single page. */
|
|
1708
|
+
private listUrl;
|
|
1709
|
+
private objectUrl;
|
|
1710
|
+
private listObjects;
|
|
1711
|
+
private readObject;
|
|
1712
|
+
/** Cheap authenticated no-op: ListObjectsV2 on the bucket, max-keys 1. */
|
|
1713
|
+
health(signal?: AbortSignal): Promise<SourceHealth>;
|
|
1714
|
+
fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
|
|
1715
|
+
}
|
|
1716
|
+
/** Registry entry: build a Cloudflare source from env when its auth fields are set. */
|
|
1717
|
+
declare const cloudflareEvidenceProvider: EvidenceSourceProvider;
|
|
1718
|
+
|
|
1719
|
+
interface JsonRpcRequest {
|
|
1720
|
+
jsonrpc: "2.0";
|
|
1721
|
+
id?: number | string;
|
|
1722
|
+
method: string;
|
|
1723
|
+
params?: Record<string, unknown>;
|
|
1724
|
+
}
|
|
1725
|
+
interface JsonRpcResponse {
|
|
1726
|
+
jsonrpc: "2.0";
|
|
1727
|
+
id: number | string;
|
|
1728
|
+
result?: unknown;
|
|
1729
|
+
error?: {
|
|
1730
|
+
code: number;
|
|
1731
|
+
message: string;
|
|
1732
|
+
data?: unknown;
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
interface McpServerConfig {
|
|
1736
|
+
outputDir: string;
|
|
1737
|
+
/**
|
|
1738
|
+
* Test-only seam: overrides how the git-host client is constructed for
|
|
1739
|
+
* `solveContext`'s intent-inference path. Production code leaves this
|
|
1740
|
+
* unset and builds a `GitHubRestClient` from `CRUMBTRAIL_GITHUB_TOKEN`.
|
|
1741
|
+
*/
|
|
1742
|
+
gitHostClientFactory?: (gitHost: {
|
|
1743
|
+
owner: string;
|
|
1744
|
+
repo: string;
|
|
1745
|
+
}) => GitHostClient;
|
|
1746
|
+
/**
|
|
1747
|
+
* Test-only seam: overrides how the ticket connector is constructed for
|
|
1748
|
+
* `solveContext`'s `ticket` input. Production code leaves this unset and
|
|
1749
|
+
* builds a connector from the documented provider env vars.
|
|
1750
|
+
*/
|
|
1751
|
+
ticketConnectorFactory?: (provider: TicketProvider) => TicketConnector;
|
|
1752
|
+
/**
|
|
1753
|
+
* Test-only seam: overrides how the on-demand `Reproducer` is constructed
|
|
1754
|
+
* for `solveContext`'s thin-evidence reproduction path. Production code
|
|
1755
|
+
* leaves this unset, which disables reproduction entirely — the gate
|
|
1756
|
+
* short-circuits and no reproducer (not even `NoopReproducer`) is built.
|
|
1757
|
+
*/
|
|
1758
|
+
reproducerFactory?: () => Reproducer;
|
|
1759
|
+
/**
|
|
1760
|
+
* Test-only seam: overrides how the client's evidence sources are constructed
|
|
1761
|
+
* for `solveContext`'s adapter phase (sessionless Mode A + blended). Production
|
|
1762
|
+
* code leaves this unset and builds them from env via evidenceSourcesFromEnv().
|
|
1763
|
+
*/
|
|
1764
|
+
evidenceSourcesFactory?: () => EvidenceSource[];
|
|
1765
|
+
}
|
|
1766
|
+
declare class McpServer {
|
|
1767
|
+
private outputDir;
|
|
1768
|
+
private bugQueue;
|
|
1769
|
+
private gitHostClientFactory?;
|
|
1770
|
+
private ticketConnectorFactory?;
|
|
1771
|
+
private reproducerFactory?;
|
|
1772
|
+
private evidenceSourcesFactory?;
|
|
1773
|
+
constructor(config: McpServerConfig);
|
|
1774
|
+
/** Evidence sources for solveContext's adapter phase — injected in tests,
|
|
1775
|
+
* built from env in production. */
|
|
1776
|
+
private evidenceSources;
|
|
1777
|
+
start(): void;
|
|
1778
|
+
handleMessage(msg: JsonRpcRequest): Promise<JsonRpcResponse | null>;
|
|
1779
|
+
private callTool;
|
|
1780
|
+
private sessionDir;
|
|
1781
|
+
private isSafeSessionId;
|
|
1782
|
+
private readEvents;
|
|
1783
|
+
/**
|
|
1784
|
+
* Resolve a tool's args to a target directory. A bug is a named window into
|
|
1785
|
+
* a session: if `bugId` is present, gate on its existence and resolve to the
|
|
1786
|
+
* bug's dir; otherwise resolve to the session's dir. Session targets are not
|
|
1787
|
+
* pre-checked here (existence is discovered downstream by the specific tool,
|
|
1788
|
+
* matching pre-seam behavior), so this only errors for a missing bug.
|
|
1789
|
+
*/
|
|
1790
|
+
private resolveTarget;
|
|
1791
|
+
/** Shared kind/after/before filtering; per-tool limit/compact stay caller-side. */
|
|
1792
|
+
private filterEvents;
|
|
1793
|
+
/** Shared error-context body for both session and bug tools. */
|
|
1794
|
+
private errorContextFor;
|
|
1795
|
+
/** Shared failed-requests body; notFoundMsg differs per caller. */
|
|
1796
|
+
private failedRequestsFor;
|
|
1797
|
+
private toolListSessions;
|
|
1798
|
+
private sessionMetadataMatches;
|
|
1799
|
+
private toolGetIndex;
|
|
1800
|
+
private toolGetEvents;
|
|
1801
|
+
private toolGetErrorContext;
|
|
1802
|
+
private toolGetFailedRequests;
|
|
1803
|
+
private toolGetLinkedRequestContext;
|
|
1804
|
+
private matchesFullStackGap;
|
|
1805
|
+
private linkedRequestDiagnostics;
|
|
1806
|
+
private fullStackCorrelationStatus;
|
|
1807
|
+
private compactLinkedFullStackRequest;
|
|
1808
|
+
private compactFullStackGap;
|
|
1809
|
+
private compactFrontendEvidence;
|
|
1810
|
+
private compactBackendEvidence;
|
|
1811
|
+
private compactEventRef;
|
|
1812
|
+
private compactCorrelation;
|
|
1813
|
+
private compactFrontendError;
|
|
1814
|
+
private compactBackendError;
|
|
1815
|
+
/** Parses the optional `maxTokens` arg. `{}` when absent; error when invalid. */
|
|
1816
|
+
private maxTokensOf;
|
|
1817
|
+
/**
|
|
1818
|
+
* Shared budgeted-response path: fills `payload[itemsKey]` from `items` in
|
|
1819
|
+
* their given rank order via the one shared fillToBudget helper, then
|
|
1820
|
+
* attaches `tokenEstimate` (of the final serialized payload) and, when
|
|
1821
|
+
* anything was dropped, a structured `dropReport`. `onKept` lets a caller
|
|
1822
|
+
* patch dependent fields (e.g. getWindow's returned/truncated) after the
|
|
1823
|
+
* fill. Deterministic; logs drops to stderr only (stdout carries only
|
|
1824
|
+
* JSON-RPC frames).
|
|
1825
|
+
*/
|
|
1826
|
+
private budgetedTextResult;
|
|
1827
|
+
/**
|
|
1828
|
+
* Shared response path for getFixContext and getLatestIssue. Unbudgeted
|
|
1829
|
+
* stays byte-identical to the raw contract; budgeted fills ranked_candidates
|
|
1830
|
+
* in candidate-rank order (refs are candidate ids resolvable via getEvidence).
|
|
1831
|
+
*/
|
|
1832
|
+
private fixContextResult;
|
|
1833
|
+
private toolGetFixContext;
|
|
1834
|
+
/**
|
|
1835
|
+
* One-call entry point: resolve the latest finalized session with
|
|
1836
|
+
* error-class evidence via the SAME resolveLatestIssue shared with the
|
|
1837
|
+
* `fix-context --latest` CLI flag, then reuse the getFixContext response
|
|
1838
|
+
* path (including optional maxTokens budgeting — its only input).
|
|
1839
|
+
*/
|
|
1840
|
+
private toolGetLatestIssue;
|
|
1841
|
+
private toolGetRegressionContext;
|
|
1842
|
+
private toolSolveContext;
|
|
1843
|
+
private toolListDistinctBugs;
|
|
1844
|
+
private toolGetRecurrence;
|
|
1845
|
+
private toolGetBug;
|
|
1846
|
+
/** Reads the grouped distinct bugs from the finalized hot-plane bundle (llm.json, else bundle.json). */
|
|
1847
|
+
private readDistinctBugs;
|
|
1848
|
+
private recurrenceRollups;
|
|
1849
|
+
private compactRecurrence;
|
|
1850
|
+
private signatureForBug;
|
|
1851
|
+
/**
|
|
1852
|
+
* Recall past issues that rhyme with a session or a free-text description. In
|
|
1853
|
+
* cloud deployments (CRUMBTRAIL_CLOUD_URL + CRUMBTRAIL_API_KEY set) this delegates
|
|
1854
|
+
* to the org-wide semantic index; otherwise it scans the local session store
|
|
1855
|
+
* with a text-overlap + facet analogue so self-hosted users still get recall
|
|
1856
|
+
* without a vector DB.
|
|
1857
|
+
*/
|
|
1858
|
+
private toolRecallSimilarIssues;
|
|
1859
|
+
/** Adapt this server's storage readers to the recall engine's injected seam.
|
|
1860
|
+
* Delegates to the shared buildRecallStore so the MCP tool and the inner
|
|
1861
|
+
* /api/solve-context endpoint locate against an identical store. */
|
|
1862
|
+
private recallStore;
|
|
1863
|
+
private isDistinctBugRecord;
|
|
1864
|
+
private firstString;
|
|
1865
|
+
private toolGetSessionManifest;
|
|
1866
|
+
private synthesizeManifest;
|
|
1867
|
+
private toolGetWindow;
|
|
1868
|
+
private toolGetEvidence;
|
|
1869
|
+
private windowCap;
|
|
1870
|
+
private sessionExists;
|
|
1871
|
+
/** Reads the sanitized cold event stream first; falls back to legacy/plain events when zstd is absent. */
|
|
1872
|
+
private readColdEvents;
|
|
1873
|
+
private parseEvents;
|
|
1874
|
+
private readCandidatesJsonl;
|
|
1875
|
+
private readSignatureEntries;
|
|
1876
|
+
private readInteractiveElement;
|
|
1877
|
+
/**
|
|
1878
|
+
* Reads the finalized hot-plane interactive-element identity map (bundle.browserEvidence
|
|
1879
|
+
* .interactiveElements). Prefers llm.json then bundle.json — the same finalized artifacts
|
|
1880
|
+
* listDistinctBugs/getBug read. Values are already redaction-sanitized at bundle build time;
|
|
1881
|
+
* this only ever surfaces those redacted descriptors, never raw masked values.
|
|
1882
|
+
*/
|
|
1883
|
+
private readInteractiveElements;
|
|
1884
|
+
private toolResolveSignature;
|
|
1885
|
+
private toolLocateInteractiveElements;
|
|
1886
|
+
private locateLimit;
|
|
1887
|
+
/**
|
|
1888
|
+
* Builds a deterministic, redaction-safe descriptor for one signature by combining the
|
|
1889
|
+
* finalized interactive-element map (label/text, tag, occurrence count, path) with the
|
|
1890
|
+
* cold-plane signature dictionary (first-seen, first event kind). Returns undefined when the
|
|
1891
|
+
* signature is absent from the interactive-element map.
|
|
1892
|
+
*/
|
|
1893
|
+
private buildElementDescriptor;
|
|
1894
|
+
/**
|
|
1895
|
+
* Pure descriptor builder over already-read interactive-element and signature-dictionary
|
|
1896
|
+
* arrays. Hoisting the reads out of callers keeps locateInteractiveElements O(n) instead of
|
|
1897
|
+
* re-parsing the bundle/signature files once per element.
|
|
1898
|
+
*/
|
|
1899
|
+
private buildElementDescriptorFrom;
|
|
1900
|
+
private affordanceFor;
|
|
1901
|
+
private readJsonRecord;
|
|
1902
|
+
private toolGetStorageSnapshot;
|
|
1903
|
+
private toolGetCookieChanges;
|
|
1904
|
+
private toolGetStorageChanges;
|
|
1905
|
+
private toolGetTranscript;
|
|
1906
|
+
private toolGetFrame;
|
|
1907
|
+
private toolGetFrameById;
|
|
1908
|
+
private toolListBugs;
|
|
1909
|
+
private toolGetBugReport;
|
|
1910
|
+
private toolGetBugEvents;
|
|
1911
|
+
private toolGetBugErrorContext;
|
|
1912
|
+
private toolGetBugFailedRequests;
|
|
1913
|
+
private toolGetBugVoiceTranscript;
|
|
1914
|
+
private toolResolveBug;
|
|
1915
|
+
private toolGetBugLlmContext;
|
|
1916
|
+
private safeGetBug;
|
|
1917
|
+
private safeGetBugLlmContext;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/** A database row diff correlated to the primary window. See {@link LlmBundleDbDiff}. */
|
|
1921
|
+
type FixContextDbDiff = LlmBundleDbDiff;
|
|
1922
|
+
type FixContextDbRead = LlmBundleDbRead;
|
|
1923
|
+
type FixContextDbActivity = LlmBundleDbActivity;
|
|
1924
|
+
/**
|
|
1925
|
+
* Versioned, ranked, correlated, LLM-ready "hand it to the model" bundle for a finalized
|
|
1926
|
+
* session. This is the keystone fix-context contract (V2.5).
|
|
1927
|
+
*
|
|
1928
|
+
* The shape is intentionally stable: `primary_window.db_diffs` defaults to an empty array
|
|
1929
|
+
* and `environment` defaults to null so later checkpoints (CP5 DB diffing, CP3 environment
|
|
1930
|
+
* capture) can populate them without breaking the contract.
|
|
1931
|
+
*/
|
|
1932
|
+
/**
|
|
1933
|
+
* @stability stable
|
|
1934
|
+
* Version-bump policy follows the same Fork A decision as fusion.v1 — see
|
|
1935
|
+
* wargames/wargames/01-solve-context-wargame-fields.md.
|
|
1936
|
+
*/
|
|
1937
|
+
declare const FIX_CONTEXT_SCHEMA_VERSION: "fix-context.v1";
|
|
1938
|
+
interface FixContextSession {
|
|
1939
|
+
id: string;
|
|
1940
|
+
name?: string;
|
|
1941
|
+
app?: string;
|
|
1942
|
+
source?: string;
|
|
1943
|
+
startMs: number;
|
|
1944
|
+
endMs: number;
|
|
1945
|
+
durationMs: number;
|
|
1946
|
+
}
|
|
1947
|
+
interface FixContextReproHint {
|
|
1948
|
+
title?: string;
|
|
1949
|
+
detector?: string;
|
|
1950
|
+
severity?: EvidenceCandidate["severity"];
|
|
1951
|
+
route?: string;
|
|
1952
|
+
target?: EvidenceCandidate["anchor"]["target"];
|
|
1953
|
+
elementLabel?: string;
|
|
1954
|
+
errorCode?: string;
|
|
1955
|
+
message?: string;
|
|
1956
|
+
requestId?: string;
|
|
1957
|
+
offsetMs?: number;
|
|
1958
|
+
}
|
|
1959
|
+
interface FixContextPrimaryWindow {
|
|
1960
|
+
frontend: {
|
|
1961
|
+
window: {
|
|
1962
|
+
start: number;
|
|
1963
|
+
end: number;
|
|
1964
|
+
windowId: string;
|
|
1965
|
+
} | null;
|
|
1966
|
+
anchor: EvidenceCandidate["anchor"] | null;
|
|
1967
|
+
requests: LlmBundleFrontendRequestEvidenceSummary[];
|
|
1968
|
+
};
|
|
1969
|
+
backend: {
|
|
1970
|
+
requests: LlmBundleBackendRequestEvidenceSummary[];
|
|
1971
|
+
};
|
|
1972
|
+
/**
|
|
1973
|
+
* Database row diffs correlated to the primary window (CP5 DB diffing). Empty when the session
|
|
1974
|
+
* captured no `db.diff` events in the window. Consumers MUST treat `[]` as "no DB evidence".
|
|
1975
|
+
*/
|
|
1976
|
+
db_diffs: FixContextDbDiff[];
|
|
1977
|
+
/**
|
|
1978
|
+
* Database rows read in the primary window (`db.read`, pre-state capture). Empty when read
|
|
1979
|
+
* capture is disabled or no reads matched the primary request/window.
|
|
1980
|
+
*/
|
|
1981
|
+
db_reads: FixContextDbRead[];
|
|
1982
|
+
/**
|
|
1983
|
+
* OTel DB span activity in the primary window. These are statements/operations only, never
|
|
1984
|
+
* before/after row diffs.
|
|
1985
|
+
*/
|
|
1986
|
+
db_activity: FixContextDbActivity[];
|
|
1987
|
+
}
|
|
1988
|
+
/** One downstream symptom of the primary root cause, resolved from `ranked_candidates`. */
|
|
1989
|
+
interface FixContextCausalSymptom {
|
|
1990
|
+
id: string;
|
|
1991
|
+
detector: string;
|
|
1992
|
+
title: string;
|
|
1993
|
+
attributionConfidence?: CausalConfidence;
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* The primary root-cause → symptom chain, derived ONLY from causal fields already present on the
|
|
1997
|
+
* ranked candidates (CP3). `null` when the primary candidate is isolated or has no attributed
|
|
1998
|
+
* symptoms. Purely a projection — no attribution is recomputed here.
|
|
1999
|
+
*/
|
|
2000
|
+
interface FixContextCausalChain {
|
|
2001
|
+
root: {
|
|
2002
|
+
id: string;
|
|
2003
|
+
detector: string;
|
|
2004
|
+
title: string;
|
|
2005
|
+
};
|
|
2006
|
+
symptoms: FixContextCausalSymptom[];
|
|
2007
|
+
}
|
|
2008
|
+
interface FixContext {
|
|
2009
|
+
schemaVersion: typeof FIX_CONTEXT_SCHEMA_VERSION;
|
|
2010
|
+
session: FixContextSession;
|
|
2011
|
+
ranked_candidates: EvidenceCandidate[];
|
|
2012
|
+
primary_window: FixContextPrimaryWindow;
|
|
2013
|
+
/**
|
|
2014
|
+
* Account/environment state snapshot. Defaults to `null`; CP3 (environment capture)
|
|
2015
|
+
* populates this. Consumers MUST treat null as "not captured".
|
|
2016
|
+
*/
|
|
2017
|
+
environment: Record<string, unknown> | null;
|
|
2018
|
+
/**
|
|
2019
|
+
* Primary root-cause → symptom chain projected from the ranked candidates' causal fields (CP4).
|
|
2020
|
+
* `null` when the top candidate is isolated or attributes no symptoms. Consumers MUST treat null
|
|
2021
|
+
* as "no causal structure surfaced".
|
|
2022
|
+
*/
|
|
2023
|
+
causal_chain: FixContextCausalChain | null;
|
|
2024
|
+
repro_hint: FixContextReproHint | null;
|
|
2025
|
+
}
|
|
2026
|
+
interface BuildFixContextOptions {
|
|
2027
|
+
/** Base sessions directory used to resolve a bare session id to a directory. */
|
|
2028
|
+
outputDir?: string;
|
|
2029
|
+
}
|
|
2030
|
+
declare class FixContextError extends Error {
|
|
2031
|
+
readonly code: "session-not-found";
|
|
2032
|
+
constructor(code: "session-not-found", message: string);
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Builds the fix-context contract for a finalized session by reading hot-plane artifacts
|
|
2036
|
+
* (index.json, candidates.jsonl, llm.json). It never reads raw NDJSON at query time.
|
|
2037
|
+
*/
|
|
2038
|
+
declare function buildFixContext(sessionDirOrId: string, opts?: BuildFixContextOptions): FixContext;
|
|
2039
|
+
|
|
2040
|
+
type HeaderValue = string | number | readonly string[] | undefined;
|
|
2041
|
+
type BackendRequestHeaders = Record<string, HeaderValue>;
|
|
2042
|
+
interface BackendRequestEventInput {
|
|
2043
|
+
method?: string;
|
|
2044
|
+
url?: string;
|
|
2045
|
+
originalUrl?: string;
|
|
2046
|
+
path?: string;
|
|
2047
|
+
route?: string;
|
|
2048
|
+
headers?: BackendRequestHeaders;
|
|
2049
|
+
sessionId?: string;
|
|
2050
|
+
requestId?: string;
|
|
2051
|
+
sessionStartedAt?: number | Date;
|
|
2052
|
+
now?: number;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* Default sensitive column names whose values are always dropped before a `db.diff` event rests.
|
|
2057
|
+
* Hosts can extend this list per shim via `redactColumns`. Matching is name-based and
|
|
2058
|
+
* normalization-aware (case-insensitive; `apiKey`/`api_key`/`APIKEY` all match `api_key`).
|
|
2059
|
+
*/
|
|
2060
|
+
declare const DEFAULT_SENSITIVE_DB_COLUMNS: readonly ["password", "token", "secret", "api_key", "ssn"];
|
|
2061
|
+
|
|
2062
|
+
interface BuildDbDiffEventInput {
|
|
2063
|
+
/** Engine that produced the mutation. Defaults to `"postgres"` for back-compat. */
|
|
2064
|
+
engine?: DbEngine;
|
|
2065
|
+
op: DbDiffOp;
|
|
2066
|
+
table: string;
|
|
2067
|
+
/** Primary-key column→value map, or `null` when it could not be resolved. */
|
|
2068
|
+
pk: Record<string, unknown> | null;
|
|
2069
|
+
/** Post-image of the affected row (insert/update). */
|
|
2070
|
+
after?: Record<string, unknown>;
|
|
2071
|
+
/** Pre-image of the affected row (deletes, or updates with before-capture enabled). */
|
|
2072
|
+
before?: Record<string, unknown>;
|
|
2073
|
+
/** Set only on image-less statement-level fallback events (pk `null`, no after/before). */
|
|
2074
|
+
rowCount?: number;
|
|
2075
|
+
/** Correlation id; MUST equal the active request's traceId/requestId. */
|
|
2076
|
+
requestId: string;
|
|
2077
|
+
sessionId?: string;
|
|
2078
|
+
/** Extra sensitive column names to drop, on top of {@link DEFAULT_SENSITIVE_DB_COLUMNS}. */
|
|
2079
|
+
redactColumns?: readonly string[];
|
|
2080
|
+
now?: number;
|
|
2081
|
+
sessionStartedAt?: number | Date;
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Builds the canonical `k:'db.diff'` event for one changed row. Sensitive columns are dropped from
|
|
2085
|
+
* `after`/`before`/`pk` via the shared redaction policy BEFORE the event is returned, so secret
|
|
2086
|
+
* values never rest in the event. The event carries `requestId` so it lands in the same evidence
|
|
2087
|
+
* window as the `backend.req.*` and front-end network events of the request that caused the write.
|
|
2088
|
+
*/
|
|
2089
|
+
declare function buildDbDiffEvent(input: BuildDbDiffEventInput): BugEvent;
|
|
2090
|
+
|
|
2091
|
+
interface BuildDbReadEventInput {
|
|
2092
|
+
/** Engine that produced the read. Defaults to `"postgres"` for back-compat. */
|
|
2093
|
+
engine?: DbEngine;
|
|
2094
|
+
table: string;
|
|
2095
|
+
pk: Record<string, unknown> | null;
|
|
2096
|
+
row: Record<string, unknown>;
|
|
2097
|
+
requestId: string;
|
|
2098
|
+
sessionId?: string;
|
|
2099
|
+
redactColumns?: readonly string[];
|
|
2100
|
+
now?: number;
|
|
2101
|
+
sessionStartedAt?: number | Date;
|
|
2102
|
+
}
|
|
2103
|
+
interface BuildDbReadBulkEventInput {
|
|
2104
|
+
/** Engine that produced the read. Defaults to `"postgres"` for back-compat. */
|
|
2105
|
+
engine?: DbEngine;
|
|
2106
|
+
table: string;
|
|
2107
|
+
requestId: string;
|
|
2108
|
+
rowCount: number;
|
|
2109
|
+
emittedRows: number;
|
|
2110
|
+
samplePks: Array<Record<string, unknown>>;
|
|
2111
|
+
sessionId?: string;
|
|
2112
|
+
now?: number;
|
|
2113
|
+
sessionStartedAt?: number | Date;
|
|
2114
|
+
}
|
|
2115
|
+
declare function buildDbReadEvent(input: BuildDbReadEventInput): BugEvent;
|
|
2116
|
+
declare function buildDbReadBulkEvent(input: BuildDbReadBulkEventInput): BugEvent;
|
|
2117
|
+
|
|
2118
|
+
/**
|
|
2119
|
+
* Options accepted by every `instrument*` adapter. Every field is engine-agnostic; the Postgres
|
|
2120
|
+
* shim keeps `InstrumentPgClientOptions` as a back-compat alias of this type.
|
|
2121
|
+
*/
|
|
2122
|
+
interface InstrumentDbClientOptions {
|
|
2123
|
+
/** Active request correlation id (equals the request's traceId). */
|
|
2124
|
+
requestId?: string;
|
|
2125
|
+
/** Lazily resolve the active request id (e.g. from AsyncLocalStorage); wins when `requestId` is absent. */
|
|
2126
|
+
getRequestId?: () => string | undefined;
|
|
2127
|
+
sessionId?: string;
|
|
2128
|
+
/** Sink for emitted `db.diff` events (e.g. forward to `sendBackendEvent`). */
|
|
2129
|
+
emit: (event: BugEvent) => void;
|
|
2130
|
+
/** When true, capture the pre-image of UPDATE rows via a SELECT-by-WHERE before mutating. */
|
|
2131
|
+
captureBefore?: boolean;
|
|
2132
|
+
/** When true, capture capped/redacted SELECT result rows as pre-state read evidence. Default off. */
|
|
2133
|
+
captureReads?: boolean;
|
|
2134
|
+
/** Extra sensitive column names dropped on top of the defaults. */
|
|
2135
|
+
redactColumns?: readonly string[];
|
|
2136
|
+
/** Primary-key columns per table; defaults to `['id']` for unlisted tables. */
|
|
2137
|
+
pkColumns?: Record<string, readonly string[]>;
|
|
2138
|
+
/** Maximum per-row `db.diff` events to emit for one statement before adding a bulk summary. */
|
|
2139
|
+
maxRowsPerStatement?: number;
|
|
2140
|
+
/** Maximum per-row `db.read` events to emit for one SELECT before adding a bulk summary. */
|
|
2141
|
+
maxReadRowsPerStatement?: number;
|
|
2142
|
+
/** Maximum per-row `db.read` events to emit for one request scope. */
|
|
2143
|
+
maxReadRowsPerRequest?: number;
|
|
2144
|
+
now?: () => number;
|
|
2145
|
+
sessionStartedAt?: number | Date;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
/**
|
|
2149
|
+
* Dialect-aware SQL statement parsing shared by every DB adapter. The parsers are deliberately
|
|
2150
|
+
* conservative: they recognize single-table INSERT/UPDATE/DELETE and simple SELECT-FROM shapes
|
|
2151
|
+
* across Postgres, MySQL, MSSQL, and SQLite quoting/keyword variants, and return `undefined` for
|
|
2152
|
+
* anything they do not understand so callers degrade to "no diff" rather than mis-instrument.
|
|
2153
|
+
*/
|
|
2154
|
+
interface ParsedMutation {
|
|
2155
|
+
op: DbDiffOp;
|
|
2156
|
+
table: string;
|
|
2157
|
+
whereClause?: string;
|
|
2158
|
+
}
|
|
2159
|
+
interface ParsedRead {
|
|
2160
|
+
table: string;
|
|
2161
|
+
}
|
|
2162
|
+
/** Parses op + table (+ WHERE clause) from a SQL statement. Returns undefined for non-mutations. */
|
|
2163
|
+
declare function parseMutation(sql: string): ParsedMutation | undefined;
|
|
2164
|
+
/** Parses the first table from a simple SELECT statement. Returns undefined for non-reads. */
|
|
2165
|
+
declare function parseRead(sql: string): ParsedRead | undefined;
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* Minimal duck-typed view of a `pg` Client/Pool. We never import `pg` at module top-level — the
|
|
2169
|
+
* host injects its own client/pool, so `pg` stays an optional peer and tests use a fake client.
|
|
2170
|
+
*/
|
|
2171
|
+
interface DuckTypedPgQueryResult {
|
|
2172
|
+
rows?: unknown[];
|
|
2173
|
+
rowCount?: number | null;
|
|
2174
|
+
command?: string;
|
|
2175
|
+
}
|
|
2176
|
+
interface DuckTypedPgClient {
|
|
2177
|
+
query(text: unknown, params?: unknown): Promise<DuckTypedPgQueryResult>;
|
|
2178
|
+
}
|
|
2179
|
+
/** Back-compat alias: the Postgres shim shares the engine-agnostic option shape. */
|
|
2180
|
+
type InstrumentPgClientOptions = InstrumentDbClientOptions;
|
|
2181
|
+
/**
|
|
2182
|
+
* Wraps a duck-typed `pg` client/pool so INSERT/UPDATE/DELETE statements executed within a request
|
|
2183
|
+
* scope record a `db.diff` event (op, table, primary key, after-image; before-image behind
|
|
2184
|
+
* `captureBefore`). The shim appends `RETURNING *` when absent to read the after-image, and reads
|
|
2185
|
+
* the result rows otherwise. Only the promise-returning `query(text, params)` form is instrumented;
|
|
2186
|
+
* config-object and callback forms pass straight through. Engine is Postgres only; the builder is
|
|
2187
|
+
* driver-agnostic so other engines can slot in later.
|
|
2188
|
+
*
|
|
2189
|
+
* Limitations: trigger/cascade side effects and rows changed by other tables are not captured; the
|
|
2190
|
+
* pre-image SELECT for `captureBefore` reuses the statement's WHERE clause + params, so it supports
|
|
2191
|
+
* single-table UPDATEs (not CTEs, joins, or sub-selects).
|
|
2192
|
+
*/
|
|
2193
|
+
declare function instrumentPgClient<T extends DuckTypedPgClient>(client: T, options: InstrumentPgClientOptions): T;
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* Minimal duck-typed view of a `mysql2/promise` connection/pool. We never import `mysql2` at module
|
|
2197
|
+
* top-level — the host injects its own client, so `mysql2` stays an optional peer and tests use a
|
|
2198
|
+
* fake client. Both `query` and `execute` resolve `[rowsOrResultHeader, fields]`.
|
|
2199
|
+
*/
|
|
2200
|
+
interface DuckTypedMysqlClient {
|
|
2201
|
+
query(sql: unknown, values?: unknown): Promise<unknown>;
|
|
2202
|
+
execute?(sql: unknown, values?: unknown): Promise<unknown>;
|
|
2203
|
+
}
|
|
2204
|
+
/** The mysql2 result-set header duck shape for a mutation (INSERT/UPDATE/DELETE). */
|
|
2205
|
+
interface DuckTypedMysqlResultHeader {
|
|
2206
|
+
affectedRows: number;
|
|
2207
|
+
insertId?: number;
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Wraps a duck-typed `mysql2/promise` client/pool so INSERT/UPDATE/DELETE statements executed within
|
|
2211
|
+
* a request scope record a `db.diff` event (op, table, primary key, after-image; before-image behind
|
|
2212
|
+
* `captureBefore`). MySQL has no `RETURNING`, so images come from best-effort extra SELECTs:
|
|
2213
|
+
*
|
|
2214
|
+
* - INSERT: a single-row auto-increment insert (`affectedRows === 1`, positive `insertId`, single-column
|
|
2215
|
+
* pk) is re-read by `insertId`; anything else (multi-row, missing insertId, composite pk) degrades to
|
|
2216
|
+
* an image-less `db.diff` carrying `rowCount`.
|
|
2217
|
+
* - UPDATE: a pre-SELECT (WHERE + trailing params) captures pks (and before-images when `captureBefore`),
|
|
2218
|
+
* then a post-SELECT by those pks captures after-images.
|
|
2219
|
+
* - DELETE: a pre-SELECT (WHERE + trailing params) captures the before-images.
|
|
2220
|
+
*
|
|
2221
|
+
* Both the promise-returning `query(sql, values)` and `execute(sql, values)` forms are instrumented
|
|
2222
|
+
* (identically); non-string sql / config-object forms pass straight through. The host statement is
|
|
2223
|
+
* never altered and runs exactly once — every capture SELECT and emit is wrapped so instrumentation
|
|
2224
|
+
* can never fail, double-execute, or alter the host query; the host result is returned unchanged.
|
|
2225
|
+
*
|
|
2226
|
+
* Limitations: trigger/cascade side effects and rows changed by other tables are not captured; the
|
|
2227
|
+
* WHERE-based pre-image SELECT supports single-table UPDATE/DELETE (not CTEs, joins, or sub-selects).
|
|
2228
|
+
*/
|
|
2229
|
+
declare function instrumentMysqlClient<T extends DuckTypedMysqlClient>(client: T, options: InstrumentDbClientOptions): T;
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* Minimal duck-typed view of an `mssql`-package result. We never import `mssql` at module top level —
|
|
2233
|
+
* the host injects its own pool, so `mssql` stays an optional peer and tests use a fake pool. Only the
|
|
2234
|
+
* fields the shim reads are typed; every other property (`output`, `returnValue`, …) rides through the
|
|
2235
|
+
* host-visible result untouched.
|
|
2236
|
+
*/
|
|
2237
|
+
interface DuckTypedMssqlResult {
|
|
2238
|
+
recordset?: unknown[];
|
|
2239
|
+
recordsets?: unknown[][];
|
|
2240
|
+
rowsAffected?: number[];
|
|
2241
|
+
[key: string]: unknown;
|
|
2242
|
+
}
|
|
2243
|
+
/** Duck-typed view of an `mssql` Request (`pool.request()`), recording `input()` and running `query()`. */
|
|
2244
|
+
interface DuckTypedMssqlRequest {
|
|
2245
|
+
input(name: string, ...rest: unknown[]): unknown;
|
|
2246
|
+
query(text: unknown): Promise<DuckTypedMssqlResult>;
|
|
2247
|
+
[key: string]: unknown;
|
|
2248
|
+
}
|
|
2249
|
+
/** Duck-typed view of an `mssql` ConnectionPool. `query` (the convenience single-string form) is optional. */
|
|
2250
|
+
interface DuckTypedMssqlPool {
|
|
2251
|
+
request(): DuckTypedMssqlRequest;
|
|
2252
|
+
query?(text: unknown, ...rest: unknown[]): Promise<DuckTypedMssqlResult>;
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Wraps a duck-typed `mssql` pool so INSERT/UPDATE/DELETE statements executed within a request scope
|
|
2256
|
+
* record a `db.diff` event (op, table, primary key, after-image; before-image behind `captureBefore`).
|
|
2257
|
+
* After-images are read by injecting an `OUTPUT INSERTED.*` / `OUTPUT DELETED.*` clause into the
|
|
2258
|
+
* statement (no extra round trip); the injected rows are consumed for diffs and then stripped from the
|
|
2259
|
+
* host-visible result, so the caller sees exactly what the un-instrumented statement would have
|
|
2260
|
+
* returned. SELECTs are optionally captured as read evidence (`captureReads`).
|
|
2261
|
+
*
|
|
2262
|
+
* Never-fail guarantees:
|
|
2263
|
+
* - Parse/correlation failures, missing request scope, unparseable SQL → the original statement runs
|
|
2264
|
+
* untouched.
|
|
2265
|
+
* - A pre-injection safety gate (`scanStatementSafety`) refuses to edit any statement we cannot
|
|
2266
|
+
* confidently tokenize as a single statement — multi-statement batches, unterminated
|
|
2267
|
+
* strings/comments, and quote-bearing comments run the ORIGINAL untouched (image-less diff), so the
|
|
2268
|
+
* host never receives corrupt T-SQL or loses a caller recordset.
|
|
2269
|
+
* - The host mutation executes exactly once. The only re-run is the compile-class fallback (errors
|
|
2270
|
+
* 334 / 156 / 102), which fail at COMPILE time before any rows change, so re-running the ORIGINAL
|
|
2271
|
+
* text on a fresh request with replayed inputs is safe. Every other error propagates as-is.
|
|
2272
|
+
* - All capture/emit work is wrapped; a failure degrades to fewer/no diffs, never to a failed host
|
|
2273
|
+
* query. When a mutation clearly ran but no images are obtainable, one image-less `db.diff`
|
|
2274
|
+
* (`pk: null`, `rowCount`) still records the write.
|
|
2275
|
+
*/
|
|
2276
|
+
declare function instrumentMssqlPool<T extends DuckTypedMssqlPool>(pool: T, options: InstrumentDbClientOptions): T;
|
|
2277
|
+
|
|
2278
|
+
/**
|
|
2279
|
+
* Minimal duck-typed view of a better-sqlite3 / `node:sqlite` statement. The host injects the real
|
|
2280
|
+
* database, so the sqlite driver stays an optional peer and tests use a synchronous fake. `run`
|
|
2281
|
+
* returns the mutation summary (`changes` + `lastInsertRowid`); `all`/`get` read rows back.
|
|
2282
|
+
*/
|
|
2283
|
+
interface DuckTypedSqliteStatement {
|
|
2284
|
+
run(...params: unknown[]): unknown;
|
|
2285
|
+
all(...params: unknown[]): unknown;
|
|
2286
|
+
get(...params: unknown[]): unknown;
|
|
2287
|
+
[key: string]: unknown;
|
|
2288
|
+
}
|
|
2289
|
+
interface DuckTypedSqliteRunResult {
|
|
2290
|
+
changes: number;
|
|
2291
|
+
lastInsertRowid: number | bigint;
|
|
2292
|
+
}
|
|
2293
|
+
/** Minimal duck-typed view of a better-sqlite3 / `node:sqlite` database: `prepare(sql)` → statement. */
|
|
2294
|
+
interface DuckTypedSqliteDatabase {
|
|
2295
|
+
prepare(sql: unknown, ...rest: unknown[]): DuckTypedSqliteStatement;
|
|
2296
|
+
[key: string]: unknown;
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Wraps a duck-typed better-sqlite3 / `node:sqlite` database so INSERT/UPDATE/DELETE statements
|
|
2300
|
+
* executed within a request scope record a `db.diff` event (op, table, primary key, after-image;
|
|
2301
|
+
* before-image behind `captureBefore`), and SELECTs optionally record `db.read` evidence. Everything
|
|
2302
|
+
* is fully synchronous — the sync driver's `run`/`all` return values are observed inline, so events
|
|
2303
|
+
* are visible the instant the host call returns.
|
|
2304
|
+
*
|
|
2305
|
+
* The database is wrapped with a Proxy that intercepts `prepare`; the returned statement is itself
|
|
2306
|
+
* proxied so `run` (mutations) and `all` (reads, when `captureReads`) are instrumented and every
|
|
2307
|
+
* other member (`get`, `iterate`, `pluck`, …) binds straight through. Non-string or unparseable SQL
|
|
2308
|
+
* yields the real statement untouched.
|
|
2309
|
+
*
|
|
2310
|
+
* After-images come from post-SELECTs (INSERT by `rowid`, UPDATE by the pks captured in a pre-SELECT)
|
|
2311
|
+
* — the host's SQL text is never rewritten, so `run()` returns exactly what the real driver produced.
|
|
2312
|
+
* Every capture step is wrapped: a failing pre/post-SELECT, missing rowid, WITHOUT-ROWID table, or
|
|
2313
|
+
* emit error degrades to fewer/no images (image-less fallback carrying `rowCount`), never a failed
|
|
2314
|
+
* host query.
|
|
2315
|
+
*
|
|
2316
|
+
* Limitations: trigger/cascade side effects and rows changed via other tables are not captured; the
|
|
2317
|
+
* pre-image SELECT reuses the statement's WHERE clause + params, so it supports single-table
|
|
2318
|
+
* UPDATE/DELETE (not CTEs, joins, or sub-selects).
|
|
2319
|
+
*/
|
|
2320
|
+
declare function instrumentSqliteDatabase<T extends DuckTypedSqliteDatabase>(db: T, options: InstrumentDbClientOptions): T;
|
|
2321
|
+
|
|
2322
|
+
interface DbRequestContext {
|
|
2323
|
+
requestId: string;
|
|
2324
|
+
sessionId?: string;
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Resolves the `db.diff` request scope from the SAME inputs `backend.req.*` events use — the
|
|
2328
|
+
* `X-Crumbtrail-Request-Id` / `X-Crumbtrail-Session-Id` headers (where the request id already equals
|
|
2329
|
+
* the W3C trace id) or explicit options. This guarantees a `db.diff` produced inside a request
|
|
2330
|
+
* shares the request's requestId, instead of inventing a parallel correlation scheme.
|
|
2331
|
+
*/
|
|
2332
|
+
declare function resolveDbRequestContext(input: BackendRequestEventInput): DbRequestContext;
|
|
2333
|
+
|
|
2334
|
+
/**
|
|
2335
|
+
* Concise, hot-plane-only summary of a finalized session. `crumbtrail-server inspect <session>` reads
|
|
2336
|
+
* the session manifest (manifest.json) when present and otherwise falls back to index.json. It
|
|
2337
|
+
* never reads raw events.ndjson — counts come from the pre-computed index/manifest.
|
|
2338
|
+
*/
|
|
2339
|
+
interface SessionInspectionArtifact {
|
|
2340
|
+
name: string;
|
|
2341
|
+
bytes: number;
|
|
2342
|
+
}
|
|
2343
|
+
interface SessionInspection {
|
|
2344
|
+
id: string;
|
|
2345
|
+
durationMs: number;
|
|
2346
|
+
eventCount: number;
|
|
2347
|
+
errorCount: number;
|
|
2348
|
+
failedRequestCount: number;
|
|
2349
|
+
candidateCount: number;
|
|
2350
|
+
truncated: boolean;
|
|
2351
|
+
/** Which hot-plane artifact the summary was primarily derived from. */
|
|
2352
|
+
source: "manifest" | "index";
|
|
2353
|
+
/**
|
|
2354
|
+
* Earliest error-class evidence timestamp stamped into llm.json by bundle build.
|
|
2355
|
+
* Omitted when the bundle carries no latency self-measurement (or llm.json is absent).
|
|
2356
|
+
*/
|
|
2357
|
+
firstErrorEventAt?: number;
|
|
2358
|
+
/** Self-measured detect-to-bundle latency (ms) stamped into llm.json. Omitted when absent. */
|
|
2359
|
+
detectToBundleMs?: number;
|
|
2360
|
+
artifacts: SessionInspectionArtifact[];
|
|
2361
|
+
}
|
|
2362
|
+
interface InspectSessionOptions {
|
|
2363
|
+
/** Base sessions directory used to resolve a bare session id to a directory. */
|
|
2364
|
+
outputDir?: string;
|
|
2365
|
+
}
|
|
2366
|
+
declare class InspectError extends Error {
|
|
2367
|
+
readonly code: "session-not-found";
|
|
2368
|
+
constructor(code: "session-not-found", message: string);
|
|
2369
|
+
}
|
|
2370
|
+
declare function inspectSession(sessionDirOrId: string, opts?: InspectSessionOptions): SessionInspection;
|
|
2371
|
+
declare function formatInspection(inspection: SessionInspection): string;
|
|
2372
|
+
|
|
2373
|
+
/**
|
|
2374
|
+
* Resolve the crumbtrail-node package version by walking up from this module to the nearest
|
|
2375
|
+
* package.json named `crumbtrail-node`. The same layout holds in dev/test (src/version.ts →
|
|
2376
|
+
* ../package.json) and in the published build (dist/cli.cjs → ../package.json), so no
|
|
2377
|
+
* build-time baking is required.
|
|
2378
|
+
*/
|
|
2379
|
+
declare function readPackageVersion(): string;
|
|
2380
|
+
|
|
2381
|
+
type ProviderId = "datadog" | "otel" | "sentry" | "grafana" | "splunk";
|
|
2382
|
+
interface ProviderRecipe {
|
|
2383
|
+
id: ProviderId;
|
|
2384
|
+
title: string;
|
|
2385
|
+
docFile: string;
|
|
2386
|
+
description: string;
|
|
2387
|
+
config: string;
|
|
2388
|
+
notes: string[];
|
|
2389
|
+
}
|
|
2390
|
+
declare const PROVIDER_RECIPES: ProviderRecipe[];
|
|
2391
|
+
declare const PROVIDER_IDS: ProviderId[];
|
|
2392
|
+
declare function isProviderId(value: string | undefined): value is ProviderId;
|
|
2393
|
+
declare function getProviderRecipe(provider: ProviderId): ProviderRecipe;
|
|
2394
|
+
declare function renderProviderConfig(provider: ProviderId, endpoint?: string): string;
|
|
2395
|
+
declare function renderProviderCliOutput(provider: ProviderId, endpoint?: string): string;
|
|
2396
|
+
declare function renderProviderDoc(provider: ProviderId, endpoint?: string): string;
|
|
2397
|
+
declare function renderProviderReadme(): string;
|
|
2398
|
+
|
|
2399
|
+
type BackendIntakeWarningKind = "missing-session" | "missing-fetch" | "fetch-rejected" | "http-error" | "malformed-response";
|
|
2400
|
+
interface BackendIntakeWarning {
|
|
2401
|
+
kind: BackendIntakeWarningKind;
|
|
2402
|
+
message: string;
|
|
2403
|
+
status?: number;
|
|
2404
|
+
sessionId?: string;
|
|
2405
|
+
requestId?: string;
|
|
2406
|
+
eventKind?: string;
|
|
2407
|
+
}
|
|
2408
|
+
type FetchLike$1 = (input: string | URL, init?: FetchInitLike) => Promise<ResponseLike>;
|
|
2409
|
+
type HeadersInitLike = Record<string, string>;
|
|
2410
|
+
interface FetchInitLike {
|
|
2411
|
+
method?: string;
|
|
2412
|
+
headers?: HeadersInitLike;
|
|
2413
|
+
body?: string;
|
|
2414
|
+
signal?: AbortSignal;
|
|
2415
|
+
}
|
|
2416
|
+
interface ResponseLike {
|
|
2417
|
+
ok: boolean;
|
|
2418
|
+
status: number;
|
|
2419
|
+
text?: () => Promise<string>;
|
|
2420
|
+
json?: () => Promise<unknown>;
|
|
2421
|
+
}
|
|
2422
|
+
interface SendBackendEventOptions {
|
|
2423
|
+
event: BugEvent;
|
|
2424
|
+
sessionId?: string;
|
|
2425
|
+
endpoint?: string;
|
|
2426
|
+
authToken?: string;
|
|
2427
|
+
fetch?: FetchLike$1;
|
|
2428
|
+
signal?: AbortSignal;
|
|
2429
|
+
onWarning?: (warning: BackendIntakeWarning) => void;
|
|
2430
|
+
}
|
|
2431
|
+
declare function sendBackendEvent(options: SendBackendEventOptions): Promise<void>;
|
|
2432
|
+
|
|
2433
|
+
type CrumbtrailExpressNext = (error?: unknown) => void;
|
|
2434
|
+
type CrumbtrailExpressErrorNext = (error: unknown) => void;
|
|
2435
|
+
interface CrumbtrailExpressRequest {
|
|
2436
|
+
method?: string;
|
|
2437
|
+
url?: string;
|
|
2438
|
+
originalUrl?: string;
|
|
2439
|
+
path?: string;
|
|
2440
|
+
route?: string | {
|
|
2441
|
+
path?: unknown;
|
|
2442
|
+
};
|
|
2443
|
+
headers?: BackendRequestHeaders;
|
|
2444
|
+
}
|
|
2445
|
+
interface CrumbtrailExpressResponse {
|
|
2446
|
+
statusCode?: number;
|
|
2447
|
+
once?: (event: "finish", listener: () => void) => unknown;
|
|
2448
|
+
}
|
|
2449
|
+
type CrumbtrailExpressMiddleware = (req: CrumbtrailExpressRequest, res: CrumbtrailExpressResponse, next: CrumbtrailExpressNext) => void;
|
|
2450
|
+
type CrumbtrailExpressErrorMiddleware = (error: unknown, req: CrumbtrailExpressRequest, res: CrumbtrailExpressResponse, next: CrumbtrailExpressErrorNext) => void;
|
|
2451
|
+
type RequestValueResolver = string | undefined | ((req: CrumbtrailExpressRequest) => string | undefined);
|
|
2452
|
+
type SessionStartedAtResolver = number | Date | undefined | ((req: CrumbtrailExpressRequest) => number | Date | undefined);
|
|
2453
|
+
type NowResolver = () => number;
|
|
2454
|
+
type FetchLike = Parameters<typeof sendBackendEvent>[0]["fetch"];
|
|
2455
|
+
interface CrumbtrailExpressOptions {
|
|
2456
|
+
sessionId?: RequestValueResolver;
|
|
2457
|
+
requestId?: RequestValueResolver;
|
|
2458
|
+
endpoint?: string;
|
|
2459
|
+
authToken?: string;
|
|
2460
|
+
fetch?: FetchLike;
|
|
2461
|
+
signal?: AbortSignal;
|
|
2462
|
+
sessionStartedAt?: SessionStartedAtResolver;
|
|
2463
|
+
now?: NowResolver;
|
|
2464
|
+
onWarning?: (warning: BackendIntakeWarning) => void;
|
|
2465
|
+
}
|
|
2466
|
+
declare function createCrumbtrailExpressMiddleware(options?: CrumbtrailExpressOptions): CrumbtrailExpressMiddleware;
|
|
2467
|
+
declare function createCrumbtrailExpressErrorMiddleware(options?: CrumbtrailExpressOptions): CrumbtrailExpressErrorMiddleware;
|
|
2468
|
+
|
|
2469
|
+
interface HeadlessSessionOptions {
|
|
2470
|
+
endpoint: string;
|
|
2471
|
+
sessionId: string;
|
|
2472
|
+
metadata?: Record<string, unknown>;
|
|
2473
|
+
authToken?: string;
|
|
2474
|
+
fetchImpl?: typeof fetch;
|
|
2475
|
+
}
|
|
2476
|
+
interface HeadlessSession {
|
|
2477
|
+
sessionId: string;
|
|
2478
|
+
record(events: BugEvent | BugEvent[]): Promise<void>;
|
|
2479
|
+
end(): Promise<Record<string, unknown>>;
|
|
2480
|
+
}
|
|
2481
|
+
declare function startHeadlessSession(options: HeadlessSessionOptions): Promise<HeadlessSession>;
|
|
2482
|
+
|
|
2483
|
+
declare const SESSION_COMPARE_SCHEMA_VERSION: "session-compare.v1";
|
|
2484
|
+
type ComparisonVerdict = "regression" | "clean";
|
|
2485
|
+
type ComparisonConfidence = "high" | "medium" | "low";
|
|
2486
|
+
interface SessionComparison {
|
|
2487
|
+
schemaVersion: typeof SESSION_COMPARE_SCHEMA_VERSION;
|
|
2488
|
+
verdict: ComparisonVerdict;
|
|
2489
|
+
confidence: ComparisonConfidence;
|
|
2490
|
+
a: SessionRef;
|
|
2491
|
+
b: SessionRef;
|
|
2492
|
+
alignment: {
|
|
2493
|
+
matchedSteps: number;
|
|
2494
|
+
unmatchedA: number;
|
|
2495
|
+
unmatchedB: number;
|
|
2496
|
+
};
|
|
2497
|
+
divergences: Divergence[];
|
|
2498
|
+
noise: {
|
|
2499
|
+
suppressedCount: number;
|
|
2500
|
+
rules: string[];
|
|
2501
|
+
};
|
|
2502
|
+
/** Neutral, complete projection of divergences. Consumed by cloud fusion. */
|
|
2503
|
+
evidence: EvidenceItem[];
|
|
2504
|
+
/** Intent inference. Empty until the git-host connector (slice 2) lands. */
|
|
2505
|
+
intent: IntentSignal[];
|
|
2506
|
+
}
|
|
2507
|
+
interface SessionRef {
|
|
2508
|
+
sessionId: string;
|
|
2509
|
+
release?: string;
|
|
2510
|
+
build?: string;
|
|
2511
|
+
}
|
|
2512
|
+
interface Divergence {
|
|
2513
|
+
plane: "flow" | "network" | "db" | "env";
|
|
2514
|
+
kind: string;
|
|
2515
|
+
sig?: string;
|
|
2516
|
+
requestId?: string;
|
|
2517
|
+
table?: string;
|
|
2518
|
+
pk?: Record<string, unknown>;
|
|
2519
|
+
before: unknown;
|
|
2520
|
+
after: unknown;
|
|
2521
|
+
brief: string;
|
|
2522
|
+
}
|
|
2523
|
+
interface CompareOptions {
|
|
2524
|
+
alignmentWindow?: number;
|
|
2525
|
+
disableNoiseRules?: string[];
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
declare class CompareError extends Error {
|
|
2529
|
+
constructor(message: string);
|
|
2530
|
+
}
|
|
2531
|
+
declare function compareSessions(aDir: string, bDir: string, options?: CompareOptions): Promise<SessionComparison>;
|
|
2532
|
+
|
|
2533
|
+
declare function renderCompareReport(comparison: SessionComparison): string;
|
|
2534
|
+
declare function formatComparisonSummary(comparison: SessionComparison): string;
|
|
2535
|
+
|
|
2536
|
+
declare const REGRESSION_CONTEXT_SCHEMA_VERSION: "regression-context.v1";
|
|
2537
|
+
interface RegressionContext {
|
|
2538
|
+
schemaVersion: typeof REGRESSION_CONTEXT_SCHEMA_VERSION;
|
|
2539
|
+
comparison: SessionComparison;
|
|
2540
|
+
divergent_interaction: {
|
|
2541
|
+
sig: string;
|
|
2542
|
+
label: string;
|
|
2543
|
+
path: string;
|
|
2544
|
+
} | null;
|
|
2545
|
+
causal_window: {
|
|
2546
|
+
requestIds: string[];
|
|
2547
|
+
t0: number;
|
|
2548
|
+
t1: number;
|
|
2549
|
+
hints: string[];
|
|
2550
|
+
} | null;
|
|
2551
|
+
db_rows: Array<{
|
|
2552
|
+
table: string;
|
|
2553
|
+
pk: Record<string, unknown>;
|
|
2554
|
+
before: unknown;
|
|
2555
|
+
after: unknown;
|
|
2556
|
+
}>;
|
|
2557
|
+
repro_hint: string;
|
|
2558
|
+
}
|
|
2559
|
+
declare function buildRegressionContext(comparison: SessionComparison, bDir: string): RegressionContext;
|
|
2560
|
+
|
|
2561
|
+
/** The subset of the locate envelope this builder reads. `outcome` is the only
|
|
2562
|
+
* signal that flips the comment shape; `confidence` is display-only. */
|
|
2563
|
+
interface AdvisoryCommentMatch {
|
|
2564
|
+
outcome: "matched" | "inconclusive";
|
|
2565
|
+
confidence: number;
|
|
2566
|
+
reasons?: string[];
|
|
2567
|
+
}
|
|
2568
|
+
/** One evidence gap surfaced from the bundle (mirrors core's EvidenceGap). */
|
|
2569
|
+
interface AdvisoryCommentGap {
|
|
2570
|
+
lane?: string;
|
|
2571
|
+
reason: string;
|
|
2572
|
+
suggestion?: string;
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Correlation keys carried INTO the ticket so the reader can line the matched
|
|
2576
|
+
* incident up against their own logs/traces. Every value here must come from the
|
|
2577
|
+
* located evidence — NEVER fabricated. Rendered only in the matched variant.
|
|
2578
|
+
*/
|
|
2579
|
+
interface AdvisoryCommentCorrelation {
|
|
2580
|
+
/** The located session id (match.sessionId). */
|
|
2581
|
+
sessionId?: string;
|
|
2582
|
+
/** Distinct request/trace ids pulled from the bundle's evidence refs. */
|
|
2583
|
+
requestIds?: string[];
|
|
2584
|
+
}
|
|
2585
|
+
interface BuildAdvisoryCommentInput {
|
|
2586
|
+
match: AdvisoryCommentMatch;
|
|
2587
|
+
/** Public link to the persisted bundle (`/api/bundles/:id`). Always rendered. */
|
|
2588
|
+
bundleUrl: string;
|
|
2589
|
+
gaps?: AdvisoryCommentGap[];
|
|
2590
|
+
/** Correlation keys from the located evidence (matched variant only). */
|
|
2591
|
+
correlation?: AdvisoryCommentCorrelation;
|
|
2592
|
+
}
|
|
2593
|
+
/** A minimally-typed ADF node. ADF is an open tree; we only build the handful of
|
|
2594
|
+
* node kinds we need (doc/paragraph/text/link/bulletList/listItem). */
|
|
2595
|
+
interface AdfNode {
|
|
2596
|
+
type: string;
|
|
2597
|
+
[key: string]: unknown;
|
|
2598
|
+
}
|
|
2599
|
+
interface AdfDoc {
|
|
2600
|
+
version: 1;
|
|
2601
|
+
type: "doc";
|
|
2602
|
+
content: AdfNode[];
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Build the advisory ADF comment. Two shapes, chosen by `match.outcome`:
|
|
2606
|
+
*
|
|
2607
|
+
* - matched: names that a candidate incident was located, shows the rounded
|
|
2608
|
+
* confidence as an advisory percentage, lists the match reasons in plain
|
|
2609
|
+
* language (if any), carries the correlation keys (located session + up to a
|
|
2610
|
+
* few request/trace ids, when the evidence has them) onto the ticket, and
|
|
2611
|
+
* links the full evidence bundle.
|
|
2612
|
+
* - inconclusive: states honestly that no recorded incident matched, lists the
|
|
2613
|
+
* evidence gaps (if any) so the reader knows what is missing, and still links
|
|
2614
|
+
* the (empty) bundle. It fabricates no match and reports no percentage.
|
|
2615
|
+
*
|
|
2616
|
+
* Pure and side-effect free — unit-testable in isolation.
|
|
2617
|
+
*/
|
|
2618
|
+
declare function buildAdvisoryComment(input: BuildAdvisoryCommentInput): AdfDoc;
|
|
2619
|
+
|
|
2620
|
+
declare const REPLAY_RESULT_SCHEMA_VERSION: "replay-result.v1";
|
|
2621
|
+
type StepResolution = 'exact' | 'role-label' | 'structural' | 'failed';
|
|
2622
|
+
interface ReplayStepResult {
|
|
2623
|
+
index: number;
|
|
2624
|
+
sig: string;
|
|
2625
|
+
action: string;
|
|
2626
|
+
resolution: StepResolution;
|
|
2627
|
+
durationMs: number;
|
|
2628
|
+
}
|
|
2629
|
+
interface ReplayDivergence {
|
|
2630
|
+
index: number;
|
|
2631
|
+
sig: string;
|
|
2632
|
+
reason: string;
|
|
2633
|
+
}
|
|
2634
|
+
interface ReplayResult {
|
|
2635
|
+
schemaVersion: typeof REPLAY_RESULT_SCHEMA_VERSION;
|
|
2636
|
+
sourceSessionId: string;
|
|
2637
|
+
actuatedSessionId: string;
|
|
2638
|
+
steps: ReplayStepResult[];
|
|
2639
|
+
divergences: ReplayDivergence[];
|
|
2640
|
+
completed: boolean;
|
|
2641
|
+
}
|
|
2642
|
+
declare function buildReplayResult(input: {
|
|
2643
|
+
sourceSessionId: string;
|
|
2644
|
+
actuatedSessionId: string;
|
|
2645
|
+
steps: ReplayStepResult[];
|
|
2646
|
+
divergences: ReplayDivergence[];
|
|
2647
|
+
completed: boolean;
|
|
2648
|
+
}): ReplayResult;
|
|
2649
|
+
declare function parseReplayResult(raw: unknown): ReplayResult;
|
|
2650
|
+
declare function writeReplayResult(filePath: string, result: ReplayResult): void;
|
|
2651
|
+
|
|
2652
|
+
/**
|
|
2653
|
+
* Canonical event kind emitted for an auto-captured backend error (crash or
|
|
2654
|
+
* console.error). It is deliberately NOT `backend.req.error` (that kind is
|
|
2655
|
+
* request-scoped and joins on a requestId this hook never has) and NOT
|
|
2656
|
+
* `backend.error` (that literal is a causal-graph NODE kind, not an event kind —
|
|
2657
|
+
* reusing it would collide). This request-less kind carries only the error and
|
|
2658
|
+
* the hook that surfaced it.
|
|
2659
|
+
*
|
|
2660
|
+
* Downstream wiring (all requestId-free, so every site fits): causal-graph
|
|
2661
|
+
* `nodeKindFor` maps it onto the `backend.error` node kind, post-process
|
|
2662
|
+
* `FULL_STACK_BACKEND_KINDS` + `mergeBackendEvent` summarize its error, and
|
|
2663
|
+
* evidence-index surfaces it as a `backend_request_error` candidate + error
|
|
2664
|
+
* moment — mirroring `backend.req.error` at each site.
|
|
2665
|
+
*/
|
|
2666
|
+
declare const AUTO_CAPTURE_ERROR_EVENT = "backend.uncaught";
|
|
2667
|
+
/** Hooks a crash/console capture handler can surface an error from. */
|
|
2668
|
+
type AutoCaptureSource = "uncaughtException" | "unhandledRejection" | "console.error";
|
|
2669
|
+
interface AutoCaptureOptions {
|
|
2670
|
+
/** Ingest endpoint (baked into the injected snippet by the CLI). */
|
|
2671
|
+
endpoint: string;
|
|
2672
|
+
/**
|
|
2673
|
+
* Ingest key. Defaults to `process.env.CRUMBTRAIL_KEY`, which is populated from
|
|
2674
|
+
* the project's `.env` by `autoCapture` itself (see `loadEnv`).
|
|
2675
|
+
*/
|
|
2676
|
+
authToken?: string;
|
|
2677
|
+
/** Explicit session id; a stable auto-generated one is used when omitted. */
|
|
2678
|
+
sessionId?: string;
|
|
2679
|
+
/** Extra session metadata merged into the headless session start. */
|
|
2680
|
+
metadata?: Record<string, unknown>;
|
|
2681
|
+
/** Injectable fetch (tests); forwarded to `startHeadlessSession`. */
|
|
2682
|
+
fetchImpl?: typeof fetch;
|
|
2683
|
+
/**
|
|
2684
|
+
* When true (default) attempt `process.loadEnvFile()` so the key in `.env`
|
|
2685
|
+
* lands in `process.env` before the session starts. Guarded: a no-op when the
|
|
2686
|
+
* API is unavailable (<20.12) or the `.env` file is missing/unreadable.
|
|
2687
|
+
*/
|
|
2688
|
+
loadEnv?: boolean;
|
|
2689
|
+
/** Console object to patch (tests). Defaults to the global `console`. */
|
|
2690
|
+
consoleImpl?: Pick<Console, "error">;
|
|
2691
|
+
/** Process to hook (tests). Defaults to the global `process`. */
|
|
2692
|
+
processImpl?: NodeJS.Process;
|
|
2693
|
+
/**
|
|
2694
|
+
* Called after a best-effort record on an unrecoverable crash
|
|
2695
|
+
* (`uncaughtException` / `unhandledRejection`) IN PLACE of `process.exit`.
|
|
2696
|
+
* Tests inject this to assert crash semantics are preserved without killing
|
|
2697
|
+
* the runner. Defaults to `process.exit`.
|
|
2698
|
+
*/
|
|
2699
|
+
onCrashExit?: (code: number) => void;
|
|
2700
|
+
}
|
|
2701
|
+
interface AutoCaptureHandle {
|
|
2702
|
+
/** The started session id, when the session start succeeded. */
|
|
2703
|
+
sessionId?: string;
|
|
2704
|
+
/** Restore the original console.error and remove the process hooks. */
|
|
2705
|
+
stop(): void;
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Install best-effort backend crash + console.error capture and start a headless
|
|
2709
|
+
* ingest session. Returns a handle whose `stop()` restores every hook.
|
|
2710
|
+
*
|
|
2711
|
+
* Crash semantics are preserved: on `uncaughtException` (and a suppressed
|
|
2712
|
+
* `unhandledRejection`) we best-effort record the error, bound-flush it (race the
|
|
2713
|
+
* record against a hard ~150ms ceiling so the crash event can actually reach
|
|
2714
|
+
* ingest before the process dies), then exit non-zero — the bounded flush can
|
|
2715
|
+
* never hang, and capture never converts a crash into survival.
|
|
2716
|
+
*/
|
|
2717
|
+
declare function autoCapture(options: AutoCaptureOptions): Promise<AutoCaptureHandle>;
|
|
2718
|
+
|
|
2719
|
+
interface OtlpAnyValue {
|
|
2720
|
+
stringValue?: string;
|
|
2721
|
+
boolValue?: boolean;
|
|
2722
|
+
intValue?: string | number;
|
|
2723
|
+
doubleValue?: number;
|
|
2724
|
+
arrayValue?: {
|
|
2725
|
+
values?: OtlpAnyValue[];
|
|
2726
|
+
};
|
|
2727
|
+
kvlistValue?: {
|
|
2728
|
+
values?: OtlpKeyValue[];
|
|
2729
|
+
};
|
|
2730
|
+
bytesValue?: string;
|
|
2731
|
+
}
|
|
2732
|
+
interface OtlpKeyValue {
|
|
2733
|
+
key?: string;
|
|
2734
|
+
value?: OtlpAnyValue;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
interface OtlpSpan {
|
|
2738
|
+
traceId?: string;
|
|
2739
|
+
spanId?: string;
|
|
2740
|
+
parentSpanId?: string;
|
|
2741
|
+
name?: string;
|
|
2742
|
+
kind?: number;
|
|
2743
|
+
startTimeUnixNano?: string | number;
|
|
2744
|
+
endTimeUnixNano?: string | number;
|
|
2745
|
+
attributes?: OtlpKeyValue[];
|
|
2746
|
+
status?: {
|
|
2747
|
+
code?: number;
|
|
2748
|
+
message?: string;
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
interface OtlpScopeSpans {
|
|
2752
|
+
scope?: {
|
|
2753
|
+
name?: string;
|
|
2754
|
+
version?: string;
|
|
2755
|
+
};
|
|
2756
|
+
spans?: OtlpSpan[];
|
|
2757
|
+
}
|
|
2758
|
+
interface OtlpResourceSpans {
|
|
2759
|
+
resource?: {
|
|
2760
|
+
attributes?: OtlpKeyValue[];
|
|
2761
|
+
};
|
|
2762
|
+
scopeSpans?: OtlpScopeSpans[];
|
|
2763
|
+
}
|
|
2764
|
+
interface OtlpTraceRequest {
|
|
2765
|
+
resourceSpans?: OtlpResourceSpans[];
|
|
2766
|
+
}
|
|
2767
|
+
interface OtlpLogRecord {
|
|
2768
|
+
timeUnixNano?: string | number;
|
|
2769
|
+
observedTimeUnixNano?: string | number;
|
|
2770
|
+
severityText?: string;
|
|
2771
|
+
severityNumber?: number;
|
|
2772
|
+
body?: {
|
|
2773
|
+
stringValue?: string;
|
|
2774
|
+
};
|
|
2775
|
+
traceId?: string;
|
|
2776
|
+
spanId?: string;
|
|
2777
|
+
attributes?: OtlpKeyValue[];
|
|
2778
|
+
}
|
|
2779
|
+
interface OtlpScopeLogs {
|
|
2780
|
+
scope?: {
|
|
2781
|
+
name?: string;
|
|
2782
|
+
};
|
|
2783
|
+
logRecords?: OtlpLogRecord[];
|
|
2784
|
+
}
|
|
2785
|
+
interface OtlpResourceLogs {
|
|
2786
|
+
resource?: {
|
|
2787
|
+
attributes?: OtlpKeyValue[];
|
|
2788
|
+
};
|
|
2789
|
+
scopeLogs?: OtlpScopeLogs[];
|
|
2790
|
+
}
|
|
2791
|
+
interface OtlpLogsRequest {
|
|
2792
|
+
resourceLogs?: OtlpResourceLogs[];
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
declare function decodeOtlpTraceProtobuf(buffer: Uint8Array): OtlpTraceRequest;
|
|
2796
|
+
declare function decodeOtlpLogsProtobuf(buffer: Uint8Array): OtlpLogsRequest;
|
|
2797
|
+
|
|
2798
|
+
export { AUTO_CAPTURE_ERROR_EVENT, type AdapterEvidence, type AdapterSourceStats, type AdfDoc, type AdfNode, type AdvisoryCommentGap, type AdvisoryCommentMatch, type AutoCaptureHandle, type AutoCaptureOptions, type AutoCaptureSource, type BoundedRetryOptions, type BugQueueConfig, BugQueueManager, type BuildAdvisoryCommentInput, type BuildDbDiffEventInput, type BuildDbReadBulkEventInput, type BuildDbReadEventInput, type BuildFixContextOptions, CLOUDFLARE_AUTH_FIELDS, CLOUDFLARE_DESCRIPTOR, CLOUDFLARE_R2_ACCESS_KEY_ID_ENV, CLOUDFLARE_R2_ACCOUNT_ID_ENV, CLOUDFLARE_R2_BUCKET_ENV, CLOUDFLARE_R2_DATASET_ENV, CLOUDFLARE_R2_ENDPOINT_ENV, CLOUDFLARE_R2_PREFIX_ENV, CLOUDFLARE_R2_SECRET_ACCESS_KEY_ENV, CLOUDWATCH_ACCESS_KEY_ID_ENV, CLOUDWATCH_AUTH_FIELDS, CLOUDWATCH_DESCRIPTOR, CLOUDWATCH_ENDPOINT_ENV, CLOUDWATCH_LOG_GROUPS_ENV, CLOUDWATCH_REGION_ENV, CLOUDWATCH_SECRET_ACCESS_KEY_ENV, CLOUDWATCH_SESSION_TOKEN_ENV, CRUMBTRAIL_USER_AGENT, CloudWatchEvidenceSource, type CloudWatchQueryPlan, type CloudWatchResultRow, type CloudWatchSourceConfig, type CloudflareDataset, CloudflareEvidenceSource, type CloudflarePlan, type CloudflareSourceConfig, type CommentingTicketConnector, CompareError, type CompareOptions, type ComparisonConfidence, type ComparisonVerdict, type CrumbtrailExpressErrorMiddleware, type CrumbtrailExpressErrorNext, type CrumbtrailExpressMiddleware, type CrumbtrailExpressNext, type CrumbtrailExpressOptions, type CrumbtrailExpressRequest, type CrumbtrailExpressResponse, type BackendIntakeWarning as CrumbtrailExpressWarning, type BackendIntakeWarningKind as CrumbtrailExpressWarningKind, DATADOG_API_KEY_ENV, DATADOG_APP_KEY_ENV, DATADOG_AUTH_FIELDS, DATADOG_DEFAULT_SITE, DATADOG_DESCRIPTOR, DATADOG_SITE_ENV, DEFAULT_MAX_TOTAL_BYTES, DEFAULT_SENSITIVE_DB_COLUMNS, DEFAULT_SOURCE_TIMEOUT_MS, DEFAULT_SWEEP_CHECKPOINT_MS, DEFAULT_SWEEP_IDLE_MS, DEFAULT_SWEEP_INTERVAL_MS, DISTINCT_BUGS_SCHEMA_VERSION, DatadogEvidenceSource, type DatadogLog, type DatadogQueryPlan, type DatadogSourceConfig, type DatadogSpan, type DbRequestContext, type DistinctBug, type DistinctBugEvidenceRef, type DistinctBugRecurrence, type DistinctBugRecurrenceInput, type DistinctBugRecurrenceOccurrence, type DistinctBugSeverity, type Divergence, type DuckTypedMssqlPool, type DuckTypedMssqlRequest, type DuckTypedMssqlResult, type DuckTypedMysqlClient, type DuckTypedMysqlResultHeader, type DuckTypedPgClient, type DuckTypedPgQueryResult, type DuckTypedSqliteDatabase, type DuckTypedSqliteRunResult, type DuckTypedSqliteStatement, EVIDENCE_SOURCE_PROVIDERS, CRUMBTRAIL_USER_AGENT as EVIDENCE_SOURCE_USER_AGENT, type EvidenceSource, type EvidenceSourceProvider, FIX_CONTEXT_SCHEMA_VERSION, type FetchAdapterEvidenceOptions, FilesystemSessionStore, type FixContext, type FixContextDbDiff, type FixContextDbRead, FixContextError, type FixContextPrimaryWindow, type FixContextReproHint, type FixContextSession, type HeadlessSession, type HeadlessSessionOptions, InspectError, type InspectSessionOptions, type InstrumentDbClientOptions, type InstrumentPgClientOptions, type JiraAuth, type JiraBasicAuth, type JiraBearerAuth, JiraTicketClient, type JiraTicketClientConfig, type JiraTicketClientConfigLegacy, type JiraTicketClientConfigWithAuth, McpServer, type McpServerConfig, type OtlpLogsRequest, type OtlpResourceLogs, type OtlpResourceSpans, type OtlpTraceRequest, POSTHOG_API_KEY_ENV, POSTHOG_AUTH_FIELDS, POSTHOG_DEFAULT_HOST, POSTHOG_DESCRIPTOR, POSTHOG_HOST_ENV, POSTHOG_PROJECT_ID_ENV, PROVIDER_IDS, PROVIDER_RECIPES, type PostHogEvent, PostHogEvidenceSource, type PostHogPropertyFilter, type PostHogQueryPlan, type PostHogRecording, type PostHogSourceConfig, type ProviderId, type ProviderRecipe, REGRESSION_CONTEXT_SCHEMA_VERSION, REPLAY_RESULT_SCHEMA_VERSION, type RegressionContext, type ReplayDivergence, type ReplayResult, type ReplayStepResult, SENTRY_AUTH_FIELDS, SENTRY_AUTH_TOKEN_ENV, SENTRY_DEFAULT_HOST, SENTRY_DESCRIPTOR, SENTRY_HOST_ENV, SENTRY_ORG_ENV, SESSION_COMPARE_SCHEMA_VERSION, SPLUNK_AUTH_FIELDS, SPLUNK_DESCRIPTOR, SPLUNK_HOST_ENV, SPLUNK_INDEX_ENV, SPLUNK_TOKEN_ENV, SPLUNK_WEB_URL_ENV, SentryEvidenceSource, type SentryQueryPlan, type SentrySourceConfig, type BugReport as ServerBugReport, type ServerConfig, type SessionComparison, type SessionFileFlags, type SessionFinalizationResult, type SessionInspection, type SessionInspectionArtifact, type SessionListItem, SessionManager, type SessionStore, type SessionSummary, type SessionSweepOptions, type SessionSweepResult, type SessionSweeperHandle, type Severity, type SigV4Input, type SignedHeaders, type SourceHealth, SplunkEvidenceSource, type SplunkQueryPlan, type SplunkResultRow, type SplunkSourceConfig, type StepResolution, type TicketConnector, TicketError, type TicketProvider, autoCapture, buildAdvisoryComment, buildCloudWatchQuery, buildCloudflarePlan, buildDatadogQuery, buildDbDiffEvent, buildDbReadBulkEvent, buildDbReadEvent, buildDistinctBugSignature, buildFixContext, buildPostHogQuery, buildRegressionContext, buildReplayResult, buildSentryQuery, buildSessionSummary, buildSplunkQuery, cloudWatchDeepLink, cloudWatchEvidenceProvider, cloudflareEvidenceProvider, compareSessions, createCrumbtrailExpressErrorMiddleware, createCrumbtrailExpressMiddleware, createServer, datadogAppBase, datadogEvidenceProvider, decodeOtlpLogsProtobuf, decodeOtlpTraceProtobuf, defaultSessionStore, evidenceRequestHeaders, evidenceSourcesFromEnv, fetchAdapterEvidence, formatComparisonSummary, formatDuration, formatInspection, getProviderRecipe, groupDistinctBugRecurrences, groupDistinctBugs, inspectSession, instrumentMssqlPool, instrumentMysqlClient, instrumentPgClient, instrumentSqliteDatabase, isProviderId, jiraToSymptom, normalizeCloudWatchRow, normalizeDatadogLog, normalizeDatadogSpan, normalizePostHogEvent, normalizePostHogRecording, normalizeSentryIssue, normalizeSplunkRow, parseMutation, parseRead, parseReplayResult, posthogEventDeepLink, posthogEvidenceProvider, posthogRecordingDeepLink, readPackageVersion, redactEvidenceGap, redactEvidenceItem, redactSourceResult, registerEvidenceProvider, renderCompareReport, renderProviderCliOutput, renderProviderConfig, renderProviderDoc, renderProviderReadme, resolveDbRequestContext, sentryEvidenceProvider, signSigV4, splunkEvidenceProvider, splunkSearchDeepLink, splunkWebBase, startHeadlessSession, startSessionSweeper, sweepIdleSessions, withBoundedRetry, writeReplayResult };
|