{"version":3,"file":"sentry.mjs","names":[],"sources":["../../src/adapters/sentry.ts"],"sourcesContent":["import type { WideEvent } from '../types'\nimport type { ConfigField } from './_config'\nimport { resolveAdapterConfig } from './_config'\nimport { defineDrain } from './_drain'\nimport { httpPost } from './_http'\nimport { OTEL_SEVERITY_NUMBER } from './_severity'\n\nexport interface SentryConfig {\n  /** Sentry DSN */\n  dsn: string\n  /** Environment override (defaults to event.environment) */\n  environment?: string\n  /** Release version override (defaults to event.version) */\n  release?: string\n  /** Additional tags to attach as attributes */\n  tags?: Record<string, string>\n  /** Request timeout in milliseconds. Default: 5000 */\n  timeout?: number\n}\n\n/** Sentry Log attribute value with type annotation */\nexport interface SentryAttributeValue {\n  value: string | number | boolean\n  type: 'string' | 'integer' | 'double' | 'boolean'\n}\n\n/** Sentry Structured Log payload */\nexport interface SentryLog {\n  timestamp: number\n  trace_id: string\n  level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'\n  body: string\n  severity_number: number\n  attributes?: Record<string, SentryAttributeValue>\n}\n\ninterface SentryDsnParts {\n  publicKey: string\n  secretKey?: string\n  projectId: string\n  origin: string\n  basePath: string\n}\n\nconst SENTRY_FIELDS: ConfigField<SentryConfig>[] = [\n  { key: 'dsn', env: ['NUXT_SENTRY_DSN', 'SENTRY_DSN'] },\n  { key: 'environment', env: ['NUXT_SENTRY_ENVIRONMENT', 'SENTRY_ENVIRONMENT'] },\n  { key: 'release', env: ['NUXT_SENTRY_RELEASE', 'SENTRY_RELEASE'] },\n  { key: 'tags' },\n  { key: 'timeout' },\n]\n\nfunction parseSentryDsn(dsn: string): SentryDsnParts {\n  const url = new URL(dsn)\n  const publicKey = url.username\n  if (!publicKey) {\n    throw new Error('Invalid Sentry DSN: missing public key')\n  }\n\n  const secretKey = url.password || undefined\n\n  const pathParts = url.pathname.split('/').filter(Boolean)\n  const projectId = pathParts.pop()\n  if (!projectId) {\n    throw new Error('Invalid Sentry DSN: missing project ID')\n  }\n\n  const basePath = pathParts.length > 0 ? `/${pathParts.join('/')}` : ''\n\n  return {\n    publicKey,\n    secretKey,\n    projectId,\n    origin: `${url.protocol}//${url.host}`,\n    basePath,\n  }\n}\n\nfunction getSentryEnvelopeUrl(dsn: string): { url: string, authHeader: string } {\n  const { publicKey, secretKey, projectId, origin, basePath } = parseSentryDsn(dsn)\n  const url = `${origin}${basePath}/api/${projectId}/envelope/`\n  let authHeader = `Sentry sentry_version=7, sentry_key=${publicKey}, sentry_client=mxllog`\n  if (secretKey) {\n    authHeader += `, sentry_secret=${secretKey}`\n  }\n  return { url, authHeader }\n}\n\nfunction createTraceId(): string {\n  if (typeof globalThis.crypto?.randomUUID === 'function') {\n    return globalThis.crypto.randomUUID().replace(/-/g, '')\n  }\n\n  return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('')\n}\n\nfunction getFirstStringValue(event: WideEvent, keys: string[]): string | undefined {\n  for (const key of keys) {\n    const value = event[key]\n    if (typeof value === 'string' && value.length > 0) return value\n  }\n  return undefined\n}\n\nfunction toAttributeValue(value: unknown): SentryAttributeValue | undefined {\n  if (value === null || value === undefined) {\n    return undefined\n  }\n  if (typeof value === 'string') {\n    return { value, type: 'string' }\n  }\n  if (typeof value === 'boolean') {\n    return { value, type: 'boolean' }\n  }\n  if (typeof value === 'number') {\n    if (Number.isInteger(value)) {\n      return { value, type: 'integer' }\n    }\n    return { value, type: 'double' }\n  }\n  return { value: JSON.stringify(value), type: 'string' }\n}\n\nexport function toSentryLog(event: WideEvent, config: SentryConfig): SentryLog {\n  const { timestamp, level, service, environment, version, ...rest } = event\n\n  const body = getFirstStringValue(event, ['message', 'action', 'path'])\n    ?? 'mxllog wide event'\n\n  const traceId = (typeof event.traceId === 'string' && event.traceId.length > 0)\n    ? event.traceId\n    : createTraceId()\n\n  const attributes: Record<string, SentryAttributeValue> = {}\n\n  const env = config.environment ?? environment\n  if (env) {\n    attributes['sentry.environment'] = { value: env, type: 'string' }\n  }\n\n  const rel = config.release ?? version\n  if (typeof rel === 'string' && rel.length > 0) {\n    attributes['sentry.release'] = { value: rel, type: 'string' }\n  }\n\n  attributes['service'] = { value: service, type: 'string' }\n\n  if (config.tags) {\n    for (const [key, value] of Object.entries(config.tags)) {\n      attributes[key] = { value, type: 'string' }\n    }\n  }\n\n  for (const [key, value] of Object.entries(rest)) {\n    if (key === 'traceId' || key === 'spanId') continue\n    if (value === undefined || value === null) continue\n    const attr = toAttributeValue(value)\n    if (attr) {\n      attributes[key] = attr\n    }\n  }\n\n  return {\n    timestamp: new Date(timestamp).getTime() / 1000,\n    trace_id: traceId,\n    level: level as SentryLog['level'],\n    body,\n    severity_number: OTEL_SEVERITY_NUMBER[level] ?? 9,\n    attributes,\n  }\n}\n\n/**\n * Build the Sentry Envelope body for a list of logs.\n *\n * Envelope format (line-delimited):\n * - Line 1: Envelope headers (dsn, sent_at)\n * - Line 2: Item header (type: log, item_count, content_type)\n * - Line 3: Item payload ({\"items\": [...]})\n */\nfunction buildEnvelopeBody(logs: SentryLog[], dsn: string): string {\n  const envelopeHeader = JSON.stringify({\n    dsn,\n    sent_at: new Date().toISOString(),\n  })\n\n  const itemHeader = JSON.stringify({\n    type: 'log',\n    item_count: logs.length,\n    content_type: 'application/vnd.sentry.items.log+json',\n  })\n\n  const itemPayload = JSON.stringify({ items: logs })\n\n  return `${envelopeHeader}\\n${itemHeader}\\n${itemPayload}\\n`\n}\n\n/**\n * Create a drain function for sending logs to Sentry.\n *\n * Sends wide events as Sentry Structured Logs, visible in Explore > Logs\n * in the Sentry dashboard.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createSentryDrain()\n * 2. runtimeConfig.mxllog.sentry\n * 3. runtimeConfig.sentry\n * 4. Environment variables: NUXT_SENTRY_*, SENTRY_*\n *\n * @example\n * ```ts\n * // Zero config - just set NUXT_SENTRY_DSN env var\n * nitroApp.hooks.hook('mxllog:drain', createSentryDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('mxllog:drain', createSentryDrain({\n *   dsn: 'https://public@o0.ingest.sentry.io/123',\n * }))\n * ```\n */\nexport function createSentryDrain(overrides?: Partial<SentryConfig>) {\n  return defineDrain<SentryConfig>({\n    name: 'sentry',\n    resolve: () => {\n      const config = resolveAdapterConfig<SentryConfig>('sentry', SENTRY_FIELDS, overrides)\n      if (!config.dsn) {\n        console.error('[mxllog/sentry] Missing DSN. Set NUXT_SENTRY_DSN/SENTRY_DSN env var or pass to createSentryDrain()')\n        return null\n      }\n      return config as SentryConfig\n    },\n    send: sendBatchToSentry,\n  })\n}\n\n/**\n * Send a single event to Sentry as a structured log.\n *\n * @example\n * ```ts\n * await sendToSentry(event, {\n *   dsn: process.env.SENTRY_DSN!,\n * })\n * ```\n */\nexport async function sendToSentry(event: WideEvent, config: SentryConfig): Promise<void> {\n  await sendBatchToSentry([event], config)\n}\n\n/**\n * Send a batch of events to Sentry as structured logs via the Envelope endpoint.\n *\n * @example\n * ```ts\n * await sendBatchToSentry(events, {\n *   dsn: process.env.SENTRY_DSN!,\n * })\n * ```\n */\nexport async function sendBatchToSentry(events: WideEvent[], config: SentryConfig): Promise<void> {\n  if (events.length === 0) return\n\n  const { url, authHeader } = getSentryEnvelopeUrl(config.dsn)\n\n  const logs = events.map(event => toSentryLog(event, config))\n  const body = buildEnvelopeBody(logs, config.dsn)\n\n  await httpPost({\n    url,\n    headers: {\n      'Content-Type': 'application/x-sentry-envelope',\n      'X-Sentry-Auth': authHeader,\n    },\n    body,\n    timeout: config.timeout ?? 5000,\n    label: 'Sentry',\n  })\n}\n"],"mappings":"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"}