UNPKG

autotel

Version:
1 lines 6.64 kB
{"version":3,"file":"test-span-collector.cjs","names":["SpanStatusCode"],"sources":["../src/test-span-collector.ts"],"sourcesContent":["/**\n * TestSpanCollector — SpanExporter that groups finished spans by traceId\n * and drains per-trace for embedding in test metadata.\n *\n * @example\n * ```typescript\n * import { TestSpanCollector } from 'autotel/test-span-collector';\n * import { SimpleSpanProcessor } from 'autotel/processors';\n * import { getAutotelTracerProvider } from 'autotel';\n *\n * const collector = new TestSpanCollector();\n * const provider = getAutotelTracerProvider();\n * provider.addSpanProcessor(new SimpleSpanProcessor(collector));\n *\n * // After a test span ends:\n * const spans = collector.drainTrace(traceId, rootSpanId);\n * // spans contains only descendants of rootSpanId\n * ```\n */\n\nimport type { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { SpanStatusCode } from '@opentelemetry/api';\n\n/** @see ExportResultCode from @opentelemetry/core */\nconst ExportResultCode = { SUCCESS: 0, FAILED: 1 } as const;\n\n/** Attribute value types that survive serialization */\ntype SerializableValue =\n | string\n | number\n | boolean\n | string[]\n | number[]\n | boolean[];\n\n/**\n * Portable serialized span for embedding in test metadata.\n * `startTimeMs` is derived from OTel HrTime — epoch-based wall-clock ms in the current SDK.\n *\n * Defined as a `type` (not `interface`) so it is assignable to\n * `Record<string, unknown>` in TypeScript 6+ strict mode.\n */\nexport type SerializedSpan = {\n spanId: string;\n parentSpanId?: string;\n name: string;\n startTimeMs: number;\n durationMs: number;\n status: 'ok' | 'error' | 'unset';\n statusMessage?: string;\n attributes?: Record<string, SerializableValue>;\n};\n\nexport class TestSpanCollector implements SpanExporter {\n private traces = new Map<string, ReadableSpan[]>();\n\n export(\n spans: ReadableSpan[],\n callback: (result: { code: number }) => void,\n ): void {\n for (const span of spans) {\n const traceId = span.spanContext().traceId;\n let list = this.traces.get(traceId);\n if (!list) {\n list = [];\n this.traces.set(traceId, list);\n }\n list.push(span);\n }\n callback({ code: ExportResultCode.SUCCESS });\n }\n\n /**\n * Drain and serialize spans that are descendants of `rootSpanId` within `traceId`.\n * Filters to the subtree rooted at the test span to prevent cross-test mixing.\n * Removes the entire traceId entry from the collector.\n */\n drainTrace(traceId: string, rootSpanId: string): SerializedSpan[] {\n const allSpans = this.traces.get(traceId);\n this.traces.delete(traceId);\n if (!allSpans?.length) return [];\n\n // Build spanId → span index for efficient parent-chain walking\n const byId = new Map<string, ReadableSpan>();\n for (const s of allSpans) byId.set(s.spanContext().spanId, s);\n\n // Filter to spans that are the root or descendants of rootSpanId\n const included = allSpans.filter((s) => {\n let id: string | undefined = s.spanContext().spanId;\n while (id) {\n if (id === rootSpanId) return true;\n const parent = byId.get(id);\n const parentId = parent?.parentSpanContext?.spanId || undefined;\n if (parentId === id) break; // cycle guard\n id = parentId;\n }\n return false;\n });\n\n return included.map((s) => serializeSpan(s));\n }\n\n shutdown(): Promise<void> {\n this.traces.clear();\n return Promise.resolve();\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n}\n\nfunction hrTimeToMs(hr: [number, number]): number {\n return hr[0] * 1000 + hr[1] / 1_000_000;\n}\n\nfunction isSerializable(v: unknown): v is SerializableValue {\n if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')\n return true;\n if (Array.isArray(v) && v.length > 0) {\n const t = typeof v[0];\n return (\n (t === 'string' || t === 'number' || t === 'boolean') &&\n v.every((e) => typeof e === t)\n );\n }\n return false;\n}\n\nexport function serializeSpan(span: ReadableSpan): SerializedSpan {\n const attrs: Record<string, SerializableValue> = {};\n for (const [k, v] of Object.entries(span.attributes)) {\n if (isSerializable(v)) attrs[k] = v;\n }\n return {\n spanId: span.spanContext().spanId,\n parentSpanId: span.parentSpanContext?.spanId || undefined,\n name: span.name,\n startTimeMs: hrTimeToMs(span.startTime as [number, number]),\n durationMs: hrTimeToMs(span.duration as [number, number]),\n status:\n span.status.code === SpanStatusCode.ERROR\n ? 'error'\n : span.status.code === SpanStatusCode.OK\n ? 'ok'\n : 'unset',\n statusMessage: span.status.message || undefined,\n attributes: Object.keys(attrs).length > 0 ? attrs : undefined,\n };\n}\n"],"mappings":";;;;;AAwBA,MAAM,mBAAmB;CAAE,SAAS;CAAG,QAAQ;AAAE;AA6BjD,IAAa,oBAAb,MAAuD;CACrD,AAAQ,yBAAS,IAAI,IAA4B;CAEjD,OACE,OACA,UACM;EACN,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,UAAU,KAAK,YAAY,CAAC,CAAC;GACnC,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO;GAClC,IAAI,CAAC,MAAM;IACT,OAAO,CAAC;IACR,KAAK,OAAO,IAAI,SAAS,IAAI;GAC/B;GACA,KAAK,KAAK,IAAI;EAChB;EACA,SAAS,EAAE,MAAM,iBAAiB,QAAQ,CAAC;CAC7C;;;;;;CAOA,WAAW,SAAiB,YAAsC;EAChE,MAAM,WAAW,KAAK,OAAO,IAAI,OAAO;EACxC,KAAK,OAAO,OAAO,OAAO;EAC1B,IAAI,CAAC,UAAU,QAAQ,OAAO,CAAC;EAG/B,MAAM,uBAAO,IAAI,IAA0B;EAC3C,KAAK,MAAM,KAAK,UAAU,KAAK,IAAI,EAAE,YAAY,CAAC,CAAC,QAAQ,CAAC;EAe5D,OAZiB,SAAS,QAAQ,MAAM;GACtC,IAAI,KAAyB,EAAE,YAAY,CAAC,CAAC;GAC7C,OAAO,IAAI;IACT,IAAI,OAAO,YAAY,OAAO;IAE9B,MAAM,WADS,KAAK,IAAI,EACF,CAAC,EAAE,mBAAmB,UAAU;IACtD,IAAI,aAAa,IAAI;IACrB,KAAK;GACP;GACA,OAAO;EACT,CAEc,CAAC,CAAC,KAAK,MAAM,cAAc,CAAC,CAAC;CAC7C;CAEA,WAA0B;EACxB,KAAK,OAAO,MAAM;EAClB,OAAO,QAAQ,QAAQ;CACzB;CAEA,aAA4B;EAC1B,OAAO,QAAQ,QAAQ;CACzB;AACF;AAEA,SAAS,WAAW,IAA8B;CAChD,OAAO,GAAG,KAAK,MAAO,GAAG,KAAK;AAChC;AAEA,SAAS,eAAe,GAAoC;CAC1D,IAAI,OAAO,MAAM,YAAY,OAAO,MAAM,YAAY,OAAO,MAAM,WACjE,OAAO;CACT,IAAI,MAAM,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG;EACpC,MAAM,IAAI,OAAO,EAAE;EACnB,QACG,MAAM,YAAY,MAAM,YAAY,MAAM,cAC3C,EAAE,OAAO,MAAM,OAAO,MAAM,CAAC;CAEjC;CACA,OAAO;AACT;AAEA,SAAgB,cAAc,MAAoC;CAChE,MAAM,QAA2C,CAAC;CAClD,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,KAAK,UAAU,GACjD,IAAI,eAAe,CAAC,GAAG,MAAM,KAAK;CAEpC,OAAO;EACL,QAAQ,KAAK,YAAY,CAAC,CAAC;EAC3B,cAAc,KAAK,mBAAmB,UAAU;EAChD,MAAM,KAAK;EACX,aAAa,WAAW,KAAK,SAA6B;EAC1D,YAAY,WAAW,KAAK,QAA4B;EACxD,QACE,KAAK,OAAO,SAASA,kCAAe,QAChC,UACA,KAAK,OAAO,SAASA,kCAAe,KAClC,OACA;EACR,eAAe,KAAK,OAAO,WAAW;EACtC,YAAY,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,IAAI,QAAQ;CACtD;AACF"}