@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.
@@ -0,0 +1,133 @@
1
+ import type { Logger } from "./logger.js";
2
+ import type { LevelOf, SchemaSpec } from "./schema.js";
3
+ import type { SentryLevel } from "./types.js";
4
+ /**
5
+ * Log lines, as Sentry's logger sends them: `log` items in envelopes, each
6
+ * line with a level, a body and typed attributes
7
+ * (https://develop.sentry.dev/sdk/telemetry/logs/).
8
+ */
9
+ /** The levels of a log line: OpenTelemetry's six ranges, as Sentry's logger names them. */
10
+ export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
11
+ export declare const LOG_LEVELS: readonly LogLevel[];
12
+ /** What intake keeps of a line (docs/logs.md, In), in UTF-8 bytes: a body, and an attribute's value. */
13
+ export declare const MAX_BODY_BYTES: number;
14
+ export declare const MAX_ATTRIBUTE_BYTES: number;
15
+ /** A cut body's size in bytes before the cut, as intake records it. */
16
+ export declare const TRUNCATED = "statoss.truncated";
17
+ /** Each level's severity number: the start of its OpenTelemetry range. */
18
+ export declare const LOG_SEVERITY: Readonly<Record<LogLevel, number>>;
19
+ export declare function isLogLevel(value: unknown): value is LogLevel;
20
+ /** The level a range (a schema's `like`) or a Sentry level is logged at. */
21
+ export declare function logLevelOf(range: string): LogLevel | undefined;
22
+ /** The range a severity number falls in. */
23
+ export declare function rangeOfSeverity(severity: number): LogLevel;
24
+ /** A message made by `logger.fmt`: the text, and the template and values it came from. */
25
+ export interface LogMessage {
26
+ /** The message, its values filled in. */
27
+ readonly text: string;
28
+ /** The template, each value written %s, as Sentry's logger writes it. */
29
+ readonly template: string;
30
+ readonly values: readonly unknown[];
31
+ toString(): string;
32
+ }
33
+ /**
34
+ * A message with parameters: logger.info(logger.fmt`Paid ${amount} for ${order}`).
35
+ * The line's body is the text; the template and each value go along as
36
+ * the attributes sentry.message.template and sentry.message.parameter.<n>.
37
+ */
38
+ export declare function fmt(strings: TemplateStringsArray, ...values: unknown[]): LogMessage;
39
+ export declare function isLogMessage(value: unknown): value is LogMessage;
40
+ /** A line's attributes: strings, numbers and booleans are sent as they are, anything else as JSON. */
41
+ export type LogAttributes = Record<string, unknown>;
42
+ /** A log line before it is sent, as beforeSendLog sees it. */
43
+ export interface Log {
44
+ level: LogLevel;
45
+ /** The message, with its values filled in. */
46
+ message: string;
47
+ attributes: LogAttributes;
48
+ /** 1 to 24, inside the level's range. */
49
+ severityNumber: number;
50
+ /** Seconds since the epoch, with fractions. */
51
+ timestamp: number;
52
+ }
53
+ export type LogAttributeValue = {
54
+ value: string;
55
+ type: "string";
56
+ } | {
57
+ value: number;
58
+ type: "integer";
59
+ } | {
60
+ value: number;
61
+ type: "double";
62
+ } | {
63
+ value: boolean;
64
+ type: "boolean";
65
+ };
66
+ /** A log line as an envelope's log item holds it. */
67
+ export interface SerializedLog {
68
+ timestamp: number;
69
+ level: LogLevel;
70
+ body: string;
71
+ /** 32 lower-case hex digits; Sentry's Relay drops a line without one. */
72
+ trace_id: string;
73
+ severity_number: number;
74
+ attributes: Record<string, LogAttributeValue>;
75
+ }
76
+ /** An attribute's value as Sentry types it; undefined and null are left out. */
77
+ export declare function serializeAttribute(value: unknown): LogAttributeValue | undefined;
78
+ /** A string's length in UTF-8 bytes. */
79
+ export declare function utf8Length(s: string): number;
80
+ /**
81
+ * A string cut to at most `max` UTF-8 bytes, the cut marked "...", as
82
+ * intake's cut writes it (intake/normalize.ts), never inside a character.
83
+ */
84
+ export declare function cutBytes(s: string, max: number): string;
85
+ /**
86
+ * A body past 64 KiB is cut there, with its size in bytes as
87
+ * statoss.truncated, as intake cuts it and records it: one huge line must
88
+ * not take the batch it is sent in down with it.
89
+ */
90
+ export declare function cutBody(body: string, attributes: Record<string, LogAttributeValue>): string;
91
+ /** The message, or a value in it, as text: objects as JSON. */
92
+ export declare function bodyOf(message: unknown): string;
93
+ /** A level of the app's own, as a line is logged at it. */
94
+ export interface OwnLogLevel {
95
+ level: LogLevel;
96
+ severity: number;
97
+ }
98
+ export interface LogLevelChoice {
99
+ level: LogLevel;
100
+ severity: number;
101
+ /** The app's own level, when the line names one. */
102
+ custom?: string;
103
+ }
104
+ /**
105
+ * The level a line is logged at. One of the six (or a Sentry level) is
106
+ * itself; an app's own level is its range's, with its severity and its
107
+ * name for statoss.level; a name nobody declared ranks as error, as an
108
+ * event's does. Anything else is logged as info.
109
+ */
110
+ export declare function chooseLogLevel(level: unknown, own: Record<string, OwnLogLevel>, logger: Logger): LogLevelChoice;
111
+ /** The ranges of init's `levels`: a Sentry level each. */
112
+ export declare function ownLevelsFromSentry(levels: Record<string, SentryLevel>): Record<string, OwnLogLevel>;
113
+ /** What `logger` and the handle's `logger` take as a level, typed by the schema. */
114
+ export type LogLevelOf<S = SchemaSpec> = LogLevel | LevelOf<S>;
115
+ /** The logger: one method per level, any level by name, and templates. */
116
+ export interface AppLogger<S = SchemaSpec> {
117
+ trace(message: string | LogMessage, attributes?: LogAttributes): void;
118
+ debug(message: string | LogMessage, attributes?: LogAttributes): void;
119
+ info(message: string | LogMessage, attributes?: LogAttributes): void;
120
+ warn(message: string | LogMessage, attributes?: LogAttributes): void;
121
+ error(message: string | LogMessage, attributes?: LogAttributes): void;
122
+ fatal(message: string | LogMessage, attributes?: LogAttributes): void;
123
+ /**
124
+ * A line at a level by name: one of the six, a Sentry level, or one of
125
+ * the app's own (with a schema, only its names). An own level is sent at
126
+ * its range's level with the attribute statoss.level.
127
+ */
128
+ log(level: LogLevelOf<S>, message: string | LogMessage, attributes?: LogAttributes): void;
129
+ /** A message with parameters: logger.fmt`Paid ${amount}`. */
130
+ fmt(strings: TemplateStringsArray, ...values: unknown[]): LogMessage;
131
+ }
132
+ /** A logger that hands each line to `capture`. */
133
+ export declare function makeAppLogger(capture: (level: unknown, message: unknown, attributes: LogAttributes | undefined) => void): AppLogger;
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LOG_SEVERITY = exports.TRUNCATED = exports.MAX_ATTRIBUTE_BYTES = exports.MAX_BODY_BYTES = exports.LOG_LEVELS = void 0;
4
+ exports.isLogLevel = isLogLevel;
5
+ exports.logLevelOf = logLevelOf;
6
+ exports.rangeOfSeverity = rangeOfSeverity;
7
+ exports.fmt = fmt;
8
+ exports.isLogMessage = isLogMessage;
9
+ exports.serializeAttribute = serializeAttribute;
10
+ exports.utf8Length = utf8Length;
11
+ exports.cutBytes = cutBytes;
12
+ exports.cutBody = cutBody;
13
+ exports.bodyOf = bodyOf;
14
+ exports.chooseLogLevel = chooseLogLevel;
15
+ exports.ownLevelsFromSentry = ownLevelsFromSentry;
16
+ exports.makeAppLogger = makeAppLogger;
17
+ const levels_js_1 = require("./levels.js");
18
+ const utils_js_1 = require("./utils.js");
19
+ exports.LOG_LEVELS = [
20
+ "trace",
21
+ "debug",
22
+ "info",
23
+ "warn",
24
+ "error",
25
+ "fatal",
26
+ ];
27
+ /** What intake keeps of a line (docs/logs.md, In), in UTF-8 bytes: a body, and an attribute's value. */
28
+ exports.MAX_BODY_BYTES = 64 * 1024;
29
+ exports.MAX_ATTRIBUTE_BYTES = 8 * 1024;
30
+ /** A cut body's size in bytes before the cut, as intake records it. */
31
+ exports.TRUNCATED = "statoss.truncated";
32
+ /** Each level's severity number: the start of its OpenTelemetry range. */
33
+ exports.LOG_SEVERITY = {
34
+ trace: 1,
35
+ debug: 5,
36
+ info: 9,
37
+ warn: 13,
38
+ error: 17,
39
+ fatal: 21,
40
+ };
41
+ function isLogLevel(value) {
42
+ return (typeof value === "string" &&
43
+ exports.LOG_LEVELS.includes(value));
44
+ }
45
+ /** The level a range (a schema's `like`) or a Sentry level is logged at. */
46
+ function logLevelOf(range) {
47
+ if (range === "warning")
48
+ return "warn";
49
+ return isLogLevel(range) ? range : undefined;
50
+ }
51
+ /** The range a severity number falls in. */
52
+ function rangeOfSeverity(severity) {
53
+ if (severity >= 21)
54
+ return "fatal";
55
+ if (severity >= 17)
56
+ return "error";
57
+ if (severity >= 13)
58
+ return "warn";
59
+ if (severity >= 9)
60
+ return "info";
61
+ if (severity >= 5)
62
+ return "debug";
63
+ return "trace";
64
+ }
65
+ /**
66
+ * A message with parameters: logger.info(logger.fmt`Paid ${amount} for ${order}`).
67
+ * The line's body is the text; the template and each value go along as
68
+ * the attributes sentry.message.template and sentry.message.parameter.<n>.
69
+ */
70
+ function fmt(strings, ...values) {
71
+ const parts = Array.from(strings, (s, i) => s ?? strings.raw[i]);
72
+ let text = parts[0];
73
+ for (let i = 1; i < parts.length; i++)
74
+ text += `${bodyOf(values[i - 1])}${parts[i]}`;
75
+ return Object.freeze({
76
+ text,
77
+ template: parts.map((s) => s.replace(/%/g, "%%")).join("%s"),
78
+ values: Object.freeze([...values]),
79
+ toString: () => text,
80
+ });
81
+ }
82
+ function isLogMessage(value) {
83
+ return ((0, utils_js_1.isObject)(value) &&
84
+ typeof value.text === "string" &&
85
+ typeof value.template === "string" &&
86
+ Array.isArray(value.values));
87
+ }
88
+ /** An attribute's value as Sentry types it; undefined and null are left out. */
89
+ function serializeAttribute(value) {
90
+ if (value === undefined || value === null)
91
+ return undefined;
92
+ if (typeof value === "string")
93
+ return { value: cutBytes(value, exports.MAX_ATTRIBUTE_BYTES), type: "string" };
94
+ if (typeof value === "boolean")
95
+ return { value, type: "boolean" };
96
+ if (typeof value === "number") {
97
+ if (!Number.isFinite(value))
98
+ return { value: (0, utils_js_1.safeString)(value), type: "string" };
99
+ return Number.isInteger(value)
100
+ ? { value, type: "integer" }
101
+ : { value, type: "double" };
102
+ }
103
+ if (typeof value === "bigint") {
104
+ const n = Number(value);
105
+ return Number.isSafeInteger(n)
106
+ ? { value: n, type: "integer" }
107
+ : { value: value.toString(), type: "string" };
108
+ }
109
+ const text = value instanceof String
110
+ ? String(value)
111
+ : ((0, utils_js_1.safely)(() => JSON.stringify((0, utils_js_1.normalize)(value, 4, 1024, 100))) ??
112
+ (0, utils_js_1.safeString)(value));
113
+ return { value: cutBytes(text, exports.MAX_ATTRIBUTE_BYTES), type: "string" };
114
+ }
115
+ /** UTF-8 bytes of one UTF-16 unit at i, and how many units it takes. */
116
+ function utf8At(s, i) {
117
+ const c = s.charCodeAt(i);
118
+ if (c < 0x80)
119
+ return [1, 1];
120
+ if (c < 0x800)
121
+ return [2, 1];
122
+ if (c >= 0xd800 &&
123
+ c <= 0xdbff &&
124
+ i + 1 < s.length &&
125
+ (s.charCodeAt(i + 1) & 0xfc00) === 0xdc00)
126
+ return [4, 2];
127
+ // A lone surrogate is written as U+FFFD, three bytes.
128
+ return [3, 1];
129
+ }
130
+ /** A string's length in UTF-8 bytes. */
131
+ function utf8Length(s) {
132
+ let n = 0;
133
+ for (let i = 0; i < s.length;) {
134
+ const [bytes, units] = utf8At(s, i);
135
+ n += bytes;
136
+ i += units;
137
+ }
138
+ return n;
139
+ }
140
+ /**
141
+ * A string cut to at most `max` UTF-8 bytes, the cut marked "...", as
142
+ * intake's cut writes it (intake/normalize.ts), never inside a character.
143
+ */
144
+ function cutBytes(s, max) {
145
+ if (s.length * 3 <= max || utf8Length(s) <= max)
146
+ return s;
147
+ let n = 0;
148
+ let i = 0;
149
+ while (i < s.length) {
150
+ const [bytes, units] = utf8At(s, i);
151
+ if (n + bytes > max - 3)
152
+ break;
153
+ n += bytes;
154
+ i += units;
155
+ }
156
+ // As intake: a lone high surrogate does not end a cut.
157
+ if (i > 0 && /[\ud800-\udbff]/.test(s[i - 1]))
158
+ i--;
159
+ return `${s.slice(0, i)}...`;
160
+ }
161
+ /**
162
+ * A body past 64 KiB is cut there, with its size in bytes as
163
+ * statoss.truncated, as intake cuts it and records it: one huge line must
164
+ * not take the batch it is sent in down with it.
165
+ */
166
+ function cutBody(body, attributes) {
167
+ if (body.length * 3 <= exports.MAX_BODY_BYTES)
168
+ return body;
169
+ const size = utf8Length(body);
170
+ if (size <= exports.MAX_BODY_BYTES)
171
+ return body;
172
+ attributes[exports.TRUNCATED] = { value: size, type: "integer" };
173
+ return cutBytes(body, exports.MAX_BODY_BYTES);
174
+ }
175
+ /** The message, or a value in it, as text: objects as JSON. */
176
+ function bodyOf(message) {
177
+ if (typeof message === "string")
178
+ return message;
179
+ if (isLogMessage(message))
180
+ return message.text;
181
+ if (message instanceof String)
182
+ return String(message);
183
+ if ((0, utils_js_1.isObject)(message) && !(message instanceof Error)) {
184
+ const json = (0, utils_js_1.safely)(() => JSON.stringify((0, utils_js_1.normalize)(message, 4)));
185
+ if (json !== undefined)
186
+ return json;
187
+ }
188
+ return (0, utils_js_1.safeString)(message);
189
+ }
190
+ /**
191
+ * The level a line is logged at. One of the six (or a Sentry level) is
192
+ * itself; an app's own level is its range's, with its severity and its
193
+ * name for statoss.level; a name nobody declared ranks as error, as an
194
+ * event's does. Anything else is logged as info.
195
+ */
196
+ function chooseLogLevel(level, own, logger) {
197
+ if (isLogLevel(level))
198
+ return { level, severity: exports.LOG_SEVERITY[level] };
199
+ if ((0, levels_js_1.isSentryLevel)(level)) {
200
+ const as = logLevelOf(level) ?? "error";
201
+ return { level: as, severity: exports.LOG_SEVERITY[as] };
202
+ }
203
+ if ((0, levels_js_1.isLevelName)(level)) {
204
+ const found = own[level];
205
+ if (found)
206
+ return { ...found, custom: level };
207
+ logger.warn(`Level "${level}" is not in the schema or \`levels\`, so the line is logged as error.`);
208
+ return { level: "error", severity: exports.LOG_SEVERITY.error, custom: level };
209
+ }
210
+ logger.warn(`Logged "${(0, utils_js_1.safeString)(level)}" as info: use trace, debug, info, warn, error, fatal or a level name (${levels_js_1.LEVEL_RULE}).`);
211
+ return { level: "info", severity: exports.LOG_SEVERITY.info };
212
+ }
213
+ /** The ranges of init's `levels`: a Sentry level each. */
214
+ function ownLevelsFromSentry(levels) {
215
+ const out = {};
216
+ for (const [name, sentAs] of Object.entries(levels)) {
217
+ const level = logLevelOf(sentAs) ?? "error";
218
+ out[name] = { level, severity: exports.LOG_SEVERITY[level] };
219
+ }
220
+ return out;
221
+ }
222
+ /** A logger that hands each line to `capture`. */
223
+ function makeAppLogger(capture) {
224
+ const at = (level) => (message, attributes) => capture(level, message, attributes);
225
+ return Object.freeze({
226
+ trace: at("trace"),
227
+ debug: at("debug"),
228
+ info: at("info"),
229
+ warn: at("warn"),
230
+ error: at("error"),
231
+ fatal: at("fatal"),
232
+ log: (level, message, attributes) => capture(level, message, attributes),
233
+ fmt,
234
+ });
235
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Logger } from "./logger.js";
2
+ import { type AppLogger, type OwnLogLevel } from "./logs.js";
2
3
  import type { CaptureContext, Event, Level, SentryLevel } from "./types.js";
