@guidelytech/node 0.16.0 → 0.17.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/dist/index.js CHANGED
@@ -46,6 +46,11 @@ function id(name, value, required) {
46
46
  if (length(s) > LIMITS.maxIdLength) throw new InvalidMessageError(`${name} is longer than ${LIMITS.maxIdLength} characters`);
47
47
  return s;
48
48
  }
49
+ function version(value) {
50
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
51
+ const s = String(value).trim();
52
+ return s ? s.slice(0, 200) : void 0;
53
+ }
49
54
  function isPlainObject(v) {
50
55
  if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
51
56
  const proto = Object.getPrototypeOf(v);
@@ -56,11 +61,11 @@ function object(name, value) {
56
61
  if (!isPlainObject(value)) throw new InvalidMessageError(`${name} must be an object`);
57
62
  return value;
58
63
  }
59
- function buildMessage(type, args, now = () => /* @__PURE__ */ new Date()) {
60
- if (!isPlainObject(args)) throw new InvalidMessageError(`${type}() needs an object argument`);
64
+ function buildMessage(type2, args, now = () => /* @__PURE__ */ new Date()) {
65
+ if (!isPlainObject(args)) throw new InvalidMessageError(`${type2}() needs an object argument`);
61
66
  const timestamp = args.timestamp === void 0 || args.timestamp === null ? now().toISOString() : formatTimestamp(args.timestamp);
62
- const base = { type, messageId: newMessageId(), timestamp };
63
- switch (type) {
67
+ const base = { type: type2, messageId: newMessageId(), timestamp };
68
+ switch (type2) {
64
69
  case "identify": {
65
70
  const userId = id("userId", args.userId, true);
66
71
  const traits = object("traits", args.traits);
@@ -76,13 +81,15 @@ function buildMessage(type, args, now = () => /* @__PURE__ */ new Date()) {
76
81
  if (length(event) > LIMITS.maxEventLength) throw new InvalidMessageError(`event is longer than ${LIMITS.maxEventLength} characters`);
77
82
  const properties = object("properties", args.properties);
78
83
  const accountId = id("accountId", args.accountId, false);
84
+ const appVersion = version(args.appVersion);
79
85
  return {
80
86
  ...base,
81
87
  ...userId ? { userId } : {},
82
88
  ...anonymousId ? { anonymousId } : {},
83
89
  event,
84
90
  ...properties ? { properties } : {},
85
- ...accountId ? { accountId } : {}
91
+ ...accountId ? { accountId } : {},
92
+ ...appVersion ? { appVersion } : {}
86
93
  };
87
94
  }
88
95
  case "group": {
@@ -115,12 +122,1249 @@ function jitter(delayMs, random) {
115
122
  return Math.round(delayMs * (0.8 + 0.4 * random));
116
123
  }
117
124
 
125
+ // src/errors/event.ts
126
+ import { randomUUID } from "crypto";
127
+ import { readFileSync } from "fs";
128
+ import * as os from "os";
129
+
130
+ // src/errors/normalize.ts
131
+ var SENSITIVE_KEY = /(password|passwd|secret|token|credit|card|cvv|ssn|iban|authorization|cookie|api[-_]?key|private[-_]?key)/i;
132
+ var SENSITIVE_HEADER = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-auth-token|x-csrf-token|x-xsrf-token|x-guidely-key|x-guidely-conversation)$|secret|token|password|passwd|api[-_]?key|signature|session/i;
133
+ var SENSITIVE_PARAM = /(token|key|secret|password|passwd|pwd|auth|session|code|sig|signature|credential|ssn|card|cvv|iban)/i;
134
+ var IP_HEADER = /^(x-forwarded-for|x-real-ip|forwarded|cf-connecting-ip|true-client-ip|x-client-ip)$/i;
135
+ var FILTERED = "[Filtered]";
136
+ var MAX_STRING = 2e3;
137
+ var MAX_KEYS = 100;
138
+ var MAX_ITEMS = 100;
139
+ function normalize(value, depth = 4, seen = /* @__PURE__ */ new WeakSet()) {
140
+ switch (typeof value) {
141
+ case "string":
142
+ return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}\u2026` : value;
143
+ case "number":
144
+ return Number.isFinite(value) ? value : String(value);
145
+ case "boolean":
146
+ return value;
147
+ case "bigint":
148
+ return `${value}n`;
149
+ case "symbol":
150
+ return value.toString();
151
+ case "function":
152
+ return `[Function${value.name ? `: ${value.name}` : ""}]`;
153
+ case "undefined":
154
+ return void 0;
155
+ }
156
+ if (value === null) return null;
157
+ const obj = value;
158
+ if (obj instanceof Date) return Number.isNaN(obj.getTime()) ? "Invalid Date" : obj.toISOString();
159
+ if (obj instanceof Error) return `${obj.name}: ${obj.message}`.slice(0, MAX_STRING);
160
+ if (obj instanceof RegExp) return obj.toString();
161
+ if (ArrayBuffer.isView(obj)) return `[${obj.constructor?.name ?? "Buffer"} ${obj.byteLength} bytes]`;
162
+ if (seen.has(obj)) return "[Circular]";
163
+ if (depth <= 0) return Array.isArray(obj) ? "[Array]" : "[Object]";
164
+ seen.add(obj);
165
+ try {
166
+ if (Array.isArray(obj) || obj instanceof Set) {
167
+ const items = Array.isArray(obj) ? obj : [...obj];
168
+ const out2 = items.slice(0, MAX_ITEMS).map((v) => normalize(v, depth - 1, seen) ?? null);
169
+ if (items.length > MAX_ITEMS) out2.push(`[${items.length - MAX_ITEMS} more]`);
170
+ return out2;
171
+ }
172
+ const entries = obj instanceof Map ? [...obj].map(([k, v]) => [String(k), v]) : Object.keys(obj).map((k) => [k, void 0]);
173
+ const out = {};
174
+ let n = 0;
175
+ for (const [key, mapped] of entries) {
176
+ if (n >= MAX_KEYS) {
177
+ out["\u2026"] = `${entries.length - MAX_KEYS} more keys`;
178
+ break;
179
+ }
180
+ if (SENSITIVE_KEY.test(key)) continue;
181
+ let v;
182
+ try {
183
+ v = obj instanceof Map ? mapped : obj[key];
184
+ } catch {
185
+ v = "[Getter threw]";
186
+ }
187
+ const nv = normalize(v, depth - 1, seen);
188
+ if (nv === void 0) continue;
189
+ out[key] = nv;
190
+ n++;
191
+ }
192
+ return out;
193
+ } finally {
194
+ seen.delete(obj);
195
+ }
196
+ }
197
+ function normalizeObject(value, depth = 4) {
198
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
199
+ const out = normalize(value, depth);
200
+ return out && typeof out === "object" && !Array.isArray(out) ? out : void 0;
201
+ }
202
+ function scrubQuery(query) {
203
+ return query.replace(/^\?/, "").split("&").filter(Boolean).slice(0, 100).map((pair) => {
204
+ const i = pair.indexOf("=");
205
+ const key = i < 0 ? pair : pair.slice(0, i);
206
+ let name = key;
207
+ try {
208
+ name = decodeURIComponent(key.replace(/\+/g, " "));
209
+ } catch {
210
+ }
211
+ return i >= 0 && SENSITIVE_PARAM.test(name) ? `${key}=${FILTERED}` : pair;
212
+ }).join("&").slice(0, 2e3);
213
+ }
214
+ function stripQuery(url) {
215
+ const i = url.search(/[?#]/);
216
+ return (i < 0 ? url : url.slice(0, i)).slice(0, 2e3);
217
+ }
218
+
219
+ // src/errors/stack.ts
220
+ import { posix } from "path";
221
+ import { fileURLToPath } from "url";
222
+ var MAX_DETAIL_FRAMES = 250;
223
+ var MAX_PROPERTY_FRAMES = 30;
224
+ var LINE = /^\s*at (?:async )?(?:(.+?) \((.*)\)|(.+))$/;
225
+ var LOCATION = /^(.*?):(\d+)(?::(\d+))?$/;
226
+ var EVAL_ORIGIN = /\(([^()]*?:\d+:\d+)\)/;
227
+ function parseStack(stack, o = {}) {
228
+ if (typeof stack !== "string" || !stack) return [];
229
+ const frames = [];
230
+ for (const raw of stack.split("\n")) {
231
+ if (frames.length >= MAX_DETAIL_FRAMES * 2) break;
232
+ const m = LINE.exec(raw.length > 2e3 ? raw.slice(0, 2e3) : raw);
233
+ if (!m) continue;
234
+ const fn = m[1]?.trim();
235
+ let location = (m[2] ?? m[3] ?? "").trim();
236
+ if (location.startsWith("eval at ")) location = EVAL_ORIGIN.exec(location)?.[1] ?? location;
237
+ const at = LOCATION.exec(location);
238
+ const frame = {};
239
+ if (fn) frame.function = fn.slice(0, 200);
240
+ if (at) {
241
+ const file = filePath(at[1]);
242
+ frame.filename = file.slice(0, 1e3);
243
+ frame.abs_path = file.slice(0, 1e3);
244
+ frame.lineno = Number(at[2]);
245
+ if (at[3]) frame.colno = Number(at[3]);
246
+ const mod = moduleOf(file, o.root);
247
+ if (mod) frame.module = mod;
248
+ frame.in_app = isInApp(file, o.inAppExclude);
249
+ } else {
250
+ if (!fn && !location) continue;
251
+ if (location && !/^index \d+$/.test(location)) frame.filename = location.slice(0, 200);
252
+ frame.in_app = false;
253
+ }
254
+ frames.push(frame);
255
+ }
256
+ frames.reverse();
257
+ return frames.length > MAX_DETAIL_FRAMES ? frames.slice(-MAX_DETAIL_FRAMES) : frames;
258
+ }
259
+ function filePath(location) {
260
+ if (!location.startsWith("file://")) return location;
261
+ try {
262
+ return fileURLToPath(location);
263
+ } catch {
264
+ return location;
265
+ }
266
+ }
267
+ function isInApp(file, exclude) {
268
+ if (!file || file.startsWith("node:") || file.startsWith("internal/")) return false;
269
+ if (!file.startsWith("/") && !/^[a-z]:[\\/]/i.test(file) && !file.startsWith("\\\\")) return false;
270
+ if (/[\\/]node_modules[\\/]/.test(file)) return false;
271
+ for (const p of exclude ?? []) {
272
+ if (typeof p === "string") {
273
+ if (p && file.includes(p)) return false;
274
+ } else if (p instanceof RegExp) {
275
+ p.lastIndex = 0;
276
+ if (p.test(file)) return false;
277
+ }
278
+ }
279
+ return true;
280
+ }
281
+ function moduleOf(file, root) {
282
+ if (!file || file.startsWith("node:") || file.startsWith("internal/")) return void 0;
283
+ const normalized = file.replace(/^[a-z]:/i, "").replace(/\\/g, "/");
284
+ if (!normalized.startsWith("/")) return void 0;
285
+ const parsed = posix.parse(normalized);
286
+ let name = /^\.[cm]?[jt]sx?$/.test(parsed.ext) ? parsed.name : parsed.base;
287
+ try {
288
+ name = decodeURIComponent(name);
289
+ } catch {
290
+ }
291
+ const dir = parsed.dir || ".";
292
+ const n = dir.lastIndexOf("/node_modules");
293
+ if (n > -1) return `${dir.slice(n + 14).replace(/\//g, ".")}:${name}`;
294
+ const base = root?.replace(/^[a-z]:/i, "").replace(/\\/g, "/").replace(/\/+$/, "");
295
+ if (base && (dir === base || dir.startsWith(`${base}/`))) {
296
+ const mod = dir.slice(base.length + 1).replace(/\//g, ".");
297
+ return mod ? `${mod}:${name}` : name;
298
+ }
299
+ return name;
300
+ }
301
+ function toGuidelyFrames(frames) {
302
+ const out = [];
303
+ if (!frames) return out;
304
+ for (let i = frames.length - 1; i >= 0 && out.length < MAX_PROPERTY_FRAMES; i--) {
305
+ const f = frames[i];
306
+ const file = f.abs_path || f.filename;
307
+ out.push({
308
+ ...f.function ? { fn: f.function.slice(0, 200) } : {},
309
+ ...file ? { file: file.slice(0, 500) } : {},
310
+ ...typeof f.lineno === "number" ? { line: f.lineno } : {},
311
+ ...typeof f.colno === "number" ? { col: f.colno } : {},
312
+ ...typeof f.in_app === "boolean" ? { inApp: f.in_app } : {}
313
+ });
314
+ }
315
+ return out;
316
+ }
317
+
318
+ // src/errors/event.ts
319
+ var ERROR_LIMITS = {
320
+ /** `detail` (the Sentry-shaped event) after trimming. */
321
+ maxDetailBytes: 65536,
322
+ /** `properties`, inside the event's property budget. */
323
+ maxPropertyBytes: 8192,
324
+ /** A whole `exception` message: properties + detail + envelope. Larger ones lose their `detail`. */
325
+ maxMessageBytes: 81920,
326
+ /** Exceptions in a cause / AggregateError chain. */
327
+ maxChain: 20
328
+ };
329
+ var newEventId = () => randomUUID().replace(/-/g, "");
330
+ var errorLike = (v) => v instanceof Error || !!v && typeof v === "object" && typeof v.message === "string" && (typeof v.stack === "string" || typeof v.name === "string");
331
+ function typeOf(e) {
332
+ const name = typeof e.name === "string" && e.name ? e.name : void 0;
333
+ const ctor = e.constructor?.name;
334
+ return (name === "Error" && typeof ctor === "string" && ctor && ctor !== "Error" ? ctor : name) ?? (typeof ctor === "string" && ctor ? ctor : "Error");
335
+ }
336
+ function describe(value) {
337
+ if (typeof value === "string") return value;
338
+ if (value === void 0 || value === null || typeof value !== "object") return String(value);
339
+ const keys = Object.keys(value).slice(0, 10);
340
+ return keys.length ? `Non-Error exception captured with keys: ${keys.join(", ")}` : `Non-Error exception captured: ${Object.prototype.toString.call(value)}`;
341
+ }
342
+ function exceptionChain(error, mechanism, o, fallbackType = "Error") {
343
+ const out = [];
344
+ const seen = /* @__PURE__ */ new Set();
345
+ const visit = (value, mech) => {
346
+ if (out.length >= ERROR_LIMITS.maxChain) return;
347
+ seen.add(value);
348
+ const id2 = out.length;
349
+ const isError = errorLike(value);
350
+ const frames = isError ? parseStack(value.stack, o) : [];
351
+ const aggregate = isError && Array.isArray(value.errors);
352
+ const ex = {
353
+ type: (isError ? typeOf(value) : fallbackType).slice(0, 200),
354
+ value: (isError ? String(value.message ?? "") : describe(value)).slice(0, 8e3),
355
+ mechanism: { ...mech, exception_id: id2, ...aggregate ? { is_exception_group: true } : {} },
356
+ ...frames.length ? { stacktrace: { frames } } : {}
357
+ };
358
+ out.push(ex);
359
+ if (!isError) return;
360
+ const link = (child, source) => {
361
+ if (!errorLike(child) || seen.has(child)) return;
362
+ visit(child, { type: "chained", handled: mechanism.handled, source, parent_id: id2 });
363
+ };
364
+ if (aggregate) value.errors.slice(0, ERROR_LIMITS.maxChain).forEach((child, i) => link(child, `errors[${i}]`));
365
+ link(value.cause, "cause");
366
+ };
367
+ visit(error, { type: mechanism.type, handled: mechanism.handled });
368
+ return out.reverse();
369
+ }
370
+ function linuxRelease() {
371
+ try {
372
+ const text = readFileSync("/etc/os-release", "utf8").slice(0, 1e4);
373
+ const field = (k) => new RegExp(`^${k}="?([^"\\n]*)"?$`, "m").exec(text)?.[1];
374
+ return { name: field("NAME"), version: field("VERSION_ID") };
375
+ } catch {
376
+ return {};
377
+ }
378
+ }
379
+ function macosVersion(release2) {
380
+ const major = Number(release2.split(".")[0]);
381
+ if (!Number.isFinite(major)) return void 0;
382
+ if (major >= 25) return String(major + 1);
383
+ if (major >= 20) return String(major - 9);
384
+ return major >= 5 ? `10.${major - 4}` : void 0;
385
+ }
386
+ var staticContexts = null;
387
+ function processContexts() {
388
+ if (!staticContexts) {
389
+ const release2 = safe(() => os.release(), "");
390
+ const platform = process.platform;
391
+ const linux = platform === "linux" ? linuxRelease() : {};
392
+ const osName = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform === "linux" ? linux.name ?? "Linux" : safe(() => os.type(), platform);
393
+ const osVersion = platform === "darwin" ? macosVersion(release2) : platform === "linux" ? linux.version : release2;
394
+ const cpus2 = safe(() => os.cpus(), []);
395
+ const intl = safe(() => Intl.DateTimeFormat().resolvedOptions(), void 0);
396
+ staticContexts = {
397
+ runtime: { name: "node", version: process.version },
398
+ os: { name: osName, ...osVersion ? { version: osVersion } : {}, ...release2 ? { kernel_version: release2 } : {} },
399
+ device: {
400
+ arch: process.arch,
401
+ memory_size: safe(() => os.totalmem(), void 0),
402
+ processor_count: cpus2.length || void 0,
403
+ cpu_description: cpus2[0]?.model,
404
+ processor_frequency: cpus2[0]?.speed || void 0,
405
+ boot_time: safe(() => new Date(Date.now() - os.uptime() * 1e3).toISOString(), void 0)
406
+ },
407
+ app: { app_start_time: new Date(Date.now() - process.uptime() * 1e3).toISOString() },
408
+ culture: intl ? { locale: intl.locale, timezone: intl.timeZone } : {}
409
+ };
410
+ }
411
+ const s = staticContexts;
412
+ return {
413
+ runtime: { ...s.runtime },
414
+ os: { ...s.os },
415
+ device: { ...s.device, free_memory: safe(() => os.freemem(), void 0) },
416
+ app: { ...s.app, app_memory: safe(() => process.memoryUsage().rss, void 0) },
417
+ culture: { ...s.culture }
418
+ };
419
+ }
420
+ function safe(fn, fallback) {
421
+ try {
422
+ return fn();
423
+ } catch {
424
+ return fallback;
425
+ }
426
+ }
427
+ var bytes = (v) => Buffer.byteLength(JSON.stringify(v) ?? "", "utf8");
428
+ function trimDetail(event, max = ERROR_LIMITS.maxDetailBytes) {
429
+ let size = bytes(event);
430
+ if (size <= max) return event;
431
+ const e = { ...event };
432
+ const frames = () => (e.exception?.values ?? []).flatMap((x) => x.stacktrace?.frames ?? []);
433
+ let crumbs = e.breadcrumbs?.values ?? [];
434
+ while (crumbs.length > 0 && size > max) {
435
+ crumbs = crumbs.slice(Math.ceil(crumbs.length / 2));
436
+ e.breadcrumbs = { values: crumbs };
437
+ size = bytes(e);
438
+ }
439
+ if (size <= max) return e;
440
+ delete e.breadcrumbs;
441
+ e.exception = e.exception ? { values: e.exception.values.map((x) => ({ ...x, ...x.stacktrace ? { stacktrace: { frames: x.stacktrace.frames.map((f) => ({ ...f })) } } : {} })) } : void 0;
442
+ if (!e.exception) delete e.exception;
443
+ for (const f of frames()) {
444
+ delete f.pre_context;
445
+ delete f.post_context;
446
+ delete f.context_line;
447
+ delete f.vars;
448
+ }
449
+ size = bytes(e);
450
+ if (size <= max) return e;
451
+ e.extra = { _trimmed: true };
452
+ if (e.request) e.request = { ...e.request, headers: {} };
453
+ size = bytes(e);
454
+ if (size <= max) return e;
455
+ for (const x of e.exception?.values ?? []) if (x.stacktrace && x.stacktrace.frames.length > 50) x.stacktrace.frames = x.stacktrace.frames.slice(-50);
456
+ while (e.exception && e.exception.values.length > 1 && bytes(e) > max) e.exception.values.shift();
457
+ if (bytes(e) <= max) return e;
458
+ e.contexts = { runtime: e.contexts?.runtime ?? {} };
459
+ delete e.extra;
460
+ return bytes(e) <= max ? e : void 0;
461
+ }
462
+ function propertiesOf(event, source, release2, extra) {
463
+ const main = event.exception?.values?.[event.exception.values.length - 1];
464
+ const os3 = event.contexts?.os;
465
+ const props = {
466
+ type: (main?.type ?? "Message").slice(0, 200),
467
+ message: (main?.value ?? event.logentry?.formatted ?? "").slice(0, 1e3),
468
+ frames: toGuidelyFrames(main?.stacktrace?.frames),
469
+ source,
470
+ handled: main?.mechanism ? main.mechanism.handled !== false : event.level !== "fatal",
471
+ level: event.level,
472
+ platform: "node",
473
+ ...main?.mechanism?.type ? { mechanism: main.mechanism.type.slice(0, 60) } : {},
474
+ ...release2 ? { release: release2.slice(0, 200) } : {},
475
+ ...Array.isArray(event.fingerprint) && event.fingerprint.length ? { fingerprintParts: event.fingerprint.slice(0, 20).map((p) => String(p).slice(0, 200)) } : {},
476
+ ...event.transaction ? { transaction: String(event.transaction).slice(0, 200) } : {},
477
+ ...os3?.name ? { os: [os3.name, os3.version].filter(Boolean).join(" ").slice(0, 60) } : {}
478
+ };
479
+ const user = normalize(extra, 3);
480
+ for (const [k, v] of Object.entries(user ?? {})) if (!(k in props) && !k.startsWith("$")) props[k] = v;
481
+ return fitProperties(props);
482
+ }
483
+ function fitProperties(props) {
484
+ if (bytes(props) <= ERROR_LIMITS.maxPropertyBytes) return props;
485
+ const core = /* @__PURE__ */ new Set(["type", "message", "frames", "source", "handled", "level", "platform", "mechanism", "release", "fingerprintParts", "transaction", "os"]);
486
+ const out = {};
487
+ for (const [k, v] of Object.entries(props)) if (core.has(k)) out[k] = v;
488
+ const frames = out.frames.slice();
489
+ while (frames.length > 1 && bytes(out) > ERROR_LIMITS.maxPropertyBytes) {
490
+ frames.pop();
491
+ out.frames = frames;
492
+ }
493
+ if (bytes(out) > ERROR_LIMITS.maxPropertyBytes) out.message = String(out.message).slice(0, 200);
494
+ return out;
495
+ }
496
+
497
+ // src/errors/monitor.ts
498
+ import { AsyncLocalStorage, AsyncResource } from "async_hooks";
499
+ import * as os2 from "os";
500
+ import { dirname } from "path";
501
+
502
+ // src/errors/breadcrumbs.ts
503
+ import * as dc from "diagnostics_channel";
504
+ import { format } from "util";
505
+ var CONSOLE_LEVEL = { debug: "debug", info: "info", log: "log", warn: "warning", error: "error" };
506
+ var wrapped = /* @__PURE__ */ new Map();
507
+ var inConsole = false;
508
+ function instrumentConsole(levels, record) {
509
+ const target = console;
510
+ const mine = [];
511
+ for (const level of new Set(levels)) {
512
+ if (typeof target[level] !== "function") continue;
513
+ let w = wrapped.get(level);
514
+ if (w && w.listeners.size === 0 && target[level] !== w.wrapper) {
515
+ wrapped.delete(level);
516
+ w = void 0;
517
+ }
518
+ if (!w) {
519
+ const original = target[level];
520
+ const listeners = /* @__PURE__ */ new Set();
521
+ const wrapper = function(...args) {
522
+ if (!inConsole && listeners.size > 0) {
523
+ inConsole = true;
524
+ try {
525
+ const message = format(...args);
526
+ if (!message.startsWith("[guidely-node]")) {
527
+ const crumb = { category: "console", level: CONSOLE_LEVEL[level], message: message.slice(0, 1e3), data: { arguments: normalize(args, 3), logger: "console" } };
528
+ for (const l of listeners) l({ ...crumb }, { input: args, level });
529
+ }
530
+ } catch {
531
+ } finally {
532
+ inConsole = false;
533
+ }
534
+ }
535
+ return original.apply(this, args);
536
+ };
537
+ w = { original, wrapper, listeners };
538
+ wrapped.set(level, w);
539
+ target[level] = wrapper;
540
+ }
541
+ w.listeners.add(record);
542
+ mine.push(level);
543
+ }
544
+ return () => {
545
+ for (const level of mine) {
546
+ const w = wrapped.get(level);
547
+ if (!w) continue;
548
+ w.listeners.delete(record);
549
+ if (w.listeners.size === 0 && target[level] === w.wrapper) {
550
+ target[level] = w.original;
551
+ wrapped.delete(level);
552
+ }
553
+ }
554
+ };
555
+ }
556
+ function subscribe2(name, handler) {
557
+ try {
558
+ if (typeof dc.subscribe === "function") {
559
+ dc.subscribe(name, handler);
560
+ return () => dc.unsubscribe(name, handler);
561
+ }
562
+ const ch = dc.channel(name);
563
+ ch.subscribe(handler);
564
+ return () => ch.unsubscribe(handler);
565
+ } catch {
566
+ return () => {
567
+ };
568
+ }
569
+ }
570
+ var levelOf = (status) => status === void 0 || status >= 500 ? "error" : status >= 400 ? "warning" : "info";
571
+ function instrumentHttp(record, current, ignore) {
572
+ const started = /* @__PURE__ */ new WeakMap();
573
+ const begin = (request, method, url) => {
574
+ if (ignore(url)) return;
575
+ started.set(request, { method: typeof method === "string" ? method.toUpperCase() : "GET", url: stripQuery(url), start: Date.now(), target: current() });
576
+ };
577
+ const end = (request, response, status, error) => {
578
+ const s = started.get(request);
579
+ if (!s) return;
580
+ started.delete(request);
581
+ const data = { method: s.method, url: s.url, ...status !== void 0 ? { status_code: status } : {}, duration_ms: Date.now() - s.start };
582
+ if (error) data.error = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
583
+ try {
584
+ record({ type: "http", category: "http", level: levelOf(status), data }, { request, response }, s.target);
585
+ } catch {
586
+ }
587
+ };
588
+ const statusOf2 = (response) => {
589
+ const code = response?.statusCode;
590
+ return typeof code === "number" ? code : void 0;
591
+ };
592
+ const offs = [
593
+ // http / https
594
+ subscribe2("http.client.request.start", (m) => {
595
+ const req = m.request;
596
+ if (!req || typeof req !== "object") return;
597
+ const host = typeof req.getHeader === "function" ? req.getHeader("host") : void 0;
598
+ begin(req, req.method, `${req.protocol ?? "http:"}//${typeof host === "string" ? host : req.host ?? ""}${req.path ?? "/"}`);
599
+ }),
600
+ subscribe2("http.client.response.finish", (m) => {
601
+ const { request, response } = m;
602
+ if (request) end(request, response, statusOf2(response));
603
+ }),
604
+ subscribe2("http.client.request.error", (m) => {
605
+ const { request, error } = m;
606
+ if (request) end(request, void 0, void 0, error);
607
+ }),
608
+ // fetch / undici
609
+ subscribe2("undici:request:create", (m) => {
610
+ const req = m.request;
611
+ if (!req || typeof req !== "object") return;
612
+ const origin = String(req.origin ?? "").replace(/\/+$/, "");
613
+ begin(req, req.method, `${origin}${req.path ?? "/"}`);
614
+ }),
615
+ subscribe2("undici:request:headers", (m) => {
616
+ const { request, response } = m;
617
+ if (request) end(request, response, statusOf2(response));
618
+ }),
619
+ subscribe2("undici:request:error", (m) => {
620
+ const { request, error } = m;
621
+ if (request) end(request, void 0, void 0, error);
622
+ })
623
+ ];
624
+ return () => offs.forEach((off) => off());
625
+ }
626
+
627
+ // src/errors/context-lines.ts
628
+ import { readFile, stat } from "fs/promises";
629
+ import { isAbsolute } from "path";
630
+ var MAX_FILE_BYTES = 1e6;
631
+ var MAX_CACHED_FILES = 20;
632
+ var MAX_FRAMES = 50;
633
+ var MAX_LINE = 200;
634
+ var MAX_CONTEXT_LINES = 10;
635
+ var cache = /* @__PURE__ */ new Map();
636
+ var loading = /* @__PURE__ */ new Map();
637
+ function remember(file, lines) {
638
+ cache.delete(file);
639
+ cache.set(file, lines);
640
+ while (cache.size > MAX_CACHED_FILES) cache.delete(cache.keys().next().value);
641
+ }
642
+ function linesOf(file) {
643
+ if (cache.has(file)) {
644
+ const lines = cache.get(file);
645
+ remember(file, lines);
646
+ return Promise.resolve(lines);
647
+ }
648
+ let p = loading.get(file);
649
+ if (!p) {
650
+ p = (async () => {
651
+ try {
652
+ const s = await stat(file);
653
+ if (!s.isFile() || s.size > MAX_FILE_BYTES) return null;
654
+ return (await readFile(file, "utf8")).split(/\r?\n/);
655
+ } catch {
656
+ return null;
657
+ }
658
+ })().then((lines) => {
659
+ loading.delete(file);
660
+ remember(file, lines);
661
+ return lines;
662
+ });
663
+ loading.set(file, p);
664
+ }
665
+ return p;
666
+ }
667
+ function snip(line, colno) {
668
+ if (line.length <= MAX_LINE) return line;
669
+ const at = colno ? Math.max(0, colno - 1) : 0;
670
+ const start = Math.max(0, Math.min(at - 60, line.length - MAX_LINE));
671
+ const end = Math.min(line.length, start + MAX_LINE);
672
+ return `${start > 0 ? "{snip} " : ""}${line.slice(start, end)}${end < line.length ? " {snip}" : ""}`;
673
+ }
674
+ function wantsContext(frames) {
675
+ return frames.some((f) => f.in_app && f.lineno && f.abs_path && isAbsolute(f.abs_path));
676
+ }
677
+ async function addContextLines(frames, n) {
678
+ const count = Math.min(Math.max(0, Math.floor(n)), MAX_CONTEXT_LINES);
679
+ if (count === 0) return;
680
+ const targets = frames.filter((f) => f.in_app && f.lineno && f.abs_path && isAbsolute(f.abs_path)).slice(-MAX_FRAMES);
681
+ await Promise.all(
682
+ targets.map(async (f) => {
683
+ const lines = await linesOf(f.abs_path);
684
+ const i = f.lineno - 1;
685
+ if (!lines || i < 0 || i >= lines.length) return;
686
+ f.pre_context = lines.slice(Math.max(0, i - count), i).map((l) => snip(l));
687
+ f.context_line = snip(lines[i], f.colno);
688
+ f.post_context = lines.slice(i + 1, i + 1 + count).map((l) => snip(l));
689
+ })
690
+ );
691
+ }
692
+
693
+ // src/errors/process-handlers.ts
694
+ import { inspect } from "util";
695
+ var OURS = /* @__PURE__ */ Symbol.for("guidely.processHandler");
696
+ var isOurs = (l) => typeof l === "function" && l[OURS] === true;
697
+ function nodeMode(proc) {
698
+ const args = [...proc.execArgv ?? [], ...(proc.env?.NODE_OPTIONS ?? "").split(/\s+/)];
699
+ for (const a of args) {
700
+ const m = /^--unhandled-rejections=([\w-]+)$/.exec(a);
701
+ if (m && ["throw", "strict", "warn", "warn-with-error-code", "none"].includes(m[1])) return m[1];
702
+ }
703
+ return "throw";
704
+ }
705
+ function installProcessHandlers(host, o = {}) {
706
+ const proc = o.process ?? process;
707
+ const exitOnUncaught = o.exitOnUncaught !== false;
708
+ const mode = o.unhandledRejection ?? nodeMode(proc);
709
+ const flushTimeoutMs = typeof o.flushTimeoutMs === "number" && o.flushTimeoutMs >= 0 ? o.flushTimeoutMs : 2e3;
710
+ let exiting = false;
711
+ const others = (event) => proc.listeners(event).filter((l) => !isOurs(l)).length;
712
+ const exit = (error, prefix) => {
713
+ if (exiting) return;
714
+ exiting = true;
715
+ try {
716
+ console.error(`${prefix}${inspect(error)}`);
717
+ } catch {
718
+ }
719
+ let timer;
720
+ const timeout = new Promise((resolve) => {
721
+ timer = setTimeout(resolve, flushTimeoutMs);
722
+ });
723
+ void Promise.race([host.flush().catch(() => {
724
+ }), timeout]).then(() => {
725
+ clearTimeout(timer);
726
+ proc.exitCode = 1;
727
+ proc.exit(1);
728
+ });
729
+ };
730
+ const onUncaught = (error) => {
731
+ const crash = exitOnUncaught && others("uncaughtException") === 0;
732
+ host.capture(error, { mechanism: "uncaughtException", level: "fatal" });
733
+ if (crash) exit(error, "Uncaught ");
734
+ };
735
+ const onRejection = (reason) => {
736
+ const raised = mode === "strict" || mode === "throw" && others("unhandledRejection") === 0;
737
+ const crash = exitOnUncaught && raised && others("uncaughtException") === 0;
738
+ host.capture(reason, { mechanism: "unhandledRejection", level: crash ? "fatal" : "error", fallbackType: "UnhandledRejection" });
739
+ if (crash) return exit(reason, "Unhandled promise rejection: ");
740
+ if (mode === "none" || others("unhandledRejection") > 0) return;
741
+ if (mode === "warn-with-error-code") proc.exitCode = 1;
742
+ try {
743
+ console.warn(`Unhandled promise rejection: ${inspect(reason)}`);
744
+ } catch {
745
+ }
746
+ };
747
+ for (const l of [onUncaught, onRejection]) l[OURS] = true;
748
+ proc.on("uncaughtException", onUncaught);
749
+ proc.on("unhandledRejection", onRejection);
750
+ let installed = true;
751
+ return () => {
752
+ if (!installed) return;
753
+ installed = false;
754
+ proc.removeListener("uncaughtException", onUncaught);
755
+ proc.removeListener("unhandledRejection", onRejection);
756
+ };
757
+ }
758
+
759
+ // src/errors/request.ts
760
+ var str = (v, max = 2e3) => typeof v === "string" && v ? v.slice(0, max) : void 0;
761
+ function nodeRequestOf(r) {
762
+ const req = r.req;
763
+ return req && typeof req === "object" ? req : r;
764
+ }
765
+ function header(headers, name) {
766
+ const v = headers?.[name];
767
+ return Array.isArray(v) ? v[0] : typeof v === "string" ? v : typeof v === "number" ? String(v) : void 0;
768
+ }
769
+ function transactionOf(input) {
770
+ if (!input || typeof input !== "object") return void 0;
771
+ const r = input;
772
+ const method = str(r.method, 16)?.toUpperCase();
773
+ const routePath = typeof r.route?.path === "string" ? r.route.path : void 0;
774
+ const route = str(r.routeOptions?.url) ?? str(r.routerPath) ?? str(r._matchedRoute) ?? (routePath !== void 0 ? `${r.baseUrl ?? ""}${routePath}` || "/" : void 0);
775
+ return method && route ? `${method} ${route}`.slice(0, 200) : void 0;
776
+ }
777
+ function requestDataOf(input, sendDefaultPii) {
778
+ if (!input || typeof input !== "object") return void 0;
779
+ const r = input;
780
+ const node = nodeRequestOf(r);
781
+ const rawHeaders = (r.headers && typeof r.headers === "object" ? r.headers : node.headers) ?? {};
782
+ const headers = {};
783
+ let n = 0;
784
+ for (const [name, value] of Object.entries(rawHeaders)) {
785
+ if (n >= 50) break;
786
+ if (value === void 0 || SENSITIVE_HEADER.test(name) || !sendDefaultPii && IP_HEADER.test(name)) continue;
787
+ headers[name.toLowerCase()] = (Array.isArray(value) ? value.join(", ") : String(value)).slice(0, 1e3);
788
+ n++;
789
+ }
790
+ const raw = str(r.originalUrl) ?? str(node.originalUrl) ?? str(r.url) ?? str(node.url) ?? "";
791
+ let url = raw;
792
+ let query = typeof r.query_string === "string" ? r.query_string : void 0;
793
+ const q = raw.indexOf("?");
794
+ if (q >= 0) {
795
+ query = raw.slice(q + 1).replace(/#.*$/, "");
796
+ url = raw.slice(0, q);
797
+ }
798
+ url = url.replace(/#.*$/, "");
799
+ if (url && !/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
800
+ const host = header(rawHeaders, "host") ?? str(r.host) ?? str(r.hostname);
801
+ const protocol = (str(r.protocol) ?? (node.socket?.encrypted ? "https" : "http")).replace(/:$/, "");
802
+ if (host) url = `${protocol}://${host}${url.startsWith("/") ? "" : "/"}${url}`;
803
+ }
804
+ const request = {
805
+ ...str(r.method, 16) ? { method: str(r.method, 16).toUpperCase() } : {},
806
+ ...url ? { url: url.slice(0, 2e3) } : {},
807
+ ...query ? { query_string: scrubQuery(query) } : {},
808
+ headers
809
+ };
810
+ if (!sendDefaultPii) return { request };
811
+ const forwarded = header(rawHeaders, "x-forwarded-for")?.split(",")[0]?.trim();
812
+ const ip = str(r.ip, 64) ?? str(forwarded, 64) ?? str(node.socket?.remoteAddress, 64);
813
+ if (ip) request.env = { REMOTE_ADDR: ip };
814
+ return { request, ...ip ? { ip } : {} };
815
+ }
816
+ function scrubRequest(request, sendDefaultPii) {
817
+ if (!request || typeof request !== "object") return void 0;
818
+ const headers = {};
819
+ for (const [k, v] of Object.entries(request.headers ?? {})) if (!SENSITIVE_HEADER.test(k) && (sendDefaultPii || !IP_HEADER.test(k))) headers[k] = String(v).slice(0, 1e3);
820
+ return {
821
+ ...request,
822
+ ...typeof request.query_string === "string" ? { query_string: scrubQuery(request.query_string) } : {},
823
+ ...typeof request.url === "string" && request.url.includes("?") ? { url: `${request.url.split("?")[0]}?${scrubQuery(request.url.split("?").slice(1).join("?"))}` } : {},
824
+ headers
825
+ };
826
+ }
827
+
828
+ // src/errors/scope.ts
829
+ var MAX_BREADCRUMBS = 100;
830
+ var MAX_TAGS = 50;
831
+ var Scope = class _Scope {
832
+ /** @internal `max`: breadcrumbs kept; `hook`: the client's breadcrumb pipeline (`beforeBreadcrumb`). */
833
+ constructor(max = MAX_BREADCRUMBS, hook) {
834
+ this.max = max;
835
+ this.hook = hook;
836
+ }
837
+ max;
838
+ hook;
839
+ tags = {};
840
+ extra = {};
841
+ contexts = {};
842
+ user = null;
843
+ level;
844
+ fingerprint;
845
+ transaction;
846
+ crumbs = [];
847
+ /** @internal The framework request this scope was opened for (read when an error is captured). */
848
+ request;
849
+ /** A tag (searchable, ≤ 50 per event; keys ≤ 32 and values ≤ 200 characters). `null` removes it. */
850
+ setTag(key, value) {
851
+ if (typeof key !== "string" || !key) return this;
852
+ const k = key.slice(0, 32);
853
+ if (value === null || value === void 0) delete this.tags[k];
854
+ else if (k in this.tags || Object.keys(this.tags).length < MAX_TAGS) this.tags[k] = String(value).slice(0, 200);
855
+ return this;
856
+ }
857
+ setTags(tags) {
858
+ if (tags && typeof tags === "object") for (const [k, v] of Object.entries(tags)) this.setTag(k, v);
859
+ return this;
860
+ }
861
+ /** Arbitrary data shown with the event. `undefined` removes it. */
862
+ setExtra(key, value) {
863
+ if (typeof key !== "string" || !key) return this;
864
+ if (value === void 0) delete this.extra[key];
865
+ else this.extra[key] = value;
866
+ return this;
867
+ }
868
+ setExtras(extras) {
869
+ if (extras && typeof extras === "object") for (const [k, v] of Object.entries(extras)) this.setExtra(k, v);
870
+ return this;
871
+ }
872
+ /** A named context (`{ name: "checkout", cartSize: 3 }`); `null` removes it. */
873
+ setContext(name, context) {
874
+ if (typeof name !== "string" || !name) return this;
875
+ if (context === null || context === void 0 || typeof context !== "object") delete this.contexts[name];
876
+ else this.contexts[name] = { ...context };
877
+ return this;
878
+ }
879
+ /** The user errors are attributed to; `user.id` becomes the event's `userId`. `null` clears it. */
880
+ setUser(user) {
881
+ this.user = user && typeof user === "object" ? { ...user } : null;
882
+ return this;
883
+ }
884
+ /** Overrides the level of events captured in this scope. */
885
+ setLevel(level) {
886
+ this.level = level;
887
+ return this;
888
+ }
889
+ setFingerprint(fingerprint) {
890
+ this.fingerprint = Array.isArray(fingerprint) ? fingerprint.map(String) : void 0;
891
+ return this;
892
+ }
893
+ /** The transaction name (`GET /users/:id`); the request middlewares set it from the matched route. */
894
+ setTransactionName(name) {
895
+ this.transaction = typeof name === "string" && name ? name.slice(0, 200) : void 0;
896
+ return this;
897
+ }
898
+ /** Records a breadcrumb through the client's `beforeBreadcrumb`. */
899
+ addBreadcrumb(crumb) {
900
+ if (this.hook) this.hook(crumb, this);
901
+ else this.pushBreadcrumb(crumb);
902
+ return this;
903
+ }
904
+ /** @internal Adds an already-filtered breadcrumb, dropping the oldest beyond the limit. */
905
+ pushBreadcrumb(crumb) {
906
+ this.crumbs.push(crumb);
907
+ if (this.crumbs.length > this.max) this.crumbs.splice(0, this.crumbs.length - this.max);
908
+ }
909
+ clearBreadcrumbs() {
910
+ this.crumbs = [];
911
+ return this;
912
+ }
913
+ /** Removes everything (tags, extras, contexts, user, breadcrumbs…). */
914
+ clear() {
915
+ this.tags = {};
916
+ this.extra = {};
917
+ this.contexts = {};
918
+ this.user = null;
919
+ this.level = void 0;
920
+ this.fingerprint = void 0;
921
+ this.transaction = void 0;
922
+ this.crumbs = [];
923
+ this.request = void 0;
924
+ return this;
925
+ }
926
+ /** A copy whose changes do not affect this scope. */
927
+ clone() {
928
+ const s = new _Scope(this.max, this.hook);
929
+ s.tags = { ...this.tags };
930
+ s.extra = { ...this.extra };
931
+ s.contexts = { ...this.contexts };
932
+ s.user = this.user ? { ...this.user } : null;
933
+ s.level = this.level;
934
+ s.fingerprint = this.fingerprint;
935
+ s.transaction = this.transaction;
936
+ s.crumbs = this.crumbs.slice();
937
+ s.request = this.request;
938
+ return s;
939
+ }
940
+ /** @internal The scope's data at capture time. */
941
+ data() {
942
+ return {
943
+ tags: { ...this.tags },
944
+ extra: { ...this.extra },
945
+ contexts: { ...this.contexts },
946
+ user: this.user ? { ...this.user } : null,
947
+ level: this.level,
948
+ fingerprint: this.fingerprint,
949
+ transaction: this.transaction,
950
+ breadcrumbs: this.crumbs.slice(),
951
+ request: this.request
952
+ };
953
+ }
954
+ };
955
+
956
+ // src/errors/monitor.ts
957
+ var LEVELS = ["fatal", "error", "warning", "log", "info", "debug"];
958
+ var levelOf2 = (v) => typeof v === "string" && LEVELS.includes(v) ? v : v === "warn" ? "warning" : void 0;
959
+ var KNOWN_CONTEXT_KEYS = /* @__PURE__ */ new Set(["userId", "anonymousId", "accountId", "tags", "extra", "level", "fingerprint", "contexts", "request", "user", "mechanism", "transaction"]);
960
+ var idString = (v) => typeof v === "string" && v.trim() ? v.slice(0, 256) : typeof v === "number" && Number.isSafeInteger(v) ? String(v) : void 0;
961
+ function statusOf(error) {
962
+ if (!error || typeof error !== "object") return void 0;
963
+ const e = error;
964
+ for (const v of [e.status, e.statusCode, e.status_code, e.output?.statusCode]) {
965
+ const n = typeof v === "string" ? Number(v) : v;
966
+ if (typeof n === "number" && Number.isInteger(n) && n >= 100 && n < 600) return n;
967
+ }
968
+ return void 0;
969
+ }
970
+ var defaultShouldHandle = (error) => {
971
+ const status = statusOf(error);
972
+ return status === void 0 || status >= 500;
973
+ };
974
+ var ErrorMonitor = class {
975
+ constructor(host, options) {
976
+ this.host = host;
977
+ const o = options && typeof options === "object" ? options : {};
978
+ this.options = o;
979
+ const max = typeof o.maxBreadcrumbs === "number" && o.maxBreadcrumbs >= 0 ? Math.min(MAX_BREADCRUMBS, Math.floor(o.maxBreadcrumbs)) : MAX_BREADCRUMBS;
980
+ this.global = new Scope(max, (crumb, scope) => this.record(crumb, void 0, scope));
981
+ const entry = typeof process !== "undefined" ? process.argv?.[1] : void 0;
982
+ this.frameOptions = { root: entry ? dirname(entry) : safeCwd(), inAppExclude: Array.isArray(o.inAppExclude) ? o.inAppExclude : void 0 };
983
+ this.sampleRate = typeof o.sampleRate === "number" && Number.isFinite(o.sampleRate) ? Math.min(1, Math.max(0, o.sampleRate)) : 1;
984
+ this.contextLines = typeof o.contextLines === "number" && Number.isFinite(o.contextLines) ? Math.max(0, Math.floor(o.contextLines)) : 5;
985
+ this.pii = o.sendDefaultPii === true;
986
+ this.release = (typeof o.release === "string" && o.release.trim() ? o.release.trim() : host.appVersion)?.slice(0, 200);
987
+ let serverName;
988
+ try {
989
+ serverName = typeof o.serverName === "string" && o.serverName ? o.serverName : os2.hostname();
990
+ } catch {
991
+ serverName = void 0;
992
+ }
993
+ this.serverName = serverName?.slice(0, 200);
994
+ if (options === false) return;
995
+ const levels = o.captureConsole === false ? [] : Array.isArray(o.captureConsole) ? o.captureConsole : ["warn", "error"];
996
+ if (levels.length) this.cleanups.push(instrumentConsole(levels, (crumb, hint) => this.record(crumb, hint, this.current())));
997
+ if (o.captureHttp !== false) {
998
+ const own = originOf(host.apiUrl);
999
+ this.cleanups.push(
1000
+ instrumentHttp(
1001
+ (crumb, hint, target) => this.record(crumb, hint, target instanceof Scope ? target : this.global),
1002
+ () => this.current(),
1003
+ (url) => !!own && originOf(url) === own
1004
+ )
1005
+ );
1006
+ }
1007
+ }
1008
+ host;
1009
+ global;
1010
+ als = new AsyncLocalStorage();
1011
+ requestScopes = /* @__PURE__ */ new WeakMap();
1012
+ /** Errors already reported by an automatic hook (an error passing through two middlewares, or a middleware and the process handler). */
1013
+ captured = /* @__PURE__ */ new WeakSet();
1014
+ bound = /* @__PURE__ */ new WeakSet();
1015
+ pending = /* @__PURE__ */ new Set();
1016
+ cleanups = [];
1017
+ options;
1018
+ frameOptions;
1019
+ sampleRate;
1020
+ contextLines;
1021
+ pii;
1022
+ release;
1023
+ serverName;
1024
+ // --- scopes --------------------------------------------------------------------------------
1025
+ /** The scope of the running request / `withScope()` callback, else the process-wide one. */
1026
+ current() {
1027
+ return this.als.getStore() ?? this.global;
1028
+ }
1029
+ withScope(fn) {
1030
+ const scope = this.current().clone();
1031
+ return this.als.run(scope, () => fn(scope));
1032
+ }
1033
+ addBreadcrumb(crumb, hint) {
1034
+ this.record(crumb, hint, this.current());
1035
+ }
1036
+ /** Validates a breadcrumb, runs `beforeBreadcrumb` and adds it to `scope`. */
1037
+ record(crumb, hint, scope) {
1038
+ try {
1039
+ if (!crumb || typeof crumb !== "object") return;
1040
+ let c = {
1041
+ timestamp: typeof crumb.timestamp === "number" && Number.isFinite(crumb.timestamp) ? crumb.timestamp : Date.now() / 1e3,
1042
+ ...typeof crumb.type === "string" && crumb.type ? { type: crumb.type.slice(0, 40) } : {},
1043
+ ...typeof crumb.category === "string" && crumb.category ? { category: crumb.category.slice(0, 100) } : {},
1044
+ ...levelOf2(crumb.level) ? { level: levelOf2(crumb.level) } : {},
1045
+ ...typeof crumb.message === "string" ? { message: crumb.message.slice(0, 1e3) } : crumb.message !== void 0 ? { message: String(crumb.message).slice(0, 1e3) } : {}
1046
+ };
1047
+ const data = normalizeObject(crumb.data, 3);
1048
+ if (data) c.data = data;
1049
+ const hook = this.options.beforeBreadcrumb;
1050
+ if (typeof hook === "function") {
1051
+ let out = c;
1052
+ try {
1053
+ out = hook(c, hint);
1054
+ } catch {
1055
+ out = c;
1056
+ }
1057
+ if (out === null) return;
1058
+ if (out && typeof out === "object" && out !== c) c = { ...out, ...out.data ? { data: normalizeObject(out.data, 3) } : {} };
1059
+ }
1060
+ scope.pushBreadcrumb(c);
1061
+ } catch {
1062
+ }
1063
+ }
1064
+ // --- capture -------------------------------------------------------------------------------
1065
+ captureException(error, context) {
1066
+ const m = context?.mechanism;
1067
+ return this.capture({ error }, context, { type: m?.type ? String(m.type).slice(0, 60) : "generic", handled: m?.handled ?? true, level: "error", source: "manual" });
1068
+ }
1069
+ captureMessage(message, level, context) {
1070
+ const ctx = level && typeof level === "object" ? level : context;
1071
+ const lvl = typeof level === "string" ? levelOf2(level) : void 0;
1072
+ return this.capture({ message: typeof message === "string" ? message : String(message) }, ctx, { type: "generic", handled: true, level: lvl ?? "info", source: "manual" });
1073
+ }
1074
+ /** An error caught by an automatic hook (process handler, middleware); skipped when already reported. */
1075
+ captureAuto(error, kind) {
1076
+ if (error && typeof error === "object") {
1077
+ if (this.captured.has(error)) return void 0;
1078
+ }
1079
+ return this.capture({ error }, kind.request ? { request: kind.request } : void 0, {
1080
+ type: kind.mechanism,
1081
+ handled: false,
1082
+ level: kind.level ?? "error",
1083
+ source: kind.mechanism,
1084
+ fallbackType: kind.fallbackType
1085
+ });
1086
+ }
1087
+ capture(input, context, mech) {
1088
+ const eventId = newEventId();
1089
+ try {
1090
+ if (!this.host.accepting()) {
1091
+ this.host.report(`${"error" in input ? "captureException" : "captureMessage"}() was called after shutdown(); the event was dropped`, void 0, "closed");
1092
+ return eventId;
1093
+ }
1094
+ const ctx = context && typeof context === "object" && !Array.isArray(context) ? context : {};
1095
+ const scope = (ctx.request && typeof ctx.request === "object" ? this.requestScopes.get(ctx.request) : void 0) ?? this.current();
1096
+ const data = scope.data();
1097
+ const values = "error" in input ? exceptionChain(input.error, { type: mech.type, handled: mech.handled }, this.frameOptions, mech.fallbackType) : void 0;
1098
+ const main = values?.[values.length - 1];
1099
+ if (this.ignored(main?.type, main ? main.value : input.message)) return eventId;
1100
+ if (this.sampleRate < 1 && Math.random() >= this.sampleRate) return eventId;
1101
+ if ("error" in input && input.error && typeof input.error === "object") this.captured.add(input.error);
1102
+ const requestSource = ctx.request ?? data.request;
1103
+ const req = requestSource ? requestDataOf(requestSource, this.pii) : void 0;
1104
+ const user = { ...data.user ?? {}, ...ctx.user && typeof ctx.user === "object" ? ctx.user : {} };
1105
+ if (idString(ctx.userId)) user.id = idString(ctx.userId);
1106
+ if (req?.ip && !user.ip_address) user.ip_address = req.ip;
1107
+ const tags = new Scope().setTags(data.tags).setTags(ctx.tags ?? {}).data().tags;
1108
+ const extra = { ...data.extra, ...ctx.extra && typeof ctx.extra === "object" ? ctx.extra : {} };
1109
+ const contexts = { ...processContexts(), ...data.contexts };
1110
+ for (const [k, v] of Object.entries(ctx.contexts ?? {})) {
1111
+ if (v && typeof v === "object") contexts[k] = v;
1112
+ else delete contexts[k];
1113
+ }
1114
+ const fingerprint = Array.isArray(ctx.fingerprint) ? ctx.fingerprint.map(String) : data.fingerprint;
1115
+ const transaction = (typeof ctx.transaction === "string" && ctx.transaction ? ctx.transaction.slice(0, 200) : void 0) ?? data.transaction ?? transactionOf(requestSource);
1116
+ const level = levelOf2(ctx.level) ?? data.level ?? mech.level;
1117
+ const event = {
1118
+ event_id: eventId,
1119
+ timestamp: Date.now() / 1e3,
1120
+ platform: "node",
1121
+ level,
1122
+ ...this.release ? { release: this.release } : {},
1123
+ environment: this.host.environment,
1124
+ ...this.serverName ? { server_name: this.serverName } : {},
1125
+ ...transaction ? { transaction } : {},
1126
+ ..."message" in input ? { logentry: { formatted: input.message.slice(0, 8e3) } } : {},
1127
+ ...values ? { exception: { values } } : {},
1128
+ ...data.breadcrumbs.length ? { breadcrumbs: { values: data.breadcrumbs } } : {},
1129
+ contexts,
1130
+ ...Object.keys(tags).length ? { tags } : {},
1131
+ extra,
1132
+ user,
1133
+ ...fingerprint?.length ? { fingerprint } : {},
1134
+ ...req ? { request: req.request } : {},
1135
+ sdk: { ...this.host.sdk }
1136
+ };
1137
+ this.scrub(event);
1138
+ const properties = {};
1139
+ for (const [k, v] of Object.entries(ctx)) if (!KNOWN_CONTEXT_KEYS.has(k)) properties[k] = v;
1140
+ const ids = { anonymousId: idString(ctx.anonymousId), accountId: idString(ctx.accountId) };
1141
+ const hint = { originalException: "error" in input ? input.error : input.message, event_id: eventId };
1142
+ const finish = () => this.finish(event, hint, mech.source, properties, ids);
1143
+ const frames = values?.flatMap((v) => v.stacktrace?.frames ?? []) ?? [];
1144
+ if (this.contextLines > 0 && wantsContext(frames)) this.track(addContextLines(frames, this.contextLines).then(finish, finish));
1145
+ else {
1146
+ const done = finish();
1147
+ if (done) this.track(done);
1148
+ }
1149
+ } catch (e) {
1150
+ this.host.report(`${"error" in input ? "captureException" : "captureMessage"}() failed: ${e instanceof Error ? e.message : String(e)}`, e);
1151
+ }
1152
+ return eventId;
1153
+ }
1154
+ ignored(type2, message) {
1155
+ const patterns = this.options.ignoreErrors;
1156
+ if (!Array.isArray(patterns) || patterns.length === 0) return false;
1157
+ const candidates = [type2 && message !== void 0 ? `${type2}: ${message}` : void 0, message].filter((s) => typeof s === "string");
1158
+ return patterns.some(
1159
+ (p) => candidates.some((s) => {
1160
+ if (typeof p === "string") return !!p && s.includes(p);
1161
+ if (!(p instanceof RegExp)) return false;
1162
+ p.lastIndex = 0;
1163
+ return p.test(s);
1164
+ })
1165
+ );
1166
+ }
1167
+ /** Makes user-supplied parts of an event JSON-safe and credential-free (again after `beforeSend`). */
1168
+ scrub(event) {
1169
+ event.extra = normalizeObject(event.extra, 4) ?? {};
1170
+ const contexts = {};
1171
+ for (const [k, v] of Object.entries(event.contexts ?? {}).slice(0, 50)) {
1172
+ const c = normalizeObject(v, 4);
1173
+ if (c) contexts[k] = c;
1174
+ }
1175
+ event.contexts = contexts;
1176
+ const user = normalizeObject(event.user, 3);
1177
+ if (user && !this.pii && user.ip_address === "{{auto}}") delete user.ip_address;
1178
+ if (user && Object.keys(user).length) event.user = user;
1179
+ else delete event.user;
1180
+ if (!Object.keys(event.extra).length) delete event.extra;
1181
+ if (event.tags) {
1182
+ const tags = {};
1183
+ for (const [k, v] of Object.entries(event.tags).slice(0, MAX_TAGS)) if (v !== null && v !== void 0) tags[String(k).slice(0, 32)] = String(v).slice(0, 200);
1184
+ event.tags = tags;
1185
+ }
1186
+ if (event.request) event.request = scrubRequest(event.request, this.pii);
1187
+ if (event.breadcrumbs?.values) event.breadcrumbs = { values: event.breadcrumbs.values.slice(-MAX_BREADCRUMBS).map((b) => b.data ? { ...b, data: normalizeObject(b.data, 3) } : b) };
1188
+ }
1189
+ /** `beforeSend`, then the message: properties ≤ 8 KB, detail ≤ 64 KB. May return a promise (async `beforeSend`). */
1190
+ finish(event, hint, source, properties, ids) {
1191
+ const send = (result2) => {
1192
+ if (result2 === null) return;
1193
+ const final = result2 && typeof result2 === "object" ? result2 : event;
1194
+ this.scrub(final);
1195
+ this.send(final, source, properties, ids);
1196
+ };
1197
+ const hook = this.options.beforeSend;
1198
+ if (typeof hook !== "function") return this.send(event, source, properties, ids);
1199
+ let result;
1200
+ try {
1201
+ result = hook(event, hint);
1202
+ } catch {
1203
+ return send(event);
1204
+ }
1205
+ if (result && typeof result.then === "function") return result.then(send, () => send(event));
1206
+ return send(result);
1207
+ }
1208
+ send(event, source, extraProperties, ids) {
1209
+ try {
1210
+ const properties = propertiesOf(event, source, this.release, extraProperties);
1211
+ const userId = idString(event.user?.id);
1212
+ const timestamp = new Date(typeof event.timestamp === "number" && Number.isFinite(event.timestamp) ? event.timestamp * 1e3 : Date.now()).toISOString();
1213
+ let safe2;
1214
+ try {
1215
+ safe2 = JSON.parse(JSON.stringify(event));
1216
+ } catch {
1217
+ safe2 = normalize(event, 12);
1218
+ }
1219
+ const detail = trimDetail(safe2);
1220
+ const message = {
1221
+ type: "exception",
1222
+ messageId: event.event_id,
1223
+ timestamp,
1224
+ ...userId ? { userId } : {},
1225
+ ...ids.anonymousId ? { anonymousId: ids.anonymousId } : {},
1226
+ ...ids.accountId ? { accountId: ids.accountId } : {},
1227
+ ...this.release ? { appVersion: this.release } : {},
1228
+ properties,
1229
+ ...detail ? { detail } : {}
1230
+ };
1231
+ this.host.push(message);
1232
+ } catch (e) {
1233
+ this.host.report(`Could not build the exception event: ${e instanceof Error ? e.message : String(e)}`, e);
1234
+ }
1235
+ }
1236
+ track(p) {
1237
+ const tracked = p.catch((e) => this.host.report(`Could not build the exception event: ${e instanceof Error ? e.message : String(e)}`, e)).finally(() => {
1238
+ this.pending.delete(tracked);
1239
+ });
1240
+ this.pending.add(tracked);
1241
+ }
1242
+ /** Resolves once every event being prepared (source lines, async `beforeSend`) is queued; `null` when none is. */
1243
+ idle() {
1244
+ if (this.pending.size === 0) return null;
1245
+ return Promise.all([...this.pending]).then(() => this.idle() ?? void 0);
1246
+ }
1247
+ // --- process and framework hooks ----------------------------------------------------------
1248
+ installProcessHandlers(options) {
1249
+ try {
1250
+ const off = installProcessHandlers(
1251
+ {
1252
+ capture: (error, kind) => void this.captureAuto(error, { mechanism: kind.mechanism, level: kind.level, fallbackType: kind.fallbackType }),
1253
+ flush: () => this.host.flush()
1254
+ },
1255
+ options
1256
+ );
1257
+ this.cleanups.push(off);
1258
+ return off;
1259
+ } catch (e) {
1260
+ this.host.report(`installProcessHandlers() failed: ${e instanceof Error ? e.message : String(e)}`, e);
1261
+ return () => {
1262
+ };
1263
+ }
1264
+ }
1265
+ /** Runs `fn` in a new scope for `request`, with the request's events bound to it (body parsing, `finish`). */
1266
+ runRequest(request, emitters, fn) {
1267
+ if (!request || typeof request !== "object") return fn();
1268
+ const scope = this.current().clone();
1269
+ scope.request = request;
1270
+ scope.clearBreadcrumbs();
1271
+ this.requestScopes.set(request, scope);
1272
+ return this.als.run(scope, () => {
1273
+ for (const e of emitters) this.bindEmitter(e);
1274
+ return fn();
1275
+ });
1276
+ }
1277
+ /**
1278
+ * Listeners of a request's / response's events (`data`, `end`, `finish`) run in the async context
1279
+ * of the server's socket, not of the middleware: bind `emit` to the request scope, as domains did.
1280
+ */
1281
+ bindEmitter(emitter) {
1282
+ if (!emitter || typeof emitter !== "object" || this.bound.has(emitter)) return;
1283
+ const e = emitter;
1284
+ if (typeof e.emit !== "function") return;
1285
+ try {
1286
+ e.emit = AsyncResource.bind(e.emit, "GuidelyRequest", emitter);
1287
+ this.bound.add(emitter);
1288
+ } catch {
1289
+ }
1290
+ }
1291
+ captureRequestError(error, request, mechanism, options) {
1292
+ try {
1293
+ const should = typeof options?.shouldHandleError === "function" ? options.shouldHandleError : defaultShouldHandle;
1294
+ if (should(error)) this.captureAuto(error, { mechanism, request });
1295
+ } catch {
1296
+ }
1297
+ }
1298
+ expressRequestHandler() {
1299
+ return (req, res, next) => this.runRequest(req, [req, res], () => next());
1300
+ }
1301
+ expressErrorHandler(options) {
1302
+ return (error, req, _res, next) => {
1303
+ this.captureRequestError(error, req, "express", options);
1304
+ next(error);
1305
+ };
1306
+ }
1307
+ fastifyErrorHook(options) {
1308
+ return (request, _reply, error, done) => {
1309
+ this.captureRequestError(error, request, "fastify", options);
1310
+ done?.();
1311
+ };
1312
+ }
1313
+ fastifyPlugin(options) {
1314
+ const plugin = (fastify, _opts, done) => {
1315
+ fastify.addHook("onRequest", (request, reply, next) => this.runRequest(request, [request?.raw, reply?.raw], () => next()));
1316
+ fastify.addHook("preHandler", (request, _reply, next) => {
1317
+ const scope = request && typeof request === "object" ? this.requestScopes.get(request) : void 0;
1318
+ if (scope) this.als.run(scope, () => next());
1319
+ else next();
1320
+ });
1321
+ fastify.addHook("onError", this.fastifyErrorHook(options));
1322
+ done?.();
1323
+ };
1324
+ Object.assign(plugin, { [/* @__PURE__ */ Symbol.for("skip-override")]: true, [/* @__PURE__ */ Symbol.for("fastify.display-name")]: "@guidelytech/node" });
1325
+ return plugin;
1326
+ }
1327
+ koaMiddleware(options) {
1328
+ return (ctx, next) => this.runRequest(ctx, [ctx?.req, ctx?.res], async () => {
1329
+ try {
1330
+ return await next();
1331
+ } catch (error) {
1332
+ this.captureRequestError(error, ctx, "koa", options);
1333
+ throw error;
1334
+ }
1335
+ });
1336
+ }
1337
+ /** Stops the automatic breadcrumbs and process handlers. */
1338
+ close() {
1339
+ for (const off of this.cleanups.splice(0)) {
1340
+ try {
1341
+ off();
1342
+ } catch {
1343
+ }
1344
+ }
1345
+ }
1346
+ };
1347
+ function originOf(url) {
1348
+ try {
1349
+ return new URL(url).origin;
1350
+ } catch {
1351
+ return void 0;
1352
+ }
1353
+ }
1354
+ function safeCwd() {
1355
+ try {
1356
+ return process.cwd();
1357
+ } catch {
1358
+ return void 0;
1359
+ }
1360
+ }
1361
+
118
1362
  // src/client.ts
119
1363
  var SDK_NAME = "guidely-node";
120
- var SDK_VERSION = "0.16.0";
1364
+ var SDK_VERSION = "0.17.0";
121
1365
  var DEFAULT_API_URL = "https://guidelytech.com";
122
1366
  var BATCH_PATH = "/api/v1/sdk/server/batch";
123
- var ENVIRONMENTS = ["development", "staging", "production"];
1367
+ var ENV_KEY = /^[a-z][a-z0-9-]{0,29}$/;
124
1368
  var GuidelyError = class extends Error {
125
1369
  constructor(code, message, count = 1, status, options) {
126
1370
  super(message, options);
@@ -145,6 +1389,7 @@ var Guidely = class {
145
1389
  timeoutMs;
146
1390
  onError;
147
1391
  fetch;
1392
+ appVersion;
148
1393
  sdkJson = JSON.stringify({ name: SDK_NAME, version: SDK_VERSION });
149
1394
  queue = [];
150
1395
  droppedSinceReport = 0;
@@ -159,14 +1404,17 @@ var Guidely = class {
159
1404
  shutdownPromise = null;
160
1405
  exitDrain = null;
161
1406
  exitListener = null;
1407
+ /** Set once `shutdown()` has finished: events still being prepared then are dropped, not sent. */
1408
+ finished = false;
1409
+ monitor;
162
1410
  constructor(options) {
163
1411
  const o = options ?? {};
164
1412
  const key = typeof o.secretKey === "string" ? o.secretKey.trim() : "";
165
1413
  if (!key.startsWith("gly_sk_")) {
166
1414
  throw new TypeError("Guidely: secretKey must be a secret API key (gly_sk_\u2026) with the events:write scope");
167
1415
  }
168
- if (!ENVIRONMENTS.includes(o.environment)) {
169
- throw new TypeError('Guidely: environment must be "development", "staging" or "production"');
1416
+ if (typeof o.environment !== "string" || !ENV_KEY.test(o.environment)) {
1417
+ throw new TypeError('Guidely: environment must be an environment key: "development", "staging", "production" or one you added (e.g. "qa"), as shown in Settings \u2192 Environments');
170
1418
  }
171
1419
  const fetchImpl = o.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
172
1420
  if (!fetchImpl) throw new TypeError("Guidely: no global fetch (Node 18+ is required), pass the fetch option");
@@ -186,6 +1434,20 @@ var Guidely = class {
186
1434
  this.shutdownTimeoutMs = int(o.shutdownTimeoutMs, 1e4, 0);
187
1435
  this.timeoutMs = int(o.timeoutMs, 1e4, 1);
188
1436
  this.onError = o.onError ?? ((e) => console.warn(`[${SDK_NAME}] ${e.message}`));
1437
+ this.appVersion = typeof o.appVersion === "string" && o.appVersion.trim() ? o.appVersion.trim() : void 0;
1438
+ this.monitor = new ErrorMonitor(
1439
+ {
1440
+ environment: o.environment,
1441
+ appVersion: this.appVersion,
1442
+ apiUrl,
1443
+ sdk: { name: SDK_NAME, version: SDK_VERSION },
1444
+ push: (message) => this.enqueueException(message),
1445
+ accepting: () => !this.closed,
1446
+ report: (message, cause, code = "invalid_message") => this.report(new GuidelyError(code, message, 1, void 0, { cause })),
1447
+ flush: () => this.flush()
1448
+ },
1449
+ o.errors
1450
+ );
189
1451
  if (o.handleExit !== false && typeof process !== "undefined" && typeof process.on === "function") {
190
1452
  this.exitListener = () => this.onBeforeExit();
191
1453
  process.on("beforeExit", this.exitListener);
@@ -197,58 +1459,166 @@ var Guidely = class {
197
1459
  }
198
1460
  /** What a user (or anonymous visitor) did. */
199
1461
  track(message) {
200
- this.enqueue("track", message);
1462
+ const withVersion = this.appVersion && message && typeof message === "object" && (message.appVersion === void 0 || message.appVersion === null);
1463
+ this.enqueue("track", withVersion ? { ...message, appVersion: this.appVersion } : message);
201
1464
  }
202
1465
  /** Account traits, and optionally adds `userId` to the account. */
203
1466
  group(message) {
204
1467
  this.enqueue("group", message);
205
1468
  }
1469
+ // --- errors ----------------------------------------------------------------------------------
1470
+ /**
1471
+ * Reports an error as an `exception` event (grouped into an issue), with the current scope's tags,
1472
+ * extras, contexts, user and breadcrumbs. Returns the event id. Never throws.
1473
+ *
1474
+ * ```ts
1475
+ * guidely.captureException(err, { userId: "u_42", tags: { area: "billing" }, extra: { invoiceId } });
1476
+ * ```
1477
+ */
1478
+ captureException(error, context) {
1479
+ return this.monitor.captureException(error, context);
1480
+ }
1481
+ /** Reports a message (level `info` unless given), grouped by its text. Returns the event id. */
1482
+ captureMessage(message, level, context) {
1483
+ return this.monitor.captureMessage(message, level, context);
1484
+ }
1485
+ /** A tag on every later error of the current scope (`null` removes it). */
1486
+ setTag(key, value) {
1487
+ this.monitor.current().setTag(key, value);
1488
+ }
1489
+ setTags(tags) {
1490
+ this.monitor.current().setTags(tags);
1491
+ }
1492
+ setExtra(key, value) {
1493
+ this.monitor.current().setExtra(key, value);
1494
+ }
1495
+ setExtras(extras) {
1496
+ this.monitor.current().setExtras(extras);
1497
+ }
1498
+ /** A named context on every later error of the current scope (`null` removes it). */
1499
+ setContext(name, context) {
1500
+ this.monitor.current().setContext(name, context);
1501
+ }
1502
+ /** The user later errors of the current scope belong to (`user.id` becomes `userId`); `null` clears it. */
1503
+ setUser(user) {
1504
+ this.monitor.current().setUser(user);
1505
+ }
1506
+ /** Adds a breadcrumb to the current scope (the last 100 ride along with each error). */
1507
+ addBreadcrumb(breadcrumb, hint) {
1508
+ this.monitor.addBreadcrumb(breadcrumb, hint);
1509
+ }
1510
+ /**
1511
+ * Runs `fn` with a copy of the current scope: tags, user and breadcrumbs set inside (and in
1512
+ * everything it starts, through AsyncLocalStorage) stay there. Returns what `fn` returns.
1513
+ */
1514
+ withScope(fn) {
1515
+ return this.monitor.withScope(fn);
1516
+ }
1517
+ /**
1518
+ * Reports `uncaughtException` (fatal) and `unhandledRejection`, keeping Node's behaviour: the error
1519
+ * is printed, queued events are sent (at most `flushTimeoutMs`) and the process exits with code 1.
1520
+ * Returns a function that removes the handlers (`shutdown()` removes them too).
1521
+ */
1522
+ installProcessHandlers(options) {
1523
+ return this.monitor.installProcessHandlers(options);
1524
+ }
1525
+ /** Express: `app.use(guidely.expressRequestHandler())` first, so each request gets its own scope. */
1526
+ expressRequestHandler() {
1527
+ return this.monitor.expressRequestHandler();
1528
+ }
1529
+ /** Express: `app.use(guidely.expressErrorHandler())` after your routes, before other error handlers. */
1530
+ expressErrorHandler(options) {
1531
+ return this.monitor.expressErrorHandler(options);
1532
+ }
1533
+ /** Fastify: `await app.register(guidely.fastifyPlugin())` (request scopes + the `onError` hook). */
1534
+ fastifyPlugin(options) {
1535
+ return this.monitor.fastifyPlugin(options);
1536
+ }
1537
+ /** Fastify: `app.addHook("onError", guidely.fastifyErrorHook())`, when you do not use the plugin. */
1538
+ fastifyErrorHook(options) {
1539
+ return this.monitor.fastifyErrorHook(options);
1540
+ }
1541
+ /** Koa: `app.use(guidely.koaMiddleware())` first: a scope per request; errors are reported and rethrown. */
1542
+ koaMiddleware(options) {
1543
+ return this.monitor.koaMiddleware(options);
1544
+ }
1545
+ // --- delivery ------------------------------------------------------------------------------
206
1546
  /** Sends everything queued; resolves once it has been delivered or dropped. Never rejects. */
207
1547
  flush() {
208
- return this.startPump(true);
1548
+ const preparing = this.monitor.idle();
1549
+ return preparing ? preparing.then(() => this.startPump(true)) : this.startPump(true);
209
1550
  }
210
1551
  /**
211
- * Stops the timer and the exit hook, then flushes, waiting at most `shutdownTimeoutMs`. Later calls
212
- * are dropped (and reported). Call it before your process exits. Never rejects.
1552
+ * Stops the timer, the exit hook and the error hooks, then flushes, waiting at most
1553
+ * `shutdownTimeoutMs`. Later calls are dropped (and reported). Call it before your process exits.
1554
+ * Never rejects.
213
1555
  */
214
1556
  shutdown() {
215
1557
  if (this.shutdownPromise) return this.shutdownPromise;
216
1558
  this.closed = true;
217
1559
  this.clearTimer();
1560
+ this.monitor.close();
218
1561
  if (this.exitListener) process.removeListener("beforeExit", this.exitListener);
219
- this.shutdownPromise = this.drain(this.shutdownTimeoutMs);
1562
+ this.shutdownPromise = this.drain(this.shutdownTimeoutMs).finally(() => {
1563
+ this.finished = true;
1564
+ });
220
1565
  return this.shutdownPromise;
221
1566
  }
222
1567
  // --- queueing ------------------------------------------------------------------------------
223
- enqueue(type, args) {
1568
+ enqueue(type2, args) {
224
1569
  try {
225
1570
  if (this.closed) {
226
- this.report(new GuidelyError("closed", `${type}() was called after shutdown(); the message was dropped`));
1571
+ this.report(new GuidelyError("closed", `${type2}() was called after shutdown(); the message was dropped`));
227
1572
  return;
228
1573
  }
229
- const message = buildMessage(type, args);
1574
+ const message = buildMessage(type2, args);
230
1575
  let json;
231
1576
  try {
232
1577
  json = JSON.stringify(message);
233
1578
  } catch (e) {
234
1579
  throw new InvalidMessageError(`could not be serialized to JSON (${e instanceof Error ? e.message : String(e)})`);
235
1580
  }
236
- const bytes = Buffer.byteLength(json, "utf8");
237
- if (bytes > LIMITS.maxMessageBytes) {
238
- this.report(new GuidelyError("message_too_large", `${type}() message is ${bytes} bytes, more than the ${LIMITS.maxMessageBytes}-byte limit; it was dropped`));
1581
+ const bytes2 = Buffer.byteLength(json, "utf8");
1582
+ if (bytes2 > LIMITS.maxMessageBytes) {
1583
+ this.report(new GuidelyError("message_too_large", `${type2}() message is ${bytes2} bytes, more than the ${LIMITS.maxMessageBytes}-byte limit; it was dropped`));
239
1584
  return;
240
1585
  }
241
- if (this.queue.length >= this.maxQueueSize) {
242
- this.queue.shift();
243
- this.droppedSinceReport++;
244
- }
245
- this.queue.push({ json, bytes });
246
- if (this.queue.length >= this.flushAt) void this.startPump(false);
247
- else this.scheduleTimer();
1586
+ this.push(json, bytes2, false);
248
1587
  } catch (e) {
249
- if (e instanceof InvalidMessageError) this.report(new GuidelyError("invalid_message", `Invalid ${type}() call dropped: ${e.message}`));
250
- else this.report(new GuidelyError("invalid_message", `${type}() failed: ${e instanceof Error ? e.message : String(e)}`, 1, void 0, { cause: e }));
1588
+ if (e instanceof InvalidMessageError) this.report(new GuidelyError("invalid_message", `Invalid ${type2}() call dropped: ${e.message}`));
1589
+ else this.report(new GuidelyError("invalid_message", `${type2}() failed: ${e instanceof Error ? e.message : String(e)}`, 1, void 0, { cause: e }));
1590
+ }
1591
+ }
1592
+ /**
1593
+ * Queues a built `exception` message (it may finish preparing after `shutdown()` began, which
1594
+ * still sends it) and starts sending right away: errors should not wait for the flush interval.
1595
+ */
1596
+ enqueueException(message) {
1597
+ if (this.finished) {
1598
+ this.report(new GuidelyError("closed", "an exception finished preparing after shutdown(); it was dropped"));
1599
+ return;
1600
+ }
1601
+ let json = JSON.stringify(message);
1602
+ let bytes2 = Buffer.byteLength(json, "utf8");
1603
+ if (bytes2 > ERROR_LIMITS.maxMessageBytes && "detail" in message) {
1604
+ json = JSON.stringify({ ...message, detail: void 0 });
1605
+ bytes2 = Buffer.byteLength(json, "utf8");
1606
+ }
1607
+ if (bytes2 > ERROR_LIMITS.maxMessageBytes) {
1608
+ this.report(new GuidelyError("message_too_large", `exception message is ${bytes2} bytes, more than the ${ERROR_LIMITS.maxMessageBytes}-byte limit; it was dropped`));
1609
+ return;
1610
+ }
1611
+ this.push(json, bytes2, true);
1612
+ }
1613
+ push(json, bytes2, prompt) {
1614
+ if (this.queue.length >= this.maxQueueSize) {
1615
+ this.queue.shift();
1616
+ this.droppedSinceReport++;
251
1617
  }
1618
+ this.queue.push({ json, bytes: bytes2 });
1619
+ if (prompt) void this.startPump(true);
1620
+ else if (this.queue.length >= this.flushAt) void this.startPump(false);
1621
+ else this.scheduleTimer();
252
1622
  }
253
1623
  scheduleTimer() {
254
1624
  if (this.timer !== void 0 || this.closed) return;
@@ -358,7 +1728,7 @@ var Guidely = class {
358
1728
  async drain(timeoutMs) {
359
1729
  let timer;
360
1730
  const timedOut = await Promise.race([
361
- this.startPump(true).then(() => false),
1731
+ this.flush().then(() => false),
362
1732
  new Promise((resolve) => {
363
1733
  timer = setTimeout(() => resolve(true), timeoutMs);
364
1734
  })
@@ -377,7 +1747,7 @@ var Guidely = class {
377
1747
  if (left > 0) this.report(new GuidelyError("delivery_failed", `${left} queued messages were not sent within ${timeoutMs} ms`, left, 0));
378
1748
  }
379
1749
  onBeforeExit() {
380
- if (this.closed || this.exitDrain || this.queue.length === 0 && !this.pumping) return;
1750
+ if (this.closed || this.exitDrain || this.queue.length === 0 && !this.pumping && !this.monitor.idle()) return;
381
1751
  this.exitDrain = this.drain(this.shutdownTimeoutMs).finally(() => {
382
1752
  this.exitDrain = null;
383
1753
  });
@@ -408,6 +1778,8 @@ export {
408
1778
  Guidely,
409
1779
  GuidelyError,
410
1780
  SDK_NAME,
411
- SDK_VERSION
1781
+ SDK_VERSION,
1782
+ Scope,
1783
+ parseStack
412
1784
  };
413
1785
  //# sourceMappingURL=index.js.map