apitally 1.0.0-beta.2 → 1.0.0-beta.4

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 (74) hide show
  1. package/README.md +7 -25
  2. package/dist/activation.cjs +2 -1
  3. package/dist/activation.cjs.map +1 -1
  4. package/dist/activation.d.cts +1 -0
  5. package/dist/activation.d.ts +1 -0
  6. package/dist/activation.js +2 -1
  7. package/dist/activation.js.map +1 -1
  8. package/dist/consumers.cjs +96 -0
  9. package/dist/consumers.cjs.map +1 -0
  10. package/dist/consumers.d.cts +13 -0
  11. package/dist/consumers.d.ts +13 -0
  12. package/dist/consumers.js +93 -0
  13. package/dist/consumers.js.map +1 -0
  14. package/dist/context.cjs +2 -8
  15. package/dist/context.cjs.map +1 -1
  16. package/dist/context.d.cts +5 -5
  17. package/dist/context.d.ts +5 -5
  18. package/dist/context.js +3 -7
  19. package/dist/context.js.map +1 -1
  20. package/dist/exportWorker.cjs +6 -5
  21. package/dist/exportWorker.cjs.map +1 -1
  22. package/dist/exportWorker.js +6 -5
  23. package/dist/exportWorker.js.map +1 -1
  24. package/dist/index.cjs +2 -2
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/metrics.cjs +164 -32
  31. package/dist/metrics.cjs.map +1 -1
  32. package/dist/metrics.d.cts +4 -6
  33. package/dist/metrics.d.ts +4 -6
  34. package/dist/metrics.js +165 -33
  35. package/dist/metrics.js.map +1 -1
  36. package/dist/nestjs/index.cjs +6 -0
  37. package/dist/nestjs/index.cjs.map +1 -1
  38. package/dist/nestjs/index.js +6 -0
  39. package/dist/nestjs/index.js.map +1 -1
  40. package/dist/providers.cjs +0 -8
  41. package/dist/providers.cjs.map +1 -1
  42. package/dist/providers.d.cts +0 -2
  43. package/dist/providers.d.ts +0 -2
  44. package/dist/providers.js +1 -8
  45. package/dist/providers.js.map +1 -1
  46. package/dist/requestObservation.cjs +6 -4
  47. package/dist/requestObservation.cjs.map +1 -1
  48. package/dist/requestObservation.js +7 -5
  49. package/dist/requestObservation.js.map +1 -1
  50. package/dist/serverErrors.cjs +30 -28
  51. package/dist/serverErrors.cjs.map +1 -1
  52. package/dist/serverErrors.js +30 -28
  53. package/dist/serverErrors.js.map +1 -1
  54. package/dist/spanProcessor.cjs +0 -2
  55. package/dist/spanProcessor.cjs.map +1 -1
  56. package/dist/spanProcessor.js +0 -2
  57. package/dist/spanProcessor.js.map +1 -1
  58. package/dist/spool.cjs +5 -0
  59. package/dist/spool.cjs.map +1 -1
  60. package/dist/spool.d.cts +2 -1
  61. package/dist/spool.d.ts +2 -1
  62. package/dist/spool.js +5 -0
  63. package/dist/spool.js.map +1 -1
  64. package/dist/validationErrors.cjs +29 -26
  65. package/dist/validationErrors.cjs.map +1 -1
  66. package/dist/validationErrors.js +29 -26
  67. package/dist/validationErrors.js.map +1 -1
  68. package/package.json +2 -2
  69. package/dist/consumer.cjs +0 -61
  70. package/dist/consumer.cjs.map +0 -1
  71. package/dist/consumer.d.cts +0 -12
  72. package/dist/consumer.d.ts +0 -12
  73. package/dist/consumer.js +0 -59
  74. package/dist/consumer.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"spanProcessor.js","names":[],"sources":["../src/spanProcessor.ts"],"sourcesContent":["import { type Attributes, type Context, SpanKind } from \"@opentelemetry/api\";\nimport { hrTime, hrTimeDuration } from \"@opentelemetry/core\";\nimport type { ReadableSpan, Span, SpanProcessor } from \"@opentelemetry/sdk-trace-base\";\nimport {\n type ApitallyConfig,\n compilePatterns,\n DEFAULT_EXCLUDE_PATHS,\n EXCLUDE_USER_AGENTS,\n getConfig,\n matchesAny,\n type SamplingCallback,\n} from \"./config.js\";\nimport { writeConsumerAttributesFromContext } from \"./consumer.js\";\nimport {\n REQUEST_RECORD_KEY,\n type RequestDropReason,\n type RequestRecord,\n SPAN_HANDLE_KEY,\n type SpanHandle,\n} from \"./context.js\";\nimport { logDebug, logWarning } from \"./logger.js\";\n\nconst MAX_BUFFERED_SPANS = 1_000;\nconst MAX_TRACKED_SPAN_IDS = MAX_BUFFERED_SPANS + 1;\nconst MAX_STASHED_REQUESTS = 2_048;\nconst MAX_KEPT_SPAN_IDS = 10_000;\nconst EXCLUDE_USER_AGENT_PATTERNS = compilePatterns(EXCLUDE_USER_AGENTS);\n\n// Transport-captured headers and bodies are attached only to Apitally's export\n// copy.\nexport interface RequestStash {\n requestHeaders?: Record<string, string | string[]>;\n requestBody?: Buffer;\n requestContentEncoding?: string;\n responseHeaders?: Record<string, string | string[]>;\n responseBody?: Buffer;\n responseContentEncoding?: string;\n}\n\nexport interface ApitallySpanData {\n record?: RequestRecord;\n stash?: RequestStash;\n demoteToInternal?: boolean;\n}\n\n// Mutable ReadableSpan-shaped copy on Apitally's private export path; the span\n// handed to user-attached processors is never mutated.\nexport type SpanCopy = {\n -readonly [Key in keyof ReadableSpan]: ReadableSpan[Key];\n} & { apitallyData?: ApitallySpanData };\n\n// The public processor delegates to the process-global pipeline. Construction\n// only declares that the application owns the tracer provider.\nexport class ApitallySpanProcessor implements SpanProcessor {\n constructor() {\n setApitallySpanProcessorDeclared();\n }\n\n onStart(span: Span, parentContext: Context): void {\n let pipeline = getActiveSpanPipeline();\n if (!pipeline && span.kind === SpanKind.SERVER) {\n getServerSpanActivationCallback()?.();\n pipeline = getActiveSpanPipeline();\n }\n pipeline?.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n getActiveSpanPipeline()?.onEnd(span);\n }\n\n // Flush failures resolve instead of rejecting into the user's provider.\n forceFlush(): Promise<void> {\n const pipeline = getActiveSpanPipeline();\n if (!pipeline) {\n return Promise.resolve();\n }\n return pipeline.forceFlush().catch((error: unknown) => {\n logDebug(`Error flushing spans: ${String(error)}`);\n });\n }\n\n // A user provider's shutdown only flushes released requests; SDK shutdown owns\n // teardown.\n shutdown(): Promise<void> {\n return this.forceFlush();\n }\n}\n\ntype ServerSpanActivationCallback = () => void;\n\n// Symbol.for keys let ESM and CJS builds share processor coordination.\nconst ACTIVE_SPAN_PIPELINE_KEY = Symbol.for(\"apitally.activeSpanPipeline\");\nconst APITALLY_SPAN_PROCESSOR_DECLARED_KEY = Symbol.for(\"apitally.spanProcessorDeclared\");\nconst SERVER_SPAN_ACTIVATION_CALLBACK_KEY = Symbol.for(\"apitally.serverSpanActivationCallback\");\n\nexport function setActiveSpanPipeline(pipeline: SpanPipeline | undefined): void {\n (globalThis as Record<symbol, SpanPipeline | undefined>)[ACTIVE_SPAN_PIPELINE_KEY] = pipeline;\n}\n\nexport function getActiveSpanPipeline(): SpanPipeline | undefined {\n return (globalThis as Record<symbol, SpanPipeline | undefined>)[ACTIVE_SPAN_PIPELINE_KEY];\n}\n\nexport function setServerSpanActivationCallback(callback: ServerSpanActivationCallback): void {\n const holder = globalThis as Record<symbol, ServerSpanActivationCallback | undefined>;\n holder[SERVER_SPAN_ACTIVATION_CALLBACK_KEY] ??= callback;\n}\n\nexport function isApitallySpanProcessorDeclared(): boolean {\n return (\n (globalThis as Record<symbol, boolean | undefined>)[APITALLY_SPAN_PROCESSOR_DECLARED_KEY] ===\n true\n );\n}\n\nexport function resetSpanProcessorState(): void {\n const holder = globalThis as Record<symbol, unknown>;\n delete holder[ACTIVE_SPAN_PIPELINE_KEY];\n delete holder[APITALLY_SPAN_PROCESSOR_DECLARED_KEY];\n delete holder[SERVER_SPAN_ACTIVATION_CALLBACK_KEY];\n}\n\nfunction setApitallySpanProcessorDeclared(): void {\n (globalThis as Record<symbol, boolean>)[APITALLY_SPAN_PROCESSOR_DECLARED_KEY] = true;\n}\n\nfunction getServerSpanActivationCallback(): ServerSpanActivationCallback | undefined {\n return (globalThis as Record<symbol, ServerSpanActivationCallback | undefined>)[\n SERVER_SPAN_ACTIVATION_CALLBACK_KEY\n ];\n}\n\ninterface RequestEntry {\n readonly serverSpanId: string;\n readonly serverSpan: Span;\n readonly spanIds: Set<string>;\n record?: RequestRecord;\n buffered: ReadableSpan[];\n endedServerSpan?: ReadableSpan;\n transportCompleted: boolean;\n released: boolean;\n}\n\ninterface KeptSpanEntry {\n readonly serverSpanId: string;\n // Carries the demotion decision across release for spans that end late.\n readonly demoted: boolean;\n}\n\n// Requests remain buffered until the SERVER span and transport complete, then\n// one keep/drop decision releases them once.\nexport class SpanPipeline implements SpanProcessor {\n // Callbacks are optional because metrics and log pipelines start at activation.\n metricsRecorder?: (record: RequestRecord) => void;\n onRequestFinished?: (serverSpanId: string, kept: boolean) => void;\n private readonly downstream: SpanProcessor;\n private readonly config: ApitallyConfig;\n private readonly sampleRateBound: bigint;\n private readonly excludePathPatterns: RegExp[];\n // Every span ID remains mapped to one request until completion so late logs\n // resolve. A miss means dropped, completed, or non-request telemetry.\n private readonly requests = new Map<string, RequestEntry>();\n private readonly stash = new Map<string, RequestStash>();\n private readonly demotedSpanIds = new Set<string>();\n // Kept span IDs remain resolvable after release for late spans and logs; the\n // oldest are evicted at the cap.\n private readonly keptSpanIds = new Map<string, KeptSpanEntry>();\n\n constructor(\n downstream: SpanProcessor,\n private readonly flushExporter?: () => Promise<void>,\n ) {\n this.downstream = downstream;\n this.config = getConfig();\n this.sampleRateBound = boundForSampleRate(this.config.sampleRate);\n this.excludePathPatterns = compilePatterns(DEFAULT_EXCLUDE_PATHS, this.config.excludePaths);\n }\n\n onStart(span: Span, parentContext: Context): void {\n try {\n if (isContribPerMessageSpan(span)) {\n return;\n }\n const parent = span.parentSpanContext;\n if (!parent || parent.isRemote) {\n if (span.kind === SpanKind.SERVER) {\n this.startRequest(span, parentContext);\n }\n return;\n }\n const entry = this.requests.get(parent.spanId);\n const spanId = span.spanContext().spanId;\n if (!entry) {\n // A span started under a request already released as kept exports at\n // its end, like the released request's other late telemetry.\n const kept = this.keptSpanIds.get(parent.spanId);\n if (kept) {\n this.addKeptSpanId(spanId, {\n serverSpanId: kept.serverSpanId,\n demoted: false,\n });\n }\n return;\n }\n if (entry.spanIds.size >= MAX_TRACKED_SPAN_IDS) {\n logDebug(\"Apitally span tracking cap reached, dropping the span\");\n return;\n }\n if (span.kind === SpanKind.SERVER) {\n // A second middleware produced a SERVER span for this request. Apitally\n // demotes its copy, but user exporters retain the duplicate.\n this.demotedSpanIds.add(spanId);\n logWarning(\n `Detected a duplicate SERVER span produced by the instrumentation scope \"${span.instrumentationScope?.name ?? \"unknown\"}\" inside an active request. Apitally exports it as an INTERNAL span, but your own OpenTelemetry exporters still receive the duplicate. To resolve this, remove the middleware that produces it.`,\n );\n }\n this.requests.set(spanId, entry);\n entry.spanIds.add(spanId);\n } catch (error) {\n logWarning(`Error in the Apitally span processor: ${String(error)}`);\n }\n }\n\n onEnd(span: ReadableSpan): void {\n try {\n const spanId = span.spanContext().spanId;\n const isDemoted = this.demotedSpanIds.delete(spanId);\n const entry = this.requests.get(spanId);\n if (!entry) {\n const kept = this.keptSpanIds.get(spanId);\n if (kept) {\n let exportSpan = span;\n if (kept.demoted) {\n const copy = copySpan(span);\n copy.apitallyData = { demoteToInternal: true };\n exportSpan = copy;\n }\n this.downstream.onEnd(exportSpan);\n }\n return;\n }\n if (spanId === entry.serverSpanId) {\n if (!entry.record) {\n this.removeCompletedRequestSpanIds(entry);\n this.stash.delete(entry.serverSpanId);\n this.onRequestFinished?.(entry.serverSpanId, false);\n return;\n }\n entry.endedServerSpan = span;\n this.releaseIfComplete(entry);\n return;\n }\n // The span id stays mapped so log records emitted after this span ended\n // still resolve to the request until it completes.\n let exportSpan = span;\n if (isDemoted) {\n const copy = copySpan(span);\n copy.apitallyData = { demoteToInternal: true };\n exportSpan = copy;\n }\n if (entry.buffered.length < MAX_BUFFERED_SPANS) {\n entry.buffered.push(exportSpan);\n } else {\n logDebug(\"Apitally span buffer cap reached, dropping the span\");\n }\n } catch (error) {\n logWarning(`Error in the Apitally span processor: ${String(error)}`);\n }\n }\n\n attachRequestRecord(serverSpanId: string, record: RequestRecord): void {\n const entry = this.requests.get(serverSpanId);\n if (entry) {\n entry.record ??= record;\n }\n }\n\n // Transport completion triggers response sampling and metrics independently\n // of span-end timing. Map misses still record metrics and discard other data.\n handleTransportCompletion(record: RequestRecord): void {\n try {\n record.dropReason = resolveTransportDropReason(record.attributes) ?? record.dropReason;\n const entry =\n record.serverSpanId !== undefined ? this.requests.get(record.serverSpanId) : undefined;\n if (entry && !entry.transportCompleted && !entry.released) {\n entry.record ??= record;\n entry.transportCompleted = true;\n if (!this.isResponseSampledIn(entry)) {\n this.dropRequestOnResponse(entry, record);\n }\n }\n this.metricsRecorder?.(record);\n if (entry) {\n this.releaseIfComplete(entry);\n }\n } catch (error) {\n logWarning(`Error in the Apitally span processor: ${String(error)}`);\n }\n }\n\n // Holds captured headers and bodies until the exporter attaches them to the\n // exported SERVER span copy. Fields already stashed are kept unless replaced.\n updateStash(serverSpanId: string, update: RequestStash): void {\n try {\n // A request without an in-flight map entry can never release, so its\n // stash entry would sit unconsumed until the cap evicts it.\n if (!this.requests.has(serverSpanId)) {\n return;\n }\n let entry = this.stash.get(serverSpanId);\n if (!entry) {\n if (this.stash.size >= MAX_STASHED_REQUESTS) {\n const oldestKey = this.stash.keys().next().value;\n if (oldestKey !== undefined) {\n this.stash.delete(oldestKey);\n logDebug(\"Apitally payload stash cap reached, dropping the oldest entry\");\n }\n }\n entry = {};\n this.stash.set(serverSpanId, entry);\n }\n if (update.requestHeaders !== undefined) {\n entry.requestHeaders = update.requestHeaders;\n }\n if (update.requestBody !== undefined) {\n entry.requestBody = update.requestBody;\n }\n if (update.requestContentEncoding !== undefined) {\n entry.requestContentEncoding = update.requestContentEncoding;\n }\n if (update.responseHeaders !== undefined) {\n entry.responseHeaders = update.responseHeaders;\n }\n if (update.responseBody !== undefined) {\n entry.responseBody = update.responseBody;\n }\n if (update.responseContentEncoding !== undefined) {\n entry.responseContentEncoding = update.responseContentEncoding;\n }\n } catch (error) {\n logDebug(`Error updating the request stash: ${String(error)}`);\n }\n }\n\n // Kept requests remain resolvable after release so late logs retain their\n // request association.\n resolveServerSpanId(spanId: string): string | undefined {\n return this.requests.get(spanId)?.serverSpanId ?? this.keptSpanIds.get(spanId)?.serverSpanId;\n }\n\n isRequestInFlight(serverSpanId: string): boolean {\n return this.requests.has(serverSpanId);\n }\n\n // Exclusion answers \"never wanted\" and runs strictly before sampling; an\n // excluded request never invokes a user sampling callback.\n resolveRequestDropReasonBeforeSampling(attributes: Attributes): RequestDropReason | undefined {\n const transportDropReason = resolveTransportDropReason(attributes);\n if (transportDropReason !== undefined) {\n return transportDropReason;\n }\n const path = attributes[\"url.path\"] ?? attributes[\"http.target\"];\n if (typeof path === \"string\" && matchesAny(this.excludePathPatterns, path.split(\"?\")[0])) {\n return \"excluded\";\n }\n const userAgent = attributes[\"user_agent.original\"] ?? attributes[\"http.user_agent\"];\n if (typeof userAgent === \"string\" && matchesAny(EXCLUDE_USER_AGENT_PATTERNS, userAgent)) {\n return \"excluded\";\n }\n return undefined;\n }\n\n async forceFlush(): Promise<void> {\n await this.downstream.forceFlush();\n // OTel flushes queued batches but does not await already-running exports.\n await this.flushExporter?.();\n }\n\n // Shutdown releases requests with completed transport observation once and\n // discards incomplete requests, which cannot complete afterward.\n async shutdown(): Promise<void> {\n try {\n for (const entry of new Set(this.requests.values())) {\n if (!entry.released && entry.transportCompleted) {\n this.releaseRequest(entry, entry.endedServerSpan ?? entry.serverSpan);\n }\n }\n this.requests.clear();\n this.stash.clear();\n this.demotedSpanIds.clear();\n this.keptSpanIds.clear();\n } catch (error) {\n logWarning(`Error in the Apitally span processor: ${String(error)}`);\n }\n await this.downstream.shutdown();\n }\n\n private startRequest(span: Span, parentContext: Context): void {\n const spanId = span.spanContext().spanId;\n // The handle is filled for every local-root SERVER span, independent of the\n // keep decision, so runtime writes always reach the current request's span.\n const handle = parentContext.getValue(SPAN_HANDLE_KEY) as SpanHandle | undefined;\n if (handle) {\n handle.span = span;\n }\n const record = parentContext.getValue(REQUEST_RECORD_KEY) as RequestRecord | undefined;\n if (record) {\n record.serverSpanId = spanId;\n }\n writeConsumerAttributesFromContext(parentContext, span, record);\n writeUrlAttributesFromFullUrl(span);\n const dropReason =\n this.resolveRequestDropReasonBeforeSampling(span.attributes) ??\n (this.isRequestSampledIn(span) ? undefined : \"sampled-out\");\n if (dropReason) {\n if (record) {\n record.dropReason = dropReason;\n }\n return;\n }\n this.requests.set(spanId, {\n serverSpanId: spanId,\n serverSpan: span,\n spanIds: new Set([spanId]),\n record,\n buffered: [],\n transportCompleted: false,\n released: false,\n });\n }\n\n private isRequestSampledIn(span: Span): boolean {\n const callback = this.config.sampleOnRequest;\n const rate = callback\n ? resolveCallbackSampleRate(callback, span, \"sampleOnRequest\")\n : undefined;\n const bound = rate === undefined ? this.sampleRateBound : boundForSampleRate(rate);\n return isTraceSampledIn(span.spanContext().traceId, bound);\n }\n\n // Abstention leaves the request-stage decision standing; it never re-tests the\n // static sample rate.\n private isResponseSampledIn(entry: RequestEntry): boolean {\n const callback = this.config.sampleOnResponse;\n if (!callback) {\n return true;\n }\n const serverSpan = entry.endedServerSpan ?? entry.serverSpan;\n let snapshot: ReadableSpan = serverSpan;\n if (entry.record && Object.keys(entry.record.attributes).length > 0) {\n // The callback sees transport-observed values even when they were learned\n // after the span ended (final route, response size).\n const copy = copySpan(serverSpan);\n copy.attributes = {\n ...serverSpan.attributes,\n ...entry.record.attributes,\n };\n snapshot = copy;\n }\n const rate = resolveCallbackSampleRate(callback, snapshot, \"sampleOnResponse\");\n if (rate === undefined) {\n return true;\n }\n return isTraceSampledIn(serverSpan.spanContext().traceId, boundForSampleRate(rate));\n }\n\n private dropRequestOnResponse(entry: RequestEntry, record: RequestRecord): void {\n // Removing every span id sends the dropped request's late telemetry to the\n // lookup-miss rule, so it is discarded locally.\n entry.released = true;\n entry.buffered = [];\n record.dropReason = \"sampled-out\";\n if (entry.record) {\n entry.record.dropReason = \"sampled-out\";\n }\n this.removeCompletedRequestSpanIds(entry);\n this.stash.delete(entry.serverSpanId);\n this.onRequestFinished?.(entry.serverSpanId, false);\n }\n\n private releaseIfComplete(entry: RequestEntry): void {\n if (entry.released || !entry.transportCompleted || !entry.endedServerSpan) {\n return;\n }\n this.releaseRequest(entry, entry.endedServerSpan);\n }\n\n // Descendants, the SERVER span, and logs enter downstream processing once, in\n // that order. The exporter applies the request record and stash later.\n private releaseRequest(entry: RequestEntry, serverSpan: ReadableSpan): void {\n entry.released = true;\n for (const spanId of entry.spanIds) {\n this.requests.delete(spanId);\n this.addKeptSpanId(spanId, {\n serverSpanId: entry.serverSpanId,\n demoted: this.demotedSpanIds.delete(spanId),\n });\n }\n for (const bufferedSpan of entry.buffered) {\n this.downstream.onEnd(bufferedSpan);\n }\n entry.buffered = [];\n const stash = this.stash.get(entry.serverSpanId);\n this.stash.delete(entry.serverSpanId);\n let exportSpan = serverSpan;\n if (entry.record || stash || !serverSpan.ended) {\n const copy = copySpan(serverSpan);\n if (!serverSpan.ended) {\n // A SERVER span released during shutdown uses shutdown as its end time.\n copy.endTime = hrTime();\n copy.duration = hrTimeDuration(copy.startTime, copy.endTime);\n copy.ended = true;\n }\n copy.apitallyData = { record: entry.record, stash };\n exportSpan = copy;\n }\n this.downstream.onEnd(exportSpan);\n this.onRequestFinished?.(entry.serverSpanId, true);\n }\n\n private removeCompletedRequestSpanIds(entry: RequestEntry): void {\n for (const spanId of entry.spanIds) {\n this.requests.delete(spanId);\n this.demotedSpanIds.delete(spanId);\n }\n }\n\n private addKeptSpanId(spanId: string, kept: KeptSpanEntry): void {\n if (this.keptSpanIds.size >= MAX_KEPT_SPAN_IDS) {\n const oldestId = this.keptSpanIds.keys().next().value;\n if (oldestId !== undefined) {\n this.keptSpanIds.delete(oldestId);\n }\n }\n this.keptSpanIds.set(spanId, kept);\n }\n}\n\nexport function copySpan(span: ReadableSpan): SpanCopy {\n const spanContext = span.spanContext();\n return {\n name: span.name,\n kind: span.kind,\n spanContext: () => spanContext,\n parentSpanContext: span.parentSpanContext,\n startTime: span.startTime,\n endTime: span.endTime,\n status: span.status,\n attributes: span.attributes,\n links: span.links,\n events: span.events,\n duration: span.duration,\n ended: span.ended,\n resource: span.resource,\n instrumentationScope: span.instrumentationScope,\n droppedAttributesCount: span.droppedAttributesCount,\n droppedEventsCount: span.droppedEventsCount,\n droppedLinksCount: span.droppedLinksCount,\n };\n}\n\nfunction resolveTransportDropReason(attributes: Attributes): RequestDropReason | undefined {\n const method = attributes[\"http.request.method\"] ?? attributes[\"http.method\"];\n if (method === \"OPTIONS\") {\n return \"method\";\n }\n const scheme = attributes[\"url.scheme\"] ?? attributes[\"http.scheme\"];\n return scheme === \"ws\" || scheme === \"wss\" ? \"scheme\" : undefined;\n}\n\n// Missing path, query, and target attributes are derived from the full URL\n// because exclusion, display, and redaction require them.\nfunction writeUrlAttributesFromFullUrl(span: Span): void {\n const attributes = span.attributes;\n if (attributes[\"url.path\"] !== undefined || attributes[\"http.target\"] !== undefined) {\n return;\n }\n const url = attributes[\"url.full\"] ?? attributes[\"http.url\"];\n if (typeof url !== \"string\") {\n return;\n }\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return;\n }\n const query = parsed.search.replace(/^\\?/, \"\");\n span.setAttribute(\"url.path\", parsed.pathname);\n if (query) {\n span.setAttribute(\"url.query\", query);\n }\n span.setAttribute(\"http.target\", query ? `${parsed.pathname}?${query}` : parsed.pathname);\n}\n\nconst TRACE_ID_LOW_64_BITS_MASK = (1n << 64n) - 1n;\n\nfunction boundForSampleRate(rate: number): bigint {\n return BigInt(Math.round(rate * 2 ** 64));\n}\n\n// Low 64-bit ratio sampling is deterministic per trace. Comparing the same\n// value at both stages makes the lower rate decisive.\nfunction isTraceSampledIn(traceId: string, bound: bigint): boolean {\n return (BigInt(`0x${traceId}`) & TRACE_ID_LOW_64_BITS_MASK) < bound;\n}\n\n// A throwing or invalid-returning callback resolves to keep: a user bug must\n// never lose data without a warning. Undefined means the callback abstained.\nfunction resolveCallbackSampleRate(\n callback: SamplingCallback,\n span: ReadableSpan,\n optionName: string,\n): number | undefined {\n let result: unknown;\n try {\n result = callback(span);\n } catch {\n logWarning(`The Apitally ${optionName} callback threw an error, so the request was captured`);\n return 1;\n }\n if (result === undefined || result === null) {\n return undefined;\n }\n if (typeof result === \"boolean\") {\n return result ? 1 : 0;\n }\n if (typeof result === \"number\" && result >= 0 && result <= 1) {\n return result;\n }\n logWarning(\n `The Apitally ${optionName} callback returned an invalid value, so the request was captured. Sampling callbacks must synchronously return a number between 0 and 1, a boolean, or undefined.`,\n );\n return 1;\n}\n\nfunction isContribPerMessageSpan(span: Span): boolean {\n const scopeName = span.instrumentationScope?.name ?? \"\";\n return (\n span.kind === SpanKind.INTERNAL &&\n [\" http send\", \" http receive\", \" websocket send\", \" websocket receive\"].some((suffix) =>\n span.name.endsWith(suffix),\n ) &&\n (scopeName.startsWith(\"@opentelemetry/instrumentation\") ||\n scopeName.startsWith(\"opentelemetry.instrumentation.\"))\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"spanProcessor.js","names":[],"sources":["../src/spanProcessor.ts"],"sourcesContent":["import { type Attributes, type Context, SpanKind } from \"@opentelemetry/api\";\nimport { hrTime, hrTimeDuration } from \"@opentelemetry/core\";\nimport type { ReadableSpan, Span, SpanProcessor } from \"@opentelemetry/sdk-trace-base\";\nimport {\n type ApitallyConfig,\n compilePatterns,\n DEFAULT_EXCLUDE_PATHS,\n EXCLUDE_USER_AGENTS,\n getConfig,\n matchesAny,\n type SamplingCallback,\n} from \"./config.js\";\nimport {\n REQUEST_RECORD_KEY,\n type RequestDropReason,\n type RequestRecord,\n SPAN_HANDLE_KEY,\n type SpanHandle,\n} from \"./context.js\";\nimport { logDebug, logWarning } from \"./logger.js\";\n\nconst MAX_BUFFERED_SPANS = 1_000;\nconst MAX_TRACKED_SPAN_IDS = MAX_BUFFERED_SPANS + 1;\nconst MAX_STASHED_REQUESTS = 2_048;\nconst MAX_KEPT_SPAN_IDS = 10_000;\nconst EXCLUDE_USER_AGENT_PATTERNS = compilePatterns(EXCLUDE_USER_AGENTS);\n\n// Transport-captured headers and bodies are attached only to Apitally's export\n// copy.\nexport interface RequestStash {\n requestHeaders?: Record<string, string | string[]>;\n requestBody?: Buffer;\n requestContentEncoding?: string;\n responseHeaders?: Record<string, string | string[]>;\n responseBody?: Buffer;\n responseContentEncoding?: string;\n}\n\nexport interface ApitallySpanData {\n record?: RequestRecord;\n stash?: RequestStash;\n demoteToInternal?: boolean;\n}\n\n// Mutable ReadableSpan-shaped copy on Apitally's private export path; the span\n// handed to user-attached processors is never mutated.\nexport type SpanCopy = {\n -readonly [Key in keyof ReadableSpan]: ReadableSpan[Key];\n} & { apitallyData?: ApitallySpanData };\n\n// The public processor delegates to the process-global pipeline. Construction\n// only declares that the application owns the tracer provider.\nexport class ApitallySpanProcessor implements SpanProcessor {\n constructor() {\n setApitallySpanProcessorDeclared();\n }\n\n onStart(span: Span, parentContext: Context): void {\n let pipeline = getActiveSpanPipeline();\n if (!pipeline && span.kind === SpanKind.SERVER) {\n getServerSpanActivationCallback()?.();\n pipeline = getActiveSpanPipeline();\n }\n pipeline?.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n getActiveSpanPipeline()?.onEnd(span);\n }\n\n // Flush failures resolve instead of rejecting into the user's provider.\n forceFlush(): Promise<void> {\n const pipeline = getActiveSpanPipeline();\n if (!pipeline) {\n return Promise.resolve();\n }\n return pipeline.forceFlush().catch((error: unknown) => {\n logDebug(`Error flushing spans: ${String(error)}`);\n });\n }\n\n // A user provider's shutdown only flushes released requests; SDK shutdown owns\n // teardown.\n shutdown(): Promise<void> {\n return this.forceFlush();\n }\n}\n\ntype ServerSpanActivationCallback = () => void;\n\n// Symbol.for keys let ESM and CJS builds share processor coordination.\nconst ACTIVE_SPAN_PIPELINE_KEY = Symbol.for(\"apitally.activeSpanPipeline\");\nconst APITALLY_SPAN_PROCESSOR_DECLARED_KEY = Symbol.for(\"apitally.spanProcessorDeclared\");\nconst SERVER_SPAN_ACTIVATION_CALLBACK_KEY = Symbol.for(\"apitally.serverSpanActivationCallback\");\n\nexport function setActiveSpanPipeline(pipeline: SpanPipeline | undefined): void {\n (globalThis as Record<symbol, SpanPipeline | undefined>)[ACTIVE_SPAN_PIPELINE_KEY] = pipeline;\n}\n\nexport function getActiveSpanPipeline(): SpanPipeline | undefined {\n return (globalThis as Record<symbol, SpanPipeline | undefined>)[ACTIVE_SPAN_PIPELINE_KEY];\n}\n\nexport function setServerSpanActivationCallback(callback: ServerSpanActivationCallback): void {\n const holder = globalThis as Record<symbol, ServerSpanActivationCallback | undefined>;\n holder[SERVER_SPAN_ACTIVATION_CALLBACK_KEY] ??= callback;\n}\n\nexport function isApitallySpanProcessorDeclared(): boolean {\n return (\n (globalThis as Record<symbol, boolean | undefined>)[APITALLY_SPAN_PROCESSOR_DECLARED_KEY] ===\n true\n );\n}\n\nexport function resetSpanProcessorState(): void {\n const holder = globalThis as Record<symbol, unknown>;\n delete holder[ACTIVE_SPAN_PIPELINE_KEY];\n delete holder[APITALLY_SPAN_PROCESSOR_DECLARED_KEY];\n delete holder[SERVER_SPAN_ACTIVATION_CALLBACK_KEY];\n}\n\nfunction setApitallySpanProcessorDeclared(): void {\n (globalThis as Record<symbol, boolean>)[APITALLY_SPAN_PROCESSOR_DECLARED_KEY] = true;\n}\n\nfunction getServerSpanActivationCallback(): ServerSpanActivationCallback | undefined {\n return (globalThis as Record<symbol, ServerSpanActivationCallback | undefined>)[\n SERVER_SPAN_ACTIVATION_CALLBACK_KEY\n ];\n}\n\ninterface RequestEntry {\n readonly serverSpanId: string;\n readonly serverSpan: Span;\n readonly spanIds: Set<string>;\n record?: RequestRecord;\n buffered: ReadableSpan[];\n endedServerSpan?: ReadableSpan;\n transportCompleted: boolean;\n released: boolean;\n}\n\ninterface KeptSpanEntry {\n readonly serverSpanId: string;\n // Carries the demotion decision across release for spans that end late.\n readonly demoted: boolean;\n}\n\n// Requests remain buffered until the SERVER span and transport complete, then\n// one keep/drop decision releases them once.\nexport class SpanPipeline implements SpanProcessor {\n // Callbacks are optional because metrics and log pipelines start at activation.\n metricsRecorder?: (record: RequestRecord) => void;\n onRequestFinished?: (serverSpanId: string, kept: boolean) => void;\n private readonly downstream: SpanProcessor;\n private readonly config: ApitallyConfig;\n private readonly sampleRateBound: bigint;\n private readonly excludePathPatterns: RegExp[];\n // Every span ID remains mapped to one request until completion so late logs\n // resolve. A miss means dropped, completed, or non-request telemetry.\n private readonly requests = new Map<string, RequestEntry>();\n private readonly stash = new Map<string, RequestStash>();\n private readonly demotedSpanIds = new Set<string>();\n // Kept span IDs remain resolvable after release for late spans and logs; the\n // oldest are evicted at the cap.\n private readonly keptSpanIds = new Map<string, KeptSpanEntry>();\n\n constructor(\n downstream: SpanProcessor,\n private readonly flushExporter?: () => Promise<void>,\n ) {\n this.downstream = downstream;\n this.config = getConfig();\n this.sampleRateBound = boundForSampleRate(this.config.sampleRate);\n this.excludePathPatterns = compilePatterns(DEFAULT_EXCLUDE_PATHS, this.config.excludePaths);\n }\n\n onStart(span: Span, parentContext: Context): void {\n try {\n if (isContribPerMessageSpan(span)) {\n return;\n }\n const parent = span.parentSpanContext;\n if (!parent || parent.isRemote) {\n if (span.kind === SpanKind.SERVER) {\n this.startRequest(span, parentContext);\n }\n return;\n }\n const entry = this.requests.get(parent.spanId);\n const spanId = span.spanContext().spanId;\n if (!entry) {\n // A span started under a request already released as kept exports at\n // its end, like the released request's other late telemetry.\n const kept = this.keptSpanIds.get(parent.spanId);\n if (kept) {\n this.addKeptSpanId(spanId, {\n serverSpanId: kept.serverSpanId,\n demoted: false,\n });\n }\n return;\n }\n if (entry.spanIds.size >= MAX_TRACKED_SPAN_IDS) {\n logDebug(\"Apitally span tracking cap reached, dropping the span\");\n return;\n }\n if (span.kind === SpanKind.SERVER) {\n // A second middleware produced a SERVER span for this request. Apitally\n // demotes its copy, but user exporters retain the duplicate.\n this.demotedSpanIds.add(spanId);\n logWarning(\n `Detected a duplicate SERVER span produced by the instrumentation scope \"${span.instrumentationScope?.name ?? \"unknown\"}\" inside an active request. Apitally exports it as an INTERNAL span, but your own OpenTelemetry exporters still receive the duplicate. To resolve this, remove the middleware that produces it.`,\n );\n }\n this.requests.set(spanId, entry);\n entry.spanIds.add(spanId);\n } catch (error) {\n logWarning(`Error in the Apitally span processor: ${String(error)}`);\n }\n }\n\n onEnd(span: ReadableSpan): void {\n try {\n const spanId = span.spanContext().spanId;\n const isDemoted = this.demotedSpanIds.delete(spanId);\n const entry = this.requests.get(spanId);\n if (!entry) {\n const kept = this.keptSpanIds.get(spanId);\n if (kept) {\n let exportSpan = span;\n if (kept.demoted) {\n const copy = copySpan(span);\n copy.apitallyData = { demoteToInternal: true };\n exportSpan = copy;\n }\n this.downstream.onEnd(exportSpan);\n }\n return;\n }\n if (spanId === entry.serverSpanId) {\n if (!entry.record) {\n this.removeCompletedRequestSpanIds(entry);\n this.stash.delete(entry.serverSpanId);\n this.onRequestFinished?.(entry.serverSpanId, false);\n return;\n }\n entry.endedServerSpan = span;\n this.releaseIfComplete(entry);\n return;\n }\n // The span id stays mapped so log records emitted after this span ended\n // still resolve to the request until it completes.\n let exportSpan = span;\n if (isDemoted) {\n const copy = copySpan(span);\n copy.apitallyData = { demoteToInternal: true };\n exportSpan = copy;\n }\n if (entry.buffered.length < MAX_BUFFERED_SPANS) {\n entry.buffered.push(exportSpan);\n } else {\n logDebug(\"Apitally span buffer cap reached, dropping the span\");\n }\n } catch (error) {\n logWarning(`Error in the Apitally span processor: ${String(error)}`);\n }\n }\n\n attachRequestRecord(serverSpanId: string, record: RequestRecord): void {\n const entry = this.requests.get(serverSpanId);\n if (entry) {\n entry.record ??= record;\n }\n }\n\n // Transport completion triggers response sampling and metrics independently\n // of span-end timing. Map misses still record metrics and discard other data.\n handleTransportCompletion(record: RequestRecord): void {\n try {\n record.dropReason = resolveTransportDropReason(record.attributes) ?? record.dropReason;\n const entry =\n record.serverSpanId !== undefined ? this.requests.get(record.serverSpanId) : undefined;\n if (entry && !entry.transportCompleted && !entry.released) {\n entry.record ??= record;\n entry.transportCompleted = true;\n if (!this.isResponseSampledIn(entry)) {\n this.dropRequestOnResponse(entry, record);\n }\n }\n this.metricsRecorder?.(record);\n if (entry) {\n this.releaseIfComplete(entry);\n }\n } catch (error) {\n logWarning(`Error in the Apitally span processor: ${String(error)}`);\n }\n }\n\n // Holds captured headers and bodies until the exporter attaches them to the\n // exported SERVER span copy. Fields already stashed are kept unless replaced.\n updateStash(serverSpanId: string, update: RequestStash): void {\n try {\n // A request without an in-flight map entry can never release, so its\n // stash entry would sit unconsumed until the cap evicts it.\n if (!this.requests.has(serverSpanId)) {\n return;\n }\n let entry = this.stash.get(serverSpanId);\n if (!entry) {\n if (this.stash.size >= MAX_STASHED_REQUESTS) {\n const oldestKey = this.stash.keys().next().value;\n if (oldestKey !== undefined) {\n this.stash.delete(oldestKey);\n logDebug(\"Apitally payload stash cap reached, dropping the oldest entry\");\n }\n }\n entry = {};\n this.stash.set(serverSpanId, entry);\n }\n if (update.requestHeaders !== undefined) {\n entry.requestHeaders = update.requestHeaders;\n }\n if (update.requestBody !== undefined) {\n entry.requestBody = update.requestBody;\n }\n if (update.requestContentEncoding !== undefined) {\n entry.requestContentEncoding = update.requestContentEncoding;\n }\n if (update.responseHeaders !== undefined) {\n entry.responseHeaders = update.responseHeaders;\n }\n if (update.responseBody !== undefined) {\n entry.responseBody = update.responseBody;\n }\n if (update.responseContentEncoding !== undefined) {\n entry.responseContentEncoding = update.responseContentEncoding;\n }\n } catch (error) {\n logDebug(`Error updating the request stash: ${String(error)}`);\n }\n }\n\n // Kept requests remain resolvable after release so late logs retain their\n // request association.\n resolveServerSpanId(spanId: string): string | undefined {\n return this.requests.get(spanId)?.serverSpanId ?? this.keptSpanIds.get(spanId)?.serverSpanId;\n }\n\n isRequestInFlight(serverSpanId: string): boolean {\n return this.requests.has(serverSpanId);\n }\n\n // Exclusion answers \"never wanted\" and runs strictly before sampling; an\n // excluded request never invokes a user sampling callback.\n resolveRequestDropReasonBeforeSampling(attributes: Attributes): RequestDropReason | undefined {\n const transportDropReason = resolveTransportDropReason(attributes);\n if (transportDropReason !== undefined) {\n return transportDropReason;\n }\n const path = attributes[\"url.path\"] ?? attributes[\"http.target\"];\n if (typeof path === \"string\" && matchesAny(this.excludePathPatterns, path.split(\"?\")[0])) {\n return \"excluded\";\n }\n const userAgent = attributes[\"user_agent.original\"] ?? attributes[\"http.user_agent\"];\n if (typeof userAgent === \"string\" && matchesAny(EXCLUDE_USER_AGENT_PATTERNS, userAgent)) {\n return \"excluded\";\n }\n return undefined;\n }\n\n async forceFlush(): Promise<void> {\n await this.downstream.forceFlush();\n // OTel flushes queued batches but does not await already-running exports.\n await this.flushExporter?.();\n }\n\n // Shutdown releases requests with completed transport observation once and\n // discards incomplete requests, which cannot complete afterward.\n async shutdown(): Promise<void> {\n try {\n for (const entry of new Set(this.requests.values())) {\n if (!entry.released && entry.transportCompleted) {\n this.releaseRequest(entry, entry.endedServerSpan ?? entry.serverSpan);\n }\n }\n this.requests.clear();\n this.stash.clear();\n this.demotedSpanIds.clear();\n this.keptSpanIds.clear();\n } catch (error) {\n logWarning(`Error in the Apitally span processor: ${String(error)}`);\n }\n await this.downstream.shutdown();\n }\n\n private startRequest(span: Span, parentContext: Context): void {\n const spanId = span.spanContext().spanId;\n // The handle is filled for every local-root SERVER span, independent of the\n // keep decision, so runtime writes always reach the current request's span.\n const handle = parentContext.getValue(SPAN_HANDLE_KEY) as SpanHandle | undefined;\n if (handle) {\n handle.span = span;\n }\n const record = parentContext.getValue(REQUEST_RECORD_KEY) as RequestRecord | undefined;\n if (record) {\n record.serverSpanId = spanId;\n }\n writeUrlAttributesFromFullUrl(span);\n const dropReason =\n this.resolveRequestDropReasonBeforeSampling(span.attributes) ??\n (this.isRequestSampledIn(span) ? undefined : \"sampled-out\");\n if (dropReason) {\n if (record) {\n record.dropReason = dropReason;\n }\n return;\n }\n this.requests.set(spanId, {\n serverSpanId: spanId,\n serverSpan: span,\n spanIds: new Set([spanId]),\n record,\n buffered: [],\n transportCompleted: false,\n released: false,\n });\n }\n\n private isRequestSampledIn(span: Span): boolean {\n const callback = this.config.sampleOnRequest;\n const rate = callback\n ? resolveCallbackSampleRate(callback, span, \"sampleOnRequest\")\n : undefined;\n const bound = rate === undefined ? this.sampleRateBound : boundForSampleRate(rate);\n return isTraceSampledIn(span.spanContext().traceId, bound);\n }\n\n // Abstention leaves the request-stage decision standing; it never re-tests the\n // static sample rate.\n private isResponseSampledIn(entry: RequestEntry): boolean {\n const callback = this.config.sampleOnResponse;\n if (!callback) {\n return true;\n }\n const serverSpan = entry.endedServerSpan ?? entry.serverSpan;\n let snapshot: ReadableSpan = serverSpan;\n if (entry.record && Object.keys(entry.record.attributes).length > 0) {\n // The callback sees transport-observed values even when they were learned\n // after the span ended (final route, response size).\n const copy = copySpan(serverSpan);\n copy.attributes = {\n ...serverSpan.attributes,\n ...entry.record.attributes,\n };\n snapshot = copy;\n }\n const rate = resolveCallbackSampleRate(callback, snapshot, \"sampleOnResponse\");\n if (rate === undefined) {\n return true;\n }\n return isTraceSampledIn(serverSpan.spanContext().traceId, boundForSampleRate(rate));\n }\n\n private dropRequestOnResponse(entry: RequestEntry, record: RequestRecord): void {\n // Removing every span id sends the dropped request's late telemetry to the\n // lookup-miss rule, so it is discarded locally.\n entry.released = true;\n entry.buffered = [];\n record.dropReason = \"sampled-out\";\n if (entry.record) {\n entry.record.dropReason = \"sampled-out\";\n }\n this.removeCompletedRequestSpanIds(entry);\n this.stash.delete(entry.serverSpanId);\n this.onRequestFinished?.(entry.serverSpanId, false);\n }\n\n private releaseIfComplete(entry: RequestEntry): void {\n if (entry.released || !entry.transportCompleted || !entry.endedServerSpan) {\n return;\n }\n this.releaseRequest(entry, entry.endedServerSpan);\n }\n\n // Descendants, the SERVER span, and logs enter downstream processing once, in\n // that order. The exporter applies the request record and stash later.\n private releaseRequest(entry: RequestEntry, serverSpan: ReadableSpan): void {\n entry.released = true;\n for (const spanId of entry.spanIds) {\n this.requests.delete(spanId);\n this.addKeptSpanId(spanId, {\n serverSpanId: entry.serverSpanId,\n demoted: this.demotedSpanIds.delete(spanId),\n });\n }\n for (const bufferedSpan of entry.buffered) {\n this.downstream.onEnd(bufferedSpan);\n }\n entry.buffered = [];\n const stash = this.stash.get(entry.serverSpanId);\n this.stash.delete(entry.serverSpanId);\n let exportSpan = serverSpan;\n if (entry.record || stash || !serverSpan.ended) {\n const copy = copySpan(serverSpan);\n if (!serverSpan.ended) {\n // A SERVER span released during shutdown uses shutdown as its end time.\n copy.endTime = hrTime();\n copy.duration = hrTimeDuration(copy.startTime, copy.endTime);\n copy.ended = true;\n }\n copy.apitallyData = { record: entry.record, stash };\n exportSpan = copy;\n }\n this.downstream.onEnd(exportSpan);\n this.onRequestFinished?.(entry.serverSpanId, true);\n }\n\n private removeCompletedRequestSpanIds(entry: RequestEntry): void {\n for (const spanId of entry.spanIds) {\n this.requests.delete(spanId);\n this.demotedSpanIds.delete(spanId);\n }\n }\n\n private addKeptSpanId(spanId: string, kept: KeptSpanEntry): void {\n if (this.keptSpanIds.size >= MAX_KEPT_SPAN_IDS) {\n const oldestId = this.keptSpanIds.keys().next().value;\n if (oldestId !== undefined) {\n this.keptSpanIds.delete(oldestId);\n }\n }\n this.keptSpanIds.set(spanId, kept);\n }\n}\n\nexport function copySpan(span: ReadableSpan): SpanCopy {\n const spanContext = span.spanContext();\n return {\n name: span.name,\n kind: span.kind,\n spanContext: () => spanContext,\n parentSpanContext: span.parentSpanContext,\n startTime: span.startTime,\n endTime: span.endTime,\n status: span.status,\n attributes: span.attributes,\n links: span.links,\n events: span.events,\n duration: span.duration,\n ended: span.ended,\n resource: span.resource,\n instrumentationScope: span.instrumentationScope,\n droppedAttributesCount: span.droppedAttributesCount,\n droppedEventsCount: span.droppedEventsCount,\n droppedLinksCount: span.droppedLinksCount,\n };\n}\n\nfunction resolveTransportDropReason(attributes: Attributes): RequestDropReason | undefined {\n const method = attributes[\"http.request.method\"] ?? attributes[\"http.method\"];\n if (method === \"OPTIONS\") {\n return \"method\";\n }\n const scheme = attributes[\"url.scheme\"] ?? attributes[\"http.scheme\"];\n return scheme === \"ws\" || scheme === \"wss\" ? \"scheme\" : undefined;\n}\n\n// Missing path, query, and target attributes are derived from the full URL\n// because exclusion, display, and redaction require them.\nfunction writeUrlAttributesFromFullUrl(span: Span): void {\n const attributes = span.attributes;\n if (attributes[\"url.path\"] !== undefined || attributes[\"http.target\"] !== undefined) {\n return;\n }\n const url = attributes[\"url.full\"] ?? attributes[\"http.url\"];\n if (typeof url !== \"string\") {\n return;\n }\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return;\n }\n const query = parsed.search.replace(/^\\?/, \"\");\n span.setAttribute(\"url.path\", parsed.pathname);\n if (query) {\n span.setAttribute(\"url.query\", query);\n }\n span.setAttribute(\"http.target\", query ? `${parsed.pathname}?${query}` : parsed.pathname);\n}\n\nconst TRACE_ID_LOW_64_BITS_MASK = (1n << 64n) - 1n;\n\nfunction boundForSampleRate(rate: number): bigint {\n return BigInt(Math.round(rate * 2 ** 64));\n}\n\n// Low 64-bit ratio sampling is deterministic per trace. Comparing the same\n// value at both stages makes the lower rate decisive.\nfunction isTraceSampledIn(traceId: string, bound: bigint): boolean {\n return (BigInt(`0x${traceId}`) & TRACE_ID_LOW_64_BITS_MASK) < bound;\n}\n\n// A throwing or invalid-returning callback resolves to keep: a user bug must\n// never lose data without a warning. Undefined means the callback abstained.\nfunction resolveCallbackSampleRate(\n callback: SamplingCallback,\n span: ReadableSpan,\n optionName: string,\n): number | undefined {\n let result: unknown;\n try {\n result = callback(span);\n } catch {\n logWarning(`The Apitally ${optionName} callback threw an error, so the request was captured`);\n return 1;\n }\n if (result === undefined || result === null) {\n return undefined;\n }\n if (typeof result === \"boolean\") {\n return result ? 1 : 0;\n }\n if (typeof result === \"number\" && result >= 0 && result <= 1) {\n return result;\n }\n logWarning(\n `The Apitally ${optionName} callback returned an invalid value, so the request was captured. Sampling callbacks must synchronously return a number between 0 and 1, a boolean, or undefined.`,\n );\n return 1;\n}\n\nfunction isContribPerMessageSpan(span: Span): boolean {\n const scopeName = span.instrumentationScope?.name ?? \"\";\n return (\n span.kind === SpanKind.INTERNAL &&\n [\" http send\", \" http receive\", \" websocket send\", \" websocket receive\"].some((suffix) =>\n span.name.endsWith(suffix),\n ) &&\n (scopeName.startsWith(\"@opentelemetry/instrumentation\") ||\n scopeName.startsWith(\"opentelemetry.instrumentation.\"))\n );\n}\n"],"mappings":"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"}
