@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.
@@ -0,0 +1,153 @@
1
+ import { CATEGORY_RULE, isCategoryName, isLevelName, isSentryLevel, LEVEL_RULE, } from "./levels.js";
2
+ import { LOG_SEVERITY, logLevelOf, rangeOfSeverity, } from "./logs.js";
3
+ import { isObject, safeString } from "./utils.js";
4
+ // The server's own lists (src/lib/app-schema.ts, src/lib/schema-colors.ts,
5
+ // src/db/schema.ts), kept equal by a test.
6
+ /** The ranges a level ranks in: Sentry's five and OpenTelemetry's trace. */
7
+ export const LIKES = [
8
+ "fatal",
9
+ "error",
10
+ "warning",
11
+ "info",
12
+ "debug",
13
+ "trace",
14
+ ];
15
+ /** The colours a level or a category takes, by name. */
16
+ export const SCHEMA_COLORS = [
17
+ "crimson",
18
+ "red",
19
+ "orange",
20
+ "amber",
21
+ "yellow",
22
+ "green",
23
+ "aqua",
24
+ "teal",
25
+ "blue",
26
+ "violet",
27
+ "indigo",
28
+ "magenta",
29
+ "plum",
30
+ "grey",
31
+ ];
32
+ /** What a rule tests: a tag by key, the logger, or one of the event's own fields. */
33
+ export const RULE_SUBJECTS = [
34
+ "tag",
35
+ "logger",
36
+ "exception",
37
+ "culprit",
38
+ "title",
39
+ "message",
40
+ ];
41
+ export const RULE_MATCHES = ["is", "starts", "contains"];
42
+ export const FIELD_TYPES = ["text", "number"];
43
+ /**
44
+ * Declares an app's levels, categories, fields and rules. Returns the schema
45
+ * as given: plain data in the shape the server takes, so it can be sent as
46
+ * it is. Its names become types: pass it to init as `schema` and the capture
47
+ * functions init returns take only these names.
48
+ */
49
+ export function defineSchema(schema) {
50
+ return schema;
51
+ }
52
+ // At runtime
53
+ /** The Sentry level a level of the schema is sent as. */
54
+ function sentAs(name, value, logger) {
55
+ const settings = isObject(value) ? value : {};
56
+ const like = typeof value === "string" ? value : settings.like;
57
+ if (isSentryLevel(like))
58
+ return like;
59
+ if (like === "trace")
60
+ return "debug";
61
+ if (like !== undefined || (typeof value !== "string" && !isObject(value))) {
62
+ logger.warn(`Level "${name}" in the schema: like should be ${LIKES.join(", ")}; sent as error.`);
63
+ return "error";
64
+ }
65
+ const severity = settings.severity;
66
+ if (typeof severity !== "number" || severity < 1 || severity > 24)
67
+ return "error";
68
+ if (severity >= 21)
69
+ return "fatal";
70
+ if (severity >= 17)
71
+ return "error";
72
+ if (severity >= 13)
73
+ return "warning";
74
+ if (severity >= 9)
75
+ return "info";
76
+ return "debug";
77
+ }
78
+ /**
79
+ * How a line at each level of init's `schema` is logged: at its range's
80
+ * level (trace stays trace), with its severity when it gives one inside
81
+ * that range. Names readSchema refuses are left out.
82
+ */
83
+ export function schemaLogLevels(schema) {
84
+ const out = {};
85
+ if (!isObject(schema) || Array.isArray(schema))
86
+ return out;
87
+ const { levels } = schema;
88
+ if (!isObject(levels) || Array.isArray(levels))
89
+ return out;
90
+ for (const [name, value] of Object.entries(levels)) {
91
+ if (isSentryLevel(name) || !isLevelName(name))
92
+ continue;
93
+ const settings = isObject(value) ? value : {};
94
+ const like = typeof value === "string" ? value : settings.like;
95
+ const given = settings.severity;
96
+ const severity = typeof given === "number" &&
97
+ Number.isInteger(given) &&
98
+ given >= 1 &&
99
+ given <= 24
100
+ ? given
101
+ : undefined;
102
+ let level = "error";
103
+ if (typeof like === "string" && LIKES.includes(like))
104
+ level = logLevelOf(like) ?? "error";
105
+ else if (like === undefined && severity !== undefined)
106
+ level = rangeOfSeverity(severity);
107
+ out[name] = {
108
+ level,
109
+ severity: severity !== undefined && rangeOfSeverity(severity) === level
110
+ ? severity
111
+ : LOG_SEVERITY[level],
112
+ };
113
+ }
114
+ return out;
115
+ }
116
+ /**
117
+ * The Sentry level each level of init's `schema` is sent as. Built-in
118
+ * levels are left out; names the SDK cannot send are logged in debug mode.
119
+ */
120
+ export function readSchema(schema, logger) {
121
+ const out = {};
122
+ if (schema === undefined || schema === null)
123
+ return out;
124
+ if (!isObject(schema) || Array.isArray(schema)) {
125
+ logger.warn("Ignored `schema`: pass what defineSchema returns.");
126
+ return out;
127
+ }
128
+ const { levels, categories } = schema;
129
+ if (isObject(levels) && !Array.isArray(levels)) {
130
+ for (const [name, value] of Object.entries(levels)) {
131
+ // A built-in level, given a label or a colour.
132
+ if (isSentryLevel(name))
133
+ continue;
134
+ if (isLevelName(name))
135
+ out[name] = sentAs(name, value, logger);
136
+ else
137
+ logger.warn(`Ignored level "${name}" in the schema: ${LEVEL_RULE}.`);
138
+ }
139
+ }
140
+ else if (levels !== undefined && levels !== null) {
141
+ logger.warn('Ignored the schema\'s levels: they should be an object such as { "payment-failed": "error" }.');
142
+ }
143
+ const names = Array.isArray(categories)
144
+ ? categories
145
+ : isObject(categories)
146
+ ? Object.keys(categories)
147
+ : [];
148
+ for (const name of names) {
149
+ if (!isCategoryName(name))
150
+ logger.warn(`Category "${safeString(name)}" in the schema cannot be sent: ${CATEGORY_RULE}.`);
151
+ }
152
+ return out;
153
+ }
@@ -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,7 +1,9 @@
1
- import { itemCategory, serializeEnvelope } from "./envelope.js";
1
+ import { isLogEnvelope, itemCategory, itemQuantity, serializeEnvelope, } from "./envelope.js";
2
2
  import { isRateLimited, updateRateLimits, } from "./ratelimit.js";
