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
@@ -0,0 +1,74 @@
1
+ /**
2
+ * midline-agent/browser — error, network and Web Vitals monitoring for web apps.
3
+ *
4
+ * import * as Midline from "midline-agent/browser";
5
+ * Midline.init({ apiKey: "pk_…", service: "checkout-web" });
6
+ *
7
+ * Framework-agnostic: it instruments the page (window errors, fetch, XHR,
8
+ * history), not React, Vue or Angular, so it works under all of them. Call
9
+ * `captureException` from a framework's own error hook for errors it swallows.
10
+ */
11
+ import { BrowserClient } from "./client.js";
12
+ import type { CaptureContext, MidlineBrowserConfig, MidlineUser } from "./types.js";
13
+ import type { EventSeverity } from "../types.js";
14
+
15
+ let client: BrowserClient | undefined;
16
+
17
+ /**
18
+ * Starts monitoring. Safe to call during server-side rendering (it does nothing
19
+ * without a window) and safe to call twice (the first instance is closed).
20
+ * A bad config logs one console warning and leaves the page untouched.
21
+ */
22
+ export function init(config: MidlineBrowserConfig): void {
23
+ if (client) void client.close();
24
+ client = BrowserClient.create(config);
25
+ }
26
+
27
+ /** Reports an error your code caught, e.g. from a React error boundary. */
28
+ export function captureException(error: unknown, context?: CaptureContext): void {
29
+ client?.captureException(error, context);
30
+ }
31
+
32
+ export function captureMessage(message: string, severity?: EventSeverity, context?: CaptureContext): void {
33
+ client?.captureMessage(message, severity, context);
34
+ }
35
+
36
+ /** Attaches a user id to later events. Pass null on sign-out. */
37
+ export function setUser(user: MidlineUser | null): void {
38
+ client?.setUser(user);
39
+ }
40
+
41
+ export function setTag(key: string, value: string): void {
42
+ client?.setTag(key, value);
43
+ }
44
+
45
+ export function addBreadcrumb(message: string, type?: string): void {
46
+ client?.addBreadcrumb(message, type);
47
+ }
48
+
49
+ /** Sends anything queued now. */
50
+ export function flush(): Promise<void> {
51
+ return client ? client.flush() : Promise.resolve();
52
+ }
53
+
54
+ /** Restores everything the SDK wrapped and sends what's queued. */
55
+ export async function close(): Promise<void> {
56
+ const closing = client;
57
+ client = undefined;
58
+ await closing?.close();
59
+ }
60
+
61
+ export const Midline = { init, captureException, captureMessage, setUser, setTag, addBreadcrumb, flush, close };
62
+
63
+ export { resolveBatchUrl } from "./client.js";
64
+ export { BROWSER_SDK_VERSION } from "./version.js";
65
+ export type {
66
+ BrowserEvent,
67
+ CaptureContext,
68
+ ConsoleLevel,
69
+ MidlineBrowserConfig,
70
+ MidlineUser,
71
+ RequestCapture,
72
+ } from "./types.js";
73
+ export type { VitalName, VitalRating, VitalsReport } from "./vitals.js";
74
+ export type { EventSeverity } from "../types.js";
@@ -0,0 +1,275 @@
1
+ import type { ConsoleLevel } from "./types.js";
2
+
3
+ /** A finished fetch or XHR call. `status` is absent when no response arrived. */
4
+ export interface RequestRecord {
5
+ kind: "fetch" | "xhr";
6
+ method: string;
7
+ url: URL;
8
+ status?: number;
9
+ durationMs: number;
10
+ aborted?: boolean;
11
+ spanId?: string;
12
+ }
13
+
14
+ export interface Propagation {
15
+ headers: Record<string, string>;
16
+ spanId: string;
17
+ }
18
+
19
+ export interface InstrumentHooks {
20
+ onError(error: unknown, mechanism: "onerror" | "unhandledrejection", location?: ErrorLocation): void;
21
+ onRequest(record: RequestRecord): void;
22
+ onConsole(level: ConsoleLevel, args: unknown[]): void;
23
+ onNavigation(from: string, to: string): void;
24
+ /** Headers to add to a request, or undefined to leave it untouched. */
25
+ propagation(url: URL): Propagation | undefined;
26
+ /** Midline's own delivery requests, which are never recorded. */
27
+ isOwnRequest(url: URL): boolean;
28
+ }
29
+
30
+ export interface ErrorLocation {
31
+ message?: string;
32
+ filename?: string;
33
+ lineno?: number;
34
+ colno?: number;
35
+ }
36
+
37
+ type Teardown = () => void;
38
+
39
+ const noop: Teardown = () => {};
40
+
41
+ /** Instrumentation runs inside the page's own calls; a bug here must never become the page's bug. */
42
+ function safely(fn: () => void): void {
43
+ try {
44
+ fn();
45
+ } catch {
46
+ // swallowed on purpose
47
+ }
48
+ }
49
+
50
+ const now = (): number =>
51
+ typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
52
+
53
+ export function instrumentErrors(win: Window, hooks: InstrumentHooks): Teardown {
54
+ // Registered in the bubble phase: resource load failures (img, script) don't
55
+ // bubble to window, so only script errors arrive here.
56
+ const onError = (event: Event) =>
57
+ safely(() => {
58
+ const e = event as ErrorEvent;
59
+ hooks.onError(e.error ?? e.message, "onerror", {
60
+ message: e.message,
61
+ filename: e.filename,
62
+ lineno: e.lineno,
63
+ colno: e.colno,
64
+ });
65
+ });
66
+ const onRejection = (event: Event) =>
67
+ safely(() => hooks.onError((event as PromiseRejectionEvent).reason, "unhandledrejection"));
68
+
69
+ win.addEventListener("error", onError);
70
+ win.addEventListener("unhandledrejection", onRejection);
71
+ return () => {
72
+ win.removeEventListener("error", onError);
73
+ win.removeEventListener("unhandledrejection", onRejection);
74
+ };
75
+ }
76
+
77
+ export function instrumentFetch(win: Window, original: typeof fetch, hooks: InstrumentHooks): Teardown {
78
+ if (typeof original !== "function") return noop;
79
+ const RequestCtor = typeof Request === "function" ? Request : undefined;
80
+
81
+ const wrapped = function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
82
+ let url: URL | undefined;
83
+ let method = "GET";
84
+ let nextInit = init;
85
+ let spanId: string | undefined;
86
+
87
+ try {
88
+ const isRequest = RequestCtor !== undefined && input instanceof RequestCtor;
89
+ url = new URL(isRequest ? (input as Request).url : String(input), win.location.href);
90
+ method = String(init?.method ?? (isRequest ? (input as Request).method : "GET")).toUpperCase();
91
+
92
+ if (hooks.isOwnRequest(url)) {
93
+ url = undefined;
94
+ } else {
95
+ const propagation = hooks.propagation(url);
96
+ if (propagation) {
97
+ // fetch(request, init) replaces the request's headers with init's, so
98
+ // start from whichever the caller actually supplied.
99
+ const headers = new Headers(init?.headers ?? (isRequest ? (input as Request).headers : undefined));
100
+ for (const [name, value] of Object.entries(propagation.headers)) {
101
+ if (!headers.has(name)) headers.set(name, value);
102
+ }
103
+ nextInit = { ...init, headers };
104
+ spanId = propagation.spanId;
105
+ }
106
+ }
107
+ } catch {
108
+ url = undefined;
109
+ nextInit = init;
110
+ }
111
+
112
+ const startedAt = now();
113
+ const promise = original.call(win, input, nextInit);
114
+ if (url) {
115
+ const target = url;
116
+ promise.then(
117
+ (response) =>
118
+ safely(() =>
119
+ hooks.onRequest({ kind: "fetch", method, url: target, status: response.status || undefined, durationMs: now() - startedAt, spanId }),
120
+ ),
121
+ (error) =>
122
+ safely(() =>
123
+ hooks.onRequest({
124
+ kind: "fetch",
125
+ method,
126
+ url: target,
127
+ durationMs: now() - startedAt,
128
+ aborted: (error as Error | undefined)?.name === "AbortError",
129
+ spanId,
130
+ }),
131
+ ),
132
+ );
133
+ }
134
+ return promise;
135
+ };
136
+
137
+ win.fetch = wrapped as typeof fetch;
138
+ return () => {
139
+ if (win.fetch === wrapped) win.fetch = original;
140
+ };
141
+ }
142
+
143
+ interface XhrState {
144
+ method: string;
145
+ url?: URL;
146
+ headerNames: Set<string>;
147
+ }
148
+
149
+ export function instrumentXhr(win: Window, hooks: InstrumentHooks): Teardown {
150
+ const Xhr = (win as Window & { XMLHttpRequest?: typeof XMLHttpRequest }).XMLHttpRequest;
151
+ if (typeof Xhr !== "function") return noop;
152
+
153
+ const proto = Xhr.prototype;
154
+ const originalOpen = proto.open;
155
+ const originalSend = proto.send;
156
+ const originalSetRequestHeader = proto.setRequestHeader;
157
+ const states = new WeakMap<XMLHttpRequest, XhrState>();
158
+
159
+ const open = function (this: XMLHttpRequest, method: string, url: string | URL, ...rest: unknown[]) {
160
+ safely(() => {
161
+ states.set(this, {
162
+ method: String(method || "GET").toUpperCase(),
163
+ url: new URL(String(url), win.location.href),
164
+ headerNames: new Set(),
165
+ });
166
+ });
167
+ return (originalOpen as (...args: unknown[]) => void).apply(this, [method, url, ...rest]);
168
+ };
169
+
170
+ const setRequestHeader = function (this: XMLHttpRequest, name: string, value: string) {
171
+ safely(() => states.get(this)?.headerNames.add(String(name).toLowerCase()));
172
+ return originalSetRequestHeader.call(this, name, value);
173
+ };
174
+
175
+ const send = function (this: XMLHttpRequest, body?: Document | XMLHttpRequestBodyInit | null) {
176
+ const state = states.get(this);
177
+ if (state?.url && !hooks.isOwnRequest(state.url)) {
178
+ const url = state.url;
179
+ let spanId: string | undefined;
180
+ safely(() => {
181
+ const propagation = hooks.propagation(url);
182
+ if (!propagation) return;
183
+ for (const [name, value] of Object.entries(propagation.headers)) {
184
+ // setRequestHeader appends, so a caller's own traceparent would become "a, b".
185
+ if (!state.headerNames.has(name)) originalSetRequestHeader.call(this, name, value);
186
+ }
187
+ spanId = propagation.spanId;
188
+ });
189
+
190
+ const startedAt = now();
191
+ let aborted = false;
192
+ this.addEventListener("abort", () => {
193
+ aborted = true;
194
+ });
195
+ this.addEventListener("loadend", () =>
196
+ safely(() =>
197
+ hooks.onRequest({
198
+ kind: "xhr",
199
+ method: state.method,
200
+ url,
201
+ status: this.status || undefined,
202
+ durationMs: now() - startedAt,
203
+ aborted,
204
+ spanId,
205
+ }),
206
+ ),
207
+ );
208
+ }
209
+ return originalSend.call(this, body);
210
+ };
211
+
212
+ proto.open = open as typeof proto.open;
213
+ proto.setRequestHeader = setRequestHeader;
214
+ proto.send = send;
215
+ return () => {
216
+ if (proto.open === open) proto.open = originalOpen;
217
+ if (proto.setRequestHeader === setRequestHeader) proto.setRequestHeader = originalSetRequestHeader;
218
+ if (proto.send === send) proto.send = originalSend;
219
+ };
220
+ }
221
+
222
+ export function instrumentConsole(target: Console, levels: ConsoleLevel[], hooks: InstrumentHooks): Teardown {
223
+ const restores: Teardown[] = [];
224
+ for (const level of levels) {
225
+ const original = target[level];
226
+ if (typeof original !== "function") continue;
227
+ const wrapped = function (...args: unknown[]) {
228
+ safely(() => hooks.onConsole(level, args));
229
+ return original.apply(target, args);
230
+ };
231
+ target[level] = wrapped;
232
+ restores.push(() => {
233
+ if (target[level] === wrapped) target[level] = original;
234
+ });
235
+ }
236
+ return () => restores.forEach((restore) => restore());
237
+ }
238
+
239
+ /** Single-page-app route changes, for breadcrumbs and to start a fresh trace per view. */
240
+ export function instrumentHistory(win: Window, hooks: InstrumentHooks): Teardown {
241
+ const history = win.history;
242
+ if (!history || typeof history.pushState !== "function") return noop;
243
+
244
+ let last = win.location.href;
245
+ const changed = () =>
246
+ safely(() => {
247
+ const next = win.location.href;
248
+ if (next === last) return;
249
+ const previous = last;
250
+ last = next;
251
+ hooks.onNavigation(previous, next);
252
+ });
253
+
254
+ const originalPush = history.pushState;
255
+ const originalReplace = history.replaceState;
256
+ const push = function (this: History, ...args: Parameters<History["pushState"]>) {
257
+ const result = originalPush.apply(this, args);
258
+ changed();
259
+ return result;
260
+ };
261
+ const replace = function (this: History, ...args: Parameters<History["replaceState"]>) {
262
+ const result = originalReplace.apply(this, args);
263
+ changed();
264
+ return result;
265
+ };
266
+
267
+ history.pushState = push;
268
+ history.replaceState = replace;
269
+ win.addEventListener("popstate", changed);
270
+ return () => {
271
+ if (history.pushState === push) history.pushState = originalPush;
272
+ if (history.replaceState === replace) history.replaceState = originalReplace;
273
+ win.removeEventListener("popstate", changed);
274
+ };
275
+ }
@@ -0,0 +1,184 @@
1
+ import type { BrowserEvent } from "./types.js";
2
+
3
+ /** Browsers cap the combined body of in-flight keepalive requests at 64 KiB. */
4
+ const KEEPALIVE_BYTES = 60_000;
5
+ const MAX_RETRY_DELAY_MS = 300_000;
6
+
7
+ type Outcome = "ok" | "retry" | "drop" | "stop";
8
+
9
+ export interface TransportOptions {
10
+ batchUrl: string;
11
+ apiKey: string;
12
+ /** The page's fetch as it was before instrumentation, so delivery is never recorded as traffic. */
13
+ fetch: typeof fetch;
14
+ flushIntervalMs: number;
15
+ maxBatchSize: number;
16
+ maxQueueSize: number;
17
+ debug: (message: string) => void;
18
+ /** Called once when the server refuses the key; retrying could not help. */
19
+ onStop: (message: string) => void;
20
+ }
21
+
22
+ /**
23
+ * Batches events and posts them to the ingest API. It never throws into the page
24
+ * and never blocks it: a Midline outage costs queued telemetry, not the app.
25
+ */
26
+ export class BrowserTransport {
27
+ private queue: BrowserEvent[] = [];
28
+ private timer: ReturnType<typeof setInterval> | undefined;
29
+ private sending = false;
30
+ private stopped = false;
31
+ private retryAt = 0;
32
+ private backoffMs = 0;
33
+ dropped = 0;
34
+
35
+ constructor(private readonly options: TransportOptions) {}
36
+
37
+ start(): void {
38
+ if (this.timer || this.stopped) return;
39
+ this.timer = setInterval(() => void this.flush(), this.options.flushIntervalMs);
40
+ }
41
+
42
+ enqueue(event: BrowserEvent): void {
43
+ if (this.stopped) return;
44
+ this.queue.push(event);
45
+ this.trim();
46
+ if (this.queue.length >= this.options.maxBatchSize && Date.now() >= this.retryAt) {
47
+ void this.flush();
48
+ }
49
+ }
50
+
51
+ get size(): number {
52
+ return this.queue.length;
53
+ }
54
+
55
+ /** Sends everything queued, one batch at a time, unless Midline asked us to back off. */
56
+ async flush(): Promise<void> {
57
+ if (this.stopped || this.sending || !this.queue.length || Date.now() < this.retryAt) return;
58
+ this.sending = true;
59
+ try {
60
+ while (this.queue.length && !this.stopped) {
61
+ const batch = this.queue.splice(0, this.options.maxBatchSize);
62
+ const outcome = await this.send(batch, false);
63
+ if (outcome === "retry") {
64
+ this.queue.unshift(...batch);
65
+ this.trim();
66
+ return;
67
+ }
68
+ if (outcome === "stop") return;
69
+ }
70
+ } finally {
71
+ this.sending = false;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * The page is being hidden and may never come back. Hands whatever is queued to
77
+ * keepalive requests without waiting on them; an in-flight regular flush is left
78
+ * alone, since its batch already left the queue.
79
+ */
80
+ flushOnHide(): void {
81
+ if (this.stopped || !this.queue.length) return;
82
+ let budget = KEEPALIVE_BYTES;
83
+ while (this.queue.length) {
84
+ const batch: BrowserEvent[] = [];
85
+ let bytes = 12;
86
+ while (this.queue.length && batch.length < this.options.maxBatchSize) {
87
+ const size = JSON.stringify(this.queue[0]).length + 1;
88
+ if (batch.length && bytes + size > budget) break;
89
+ batch.push(this.queue.shift()!);
90
+ bytes += size;
91
+ }
92
+ budget -= bytes;
93
+ void this.send(batch, budget > 0);
94
+ if (budget <= 0) break;
95
+ }
96
+ }
97
+
98
+ stop(): void {
99
+ this.stopped = true;
100
+ this.queue = [];
101
+ if (this.timer) clearInterval(this.timer);
102
+ this.timer = undefined;
103
+ }
104
+
105
+ private trim(): void {
106
+ const excess = this.queue.length - this.options.maxQueueSize;
107
+ if (excess > 0) {
108
+ this.queue.splice(0, excess);
109
+ this.dropped += excess;
110
+ }
111
+ }
112
+
113
+ private async send(batch: BrowserEvent[], keepalive: boolean): Promise<Outcome> {
114
+ let response: Response;
115
+ try {
116
+ response = await this.options.fetch(this.options.batchUrl, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json", "X-API-Key": this.options.apiKey },
119
+ body: JSON.stringify({ events: batch }),
120
+ keepalive,
121
+ credentials: "omit",
122
+ mode: "cors",
123
+ });
124
+ } catch (error) {
125
+ // Offline, blocked by an extension, or a server that doesn't answer CORS.
126
+ this.options.debug(`midline: could not reach Midline (${describe(error)}); will retry.`);
127
+ this.backOff();
128
+ return "retry";
129
+ }
130
+
131
+ const { status } = response;
132
+ if (status >= 200 && status < 300) {
133
+ this.backoffMs = 0;
134
+ this.retryAt = 0;
135
+ return "ok";
136
+ }
137
+ if (status === 401 || status === 403) {
138
+ const detail = await serverMessage(response);
139
+ this.stop();
140
+ this.options.onStop(
141
+ `midline: the Midline server refused this key (HTTP ${status}${detail ? `: ${detail}` : ""}). ` +
142
+ "Browser monitoring is now off.",
143
+ );
144
+ return "stop";
145
+ }
146
+ if (status === 408 || status === 429 || status >= 500) {
147
+ this.backOff(parseRetryAfter(response.headers.get("retry-after")));
148
+ this.options.debug(`midline: Midline answered HTTP ${status}; events are kept and retried.`);
149
+ return "retry";
150
+ }
151
+ this.dropped += batch.length;
152
+ const detail = await serverMessage(response);
153
+ this.options.debug(`midline: Midline rejected ${batch.length} events (HTTP ${status}${detail ? `: ${detail}` : ""}).`);
154
+ return "drop";
155
+ }
156
+
157
+ private backOff(retryAfterMs = 0): void {
158
+ this.backoffMs = Math.min(Math.max(this.backoffMs * 2, 1000), MAX_RETRY_DELAY_MS);
159
+ this.retryAt = Date.now() + Math.max(this.backoffMs, Math.min(retryAfterMs, MAX_RETRY_DELAY_MS));
160
+ }
161
+ }
162
+
163
+ function parseRetryAfter(value: string | null): number {
164
+ if (!value) return 0;
165
+ const seconds = Number(value);
166
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
167
+ const date = Date.parse(value);
168
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0;
169
+ }
170
+
171
+ async function serverMessage(response: Response): Promise<string> {
172
+ try {
173
+ const text = await response.text();
174
+ const parsed = JSON.parse(text);
175
+ const message = Array.isArray(parsed?.message) ? parsed.message.join("; ") : parsed?.message;
176
+ return typeof message === "string" ? message.slice(0, 300) : "";
177
+ } catch {
178
+ return "";
179
+ }
180
+ }
181
+
182
+ function describe(error: unknown): string {
183
+ return error instanceof Error ? error.message : String(error);
184
+ }
@@ -0,0 +1,105 @@
1
+ import type { EventCategory, EventSeverity } from "../types.js";
2
+
3
+ /** Which fetch/XHR calls become events. Every call is a breadcrumb regardless. */
4
+ export type RequestCapture = "failed" | "all" | false;
5
+
6
+ export type ConsoleLevel = "error" | "warn" | "info" | "log" | "debug";
7
+
8
+ /** Kept deliberately small: an id is enough to find a user's sessions, and anything more is personal data. */
9
+ export interface MidlineUser {
10
+ id?: string;
11
+ username?: string;
12
+ }
13
+
14
+ export interface MidlineBrowserConfig {
15
+ /**
16
+ * A **browser key** (`pk_…`) from the Midline dashboard. Browser keys are public
17
+ * and only accepted from the origins listed on the key. Server keys (`ak_…`) are
18
+ * secrets; the SDK refuses to start with one, and the server refuses them from
19
+ * browsers anyway.
20
+ */
21
+ apiKey: string;
22
+ /** The Midline server. Default `https://api.usemidline.com`. `http://` only for localhost. */
23
+ endpoint?: string;
24
+
25
+ /** Name for this app, e.g. `checkout-web`. */
26
+ service?: string;
27
+ environment?: string;
28
+ /** Build or version tag, so an error can be tied to the deploy that introduced it. */
29
+ release?: string;
30
+ /** Set to false to keep the SDK inert (e.g. in local development). */
31
+ enabled?: boolean;
32
+
33
+ /** Uncaught errors and unhandled promise rejections. Default true. */
34
+ captureErrors?: boolean;
35
+ /** fetch and XMLHttpRequest calls. Default `"failed"`: 4xx, 5xx and network errors. */
36
+ captureRequests?: RequestCapture;
37
+ /**
38
+ * Also send console output as `console` events. `true` means `["error", "warn"]`.
39
+ * Default false. Wrapping console makes devtools attribute log lines to the SDK,
40
+ * which is why it is opt-in.
41
+ */
42
+ captureConsole?: boolean | ConsoleLevel[];
43
+ /** LCP, INP, CLS, FCP and TTFB, reported once when the page is first hidden. Default true. */
44
+ captureWebVitals?: boolean;
45
+
46
+ /**
47
+ * Requests that get a W3C `traceparent` header, so the backend's midline-agent
48
+ * links its request to this page. Strings match as URL prefixes (or path prefixes
49
+ * when they start with `/`), RegExps against the full URL. Default: same-origin
50
+ * requests only, because a new header on a cross-origin call makes that API
51
+ * answer a CORS preflight.
52
+ */
53
+ tracePropagationTargets?: Array<string | RegExp>;
54
+ /** Error messages to drop. Strings match as substrings. */
55
+ ignoreErrors?: Array<string | RegExp>;
56
+ /** Request URLs never recorded, e.g. analytics beacons. Strings match as substrings. */
57
+ ignoreUrls?: Array<string | RegExp>;
58
+
59
+ /** Fraction of events sent, 0–1. Default 1. */
60
+ sampleRate?: number;
61
+ /** Ceiling on events per minute, so an error in a render loop can't flood the project. Default 120. */
62
+ maxEventsPerMinute?: number;
63
+ /** Extra field names to redact, on top of the built-in list. */
64
+ redactFields?: string[];
65
+ /**
66
+ * Last look at every event. Return null to drop it. Change `payload` and
67
+ * `metadata` freely; other unknown top-level fields are removed, because the
68
+ * server rejects them.
69
+ */
70
+ beforeSend?: (event: BrowserEvent) => BrowserEvent | null | undefined;
71
+
72
+ /** Log the SDK's own diagnostics to the console. */
73
+ debug?: boolean;
74
+ /** How often queued events are sent, in ms. Default 5000. */
75
+ flushIntervalMs?: number;
76
+ /** Events per request. Default 20. */
77
+ maxBatchSize?: number;
78
+ /** Events held while Midline is unreachable; oldest dropped past this. Default 200. */
79
+ maxQueueSize?: number;
80
+ }
81
+
82
+ /** An event as it is sent to the ingest API. */
83
+ export interface BrowserEvent {
84
+ eventType: "error" | "request" | "console" | "performance" | "custom";
85
+ route: string;
86
+ method?: string;
87
+ statusCode?: number;
88
+ responseTime?: number;
89
+ severity: EventSeverity;
90
+ category?: EventCategory;
91
+ timestamp: string;
92
+ service?: string;
93
+ environment?: string;
94
+ release?: string;
95
+ userAgent?: string;
96
+ traceId?: string;
97
+ spanId?: string;
98
+ payload?: Record<string, unknown>;
99
+ metadata: Record<string, unknown>;
100
+ }
101
+
102
+ export interface CaptureContext {
103
+ tags?: Record<string, string>;
104
+ extra?: Record<string, unknown>;
105
+ }
@@ -0,0 +1,2 @@
1
+ /** Kept in step with package.json by test/browser.test.js; a browser bundle can't read package.json. */
2
+ export const BROWSER_SDK_VERSION = "0.4.0";