import { context, trace, type SpanAttributes } from '@opentelemetry/api'; import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; import { ExportResultCode, type ExportResult } from '@opentelemetry/core'; import { BasicTracerProvider, SimpleSpanProcessor, type ReadableSpan, type SpanExporter } from '@opentelemetry/sdk-trace-base'; type OTelAttributeValue = | { stringValue: string } | { intValue: number } | { doubleValue: number } | { boolValue: boolean }; const PZ_DASHBOARD_TRACING_STARTED_KEY = '__pzDashboardTracingStarted__'; const SERVICE_NAME = 'pz-next-app'; const DASHBOARD_FALLBACK_URL = 'http://localhost:3005'; type PzDashboardGlobal = typeof globalThis & { [PZ_DASHBOARD_TRACING_STARTED_KEY]?: boolean; }; const getDashboardTraceUrl = () => { const baseUrl = process.env.PZ_DASHBOARD_URL ?? DASHBOARD_FALLBACK_URL; return `${baseUrl.replace(/\/$/, '')}/api/traces`; }; const hrTimeToUnixNano = ([seconds, nanos]: [number, number]) => (BigInt(seconds) * BigInt(1000000000) + BigInt(nanos)).toString(); const getAttributeValue = (value: unknown): OTelAttributeValue => { if (typeof value === 'string') { return { stringValue: value }; } if (typeof value === 'number') { return Number.isInteger(value) ? { intValue: value } : { doubleValue: value }; } if (typeof value === 'boolean') { return { boolValue: value }; } return { stringValue: JSON.stringify(value ?? '') }; }; const getAttributes = (attributes: SpanAttributes = {}) => Object.entries(attributes) .filter(([, value]) => value !== undefined) .map(([key, value]) => ({ key, value: getAttributeValue(value) })); const getParentSpanId = (span: ReadableSpan) => { if ('parentSpanId' in span && typeof span.parentSpanId === 'string') { return span.parentSpanId; } if ( 'parentSpanContext' in span && span.parentSpanContext && typeof span.parentSpanContext === 'object' && 'spanId' in span.parentSpanContext ) { return String(span.parentSpanContext.spanId); } return undefined; }; const getResourceAttributes = (span: ReadableSpan) => ({ ...(span.resource?.attributes ?? {}), 'service.name': process.env.OTEL_SERVICE_NAME ?? process.env.NEXT_PUBLIC_APP_NAME ?? SERVICE_NAME }); const getScope = (span: ReadableSpan) => { const scopedSpan = span as ReadableSpan & { instrumentationLibrary?: { name?: string; version?: string; }; instrumentationScope?: { name?: string; version?: string; }; }; const scope = scopedSpan.instrumentationScope ?? scopedSpan.instrumentationLibrary; return { name: scope?.name ?? SERVICE_NAME, version: scope?.version ?? '' }; }; const getSpanPayload = (span: ReadableSpan) => ({ traceId: span.spanContext().traceId, spanId: span.spanContext().spanId, parentSpanId: getParentSpanId(span), name: span.name, kind: span.kind, startTimeUnixNano: hrTimeToUnixNano(span.startTime), endTimeUnixNano: hrTimeToUnixNano(span.endTime), attributes: getAttributes(span.attributes), droppedAttributesCount: span.droppedAttributesCount ?? 0, events: span.events?.map((event) => ({ name: event.name, timeUnixNano: hrTimeToUnixNano(event.time), attributes: getAttributes(event.attributes), droppedAttributesCount: event.droppedAttributesCount ?? 0 })) ?? [], droppedEventsCount: span.droppedEventsCount ?? 0, status: span.status, links: span.links?.map((link) => ({ traceId: link.context.traceId, spanId: link.context.spanId, attributes: getAttributes(link.attributes), droppedAttributesCount: link.droppedAttributesCount ?? 0 })) ?? [], droppedLinksCount: span.droppedLinksCount ?? 0 }); class PzDashboardTraceExporter implements SpanExporter { constructor(private readonly url: string) {} export( spans: ReadableSpan[], resultCallback: (result: ExportResult) => void ) { const [firstSpan] = spans; if (!firstSpan) { resultCallback({ code: ExportResultCode.SUCCESS }); return; } void this.sendSpans(spans, firstSpan, resultCallback); } private async sendSpans( spans: ReadableSpan[], firstSpan: ReadableSpan, resultCallback: (result: ExportResult) => void ) { try { const response = await fetch(this.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resourceSpans: [ { resource: { attributes: getAttributes(getResourceAttributes(firstSpan)), droppedAttributesCount: 0 }, scopeSpans: spans.map((span) => ({ scope: getScope(span), spans: [getSpanPayload(span)] })) } ] }) }); if (!response.ok) { throw new Error(`PZ Dashboard trace export failed: ${response.status}`); } resultCallback({ code: ExportResultCode.SUCCESS }); } catch (error) { resultCallback({ code: ExportResultCode.FAILED, error: error instanceof Error ? error : new Error(String(error)) }); } } shutdown() { return Promise.resolve(); } } const startDashboardTracing = () => { const pzDashboardGlobal = globalThis as PzDashboardGlobal; if (pzDashboardGlobal[PZ_DASHBOARD_TRACING_STARTED_KEY]) { return; } const provider = new BasicTracerProvider({ spanProcessors: [ new SimpleSpanProcessor( new PzDashboardTraceExporter(getDashboardTraceUrl()) ) ] }); context.setGlobalContextManager( new AsyncLocalStorageContextManager().enable() ); const registered = trace.setGlobalTracerProvider(provider); if (!registered) { return; } pzDashboardGlobal[PZ_DASHBOARD_TRACING_STARTED_KEY] = true; }; if (process.env.NODE_ENV === 'development') { startDashboardTracing(); }