@statoss/sdk 0.2.0 → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  import { addDebugImages } from "./debug-ids.js";
2
2
  import { authHeader, envelopeEndpoint, envelopeUrlWithQueryAuth, parseDsn, } from "./dsn.js";
3
- import { clientReportEnvelope, eventEnvelope, } from "./envelope.js";
3
+ import { clientReportEnvelope, eventEnvelope, logEnvelope, } from "./envelope.js";
4
4
  import { addInitialFrame, eventFromUnknown } from "./event.js";
5
5
  import { getCurrentScope } from "./hub.js";
6
6
  import { installClicks, installNavigation } from "./integrations/browser.js";
@@ -10,13 +10,20 @@ import { installFetch } from "./integrations/fetch.js";
10
10
  import { installGlobalHandlers } from "./integrations/handlers.js";
11
11
  import { CATEGORY_TAG, chooseLevel, isSentryLevel, LEVEL_TAG, readCategory, readLevels, } from "./levels.js";
12
12
  import { makeLogger } from "./logger.js";
13
+ import { bodyOf, chooseLogLevel, cutBody, isLogLevel, isLogMessage, LOG_SEVERITY, logLevelOf, ownLevelsFromSentry, serializeAttribute, } from "./logs.js";
13
14
  import { describeRuntime } from "./runtime.js";
15
+ import { readSchema, schemaLogLevels } from "./schema.js";
14
16
  import { MAX_BREADCRUMBS } from "./scope.js";
15
17
  import { fetchTransport, Sender } from "./transport.js";
16
18
  import { isObject, isThenable, normalize, now, safely, safeString, scrubUrl, truncate, unref, uuid4, } from "./utils.js";
17
19
  import { SDK_NAME, SDK_PACKAGE, SDK_VERSION } from "./version.js";
18
20
  const REPORT_INTERVAL_MS = 30000;
19
21
  const DEFAULT_FLUSH_MS = 2000;
22
+ // Log lines go out 100 at a time, or 5 seconds after the first waits, as
23
+ // Sentry's logger sends them, and well under an envelope item's 1 MiB.
24
+ const LOG_BATCH = 100;
25
+ const LOG_WAIT_MS = 5000;
26
+ const LOG_BATCH_CHARS = 800000;
20
27
  // The server drops event items over 1 MiB.
21
28
  const MAX_EVENT_CHARS = 1000000;
22
29
  const MAX_TAG_VALUE = 200;
@@ -51,6 +58,67 @@ function cleanTags(tags) {
51
58
  }
52
59
  return out;
53
60
  }
