@statoss/sdk 0.3.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0 (unreleased)
4
+
5
+ - Logs: `init({ logs: true })` sends the lines of `logger` as Sentry's log
6
+ items. `logger.trace`, `debug`, `info`, `warn`, `error` and `fatal` take a
7
+ message and attributes; `logger.fmt` sends a template and its values.
8
+ - `logger.log(level, message, attributes)` takes the app's own levels, typed
9
+ by the schema on the logger `init` returns. An own level is sent at its
10
+ range's level with its severity and the attribute `statoss.level`.
11
+ - `beforeSendLog` changes or drops a line.
12
+ - Lines go out 100 at a time or 5 seconds after the first, on `flush` and
13
+ `close`, and in Node, Bun and Deno when the process runs out of work.
14
+ - Envelopes of log lines take at most 10 of the 30 places in the send
15
+ queue, so errors still go during a flood of lines.
16
+ - A body past 64 KiB is cut there, with its size in bytes before the cut as
17
+ `statoss.truncated`, and an attribute's value at 8 KiB, in UTF-8 bytes,
18
+ as StatOSS's intake cuts and records them.
19
+ - Every line carries a `trace_id` of 32 hex digits, one per client for its
20
+ life, since Sentry's Relay drops a line without one.
21
+
22
+ ## 0.3.0 (8 Oct 2026)
23
+
24
+ - Levels, categories and fields in a typed schema: `defineSchema`, and
25
+ capture functions from `init` that take only the schema's names.
26
+ - A level name is at most 32 characters, as the server keeps it (it was 64);
27
+ a longer name is sent as plain `error`.
28
+
29
+ ## 0.2.0 (8 Oct 2026)
30
+
31
+ - Debug IDs: events name the debug IDs of the files their frames ran in,
32
+ for source maps uploaded with sentry-cli or `@statoss/cli`.
33
+
34
+ ## 0.1.1 (7 Oct 2026)
35
+
36
+ - The first release published from GitHub Actions with trusted publishing.
37
+
38
+ ## 0.1.0 (7 Oct 2026)
39
+
40
+ - Error tracking for browsers, Node, Bun, Deno, Cloudflare Workers and
41
+ Vercel Edge, in Sentry's protocol.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @statoss/sdk
2
2
 
