midline-agent 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.
Files changed (53) hide show
  1. package/README.md +115 -2
  2. package/browser/package.json +8 -0
  3. package/dist/agent.d.ts +14 -1
  4. package/dist/agent.js +106 -20
  5. package/dist/browser/client.d.ts +59 -0
  6. package/dist/browser/client.js +608 -0
  7. package/dist/browser/index.d.ts +34 -0
  8. package/dist/browser/index.js +65 -0
  9. package/dist/browser/instrument.d.ts +39 -0
  10. package/dist/browser/instrument.js +217 -0
  11. package/dist/browser/transport.d.ts +43 -0
  12. package/dist/browser/transport.js +168 -0
  13. package/dist/browser/types.d.ts +94 -0
  14. package/dist/browser/types.js +2 -0
  15. package/dist/browser/version.d.ts +2 -0
  16. package/dist/browser/version.js +5 -0
  17. package/dist/browser/vitals.d.ts +16 -0
  18. package/dist/browser/vitals.js +135 -0
  19. package/dist/cli.js +0 -0
  20. package/dist/config.d.ts +1 -0
  21. package/dist/config.js +1 -0
  22. package/dist/console.d.ts +46 -0
  23. package/dist/console.js +167 -0
  24. package/dist/esm/browser/client.js +601 -0
  25. package/dist/esm/browser/index.js +52 -0
  26. package/dist/esm/browser/instrument.js +210 -0
  27. package/dist/esm/browser/transport.js +164 -0
  28. package/dist/esm/browser/types.js +1 -0
  29. package/dist/esm/browser/version.js +2 -0
  30. package/dist/esm/browser/vitals.js +132 -0
  31. package/dist/esm/package.json +1 -0
  32. package/dist/esm/redact.js +224 -0
  33. package/dist/esm/types.js +1 -0
  34. package/dist/redact.d.ts +3 -0
  35. package/dist/redact.js +12 -6
  36. package/dist/types.d.ts +9 -2
  37. package/package.json +27 -4
  38. package/scripts/mark-esm.js +6 -0
  39. package/src/agent.ts +111 -15
  40. package/src/browser/client.ts +686 -0
  41. package/src/browser/index.ts +74 -0
  42. package/src/browser/instrument.ts +275 -0
  43. package/src/browser/transport.ts +184 -0
  44. package/src/browser/types.ts +105 -0
  45. package/src/browser/version.ts +2 -0
  46. package/src/browser/vitals.ts +149 -0
  47. package/src/config.ts +2 -0
  48. package/src/console.ts +182 -0
  49. package/src/redact.ts +12 -6
  50. package/src/types.ts +9 -2
  51. package/test/browser.test.js +328 -0
  52. package/test/console.test.js +182 -0
  53. package/tsconfig.esm.json +14 -0
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # midline-agent
2
2
 
3
3
  **The Node.js SDK for Midline** — request and error monitoring with security and threat detection.