61
+ /**
62
+ * A line as a log item holds it, made valid again after beforeSendLog: one
63
+ * of the six levels, a severity inside its range, typed attributes.
64
+ */
65
+ function serializeLog(log, traceId) {
66
+ const level = isLogLevel(log.level)
67
+ ? log.level
68
+ : (logLevelOf(safeString(log.level)) ?? "info");
69
+ const n = log.severityNumber;
70
+ const severity = typeof n === "number" &&
71
+ Number.isInteger(n) &&
72
+ n >= LOG_SEVERITY[level] &&
73
+ n <= LOG_SEVERITY[level] + 3
74
+ ? n
75
+ : LOG_SEVERITY[level];
76
+ const attributes = {};
77
+ if (isObject(log.attributes)) {
78
+ for (const [key, value] of Object.entries(log.attributes)) {
79
+ const typed = serializeAttribute(value);
80
+ if (key !== "" && typed)
81
+ attributes[key] = typed;
82
+ }
83
+ }
84
+ const body = cutBody(bodyOf(log.message), attributes);
85
+ return {
86
+ timestamp: typeof log.timestamp === "number" && Number.isFinite(log.timestamp)
87
+ ? log.timestamp
88
+ : now(),
89
+ level,
90
+ body,
91
+ trace_id: traceId,
92
+ severity_number: severity,
93
+ attributes,
94
+ };
95
+ }
96
+ /** A capture's fields, as tags. Keys starting statoss. are not fields. */
97
+ function fieldTags(fields, logger) {
98
+ const out = {};
99
+ if (fields === undefined || fields === null)
100
+ return out;
101
+ if (!isObject(fields) || Array.isArray(fields)) {
102
+ logger.warn('Ignored `fields`: it should be an object such as { route: "/pay" }.');
103
+ return out;
104
+ }
105
+ for (const [key, value] of Object.entries(fields)) {
106
+ if (value === undefined || value === null)
107
+ continue;
108
+ if (key === "" || key.startsWith("statoss.")) {
109
+ logger.warn(`Ignored field "${key}": fields are not empty and do not start statoss.`);
110
+ }
111
+ else if (typeof value === "string" ||
112
+ typeof value === "number" ||
113
+ typeof value === "boolean") {
114
+ out[key] = value;
115
+ }
116
+ else {
117
+ logger.warn(`Ignored field "${key}": its value should be a string, number or boolean.`);
118
+ }
119
+ }
120
+ return out;
121
+ }
54
122
  function cleanContexts(contexts) {
55
123
  const out = {};
56
124
  for (const [name, value] of Object.entries(contexts)) {
@@ -97,10 +165,20 @@ export class Client {
97
165
  this.pending = new Set();
98
166
  this.teardowns = [];
99
167
  this.closed = false;
168
+ this.logBuffer = [];
169
+ this.logChars = 0;
170
+ this.saidLogsOff = false;
100
171
  this.options = options;
101
172
  this.logger = makeLogger(options.debug === true);
102
173
  this.runtime = describeRuntime();
103
- this.levels = readLevels(options.levels, this.logger);
174
+ // What `levels` says wins over the schema.
175
+ const given = readLevels(options.levels, this.logger);
176
+ this.levels = { ...readSchema(options.schema, this.logger), ...given };
177
+ this.logLevels = {
178
+ ...schemaLogLevels(options.schema),
179
+ ...ownLevelsFromSentry(given),
180
+ };
181
+ this.logsOn = options.logs === true;
104
182
  const rate = options.sampleRate;
105
183
  if (rate !== undefined &&
106
184
  !(typeof rate === "number" && rate >= 0 && rate <= 1)) {
@@ -179,8 +257,90 @@ export class Client {
179
257
  this.integrations.push("Dedupe");
180
258
  if (browser)
181
259
  this.flushReportsWhenHidden();
260
+ if (this.logsOn)
261
+ this.sendLogsBeforeExit();
182
262
  this.logger.log(`Integrations: ${this.integrations.join(", ") || "none"}.`);
183
263
  }
264
+ /**
265
+ * Keeps a log line for the next envelope of lines. The level is one of
266
+ * the six, a Sentry level, or the app's own (sent at its range's level
267
+ * with statoss.level); attributes go along typed, as Sentry's logger
268
+ * sends them.
269
+ */
270
+ captureLog(level, message, attributes, scope) {
271
+ if (!this.enabled || this.closed)
272
+ return;
273
+ if (!this.logsOn) {
274
+ if (!this.saidLogsOff) {
275
+ this.saidLogsOff = true;
276
+ this.logger.log("Log lines are not sent: init has no logs: true.");
277
+ }
278
+ return;
279
+ }
280
+ try {
281
+ const choice = chooseLogLevel(level, this.logLevels, this.logger);
282
+ const attrs = {};
283
+ if (isObject(attributes) && !Array.isArray(attributes)) {
284
+ for (const [key, value] of Object.entries(attributes))
285
+ if (key !== "")
286
+ attrs[key] = value;
287
+ }
288
+ else if (attributes !== undefined && attributes !== null) {
289
+ this.logger.warn("Ignored a log line's attributes: they should be an object such as { orderId: 7 }.");
290
+ }
291
+ // What the line says wins over what the SDK adds.
292
+ const add = (key, value) => {
293
+ if (value !== undefined && value !== null && !(key in attrs))
294
+ attrs[key] = value;
295
+ };
296
+ add("sentry.environment", this.options.environment ?? "production");
297
+ add("sentry.release", this.options.release);
298
+ add("sentry.sdk.name", SDK_NAME);
299
+ add("sentry.sdk.version", SDK_VERSION);
300
+ add("server.address", this.options.serverName ?? this.runtime.serverName);
301
+ // The user goes on a line only with sendDefaultPii.
302
+ const user = scope._user;
303
+ if (user && this.options.sendDefaultPii) {
304
+ add("user.id", user.id);
305
+ add("user.email", user.email);
306
+ add("user.name", user.username);
307
+ }
308
+ if (isLogMessage(message) && message.values.length > 0) {
309
+ attrs["sentry.message.template"] = message.template;
310
+ message.values.forEach((value, i) => {
311
+ attrs[`sentry.message.parameter.${i}`] = value;
312
+ });
313
+ }
314
+ if (choice.custom)
315
+ attrs[LEVEL_TAG] = choice.custom;
316
+ let log = {
317
+ level: choice.level,
318
+ message: bodyOf(message),
319
+ attributes: attrs,
320
+ severityNumber: choice.severity,
321
+ timestamp: now(),
322
+ };
323
+ if (this.options.beforeSendLog) {
324
+ try {
325
+ log = this.options.beforeSendLog(log);
326
+ }
327
+ catch (error) {
328
+ this.logger.error("beforeSendLog threw, so the line was dropped:", error);
329
+ this.recordLoss("internal_sdk_error", "log_item");
330
+ return;
331
+ }
332
+ if (!isObject(log)) {
333
+ this.recordLoss("before_send", "log_item");
334
+ return;
335
+ }
336
+ }
337
+ this.logTraceId ?? (this.logTraceId = uuid4());
338
+ this.keepLog(serializeLog(log, this.logTraceId));
339
+ }
340
+ catch (error) {
341
+ this.logger.error("Could not capture a log line:", error);
342
+ }
343
+ }
184
344
  captureException(error, context, scope) {
185
345
  return this.capture(() => eventFromUnknown(error, { type: "generic", handled: true }), context, scope, { originalException: error });
186
346
  }
@@ -263,6 +423,7 @@ export class Client {
263
423
  async flush(timeoutMs = DEFAULT_FLUSH_MS) {
264
424
  if (!this.sender)
265
425
  return true;
426
+ this.sendLogs();
266
427
  const deadline = Date.now() + timeoutMs;
267
428
  const left = () => Math.max(0, deadline - Date.now());
268
429
  const processed = this.pending.size
@@ -283,18 +444,63 @@ export class Client {
283
444
  this.teardown();
284
445
  return this.flush(timeoutMs);
285
446
  }
286
- /** Removes the integrations at once, without waiting. */
447
+ /** Removes the integrations at once, without waiting; log lines kept go out. */
287
448
  teardown() {
288
449
  for (const teardown of this.teardowns.splice(0))
289
450
  safely(teardown);
451
+ this.sendLogs();
452
+ }
453
+ keepLog(log) {
454
+ const size = safely(() => JSON.stringify(log).length, 0) ?? 0;
455
+ if (this.logBuffer.length > 0 && this.logChars + size > LOG_BATCH_CHARS)
456
+ this.sendLogs();
457
+ this.logBuffer.push(log);
458
+ this.logChars += size;
459
+ if (this.logBuffer.length >= LOG_BATCH) {
460
+ this.sendLogs();
461
+ }
462
+ else if (this.logTimer === undefined) {
463
+ this.logTimer = safely(() => setTimeout(() => {
464
+ this.logTimer = undefined;
465
+ this.sendLogs();
466
+ }, LOG_WAIT_MS));
467
+ unref(this.logTimer);
468
+ }
469
+ }
470
+ /** Sends the log lines kept, as one envelope. */
471
+ sendLogs() {
472
+ if (this.logTimer !== undefined) {
473
+ clearTimeout(this.logTimer);
474
+ this.logTimer = undefined;
475
+ }
476
+ if (this.logBuffer.length === 0 || !this.sender || !this.dsn)
477
+ return;
478
+ const logs = this.logBuffer;
479
+ this.logBuffer = [];
480
+ this.logChars = 0;
481
+ this.sender.send(logEnvelope(logs, this.dsn));
482
+ }
483
+ /**
484
+ * In Node, Bun and Deno: sends the lines kept when the process runs out
485
+ * of work, since the timer that would send them does not keep it alive.
486
+ */
487
+ sendLogsBeforeExit() {
488
+ const proc = globalThis.process;
489
+ if (!proc || typeof proc.on !== "function")
490
+ return;
491
+ const onBeforeExit = () => safely(() => this.sendLogs());
492
+ safely(() => proc.on("beforeExit", onBeforeExit));
493
+ this.teardowns.push(() => proc.removeListener?.("beforeExit", onBeforeExit));
290
494
  }
291
495
  flushReportsWhenHidden() {
292
496
  const doc = globalThis.document;
293
497
  if (!doc || typeof doc.addEventListener !== "function")
294
498
  return;
295
499
  const onChange = () => {
296
- if (doc.visibilityState === "hidden")
297
- safely(() => this.sendClientReport());
500
+ if (doc.visibilityState !== "hidden")
501
+ return;
502
+ safely(() => this.sendLogs());
503
+ safely(() => this.sendClientReport());
298
504
  };
299
505
  doc.addEventListener("visibilitychange", onChange);
300
506
  this.teardowns.push(() => doc.removeEventListener?.("visibilitychange", onChange));
@@ -326,6 +532,7 @@ export class Client {
326
532
  const tags = {
327
533
  ...scope._tags,
328
534
  ...ctx.tags,
535
+ ...fieldTags(ctx.fields, this.logger),
329
536
  ...base.tags,
330
537
  };
331
538
  if (choice.custom)
@@ -1,4 +1,5 @@
1
1
  import { type Dsn } from "./dsn.js";
2
+ import type { SerializedLog } from "./logs.js";
2
3
  import type { Event } from "./types.js";
3
4
  export interface DiscardedEvent {
4
5
  reason: string;
@@ -26,11 +27,29 @@ export type ClientReportItem = [
26
27
  discarded_events: DiscardedEvent[];
27
28
  }
28
29
  ];
29
- export type EnvelopeItem = EventItem | ClientReportItem;
30
+ export declare const LOG_CONTENT_TYPE = "application/vnd.sentry.items.log+json";
31
+ export type LogItem = [
32
+ {
33
+ type: "log";
34
+ item_count: number;
35
+ content_type: typeof LOG_CONTENT_TYPE;
36
+ },
37
+ {
38
+ version: 2;
39
+ items: SerializedLog[];
40
+ }
41
+ ];
42
+ export type EnvelopeItem = EventItem | ClientReportItem | LogItem;
30
43
  export type Envelope = [EnvelopeHeader, EnvelopeItem[]];
31
44
  export declare function eventEnvelope(event: Event, dsn: Dsn): Envelope;
32
45
  export declare function clientReportEnvelope(discarded: DiscardedEvent[], dsn: Dsn, timestamp?: number): Envelope;
46
+ /** Log lines in one envelope, as Sentry's logger sends them. */
47
+ export declare function logEnvelope(logs: SerializedLog[], dsn: Dsn): Envelope;
33
48
  /** Sentry's rate-limit category for an envelope item. */
34
49
  export declare function itemCategory(item: EnvelopeItem): string;
50
+ /** How many things an item holds, as client reports count them. */
51
+ export declare function itemQuantity(item: EnvelopeItem): number;
52
+ /** True for an envelope of log lines. */
53
+ export declare function isLogEnvelope(envelope: Envelope): boolean;
35
54
  /** Byte for byte what Sentry's serializeEnvelope produces for string payloads. */
36
55
  export declare function serializeEnvelope(envelope: Envelope): string;
@@ -1,6 +1,7 @@
1
1
  import { dsnToString } from "./dsn.js";
2
2
  import { normalize } from "./utils.js";
3
3
  import { SDK_NAME, SDK_VERSION } from "./version.js";
4
+ export const LOG_CONTENT_TYPE = "application/vnd.sentry.items.log+json";
4
5
  function sdkHeader() {
5
6
  return { name: SDK_NAME, version: SDK_VERSION };
6
7
  }
@@ -25,9 +26,44 @@ export function clientReportEnvelope(discarded, dsn, timestamp = Date.now() / 10
25
26
  [[{ type: "client_report" }, { timestamp, discarded_events: discarded }]],
26
27
  ];
27
28
  }
29
+ /** Log lines in one envelope, as Sentry's logger sends them. */
30
+ export function logEnvelope(logs, dsn) {
31
+ return [
32
+ {
33
+ sent_at: new Date().toISOString(),
34
+ dsn: dsnToString(dsn),
35
+ sdk: sdkHeader(),
36
+ },
37
+ [
38
+ [
39
+ {
40
+ type: "log",
41
+ item_count: logs.length,
42
+ content_type: LOG_CONTENT_TYPE,
43
+ },
44
+ { version: 2, items: logs },
45
+ ],
46
+ ],
47
+ ];
48
+ }
28
49
  /** Sentry's rate-limit category for an envelope item. */
29
50
  export function itemCategory(item) {
30
- return item[0].type === "event" ? "error" : "internal";
51
+ switch (item[0].type) {
52
+ case "event":
53
+ return "error";
54
+ case "log":
55
+ return "log_item";
56
+ default:
57
+ return "internal";
58
+ }
59
+ }
60
+ /** How many things an item holds, as client reports count them. */
61
+ export function itemQuantity(item) {
62
+ return item[0].type === "log" ? item[0].item_count : 1;
63
+ }
64
+ /** True for an envelope of log lines. */
65
+ export function isLogEnvelope(envelope) {
66
+ return envelope[1].some((item) => item[0].type === "log");
31
67
  }
32
68
  function stringify(payload) {
33
69
  try {
@@ -1,14 +1,25 @@
1
+ import { type AppLogger } from "./logs.js";
2
+ import type { CheckSchema, Handle, SchemaSpec } from "./schema.js";
1
3
  import type { Breadcrumb, CaptureContext, Event, InitOptions, Level, Primitive, User } from "./types.js";
2
4
  export { withScope } from "./hub.js";
5
+ export type { AppLogger, Log, LogAttributes, LogAttributeValue, LogLevel, LogLevelOf, LogMessage, SerializedLog, } from "./logs.js";
6
+ export { defineSchema } from "./schema.js";
7
+ export type { CategoryOf, CategorySettings, CheckSchema, FieldOf, FieldSettings, FieldType, FieldValue, Handle, LevelOf, LevelSettings, Like, RuleMatch, RuleSubject, SchemaColor, SchemaContext, SchemaRule, SchemaSpec, Severity, } from "./schema.js";
3
8
  export type { Scope } from "./scope.js";
4
9
  export { SDK_NAME, SDK_VERSION } from "./version.js";
5
10
  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
11
  /**
7
12
  * Starts the SDK. Call it once, as early as you can. Calling it again replaces
8
13
  * the earlier setup. Without a valid DSN, or with enabled: false, nothing is
9
- * sent and nothing is patched.
14
+ * sent and nothing is patched. With `logs: true`, `logger`'s lines are sent.
15
+ *
16
+ * With a `schema` from defineSchema, its levels are sent as the Sentry level
17
+ * each ranks like, and the capture functions and logger it returns take
18
+ * only the schema's levels, categories and fields.
10
19
  */
11
- export declare function init(options?: InitOptions): void;
20
+ export declare function init<const S extends SchemaSpec & CheckSchema<S> = SchemaSpec>(options?: InitOptions & {
21
+ schema?: S;
22
+ }): Handle<S>;
12
23
  /** Sends an error (or anything thrown). Returns the event id. */
13
24
  export declare function captureException(error: unknown, context?: CaptureContext): string;
14
25
  /**
@@ -18,6 +29,12 @@ export declare function captureException(error: unknown, context?: CaptureContex
18
29
  export declare function captureMessage(message: string, levelOrContext?: Level | CaptureContext): string;
19
30
  /** Sends an event you built in Sentry's schema. Returns the event id. */
20
31
  export declare function captureEvent(event: Event, context?: CaptureContext): string;
32
+ /**
33
+ * Log lines, sent when init has `logs: true`: logger.info("Paid", { orderId: 7 }),
34
+ * logger.warn(logger.fmt`Took ${ms} ms`), logger.log("payment-failed", "Card declined").
35
+ * Lines go out in batches; flush() and close() send what waits.
36
+ */
37
+ export declare const logger: AppLogger;
21
38
  /** Pass null to forget the user. */
22
39
  export declare function setUser(user: User | null): void;
23
40
  export declare function setTag(key: string, value: Primitive): void;
package/dist/esm/index.js CHANGED
@@ -1,13 +1,10 @@
1
1
  import { Client } from "./client.js";
2
2
  import { captureWith, getCarrier, getCurrentScope, messageContext, } from "./hub.js";
3
+ import { makeAppLogger } from "./logs.js";
3
4
  import { isObject, safely } from "./utils.js";
4
5
  export { withScope } from "./hub.js";
6
+ export { defineSchema } from "./schema.js";
5
7
  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
8
  export function init(options = {}) {
12
9
  try {
13
10
  const carrier = getCarrier();
@@ -26,6 +23,7 @@ export function init(options = {}) {
26
23
  safely(() => console.error("[StatOSS] init failed:", error));
27
24
  }
28
25
  }
26
+ return HANDLE;
29
27
  }
30
28
  /** Sends an error (or anything thrown). Returns the event id. */
31
29
  export function captureException(error, context) {
@@ -42,6 +40,20 @@ export function captureMessage(message, levelOrContext) {
42
40
  export function captureEvent(event, context) {
43
41
  return captureWith(undefined, (client, scope) => client.captureEvent(event, context, scope));
44
42
  }
43
+ /**
44
+ * Log lines, sent when init has `logs: true`: logger.info("Paid", { orderId: 7 }),
45
+ * logger.warn(logger.fmt`Took ${ms} ms`), logger.log("payment-failed", "Card declined").
46
+ * Lines go out in batches; flush() and close() send what waits.
47
+ */
48
+ export const logger = makeAppLogger((level, message, attributes) => safely(() => getCarrier().client?.captureLog(level, message, attributes, getCurrentScope())));
49
+ // What init returns: the same functions, typed by the schema. The client
50
+ // reads a context's fields whichever function it came through.
51
+ const HANDLE = Object.freeze({
52
+ captureException,
53
+ captureMessage,
54
+ captureEvent,
55
+ logger,
56
+ });
45
57
  /** Pass null to forget the user. */
46
58
  export function setUser(user) {
47
59
  safely(() => getCurrentScope().setUser(user));
@@ -1,10 +1,13 @@
1
1
  import type { Logger } from "./logger.js";
2
2
  import type { SentryLevel } from "./types.js";
3
3
  export declare const SENTRY_LEVELS: readonly SentryLevel[];
4
+ export declare const LEVEL_RULE = "use 1 to 32 lowercase letters, digits, dots, dashes or underscores";
5
+ export declare const CATEGORY_RULE = "use 1 to 64 lowercase letters, digits, dots, dashes or underscores";
4
6
  export declare const LEVEL_TAG = "statoss.level";
5
7
  export declare const CATEGORY_TAG = "statoss.category";
6
8
  export declare function isSentryLevel(value: unknown): value is SentryLevel;
7
- export declare function isValidName(value: unknown): value is string;
9
+ export declare function isLevelName(value: unknown): value is string;
10
+ export declare function isCategoryName(value: unknown): value is string;
8
11
  /** Keeps the entries of init's `levels` that are usable, logging the rest in debug mode. */
9
12
  export declare function readLevels(input: unknown, logger: Logger): Record<string, SentryLevel>;
10
13
  export interface LevelChoice {
@@ -6,16 +6,26 @@ export const SENTRY_LEVELS = [
6
6
  "info",
7
7
  "debug",
8
8
  ];
9
- /** Custom level and category names: what the server accepts as tag values to filter on. */
10
- const NAME_RE = /^[a-z0-9._-]{1,64}$/;
9
+ /**
10
+ * Names of your own: lowercase letters, digits, dots, dashes and
11
+ * underscores. A level is at most 32 characters, as the server keeps it; a
12
+ * category at most 64.
13
+ */
14
+ const LEVEL_RE = /^[a-z0-9._-]{1,32}$/;
15
+ const CATEGORY_RE = /^[a-z0-9._-]{1,64}$/;
16
+ export const LEVEL_RULE = "use 1 to 32 lowercase letters, digits, dots, dashes or underscores";
17
+ export const CATEGORY_RULE = "use 1 to 64 lowercase letters, digits, dots, dashes or underscores";
11
18
  export const LEVEL_TAG = "statoss.level";
12
19
  export const CATEGORY_TAG = "statoss.category";
13
20
  export function isSentryLevel(value) {
14
21
  return (typeof value === "string" &&
15
22
  SENTRY_LEVELS.includes(value));
16
23
  }
17
- export function isValidName(value) {
18
- return typeof value === "string" && NAME_RE.test(value);
24
+ export function isLevelName(value) {
25
+ return typeof value === "string" && LEVEL_RE.test(value);
26
+ }
27
+ export function isCategoryName(value) {
28
+ return typeof value === "string" && CATEGORY_RE.test(value);
19
29
  }
20
30
  /** Keeps the entries of init's `levels` that are usable, logging the rest in debug mode. */
21
31
  export function readLevels(input, logger) {
@@ -30,8 +40,8 @@ export function readLevels(input, logger) {
30
40
  if (isSentryLevel(name)) {
31
41
  logger.warn(`Ignored level "${name}" in \`levels\`: it is already a Sentry level.`);
32
42
  }
33
- else if (!isValidName(name)) {
34
- logger.warn(`Ignored level "${name}" in \`levels\`: use 1 to 64 lowercase letters, digits, dots, dashes or underscores.`);
43
+ else if (!isLevelName(name)) {
44
+ logger.warn(`Ignored level "${name}" in \`levels\`: ${LEVEL_RULE}.`);
35
45
  }
36
46
  else if (!isSentryLevel(level)) {
37
47
  logger.warn(`Ignored level "${name}" in \`levels\`: map it to fatal, error, warning, info or debug.`);
@@ -52,18 +62,18 @@ export function chooseLevel(candidates, levels, fallback, logger) {
52
62
  continue;
53
63
  if (isSentryLevel(candidate))
54
64
  return { level: candidate };
55
- if (isValidName(candidate)) {
65
+ if (isLevelName(candidate)) {
56
66
  return { level: levels[candidate] ?? "error", custom: candidate };
57
67
  }
58
- logger.warn(`Ignored level "${safeString(candidate)}": use a Sentry level or 1 to 64 lowercase letters, digits, dots, dashes or underscores.`);
68
+ logger.warn(`Ignored level "${safeString(candidate)}": use a Sentry level or 1 to 32 lowercase letters, digits, dots, dashes or underscores.`);
59
69
  }
60
70
  return { level: fallback };
61
71
  }
62
72
  export function readCategory(value, logger) {
63
73
  if (value === undefined || value === null)
64
74
  return undefined;
65
- if (isValidName(value))
75
+ if (isCategoryName(value))
66
76
  return value;
67
- logger.warn(`Ignored category "${safeString(value)}": use 1 to 64 lowercase letters, digits, dots, dashes or underscores.`);
77
+ logger.warn(`Ignored category "${safeString(value)}": ${CATEGORY_RULE}.`);
68
78
  return undefined;
69
79
  }