3
- Error tracking for [StatOSS](https://statoss.com). One package for browsers,
4
- Node, Bun, Deno, Cloudflare Workers and Vercel Edge, with TypeScript types
5
- included and no dependencies.
3
+ Error tracking and logs for [StatOSS](https://statoss.com). One package for
4
+ browsers, Node, Bun, Deno, Cloudflare Workers and Vercel Edge, with
5
+ TypeScript types included and no dependencies.
6
6
 
7
7
  The SDK speaks Sentry's protocol (envelopes, event schema, rate limits,
8
8
  client reports), so it can also send to Sentry, and Sentry's own SDKs can
@@ -369,6 +369,57 @@ StatOSS.captureException(error, {
369
369
  one.
370
370
  - StatOSS filters and groups on these tags like any other.
371
371
 
372
+ ## Logs
373
+
374
+ With `logs: true`, `logger`'s lines are sent as Sentry's log items, which
375
+ StatOSS's Logs area searches. Without it they are dropped.
376
+
377
+ ```ts
378
+ import * as StatOSS from "@statoss/sdk";
379
+
380
+ StatOSS.init({ dsn: "...", logs: true });
381
+
382
+ StatOSS.logger.info("Order paid", { orderId: 7, amount: 12.5 });
383
+ StatOSS.logger.warn(StatOSS.logger.fmt`Search for ${term} took ${ms} ms`);
384
+ StatOSS.logger.log("error", "Card declined", { orderId: 7 });
385
+ ```
386
+
387
+ - `trace`, `debug`, `info`, `warn`, `error` and `fatal` each take a message
388
+ and attributes. Strings, numbers and booleans are sent typed; anything
389
+ else as JSON.
390
+ - `logger.fmt` fills the values in and sends the template and each value
391
+ as `sentry.message.template` and `sentry.message.parameter.<n>`.
392
+ - `logger.log(level, message, attributes)` takes a level by name: one of the
393
+ six, a Sentry level (`warning`), or one of your own. Your own level is
394
+ sent at its range's level (`trace` stays `trace`), with its severity if
395
+ the schema gives one, and its name as the attribute `statoss.level`. A
396
+ name in neither the schema nor `levels` is sent as `error`.
397
+ - Each line carries `sentry.environment`, `sentry.release`,
398
+ `sentry.sdk.name`, `sentry.sdk.version` and `server.address`, and with
399
+ `sendDefaultPii: true` the scope's `user.id`, `user.email` and
400
+ `user.name`. A line's own attributes win.
401
+ - Each line carries a `trace_id` of 32 hex digits, one for the life of the
402
+ client, since Sentry's Relay drops a line without one.
403
+ - `beforeSendLog(log)` changes a line or drops it (return `null`).
404
+ - A body past 64 KiB is cut there, with its size in bytes as
405
+ `statoss.truncated`, and an attribute's value at 8 KiB, in UTF-8 bytes,
406
+ as StatOSS's intake cuts and records them.
407
+ - Lines go out 100 at a time, or 5 seconds after the first waits; `flush`
408
+ and `close` send what waits, and in Node, Bun and Deno so does the
409
+ process running out of work.
410
+
411
+ With a schema, the logger `init` returns takes only its levels:
412
+
413
+ ```ts
414
+ const statoss = StatOSS.init({ dsn: "...", schema, logs: true });
415
+
416
+ statoss.logger.log("payment-failed", "Card declined", { orderId: 7 });
417
+ // level "error", severity 17, attributes { "statoss.level": "payment-failed", orderId: 7 }
418
+
419
+ // @ts-expect-error: not a level of the schema
420
+ statoss.logger.log("payment-faild", "Card declined");
421
+ ```
422
+
372
423
  ## What is captured
373
424
 
374
425
  | Integration | Runtimes | What it does |
@@ -423,8 +474,11 @@ await StatOSS.withScope(async (scope) => {
423
474
  ## API
424
475
 
425
476
  ```ts
426
- init(options): handle // handle.captureException, captureMessage, captureEvent
477
+ init(options): handle // handle.captureException, captureMessage, captureEvent, logger
427
478
  defineSchema(schema): schema // the schema as given, its names as types
479
+ logger.trace, debug, info, warn, error, fatal(message, attributes?)
480
+ logger.log(level, message, attributes?) // a level by name, your own too
481
+ logger.fmt`text ${value}` // a message with its template and values
428
482
  captureException(error, context?): string // returns the event id
429
483
  captureMessage(message, levelOrContext?): string
430
484
  captureEvent(event, context?): string
@@ -458,11 +512,13 @@ that one event.
458
512
  | `beforeSend` | | `(event, hint) => event \| null`, may be async |
459
513
  | `beforeBreadcrumb` | | `(breadcrumb, hint) => breadcrumb \| null` |
460
514
  | `maxBreadcrumbs` | `100` | 100 at most |
461
- | `sendDefaultPii` | `false` | Record the sender's IP address |
515
+ | `sendDefaultPii` | `false` | Record the sender's IP address; the user on log lines |
462
516
  | `tags` | | Tags on every event |
463
517
  | `initialScope` | | `{ tags, extra, contexts, user, level, fingerprint }` |
464
518
  | `schema` | | From `defineSchema`: your levels, categories, fields |
465
519
  | `levels` | | Your level names and the Sentry level each is sent as |
520
+ | `logs` | `false` | Send `logger`'s lines |
521
+ | `beforeSendLog` | | `(log) => log \| null` |
466
522
  | `integrations` | all on | See above |
467
523
  | `enabled` | `true` | |
468
524
  | `debug` | `false` | Log what the SDK does |
@@ -470,14 +526,16 @@ that one event.
470
526
 
471
527
  ## Sending to Sentry
472
528
 
473
- Use a Sentry DSN instead. Events, client reports, rate limits (including a
474
- used-up quota) and the `X-Sentry-Auth` header all follow Sentry's protocol.
475
- Custom levels arrive as Sentry levels with the `statoss.level` tag.
529
+ Use a Sentry DSN instead. Events, log lines, client reports, rate limits
530
+ (including a used-up quota) and the `X-Sentry-Auth` header all follow
531
+ Sentry's protocol. Custom levels arrive as Sentry levels with the
532
+ `statoss.level` tag, on events and log lines alike.
476
533
 
477
534
  ## Network behaviour
478
535
 
479
536
  Envelopes go to `https://<host>/api/<app number>/envelope/`, at most 5 at a
480
- time and 30 queued or in flight; more are dropped and counted. When the server answers 429, the SDK holds back
537
+ time and 30 queued or in flight, of them at most 10 of log lines; more are
538
+ dropped and counted. When the server answers 429, the SDK holds back
481
539
  the limited categories for as long as `Retry-After` or `X-Sentry-Rate-Limits`
482
540
  says. Events it could not send (queue full, rate limited, network error,
483
541
  sampled out, dropped by `beforeSend`) are counted and reported as Sentry
@@ -16,6 +16,8 @@ export declare class Client {
16
16
  private readonly endpoint?;
17
17
  private readonly sender?;
18
18
  private readonly levels;
19
+ private readonly logLevels;
20
+ private readonly logsOn;
19
21
  private readonly sampleRate;
20
22
  private readonly maxBreadcrumbs;
21
23
  private readonly discarded;
@@ -24,9 +26,22 @@ export declare class Client {
24
26
  private reportTimer?;
25
27
  private previousSignature?;
26
28
  private closed;
29
+ private logBuffer;
30
+ private logChars;
31
+ private logTimer?;
32
+ private saidLogsOff;
33
+ /** The trace id of every log line: one per client, as Sentry's SDKs do without tracing. */
34
+ private logTraceId;
27
35
  constructor(options: ClientOptions);
28
36
  /** Installs the integrations that apply to this runtime. */
29
37
  setup(): void;
38
+ /**
39
+ * Keeps a log line for the next envelope of lines. The level is one of
40
+ * the six, a Sentry level, or the app's own (sent at its range's level
41
+ * with statoss.level); attributes go along typed, as Sentry's logger
42
+ * sends them.
43
+ */
44
+ captureLog(level: unknown, message: unknown, attributes: unknown, scope: Scope): void;
30
45
  captureException(error: unknown, context: CaptureContext | undefined, scope: Scope): string;
31
46
  captureMessage(message: unknown, context: CaptureContext | undefined, scope: Scope): string;
32
47
  captureEvent(event: Event, context: CaptureContext | undefined, scope: Scope): string;
@@ -41,8 +56,16 @@ export declare class Client {
41
56
  flush(timeoutMs?: number): Promise<boolean>;
42
57
  /** Flushes, then removes the integrations. Nothing is captured afterwards. */
43
58
  close(timeoutMs?: number): Promise<boolean>;
44
- /** Removes the integrations at once, without waiting. */
59
+ /** Removes the integrations at once, without waiting; log lines kept go out. */
45
60
  teardown(): void;
61
+ private keepLog;
62
+ /** Sends the log lines kept, as one envelope. */
63
+ private sendLogs;
64
+ /**
65
+ * In Node, Bun and Deno: sends the lines kept when the process runs out
66
+ * of work, since the timer that would send them does not keep it alive.
67
+ */
68
+ private sendLogsBeforeExit;
46
69
  private flushReportsWhenHidden;
47
70
  private capture;
48
71
  /** Merges scope, context and options into the event, in Sentry's field order. */
@@ -13,6 +13,7 @@ const fetch_js_1 = require("./integrations/fetch.js");
13
13
  const handlers_js_1 = require("./integrations/handlers.js");
14
14
  const levels_js_1 = require("./levels.js");
15
15
  const logger_js_1 = require("./logger.js");
16
+ const logs_js_1 = require("./logs.js");
16
17
  const runtime_js_1 = require("./runtime.js");
17
18
  const schema_js_1 = require("./schema.js");
18
19
  const scope_js_1 = require("./scope.js");
@@ -21,6 +22,11 @@ const utils_js_1 = require("./utils.js");
21
22
  const version_js_1 = require("./version.js");
22
23
  const REPORT_INTERVAL_MS = 30000;
23
24
  const DEFAULT_FLUSH_MS = 2000;
25
+ // Log lines go out 100 at a time, or 5 seconds after the first waits, as
26
+ // Sentry's logger sends them, and well under an envelope item's 1 MiB.
27
+ const LOG_BATCH = 100;
28
+ const LOG_WAIT_MS = 5000;
29
+ const LOG_BATCH_CHARS = 800000;
24
30
  // The server drops event items over 1 MiB.
25
31
  const MAX_EVENT_CHARS = 1000000;
26
32
  const MAX_TAG_VALUE = 200;
@@ -55,6 +61,41 @@ function cleanTags(tags) {
55
61
  }
56
62
  return out;
57
63
  }
64
+ /**
65
+ * A line as a log item holds it, made valid again after beforeSendLog: one
66
+ * of the six levels, a severity inside its range, typed attributes.
67
+ */
68
+ function serializeLog(log, traceId) {
69
+ const level = (0, logs_js_1.isLogLevel)(log.level)
70
+ ? log.level
71
+ : ((0, logs_js_1.logLevelOf)((0, utils_js_1.safeString)(log.level)) ?? "info");
72
+ const n = log.severityNumber;
73
+ const severity = typeof n === "number" &&
74
+ Number.isInteger(n) &&
75
+ n >= logs_js_1.LOG_SEVERITY[level] &&
76
+ n <= logs_js_1.LOG_SEVERITY[level] + 3
77
+ ? n
78
+ : logs_js_1.LOG_SEVERITY[level];
79
+ const attributes = {};
80
+ if ((0, utils_js_1.isObject)(log.attributes)) {
81
+ for (const [key, value] of Object.entries(log.attributes)) {
82
+ const typed = (0, logs_js_1.serializeAttribute)(value);
83
+ if (key !== "" && typed)
84
+ attributes[key] = typed;
85
+ }
86
+ }
87
+ const body = (0, logs_js_1.cutBody)((0, logs_js_1.bodyOf)(log.message), attributes);
88
+ return {
89
+ timestamp: typeof log.timestamp === "number" && Number.isFinite(log.timestamp)
90
+ ? log.timestamp
91
+ : (0, utils_js_1.now)(),
92
+ level,
93
+ body,
94
+ trace_id: traceId,
95
+ severity_number: severity,
96
+ attributes,
97
+ };
98
+ }
58
99
  /** A capture's fields, as tags. Keys starting statoss. are not fields. */
59
100
  function fieldTags(fields, logger) {
60
101
  const out = {};
@@ -127,14 +168,20 @@ class Client {
127
168
  this.pending = new Set();
128
169
  this.teardowns = [];
129
170
  this.closed = false;
171
+ this.logBuffer = [];
172
+ this.logChars = 0;
173
+ this.saidLogsOff = false;
130
174
  this.options = options;
131
175
  this.logger = (0, logger_js_1.makeLogger)(options.debug === true);
132
176
  this.runtime = (0, runtime_js_1.describeRuntime)();
133
177
  // What `levels` says wins over the schema.
134
- this.levels = {
135
- ...(0, schema_js_1.readSchema)(options.schema, this.logger),
136
- ...(0, levels_js_1.readLevels)(options.levels, this.logger),
178
+ const given = (0, levels_js_1.readLevels)(options.levels, this.logger);
179
+ this.levels = { ...(0, schema_js_1.readSchema)(options.schema, this.logger), ...given };
180
+ this.logLevels = {
181
+ ...(0, schema_js_1.schemaLogLevels)(options.schema),
182
+ ...(0, logs_js_1.ownLevelsFromSentry)(given),
137
183
  };
184
+ this.logsOn = options.logs === true;
138
185
  const rate = options.sampleRate;
139
186
  if (rate !== undefined &&
140
187
  !(typeof rate === "number" && rate >= 0 && rate <= 1)) {
@@ -213,8 +260,90 @@ class Client {
213
260
  this.integrations.push("Dedupe");
214
261
  if (browser)
215
262
  this.flushReportsWhenHidden();
263
+ if (this.logsOn)
264
+ this.sendLogsBeforeExit();
216
265
  this.logger.log(`Integrations: ${this.integrations.join(", ") || "none"}.`);
217
266
  }
267
+ /**
268
+ * Keeps a log line for the next envelope of lines. The level is one of
269
+ * the six, a Sentry level, or the app's own (sent at its range's level
270
+ * with statoss.level); attributes go along typed, as Sentry's logger
271
+ * sends them.
272
+ */
273
+ captureLog(level, message, attributes, scope) {
274
+ if (!this.enabled || this.closed)
275
+ return;
276
+ if (!this.logsOn) {
277
+ if (!this.saidLogsOff) {
278
+ this.saidLogsOff = true;
279
+ this.logger.log("Log lines are not sent: init has no logs: true.");
280
+ }
281
+ return;
282
+ }
283
+ try {
284
+ const choice = (0, logs_js_1.chooseLogLevel)(level, this.logLevels, this.logger);
285
+ const attrs = {};
286
+ if ((0, utils_js_1.isObject)(attributes) && !Array.isArray(attributes)) {
287
+ for (const [key, value] of Object.entries(attributes))
288
+ if (key !== "")
289
+ attrs[key] = value;
290
+ }
291
+ else if (attributes !== undefined && attributes !== null) {
292
+ this.logger.warn("Ignored a log line's attributes: they should be an object such as { orderId: 7 }.");
293
+ }
294
+ // What the line says wins over what the SDK adds.
295
+ const add = (key, value) => {
296
+ if (value !== undefined && value !== null && !(key in attrs))
297
+ attrs[key] = value;
298
+ };
299
+ add("sentry.environment", this.options.environment ?? "production");
300
+ add("sentry.release", this.options.release);
301
+ add("sentry.sdk.name", version_js_1.SDK_NAME);
302
+ add("sentry.sdk.version", version_js_1.SDK_VERSION);
303
+ add("server.address", this.options.serverName ?? this.runtime.serverName);
304
+ // The user goes on a line only with sendDefaultPii.
305
+ const user = scope._user;
306
+ if (user && this.options.sendDefaultPii) {
307
+ add("user.id", user.id);
308
+ add("user.email", user.email);
309
+ add("user.name", user.username);
310
+ }
311
+ if ((0, logs_js_1.isLogMessage)(message) && message.values.length > 0) {
312
+ attrs["sentry.message.template"] = message.template;
313
+ message.values.forEach((value, i) => {
314
+ attrs[`sentry.message.parameter.${i}`] = value;
315
+ });
316
+ }
317
+ if (choice.custom)
318
+ attrs[levels_js_1.LEVEL_TAG] = choice.custom;
319
+ let log = {
320
+ level: choice.level,
321
+ message: (0, logs_js_1.bodyOf)(message),
322
+ attributes: attrs,
323
+ severityNumber: choice.severity,
324
+ timestamp: (0, utils_js_1.now)(),
325
+ };
326
+ if (this.options.beforeSendLog) {
327
+ try {
328
+ log = this.options.beforeSendLog(log);
329
+ }
330
+ catch (error) {
331
+ this.logger.error("beforeSendLog threw, so the line was dropped:", error);
332
+ this.recordLoss("internal_sdk_error", "log_item");
333
+ return;
334
+ }
335
+ if (!(0, utils_js_1.isObject)(log)) {
336
+ this.recordLoss("before_send", "log_item");
337
+ return;
338
+ }
339
+ }
340
+ this.logTraceId ?? (this.logTraceId = (0, utils_js_1.uuid4)());
341
+ this.keepLog(serializeLog(log, this.logTraceId));
342
+ }
343
+ catch (error) {
344
+ this.logger.error("Could not capture a log line:", error);
345
+ }
346
+ }
218
347
  captureException(error, context, scope) {
219
348
  return this.capture(() => (0, event_js_1.eventFromUnknown)(error, { type: "generic", handled: true }), context, scope, { originalException: error });
220
349
  }
@@ -297,6 +426,7 @@ class Client {
297
426
  async flush(timeoutMs = DEFAULT_FLUSH_MS) {
298
427
  if (!this.sender)
299
428
  return true;
429
+ this.sendLogs();
300
430
  const deadline = Date.now() + timeoutMs;
301
431
  const left = () => Math.max(0, deadline - Date.now());
302
432
  const processed = this.pending.size
@@ -317,18 +447,63 @@ class Client {
317
447
  this.teardown();
318
448
  return this.flush(timeoutMs);
319
449
  }
320
- /** Removes the integrations at once, without waiting. */
450
+ /** Removes the integrations at once, without waiting; log lines kept go out. */
321
451
  teardown() {
322
452
  for (const teardown of this.teardowns.splice(0))
323
453
  (0, utils_js_1.safely)(teardown);
454
+ this.sendLogs();
455
+ }
456
+ keepLog(log) {
457
+ const size = (0, utils_js_1.safely)(() => JSON.stringify(log).length, 0) ?? 0;
458
+ if (this.logBuffer.length > 0 && this.logChars + size > LOG_BATCH_CHARS)
459
+ this.sendLogs();
460
+ this.logBuffer.push(log);
461
+ this.logChars += size;
462
+ if (this.logBuffer.length >= LOG_BATCH) {
463
+ this.sendLogs();
464
+ }
465
+ else if (this.logTimer === undefined) {
466
+ this.logTimer = (0, utils_js_1.safely)(() => setTimeout(() => {
467
+ this.logTimer = undefined;
468
+ this.sendLogs();
469
+ }, LOG_WAIT_MS));
470
+ (0, utils_js_1.unref)(this.logTimer);
471
+ }
472
+ }
473
+ /** Sends the log lines kept, as one envelope. */
474
+ sendLogs() {
475
+ if (this.logTimer !== undefined) {
476
+ clearTimeout(this.logTimer);
477
+ this.logTimer = undefined;
478
+ }
479
+ if (this.logBuffer.length === 0 || !this.sender || !this.dsn)
480
+ return;
481
+ const logs = this.logBuffer;
482
+ this.logBuffer = [];
483
+ this.logChars = 0;
484
+ this.sender.send((0, envelope_js_1.logEnvelope)(logs, this.dsn));
485
+ }
486
+ /**
487
+ * In Node, Bun and Deno: sends the lines kept when the process runs out
488
+ * of work, since the timer that would send them does not keep it alive.
489
+ */
490
+ sendLogsBeforeExit() {
491
+ const proc = globalThis.process;
492
+ if (!proc || typeof proc.on !== "function")
493
+ return;
494
+ const onBeforeExit = () => (0, utils_js_1.safely)(() => this.sendLogs());
495
+ (0, utils_js_1.safely)(() => proc.on("beforeExit", onBeforeExit));
496
+ this.teardowns.push(() => proc.removeListener?.("beforeExit", onBeforeExit));
324
497
  }
325
498
  flushReportsWhenHidden() {
326
499
  const doc = globalThis.document;
327
500
  if (!doc || typeof doc.addEventListener !== "function")
328
501
  return;
329
502
  const onChange = () => {
330
- if (doc.visibilityState === "hidden")
331
- (0, utils_js_1.safely)(() => this.sendClientReport());
503
+ if (doc.visibilityState !== "hidden")
504
+ return;
505
+ (0, utils_js_1.safely)(() => this.sendLogs());
506
+ (0, utils_js_1.safely)(() => this.sendClientReport());
332
507
  };
333
508
  doc.addEventListener("visibilitychange", onChange);
334
509
  this.teardowns.push(() => doc.removeEventListener?.("visibilitychange", onChange));
@@ -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,12 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LOG_CONTENT_TYPE = void 0;
3
4
  exports.eventEnvelope = eventEnvelope;
4
5
  exports.clientReportEnvelope = clientReportEnvelope;
6
+ exports.logEnvelope = logEnvelope;
5
7
  exports.itemCategory = itemCategory;
8
+ exports.itemQuantity = itemQuantity;
9
+ exports.isLogEnvelope = isLogEnvelope;
6
10
  exports.serializeEnvelope = serializeEnvelope;
7
11
  const dsn_js_1 = require("./dsn.js");
8
12
  const utils_js_1 = require("./utils.js");
9
13
  const version_js_1 = require("./version.js");
14
+ exports.LOG_CONTENT_TYPE = "application/vnd.sentry.items.log+json";
10
15
  function sdkHeader() {
11
16
  return { name: version_js_1.SDK_NAME, version: version_js_1.SDK_VERSION };
12
17
  }
@@ -31,9 +36,44 @@ function clientReportEnvelope(discarded, dsn, timestamp = Date.now() / 1000) {
31
36
  [[{ type: "client_report" }, { timestamp, discarded_events: discarded }]],
32
37
  ];
33
38
  }
39
+ /** Log lines in one envelope, as Sentry's logger sends them. */
40
+ function logEnvelope(logs, dsn) {
41
+ return [
42
+ {
43
+ sent_at: new Date().toISOString(),
44
+ dsn: (0, dsn_js_1.dsnToString)(dsn),
45
+ sdk: sdkHeader(),
46
+ },
47
+ [
48
+ [
49
+ {
50
+ type: "log",
51
+ item_count: logs.length,
52
+ content_type: exports.LOG_CONTENT_TYPE,
53
+ },
54
+ { version: 2, items: logs },
55
+ ],
56
+ ],
57
+ ];
58
+ }
34
59
  /** Sentry's rate-limit category for an envelope item. */
35
60
  function itemCategory(item) {
36
- return item[0].type === "event" ? "error" : "internal";
61
+ switch (item[0].type) {
62
+ case "event":
63
+ return "error";
64
+ case "log":
65
+ return "log_item";
66
+ default:
67
+ return "internal";
68
+ }
69
+ }
70
+ /** How many things an item holds, as client reports count them. */
71
+ function itemQuantity(item) {
72
+ return item[0].type === "log" ? item[0].item_count : 1;
73
+ }
74
+ /** True for an envelope of log lines. */
75
+ function isLogEnvelope(envelope) {
76
+ return envelope[1].some((item) => item[0].type === "log");
37
77
  }
38
78
  function stringify(payload) {
39
79
  try {
@@ -1,6 +1,8 @@
1
+ import { type AppLogger } from "./logs.js";
1
2
  import type { CheckSchema, Handle, SchemaSpec } from "./schema.js";
2
3
  import type { Breadcrumb, CaptureContext, Event, InitOptions, Level, Primitive, User } from "./types.js";
3
4
  export { withScope } from "./hub.js";
5
+ export type { AppLogger, Log, LogAttributes, LogAttributeValue, LogLevel, LogLevelOf, LogMessage, SerializedLog, } from "./logs.js";
4
6
  export { defineSchema } from "./schema.js";
5
7
  export type { CategoryOf, CategorySettings, CheckSchema, FieldOf, FieldSettings, FieldType, FieldValue, Handle, LevelOf, LevelSettings, Like, RuleMatch, RuleSubject, SchemaColor, SchemaContext, SchemaRule, SchemaSpec, Severity, } from "./schema.js";
6
8
  export type { Scope } from "./scope.js";
@@ -9,11 +11,11 @@ export type { Breadcrumb, BreadcrumbHint, CaptureContext, DebugImage, Event, Eve
9
11
  /**
10
12
  * Starts the SDK. Call it once, as early as you can. Calling it again replaces
11
13
  * the earlier setup. Without a valid DSN, or with enabled: false, nothing is
12
- * sent and nothing is patched.
14
+ * sent and nothing is patched. With `logs: true`, `logger`'s lines are sent.
13
15
  *
14
16
  * 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.
17
+ * each ranks like, and the capture functions and logger it returns take
18
+ * only the schema's levels, categories and fields.
17
19
  */
18
20
  export declare function init<const S extends SchemaSpec & CheckSchema<S> = SchemaSpec>(options?: InitOptions & {
19
21
  schema?: S;
@@ -27,6 +29,12 @@ export declare function captureException(error: unknown, context?: CaptureContex
27
29
  export declare function captureMessage(message: string, levelOrContext?: Level | CaptureContext): string;
28
30
  /** Sends an event you built in Sentry's schema. Returns the event id. */
29
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;
30
38
  /** Pass null to forget the user. */
31
39
  export declare function setUser(user: User | null): void;
32
40
  export declare function setTag(key: string, value: Primitive): void;
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SDK_VERSION = exports.SDK_NAME = exports.defineSchema = exports.withScope = void 0;
3
+ exports.logger = exports.SDK_VERSION = exports.SDK_NAME = exports.defineSchema = exports.withScope = void 0;
4
4
  exports.init = init;
5
5
  exports.captureException = captureException;
6
6
  exports.captureMessage = captureMessage;
@@ -16,6 +16,7 @@ exports.close = close;
16
16
  exports.lastEventId = lastEventId;
17
17
  const client_js_1 = require("./client.js");
18
18
  const hub_js_1 = require("./hub.js");
19
+ const logs_js_1 = require("./logs.js");
19
20
  const utils_js_1 = require("./utils.js");
20
21
  var hub_js_2 = require("./hub.js");
21
22
  Object.defineProperty(exports, "withScope", { enumerable: true, get: function () { return hub_js_2.withScope; } });
@@ -59,12 +60,19 @@ function captureMessage(message, levelOrContext) {
59
60
  function captureEvent(event, context) {
60
61
  return (0, hub_js_1.captureWith)(undefined, (client, scope) => client.captureEvent(event, context, scope));
61
62
  }
63
+ /**
64
+ * Log lines, sent when init has `logs: true`: logger.info("Paid", { orderId: 7 }),
65
+ * logger.warn(logger.fmt`Took ${ms} ms`), logger.log("payment-failed", "Card declined").
66
+ * Lines go out in batches; flush() and close() send what waits.
67
+ */
68
+ exports.logger = (0, logs_js_1.makeAppLogger)((level, message, attributes) => (0, utils_js_1.safely)(() => (0, hub_js_1.getCarrier)().client?.captureLog(level, message, attributes, (0, hub_js_1.getCurrentScope)())));
62
69
  // What init returns: the same functions, typed by the schema. The client
63
70
  // reads a context's fields whichever function it came through.
64
71
  const HANDLE = Object.freeze({
65
72
  captureException,
66
73
  captureMessage,
67
74
  captureEvent,
75
+ logger: exports.logger,
68
76
  });
69
77
  /** Pass null to forget the user. */
70
78
  function setUser(user) {