@rasputin-ai/core 0.1.2

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.
Files changed (52) hide show
  1. package/dist/announce-deploy/announce-deploy.d.ts +19 -0
  2. package/dist/announce-deploy/announce-deploy.d.ts.map +1 -0
  3. package/dist/create-client/create-client-types.d.ts +110 -0
  4. package/dist/create-client/create-client-types.d.ts.map +1 -0
  5. package/dist/create-client/create-client.d.ts +9 -0
  6. package/dist/create-client/create-client.d.ts.map +1 -0
  7. package/dist/create-client/is-client-enabled.d.ts +5 -0
  8. package/dist/create-client/is-client-enabled.d.ts.map +1 -0
  9. package/dist/create-client/resolve-endpoint-urls.d.ts +7 -0
  10. package/dist/create-client/resolve-endpoint-urls.d.ts.map +1 -0
  11. package/dist/create-ingest-event/create-ingest-event-types.d.ts +47 -0
  12. package/dist/create-ingest-event/create-ingest-event-types.d.ts.map +1 -0
  13. package/dist/create-ingest-event/create-ingest-event.d.ts +9 -0
  14. package/dist/create-ingest-event/create-ingest-event.d.ts.map +1 -0
  15. package/dist/create-ingest-event/finalize-resolution.d.ts +4 -0
  16. package/dist/create-ingest-event/finalize-resolution.d.ts.map +1 -0
  17. package/dist/detect-release/detect-release.d.ts +13 -0
  18. package/dist/detect-release/detect-release.d.ts.map +1 -0
  19. package/dist/index.d.ts +18 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +1013 -0
  22. package/dist/normalize-frames/normalize-frames.d.ts +24 -0
  23. package/dist/normalize-frames/normalize-frames.d.ts.map +1 -0
  24. package/dist/parse-stack/parse-stack.d.ts +4 -0
  25. package/dist/parse-stack/parse-stack.d.ts.map +1 -0
  26. package/dist/parse-stack/raw-frame.d.ts +8 -0
  27. package/dist/parse-stack/raw-frame.d.ts.map +1 -0
  28. package/dist/resolve-source-maps/resolve-source-maps.d.ts +7 -0
  29. package/dist/resolve-source-maps/resolve-source-maps.d.ts.map +1 -0
  30. package/dist/resolve-source-maps/resolved-frame.d.ts +25 -0
  31. package/dist/resolve-source-maps/resolved-frame.d.ts.map +1 -0
  32. package/dist/sdk-meta.d.ts +4 -0
  33. package/dist/sdk-meta.d.ts.map +1 -0
  34. package/dist/suppression/compute-burst-key.d.ts +7 -0
  35. package/dist/suppression/compute-burst-key.d.ts.map +1 -0
  36. package/dist/suppression/create-suppression.d.ts +12 -0
  37. package/dist/suppression/create-suppression.d.ts.map +1 -0
  38. package/dist/suppression/identity-needs-message-template.d.ts +6 -0
  39. package/dist/suppression/identity-needs-message-template.d.ts.map +1 -0
  40. package/dist/suppression/parameterize-message.d.ts +2 -0
  41. package/dist/suppression/parameterize-message.d.ts.map +1 -0
  42. package/dist/suppression/suppression-types.d.ts +46 -0
  43. package/dist/suppression/suppression-types.d.ts.map +1 -0
  44. package/dist/transport/create-transport.d.ts +3 -0
  45. package/dist/transport/create-transport.d.ts.map +1 -0
  46. package/dist/transport/send-batch.d.ts +19 -0
  47. package/dist/transport/send-batch.d.ts.map +1 -0
  48. package/dist/transport/transport-queue.d.ts +9 -0
  49. package/dist/transport/transport-queue.d.ts.map +1 -0
  50. package/dist/transport/transport-types.d.ts +53 -0
  51. package/dist/transport/transport-types.d.ts.map +1 -0
  52. package/package.json +40 -0
