@statoss/sdk 0.0.0-stage → 0.1.1

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -2
  3. package/dist/cjs/client.d.ts +50 -0
  4. package/dist/cjs/client.js +462 -0
  5. package/dist/cjs/dsn.d.ts +25 -0
  6. package/dist/cjs/dsn.js +62 -0
  7. package/dist/cjs/envelope.d.ts +36 -0
  8. package/dist/cjs/envelope.js +55 -0
  9. package/dist/cjs/event.d.ts +9 -0
  10. package/dist/cjs/event.js +128 -0
  11. package/dist/cjs/hub.d.ts +37 -0
  12. package/dist/cjs/hub.js +119 -0
  13. package/dist/cjs/index.d.ts +38 -0
  14. package/dist/cjs/index.js +107 -0
  15. package/dist/cjs/integrations/browser.d.ts +7 -0
  16. package/dist/cjs/integrations/browser.js +124 -0
  17. package/dist/cjs/integrations/console.d.ts +3 -0
  18. package/dist/cjs/integrations/console.js +106 -0
  19. package/dist/cjs/integrations/dedupe.d.ts +7 -0
  20. package/dist/cjs/integrations/dedupe.js +32 -0
  21. package/dist/cjs/integrations/fetch.d.ts +3 -0
  22. package/dist/cjs/integrations/fetch.js +69 -0
  23. package/dist/cjs/integrations/handlers.d.ts +9 -0
  24. package/dist/cjs/integrations/handlers.js +192 -0
  25. package/dist/cjs/levels.d.ts +20 -0
  26. package/dist/cjs/levels.js +77 -0
  27. package/dist/cjs/logger.d.ts +18 -0
  28. package/dist/cjs/logger.js +38 -0
  29. package/dist/cjs/package.json +1 -0
  30. package/dist/cjs/ratelimit.d.ts +11 -0
  31. package/dist/cjs/ratelimit.js +50 -0
  32. package/dist/cjs/runtime.d.ts +42 -0
  33. package/dist/cjs/runtime.js +188 -0
  34. package/dist/cjs/scope.d.ts +30 -0
  35. package/dist/cjs/scope.js +127 -0
  36. package/dist/cjs/stack.d.ts +9 -0
  37. package/dist/cjs/stack.js +129 -0
  38. package/dist/cjs/transport.d.ts +40 -0
  39. package/dist/cjs/transport.js +218 -0
  40. package/dist/cjs/types.d.ts +180 -0
  41. package/dist/cjs/types.js +2 -0
  42. package/dist/cjs/utils.d.ts +27 -0
  43. package/dist/cjs/utils.js +227 -0
  44. package/dist/cjs/version.d.ts +3 -0
  45. package/dist/cjs/version.js +7 -0
  46. package/dist/esm/client.d.ts +50 -0
  47. package/dist/esm/client.js +458 -0
  48. package/dist/esm/dsn.d.ts +25 -0
  49. package/dist/esm/dsn.js +55 -0
  50. package/dist/esm/envelope.d.ts +36 -0
  51. package/dist/esm/envelope.js +49 -0
  52. package/dist/esm/event.d.ts +9 -0
  53. package/dist/esm/event.js +124 -0
  54. package/dist/esm/hub.d.ts +37 -0
  55. package/dist/esm/hub.js +111 -0
  56. package/dist/esm/index.d.ts +38 -0
  57. package/dist/esm/index.js +88 -0
  58. package/dist/esm/integrations/browser.d.ts +7 -0
  59. package/dist/esm/integrations/browser.js +119 -0
  60. package/dist/esm/integrations/console.d.ts +3 -0
  61. package/dist/esm/integrations/console.js +103 -0
  62. package/dist/esm/integrations/dedupe.d.ts +7 -0
  63. package/dist/esm/integrations/dedupe.js +29 -0
  64. package/dist/esm/integrations/fetch.d.ts +3 -0
  65. package/dist/esm/integrations/fetch.js +66 -0
  66. package/dist/esm/integrations/handlers.d.ts +9 -0
  67. package/dist/esm/integrations/handlers.js +188 -0
  68. package/dist/esm/levels.d.ts +20 -0
  69. package/dist/esm/levels.js +69 -0
  70. package/dist/esm/logger.d.ts +18 -0
  71. package/dist/esm/logger.js +33 -0
  72. package/dist/esm/ratelimit.d.ts +11 -0
  73. package/dist/esm/ratelimit.js +45 -0
  74. package/dist/esm/runtime.d.ts +42 -0
  75. package/dist/esm/runtime.js +181 -0
  76. package/dist/esm/scope.d.ts +30 -0
  77. package/dist/esm/scope.js +123 -0
  78. package/dist/esm/stack.d.ts +9 -0
  79. package/dist/esm/stack.js +125 -0
  80. package/dist/esm/transport.d.ts +40 -0
  81. package/dist/esm/transport.js +213 -0
  82. package/dist/esm/types.d.ts +180 -0
  83. package/dist/esm/types.js +1 -0
  84. package/dist/esm/utils.d.ts +27 -0
  85. package/dist/esm/utils.js +211 -0
  86. package/dist/esm/version.d.ts +3 -0
  87. package/dist/esm/version.js +4 -0
  88. package/package.json +48 -4
