@statoss/sdk 0.1.1 → 0.3.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.
@@ -0,0 +1,199 @@
1
+ import type { Logger } from "./logger.js";
2
+ import type { CaptureContext, Event, Level, SentryLevel } from "./types.js";
3
+ /** The ranges a level ranks in: Sentry's five and OpenTelemetry's trace. */
4
+ export declare const LIKES: readonly ["fatal", "error", "warning", "info", "debug", "trace"];
5
+ export type Like = (typeof LIKES)[number];
6
+ /** The colours a level or a category takes, by name. */
7
+ export declare const SCHEMA_COLORS: readonly ["crimson", "red", "orange", "amber", "yellow", "green", "aqua", "teal", "blue", "violet", "indigo", "magenta", "plum", "grey"];
8
+ export type SchemaColor = (typeof SCHEMA_COLORS)[number];
9
+ /** What a rule tests: a tag by key, the logger, or one of the event's own fields. */
10
+ export declare const RULE_SUBJECTS: readonly ["tag", "logger", "exception", "culprit", "title", "message"];
11
+ export type RuleSubject = (typeof RULE_SUBJECTS)[number];
12
+ export declare const RULE_MATCHES: readonly ["is", "starts", "contains"];
13
+ export type RuleMatch = (typeof RULE_MATCHES)[number];
14
+ export declare const FIELD_TYPES: readonly ["text", "number"];
15
+ export type FieldType = (typeof FIELD_TYPES)[number];
16
+ /** Each range's severities, as OpenTelemetry numbers them. */
17
+ interface Severities {
18
+ fatal: 21 | 22 | 23 | 24;
19
+ error: 17 | 18 | 19 | 20;
20
+ warning: 13 | 14 | 15 | 16;
21
+ info: 9 | 10 | 11 | 12;
22
+ debug: 5 | 6 | 7 | 8;
23
+ trace: 1 | 2 | 3 | 4;
24
+ }
25
+ export type Severity = Severities[Like];
26
+ /** A level of your own. Null clears a setting. */
27
+ export interface LevelSettings {
28
+ /** The range it ranks in. Without it (and without severity) it ranks as error. */
29
+ like?: Like;
30
+ /** 1 to 24, inside the range of `like`. */
31
+ severity?: Severity;
32
+ label?: string | null;
33
+ color?: SchemaColor | null;
34
+ }
35
+ /** A category. Null clears a setting. */
36
+ export interface CategorySettings {
37
+ label?: string | null;
38
+ color?: SchemaColor | null;
39
+ /** Left out of filters and lists; its events still carry it. */
40
+ hidden?: boolean;
41
+ /** A team member's email. */
42
+ owner?: string | null;
43
+ }
44
+ /** A field: a tag key. Null clears a setting. */
45
+ export interface FieldSettings {
46
+ label?: string | null;
47
+ type?: FieldType;
48
+ unit?: string | null;
49
+ /** Gets a picker in Explore's filters. */
50
+ filterable?: boolean;
51
+ }
52
+ type RuleTests = {
53
+ subject: "tag";
54
+ tagKey: string;
55
+ } | {
56
+ subject: Exclude<RuleSubject, "tag">;
57
+ tagKey?: undefined;
58
+ };
59
+ type RuleSets<L extends string, C extends string> = {
60
+ level: L;
61
+ category?: C;
62
+ } | {
63
+ level?: L;
64
+ category: C;
65
+ };
66
+ /**
67
+ * Sets a level, a category or both for events that match: the first rule
68
+ * that matches wins. `match` defaults to "is".
69
+ */
70
+ export type SchemaRule<L extends string = string, C extends string = string> = RuleTests & {
71
+ match?: RuleMatch;
72
+ value: string;
73
+ } & RuleSets<L, C>;
74
+ /** An app's levels, categories, fields and rules, as the server takes them. */
75
+ export interface SchemaSpec {
76
+ /** Each name with the range it ranks in, or with its settings. */
77
+ levels?: {
78
+ readonly [name: string]: Like | LevelSettings;
79
+ };
80
+ categories?: readonly string[] | {
81
+ readonly [name: string]: CategorySettings;
82
+ };
83
+ fields?: readonly string[] | {
84
+ readonly [name: string]: FieldSettings;
85
+ };
86
+ rules?: readonly SchemaRule[];
87
+ }
88
+ type NamesIn<T> = T extends readonly (infer N)[] ? N & string : T extends object ? keyof T & string : never;
89
+ type LevelNames<S> = S extends {
90
+ levels?: infer L;
91
+ } ? NamesIn<NonNullable<L>> : never;
92
+ type CategoryNames<S> = S extends {
93
+ categories?: infer C;
94
+ } ? NamesIn<NonNullable<C>> : never;
95
+ type FieldNames<S> = S extends {
96
+ fields?: infer F;
97
+ } ? NamesIn<NonNullable<F>> : never;
98
+ /** A Sentry level or a level of the schema. */
99
+ export type LevelOf<S> = string extends LevelNames<S> ? Level : SentryLevel | LevelNames<S>;
100
+ /** A category of the schema. */
101
+ export type CategoryOf<S> = CategoryNames<S>;
102
+ /** A field of the schema. */
103
+ export type FieldOf<S> = FieldNames<S>;
104
+ type NameChar = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "." | "-" | "_";
105
+ /** True for 1 to Max of NameChar. A name only known as string passes. */
106
+ type IsName<S extends string, Max extends number, Seen extends 0[] = []> = string extends S ? true : S extends `${infer C}${infer Rest}` ? C extends NameChar ? Seen["length"] extends Max ? false : IsName<Rest, Max, [...Seen, 0]> : false : Seen extends [] ? false : true;
107
+ type LevelRule = "Level names are 1 to 32 lowercase letters, digits, dots, dashes or underscores";
108
+ type CategoryRule = "Category names are 1 to 64 lowercase letters, digits, dots, dashes or underscores";
109
+ type FieldRule = "Field names are not empty and do not start statoss.";
110
+ type Only<V, Keys extends PropertyKey, Message extends string> = {
111
+ [P in Exclude<keyof V, Keys>]: {
112
+ [M in Message]: true;
113
+ };
114
+ };
115
+ type LevelCheck<V> = V extends string ? Like : {
116
+ like?: Like;
117
+ severity?: V extends {
118
+ like: infer K extends Like;
119
+ } ? Severities[K] : Severity;
120
+ label?: string | null;
121
+ color?: SchemaColor | null;
122
+ } & Only<V, keyof LevelSettings, "A level takes like, severity, label and color">;
123
+ /** Sentry's own levels rank as themselves: a label and a colour only. */
124
+ type BuiltinLevelCheck<K extends SentryLevel, V> = V extends string ? K : {
125
+ like?: K;
126
+ label?: string | null;
127
+ color?: SchemaColor | null;
128
+ } & Only<V, "like" | "label" | "color", "A built-in level takes label and color, and ranks as itself">;
129
+ type CheckLevels<T> = {
130
+ [K in keyof T]: K extends SentryLevel ? BuiltinLevelCheck<K, T[K]> : K extends string ? IsName<K, 32> extends true ? LevelCheck<T[K]> : {
131
+ [M in LevelRule]: true;
132
+ } : never;
133
+ };
134
+ type CheckCategories<T> = T extends readonly unknown[] ? {
135
+ [I in keyof T]: T[I] extends string ? IsName<T[I], 64> extends true ? T[I] : {
136
+ [M in CategoryRule]: true;
137
+ } : string;
138
+ } : {
139
+ [K in keyof T]: K extends string ? IsName<K, 64> extends true ? CategorySettings & Only<T[K], keyof CategorySettings, "A category takes label, color, hidden and owner"> : {
140
+ [M in CategoryRule]: true;
141
+ } : never;
142
+ };
143
+ type IsField<K extends string> = K extends "" | `statoss.${string}` ? false : true;
144
+ type CheckFields<T> = T extends readonly unknown[] ? {
145
+ [I in keyof T]: T[I] extends string ? IsField<T[I]> extends true ? T[I] : {
146
+ [M in FieldRule]: true;
147
+ } : string;
148
+ } : {
149
+ [K in keyof T]: K extends string ? IsField<K> extends true ? FieldSettings & Only<T[K], keyof FieldSettings, "A field takes label, type, unit and filterable"> : {
150
+ [M in FieldRule]: true;
151
+ } : never;
152
+ };
153
+ type CheckRules<T, L extends string, C extends string> = {
154
+ [I in keyof T]: SchemaRule<L, C> & Only<T[I], "subject" | "tagKey" | "match" | "value" | "level" | "category", "A rule takes subject, tagKey, match, value, level and category">;
155
+ };
156
+ /** What defineSchema holds a schema to, read from the schema itself. */
157
+ export type CheckSchema<S> = {
158
+ [K in keyof S]: K extends "levels" ? CheckLevels<S[K]> : K extends "categories" ? CheckCategories<S[K]> : K extends "fields" ? CheckFields<S[K]> : K extends "rules" ? CheckRules<S[K], SentryLevel | LevelNames<S>, CategoryNames<S>> : {
159
+ "A schema takes levels, categories, fields and rules": true;
160
+ };
161
+ };
162
+ /**
163
+ * Declares an app's levels, categories, fields and rules. Returns the schema
164
+ * as given: plain data in the shape the server takes, so it can be sent as
165
+ * it is. Its names become types: pass it to init as `schema` and the capture
166
+ * functions init returns take only these names.
167
+ */
168
+ export declare function defineSchema<const S extends SchemaSpec & CheckSchema<S>>(schema: S): S;
169
+ /** A field's value; sent as a tag. */
170
+ export type FieldValue = string | number | boolean;
171
+ /** What the capture functions of init's handle take, typed by the schema. */
172
+ export interface SchemaContext<S = SchemaSpec> extends Omit<CaptureContext, "level" | "category"> {
173
+ /** A Sentry level or a level of the schema. */
174
+ level?: LevelOf<S>;
175
+ /** A category of the schema, sent as the `statoss.category` tag. */
176
+ category?: CategoryOf<S>;
177
+ /**
178
+ * Fields of the schema, sent as tags; a schema that declares none takes
179
+ * none here.
180
+ */
181
+ fields?: [FieldOf<S>] extends [never] ? never : {
182
+ [K in FieldOf<S>]?: FieldValue;
183
+ };
184
+ }
185
+ /** The capture functions, typed by the schema given to init. */
186
+ export interface Handle<S = SchemaSpec> {
187
+ /** Sends an error (or anything thrown). Returns the event id. */
188
+ captureException(error: unknown, context?: SchemaContext<S>): string;
189
+ /** Sends a message, with a level or a context. Returns the event id. */
190
+ captureMessage(message: string, levelOrContext?: LevelOf<S> | SchemaContext<S>): string;
191
+ /** Sends an event you built in Sentry's schema. Returns the event id. */
192
+ captureEvent(event: Event, context?: SchemaContext<S>): string;
193
+ }
194
+ /**
195
+ * The Sentry level each level of init's `schema` is sent as. Built-in
196
+ * levels are left out; names the SDK cannot send are logged in debug mode.
197
+ */
198
+ export declare function readSchema(schema: unknown, logger: Logger): Record<string, SentryLevel>;
199
+ export {};
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FIELD_TYPES = exports.RULE_MATCHES = exports.RULE_SUBJECTS = exports.SCHEMA_COLORS = exports.LIKES = void 0;
4
+ exports.defineSchema = defineSchema;
5
+ exports.readSchema = readSchema;
6
+ const levels_js_1 = require("./levels.js");
7
+ const utils_js_1 = require("./utils.js");
8
+ // The server's own lists (src/lib/app-schema.ts, src/lib/schema-colors.ts,
9
+ // src/db/schema.ts), kept equal by a test.
10
+ /** The ranges a level ranks in: Sentry's five and OpenTelemetry's trace. */
11
+ exports.LIKES = [
12
+ "fatal",
13
+ "error",
14
+ "warning",
15
+ "info",
16
+ "debug",
17
+ "trace",
18
+ ];
19
+ /** The colours a level or a category takes, by name. */
20
+ exports.SCHEMA_COLORS = [
21
+ "crimson",
22
+ "red",
23
+ "orange",
24
+ "amber",
25
+ "yellow",
26
+ "green",
27
+ "aqua",
28
+ "teal",
29
+ "blue",
30
+ "violet",
31
+ "indigo",
32
+ "magenta",
33
+ "plum",
34
+ "grey",
35
+ ];
36
+ /** What a rule tests: a tag by key, the logger, or one of the event's own fields. */
37
+ exports.RULE_SUBJECTS = [
38
+ "tag",
39
+ "logger",
40
+ "exception",
41
+ "culprit",
42
+ "title",
43
+ "message",
44
+ ];
45
+ exports.RULE_MATCHES = ["is", "starts", "contains"];
46
+ exports.FIELD_TYPES = ["text", "number"];
47
+ /**
48
+ * Declares an app's levels, categories, fields and rules. Returns the schema
49
+ * as given: plain data in the shape the server takes, so it can be sent as
50
+ * it is. Its names become types: pass it to init as `schema` and the capture
51
+ * functions init returns take only these names.
52
+ */
53
+ function defineSchema(schema) {
54
+ return schema;
55
+ }
56
+ // At runtime
57
+ /** The Sentry level a level of the schema is sent as. */
58
+ function sentAs(name, value, logger) {
59
+ const settings = (0, utils_js_1.isObject)(value) ? value : {};
60
+ const like = typeof value === "string" ? value : settings.like;
61
+ if ((0, levels_js_1.isSentryLevel)(like))
62
+ return like;
63
+ if (like === "trace")
64
+ return "debug";
65
+ if (like !== undefined || (typeof value !== "string" && !(0, utils_js_1.isObject)(value))) {
66
+ logger.warn(`Level "${name}" in the schema: like should be ${exports.LIKES.join(", ")}; sent as error.`);
67
+ return "error";
68
+ }
69
+ const severity = settings.severity;
70
+ if (typeof severity !== "number" || severity < 1 || severity > 24)
71
+ return "error";
72
+ if (severity >= 21)
73
+ return "fatal";
74
+ if (severity >= 17)
75
+ return "error";
76
+ if (severity >= 13)
77
+ return "warning";
78
+ if (severity >= 9)
79
+ return "info";
80
+ return "debug";
81
+ }
82
+ /**
83
+ * The Sentry level each level of init's `schema` is sent as. Built-in
84
+ * levels are left out; names the SDK cannot send are logged in debug mode.
85
+ */
86
+ function readSchema(schema, logger) {
87
+ const out = {};
88
+ if (schema === undefined || schema === null)
89
+ return out;
90
+ if (!(0, utils_js_1.isObject)(schema) || Array.isArray(schema)) {
91
+ logger.warn("Ignored `schema`: pass what defineSchema returns.");
92
+ return out;
93
+ }
94
+ const { levels, categories } = schema;
95
+ if ((0, utils_js_1.isObject)(levels) && !Array.isArray(levels)) {
96
+ for (const [name, value] of Object.entries(levels)) {
97
+ // A built-in level, given a label or a colour.
98
+ if ((0, levels_js_1.isSentryLevel)(name))
99
+ continue;
100
+ if ((0, levels_js_1.isLevelName)(name))
101
+ out[name] = sentAs(name, value, logger);
102
+ else
103
+ logger.warn(`Ignored level "${name}" in the schema: ${levels_js_1.LEVEL_RULE}.`);
104
+ }
105
+ }
106
+ else if (levels !== undefined && levels !== null) {
107
+ logger.warn('Ignored the schema\'s levels: they should be an object such as { "payment-failed": "error" }.');
108
+ }
109
+ const names = Array.isArray(categories)
110
+ ? categories
111
+ : (0, utils_js_1.isObject)(categories)
112
+ ? Object.keys(categories)
113
+ : [];
114
+ for (const name of names) {
115
+ if (!(0, levels_js_1.isCategoryName)(name))
116
+ logger.warn(`Category "${(0, utils_js_1.safeString)(name)}" in the schema cannot be sent: ${levels_js_1.CATEGORY_RULE}.`);
117
+ }
118
+ return out;
119
+ }
@@ -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.3.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.3.0";
7
7
  exports.SDK_PACKAGE = "npm:@statoss/sdk";