3
4
  /** The ranges a level ranks in: Sentry's five and OpenTelemetry's trace. */
4
5
  export declare const LIKES: readonly ["fatal", "error", "warning", "info", "debug", "trace"];
@@ -190,7 +191,18 @@ export interface Handle<S = SchemaSpec> {
190
191
  captureMessage(message: string, levelOrContext?: LevelOf<S> | SchemaContext<S>): string;
191
192
  /** Sends an event you built in Sentry's schema. Returns the event id. */
192
193
  captureEvent(event: Event, context?: SchemaContext<S>): string;
194
+ /**
195
+ * Log lines, sent when init has `logs: true`; `log` takes the schema's
196
+ * own levels.
197
+ */
198
+ logger: AppLogger<S>;
193
199
  }
200
+ /**
201
+ * How a line at each level of init's `schema` is logged: at its range's
202
+ * level (trace stays trace), with its severity when it gives one inside
203
+ * that range. Names readSchema refuses are left out.
204
+ */
205
+ export declare function schemaLogLevels(schema: unknown): Record<string, OwnLogLevel>;
194
206
  /**
195
207
  * The Sentry level each level of init's `schema` is sent as. Built-in
196
208
  * levels are left out; names the SDK cannot send are logged in debug mode.
@@ -2,8 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FIELD_TYPES = exports.RULE_MATCHES = exports.RULE_SUBJECTS = exports.SCHEMA_COLORS = exports.LIKES = void 0;
4
4
  exports.defineSchema = defineSchema;
5
+ exports.schemaLogLevels = schemaLogLevels;
5
6
  exports.readSchema = readSchema;
6
7
  const levels_js_1 = require("./levels.js");
8
+ const logs_js_1 = require("./logs.js");
7
9
  const utils_js_1 = require("./utils.js");
8
10
  // The server's own lists (src/lib/app-schema.ts, src/lib/schema-colors.ts,
9
11
  // src/db/schema.ts), kept equal by a test.
@@ -79,6 +81,44 @@ function sentAs(name, value, logger) {
79
81
  return "info";
80
82
  return "debug";
81
83
  }
84
+ /**
85
+ * How a line at each level of init's `schema` is logged: at its range's
86
+ * level (trace stays trace), with its severity when it gives one inside
87
+ * that range. Names readSchema refuses are left out.
88
+ */
89
+ function schemaLogLevels(schema) {
90
+ const out = {};
91
+ if (!(0, utils_js_1.isObject)(schema) || Array.isArray(schema))
92
+ return out;
93
+ const { levels } = schema;
94
+ if (!(0, utils_js_1.isObject)(levels) || Array.isArray(levels))
95
+ return out;
96
+ for (const [name, value] of Object.entries(levels)) {
97
+ if ((0, levels_js_1.isSentryLevel)(name) || !(0, levels_js_1.isLevelName)(name))
98
+ continue;
99
+ const settings = (0, utils_js_1.isObject)(value) ? value : {};
100
+ const like = typeof value === "string" ? value : settings.like;
101
+ const given = settings.severity;
102
+ const severity = typeof given === "number" &&
103
+ Number.isInteger(given) &&
104
+ given >= 1 &&
105
+ given <= 24
106
+ ? given
107
+ : undefined;
108
+ let level = "error";
109
+ if (typeof like === "string" && exports.LIKES.includes(like))
110
+ level = (0, logs_js_1.logLevelOf)(like) ?? "error";
111
+ else if (like === undefined && severity !== undefined)
112
+ level = (0, logs_js_1.rangeOfSeverity)(severity);
113
+ out[name] = {
114
+ level,
115
+ severity: severity !== undefined && (0, logs_js_1.rangeOfSeverity)(severity) === level
116
+ ? severity
117
+ : logs_js_1.LOG_SEVERITY[level],
118
+ };
119
+ }
120
+ return out;
121
+ }
82
122
  /**
83
123
  * The Sentry level each level of init's `schema` is sent as. Built-in
84
124
  * levels are left out; names the SDK cannot send are logged in debug mode.
@@ -2,6 +2,8 @@ import { type Envelope } from "./envelope.js";
2
2
  import type { Logger } from "./logger.js";
3
3
  import type { Transport } from "./types.js";
4
4
  export declare const QUEUE_LIMIT = 30;
5
+ /** Of the queue, what log envelopes may take, so a flood of lines never pushes out an error. */
6
+ export declare const LOG_QUEUE_LIMIT = 10;
5
7
  export declare const CONCURRENCY = 5;
