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/src/agent.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ConfigError, ResolvedCapture, ResolvedConfig, envFlag, resolveConfig }
|
|
|
2
2
|
import { ConsoleCapture, ConsoleLevel, ConsoleLine, MAX_LINE_CHARS, withoutConsoleCapture } from "./console";
|
|
3
3
|
import type { RequestContext } from "./context";
|
|
4
4
|
import { Redactor } from "./redact";
|
|
5
|
-
import {
|
|
5
|
+
import { IngestAck, SocketTransport } from "./socket-transport";
|
|
6
6
|
import { CapturedMessage, EventCategory, EventSeverity, MidlineConfig, MidlineEvent } from "./types";
|
|
7
7
|
|
|
8
8
|
export const SDK_VERSION: string = (() => {
|
|
@@ -117,7 +117,7 @@ export class MidlineAgent {
|
|
|
117
117
|
|
|
118
118
|
private readonly config: ResolvedConfig | null = null;
|
|
119
119
|
private readonly redactor: Redactor;
|
|
120
|
-
private readonly transport:
|
|
120
|
+
private readonly transport: SocketTransport | null = null;
|
|
121
121
|
private readonly onErrorHook?: (message: string) => void;
|
|
122
122
|
private readonly debugLogs: boolean;
|
|
123
123
|
|
|
@@ -165,16 +165,20 @@ export class MidlineAgent {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
this.config = resolved;
|
|
168
|
-
this.transport = new
|
|
168
|
+
this.transport = new SocketTransport(resolved.socketOrigin, {
|
|
169
|
+
apiKey: resolved.apiKey,
|
|
169
170
|
ca: resolved.ca,
|
|
170
171
|
connectTimeoutMs: resolved.connectTimeoutMs,
|
|
171
172
|
timeoutMs: resolved.timeoutMs,
|
|
173
|
+
reconnectionDelayMs: resolved.flushIntervalMs,
|
|
174
|
+
reconnectionDelayMaxMs: resolved.maxRetryDelayMs,
|
|
175
|
+
idleDisconnectMs: Math.max(5 * resolved.flushIntervalMs, 30_000),
|
|
172
176
|
userAgent: `midline-agent/${SDK_VERSION} node/${process.version}`,
|
|
173
177
|
});
|
|
174
178
|
|
|
175
179
|
this.timer = setInterval(() => {
|
|
176
180
|
this.consoleCapture?.flushPending();
|
|
177
|
-
void this.drain(
|
|
181
|
+
void this.drain();
|
|
178
182
|
}, resolved.flushIntervalMs);
|
|
179
183
|
// Telemetry must never be the reason a process refuses to exit.
|
|
180
184
|
this.timer.unref?.();
|
|
@@ -288,7 +292,15 @@ export class MidlineAgent {
|
|
|
288
292
|
return true;
|
|
289
293
|
}
|
|
290
294
|
|
|
291
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* Sends whatever is buffered now, ignoring backoff.
|
|
297
|
+
*
|
|
298
|
+
* The transport's connection is always unref'd (telemetry alone must never
|
|
299
|
+
* keep an otherwise-idle process alive), so this relies on something else in
|
|
300
|
+
* the process keeping the event loop open until the await resolves — true for
|
|
301
|
+
* the overwhelmingly common case (a running server), not guaranteed for a
|
|
302
|
+
* bare script whose last statement is `await agent.flush()`.
|
|
303
|
+
*/
|
|
292
304
|
async flush(): Promise<void> {
|
|
293
305
|
if (!this.active) return;
|
|
294
306
|
this.consoleCapture?.flushPending(true);
|
|
@@ -296,7 +308,7 @@ export class MidlineAgent {
|
|
|
296
308
|
if (this.drainPromise) {
|
|
297
309
|
await this.drainPromise;
|
|
298
310
|
}
|
|
299
|
-
await this.drain(
|
|
311
|
+
await this.drain();
|
|
300
312
|
}
|
|
301
313
|
|
|
302
314
|
/** Flushes with a deadline, then closes. For graceful shutdown. */
|
|
@@ -493,23 +505,23 @@ export class MidlineAgent {
|
|
|
493
505
|
return batch;
|
|
494
506
|
}
|
|
495
507
|
|
|
496
|
-
private drain(
|
|
508
|
+
private drain(): Promise<void> {
|
|
497
509
|
if (this.drainPromise) return this.drainPromise;
|
|
498
510
|
if (!this.active || !this.queue.length || Date.now() < this.retryAfter) {
|
|
499
511
|
return Promise.resolve();
|
|
500
512
|
}
|
|
501
|
-
this.drainPromise = this.runDrain(
|
|
513
|
+
this.drainPromise = this.runDrain().finally(() => {
|
|
502
514
|
this.drainPromise = null;
|
|
503
515
|
});
|
|
504
516
|
return this.drainPromise;
|
|
505
517
|
}
|
|
506
518
|
|
|
507
|
-
private async runDrain(
|
|
519
|
+
private async runDrain(): Promise<void> {
|
|
508
520
|
while (this.queue.length && this.active) {
|
|
509
521
|
// Taken off the queue while in flight, so overflow trimming can't remove
|
|
510
522
|
// events that are mid-send and then be confused about what was accepted.
|
|
511
523
|
const batch = this.takeBatch();
|
|
512
|
-
const outcome = await this.send(batch
|
|
524
|
+
const outcome = await this.send(batch);
|
|
513
525
|
|
|
514
526
|
if (outcome.kind === "ok") {
|
|
515
527
|
this.onSuccess();
|
|
@@ -530,7 +542,7 @@ export class MidlineAgent {
|
|
|
530
542
|
this.requeue(batch);
|
|
531
543
|
} else {
|
|
532
544
|
this.dropped += 1;
|
|
533
|
-
this.report("
|
|
545
|
+
this.report("too-large", "midline: the Midline server rejected an event as too large; dropped it.");
|
|
534
546
|
}
|
|
535
547
|
continue;
|
|
536
548
|
}
|
|
@@ -543,7 +555,7 @@ export class MidlineAgent {
|
|
|
543
555
|
}
|
|
544
556
|
// One malformed event fails validation for the whole batch. Send them one
|
|
545
557
|
// at a time so it only costs that event.
|
|
546
|
-
const isolated = await this.sendIndividually(batch
|
|
558
|
+
const isolated = await this.sendIndividually(batch);
|
|
547
559
|
if (!isolated) return;
|
|
548
560
|
continue;
|
|
549
561
|
}
|
|
@@ -555,14 +567,14 @@ export class MidlineAgent {
|
|
|
555
567
|
}
|
|
556
568
|
|
|
557
569
|
/** Returns false if delivery should stop for this drain. */
|
|
558
|
-
private async sendIndividually(batch: QueuedEvent[]
|
|
570
|
+
private async sendIndividually(batch: QueuedEvent[]): Promise<boolean> {
|
|
559
571
|
for (let index = 0; index < batch.length; index++) {
|
|
560
572
|
if (this.consoleUnsupported && batch[index].wire.eventType === "console") continue;
|
|
561
|
-
const outcome = await this.send([batch[index]]
|
|
573
|
+
const outcome = await this.send([batch[index]]);
|
|
562
574
|
if (outcome.kind === "ok") {
|
|
563
575
|
this.onSuccess();
|
|
564
576
|
} else if (outcome.kind === "rejected" || outcome.kind === "tooLarge") {
|
|
565
|
-
const detail = outcome.kind === "rejected" ? outcome.detail : "
|
|
577
|
+
const detail = outcome.kind === "rejected" ? outcome.detail : "too_large";
|
|
566
578
|
if (outcome.kind === "rejected" && this.refusedConsole(batch[index], detail)) continue;
|
|
567
579
|
this.dropped += 1;
|
|
568
580
|
this.report(`rejected:${detail}`, `midline: the Midline server rejected an event (${detail}); dropped it.`);
|
|
@@ -578,60 +590,44 @@ export class MidlineAgent {
|
|
|
578
590
|
return true;
|
|
579
591
|
}
|
|
580
592
|
|
|
581
|
-
private async send(batch: QueuedEvent[]
|
|
582
|
-
|
|
583
|
-
const body = JSON.stringify({ events: batch.map((item) => item.wire) });
|
|
584
|
-
|
|
585
|
-
let result;
|
|
593
|
+
private async send(batch: QueuedEvent[]): Promise<Outcome> {
|
|
594
|
+
let ack: IngestAck;
|
|
586
595
|
try {
|
|
587
|
-
|
|
596
|
+
ack = await this.transport!.send(batch.map((item) => item.wire));
|
|
588
597
|
} catch (err) {
|
|
589
598
|
this.report(`transport:${errorCode(err)}`, this.describe(err, batch.length));
|
|
590
599
|
return { kind: "retry" };
|
|
591
600
|
}
|
|
592
601
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const summary = parseJson(result.body);
|
|
597
|
-
const failed = typeof summary?.failed === "number" ? summary.failed : 0;
|
|
598
|
-
if (failed >= batch.length && batch.length > 0) {
|
|
602
|
+
if (ack.ok) {
|
|
603
|
+
// Servers that predate 401-on-bad-key ack ok and count the rejects.
|
|
604
|
+
if (ack.rejected >= batch.length && batch.length > 0) {
|
|
599
605
|
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.");
|
|
600
606
|
return { kind: "stop" };
|
|
601
607
|
}
|
|
602
|
-
if (
|
|
603
|
-
this.dropped +=
|
|
604
|
-
this.report("partial", `midline: the Midline server rejected ${
|
|
608
|
+
if (ack.rejected > 0) {
|
|
609
|
+
this.dropped += ack.rejected;
|
|
610
|
+
this.report("partial", `midline: the Midline server rejected ${ack.rejected} of ${batch.length} events.`);
|
|
605
611
|
}
|
|
606
612
|
return { kind: "ok" };
|
|
607
613
|
}
|
|
608
|
-
|
|
614
|
+
|
|
615
|
+
if (ack.code === "unauthorized" || ack.code === "forbidden") {
|
|
609
616
|
this.log(
|
|
610
617
|
"error",
|
|
611
|
-
`midline: the Midline server rejected the API key (
|
|
618
|
+
`midline: the Midline server rejected the API key (${ack.code}). ` +
|
|
612
619
|
"Monitoring is now off — retrying wouldn't help. Check apiKey / MIDLINE_API_KEY.",
|
|
613
620
|
);
|
|
614
621
|
return { kind: "stop" };
|
|
615
622
|
}
|
|
616
|
-
if (
|
|
623
|
+
if (ack.code === "too_large") {
|
|
617
624
|
return { kind: "tooLarge" };
|
|
618
625
|
}
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return { kind: "retry", retryAfterMs };
|
|
623
|
-
}
|
|
624
|
-
if (status >= 300 && status < 400) {
|
|
625
|
-
// Never followed: that would hand the API key to wherever the redirect points.
|
|
626
|
-
const location = String(result.headers.location ?? "").slice(0, 200);
|
|
627
|
-
this.report(
|
|
628
|
-
`http-${status}`,
|
|
629
|
-
`midline: the Midline endpoint redirected (HTTP ${status}${location ? ` to ${location}` : ""}). ` +
|
|
630
|
-
"Redirects are not followed; set MIDLINE_ENDPOINT to the final URL.",
|
|
631
|
-
);
|
|
632
|
-
return { kind: "retry" };
|
|
626
|
+
if (ack.code === "rate_limited") {
|
|
627
|
+
this.report("rate_limited", `midline: the Midline server is rate-limiting this project; events are buffered and will be retried.`);
|
|
628
|
+
return { kind: "retry", retryAfterMs: ack.retryAfterMs };
|
|
633
629
|
}
|
|
634
|
-
return { kind: "rejected", detail:
|
|
630
|
+
return { kind: "rejected", detail: `${ack.code}: ${ack.message}` };
|
|
635
631
|
}
|
|
636
632
|
|
|
637
633
|
/**
|
|
@@ -666,7 +662,7 @@ export class MidlineAgent {
|
|
|
666
662
|
|
|
667
663
|
private describe(err: unknown, inFlight: number): string {
|
|
668
664
|
const config = this.config!;
|
|
669
|
-
const origin = config.
|
|
665
|
+
const origin = config.socketOrigin.origin;
|
|
670
666
|
const code = errorCode(err);
|
|
671
667
|
const buffered = ` ${this.queue.length + inFlight} event(s) buffered; your application is unaffected.`;
|
|
672
668
|
|
|
@@ -687,7 +683,7 @@ export class MidlineAgent {
|
|
|
687
683
|
return `midline: could not connect to ${origin} within ${config.connectTimeoutMs}ms; retrying with backoff.${buffered}`;
|
|
688
684
|
}
|
|
689
685
|
if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
690
|
-
return `midline: cannot resolve ${config.
|
|
686
|
+
return `midline: cannot resolve ${config.socketOrigin.hostname} (${code}) — check MIDLINE_ENDPOINT and DNS; retrying with backoff.${buffered}`;
|
|
691
687
|
}
|
|
692
688
|
if (code === "ECONNREFUSED") {
|
|
693
689
|
return `midline: ${origin} refused the connection; retrying with backoff.${buffered}`;
|
|
@@ -828,26 +824,3 @@ function errorCode(err: unknown): string {
|
|
|
828
824
|
const e = err as any;
|
|
829
825
|
return String(e?.code ?? e?.cause?.code ?? e?.errno ?? e?.name ?? "");
|
|
830
826
|
}
|
|
831
|
-
|
|
832
|
-
function parseJson(text: string): any {
|
|
833
|
-
try {
|
|
834
|
-
return JSON.parse(text);
|
|
835
|
-
} catch {
|
|
836
|
-
return undefined;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function serverMessage(body: string): string {
|
|
841
|
-
const message = parseJson(body)?.message;
|
|
842
|
-
const text = Array.isArray(message) ? message.join("; ") : typeof message === "string" ? message : "";
|
|
843
|
-
return text ? `: ${text.slice(0, 300)}` : "";
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function parseRetryAfter(header: string | string[] | undefined): number | undefined {
|
|
847
|
-
const value = Array.isArray(header) ? header[0] : header;
|
|
848
|
-
if (!value) return undefined;
|
|
849
|
-
const seconds = Number(value);
|
|
850
|
-
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
|
|
851
|
-
const date = Date.parse(value);
|
|
852
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
|
|
853
|
-
}
|