{"version":3,"file":"enrichers.mjs","names":[],"sources":["../src/enrichers/index.ts"],"sourcesContent":["import type { EnrichContext } from '../types'\n\nexport interface EnricherOptions {\n  /**\n   * When true, overwrite any existing fields in the event.\n   * Defaults to false to preserve user-provided data.\n   */\n  overwrite?: boolean\n}\n\nexport interface UserAgentInfo {\n  raw: string\n  browser?: { name: string; version?: string }\n  os?: { name: string; version?: string }\n  device?: { type: 'mobile' | 'tablet' | 'desktop' | 'bot' | 'unknown' }\n}\n\nexport interface GeoInfo {\n  country?: string\n  region?: string\n  regionCode?: string\n  city?: string\n  latitude?: number\n  longitude?: number\n}\n\nexport interface RequestSizeInfo {\n  requestBytes?: number\n  responseBytes?: number\n}\n\nexport interface TraceContextInfo {\n  traceparent?: string\n  tracestate?: string\n  traceId?: string\n  spanId?: string\n}\n\nfunction getHeader(headers: Record<string, string> | undefined, name: string): string | undefined {\n  if (!headers) return undefined\n  if (headers[name] !== undefined) return headers[name]\n  const lowerName = name.toLowerCase()\n  if (headers[lowerName] !== undefined) return headers[lowerName]\n  for (const [key, value] of Object.entries(headers)) {\n    if (key.toLowerCase() === lowerName) return value\n  }\n  return undefined\n}\n\nfunction parseUserAgent(ua: string): UserAgentInfo {\n  const lower = ua.toLowerCase()\n\n  let deviceType: UserAgentInfo['device'] = { type: 'unknown' }\n  if (/bot|crawl|spider|slurp|bingpreview/.test(lower)) {\n    deviceType = { type: 'bot' }\n  } else if (/ipad|tablet/.test(lower)) {\n    deviceType = { type: 'tablet' }\n  } else if (/mobi|iphone|android/.test(lower)) {\n    deviceType = { type: 'mobile' }\n  } else if (ua.length > 0) {\n    deviceType = { type: 'desktop' }\n  }\n\n  const browserMatchers: Array<{ name: string, regex: RegExp }> = [\n    { name: 'Edge', regex: /edg\\/([\\d.]+)/i },\n    { name: 'Chrome', regex: /chrome\\/([\\d.]+)/i },\n    { name: 'Firefox', regex: /firefox\\/([\\d.]+)/i },\n    { name: 'Safari', regex: /version\\/([\\d.]+).*safari/i },\n  ]\n\n  let browser: UserAgentInfo['browser']\n  for (const matcher of browserMatchers) {\n    const match = ua.match(matcher.regex)\n    if (match) {\n      browser = { name: matcher.name, version: match[1] }\n      break\n    }\n  }\n\n  let os: UserAgentInfo['os']\n  if (/windows nt/i.test(ua)) {\n    const match = ua.match(/windows nt ([\\d.]+)/i)\n    os = { name: 'Windows', version: match?.[1] }\n  } else if (/mac os x/i.test(ua) && !/iphone|ipad|ipod/i.test(ua)) {\n    const match = ua.match(/mac os x ([\\d_]+)/i)\n    os = { name: 'macOS', version: match?.[1]?.replace(/_/g, '.') }\n  } else if (/iphone|ipad|ipod/i.test(ua)) {\n    const match = ua.match(/os ([\\d_]+)/i)\n    os = { name: 'iOS', version: match?.[1]?.replace(/_/g, '.') }\n  } else if (/android/i.test(ua)) {\n    const match = ua.match(/android ([\\d.]+)/i)\n    os = { name: 'Android', version: match?.[1] }\n  } else if (/linux/i.test(ua)) {\n    os = { name: 'Linux' }\n  }\n\n  return {\n    raw: ua,\n    browser,\n    os,\n    device: deviceType,\n  }\n}\n\nfunction parseTraceparent(traceparent: string): Pick<TraceContextInfo, 'traceId' | 'spanId'> | undefined {\n  const match = traceparent.match(/^[\\da-f]{2}-([\\da-f]{32})-([\\da-f]{16})-[\\da-f]{2}$/i)\n  if (!match) return undefined\n  return { traceId: match[1], spanId: match[2] }\n}\n\nfunction mergeEventField<T extends object>(\n  existing: unknown,\n  computed: T,\n  overwrite?: boolean,\n): T {\n  if (overwrite || existing === undefined || existing === null || typeof existing !== 'object') {\n    return computed\n  }\n  return { ...computed, ...(existing as T) }\n}\n\nfunction normalizeNumber(value: string | undefined): number | undefined {\n  if (!value) return undefined\n  const parsed = Number(value)\n  return Number.isFinite(parsed) ? parsed : undefined\n}\n\n/**\n * Enrich events with parsed user agent data.\n * Sets `event.userAgent` with `UserAgentInfo` shape: `{ raw, browser?, os?, device? }`.\n */\nexport function createUserAgentEnricher(options: EnricherOptions = {}): (ctx: EnrichContext) => void {\n  return (ctx) => {\n    const ua = getHeader(ctx.headers, 'user-agent')\n    if (!ua) return\n    const info = parseUserAgent(ua)\n    ctx.event.userAgent = mergeEventField<UserAgentInfo>(ctx.event.userAgent, info, options.overwrite)\n  }\n}\n\n/**\n * Enrich events with geo data from platform headers.\n * Sets `event.geo` with `GeoInfo` shape: `{ country?, region?, regionCode?, city?, latitude?, longitude? }`.\n *\n * Supports Vercel (`x-vercel-ip-*`) headers out of the box.\n *\n * **Cloudflare note:** Only `cf-ipcountry` is an actual HTTP header added by Cloudflare.\n * The `cf-region`, `cf-city`, `cf-latitude`, `cf-longitude` headers are NOT standard\n * Cloudflare headers — they are properties of `request.cf` which is not exposed as HTTP\n * headers. For full geo data on Cloudflare, write a custom enricher that reads `request.cf`\n * or use a Workers middleware to copy `cf` properties into custom headers.\n */\nexport function createGeoEnricher(options: EnricherOptions = {}): (ctx: EnrichContext) => void {\n  return (ctx) => {\n    const { headers } = ctx\n    if (!headers) return\n\n    const geo: GeoInfo = {\n      country: getHeader(headers, 'x-vercel-ip-country') ?? getHeader(headers, 'cf-ipcountry'),\n      region: getHeader(headers, 'x-vercel-ip-country-region') ?? getHeader(headers, 'cf-region'),\n      regionCode: getHeader(headers, 'x-vercel-ip-country-region-code') ?? getHeader(headers, 'cf-region-code'),\n      city: getHeader(headers, 'x-vercel-ip-city') ?? getHeader(headers, 'cf-city'),\n      latitude: normalizeNumber(getHeader(headers, 'x-vercel-ip-latitude') ?? getHeader(headers, 'cf-latitude')),\n      longitude: normalizeNumber(getHeader(headers, 'x-vercel-ip-longitude') ?? getHeader(headers, 'cf-longitude')),\n    }\n\n    if (Object.values(geo).every(value => value === undefined)) return\n    ctx.event.geo = mergeEventField<GeoInfo>(ctx.event.geo, geo, options.overwrite)\n  }\n}\n\n/**\n * Enrich events with request/response payload sizes.\n * Sets `event.requestSize` with `RequestSizeInfo` shape: `{ requestBytes?, responseBytes? }`.\n */\nexport function createRequestSizeEnricher(options: EnricherOptions = {}): (ctx: EnrichContext) => void {\n  return (ctx) => {\n    const requestBytes = normalizeNumber(getHeader(ctx.headers, 'content-length'))\n    const responseBytes = normalizeNumber(getHeader(ctx.response?.headers, 'content-length'))\n\n    const sizes: RequestSizeInfo = {\n      requestBytes,\n      responseBytes,\n    }\n\n    if (requestBytes === undefined && responseBytes === undefined) return\n    ctx.event.requestSize = mergeEventField<RequestSizeInfo>(ctx.event.requestSize, sizes, options.overwrite)\n  }\n}\n\n/**\n * Enrich events with W3C trace context data.\n * Sets `event.traceContext` with `TraceContextInfo` shape: `{ traceparent?, tracestate?, traceId?, spanId? }`.\n * Also sets `event.traceId` and `event.spanId` at the top level.\n */\nexport function createTraceContextEnricher(options: EnricherOptions = {}): (ctx: EnrichContext) => void {\n  return (ctx) => {\n    const traceparent = getHeader(ctx.headers, 'traceparent')\n    const tracestate = getHeader(ctx.headers, 'tracestate')\n    if (!traceparent && !tracestate) return\n\n    const parsed = traceparent ? parseTraceparent(traceparent) : undefined\n    const incomingTraceContext: TraceContextInfo = {\n      traceparent,\n      tracestate,\n      traceId: parsed?.traceId ?? (ctx.event.traceId as string | undefined),\n      spanId: parsed?.spanId ?? (ctx.event.spanId as string | undefined),\n    }\n\n    const mergedTraceContext = mergeEventField<TraceContextInfo>(\n      ctx.event.traceContext,\n      incomingTraceContext,\n      options.overwrite,\n    )\n    ctx.event.traceContext = mergedTraceContext\n\n    if (mergedTraceContext.traceId && (options.overwrite || ctx.event.traceId === undefined)) {\n      ctx.event.traceId = mergedTraceContext.traceId\n    }\n    if (mergedTraceContext.spanId && (options.overwrite || ctx.event.spanId === undefined)) {\n      ctx.event.spanId = mergedTraceContext.spanId\n    }\n  }\n}\n"],"mappings":"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"}