@logbrew/sdk 0.1.38 → 0.1.39

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 (90) hide show
  1. package/automatic-delivery.cjs +271 -0
  2. package/batch-sender.cjs +164 -0
  3. package/bounded-number.cjs +34 -0
  4. package/browser-breadcrumbs.cjs +346 -136
  5. package/capture-support.cjs +100 -0
  6. package/client-options.cjs +292 -0
  7. package/client-report.cjs +232 -0
  8. package/client.cjs +447 -0
  9. package/console-arguments.cjs +151 -0
  10. package/console-capture.cjs +194 -0
  11. package/core.cjs +30 -2810
  12. package/delivery-health.cjs +133 -0
  13. package/error-causes.cjs +130 -0
  14. package/error-details.cjs +124 -0
  15. package/error-issue.cjs +187 -0
  16. package/event-clone.cjs +55 -0
  17. package/event-metadata.cjs +40 -0
  18. package/event-pipeline.cjs +325 -0
  19. package/event-queue.cjs +306 -0
  20. package/event-store.cjs +237 -0
  21. package/event-validation.cjs +159 -0
  22. package/examples/agent-timeline.cjs +57 -46
  23. package/examples/agent-timeline.mjs +41 -35
  24. package/examples/index.mjs +122 -94
  25. package/examples/readme-example.cjs +36 -27
  26. package/examples/readme-example.mjs +21 -17
  27. package/examples/real-user-smoke.cjs +41 -32
  28. package/examples/real-user-smoke.mjs +26 -22
  29. package/exception-chain.cjs +205 -0
  30. package/index.d.ts +144 -1076
  31. package/index.js +1 -1
  32. package/issue-exception.cjs +13 -8
  33. package/metric-validation.cjs +95 -0
  34. package/network-milestone.cjs +155 -0
  35. package/null-value.cjs +4 -0
  36. package/opentelemetry-bridge.cjs +330 -0
  37. package/opentelemetry-context.cjs +245 -0
  38. package/opentelemetry-exceptions.cjs +69 -0
  39. package/opentelemetry-keys.cjs +183 -0
  40. package/opentelemetry-span.cjs +286 -0
  41. package/opentelemetry-summary.cjs +213 -0
  42. package/opentelemetry-time.cjs +131 -0
  43. package/opentelemetry.cjs +99 -1068
  44. package/package.json +55 -1
  45. package/pino-destination.cjs +108 -0
  46. package/pino-record.cjs +237 -0
  47. package/product-action.cjs +163 -0
  48. package/provided-breadcrumbs.cjs +117 -0
  49. package/random-hex.cjs +29 -0
  50. package/react-native.js +1 -1
  51. package/recording-transport.cjs +68 -0
  52. package/release-artifacts-build.cjs +263 -84
  53. package/release-artifacts-common.js +347 -66
  54. package/release-artifacts-debug-id.js +178 -0
  55. package/release-artifacts-inspect.js +213 -0
  56. package/release-artifacts-issue-event.js +99 -0
  57. package/release-artifacts-manifest-artifact.js +107 -0
  58. package/release-artifacts-manifest.js +77 -0
  59. package/release-artifacts-mappings.js +265 -0
  60. package/release-artifacts-prepare.js +297 -0
  61. package/release-artifacts-source-context.js +210 -0
  62. package/release-artifacts-symbolication.js +272 -421
  63. package/release-artifacts-upload-files.js +122 -0
  64. package/release-artifacts-upload-request.js +246 -0
  65. package/release-artifacts-upload.js +155 -300
  66. package/release-artifacts-urls.js +49 -0
  67. package/release-artifacts.js +189 -662
  68. package/retry-delay.cjs +32 -0
  69. package/span-validation.cjs +191 -0
  70. package/stored-events.cjs +243 -0
  71. package/types/client.d.ts +67 -0
  72. package/types/delivery.d.ts +125 -0
  73. package/types/issues.d.ts +249 -0
  74. package/types/logging.d.ts +175 -0
  75. package/types/opentelemetry.d.ts +205 -0
  76. package/types/product.d.ts +128 -0
  77. package/types/recording-transport.d.ts +15 -0
  78. package/types/sdk-error.d.ts +9 -0
  79. package/types/telemetry.d.ts +171 -0
  80. package/types/trace-context.d.ts +110 -0
  81. package/types/transport-error.d.ts +10 -0
  82. package/utf8.cjs +37 -0
  83. package/validation.cjs +49 -1
  84. package/vite-release-artifacts-config.cjs +120 -0
  85. package/vite-release-artifacts.cjs +199 -99
  86. package/vite-release-artifacts.d.cts +65 -40
  87. package/vite-release-artifacts.d.ts +65 -40
  88. package/vite-release-artifacts.js +2 -2
  89. package/winston-info.cjs +205 -0
  90. package/winston.cjs +189 -289