package/dist/index.js ADDED
@@ -0,0 +1,1013 @@
1
+ // src/announce-deploy/announce-deploy.ts
2
+ var announceDeploy = async (options) => {
3
+ if (options.enabled === false) return "skipped";
4
+ const release = options.release?.trim();
5
+ const environment = options.environment.trim();
6
+ if (!release || !environment) return "skipped";
7
+ const fetchFn = options.fetch ?? globalThis.fetch;
8
+ try {
9
+ const response = await fetchFn(options.deployUrl, {
10
+ method: "POST",
11
+ headers: {
12
+ Authorization: `Bearer ${options.projectApiKey}`,
13
+ "Content-Type": "application/json",
14
+ Accept: "application/json"
15
+ },
16
+ body: JSON.stringify({ release, environment })
17
+ });
18
+ if (response.ok) return "ok";
19
+ return "failed";
20
+ } catch {
21
+ return "failed";
22
+ }
23
+ };
24
+
25
+ // src/create-ingest-event/create-ingest-event.ts
26
+ var MAX_MESSAGE_BYTES = 8 * 1024;
27
+ var MAX_STACK_FRAMES = 50;
28
+ var truncateUtf8 = (value, maxBytes) => {
29
+ if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
30
+ let end = Math.min(value.length, maxBytes);
31
+ while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > maxBytes) end--;
32
+ return value.slice(0, end);
33
+ };
34
+ var errorFields = (error) => {
35
+ if (error instanceof Error) {
36
+ const code = "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? String(error.code) : void 0;
37
+ return {
38
+ type: error.name || "Error",
39
+ message: error.message || String(error),
40
+ code
41
+ };
42
+ }
43
+ if (typeof error === "string") {
44
+ return { type: "string", message: error };
45
+ }
46
+ return { type: "Error", message: String(error) };
47
+ };
48
+ var toStackFrame = (frame) => {
49
+ const preferred = frame.original ?? frame.generated;
50
+ const filename = (preferred.file || frame.generated.file).trim();
51
+ if (!filename) return null;
52
+ const fn = frame.function ?? frame.original?.name;
53
+ return {
54
+ filename,
55
+ ...fn ? { function: fn } : {},
56
+ ...preferred.line !== void 0 ? { lineno: preferred.line } : {},
57
+ ...preferred.column !== void 0 ? { colno: preferred.column } : {},
58
+ in_app: frame.in_app
59
+ };
60
+ };
61
+ var sanitizeRequest = (request) => {
62
+ if (!request) return void 0;
63
+ const { method, route, status } = request;
64
+ if (method == null && route == null && status == null) return void 0;
65
+ return {
66
+ ...method != null ? { method } : {},
67
+ ...route != null ? { route } : {},
68
+ ...status != null ? { status } : {}
69
+ };
70
+ };
71
+ var framesToStackFrames = (frames) => frames.slice(0, MAX_STACK_FRAMES).map(toStackFrame).filter((frame) => frame !== null);
72
+ var withResolvedFrames = (event, frames, resolution) => ({
73
+ ...event,
74
+ stack_frames: framesToStackFrames(frames),
75
+ resolution
76
+ });
77
+ var createIngestEvent = (input) => {
78
+ const extracted = errorFields(input.error);
79
+ const occurred = input.occurredAt === void 0 ? (/* @__PURE__ */ new Date()).toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : input.occurredAt.toISOString();
80
+ const event = {
81
+ type: extracted.type,
82
+ message: truncateUtf8(extracted.message, MAX_MESSAGE_BYTES),
83
+ stack_frames: framesToStackFrames(input.frames),
84
+ occurred_at: occurred,
85
+ environment: input.environment,
86
+ origin: input.origin,
87
+ resolution: input.resolution
88
+ };
89
+ const code = input.code !== void 0 ? input.code : extracted.code;
90
+ if (code !== void 0) event.code = code;
91
+ if (input.release !== void 0) event.release = input.release;
92
+ const request = sanitizeRequest(input.request);
93
+ if (request) event.request = request;
94
+ return event;
95
+ };
96
+
97
+ // src/create-ingest-event/finalize-resolution.ts
98
+ var SOURCE_EXT = /\.(ts|tsx|mts|cts)$/i;
99
+ var isAbsolutePath = (file) => file.startsWith("/") || /^[A-Za-z]:[\\/]/.test(file);
100
+ var isRuntimeFrame = (file) => !file || file === "native" || /^(node:|bun:)/.test(file);
101
+ var finalizeResolution = (resolution, stackFrames) => {
102
+ if (resolution !== "exact") return resolution;
103
+ const mapped = stackFrames.filter((frame) => !isRuntimeFrame(frame.filename));
104
+ if (mapped.some((frame) => frame.filename.includes("..") || isAbsolutePath(frame.filename))) {
105
+ return "partial";
106
+ }
107
+ const resolvedSource = mapped.some((frame) => SOURCE_EXT.test(frame.filename));
108
+ const hasInApp = stackFrames.some((frame) => frame.in_app);
109
+ if (resolvedSource && !hasInApp) return "partial";
110
+ return "exact";
111
+ };
112
+
113
+ // src/detect-release/detect-release.ts
114
+ var ENV_KEYS = [
115
+ "VERCEL_GIT_COMMIT_SHA",
116
+ "RAILWAY_GIT_COMMIT_SHA",
117
+ "RENDER_GIT_COMMIT",
118
+ "SOURCE_VERSION",
119
+ "GIT_COMMIT",
120
+ "COMMIT_SHA",
121
+ "GIT_SHA"
122
+ ];
123
+ var detectRelease = (options = {}) => {
124
+ const explicit = options.release?.trim();
125
+ if (explicit) return explicit;
126
+ const env = options.env ?? process.env;
127
+ for (const key of ENV_KEYS) {
128
+ const value = env[key]?.trim();
129
+ if (value) return value;
130
+ }
131
+ console.warn(
132
+ "[rasputin] No release detected. Set `release` in RasputinInit or a commit SHA env var (e.g. GIT_COMMIT, VERCEL_GIT_COMMIT_SHA). Without a release, deploy correlation and diagnosis confidence are degraded."
133
+ );
134
+ return void 0;
135
+ };
136
+
137
+ // src/normalize-frames/normalize-frames.ts
138
+ import { isAbsolute, resolve } from "node:path";
139
+ import { fileURLToPath } from "node:url";
140
+ var NON_SOURCE = /* @__PURE__ */ new Set(["", "native", "<anonymous>", "unknown location"]);
141
+ var toPosix = (path) => path.replaceAll("\\", "/");
142
+ var stripFileUrl = (file) => {
143
+ if (!file.startsWith("file://")) return file;
144
+ try {
145
+ return fileURLToPath(file);
146
+ } catch {
147
+ return file.replace(/^file:\/\//, "");
148
+ }
149
+ };
150
+ var isSchemePath = (file) => NON_SOURCE.has(file) || /^(node:|bun:|https?:|webpack:|blob:|data:|eval\b)/i.test(file);
151
+ var normalizeFile = (file, appRoot) => {
152
+ if (!file || isSchemePath(file)) return { file, underRoot: false };
153
+ const cleaned = toPosix(stripFileUrl(file)).split("?")[0]?.trim() ?? "";
154
+ if (!cleaned || isSchemePath(cleaned)) return { file: cleaned, underRoot: false };
155
+ const root = toPosix(resolve(appRoot));
156
+ const absolute = toPosix(
157
+ isAbsolute(cleaned) || /^[A-Za-z]:\//.test(cleaned) ? resolve(cleaned) : resolve(root, cleaned)
158
+ );
159
+ if (absolute === root) return { file: ".", underRoot: true };
160
+ const prefix = root.endsWith("/") ? root : `${root}/`;
161
+ if (absolute.startsWith(prefix)) {
162
+ return { file: absolute.slice(prefix.length), underRoot: true };
163
+ }
164
+ return { file: absolute, underRoot: false };
165
+ };
166
+ var isInApp = (file, underRoot) => {
167
+ if (!underRoot || isSchemePath(file)) return false;
168
+ if (/(^|\/)node_modules\//.test(file)) return false;
169
+ return true;
170
+ };
171
+ var normalizeFrames = (frames, options = {}) => {
172
+ const appRoot = options.appRoot ?? process.cwd();
173
+ return frames.map((frame) => {
174
+ const generated = normalizeFile(frame.generated.file, appRoot);
175
+ const original = frame.original ? normalizeFile(frame.original.file, appRoot) : void 0;
176
+ const preferred = original ?? generated;
177
+ return {
178
+ generated: {
179
+ file: generated.file,
180
+ line: frame.generated.line,
181
+ column: frame.generated.column
182
+ },
183
+ original: frame.original ? {
184
+ file: original?.file ?? frame.original.file,
185
+ line: frame.original.line,
186
+ column: frame.original.column,
187
+ name: frame.original.name
188
+ } : void 0,
189
+ function: frame.function,
190
+ in_app: isInApp(preferred.file, preferred.underRoot)
191
+ };
192
+ });
193
+ };
194
+
195
+ // src/parse-stack/parse-stack.ts
196
+ var V8 = /^\s*at (?<fn>.*?) ?\((?<loc>(?:file|https?|blob|native|eval|webpack|rsc|node|bun|data|<anonymous>|\/|[a-z]:\\|\\\\).*?)(?::(?<line>\d+))?(?::(?<col>\d+))?\)?\s*$/i;
197
+ var V8_EVAL = /\((?<loc>\S+)(?::(?<line>\d+))(?::(?<col>\d+))\)/;
198
+ var NODE = /^\s*at (?:(?<fn>(?:\[object object\])?[^\\/]+(?: \[as \S+\])?) )?\(?(?<loc>.+?):(?<line>\d+)(?::(?<col>\d+))?\)?\s*$/i;
199
+ var JSC = /^\s*(?:(?<fn>[^@]*)(?:\(.*?\))?@)?(?<loc>\S.*?):(?<line>\d+)(?::(?<col>\d+))?\s*$/i;
200
+ var convertToNumber = (value) => {
201
+ if (value === void 0 || value === "") return void 0;
202
+ const n = Number(value);
203
+ return Number.isFinite(n) ? n : void 0;
204
+ };
205
+ var cleanFunctionName = (fn) => {
206
+ if (!fn) return void 0;
207
+ const trimmed = fn.trim();
208
+ if (!trimmed || trimmed === "<anonymous>" || trimmed === "<unknown>") return void 0;
209
+ return trimmed.startsWith("async ") ? trimmed.slice(6) || void 0 : trimmed;
210
+ };
211
+ var cleanFilePath = (loc) => {
212
+ if (loc.startsWith("file:///")) {
213
+ const rest = loc.slice("file://".length);
214
+ return /^\/[A-Za-z]:/.test(rest) ? rest.slice(1) : rest;
215
+ }
216
+ if (loc.startsWith("file://")) return loc.slice("file://".length);
217
+ return loc.startsWith("native") ? "native" : loc;
218
+ };
219
+ var createFrame = (fn, loc, line, col) => ({
220
+ file: cleanFilePath(loc),
221
+ function: cleanFunctionName(fn),
222
+ line: convertToNumber(line),
223
+ column: convertToNumber(col)
224
+ });
225
+ var parseV8 = (line) => {
226
+ const g = V8.exec(line)?.groups;
227
+ if (!g?.loc) return null;
228
+ if (g.loc.startsWith("eval")) {
229
+ const inner = V8_EVAL.exec(g.loc)?.groups;
230
+ if (inner?.loc) return createFrame(g.fn, inner.loc, inner.line, inner.col);
231
+ }
232
+ return createFrame(g.fn, g.loc, g.line, g.col);
233
+ };
234
+ var parseNode = (line) => {
235
+ const g = NODE.exec(line)?.groups;
236
+ return g?.loc ? createFrame(g.fn, g.loc, g.line, g.col) : null;
237
+ };
238
+ var parseJsc = (line) => {
239
+ if (!line.includes("@")) return null;
240
+ const g = JSC.exec(line)?.groups;
241
+ return g?.loc ? createFrame(g.fn, g.loc, g.line, g.col) : null;
242
+ };
243
+ var parseLine = (line) => parseV8(line) ?? parseNode(line) ?? parseJsc(line);
244
+ var parseStack = (stack) => {
245
+ const frames = [];
246
+ for (const line of stack.split(/\r?\n/)) {
247
+ const parsed = parseLine(line);
248
+ if (parsed) frames.push(parsed);
249
+ }
250
+ return frames;
251
+ };
252
+
253
+ // src/resolve-source-maps/resolve-source-maps.ts
254
+ import { readFile } from "node:fs/promises";
255
+ import { dirname, isAbsolute as isAbsolute2, join } from "node:path";
256
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
257
+ import { LEAST_UPPER_BOUND, originalPositionFor, TraceMap } from "@jridgewell/trace-mapping";
258
+ var SOURCE_EXT2 = /\.(ts|tsx|mts|cts)$/i;
259
+ var SOURCEMAP_COMMENT = /(?:\/\/[#@][ \t]*sourceMappingURL=([^\s'"]+)|\/\*[#@][ \t]*sourceMappingURL=([^\s*'"]+)[ \t]*\*\/)\s*$/;
260
+ var mapCache = /* @__PURE__ */ new Map();
261
+ var toPath = (file) => {
262
+ if (file.startsWith("file://")) {
263
+ try {
264
+ return fileURLToPath2(file);
265
+ } catch {
266
+ return file;
267
+ }
268
+ }
269
+ return file;
270
+ };
271
+ var isSourcePath = (file) => SOURCE_EXT2.test(file);
272
+ var isResolvableFile = (file) => {
273
+ if (isSourcePath(file) || file === "<anonymous>" || file === "native") return false;
274
+ return !/^(node:|bun:|https?:|webpack:|blob:|data:|eval\b)/i.test(file);
275
+ };
276
+ var decodeDataUrl = (url) => {
277
+ const base64 = /^data:application\/json(?:;charset=[^;,]+)?;base64,(.+)$/i.exec(url);
278
+ if (base64?.[1]) {
279
+ try {
280
+ return Buffer.from(base64[1], "base64").toString("utf8");
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
285
+ const raw = /^data:application\/json(?:;charset=[^;,]+)?,(.+)$/i.exec(url);
286
+ if (!raw?.[1]) return null;
287
+ try {
288
+ return decodeURIComponent(raw[1]);
289
+ } catch {
290
+ return null;
291
+ }
292
+ };
293
+ var readMapPayload = async (generatedPath) => {
294
+ let source;
295
+ try {
296
+ source = await readFile(generatedPath, "utf8");
297
+ } catch {
298
+ return null;
299
+ }
300
+ const match = SOURCEMAP_COMMENT.exec(source);
301
+ const comment = match?.[1] ?? match?.[2];
302
+ if (comment?.startsWith("data:")) return decodeDataUrl(comment);
303
+ if (comment) {
304
+ try {
305
+ return await readFile(join(dirname(generatedPath), comment), "utf8");
306
+ } catch {
307
+ }
308
+ }
309
+ try {
310
+ return await readFile(`${generatedPath}.map`, "utf8");
311
+ } catch {
312
+ return null;
313
+ }
314
+ };
315
+ var loadTraceMap = async (generatedPath) => {
316
+ const cached = mapCache.get(generatedPath);
317
+ if (cached !== void 0) return cached;
318
+ const payload = await readMapPayload(generatedPath);
319
+ if (!payload) {
320
+ mapCache.set(generatedPath, null);
321
+ return null;
322
+ }
323
+ try {
324
+ const map = new TraceMap(payload);
325
+ mapCache.set(generatedPath, map);
326
+ return map;
327
+ } catch {
328
+ mapCache.set(generatedPath, null);
329
+ return null;
330
+ }
331
+ };
332
+ var isMapCandidate = (raw) => raw.line !== void 0 && isResolvableFile(raw.file) && !isSourcePath(raw.file);
333
+ var resolveFrame = async (raw) => {
334
+ const generated = { file: raw.file, line: raw.line, column: raw.column };
335
+ const base = { generated, function: raw.function };
336
+ if (isSourcePath(raw.file)) {
337
+ return { ...base, original: { ...generated } };
338
+ }
339
+ if (raw.line === void 0 || !isResolvableFile(raw.file)) return base;
340
+ const generatedPath = toPath(raw.file);
341
+ const map = await loadTraceMap(generatedPath);
342
+ if (!map) return base;
343
+ const column = raw.column ?? 0;
344
+ let pos = originalPositionFor(map, { line: raw.line, column });
345
+ if ((!pos.source || pos.line == null) && column === 0) {
346
+ pos = originalPositionFor(map, { line: raw.line, column: 0, bias: LEAST_UPPER_BOUND });
347
+ }
348
+ if (!pos.source || pos.line == null) return base;
349
+ return {
350
+ ...base,
351
+ original: {
352
+ file: isAbsolute2(pos.source) ? pos.source : join(dirname(generatedPath), pos.source),
353
+ line: pos.line,
354
+ column: pos.column ?? void 0,
355
+ name: pos.name ?? void 0
356
+ }
357
+ };
358
+ };
359
+ var resolveSourceMaps = async (frames) => {
360
+ const resolved = await Promise.all(frames.map(resolveFrame));
361
+ const candidateIndexes = frames.flatMap((raw, index) => isMapCandidate(raw) ? [index] : []);
362
+ const unresolvedGenerated = candidateIndexes.length > 0 && candidateIndexes.every((index) => resolved[index]?.original === void 0);
363
+ return {
364
+ frames: resolved,
365
+ resolution: resolved.some((f) => f.original) ? "exact" : "none",
366
+ unresolvedGenerated
367
+ };
368
+ };
369
+
370
+ // src/sdk-meta.ts
371
+ var SDK_NAME = "@rasputin-ai/core";
372
+ var SDK_VERSION = "0.1.2";
373
+
374
+ // src/suppression/identity-needs-message-template.ts
375
+ var identityNeedsMessageTemplate = (type, code) => {
376
+ if (code != null && code !== "") return false;
377
+ return type === "Error";
378
+ };
379
+
380
+ // src/suppression/parameterize-message.ts
381
+ import { isIP } from "node:net";
382
+ var MAX_INPUT_LENGTH = 8192;
383
+ var RULES = [
384
+ {
385
+ token: "email",
386
+ pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}\b/giu
387
+ },
388
+ {
389
+ token: "url",
390
+ pattern: /\b(?:https?|wss?):\/\/[^\s"'<>]+/giu
391
+ },
392
+ {
393
+ token: "traceparent",
394
+ pattern: /\b00-[0-9a-f]{32}-[0-9a-f]{16}-0[01]\b/giu
395
+ },
396
+ {
397
+ token: "aws_trace_id",
398
+ pattern: /\b1-[0-9a-f]{8}-[0-9a-f]{24}\b/giu
399
+ },
400
+ {
401
+ token: "uuid",
402
+ pattern: /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/giu
403
+ },
404
+ {
405
+ token: "timestamp",
406
+ pattern: /\b\d{4}-\d{2}-\d{2}[T ][0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b/gu
407
+ },
408
+ {
409
+ token: "mac",
410
+ pattern: /\b(?:[0-9a-f]{2}[:-]){5}[0-9a-f]{2}\b/giu
411
+ },
412
+ {
413
+ token: "ip",
414
+ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b|(?<![\w:])(?:[0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}(?![\w:])/giu,
415
+ validate: (candidate) => isIP(candidate) !== 0
416
+ },
417
+ {
418
+ token: "hash",
419
+ pattern: /\b[0-9a-f]{16,128}\b/giu
420
+ },
421
+ {
422
+ token: "hex",
423
+ pattern: /\b0x[0-9a-f]+\b/giu
424
+ },
425
+ {
426
+ token: "duration",
427
+ pattern: /\b\d+(?:\.\d+)?\s?(?:ns|µs|us|ms|secs?|seconds?|mins?|minutes?|hrs?|hours?|days?)\b/giu
428
+ },
429
+ {
430
+ token: "id",
431
+ pattern: /\b(?=[a-z0-9]*[a-z])(?=[a-z0-9]*\d)[a-z0-9]{8,}\b/giu
432
+ }
433
+ ];
434
+ var parameterizeMessage = (rawMessage) => {
435
+ if (rawMessage.length > MAX_INPUT_LENGTH) return rawMessage;
436
+ const normalized = rawMessage.normalize("NFKC").replace(/\r\n?/g, "\n").trim();
437
+ const lines = normalized.split("\n").filter((line) => line.trim());
438
+ let result = lines.slice(0, 2).join("\n");
439
+ if (result !== normalized) {
440
+ result += "...";
441
+ }
442
+ result = result.replace(/\b([A-Za-z_][\w.-]*)=(["'])(?:\\.|(?!\2).)*\2/gu, "$1=<string>");
443
+ result = result.replace(/\b([A-Za-z_][\w.-]*)=(?:true|false)\b/giu, "$1=<bool>");
444
+ for (const rule of RULES) {
445
+ result = result.replace(rule.pattern, (match) => {
446
+ if (rule.validate && !rule.validate(match)) {
447
+ return match;
448
+ }
449
+ return `<${rule.token}>`;
450
+ });
451
+ }
452
+ result = result.replace(/(?<![\w.])-?\d+\.\d+(?:e[+-]?\d+)?(?![\w.])/giu, "<float>").replace(/(?<![\w.])-?\d+(?![\w.])/gu, "<int>");
453
+ return result;
454
+ };
455
+
456
+ // src/suppression/compute-burst-key.ts
457
+ var preferredFile = (frame) => (frame.original ?? frame.generated).file;
458
+ var frameBurstParts = (frames) => {
459
+ return frames.filter((frame) => frame.in_app).slice(0, 3).map((frame) => {
460
+ const file = preferredFile(frame);
461
+ const fn = frame.function ?? frame.original?.name ?? "";
462
+ if (!frame.original && frame.generated.line !== void 0) {
463
+ return `${file}:${fn}:${frame.generated.line}:${frame.generated.column ?? 0}`;
464
+ }
465
+ return `${file}:${fn}`;
466
+ });
467
+ };
468
+ var computeBurstKey = (errorType, message, code, frames) => {
469
+ const parts = frameBurstParts(frames);
470
+ let key = `${errorType}\0${parts.join("\0")}`;
471
+ if (identityNeedsMessageTemplate(errorType, code)) {
472
+ key += `\0${parameterizeMessage(message)}`;
473
+ }
474
+ return key;
475
+ };
476
+
477
+ // src/suppression/create-suppression.ts
478
+ var DEFAULTS = {
479
+ windowMs: 6e4,
480
+ capPerWindow: 5,
481
+ maxBurstBuckets: 1e3
482
+ };
483
+ var sameDim = (a, b) => (a ?? null) === (b ?? null);
484
+ var captureIdentity = (input) => ({
485
+ type: input.errorType,
486
+ message: input.message,
487
+ stack_frames: framesToStackFrames(input.frames),
488
+ origin: input.origin,
489
+ ...input.code !== void 0 ? { code: input.code } : {},
490
+ resolution: "none"
491
+ });
492
+ var createSuppression = (options = {}) => {
493
+ const windowMs = options.windowMs ?? DEFAULTS.windowMs;
494
+ const capPerWindow = options.capPerWindow ?? DEFAULTS.capPerWindow;
495
+ const maxBurstBuckets = options.maxBurstBuckets ?? DEFAULTS.maxBurstBuckets;
496
+ const burstBuckets = /* @__PURE__ */ new Map();
497
+ const pending = [];
498
+ const touch = (burstKey, bucket) => {
499
+ burstBuckets.delete(burstKey);
500
+ burstBuckets.set(burstKey, bucket);
501
+ };
502
+ const evictIfNeeded = () => {
503
+ while (burstBuckets.size > maxBurstBuckets) {
504
+ const oldest = burstBuckets.keys().next().value;
505
+ if (oldest === void 0) break;
506
+ const bucket = burstBuckets.get(oldest);
507
+ if (bucket && bucket.suppressed > 0) {
508
+ pending.push(toDraft(bucket, Date.now()));
509
+ }
510
+ burstBuckets.delete(oldest);
511
+ }
512
+ };
513
+ const toDraft = (bucket, windowEndMs) => ({
514
+ count: bucket.suppressed,
515
+ windowStart: bucket.windowStart,
516
+ windowEndMs,
517
+ lastRelease: bucket.lastRelease,
518
+ lastEnvironment: bucket.lastEnvironment,
519
+ identity: bucket.identity,
520
+ captureRawFrames: bucket.captureRawFrames
521
+ });
522
+ const toSummary = (draft, identity) => ({
523
+ count: draft.count,
524
+ window_start: new Date(draft.windowStart).toISOString(),
525
+ window_end: new Date(draft.windowEndMs).toISOString(),
526
+ release: draft.lastRelease,
527
+ environment: draft.lastEnvironment,
528
+ ...identity
529
+ });
530
+ const flushSuppressed = (bucket, windowEndMs) => {
531
+ if (bucket.suppressed <= 0) return;
532
+ pending.push(toDraft(bucket, windowEndMs));
533
+ bucket.suppressed = 0;
534
+ };
535
+ const decide = (input) => {
536
+ const now = input.now ?? Date.now();
537
+ const identity = captureIdentity(input);
538
+ const captureRawFrames = (input.rawFrames ?? []).slice(0, MAX_STACK_FRAMES);
539
+ const burstKey = computeBurstKey(input.errorType, input.message, input.code, input.frames);
540
+ const release = input.release ?? null;
541
+ const route = input.route ?? null;
542
+ let bucket = burstBuckets.get(burstKey);
543
+ if (!bucket) {
544
+ bucket = {
545
+ sentInWindow: 1,
546
+ suppressed: 0,
547
+ windowStart: now,
548
+ lastRelease: release,
549
+ lastRoute: route,
550
+ lastEnvironment: input.environment,
551
+ identity,
552
+ captureRawFrames
553
+ };
554
+ burstBuckets.set(burstKey, bucket);
555
+ evictIfNeeded();
556
+ return { action: "send", priority: "first" };
557
+ }
558
+ touch(burstKey, bucket);
559
+ const dimensionChanged = !sameDim(release, bucket.lastRelease) || !sameDim(route, bucket.lastRoute) || bucket.lastEnvironment !== input.environment;
560
+ const windowExpired = now - bucket.windowStart >= windowMs;
561
+ if (dimensionChanged || windowExpired) {
562
+ flushSuppressed(bucket, now);
563
+ bucket.sentInWindow = 1;
564
+ bucket.windowStart = now;
565
+ bucket.lastRelease = release;
566
+ bucket.lastRoute = route;
567
+ bucket.lastEnvironment = input.environment;
568
+ bucket.identity = identity;
569
+ bucket.captureRawFrames = captureRawFrames;
570
+ return { action: "send", priority: "first" };
571
+ }
572
+ if (bucket.sentInWindow < capPerWindow) {
573
+ bucket.sentInWindow += 1;
574
+ bucket.lastRelease = release;
575
+ bucket.lastRoute = route;
576
+ bucket.lastEnvironment = input.environment;
577
+ return { action: "send", priority: "repeat" };
578
+ }
579
+ bucket.suppressed += 1;
580
+ return { action: "suppress" };
581
+ };
582
+ const takeSummaries = async (now = Date.now(), prepareSummary) => {
583
+ const drafts = pending.splice(0, pending.length);
584
+ for (const bucket of burstBuckets.values()) {
585
+ if (bucket.suppressed > 0) {
586
+ drafts.push(toDraft(bucket, now));
587
+ bucket.suppressed = 0;
588
+ }
589
+ }
590
+ const out = [];
591
+ for (const draft of drafts) {
592
+ let identity = draft.identity;
593
+ if (prepareSummary && draft.captureRawFrames.length > 0) {
594
+ try {
595
+ const resolved = await prepareSummary(draft.captureRawFrames);
596
+ identity = { ...identity, ...resolved };
597
+ } catch {
598
+ }
599
+ }
600
+ out.push(toSummary(draft, identity));
601
+ }
602
+ return out;
603
+ };
604
+ return {
605
+ decide,
606
+ takeSummaries,
607
+ /** Test helper — number of active burst buckets. */
608
+ size: () => burstBuckets.size
609
+ };
610
+ };
611
+
612
+ // src/transport/send-batch.ts
613
+ import { gzipSync } from "node:zlib";
614
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
615
+ var backoffMs = (attempt) => {
616
+ const base = Math.min(3e4, 500 * 2 ** attempt);
617
+ return Math.floor(base * (0.5 + Math.random()));
618
+ };
619
+ var retryAfterMs = (response) => {
620
+ const header = response.headers.get("retry-after");
621
+ if (!header) return null;
622
+ const seconds = Number(header);
623
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
624
+ const date = Date.parse(header);
625
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : null;
626
+ };
627
+ var sendBatch = async (options) => {
628
+ const summaries = options.summaries ?? [];
629
+ let payload;
630
+ if (summaries.length > 0) {
631
+ payload = { events: options.events, summaries };
632
+ } else {
633
+ payload = { events: options.events };
634
+ }
635
+ if (options.sdkVersion) payload.sdk_version = options.sdkVersion;
636
+ if (options.sdkName) payload.sdk_name = options.sdkName;
637
+ const json = JSON.stringify(payload);
638
+ const headers = {
639
+ Authorization: `Bearer ${options.projectApiKey}`,
640
+ "Content-Type": "application/json",
641
+ Accept: "application/json"
642
+ };
643
+ let body = json;
644
+ if (Buffer.byteLength(json, "utf8") >= options.gzipThresholdBytes) {
645
+ const compressed = gzipSync(json);
646
+ const bytes = new Uint8Array(compressed.byteLength);
647
+ bytes.set(compressed);
648
+ body = bytes;
649
+ headers["Content-Encoding"] = "gzip";
650
+ }
651
+ for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
652
+ if (options.signal?.aborted) return "failed";
653
+ try {
654
+ const response = await options.fetch(options.ingestUrl, {
655
+ method: "POST",
656
+ headers,
657
+ body,
658
+ signal: options.signal
659
+ });
660
+ if (response.ok) return "ok";
661
+ if (response.status === 429) {
662
+ if (attempt === options.maxRetries) return "failed";
663
+ const wait = retryAfterMs(response) ?? backoffMs(attempt);
664
+ await sleep(wait);
665
+ continue;
666
+ }
667
+ if (response.status >= 400 && response.status < 500) return "dropped";
668
+ if (attempt === options.maxRetries) return "failed";
669
+ await sleep(backoffMs(attempt));
670
+ } catch {
671
+ if (options.signal?.aborted) return "failed";
672
+ if (attempt === options.maxRetries) return "failed";
673
+ await sleep(backoffMs(attempt));
674
+ }
675
+ }
676
+ return "failed";
677
+ };
678
+
679
+ // src/transport/transport-queue.ts
680
+ var RANK = {
681
+ repeat: 0,
682
+ summary: 1,
683
+ first: 2
684
+ };
685
+ var createQueue = (maxSize) => {
686
+ const items = [];
687
+ const oldestLowestIndex = () => {
688
+ let best = 0;
689
+ for (let i = 1; i < items.length; i++) {
690
+ const current = items[i];
691
+ const champion = items[best];
692
+ if (!current || !champion) continue;
693
+ if (RANK[current.priority] < RANK[champion.priority]) best = i;
694
+ }
695
+ return best;
696
+ };
697
+ const enqueue = (item) => {
698
+ while (items.length >= maxSize) {
699
+ const victimIndex = oldestLowestIndex();
700
+ const victim = items[victimIndex];
701
+ if (!victim) break;
702
+ if (RANK[item.priority] < RANK[victim.priority]) return;
703
+ items.splice(victimIndex, 1);
704
+ }
705
+ items.push(item);
706
+ };
707
+ const drain = (count) => items.splice(0, count);
708
+ return {
709
+ enqueue,
710
+ drain,
711
+ size: () => items.length,
712
+ clear: () => {
713
+ items.length = 0;
714
+ }
715
+ };
716
+ };
717
+
718
+ // src/transport/create-transport.ts
719
+ var DEFAULTS2 = {
720
+ enabled: true,
721
+ maxQueueSize: 500,
722
+ batchSize: 30,
723
+ flushIntervalMs: 5e3,
724
+ gzipThresholdBytes: 10 * 1024,
725
+ maxRetries: 5,
726
+ installSignalHandlers: true
727
+ };
728
+ var createTransport = (options) => {
729
+ const enabled = options.enabled ?? DEFAULTS2.enabled;
730
+ const batchSize = options.batchSize ?? DEFAULTS2.batchSize;
731
+ const flushIntervalMs = options.flushIntervalMs ?? DEFAULTS2.flushIntervalMs;
732
+ const gzipThresholdBytes = options.gzipThresholdBytes ?? DEFAULTS2.gzipThresholdBytes;
733
+ const maxRetries = options.maxRetries ?? DEFAULTS2.maxRetries;
734
+ const fetchFn = options.fetch ?? globalThis.fetch;
735
+ const queue = createQueue(options.maxQueueSize ?? DEFAULTS2.maxQueueSize);
736
+ let timer;
737
+ let flushing;
738
+ let closed = false;
739
+ const schedule = () => {
740
+ if (timer || !enabled || closed) return;
741
+ timer = setInterval(() => {
742
+ void flush();
743
+ }, flushIntervalMs);
744
+ timer.unref?.();
745
+ };
746
+ const stopTimer = () => {
747
+ if (!timer) return;
748
+ clearInterval(timer);
749
+ timer = void 0;
750
+ };
751
+ const flush = async (timeoutMs = 1e4) => {
752
+ if (!enabled || closed) {
753
+ queue.clear();
754
+ return;
755
+ }
756
+ if (flushing) return flushing;
757
+ flushing = (async () => {
758
+ const controller = new AbortController();
759
+ const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
760
+ try {
761
+ while (queue.size() > 0) {
762
+ if (controller.signal.aborted) break;
763
+ const batch = queue.drain(batchSize);
764
+ if (batch.length === 0) break;
765
+ const events = [];
766
+ const summaries = [];
767
+ for (const item of batch) {
768
+ if (item.kind === "summary") {
769
+ summaries.push(item.summary);
770
+ continue;
771
+ }
772
+ if (options.prepareEvent && item.rawFrames.length > 0) {
773
+ try {
774
+ events.push(
775
+ await options.prepareEvent({
776
+ event: item.event,
777
+ rawFrames: item.rawFrames
778
+ })
779
+ );
780
+ } catch {
781
+ events.push(item.event);
782
+ }
783
+ } else {
784
+ events.push(item.event);
785
+ }
786
+ }
787
+ const result = await sendBatch({
788
+ ingestUrl: options.ingestUrl,
789
+ projectApiKey: options.projectApiKey,
790
+ events,
791
+ summaries,
792
+ sdkVersion: options.sdkVersion,
793
+ sdkName: options.sdkName,
794
+ gzipThresholdBytes,
795
+ maxRetries,
796
+ fetch: fetchFn,
797
+ signal: controller.signal
798
+ });
799
+ if (result === "failed") {
800
+ for (const item of batch) queue.enqueue(item);
801
+ break;
802
+ }
803
+ }
804
+ } finally {
805
+ if (timeout) clearTimeout(timeout);
806
+ if (queue.size() === 0) stopTimer();
807
+ flushing = void 0;
808
+ }
809
+ })();
810
+ return flushing;
811
+ };
812
+ const enqueue = (event, priority = "first", rawFrames = []) => {
813
+ if (!enabled || closed) return;
814
+ queue.enqueue({ kind: "event", event, priority, rawFrames });
815
+ schedule();
816
+ if (queue.size() >= batchSize) void flush();
817
+ };
818
+ const enqueueSummary = (summary) => {
819
+ if (!enabled || closed) return;
820
+ queue.enqueue({ kind: "summary", summary, priority: "summary" });
821
+ schedule();
822
+ if (queue.size() >= batchSize) void flush();
823
+ };
824
+ const onSignal = () => {
825
+ try {
826
+ void flush(2e3);
827
+ } catch {
828
+ }
829
+ };
830
+ if (enabled && (options.installSignalHandlers ?? DEFAULTS2.installSignalHandlers)) {
831
+ process.on("beforeExit", onSignal);
832
+ process.on("SIGTERM", onSignal);
833
+ process.on("SIGINT", onSignal);
834
+ }
835
+ const close = async () => {
836
+ closed = true;
837
+ stopTimer();
838
+ process.off("beforeExit", onSignal);
839
+ process.off("SIGTERM", onSignal);
840
+ process.off("SIGINT", onSignal);
841
+ await flush(2e3);
842
+ queue.clear();
843
+ };
844
+ return {
845
+ enqueue,
846
+ enqueueSummary,
847
+ flush,
848
+ close,
849
+ size: () => queue.size()
850
+ };
851
+ };
852
+
853
+ // src/create-client/is-client-enabled.ts
854
+ var isClientEnabled = (options) => options.enabled ?? true;
855
+ var noopClient = () => ({
856
+ captureException: () => {
857
+ },
858
+ flush: async () => {
859
+ },
860
+ close: async () => {
861
+ }
862
+ });
863
+
864
+ // src/create-client/resolve-endpoint-urls.ts
865
+ var DEFAULT_API_URL = "https://rasputin-api.fly.dev";
866
+ var resolveEndpointUrls = (apiUrl = DEFAULT_API_URL) => {
867
+ const base = apiUrl.replace(/\/+$/, "");
868
+ return {
869
+ ingestUrl: `${base}/ingest`,
870
+ deployUrl: `${base}/deploy`
871
+ };
872
+ };
873
+
874
+ // src/create-client/create-client.ts
875
+ var warnedUnresolvedMaps = false;
876
+ var warnedPartialResolution = false;
877
+ var warnUnresolvedMaps = (unresolved) => {
878
+ if (!unresolved || warnedUnresolvedMaps) return;
879
+ warnedUnresolvedMaps = true;
880
+ console.warn(
881
+ "[rasputin] Could not resolve source maps for one or more stack frames; fingerprinting uses generated paths"
882
+ );
883
+ };
884
+ var warnPartialResolution = () => {
885
+ if (warnedPartialResolution) return;
886
+ warnedPartialResolution = true;
887
+ console.warn(
888
+ "[rasputin] Source map paths could not be normalized to the repo root; fingerprinting may be degraded"
889
+ );
890
+ };
891
+ var prepareResolvedEvent = (event, normalized, resolution) => {
892
+ const stackFrames = framesToStackFrames(normalized);
893
+ const finalResolution = finalizeResolution(resolution, stackFrames);
894
+ if (finalResolution === "partial") warnPartialResolution();
895
+ return withResolvedFrames(event, normalized, finalResolution);
896
+ };
897
+ var toResolvedFrames = (raw) => raw.map((frame) => ({
898
+ generated: { file: frame.file, line: frame.line, column: frame.column },
899
+ ...frame.function ? { function: frame.function } : {}
900
+ }));
901
+ var stackOf = (error) => {
902
+ if (error instanceof Error && error.stack) return error.stack;
903
+ return "";
904
+ };
905
+ var createClient = (options, hooks = {}) => {
906
+ try {
907
+ if (!isClientEnabled(options)) return noopClient();
908
+ const projectApiKey = options.projectApiKey.trim();
909
+ const release = detectRelease({ release: options.release });
910
+ const environment = options.environment;
911
+ const { ingestUrl, deployUrl } = resolveEndpointUrls(options.apiUrl);
912
+ const repoRoot = options.repoRoot;
913
+ const transport = createTransport({
914
+ ingestUrl,
915
+ projectApiKey,
916
+ enabled: true,
917
+ fetch: hooks.fetch,
918
+ installSignalHandlers: hooks.installTransportSignalHandlers,
919
+ sdkVersion: hooks.sdkVersion ?? SDK_VERSION,
920
+ sdkName: hooks.sdkName ?? SDK_NAME,
921
+ prepareEvent: async ({ event, rawFrames }) => {
922
+ const { frames, resolution, unresolvedGenerated } = await resolveSourceMaps(rawFrames);
923
+ warnUnresolvedMaps(unresolvedGenerated);
924
+ const normalized = normalizeFrames(frames, { appRoot: repoRoot });
925
+ return prepareResolvedEvent(event, normalized, resolution);
926
+ }
927
+ });
928
+ const suppression = createSuppression();
929
+ if (hooks.announceDeploy ?? true) {
930
+ void announceDeploy({
931
+ deployUrl,
932
+ projectApiKey,
933
+ environment,
934
+ release,
935
+ enabled: true,
936
+ fetch: hooks.fetch
937
+ });
938
+ }
939
+ const captureException = (error, context) => {
940
+ try {
941
+ const rawFrames = parseStack(stackOf(error));
942
+ const frames = normalizeFrames(toResolvedFrames(rawFrames), { appRoot: repoRoot });
943
+ const event = createIngestEvent({
944
+ error,
945
+ frames,
946
+ environment,
947
+ release,
948
+ origin: "server",
949
+ resolution: "none",
950
+ request: context?.request
951
+ });
952
+ const decision = suppression.decide({
953
+ errorType: event.type,
954
+ message: event.message,
955
+ frames,
956
+ rawFrames,
957
+ origin: event.origin,
958
+ code: event.code,
959
+ environment,
960
+ release,
961
+ route: context?.request?.route
962
+ });
963
+ if (decision.action === "send") {
964
+ transport.enqueue(event, decision.priority, rawFrames);
965
+ }
966
+ } catch {
967
+ }
968
+ };
969
+ const flush = async (timeoutMs) => {
970
+ try {
971
+ const summaries = await suppression.takeSummaries(Date.now(), async (raw) => {
972
+ const { frames, resolution, unresolvedGenerated } = await resolveSourceMaps(raw);
973
+ warnUnresolvedMaps(unresolvedGenerated);
974
+ const normalized = normalizeFrames(frames, { appRoot: repoRoot });
975
+ const stack_frames = framesToStackFrames(normalized);
976
+ const finalResolution = finalizeResolution(resolution, stack_frames);
977
+ if (finalResolution === "partial") warnPartialResolution();
978
+ return {
979
+ stack_frames,
980
+ resolution: finalResolution
981
+ };
982
+ });
983
+ for (const summary of summaries) {
984
+ transport.enqueueSummary(summary);
985
+ }
986
+ await transport.flush(timeoutMs);
987
+ } catch {
988
+ }
989
+ };
990
+ const close = async () => {
991
+ try {
992
+ await flush(2e3);
993
+ await transport.close();
994
+ } catch {
995
+ }
996
+ };
997
+ return { captureException, flush, close };
998
+ } catch {
999
+ return noopClient();
1000
+ }
1001
+ };
1002
+ export {
1003
+ announceDeploy,
1004
+ createClient,
1005
+ createIngestEvent,
1006
+ createSuppression,
1007
+ createTransport,
1008
+ detectRelease,
1009
+ isClientEnabled,
1010
+ normalizeFrames,
1011
+ parseStack,
1012
+ resolveSourceMaps
1013
+ };