4
+ It also ships **`midline-agent/browser`** for web apps: see [Browser apps](#browser-apps).
4
5
 
5
6
  Midline itself is not tied to Node. Every event lands on the same stream through a plain JSON
6
7
  endpoint, so a Django, Rails, Laravel, Spring, Go or .NET service reports exactly what an Express
@@ -102,7 +103,8 @@ bootstrap();
102
103
  ```
103
104
 
104
105
  Nest handles exceptions in its own filters, so they rarely reach Express error middleware. The
105
- request middleware still records every 4xx/5xx response.
106
+ request middleware still records every 4xx/5xx response, and Midline groups the 5xx responses into
107
+ Issues by method, route and status.
106
108
 
107
109
  ### Plain Node `http`
108
110
 
@@ -246,6 +248,7 @@ MidlineAgent.init({
246
248
  },
247
249
  redactFields?: string[], // added to the built-in list (maskFields still works)
248
250
  redactHeaders?: string[],
251
+ captureConsole?: boolean, // MIDLINE_CAPTURE_CONSOLE — also send what the process prints (off by default)
249
252
 
250
253
  // Delivery
251
254
  flushIntervalMs?: number, // default 1500
@@ -266,6 +269,7 @@ MidlineAgent.init({
266
269
  ```env
267
270
  MIDLINE_API_KEY=...
268
271
  MIDLINE_ENDPOINT=https://api.usemidline.com
272
+ # MIDLINE_CAPTURE_CONSOLE=true
269
273
 
270
274
  # proxy mode
271
275
  TARGET_API_URL=http://localhost:4000
@@ -278,6 +282,33 @@ TARGET_API_URL=http://localhost:4000
278
282
 
279
283
  ---
280
284
 
285
+ ## Console output
286
+
287
+ Set `captureConsole: true` (or `MIDLINE_CAPTURE_CONSOLE=true`) and whatever the process prints
288
+ is sent as `console` events, one per line, and shows up on the dashboard's **Terminal** page. That
289
+ covers `console.log`, Nest's logger, pino, winston: anything written to stdout or stderr. The Logs page
290
+ stays request traffic.
291
+
292
+ ```ts
293
+ MidlineAgent.init({ apiKey: process.env.MIDLINE_API_KEY, captureConsole: true });
294
+ const app = await NestFactory.create(AppModule); // startup lines are captured from here on
295
+ ```
296
+
297
+ - Initialise the agent before creating the app. Anything printed before `init()` isn't captured.
298
+ - Your terminal output doesn't change. The agent reads each write after it has happened.
299
+ - Colour codes are stripped, each line is capped at 4096 characters, and lines are redacted like any
300
+ other text.
301
+ - Severity comes from the line: `ERROR` or `FATAL` is high, `WARN` is medium, other stderr output is
302
+ medium, and everything else is low.
303
+ - Up to 100 lines a second are sent, with room for a 1,000-line burst such as Nest mapping its routes
304
+ at startup. Lines past that still print but aren't sent.
305
+ - Printed lines don't count towards request totals, error rates, latency or Issues.
306
+ - It needs a Midline server that knows the `console` event type. An older server refuses the first
307
+ line; the agent then turns console capture off, says so once, and keeps sending requests and errors.
308
+ - The agent's own diagnostics are never captured.
309
+
310
+ ---
311
+
281
312
  ## Sensitive data
282
313
 
283
314
  Redaction happens **in your process, before an event is queued**. What's masked never reaches a
@@ -293,7 +324,7 @@ defence.
293
324
  `privatekey`, `authorization`, `cookie`, `session`, `credential`, `csrf`, `xsrf`, `signature`,
294
325
  `creditcard`, `cardnumber`, `cvv`, `cvc`, `ssn`, `socialsecurity`; and the exact keys `auth`,
295
326
  `pwd`, `pin`, `otp`, `sid`, `jwt`, `bearer`.
296
- - **Values inside any text** — error messages, stack traces, routes: bearer/basic credentials,
327
+ - **Values inside any text** — error messages, stack traces, routes, console lines: bearer/basic credentials,
297
328
  JWTs, Midline keys (`ak_…`), Stripe and AWS key formats, PEM private keys, `user:pass@` in URLs,
298
329
  and `key=value` / `"key": value` pairs whose key is sensitive.
299
330
  - Query strings are never part of `route`.
@@ -344,6 +375,88 @@ app.get("/x", (req, res) => { logger.info({ requestId: getRequestContext(req)?.r
344
375
 
345
376
  ---
346
377
 
378
+ ## Browser apps
379
+
380
+ `midline-agent/browser` monitors a web app from the page: uncaught errors and unhandled rejections,
381
+ failed `fetch` and `XMLHttpRequest` calls, Web Vitals (LCP, INP, CLS, FCP, TTFB) and, if you ask,
382
+ console output. It has no dependencies, no Node code, and works under React, Vue, Angular, Svelte,
383
+ Next.js or no framework at all, because it instruments the page rather than a framework.
384
+
385
+ ```ts
386
+ import * as Midline from "midline-agent/browser";
387
+
388
+ Midline.init({
389
+ apiKey: import.meta.env.VITE_MIDLINE_BROWSER_KEY, // pk_… browser key
390
+ service: "checkout-web",
391
+ environment: import.meta.env.MODE,
392
+ release: import.meta.env.VITE_RELEASE,
393
+ });
394
+ ```
395
+
396
+ **Use a browser key, never a server key.** Anything in a bundle can be read by whoever loads the page,
397
+ so browser keys (`pk_…`) are public by design and the Midline server accepts them only from the
398
+ origins listed on the key (create one under **Project → API Keys → Browser**). Server keys (`ak_…`)
399
+ are refused whenever a browser sends them, and the SDK won't start with one.
400
+
401
+ Errors a framework catches itself never reach the window. Forward them:
402
+
403
+ ```tsx
404
+ // React error boundary
405
+ componentDidCatch(error: Error, info: ErrorInfo) {
406
+ Midline.captureException(error, { extra: { componentStack: info.componentStack } });
407
+ }
408
+
409
+ // Vue
410
+ app.config.errorHandler = (error, _instance, info) => Midline.captureException(error, { extra: { info } });
411
+ ```
412
+
413
+ **Linking a page to your backend.** Same-origin requests get a W3C `traceparent` header, which the
414
+ Node agent on the backend turns into `traceId`/`spanId` (see above), so the page's failed call and
415
+ the server request behind it share a trace id. For an API on another origin, list it in
416
+ `tracePropagationTargets` and allow the `traceparent` header in that API's CORS configuration, or
417
+ the browser will block the call's preflight.
418
+
419
+ | Option | Default | |
420
+ | --- | --- | --- |
421
+ | `apiKey` | — | Browser key (`pk_…`). |
422
+ | `endpoint` | `https://api.usemidline.com` | `http://` only for localhost. |
423
+ | `service`, `environment`, `release` | — | Stamped on every event. |
424
+ | `captureErrors` | `true` | Uncaught errors and unhandled rejections. |
425
+ | `captureRequests` | `"failed"` | `"failed"` (4xx, 5xx, network errors), `"all"`, or `false`. Every call is a breadcrumb either way. |
426
+ | `captureWebVitals` | `true` | Reported once, when the page is first hidden. |
427
+ | `captureConsole` | `false` | `true` for `error` and `warn`, or a list of levels. Lines appear on the Terminal page. Opt-in because wrapped console calls show the SDK as their source in devtools. |
428
+ | `tracePropagationTargets` | same origin | Strings match as URL prefixes (or path prefixes starting with `/`); RegExps match the full URL. |
429
+ | `ignoreErrors`, `ignoreUrls` | `[]` | Strings match as substrings. |
430
+ | `sampleRate` | `1` | Fraction of events sent. |
431
+ | `maxEventsPerMinute` | `120` | So an error in a render loop can't flood the project. |
432
+ | `beforeSend` | — | Return `null` to drop an event; edit `payload` and `metadata` freely. |
433
+ | `redactFields` | — | Extra field names to redact. |
434
+ | `enabled`, `debug` | `true`, `false` | |
435
+
436
+ Also: `captureMessage(message, severity)`, `setUser({ id })`, `setTag(key, value)`,
437
+ `addBreadcrumb(message)`, `flush()` and `close()`, which restores everything the SDK wrapped.
438
+
439
+ What it will not do: it never sends cookies, query strings of the page URL, or request and response
440
+ bodies; it redacts the same credential patterns as the Node agent before an event is queued; it
441
+ never throws into your code; and if Midline is down it keeps a bounded queue and backs off. Pending
442
+ events leave with `keepalive` when the tab is hidden. During server-side rendering, `init` does
443
+ nothing.
444
+
445
+ No bundler? Load the ES module build directly:
446
+
447
+ ```html
448
+ <script type="module">
449
+ import * as Midline from "https://cdn.jsdelivr.net/npm/midline-agent@0.4.0/dist/esm/browser/index.js";
450
+ Midline.init({ apiKey: "pk_…", service: "web" });
451
+ </script>
452
+ ```
453
+
454
+ The Midline server must be recent enough to accept browser keys (it answers the CORS preflight on
455
+ the ingest routes). Against an older server the SDK logs nothing unless `debug` is on and keeps
456
+ retrying with backoff.
457
+
458
+ ---
459
+
347
460
  ## Any other backend
348
461
 
349
462
  The agent does two things: it turns a request or an error into an event, and it POSTs that event to
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "midline-agent/browser",
3
+ "private": true,
4
+ "main": "../dist/browser/index.js",
5
+ "module": "../dist/esm/browser/index.js",
6
+ "types": "../dist/browser/index.d.ts",
7
+ "sideEffects": false
8
+ }
package/dist/agent.d.ts CHANGED
@@ -34,7 +34,7 @@ export interface ExchangeMessage {
34
34
  truncated?: boolean;
35
35
  }
36
36
  /**
37
- * Ships request/error events to the Midline server.
37
+ * Ships request, error and console events to the Midline server.
38
38
  *
39
39
  * The contract with the host application is that the agent is never allowed to
40
40
  * affect it: an unreachable, untrusted, slow or misconfigured Midline server costs a
@@ -65,6 +65,11 @@ export declare class MidlineAgent {
65
65
  private dropped;
66
66
  private maxRequestBytes;
67
67
  private batchLimit;
68
+ private consoleCapture;
69
+ private consoleTokens;
70
+ private consoleRefilledAt;
71
+ /** The server refused a console event: it predates them, so no more are sent. */
72
+ private consoleUnsupported;
68
73
  constructor(config?: MidlineConfig);
69
74
  /** False when the agent is off: no key, bad config, disabled, rejected key, or closed. */
70
75
  get active(): boolean;
@@ -73,6 +78,8 @@ export declare class MidlineAgent {
73
78
  addEvent(event: MidlineEvent): void;
74
79
  /** Records one HTTP exchange. Captured parts are redacted here, before queueing. */
75
80
  recordHttp(exchange: HttpExchange): void;
81
+ private recordConsole;
82
+ private takeConsoleToken;
76
83
  /** Sends whatever is buffered now, ignoring backoff, and keeps the process alive until done. */
77
84
  flush(): Promise<void>;
78
85
  /** Flushes with a deadline, then closes. For graceful shutdown. */
@@ -98,6 +105,12 @@ export declare class MidlineAgent {
98
105
  /** Returns false if delivery should stop for this drain. */
99
106
  private sendIndividually;
100
107
  private send;
108
+ /**
109
+ * True when a rejected event is a console line the server has no event type for,
110
+ * i.e. the server predates console capture. Capture switches itself off rather
111
+ * than paying a refused request for every line printed from then on.
112
+ */
113
+ private refusedConsole;
101
114
  /** One line per distinct fault, not one per failed event. */
102
115
  private report;
103
116
  private describe;
package/dist/agent.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidlineAgent = exports.SDK_VERSION = void 0;
4
4
  const config_1 = require("./config");
5
+ const console_1 = require("./console");
5
6
  const redact_1 = require("./redact");
6
7
  const transport_1 = require("./transport");
7
8
  exports.SDK_VERSION = (() => {
@@ -15,6 +16,10 @@ exports.SDK_VERSION = (() => {
15
16
  const MAX_REQUEST_BYTES = 512 * 1024;
16
17
  const MIN_REQUEST_BYTES = 16 * 1024;
17
18
  const MAX_QUEUE_BYTES = 16 * 1024 * 1024;
19
+ /** Console lines: a startup burst (Nest mapping every route) fits, a log storm can't crowd out requests. */
20
+ const CONSOLE_BURST_LINES = 1000;
21
+ const CONSOLE_LINES_PER_SECOND = 100;
22
+ const CONSOLE_SEVERITY = { info: "low", warn: "medium", error: "high" };
18
23
  /** Verification failures. Retrying is still right: they clear once the server is fixed. */
19
24
  const TLS_ERROR_REASONS = {
20
25
  DEPTH_ZERO_SELF_SIGNED_CERT: "presented a self-signed certificate",
@@ -31,7 +36,7 @@ const TLS_ERROR_REASONS = {
31
36
  HOSTNAME_MISMATCH: "presented a certificate issued for a different hostname",
32
37
  };
33
38
  /**
34
- * Ships request/error events to the Midline server.
39
+ * Ships request, error and console events to the Midline server.
35
40
  *
36
41
  * The contract with the host application is that the agent is never allowed to
37
42
  * affect it: an unreachable, untrusted, slow or misconfigured Midline server costs a
@@ -71,6 +76,11 @@ class MidlineAgent {
71
76
  this.dropped = 0;
72
77
  this.maxRequestBytes = MAX_REQUEST_BYTES;
73
78
  this.batchLimit = Number.MAX_SAFE_INTEGER;
79
+ this.consoleCapture = null;
80
+ this.consoleTokens = CONSOLE_BURST_LINES;
81
+ this.consoleRefilledAt = Date.now();
82
+ /** The server refused a console event: it predates them, so no more are sent. */
83
+ this.consoleUnsupported = false;
74
84
  this.onErrorHook = config.onError;
75
85
  this.debugLogs = config.debug ?? (0, config_1.envFlag)("MIDLINE_DEBUG") ?? false;
76
86
  this.redactor = new redact_1.Redactor([...(config.redactFields ?? []), ...(config.maskFields ?? [])], config.redactHeaders);
@@ -101,10 +111,15 @@ class MidlineAgent {
101
111
  userAgent: `midline-agent/${exports.SDK_VERSION} node/${process.version}`,
102
112
  });
103
113
  this.timer = setInterval(() => {
114
+ this.consoleCapture?.flushPending();
104
115
  void this.drain(false);
105
116
  }, resolved.flushIntervalMs);
106
117
  // Telemetry must never be the reason a process refuses to exit.
107
118
  this.timer.unref?.();
119
+ if (resolved.captureConsole) {
120
+ this.consoleCapture = new console_1.ConsoleCapture((line) => this.recordConsole(line));
121
+ this.consoleCapture.install();
122
+ }
108
123
  }
109
124
  /** False when the agent is off: no key, bad config, disabled, rejected key, or closed. */
110
125
  get active() {
@@ -165,10 +180,43 @@ class MidlineAgent {
165
180
  this.report("event-build", `midline: could not record a request (${err?.message}); skipped it.`);
166
181
  }
167
182
  }
183
+ recordConsole(line) {
184
+ if (!this.active || this.consoleUnsupported)
185
+ return;
186
+ if (!this.takeConsoleToken()) {
187
+ this.dropped += 1;
188
+ this.report("console-rate", `midline: console output is arriving faster than ${CONSOLE_LINES_PER_SECOND} lines/s; the extra lines still print but are not sent.`);
189
+ return;
190
+ }
191
+ try {
192
+ this.enqueue(this.toWire({
193
+ type: "console",
194
+ path: line.stream,
195
+ message: line.text,
196
+ timestamp: line.timestamp,
197
+ severity: CONSOLE_SEVERITY[line.level],
198
+ category: "application",
199
+ integration: "console",
200
+ }, false));
201
+ }
202
+ catch (err) {
203
+ this.report("event-build", `midline: could not record a console line (${err?.message}); skipped it.`);
204
+ }
205
+ }
206
+ takeConsoleToken() {
207
+ const now = Date.now();
208
+ this.consoleTokens = Math.min(CONSOLE_BURST_LINES, this.consoleTokens + ((now - this.consoleRefilledAt) / 1000) * CONSOLE_LINES_PER_SECOND);
209
+ this.consoleRefilledAt = now;
210
+ if (this.consoleTokens < 1)
211
+ return false;
212
+ this.consoleTokens -= 1;
213
+ return true;
214
+ }
168
215
  /** Sends whatever is buffered now, ignoring backoff, and keeps the process alive until done. */
169
216
  async flush() {
170
217
  if (!this.active)
171
218
  return;
219
+ this.consoleCapture?.flushPending(true);
172
220
  this.retryAfter = 0;
173
221
  if (this.drainPromise) {
174
222
  await this.drainPromise;
@@ -197,6 +245,8 @@ class MidlineAgent {
197
245
  clearInterval(this.timer);
198
246
  this.timer = null;
199
247
  }
248
+ this.consoleCapture?.uninstall();
249
+ this.consoleCapture = null;
200
250
  this.queue = [];
201
251
  this.queueBytes = 0;
202
252
  this.transport?.destroy();
@@ -241,13 +291,17 @@ class MidlineAgent {
241
291
  toWire(event, preRedacted) {
242
292
  const config = this.config;
243
293
  const type = event.type;
294
+ const isConsole = type === "console";
244
295
  const statusCode = Number.isInteger(event.statusCode) && event.statusCode >= 100 && event.statusCode <= 599
245
296
  ? event.statusCode
246
297
  : type === "error" ? 500 : undefined;
247
298
  const { severity, category } = classify(event, statusCode);
248
299
  const route = this.redactor.string(stripQuery(event.path) || "/", 2048);
249
300
  const payload = {};
250
- if (type === "error" || event.message) {
301
+ if (isConsole) {
302
+ payload.message = this.redactor.string(event.message ?? "", console_1.MAX_LINE_CHARS);
303
+ }
304
+ else if (type === "error" || event.message) {
251
305
  payload.error = event.message ? this.redactor.string(event.message, 1024) : type === "error" ? "Unknown error" : undefined;
252
306
  }
253
307
  if (event.stack)
@@ -290,9 +344,10 @@ class MidlineAgent {
290
344
  apiKey: config.apiKey,
291
345
  eventType: type,
292
346
  route,
293
- method: (event.method || "GET").toUpperCase().slice(0, 16),
347
+ // A printed line has no method or timing; defaults here would make it look like a request.
348
+ method: isConsole ? undefined : (event.method || "GET").toUpperCase().slice(0, 16),
294
349
  statusCode,
295
- responseTime: Math.min(Math.max(0, Math.round(Number(event.duration) || 0)), 86400000),
350
+ responseTime: isConsole ? undefined : Math.min(Math.max(0, Math.round(Number(event.duration) || 0)), 86400000),
296
351
  timestamp: validTimestamp(event.timestamp),
297
352
  // Clamped to the server's validation limits: one over-long field would
298
353
  // otherwise get every event rejected.
@@ -405,6 +460,8 @@ class MidlineAgent {
405
460
  }
406
461
  if (outcome.kind === "rejected") {
407
462
  if (batch.length === 1) {
463
+ if (this.refusedConsole(batch[0], outcome.detail))
464
+ continue;
408
465
  this.dropped += 1;
409
466
  this.report(`rejected:${outcome.detail}`, `midline: the Midline server rejected an event (${outcome.detail}); dropped it.`);
410
467
  continue;
@@ -424,13 +481,17 @@ class MidlineAgent {
424
481
  /** Returns false if delivery should stop for this drain. */
425
482
  async sendIndividually(batch, keepProcessAlive) {
426
483
  for (let index = 0; index < batch.length; index++) {
484
+ if (this.consoleUnsupported && batch[index].wire.eventType === "console")
485
+ continue;
427
486
  const outcome = await this.send([batch[index]], keepProcessAlive);
428
487
  if (outcome.kind === "ok") {
429
488
  this.onSuccess();
430
489
  }
431
490
  else if (outcome.kind === "rejected" || outcome.kind === "tooLarge") {
432
- this.dropped += 1;
433
491
  const detail = outcome.kind === "rejected" ? outcome.detail : "HTTP 413";
492
+ if (outcome.kind === "rejected" && this.refusedConsole(batch[index], detail))
493
+ continue;
494
+ this.dropped += 1;
434
495
  this.report(`rejected:${detail}`, `midline: the Midline server rejected an event (${detail}); dropped it.`);
435
496
  }
436
497
  else if (outcome.kind === "stop") {
@@ -493,6 +554,24 @@ class MidlineAgent {
493
554
  }
494
555
  return { kind: "rejected", detail: `HTTP ${status}${serverMessage(result.body)}` };
495
556
  }
557
+ /**
558
+ * True when a rejected event is a console line the server has no event type for,
559
+ * i.e. the server predates console capture. Capture switches itself off rather
560
+ * than paying a refused request for every line printed from then on.
561
+ */
562
+ refusedConsole(item, detail) {
563
+ if (item.wire.eventType !== "console" || !detail.includes("eventType"))
564
+ return false;
565
+ if (!this.consoleUnsupported) {
566
+ this.consoleUnsupported = true;
567
+ this.consoleCapture?.uninstall();
568
+ this.consoleCapture = null;
569
+ this.queue = this.queue.filter((queued) => queued.wire.eventType !== "console");
570
+ this.queueBytes = this.queue.reduce((sum, queued) => sum + queued.bytes, 0);
571
+ this.log("warn", "midline: this Midline server does not accept console events yet (it needs updating), so console capture is off. Request and error monitoring carry on.");
572
+ }
573
+ return true;
574
+ }
496
575
  /** One line per distinct fault, not one per failed event. */
497
576
  report(signature, message) {
498
577
  if (this.lastNotice === signature && !this.debugLogs) {
@@ -558,22 +637,26 @@ class MidlineAgent {
558
637
  this.close();
559
638
  }
560
639
  log(level, message) {
561
- if (this.onErrorHook) {
562
- try {
563
- this.onErrorHook(message);
564
- }
565
- catch {
566
- // A broken logging hook must not become the host application's problem.
640
+ // The agent's own diagnostics are never the application's console output,
641
+ // including when a hook hands them to a logger that prints them.
642
+ (0, console_1.withoutConsoleCapture)(() => {
643
+ if (this.onErrorHook) {
644
+ try {
645
+ this.onErrorHook(message);
646
+ }
647
+ catch {
648
+ // A broken logging hook must not become the host application's problem.
649
+ }
650
+ if (!this.debugLogs)
651
+ return;
567
652
  }
568
- if (!this.debugLogs)
569
- return;
570
- }
571
- if (level === "error")
572
- console.error(message);
573
- else if (level === "warn")
574
- console.warn(message);
575
- else
576
- console.info(message);
653
+ if (level === "error")
654
+ console.error(message);
655
+ else if (level === "warn")
656
+ console.warn(message);
657
+ else
658
+ console.info(message);
659
+ });
577
660
  }
578
661
  }
579
662
  exports.MidlineAgent = MidlineAgent;
@@ -595,6 +678,9 @@ function classify(event, statusCode) {
595
678
  else if (event.type === "custom") {
596
679
  category = "business";
597
680
  }
681
+ else if (event.type === "console") {
682
+ category = "application";
683
+ }
598
684
  else if (statusCode && statusCode >= 500) {
599
685
  severity = "high";
600
686
  category = "application";
@@ -0,0 +1,59 @@
1
+ import type { EventSeverity } from "../types.js";
2
+ import type { CaptureContext, MidlineBrowserConfig, MidlineUser } from "./types.js";
3
+ export declare class BrowserConfigError extends Error {
4
+ }
5
+ /** Accepts a base URL or the ingest URL, and returns the batch URL. */
6
+ export declare function resolveBatchUrl(endpoint?: string): string;
7
+ /**
8
+ * One installed SDK instance. Everything it does to the page — wrapped fetch,
9
+ * XHR, console and history, window listeners — is undone by `close()`.
10
+ */
11
+ export declare class BrowserClient {
12
+ private readonly win;
13
+ private readonly config;
14
+ private readonly redactor;
15
+ private readonly transport;
16
+ private readonly teardowns;
17
+ private readonly breadcrumbs;
18
+ private readonly recentErrors;
19
+ private readonly sessionId;
20
+ private readonly originalConsole;
21
+ private traceId;
22
+ private user;
23
+ private tags;
24
+ private tokens;
25
+ private lastRefill;
26
+ private active;
27
+ private inConsoleHook;
28
+ private rateLimitNoted;
29
+ private constructor();
30
+ /** Returns undefined, after one console warning, when the SDK can't run. It never throws into the app. */
31
+ static create(config: MidlineBrowserConfig): BrowserClient | undefined;
32
+ private install;
33
+ captureException(error: unknown, context?: CaptureContext): void;
34
+ captureMessage(message: string, severity?: EventSeverity, context?: CaptureContext): void;
35
+ setUser(user: MidlineUser | null): void;
36
+ setTag(key: string, value: string): void;
37
+ addBreadcrumb(message: string, type?: string): void;
38
+ flush(): Promise<void>;
39
+ close(): Promise<void>;
40
+ private handleError;
41
+ private handleRequest;
42
+ private handleConsole;
43
+ private handleNavigation;
44
+ private handleVitals;
45
+ private propagation;
46
+ private base;
47
+ private emit;
48
+ private takeToken;
49
+ private breadcrumb;
50
+ private contextPayload;
51
+ private applyTags;
52
+ /** The page path, plus a `#/…` hash route when the app routes by hash. Never the query string. */
53
+ private currentRoute;
54
+ private pageUrl;
55
+ private guard;
56
+ private debug;
57
+ }
58
+ /** W3C ids: lowercase hex, never all zeros. */
59
+ export declare function randomHex(bytes: number): string;