{"version":3,"sources":["../../src/server/openai-conversions-api.ts"],"sourcesContent":["/**\n * OpenAI Conversions API\n * https://developers.openai.com/ads/conversions-api\n * https://developers.openai.com/ads/supported-events\n */\nimport { createHash } from 'crypto';\nimport { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { type EventData, NON_AD_EVENTS, mapOAIEvent } from '../track/oaiq';\nimport type { TrackEvent, TrackTags, UserProvidedData } from '../track/types';\nimport { getFirst } from '../utils/field';\n\nconst ENDPOINT = 'https://bzr.openai.com/v1/events';\n\ntype ActionSource =\n  | 'web'\n  | 'mobile_app'\n  | 'offline'\n  | 'physical_store'\n  | 'phone_call'\n  | 'email'\n  | 'other';\n\n/**\n * User/identity fields. Email and external id must be sent as lowercase 64-char SHA-256 hex\n * strings; geographic, IP, and user-agent fields are sent as raw values.\n */\nexport interface OpenAIUser {\n  email_sha256?: string;\n  external_id_sha256?: string;\n  /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n  country?: string;\n  city?: string;\n  zip_code?: string;\n  ip_address?: string;\n  user_agent?: string;\n}\n\nexport interface OpenAIEvent {\n  /** Unique event id; combined with `type` for deduplication against pixel events. */\n  id: string;\n  /** Standard event name or `custom`. */\n  type: string;\n  /** Required when `type` is `custom`. */\n  custom_event_name?: string;\n  /** Event timestamp in ms; must be within 7 days and no more than 10 minutes in the future. */\n  timestamp_ms: number;\n  /** Required for `action_source: \"web\"`. */\n  source_url?: string;\n  action_source?: ActionSource;\n  /** OpenAI-provided privacy-preserving identifier. */\n  oppref?: string;\n  /** When true, opts the event out of personalization. */\n  opt_out?: boolean;\n  user?: OpenAIUser;\n  data: EventData;\n}\n\nexport interface CreateOpenAIEventsDTO {\n  /** When true, validates the events without persisting them. */\n  validate_only?: boolean;\n  events: OpenAIEvent[];\n}\n\nfunction sha256(value: string): string {\n  return createHash('sha256').update(value).digest('hex');\n}\n\nfunction mapActionSource(source: TrackTags['source']): ActionSource | undefined {\n  switch (source) {\n    case 'web':\n      return 'web';\n    case 'app':\n      return 'mobile_app';\n    case 'offline':\n      return 'offline';\n    default:\n      return undefined;\n  }\n}\n\nfunction getUser(data: UserProvidedData): OpenAIUser | undefined {\n  const email = getFirst(data.email)?.trim().toLowerCase();\n  const address = getFirst(data.address);\n\n  const user: OpenAIUser = {\n    email_sha256: email ? sha256(email) : undefined,\n    external_id_sha256: data.user_id ? sha256(data.user_id) : undefined,\n    country: address?.country?.trim().toUpperCase(),\n    city: address?.city?.trim().toLowerCase(),\n    zip_code: address?.postal_code,\n    ip_address: data.ip_address,\n    user_agent: data.user_agent,\n  };\n\n  return Object.values(user).some((value) => value !== undefined) ? user : undefined;\n}\n\nexport function getServerEvent(\n  // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n  event: TrackEvent<any>,\n  data: UserProvidedData\n): OpenAIEvent {\n  const { type, data: eventData } = mapOAIEvent(event.name, event.properties);\n\n  return {\n    id: event.tags.idempotency_key ?? event.id.toString(),\n    type,\n    // For custom events the original track name is the OpenAI custom_event_name; this matches\n    // the browser pixel so the two deduplicate. Standard events omit it.\n    custom_event_name: type === 'custom' ? event.name : undefined,\n    timestamp_ms: Date.now(),\n    source_url: event.tags.source_url,\n    action_source: mapActionSource(event.tags.source),\n    user: getUser(data),\n    data: eventData,\n  };\n}\n\nexport async function sendEvents(\n  apiKey: string,\n  pixelId: string,\n  // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n  events: TrackEvent<any>[],\n  data: UserProvidedData = {},\n  validateOnly = false\n) {\n  const dto: CreateOpenAIEventsDTO = {\n    validate_only: validateOnly,\n    events: events\n      .filter((event) => !IGNORED_EVENTS.includes(event.name))\n      .filter((event) => !NON_AD_EVENTS.includes(event.name))\n      .map((event) => getServerEvent(event, data)),\n  };\n\n  if (dto.events.length === 0) return;\n\n  try {\n    const response = await fetch(`${ENDPOINT}?pid=${pixelId}`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        Accept: 'application/json',\n        Authorization: `Bearer ${apiKey}`,\n      },\n      body: JSON.stringify(dto),\n    });\n\n    if (response.ok) return;\n    const { status } = response;\n    const message = await response.text();\n    console.error(`Failed to send OpenAI conversion, status: ${status}, body: ${message}`);\n  } catch (error) {\n    console.error('Failed to send OpenAI conversion, network error:', error);\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,oBAA2B;AAC3B,mBAAsB;AACtB,4BAA+B;AAC/B,kBAA2D;AAE3D,mBAAyB;AAEzB,IAAM,WAAW;AAoDjB,SAAS,OAAO,OAAuB;AACrC,aAAO,0BAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxD;AAEA,SAAS,gBAAgB,QAAuD;AAC9E,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,QAAQ,MAAgD;AAjFjE;AAkFE,QAAM,SAAQ,gCAAS,KAAK,KAAK,MAAnB,mBAAsB,OAAO;AAC3C,QAAM,cAAU,uBAAS,KAAK,OAAO;AAErC,QAAM,OAAmB;AAAA,IACvB,cAAc,QAAQ,OAAO,KAAK,IAAI;AAAA,IACtC,oBAAoB,KAAK,UAAU,OAAO,KAAK,OAAO,IAAI;AAAA,IAC1D,UAAS,wCAAS,YAAT,mBAAkB,OAAO;AAAA,IAClC,OAAM,wCAAS,SAAT,mBAAe,OAAO;AAAA,IAC5B,UAAU,mCAAS;AAAA,IACnB,YAAY,KAAK;AAAA,IACjB,YAAY,KAAK;AAAA,EACnB;AAEA,SAAO,OAAO,OAAO,IAAI,EAAE,KAAK,CAAC,UAAU,UAAU,MAAS,IAAI,OAAO;AAC3E;AAEO,SAAS,eAEd,OACA,MACa;AACb,QAAM,EAAE,MAAM,MAAM,UAAU,QAAI,yBAAY,MAAM,MAAM,MAAM,UAAU;AAE1E,SAAO;AAAA,IACL,IAAI,MAAM,KAAK,mBAAmB,MAAM,GAAG,SAAS;AAAA,IACpD;AAAA;AAAA;AAAA,IAGA,mBAAmB,SAAS,WAAW,MAAM,OAAO;AAAA,IACpD,cAAc,KAAK,IAAI;AAAA,IACvB,YAAY,MAAM,KAAK;AAAA,IACvB,eAAe,gBAAgB,MAAM,KAAK,MAAM;AAAA,IAChD,MAAM,QAAQ,IAAI;AAAA,IAClB,MAAM;AAAA,EACR;AACF;AAEA,eAAsB,WACpB,QACA,SAEA,QACA,OAAyB,CAAC,GAC1B,eAAe,OACf;AACA,QAAM,MAA6B;AAAA,IACjC,eAAe;AAAA,IACf,QAAQ,OACL,OAAO,CAAC,UAAU,CAAC,qCAAe,SAAS,MAAM,IAAI,CAAC,EACtD,OAAO,CAAC,UAAU,CAAC,0BAAc,SAAS,MAAM,IAAI,CAAC,EACrD,IAAI,CAAC,UAAU,eAAe,OAAO,IAAI,CAAC;AAAA,EAC/C;AAEA,MAAI,IAAI,OAAO,WAAW,EAAG;AAE7B,MAAI;AACF,UAAM,WAAW,UAAM,oBAAM,GAAG,QAAQ,QAAQ,OAAO,IAAI;AAAA,MACzD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,QAAQ;AAAA,QACR,eAAe,UAAU,MAAM;AAAA,MACjC;AAAA,MACA,MAAM,KAAK,UAAU,GAAG;AAAA,IAC1B,CAAC;AAED,QAAI,SAAS,GAAI;AACjB,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,YAAQ,MAAM,6CAA6C,MAAM,WAAW,OAAO,EAAE;AAAA,EACvF,SAAS,OAAO;AACd,YAAQ,MAAM,oDAAoD,KAAK;AAAA,EACzE;AACF;","names":[]}