@@ -2,6 +2,10 @@ import { type Logger } from "./logger.js";
2
2
  import { type RuntimeInfo } from "./runtime.js";
3
3
  import { type Scope } from "./scope.js";
4
4
  import type { Breadcrumb, BreadcrumbHint, CaptureContext, Event, InitOptions } from "./types.js";
5
+ /** init's options as the client reads them: init may pass a schema. */
6
+ export type ClientOptions = InitOptions & {
7
+ schema?: unknown;
8
+ };
5
9
  export declare class Client {
6
10
  readonly logger: Logger;
7
11
  readonly runtime: RuntimeInfo;
@@ -20,7 +24,7 @@ export declare class Client {
20
24
  private reportTimer?;
21
25
  private previousSignature?;
22
26
  private closed;
23
- constructor(options: InitOptions);
27
+ constructor(options: ClientOptions);
24
28
  /** Installs the integrations that apply to this runtime. */
25
29
  setup(): void;
26
30
  captureException(error: unknown, context: CaptureContext | undefined, scope: Scope): string;
@@ -44,7 +48,10 @@ export declare class Client {
44
48
  /** Merges scope, context and options into the event, in Sentry's field order. */
45
49
  private prepare;
46
50
  private process;
47
- /** Makes what beforeSend handed back valid again: a Sentry level, string tags, a sane size. */
51
+ /**
52
+ * Makes what beforeSend handed back valid again (a Sentry level, string
53
+ * tags, a sane size) and names the debug ids of the files its frames ran in.
54
+ */
48
55
  private finish;
49
56
  private send;
50
57
  }
@@ -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";
@@ -10,6 +11,7 @@ import { installGlobalHandlers } from "./integrations/handlers.js";
10
11
  import { CATEGORY_TAG, chooseLevel, isSentryLevel, LEVEL_TAG, readCategory, readLevels, } from "./levels.js";
11
12
  import { makeLogger } from "./logger.js";
12
13
  import { describeRuntime } from "./runtime.js";
14
+ import { readSchema } from "./schema.js";
13
15
  import { MAX_BREADCRUMBS } from "./scope.js";
14
16
  import { fetchTransport, Sender } from "./transport.js";
15
17
  import { isObject, isThenable, normalize, now, safely, safeString, scrubUrl, truncate, unref, uuid4, } from "./utils.js";
@@ -50,6 +52,32 @@ function cleanTags(tags) {
50
52
  }
51
53
  return out;
52
54
  }
55
+ /** A capture's fields, as tags. Keys starting statoss. are not fields. */
56
+ function fieldTags(fields, logger) {
57
+ const out = {};
58
+ if (fields === undefined || fields === null)
59
+ return out;
60
+ if (!isObject(fields) || Array.isArray(fields)) {
61
+ logger.warn('Ignored `fields`: it should be an object such as { route: "/pay" }.');
62
+ return out;
63
+ }
64
+ for (const [key, value] of Object.entries(fields)) {
65
+ if (value === undefined || value === null)
66
+ continue;
67
+ if (key === "" || key.startsWith("statoss.")) {
68
+ logger.warn(`Ignored field "${key}": fields are not empty and do not start statoss.`);
69
+ }
70
+ else if (typeof value === "string" ||
71
+ typeof value === "number" ||
72
+ typeof value === "boolean") {
73
+ out[key] = value;
74
+ }
75
+ else {
76
+ logger.warn(`Ignored field "${key}": its value should be a string, number or boolean.`);
77
+ }
78
+ }
79
+ return out;
80
+ }
53
81
  function cleanContexts(contexts) {
54
82
  const out = {};
55
83
  for (const [name, value] of Object.entries(contexts)) {
@@ -99,7 +127,11 @@ export class Client {
99
127
  this.options = options;
100
128
  this.logger = makeLogger(options.debug === true);
101
129
  this.runtime = describeRuntime();
102
- this.levels = readLevels(options.levels, this.logger);
130
+ // What `levels` says wins over the schema.
131
+ this.levels = {
132
+ ...readSchema(options.schema, this.logger),
133
+ ...readLevels(options.levels, this.logger),
134
+ };
103
135
  const rate = options.sampleRate;
104
136
  if (rate !== undefined &&
105
137
  !(typeof rate === "number" && rate >= 0 && rate <= 1)) {
@@ -325,6 +357,7 @@ export class Client {
325
357
  const tags = {
326
358
  ...scope._tags,
327
359
  ...ctx.tags,
360
+ ...fieldTags(ctx.fields, this.logger),
328
361
  ...base.tags,
329
362
  };
330
363
  if (choice.custom)
@@ -431,7 +464,10 @@ export class Client {
431
464
  }
432
465
  this.send(this.finish(final));
433
466
  }
434
- /** Makes what beforeSend handed back valid again: a Sentry level, string tags, a sane size. */
467
+ /**
468
+ * Makes what beforeSend handed back valid again (a Sentry level, string
469
+ * tags, a sane size) and names the debug ids of the files its frames ran in.
470
+ */
435
471
  finish(event) {
436
472
  if (!isSentryLevel(event.level)) {
437
473
  const choice = chooseLevel([event.level], this.levels, "error", this.logger);
@@ -441,6 +477,7 @@ export class Client {
441
477
  }
442
478
  if (isObject(event.tags))
443
479
  event.tags = cleanTags(event.tags);
480
+ addDebugImages(event);
444
481
  const size = () => safely(() => JSON.stringify(event).length, 0) ?? 0;
445
482
  if (size() > MAX_EVENT_CHARS) {
446
483
  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
+ }
@@ -1,14 +1,23 @@
1
+ import type { CheckSchema, Handle, SchemaSpec } from "./schema.js";
1
2
  import type { Breadcrumb, CaptureContext, Event, InitOptions, Level, Primitive, User } from "./types.js";
2
3
  export { withScope } from "./hub.js";
4
+ export { defineSchema } from "./schema.js";
5
+ export type { CategoryOf, CategorySettings, CheckSchema, FieldOf, FieldSettings, FieldType, FieldValue, Handle, LevelOf, LevelSettings, Like, RuleMatch, RuleSubject, SchemaColor, SchemaContext, SchemaRule, SchemaSpec, Severity, } from "./schema.js";
3
6
  export type { Scope } from "./scope.js";
4
7
  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";
8
+ 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
9
  /**
7
10
  * Starts the SDK. Call it once, as early as you can. Calling it again replaces
8
11
  * the earlier setup. Without a valid DSN, or with enabled: false, nothing is
9
12
  * sent and nothing is patched.
13
+ *
14
+ * With a `schema` from defineSchema, its levels are sent as the Sentry level
15
+ * each ranks like, and the capture functions it returns take only the
16
+ * schema's levels, categories and fields.
10
17
  */
11
- export declare function init(options?: InitOptions): void;
18
+ export declare function init<const S extends SchemaSpec & CheckSchema<S> = SchemaSpec>(options?: InitOptions & {
19
+ schema?: S;
20
+ }): Handle<S>;
12
21
  /** Sends an error (or anything thrown). Returns the event id. */
13
22
  export declare function captureException(error: unknown, context?: CaptureContext): string;
14
23
  /**
package/dist/esm/index.js CHANGED
@@ -2,12 +2,8 @@ import { Client } from "./client.js";
2
2
  import { captureWith, getCarrier, getCurrentScope, messageContext, } from "./hub.js";
3
3
  import { isObject, safely } from "./utils.js";
4
4
  export { withScope } from "./hub.js";
5
+ export { defineSchema } from "./schema.js";
5
6
  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
7
  export function init(options = {}) {
12
8
  try {
13
9
  const carrier = getCarrier();
@@ -26,6 +22,7 @@ export function init(options = {}) {
26
22
  safely(() => console.error("[StatOSS] init failed:", error));
27
23
  }
28
24
  }
25
+ return HANDLE;
29
26
  }
30
27
  /** Sends an error (or anything thrown). Returns the event id. */
31
28
  export function captureException(error, context) {
@@ -42,6 +39,13 @@ export function captureMessage(message, levelOrContext) {
42
39
  export function captureEvent(event, context) {
43
40
  return captureWith(undefined, (client, scope) => client.captureEvent(event, context, scope));
44
41
  }
42
+ // What init returns: the same functions, typed by the schema. The client
43
+ // reads a context's fields whichever function it came through.
44
+ const HANDLE = Object.freeze({
45
+ captureException,
46
+ captureMessage,
47
+ captureEvent,
48
+ });
45
49
  /** Pass null to forget the user. */
46
50
  export function setUser(user) {
47
51
  safely(() => getCurrentScope().setUser(user));