@aidenappleby/monitor-js 1.1.0 → 1.2.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 +109 -0
- package/dist/index.d.mts +93 -4
- package/dist/index.d.ts +93 -4
- package/dist/index.js +346 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +341 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/axios.ts"],"sourcesContent":["export { Monitor } from \"./client\";\nexport { attachAxiosMonitor } from \"./axios\";\nexport type { MonitorConfig, MonitorEvent, EmitOptions, LogLevel } from \"./types\";\nexport type { AxiosMonitorOptions } from \"./axios\";\n","import type { MonitorConfig, MonitorEvent, EmitOptions, LogLevel } from \"./types\";\n\nconst DEFAULT_FLUSH_INTERVAL = 2000;\nconst DEFAULT_BATCH_SIZE = 20;\nconst MAX_QUEUE_SIZE = 500;\n\nexport class Monitor {\n private config: Required<\n Pick<MonitorConfig, \"service\" | \"ingestUrl\" | \"apiKey\" | \"env\" | \"flushInterval\" | \"batchSize\" | \"debug\">\n >;\n private ignoreErrors: (string | RegExp)[] = [];\n private queue: MonitorEvent[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private userId: string = \"\";\n private jobId: string = \"\";\n private active = false;\n\n constructor(config: MonitorConfig) {\n this.config = {\n service: config.service,\n ingestUrl: config.ingestUrl,\n apiKey: config.apiKey,\n env: config.env ?? \"production\",\n flushInterval: config.flushInterval ?? DEFAULT_FLUSH_INTERVAL,\n batchSize: config.batchSize ?? DEFAULT_BATCH_SIZE,\n debug: config.debug ?? false,\n };\n this.ignoreErrors = config.ignoreErrors ?? [];\n\n this.start();\n\n if (config.captureErrors !== false) {\n this.installErrorHandler();\n }\n if (config.captureUnhandledRejections !== false) {\n this.installRejectionHandler();\n }\n }\n\n /** Set a persistent user ID for all subsequent events */\n setUser(userId: string): void {\n this.userId = userId;\n }\n\n /** Clear the user ID */\n clearUser(): void {\n this.userId = \"\";\n }\n\n /** Set a persistent job ID (session-level identifier) */\n setJobId(jobId: string): void {\n this.jobId = jobId;\n }\n\n /** Emit an event at a specific level */\n emit(name: string, level: LogLevel, opts?: EmitOptions): void {\n if (!this.active) return;\n\n const event: MonitorEvent = {\n timestamp: new Date().toISOString(),\n service: this.config.service,\n env: this.config.env,\n job_id: this.jobId,\n request_id: opts?.requestId ?? \"\",\n trace_id: opts?.traceId ?? \"\",\n user_id: opts?.userId ?? this.userId,\n name,\n level,\n data: opts?.data ?? {},\n };\n\n if (this.queue.length >= MAX_QUEUE_SIZE) {\n // Drop oldest events to prevent unbounded memory growth\n this.queue.shift();\n }\n\n this.queue.push(event);\n\n if (this.config.debug) {\n console.debug(`[monitor] ${level} ${name}`, opts?.data);\n }\n\n if (this.queue.length >= this.config.batchSize) {\n this.flush();\n }\n }\n\n /** Emit a debug event */\n debug(name: string, opts?: EmitOptions): void {\n this.emit(name, \"debug\", opts);\n }\n\n /** Emit an info event */\n info(name: string, opts?: EmitOptions): void {\n this.emit(name, \"info\", opts);\n }\n\n /** Emit a warning event */\n warn(name: string, opts?: EmitOptions): void {\n this.emit(name, \"warn\", opts);\n }\n\n /** Emit an error event */\n error(name: string, opts?: EmitOptions): void {\n this.emit(name, \"error\", opts);\n }\n\n /** Emit a fatal event */\n fatal(name: string, opts?: EmitOptions): void {\n this.emit(name, \"fatal\", opts);\n }\n\n /** Flush all queued events to the ingest endpoint */\n flush(): void {\n if (this.queue.length === 0) return;\n\n const batch = this.queue.splice(0);\n const payload = batch.map((e) => JSON.stringify(e)).join(\"\\n\");\n\n if (typeof fetch === \"undefined\") return;\n\n fetch(this.config.ingestUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-ndjson\",\n \"X-Api-Key\": this.config.apiKey,\n },\n body: payload,\n keepalive: true,\n }).catch((err) => {\n if (this.config.debug) {\n console.warn(\"[monitor] flush failed:\", err);\n }\n // Re-queue failed events if there's room\n if (this.queue.length + batch.length <= MAX_QUEUE_SIZE) {\n this.queue = batch.concat(this.queue);\n }\n });\n }\n\n /** Stop the monitor and flush remaining events */\n shutdown(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n this.flush();\n this.removeListeners();\n this.active = false;\n }\n\n private start(): void {\n if (this.active) return;\n this.active = true;\n\n this.timer = setInterval(() => this.flush(), this.config.flushInterval);\n\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", this.handleVisibilityChange);\n }\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"pagehide\", this.handlePageHide);\n }\n }\n\n private handleVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") {\n this.flush();\n }\n };\n\n private handlePageHide = (): void => {\n this.flush();\n };\n\n private shouldIgnoreError(message: string, stack?: string): boolean {\n if (this.ignoreErrors.length === 0) return false;\n for (const pattern of this.ignoreErrors) {\n if (typeof pattern === \"string\") {\n if (message.includes(pattern) || (stack !== undefined && stack.includes(pattern))) {\n return true;\n }\n } else {\n if (pattern.test(message) || (stack !== undefined && pattern.test(stack))) {\n return true;\n }\n }\n }\n return false;\n }\n\n private errorHandler = (event: ErrorEvent): void => {\n const stack = event.error?.stack;\n if (this.shouldIgnoreError(event.message ?? \"\", stack)) return;\n this.emit(\"client.error.uncaught\", \"error\", {\n data: {\n message: event.message,\n filename: event.filename,\n lineno: event.lineno,\n colno: event.colno,\n stack,\n },\n });\n };\n\n private rejectionHandler = (event: PromiseRejectionEvent): void => {\n const reason = event.reason;\n const message = reason?.message ?? String(reason);\n const stack = reason?.stack;\n if (this.shouldIgnoreError(message, stack)) return;\n this.emit(\"client.error.unhandled_rejection\", \"error\", {\n data: {\n message,\n stack,\n },\n });\n };\n\n private installErrorHandler(): void {\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"error\", this.errorHandler);\n }\n }\n\n private installRejectionHandler(): void {\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"unhandledrejection\", this.rejectionHandler);\n }\n }\n\n private removeListeners(): void {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"error\", this.errorHandler);\n window.removeEventListener(\"unhandledrejection\", this.rejectionHandler);\n window.removeEventListener(\"pagehide\", this.handlePageHide);\n }\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", this.handleVisibilityChange);\n }\n }\n}\n","import type { Monitor } from \"./client\";\nimport type { LogLevel } from \"./types\";\n\ninterface AxiosInstance {\n interceptors: {\n request: { use: (onFulfilled: (config: any) => any) => void };\n response: {\n use: (onFulfilled: (response: any) => any, onRejected: (error: any) => any) => void;\n };\n };\n}\n\nexport interface AxiosMonitorOptions {\n /** Only report events for responses with these status codes or above (default: 400) */\n minStatus?: number;\n /** Report successful requests too (default: false) */\n reportSuccess?: boolean;\n /** Paths to ignore (e.g. [\"/healthcheck\", \"/api/health\"]) */\n ignorePaths?: string[];\n}\n\n/**\n * Attaches Monitor interceptors to an Axios instance.\n * Automatically reports API failures with request_id correlation.\n *\n * Works with both standard axios error handling AND `validateStatus: () => true`\n * (where all HTTP responses go through the fulfilled handler).\n */\nexport function attachAxiosMonitor(\n axiosInstance: AxiosInstance,\n monitor: Monitor,\n opts?: AxiosMonitorOptions\n): void {\n const minStatus = opts?.minStatus ?? 400;\n const reportSuccess = opts?.reportSuccess ?? false;\n const ignorePaths = opts?.ignorePaths ?? [];\n\n // Stamp request start time\n axiosInstance.interceptors.request.use((config: any) => {\n config.metadata = { startTime: Date.now() };\n return config;\n });\n\n axiosInstance.interceptors.response.use(\n (response: any) => {\n const url: string = response.config?.url ?? \"\";\n if (ignorePaths.some((p) => url.includes(p))) return response;\n\n const statusCode: number = response.status ?? 0;\n const requestId: string = response.headers?.[\"x-request-id\"] ?? \"\";\n const durationMs = response.config?.metadata?.startTime\n ? Date.now() - response.config.metadata.startTime\n : undefined;\n\n // Handle error responses that come through fulfilled handler\n // (when validateStatus: () => true is used)\n if (statusCode >= minStatus) {\n const level: LogLevel = statusCode >= 500 ? \"error\" : \"warn\";\n const name =\n statusCode >= 500 ? \"api.request.server_error\" : \"api.request.client_error\";\n\n monitor.emit(name, level, {\n requestId,\n data: {\n method: (response.config?.method ?? \"\").toUpperCase(),\n url,\n status_code: statusCode,\n error: response.data?.error,\n error_message: response.data?.error_message,\n duration_ms: durationMs,\n },\n });\n\n return response;\n }\n\n // Report successful requests if enabled\n if (reportSuccess && statusCode > 0) {\n monitor.info(\"api.request.success\", {\n requestId,\n data: {\n method: (response.config?.method ?? \"\").toUpperCase(),\n url,\n status_code: statusCode,\n duration_ms: durationMs,\n },\n });\n }\n\n return response;\n },\n (error: any) => {\n const url: string = error.config?.url ?? \"\";\n if (ignorePaths.some((p) => url.includes(p))) {\n return Promise.reject(error);\n }\n\n const durationMs = error.config?.metadata?.startTime\n ? Date.now() - error.config.metadata.startTime\n : undefined;\n\n // Network errors (no response — timeout, DNS failure, CORS blocked)\n if (!error.response) {\n monitor.error(\"api.request.network_error\", {\n data: {\n method: (error.config?.method ?? \"\").toUpperCase(),\n url,\n error_code: error.code,\n error_message: error.message,\n duration_ms: durationMs,\n },\n });\n return Promise.reject(error);\n }\n\n // HTTP errors (when validateStatus is default — throws on non-2xx)\n const statusCode: number = error.response.status ?? 0;\n const requestId: string = error.response.headers?.[\"x-request-id\"] ?? \"\";\n\n if (statusCode >= minStatus) {\n const level: LogLevel = statusCode >= 500 ? \"error\" : \"warn\";\n const name =\n statusCode >= 500 ? \"api.request.server_error\" : \"api.request.client_error\";\n\n monitor.emit(name, level, {\n requestId,\n data: {\n method: (error.config?.method ?? \"\").toUpperCase(),\n url,\n status_code: statusCode,\n error: error.response.data?.error,\n error_message: error.response.data?.error_message,\n duration_ms: durationMs,\n },\n });\n }\n\n return Promise.reject(error);\n }\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/ids.ts","../src/client.ts","../src/axios.ts"],"sourcesContent":["export { Monitor } from \"./client\";\nexport { attachAxiosMonitor } from \"./axios\";\nexport { isValidCorrelationId, newRequestId, newTraceId, newJobId } from \"./ids\";\nexport type { MonitorConfig, MonitorEvent, EmitOptions, LogLevel, MonitorStats } from \"./types\";\nexport type { AxiosMonitorOptions } from \"./axios\";\n","/**\n * monitor-core's correlation-id rule (structs.correlationIDRegex), verbatim.\n *\n * Ingest validates job_id, request_id and trace_id against it and rejects the\n * WHOLE request when any line fails — so one malformed id, passed through\n * unchecked, loses every event batched with it.\n */\nconst CORRELATION_ID =\n /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9a-fA-F]{8,64})$/;\n\n/**\n * Whether monitor-core would accept `id` as a job_id, request_id or trace_id.\n * The empty string is valid: the server skips empty ids.\n */\nexport function isValidCorrelationId(id: string): boolean {\n return id === \"\" || CORRELATION_ID.test(id);\n}\n\nfunction randomBytes(n: number): Uint8Array {\n const out = new Uint8Array(n);\n const c = (globalThis as { crypto?: { getRandomValues?: (a: Uint8Array) => Uint8Array } }).crypto;\n if (c && typeof c.getRandomValues === \"function\") {\n c.getRandomValues(out);\n return out;\n }\n // Node 18 has no global crypto. Correlation ids need uniqueness, not secrecy.\n for (let i = 0; i < n; i++) out[i] = Math.floor(Math.random() * 256);\n return out;\n}\n\nfunction hex(bytes: Uint8Array): string {\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** A request_id monitor-core accepts: 16 hex characters. */\nexport function newRequestId(): string {\n return hex(randomBytes(8));\n}\n\n/** A job_id monitor-core accepts: 16 hex characters. */\nexport function newJobId(): string {\n return hex(randomBytes(8));\n}\n\n/** A trace_id monitor-core accepts: a hyphenated UUID v4. */\nexport function newTraceId(): string {\n const b = randomBytes(16);\n b[6] = (b[6] & 0x0f) | 0x40; // version 4\n b[8] = (b[8] & 0x3f) | 0x80; // RFC 4122 variant\n const h = hex(b);\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n}\n","import type { MonitorConfig, MonitorEvent, EmitOptions, LogLevel, MonitorStats } from \"./types\";\nimport { isValidCorrelationId, newJobId } from \"./ids\";\n\n// Minimal ambient shape for the Node `process` global — this package has no\n// @types/node dependency and targets the browser too, so `process` may be absent.\n// Guarded with `typeof process !== \"undefined\"` before use.\ndeclare const process:\n | {\n on?(event: string, listener: (...args: unknown[]) => void): void;\n removeListener?(event: string, listener: (...args: unknown[]) => void): void;\n listenerCount?(event: string): number;\n nextTick?(callback: () => void): void;\n exit?(code?: number): void;\n }\n | undefined;\n\nconst DEFAULT_FLUSH_INTERVAL = 2000;\nconst DEFAULT_BATCH_SIZE = 20;\nconst MAX_QUEUE_SIZE = 500;\n\n/**\n * monitor-core scans NDJSON with a 1 MiB line buffer and rejects the WHOLE\n * request when one line overflows it, so an oversized event is shrunk before it\n * is sent rather than discovered by a 400.\n */\nconst MAX_LINE_BYTES = 1_000_000;\n/** Characters kept per grouping field when an oversized event is shrunk. */\nconst MAX_FIELD_CHARS = 4096;\n/**\n * Browsers refuse a keepalive request once the page's in-flight keepalive\n * bodies exceed 64 KiB, and the refusal is a plain TypeError. Asking for\n * keepalive on a bigger body would fail — and be retried — forever.\n */\nconst KEEPALIVE_MAX_BYTES = 60_000;\n/** Backoff bounds after a transient ingest failure. */\nconst BASE_BACKOFF_MS = 1000;\nconst MAX_BACKOFF_MS = 60_000;\n\n/** The data keys monitor-core's issue fingerprint reads; they survive shrinking. */\nconst GROUPING_KEYS = [\"error\", \"error_message\", \"message\", \"path\", \"uri\", \"method\", \"reason\", \"status_code\"];\n\ntype Outcome = \"delivered\" | \"rejected\" | \"misconfigured\" | \"retryable\";\n\n/** How an ingest response should be handled. Mirrors go-monitor's classifyStatus. */\nfunction classify(status: number): Outcome {\n if (status >= 200 && status < 400) return \"delivered\";\n if (status === 408 || status === 429) return \"retryable\";\n if (status === 401 || status === 403 || status === 404 || status === 405) return \"misconfigured\";\n if (status >= 400 && status < 500) return \"rejected\";\n return \"retryable\";\n}\n\n/** Extra requests allowed to isolate malformed events in a batch of n. */\nfunction bisectBudget(n: number): number {\n let depth = 0;\n for (let x = n; x > 1; x = Math.ceil(x / 2)) depth++;\n return 4 * depth + 4;\n}\n\nlet encoder: TextEncoder | undefined;\nfunction byteLength(s: string): number {\n if (typeof TextEncoder === \"undefined\") return s.length * 3;\n encoder ??= new TextEncoder();\n return encoder.encode(s).length;\n}\n\n/**\n * monitor-core stores level verbatim and groups only exact \"error\"/\"fatal\" into\n * issues, so \"ERROR\" or \"warning\" would land and silently never be tracked.\n */\nfunction normalizeLevel(level: string): string {\n const l = (level || \"info\").toLowerCase();\n return l === \"warning\" ? \"warn\" : l;\n}\n\nexport class Monitor {\n private config: Required<\n Pick<MonitorConfig, \"service\" | \"ingestUrl\" | \"apiKey\" | \"env\" | \"flushInterval\" | \"batchSize\" | \"debug\">\n >;\n private ignoreErrors: (string | RegExp)[] = [];\n private onDrop?: (total: number) => void;\n private queue: MonitorEvent[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private userId: string = \"\";\n private jobId: string;\n private active = false;\n private backoffUntil = 0;\n private failures = 0;\n private warnedMisconfigured = false;\n private counters = { enqueued: 0, flushed: 0, dropped: 0, quarantined: 0 };\n\n constructor(config: MonitorConfig) {\n this.config = {\n service: config.service,\n ingestUrl: config.ingestUrl,\n apiKey: config.apiKey,\n env: config.env ?? \"production\",\n flushInterval: config.flushInterval ?? DEFAULT_FLUSH_INTERVAL,\n batchSize: config.batchSize ?? DEFAULT_BATCH_SIZE,\n debug: config.debug ?? false,\n };\n this.ignoreErrors = config.ignoreErrors ?? [];\n this.onDrop = config.onDrop;\n // One id per page load (or process): every event from this session\n // shares it, so a session's events can be pulled up together.\n this.jobId = newJobId();\n\n this.start();\n\n if (config.captureErrors !== false) {\n this.installErrorHandler();\n }\n if (config.captureUnhandledRejections !== false) {\n this.installRejectionHandler();\n }\n }\n\n /** Set a persistent user ID for all subsequent events */\n setUser(userId: string): void {\n this.userId = userId;\n }\n\n /** Clear the user ID */\n clearUser(): void {\n this.userId = \"\";\n }\n\n /**\n * Set a persistent job ID (session-level identifier). It must be a UUID or\n * 8-64 hex characters — see `isValidCorrelationId`; anything else is\n * cleared from each event and kept in data.invalid_job_id.\n */\n setJobId(jobId: string): void {\n this.jobId = jobId;\n }\n\n /** Emit an event at a specific level */\n emit(name: string, level: LogLevel, opts?: EmitOptions): void {\n if (!this.active) return;\n\n // An id monitor-core would reject is cleared, not sent: one bad id\n // fails the whole request. The original is kept where it is useful.\n let data: Record<string, unknown> = opts?.data ?? {};\n const repair = (field: string, value: string): string => {\n if (isValidCorrelationId(value)) return value;\n data = { ...data, [`invalid_${field}`]: value.slice(0, 128) };\n if (this.config.debug) {\n console.warn(`[monitor] cleared invalid ${field} ${JSON.stringify(value)} (monitor-core accepts a UUID or 8-64 hex characters)`);\n }\n return \"\";\n };\n const jobId = repair(\"job_id\", this.jobId);\n const requestId = repair(\"request_id\", opts?.requestId ?? \"\");\n const traceId = repair(\"trace_id\", opts?.traceId ?? \"\");\n\n const event: MonitorEvent = {\n timestamp: new Date().toISOString(),\n service: this.config.service,\n env: this.config.env,\n job_id: jobId,\n request_id: requestId,\n trace_id: traceId,\n user_id: opts?.userId ?? this.userId,\n name: name || \"event.unnamed\",\n level: normalizeLevel(level),\n data,\n };\n\n if (this.queue.length >= MAX_QUEUE_SIZE) {\n // Drop oldest events to prevent unbounded memory growth\n this.queue.shift();\n this.recordDrop(1);\n }\n\n this.queue.push(event);\n this.counters.enqueued++;\n\n if (this.config.debug) {\n console.debug(`[monitor] ${level} ${name}`, opts?.data);\n }\n\n if (this.queue.length >= this.config.batchSize) {\n this.flush();\n }\n }\n\n /** Emit a debug event */\n debug(name: string, opts?: EmitOptions): void {\n this.emit(name, \"debug\", opts);\n }\n\n /** Emit an info event */\n info(name: string, opts?: EmitOptions): void {\n this.emit(name, \"info\", opts);\n }\n\n /** Emit a warning event */\n warn(name: string, opts?: EmitOptions): void {\n this.emit(name, \"warn\", opts);\n }\n\n /** Emit an error event */\n error(name: string, opts?: EmitOptions): void {\n this.emit(name, \"error\", opts);\n }\n\n /** Emit a fatal event */\n fatal(name: string, opts?: EmitOptions): void {\n this.emit(name, \"fatal\", opts);\n }\n\n /**\n * Lifetime counters. Surface them wherever loss would otherwise go\n * unnoticed: the system that would report dropped telemetry is the one\n * dropping it.\n */\n stats(): MonitorStats {\n return { ...this.counters, queued: this.queue.length };\n }\n\n /** Flush all queued events to the ingest endpoint */\n flush(): void {\n this.flushQueue(false);\n }\n\n /** Stop the monitor and flush remaining events */\n shutdown(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n this.flushQueue(true);\n this.removeListeners();\n this.active = false;\n }\n\n /**\n * @param unloading the page (or process) is going away: ignore the backoff,\n * since this is the last chance these events get.\n */\n private flushQueue(unloading: boolean): void {\n if (this.queue.length === 0) return;\n\n // Check for global fetch BEFORE removing events from the queue — otherwise\n // on a runtime without fetch (Node <18) the batch would be dropped and lost.\n if (typeof fetch === \"undefined\") return;\n\n // After a transient failure, wait out the backoff instead of hitting a\n // struggling ingest again on every emit.\n if (!unloading && Date.now() < this.backoffUntil) return;\n\n const batch = this.queue.splice(0);\n this.send(batch, { remaining: bisectBudget(batch.length) });\n }\n\n private send(events: MonitorEvent[], budget: { remaining: number }): void {\n const lines: string[] = [];\n const sent: MonitorEvent[] = [];\n for (const e of events) {\n const line = this.serialize(e);\n if (line === null) {\n this.recordDrop(1);\n continue;\n }\n lines.push(line);\n sent.push(e);\n }\n if (lines.length === 0) return;\n\n const body = lines.join(\"\\n\");\n let request: Promise<{ ok?: boolean; status?: number } | undefined>;\n try {\n request = fetch(this.config.ingestUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-ndjson\",\n \"X-Api-Key\": this.config.apiKey,\n },\n body,\n keepalive: byteLength(body) <= KEEPALIVE_MAX_BYTES,\n });\n } catch (err) {\n request = Promise.reject(err);\n }\n\n request.then(\n (res) => this.handleResponse(res, sent, budget),\n (err) => {\n if (this.config.debug) {\n console.warn(\"[monitor] flush failed:\", err);\n }\n this.retryLater(sent);\n }\n );\n }\n\n private handleResponse(\n res: { ok?: boolean; status?: number } | undefined,\n events: MonitorEvent[],\n budget: { remaining: number }\n ): void {\n const status = typeof res?.status === \"number\" ? res.status : 0;\n const outcome: Outcome = res?.ok ? \"delivered\" : classify(status);\n\n switch (outcome) {\n case \"delivered\":\n this.counters.flushed += events.length;\n this.failures = 0;\n this.backoffUntil = 0;\n return;\n\n case \"rejected\":\n // Ingest refuses a whole request when one event in it is\n // malformed. Split and resend until the bad one stands alone.\n if (events.length > 1 && budget.remaining > 0) {\n budget.remaining--;\n const mid = events.length >> 1;\n this.send(events.slice(0, mid), budget);\n this.send(events.slice(mid), budget);\n return;\n }\n this.counters.quarantined += events.length;\n this.recordDrop(events.length);\n if (this.config.debug) {\n console.warn(`[monitor] ingest rejected ${events.length} event(s) as malformed (status ${status}):`, events.map((e) => e.name));\n }\n return;\n\n case \"misconfigured\":\n // Nothing will be accepted until the key or URL changes.\n this.recordDrop(events.length);\n if (!this.warnedMisconfigured) {\n this.warnedMisconfigured = true;\n console.warn(`[monitor] ingest refused events with status ${status} — check ingestUrl and apiKey. Events are being dropped.`);\n }\n return;\n\n default:\n this.retryLater(events);\n }\n }\n\n /** Put events back at the front of the queue and back off before retrying. */\n private retryLater(events: MonitorEvent[]): void {\n this.failures++;\n const ceiling = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** Math.min(this.failures - 1, 16));\n // Full jitter: every open tab sees ingest recover at the same moment.\n this.backoffUntil = Date.now() + 50 + Math.random() * ceiling;\n\n const room = MAX_QUEUE_SIZE - this.queue.length;\n const keep = room <= 0 ? [] : events.length > room ? events.slice(events.length - room) : events;\n this.recordDrop(events.length - keep.length);\n if (keep.length > 0) {\n this.queue = keep.concat(this.queue);\n }\n }\n\n /**\n * One NDJSON line for e, or null if it cannot be serialized. Never throws:\n * flush runs inside emit's auto-flush, and emit must never throw into the\n * caller.\n */\n private serialize(e: MonitorEvent): string | null {\n try {\n const line = JSON.stringify(e);\n // Only strings this long can exceed the limit once UTF-8 encoded.\n if (line.length <= MAX_LINE_BYTES / 3) return line;\n const size = byteLength(line);\n if (size <= MAX_LINE_BYTES) return line;\n\n const kept: Record<string, unknown> = { truncated: true, original_size_bytes: size };\n for (const k of GROUPING_KEYS) {\n const v = e.data[k];\n if (typeof v === \"string\") kept[k] = v.slice(0, MAX_FIELD_CHARS);\n else if (typeof v === \"number\" || typeof v === \"boolean\") kept[k] = v;\n }\n const shrunk = JSON.stringify({ ...e, data: kept });\n return byteLength(shrunk) <= MAX_LINE_BYTES ? shrunk : null;\n } catch {\n return null;\n }\n }\n\n private recordDrop(n: number): void {\n if (n <= 0) return;\n this.counters.dropped += n;\n if (this.onDrop) {\n try {\n this.onDrop(this.counters.dropped);\n } catch {\n // A broken callback must not break delivery.\n }\n }\n }\n\n private start(): void {\n if (this.active) return;\n this.active = true;\n\n const t = setInterval(() => this.flush(), this.config.flushInterval);\n // In Node, unref() lets the process exit even while the flush timer is pending.\n // Browser timers have no unref(), so guard on its presence.\n if (typeof (t as any).unref === \"function\") (t as any).unref();\n this.timer = t;\n\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", this.handleVisibilityChange);\n }\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"pagehide\", this.handlePageHide);\n }\n }\n\n private handleVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") {\n this.flushQueue(true);\n }\n };\n\n private handlePageHide = (): void => {\n this.flushQueue(true);\n };\n\n private shouldIgnoreError(message: string, stack?: string): boolean {\n if (this.ignoreErrors.length === 0) return false;\n for (const pattern of this.ignoreErrors) {\n if (typeof pattern === \"string\") {\n if (message.includes(pattern) || (stack !== undefined && stack.includes(pattern))) {\n return true;\n }\n } else {\n if (pattern.test(message) || (stack !== undefined && pattern.test(stack))) {\n return true;\n }\n }\n }\n return false;\n }\n\n /**\n * The route a browser error happened on.\n *\n * Deliberately `pathname` only — never the search string or hash. Query\n * parameters routinely carry tokens, emails and other personal data, and this\n * value is both stored on the event and folded into the server-side issue\n * fingerprint, so anything included here is retained and grouped on.\n *\n * Returns undefined outside a browser so the Node handlers stay unaffected.\n */\n private currentPath(): string | undefined {\n if (typeof window === \"undefined\" || !window.location) return undefined;\n return window.location.pathname;\n }\n\n private errorHandler = (event: ErrorEvent): void => {\n const stack = event.error?.stack;\n if (this.shouldIgnoreError(event.message ?? \"\", stack)) return;\n this.emit(\"client.error.uncaught\", \"error\", {\n data: {\n message: event.message,\n filename: event.filename,\n lineno: event.lineno,\n colno: event.colno,\n stack,\n path: this.currentPath(),\n },\n });\n };\n\n private rejectionHandler = (event: PromiseRejectionEvent): void => {\n const reason = event.reason;\n const message = reason?.message ?? String(reason);\n const stack = reason?.stack;\n if (this.shouldIgnoreError(message, stack)) return;\n this.emit(\"client.error.unhandled_rejection\", \"error\", {\n data: {\n message,\n stack,\n path: this.currentPath(),\n },\n });\n };\n\n // --- Node process handlers ---\n // Adding an uncaughtException or unhandledRejection listener changes what Node\n // does. With no listener, either one prints the error and exits with code 1;\n // with any listener, Node assumes it was handled and keeps running — in\n // whatever state the failure left it. So when this SDK is the only listener,\n // it reports the error and then does what Node would have done. When the app\n // has a listener of its own, the app has already decided; the SDK only reports.\n\n /** Grace for the final batch to leave before a Node-style crash exits. */\n private static readonly NODE_CRASH_GRACE_MS = 1500;\n\n /** Rejections already reported, so the re-raise below is not reported twice. */\n private reportedRejections = new WeakSet<object>();\n\n private nodeExceptionHandler = (err: unknown): void => {\n const alreadyReported =\n typeof err === \"object\" && err !== null && this.reportedRejections.has(err);\n const e = err as { message?: string; stack?: string } | undefined;\n const message = e?.message ?? String(err);\n const stack = e?.stack;\n if (!alreadyReported && !this.shouldIgnoreError(message, stack)) {\n this.emit(\"client.error.uncaught\", \"error\", {\n data: {\n message,\n stack,\n },\n });\n }\n if (this.isSoleListener(\"uncaughtException\")) {\n this.crashLikeNode(err);\n }\n };\n\n private nodeRejectionHandler = (reason: unknown): void => {\n const r = reason as { message?: string; stack?: string } | undefined;\n const message = r?.message ?? String(reason);\n const stack = r?.stack;\n if (!this.shouldIgnoreError(message, stack)) {\n this.emit(\"client.error.unhandled_rejection\", \"error\", {\n data: {\n message,\n stack,\n },\n });\n }\n // Node's default is to raise an unhandled rejection as an uncaught\n // exception. This listener suppressed that, so re-raise it when nothing\n // else listens for rejections: the app's own uncaughtException handling,\n // or Node's crash, then applies exactly as it would without the SDK.\n if (this.isSoleListener(\"unhandledRejection\")) {\n if (typeof reason === \"object\" && reason !== null) {\n this.reportedRejections.add(reason);\n }\n this.reraise(reason);\n }\n };\n\n /** Hand an unhandled rejection back to Node as an uncaught exception. */\n private reraise(reason: unknown): void {\n process?.nextTick?.(() => {\n throw reason;\n });\n }\n\n /** True when this instance's own handler is the only listener for the event. */\n private isSoleListener(event: \"uncaughtException\" | \"unhandledRejection\"): boolean {\n const count = typeof process === \"undefined\" ? undefined : process.listenerCount;\n if (typeof count !== \"function\") {\n return false;\n }\n return count.call(process, event) <= 1;\n }\n\n /** Print the error as Node would, give the batch a moment to leave, exit 1. */\n private crashLikeNode(err: unknown): void {\n console.error(err);\n this.flush();\n setTimeout(() => process?.exit?.(1), Monitor.NODE_CRASH_GRACE_MS);\n }\n\n private installErrorHandler(): void {\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"error\", this.errorHandler);\n } else if (typeof process !== \"undefined\" && typeof process.on === \"function\") {\n process.on(\"uncaughtException\", this.nodeExceptionHandler);\n }\n }\n\n private installRejectionHandler(): void {\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"unhandledrejection\", this.rejectionHandler);\n } else if (typeof process !== \"undefined\" && typeof process.on === \"function\") {\n process.on(\"unhandledRejection\", this.nodeRejectionHandler);\n }\n }\n\n private removeListeners(): void {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"error\", this.errorHandler);\n window.removeEventListener(\"unhandledrejection\", this.rejectionHandler);\n window.removeEventListener(\"pagehide\", this.handlePageHide);\n }\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", this.handleVisibilityChange);\n }\n if (typeof process !== \"undefined\" && typeof process.removeListener === \"function\") {\n process.removeListener(\"uncaughtException\", this.nodeExceptionHandler);\n process.removeListener(\"unhandledRejection\", this.nodeRejectionHandler);\n }\n }\n}\n","import type { Monitor } from \"./client\";\nimport type { LogLevel } from \"./types\";\n\ninterface AxiosInstance {\n interceptors: {\n request: { use: (onFulfilled: (config: any) => any) => void };\n response: {\n use: (onFulfilled: (response: any) => any, onRejected: (error: any) => any) => void;\n };\n };\n}\n\nexport interface AxiosMonitorOptions {\n /** Only report events for responses with these status codes or above (default: 400) */\n minStatus?: number;\n /** Report successful requests too (default: false) */\n reportSuccess?: boolean;\n /** Paths to ignore (e.g. [\"/healthcheck\", \"/api/health\"]) */\n ignorePaths?: string[];\n}\n\n/**\n * The request URL without its query string or fragment. Query strings are where\n * tokens and email addresses travel in URLs, and anything reported is retained\n * for the life of the event store.\n */\nfunction stripQuery(url: string): string {\n const i = url.search(/[?#]/);\n return i === -1 ? url : url.slice(0, i);\n}\n\n/**\n * Attaches Monitor interceptors to an Axios instance.\n * Automatically reports API failures with request_id correlation.\n *\n * Works with both standard axios error handling AND `validateStatus: () => true`\n * (where all HTTP responses go through the fulfilled handler).\n */\nexport function attachAxiosMonitor(\n axiosInstance: AxiosInstance,\n monitor: Monitor,\n opts?: AxiosMonitorOptions\n): void {\n const minStatus = opts?.minStatus ?? 400;\n const reportSuccess = opts?.reportSuccess ?? false;\n const ignorePaths = opts?.ignorePaths ?? [];\n\n // Stamp request start time\n axiosInstance.interceptors.request.use((config: any) => {\n config.metadata = { startTime: Date.now() };\n return config;\n });\n\n axiosInstance.interceptors.response.use(\n (response: any) => {\n const url: string = stripQuery(response.config?.url ?? \"\");\n if (ignorePaths.some((p) => url.includes(p))) return response;\n\n const statusCode: number = response.status ?? 0;\n const requestId: string = response.headers?.[\"x-request-id\"] ?? \"\";\n const durationMs = response.config?.metadata?.startTime\n ? Date.now() - response.config.metadata.startTime\n : undefined;\n\n // Handle error responses that come through fulfilled handler\n // (when validateStatus: () => true is used)\n if (statusCode >= minStatus) {\n const level: LogLevel = statusCode >= 500 ? \"error\" : \"warn\";\n const name =\n statusCode >= 500 ? \"api.request.server_error\" : \"api.request.client_error\";\n\n monitor.emit(name, level, {\n requestId,\n data: {\n method: (response.config?.method ?? \"\").toUpperCase(),\n url,\n status_code: statusCode,\n error: response.data?.error,\n error_message: response.data?.error_message,\n duration_ms: durationMs,\n },\n });\n\n return response;\n }\n\n // Report successful requests if enabled\n if (reportSuccess && statusCode > 0) {\n monitor.info(\"api.request.success\", {\n requestId,\n data: {\n method: (response.config?.method ?? \"\").toUpperCase(),\n url,\n status_code: statusCode,\n duration_ms: durationMs,\n },\n });\n }\n\n return response;\n },\n (error: any) => {\n const url: string = stripQuery(error.config?.url ?? \"\");\n if (ignorePaths.some((p) => url.includes(p))) {\n return Promise.reject(error);\n }\n\n const durationMs = error.config?.metadata?.startTime\n ? Date.now() - error.config.metadata.startTime\n : undefined;\n\n // Network errors (no response — timeout, DNS failure, CORS blocked)\n if (!error.response) {\n monitor.error(\"api.request.network_error\", {\n data: {\n method: (error.config?.method ?? \"\").toUpperCase(),\n url,\n error_code: error.code,\n error_message: error.message,\n duration_ms: durationMs,\n },\n });\n return Promise.reject(error);\n }\n\n // HTTP errors (when validateStatus is default — throws on non-2xx)\n const statusCode: number = error.response.status ?? 0;\n const requestId: string = error.response.headers?.[\"x-request-id\"] ?? \"\";\n\n if (statusCode >= minStatus) {\n const level: LogLevel = statusCode >= 500 ? \"error\" : \"warn\";\n const name =\n statusCode >= 500 ? \"api.request.server_error\" : \"api.request.client_error\";\n\n monitor.emit(name, level, {\n requestId,\n data: {\n method: (error.config?.method ?? \"\").toUpperCase(),\n url,\n status_code: statusCode,\n error: error.response.data?.error,\n error_message: error.response.data?.error_message,\n duration_ms: durationMs,\n },\n });\n }\n\n return Promise.reject(error);\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACOA,IAAM,iBACF;AAMG,SAAS,qBAAqB,IAAqB;AACtD,SAAO,OAAO,MAAM,eAAe,KAAK,EAAE;AAC9C;AAEA,SAAS,YAAY,GAAuB;AACxC,QAAM,MAAM,IAAI,WAAW,CAAC;AAC5B,QAAM,IAAK,WAAgF;AAC3F,MAAI,KAAK,OAAO,EAAE,oBAAoB,YAAY;AAC9C,MAAE,gBAAgB,GAAG;AACrB,WAAO;AAAA,EACX;AAEA,WAAS,IAAI,GAAG,IAAI,GAAG,IAAK,KAAI,CAAC,IAAI,KAAK,MAAM,KAAK,OAAO,IAAI,GAAG;AACnE,SAAO;AACX;AAEA,SAAS,IAAI,OAA2B;AACpC,SAAO,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC5E;AAGO,SAAS,eAAuB;AACnC,SAAO,IAAI,YAAY,CAAC,CAAC;AAC7B;AAGO,SAAS,WAAmB;AAC/B,SAAO,IAAI,YAAY,CAAC,CAAC;AAC7B;AAGO,SAAS,aAAqB;AACjC,QAAM,IAAI,YAAY,EAAE;AACxB,IAAE,CAAC,IAAK,EAAE,CAAC,IAAI,KAAQ;AACvB,IAAE,CAAC,IAAK,EAAE,CAAC,IAAI,KAAQ;AACvB,QAAM,IAAI,IAAI,CAAC;AACf,SAAO,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;AAClG;;;ACnCA,IAAM,yBAAyB;AAC/B,IAAM,qBAAqB;AAC3B,IAAM,iBAAiB;AAOvB,IAAM,iBAAiB;AAEvB,IAAM,kBAAkB;AAMxB,IAAM,sBAAsB;AAE5B,IAAM,kBAAkB;AACxB,IAAM,iBAAiB;AAGvB,IAAM,gBAAgB,CAAC,SAAS,iBAAiB,WAAW,QAAQ,OAAO,UAAU,UAAU,aAAa;AAK5G,SAAS,SAAS,QAAyB;AACvC,MAAI,UAAU,OAAO,SAAS,IAAK,QAAO;AAC1C,MAAI,WAAW,OAAO,WAAW,IAAK,QAAO;AAC7C,MAAI,WAAW,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,IAAK,QAAO;AACjF,MAAI,UAAU,OAAO,SAAS,IAAK,QAAO;AAC1C,SAAO;AACX;AAGA,SAAS,aAAa,GAAmB;AACrC,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,GAAG,IAAI,KAAK,KAAK,IAAI,CAAC,EAAG;AAC7C,SAAO,IAAI,QAAQ;AACvB;AAEA,IAAI;AACJ,SAAS,WAAW,GAAmB;AACnC,MAAI,OAAO,gBAAgB,YAAa,QAAO,EAAE,SAAS;AAC1D,cAAY,IAAI,YAAY;AAC5B,SAAO,QAAQ,OAAO,CAAC,EAAE;AAC7B;AAMA,SAAS,eAAe,OAAuB;AAC3C,QAAM,KAAK,SAAS,QAAQ,YAAY;AACxC,SAAO,MAAM,YAAY,SAAS;AACtC;AAEO,IAAM,UAAN,MAAM,SAAQ;AAAA,EACT;AAAA,EAGA,eAAoC,CAAC;AAAA,EACrC;AAAA,EACA,QAAwB,CAAC;AAAA,EACzB,QAA+C;AAAA,EAC/C,SAAiB;AAAA,EACjB;AAAA,EACA,SAAS;AAAA,EACT,eAAe;AAAA,EACf,WAAW;AAAA,EACX,sBAAsB;AAAA,EACtB,WAAW,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,EAAE;AAAA,EAEzE,YAAY,QAAuB;AAC/B,SAAK,SAAS;AAAA,MACV,SAAS,OAAO;AAAA,MAChB,WAAW,OAAO;AAAA,MAClB,QAAQ,OAAO;AAAA,MACf,KAAK,OAAO,OAAO;AAAA,MACnB,eAAe,OAAO,iBAAiB;AAAA,MACvC,WAAW,OAAO,aAAa;AAAA,MAC/B,OAAO,OAAO,SAAS;AAAA,IAC3B;AACA,SAAK,eAAe,OAAO,gBAAgB,CAAC;AAC5C,SAAK,SAAS,OAAO;AAGrB,SAAK,QAAQ,SAAS;AAEtB,SAAK,MAAM;AAEX,QAAI,OAAO,kBAAkB,OAAO;AAChC,WAAK,oBAAoB;AAAA,IAC7B;AACA,QAAI,OAAO,+BAA+B,OAAO;AAC7C,WAAK,wBAAwB;AAAA,IACjC;AAAA,EACJ;AAAA;AAAA,EAGA,QAAQ,QAAsB;AAC1B,SAAK,SAAS;AAAA,EAClB;AAAA;AAAA,EAGA,YAAkB;AACd,SAAK,SAAS;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAS,OAAqB;AAC1B,SAAK,QAAQ;AAAA,EACjB;AAAA;AAAA,EAGA,KAAK,MAAc,OAAiB,MAA0B;AAC1D,QAAI,CAAC,KAAK,OAAQ;AAIlB,QAAI,OAAgC,MAAM,QAAQ,CAAC;AACnD,UAAM,SAAS,CAAC,OAAe,UAA0B;AACrD,UAAI,qBAAqB,KAAK,EAAG,QAAO;AACxC,aAAO,EAAE,GAAG,MAAM,CAAC,WAAW,KAAK,EAAE,GAAG,MAAM,MAAM,GAAG,GAAG,EAAE;AAC5D,UAAI,KAAK,OAAO,OAAO;AACnB,gBAAQ,KAAK,6BAA6B,KAAK,IAAI,KAAK,UAAU,KAAK,CAAC,uDAAuD;AAAA,MACnI;AACA,aAAO;AAAA,IACX;AACA,UAAM,QAAQ,OAAO,UAAU,KAAK,KAAK;AACzC,UAAM,YAAY,OAAO,cAAc,MAAM,aAAa,EAAE;AAC5D,UAAM,UAAU,OAAO,YAAY,MAAM,WAAW,EAAE;AAEtD,UAAM,QAAsB;AAAA,MACxB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,SAAS,KAAK,OAAO;AAAA,MACrB,KAAK,KAAK,OAAO;AAAA,MACjB,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,SAAS,MAAM,UAAU,KAAK;AAAA,MAC9B,MAAM,QAAQ;AAAA,MACd,OAAO,eAAe,KAAK;AAAA,MAC3B;AAAA,IACJ;AAEA,QAAI,KAAK,MAAM,UAAU,gBAAgB;AAErC,WAAK,MAAM,MAAM;AACjB,WAAK,WAAW,CAAC;AAAA,IACrB;AAEA,SAAK,MAAM,KAAK,KAAK;AACrB,SAAK,SAAS;AAEd,QAAI,KAAK,OAAO,OAAO;AACnB,cAAQ,MAAM,aAAa,KAAK,IAAI,IAAI,IAAI,MAAM,IAAI;AAAA,IAC1D;AAEA,QAAI,KAAK,MAAM,UAAU,KAAK,OAAO,WAAW;AAC5C,WAAK,MAAM;AAAA,IACf;AAAA,EACJ;AAAA;AAAA,EAGA,MAAM,MAAc,MAA0B;AAC1C,SAAK,KAAK,MAAM,SAAS,IAAI;AAAA,EACjC;AAAA;AAAA,EAGA,KAAK,MAAc,MAA0B;AACzC,SAAK,KAAK,MAAM,QAAQ,IAAI;AAAA,EAChC;AAAA;AAAA,EAGA,KAAK,MAAc,MAA0B;AACzC,SAAK,KAAK,MAAM,QAAQ,IAAI;AAAA,EAChC;AAAA;AAAA,EAGA,MAAM,MAAc,MAA0B;AAC1C,SAAK,KAAK,MAAM,SAAS,IAAI;AAAA,EACjC;AAAA;AAAA,EAGA,MAAM,MAAc,MAA0B;AAC1C,SAAK,KAAK,MAAM,SAAS,IAAI;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAsB;AAClB,WAAO,EAAE,GAAG,KAAK,UAAU,QAAQ,KAAK,MAAM,OAAO;AAAA,EACzD;AAAA;AAAA,EAGA,QAAc;AACV,SAAK,WAAW,KAAK;AAAA,EACzB;AAAA;AAAA,EAGA,WAAiB;AACb,QAAI,KAAK,OAAO;AACZ,oBAAc,KAAK,KAAK;AACxB,WAAK,QAAQ;AAAA,IACjB;AACA,SAAK,WAAW,IAAI;AACpB,SAAK,gBAAgB;AACrB,SAAK,SAAS;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,WAAW,WAA0B;AACzC,QAAI,KAAK,MAAM,WAAW,EAAG;AAI7B,QAAI,OAAO,UAAU,YAAa;AAIlC,QAAI,CAAC,aAAa,KAAK,IAAI,IAAI,KAAK,aAAc;AAElD,UAAM,QAAQ,KAAK,MAAM,OAAO,CAAC;AACjC,SAAK,KAAK,OAAO,EAAE,WAAW,aAAa,MAAM,MAAM,EAAE,CAAC;AAAA,EAC9D;AAAA,EAEQ,KAAK,QAAwB,QAAqC;AACtE,UAAM,QAAkB,CAAC;AACzB,UAAM,OAAuB,CAAC;AAC9B,eAAW,KAAK,QAAQ;AACpB,YAAM,OAAO,KAAK,UAAU,CAAC;AAC7B,UAAI,SAAS,MAAM;AACf,aAAK,WAAW,CAAC;AACjB;AAAA,MACJ;AACA,YAAM,KAAK,IAAI;AACf,WAAK,KAAK,CAAC;AAAA,IACf;AACA,QAAI,MAAM,WAAW,EAAG;AAExB,UAAM,OAAO,MAAM,KAAK,IAAI;AAC5B,QAAI;AACJ,QAAI;AACA,gBAAU,MAAM,KAAK,OAAO,WAAW;AAAA,QACnC,QAAQ;AAAA,QACR,SAAS;AAAA,UACL,gBAAgB;AAAA,UAChB,aAAa,KAAK,OAAO;AAAA,QAC7B;AAAA,QACA;AAAA,QACA,WAAW,WAAW,IAAI,KAAK;AAAA,MACnC,CAAC;AAAA,IACL,SAAS,KAAK;AACV,gBAAU,QAAQ,OAAO,GAAG;AAAA,IAChC;AAEA,YAAQ;AAAA,MACJ,CAAC,QAAQ,KAAK,eAAe,KAAK,MAAM,MAAM;AAAA,MAC9C,CAAC,QAAQ;AACL,YAAI,KAAK,OAAO,OAAO;AACnB,kBAAQ,KAAK,2BAA2B,GAAG;AAAA,QAC/C;AACA,aAAK,WAAW,IAAI;AAAA,MACxB;AAAA,IACJ;AAAA,EACJ;AAAA,EAEQ,eACJ,KACA,QACA,QACI;AACJ,UAAM,SAAS,OAAO,KAAK,WAAW,WAAW,IAAI,SAAS;AAC9D,UAAM,UAAmB,KAAK,KAAK,cAAc,SAAS,MAAM;AAEhE,YAAQ,SAAS;AAAA,MACb,KAAK;AACD,aAAK,SAAS,WAAW,OAAO;AAChC,aAAK,WAAW;AAChB,aAAK,eAAe;AACpB;AAAA,MAEJ,KAAK;AAGD,YAAI,OAAO,SAAS,KAAK,OAAO,YAAY,GAAG;AAC3C,iBAAO;AACP,gBAAM,MAAM,OAAO,UAAU;AAC7B,eAAK,KAAK,OAAO,MAAM,GAAG,GAAG,GAAG,MAAM;AACtC,eAAK,KAAK,OAAO,MAAM,GAAG,GAAG,MAAM;AACnC;AAAA,QACJ;AACA,aAAK,SAAS,eAAe,OAAO;AACpC,aAAK,WAAW,OAAO,MAAM;AAC7B,YAAI,KAAK,OAAO,OAAO;AACnB,kBAAQ,KAAK,6BAA6B,OAAO,MAAM,kCAAkC,MAAM,MAAM,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AAAA,QAClI;AACA;AAAA,MAEJ,KAAK;AAED,aAAK,WAAW,OAAO,MAAM;AAC7B,YAAI,CAAC,KAAK,qBAAqB;AAC3B,eAAK,sBAAsB;AAC3B,kBAAQ,KAAK,+CAA+C,MAAM,+DAA0D;AAAA,QAChI;AACA;AAAA,MAEJ;AACI,aAAK,WAAW,MAAM;AAAA,IAC9B;AAAA,EACJ;AAAA;AAAA,EAGQ,WAAW,QAA8B;AAC7C,SAAK;AACL,UAAM,UAAU,KAAK,IAAI,gBAAgB,kBAAkB,KAAK,KAAK,IAAI,KAAK,WAAW,GAAG,EAAE,CAAC;AAE/F,SAAK,eAAe,KAAK,IAAI,IAAI,KAAK,KAAK,OAAO,IAAI;AAEtD,UAAM,OAAO,iBAAiB,KAAK,MAAM;AACzC,UAAM,OAAO,QAAQ,IAAI,CAAC,IAAI,OAAO,SAAS,OAAO,OAAO,MAAM,OAAO,SAAS,IAAI,IAAI;AAC1F,SAAK,WAAW,OAAO,SAAS,KAAK,MAAM;AAC3C,QAAI,KAAK,SAAS,GAAG;AACjB,WAAK,QAAQ,KAAK,OAAO,KAAK,KAAK;AAAA,IACvC;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,UAAU,GAAgC;AAC9C,QAAI;AACA,YAAM,OAAO,KAAK,UAAU,CAAC;AAE7B,UAAI,KAAK,UAAU,iBAAiB,EAAG,QAAO;AAC9C,YAAM,OAAO,WAAW,IAAI;AAC5B,UAAI,QAAQ,eAAgB,QAAO;AAEnC,YAAM,OAAgC,EAAE,WAAW,MAAM,qBAAqB,KAAK;AACnF,iBAAW,KAAK,eAAe;AAC3B,cAAM,IAAI,EAAE,KAAK,CAAC;AAClB,YAAI,OAAO,MAAM,SAAU,MAAK,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe;AAAA,iBACtD,OAAO,MAAM,YAAY,OAAO,MAAM,UAAW,MAAK,CAAC,IAAI;AAAA,MACxE;AACA,YAAM,SAAS,KAAK,UAAU,EAAE,GAAG,GAAG,MAAM,KAAK,CAAC;AAClD,aAAO,WAAW,MAAM,KAAK,iBAAiB,SAAS;AAAA,IAC3D,QAAQ;AACJ,aAAO;AAAA,IACX;AAAA,EACJ;AAAA,EAEQ,WAAW,GAAiB;AAChC,QAAI,KAAK,EAAG;AACZ,SAAK,SAAS,WAAW;AACzB,QAAI,KAAK,QAAQ;AACb,UAAI;AACA,aAAK,OAAO,KAAK,SAAS,OAAO;AAAA,MACrC,QAAQ;AAAA,MAER;AAAA,IACJ;AAAA,EACJ;AAAA,EAEQ,QAAc;AAClB,QAAI,KAAK,OAAQ;AACjB,SAAK,SAAS;AAEd,UAAM,IAAI,YAAY,MAAM,KAAK,MAAM,GAAG,KAAK,OAAO,aAAa;AAGnE,QAAI,OAAQ,EAAU,UAAU,WAAY,CAAC,EAAU,MAAM;AAC7D,SAAK,QAAQ;AAEb,QAAI,OAAO,aAAa,aAAa;AACjC,eAAS,iBAAiB,oBAAoB,KAAK,sBAAsB;AAAA,IAC7E;AACA,QAAI,OAAO,WAAW,aAAa;AAC/B,aAAO,iBAAiB,YAAY,KAAK,cAAc;AAAA,IAC3D;AAAA,EACJ;AAAA,EAEQ,yBAAyB,MAAY;AACzC,QAAI,SAAS,oBAAoB,UAAU;AACvC,WAAK,WAAW,IAAI;AAAA,IACxB;AAAA,EACJ;AAAA,EAEQ,iBAAiB,MAAY;AACjC,SAAK,WAAW,IAAI;AAAA,EACxB;AAAA,EAEQ,kBAAkB,SAAiB,OAAyB;AAChE,QAAI,KAAK,aAAa,WAAW,EAAG,QAAO;AAC3C,eAAW,WAAW,KAAK,cAAc;AACrC,UAAI,OAAO,YAAY,UAAU;AAC7B,YAAI,QAAQ,SAAS,OAAO,KAAM,UAAU,UAAa,MAAM,SAAS,OAAO,GAAI;AAC/E,iBAAO;AAAA,QACX;AAAA,MACJ,OAAO;AACH,YAAI,QAAQ,KAAK,OAAO,KAAM,UAAU,UAAa,QAAQ,KAAK,KAAK,GAAI;AACvE,iBAAO;AAAA,QACX;AAAA,MACJ;AAAA,IACJ;AACA,WAAO;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYQ,cAAkC;AACtC,QAAI,OAAO,WAAW,eAAe,CAAC,OAAO,SAAU,QAAO;AAC9D,WAAO,OAAO,SAAS;AAAA,EAC3B;AAAA,EAEQ,eAAe,CAAC,UAA4B;AAChD,UAAM,QAAQ,MAAM,OAAO;AAC3B,QAAI,KAAK,kBAAkB,MAAM,WAAW,IAAI,KAAK,EAAG;AACxD,SAAK,KAAK,yBAAyB,SAAS;AAAA,MACxC,MAAM;AAAA,QACF,SAAS,MAAM;AAAA,QACf,UAAU,MAAM;AAAA,QAChB,QAAQ,MAAM;AAAA,QACd,OAAO,MAAM;AAAA,QACb;AAAA,QACA,MAAM,KAAK,YAAY;AAAA,MAC3B;AAAA,IACJ,CAAC;AAAA,EACL;AAAA,EAEQ,mBAAmB,CAAC,UAAuC;AAC/D,UAAM,SAAS,MAAM;AACrB,UAAM,UAAU,QAAQ,WAAW,OAAO,MAAM;AAChD,UAAM,QAAQ,QAAQ;AACtB,QAAI,KAAK,kBAAkB,SAAS,KAAK,EAAG;AAC5C,SAAK,KAAK,oCAAoC,SAAS;AAAA,MACnD,MAAM;AAAA,QACF;AAAA,QACA;AAAA,QACA,MAAM,KAAK,YAAY;AAAA,MAC3B;AAAA,IACJ,CAAC;AAAA,EACL;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,OAAwB,sBAAsB;AAAA;AAAA,EAGtC,qBAAqB,oBAAI,QAAgB;AAAA,EAEzC,uBAAuB,CAAC,QAAuB;AACnD,UAAM,kBACF,OAAO,QAAQ,YAAY,QAAQ,QAAQ,KAAK,mBAAmB,IAAI,GAAG;AAC9E,UAAM,IAAI;AACV,UAAM,UAAU,GAAG,WAAW,OAAO,GAAG;AACxC,UAAM,QAAQ,GAAG;AACjB,QAAI,CAAC,mBAAmB,CAAC,KAAK,kBAAkB,SAAS,KAAK,GAAG;AAC7D,WAAK,KAAK,yBAAyB,SAAS;AAAA,QACxC,MAAM;AAAA,UACF;AAAA,UACA;AAAA,QACJ;AAAA,MACJ,CAAC;AAAA,IACL;AACA,QAAI,KAAK,eAAe,mBAAmB,GAAG;AAC1C,WAAK,cAAc,GAAG;AAAA,IAC1B;AAAA,EACJ;AAAA,EAEQ,uBAAuB,CAAC,WAA0B;AACtD,UAAM,IAAI;AACV,UAAM,UAAU,GAAG,WAAW,OAAO,MAAM;AAC3C,UAAM,QAAQ,GAAG;AACjB,QAAI,CAAC,KAAK,kBAAkB,SAAS,KAAK,GAAG;AACzC,WAAK,KAAK,oCAAoC,SAAS;AAAA,QACnD,MAAM;AAAA,UACF;AAAA,UACA;AAAA,QACJ;AAAA,MACJ,CAAC;AAAA,IACL;AAKA,QAAI,KAAK,eAAe,oBAAoB,GAAG;AAC3C,UAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AAC/C,aAAK,mBAAmB,IAAI,MAAM;AAAA,MACtC;AACA,WAAK,QAAQ,MAAM;AAAA,IACvB;AAAA,EACJ;AAAA;AAAA,EAGQ,QAAQ,QAAuB;AACnC,aAAS,WAAW,MAAM;AACtB,YAAM;AAAA,IACV,CAAC;AAAA,EACL;AAAA;AAAA,EAGQ,eAAe,OAA4D;AAC/E,UAAM,QAAQ,OAAO,YAAY,cAAc,SAAY,QAAQ;AACnE,QAAI,OAAO,UAAU,YAAY;AAC7B,aAAO;AAAA,IACX;AACA,WAAO,MAAM,KAAK,SAAS,KAAK,KAAK;AAAA,EACzC;AAAA;AAAA,EAGQ,cAAc,KAAoB;AACtC,YAAQ,MAAM,GAAG;AACjB,SAAK,MAAM;AACX,eAAW,MAAM,SAAS,OAAO,CAAC,GAAG,SAAQ,mBAAmB;AAAA,EACpE;AAAA,EAEQ,sBAA4B;AAChC,QAAI,OAAO,WAAW,aAAa;AAC/B,aAAO,iBAAiB,SAAS,KAAK,YAAY;AAAA,IACtD,WAAW,OAAO,YAAY,eAAe,OAAO,QAAQ,OAAO,YAAY;AAC3E,cAAQ,GAAG,qBAAqB,KAAK,oBAAoB;AAAA,IAC7D;AAAA,EACJ;AAAA,EAEQ,0BAAgC;AACpC,QAAI,OAAO,WAAW,aAAa;AAC/B,aAAO,iBAAiB,sBAAsB,KAAK,gBAAgB;AAAA,IACvE,WAAW,OAAO,YAAY,eAAe,OAAO,QAAQ,OAAO,YAAY;AAC3E,cAAQ,GAAG,sBAAsB,KAAK,oBAAoB;AAAA,IAC9D;AAAA,EACJ;AAAA,EAEQ,kBAAwB;AAC5B,QAAI,OAAO,WAAW,aAAa;AAC/B,aAAO,oBAAoB,SAAS,KAAK,YAAY;AACrD,aAAO,oBAAoB,sBAAsB,KAAK,gBAAgB;AACtE,aAAO,oBAAoB,YAAY,KAAK,cAAc;AAAA,IAC9D;AACA,QAAI,OAAO,aAAa,aAAa;AACjC,eAAS,oBAAoB,oBAAoB,KAAK,sBAAsB;AAAA,IAChF;AACA,QAAI,OAAO,YAAY,eAAe,OAAO,QAAQ,mBAAmB,YAAY;AAChF,cAAQ,eAAe,qBAAqB,KAAK,oBAAoB;AACrE,cAAQ,eAAe,sBAAsB,KAAK,oBAAoB;AAAA,IAC1E;AAAA,EACJ;AACJ;;;ACvjBA,SAAS,WAAW,KAAqB;AACrC,QAAM,IAAI,IAAI,OAAO,MAAM;AAC3B,SAAO,MAAM,KAAK,MAAM,IAAI,MAAM,GAAG,CAAC;AAC1C;AASO,SAAS,mBACZ,eACA,SACA,MACI;AACJ,QAAM,YAAY,MAAM,aAAa;AACrC,QAAM,gBAAgB,MAAM,iBAAiB;AAC7C,QAAM,cAAc,MAAM,eAAe,CAAC;AAG1C,gBAAc,aAAa,QAAQ,IAAI,CAAC,WAAgB;AACpD,WAAO,WAAW,EAAE,WAAW,KAAK,IAAI,EAAE;AAC1C,WAAO;AAAA,EACX,CAAC;AAED,gBAAc,aAAa,SAAS;AAAA,IAChC,CAAC,aAAkB;AACf,YAAM,MAAc,WAAW,SAAS,QAAQ,OAAO,EAAE;AACzD,UAAI,YAAY,KAAK,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC,EAAG,QAAO;AAErD,YAAM,aAAqB,SAAS,UAAU;AAC9C,YAAM,YAAoB,SAAS,UAAU,cAAc,KAAK;AAChE,YAAM,aAAa,SAAS,QAAQ,UAAU,YACxC,KAAK,IAAI,IAAI,SAAS,OAAO,SAAS,YACtC;AAIN,UAAI,cAAc,WAAW;AACzB,cAAM,QAAkB,cAAc,MAAM,UAAU;AACtD,cAAM,OACF,cAAc,MAAM,6BAA6B;AAErD,gBAAQ,KAAK,MAAM,OAAO;AAAA,UACtB;AAAA,UACA,MAAM;AAAA,YACF,SAAS,SAAS,QAAQ,UAAU,IAAI,YAAY;AAAA,YACpD;AAAA,YACA,aAAa;AAAA,YACb,OAAO,SAAS,MAAM;AAAA,YACtB,eAAe,SAAS,MAAM;AAAA,YAC9B,aAAa;AAAA,UACjB;AAAA,QACJ,CAAC;AAED,eAAO;AAAA,MACX;AAGA,UAAI,iBAAiB,aAAa,GAAG;AACjC,gBAAQ,KAAK,uBAAuB;AAAA,UAChC;AAAA,UACA,MAAM;AAAA,YACF,SAAS,SAAS,QAAQ,UAAU,IAAI,YAAY;AAAA,YACpD;AAAA,YACA,aAAa;AAAA,YACb,aAAa;AAAA,UACjB;AAAA,QACJ,CAAC;AAAA,MACL;AAEA,aAAO;AAAA,IACX;AAAA,IACA,CAAC,UAAe;AACZ,YAAM,MAAc,WAAW,MAAM,QAAQ,OAAO,EAAE;AACtD,UAAI,YAAY,KAAK,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC,GAAG;AAC1C,eAAO,QAAQ,OAAO,KAAK;AAAA,MAC/B;AAEA,YAAM,aAAa,MAAM,QAAQ,UAAU,YACrC,KAAK,IAAI,IAAI,MAAM,OAAO,SAAS,YACnC;AAGN,UAAI,CAAC,MAAM,UAAU;AACjB,gBAAQ,MAAM,6BAA6B;AAAA,UACvC,MAAM;AAAA,YACF,SAAS,MAAM,QAAQ,UAAU,IAAI,YAAY;AAAA,YACjD;AAAA,YACA,YAAY,MAAM;AAAA,YAClB,eAAe,MAAM;AAAA,YACrB,aAAa;AAAA,UACjB;AAAA,QACJ,CAAC;AACD,eAAO,QAAQ,OAAO,KAAK;AAAA,MAC/B;AAGA,YAAM,aAAqB,MAAM,SAAS,UAAU;AACpD,YAAM,YAAoB,MAAM,SAAS,UAAU,cAAc,KAAK;AAEtE,UAAI,cAAc,WAAW;AACzB,cAAM,QAAkB,cAAc,MAAM,UAAU;AACtD,cAAM,OACF,cAAc,MAAM,6BAA6B;AAErD,gBAAQ,KAAK,MAAM,OAAO;AAAA,UACtB;AAAA,UACA,MAAM;AAAA,YACF,SAAS,MAAM,QAAQ,UAAU,IAAI,YAAY;AAAA,YACjD;AAAA,YACA,aAAa;AAAA,YACb,OAAO,MAAM,SAAS,MAAM;AAAA,YAC5B,eAAe,MAAM,SAAS,MAAM;AAAA,YACpC,aAAa;AAAA,UACjB;AAAA,QACJ,CAAC;AAAA,MACL;AAEA,aAAO,QAAQ,OAAO,KAAK;AAAA,IAC/B;AAAA,EACJ;AACJ;","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,80 @@
|
|
|
1
|
+
// src/ids.ts
|
|
2
|
+
var CORRELATION_ID = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9a-fA-F]{8,64})$/;
|
|
3
|
+
function isValidCorrelationId(id) {
|
|
4
|
+
return id === "" || CORRELATION_ID.test(id);
|
|
5
|
+
}
|
|
6
|
+
function randomBytes(n) {
|
|
7
|
+
const out = new Uint8Array(n);
|
|
8
|
+
const c = globalThis.crypto;
|
|
9
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
10
|
+
c.getRandomValues(out);
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
for (let i = 0; i < n; i++) out[i] = Math.floor(Math.random() * 256);
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
function hex(bytes) {
|
|
17
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
18
|
+
}
|
|
19
|
+
function newRequestId() {
|
|
20
|
+
return hex(randomBytes(8));
|
|
21
|
+
}
|
|
22
|
+
function newJobId() {
|
|
23
|
+
return hex(randomBytes(8));
|
|
24
|
+
}
|
|
25
|
+
function newTraceId() {
|
|
26
|
+
const b = randomBytes(16);
|
|
27
|
+
b[6] = b[6] & 15 | 64;
|
|
28
|
+
b[8] = b[8] & 63 | 128;
|
|
29
|
+
const h = hex(b);
|
|
30
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
1
33
|
// src/client.ts
|
|
2
34
|
var DEFAULT_FLUSH_INTERVAL = 2e3;
|
|
3
35
|
var DEFAULT_BATCH_SIZE = 20;
|
|
4
36
|
var MAX_QUEUE_SIZE = 500;
|
|
5
|
-
var
|
|
37
|
+
var MAX_LINE_BYTES = 1e6;
|
|
38
|
+
var MAX_FIELD_CHARS = 4096;
|
|
39
|
+
var KEEPALIVE_MAX_BYTES = 6e4;
|
|
40
|
+
var BASE_BACKOFF_MS = 1e3;
|
|
41
|
+
var MAX_BACKOFF_MS = 6e4;
|
|
42
|
+
var GROUPING_KEYS = ["error", "error_message", "message", "path", "uri", "method", "reason", "status_code"];
|
|
43
|
+
function classify(status) {
|
|
44
|
+
if (status >= 200 && status < 400) return "delivered";
|
|
45
|
+
if (status === 408 || status === 429) return "retryable";
|
|
46
|
+
if (status === 401 || status === 403 || status === 404 || status === 405) return "misconfigured";
|
|
47
|
+
if (status >= 400 && status < 500) return "rejected";
|
|
48
|
+
return "retryable";
|
|
49
|
+
}
|
|
50
|
+
function bisectBudget(n) {
|
|
51
|
+
let depth = 0;
|
|
52
|
+
for (let x = n; x > 1; x = Math.ceil(x / 2)) depth++;
|
|
53
|
+
return 4 * depth + 4;
|
|
54
|
+
}
|
|
55
|
+
var encoder;
|
|
56
|
+
function byteLength(s) {
|
|
57
|
+
if (typeof TextEncoder === "undefined") return s.length * 3;
|
|
58
|
+
encoder ??= new TextEncoder();
|
|
59
|
+
return encoder.encode(s).length;
|
|
60
|
+
}
|
|
61
|
+
function normalizeLevel(level) {
|
|
62
|
+
const l = (level || "info").toLowerCase();
|
|
63
|
+
return l === "warning" ? "warn" : l;
|
|
64
|
+
}
|
|
65
|
+
var Monitor = class _Monitor {
|
|
6
66
|
config;
|
|
7
67
|
ignoreErrors = [];
|
|
68
|
+
onDrop;
|
|
8
69
|
queue = [];
|
|
9
70
|
timer = null;
|
|
10
71
|
userId = "";
|
|
11
|
-
jobId
|
|
72
|
+
jobId;
|
|
12
73
|
active = false;
|
|
74
|
+
backoffUntil = 0;
|
|
75
|
+
failures = 0;
|
|
76
|
+
warnedMisconfigured = false;
|
|
77
|
+
counters = { enqueued: 0, flushed: 0, dropped: 0, quarantined: 0 };
|
|
13
78
|
constructor(config) {
|
|
14
79
|
this.config = {
|
|
15
80
|
service: config.service,
|
|
@@ -21,6 +86,8 @@ var Monitor = class {
|
|
|
21
86
|
debug: config.debug ?? false
|
|
22
87
|
};
|
|
23
88
|
this.ignoreErrors = config.ignoreErrors ?? [];
|
|
89
|
+
this.onDrop = config.onDrop;
|
|
90
|
+
this.jobId = newJobId();
|
|
24
91
|
this.start();
|
|
25
92
|
if (config.captureErrors !== false) {
|
|
26
93
|
this.installErrorHandler();
|
|
@@ -37,29 +104,47 @@ var Monitor = class {
|
|
|
37
104
|
clearUser() {
|
|
38
105
|
this.userId = "";
|
|
39
106
|
}
|
|
40
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Set a persistent job ID (session-level identifier). It must be a UUID or
|
|
109
|
+
* 8-64 hex characters — see `isValidCorrelationId`; anything else is
|
|
110
|
+
* cleared from each event and kept in data.invalid_job_id.
|
|
111
|
+
*/
|
|
41
112
|
setJobId(jobId) {
|
|
42
113
|
this.jobId = jobId;
|
|
43
114
|
}
|
|
44
115
|
/** Emit an event at a specific level */
|
|
45
116
|
emit(name, level, opts) {
|
|
46
117
|
if (!this.active) return;
|
|
118
|
+
let data = opts?.data ?? {};
|
|
119
|
+
const repair = (field, value) => {
|
|
120
|
+
if (isValidCorrelationId(value)) return value;
|
|
121
|
+
data = { ...data, [`invalid_${field}`]: value.slice(0, 128) };
|
|
122
|
+
if (this.config.debug) {
|
|
123
|
+
console.warn(`[monitor] cleared invalid ${field} ${JSON.stringify(value)} (monitor-core accepts a UUID or 8-64 hex characters)`);
|
|
124
|
+
}
|
|
125
|
+
return "";
|
|
126
|
+
};
|
|
127
|
+
const jobId = repair("job_id", this.jobId);
|
|
128
|
+
const requestId = repair("request_id", opts?.requestId ?? "");
|
|
129
|
+
const traceId = repair("trace_id", opts?.traceId ?? "");
|
|
47
130
|
const event = {
|
|
48
131
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49
132
|
service: this.config.service,
|
|
50
133
|
env: this.config.env,
|
|
51
|
-
job_id:
|
|
52
|
-
request_id:
|
|
53
|
-
trace_id:
|
|
134
|
+
job_id: jobId,
|
|
135
|
+
request_id: requestId,
|
|
136
|
+
trace_id: traceId,
|
|
54
137
|
user_id: opts?.userId ?? this.userId,
|
|
55
|
-
name,
|
|
56
|
-
level,
|
|
57
|
-
data
|
|
138
|
+
name: name || "event.unnamed",
|
|
139
|
+
level: normalizeLevel(level),
|
|
140
|
+
data
|
|
58
141
|
};
|
|
59
142
|
if (this.queue.length >= MAX_QUEUE_SIZE) {
|
|
60
143
|
this.queue.shift();
|
|
144
|
+
this.recordDrop(1);
|
|
61
145
|
}
|
|
62
146
|
this.queue.push(event);
|
|
147
|
+
this.counters.enqueued++;
|
|
63
148
|
if (this.config.debug) {
|
|
64
149
|
console.debug(`[monitor] ${level} ${name}`, opts?.data);
|
|
65
150
|
}
|
|
@@ -87,28 +172,17 @@ var Monitor = class {
|
|
|
87
172
|
fatal(name, opts) {
|
|
88
173
|
this.emit(name, "fatal", opts);
|
|
89
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Lifetime counters. Surface them wherever loss would otherwise go
|
|
177
|
+
* unnoticed: the system that would report dropped telemetry is the one
|
|
178
|
+
* dropping it.
|
|
179
|
+
*/
|
|
180
|
+
stats() {
|
|
181
|
+
return { ...this.counters, queued: this.queue.length };
|
|
182
|
+
}
|
|
90
183
|
/** Flush all queued events to the ingest endpoint */
|
|
91
184
|
flush() {
|
|
92
|
-
|
|
93
|
-
const batch = this.queue.splice(0);
|
|
94
|
-
const payload = batch.map((e) => JSON.stringify(e)).join("\n");
|
|
95
|
-
if (typeof fetch === "undefined") return;
|
|
96
|
-
fetch(this.config.ingestUrl, {
|
|
97
|
-
method: "POST",
|
|
98
|
-
headers: {
|
|
99
|
-
"Content-Type": "application/x-ndjson",
|
|
100
|
-
"X-Api-Key": this.config.apiKey
|
|
101
|
-
},
|
|
102
|
-
body: payload,
|
|
103
|
-
keepalive: true
|
|
104
|
-
}).catch((err) => {
|
|
105
|
-
if (this.config.debug) {
|
|
106
|
-
console.warn("[monitor] flush failed:", err);
|
|
107
|
-
}
|
|
108
|
-
if (this.queue.length + batch.length <= MAX_QUEUE_SIZE) {
|
|
109
|
-
this.queue = batch.concat(this.queue);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
185
|
+
this.flushQueue(false);
|
|
112
186
|
}
|
|
113
187
|
/** Stop the monitor and flush remaining events */
|
|
114
188
|
shutdown() {
|
|
@@ -116,14 +190,144 @@ var Monitor = class {
|
|
|
116
190
|
clearInterval(this.timer);
|
|
117
191
|
this.timer = null;
|
|
118
192
|
}
|
|
119
|
-
this.
|
|
193
|
+
this.flushQueue(true);
|
|
120
194
|
this.removeListeners();
|
|
121
195
|
this.active = false;
|
|
122
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* @param unloading the page (or process) is going away: ignore the backoff,
|
|
199
|
+
* since this is the last chance these events get.
|
|
200
|
+
*/
|
|
201
|
+
flushQueue(unloading) {
|
|
202
|
+
if (this.queue.length === 0) return;
|
|
203
|
+
if (typeof fetch === "undefined") return;
|
|
204
|
+
if (!unloading && Date.now() < this.backoffUntil) return;
|
|
205
|
+
const batch = this.queue.splice(0);
|
|
206
|
+
this.send(batch, { remaining: bisectBudget(batch.length) });
|
|
207
|
+
}
|
|
208
|
+
send(events, budget) {
|
|
209
|
+
const lines = [];
|
|
210
|
+
const sent = [];
|
|
211
|
+
for (const e of events) {
|
|
212
|
+
const line = this.serialize(e);
|
|
213
|
+
if (line === null) {
|
|
214
|
+
this.recordDrop(1);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
lines.push(line);
|
|
218
|
+
sent.push(e);
|
|
219
|
+
}
|
|
220
|
+
if (lines.length === 0) return;
|
|
221
|
+
const body = lines.join("\n");
|
|
222
|
+
let request;
|
|
223
|
+
try {
|
|
224
|
+
request = fetch(this.config.ingestUrl, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: {
|
|
227
|
+
"Content-Type": "application/x-ndjson",
|
|
228
|
+
"X-Api-Key": this.config.apiKey
|
|
229
|
+
},
|
|
230
|
+
body,
|
|
231
|
+
keepalive: byteLength(body) <= KEEPALIVE_MAX_BYTES
|
|
232
|
+
});
|
|
233
|
+
} catch (err) {
|
|
234
|
+
request = Promise.reject(err);
|
|
235
|
+
}
|
|
236
|
+
request.then(
|
|
237
|
+
(res) => this.handleResponse(res, sent, budget),
|
|
238
|
+
(err) => {
|
|
239
|
+
if (this.config.debug) {
|
|
240
|
+
console.warn("[monitor] flush failed:", err);
|
|
241
|
+
}
|
|
242
|
+
this.retryLater(sent);
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
handleResponse(res, events, budget) {
|
|
247
|
+
const status = typeof res?.status === "number" ? res.status : 0;
|
|
248
|
+
const outcome = res?.ok ? "delivered" : classify(status);
|
|
249
|
+
switch (outcome) {
|
|
250
|
+
case "delivered":
|
|
251
|
+
this.counters.flushed += events.length;
|
|
252
|
+
this.failures = 0;
|
|
253
|
+
this.backoffUntil = 0;
|
|
254
|
+
return;
|
|
255
|
+
case "rejected":
|
|
256
|
+
if (events.length > 1 && budget.remaining > 0) {
|
|
257
|
+
budget.remaining--;
|
|
258
|
+
const mid = events.length >> 1;
|
|
259
|
+
this.send(events.slice(0, mid), budget);
|
|
260
|
+
this.send(events.slice(mid), budget);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
this.counters.quarantined += events.length;
|
|
264
|
+
this.recordDrop(events.length);
|
|
265
|
+
if (this.config.debug) {
|
|
266
|
+
console.warn(`[monitor] ingest rejected ${events.length} event(s) as malformed (status ${status}):`, events.map((e) => e.name));
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
case "misconfigured":
|
|
270
|
+
this.recordDrop(events.length);
|
|
271
|
+
if (!this.warnedMisconfigured) {
|
|
272
|
+
this.warnedMisconfigured = true;
|
|
273
|
+
console.warn(`[monitor] ingest refused events with status ${status} \u2014 check ingestUrl and apiKey. Events are being dropped.`);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
default:
|
|
277
|
+
this.retryLater(events);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Put events back at the front of the queue and back off before retrying. */
|
|
281
|
+
retryLater(events) {
|
|
282
|
+
this.failures++;
|
|
283
|
+
const ceiling = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** Math.min(this.failures - 1, 16));
|
|
284
|
+
this.backoffUntil = Date.now() + 50 + Math.random() * ceiling;
|
|
285
|
+
const room = MAX_QUEUE_SIZE - this.queue.length;
|
|
286
|
+
const keep = room <= 0 ? [] : events.length > room ? events.slice(events.length - room) : events;
|
|
287
|
+
this.recordDrop(events.length - keep.length);
|
|
288
|
+
if (keep.length > 0) {
|
|
289
|
+
this.queue = keep.concat(this.queue);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* One NDJSON line for e, or null if it cannot be serialized. Never throws:
|
|
294
|
+
* flush runs inside emit's auto-flush, and emit must never throw into the
|
|
295
|
+
* caller.
|
|
296
|
+
*/
|
|
297
|
+
serialize(e) {
|
|
298
|
+
try {
|
|
299
|
+
const line = JSON.stringify(e);
|
|
300
|
+
if (line.length <= MAX_LINE_BYTES / 3) return line;
|
|
301
|
+
const size = byteLength(line);
|
|
302
|
+
if (size <= MAX_LINE_BYTES) return line;
|
|
303
|
+
const kept = { truncated: true, original_size_bytes: size };
|
|
304
|
+
for (const k of GROUPING_KEYS) {
|
|
305
|
+
const v = e.data[k];
|
|
306
|
+
if (typeof v === "string") kept[k] = v.slice(0, MAX_FIELD_CHARS);
|
|
307
|
+
else if (typeof v === "number" || typeof v === "boolean") kept[k] = v;
|
|
308
|
+
}
|
|
309
|
+
const shrunk = JSON.stringify({ ...e, data: kept });
|
|
310
|
+
return byteLength(shrunk) <= MAX_LINE_BYTES ? shrunk : null;
|
|
311
|
+
} catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
recordDrop(n) {
|
|
316
|
+
if (n <= 0) return;
|
|
317
|
+
this.counters.dropped += n;
|
|
318
|
+
if (this.onDrop) {
|
|
319
|
+
try {
|
|
320
|
+
this.onDrop(this.counters.dropped);
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
123
325
|
start() {
|
|
124
326
|
if (this.active) return;
|
|
125
327
|
this.active = true;
|
|
126
|
-
|
|
328
|
+
const t = setInterval(() => this.flush(), this.config.flushInterval);
|
|
329
|
+
if (typeof t.unref === "function") t.unref();
|
|
330
|
+
this.timer = t;
|
|
127
331
|
if (typeof document !== "undefined") {
|
|
128
332
|
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
129
333
|
}
|
|
@@ -133,11 +337,11 @@ var Monitor = class {
|
|
|
133
337
|
}
|
|
134
338
|
handleVisibilityChange = () => {
|
|
135
339
|
if (document.visibilityState === "hidden") {
|
|
136
|
-
this.
|
|
340
|
+
this.flushQueue(true);
|
|
137
341
|
}
|
|
138
342
|
};
|
|
139
343
|
handlePageHide = () => {
|
|
140
|
-
this.
|
|
344
|
+
this.flushQueue(true);
|
|
141
345
|
};
|
|
142
346
|
shouldIgnoreError(message, stack) {
|
|
143
347
|
if (this.ignoreErrors.length === 0) return false;
|
|
@@ -154,6 +358,20 @@ var Monitor = class {
|
|
|
154
358
|
}
|
|
155
359
|
return false;
|
|
156
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* The route a browser error happened on.
|
|
363
|
+
*
|
|
364
|
+
* Deliberately `pathname` only — never the search string or hash. Query
|
|
365
|
+
* parameters routinely carry tokens, emails and other personal data, and this
|
|
366
|
+
* value is both stored on the event and folded into the server-side issue
|
|
367
|
+
* fingerprint, so anything included here is retained and grouped on.
|
|
368
|
+
*
|
|
369
|
+
* Returns undefined outside a browser so the Node handlers stay unaffected.
|
|
370
|
+
*/
|
|
371
|
+
currentPath() {
|
|
372
|
+
if (typeof window === "undefined" || !window.location) return void 0;
|
|
373
|
+
return window.location.pathname;
|
|
374
|
+
}
|
|
157
375
|
errorHandler = (event) => {
|
|
158
376
|
const stack = event.error?.stack;
|
|
159
377
|
if (this.shouldIgnoreError(event.message ?? "", stack)) return;
|
|
@@ -163,7 +381,8 @@ var Monitor = class {
|
|
|
163
381
|
filename: event.filename,
|
|
164
382
|
lineno: event.lineno,
|
|
165
383
|
colno: event.colno,
|
|
166
|
-
stack
|
|
384
|
+
stack,
|
|
385
|
+
path: this.currentPath()
|
|
167
386
|
}
|
|
168
387
|
});
|
|
169
388
|
};
|
|
@@ -175,18 +394,90 @@ var Monitor = class {
|
|
|
175
394
|
this.emit("client.error.unhandled_rejection", "error", {
|
|
176
395
|
data: {
|
|
177
396
|
message,
|
|
178
|
-
stack
|
|
397
|
+
stack,
|
|
398
|
+
path: this.currentPath()
|
|
179
399
|
}
|
|
180
400
|
});
|
|
181
401
|
};
|
|
402
|
+
// --- Node process handlers ---
|
|
403
|
+
// Adding an uncaughtException or unhandledRejection listener changes what Node
|
|
404
|
+
// does. With no listener, either one prints the error and exits with code 1;
|
|
405
|
+
// with any listener, Node assumes it was handled and keeps running — in
|
|
406
|
+
// whatever state the failure left it. So when this SDK is the only listener,
|
|
407
|
+
// it reports the error and then does what Node would have done. When the app
|
|
408
|
+
// has a listener of its own, the app has already decided; the SDK only reports.
|
|
409
|
+
/** Grace for the final batch to leave before a Node-style crash exits. */
|
|
410
|
+
static NODE_CRASH_GRACE_MS = 1500;
|
|
411
|
+
/** Rejections already reported, so the re-raise below is not reported twice. */
|
|
412
|
+
reportedRejections = /* @__PURE__ */ new WeakSet();
|
|
413
|
+
nodeExceptionHandler = (err) => {
|
|
414
|
+
const alreadyReported = typeof err === "object" && err !== null && this.reportedRejections.has(err);
|
|
415
|
+
const e = err;
|
|
416
|
+
const message = e?.message ?? String(err);
|
|
417
|
+
const stack = e?.stack;
|
|
418
|
+
if (!alreadyReported && !this.shouldIgnoreError(message, stack)) {
|
|
419
|
+
this.emit("client.error.uncaught", "error", {
|
|
420
|
+
data: {
|
|
421
|
+
message,
|
|
422
|
+
stack
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
if (this.isSoleListener("uncaughtException")) {
|
|
427
|
+
this.crashLikeNode(err);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
nodeRejectionHandler = (reason) => {
|
|
431
|
+
const r = reason;
|
|
432
|
+
const message = r?.message ?? String(reason);
|
|
433
|
+
const stack = r?.stack;
|
|
434
|
+
if (!this.shouldIgnoreError(message, stack)) {
|
|
435
|
+
this.emit("client.error.unhandled_rejection", "error", {
|
|
436
|
+
data: {
|
|
437
|
+
message,
|
|
438
|
+
stack
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
if (this.isSoleListener("unhandledRejection")) {
|
|
443
|
+
if (typeof reason === "object" && reason !== null) {
|
|
444
|
+
this.reportedRejections.add(reason);
|
|
445
|
+
}
|
|
446
|
+
this.reraise(reason);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
/** Hand an unhandled rejection back to Node as an uncaught exception. */
|
|
450
|
+
reraise(reason) {
|
|
451
|
+
process?.nextTick?.(() => {
|
|
452
|
+
throw reason;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/** True when this instance's own handler is the only listener for the event. */
|
|
456
|
+
isSoleListener(event) {
|
|
457
|
+
const count = typeof process === "undefined" ? void 0 : process.listenerCount;
|
|
458
|
+
if (typeof count !== "function") {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
return count.call(process, event) <= 1;
|
|
462
|
+
}
|
|
463
|
+
/** Print the error as Node would, give the batch a moment to leave, exit 1. */
|
|
464
|
+
crashLikeNode(err) {
|
|
465
|
+
console.error(err);
|
|
466
|
+
this.flush();
|
|
467
|
+
setTimeout(() => process?.exit?.(1), _Monitor.NODE_CRASH_GRACE_MS);
|
|
468
|
+
}
|
|
182
469
|
installErrorHandler() {
|
|
183
470
|
if (typeof window !== "undefined") {
|
|
184
471
|
window.addEventListener("error", this.errorHandler);
|
|
472
|
+
} else if (typeof process !== "undefined" && typeof process.on === "function") {
|
|
473
|
+
process.on("uncaughtException", this.nodeExceptionHandler);
|
|
185
474
|
}
|
|
186
475
|
}
|
|
187
476
|
installRejectionHandler() {
|
|
188
477
|
if (typeof window !== "undefined") {
|
|
189
478
|
window.addEventListener("unhandledrejection", this.rejectionHandler);
|
|
479
|
+
} else if (typeof process !== "undefined" && typeof process.on === "function") {
|
|
480
|
+
process.on("unhandledRejection", this.nodeRejectionHandler);
|
|
190
481
|
}
|
|
191
482
|
}
|
|
192
483
|
removeListeners() {
|
|
@@ -198,10 +489,18 @@ var Monitor = class {
|
|
|
198
489
|
if (typeof document !== "undefined") {
|
|
199
490
|
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
200
491
|
}
|
|
492
|
+
if (typeof process !== "undefined" && typeof process.removeListener === "function") {
|
|
493
|
+
process.removeListener("uncaughtException", this.nodeExceptionHandler);
|
|
494
|
+
process.removeListener("unhandledRejection", this.nodeRejectionHandler);
|
|
495
|
+
}
|
|
201
496
|
}
|
|
202
497
|
};
|
|
203
498
|
|
|
204
499
|
// src/axios.ts
|
|
500
|
+
function stripQuery(url) {
|
|
501
|
+
const i = url.search(/[?#]/);
|
|
502
|
+
return i === -1 ? url : url.slice(0, i);
|
|
503
|
+
}
|
|
205
504
|
function attachAxiosMonitor(axiosInstance, monitor, opts) {
|
|
206
505
|
const minStatus = opts?.minStatus ?? 400;
|
|
207
506
|
const reportSuccess = opts?.reportSuccess ?? false;
|
|
@@ -212,7 +511,7 @@ function attachAxiosMonitor(axiosInstance, monitor, opts) {
|
|
|
212
511
|
});
|
|
213
512
|
axiosInstance.interceptors.response.use(
|
|
214
513
|
(response) => {
|
|
215
|
-
const url = response.config?.url ?? "";
|
|
514
|
+
const url = stripQuery(response.config?.url ?? "");
|
|
216
515
|
if (ignorePaths.some((p) => url.includes(p))) return response;
|
|
217
516
|
const statusCode = response.status ?? 0;
|
|
218
517
|
const requestId = response.headers?.["x-request-id"] ?? "";
|
|
@@ -247,7 +546,7 @@ function attachAxiosMonitor(axiosInstance, monitor, opts) {
|
|
|
247
546
|
return response;
|
|
248
547
|
},
|
|
249
548
|
(error) => {
|
|
250
|
-
const url = error.config?.url ?? "";
|
|
549
|
+
const url = stripQuery(error.config?.url ?? "");
|
|
251
550
|
if (ignorePaths.some((p) => url.includes(p))) {
|
|
252
551
|
return Promise.reject(error);
|
|
253
552
|
}
|
|
@@ -287,6 +586,10 @@ function attachAxiosMonitor(axiosInstance, monitor, opts) {
|
|
|
287
586
|
}
|
|
288
587
|
export {
|
|
289
588
|
Monitor,
|
|
290
|
-
attachAxiosMonitor
|
|
589
|
+
attachAxiosMonitor,
|
|
590
|
+
isValidCorrelationId,
|
|
591
|
+
newJobId,
|
|
592
|
+
newRequestId,
|
|
593
|
+
newTraceId
|
|
291
594
|
};
|
|
292
595
|
//# sourceMappingURL=index.mjs.map
|