@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.
- package/CHANGELOG.md +41 -0
- package/README.md +151 -23
- package/dist/cjs/client.d.ts +29 -2
- package/dist/cjs/client.js +211 -4
- package/dist/cjs/envelope.d.ts +20 -1
- package/dist/cjs/envelope.js +41 -1
- package/dist/cjs/index.d.ts +19 -2
- package/dist/cjs/index.js +19 -6
- package/dist/cjs/levels.d.ts +4 -1
- package/dist/cjs/levels.js +23 -12
- package/dist/cjs/logs.d.ts +133 -0
- package/dist/cjs/logs.js +235 -0
- package/dist/cjs/schema.d.ts +211 -0
- package/dist/cjs/schema.js +159 -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 +29 -2
- package/dist/esm/client.js +212 -5
- package/dist/esm/envelope.d.ts +20 -1
- package/dist/esm/envelope.js +37 -1
- package/dist/esm/index.d.ts +19 -2
- package/dist/esm/index.js +17 -5
- package/dist/esm/levels.d.ts +4 -1
- package/dist/esm/levels.js +20 -10
- package/dist/esm/logs.d.ts +133 -0
- package/dist/esm/logs.js +219 -0
- package/dist/esm/schema.d.ts +211 -0
- package/dist/esm/schema.js +153 -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
|
|
@@ -269,20 +269,88 @@ Adding `--release <release>` to the upload is optional. Deploy the bundles as
|
|
|
269
269
|
`inject` left them: the SDK reads their debug ids and sends them with each
|
|
270
270
|
event. Frames are then shown as source and grouped by their source names.
|
|
271
271
|
|
|
272
|
-
##
|
|
272
|
+
## Your own levels, categories and fields
|
|
273
273
|
|
|
274
274
|
Sentry has five levels: `fatal`, `error`, `warning`, `info` and `debug`.
|
|
275
|
-
StatOSS
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
StatOSS adds levels of your own, categories and fields. They travel as tags
|
|
276
|
+
next to the nearest Sentry level, so Sentry and any Sentry-based tool still
|
|
277
|
+
read the event normally.
|
|
278
|
+
|
|
279
|
+
Declare them with `defineSchema` and give the schema to `init`. The capture
|
|
280
|
+
functions `init` returns take only the schema's names, so a wrong one fails
|
|
281
|
+
to compile.
|
|
278
282
|
|
|
279
283
|
```ts
|
|
280
|
-
StatOSS
|
|
281
|
-
|
|
284
|
+
import * as StatOSS from "@statoss/sdk";
|
|
285
|
+
|
|
286
|
+
export const schema = StatOSS.defineSchema({
|
|
282
287
|
levels: {
|
|
283
|
-
"payment-failed": "error",
|
|
288
|
+
"payment-failed": { like: "error", label: "Payment failed" },
|
|
284
289
|
"slow-query": "warning",
|
|
285
290
|
},
|
|
291
|
+
categories: {
|
|
292
|
+
billing: { label: "Billing", color: "plum" },
|
|
293
|
+
auth: {},
|
|
294
|
+
},
|
|
295
|
+
fields: {
|
|
296
|
+
route: { label: "Route", filterable: true },
|
|
297
|
+
duration_ms: { type: "number", unit: "ms", label: "Duration" },
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const statoss = StatOSS.init({ dsn: process.env.STATOSS_DSN, schema });
|
|
302
|
+
|
|
303
|
+
statoss.captureException(error, {
|
|
304
|
+
level: "payment-failed",
|
|
305
|
+
category: "billing",
|
|
306
|
+
fields: { route: "/pay" },
|
|
307
|
+
});
|
|
308
|
+
// level "error", tags { "statoss.level": "payment-failed",
|
|
309
|
+
// "statoss.category": "billing", route: "/pay" }
|
|
310
|
+
|
|
311
|
+
statoss.captureMessage("Search took 2.4 s", {
|
|
312
|
+
level: "slow-query",
|
|
313
|
+
fields: { duration_ms: 2400 },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// @ts-expect-error: not a level of this schema
|
|
317
|
+
statoss.captureException(error, { level: "payment-faild" });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
- A level ranks like `fatal`, `error`, `warning`, `info`, `debug` or
|
|
321
|
+
`trace`: give that alone, or `like` with an optional `severity` (1 to 24,
|
|
322
|
+
inside its range), `label` and `color`. It is sent as the nearest Sentry
|
|
323
|
+
level (`trace` as `debug`; `error` when none is given).
|
|
324
|
+
- A category takes an optional `label`, `color`, `owner` (a team member's
|
|
325
|
+
email) and `hidden`.
|
|
326
|
+
- A field is a tag key, with an optional `label`, `type` (`text` or
|
|
327
|
+
`number`), `unit` and `filterable`. Field values are strings, numbers or
|
|
328
|
+
booleans.
|
|
329
|
+
- Categories and fields can also be lists of names.
|
|
330
|
+
- `rules` set a level or a category for events that match, such as
|
|
331
|
+
`{ subject: "logger", value: "payments", category: "billing" }`. They name
|
|
332
|
+
the schema's own levels and categories.
|
|
333
|
+
- Colours: `crimson`, `red`, `orange`, `amber`, `yellow`, `green`, `aqua`,
|
|
334
|
+
`teal`, `blue`, `violet`, `indigo`, `magenta`, `plum` and `grey`.
|
|
335
|
+
- Names: a level is 1 to 32 characters and a category 1 to 64, of lowercase
|
|
336
|
+
letters, digits, dots, dashes and underscores. A field is any tag key not
|
|
337
|
+
starting `statoss.`.
|
|
338
|
+
|
|
339
|
+
`defineSchema` refuses a wrong name, range, severity, colour or setting at
|
|
340
|
+
compile time (TypeScript 5.0 or later). It returns the schema as given:
|
|
341
|
+
plain data in the shape StatOSS's schema API takes. A function of yours
|
|
342
|
+
that hands a schema to `init` types it as
|
|
343
|
+
`<const S extends SchemaSpec & CheckSchema<S>>(schema: S)`.
|
|
344
|
+
|
|
345
|
+
### Without a schema
|
|
346
|
+
|
|
347
|
+
The module-level functions take any level or category name, and `levels` in
|
|
348
|
+
`init` maps your names to Sentry levels:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
StatOSS.init({
|
|
352
|
+
dsn: "...",
|
|
353
|
+
levels: { "payment-failed": "error", "slow-query": "warning" },
|
|
286
354
|
});
|
|
287
355
|
|
|
288
356
|
StatOSS.captureMessage("Card declined", "payment-failed");
|
|
@@ -295,13 +363,63 @@ StatOSS.captureException(error, {
|
|
|
295
363
|
// level "warning", tags { "statoss.level": "slow-query", "statoss.category": "db.postgres" }
|
|
296
364
|
```
|
|
297
365
|
|
|
298
|
-
-
|
|
299
|
-
`levels`
|
|
300
|
-
-
|
|
301
|
-
|
|
302
|
-
underscores. Others are ignored; with `debug: true` the SDK logs each one.
|
|
366
|
+
- A name in neither `levels` nor the schema is sent as `error`. Given both,
|
|
367
|
+
`levels` wins.
|
|
368
|
+
- Names the SDK cannot send are ignored; with `debug: true` it logs each
|
|
369
|
+
one.
|
|
303
370
|
- StatOSS filters and groups on these tags like any other.
|
|
304
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
|
+
|
|
305
423
|
## What is captured
|
|
306
424
|
|
|
307
425
|
| Integration | Runtimes | What it does |
|
|
@@ -356,7 +474,11 @@ await StatOSS.withScope(async (scope) => {
|
|
|
356
474
|
## API
|
|
357
475
|
|
|
358
476
|
```ts
|
|
359
|
-
init(options):
|
|
477
|
+
init(options): handle // handle.captureException, captureMessage, captureEvent, logger
|
|
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
|
|
360
482
|
captureException(error, context?): string // returns the event id
|
|
361
483
|
captureMessage(message, levelOrContext?): string
|
|
362
484
|
captureEvent(event, context?): string
|
|
@@ -374,8 +496,9 @@ scope.setUser, setTag, setTags, setExtra, setExtras, setContext, setLevel,
|
|
|
374
496
|
scope.captureException, captureMessage, captureEvent // with this scope
|
|
375
497
|
```
|
|
376
498
|
|
|
377
|
-
`context` is `{ level, tags, extra, contexts, user, fingerprint, category }
|
|
378
|
-
It wins over the scope for
|
|
499
|
+
`context` is `{ level, tags, extra, contexts, user, fingerprint, category }`,
|
|
500
|
+
and on the handle `init` returns also `fields`. It wins over the scope for
|
|
501
|
+
that one event.
|
|
379
502
|
|
|
380
503
|
### Options
|
|
381
504
|
|
|
@@ -389,10 +512,13 @@ It wins over the scope for that one event.
|
|
|
389
512
|
| `beforeSend` | | `(event, hint) => event \| null`, may be async |
|
|
390
513
|
| `beforeBreadcrumb` | | `(breadcrumb, hint) => breadcrumb \| null` |
|
|
391
514
|
| `maxBreadcrumbs` | `100` | 100 at most |
|
|
392
|
-
| `sendDefaultPii` | `false` | Record the sender's IP address
|
|
515
|
+
| `sendDefaultPii` | `false` | Record the sender's IP address; the user on log lines |
|
|
393
516
|
| `tags` | | Tags on every event |
|
|
394
517
|
| `initialScope` | | `{ tags, extra, contexts, user, level, fingerprint }` |
|
|
518
|
+
| `schema` | | From `defineSchema`: your levels, categories, fields |
|
|
395
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` |
|
|
396
522
|
| `integrations` | all on | See above |
|
|
397
523
|
| `enabled` | `true` | |
|
|
398
524
|
| `debug` | `false` | Log what the SDK does |
|
|
@@ -400,14 +526,16 @@ It wins over the scope for that one event.
|
|
|
400
526
|
|
|
401
527
|
## Sending to Sentry
|
|
402
528
|
|
|
403
|
-
Use a Sentry DSN instead. Events, client reports, rate limits
|
|
404
|
-
used-up quota) and the `X-Sentry-Auth` header all follow
|
|
405
|
-
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.
|
|
406
533
|
|
|
407
534
|
## Network behaviour
|
|
408
535
|
|
|
409
536
|
Envelopes go to `https://<host>/api/<app number>/envelope/`, at most 5 at a
|
|
410
|
-
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
|
|
411
539
|
the limited categories for as long as `Retry-After` or `X-Sentry-Rate-Limits`
|
|
412
540
|
says. Events it could not send (queue full, rate limited, network error,
|
|
413
541
|
sampled out, dropped by `beforeSend`) are counted and reported as Sentry
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -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;
|
|
@@ -12,6 +16,8 @@ export declare class Client {
|
|
|
12
16
|
private readonly endpoint?;
|
|
13
17
|
private readonly sender?;
|
|
14
18
|
private readonly levels;
|
|
19
|
+
private readonly logLevels;
|
|
20
|
+
private readonly logsOn;
|
|
15
21
|
private readonly sampleRate;
|
|
16
22
|
private readonly maxBreadcrumbs;
|
|
17
23
|
private readonly discarded;
|
|
@@ -20,9 +26,22 @@ export declare class Client {
|
|
|
20
26
|
private reportTimer?;
|
|
21
27
|
private previousSignature?;
|
|
22
28
|
private closed;
|
|
23
|
-
|
|
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;
|
|
35
|
+
constructor(options: ClientOptions);
|
|
24
36
|
/** Installs the integrations that apply to this runtime. */
|
|
25
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;
|
|
26
45
|
captureException(error: unknown, context: CaptureContext | undefined, scope: Scope): string;
|
|
27
46
|
captureMessage(message: unknown, context: CaptureContext | undefined, scope: Scope): string;
|
|
28
47
|
captureEvent(event: Event, context: CaptureContext | undefined, scope: Scope): string;
|
|
@@ -37,8 +56,16 @@ export declare class Client {
|
|
|
37
56
|
flush(timeoutMs?: number): Promise<boolean>;
|
|
38
57
|
/** Flushes, then removes the integrations. Nothing is captured afterwards. */
|
|
39
58
|
close(timeoutMs?: number): Promise<boolean>;
|
|
40
|
-
/** Removes the integrations at once, without waiting. */
|
|
59
|
+
/** Removes the integrations at once, without waiting; log lines kept go out. */
|
|
41
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;
|
|
42
69
|
private flushReportsWhenHidden;
|
|
43
70
|
private capture;
|
|
44
71
|
/** Merges scope, context and options into the event, in Sentry's field order. */
|
package/dist/cjs/client.js
CHANGED
|
@@ -13,13 +13,20 @@ 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");
|
|
18
|
+
const schema_js_1 = require("./schema.js");
|
|
17
19
|
const scope_js_1 = require("./scope.js");
|
|
18
20
|
const transport_js_1 = require("./transport.js");
|
|
19
21
|
const utils_js_1 = require("./utils.js");
|
|
20
22
|
const version_js_1 = require("./version.js");
|
|
21
23
|
const REPORT_INTERVAL_MS = 30000;
|
|
22
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;
|
|
23
30
|
// The server drops event items over 1 MiB.
|
|
24
31
|
const MAX_EVENT_CHARS = 1000000;
|
|
25
32
|
const MAX_TAG_VALUE = 200;
|
|
@@ -54,6 +61,67 @@ function cleanTags(tags) {
|
|
|
54
61
|
}
|
|
55
62
|
return out;
|
|
56
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
|
+
}
|
|
99
|
+
/** A capture's fields, as tags. Keys starting statoss. are not fields. */
|
|
100
|
+
function fieldTags(fields, logger) {
|
|
101
|
+
const out = {};
|
|
102
|
+
if (fields === undefined || fields === null)
|
|
103
|
+
return out;
|
|
104
|
+
if (!(0, utils_js_1.isObject)(fields) || Array.isArray(fields)) {
|
|
105
|
+
logger.warn('Ignored `fields`: it should be an object such as { route: "/pay" }.');
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
109
|
+
if (value === undefined || value === null)
|
|
110
|
+
continue;
|
|
111
|
+
if (key === "" || key.startsWith("statoss.")) {
|
|
112
|
+
logger.warn(`Ignored field "${key}": fields are not empty and do not start statoss.`);
|
|
113
|
+
}
|
|
114
|
+
else if (typeof value === "string" ||
|
|
115
|
+
typeof value === "number" ||
|
|
116
|
+
typeof value === "boolean") {
|
|
117
|
+
out[key] = value;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
logger.warn(`Ignored field "${key}": its value should be a string, number or boolean.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
57
125
|
function cleanContexts(contexts) {
|
|
58
126
|
const out = {};
|
|
59
127
|
for (const [name, value] of Object.entries(contexts)) {
|
|
@@ -100,10 +168,20 @@ class Client {
|
|
|
100
168
|
this.pending = new Set();
|
|
101
169
|
this.teardowns = [];
|
|
102
170
|
this.closed = false;
|
|
171
|
+
this.logBuffer = [];
|
|
172
|
+
this.logChars = 0;
|
|
173
|
+
this.saidLogsOff = false;
|
|
103
174
|
this.options = options;
|
|
104
175
|
this.logger = (0, logger_js_1.makeLogger)(options.debug === true);
|
|
105
176
|
this.runtime = (0, runtime_js_1.describeRuntime)();
|
|
106
|
-
|
|
177
|
+
// What `levels` says wins over the schema.
|
|
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),
|
|
183
|
+
};
|
|
184
|
+
this.logsOn = options.logs === true;
|
|
107
185
|
const rate = options.sampleRate;
|
|
108
186
|
if (rate !== undefined &&
|
|
109
187
|
!(typeof rate === "number" && rate >= 0 && rate <= 1)) {
|
|
@@ -182,8 +260,90 @@ class Client {
|
|
|
182
260
|
this.integrations.push("Dedupe");
|
|
183
261
|
if (browser)
|
|
184
262
|
this.flushReportsWhenHidden();
|
|
263
|
+
if (this.logsOn)
|
|
264
|
+
this.sendLogsBeforeExit();
|
|
185
265
|
this.logger.log(`Integrations: ${this.integrations.join(", ") || "none"}.`);
|
|
186
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
|
+
}
|
|
187
347
|
captureException(error, context, scope) {
|
|
188
348
|
return this.capture(() => (0, event_js_1.eventFromUnknown)(error, { type: "generic", handled: true }), context, scope, { originalException: error });
|
|
189
349
|
}
|
|
@@ -266,6 +426,7 @@ class Client {
|
|
|
266
426
|
async flush(timeoutMs = DEFAULT_FLUSH_MS) {
|
|
267
427
|
if (!this.sender)
|
|
268
428
|
return true;
|
|
429
|
+
this.sendLogs();
|
|
269
430
|
const deadline = Date.now() + timeoutMs;
|
|
270
431
|
const left = () => Math.max(0, deadline - Date.now());
|
|
271
432
|
const processed = this.pending.size
|
|
@@ -286,18 +447,63 @@ class Client {
|
|
|
286
447
|
this.teardown();
|
|
287
448
|
return this.flush(timeoutMs);
|
|
288
449
|
}
|
|
289
|
-
/** Removes the integrations at once, without waiting. */
|
|
450
|
+
/** Removes the integrations at once, without waiting; log lines kept go out. */
|
|
290
451
|
teardown() {
|
|
291
452
|
for (const teardown of this.teardowns.splice(0))
|
|
292
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));
|
|
293
497
|
}
|
|
294
498
|
flushReportsWhenHidden() {
|
|
295
499
|
const doc = globalThis.document;
|
|
296
500
|
if (!doc || typeof doc.addEventListener !== "function")
|
|
297
501
|
return;
|
|
298
502
|
const onChange = () => {
|
|
299
|
-
if (doc.visibilityState
|
|
300
|
-
|
|
503
|
+
if (doc.visibilityState !== "hidden")
|
|
504
|
+
return;
|
|
505
|
+
(0, utils_js_1.safely)(() => this.sendLogs());
|
|
506
|
+
(0, utils_js_1.safely)(() => this.sendClientReport());
|
|
301
507
|
};
|
|
302
508
|
doc.addEventListener("visibilitychange", onChange);
|
|
303
509
|
this.teardowns.push(() => doc.removeEventListener?.("visibilitychange", onChange));
|
|
@@ -329,6 +535,7 @@ class Client {
|
|
|
329
535
|
const tags = {
|
|
330
536
|
...scope._tags,
|
|
331
537
|
...ctx.tags,
|
|
538
|
+
...fieldTags(ctx.fields, this.logger),
|
|
332
539
|
...base.tags,
|
|
333
540
|
};
|
|
334
541
|
if (choice.custom)
|
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;
|