@statoss/sdk 0.1.1 → 0.2.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 CHANGED
@@ -252,6 +252,23 @@ The Edge runtime has no global error hook either: capture where you handle
252
252
  errors and `await StatOSS.flush(2000)` (or pass it to `waitUntil`) before
253
253
  returning. In Next.js, the `instrumentation.ts` above covers it.
254
254
 
255
+ ## Source maps
256
+
257
+ After the build, give the bundles and their maps debug ids, then upload the
258
+ maps with Sentry's CLI:
259
+
260
+ ```sh
261
+ npx @sentry/cli sourcemaps inject ./dist
262
+
263
+ SENTRY_URL=https://statoss.com \
264
+ SENTRY_AUTH_TOKEN=<a StatOSS API key with write access> \
265
+ npx @sentry/cli sourcemaps upload --org statoss --project <app slug> ./dist
266
+ ```
267
+
268
+ Adding `--release <release>` to the upload is optional. Deploy the bundles as
269
+ `inject` left them: the SDK reads their debug ids and sends them with each
270
+ event. Frames are then shown as source and grouped by their source names.
271
+
255
272
  ## Custom levels and categories
256
273
 
257
274
  Sentry has five levels: `fatal`, `error`, `warning`, `info` and `debug`.
@@ -44,7 +44,10 @@ export declare class Client {
44
44
  /** Merges scope, context and options into the event, in Sentry's field order. */
45
45
  private prepare;
46
46
  private process;
47
- /** Makes what beforeSend handed back valid again: a Sentry level, string tags, a sane size. */
47
+ /**
48
+ * Makes what beforeSend handed back valid again (a Sentry level, string
49
+ * tags, a sane size) and names the debug ids of the files its frames ran in.
50
+ */
48
51
  private finish;
49
52
  private send;
50
53
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Client = void 0;
4
+ const debug_ids_js_1 = require("./debug-ids.js");
4
5
  const dsn_js_1 = require("./dsn.js");
5
6
  const envelope_js_1 = require("./envelope.js");
6
7
  const event_js_1 = require("./event.js");
@@ -434,7 +435,10 @@ class Client {
434
435
  }
435
436
  this.send(this.finish(final));
436
437
  }