@@ -1,196 +1,406 @@
1
1
  "use strict";
2
2
 
3
+ /**
4
+ * @import { IssueBreadcrumbLevel } from "./index.cjs"
5
+ */
6
+
7
+ /**
8
+ * @typedef {Readonly<{
9
+ * category: string;
10
+ * data?: Readonly<Record<string, number | string>>;
11
+ * level?: IssueBreadcrumbLevel;
12
+ * message?: string;
13
+ * type: string;
14
+ * }>} BrowserBreadcrumb
15
+ * @typedef {(breadcrumb: BrowserBreadcrumb) => boolean} AddBreadcrumb
16
+ * @typedef {() => void} Restore
17
+ * @typedef {(this: unknown, ...args: readonly unknown[]) => unknown} ObservedMethod
18
+ * @typedef {Readonly<{ method: string; url: string | undefined }>} HttpRequest
19
+ * @typedef {Readonly<{ method?: unknown; url?: unknown }> | undefined} FetchInput
20
+ * @typedef {Readonly<{ method?: unknown }> | undefined} FetchInit
21
+ * @typedef {Readonly<{ status?: unknown }> | undefined} FetchResponse
22
+ * @typedef {(this: unknown, input: FetchInput, init: FetchInit) => Promise<FetchResponse>} FetchMethod
23
+ * @typedef {Readonly<{
24
+ * addEventListener?: (type: string, listener: () => void) => unknown;
25
+ * status?: unknown;
26
+ * }>} XhrInstance
27
+ * @typedef {(this: XhrInstance, method: unknown, url: unknown, ...rest: readonly unknown[]) => unknown} XhrOpen
28
+ * @typedef {(this: XhrInstance, ...args: readonly unknown[]) => unknown} XhrSend
29
+ * @typedef {Readonly<{ open?: XhrOpen; send?: XhrSend }>} XhrPrototype
30
+ * @typedef {"debug" | "error" | "info" | "log" | "warn"} ConsoleMethod
31
+ * @typedef {Readonly<Partial<Record<ConsoleMethod, ObservedMethod>>>} BrowserConsole
32
+ * @typedef {Readonly<{ pushState?: ObservedMethod; replaceState?: ObservedMethod }>} BrowserHistory
33
+ * @typedef {Readonly<{
34
+ * addEventListener?: (type: string, listener: () => void) => unknown;
35
+ * console?: BrowserConsole;
36
+ * fetch?: FetchMethod;
37
+ * history?: BrowserHistory;
38
+ * location?: Readonly<{ href?: unknown; pathname?: unknown }>;
39
+ * removeEventListener?: (type: string, listener: () => void) => unknown;
40
+ * XMLHttpRequest?: Readonly<{ prototype?: XhrPrototype }>;
41
+ * }>} BrowserWindow
42
+ * @typedef {Readonly<{ addBreadcrumb: (breadcrumb: BrowserBreadcrumb) => unknown }>} BreadcrumbClient
43
+ * @typedef {Readonly<{
44
+ * console?: boolean;
45
+ * ignoredUrls?: readonly unknown[];
46
+ * navigation?: boolean;
47
+ * network?: boolean;
48
+ * }>} BrowserBreadcrumbOptions
49
+ */
50
+
3
51
  const MAX_MESSAGE_LENGTH = 512;
