@edraj/sauron-node 1.0.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/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @edraj/sauron-node
2
+
3
+ Server-side Node/TypeScript SDK for [Sauron](https://sauron.dev) — dispatch
4
+ product-analytics events and captured exceptions from your Node backends.
5
+
6
+ This is the **server-side** SDK (no browser/DOM/auto-instrumentation). For the
7
+ browser, use `@edraj/sauron-browser` (`sdks/js`).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @edraj/sauron-node
13
+ ```
14
+
15
+ Requires Node >= 18 (uses the global `fetch` and `zlib`).
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import {
21
+ init,
22
+ track,
23
+ captureException,
24
+ captureMessage,
25
+ identify,
26
+ trackTransaction,
27
+ addBreadcrumb,
28
+ withScope,
29
+ setUser,
30
+ setTag,
31
+ flush,
32
+ close,
33
+ } from '@edraj/sauron-node';
34
+
35
+ init({
36
+ dsn: 'https://<public_key>@<host>/<project_id>',
37
+ environment: 'production',
38
+ release: '1.4.2',
39
+ // Opt-in (both default off):
40
+ autoCaptureUnhandled: true, // capture uncaughtException / unhandledRejection
41
+ autoShutdown: true, // flush on beforeExit / SIGTERM / SIGINT
42
+ });
43
+
44
+ // Product analytics — distinctId is required.
45
+ track('order_completed', 'user-123', { total: 42.5, currency: 'USD' });
46
+
47
+ // Exceptions
48
+ try {
49
+ doWork();
50
+ } catch (err) {
51
+ captureException(err, { user: { id: 'user-123' }, tags: { area: 'checkout' } });
52
+ }
53
+
54
+ captureMessage('cache warm-up finished', 'info');
55
+ identify('user-123', { plan: 'pro' });
56
+
57
+ // On shutdown
58
+ await close(); // flushes then stops the background timer
59
+ ```
60
+
61
+ ### Per-request scope
62
+
63
+ Isolate user/tags/breadcrumbs per request with `withScope` — backed by
64
+ `AsyncLocalStorage`, so concurrent requests never leak state into each other:
65
+
66
+ ```ts
67
+ app.use((req, res, next) => {
68
+ withScope(() => {
69
+ setUser({ id: req.userId });
70
+ setTag('route', req.route.path);
71
+ addBreadcrumb({ category: 'http', message: `${req.method} ${req.url}` });
72
+ next();
73
+ });
74
+ });
75
+ ```
76
+
77
+ `captureException` automatically attaches the active scope's user, tags and
78
+ breadcrumb trail. An optional `fingerprint` override is honored verbatim by the
79
+ backend.
80
+
81
+ ### Transactions
82
+
83
+ ```ts
84
+ trackTransaction({
85
+ name: 'GET /api/users',
86
+ op: 'http',
87
+ duration_ms: 12.5,
88
+ http_method: 'GET',
89
+ http_status: 200,
90
+ }); // distinct_id falls back to the scoped user's id when omitted
91
+ ```
92
+
93
+ ## API
94
+
95
+ | Function | Description |
96
+ | --- | --- |
97
+ | `init(options)` | Create the global client. Throws `DsnError` on an invalid DSN. |
98
+ | `track(event, distinctId, properties?)` | Capture an analytics event. |
99
+ | `captureException(error, options?)` | Capture a native `Error` (attaches scope). |
100
+ | `captureMessage(message, level?)` | Capture a bare message. |
101
+ | `identify(distinctId, traits?)` | Associate traits with a user. |
102
+ | `trackTransaction(input)` | Emit a performance transaction. |
103
+ | `addBreadcrumb(crumb)` | Add a breadcrumb to the active scope (runs `beforeBreadcrumb`). |
104
+ | `setUser / setTag / setTags / setContext / setExtra` | Mutate the active scope. |
105
+ | `withScope(cb)` / `configureScope(cb)` | Run with an isolated child scope / mutate the current one. |
106
+ | `installShutdownHooks(client)` | Wire `beforeExit`/`SIGTERM`/`SIGINT` to `close()`. |
107
+ | `flush()` | Send buffered items immediately. |
108
+ | `close()` | Flush, stop the timer, and remove any installed process hooks. |
109
+
110
+ Every dispatch function is a no-op before `init` / when the SDK is disabled.
111
+
112
+ ### `init` options
113
+
114
+ `environment`, `release`, `sampleRate`, `flushInterval`, `maxBatch`,
115
+ `maxBreadcrumbs` (default 100), `gzipThresholdBytes` (default 1024),
116
+ `maxQueueBytes` (default 1 MiB), `offlineDir` (opt-in FIFO disk persistence),
117
+ `maxRetries` (default 3), `autoCaptureUnhandled` (default off),
118
+ `autoShutdown` (default off), `beforeSend(item)`, `beforeBreadcrumb(crumb)`.
119
+
120
+ ## Transport
121
+
122
+ Items buffer in a byte-bounded in-memory queue (drop-oldest past `maxQueueBytes`,
123
+ optionally persisted to `offlineDir` for at-least-once delivery across restarts)
124
+ and flush every `flushInterval` ms (default 5000) or once `maxBatch` items
125
+ (default 30) accumulate. The flush timer is `unref`'d so it never keeps your
126
+ process alive. Each flush POSTs one envelope to
127
+ `{proto}://{host}/api/{project_id}/envelope` with an `X-Sauron-Key` header,
128
+ gzipping the body once it crosses `gzipThresholdBytes` (`Content-Encoding: gzip`).
129
+ Transient failures (408/413/429/5xx, network) retry with exponential backoff +
130
+ jitter honoring `Retry-After`; 400/401/403/404 drop without retry.
131
+
132
+ ## Development
133
+
134
+ ```bash
135
+ npm install
136
+ npm run build
137
+ npm test
138
+ ```
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Opt-in process-level hooks: auto-capture of uncaught errors and graceful
3
+ * shutdown. Both are OFF by default (see {@link InitOptions.autoCaptureUnhandled}
4
+ * / {@link InitOptions.autoShutdown}) and only installed when the consumer opts
5
+ * in.
6
+ *
7
+ * Auto-capture never *swallows* a crash. An uncaught exception is captured with
8
+ * `mechanism.handled = false`, the batch is flushed, and Node's default
9
+ * behavior is preserved: if this SDK is the *sole* `uncaughtException` handler,
10
+ * the process still exits non-zero (as Node would with no handler at all);
11
+ * if another handler is registered, that handler decides the process's fate.
12
+ * Unhandled rejections are captured but never terminate the process on their
13
+ * own — Node's own `unhandledRejection` mode still governs that.
14
+ */
15
+ import type { ProcessLike } from './types.js';
16
+ import type { SauronClient } from './client.js';
17
+ export interface AutoCaptureOptions {
18
+ /** Injected process (tests). Defaults to the real Node `process`. */
19
+ process?: ProcessLike;
20
+ }
21
+ type Uninstaller = () => void;
22
+ /**
23
+ * Register `uncaughtException` + `unhandledRejection` handlers that capture with
24
+ * `mechanism.handled = false`. Idempotent per client; returns an uninstaller
25
+ * that removes the listeners.
26
+ */
27
+ export declare function installAutoCapture(client: SauronClient, options?: AutoCaptureOptions): Uninstaller;
28
+ /**
29
+ * Wire `beforeExit`/`SIGTERM`/`SIGINT` to `client.close()` for a graceful flush
30
+ * on shutdown. `beforeExit` (event loop drained) just closes; a terminating
31
+ * signal closes then exits with the conventional code so the SDK does not hang
32
+ * the process. Idempotent per client; returns an uninstaller.
33
+ */
34
+ export declare function installShutdownHooks(client: SauronClient, options?: AutoCaptureOptions): Uninstaller;
35
+ export {};
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Opt-in process-level hooks: auto-capture of uncaught errors and graceful
3
+ * shutdown. Both are OFF by default (see {@link InitOptions.autoCaptureUnhandled}
4
+ * / {@link InitOptions.autoShutdown}) and only installed when the consumer opts
5
+ * in.
6
+ *
7
+ * Auto-capture never *swallows* a crash. An uncaught exception is captured with
8
+ * `mechanism.handled = false`, the batch is flushed, and Node's default
9
+ * behavior is preserved: if this SDK is the *sole* `uncaughtException` handler,
10
+ * the process still exits non-zero (as Node would with no handler at all);
11
+ * if another handler is registered, that handler decides the process's fate.
12
+ * Unhandled rejections are captured but never terminate the process on their
13
+ * own — Node's own `unhandledRejection` mode still governs that.
14
+ */
15
+ /** Conventional exit code for a terminating signal (`128 + signal number`). */
16
+ const SIGNAL_EXIT_CODE = { SIGINT: 130, SIGTERM: 143 };
17
+ const autoCaptureInstalled = new WeakMap();
18
+ const shutdownInstalled = new WeakMap();
19
+ function realProcess() {
20
+ return process;
21
+ }
22
+ /**
23
+ * Register `uncaughtException` + `unhandledRejection` handlers that capture with
24
+ * `mechanism.handled = false`. Idempotent per client; returns an uninstaller
25
+ * that removes the listeners.
26
+ */
27
+ export function installAutoCapture(client, options = {}) {
28
+ const existing = autoCaptureInstalled.get(client);
29
+ if (existing)
30
+ return existing;
31
+ const proc = options.process ?? realProcess();
32
+ // Guard against a capture path itself throwing and re-entering the handler.
33
+ let capturing = false;
34
+ const onUncaught = (error) => {
35
+ if (capturing)
36
+ return;
37
+ capturing = true;
38
+ try {
39
+ client.captureException(error, { level: 'fatal', handled: false });
40
+ }
41
+ catch {
42
+ // Never let a capture failure mask the original crash.
43
+ }
44
+ capturing = false;
45
+ void client.flush().then(exitIfSole, exitIfSole);
46
+ };
47
+ const exitIfSole = () => {
48
+ const others = proc
49
+ .listeners('uncaughtException')
50
+ .filter((listener) => listener !== onUncaught);
51
+ if (others.length === 0)
52
+ proc.exit(1);
53
+ };
54
+ const onRejection = (reason) => {
55
+ if (capturing)
56
+ return;
57
+ capturing = true;
58
+ try {
59
+ client.captureException(reason, { level: 'error', handled: false });
60
+ }
61
+ catch {
62
+ // ignore — see above.
63
+ }
64
+ capturing = false;
65
+ void client.flush();
66
+ };
67
+ proc.on('uncaughtException', onUncaught);
68
+ proc.on('unhandledRejection', onRejection);
69
+ const uninstall = () => {
70
+ proc.removeListener('uncaughtException', onUncaught);
71
+ proc.removeListener('unhandledRejection', onRejection);
72
+ autoCaptureInstalled.delete(client);
73
+ };
74
+ autoCaptureInstalled.set(client, uninstall);
75
+ return uninstall;
76
+ }
77
+ /**
78
+ * Wire `beforeExit`/`SIGTERM`/`SIGINT` to `client.close()` for a graceful flush
79
+ * on shutdown. `beforeExit` (event loop drained) just closes; a terminating
80
+ * signal closes then exits with the conventional code so the SDK does not hang
81
+ * the process. Idempotent per client; returns an uninstaller.
82
+ */
83
+ export function installShutdownHooks(client, options = {}) {
84
+ const existing = shutdownInstalled.get(client);
85
+ if (existing)
86
+ return existing;
87
+ const proc = options.process ?? realProcess();
88
+ let closing = false;
89
+ const onBeforeExit = () => {
90
+ if (closing)
91
+ return;
92
+ closing = true;
93
+ void client.close();
94
+ };
95
+ const makeSignalHandler = (signal) => () => {
96
+ if (closing)
97
+ return;
98
+ closing = true;
99
+ const code = SIGNAL_EXIT_CODE[signal] ?? 0;
100
+ void client.close().then(() => proc.exit(code), () => proc.exit(code));
101
+ };
102
+ const onSigterm = makeSignalHandler('SIGTERM');
103
+ const onSigint = makeSignalHandler('SIGINT');
104
+ proc.on('beforeExit', onBeforeExit);
105
+ proc.on('SIGTERM', onSigterm);
106
+ proc.on('SIGINT', onSigint);
107
+ const uninstall = () => {
108
+ proc.removeListener('beforeExit', onBeforeExit);
109
+ proc.removeListener('SIGTERM', onSigterm);
110
+ proc.removeListener('SIGINT', onSigint);
111
+ shutdownInstalled.delete(client);
112
+ };
113
+ shutdownInstalled.set(client, uninstall);
114
+ return uninstall;
115
+ }
@@ -0,0 +1,42 @@
1
+ import type { BreadcrumbInput, CaptureExceptionOptions, InitOptions, Level, MetadataOptions, TransactionInput } from './types.js';
2
+ /**
3
+ * The Sauron server-side client. Buffers events/errors and dispatches them via
4
+ * a background transport. Constructed by {@link init}.
5
+ */
6
+ export declare class SauronClient {
7
+ private readonly options;
8
+ private readonly transport;
9
+ /** Uninstallers for any opt-in process-level hooks, torn down on {@link close}. */
10
+ private readonly hookUninstallers;
11
+ constructor(options: InitOptions);
12
+ /**
13
+ * The single enqueue chokepoint. Runs `beforeSend` on every item; a `null`
14
+ * return drops it, a returned item replaces it, then it is handed to the
15
+ * transport.
16
+ */
17
+ private dispatch;
18
+ /**
19
+ * Add a breadcrumb to the active scope. Runs `beforeBreadcrumb` first; a
20
+ * `null` return drops the crumb.
21
+ */
22
+ addBreadcrumb(crumb: BreadcrumbInput): void;
23
+ /** Emit a performance transaction item. */
24
+ trackTransaction(input: TransactionInput): void;
25
+ /** Capture a product-analytics event. `distinctId` is required. */
26
+ track(event: string, distinctId: string, properties?: Record<string, unknown>, options?: MetadataOptions): void;
27
+ /** Capture a native `Error` (or error-like value) as an error item. */
28
+ captureException(error: unknown, options?: CaptureExceptionOptions): void;
29
+ /** Capture a bare message as an error item (no exception payload). */
30
+ captureMessage(message: string, level?: Level, options?: MetadataOptions): void;
31
+ /** Associate traits with a distinct id. */
32
+ identify(distinctId: string, traits?: Record<string, unknown>): void;
33
+ /** Send any buffered items immediately. */
34
+ flush(): Promise<void>;
35
+ /** Flush then stop the background timer, and remove any opt-in process hooks. */
36
+ close(): Promise<void>;
37
+ }
38
+ /** Derive `{type, value}` from an arbitrary thrown value. */
39
+ export declare function describeError(error: unknown): {
40
+ type: string;
41
+ value: string | null;
42
+ };
package/dist/client.js ADDED
@@ -0,0 +1,291 @@
1
+ import os from 'node:os';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { parseDsn } from './dsn.js';
4
+ import { Transport } from './transport.js';
5
+ import { parseError } from './stacktrace.js';
6
+ import { installAutoCapture, installShutdownHooks } from './autocapture.js';
7
+ import { getCurrentScope, getGlobalScope, normalizeBreadcrumb, } from './scope.js';
8
+ const DEFAULTS = {
9
+ environment: 'production',
10
+ release: null,
11
+ sampleRate: 1,
12
+ flushInterval: 5000,
13
+ maxBatch: 30,
14
+ maxBreadcrumbs: 100,
15
+ gzipThresholdBytes: 1024,
16
+ maxQueueBytes: 1_048_576,
17
+ maxRetries: 3,
18
+ debug: false,
19
+ };
20
+ function resolveOptions(options) {
21
+ if (!options || typeof options.dsn !== 'string') {
22
+ throw new Error('[sauron] init requires a { dsn } option');
23
+ }
24
+ const sampleRate = typeof options.sampleRate === 'number' ? options.sampleRate : DEFAULTS.sampleRate;
25
+ return {
26
+ dsn: options.dsn,
27
+ environment: options.environment ?? DEFAULTS.environment,
28
+ release: options.release ?? DEFAULTS.release,
29
+ tags: options.tags ?? {},
30
+ contexts: options.contexts ?? {},
31
+ extra: options.extra ?? {},
32
+ sampleRate: Math.min(1, Math.max(0, sampleRate)),
33
+ flushInterval: typeof options.flushInterval === 'number'
34
+ ? options.flushInterval
35
+ : DEFAULTS.flushInterval,
36
+ maxBatch: typeof options.maxBatch === 'number' ? options.maxBatch : DEFAULTS.maxBatch,
37
+ maxBreadcrumbs: typeof options.maxBreadcrumbs === 'number'
38
+ ? options.maxBreadcrumbs
39
+ : DEFAULTS.maxBreadcrumbs,
40
+ gzipThresholdBytes: typeof options.gzipThresholdBytes === 'number'
41
+ ? options.gzipThresholdBytes
42
+ : DEFAULTS.gzipThresholdBytes,
43
+ maxQueueBytes: typeof options.maxQueueBytes === 'number'
44
+ ? options.maxQueueBytes
45
+ : DEFAULTS.maxQueueBytes,
46
+ offlineDir: options.offlineDir ?? null,
47
+ maxRetries: typeof options.maxRetries === 'number' ? options.maxRetries : DEFAULTS.maxRetries,
48
+ autoCaptureUnhandled: options.autoCaptureUnhandled ?? false,
49
+ autoShutdown: options.autoShutdown ?? false,
50
+ beforeSend: options.beforeSend,
51
+ beforeBreadcrumb: options.beforeBreadcrumb,
52
+ fetchImpl: options.fetchImpl,
53
+ debug: options.debug ?? DEFAULTS.debug,
54
+ };
55
+ }
56
+ /** Minimal server-side context assembled once at init. */
57
+ function buildContext() {
58
+ return {
59
+ device: { device_id: randomUUID() },
60
+ os: { name: process.platform || null, version: os.release() || null },
61
+ app: {},
62
+ runtime: { name: 'node', version: process.versions.node ?? null },
63
+ user: null,
64
+ };
65
+ }
66
+ function isoNow() {
67
+ return new Date().toISOString();
68
+ }
69
+ function normalizeUser(user) {
70
+ if (!user)
71
+ return null;
72
+ return {
73
+ id: user.id ?? null,
74
+ email: user.email ?? null,
75
+ username: user.username ?? null,
76
+ };
77
+ }
78
+ /**
79
+ * The Sauron server-side client. Buffers events/errors and dispatches them via
80
+ * a background transport. Constructed by {@link init}.
81
+ */
82
+ export class SauronClient {
83
+ options;
84
+ transport;
85
+ /** Uninstallers for any opt-in process-level hooks, torn down on {@link close}. */
86
+ hookUninstallers = [];
87
+ constructor(options) {
88
+ this.options = resolveOptions(options);
89
+ const dsn = parseDsn(this.options.dsn);
90
+ const globalScope = getGlobalScope();
91
+ globalScope.setMaxBreadcrumbs(this.options.maxBreadcrumbs);
92
+ globalScope.setTags(this.options.tags);
93
+ for (const [name, block] of Object.entries(this.options.contexts)) {
94
+ globalScope.setContext(name, block);
95
+ }
96
+ for (const [key, value] of Object.entries(this.options.extra)) {
97
+ globalScope.setExtra(key, value);
98
+ }
99
+ this.transport = new Transport({
100
+ dsn,
101
+ environment: this.options.environment,
102
+ release: this.options.release,
103
+ context: buildContext(),
104
+ flushInterval: this.options.flushInterval,
105
+ maxBatch: this.options.maxBatch,
106
+ gzipThresholdBytes: this.options.gzipThresholdBytes,
107
+ maxQueueBytes: this.options.maxQueueBytes,
108
+ offlineDir: this.options.offlineDir,
109
+ maxRetries: this.options.maxRetries,
110
+ fetchImpl: this.options.fetchImpl,
111
+ debug: this.options.debug,
112
+ });
113
+ if (this.options.autoCaptureUnhandled) {
114
+ this.hookUninstallers.push(installAutoCapture(this));
115
+ }
116
+ if (this.options.autoShutdown) {
117
+ this.hookUninstallers.push(installShutdownHooks(this));
118
+ }
119
+ }
120
+ /**
121
+ * The single enqueue chokepoint. Runs `beforeSend` on every item; a `null`
122
+ * return drops it, a returned item replaces it, then it is handed to the
123
+ * transport.
124
+ */
125
+ dispatch(item) {
126
+ const beforeSend = this.options.beforeSend;
127
+ if (beforeSend) {
128
+ const result = beforeSend(item);
129
+ if (result == null)
130
+ return;
131
+ this.transport.enqueue(result);
132
+ return;
133
+ }
134
+ this.transport.enqueue(item);
135
+ }
136
+ /**
137
+ * Add a breadcrumb to the active scope. Runs `beforeBreadcrumb` first; a
138
+ * `null` return drops the crumb.
139
+ */
140
+ addBreadcrumb(crumb) {
141
+ const stamped = normalizeBreadcrumb(crumb);
142
+ const beforeBreadcrumb = this.options.beforeBreadcrumb;
143
+ if (beforeBreadcrumb) {
144
+ const result = beforeBreadcrumb(stamped);
145
+ if (result == null)
146
+ return;
147
+ getCurrentScope().addBreadcrumb(result);
148
+ return;
149
+ }
150
+ getCurrentScope().addBreadcrumb(stamped);
151
+ }
152
+ /** Emit a performance transaction item. */
153
+ trackTransaction(input) {
154
+ if (typeof input?.name !== 'string' || input.name.length === 0)
155
+ return;
156
+ const distinctId = input.distinct_id ?? getCurrentScope().data.user?.id ?? undefined;
157
+ const item = {
158
+ type: 'transaction',
159
+ name: input.name,
160
+ op: input.op ?? 'custom',
161
+ duration_ms: input.duration_ms,
162
+ timestamp: isoNow(),
163
+ };
164
+ if (input.status !== undefined)
165
+ item.status = input.status;
166
+ if (input.http_method !== undefined)
167
+ item.http_method = input.http_method;
168
+ if (input.http_status !== undefined)
169
+ item.http_status = input.http_status;
170
+ if (input.url !== undefined)
171
+ item.url = input.url;
172
+ if (distinctId != null)
173
+ item.distinct_id = distinctId;
174
+ this.dispatch(item);
175
+ }
176
+ /** Capture a product-analytics event. `distinctId` is required. */
177
+ track(event, distinctId, properties, options = {}) {
178
+ if (typeof event !== 'string' || event.length === 0)
179
+ return;
180
+ if (typeof distinctId !== 'string' || distinctId.length === 0)
181
+ return;
182
+ const item = {
183
+ type: 'event',
184
+ name: event,
185
+ distinct_id: distinctId,
186
+ properties: properties ?? {},
187
+ timestamp: isoNow(),
188
+ session_id: null,
189
+ screen: null,
190
+ ...getCurrentScope().mergeMetadata(options),
191
+ };
192
+ this.dispatch(item);
193
+ }
194
+ /** Capture a native `Error` (or error-like value) as an error item. */
195
+ captureException(error, options = {}) {
196
+ if (this.options.sampleRate < 1 && Math.random() >= this.options.sampleRate) {
197
+ return;
198
+ }
199
+ const { type, value } = describeError(error);
200
+ const item = {
201
+ type: 'error',
202
+ event_id: randomUUID(),
203
+ level: options.level ?? 'error',
204
+ timestamp: isoNow(),
205
+ exception: {
206
+ type,
207
+ value,
208
+ mechanism: { type: 'generic', handled: options.handled ?? true },
209
+ stacktrace: parseError(error),
210
+ },
211
+ message: null,
212
+ breadcrumbs: [],
213
+ tags: options.tags ?? {},
214
+ contexts: options.contexts ?? {},
215
+ extra: options.extra ?? {},
216
+ fingerprint: options.fingerprint ?? null,
217
+ user: normalizeUser(options.user),
218
+ session_id: null,
219
+ screen: null,
220
+ };
221
+ getCurrentScope().applyToErrorItem(item);
222
+ this.dispatch(item);
223
+ }
224
+ /** Capture a bare message as an error item (no exception payload). */
225
+ captureMessage(message, level = 'info', options = {}) {
226
+ const item = {
227
+ type: 'error',
228
+ event_id: randomUUID(),
229
+ level,
230
+ timestamp: isoNow(),
231
+ exception: {
232
+ type: 'Message',
233
+ value: message,
234
+ mechanism: { type: 'generic', handled: true },
235
+ stacktrace: [],
236
+ },
237
+ message,
238
+ breadcrumbs: [],
239
+ tags: options.tags ?? {},
240
+ contexts: options.contexts ?? {},
241
+ extra: options.extra ?? {},
242
+ fingerprint: null,
243
+ user: null,
244
+ session_id: null,
245
+ screen: null,
246
+ };
247
+ getCurrentScope().applyToErrorItem(item);
248
+ this.dispatch(item);
249
+ }
250
+ /** Associate traits with a distinct id. */
251
+ identify(distinctId, traits) {
252
+ if (typeof distinctId !== 'string' || distinctId.length === 0)
253
+ return;
254
+ const item = {
255
+ type: 'identify',
256
+ distinct_id: distinctId,
257
+ anonymous_id: null,
258
+ traits: traits ?? {},
259
+ timestamp: isoNow(),
260
+ };
261
+ this.dispatch(item);
262
+ }
263
+ /** Send any buffered items immediately. */
264
+ flush() {
265
+ return this.transport.flush();
266
+ }
267
+ /** Flush then stop the background timer, and remove any opt-in process hooks. */
268
+ close() {
269
+ for (const uninstall of this.hookUninstallers.splice(0))
270
+ uninstall();
271
+ return this.transport.close();
272
+ }
273
+ }
274
+ /** Derive `{type, value}` from an arbitrary thrown value. */
275
+ export function describeError(error) {
276
+ if (error instanceof Error) {
277
+ return { type: error.name || 'Error', value: error.message || null };
278
+ }
279
+ if (typeof error === 'string') {
280
+ return { type: 'Error', value: error };
281
+ }
282
+ if (error && typeof error === 'object') {
283
+ const name = error.name;
284
+ const message = error.message;
285
+ return {
286
+ type: typeof name === 'string' && name ? name : 'Error',
287
+ value: typeof message === 'string' ? message : null,
288
+ };
289
+ }
290
+ return { type: 'Error', value: error === undefined ? null : String(error) };
291
+ }
package/dist/dsn.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * DSN parsing.
3
+ *
4
+ * A DSN looks like `https://<public_key>@<host>/<project_id>`. The public key
5
+ * is a non-secret, write-only credential.
6
+ */
7
+ export interface Dsn {
8
+ /** The raw DSN string (embedded verbatim into the envelope header). */
9
+ raw: string;
10
+ publicKey: string;
11
+ /** `host:port` — used to build the endpoint. */
12
+ host: string;
13
+ /** Hostname without port. */
14
+ hostname: string;
15
+ /** `https` or `http` (no trailing colon). */
16
+ protocol: string;
17
+ projectId: string;
18
+ /** `POST` target: `{protocol}://{host}/api/{project_id}/envelope`. */
19
+ envelopeUrl: string;
20
+ }
21
+ export declare class DsnError extends Error {
22
+ constructor(message: string);
23
+ }
24
+ /** Parse and validate a DSN, deriving the transport URL. */
25
+ export declare function parseDsn(dsn: string): Dsn;