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.
- package/README.md +115 -2
- package/browser/package.json +8 -0
- package/dist/agent.d.ts +14 -1
- package/dist/agent.js +106 -20
- package/dist/browser/client.d.ts +59 -0
- package/dist/browser/client.js +608 -0
- package/dist/browser/index.d.ts +34 -0
- package/dist/browser/index.js +65 -0
- package/dist/browser/instrument.d.ts +39 -0
- package/dist/browser/instrument.js +217 -0
- package/dist/browser/transport.d.ts +43 -0
- package/dist/browser/transport.js +168 -0
- package/dist/browser/types.d.ts +94 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/version.d.ts +2 -0
- package/dist/browser/version.js +5 -0
- package/dist/browser/vitals.d.ts +16 -0
- package/dist/browser/vitals.js +135 -0
- package/dist/cli.js +0 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/console.d.ts +46 -0
- package/dist/console.js +167 -0
- package/dist/esm/browser/client.js +601 -0
- package/dist/esm/browser/index.js +52 -0
- package/dist/esm/browser/instrument.js +210 -0
- package/dist/esm/browser/transport.js +164 -0
- package/dist/esm/browser/types.js +1 -0
- package/dist/esm/browser/version.js +2 -0
- package/dist/esm/browser/vitals.js +132 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/redact.js +224 -0
- package/dist/esm/types.js +1 -0
- package/dist/redact.d.ts +3 -0
- package/dist/redact.js +12 -6
- package/dist/types.d.ts +9 -2
- package/package.json +27 -4
- package/scripts/mark-esm.js +6 -0
- package/src/agent.ts +111 -15
- package/src/browser/client.ts +686 -0
- package/src/browser/index.ts +74 -0
- package/src/browser/instrument.ts +275 -0
- package/src/browser/transport.ts +184 -0
- package/src/browser/types.ts +105 -0
- package/src/browser/version.ts +2 -0
- package/src/browser/vitals.ts +149 -0
- package/src/config.ts +2 -0
- package/src/console.ts +182 -0
- package/src/redact.ts +12 -6
- package/src/types.ts +9 -2
- package/test/browser.test.js +328 -0
- package/test/console.test.js +182 -0
- 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
|
package/dist/agent.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface ExchangeMessage {
|
|
|
34
34
|
truncated?: boolean;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Ships request
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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 (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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;
|