@rasputin-ai/core 0.1.4 → 0.3.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.
Files changed (51) hide show
  1. package/dist/announce-deploy/announce-deploy.d.ts +2 -2
  2. package/dist/announce-deploy/announce-deploy.d.ts.map +1 -1
  3. package/dist/create-client/create-client-types.d.ts +47 -31
  4. package/dist/create-client/create-client-types.d.ts.map +1 -1
  5. package/dist/create-client/create-client.d.ts +1 -1
  6. package/dist/create-client/create-client.d.ts.map +1 -1
  7. package/dist/create-client/is-client-enabled.d.ts.map +1 -1
  8. package/dist/create-client/resolve-endpoint-urls.d.ts +1 -0
  9. package/dist/create-client/resolve-endpoint-urls.d.ts.map +1 -1
  10. package/dist/create-ingest-event/create-ingest-event-types.d.ts +6 -2
  11. package/dist/create-ingest-event/create-ingest-event-types.d.ts.map +1 -1
  12. package/dist/create-ingest-event/create-ingest-event.d.ts.map +1 -1
  13. package/dist/create-ingest-event/runtime-execution-state-types.d.ts +87 -0
  14. package/dist/create-ingest-event/runtime-execution-state-types.d.ts.map +1 -0
  15. package/dist/create-ingest-event/truncate-ingest-message.d.ts +3 -0
  16. package/dist/create-ingest-event/truncate-ingest-message.d.ts.map +1 -0
  17. package/dist/detect-release/detect-release.d.ts +6 -3
  18. package/dist/detect-release/detect-release.d.ts.map +1 -1
  19. package/dist/index.d.ts +9 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +503 -95
  22. package/dist/instrumentation-manifest/instrumentation-manifest-types.d.ts +75 -0
  23. package/dist/instrumentation-manifest/instrumentation-manifest-types.d.ts.map +1 -0
  24. package/dist/instrumentation-manifest/upload-instrumentation-manifest.d.ts +16 -0
  25. package/dist/instrumentation-manifest/upload-instrumentation-manifest.d.ts.map +1 -0
  26. package/dist/normalize-frames/normalize-frames.d.ts +3 -3
  27. package/dist/normalize-frames/normalize-frames.d.ts.map +1 -1
  28. package/dist/repo-root/repo-root-from.d.ts +8 -0
  29. package/dist/repo-root/repo-root-from.d.ts.map +1 -0
  30. package/dist/repo-root/resolve-repo-root.d.ts +6 -0
  31. package/dist/repo-root/resolve-repo-root.d.ts.map +1 -0
  32. package/dist/repo-root/warn-repo-root-setup.d.ts +5 -0
  33. package/dist/repo-root/warn-repo-root-setup.d.ts.map +1 -0
  34. package/dist/sdk-meta.d.ts +1 -1
  35. package/dist/suppression/compute-burst-key.d.ts +0 -4
  36. package/dist/suppression/compute-burst-key.d.ts.map +1 -1
  37. package/dist/suppression/create-suppression.d.ts +9 -2
  38. package/dist/suppression/create-suppression.d.ts.map +1 -1
  39. package/dist/suppression/identity-needs-message-template.d.ts +3 -1
  40. package/dist/suppression/identity-needs-message-template.d.ts.map +1 -1
  41. package/dist/suppression/parameterize-message.d.ts.map +1 -1
  42. package/dist/suppression/suppression-types.d.ts +2 -2
  43. package/dist/suppression/suppression-types.d.ts.map +1 -1
  44. package/dist/transport/create-transport.d.ts.map +1 -1
  45. package/dist/transport/send-batch.d.ts +1 -0
  46. package/dist/transport/send-batch.d.ts.map +1 -1
  47. package/dist/transport/transport-queue.d.ts +7 -0
  48. package/dist/transport/transport-queue.d.ts.map +1 -1
  49. package/dist/transport/transport-types.d.ts +17 -4
  50. package/dist/transport/transport-types.d.ts.map +1 -1
  51. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/announce-deploy/announce-deploy.ts