3
3
  import { safely, unref } from "./utils.js";
4
4
  export 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 const LOG_QUEUE_LIMIT = 10;
5
7
  export const CONCURRENCY = 5;
6
8
  const REQUEST_TIMEOUT_MS = 30000;
7
9
  // Browsers refuse keepalive requests past 64 KiB in flight in total.
@@ -57,13 +59,16 @@ function header(response, name) {
57
59
  }
58
60
  /**
59
61
  * Sends envelopes in order of arrival, a few at a time, keeping at most 30
60
- * waiting or in flight. Whatever cannot be sent is counted for client reports.
62
+ * waiting or in flight, of them at most 10 of log lines. Whatever cannot
63
+ * be sent is counted for client reports.
61
64
  */
62
65
  export class Sender {
63
66
  constructor(options) {
64
67
  this.options = options;
65
68
  this.queue = [];
66
69
  this.active = 0;
70
+ /** Log envelopes waiting or in flight. */
71
+ this.logs = 0;
67
72
  this.limits = {};
68
73
  this.waiters = [];
69
74
  this.keepaliveBytes = 0;
@@ -73,11 +78,15 @@ export class Sender {
73
78
  const allowed = this.withoutLimited(envelope);
74
79
  if (!allowed)
75
80
  return;
76
- if (this.queue.length + this.active >= QUEUE_LIMIT) {
81
+ const log = isLogEnvelope(allowed);
82
+ if (this.queue.length + this.active >= QUEUE_LIMIT ||
83
+ (log && this.logs >= LOG_QUEUE_LIMIT)) {
77
84
  this.options.logger.warn("Dropped an envelope: the send queue is full.");
78
85
  this.lose(allowed, "queue_overflow");
79
86
  return;
80
87
  }
88
+ if (log)
89
+ this.logs++;
81
90
  this.queue.push(allowed);
82
91
  this.pump();
83
92
  }
@@ -126,7 +135,7 @@ export class Sender {
126
135
  if (!isRateLimited(this.limits, category, now))
127
136
  return true;
128
137
  if (category !== "internal")
129
- this.options.recordLoss("ratelimit_backoff", category);
138
+ this.options.recordLoss("ratelimit_backoff", category, itemQuantity(item));
130
139
  return false;
131
140
  });
132
141
  if (items.length < envelope[1].length) {
@@ -136,13 +145,20 @@ export class Sender {
136
145
  }
137
146
  pump() {
138
147
  while (this.active < CONCURRENCY && this.queue.length > 0) {
148
+ const waited = this.queue.shift();
149
+ const log = isLogEnvelope(waited);
139
150
  // A 429 may have come in while it waited.
140
- const next = this.withoutLimited(this.queue.shift());
141
- if (!next)
151
+ const next = this.withoutLimited(waited);
152
+ if (!next) {
153
+ if (log)
154
+ this.logs--;
142
155
  continue;
156
+ }
143
157
  this.active++;
144
158
  const done = () => {
145
159
  this.active--;
160
+ if (log)
161
+ this.logs--;
146
162
  this.pump();
147
163
  };
148
164
  void this.deliver(next).then(done, done);
@@ -207,7 +223,7 @@ export class Sender {
207
223
  const category = itemCategory(item);
208
224
  // A lost client report is not reported again.
209
225
  if (category !== "internal")
210
- this.options.recordLoss(reason, category);
226
+ this.options.recordLoss(reason, category, itemQuantity(item));
211
227
  }
212
228
  }
213
229
  }
@@ -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.2.0";
2
+ export declare const SDK_VERSION = "0.4.0";
3
3
  export declare const SDK_PACKAGE = "npm:@statoss/sdk";
@@ -1,4 +1,4 @@
1
1
  export const SDK_NAME = "statoss.javascript";
2
2
  // Kept equal to package.json by a test.
3
- export const SDK_VERSION = "0.2.0";
3
+ export const SDK_VERSION = "0.4.0";
4
4
  export const SDK_PACKAGE = "npm:@statoss/sdk";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@statoss/sdk",
3
- "version": "0.2.0",
4
- "description": "Error tracking for StatOSS: browsers, Node, Bun, Deno, Cloudflare Workers and Vercel Edge. Speaks Sentry's protocol.",
3
+ "version": "0.4.0",
4
+ "description": "Error tracking and logs for StatOSS: browsers, Node, Bun, Deno, Cloudflare Workers and Vercel Edge. Speaks Sentry's protocol.",
5
5
  "license": "MIT",
6
6
  "author": "StatOSS",
7
7
  "homepage": "https://statoss.com",
@@ -14,6 +14,7 @@
14
14
  "statoss",
15
15
  "errors",
16
16
  "error-tracking",
17
+ "logs",
17
18
  "sentry",
18
19
  "monitoring"
19
20
  ],
@@ -38,6 +39,7 @@
38
39
  "files": [
39
40
  "dist",
40
41
  "README.md",
42
+ "CHANGELOG.md",
41
43
  "LICENSE"
42
44
  ],
43
45
  "engines": {