437
- /** Makes what beforeSend handed back valid again: a Sentry level, string tags, a sane size. */
438
+ /**
439
+ * Makes what beforeSend handed back valid again (a Sentry level, string
440
+ * tags, a sane size) and names the debug ids of the files its frames ran in.
441
+ */
438
442
  finish(event) {
439
443
  if (!(0, levels_js_1.isSentryLevel)(event.level)) {
440
444
  const choice = (0, levels_js_1.chooseLevel)([event.level], this.levels, "error", this.logger);
@@ -444,6 +448,7 @@ class Client {
444
448
  }
445
449
  if ((0, utils_js_1.isObject)(event.tags))
446
450
  event.tags = cleanTags(event.tags);
451
+ (0, debug_ids_js_1.addDebugImages)(event);
447
452
  const size = () => (0, utils_js_1.safely)(() => JSON.stringify(event).length, 0) ?? 0;
448
453
  if (size() > MAX_EVENT_CHARS) {
449
454
  this.logger.warn("The event is over 1 MB; sending it without breadcrumbs and extra.");
@@ -0,0 +1,16 @@
1
+ import type { Event } from "./types.js";
2
+ /**
3
+ * Debug ids by the abs_path of the bundle they belong to, as Sentry's
4
+ * getFilenameToDebugIdMap reads them. `sentry-cli sourcemaps inject` and
5
+ * Sentry's bundler plugins put a snippet in each bundle that sets
6
+ * `globalThis._sentryDebugIds[new Error().stack] = "<debug id>"`, so the
7
+ * newest frame of each key ran in that bundle. Read again only when the
8
+ * number of entries changes (or the object is replaced).
9
+ */
10
+ export declare function debugIdsByFile(): ReadonlyMap<string, string>;
11
+ /**
12
+ * Adds `{ type: "sourcemap", code_file, debug_id }` to event.debug_meta.images
13
+ * for each file in the event's frames that has a debug id, after the images
14
+ * already there, so the server can map those frames to source. Never throws.
15
+ */
16
+ export declare function addDebugImages(event: Event): void;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.debugIdsByFile = debugIdsByFile;
4
+ exports.addDebugImages = addDebugImages;
5
+ const stack_js_1 = require("./stack.js");
6
+ const utils_js_1 = require("./utils.js");
7
+ const NONE = new Map();
8
+ // The file each stack string names, so each is parsed once.
9
+ const fileOfStack = new Map();
10
+ let lastIds;
11
+ let lastCount = -1;
12
+ let lastMap = NONE;
13
+ /**
14
+ * Debug ids by the abs_path of the bundle they belong to, as Sentry's
15
+ * getFilenameToDebugIdMap reads them. `sentry-cli sourcemaps inject` and
16
+ * Sentry's bundler plugins put a snippet in each bundle that sets
17
+ * `globalThis._sentryDebugIds[new Error().stack] = "<debug id>"`, so the
18
+ * newest frame of each key ran in that bundle. Read again only when the
19
+ * number of entries changes (or the object is replaced).
20
+ */
21
+ function debugIdsByFile() {
22
+ const ids = globalThis._sentryDebugIds;
23
+ if (!(0, utils_js_1.isObject)(ids) || Array.isArray(ids))
24
+ return NONE;
25
+ const keys = Object.keys(ids);
26
+ if (ids === lastIds && keys.length === lastCount)
27
+ return lastMap;
28
+ const map = new Map();
29
+ for (const key of keys) {
30
+ const id = ids[key];
31
+ if (typeof id !== "string" || id === "")
32
+ continue;
33
+ if (!fileOfStack.has(key))
34
+ fileOfStack.set(key, newestFile(key));
35
+ const file = fileOfStack.get(key);
36
+ if (file)
37
+ map.set(file, id);
38
+ }
39
+ lastIds = ids;
40
+ lastCount = keys.length;
41
+ lastMap = map;
42
+ return map;
43
+ }
44
+ // As Sentry does: walking from the newest frame, the first that names a file.
45
+ function newestFile(stack) {
46
+ const frames = (0, stack_js_1.parseStack)(stack);
47
+ for (let i = frames.length - 1; i >= 0; i--) {
48
+ const file = frames[i].abs_path;
49
+ if (file)
50
+ return file;
51
+ }
52
+ return undefined;
53
+ }
54
+ // Sentry's schema allows { values: [...] } or a bare list.
55
+ function valuesOf(value) {
56
+ if (Array.isArray(value))
57
+ return value;
58
+ if ((0, utils_js_1.isObject)(value) && Array.isArray(value.values))
59
+ return value.values;
60
+ return [];
61
+ }
62
+ /** The abs_path of every frame in the event's exceptions and threads. */
63
+ function framePaths(event) {
64
+ const paths = new Set();
65
+ const holders = [...valuesOf(event.exception), ...valuesOf(event.threads)];
66
+ for (const holder of holders) {
67
+ if (!(0, utils_js_1.isObject)(holder) || !(0, utils_js_1.isObject)(holder.stacktrace))
68
+ continue;
69
+ const frames = holder.stacktrace.frames;
70
+ if (!Array.isArray(frames))
71
+ continue;
72
+ for (const frame of frames) {
73
+ if ((0, utils_js_1.isObject)(frame) && typeof frame.abs_path === "string")
74
+ paths.add(frame.abs_path);
75
+ }
76
+ }
77
+ return paths;
78
+ }
79
+ /**
80
+ * Adds `{ type: "sourcemap", code_file, debug_id }` to event.debug_meta.images
81
+ * for each file in the event's frames that has a debug id, after the images
82
+ * already there, so the server can map those frames to source. Never throws.
83
+ */
84
+ function addDebugImages(event) {
85
+ try {
86
+ const ids = debugIdsByFile();
87
+ if (!ids.size)
88
+ return;
89
+ const meta = (0, utils_js_1.isObject)(event.debug_meta)
90
+ ? event.debug_meta
91
+ : {};
92
+ const images = Array.isArray(meta.images)
93
+ ? [...meta.images]
94
+ : [];
95
+ const named = new Set();
96
+ for (const image of images) {
97
+ if ((0, utils_js_1.isObject)(image) && image.type === "sourcemap")
98
+ named.add(image.code_file);
99
+ }
100
+ const before = images.length;
101
+ for (const path of framePaths(event)) {
102
+ const id = ids.get(path);
103
+ if (id !== undefined && !named.has(path))
104
+ images.push({ type: "sourcemap", code_file: path, debug_id: id });
105
+ }
106
+ if (images.length > before)
107
+ event.debug_meta = { ...meta, images: images };
108
+ }
109
+ catch {
110
+ // A malformed _sentryDebugIds or event costs the images, nothing more.
111
+ }
112
+ }
@@ -2,7 +2,7 @@ import type { Breadcrumb, CaptureContext, Event, InitOptions, Level, Primitive,
2
2
  export { withScope } from "./hub.js";
3
3
  export type { Scope } from "./scope.js";
4
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";
5
+ export type { Breadcrumb, BreadcrumbHint, CaptureContext, DebugImage, Event, EventHint, Exception, InitOptions, IntegrationToggles, Level, Mechanism, Primitive, RequestInfo, ScopeData, SdkInfo, SentryLevel, StackFrame, Transport, TransportRequest, TransportResponse, User, } from "./types.js";
6
6
  /**
7
7
  * Starts the SDK. Call it once, as early as you can. Calling it again replaces
8
8
  * the earlier setup. Without a valid DSN, or with enabled: false, nothing is
@@ -64,6 +64,13 @@ export interface SdkInfo {
64
64
  version: string;
65
65
  }[];
66
66
  }
67
+ /** A file the event's frames ran in; type "sourcemap" names its debug id. */
68
+ export interface DebugImage {
69
+ type: string;
70
+ code_file?: string;
71
+ debug_id?: string;
72
+ [key: string]: unknown;
73
+ }
67
74
  /** An event in Sentry's schema, as the SDK sends it. */
68
75
  export interface Event {
69
76
  event_id?: string;
@@ -91,6 +98,9 @@ export interface Event {
91
98
  request?: RequestInfo;
92
99
  fingerprint?: string[];
93
100
  sdk?: SdkInfo;
101
+ debug_meta?: {
102
+ images?: DebugImage[];
103
+ };
94
104
  [key: string]: unknown;
95
105
  }
96
106
  /** What `captureException`, `captureMessage` and `captureEvent` take as context. */
@@ -1,3 +1,3 @@
1
1
  export declare const SDK_NAME = "statoss.javascript";
2
- export declare const SDK_VERSION = "0.1.1";
2
+ export declare const SDK_VERSION = "0.2.0";
3
3
  export declare const SDK_PACKAGE = "npm:@statoss/sdk";
@@ -3,5 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_PACKAGE = exports.SDK_VERSION = exports.SDK_NAME = void 0;
4
4
  exports.SDK_NAME = "statoss.javascript";
5
5
  // Kept equal to package.json by a test.
6
- exports.SDK_VERSION = "0.1.1";
6
+ exports.SDK_VERSION = "0.2.0";
7
7
  exports.SDK_PACKAGE = "npm:@statoss/sdk";
@@ -44,7 +44,10 @@ export declare class Client {
44
44
  /** Merges scope, context and options into the event, in Sentry's field order. */
45
45
  private prepare;
46
46
  private process;
47
- /** Makes what beforeSend handed back valid again: a Sentry level, string tags, a sane size. */
47
+ /**
48
+ * Makes what beforeSend handed back valid again (a Sentry level, string
49
+ * tags, a sane size) and names the debug ids of the files its frames ran in.
50
+ */
48
51
  private finish;
49
52
  private send;
50
53
  }
@@ -1,3 +1,4 @@
1
+ import { addDebugImages } from "./debug-ids.js";
1
2
  import { authHeader, envelopeEndpoint, envelopeUrlWithQueryAuth, parseDsn, } from "./dsn.js";
2
3
  import { clientReportEnvelope, eventEnvelope, } from "./envelope.js";
3
4
  import { addInitialFrame, eventFromUnknown } from "./event.js";
@@ -431,7 +432,10 @@ export class Client {
431
432
  }
432
433
  this.send(this.finish(final));
433
434
  }
434
- /** Makes what beforeSend handed back valid again: a Sentry level, string tags, a sane size. */
435
+ /**
436
+ * Makes what beforeSend handed back valid again (a Sentry level, string
437
+ * tags, a sane size) and names the debug ids of the files its frames ran in.
438
+ */
435
439
  finish(event) {
436
440
  if (!isSentryLevel(event.level)) {
437
441
  const choice = chooseLevel([event.level], this.levels, "error", this.logger);
@@ -441,6 +445,7 @@ export class Client {
441
445
  }
442
446
  if (isObject(event.tags))
443
447
  event.tags = cleanTags(event.tags);
448
+ addDebugImages(event);
444
449
  const size = () => safely(() => JSON.stringify(event).length, 0) ?? 0;
445
450
  if (size() > MAX_EVENT_CHARS) {
446
451
  this.logger.warn("The event is over 1 MB; sending it without breadcrumbs and extra.");
@@ -0,0 +1,16 @@
1
+ import type { Event } from "./types.js";
2
+ /**
3
+ * Debug ids by the abs_path of the bundle they belong to, as Sentry's
4
+ * getFilenameToDebugIdMap reads them. `sentry-cli sourcemaps inject` and
5
+ * Sentry's bundler plugins put a snippet in each bundle that sets
6
+ * `globalThis._sentryDebugIds[new Error().stack] = "<debug id>"`, so the
7
+ * newest frame of each key ran in that bundle. Read again only when the
8
+ * number of entries changes (or the object is replaced).
9
+ */
10
+ export declare function debugIdsByFile(): ReadonlyMap<string, string>;
11
+ /**
12
+ * Adds `{ type: "sourcemap", code_file, debug_id }` to event.debug_meta.images
13
+ * for each file in the event's frames that has a debug id, after the images
14
+ * already there, so the server can map those frames to source. Never throws.
15
+ */
16
+ export declare function addDebugImages(event: Event): void;
@@ -0,0 +1,108 @@
1
+ import { parseStack } from "./stack.js";
2
+ import { isObject } from "./utils.js";
3
+ const NONE = new Map();
4
+ // The file each stack string names, so each is parsed once.
5
+ const fileOfStack = new Map();
6
+ let lastIds;
7
+ let lastCount = -1;
8
+ let lastMap = NONE;
9
+ /**
10
+ * Debug ids by the abs_path of the bundle they belong to, as Sentry's
11
+ * getFilenameToDebugIdMap reads them. `sentry-cli sourcemaps inject` and
12
+ * Sentry's bundler plugins put a snippet in each bundle that sets
13
+ * `globalThis._sentryDebugIds[new Error().stack] = "<debug id>"`, so the
14
+ * newest frame of each key ran in that bundle. Read again only when the
15
+ * number of entries changes (or the object is replaced).
16
+ */
17
+ export function debugIdsByFile() {
18
+ const ids = globalThis._sentryDebugIds;
19
+ if (!isObject(ids) || Array.isArray(ids))
20
+ return NONE;
21
+ const keys = Object.keys(ids);
22
+ if (ids === lastIds && keys.length === lastCount)
23
+ return lastMap;
24
+ const map = new Map();
25
+ for (const key of keys) {
26
+ const id = ids[key];
27
+ if (typeof id !== "string" || id === "")
28
+ continue;
29
+ if (!fileOfStack.has(key))
30
+ fileOfStack.set(key, newestFile(key));
31
+ const file = fileOfStack.get(key);
32
+ if (file)
33
+ map.set(file, id);
34
+ }
35
+ lastIds = ids;
36
+ lastCount = keys.length;
37
+ lastMap = map;
38
+ return map;
39
+ }
40
+ // As Sentry does: walking from the newest frame, the first that names a file.
41
+ function newestFile(stack) {
42
+ const frames = parseStack(stack);
43
+ for (let i = frames.length - 1; i >= 0; i--) {
44
+ const file = frames[i].abs_path;
45
+ if (file)
46
+ return file;
47
+ }
48
+ return undefined;
49
+ }
50
+ // Sentry's schema allows { values: [...] } or a bare list.
51
+ function valuesOf(value) {
52
+ if (Array.isArray(value))
53
+ return value;
54
+ if (isObject(value) && Array.isArray(value.values))
55
+ return value.values;
56
+ return [];
57
+ }
58
+ /** The abs_path of every frame in the event's exceptions and threads. */
59
+ function framePaths(event) {
60
+ const paths = new Set();
61
+ const holders = [...valuesOf(event.exception), ...valuesOf(event.threads)];
62
+ for (const holder of holders) {
63
+ if (!isObject(holder) || !isObject(holder.stacktrace))
64
+ continue;
65
+ const frames = holder.stacktrace.frames;
66
+ if (!Array.isArray(frames))
67
+ continue;
68
+ for (const frame of frames) {
69
+ if (isObject(frame) && typeof frame.abs_path === "string")
70
+ paths.add(frame.abs_path);
71
+ }
72
+ }
73
+ return paths;
74
+ }
75
+ /**
76
+ * Adds `{ type: "sourcemap", code_file, debug_id }` to event.debug_meta.images
77
+ * for each file in the event's frames that has a debug id, after the images
78
+ * already there, so the server can map those frames to source. Never throws.
79
+ */
80
+ export function addDebugImages(event) {
81
+ try {
82
+ const ids = debugIdsByFile();
83
+ if (!ids.size)
84
+ return;
85
+ const meta = isObject(event.debug_meta)
86
+ ? event.debug_meta
87
+ : {};
88
+ const images = Array.isArray(meta.images)
89
+ ? [...meta.images]
90
+ : [];
91
+ const named = new Set();
92
+ for (const image of images) {
93
+ if (isObject(image) && image.type === "sourcemap")
94
+ named.add(image.code_file);
95
+ }
96
+ const before = images.length;
97
+ for (const path of framePaths(event)) {
98
+ const id = ids.get(path);
99
+ if (id !== undefined && !named.has(path))
100
+ images.push({ type: "sourcemap", code_file: path, debug_id: id });
101
+ }
102
+ if (images.length > before)
103
+ event.debug_meta = { ...meta, images: images };
104
+ }
105
+ catch {
106
+ // A malformed _sentryDebugIds or event costs the images, nothing more.
107
+ }
108
+ }
@@ -2,7 +2,7 @@ import type { Breadcrumb, CaptureContext, Event, InitOptions, Level, Primitive,
2
2
  export { withScope } from "./hub.js";
3
3
  export type { Scope } from "./scope.js";
4
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";
5
+ export type { Breadcrumb, BreadcrumbHint, CaptureContext, DebugImage, Event, EventHint, Exception, InitOptions, IntegrationToggles, Level, Mechanism, Primitive, RequestInfo, ScopeData, SdkInfo, SentryLevel, StackFrame, Transport, TransportRequest, TransportResponse, User, } from "./types.js";
6
6
  /**
7
7
  * Starts the SDK. Call it once, as early as you can. Calling it again replaces
8
8
  * the earlier setup. Without a valid DSN, or with enabled: false, nothing is
@@ -64,6 +64,13 @@ export interface SdkInfo {
64
64
  version: string;
65
65
  }[];
66
66
  }
67
+ /** A file the event's frames ran in; type "sourcemap" names its debug id. */
68
+ export interface DebugImage {
69
+ type: string;
70
+ code_file?: string;
71
+ debug_id?: string;
72
+ [key: string]: unknown;
73
+ }
67
74
  /** An event in Sentry's schema, as the SDK sends it. */
68
75
  export interface Event {
69
76
  event_id?: string;
@@ -91,6 +98,9 @@ export interface Event {
91
98
  request?: RequestInfo;
92
99
  fingerprint?: string[];
93
100
  sdk?: SdkInfo;
101
+ debug_meta?: {
102
+ images?: DebugImage[];
103
+ };
94
104
  [key: string]: unknown;
95
105
  }
96
106
  /** What `captureException`, `captureMessage` and `captureEvent` take as context. */
@@ -1,3 +1,3 @@
1
1
  export declare const SDK_NAME = "statoss.javascript";
2
- export declare const SDK_VERSION = "0.1.1";
2
+ export declare const SDK_VERSION = "0.2.0";
3
3
  export declare const SDK_PACKAGE = "npm:@statoss/sdk";
@@ -1,4 +1,4 @@
1
1
  export const SDK_NAME = "statoss.javascript";
2
2
  // Kept equal to package.json by a test.
3
- export const SDK_VERSION = "0.1.1";
3
+ export const SDK_VERSION = "0.2.0";
4
4
  export const SDK_PACKAGE = "npm:@statoss/sdk";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statoss/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Error tracking for StatOSS: browsers, Node, Bun, Deno, Cloudflare Workers and Vercel Edge. Speaks Sentry's protocol.",
5
5
  "license": "MIT",
6
6
  "author": "StatOSS",