midline-agent 0.3.0 → 0.4.1
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 +90 -5
- package/browser/package.json +8 -0
- package/dist/agent.d.ts +9 -1
- package/dist/agent.js +44 -68
- 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 +8 -7
- package/dist/config.js +12 -24
- 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/socket-transport.d.ts +58 -0
- package/dist/socket-transport.js +157 -0
- package/package.json +31 -4
- package/scripts/mark-esm.js +6 -0
- package/src/agent.ts +46 -73
- 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 +12 -23
- package/src/redact.ts +12 -6
- package/src/socket-transport.ts +188 -0
- package/test/agent.test.js +47 -51
- package/test/browser.test.js +328 -0
- package/test/console.test.js +11 -10
- package/test/helpers.js +54 -1
- package/test/middleware.test.js +5 -5
- package/test/proxy.test.js +4 -4
- package/tsconfig.esm.json +14 -0
- package/src/transport.ts +0 -125
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# midline-agent
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**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
|
|
|
@@ -283,8 +285,9 @@ TARGET_API_URL=http://localhost:4000
|
|
|
283
285
|
## Console output
|
|
284
286
|
|
|
285
287
|
Set `captureConsole: true` (or `MIDLINE_CAPTURE_CONSOLE=true`) and whatever the process prints
|
|
286
|
-
|
|
287
|
-
covers `console.log`, Nest's logger, pino, winston: anything written to stdout or stderr.
|
|
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.
|
|
288
291
|
|
|
289
292
|
```ts
|
|
290
293
|
MidlineAgent.init({ apiKey: process.env.MIDLINE_API_KEY, captureConsole: true });
|
|
@@ -299,7 +302,7 @@ const app = await NestFactory.create(AppModule); // startup lines are captured f
|
|
|
299
302
|
medium, and everything else is low.
|
|
300
303
|
- Up to 100 lines a second are sent, with room for a 1,000-line burst such as Nest mapping its routes
|
|
301
304
|
at startup. Lines past that still print but aren't sent.
|
|
302
|
-
- Printed lines don't count towards request totals, error rates or
|
|
305
|
+
- Printed lines don't count towards request totals, error rates, latency or Issues.
|
|
303
306
|
- It needs a Midline server that knows the `console` event type. An older server refuses the first
|
|
304
307
|
line; the agent then turns console capture off, says so once, and keeps sending requests and errors.
|
|
305
308
|
- The agent's own diagnostics are never captured.
|
|
@@ -372,6 +375,88 @@ app.get("/x", (req, res) => { logger.info({ requestId: getRequestContext(req)?.r
|
|
|
372
375
|
|
|
373
376
|
---
|
|
374
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
|
+
|
|
375
460
|
## Any other backend
|
|
376
461
|
|
|
377
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
|
@@ -80,7 +80,15 @@ export declare class MidlineAgent {
|
|
|
80
80
|
recordHttp(exchange: HttpExchange): void;
|
|
81
81
|
private recordConsole;
|
|
82
82
|
private takeConsoleToken;
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Sends whatever is buffered now, ignoring backoff.
|
|
85
|
+
*
|
|
86
|
+
* The transport's connection is always unref'd (telemetry alone must never
|
|
87
|
+
* keep an otherwise-idle process alive), so this relies on something else in
|
|
88
|
+
* the process keeping the event loop open until the await resolves — true for
|
|
89
|
+
* the overwhelmingly common case (a running server), not guaranteed for a
|
|
90
|
+
* bare script whose last statement is `await agent.flush()`.
|
|
91
|
+
*/
|
|
84
92
|
flush(): Promise<void>;
|
|
85
93
|
/** Flushes with a deadline, then closes. For graceful shutdown. */
|
|
86
94
|
shutdown(timeoutMs?: number): Promise<void>;
|
package/dist/agent.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.MidlineAgent = exports.SDK_VERSION = void 0;
|
|
|
4
4
|
const config_1 = require("./config");
|
|
5
5
|
const console_1 = require("./console");
|
|
6
6
|
const redact_1 = require("./redact");
|
|
7
|
-
const
|
|
7
|
+
const socket_transport_1 = require("./socket-transport");
|
|
8
8
|
exports.SDK_VERSION = (() => {
|
|
9
9
|
try {
|
|
10
10
|
return require("../package.json").version;
|
|
@@ -104,15 +104,19 @@ class MidlineAgent {
|
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
this.config = resolved;
|
|
107
|
-
this.transport = new
|
|
107
|
+
this.transport = new socket_transport_1.SocketTransport(resolved.socketOrigin, {
|
|
108
|
+
apiKey: resolved.apiKey,
|
|
108
109
|
ca: resolved.ca,
|
|
109
110
|
connectTimeoutMs: resolved.connectTimeoutMs,
|
|
110
111
|
timeoutMs: resolved.timeoutMs,
|
|
112
|
+
reconnectionDelayMs: resolved.flushIntervalMs,
|
|
113
|
+
reconnectionDelayMaxMs: resolved.maxRetryDelayMs,
|
|
114
|
+
idleDisconnectMs: Math.max(5 * resolved.flushIntervalMs, 30000),
|
|
111
115
|
userAgent: `midline-agent/${exports.SDK_VERSION} node/${process.version}`,
|
|
112
116
|
});
|
|
113
117
|
this.timer = setInterval(() => {
|
|
114
118
|
this.consoleCapture?.flushPending();
|
|
115
|
-
void this.drain(
|
|
119
|
+
void this.drain();
|
|
116
120
|
}, resolved.flushIntervalMs);
|
|
117
121
|
// Telemetry must never be the reason a process refuses to exit.
|
|
118
122
|
this.timer.unref?.();
|
|
@@ -212,7 +216,15 @@ class MidlineAgent {
|
|
|
212
216
|
this.consoleTokens -= 1;
|
|
213
217
|
return true;
|
|
214
218
|
}
|
|
215
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* Sends whatever is buffered now, ignoring backoff.
|
|
221
|
+
*
|
|
222
|
+
* The transport's connection is always unref'd (telemetry alone must never
|
|
223
|
+
* keep an otherwise-idle process alive), so this relies on something else in
|
|
224
|
+
* the process keeping the event loop open until the await resolves — true for
|
|
225
|
+
* the overwhelmingly common case (a running server), not guaranteed for a
|
|
226
|
+
* bare script whose last statement is `await agent.flush()`.
|
|
227
|
+
*/
|
|
216
228
|
async flush() {
|
|
217
229
|
if (!this.active)
|
|
218
230
|
return;
|
|
@@ -221,7 +233,7 @@ class MidlineAgent {
|
|
|
221
233
|
if (this.drainPromise) {
|
|
222
234
|
await this.drainPromise;
|
|
223
235
|
}
|
|
224
|
-
await this.drain(
|
|
236
|
+
await this.drain();
|
|
225
237
|
}
|
|
226
238
|
/** Flushes with a deadline, then closes. For graceful shutdown. */
|
|
227
239
|
async shutdown(timeoutMs = 5000) {
|
|
@@ -417,23 +429,23 @@ class MidlineAgent {
|
|
|
417
429
|
}
|
|
418
430
|
return batch;
|
|
419
431
|
}
|
|
420
|
-
drain(
|
|
432
|
+
drain() {
|
|
421
433
|
if (this.drainPromise)
|
|
422
434
|
return this.drainPromise;
|
|
423
435
|
if (!this.active || !this.queue.length || Date.now() < this.retryAfter) {
|
|
424
436
|
return Promise.resolve();
|
|
425
437
|
}
|
|
426
|
-
this.drainPromise = this.runDrain(
|
|
438
|
+
this.drainPromise = this.runDrain().finally(() => {
|
|
427
439
|
this.drainPromise = null;
|
|
428
440
|
});
|
|
429
441
|
return this.drainPromise;
|
|
430
442
|
}
|
|
431
|
-
async runDrain(
|
|
443
|
+
async runDrain() {
|
|
432
444
|
while (this.queue.length && this.active) {
|
|
433
445
|
// Taken off the queue while in flight, so overflow trimming can't remove
|
|
434
446
|
// events that are mid-send and then be confused about what was accepted.
|
|
435
447
|
const batch = this.takeBatch();
|
|
436
|
-
const outcome = await this.send(batch
|
|
448
|
+
const outcome = await this.send(batch);
|
|
437
449
|
if (outcome.kind === "ok") {
|
|
438
450
|
this.onSuccess();
|
|
439
451
|
continue;
|
|
@@ -454,7 +466,7 @@ class MidlineAgent {
|
|
|
454
466
|
}
|
|
455
467
|
else {
|
|
456
468
|
this.dropped += 1;
|
|
457
|
-
this.report("
|
|
469
|
+
this.report("too-large", "midline: the Midline server rejected an event as too large; dropped it.");
|
|
458
470
|
}
|
|
459
471
|
continue;
|
|
460
472
|
}
|
|
@@ -468,7 +480,7 @@ class MidlineAgent {
|
|
|
468
480
|
}
|
|
469
481
|
// One malformed event fails validation for the whole batch. Send them one
|
|
470
482
|
// at a time so it only costs that event.
|
|
471
|
-
const isolated = await this.sendIndividually(batch
|
|
483
|
+
const isolated = await this.sendIndividually(batch);
|
|
472
484
|
if (!isolated)
|
|
473
485
|
return;
|
|
474
486
|
continue;
|
|
@@ -479,16 +491,16 @@ class MidlineAgent {
|
|
|
479
491
|
}
|
|
480
492
|
}
|
|
481
493
|
/** Returns false if delivery should stop for this drain. */
|
|
482
|
-
async sendIndividually(batch
|
|
494
|
+
async sendIndividually(batch) {
|
|
483
495
|
for (let index = 0; index < batch.length; index++) {
|
|
484
496
|
if (this.consoleUnsupported && batch[index].wire.eventType === "console")
|
|
485
497
|
continue;
|
|
486
|
-
const outcome = await this.send([batch[index]]
|
|
498
|
+
const outcome = await this.send([batch[index]]);
|
|
487
499
|
if (outcome.kind === "ok") {
|
|
488
500
|
this.onSuccess();
|
|
489
501
|
}
|
|
490
502
|
else if (outcome.kind === "rejected" || outcome.kind === "tooLarge") {
|
|
491
|
-
const detail = outcome.kind === "rejected" ? outcome.detail : "
|
|
503
|
+
const detail = outcome.kind === "rejected" ? outcome.detail : "too_large";
|
|
492
504
|
if (outcome.kind === "rejected" && this.refusedConsole(batch[index], detail))
|
|
493
505
|
continue;
|
|
494
506
|
this.dropped += 1;
|
|
@@ -506,53 +518,40 @@ class MidlineAgent {
|
|
|
506
518
|
}
|
|
507
519
|
return true;
|
|
508
520
|
}
|
|
509
|
-
async send(batch
|
|
510
|
-
|
|
511
|
-
const body = JSON.stringify({ events: batch.map((item) => item.wire) });
|
|
512
|
-
let result;
|
|
521
|
+
async send(batch) {
|
|
522
|
+
let ack;
|
|
513
523
|
try {
|
|
514
|
-
|
|
524
|
+
ack = await this.transport.send(batch.map((item) => item.wire));
|
|
515
525
|
}
|
|
516
526
|
catch (err) {
|
|
517
527
|
this.report(`transport:${errorCode(err)}`, this.describe(err, batch.length));
|
|
518
528
|
return { kind: "retry" };
|
|
519
529
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const summary = parseJson(result.body);
|
|
524
|
-
const failed = typeof summary?.failed === "number" ? summary.failed : 0;
|
|
525
|
-
if (failed >= batch.length && batch.length > 0) {
|
|
530
|
+
if (ack.ok) {
|
|
531
|
+
// Servers that predate 401-on-bad-key ack ok and count the rejects.
|
|
532
|
+
if (ack.rejected >= batch.length && batch.length > 0) {
|
|
526
533
|
this.log("error", "midline: the Midline server did not accept this API key. Monitoring is now off — retrying wouldn't help. Check apiKey / MIDLINE_API_KEY.");
|
|
527
534
|
return { kind: "stop" };
|
|
528
535
|
}
|
|
529
|
-
if (
|
|
530
|
-
this.dropped +=
|
|
531
|
-
this.report("partial", `midline: the Midline server rejected ${
|
|
536
|
+
if (ack.rejected > 0) {
|
|
537
|
+
this.dropped += ack.rejected;
|
|
538
|
+
this.report("partial", `midline: the Midline server rejected ${ack.rejected} of ${batch.length} events.`);
|
|
532
539
|
}
|
|
533
540
|
return { kind: "ok" };
|
|
534
541
|
}
|
|
535
|
-
if (
|
|
536
|
-
this.log("error", `midline: the Midline server rejected the API key (
|
|
542
|
+
if (ack.code === "unauthorized" || ack.code === "forbidden") {
|
|
543
|
+
this.log("error", `midline: the Midline server rejected the API key (${ack.code}). ` +
|
|
537
544
|
"Monitoring is now off — retrying wouldn't help. Check apiKey / MIDLINE_API_KEY.");
|
|
538
545
|
return { kind: "stop" };
|
|
539
546
|
}
|
|
540
|
-
if (
|
|
547
|
+
if (ack.code === "too_large") {
|
|
541
548
|
return { kind: "tooLarge" };
|
|
542
549
|
}
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
return { kind: "retry", retryAfterMs };
|
|
550
|
+
if (ack.code === "rate_limited") {
|
|
551
|
+
this.report("rate_limited", `midline: the Midline server is rate-limiting this project; events are buffered and will be retried.`);
|
|
552
|
+
return { kind: "retry", retryAfterMs: ack.retryAfterMs };
|
|
547
553
|
}
|
|
548
|
-
|
|
549
|
-
// Never followed: that would hand the API key to wherever the redirect points.
|
|
550
|
-
const location = String(result.headers.location ?? "").slice(0, 200);
|
|
551
|
-
this.report(`http-${status}`, `midline: the Midline endpoint redirected (HTTP ${status}${location ? ` to ${location}` : ""}). ` +
|
|
552
|
-
"Redirects are not followed; set MIDLINE_ENDPOINT to the final URL.");
|
|
553
|
-
return { kind: "retry" };
|
|
554
|
-
}
|
|
555
|
-
return { kind: "rejected", detail: `HTTP ${status}${serverMessage(result.body)}` };
|
|
554
|
+
return { kind: "rejected", detail: `${ack.code}: ${ack.message}` };
|
|
556
555
|
}
|
|
557
556
|
/**
|
|
558
557
|
* True when a rejected event is a console line the server has no event type for,
|
|
@@ -582,7 +581,7 @@ class MidlineAgent {
|
|
|
582
581
|
}
|
|
583
582
|
describe(err, inFlight) {
|
|
584
583
|
const config = this.config;
|
|
585
|
-
const origin = config.
|
|
584
|
+
const origin = config.socketOrigin.origin;
|
|
586
585
|
const code = errorCode(err);
|
|
587
586
|
const buffered = ` ${this.queue.length + inFlight} event(s) buffered; your application is unaffected.`;
|
|
588
587
|
if (TLS_ERROR_REASONS[code] || code.startsWith("ERR_SSL") || code === "EPROTO") {
|
|
@@ -600,7 +599,7 @@ class MidlineAgent {
|
|
|
600
599
|
return `midline: could not connect to ${origin} within ${config.connectTimeoutMs}ms; retrying with backoff.${buffered}`;
|
|
601
600
|
}
|
|
602
601
|
if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
603
|
-
return `midline: cannot resolve ${config.
|
|
602
|
+
return `midline: cannot resolve ${config.socketOrigin.hostname} (${code}) — check MIDLINE_ENDPOINT and DNS; retrying with backoff.${buffered}`;
|
|
604
603
|
}
|
|
605
604
|
if (code === "ECONNREFUSED") {
|
|
606
605
|
return `midline: ${origin} refused the connection; retrying with backoff.${buffered}`;
|
|
@@ -745,26 +744,3 @@ function errorCode(err) {
|
|
|
745
744
|
const e = err;
|
|
746
745
|
return String(e?.code ?? e?.cause?.code ?? e?.errno ?? e?.name ?? "");
|
|
747
746
|
}
|
|
748
|
-
function parseJson(text) {
|
|
749
|
-
try {
|
|
750
|
-
return JSON.parse(text);
|
|
751
|
-
}
|
|
752
|
-
catch {
|
|
753
|
-
return undefined;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
function serverMessage(body) {
|
|
757
|
-
const message = parseJson(body)?.message;
|
|
758
|
-
const text = Array.isArray(message) ? message.join("; ") : typeof message === "string" ? message : "";
|
|
759
|
-
return text ? `: ${text.slice(0, 300)}` : "";
|
|
760
|
-
}
|
|
761
|
-
function parseRetryAfter(header) {
|
|
762
|
-
const value = Array.isArray(header) ? header[0] : header;
|
|
763
|
-
if (!value)
|
|
764
|
-
return undefined;
|
|
765
|
-
const seconds = Number(value);
|
|
766
|
-
if (Number.isFinite(seconds))
|
|
767
|
-
return Math.max(0, seconds * 1000);
|
|
768
|
-
const date = Date.parse(value);
|
|
769
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
|
|
770
|
-
}
|
|
@@ -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;
|