@@ -0,0 +1,124 @@
1
+ import { parseStack } from "./stack.js";
2
+ import { summarize, isErrorLike, isObject, normalize, safeString, safely, toStringTag, truncate, } from "./utils.js";
3
+ const MAX_CAUSES = 5;
4
+ function exceptionFromError(error, mechanism) {
5
+ const name = safely(() => error.name);
6
+ const message = safely(() => error.message);
7
+ const exception = {
8
+ type: typeof name === "string" && name ? name : "Error",
9
+ value: typeof message === "string" ? truncate(message) : "",
10
+ mechanism,
11
+ };
12
+ const frames = parseStack(safely(() => error.stack));
13
+ if (frames.length)
14
+ exception.stacktrace = { frames };
15
+ return exception;
16
+ }
17
+ /** The error and its causes, as Sentry orders them: the thrown one last. */
18
+ function exceptionChain(error, mechanism) {
19
+ const chain = [];
20
+ let current = error;
21
+ while (isErrorLike(current) &&
22
+ chain.length < MAX_CAUSES &&
23
+ !chain.includes(current)) {
24
+ chain.push(current);
25
+ current = safely(() => current.cause);
26
+ }
27
+ const values = chain.map((e, i) => exceptionFromError(e, i === 0
28
+ ? chain.length > 1
29
+ ? { ...mechanism, exception_id: 0 }
30
+ : mechanism
31
+ : {
32
+ type: "chained",
33
+ handled: mechanism.handled,
34
+ source: "cause",
35
+ exception_id: i,
36
+ parent_id: i - 1,
37
+ }));
38
+ return values.reverse();
39
+ }
40
+ function keysForMessage(value) {
41
+ // Only the first keys: the message holds 40 characters of them.
42
+ const keys = [];
43
+ for (const key in value) {
44
+ if (keys.length >= 100)
45
+ break;
46
+ if (Object.prototype.hasOwnProperty.call(value, key))
47
+ keys.push(key);
48
+ }
49
+ keys.sort();
50
+ let out = "";
51
+ for (const key of keys) {
52
+ const next = out ? `${out}, ${key}` : key;
53
+ if (next.length > 40)
54
+ return out || truncate(key, 40);
55
+ out = next;
56
+ }
57
+ return out || "[none]";
58
+ }
59
+ /**
60
+ * Builds an event from anything thrown or rejected, as Sentry's SDKs do:
61
+ * errors with their stacks and causes, other objects and plain values as
62
+ * synthetic exceptions.
63
+ */
64
+ export function eventFromUnknown(input, mechanism, rejection = false) {
65
+ let value = input;
66
+ const tag = toStringTag(value);
67
+ if (tag === "[object ErrorEvent]" &&
68
+ isObject(value) &&
69
+ isErrorLike(value.error)) {
70
+ value = value.error;
71
+ }
72
+ else if (tag === "[object PromiseRejectionEvent]" &&
73
+ isObject(value) &&
74
+ "reason" in value) {
75
+ value = value.reason;
76
+ }
77
+ if (isErrorLike(value)) {
78
+ return { exception: { values: exceptionChain(value, mechanism) } };
79
+ }
80
+ const synthetic = { ...mechanism, synthetic: true };
81
+ if (isObject(value)) {
82
+ const name = safely(() => value.name);
83
+ const message = safely(() => value.message);
84
+ return {
85
+ exception: {
86
+ values: [
87
+ {
88
+ type: typeof name === "string" && name
89
+ ? name
90
+ : rejection
91
+ ? "UnhandledRejection"
92
+ : "Error",
93
+ value: typeof message === "string"
94
+ ? truncate(message)
95
+ : `Object captured as ${rejection ? "promise rejection" : "exception"} ${summarize(value) ?? `with keys: ${keysForMessage(value)}`}`,
96
+ mechanism: synthetic,
97
+ },
98
+ ],
99
+ },
100
+ extra: { __serialized__: normalize(value, 3) },
101
+ };
102
+ }
103
+ const text = safeString(value);
104
+ return {
105
+ exception: {
106
+ values: [
107
+ {
108
+ type: rejection ? "UnhandledRejection" : "Error",
109
+ value: truncate(rejection
110
+ ? `Non-Error promise rejection captured with value: ${text}`
111
+ : text),
112
+ mechanism: synthetic,
113
+ },
114
+ ],
115
+ },
116
+ };
117
+ }
118
+ /** For a browser error event that carried no Error: the file and line it names. */
119
+ export function addInitialFrame(event, frame) {
120
+ const first = event.exception?.values[0];
121
+ if (first && !first.stacktrace)
122
+ first.stacktrace = { frames: [frame] };
123
+ return event;
124
+ }
@@ -0,0 +1,37 @@
1
+ import type { Client } from "./client.js";
2
+ import { Scope } from "./scope.js";
3
+ import type { CaptureContext, Level } from "./types.js";
4
+ interface ScopeStorage {
5
+ getStore(): Scope | undefined;
6
+ run<T>(store: Scope, callback: () => T): T;
7
+ }
8
+ export interface Carrier {
9
+ client?: Client;
10
+ root: Scope;
11
+ /** Without AsyncLocalStorage: the scopes of withScope callbacks still running synchronously. */
12
+ stack: Scope[];
13
+ /** Undefined until first needed, null when the runtime has none. */
14
+ storage?: ScopeStorage | null;
15
+ warnedNoStorage?: boolean;
16
+ lastEventId?: string;
17
+ }
18
+ export declare function getCarrier(): Carrier;
19
+ export declare function getClient(): Client | undefined;
20
+ export declare function getCurrentScope(): Scope;
21
+ /**
22
+ * Runs callback with a copy of the current scope, so what it sets applies only
23
+ * to events captured inside it.
24
+ *
25
+ * With AsyncLocalStorage (Node 20.16+ and 22.3+, Bun, Deno, Vercel Edge,
26
+ * Workers with nodejs_compat) the copy follows the callback's async work.
27
+ * Without it (browsers, Workers without nodejs_compat) the copy is current
28
+ * only until the callback first awaits: it is never left current for other
29
+ * work to pick up, which would send one request's user with another's error.
30
+ * After an await, capture through the scope argument (scope.captureException).
31
+ */
32
+ export declare function withScope<T>(callback: (scope: Scope) => T): T;
33
+ /** Runs a capture against the client with the given scope, or the current one. */
34
+ export declare function captureWith(scope: Scope | undefined, run: (client: Client, scope: Scope) => string): string;
35
+ /** captureMessage's second argument: a level or a context. */
36
+ export declare function messageContext(levelOrContext: Level | CaptureContext | undefined): CaptureContext | undefined;
37
+ export {};
@@ -0,0 +1,111 @@
1
+ import { getBuiltin } from "./runtime.js";
2
+ import { Scope } from "./scope.js";
3
+ import { uuid4 } from "./utils.js";
4
+ // On globalThis so the ESM and CJS builds, if both get loaded, share one SDK.
5
+ const KEY = "__statossSdk";
6
+ let fallback;
7
+ export function getCarrier() {
8
+ const g = globalThis;
9
+ const existing = g[KEY] ?? fallback;
10
+ if (existing)
11
+ return existing;
12
+ const carrier = { root: new Scope(), stack: [] };
13
+ try {
14
+ Object.defineProperty(g, KEY, {
15
+ value: carrier,
16
+ configurable: true,
17
+ enumerable: false,
18
+ writable: true,
19
+ });
20
+ }
21
+ catch {
22
+ fallback = carrier;
23
+ }
24
+ return carrier;
25
+ }
26
+ export function getClient() {
27
+ return getCarrier().client;
28
+ }
29
+ function getStorage(carrier) {
30
+ if (carrier.storage !== undefined)
31
+ return carrier.storage;
32
+ let storage = null;
33
+ try {
34
+ // Vercel Edge and Next.js edge expose it as a global; Node, Bun and Deno through node:async_hooks.
35
+ const global = globalThis
36
+ .AsyncLocalStorage;
37
+ const Ctor = typeof global === "function"
38
+ ? global
39
+ : getBuiltin("node:async_hooks")
40
+ ?.AsyncLocalStorage;
41
+ if (typeof Ctor === "function")
42
+ storage = new Ctor();
43
+ }
44
+ catch {
45
+ storage = null;
46
+ }
47
+ carrier.storage = storage;
48
+ return storage;
49
+ }
50
+ export function getCurrentScope() {
51
+ const carrier = getCarrier();
52
+ const fromStorage = carrier.storage ? carrier.storage.getStore() : undefined;
53
+ if (fromStorage)
54
+ return fromStorage;
55
+ return carrier.stack.length > 0
56
+ ? carrier.stack[carrier.stack.length - 1]
57
+ : carrier.root;
58
+ }
59
+ /**
60
+ * Runs callback with a copy of the current scope, so what it sets applies only
61
+ * to events captured inside it.
62
+ *
63
+ * With AsyncLocalStorage (Node 20.16+ and 22.3+, Bun, Deno, Vercel Edge,
64
+ * Workers with nodejs_compat) the copy follows the callback's async work.
65
+ * Without it (browsers, Workers without nodejs_compat) the copy is current
66
+ * only until the callback first awaits: it is never left current for other
67
+ * work to pick up, which would send one request's user with another's error.
68
+ * After an await, capture through the scope argument (scope.captureException).
69
+ */
70
+ export function withScope(callback) {
71
+ const carrier = getCarrier();
72
+ const scope = getCurrentScope().clone();
73
+ const storage = getStorage(carrier);
74
+ if (storage)
75
+ return storage.run(scope, () => callback(scope));
76
+ const logger = carrier.client?.logger;
77
+ if (logger?.enabled && !carrier.warnedNoStorage) {
78
+ carrier.warnedNoStorage = true;
79
+ logger.warn("withScope cannot isolate across awaits here (no AsyncLocalStorage): its scope is current only until the callback first awaits. After that, capture with the scope it passes, such as scope.captureException(error).");
80
+ }
81
+ carrier.stack.push(scope);
82
+ try {
83
+ return callback(scope);
84
+ }
85
+ finally {
86
+ const i = carrier.stack.lastIndexOf(scope);
87
+ if (i >= 0)
88
+ carrier.stack.splice(i, 1);
89
+ }
90
+ }
91
+ /** Runs a capture against the client with the given scope, or the current one. */
92
+ export function captureWith(scope, run) {
93
+ try {
94
+ const carrier = getCarrier();
95
+ if (carrier.client) {
96
+ const id = run(carrier.client, scope ?? getCurrentScope());
97
+ carrier.lastEventId = id;
98
+ return id;
99
+ }
100
+ }
101
+ catch {
102
+ // never throw into the app
103
+ }
104
+ return uuid4();
105
+ }
106
+ /** captureMessage's second argument: a level or a context. */
107
+ export function messageContext(levelOrContext) {
108
+ return typeof levelOrContext === "string"
109
+ ? { level: levelOrContext }
110
+ : levelOrContext;
111
+ }
@@ -0,0 +1,38 @@
1
+ import type { Breadcrumb, CaptureContext, Event, InitOptions, Level, Primitive, User } from "./types.js";
2
+ export { withScope } from "./hub.js";
3
+ export type { Scope } from "./scope.js";
4
+ export { SDK_NAME, SDK_VERSION } from "./version.js";
5
+ export type { Breadcrumb, BreadcrumbHint, CaptureContext, Event, EventHint, Exception, InitOptions, IntegrationToggles, Level, Mechanism, Primitive, RequestInfo, ScopeData, SdkInfo, SentryLevel, StackFrame, Transport, TransportRequest, TransportResponse, User, } from "./types.js";
6
+ /**
7
+ * Starts the SDK. Call it once, as early as you can. Calling it again replaces
8
+ * the earlier setup. Without a valid DSN, or with enabled: false, nothing is
9
+ * sent and nothing is patched.
10
+ */
11
+ export declare function init(options?: InitOptions): void;
12
+ /** Sends an error (or anything thrown). Returns the event id. */
13
+ export declare function captureException(error: unknown, context?: CaptureContext): string;
14
+ /**
15
+ * Sends a message. The second argument is a level (Sentry's or your own) or
16
+ * a context. Returns the event id.
17
+ */
18
+ export declare function captureMessage(message: string, levelOrContext?: Level | CaptureContext): string;
19
+ /** Sends an event you built in Sentry's schema. Returns the event id. */
20
+ export declare function captureEvent(event: Event, context?: CaptureContext): string;
21
+ /** Pass null to forget the user. */
22
+ export declare function setUser(user: User | null): void;
23
+ export declare function setTag(key: string, value: Primitive): void;
24
+ export declare function setTags(tags: Record<string, Primitive>): void;
25
+ /** Pass null to remove the context. */
26
+ export declare function setContext(name: string, context: Record<string, unknown> | null): void;
27
+ export declare function setExtra(key: string, value: unknown): void;
28
+ /** Records a breadcrumb on the current scope, through beforeBreadcrumb. Ignored before init. */
29
+ export declare function addBreadcrumb(breadcrumb: Breadcrumb): void;
30
+ /**
31
+ * Waits up to timeoutMs (default 2000) for captured events to be sent.
32
+ * Resolves true once nothing is left to send, false on timeout or before init.
33
+ */
34
+ export declare function flush(timeoutMs?: number): Promise<boolean>;
35
+ /** Flushes, then stops the SDK and removes its integrations. */
36
+ export declare function close(timeoutMs?: number): Promise<boolean>;
37
+ /** The id of the last event captured, sent or not. */
38
+ export declare function lastEventId(): string | undefined;
@@ -0,0 +1,88 @@
1
+ import { Client } from "./client.js";
2
+ import { captureWith, getCarrier, getCurrentScope, messageContext, } from "./hub.js";
3
+ import { isObject, safely } from "./utils.js";
4
+ export { withScope } from "./hub.js";
5
+ export { SDK_NAME, SDK_VERSION } from "./version.js";
6
+ /**
7
+ * Starts the SDK. Call it once, as early as you can. Calling it again replaces
8
+ * the earlier setup. Without a valid DSN, or with enabled: false, nothing is
9
+ * sent and nothing is patched.
10
+ */
11
+ export function init(options = {}) {
12
+ try {
13
+ const carrier = getCarrier();
14
+ carrier.client?.teardown();
15
+ carrier.client = undefined;
16
+ const opts = isObject(options) ? options : {};
17
+ const client = new Client(opts);
18
+ carrier.root._apply(opts.initialScope);
19
+ if (opts.tags)
20
+ carrier.root.setTags(opts.tags);
21
+ carrier.client = client;
22
+ client.setup();
23
+ }
24
+ catch (error) {
25
+ if (isObject(options) && options.debug) {
26
+ safely(() => console.error("[StatOSS] init failed:", error));
27
+ }
28
+ }
29
+ }
30
+ /** Sends an error (or anything thrown). Returns the event id. */
31
+ export function captureException(error, context) {
32
+ return captureWith(undefined, (client, scope) => client.captureException(error, context, scope));
33
+ }
34
+ /**
35
+ * Sends a message. The second argument is a level (Sentry's or your own) or
36
+ * a context. Returns the event id.
37
+ */
38
+ export function captureMessage(message, levelOrContext) {
39
+ return captureWith(undefined, (client, scope) => client.captureMessage(message, messageContext(levelOrContext), scope));
40
+ }
41
+ /** Sends an event you built in Sentry's schema. Returns the event id. */
42
+ export function captureEvent(event, context) {
43
+ return captureWith(undefined, (client, scope) => client.captureEvent(event, context, scope));
44
+ }
45
+ /** Pass null to forget the user. */
46
+ export function setUser(user) {
47
+ safely(() => getCurrentScope().setUser(user));
48
+ }
49
+ export function setTag(key, value) {
50
+ safely(() => getCurrentScope().setTag(key, value));
51
+ }
52
+ export function setTags(tags) {
53
+ safely(() => getCurrentScope().setTags(tags));
54
+ }
55
+ /** Pass null to remove the context. */
56
+ export function setContext(name, context) {
57
+ safely(() => getCurrentScope().setContext(name, context));
58
+ }
59
+ export function setExtra(key, value) {
60
+ safely(() => getCurrentScope().setExtra(key, value));
61
+ }
62
+ /** Records a breadcrumb on the current scope, through beforeBreadcrumb. Ignored before init. */
63
+ export function addBreadcrumb(breadcrumb) {
64
+ safely(() => getCarrier().client?.addBreadcrumb(breadcrumb));
65
+ }
66
+ /**
67
+ * Waits up to timeoutMs (default 2000) for captured events to be sent.
68
+ * Resolves true once nothing is left to send, false on timeout or before init.
69
+ */
70
+ export function flush(timeoutMs) {
71
+ const client = safely(() => getCarrier().client);
72
+ if (!client)
73
+ return Promise.resolve(false);
74
+ return client.flush(timeoutMs).catch(() => false);
75
+ }
76
+ /** Flushes, then stops the SDK and removes its integrations. */
77
+ export function close(timeoutMs) {
78
+ const carrier = safely(() => getCarrier());
79
+ const client = carrier?.client;
80
+ if (!carrier || !client)
81
+ return Promise.resolve(false);
82
+ carrier.client = undefined;
83
+ return client.close(timeoutMs).catch(() => false);
84
+ }
85
+ /** The id of the last event captured, sent or not. */
86
+ export function lastEventId() {
87
+ return safely(() => getCarrier().lastEventId);
88
+ }
@@ -0,0 +1,7 @@
1
+ import type { Client } from "../client.js";
2
+ /** Breadcrumbs for pushState, replaceState and popstate. */
3
+ export declare function installNavigation(client: Client): (() => void) | undefined;
4
+ /** A short CSS-like path to an element, such as `div#app > button.buy[type="submit"]`. */
5
+ export declare function selectorFor(target: unknown): string;
6
+ /** A breadcrumb per click, once a second at most for the same element. */
7
+ export declare function installClicks(client: Client): (() => void) | undefined;
@@ -0,0 +1,119 @@
1
+ import { isObject, safely, scrubUrl } from "../utils.js";
2
+ function relative(url, origin) {
3
+ const clean = scrubUrl(url);
4
+ return origin && clean.startsWith(`${origin}/`)
5
+ ? clean.slice(origin.length)
6
+ : clean;
7
+ }
8
+ /** Breadcrumbs for pushState, replaceState and popstate. */
9
+ export function installNavigation(client) {
10
+ const w = globalThis;
11
+ const history = w.history;
12
+ if (!history || !w.location)
13
+ return undefined;
14
+ let last = safely(() => w.location?.href) ?? "";
15
+ const record = () => {
16
+ const to = safely(() => w.location?.href);
17
+ if (typeof to !== "string" || to === last)
18
+ return;
19
+ const origin = safely(() => w.location?.origin);
20
+ const from = last;
21
+ last = to;
22
+ client.addBreadcrumb({
23
+ category: "navigation",
24
+ data: { from: relative(from, origin), to: relative(to, origin) },
25
+ }, { from, to });
26
+ };
27
+ const restore = [];
28
+ for (const name of ["pushState", "replaceState"]) {
29
+ const original = history[name];
30
+ if (typeof original !== "function")
31
+ continue;
32
+ const fn = original;
33
+ const wrapper = function (...args) {
34
+ const result = fn.apply(this, args);
35
+ safely(record);
36
+ return result;
37
+ };
38
+ history[name] = wrapper;
39
+ restore.push(() => {
40
+ if (history[name] === wrapper)
41
+ history[name] = original;
42
+ });
43
+ }
44
+ w.addEventListener?.("popstate", record);
45
+ return () => {
46
+ for (const undo of restore)
47
+ undo();
48
+ safely(() => w.removeEventListener?.("popstate", record));
49
+ };
50
+ }
51
+ const ATTRIBUTES = ["name", "type", "title", "alt", "aria-label"];
52
+ const MAX_SELECTOR = 80;
53
+ const MAX_DEPTH = 5;
54
+ function describeElement(el) {
55
+ if (typeof el.tagName !== "string")
56
+ return "";
57
+ let out = el.tagName.toLowerCase();
58
+ if (typeof el.id === "string" && el.id)
59
+ out += `#${el.id}`;
60
+ if (typeof el.className === "string" && el.className.trim()) {
61
+ out += el.className
62
+ .trim()
63
+ .split(/\s+/)
64
+ .slice(0, 3)
65
+ .map((c) => `.${c}`)
66
+ .join("");
67
+ }
68
+ for (const attr of ATTRIBUTES) {
69
+ const value = safely(() => el.getAttribute?.(attr));
70
+ if (typeof value === "string" && value)
71
+ out += `[${attr}="${value.slice(0, 30)}"]`;
72
+ }
73
+ return out;
74
+ }
75
+ /** A short CSS-like path to an element, such as `div#app > button.buy[type="submit"]`. */
76
+ export function selectorFor(target) {
77
+ const parts = [];
78
+ let el = target;
79
+ let length = 0;
80
+ for (let depth = 0; isObject(el) && depth < MAX_DEPTH; depth++) {
81
+ const part = describeElement(el);
82
+ if (!part || part === "html")
83
+ break;
84
+ if (parts.length > 0 && length + part.length + 3 > MAX_SELECTOR)
85
+ break;
86
+ parts.unshift(part);
87
+ length += part.length + 3;
88
+ el = el.parentElement;
89
+ }
90
+ return parts.join(" > ");
91
+ }
92
+ /** A breadcrumb per click, once a second at most for the same element. */
93
+ export function installClicks(client) {
94
+ const doc = globalThis.document;
95
+ if (!doc || typeof doc.addEventListener !== "function")
96
+ return undefined;
97
+ let lastTarget;
98
+ let lastTime = 0;
99
+ const onClick = (event) => {
100
+ safely(() => {
101
+ const target = isObject(event) ? event.target : undefined;
102
+ if (!isObject(target))
103
+ return;
104
+ const time = Date.now();
105
+ if (target === lastTarget && time - lastTime < 1000)
106
+ return;
107
+ lastTarget = target;
108
+ lastTime = time;
109
+ const selector = selectorFor(target);
110
+ if (selector)
111
+ client.addBreadcrumb({ category: "ui.click", message: selector }, { event });
112
+ });
113
+ };
114
+ const options = { capture: true, passive: true };
115
+ doc.addEventListener("click", onClick, options);
116
+ return () => {
117
+ safely(() => doc.removeEventListener?.("click", onClick, options));
118
+ };
119
+ }
@@ -0,0 +1,3 @@
1
+ import type { Client } from "../client.js";
2
+ /** Breadcrumbs for console calls; the original methods still run, unchanged. */
3
+ export declare function installConsole(client: Client): (() => void) | undefined;
@@ -0,0 +1,103 @@
1
+ import { originalConsole } from "../logger.js";
2
+ import { isErrorLike, normalize, safely, safeString, truncate, } from "../utils.js";
3
+ const METHODS = [
4
+ "debug",
5
+ "info",
6
+ "warn",
7
+ "error",
8
+ "log",
9
+ "assert",
10
+ ];
11
+ const LEVELS = {
12
+ debug: "debug",
13
+ info: "info",
14
+ warn: "warning",
15
+ error: "error",
16
+ log: "info",
17
+ assert: "error",
18
+ };
19
+ const MAX_MESSAGE = 1024;
20
+ // Every part is cut before it is joined, so a huge argument is never copied whole.
21
+ function format(arg) {
22
+ if (typeof arg === "string")
23
+ return truncate(arg, MAX_MESSAGE);
24
+ if (isErrorLike(arg)) {
25
+ return `${truncate(safeString(arg.name), 200)}: ${truncate(safeString(arg.message), MAX_MESSAGE)}`;
26
+ }
27
+ if (arg !== null && typeof arg === "object") {
28
+ const normalized = normalize(arg, 2, 200, 20);
29
+ if (typeof normalized === "string")
30
+ return normalized;
31
+ try {
32
+ return truncate(JSON.stringify(normalized), MAX_MESSAGE);
33
+ }
34
+ catch {
35
+ return "[Object]";
36
+ }
37
+ }
38
+ return truncate(safeString(arg), MAX_MESSAGE);
39
+ }
40
+ function join(args) {
41
+ let out = "";
42
+ for (let i = 0; i < args.length && out.length <= MAX_MESSAGE; i++) {
43
+ out += (i > 0 ? " " : "") + format(args[i]);
44
+ }
45
+ return out;
46
+ }
47
+ /** Breadcrumbs for console calls; the original methods still run, unchanged. */
48
+ export function installConsole(client) {
49
+ const target = globalThis.console;
50
+ if (!target)
51
+ return undefined;
52
+ const wrapped = {};
53
+ let inside = false;
54
+ for (const method of METHODS) {
55
+ const original = target[method];
56
+ if (typeof original !== "function")
57
+ continue;
58
+ const fn = original;
59
+ originalConsole[method] = fn;
60
+ // One breadcrumb per call: console.assert, for one, calls console.warn itself.
61
+ const wrapper = function (...args) {
62
+ if (inside)
63
+ return fn.apply(this, args);
64
+ inside = true;
65
+ try {
66
+ safely(() => record(client, method, args));
67
+ return fn.apply(this, args);
68
+ }
69
+ finally {
70
+ inside = false;
71
+ }
72
+ };
73
+ wrapped[method] = wrapper;
74
+ target[method] = wrapper;
75
+ }
76
+ return () => {
77
+ for (const method of METHODS) {
78
+ // Leave it if something else has wrapped the console since.
79
+ if (wrapped[method] && target[method] === wrapped[method]) {
80
+ target[method] = originalConsole[method];
81
+ }
82
+ delete originalConsole[method];
83
+ }
84
+ };
85
+ }
86
+ function record(client, method, args) {
87
+ let message;
88
+ if (method === "assert") {
89
+ if (args[0])
90
+ return;
91
+ const rest = join(args.slice(1));
92
+ message = `Assertion failed: ${rest || "console.assert"}`;
93
+ }
94
+ else {
95
+ message = join(args);
96
+ }
97
+ client.addBreadcrumb({
98
+ category: "console",
99
+ level: LEVELS[method],
100
+ message: truncate(message, MAX_MESSAGE),
101
+ data: { logger: "console" },
102
+ }, { input: args, level: method });
103
+ }
@@ -0,0 +1,7 @@
1
+ import type { Event } from "../types.js";
2
+ /**
3
+ * What makes two events the same for dedupe, as Sentry's Dedupe compares
4
+ * them: message, exception types and values, frames and fingerprint.
5
+ * Undefined for events with neither message nor exception.
6
+ */
7
+ export declare function eventSignature(event: Event): string | undefined;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * What makes two events the same for dedupe, as Sentry's Dedupe compares
3
+ * them: message, exception types and values, frames and fingerprint.
4
+ * Undefined for events with neither message nor exception.
5
+ */
6
+ export function eventSignature(event) {
7
+ const exceptions = event.exception?.values?.map((e) => [
8
+ e.type,
9
+ e.value,
10
+ (e.stacktrace?.frames ?? []).map((f) => [
11
+ f.filename,
12
+ f.function,
13
+ f.lineno,
14
+ f.colno,
15
+ ]),
16
+ ]);
17
+ if (!exceptions && event.message === undefined)
18
+ return undefined;
19
+ try {
20
+ return JSON.stringify([
21
+ event.message ?? null,
22
+ exceptions ?? null,
23
+ event.fingerprint ?? null,
24
+ ]);
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
@@ -0,0 +1,3 @@
1
+ import type { Client } from "../client.js";
2
+ /** Breadcrumbs for fetch calls: method, URL without secrets, status. */
3
+ export declare function installFetch(client: Client): (() => void) | undefined;