{"version":3,"sources":["../../src/telemetry/utils.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { Attributes } from '@opentelemetry/api';\nimport { type Span, SpanStatusCode, context as otelContext, trace } from '@opentelemetry/api';\nimport { getJobContext } from '../job.js';\nimport type { RealtimeModelMetrics } from '../metrics/base.js';\nimport { ATTRIBUTE_REDACTION_ENABLED } from '../types.js';\nimport { realtimeUsageAttributes, setErrorType } from './gen_ai.js';\nimport { REDACTED_EXCEPTION_MESSAGE } from './redaction.js';\nimport * as traceTypes from './trace_types.js';\nimport { tracer } from './traces.js';\n\n/**\n * Whether the project has mandated PII redaction.\n *\n * Set in the LiveKit Cloud dashboard (or per session with `record: { redaction: true }`) and\n * never weakened from here — when it is on, PII is stripped for every destination, LiveKit\n * Cloud included. `spanAttributes` lets a span ended outside the job's async context resolve\n * from the flag stamped on it at span start.\n *\n * Stripping PII for *third-party* exporters is not this flag: that is the default, and is\n * lifted per provider with `setTracerProvider(provider, { allowPii: true })`.\n */\nexport function redactionEnabled(spanAttributes?: Attributes): boolean {\n  if (spanAttributes?.[ATTRIBUTE_REDACTION_ENABLED]) return true;\n  return getJobContext(false)?._redactionEnabled ?? false;\n}\n\nexport { REDACTED_EXCEPTION_MESSAGE } from './redaction.js';\n\nexport interface RecordExceptionOptions {\n  /**\n   * Whether to omit exception messages and stack traces from telemetry. Defaults to the resolved\n   * redaction setting for the current session.\n   */\n  redacted?: boolean;\n}\n\nexport function recordException(\n  span: Span,\n  error: Error,\n  options: RecordExceptionOptions = {},\n): void {\n  const redacted = options.redacted ?? redactionEnabled();\n\n  // `error.type` is the GenAI/HTTP conventions' low-cardinality error identifier; unlike the\n  // message it never carries user data, so it is set either way\n  setErrorType(span, error);\n\n  if (redacted) {\n    const attrs = {\n      [traceTypes.ATTR_EXCEPTION_TYPE]: error.constructor.name,\n      [traceTypes.ATTR_EXCEPTION_MESSAGE]: REDACTED_EXCEPTION_MESSAGE,\n    };\n    span.addEvent('exception', attrs);\n    span.setStatus({\n      code: SpanStatusCode.ERROR,\n      message: REDACTED_EXCEPTION_MESSAGE,\n    });\n    span.setAttributes(attrs);\n    return;\n  }\n\n  span.recordException(error);\n  span.setStatus({\n    code: SpanStatusCode.ERROR,\n    message: error.message,\n  });\n\n  // Set exception attributes for better visibility\n  // (in case the exception event is not rendered by the backend)\n  span.setAttributes({\n    [traceTypes.ATTR_EXCEPTION_TYPE]: error.constructor.name,\n    [traceTypes.ATTR_EXCEPTION_MESSAGE]: error.message,\n    [traceTypes.ATTR_EXCEPTION_TRACE]: error.stack || '',\n  });\n}\n\nexport function recordRealtimeMetrics(span: Span, metrics: RealtimeModelMetrics): void {\n  const attrs: Record<string, string | number | boolean> = {\n    // a realtime turn is a multimodal generation; `gen_ai.output.type` is set on the\n    // inference span, which knows whether this session outputs audio\n    [traceTypes.ATTR_GEN_AI_OPERATION_NAME]: traceTypes.GenAIOperationName.GENERATE_CONTENT,\n    [traceTypes.ATTR_REALTIME_MODEL_METRICS]: JSON.stringify(metrics),\n    ...(realtimeUsageAttributes(metrics) as Record<string, number>),\n    // unofficial spellings LangFuse reads, kept alongside the official ones\n    [traceTypes.ATTR_GEN_AI_USAGE_INPUT_TEXT_TOKENS]: metrics.inputTokenDetails.textTokens,\n    [traceTypes.ATTR_GEN_AI_USAGE_INPUT_AUDIO_TOKENS]: metrics.inputTokenDetails.audioTokens,\n    [traceTypes.ATTR_GEN_AI_USAGE_INPUT_CACHED_TOKENS]: metrics.inputTokenDetails.cachedTokens,\n    [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TEXT_TOKENS]: metrics.outputTokenDetails.textTokens,\n    [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_AUDIO_TOKENS]: metrics.outputTokenDetails.audioTokens,\n  };\n\n  // `metrics.label` names the plugin, not the model, so it is never reported as one\n  const modelName = metrics.metadata?.modelName;\n  if (modelName) {\n    attrs[traceTypes.ATTR_GEN_AI_REQUEST_MODEL] = modelName;\n    attrs[traceTypes.ATTR_GEN_AI_RESPONSE_MODEL] = modelName;\n  }\n  const provider = traceTypes.genAIProviderName(metrics.metadata?.modelProvider);\n  if (provider) attrs[traceTypes.ATTR_GEN_AI_PROVIDER_NAME] = provider;\n  if (metrics.requestId) attrs[traceTypes.ATTR_GEN_AI_RESPONSE_ID] = metrics.requestId;\n  if (metrics.ttftMs !== undefined && metrics.ttftMs >= 0) {\n    attrs[traceTypes.ATTR_GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK] = metrics.ttftMs / 1000;\n  }\n\n  // Add LangFuse-specific completion start time if TTFT is available\n  if (metrics.ttftMs !== undefined && metrics.ttftMs !== -1) {\n    const completionStartTime = metrics.timestamp + metrics.ttftMs;\n    // Convert to UTC ISO string for LangFuse compatibility\n    const completionStartTimeUtc = new Date(completionStartTime).toISOString();\n    attrs[traceTypes.ATTR_LANGFUSE_COMPLETION_START_TIME] = completionStartTimeUtc;\n  }\n\n  if (span.isRecording()) {\n    span.setAttributes(attrs);\n  } else {\n    const currentContext = otelContext.active();\n    const spanContext = trace.setSpan(currentContext, span);\n\n    // Create a dedicated child span for orphaned metrics\n    tracer.getTracer().startActiveSpan('realtime_metrics', {}, spanContext, (child) => {\n      try {\n        child.setAttributes(attrs);\n      } finally {\n        child.end();\n      }\n    });\n  }\n}\n"],"mappings":"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","names":["import_redaction","otelContext"]}