6
8
  /** The default transport: fetch, called on globalThis (Workers refuse any other this). */
7
9
  export declare function fetchTransport(browserLike: boolean): Transport;
@@ -16,12 +18,15 @@ export interface SenderOptions {
16
18
  }
17
19
  /**
18
20
  * Sends envelopes in order of arrival, a few at a time, keeping at most 30
19
- * waiting or in flight. Whatever cannot be sent is counted for client reports.
21
+ * waiting or in flight, of them at most 10 of log lines. Whatever cannot
22
+ * be sent is counted for client reports.
20
23
  */
21
24
  export declare class Sender {
22
25
  private readonly options;
23
26
  private readonly queue;
24
27
  private active;
28
+ /** Log envelopes waiting or in flight. */
29
+ private logs;
25
30
  private limits;
26
31
  private waiters;
27
32
  private keepaliveBytes;
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Sender = exports.CONCURRENCY = exports.QUEUE_LIMIT = void 0;
3
+ exports.Sender = exports.CONCURRENCY = exports.LOG_QUEUE_LIMIT = exports.QUEUE_LIMIT = void 0;
4
4
  exports.fetchTransport = fetchTransport;
5
5
  const envelope_js_1 = require("./envelope.js");
6
6
  const ratelimit_js_1 = require("./ratelimit.js");
7
7
  const utils_js_1 = require("./utils.js");
8
8
  exports.QUEUE_LIMIT = 30;
9
+ /** Of the queue, what log envelopes may take, so a flood of lines never pushes out an error. */
10
+ exports.LOG_QUEUE_LIMIT = 10;
9
11
  exports.CONCURRENCY = 5;
10
12
  const REQUEST_TIMEOUT_MS = 30000;
11
13
  // Browsers refuse keepalive requests past 64 KiB in flight in total.
@@ -61,13 +63,16 @@ function header(response, name) {
61
63
  }
62
64
  /**
63
65
  * Sends envelopes in order of arrival, a few at a time, keeping at most 30
64
- * waiting or in flight. Whatever cannot be sent is counted for client reports.
66
+ * waiting or in flight, of them at most 10 of log lines. Whatever cannot
67
+ * be sent is counted for client reports.
65
68
  */
66
69
  class Sender {
67
70
  constructor(options) {
68
71
  this.options = options;
69
72
  this.queue = [];
70
73
  this.active = 0;
74
+ /** Log envelopes waiting or in flight. */
75
+ this.logs = 0;
71
76
  this.limits = {};
72
77
  this.waiters = [];
73
78
  this.keepaliveBytes = 0;
@@ -77,11 +82,15 @@ class Sender {
77
82
  const allowed = this.withoutLimited(envelope);
78
83
  if (!allowed)
79
84
  return;
80
- if (this.queue.length + this.active >= exports.QUEUE_LIMIT) {
85
+ const log = (0, envelope_js_1.isLogEnvelope)(allowed);
86
+ if (this.queue.length + this.active >= exports.QUEUE_LIMIT ||
87
+ (log && this.logs >= exports.LOG_QUEUE_LIMIT)) {
81
88
  this.options.logger.warn("Dropped an envelope: the send queue is full.");
82
89
  this.lose(allowed, "queue_overflow");
83
90
  return;
84
91
  }
92
+ if (log)
93
+ this.logs++;
85
94
  this.queue.push(allowed);
86
95
  this.pump();
87
96
  }
@@ -130,7 +139,7 @@ class Sender {
130
139
  if (!(0, ratelimit_js_1.isRateLimited)(this.limits, category, now))
131
140
  return true;
132
141
  if (category !== "internal")
133
- this.options.recordLoss("ratelimit_backoff", category);
142
+ this.options.recordLoss("ratelimit_backoff", category, (0, envelope_js_1.itemQuantity)(item));
134
143
  return false;
135
144
  });
136
145
  if (items.length < envelope[1].length) {
@@ -140,13 +149,20 @@ class Sender {
140
149
  }
141
150
  pump() {
142
151
  while (this.active < exports.CONCURRENCY && this.queue.length > 0) {
152
+ const waited = this.queue.shift();
153
+ const log = (0, envelope_js_1.isLogEnvelope)(waited);
143
154
  // A 429 may have come in while it waited.
144
- const next = this.withoutLimited(this.queue.shift());
145
- if (!next)
155
+ const next = this.withoutLimited(waited);
156
+ if (!next) {
157
+ if (log)
158
+ this.logs--;
146
159
  continue;
160
+ }
147
161
  this.active++;
148
162
  const done = () => {
149
163
  this.active--;
164
+ if (log)
165
+ this.logs--;
150
166
  this.pump();
151
167
  };
152
168
  void this.deliver(next).then(done, done);
@@ -211,7 +227,7 @@ class Sender {
211
227
  const category = (0, envelope_js_1.itemCategory)(item);
212
228
  // A lost client report is not reported again.
213
229
  if (category !== "internal")
214
- this.options.recordLoss(reason, category);
230
+ this.options.recordLoss(reason, category, (0, envelope_js_1.itemQuantity)(item));
215
231
  }
216
232
  }
217
233
  }
@@ -1,3 +1,4 @@
1
+ import type { Log } from "./logs.js";
1
2
  /** The five levels Sentry knows. */
2
3
  export type SentryLevel = "fatal" | "error" | "warning" | "info" | "debug";
3
4
  /**
@@ -174,7 +175,10 @@ export interface InitOptions {
174
175
  beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null;
175
176
  /** Defaults to 100, which is also the most kept. */
176
177
  maxBreadcrumbs?: number;
177
- /** When true, browsers ask the server to record the sender's IP address. Defaults to false. */
178
+ /**
179
+ * When true, browsers ask the server to record the sender's IP address,
180
+ * and log lines carry the scope's user. Defaults to false.
181
+ */
178
182
  sendDefaultPii?: boolean;
179
183
  /** Tags on every event. */
180
184
  tags?: Record<string, Primitive>;
@@ -187,4 +191,8 @@ export interface InitOptions {
187
191
  integrations?: IntegrationToggles;
188
192
  /** Your own level names and the Sentry level each is sent as, such as { "payment-failed": "error" }. */
189
193
  levels?: Record<string, SentryLevel>;
194
+ /** Sends the lines of `logger`, as Sentry's enableLogs does. Defaults to false. */
195
+ logs?: boolean;
196
+ /** Change or drop (return null) a log line before it is sent. */
197
+ beforeSendLog?: (log: Log) => Log | null;
190
198
  }
@@ -1,3 +1,3 @@
1
1
  export declare const SDK_NAME = "statoss.javascript";
2
- export declare const SDK_VERSION = "0.3.0";
2
+ export declare const SDK_VERSION = "0.4.0";
3
3
  export declare const SDK_PACKAGE = "npm:@statoss/sdk";
@@ -3,5 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_PACKAGE = exports.SDK_VERSION = exports.SDK_NAME = void 0;
4
4
  exports.SDK_NAME = "statoss.javascript";
5
5
  // Kept equal to package.json by a test.
6
- exports.SDK_VERSION = "0.3.0";
6
+ exports.SDK_VERSION = "0.4.0";
7
7
  exports.SDK_PACKAGE = "npm:@statoss/sdk";
@@ -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. */