2
2
  var announceDeploy = async (options) => {
3
3
  if (options.enabled === false) return "skipped";
4
- const release = options.release?.trim();
4
+ const release = options.release?.trim() ?? "";
5
5
  const environment = options.environment.trim();
6
- if (!release || !environment) return "skipped";
6
+ if (!environment) return "skipped";
7
7
  const fetchFn = options.fetch ?? globalThis.fetch;
8
8
  try {
9
9
  const response = await fetchFn(options.deployUrl, {
@@ -22,15 +22,116 @@ var announceDeploy = async (options) => {
22
22
  }
23
23
  };
24
24
 
25
- // src/create-ingest-event/create-ingest-event.ts
25
+ // src/create-ingest-event/truncate-ingest-message.ts
26
26
  var MAX_MESSAGE_BYTES = 8 * 1024;
27
- var MAX_STACK_FRAMES = 50;
27
+ var utf8Bytes = (value) => Buffer.byteLength(value, "utf8");
28
28
  var truncateUtf8 = (value, maxBytes) => {
29
- if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
29
+ if (utf8Bytes(value) <= maxBytes) return value;
30
30
  let end = Math.min(value.length, maxBytes);
31
- while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > maxBytes) end--;
31
+ while (end > 0 && utf8Bytes(value.slice(0, end)) > maxBytes) end--;
32
32
  return value.slice(0, end);
33
33
  };
34
+ var truncation = (reason, extra = {}) => ({
35
+ __rasputin_truncated: true,
36
+ __reason: reason,
37
+ ...extra
38
+ });
39
+ var mapJson = (value, maxStringChars, maxArrayItems, maxObjectKeys) => {
40
+ if (typeof value === "string") {
41
+ if (value.length <= maxStringChars) return value;
42
+ if (maxStringChars <= 1) return value.slice(0, maxStringChars);
43
+ return `${value.slice(0, maxStringChars - 1)}\u2026`;
44
+ }
45
+ if (Array.isArray(value)) {
46
+ const keep = maxArrayItems === null ? value.length : Math.min(value.length, maxArrayItems);
47
+ const items = value.slice(0, keep).map((item) => mapJson(item, maxStringChars, maxArrayItems, maxObjectKeys));
48
+ if (keep < value.length) {
49
+ items.push(truncation("max_array_elements", { __original_length: value.length }));
50
+ }
51
+ return items;
52
+ }
53
+ if (value && typeof value === "object") {
54
+ const entries = Object.entries(value);
55
+ const keep = maxObjectKeys === null ? entries.length : Math.min(entries.length, maxObjectKeys);
56
+ const out = {};
57
+ for (const [key, child] of entries.slice(0, keep)) {
58
+ out[key] = mapJson(child, maxStringChars, maxArrayItems, maxObjectKeys);
59
+ }
60
+ if (keep < entries.length) {
61
+ Object.assign(out, truncation("max_object_keys", { __original_length: entries.length }));
62
+ }
63
+ return out;
64
+ }
65
+ return value;
66
+ };
67
+ var jsonIfFits = (value, maxBytes) => {
68
+ const json = JSON.stringify(value);
69
+ return utf8Bytes(json) <= maxBytes ? json : null;
70
+ };
71
+ var boundJsonMessage = (value, maxBytes) => {
72
+ const fit = (maxStringChars, maxArrayItems, maxObjectKeys) => jsonIfFits(mapJson(value, maxStringChars, maxArrayItems, maxObjectKeys), maxBytes);
73
+ const longestStringsThatFit = (maxArrayItems, maxObjectKeys) => {
74
+ let lo = 0;
75
+ let hi = maxBytes;
76
+ let best = null;
77
+ while (lo <= hi) {
78
+ const mid = lo + hi >> 1;
79
+ const candidate = fit(mid, maxArrayItems, maxObjectKeys);
80
+ if (candidate) {
81
+ best = candidate;
82
+ lo = mid + 1;
83
+ } else {
84
+ hi = mid - 1;
85
+ }
86
+ }
87
+ return best;
88
+ };
89
+ const withAllKeys = longestStringsThatFit(null, null);
90
+ if (withAllKeys) return withAllKeys;
91
+ const searchCount = (emptyFits, fillStrings) => {
92
+ let lo = 0;
93
+ let hi = 1;
94
+ while (!emptyFits(hi) && hi < 1048576) hi *= 2;
95
+ let best = 0;
96
+ while (lo <= hi) {
97
+ const mid = lo + hi >> 1;
98
+ if (emptyFits(mid)) {
99
+ best = mid;
100
+ lo = mid + 1;
101
+ } else {
102
+ hi = mid - 1;
103
+ }
104
+ }
105
+ return fillStrings(best);
106
+ };
107
+ const withFewerArrayItems = searchCount(
108
+ (count) => fit(0, count, null) !== null,
109
+ (count) => longestStringsThatFit(count, null)
110
+ );
111
+ if (withFewerArrayItems) return withFewerArrayItems;
112
+ return searchCount(
113
+ (count) => fit(0, 0, count) !== null,
114
+ (count) => longestStringsThatFit(0, count)
115
+ ) ?? JSON.stringify(
116
+ Array.isArray(value) ? [truncation("max_array_elements", { __original_length: value.length })] : truncation("max_object_keys", { __original_length: Object.keys(value).length })
117
+ );
118
+ };
119
+ var truncateIngestMessage = (value, maxBytes = MAX_MESSAGE_BYTES) => {
120
+ if (utf8Bytes(value) <= maxBytes) return value;
121
+ try {
122
+ const parsed = JSON.parse(value);
123
+ if (parsed !== null && typeof parsed === "object") {
124
+ const compact = JSON.stringify(parsed);
125
+ if (utf8Bytes(compact) <= maxBytes) return compact;
126
+ return boundJsonMessage(parsed, maxBytes);
127
+ }
128
+ } catch {
129
+ }
130
+ return truncateUtf8(value, maxBytes);
131
+ };
132
+
133
+ // src/create-ingest-event/create-ingest-event.ts
134
+ var MAX_STACK_FRAMES = 50;
34
135
  var errorFields = (error) => {
35
136
  if (error instanceof Error) {
36
137
  const code = "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? String(error.code) : void 0;
@@ -79,18 +180,19 @@ var createIngestEvent = (input) => {
79
180
  const occurred = input.occurredAt === void 0 ? (/* @__PURE__ */ new Date()).toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : input.occurredAt.toISOString();
80
181
  const event = {
81
182
  type: extracted.type,
82
- message: truncateUtf8(extracted.message, MAX_MESSAGE_BYTES),
183
+ message: truncateIngestMessage(extracted.message),
83
184
  stack_frames: framesToStackFrames(input.frames),
84
185
  occurred_at: occurred,
85
186
  environment: input.environment,
86
187
  origin: input.origin,
87
- resolution: input.resolution
188
+ resolution: input.resolution,
189
+ release: input.release
88
190
  };
89
191
  const code = input.code !== void 0 ? input.code : extracted.code;
90
192
  if (code !== void 0) event.code = code;
91
- if (input.release !== void 0) event.release = input.release;
92
193
  const request = sanitizeRequest(input.request);
93
194
  if (request) event.request = request;
195
+ if (input.runtimeState) event.runtime_state = input.runtimeState;
94
196
  return event;
95
197
  };
96
198
 
@@ -111,25 +213,62 @@ var finalizeResolution = (resolution, stackFrames) => {
111
213
  };
112
214
 
113
215
  // src/detect-release/detect-release.ts
216
+ import { readFileSync } from "node:fs";
217
+ import { join } from "node:path";
114
218
  var ENV_KEYS = [
219
+ "RASPUTIN_RELEASE",
115
220
  "VERCEL_GIT_COMMIT_SHA",
116
221
  "RAILWAY_GIT_COMMIT_SHA",
117
222
  "RENDER_GIT_COMMIT",
118
- "SOURCE_VERSION",
223
+ "COMMIT_REF",
224
+ "CF_PAGES_COMMIT_SHA",
225
+ "HEROKU_SLUG_COMMIT",
119
226
  "GIT_COMMIT",
120
227
  "COMMIT_SHA",
121
228
  "GIT_SHA"
122
229
  ];
230
+ var SHA_RE = /^[0-9a-f]{7,40}$/i;
231
+ var isSha = (value) => SHA_RE.test(value);
232
+ var warnInvalid = (source, value) => {
233
+ console.warn(
234
+ `[rasputin] Ignoring invalid release from ${source}: "${value}". Expected a git commit SHA (7\u201340 hex characters).`
235
+ );
236
+ };
237
+ var readGitHead = (cwd) => {
238
+ try {
239
+ const gitDir = join(cwd, ".git");
240
+ const head = readFileSync(join(gitDir, "HEAD"), "utf8").trim();
241
+ if (isSha(head)) return head;
242
+ const ref = head.replace(/^ref:\s*/, "");
243
+ try {
244
+ const sha = readFileSync(join(gitDir, ref), "utf8").trim();
245
+ return isSha(sha) ? sha : void 0;
246
+ } catch {
247
+ const packed = readFileSync(join(gitDir, "packed-refs"), "utf8");
248
+ const match = packed.match(new RegExp(`^([0-9a-f]{40}) ${ref}$`, "m"));
249
+ return match?.[1];
250
+ }
251
+ } catch {
252
+ return void 0;
253
+ }
254
+ };
123
255
  var detectRelease = (options = {}) => {
124
256
  const explicit = options.release?.trim();
125
- if (explicit) return explicit;
257
+ if (explicit) {
258
+ if (isSha(explicit)) return explicit;
259
+ warnInvalid("release option", explicit);
260
+ }
126
261
  const env = options.env ?? process.env;
127
262
  for (const key of ENV_KEYS) {
128
263
  const value = env[key]?.trim();
129
- if (value) return value;
264
+ if (!value) continue;
265
+ if (isSha(value)) return value;
266
+ warnInvalid(key, value);
130
267
  }
268
+ const fromGit = readGitHead(options.cwd ?? process.cwd());
269
+ if (fromGit) return fromGit;
131
270
  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."
271
+ "[rasputin] No release detected. Set `RASPUTIN_RELEASE` to your git commit SHA, pass `release` in RasputinInit, or deploy on a platform that exposes a commit SHA env var (e.g. VERCEL_GIT_COMMIT_SHA). Events are not sent without a release."
133
272
  );
134
273
  return void 0;
135
274
  };
@@ -168,8 +307,8 @@ var isInApp = (file, underRoot) => {
168
307
  if (/(^|\/)node_modules\//.test(file)) return false;
169
308
  return true;
170
309
  };
171
- var normalizeFrames = (frames, options = {}) => {
172
- const appRoot = options.appRoot ?? process.cwd();
310
+ var normalizeFrames = (frames, options) => {
311
+ const appRoot = options.appRoot;
173
312
  return frames.map((frame) => {
174
313
  const generated = normalizeFile(frame.generated.file, appRoot);
175
314
  const original = frame.original ? normalizeFile(frame.original.file, appRoot) : void 0;
@@ -250,10 +389,93 @@ var parseStack = (stack) => {
250
389
  return frames;
251
390
  };
252
391
 
392
+ // src/repo-root/repo-root-from.ts
393
+ import { existsSync } from "node:fs";
394
+ import { dirname, join as join2, resolve as resolve2 } from "node:path";
395
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
396
+ var isGitRoot = (dir) => existsSync(join2(dir, ".git"));
397
+ var hasPackageJson = (dir) => existsSync(join2(dir, "package.json"));
398
+ var repoRootFrom = (moduleUrl) => {
399
+ let current = resolve2(dirname(fileURLToPath2(moduleUrl)));
400
+ let packageRoot;
401
+ while (true) {
402
+ if (isGitRoot(current)) return current;
403
+ if (hasPackageJson(current)) packageRoot = current;
404
+ const parent = dirname(current);
405
+ if (parent === current) break;
406
+ current = parent;
407
+ }
408
+ return packageRoot ?? resolve2(dirname(fileURLToPath2(moduleUrl)));
409
+ };
410
+
411
+ // src/repo-root/resolve-repo-root.ts
412
+ var resolveRepoRoot = (options) => {
413
+ const repoRoot = options.repoRoot?.trim();
414
+ if (repoRoot) return repoRoot;
415
+ const moduleUrl = options.moduleUrl?.trim();
416
+ if (moduleUrl) return repoRootFrom(moduleUrl);
417
+ return void 0;
418
+ };
419
+
420
+ // src/repo-root/warn-repo-root-setup.ts
421
+ var BUILD_OUTPUT_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "out", ".next", "coverage"]);
422
+ var warnedMissing = false;
423
+ var warnedSuspicious = false;
424
+ var BANNER = "======== [rasputin] WARNING";
425
+ var looksLikeBuildOutputRoot = (root) => {
426
+ const posix = root.replaceAll("\\", "/").replace(/\/+$/, "");
427
+ return posix.split("/").some((segment) => BUILD_OUTPUT_SEGMENTS.has(segment));
428
+ };
429
+ var warnIfMissingRepoRoot = () => {
430
+ if (warnedMissing) return;
431
+ warnedMissing = true;
432
+ console.warn(
433
+ `
434
+
435
+ ${BANNER}: we don't know where your project is ========
436
+ Rasputin needs to know which folder your app lives in so errors show
437
+ the right file paths in the dashboard.
438
+
439
+ Add this to RasputinInit (from the same file you call it in):
440
+
441
+ moduleUrl: import.meta.url
442
+
443
+ Or set repoRoot to the folder you open in your editor.
444
+
445
+ Nothing will be sent until this is set.
446
+ ======================================================================
447
+ `
448
+ );
449
+ };
450
+ var warnIfSuspiciousRepoRoot = (root) => {
451
+ if (warnedSuspicious || !looksLikeBuildOutputRoot(root)) return;
452
+ warnedSuspicious = true;
453
+ console.warn(
454
+ `
455
+
456
+ ${BANNER}: this looks like a build folder, not your project ========
457
+ Rasputin thinks your project is here:
458
+ ${root}
459
+
460
+ That's usually compiled output (dist, build, .next, \u2026), not the folder
461
+ you write code in. Error locations in the dashboard will be wrong.
462
+
463
+ Point Rasputin at the folder you open in your editor.
464
+
465
+ Easiest fix \u2014 from the file where you call RasputinInit:
466
+
467
+ moduleUrl: import.meta.url
468
+
469
+ Or set repoRoot yourself.
470
+ ======================================================================
471
+ `
472
+ );
473
+ };
474
+
253
475
  // src/resolve-source-maps/resolve-source-maps.ts
254
476
  import { readFile } from "node:fs/promises";
255
- import { dirname, isAbsolute as isAbsolute2, join } from "node:path";
256
- import { fileURLToPath as fileURLToPath2 } from "node:url";
477
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
478
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
257
479
  import { LEAST_UPPER_BOUND, originalPositionFor, TraceMap } from "@jridgewell/trace-mapping";
258
480
  var SOURCE_EXT2 = /\.(ts|tsx|mts|cts)$/i;
259
481
  var SOURCEMAP_COMMENT = /(?:\/\/[#@][ \t]*sourceMappingURL=([^\s'"]+)|\/\*[#@][ \t]*sourceMappingURL=([^\s*'"]+)[ \t]*\*\/)\s*$/;
@@ -261,7 +483,7 @@ var mapCache = /* @__PURE__ */ new Map();
261
483
  var toPath = (file) => {
262
484
  if (file.startsWith("file://")) {
263
485
  try {
264
- return fileURLToPath2(file);
486
+ return fileURLToPath3(file);
265
487
  } catch {
266
488
  return file;
267
489
  }
@@ -302,7 +524,7 @@ var readMapPayload = async (generatedPath) => {
302
524
  if (comment?.startsWith("data:")) return decodeDataUrl(comment);
303
525
  if (comment) {
304
526
  try {
305
- return await readFile(join(dirname(generatedPath), comment), "utf8");
527
+ return await readFile(join3(dirname2(generatedPath), comment), "utf8");
306
528
  } catch {
307
529
  }
308
530
  }
@@ -349,7 +571,7 @@ var resolveFrame = async (raw) => {
349
571
  return {
350
572
  ...base,
351
573
  original: {
352
- file: isAbsolute2(pos.source) ? pos.source : join(dirname(generatedPath), pos.source),
574
+ file: isAbsolute2(pos.source) ? pos.source : join3(dirname2(generatedPath), pos.source),
353
575
  line: pos.line,
354
576
  column: pos.column ?? void 0,
355
577
  name: pos.name ?? void 0
@@ -369,7 +591,7 @@ var resolveSourceMaps = async (frames) => {
369
591
 
370
592
  // src/sdk-meta.ts
371
593
  var SDK_NAME = "@rasputin-ai/core";
372
- var SDK_VERSION = "0.1.4";
594
+ var SDK_VERSION = "0.3.0";
373
595
 
374
596
  // src/suppression/identity-needs-message-template.ts
375
597
  var identityNeedsMessageTemplate = (type, code) => {
@@ -431,14 +653,22 @@ var RULES = [
431
653
  pattern: /\b(?=[a-z0-9]*[a-z])(?=[a-z0-9]*\d)[a-z0-9]{8,}\b/giu
432
654
  }
433
655
  ];
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();
656
+ var normalizeForIdentity = (normalized) => {
657
+ try {
658
+ return JSON.stringify(JSON.parse(normalized));
659
+ } catch {
660
+ }
437
661
  const lines = normalized.split("\n").filter((line) => line.trim());
438
662
  let result = lines.slice(0, 2).join("\n");
439
663
  if (result !== normalized) {
440
664
  result += "...";
441
665
  }
666
+ return result;
667
+ };
668
+ var parameterizeMessage = (rawMessage) => {
669
+ if (rawMessage.length > MAX_INPUT_LENGTH) return rawMessage;
670
+ const normalized = rawMessage.normalize("NFKC").replace(/\r\n?/g, "\n").trim();
671
+ let result = normalizeForIdentity(normalized);
442
672
  result = result.replace(/\b([A-Za-z_][\w.-]*)=(["'])(?:\\.|(?!\2).)*\2/gu, "$1=<string>");
443
673
  result = result.replace(/\b([A-Za-z_][\w.-]*)=(?:true|false)\b/giu, "$1=<bool>");
444
674
  for (const rule of RULES) {
@@ -454,21 +684,43 @@ var parameterizeMessage = (rawMessage) => {
454
684
  };
455
685
 
456
686
  // src/suppression/compute-burst-key.ts
687
+ var FRAME_COUNT = 4;
457
688
  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) => {
689
+ var normalizeFunction = (raw) => {
690
+ if (!raw) return "";
691
+ const name = raw.trim();
692
+ if (name === "<anonymous>" || name === "anonymous") return "";
693
+ return name.replace(/^(async|new|get|set|bound)\s+/, "").replace(/^Object\./, "").replace(/\s+\[as .+\]$/, "");
694
+ };
695
+ var collapseConsecutive = (frames) => {
696
+ const out = [];
697
+ let previousKey = null;
698
+ for (const frame of frames) {
460
699
  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}`;
700
+ const fn = normalizeFunction(frame.function ?? frame.original?.name);
701
+ const key = `${file}#${fn}`;
702
+ if (key !== previousKey) {
703
+ out.push(frame);
704
+ previousKey = key;
464
705
  }
706
+ }
707
+ return out;
708
+ };
709
+ var frameBurstParts = (frames) => {
710
+ const collapsed = collapseConsecutive(frames);
711
+ const inApp = collapsed.filter((frame) => frame.in_app);
712
+ const selected = (inApp.length > 0 ? inApp : collapsed).slice(0, FRAME_COUNT);
713
+ return selected.map((frame) => {
714
+ const file = preferredFile(frame);
715
+ const fn = normalizeFunction(frame.function ?? frame.original?.name);
465
716
  return `${file}:${fn}`;
466
717
  });
467
718
  };
468
719
  var computeBurstKey = (errorType, message, code, frames) => {
469
720
  const parts = frameBurstParts(frames);
470
- let key = `${errorType}\0${parts.join("\0")}`;
471
- if (identityNeedsMessageTemplate(errorType, code)) {
721
+ const codePart = code ?? "";
722
+ let key = `${errorType}\0${codePart}\0${parts.join("\0")}`;
723
+ if (parts.length === 0 || identityNeedsMessageTemplate(errorType, code)) {
472
724
  key += `\0${parameterizeMessage(message)}`;
473
725
  }
474
726
  return key;
@@ -537,7 +789,7 @@ var createSuppression = (options = {}) => {
537
789
  const identity = captureIdentity(input);
538
790
  const captureRawFrames = (input.rawFrames ?? []).slice(0, MAX_STACK_FRAMES);
539
791
  const burstKey = computeBurstKey(input.errorType, input.message, input.code, input.frames);
540
- const release = input.release ?? null;
792
+ const release = input.release;
541
793
  const route = input.route ?? null;
542
794
  let bucket = burstBuckets.get(burstKey);
543
795
  if (!bucket) {
@@ -579,7 +831,7 @@ var createSuppression = (options = {}) => {
579
831
  bucket.suppressed += 1;
580
832
  return { action: "suppress" };
581
833
  };
582
- const takeSummaries = async (now = Date.now(), prepareSummary) => {
834
+ const drainSummaries = (now = Date.now()) => {
583
835
  const drafts = pending.splice(0, pending.length);
584
836
  for (const bucket of burstBuckets.values()) {
585
837
  if (bucket.suppressed > 0) {
@@ -587,22 +839,32 @@ var createSuppression = (options = {}) => {
587
839
  bucket.suppressed = 0;
588
840
  }
589
841
  }
842
+ return drafts.map((draft) => ({
843
+ summary: toSummary(draft, draft.identity),
844
+ rawFrames: draft.captureRawFrames
845
+ }));
846
+ };
847
+ const takeSummaries = async (now = Date.now(), prepareSummary) => {
848
+ const drafts = drainSummaries(now);
849
+ if (!prepareSummary) return drafts.map((draft) => draft.summary);
590
850
  const out = [];
591
851
  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
- }
852
+ if (draft.rawFrames.length === 0) {
853
+ out.push(draft.summary);
854
+ continue;
855
+ }
856
+ try {
857
+ const resolved = await prepareSummary(draft.rawFrames);
858
+ out.push({ ...draft.summary, ...resolved });
859
+ } catch {
860
+ out.push(draft.summary);
599
861
  }
600
- out.push(toSummary(draft, identity));
601
862
  }
602
863
  return out;
603
864
  };
604
865
  return {
605
866
  decide,
867
+ drainSummaries,
606
868
  takeSummaries,
607
869
  /** Test helper — number of active burst buckets. */
608
870
  size: () => burstBuckets.size
@@ -611,7 +873,21 @@ var createSuppression = (options = {}) => {
611
873
 
612
874
  // src/transport/send-batch.ts
613
875
  import { gzipSync } from "node:zlib";
614
- var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
876
+ var sleep = (ms, signal) => new Promise((resolve3) => {
877
+ if (signal?.aborted) {
878
+ resolve3();
879
+ return;
880
+ }
881
+ const timer = setTimeout(resolve3, ms);
882
+ signal?.addEventListener(
883
+ "abort",
884
+ () => {
885
+ clearTimeout(timer);
886
+ resolve3();
887
+ },
888
+ { once: true }
889
+ );
890
+ });
615
891
  var backoffMs = (attempt) => {
616
892
  const base = Math.min(3e4, 500 * 2 ** attempt);
617
893
  return Math.floor(base * (0.5 + Math.random()));
@@ -634,6 +910,7 @@ var sendBatch = async (options) => {
634
910
  }
635
911
  if (options.sdkVersion) payload.sdk_version = options.sdkVersion;
636
912
  if (options.sdkName) payload.sdk_name = options.sdkName;
913
+ if (options.idempotencyKey) payload.idempotency_key = options.idempotencyKey;
637
914
  const json = JSON.stringify(payload);
638
915
  const headers = {
639
916
  Authorization: `Bearer ${options.projectApiKey}`,
@@ -661,16 +938,16 @@ var sendBatch = async (options) => {
661
938
  if (response.status === 429) {
662
939
  if (attempt === options.maxRetries) return "failed";
663
940
  const wait = retryAfterMs(response) ?? backoffMs(attempt);
664
- await sleep(wait);
941
+ await sleep(wait, options.signal);
665
942
  continue;
666
943
  }
667
944
  if (response.status >= 400 && response.status < 500) return "dropped";
668
945
  if (attempt === options.maxRetries) return "failed";
669
- await sleep(backoffMs(attempt));
946
+ await sleep(backoffMs(attempt), options.signal);
670
947
  } catch {
671
948
  if (options.signal?.aborted) return "failed";
672
949
  if (attempt === options.maxRetries) return "failed";
673
- await sleep(backoffMs(attempt));
950
+ await sleep(backoffMs(attempt), options.signal);
674
951
  }
675
952
  }
676
953
  return "failed";
@@ -684,6 +961,9 @@ var RANK = {
684
961
  };
685
962
  var createQueue = (maxSize) => {
686
963
  const items = [];
964
+ let enqueued = 0;
965
+ let droppedIncoming = 0;
966
+ let droppedQueued = 0;
687
967
  const oldestLowestIndex = () => {
688
968
  let best = 0;
689
969
  for (let i = 1; i < items.length; i++) {
@@ -699,16 +979,28 @@ var createQueue = (maxSize) => {
699
979
  const victimIndex = oldestLowestIndex();
700
980
  const victim = items[victimIndex];
701
981
  if (!victim) break;
702
- if (RANK[item.priority] < RANK[victim.priority]) return;
982
+ if (RANK[item.priority] < RANK[victim.priority]) {
983
+ droppedIncoming++;
984
+ return;
985
+ }
703
986
  items.splice(victimIndex, 1);
987
+ droppedQueued++;
704
988
  }
705
989
  items.push(item);
990
+ enqueued++;
706
991
  };
707
992
  const drain = (count) => items.splice(0, count);
708
993
  return {
709
994
  enqueue,
710
995
  drain,
711
996
  size: () => items.length,
997
+ getStats: () => ({
998
+ queued: items.length,
999
+ enqueued,
1000
+ droppedIncoming,
1001
+ droppedQueued,
1002
+ droppedTotal: droppedIncoming + droppedQueued
1003
+ }),
712
1004
  clear: () => {
713
1005
  items.length = 0;
714
1006
  }
@@ -725,6 +1017,20 @@ var DEFAULTS2 = {
725
1017
  maxRetries: 5,
726
1018
  installSignalHandlers: true
727
1019
  };
1020
+ var removeProcessListener = process.off.bind(process);
1021
+ var holdProcessExitUntil = (done) => {
1022
+ const nativeExit = process.exit.bind(process);
1023
+ let heldCode;
1024
+ const restore = () => {
1025
+ process.exit = nativeExit;
1026
+ if (heldCode !== void 0) nativeExit(heldCode);
1027
+ };
1028
+ const holdExit = ((code) => {
1029
+ heldCode = code ?? 0;
1030
+ });
1031
+ process.exit = holdExit;
1032
+ void done.finally(restore);
1033
+ };
728
1034
  var createTransport = (options) => {
729
1035
  const enabled = options.enabled ?? DEFAULTS2.enabled;
730
1036
  const batchSize = options.batchSize ?? DEFAULTS2.batchSize;
@@ -736,6 +1042,8 @@ var createTransport = (options) => {
736
1042
  let timer;
737
1043
  let flushing;
738
1044
  let closed = false;
1045
+ let shuttingDown = false;
1046
+ let retryBatch = null;
739
1047
  const schedule = () => {
740
1048
  if (timer || !enabled || closed) return;
741
1049
  timer = setInterval(() => {
@@ -748,9 +1056,19 @@ var createTransport = (options) => {
748
1056
  clearInterval(timer);
749
1057
  timer = void 0;
750
1058
  };
1059
+ const prepareSummary = async (item) => {
1060
+ if (!options.prepareSummary || item.rawFrames.length === 0) return item.summary;
1061
+ try {
1062
+ const resolved = await options.prepareSummary(item.rawFrames);
1063
+ return { ...item.summary, ...resolved };
1064
+ } catch {
1065
+ return item.summary;
1066
+ }
1067
+ };
751
1068
  const flush = async (timeoutMs = 1e4) => {
752
1069
  if (!enabled || closed) {
753
1070
  queue.clear();
1071
+ retryBatch = null;
754
1072
  return;
755
1073
  }
756
1074
  if (flushing) return flushing;
@@ -758,15 +1076,17 @@ var createTransport = (options) => {
758
1076
  const controller = new AbortController();
759
1077
  const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
760
1078
  try {
761
- while (queue.size() > 0) {
1079
+ while (queue.size() > 0 || retryBatch) {
762
1080
  if (controller.signal.aborted) break;
763
- const batch = queue.drain(batchSize);
1081
+ const batch = retryBatch?.items ?? queue.drain(batchSize);
1082
+ const idempotencyKey = retryBatch?.idempotencyKey ?? crypto.randomUUID();
1083
+ retryBatch = null;
764
1084
  if (batch.length === 0) break;
765
1085
  const events = [];
766
1086
  const summaries = [];
767
1087
  for (const item of batch) {
768
1088
  if (item.kind === "summary") {
769
- summaries.push(item.summary);
1089
+ summaries.push(await prepareSummary(item));
770
1090
  continue;
771
1091
  }
772
1092
  if (options.prepareEvent && item.rawFrames.length > 0) {
@@ -789,6 +1109,7 @@ var createTransport = (options) => {
789
1109
  projectApiKey: options.projectApiKey,
790
1110
  events,
791
1111
  summaries,
1112
+ idempotencyKey,
792
1113
  sdkVersion: options.sdkVersion,
793
1114
  sdkName: options.sdkName,
794
1115
  gzipThresholdBytes,
@@ -797,13 +1118,13 @@ var createTransport = (options) => {
797
1118
  signal: controller.signal
798
1119
  });
799
1120
  if (result === "failed") {
800
- for (const item of batch) queue.enqueue(item);
1121
+ retryBatch = { items: batch, idempotencyKey };
801
1122
  break;
802
1123
  }
803
1124
  }
804
1125
  } finally {
805
1126
  if (timeout) clearTimeout(timeout);
806
- if (queue.size() === 0) stopTimer();
1127
+ if (queue.size() === 0 && !retryBatch) stopTimer();
807
1128
  flushing = void 0;
808
1129
  }
809
1130
  })();
@@ -815,38 +1136,53 @@ var createTransport = (options) => {
815
1136
  schedule();
816
1137
  if (queue.size() >= batchSize) void flush();
817
1138
  };
818
- const enqueueSummary = (summary) => {
1139
+ const enqueueSummary = (summary, rawFrames = []) => {
819
1140
  if (!enabled || closed) return;
820
- queue.enqueue({ kind: "summary", summary, priority: "summary" });
1141
+ queue.enqueue({ kind: "summary", summary, priority: "summary", rawFrames });
821
1142
  schedule();
822
1143
  if (queue.size() >= batchSize) void flush();
823
1144
  };
1145
+ const onBeforeExit = () => {
1146
+ if (closed || shuttingDown) return;
1147
+ if (queue.size() === 0 && !retryBatch && !flushing) return;
1148
+ void flush(2e3).catch(() => {
1149
+ });
1150
+ };
824
1151
  const onSignal = () => {
825
- try {
826
- void flush(2e3);
827
- } catch {
828
- }
1152
+ if (shuttingDown) return;
1153
+ shuttingDown = true;
1154
+ stopTimer();
1155
+ holdProcessExitUntil(flush(2e3).catch(() => {
1156
+ }));
829
1157
  };
830
1158
  if (enabled && (options.installSignalHandlers ?? DEFAULTS2.installSignalHandlers)) {
831
- process.on("beforeExit", onSignal);
1159
+ process.on("beforeExit", onBeforeExit);
832
1160
  process.on("SIGTERM", onSignal);
833
1161
  process.on("SIGINT", onSignal);
834
1162
  }
835
1163
  const close = async () => {
836
- closed = true;
837
1164
  stopTimer();
838
- process.off("beforeExit", onSignal);
839
- process.off("SIGTERM", onSignal);
840
- process.off("SIGINT", onSignal);
1165
+ removeProcessListener("beforeExit", onBeforeExit);
1166
+ removeProcessListener("SIGTERM", onSignal);
1167
+ removeProcessListener("SIGINT", onSignal);
841
1168
  await flush(2e3);
1169
+ closed = true;
842
1170
  queue.clear();
1171
+ retryBatch = null;
843
1172
  };
844
1173
  return {
845
1174
  enqueue,
846
1175
  enqueueSummary,
847
1176
  flush,
848
1177
  close,
849
- size: () => queue.size()
1178
+ size: () => queue.size() + (retryBatch?.items.length ?? 0),
1179
+ getStats: () => {
1180
+ const queueStats = queue.getStats();
1181
+ return {
1182
+ ...queueStats,
1183
+ queued: queueStats.queued + (retryBatch?.items.length ?? 0)
1184
+ };
1185
+ }
850
1186
  };
851
1187
  };
852
1188
 
@@ -858,7 +1194,16 @@ var noopClient = () => ({
858
1194
  flush: async () => {
859
1195
  },
860
1196
  close: async () => {
861
- }
1197
+ },
1198
+ getStats: () => ({
1199
+ transport: {
1200
+ queued: 0,
1201
+ enqueued: 0,
1202
+ droppedIncoming: 0,
1203
+ droppedQueued: 0,
1204
+ droppedTotal: 0
1205
+ }
1206
+ })
862
1207
  });
863
1208
 
864
1209
  // src/create-client/resolve-endpoint-urls.ts
@@ -867,7 +1212,8 @@ var resolveEndpointUrls = (apiUrl = DEFAULT_API_URL) => {
867
1212
  const base = apiUrl.replace(/\/+$/, "");
868
1213
  return {
869
1214
  ingestUrl: `${base}/ingest`,
870
- deployUrl: `${base}/deploy`
1215
+ deployUrl: `${base}/deploy`,
1216
+ instrumentationManifestUrl: `${base}/instrumentationManifest`
871
1217
  };
872
1218
  };
873
1219
 
@@ -888,12 +1234,6 @@ var warnPartialResolution = () => {
888
1234
  "[rasputin] Source map paths could not be normalized to the repo root; fingerprinting may be degraded"
889
1235
  );
890
1236
  };
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
1237
  var toResolvedFrames = (raw) => raw.map((frame) => ({
898
1238
  generated: { file: frame.file, line: frame.line, column: frame.column },
899
1239
  ...frame.function ? { function: frame.function } : {}
@@ -909,7 +1249,46 @@ var createClient = (options, hooks = {}) => {
909
1249
  const release = detectRelease({ release: options.release });
910
1250
  const environment = options.environment;
911
1251
  const { ingestUrl, deployUrl } = resolveEndpointUrls(options.apiUrl);
912
- const repoRoot = options.repoRoot;
1252
+ if (!release) {
1253
+ if (hooks.announceDeploy ?? true) {
1254
+ void announceDeploy({
1255
+ deployUrl,
1256
+ projectApiKey,
1257
+ environment,
1258
+ fetch: hooks.fetch
1259
+ });
1260
+ }
1261
+ return noopClient();
1262
+ }
1263
+ const repoRoot = resolveRepoRoot(options);
1264
+ if (!repoRoot) {
1265
+ warnIfMissingRepoRoot();
1266
+ return noopClient();
1267
+ }
1268
+ warnIfSuspiciousRepoRoot(repoRoot);
1269
+ const MAX_STACK_RESOLVE_CACHE = 32;
1270
+ const stackResolveCache = /* @__PURE__ */ new Map();
1271
+ const stackCacheKey = (rawFrames) => rawFrames.map((frame) => `${frame.file}\0${frame.line}\0${frame.column}\0${frame.function ?? ""}`).join("\n");
1272
+ const resolveStack = async (rawFrames) => {
1273
+ const key = stackCacheKey(rawFrames);
1274
+ const cached = stackResolveCache.get(key);
1275
+ if (cached) return cached;
1276
+ const pending = (async () => {
1277
+ const { frames, resolution, unresolvedGenerated } = await resolveSourceMaps(rawFrames);
1278
+ warnUnresolvedMaps(unresolvedGenerated);
1279
+ const normalized = normalizeFrames(frames, { appRoot: repoRoot });
1280
+ const stack_frames = framesToStackFrames(normalized);
1281
+ const finalResolution = finalizeResolution(resolution, stack_frames);
1282
+ if (finalResolution === "partial") warnPartialResolution();
1283
+ return { frames: normalized, stack_frames, resolution: finalResolution };
1284
+ })();
1285
+ stackResolveCache.set(key, pending);
1286
+ if (stackResolveCache.size > MAX_STACK_RESOLVE_CACHE) {
1287
+ const oldest = stackResolveCache.keys().next().value;
1288
+ if (oldest !== void 0) stackResolveCache.delete(oldest);
1289
+ }
1290
+ return pending;
1291
+ };
913
1292
  const transport = createTransport({
914
1293
  ingestUrl,
915
1294
  projectApiKey,
@@ -919,10 +1298,12 @@ var createClient = (options, hooks = {}) => {
919
1298
  sdkVersion: hooks.sdkVersion ?? SDK_VERSION,
920
1299
  sdkName: hooks.sdkName ?? SDK_NAME,
921
1300
  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);
1301
+ const prepared = await resolveStack(rawFrames);
1302
+ return withResolvedFrames(event, prepared.frames, prepared.resolution);
1303
+ },
1304
+ prepareSummary: async (rawFrames) => {
1305
+ const prepared = await resolveStack(rawFrames);
1306
+ return { stack_frames: prepared.stack_frames, resolution: prepared.resolution };
926
1307
  }
927
1308
  });
928
1309
  const suppression = createSuppression();
@@ -947,7 +1328,8 @@ var createClient = (options, hooks = {}) => {
947
1328
  release,
948
1329
  origin: "server",
949
1330
  resolution: "none",
950
- request: context?.request
1331
+ request: context?.request,
1332
+ runtimeState: context?.runtimeState
951
1333
  });
952
1334
  const decision = suppression.decide({
953
1335
  errorType: event.type,
@@ -966,40 +1348,62 @@ var createClient = (options, hooks = {}) => {
966
1348
  } catch {
967
1349
  }
968
1350
  };
1351
+ const enqueuePendingSummaries = () => {
1352
+ for (const { summary, rawFrames } of suppression.drainSummaries()) {
1353
+ transport.enqueueSummary(summary, rawFrames);
1354
+ }
1355
+ };
969
1356
  const flush = async (timeoutMs) => {
970
1357
  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
- }
1358
+ enqueuePendingSummaries();
986
1359
  await transport.flush(timeoutMs);
987
1360
  } catch {
988
1361
  }
989
1362
  };
990
1363
  const close = async () => {
991
1364
  try {
992
- await flush(2e3);
1365
+ enqueuePendingSummaries();
993
1366
  await transport.close();
994
1367
  } catch {
995
1368
  }
996
1369
  };
997
- return { captureException, flush, close };
1370
+ return {
1371
+ captureException,
1372
+ flush,
1373
+ close,
1374
+ getStats: () => ({ transport: transport.getStats() })
1375
+ };
998
1376
  } catch {
999
1377
  return noopClient();
1000
1378
  }
1001
1379
  };
1380
+
1381
+ // src/instrumentation-manifest/instrumentation-manifest-types.ts
1382
+ var INSTRUMENTATION_MANIFEST_SCHEMA_VERSION = 1;
1383
+
1384
+ // src/instrumentation-manifest/upload-instrumentation-manifest.ts
1385
+ var uploadInstrumentationManifest = async (options) => {
1386
+ const release = options.release.trim();
1387
+ if (!release) return "failed";
1388
+ const fetchFn = options.fetch ?? globalThis.fetch;
1389
+ try {
1390
+ const response = await fetchFn(options.url, {
1391
+ method: "POST",
1392
+ headers: {
1393
+ Authorization: `Bearer ${options.projectApiKey}`,
1394
+ "Content-Type": "application/json",
1395
+ Accept: "application/json"
1396
+ },
1397
+ body: JSON.stringify({ release, manifest: options.manifest })
1398
+ });
1399
+ if (response.ok) return "ok";
1400
+ return "failed";
1401
+ } catch {
1402
+ return "failed";
1403
+ }
1404
+ };
1002
1405
  export {
1406
+ INSTRUMENTATION_MANIFEST_SCHEMA_VERSION,
1003
1407
  announceDeploy,
1004
1408
  createClient,
1005
1409
  createIngestEvent,
@@ -1009,5 +1413,9 @@ export {
1009
1413
  isClientEnabled,
1010
1414
  normalizeFrames,
1011
1415
  parseStack,
1012
- resolveSourceMaps
1416
+ repoRootFrom,
1417
+ resolveEndpointUrls,
1418
+ resolveRepoRoot,
1419
+ resolveSourceMaps,
1420
+ uploadInstrumentationManifest
1013
1421
  };