4
52
  const MAX_URL_LENGTH = 256;
5
- const CONSOLE_LEVELS = Object.freeze({
6
- debug: "debug",
7
- error: "error",
8
- info: "info",
9
- log: "info",
10
- warn: "warning"
11
- });
12
-
13
- // Records console calls, fetch and XHR requests, and route changes as issue breadcrumbs.
14
- // Browser-safe and shared by @logbrew/browser and @logbrew/next/client. Returns an uninstall function
15
- // that puts back every patched method it still owns.
16
- function installBrowserBreadcrumbs(browserWindow, client, {
17
- console: captureConsole = true,
18
- ignoredUrls = [],
19
- navigation = true,
20
- network = true
21
- } = {}) {
22
- const ignored = new Set(ignoredUrls.map((url) => sanitizeUrl(url, browserWindow)).filter(Boolean));
23
- const add = (breadcrumb) => {
24
- try {
25
- client.addBreadcrumb(breadcrumb);
26
- } catch {
27
- // Breadcrumbs are advisory and never reach application code.
28
- }
29
- };
30
- const restores = [];
31
- if (captureConsole) {
32
- restores.push(...patchConsole(browserWindow.console ?? globalThis.console, add));
53
+ const MAX_METHOD_LENGTH = 16;
54
+ const TEXT_START = 0;
55
+ const FIRST_RESTORE = 0;
56
+ const NO_STATUS = 0;
57
+ const CLIENT_ERROR_STATUS = 400;
58
+ const SERVER_ERROR_STATUS = 500;
59
+ const LAST_C0_CONTROL = 31;
60
+ const FIRST_C1_CONTROL = 127;
61
+ const LAST_C1_CONTROL = 159;
62
+ const WHITESPACE_PATTERN = /\s+/gu;
63
+ /** @type {ReadonlyMap<ConsoleMethod, IssueBreadcrumbLevel>} */
64
+ const CONSOLE_LEVELS = new Map([
65
+ ["debug", "debug"],
66
+ ["error", "error"],
67
+ ["info", "info"],
68
+ ["log", "info"],
69
+ ["warn", "warning"]
70
+ ]);
71
+ /** @type {readonly (keyof BrowserHistory)[]} */
72
+ const NAVIGATION_METHODS = ["pushState", "replaceState"];
73
+
74
+ /** @type {(value: unknown) => string} */
75
+ const text = String;
76
+
77
+ /**
78
+ * @param {unknown} value
79
+ * @param {number} limit
80
+ * @returns {string}
81
+ */
82
+ function boundedText(value, limit) {
83
+ const characters = Array.from(text(value), (character) => {
84
+ const code = character.codePointAt(TEXT_START);
85
+ return code !== undefined && (code <= LAST_C0_CONTROL || (code >= FIRST_C1_CONTROL && code <= LAST_C1_CONTROL))
86
+ ? " "
87
+ : character;
88
+ });
89
+ return [...characters.join("").replaceAll(WHITESPACE_PATTERN, " ").trim()].slice(TEXT_START, limit).join("");
90
+ }
91
+
92
+ /**
93
+ * @param {unknown} value
94
+ * @param {BrowserWindow} browserWindow
95
+ * @returns {string | undefined}
96
+ */
97
+ function sanitizeUrl(value, browserWindow) {
98
+ if (value === undefined || value === null) {
99
+ return undefined;
33
100
  }
34
- if (network) {
35
- restores.push(...patchFetch(browserWindow, add, ignored), ...patchXhr(browserWindow, add, ignored));
101
+ try {
102
+ const url = new URL(text(value), text(browserWindow.location?.href ?? "http://localhost/"));
103
+ const sanitized = boundedText(`${url.protocol}//${url.host}${url.pathname}`, MAX_URL_LENGTH);
104
+ return sanitized === "" ? undefined : sanitized;
105
+ } catch {
106
+ return undefined;
36
107
  }
37
- if (navigation) {
38
- restores.push(...patchNavigation(browserWindow, add));
108
+ }
109
+
110
+ /**
111
+ * @param {BrowserWindow} browserWindow
112
+ * @returns {string | undefined}
113
+ */
114
+ function currentPath(browserWindow) {
115
+ const pathname = browserWindow.location?.pathname;
116
+ return typeof pathname === "string" ? boundedText(pathname, MAX_URL_LENGTH) : undefined;
117
+ }
118
+
119
+ /**
120
+ * @param {Readonly<Error>} error
121
+ * @returns {string}
122
+ */
123
+ function errorName(error) {
124
+ if (error.name) {
125
+ return error.name;
39
126
  }
40
- return () => {
41
- for (const restore of restores.splice(0).reverse()) {
42
- restore();
127
+ return "Error";
128
+ }
129
+
130
+ /**
131
+ * @param {readonly unknown[]} args
132
+ * @returns {string}
133
+ */
134
+ function consoleTemplate(args) {
135
+ const [first] = args;
136
+ if (typeof first === "string") {
137
+ return first;
138
+ }
139
+ return first instanceof Error ? errorName(first) : "";
140
+ }
141
+
142
+ /**
143
+ * @param {number | undefined} statusCode
144
+ * @returns {IssueBreadcrumbLevel}
145
+ */
146
+ function httpLevel(statusCode) {
147
+ if (statusCode === undefined || statusCode < CLIENT_ERROR_STATUS) {
148
+ return "info";
149
+ }
150
+ return statusCode >= SERVER_ERROR_STATUS ? "error" : "warning";
151
+ }
152
+
153
+ /**
154
+ * @param {string} category
155
+ * @param {HttpRequest} request
156
+ * @param {unknown} [status]
157
+ * @returns {BrowserBreadcrumb}
158
+ */
159
+ function httpBreadcrumb(category, request, status) {
160
+ const statusCode = typeof status === "number" && Number.isInteger(status) && status > NO_STATUS ? status : undefined;
161
+ const method = request.method.slice(TEXT_START, MAX_METHOD_LENGTH);
162
+ return {
163
+ type: "http",
164
+
165
+ category,
166
+ level: httpLevel(statusCode),
167
+
168
+ data: {
169
+ method: method === "" ? "GET" : method,
170
+ ...(request.url === undefined ? {} : { url: request.url }),
171
+ ...(statusCode === undefined ? {} : { status_code: statusCode })
43
172
  }
44
173
  };
45
174
  }
46
175
 
176
+ /**
177
+ * @template {object} Target
178
+ * @template {keyof Target & string} Name
179
+ * @param {Target | undefined} target
180
+ * @param {Name} name
181
+ * @param {(original: NonNullable<Target[Name]>) => NonNullable<Target[Name]>} wrap
182
+ * @returns {Restore[]}
183
+ */
184
+ function replaceMethod(target, name, wrap) {
185
+ const original = target?.[name];
186
+ if (target === undefined || typeof original !== "function") {
187
+ return [];
188
+ }
189
+ const replacement = wrap(original);
190
+ Object.assign(target, { [name]: replacement });
191
+ return [
192
+ () => {
193
+ if (target[name] === replacement) {
194
+ Object.assign(target, { [name]: original });
195
+ }
196
+ }
197
+ ];
198
+ }
199
+
200
+ /**
201
+ * @param {BrowserConsole | undefined} target
202
+ * @param {AddBreadcrumb} add
203
+ * @returns {Restore[]}
204
+ */
47
205
  function patchConsole(target, add) {
48
206
  if (!target) {
49
207
  return [];
50
208
  }
51
- return Object.entries(CONSOLE_LEVELS).flatMap(([method, level]) => replaceMethod(target, method, (original) => (
52
- /** @this {Console} */
53
- function logBrewConsoleBreadcrumb(...args) {
54
- const message = boundedText(consoleTemplate(args), MAX_MESSAGE_LENGTH);
55
- if (message) {
56
- add({ type: "debug", category: "console", level, message });
209
+ /** @type {Restore[]} */
210
+ const restores = [];
211
+ for (const [method, level] of CONSOLE_LEVELS) {
212
+ restores.push(...replaceMethod(target, method, (original) => (
213
+ /**
214
+ * @this {unknown}
215
+ * @param {readonly unknown[]} args
216
+ * @returns {unknown}
217
+ */
218
+ function logBrewConsoleBreadcrumb(...args) {
219
+ const message = boundedText(consoleTemplate(args), MAX_MESSAGE_LENGTH);
220
+ if (message) {
221
+ add({
222
+ type: "debug",
223
+
224
+ category: "console",
225
+ level,
226
+ message
227
+ });
228
+ }
229
+ return Reflect.apply(original, this, args);
57
230
  }
58
- return original.apply(this, args);
59
- }
60
- )));
231
+ )));
232
+ }
233
+ return restores;
61
234
  }
62
235
 
236
+ /**
237
+ * @param {BrowserWindow} browserWindow
238
+ * @param {AddBreadcrumb} add
239
+ * @param {Readonly<ReadonlySet<string | undefined>>} ignored
240
+ * @returns {Restore[]}
241
+ */
63
242
  function patchFetch(browserWindow, add, ignored) {
64
- return replaceMethod(browserWindow, "fetch", (original) => /** @this {*} */ async function logBrewFetchBreadcrumb(input, init) {
65
- const method = String(init?.method ?? input?.method ?? "GET").toUpperCase();
66
- const url = sanitizeUrl(typeof input === "string" || input instanceof URL ? input : input?.url, browserWindow);
67
- try {
68
- const response = await original.call(this, input, init);
69
- if (!ignored.has(url)) {
70
- add(httpBreadcrumb("fetch", method, url, response?.status));
243
+ return replaceMethod(browserWindow, "fetch", (original) => (
244
+ /**
245
+ * @this {unknown}
246
+ * @param {FetchInput} input
247
+ * @param {FetchInit} init
248
+ * @returns {Promise<FetchResponse>}
249
+ */
250
+ async function logBrewFetchBreadcrumb(input, init) {
251
+ const request = {
252
+ method: text(init?.method ?? input?.method ?? "GET").toUpperCase(),
253
+ url: sanitizeUrl(typeof input === "string" || input instanceof URL ? input : input?.url, browserWindow)
254
+ };
255
+ try {
256
+ const response = await original.call(this, input, init);
257
+ if (!ignored.has(request.url)) {
258
+ add(httpBreadcrumb("fetch", request, response?.status));
259
+ }
260
+ return response;
261
+ } catch (error) {
262
+ if (!ignored.has(request.url)) {
263
+ add({ ...httpBreadcrumb("fetch", request), level: "error" });
264
+ }
265
+ throw error;
71
266
  }
72
- return response;
73
- } catch (error) {
74
- if (!ignored.has(url)) {
75
- add({ ...httpBreadcrumb("fetch", method, url), level: "error" });
76
- }
77
- throw error;
78
267
  }
79
- });
268
+ ));
80
269
  }
81
270
 
271
+ /**
272
+ * @param {BrowserWindow} browserWindow
273
+ * @param {AddBreadcrumb} add
274
+ * @param {Readonly<ReadonlySet<string | undefined>>} ignored
275
+ * @returns {Restore[]}
276
+ */
82
277
  function patchXhr(browserWindow, add, ignored) {
83
278
  const prototype = browserWindow.XMLHttpRequest?.prototype;
84
279
  if (!prototype) {
85
280
  return [];
86
281
  }
282
+ /** @type {WeakMap<XhrInstance, HttpRequest>} */
87
283
  const state = new WeakMap();
88
284
  return [
89
- ...replaceMethod(prototype, "open", (original) => /** @this {XMLHttpRequest} */ function logBrewXhrOpen(method, url, ...rest) {
90
- state.set(this, { method: String(method ?? "GET").toUpperCase(), url: sanitizeUrl(url, browserWindow) });
91
- return original.call(this, method, url, ...rest);
92
- }),
93
- ...replaceMethod(prototype, "send", (original) => /** @this {XMLHttpRequest} */ function logBrewXhrSend(...args) {
94
- const request = state.get(this);
95
- if (request && !ignored.has(request.url) && typeof this.addEventListener === "function") {
96
- this.addEventListener("loadend", () => {
97
- add(httpBreadcrumb("xhr", request.method, request.url, this.status || undefined));
98
- });
285
+ ...replaceMethod(prototype, "open", (original) => (
286
+ /**
287
+ * @this {XhrInstance}
288
+ * @param {unknown} method
289
+ * @param {unknown} url
290
+ * @param {readonly unknown[]} rest
291
+ * @returns {unknown}
292
+ */
293
+ function logBrewXhrOpen(method, url, ...rest) {
294
+ state.set(this, { method: text(method ?? "GET").toUpperCase(), url: sanitizeUrl(url, browserWindow) });
295
+ return original.call(this, method, url, ...rest);
99
296
  }
100
- return original.apply(this, args);
101
- })
297
+ )),
298
+ ...replaceMethod(prototype, "send", (original) => (
299
+ /**
300
+ * @this {XhrInstance}
301
+ * @param {readonly unknown[]} args
302
+ * @returns {unknown}
303
+ */
304
+ function logBrewXhrSend(...args) {
305
+ const request = state.get(this);
306
+ if (request && !ignored.has(request.url) && typeof this.addEventListener === "function") {
307
+ this.addEventListener("loadend", () => {
308
+ add(httpBreadcrumb("xhr", request, this.status));
309
+ });
310
+ }
311
+ return Reflect.apply(original, this, args);
312
+ }
313
+ ))
102
314
  ];
103
315
  }
104
316
 
317
+ /**
318
+ * @param {BrowserWindow} browserWindow
319
+ * @param {AddBreadcrumb} add
320
+ * @returns {Restore[]}
321
+ */
105
322
  function patchNavigation(browserWindow, add) {
106
- const history = browserWindow.history;
323
+ const { history } = browserWindow;
107
324
  if (!history) {
108
325
  return [];
109
326
  }
110
327
  let from = currentPath(browserWindow);
111
- const record = () => {
328
+ /** @returns {void} */
329
+ function record() {
112
330
  const to = currentPath(browserWindow);
113
- if (to && to !== from) {
114
- add({ type: "navigation", category: "navigation", data: { ...(from ? { from } : {}), to } });
331
+ if (to !== undefined && to !== "" && to !== from) {
332
+ add({
333
+ type: "navigation",
334
+
335
+ category: "navigation",
336
+ data: { ...(from === undefined || from === "" ? {} : { from }), to }
337
+ });
115
338
  from = to;
116
339
  }
117
- };
118
- const restores = ["pushState", "replaceState"].flatMap((method) => replaceMethod(history, method, (original) => (
119
- /** @this {History} */
340
+ }
341
+ const restores = NAVIGATION_METHODS.flatMap((method) => replaceMethod(history, method, (original) => (
342
+ /**
343
+ * @this {unknown}
344
+ * @param {readonly unknown[]} args
345
+ * @returns {unknown}
346
+ */
120
347
  function logBrewNavigationBreadcrumb(...args) {
121
- const result = original.apply(this, args);
348
+ const result = Reflect.apply(original, this, args);
122
349
  record();
123
350
  return result;
124
351
  }
125
352
  )));
126
353
  if (typeof browserWindow.addEventListener === "function") {
127
354
  browserWindow.addEventListener("popstate", record);
128
- restores.push(() => browserWindow.removeEventListener?.("popstate", record));
355
+ restores.push(() => {
356
+ browserWindow.removeEventListener?.("popstate", record);
357
+ });
129
358
  }
130
359
  return restores;
131
360
  }
132
361
 
133
- function replaceMethod(target, name, wrap) {
134
- const original = target?.[name];
135
- if (typeof original !== "function") {
136
- return [];
137
- }
138
- const replacement = wrap(original);
139
- target[name] = replacement;
140
- return [() => {
141
- if (target[name] === replacement) {
142
- target[name] = original;
143
- }
144
- }];
145
- }
146
-
147
- function httpBreadcrumb(category, method, url, status) {
148
- const statusCode = Number.isInteger(status) && status > 0 ? status : undefined;
149
- return {
150
- type: "http",
151
- category,
152
- level: statusCode === undefined || statusCode < 400 ? "info" : statusCode >= 500 ? "error" : "warning",
153
- data: {
154
- method: method.slice(0, 16) || "GET",
155
- ...(url ? { url } : {}),
156
- ...(statusCode === undefined ? {} : { status_code: statusCode })
362
+ /**
363
+ * @param {BrowserWindow} browserWindow
364
+ * @param {BreadcrumbClient} client
365
+ * @param {BrowserBreadcrumbOptions} [options]
366
+ * @returns {Restore}
367
+ */
368
+ function installBrowserBreadcrumbs(browserWindow, client, {
369
+ console: captureConsole = true,
370
+ ignoredUrls = [],
371
+ navigation = true,
372
+ network = true
373
+ } = {}) {
374
+ const ignored = new Set(ignoredUrls.map((url) => sanitizeUrl(url, browserWindow)).filter(Boolean));
375
+ /**
376
+ * @param {BrowserBreadcrumb} breadcrumb
377
+ * @returns {boolean}
378
+ */
379
+ function add(breadcrumb) {
380
+ try {
381
+ client.addBreadcrumb(breadcrumb);
382
+ } catch {
383
+ return false;
157
384
  }
158
- };
159
- }
160
-
161
- // Records the message template, never the interpolated arguments.
162
- function consoleTemplate(args) {
163
- const [first] = args;
164
- if (typeof first === "string") {
165
- return first;
385
+ return true;
166
386
  }
167
- return first instanceof Error ? first.name || "Error" : "";
168
- }
169
-
170
- function currentPath(browserWindow) {
171
- const pathname = browserWindow.location?.pathname;
172
- return typeof pathname === "string" ? boundedText(pathname, MAX_URL_LENGTH) : undefined;
173
- }
174
-
175
- // Keeps scheme, host, and path; drops user info, query, and fragment.
176
- function sanitizeUrl(value, browserWindow) {
177
- if (value === undefined || value === null) {
178
- return undefined;
387
+ /** @type {Restore[]} */
388
+ const restores = [];
389
+ if (captureConsole) {
390
+ restores.push(...patchConsole(browserWindow.console ?? globalThis.console, add));
179
391
  }
180
- try {
181
- const url = new URL(String(value), String(browserWindow.location?.href ?? "http://localhost/"));
182
- return boundedText(`${url.protocol}//${url.host}${url.pathname}`, MAX_URL_LENGTH) || undefined;
183
- } catch {
184
- return undefined;
392
+ if (network) {
393
+ restores.push(...patchFetch(browserWindow, add, ignored), ...patchXhr(browserWindow, add, ignored));
185
394
  }
186
- }
187
-
188
- function boundedText(value, limit) {
189
- const characters = Array.from(String(value), (character) => {
190
- const code = character.codePointAt(0);
191
- return code <= 31 || (code >= 127 && code <= 159) ? " " : character;
192
- });
193
- return Array.from(characters.join("").replace(/\s+/gu, " ").trim()).slice(0, limit).join("");
395
+ if (navigation) {
396
+ restores.push(...patchNavigation(browserWindow, add));
397
+ }
398
+ return () => {
399
+ const pending = restores.splice(FIRST_RESTORE);
400
+ for (let restore = pending.pop(); restore !== undefined; restore = pending.pop()) {
401
+ restore();
402
+ }
403
+ };
194
404
  }
195
405
 
196
406
  module.exports = { installBrowserBreadcrumbs };
@@ -0,0 +1,100 @@
1
+ const { compactMetadata } = require("./log-context.cjs");
2
+ const { NULL_VALUE } = require("./null-value.cjs");
3
+
4
+ /**
5
+ * @import { Metadata } from "./index.cjs"
6
+ * @typedef {(reason: unknown) => void} ErrorHandler
7
+ * @typedef {Readonly<{
8
+ * log: (id: string, timestamp: unknown, attributes: Readonly<Record<string, unknown>>) => void,
9
+ * flush: (transport: unknown) => Promise<unknown>,
10
+ * pendingEvents: () => number
11
+ * }>} CaptureClient
12
+ * @typedef {{
13
+ * transport: unknown,
14
+ * flushOnEvent: boolean,
15
+ * includeErrorStack: boolean,
16
+ * logger: unknown,
17
+ * metadata: Metadata,
18
+ * timestamp: () => unknown,
19
+ * eventIdPrefix: unknown,
20
+ * onError: ErrorHandler
21
+ * }} CaptureSettings
22
+ */
23
+
24
+ /**
25
+ * @param {unknown} flushing
26
+ * @param {ErrorHandler} onError
27
+ * @returns {Promise<unknown>}
28
+ */
29
+ async function settledFlush(flushing, onError) {
30
+ try {
31
+ return await flushing;
32
+ } catch (error) {
33
+ onError(error);
34
+ return NULL_VALUE;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * @param {unknown} source
40
+ * @returns {(() => unknown) | undefined}
41
+ */
42
+ function callableOrUndefined(source) {
43
+ if (typeof source !== "function") {
44
+ return undefined;
45
+ }
46
+ return () => {
47
+ /** @type {unknown} */
48
+ const value = Reflect.apply(source, undefined, []);
49
+ return value;
50
+ };
51
+ }
52
+
53
+ /**
54
+ * @param {unknown} handler
55
+ * @returns {ErrorHandler}
56
+ */
57
+ function errorHandler(handler) {
58
+ if (typeof handler !== "function") {
59
+ return () => undefined;
60
+ }
61
+ return (reason) => {
62
+ Reflect.apply(handler, undefined, [reason]);
63
+ };
64
+ }
65
+
66
+ /**
67
+ * @param {unknown} timestamp
68
+ * @returns {() => unknown}
69
+ */
70
+ function timestampSource(timestamp) {
71
+ return callableOrUndefined(timestamp) ?? (() => new Date().toISOString());
72
+ }
73
+
74
+ /**
75
+ * @param {Readonly<Record<string, unknown>>} config
76
+ * @param {string} flushKey
77
+ * @param {string} defaultName
78
+ * @returns {CaptureSettings}
79
+ */
80
+ function captureSettings(config, flushKey, defaultName) {
81
+ return {
82
+ transport: config["transport"],
83
+
84
+ flushOnEvent: config[flushKey] === true,
85
+
86
+ includeErrorStack: config["includeErrorStack"] === true,
87
+
88
+ logger: config["logger"] ?? defaultName,
89
+
90
+ metadata: compactMetadata(config["metadata"]),
91
+
92
+ timestamp: timestampSource(config["timestamp"]),
93
+
94
+ eventIdPrefix: config["eventIdPrefix"] ?? defaultName,
95
+
96
+ onError: errorHandler(config["onError"])
97
+ };
98
+ }
99
+
100
+ module.exports = { callableOrUndefined, captureSettings, errorHandler, settledFlush };