package/dist/spool.cjs CHANGED
@@ -24,6 +24,7 @@ var Spool = class {
24
24
  maxSize;
25
25
  current = /* @__PURE__ */ new Map();
26
26
  closed = [];
27
+ filesClosedSinceExportRotation = 0;
27
28
  tempDir;
28
29
  queues = {
29
30
  traces: Promise.resolve(),
@@ -63,6 +64,9 @@ var Spool = class {
63
64
  if (this.current.has(signal) && !this.closed.some((file) => file.signal === signal)) await this.closeCurrentFile(signal);
64
65
  });
65
66
  await this.evict();
67
+ const closedFileCount = this.filesClosedSinceExportRotation;
68
+ this.filesClosedSinceExportRotation = 0;
69
+ return closedFileCount;
66
70
  }
67
71
  async closeCurrentFiles() {
68
72
  for (const signal of SIGNALS) await this.enqueue(signal, () => this.closeCurrentFile(signal));
@@ -100,6 +104,7 @@ var Spool = class {
100
104
  try {
101
105
  await file.close();
102
106
  this.closed.push(file);
107
+ this.filesClosedSinceExportRotation += 1;
103
108
  } catch {
104
109
  require_logger.logWarning(WRITE_FAILURE_WARNING);
105
110
  await file.delete();
@@ -1 +1 @@
1
- {"version":3,"file":"spool.cjs","names":["tmpdir","createGzip","join","randomUUID","openSync","createWriteStream","readFile","unlink","readdirSync","statSync"],"sources":["../src/spool.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n createWriteStream,\n openSync,\n readdirSync,\n statSync,\n unlinkSync,\n utimesSync,\n type WriteStream,\n writeFileSync,\n} from \"node:fs\";\nimport { readFile, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createGzip, type Gzip } from \"node:zlib\";\nimport { logWarning, resetWarning } from \"./logger.js\";\n\nconst WRITE_FAILURE_WARNING = \"Error writing to disk, dropping buffered telemetry\";\n\nconst SIGNALS = [\"traces\", \"logs\", \"metrics\"] as const;\nexport type Signal = (typeof SIGNALS)[number];\n\nconst MAX_UNCOMPRESSED_FILE_SIZE = 4_000_000;\nconst MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS = 59 * 60 * 1000;\nconst MAX_SPOOL_SIZE_DISK = 50_000_000;\nconst MAX_SPOOL_SIZE_MEMORY = 10_000_000;\nconst MAX_UNTOUCHED_FILE_AGE_MILLIS = 2 * 60 * 60 * 1000;\nconst SPOOL_FILE_NAME_PATTERN = /^apitally-.*\\.gz$/;\n\n// Operations for each signal are serialized because gzip and file streams\n// complete asynchronously.\nexport class Spool {\n readonly inMemory: boolean;\n maxSize: number;\n readonly current = new Map<Signal, SpoolFile>();\n private readonly closed: SpoolFile[] = [];\n private readonly tempDir: string;\n private readonly queues: Record<Signal, Promise<void>> = {\n traces: Promise.resolve(),\n logs: Promise.resolve(),\n metrics: Promise.resolve(),\n };\n\n constructor(tempDir: string = tmpdir()) {\n this.tempDir = tempDir;\n this.inMemory = !isTempDirWritable(tempDir);\n if (this.inMemory) {\n logWarning(\n `Unable to create temporary files, buffering telemetry in memory (max ${MAX_SPOOL_SIZE_MEMORY / 1_000_000} MB)`,\n );\n } else {\n cleanupOrphanedFiles(tempDir);\n }\n this.maxSize = this.inMemory ? MAX_SPOOL_SIZE_MEMORY : MAX_SPOOL_SIZE_DISK;\n }\n\n append(signal: Signal, payload: Uint8Array): Promise<void> {\n return this.enqueue(signal, async () => {\n let file = this.current.get(signal);\n if (file && file.uncompressedSize + payload.length > MAX_UNCOMPRESSED_FILE_SIZE) {\n await this.closeCurrentFile(signal);\n file = undefined;\n }\n try {\n if (!file) {\n file = new SpoolFile(signal, this.inMemory ? undefined : this.tempDir);\n this.current.set(signal, file);\n }\n await file.write(payload);\n // Writes recovered; the write-failure warning may fire again later.\n resetWarning(WRITE_FAILURE_WARNING);\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await this.discardCurrentFile(signal);\n }\n await this.evict();\n });\n }\n\n // Current files close only when no backlog exists, avoiding one file per cycle.\n // Sequential rotation preserves send order.\n async rotateForExport(): Promise<void> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, async () => {\n if (this.current.has(signal) && !this.closed.some((file) => file.signal === signal)) {\n await this.closeCurrentFile(signal);\n }\n });\n }\n await this.evict();\n }\n\n async closeCurrentFiles(): Promise<void> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, () => this.closeCurrentFile(signal));\n }\n }\n\n pendingFiles(): SpoolFile[] {\n return [...this.closed];\n }\n\n async deleteFile(file: SpoolFile): Promise<void> {\n const index = this.closed.indexOf(file);\n if (index >= 0) {\n this.closed.splice(index, 1);\n }\n await file.delete();\n }\n\n // Apitally ingest deduplicates for one hour; a later retry could ingest the file twice.\n deleteIfExpired(file: SpoolFile): Promise<void> | undefined {\n if (!file.isExpired()) {\n return undefined;\n }\n logWarning(`Buffered ${file.signal} could not be delivered within an hour and were dropped`);\n return this.deleteFile(file);\n }\n\n // Refresh mtimes so orphan cleanup in another process preserves live files.\n touchFiles(): void {\n for (const file of [...this.current.values(), ...this.closed]) {\n file.touch();\n }\n }\n\n async clear(): Promise<void> {\n await Promise.all(\n SIGNALS.map((signal) => this.enqueue(signal, () => this.discardCurrentFile(signal))),\n );\n const files = this.closed.splice(0);\n await Promise.all(files.map((file) => file.delete()));\n }\n\n private enqueue(signal: Signal, operation: () => Promise<void>): Promise<void> {\n const result = this.queues[signal].then(operation);\n this.queues[signal] = result.catch(() => undefined);\n return result;\n }\n\n private async closeCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (!file) {\n return;\n }\n this.current.delete(signal);\n try {\n await file.close();\n this.closed.push(file);\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await file.delete();\n }\n }\n\n private async discardCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (file) {\n this.current.delete(signal);\n await file.delete();\n }\n }\n\n private async evict(): Promise<void> {\n const deletions = [...this.closed].map((file) => this.deleteIfExpired(file));\n while (this.totalSize() > this.maxSize) {\n // Prefer retaining metrics, but enforce the size limit when only metrics remain.\n const oldest = this.closed.find((file) => file.signal !== \"metrics\") ?? this.closed[0];\n if (!oldest) {\n break;\n }\n logWarning(`Buffer size limit reached, dropping oldest buffered ${oldest.signal}`);\n deletions.push(this.deleteFile(oldest));\n }\n await Promise.all(deletions);\n }\n\n private totalSize(): number {\n let total = 0;\n for (const file of [...this.closed, ...this.current.values()]) {\n total += file.storedSize;\n }\n return total;\n }\n}\n\n// One gzip stream of concatenated OTLP request payloads for a single signal,\n// written through to a temp file or, as fallback, an in-memory buffer.\nexport class SpoolFile {\n readonly signal: Signal;\n readonly path?: string;\n firstAttemptAtMillis?: number;\n uncompressedSize = 0;\n readonly gzip: Gzip;\n private readonly fileStream?: WriteStream;\n private readonly memoryChunks?: Buffer[];\n private memorySize = 0;\n private streamError?: Error;\n private readonly closedPromise: Promise<void>;\n\n constructor(signal: Signal, tempDir?: string) {\n this.signal = signal;\n this.gzip = createGzip();\n let closeTarget: Gzip | WriteStream;\n let closeEvent: string;\n if (tempDir === undefined) {\n const chunks: Buffer[] = [];\n this.memoryChunks = chunks;\n this.gzip.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n this.memorySize += chunk.length;\n });\n closeTarget = this.gzip;\n closeEvent = \"end\";\n } else {\n this.path = join(tempDir, `apitally-${randomUUID()}.gz`);\n // A synchronous exclusive open reports creation errors to the triggering append.\n const fd = openSync(this.path, \"wx\", 0o600);\n this.fileStream = createWriteStream(this.path, { fd });\n this.gzip.pipe(this.fileStream);\n closeTarget = this.fileStream;\n closeEvent = \"close\";\n }\n const streams: (Gzip | WriteStream)[] = this.fileStream\n ? [this.gzip, this.fileStream]\n : [this.gzip];\n this.closedPromise = new Promise((resolve, reject) => {\n closeTarget.once(closeEvent, resolve);\n for (const stream of streams) {\n stream.once(\"error\", reject);\n }\n });\n // Stream errors can occur outside an awaited operation; rejection handling\n // prevents an unhandled rejection.\n this.closedPromise.catch(() => undefined);\n for (const stream of streams) {\n stream.on(\"error\", (error: Error) => {\n this.streamError ??= error;\n });\n }\n }\n\n write(payload: Uint8Array): Promise<void> {\n return new Promise((resolve, reject) => {\n if (this.streamError) {\n reject(this.streamError);\n return;\n }\n this.gzip.write(payload, (error) => {\n const failure = error ?? this.streamError;\n if (failure) {\n reject(failure);\n } else {\n this.uncompressedSize += payload.length;\n resolve();\n }\n });\n });\n }\n\n async close(): Promise<void> {\n if (this.streamError) {\n throw this.streamError;\n }\n this.gzip.end();\n await this.closedPromise;\n }\n\n get storedSize(): number {\n return this.fileStream ? this.fileStream.bytesWritten : this.memorySize;\n }\n\n markAttempt(): void {\n this.firstAttemptAtMillis ??= performance.now();\n }\n\n isExpired(): boolean {\n return (\n this.firstAttemptAtMillis !== undefined &&\n performance.now() - this.firstAttemptAtMillis > MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS\n );\n }\n\n touch(): void {\n if (this.path) {\n try {\n const now = new Date();\n utimesSync(this.path, now, now);\n } catch {\n // The file may already be gone; orphan cleanup only targets stale files.\n }\n }\n }\n\n readStoredBytes(): Promise<Buffer> {\n if (this.path) {\n return readFile(this.path);\n }\n return Promise.resolve(Buffer.concat(this.memoryChunks ?? []));\n }\n\n async delete(): Promise<void> {\n this.gzip.destroy();\n this.fileStream?.destroy();\n if (this.path) {\n await unlink(this.path).catch(() => undefined);\n }\n }\n}\n\nfunction isTempDirWritable(tempDir: string): boolean {\n const probePath = join(tempDir, `apitally-${randomUUID()}.gz`);\n try {\n writeFileSync(probePath, \"\", { flag: \"wx\", mode: 0o600 });\n unlinkSync(probePath);\n return true;\n } catch {\n return false;\n }\n}\n\n// Live processes refresh mtimes each cycle, so only stale spool files from dead\n// processes are removed.\nfunction cleanupOrphanedFiles(tempDir: string): void {\n const cutoff = Date.now() - MAX_UNTOUCHED_FILE_AGE_MILLIS;\n try {\n for (const name of readdirSync(tempDir)) {\n if (!SPOOL_FILE_NAME_PATTERN.test(name)) {\n continue;\n }\n try {\n const filePath = join(tempDir, name);\n if (statSync(filePath).mtimeMs < cutoff) {\n unlinkSync(filePath);\n }\n } catch {\n // Another process may have removed the file first.\n }\n }\n } catch {\n // Reading the temporary directory is best-effort.\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"spool.cjs","names":["tmpdir","createGzip","join","randomUUID","openSync","createWriteStream","readFile","unlink","readdirSync","statSync"],"sources":["../src/spool.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n createWriteStream,\n openSync,\n readdirSync,\n statSync,\n unlinkSync,\n utimesSync,\n type WriteStream,\n writeFileSync,\n} from \"node:fs\";\nimport { readFile, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createGzip, type Gzip } from \"node:zlib\";\nimport { logWarning, resetWarning } from \"./logger.js\";\n\nconst WRITE_FAILURE_WARNING = \"Error writing to disk, dropping buffered telemetry\";\n\nconst SIGNALS = [\"traces\", \"logs\", \"metrics\"] as const;\nexport type Signal = (typeof SIGNALS)[number];\n\nconst MAX_UNCOMPRESSED_FILE_SIZE = 4_000_000;\nconst MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS = 59 * 60 * 1000;\nconst MAX_SPOOL_SIZE_DISK = 50_000_000;\nconst MAX_SPOOL_SIZE_MEMORY = 10_000_000;\nconst MAX_UNTOUCHED_FILE_AGE_MILLIS = 2 * 60 * 60 * 1000;\nconst SPOOL_FILE_NAME_PATTERN = /^apitally-.*\\.gz$/;\n\n// Operations for each signal are serialized because gzip and file streams\n// complete asynchronously.\nexport class Spool {\n readonly inMemory: boolean;\n maxSize: number;\n readonly current = new Map<Signal, SpoolFile>();\n private readonly closed: SpoolFile[] = [];\n private filesClosedSinceExportRotation = 0;\n private readonly tempDir: string;\n private readonly queues: Record<Signal, Promise<void>> = {\n traces: Promise.resolve(),\n logs: Promise.resolve(),\n metrics: Promise.resolve(),\n };\n\n constructor(tempDir: string = tmpdir()) {\n this.tempDir = tempDir;\n this.inMemory = !isTempDirWritable(tempDir);\n if (this.inMemory) {\n logWarning(\n `Unable to create temporary files, buffering telemetry in memory (max ${MAX_SPOOL_SIZE_MEMORY / 1_000_000} MB)`,\n );\n } else {\n cleanupOrphanedFiles(tempDir);\n }\n this.maxSize = this.inMemory ? MAX_SPOOL_SIZE_MEMORY : MAX_SPOOL_SIZE_DISK;\n }\n\n append(signal: Signal, payload: Uint8Array): Promise<void> {\n return this.enqueue(signal, async () => {\n let file = this.current.get(signal);\n if (file && file.uncompressedSize + payload.length > MAX_UNCOMPRESSED_FILE_SIZE) {\n await this.closeCurrentFile(signal);\n file = undefined;\n }\n try {\n if (!file) {\n file = new SpoolFile(signal, this.inMemory ? undefined : this.tempDir);\n this.current.set(signal, file);\n }\n await file.write(payload);\n // Writes recovered; the write-failure warning may fire again later.\n resetWarning(WRITE_FAILURE_WARNING);\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await this.discardCurrentFile(signal);\n }\n await this.evict();\n });\n }\n\n // Current files close only when no backlog exists, avoiding one file per cycle.\n // Sequential rotation preserves send order. Returns the number of files closed\n // since the previous call, including files closed at the size limit.\n async rotateForExport(): Promise<number> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, async () => {\n if (this.current.has(signal) && !this.closed.some((file) => file.signal === signal)) {\n await this.closeCurrentFile(signal);\n }\n });\n }\n await this.evict();\n const closedFileCount = this.filesClosedSinceExportRotation;\n this.filesClosedSinceExportRotation = 0;\n return closedFileCount;\n }\n\n async closeCurrentFiles(): Promise<void> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, () => this.closeCurrentFile(signal));\n }\n }\n\n pendingFiles(): SpoolFile[] {\n return [...this.closed];\n }\n\n async deleteFile(file: SpoolFile): Promise<void> {\n const index = this.closed.indexOf(file);\n if (index >= 0) {\n this.closed.splice(index, 1);\n }\n await file.delete();\n }\n\n // Apitally ingest deduplicates for one hour; a later retry could ingest the file twice.\n deleteIfExpired(file: SpoolFile): Promise<void> | undefined {\n if (!file.isExpired()) {\n return undefined;\n }\n logWarning(`Buffered ${file.signal} could not be delivered within an hour and were dropped`);\n return this.deleteFile(file);\n }\n\n // Refresh mtimes so orphan cleanup in another process preserves live files.\n touchFiles(): void {\n for (const file of [...this.current.values(), ...this.closed]) {\n file.touch();\n }\n }\n\n async clear(): Promise<void> {\n await Promise.all(\n SIGNALS.map((signal) => this.enqueue(signal, () => this.discardCurrentFile(signal))),\n );\n const files = this.closed.splice(0);\n await Promise.all(files.map((file) => file.delete()));\n }\n\n private enqueue(signal: Signal, operation: () => Promise<void>): Promise<void> {\n const result = this.queues[signal].then(operation);\n this.queues[signal] = result.catch(() => undefined);\n return result;\n }\n\n private async closeCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (!file) {\n return;\n }\n this.current.delete(signal);\n try {\n await file.close();\n this.closed.push(file);\n this.filesClosedSinceExportRotation += 1;\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await file.delete();\n }\n }\n\n private async discardCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (file) {\n this.current.delete(signal);\n await file.delete();\n }\n }\n\n private async evict(): Promise<void> {\n const deletions = [...this.closed].map((file) => this.deleteIfExpired(file));\n while (this.totalSize() > this.maxSize) {\n // Prefer retaining metrics, but enforce the size limit when only metrics remain.\n const oldest = this.closed.find((file) => file.signal !== \"metrics\") ?? this.closed[0];\n if (!oldest) {\n break;\n }\n logWarning(`Buffer size limit reached, dropping oldest buffered ${oldest.signal}`);\n deletions.push(this.deleteFile(oldest));\n }\n await Promise.all(deletions);\n }\n\n private totalSize(): number {\n let total = 0;\n for (const file of [...this.closed, ...this.current.values()]) {\n total += file.storedSize;\n }\n return total;\n }\n}\n\n// One gzip stream of concatenated OTLP request payloads for a single signal,\n// written through to a temp file or, as fallback, an in-memory buffer.\nexport class SpoolFile {\n readonly signal: Signal;\n readonly path?: string;\n firstAttemptAtMillis?: number;\n uncompressedSize = 0;\n readonly gzip: Gzip;\n private readonly fileStream?: WriteStream;\n private readonly memoryChunks?: Buffer[];\n private memorySize = 0;\n private streamError?: Error;\n private readonly closedPromise: Promise<void>;\n\n constructor(signal: Signal, tempDir?: string) {\n this.signal = signal;\n this.gzip = createGzip();\n let closeTarget: Gzip | WriteStream;\n let closeEvent: string;\n if (tempDir === undefined) {\n const chunks: Buffer[] = [];\n this.memoryChunks = chunks;\n this.gzip.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n this.memorySize += chunk.length;\n });\n closeTarget = this.gzip;\n closeEvent = \"end\";\n } else {\n this.path = join(tempDir, `apitally-${randomUUID()}.gz`);\n // A synchronous exclusive open reports creation errors to the triggering append.\n const fd = openSync(this.path, \"wx\", 0o600);\n this.fileStream = createWriteStream(this.path, { fd });\n this.gzip.pipe(this.fileStream);\n closeTarget = this.fileStream;\n closeEvent = \"close\";\n }\n const streams: (Gzip | WriteStream)[] = this.fileStream\n ? [this.gzip, this.fileStream]\n : [this.gzip];\n this.closedPromise = new Promise((resolve, reject) => {\n closeTarget.once(closeEvent, resolve);\n for (const stream of streams) {\n stream.once(\"error\", reject);\n }\n });\n // Stream errors can occur outside an awaited operation; rejection handling\n // prevents an unhandled rejection.\n this.closedPromise.catch(() => undefined);\n for (const stream of streams) {\n stream.on(\"error\", (error: Error) => {\n this.streamError ??= error;\n });\n }\n }\n\n write(payload: Uint8Array): Promise<void> {\n return new Promise((resolve, reject) => {\n if (this.streamError) {\n reject(this.streamError);\n return;\n }\n this.gzip.write(payload, (error) => {\n const failure = error ?? this.streamError;\n if (failure) {\n reject(failure);\n } else {\n this.uncompressedSize += payload.length;\n resolve();\n }\n });\n });\n }\n\n async close(): Promise<void> {\n if (this.streamError) {\n throw this.streamError;\n }\n this.gzip.end();\n await this.closedPromise;\n }\n\n get storedSize(): number {\n return this.fileStream ? this.fileStream.bytesWritten : this.memorySize;\n }\n\n markAttempt(): void {\n this.firstAttemptAtMillis ??= performance.now();\n }\n\n isExpired(): boolean {\n return (\n this.firstAttemptAtMillis !== undefined &&\n performance.now() - this.firstAttemptAtMillis > MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS\n );\n }\n\n touch(): void {\n if (this.path) {\n try {\n const now = new Date();\n utimesSync(this.path, now, now);\n } catch {\n // The file may already be gone; orphan cleanup only targets stale files.\n }\n }\n }\n\n readStoredBytes(): Promise<Buffer> {\n if (this.path) {\n return readFile(this.path);\n }\n return Promise.resolve(Buffer.concat(this.memoryChunks ?? []));\n }\n\n async delete(): Promise<void> {\n this.gzip.destroy();\n this.fileStream?.destroy();\n if (this.path) {\n await unlink(this.path).catch(() => undefined);\n }\n }\n}\n\nfunction isTempDirWritable(tempDir: string): boolean {\n const probePath = join(tempDir, `apitally-${randomUUID()}.gz`);\n try {\n writeFileSync(probePath, \"\", { flag: \"wx\", mode: 0o600 });\n unlinkSync(probePath);\n return true;\n } catch {\n return false;\n }\n}\n\n// Live processes refresh mtimes each cycle, so only stale spool files from dead\n// processes are removed.\nfunction cleanupOrphanedFiles(tempDir: string): void {\n const cutoff = Date.now() - MAX_UNTOUCHED_FILE_AGE_MILLIS;\n try {\n for (const name of readdirSync(tempDir)) {\n if (!SPOOL_FILE_NAME_PATTERN.test(name)) {\n continue;\n }\n try {\n const filePath = join(tempDir, name);\n if (statSync(filePath).mtimeMs < cutoff) {\n unlinkSync(filePath);\n }\n } catch {\n // Another process may have removed the file first.\n }\n }\n } catch {\n // Reading the temporary directory is best-effort.\n }\n}\n"],"mappings":"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"}
package/dist/spool.d.cts CHANGED
@@ -7,11 +7,12 @@ export declare class Spool {
7
7
  maxSize: number;
8
8
  readonly current: Map<"traces" | "logs" | "metrics", SpoolFile>;
9
9
  private readonly closed;
10
+ private filesClosedSinceExportRotation;
10
11
  private readonly tempDir;
11
12
  private readonly queues;
12
13
  constructor(tempDir?: string);
13
14
  append(signal: Signal, payload: Uint8Array): Promise<void>;
14
- rotateForExport(): Promise<void>;
15
+ rotateForExport(): Promise<number>;
15
16
  closeCurrentFiles(): Promise<void>;
16
17
  pendingFiles(): SpoolFile[];
17
18
  deleteFile(file: SpoolFile): Promise<void>;
package/dist/spool.d.ts CHANGED
@@ -7,11 +7,12 @@ export declare class Spool {
7
7
  maxSize: number;
8
8
  readonly current: Map<"traces" | "logs" | "metrics", SpoolFile>;
9
9
  private readonly closed;
10
+ private filesClosedSinceExportRotation;
10
11
  private readonly tempDir;
11
12
  private readonly queues;
12
13
  constructor(tempDir?: string);
13
14
  append(signal: Signal, payload: Uint8Array): Promise<void>;
14
- rotateForExport(): Promise<void>;
15
+ rotateForExport(): Promise<number>;
15
16
  closeCurrentFiles(): Promise<void>;
16
17
  pendingFiles(): SpoolFile[];
17
18
  deleteFile(file: SpoolFile): Promise<void>;
package/dist/spool.js CHANGED
@@ -23,6 +23,7 @@ var Spool = class {
23
23
  maxSize;
24
24
  current = /* @__PURE__ */ new Map();
25
25
  closed = [];
26
+ filesClosedSinceExportRotation = 0;
26
27
  tempDir;
27
28
  queues = {
28
29
  traces: Promise.resolve(),
@@ -62,6 +63,9 @@ var Spool = class {
62
63
  if (this.current.has(signal) && !this.closed.some((file) => file.signal === signal)) await this.closeCurrentFile(signal);
63
64
  });
64
65
  await this.evict();
66
+ const closedFileCount = this.filesClosedSinceExportRotation;
67
+ this.filesClosedSinceExportRotation = 0;
68
+ return closedFileCount;
65
69
  }
66
70
  async closeCurrentFiles() {
67
71
  for (const signal of SIGNALS) await this.enqueue(signal, () => this.closeCurrentFile(signal));
@@ -99,6 +103,7 @@ var Spool = class {
99
103
  try {
100
104
  await file.close();
101
105
  this.closed.push(file);
106
+ this.filesClosedSinceExportRotation += 1;
102
107
  } catch {
103
108
  logWarning(WRITE_FAILURE_WARNING);
104
109
  await file.delete();
package/dist/spool.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"spool.js","names":[],"sources":["../src/spool.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n createWriteStream,\n openSync,\n readdirSync,\n statSync,\n unlinkSync,\n utimesSync,\n type WriteStream,\n writeFileSync,\n} from \"node:fs\";\nimport { readFile, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createGzip, type Gzip } from \"node:zlib\";\nimport { logWarning, resetWarning } from \"./logger.js\";\n\nconst WRITE_FAILURE_WARNING = \"Error writing to disk, dropping buffered telemetry\";\n\nconst SIGNALS = [\"traces\", \"logs\", \"metrics\"] as const;\nexport type Signal = (typeof SIGNALS)[number];\n\nconst MAX_UNCOMPRESSED_FILE_SIZE = 4_000_000;\nconst MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS = 59 * 60 * 1000;\nconst MAX_SPOOL_SIZE_DISK = 50_000_000;\nconst MAX_SPOOL_SIZE_MEMORY = 10_000_000;\nconst MAX_UNTOUCHED_FILE_AGE_MILLIS = 2 * 60 * 60 * 1000;\nconst SPOOL_FILE_NAME_PATTERN = /^apitally-.*\\.gz$/;\n\n// Operations for each signal are serialized because gzip and file streams\n// complete asynchronously.\nexport class Spool {\n readonly inMemory: boolean;\n maxSize: number;\n readonly current = new Map<Signal, SpoolFile>();\n private readonly closed: SpoolFile[] = [];\n private readonly tempDir: string;\n private readonly queues: Record<Signal, Promise<void>> = {\n traces: Promise.resolve(),\n logs: Promise.resolve(),\n metrics: Promise.resolve(),\n };\n\n constructor(tempDir: string = tmpdir()) {\n this.tempDir = tempDir;\n this.inMemory = !isTempDirWritable(tempDir);\n if (this.inMemory) {\n logWarning(\n `Unable to create temporary files, buffering telemetry in memory (max ${MAX_SPOOL_SIZE_MEMORY / 1_000_000} MB)`,\n );\n } else {\n cleanupOrphanedFiles(tempDir);\n }\n this.maxSize = this.inMemory ? MAX_SPOOL_SIZE_MEMORY : MAX_SPOOL_SIZE_DISK;\n }\n\n append(signal: Signal, payload: Uint8Array): Promise<void> {\n return this.enqueue(signal, async () => {\n let file = this.current.get(signal);\n if (file && file.uncompressedSize + payload.length > MAX_UNCOMPRESSED_FILE_SIZE) {\n await this.closeCurrentFile(signal);\n file = undefined;\n }\n try {\n if (!file) {\n file = new SpoolFile(signal, this.inMemory ? undefined : this.tempDir);\n this.current.set(signal, file);\n }\n await file.write(payload);\n // Writes recovered; the write-failure warning may fire again later.\n resetWarning(WRITE_FAILURE_WARNING);\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await this.discardCurrentFile(signal);\n }\n await this.evict();\n });\n }\n\n // Current files close only when no backlog exists, avoiding one file per cycle.\n // Sequential rotation preserves send order.\n async rotateForExport(): Promise<void> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, async () => {\n if (this.current.has(signal) && !this.closed.some((file) => file.signal === signal)) {\n await this.closeCurrentFile(signal);\n }\n });\n }\n await this.evict();\n }\n\n async closeCurrentFiles(): Promise<void> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, () => this.closeCurrentFile(signal));\n }\n }\n\n pendingFiles(): SpoolFile[] {\n return [...this.closed];\n }\n\n async deleteFile(file: SpoolFile): Promise<void> {\n const index = this.closed.indexOf(file);\n if (index >= 0) {\n this.closed.splice(index, 1);\n }\n await file.delete();\n }\n\n // Apitally ingest deduplicates for one hour; a later retry could ingest the file twice.\n deleteIfExpired(file: SpoolFile): Promise<void> | undefined {\n if (!file.isExpired()) {\n return undefined;\n }\n logWarning(`Buffered ${file.signal} could not be delivered within an hour and were dropped`);\n return this.deleteFile(file);\n }\n\n // Refresh mtimes so orphan cleanup in another process preserves live files.\n touchFiles(): void {\n for (const file of [...this.current.values(), ...this.closed]) {\n file.touch();\n }\n }\n\n async clear(): Promise<void> {\n await Promise.all(\n SIGNALS.map((signal) => this.enqueue(signal, () => this.discardCurrentFile(signal))),\n );\n const files = this.closed.splice(0);\n await Promise.all(files.map((file) => file.delete()));\n }\n\n private enqueue(signal: Signal, operation: () => Promise<void>): Promise<void> {\n const result = this.queues[signal].then(operation);\n this.queues[signal] = result.catch(() => undefined);\n return result;\n }\n\n private async closeCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (!file) {\n return;\n }\n this.current.delete(signal);\n try {\n await file.close();\n this.closed.push(file);\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await file.delete();\n }\n }\n\n private async discardCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (file) {\n this.current.delete(signal);\n await file.delete();\n }\n }\n\n private async evict(): Promise<void> {\n const deletions = [...this.closed].map((file) => this.deleteIfExpired(file));\n while (this.totalSize() > this.maxSize) {\n // Prefer retaining metrics, but enforce the size limit when only metrics remain.\n const oldest = this.closed.find((file) => file.signal !== \"metrics\") ?? this.closed[0];\n if (!oldest) {\n break;\n }\n logWarning(`Buffer size limit reached, dropping oldest buffered ${oldest.signal}`);\n deletions.push(this.deleteFile(oldest));\n }\n await Promise.all(deletions);\n }\n\n private totalSize(): number {\n let total = 0;\n for (const file of [...this.closed, ...this.current.values()]) {\n total += file.storedSize;\n }\n return total;\n }\n}\n\n// One gzip stream of concatenated OTLP request payloads for a single signal,\n// written through to a temp file or, as fallback, an in-memory buffer.\nexport class SpoolFile {\n readonly signal: Signal;\n readonly path?: string;\n firstAttemptAtMillis?: number;\n uncompressedSize = 0;\n readonly gzip: Gzip;\n private readonly fileStream?: WriteStream;\n private readonly memoryChunks?: Buffer[];\n private memorySize = 0;\n private streamError?: Error;\n private readonly closedPromise: Promise<void>;\n\n constructor(signal: Signal, tempDir?: string) {\n this.signal = signal;\n this.gzip = createGzip();\n let closeTarget: Gzip | WriteStream;\n let closeEvent: string;\n if (tempDir === undefined) {\n const chunks: Buffer[] = [];\n this.memoryChunks = chunks;\n this.gzip.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n this.memorySize += chunk.length;\n });\n closeTarget = this.gzip;\n closeEvent = \"end\";\n } else {\n this.path = join(tempDir, `apitally-${randomUUID()}.gz`);\n // A synchronous exclusive open reports creation errors to the triggering append.\n const fd = openSync(this.path, \"wx\", 0o600);\n this.fileStream = createWriteStream(this.path, { fd });\n this.gzip.pipe(this.fileStream);\n closeTarget = this.fileStream;\n closeEvent = \"close\";\n }\n const streams: (Gzip | WriteStream)[] = this.fileStream\n ? [this.gzip, this.fileStream]\n : [this.gzip];\n this.closedPromise = new Promise((resolve, reject) => {\n closeTarget.once(closeEvent, resolve);\n for (const stream of streams) {\n stream.once(\"error\", reject);\n }\n });\n // Stream errors can occur outside an awaited operation; rejection handling\n // prevents an unhandled rejection.\n this.closedPromise.catch(() => undefined);\n for (const stream of streams) {\n stream.on(\"error\", (error: Error) => {\n this.streamError ??= error;\n });\n }\n }\n\n write(payload: Uint8Array): Promise<void> {\n return new Promise((resolve, reject) => {\n if (this.streamError) {\n reject(this.streamError);\n return;\n }\n this.gzip.write(payload, (error) => {\n const failure = error ?? this.streamError;\n if (failure) {\n reject(failure);\n } else {\n this.uncompressedSize += payload.length;\n resolve();\n }\n });\n });\n }\n\n async close(): Promise<void> {\n if (this.streamError) {\n throw this.streamError;\n }\n this.gzip.end();\n await this.closedPromise;\n }\n\n get storedSize(): number {\n return this.fileStream ? this.fileStream.bytesWritten : this.memorySize;\n }\n\n markAttempt(): void {\n this.firstAttemptAtMillis ??= performance.now();\n }\n\n isExpired(): boolean {\n return (\n this.firstAttemptAtMillis !== undefined &&\n performance.now() - this.firstAttemptAtMillis > MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS\n );\n }\n\n touch(): void {\n if (this.path) {\n try {\n const now = new Date();\n utimesSync(this.path, now, now);\n } catch {\n // The file may already be gone; orphan cleanup only targets stale files.\n }\n }\n }\n\n readStoredBytes(): Promise<Buffer> {\n if (this.path) {\n return readFile(this.path);\n }\n return Promise.resolve(Buffer.concat(this.memoryChunks ?? []));\n }\n\n async delete(): Promise<void> {\n this.gzip.destroy();\n this.fileStream?.destroy();\n if (this.path) {\n await unlink(this.path).catch(() => undefined);\n }\n }\n}\n\nfunction isTempDirWritable(tempDir: string): boolean {\n const probePath = join(tempDir, `apitally-${randomUUID()}.gz`);\n try {\n writeFileSync(probePath, \"\", { flag: \"wx\", mode: 0o600 });\n unlinkSync(probePath);\n return true;\n } catch {\n return false;\n }\n}\n\n// Live processes refresh mtimes each cycle, so only stale spool files from dead\n// processes are removed.\nfunction cleanupOrphanedFiles(tempDir: string): void {\n const cutoff = Date.now() - MAX_UNTOUCHED_FILE_AGE_MILLIS;\n try {\n for (const name of readdirSync(tempDir)) {\n if (!SPOOL_FILE_NAME_PATTERN.test(name)) {\n continue;\n }\n try {\n const filePath = join(tempDir, name);\n if (statSync(filePath).mtimeMs < cutoff) {\n unlinkSync(filePath);\n }\n } catch {\n // Another process may have removed the file first.\n }\n }\n } catch {\n // Reading the temporary directory is best-effort.\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"spool.js","names":[],"sources":["../src/spool.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n createWriteStream,\n openSync,\n readdirSync,\n statSync,\n unlinkSync,\n utimesSync,\n type WriteStream,\n writeFileSync,\n} from \"node:fs\";\nimport { readFile, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createGzip, type Gzip } from \"node:zlib\";\nimport { logWarning, resetWarning } from \"./logger.js\";\n\nconst WRITE_FAILURE_WARNING = \"Error writing to disk, dropping buffered telemetry\";\n\nconst SIGNALS = [\"traces\", \"logs\", \"metrics\"] as const;\nexport type Signal = (typeof SIGNALS)[number];\n\nconst MAX_UNCOMPRESSED_FILE_SIZE = 4_000_000;\nconst MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS = 59 * 60 * 1000;\nconst MAX_SPOOL_SIZE_DISK = 50_000_000;\nconst MAX_SPOOL_SIZE_MEMORY = 10_000_000;\nconst MAX_UNTOUCHED_FILE_AGE_MILLIS = 2 * 60 * 60 * 1000;\nconst SPOOL_FILE_NAME_PATTERN = /^apitally-.*\\.gz$/;\n\n// Operations for each signal are serialized because gzip and file streams\n// complete asynchronously.\nexport class Spool {\n readonly inMemory: boolean;\n maxSize: number;\n readonly current = new Map<Signal, SpoolFile>();\n private readonly closed: SpoolFile[] = [];\n private filesClosedSinceExportRotation = 0;\n private readonly tempDir: string;\n private readonly queues: Record<Signal, Promise<void>> = {\n traces: Promise.resolve(),\n logs: Promise.resolve(),\n metrics: Promise.resolve(),\n };\n\n constructor(tempDir: string = tmpdir()) {\n this.tempDir = tempDir;\n this.inMemory = !isTempDirWritable(tempDir);\n if (this.inMemory) {\n logWarning(\n `Unable to create temporary files, buffering telemetry in memory (max ${MAX_SPOOL_SIZE_MEMORY / 1_000_000} MB)`,\n );\n } else {\n cleanupOrphanedFiles(tempDir);\n }\n this.maxSize = this.inMemory ? MAX_SPOOL_SIZE_MEMORY : MAX_SPOOL_SIZE_DISK;\n }\n\n append(signal: Signal, payload: Uint8Array): Promise<void> {\n return this.enqueue(signal, async () => {\n let file = this.current.get(signal);\n if (file && file.uncompressedSize + payload.length > MAX_UNCOMPRESSED_FILE_SIZE) {\n await this.closeCurrentFile(signal);\n file = undefined;\n }\n try {\n if (!file) {\n file = new SpoolFile(signal, this.inMemory ? undefined : this.tempDir);\n this.current.set(signal, file);\n }\n await file.write(payload);\n // Writes recovered; the write-failure warning may fire again later.\n resetWarning(WRITE_FAILURE_WARNING);\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await this.discardCurrentFile(signal);\n }\n await this.evict();\n });\n }\n\n // Current files close only when no backlog exists, avoiding one file per cycle.\n // Sequential rotation preserves send order. Returns the number of files closed\n // since the previous call, including files closed at the size limit.\n async rotateForExport(): Promise<number> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, async () => {\n if (this.current.has(signal) && !this.closed.some((file) => file.signal === signal)) {\n await this.closeCurrentFile(signal);\n }\n });\n }\n await this.evict();\n const closedFileCount = this.filesClosedSinceExportRotation;\n this.filesClosedSinceExportRotation = 0;\n return closedFileCount;\n }\n\n async closeCurrentFiles(): Promise<void> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, () => this.closeCurrentFile(signal));\n }\n }\n\n pendingFiles(): SpoolFile[] {\n return [...this.closed];\n }\n\n async deleteFile(file: SpoolFile): Promise<void> {\n const index = this.closed.indexOf(file);\n if (index >= 0) {\n this.closed.splice(index, 1);\n }\n await file.delete();\n }\n\n // Apitally ingest deduplicates for one hour; a later retry could ingest the file twice.\n deleteIfExpired(file: SpoolFile): Promise<void> | undefined {\n if (!file.isExpired()) {\n return undefined;\n }\n logWarning(`Buffered ${file.signal} could not be delivered within an hour and were dropped`);\n return this.deleteFile(file);\n }\n\n // Refresh mtimes so orphan cleanup in another process preserves live files.\n touchFiles(): void {\n for (const file of [...this.current.values(), ...this.closed]) {\n file.touch();\n }\n }\n\n async clear(): Promise<void> {\n await Promise.all(\n SIGNALS.map((signal) => this.enqueue(signal, () => this.discardCurrentFile(signal))),\n );\n const files = this.closed.splice(0);\n await Promise.all(files.map((file) => file.delete()));\n }\n\n private enqueue(signal: Signal, operation: () => Promise<void>): Promise<void> {\n const result = this.queues[signal].then(operation);\n this.queues[signal] = result.catch(() => undefined);\n return result;\n }\n\n private async closeCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (!file) {\n return;\n }\n this.current.delete(signal);\n try {\n await file.close();\n this.closed.push(file);\n this.filesClosedSinceExportRotation += 1;\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await file.delete();\n }\n }\n\n private async discardCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (file) {\n this.current.delete(signal);\n await file.delete();\n }\n }\n\n private async evict(): Promise<void> {\n const deletions = [...this.closed].map((file) => this.deleteIfExpired(file));\n while (this.totalSize() > this.maxSize) {\n // Prefer retaining metrics, but enforce the size limit when only metrics remain.\n const oldest = this.closed.find((file) => file.signal !== \"metrics\") ?? this.closed[0];\n if (!oldest) {\n break;\n }\n logWarning(`Buffer size limit reached, dropping oldest buffered ${oldest.signal}`);\n deletions.push(this.deleteFile(oldest));\n }\n await Promise.all(deletions);\n }\n\n private totalSize(): number {\n let total = 0;\n for (const file of [...this.closed, ...this.current.values()]) {\n total += file.storedSize;\n }\n return total;\n }\n}\n\n// One gzip stream of concatenated OTLP request payloads for a single signal,\n// written through to a temp file or, as fallback, an in-memory buffer.\nexport class SpoolFile {\n readonly signal: Signal;\n readonly path?: string;\n firstAttemptAtMillis?: number;\n uncompressedSize = 0;\n readonly gzip: Gzip;\n private readonly fileStream?: WriteStream;\n private readonly memoryChunks?: Buffer[];\n private memorySize = 0;\n private streamError?: Error;\n private readonly closedPromise: Promise<void>;\n\n constructor(signal: Signal, tempDir?: string) {\n this.signal = signal;\n this.gzip = createGzip();\n let closeTarget: Gzip | WriteStream;\n let closeEvent: string;\n if (tempDir === undefined) {\n const chunks: Buffer[] = [];\n this.memoryChunks = chunks;\n this.gzip.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n this.memorySize += chunk.length;\n });\n closeTarget = this.gzip;\n closeEvent = \"end\";\n } else {\n this.path = join(tempDir, `apitally-${randomUUID()}.gz`);\n // A synchronous exclusive open reports creation errors to the triggering append.\n const fd = openSync(this.path, \"wx\", 0o600);\n this.fileStream = createWriteStream(this.path, { fd });\n this.gzip.pipe(this.fileStream);\n closeTarget = this.fileStream;\n closeEvent = \"close\";\n }\n const streams: (Gzip | WriteStream)[] = this.fileStream\n ? [this.gzip, this.fileStream]\n : [this.gzip];\n this.closedPromise = new Promise((resolve, reject) => {\n closeTarget.once(closeEvent, resolve);\n for (const stream of streams) {\n stream.once(\"error\", reject);\n }\n });\n // Stream errors can occur outside an awaited operation; rejection handling\n // prevents an unhandled rejection.\n this.closedPromise.catch(() => undefined);\n for (const stream of streams) {\n stream.on(\"error\", (error: Error) => {\n this.streamError ??= error;\n });\n }\n }\n\n write(payload: Uint8Array): Promise<void> {\n return new Promise((resolve, reject) => {\n if (this.streamError) {\n reject(this.streamError);\n return;\n }\n this.gzip.write(payload, (error) => {\n const failure = error ?? this.streamError;\n if (failure) {\n reject(failure);\n } else {\n this.uncompressedSize += payload.length;\n resolve();\n }\n });\n });\n }\n\n async close(): Promise<void> {\n if (this.streamError) {\n throw this.streamError;\n }\n this.gzip.end();\n await this.closedPromise;\n }\n\n get storedSize(): number {\n return this.fileStream ? this.fileStream.bytesWritten : this.memorySize;\n }\n\n markAttempt(): void {\n this.firstAttemptAtMillis ??= performance.now();\n }\n\n isExpired(): boolean {\n return (\n this.firstAttemptAtMillis !== undefined &&\n performance.now() - this.firstAttemptAtMillis > MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS\n );\n }\n\n touch(): void {\n if (this.path) {\n try {\n const now = new Date();\n utimesSync(this.path, now, now);\n } catch {\n // The file may already be gone; orphan cleanup only targets stale files.\n }\n }\n }\n\n readStoredBytes(): Promise<Buffer> {\n if (this.path) {\n return readFile(this.path);\n }\n return Promise.resolve(Buffer.concat(this.memoryChunks ?? []));\n }\n\n async delete(): Promise<void> {\n this.gzip.destroy();\n this.fileStream?.destroy();\n if (this.path) {\n await unlink(this.path).catch(() => undefined);\n }\n }\n}\n\nfunction isTempDirWritable(tempDir: string): boolean {\n const probePath = join(tempDir, `apitally-${randomUUID()}.gz`);\n try {\n writeFileSync(probePath, \"\", { flag: \"wx\", mode: 0o600 });\n unlinkSync(probePath);\n return true;\n } catch {\n return false;\n }\n}\n\n// Live processes refresh mtimes each cycle, so only stale spool files from dead\n// processes are removed.\nfunction cleanupOrphanedFiles(tempDir: string): void {\n const cutoff = Date.now() - MAX_UNTOUCHED_FILE_AGE_MILLIS;\n try {\n for (const name of readdirSync(tempDir)) {\n if (!SPOOL_FILE_NAME_PATTERN.test(name)) {\n continue;\n }\n try {\n const filePath = join(tempDir, name);\n if (statSync(filePath).mtimeMs < cutoff) {\n unlinkSync(filePath);\n }\n } catch {\n // Another process may have removed the file first.\n }\n }\n } catch {\n // Reading the temporary directory is best-effort.\n }\n}\n"],"mappings":"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"}
@@ -3,14 +3,13 @@ const require_bodyCapture = require("./bodyCapture.cjs");
3
3
  let node_zlib = require("node:zlib");
4
4
  //#region src/validationErrors.ts
5
5
  const VALIDATION_ERROR_EVENT_NAME = "apitally.request.validation_error";
6
- const MAX_GROUPS = 100;
6
+ const MAX_ERRORS = 100;
7
7
  const MAX_CONSUMER_LENGTH = 128;
8
8
  const MAX_PATH_LENGTH = 2e3;
9
9
  const MAX_SOURCE_LENGTH = 32;
10
10
  const MAX_FIELD_LENGTH = 2048;
11
11
  const MAX_MESSAGE_LENGTH = 2048;
12
12
  const MAX_TYPE_LENGTH = 128;
13
- const MAX_COUNT = 2 ** 32 - 1;
14
13
  const SOURCE_ALIASES = {
15
14
  body: "body",
16
15
  query: "query",
@@ -23,48 +22,52 @@ const SOURCE_ALIASES = {
23
22
  headers: "header",
24
23
  cookies: "cookie"
25
24
  };
26
- const VALIDATION_ERROR_GROUPS_KEY = Symbol.for("apitally.validationErrorGroups");
27
- const validationErrorGroupsHolder = globalThis;
28
- const validationErrorGroups = validationErrorGroupsHolder[VALIDATION_ERROR_GROUPS_KEY] ?? /* @__PURE__ */ new Map();
29
- validationErrorGroupsHolder[VALIDATION_ERROR_GROUPS_KEY] = validationErrorGroups;
25
+ const VALIDATION_ERROR_AGGREGATES_KEY = Symbol.for("apitally.validationErrorAggregates");
26
+ const validationErrorAggregatesHolder = globalThis;
27
+ const validationErrorAggregates = validationErrorAggregatesHolder[VALIDATION_ERROR_AGGREGATES_KEY] ?? /* @__PURE__ */ new Map();
28
+ validationErrorAggregatesHolder[VALIDATION_ERROR_AGGREGATES_KEY] = validationErrorAggregates;
30
29
  function isValidationResponseStatus(statusCode) {
31
30
  return statusCode === 400 || statusCode === 422;
32
31
  }
33
32
  function addValidationErrors(consumer, method, path, details) {
34
33
  method = method.toUpperCase();
35
34
  if (method === "OPTIONS" || !path) return;
35
+ consumer = consumer?.slice(0, MAX_CONSUMER_LENGTH);
36
36
  for (const detail of details) {
37
- const group = {
37
+ const fields = {
38
38
  method,
39
39
  path: path.slice(0, MAX_PATH_LENGTH),
40
40
  source: detail.source.slice(0, MAX_SOURCE_LENGTH),
41
41
  field: detail.field.slice(0, MAX_FIELD_LENGTH),
42
42
  message: detail.message.slice(0, MAX_MESSAGE_LENGTH),
43
- type: detail.type.slice(0, MAX_TYPE_LENGTH),
44
- count: 1
43
+ type: detail.type.slice(0, MAX_TYPE_LENGTH)
45
44
  };
46
- if (consumer !== void 0) group.consumer = consumer.slice(0, MAX_CONSUMER_LENGTH);
47
- const key = [
48
- group.consumer ?? "",
49
- group.method,
50
- group.path,
51
- group.source,
52
- group.field,
53
- group.message,
54
- group.type
55
- ].join("\0");
56
- const existing = validationErrorGroups.get(key);
57
- if (existing) existing.count = Math.min(existing.count + 1, MAX_COUNT);
58
- else if (validationErrorGroups.size < MAX_GROUPS) validationErrorGroups.set(key, group);
45
+ const key = Object.values(fields).join("\0");
46
+ let aggregate = validationErrorAggregates.get(key);
47
+ if (!aggregate) {
48
+ if (validationErrorAggregates.size >= MAX_ERRORS) continue;
49
+ aggregate = {
50
+ ...fields,
51
+ counts: /* @__PURE__ */ new Map()
52
+ };
53
+ validationErrorAggregates.set(key, aggregate);
54
+ }
55
+ aggregate.counts.set(consumer, (aggregate.counts.get(consumer) ?? 0) + 1);
59
56
  }
60
57
  }
61
58
  function drainValidationErrors() {
62
- const groups = [...validationErrorGroups.values()];
63
- validationErrorGroups.clear();
64
- return groups;
59
+ const aggregates = [...validationErrorAggregates.values()];
60
+ validationErrorAggregates.clear();
61
+ return aggregates.map(({ counts, ...fields }) => ({
62
+ ...fields,
63
+ counts: [...counts].map(([consumer, count]) => consumer === void 0 ? { count } : {
64
+ consumer,
65
+ count
66
+ })
67
+ }));
65
68
  }
66
69
  function resetValidationErrors() {
67
- validationErrorGroups.clear();
70
+ validationErrorAggregates.clear();
68
71
  }
69
72
  function normalizeSource(source) {
70
73
  return typeof source === "string" ? SOURCE_ALIASES[source.toLowerCase()] ?? "" : "";
@@ -1 +1 @@
1
- {"version":3,"file":"validationErrors.cjs","names":["brotliDecompressSync","MAX_BODY_SIZE","unzipSync"],"sources":["../src/validationErrors.ts"],"sourcesContent":["import { brotliDecompressSync, unzipSync } from \"node:zlib\";\nimport type { AnyValueMap } from \"@opentelemetry/api-logs\";\nimport { MAX_BODY_SIZE } from \"./bodyCapture.js\";\n\nexport const VALIDATION_ERROR_EVENT_NAME = \"apitally.request.validation_error\";\n\nconst MAX_GROUPS = 100;\nconst MAX_CONSUMER_LENGTH = 128;\nconst MAX_PATH_LENGTH = 2_000;\nconst MAX_SOURCE_LENGTH = 32;\nconst MAX_FIELD_LENGTH = 2_048;\nconst MAX_MESSAGE_LENGTH = 2_048;\nconst MAX_TYPE_LENGTH = 128;\nconst MAX_COUNT = 2 ** 32 - 1;\n\nconst SOURCE_ALIASES: Record<string, string> = {\n body: \"body\",\n query: \"query\",\n path: \"path\",\n header: \"header\",\n cookie: \"cookie\",\n querystring: \"query\",\n params: \"path\",\n path_params: \"path\",\n headers: \"header\",\n cookies: \"cookie\",\n};\n\nexport type ValidationErrorDetail = {\n source: string;\n field: string;\n message: string;\n type: string;\n};\n\ntype ValidationErrorGroup = ValidationErrorDetail & {\n consumer?: string;\n method: string;\n path: string;\n count: number;\n};\n\nconst VALIDATION_ERROR_GROUPS_KEY = Symbol.for(\"apitally.validationErrorGroups\");\nconst validationErrorGroupsHolder = globalThis as Record<\n symbol,\n Map<string, ValidationErrorGroup> | undefined\n>;\nconst validationErrorGroups =\n validationErrorGroupsHolder[VALIDATION_ERROR_GROUPS_KEY] ??\n new Map<string, ValidationErrorGroup>();\nvalidationErrorGroupsHolder[VALIDATION_ERROR_GROUPS_KEY] = validationErrorGroups;\n\nexport function isValidationResponseStatus(statusCode: number): boolean {\n return statusCode === 400 || statusCode === 422;\n}\n\nexport function addValidationErrors(\n consumer: string | undefined,\n method: string,\n path: string,\n details: ValidationErrorDetail[],\n): void {\n method = method.toUpperCase();\n if (method === \"OPTIONS\" || !path) {\n return;\n }\n for (const detail of details) {\n const group: ValidationErrorGroup = {\n method,\n path: path.slice(0, MAX_PATH_LENGTH),\n source: detail.source.slice(0, MAX_SOURCE_LENGTH),\n field: detail.field.slice(0, MAX_FIELD_LENGTH),\n message: detail.message.slice(0, MAX_MESSAGE_LENGTH),\n type: detail.type.slice(0, MAX_TYPE_LENGTH),\n count: 1,\n };\n if (consumer !== undefined) {\n group.consumer = consumer.slice(0, MAX_CONSUMER_LENGTH);\n }\n const key = [\n group.consumer ?? \"\",\n group.method,\n group.path,\n group.source,\n group.field,\n group.message,\n group.type,\n ].join(\"\\0\");\n const existing = validationErrorGroups.get(key);\n if (existing) {\n existing.count = Math.min(existing.count + 1, MAX_COUNT);\n } else if (validationErrorGroups.size < MAX_GROUPS) {\n validationErrorGroups.set(key, group);\n }\n }\n}\n\nexport function drainValidationErrors(): AnyValueMap[] {\n const groups = [...validationErrorGroups.values()];\n validationErrorGroups.clear();\n return groups;\n}\n\nexport function resetValidationErrors(): void {\n validationErrorGroups.clear();\n}\n\nexport function normalizeSource(source: unknown): string {\n return typeof source === \"string\" ? (SOURCE_ALIASES[source.toLowerCase()] ?? \"\") : \"\";\n}\n\n// Zod, Standard Schema, and TypeBox issues share this shape. A Standard Schema\n// path segment may be an object carrying the key; a TypeBox path is a JSON pointer.\nexport function formatIssues(issues: unknown, source = \"\"): ValidationErrorDetail[] {\n if (!Array.isArray(issues)) {\n return [];\n }\n const details: ValidationErrorDetail[] = [];\n for (const issue of issues) {\n if (!isRecord(issue) || typeof issue.message !== \"string\") {\n continue;\n }\n const path = Array.isArray(issue.path)\n ? issue.path\n : typeof issue.path === \"string\"\n ? issue.path\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\"))\n : [];\n details.push({\n source,\n field: path\n .map((segment: unknown) => (isRecord(segment) ? segment.key : segment))\n .filter((segment) => typeof segment === \"string\" || typeof segment === \"number\")\n .join(\".\"),\n message: issue.message,\n type: typeof issue.code === \"string\" ? issue.code : \"\",\n });\n }\n return details;\n}\n\n// zod 3 serializes issues as `error.issues`; zod 4 hides them as a JSON string\n// in `error.message`.\nexport function extractZodValidationErrors(body: unknown): ValidationErrorDetail[] {\n if (!isRecord(body)) {\n return [];\n }\n const error = isRecord(body.error) ? body.error : undefined;\n let issues = body.issues ?? error?.issues;\n if (issues === undefined && error?.name === \"ZodError\" && typeof error.message === \"string\") {\n try {\n issues = JSON.parse(error.message);\n } catch {\n return [];\n }\n }\n return formatIssues(issues);\n}\n\n// Recognized response shapes: zod issues, express-validator, and celebrate.\nexport function extractExpressValidationErrors(body: unknown): ValidationErrorDetail[] {\n if (!isRecord(body)) {\n return [];\n }\n const zodDetails = extractZodValidationErrors(body);\n if (zodDetails.length > 0) {\n return zodDetails;\n }\n if (Array.isArray(body.errors)) {\n return extractExpressValidatorErrors(body.errors);\n }\n if (isRecord(body.validation)) {\n return extractCelebrateErrors(body.validation);\n }\n return [];\n}\n\nfunction extractExpressValidatorErrors(errors: unknown[]): ValidationErrorDetail[] {\n const details: ValidationErrorDetail[] = [];\n for (const error of errors) {\n if (\n !isRecord(error) ||\n error.type !== \"field\" ||\n typeof error.path !== \"string\" ||\n typeof error.msg !== \"string\"\n ) {\n continue;\n }\n const source = normalizeSource(error.location);\n if (source) {\n details.push({ source, field: error.path, message: error.msg, type: \"\" });\n }\n }\n return details;\n}\n\n// Celebrate joins the messages of all failed keys into one sentence list, so\n// each key takes the sentence naming it.\nfunction extractCelebrateErrors(validation: Record<string, unknown>): ValidationErrorDetail[] {\n const details: ValidationErrorDetail[] = [];\n for (const error of Object.values(validation)) {\n if (!isRecord(error) || !Array.isArray(error.keys) || typeof error.message !== \"string\") {\n continue;\n }\n const sentences = error.message.split(\". \");\n for (const key of error.keys) {\n if (typeof key === \"string\") {\n details.push({\n source: normalizeSource(error.source),\n field: key,\n message: sentences.find((sentence) => sentence.includes(`\"${key}\"`)) ?? error.message,\n type: \"\",\n });\n }\n }\n }\n return details;\n}\n\nexport function parseJsonResponseBody(body: Buffer | undefined, contentEncoding: unknown): unknown {\n if (!body) {\n return undefined;\n }\n try {\n const encoding =\n typeof contentEncoding === \"string\" ? contentEncoding.trim().toLowerCase() : \"\";\n let decoded = body;\n if (encoding === \"br\") {\n decoded = brotliDecompressSync(body, { maxOutputLength: MAX_BODY_SIZE });\n } else if (encoding === \"gzip\" || encoding === \"deflate\") {\n decoded = unzipSync(body, { maxOutputLength: MAX_BODY_SIZE });\n } else if (encoding !== \"\" && encoding !== \"identity\") {\n return undefined;\n }\n return JSON.parse(decoded.toString());\n } catch {\n // A body that cannot be decoded as JSON carries no recognizable validation details.\n }\n return undefined;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validationErrors.cjs","names":["brotliDecompressSync","MAX_BODY_SIZE","unzipSync"],"sources":["../src/validationErrors.ts"],"sourcesContent":["import { brotliDecompressSync, unzipSync } from \"node:zlib\";\nimport type { AnyValueMap } from \"@opentelemetry/api-logs\";\nimport { MAX_BODY_SIZE } from \"./bodyCapture.js\";\n\nexport const VALIDATION_ERROR_EVENT_NAME = \"apitally.request.validation_error\";\n\nconst MAX_ERRORS = 100;\nconst MAX_CONSUMER_LENGTH = 128;\nconst MAX_PATH_LENGTH = 2_000;\nconst MAX_SOURCE_LENGTH = 32;\nconst MAX_FIELD_LENGTH = 2_048;\nconst MAX_MESSAGE_LENGTH = 2_048;\nconst MAX_TYPE_LENGTH = 128;\n\nconst SOURCE_ALIASES: Record<string, string> = {\n body: \"body\",\n query: \"query\",\n path: \"path\",\n header: \"header\",\n cookie: \"cookie\",\n querystring: \"query\",\n params: \"path\",\n path_params: \"path\",\n headers: \"header\",\n cookies: \"cookie\",\n};\n\nexport type ValidationErrorDetail = {\n source: string;\n field: string;\n message: string;\n type: string;\n};\n\ntype ValidationErrorAggregate = ValidationErrorDetail & {\n method: string;\n path: string;\n counts: Map<string | undefined, number>;\n};\n\nconst VALIDATION_ERROR_AGGREGATES_KEY = Symbol.for(\"apitally.validationErrorAggregates\");\nconst validationErrorAggregatesHolder = globalThis as Record<\n symbol,\n Map<string, ValidationErrorAggregate> | undefined\n>;\nconst validationErrorAggregates =\n validationErrorAggregatesHolder[VALIDATION_ERROR_AGGREGATES_KEY] ??\n new Map<string, ValidationErrorAggregate>();\nvalidationErrorAggregatesHolder[VALIDATION_ERROR_AGGREGATES_KEY] = validationErrorAggregates;\n\nexport function isValidationResponseStatus(statusCode: number): boolean {\n return statusCode === 400 || statusCode === 422;\n}\n\nexport function addValidationErrors(\n consumer: string | undefined,\n method: string,\n path: string,\n details: ValidationErrorDetail[],\n): void {\n method = method.toUpperCase();\n if (method === \"OPTIONS\" || !path) {\n return;\n }\n consumer = consumer?.slice(0, MAX_CONSUMER_LENGTH);\n for (const detail of details) {\n const fields = {\n method,\n path: path.slice(0, MAX_PATH_LENGTH),\n source: detail.source.slice(0, MAX_SOURCE_LENGTH),\n field: detail.field.slice(0, MAX_FIELD_LENGTH),\n message: detail.message.slice(0, MAX_MESSAGE_LENGTH),\n type: detail.type.slice(0, MAX_TYPE_LENGTH),\n };\n const key = Object.values(fields).join(\"\\0\");\n let aggregate = validationErrorAggregates.get(key);\n if (!aggregate) {\n if (validationErrorAggregates.size >= MAX_ERRORS) {\n continue;\n }\n aggregate = { ...fields, counts: new Map() };\n validationErrorAggregates.set(key, aggregate);\n }\n aggregate.counts.set(consumer, (aggregate.counts.get(consumer) ?? 0) + 1);\n }\n}\n\nexport function drainValidationErrors(): AnyValueMap[] {\n const aggregates = [...validationErrorAggregates.values()];\n validationErrorAggregates.clear();\n return aggregates.map(({ counts, ...fields }) => ({\n ...fields,\n counts: [...counts].map(([consumer, count]) =>\n consumer === undefined ? { count } : { consumer, count },\n ),\n }));\n}\n\nexport function resetValidationErrors(): void {\n validationErrorAggregates.clear();\n}\n\nexport function normalizeSource(source: unknown): string {\n return typeof source === \"string\" ? (SOURCE_ALIASES[source.toLowerCase()] ?? \"\") : \"\";\n}\n\n// Zod, Standard Schema, and TypeBox issues share this shape. A Standard Schema\n// path segment may be an object carrying the key; a TypeBox path is a JSON pointer.\nexport function formatIssues(issues: unknown, source = \"\"): ValidationErrorDetail[] {\n if (!Array.isArray(issues)) {\n return [];\n }\n const details: ValidationErrorDetail[] = [];\n for (const issue of issues) {\n if (!isRecord(issue) || typeof issue.message !== \"string\") {\n continue;\n }\n const path = Array.isArray(issue.path)\n ? issue.path\n : typeof issue.path === \"string\"\n ? issue.path\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\"))\n : [];\n details.push({\n source,\n field: path\n .map((segment: unknown) => (isRecord(segment) ? segment.key : segment))\n .filter((segment) => typeof segment === \"string\" || typeof segment === \"number\")\n .join(\".\"),\n message: issue.message,\n type: typeof issue.code === \"string\" ? issue.code : \"\",\n });\n }\n return details;\n}\n\n// zod 3 serializes issues as `error.issues`; zod 4 hides them as a JSON string\n// in `error.message`.\nexport function extractZodValidationErrors(body: unknown): ValidationErrorDetail[] {\n if (!isRecord(body)) {\n return [];\n }\n const error = isRecord(body.error) ? body.error : undefined;\n let issues = body.issues ?? error?.issues;\n if (issues === undefined && error?.name === \"ZodError\" && typeof error.message === \"string\") {\n try {\n issues = JSON.parse(error.message);\n } catch {\n return [];\n }\n }\n return formatIssues(issues);\n}\n\n// Recognized response shapes: zod issues, express-validator, and celebrate.\nexport function extractExpressValidationErrors(body: unknown): ValidationErrorDetail[] {\n if (!isRecord(body)) {\n return [];\n }\n const zodDetails = extractZodValidationErrors(body);\n if (zodDetails.length > 0) {\n return zodDetails;\n }\n if (Array.isArray(body.errors)) {\n return extractExpressValidatorErrors(body.errors);\n }\n if (isRecord(body.validation)) {\n return extractCelebrateErrors(body.validation);\n }\n return [];\n}\n\nfunction extractExpressValidatorErrors(errors: unknown[]): ValidationErrorDetail[] {\n const details: ValidationErrorDetail[] = [];\n for (const error of errors) {\n if (\n !isRecord(error) ||\n error.type !== \"field\" ||\n typeof error.path !== \"string\" ||\n typeof error.msg !== \"string\"\n ) {\n continue;\n }\n const source = normalizeSource(error.location);\n if (source) {\n details.push({ source, field: error.path, message: error.msg, type: \"\" });\n }\n }\n return details;\n}\n\n// Celebrate joins the messages of all failed keys into one sentence list, so\n// each key takes the sentence naming it.\nfunction extractCelebrateErrors(validation: Record<string, unknown>): ValidationErrorDetail[] {\n const details: ValidationErrorDetail[] = [];\n for (const error of Object.values(validation)) {\n if (!isRecord(error) || !Array.isArray(error.keys) || typeof error.message !== \"string\") {\n continue;\n }\n const sentences = error.message.split(\". \");\n for (const key of error.keys) {\n if (typeof key === \"string\") {\n details.push({\n source: normalizeSource(error.source),\n field: key,\n message: sentences.find((sentence) => sentence.includes(`\"${key}\"`)) ?? error.message,\n type: \"\",\n });\n }\n }\n }\n return details;\n}\n\nexport function parseJsonResponseBody(body: Buffer | undefined, contentEncoding: unknown): unknown {\n if (!body) {\n return undefined;\n }\n try {\n const encoding =\n typeof contentEncoding === \"string\" ? contentEncoding.trim().toLowerCase() : \"\";\n let decoded = body;\n if (encoding === \"br\") {\n decoded = brotliDecompressSync(body, { maxOutputLength: MAX_BODY_SIZE });\n } else if (encoding === \"gzip\" || encoding === \"deflate\") {\n decoded = unzipSync(body, { maxOutputLength: MAX_BODY_SIZE });\n } else if (encoding !== \"\" && encoding !== \"identity\") {\n return undefined;\n }\n return JSON.parse(decoded.toString());\n } catch {\n // A body that cannot be decoded as JSON carries no recognizable validation details.\n }\n return undefined;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n"],"mappings":"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"}