@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 +41 -0
- package/README.md +67 -9
- package/dist/cjs/client.d.ts +24 -1
- package/dist/cjs/client.js +181 -6
- package/dist/cjs/envelope.d.ts +20 -1
- package/dist/cjs/envelope.js +41 -1
- package/dist/cjs/index.d.ts +11 -3
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/logs.d.ts +133 -0
- package/dist/cjs/logs.js +235 -0
- package/dist/cjs/schema.d.ts +12 -0
- package/dist/cjs/schema.js +40 -0
- package/dist/cjs/transport.d.ts +6 -1
- package/dist/cjs/transport.js +23 -7
- package/dist/cjs/types.d.ts +9 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/client.d.ts +24 -1
- package/dist/esm/client.js +183 -8
- package/dist/esm/envelope.d.ts +20 -1
- package/dist/esm/envelope.js +37 -1
- package/dist/esm/index.d.ts +11 -3
- package/dist/esm/index.js +8 -0
- package/dist/esm/logs.d.ts +133 -0
- package/dist/esm/logs.js +219 -0
- package/dist/esm/schema.d.ts +12 -0
- package/dist/esm/schema.js +39 -0
- package/dist/esm/transport.d.ts +6 -1
- package/dist/esm/transport.js +23 -7
- package/dist/esm/types.d.ts +9 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +4 -2
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
|
|
4
|
-
Node, Bun, Deno, Cloudflare Workers and Vercel Edge, with
|
|
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
|
|
474
|
-
used-up quota) and the `X-Sentry-Auth` header all follow
|
|
475
|
-
Custom levels arrive as Sentry levels with the
|
|
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
|
|
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
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -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. */
|
package/dist/cjs/client.js
CHANGED
|
@@ -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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
331
|
-
|
|
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));
|
package/dist/cjs/envelope.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/cjs/envelope.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -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
|
|
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) {
|