@shware/analytics 9.0.1 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/server/google-data-manager.cjs +3 -3
  2. package/dist/server/google-data-manager.cjs.map +1 -1
  3. package/dist/server/google-data-manager.d.cts +15 -2
  4. package/dist/server/google-data-manager.d.cts.map +1 -1
  5. package/dist/server/google-data-manager.d.mts +15 -2
  6. package/dist/server/google-data-manager.d.mts.map +1 -1
  7. package/dist/server/google-data-manager.mjs +3 -3
  8. package/dist/server/google-data-manager.mjs.map +1 -1
  9. package/dist/server/index.d.cts +2 -2
  10. package/dist/server/index.d.mts +2 -2
  11. package/dist/server/linkedin-conversions-api.cjs +6 -1
  12. package/dist/server/linkedin-conversions-api.cjs.map +1 -1
  13. package/dist/server/linkedin-conversions-api.d.cts.map +1 -1
  14. package/dist/server/linkedin-conversions-api.d.mts.map +1 -1
  15. package/dist/server/linkedin-conversions-api.mjs +6 -1
  16. package/dist/server/linkedin-conversions-api.mjs.map +1 -1
  17. package/dist/server/meta-capi.cjs +7 -1
  18. package/dist/server/meta-capi.cjs.map +1 -1
  19. package/dist/server/meta-capi.d.cts.map +1 -1
  20. package/dist/server/meta-capi.d.mts.map +1 -1
  21. package/dist/server/meta-capi.mjs +7 -1
  22. package/dist/server/meta-capi.mjs.map +1 -1
  23. package/dist/server/meta-conversions-api.cjs +3 -1
  24. package/dist/server/meta-conversions-api.cjs.map +1 -1
  25. package/dist/server/meta-conversions-api.d.cts.map +1 -1
  26. package/dist/server/meta-conversions-api.d.mts.map +1 -1
  27. package/dist/server/meta-conversions-api.mjs +3 -1
  28. package/dist/server/meta-conversions-api.mjs.map +1 -1
  29. package/dist/server/microsoft-conversions-api.cjs +3 -1
  30. package/dist/server/microsoft-conversions-api.cjs.map +1 -1
  31. package/dist/server/microsoft-conversions-api.d.cts.map +1 -1
  32. package/dist/server/microsoft-conversions-api.d.mts.map +1 -1
  33. package/dist/server/microsoft-conversions-api.mjs +3 -1
  34. package/dist/server/microsoft-conversions-api.mjs.map +1 -1
  35. package/dist/server/openai-conversions-api.cjs +13 -12
  36. package/dist/server/openai-conversions-api.cjs.map +1 -1
  37. package/dist/server/openai-conversions-api.d.cts.map +1 -1
  38. package/dist/server/openai-conversions-api.d.mts.map +1 -1
  39. package/dist/server/openai-conversions-api.mjs +14 -13
  40. package/dist/server/openai-conversions-api.mjs.map +1 -1
  41. package/dist/server/reddit-conversions-api.cjs +6 -2
  42. package/dist/server/reddit-conversions-api.cjs.map +1 -1
  43. package/dist/server/reddit-conversions-api.d.cts +10 -1
  44. package/dist/server/reddit-conversions-api.d.cts.map +1 -1
  45. package/dist/server/reddit-conversions-api.d.mts +10 -1
  46. package/dist/server/reddit-conversions-api.d.mts.map +1 -1
  47. package/dist/server/reddit-conversions-api.mjs +6 -2
  48. package/dist/server/reddit-conversions-api.mjs.map +1 -1
  49. package/dist/third-parties/openai-pixel.cjs +29 -13
  50. package/dist/third-parties/openai-pixel.cjs.map +1 -1
  51. package/dist/third-parties/openai-pixel.d.cts +4 -3
  52. package/dist/third-parties/openai-pixel.d.cts.map +1 -1
  53. package/dist/third-parties/openai-pixel.d.mts +4 -3
  54. package/dist/third-parties/openai-pixel.d.mts.map +1 -1
  55. package/dist/third-parties/openai-pixel.mjs +30 -14
  56. package/dist/third-parties/openai-pixel.mjs.map +1 -1
  57. package/dist/track/oaiq.cjs +57 -9
  58. package/dist/track/oaiq.cjs.map +1 -1
  59. package/dist/track/oaiq.d.cts +33 -7
  60. package/dist/track/oaiq.d.cts.map +1 -1
  61. package/dist/track/oaiq.d.mts +33 -7
  62. package/dist/track/oaiq.d.mts.map +1 -1
  63. package/dist/track/oaiq.mjs +55 -10
  64. package/dist/track/oaiq.mjs.map +1 -1
  65. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { IGNORED_EVENTS } from "../third-parties/ignored-events.mjs";
2
2
  import { getFirst } from "../utils/field.mjs";
3
3
  import { resolveActionSource } from "./action-source.mjs";
4
4
  import { redditClickId, redditUuid } from "./click-ids.mjs";
5
+ import { pageLocation } from "./page-location.mjs";
5
6
  import { mapRDTEvent, mapServerStandardEvent } from "../track/rdt.mjs";
6
7
  import { fetch } from "@shware/utils";
7
8
  //#region src/server/reddit-conversions-api.ts
@@ -12,7 +13,8 @@ function getServerEvent(event, data, actionSource) {
12
13
  return {
13
14
  click_id: redditClickId(tags),
14
15
  event_at: new Date(created_at).getTime(),
15
- action_source: source === "web" ? "WEBSITE" : source === "app" ? "APP" : "UNKNOWN",
16
+ action_source: source === "web" ? "WEBSITE" : source === "app" ? "APP" : "OTHER",
17
+ event_source_url: source === "web" ? pageLocation(tags) : void 0,
16
18
  type: {
17
19
  tracking_type: type === "Custom" ? "CUSTOM" : mapServerStandardEvent(type),
18
20
  custom_event_name: type === "Custom" ? params.customEventName : void 0
@@ -40,10 +42,12 @@ function getServerEvent(event, data, actionSource) {
40
42
  }
41
43
  };
42
44
  }
45
+ /** `event_at` "can't be older than seven days"; a minute's margin for the trip. */
46
+ const MAX_EVENT_AGE_MS = 60474e4;
43
47
  async function sendEvents(accessToken, pixelId, events, data = {}, testId, actionSource) {
44
48
  const dto = { data: {
45
49
  test_id: testId,
46
- events: events.filter((event) => !IGNORED_EVENTS.includes(event.name)).map((event) => getServerEvent(event, data, actionSource))
50
+ events: events.filter((event) => !IGNORED_EVENTS.includes(event.name)).filter((event) => Date.now() - Date.parse(event.created_at) <= MAX_EVENT_AGE_MS).map((event) => getServerEvent(event, data, actionSource))
47
51
  } };
48
52
  if (dto.data.events.length === 0) return;
49
53
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"reddit-conversions-api.mjs","names":[],"sources":["../../src/server/reddit-conversions-api.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { type ServerStandardEvent, mapRDTEvent, mapServerStandardEvent } from '../track/rdt';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { type EventActionSource, resolveActionSource } from './action-source';\nimport { redditClickId, redditUuid } from './click-ids';\n\n/**\n * https://ads-api.reddit.com/docs/v3/operations/Post%20Conversion%20Events\n * https://business.reddithelp.com/s/article/map-a-catalog-to-a-signal-source\n */\nexport interface RedditEvent {\n /** Match keys: Share user identifiers to match conversions to a Reddit ad engagement. */\n click_id?: string;\n\n /** Unix epoch timestamp in milliseconds, event_at can't be older than seven days. */\n event_at: number;\n\n action_source: 'WEBSITE' | 'APP' | (string & {});\n\n type: {\n tracking_type: ServerStandardEvent | 'CUSTOM';\n custom_event_name?: string;\n };\n\n /**\n * Event metadata\n * Share as much additional information about your conversion event as you'd like. If you're\n * using the Conversions API with the pixel, conversion_id is required for deduplication.\n */\n metadata?: {\n conversion_id?: string;\n currency?: string; // ISO 4217 3-letter currency code\n item_count?: number;\n value?: number;\n products?: { id: string; name?: string; category?: string }[];\n };\n\n user?: {\n email?: string;\n external_id?: string;\n ip_address?: string;\n phone_number?: string;\n user_agent?: string;\n\n /** The Identifier for Advertisers (IDFA) of the user's Apple device. */\n idfa?: string;\n\n /** The Android Advertising ID (AAID) of the user's Android device. */\n aaid?: string;\n /**\n * The value from the first-party Pixel _rdt_uuid cookie on your domain. Note that it is in\n * the {timestamp}.{uuid} format. You may use the full value or just the UUID portion.\n * Example: 1684189007728.7c73f2ae-a433-4d7b-9838-f467da98f48e\n */\n uuid?: string;\n\n screen_dimensions?: { width: number; height: number };\n\n /**\n * A structure of data processing options to specify the processing type for the event\n * https://business.reddithelp.com/s/article/Limited-Data-Use\n */\n data_processing_options?: {\n country: string;\n region: string;\n modes: string[] | ['LDU'];\n };\n };\n}\n\nexport interface CreateRedditEventDTO {\n data: { test_id?: string; events: RedditEvent[] };\n}\n\nexport function getServerEvent(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData,\n actionSource?: EventActionSource\n): RedditEvent {\n const { id, name, properties, tags, platform, created_at } = event;\n const [type, params] = mapRDTEvent(name, properties, id);\n // Reddit documents WEBSITE and APP only, so an offline conversion shares the fallback with an\n // undeterminable platform rather than inventing an enum value the API may reject.\n const source = resolveActionSource(platform, actionSource);\n\n return {\n click_id: redditClickId(tags),\n event_at: new Date(created_at).getTime(),\n action_source: source === 'web' ? 'WEBSITE' : source === 'app' ? 'APP' : 'UNKNOWN',\n type: {\n tracking_type: type === 'Custom' ? 'CUSTOM' : mapServerStandardEvent(type),\n custom_event_name: type === 'Custom' ? params.customEventName : undefined,\n },\n metadata: {\n conversion_id: id,\n currency:\n 'currency' in params && typeof params.currency === 'string'\n ? params.currency.toUpperCase()\n : undefined,\n item_count:\n 'itemCount' in params && typeof params.itemCount === 'number'\n ? params.itemCount\n : undefined,\n value: 'value' in params && typeof params.value === 'number' ? params.value : undefined,\n products:\n 'products' in params && Array.isArray(params.products) && params.products.length > 0\n ? params.products\n : undefined,\n },\n user: {\n email: getFirst(data.email),\n external_id: data.user_id,\n ip_address: data.ip_address,\n phone_number: getFirst(data.phone_number),\n user_agent: data.user_agent,\n idfa: platform === 'ios' ? tags.advertising_id : undefined,\n aaid: platform === 'android' ? tags.advertising_id : undefined,\n uuid: redditUuid(tags),\n screen_dimensions:\n tags.screen_width && tags.screen_height\n ? { width: tags.screen_width, height: tags.screen_height }\n : undefined,\n },\n };\n}\n\nexport async function sendEvents(\n accessToken: string,\n pixelId: string,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {},\n testId?: string,\n actionSource?: EventActionSource\n) {\n const dto: CreateRedditEventDTO = {\n data: {\n test_id: testId,\n events: events\n .filter((event) => !IGNORED_EVENTS.includes(event.name))\n .map((event) => getServerEvent(event, data, actionSource)),\n },\n };\n\n if (dto.data.events.length === 0) return;\n\n try {\n const response = await fetch(\n `https://ads-api.reddit.com/api/v3/pixels/${pixelId}/conversion_events`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Authorization: `Bearer ${accessToken}`,\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 Reddit conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send Reddit conversion, network error:', error);\n }\n}\n"],"mappings":";;;;;;;AA4EA,SAAgB,eAEd,OACA,MACA,cACa;CACb,MAAM,EAAE,IAAI,MAAM,YAAY,MAAM,UAAU,eAAe;CAC7D,MAAM,CAAC,MAAM,UAAU,YAAY,MAAM,YAAY,EAAE;CAGvD,MAAM,SAAS,oBAAoB,UAAU,YAAY;CAEzD,OAAO;EACL,UAAU,cAAc,IAAI;EAC5B,UAAU,IAAI,KAAK,UAAU,CAAC,CAAC,QAAQ;EACvC,eAAe,WAAW,QAAQ,YAAY,WAAW,QAAQ,QAAQ;EACzE,MAAM;GACJ,eAAe,SAAS,WAAW,WAAW,uBAAuB,IAAI;GACzE,mBAAmB,SAAS,WAAW,OAAO,kBAAkB,KAAA;EAClE;EACA,UAAU;GACR,eAAe;GACf,UACE,cAAc,UAAU,OAAO,OAAO,aAAa,WAC/C,OAAO,SAAS,YAAY,IAC5B,KAAA;GACN,YACE,eAAe,UAAU,OAAO,OAAO,cAAc,WACjD,OAAO,YACP,KAAA;GACN,OAAO,WAAW,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ,KAAA;GAC9E,UACE,cAAc,UAAU,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS,IAC/E,OAAO,WACP,KAAA;EACR;EACA,MAAM;GACJ,OAAO,SAAS,KAAK,KAAK;GAC1B,aAAa,KAAK;GAClB,YAAY,KAAK;GACjB,cAAc,SAAS,KAAK,YAAY;GACxC,YAAY,KAAK;GACjB,MAAM,aAAa,QAAQ,KAAK,iBAAiB,KAAA;GACjD,MAAM,aAAa,YAAY,KAAK,iBAAiB,KAAA;GACrD,MAAM,WAAW,IAAI;GACrB,mBACE,KAAK,gBAAgB,KAAK,gBACtB;IAAE,OAAO,KAAK;IAAc,QAAQ,KAAK;GAAc,IACvD,KAAA;EACR;CACF;AACF;AAEA,eAAsB,WACpB,aACA,SAEA,QACA,OAAyB,CAAC,GAC1B,QACA,cACA;CACA,MAAM,MAA4B,EAChC,MAAM;EACJ,SAAS;EACT,QAAQ,OACL,QAAQ,UAAU,CAAC,eAAe,SAAS,MAAM,IAAI,CAAC,CAAC,CACvD,KAAK,UAAU,eAAe,OAAO,MAAM,YAAY,CAAC;CAC7D,EACF;CAEA,IAAI,IAAI,KAAK,OAAO,WAAW,GAAG;CAElC,IAAI;EACF,MAAM,WAAW,MAAM,MACrB,4CAA4C,QAAQ,qBACpD;GACE,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,QAAQ;IACR,eAAe,UAAU;GAC3B;GACA,MAAM,KAAK,UAAU,GAAG;EAC1B,CACF;EACA,IAAI,SAAS,IAAI;EACjB,MAAM,EAAE,WAAW;EACnB,MAAM,UAAU,MAAM,SAAS,KAAK;EACpC,QAAQ,MAAM,6CAA6C,OAAO,UAAU,SAAS;CACvF,SAAS,OAAO;EACd,QAAQ,MAAM,oDAAoD,KAAK;CACzE;AACF"}
1
+ {"version":3,"file":"reddit-conversions-api.mjs","names":[],"sources":["../../src/server/reddit-conversions-api.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { type ServerStandardEvent, mapRDTEvent, mapServerStandardEvent } from '../track/rdt';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { type EventActionSource, resolveActionSource } from './action-source';\nimport { redditClickId, redditUuid } from './click-ids';\nimport { pageLocation } from './page-location';\n\n/**\n * https://ads-api.reddit.com/docs/v3/operations/Post%20Conversion%20Events\n * https://business.reddithelp.com/s/article/map-a-catalog-to-a-signal-source\n */\nexport interface RedditEvent {\n /** Match keys: Share user identifiers to match conversions to a Reddit ad engagement. */\n click_id?: string;\n\n /** Unix epoch timestamp in milliseconds, event_at can't be older than seven days. */\n event_at: number;\n\n /**\n * Where the conversion happened. Pixel events are deduplicated only against `WEBSITE` ones.\n * https://business.reddithelp.com/s/article/Conversions-API#measure-impact-across-channels\n */\n action_source: 'WEBSITE' | 'APP' | 'PHYSICAL_STORE' | 'OTHER';\n\n /**\n * The page the event happened on, for `WEBSITE` events: Reddit reads the domain from it, and the\n * click id when `click_id` is missing.\n */\n event_source_url?: string;\n\n type: {\n tracking_type: ServerStandardEvent | 'CUSTOM';\n custom_event_name?: string;\n };\n\n /**\n * Event metadata\n * Share as much additional information about your conversion event as you'd like. If you're\n * using the Conversions API with the pixel, conversion_id is required for deduplication.\n */\n metadata?: {\n conversion_id?: string;\n currency?: string; // ISO 4217 3-letter currency code\n item_count?: number;\n value?: number;\n products?: { id: string; name?: string; category?: string }[];\n };\n\n user?: {\n email?: string;\n external_id?: string;\n ip_address?: string;\n phone_number?: string;\n user_agent?: string;\n\n /** The Identifier for Advertisers (IDFA) of the user's Apple device. */\n idfa?: string;\n\n /** The Android Advertising ID (AAID) of the user's Android device. */\n aaid?: string;\n /**\n * The value from the first-party Pixel _rdt_uuid cookie on your domain. Note that it is in\n * the {timestamp}.{uuid} format. You may use the full value or just the UUID portion.\n * Example: 1684189007728.7c73f2ae-a433-4d7b-9838-f467da98f48e\n */\n uuid?: string;\n\n screen_dimensions?: { width: number; height: number };\n\n /**\n * A structure of data processing options to specify the processing type for the event\n * https://business.reddithelp.com/s/article/Limited-Data-Use\n */\n data_processing_options?: {\n country: string;\n region: string;\n modes: string[] | ['LDU'];\n };\n };\n}\n\nexport interface CreateRedditEventDTO {\n data: { test_id?: string; events: RedditEvent[] };\n}\n\nexport function getServerEvent(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData,\n actionSource?: EventActionSource\n): RedditEvent {\n const { id, name, properties, tags, platform, created_at } = event;\n const [type, params] = mapRDTEvent(name, properties, id);\n // An offline conversion — in store, from a CRM, over the phone — is `OTHER` rather than\n // `PHYSICAL_STORE`, which only one of those is; so is an undeterminable platform.\n const source = resolveActionSource(platform, actionSource);\n\n return {\n click_id: redditClickId(tags),\n event_at: new Date(created_at).getTime(),\n action_source: source === 'web' ? 'WEBSITE' : source === 'app' ? 'APP' : 'OTHER',\n event_source_url: source === 'web' ? pageLocation(tags) : undefined,\n type: {\n tracking_type: type === 'Custom' ? 'CUSTOM' : mapServerStandardEvent(type),\n custom_event_name: type === 'Custom' ? params.customEventName : undefined,\n },\n metadata: {\n conversion_id: id,\n currency:\n 'currency' in params && typeof params.currency === 'string'\n ? params.currency.toUpperCase()\n : undefined,\n item_count:\n 'itemCount' in params && typeof params.itemCount === 'number'\n ? params.itemCount\n : undefined,\n value: 'value' in params && typeof params.value === 'number' ? params.value : undefined,\n products:\n 'products' in params && Array.isArray(params.products) && params.products.length > 0\n ? params.products\n : undefined,\n },\n user: {\n email: getFirst(data.email),\n external_id: data.user_id,\n ip_address: data.ip_address,\n phone_number: getFirst(data.phone_number),\n user_agent: data.user_agent,\n idfa: platform === 'ios' ? tags.advertising_id : undefined,\n aaid: platform === 'android' ? tags.advertising_id : undefined,\n uuid: redditUuid(tags),\n screen_dimensions:\n tags.screen_width && tags.screen_height\n ? { width: tags.screen_width, height: tags.screen_height }\n : undefined,\n },\n };\n}\n\n/** `event_at` \"can't be older than seven days\"; a minute's margin for the trip. */\nconst MAX_EVENT_AGE_MS = 7 * 24 * 60 * 60 * 1000 - 60 * 1000;\n\nexport async function sendEvents(\n accessToken: string,\n pixelId: string,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {},\n testId?: string,\n actionSource?: EventActionSource\n) {\n const dto: CreateRedditEventDTO = {\n data: {\n test_id: testId,\n events: events\n .filter((event) => !IGNORED_EVENTS.includes(event.name))\n .filter((event) => Date.now() - Date.parse(event.created_at) <= MAX_EVENT_AGE_MS)\n .map((event) => getServerEvent(event, data, actionSource)),\n },\n };\n\n if (dto.data.events.length === 0) return;\n\n try {\n const response = await fetch(\n `https://ads-api.reddit.com/api/v3/pixels/${pixelId}/conversion_events`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Authorization: `Bearer ${accessToken}`,\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 Reddit conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send Reddit conversion, network error:', error);\n }\n}\n"],"mappings":";;;;;;;;AAuFA,SAAgB,eAEd,OACA,MACA,cACa;CACb,MAAM,EAAE,IAAI,MAAM,YAAY,MAAM,UAAU,eAAe;CAC7D,MAAM,CAAC,MAAM,UAAU,YAAY,MAAM,YAAY,EAAE;CAGvD,MAAM,SAAS,oBAAoB,UAAU,YAAY;CAEzD,OAAO;EACL,UAAU,cAAc,IAAI;EAC5B,UAAU,IAAI,KAAK,UAAU,CAAC,CAAC,QAAQ;EACvC,eAAe,WAAW,QAAQ,YAAY,WAAW,QAAQ,QAAQ;EACzE,kBAAkB,WAAW,QAAQ,aAAa,IAAI,IAAI,KAAA;EAC1D,MAAM;GACJ,eAAe,SAAS,WAAW,WAAW,uBAAuB,IAAI;GACzE,mBAAmB,SAAS,WAAW,OAAO,kBAAkB,KAAA;EAClE;EACA,UAAU;GACR,eAAe;GACf,UACE,cAAc,UAAU,OAAO,OAAO,aAAa,WAC/C,OAAO,SAAS,YAAY,IAC5B,KAAA;GACN,YACE,eAAe,UAAU,OAAO,OAAO,cAAc,WACjD,OAAO,YACP,KAAA;GACN,OAAO,WAAW,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ,KAAA;GAC9E,UACE,cAAc,UAAU,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS,IAC/E,OAAO,WACP,KAAA;EACR;EACA,MAAM;GACJ,OAAO,SAAS,KAAK,KAAK;GAC1B,aAAa,KAAK;GAClB,YAAY,KAAK;GACjB,cAAc,SAAS,KAAK,YAAY;GACxC,YAAY,KAAK;GACjB,MAAM,aAAa,QAAQ,KAAK,iBAAiB,KAAA;GACjD,MAAM,aAAa,YAAY,KAAK,iBAAiB,KAAA;GACrD,MAAM,WAAW,IAAI;GACrB,mBACE,KAAK,gBAAgB,KAAK,gBACtB;IAAE,OAAO,KAAK;IAAc,QAAQ,KAAK;GAAc,IACvD,KAAA;EACR;CACF;AACF;;AAGA,MAAM,mBAAmB;AAEzB,eAAsB,WACpB,aACA,SAEA,QACA,OAAyB,CAAC,GAC1B,QACA,cACA;CACA,MAAM,MAA4B,EAChC,MAAM;EACJ,SAAS;EACT,QAAQ,OACL,QAAQ,UAAU,CAAC,eAAe,SAAS,MAAM,IAAI,CAAC,CAAC,CACvD,QAAQ,UAAU,KAAK,IAAI,IAAI,KAAK,MAAM,MAAM,UAAU,KAAK,gBAAgB,CAAC,CAChF,KAAK,UAAU,eAAe,OAAO,MAAM,YAAY,CAAC;CAC7D,EACF;CAEA,IAAI,IAAI,KAAK,OAAO,WAAW,GAAG;CAElC,IAAI;EACF,MAAM,WAAW,MAAM,MACrB,4CAA4C,QAAQ,qBACpD;GACE,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,QAAQ;IACR,eAAe,UAAU;GAC3B;GACA,MAAM,KAAK,UAAU,GAAG;EAC1B,CACF;EACA,IAAI,SAAS,IAAI;EACjB,MAAM,EAAE,WAAW;EACnB,MAAM,UAAU,MAAM,SAAS,KAAK;EACpC,QAAQ,MAAM,6CAA6C,OAAO,UAAU,SAAS;CACvF,SAAS,OAAO;EACd,QAAQ,MAAM,oDAAoD,KAAK;CACzE;AACF"}
@@ -21,16 +21,20 @@ function sendOpenAIEvent(name, properties, eventId) {
21
21
  if (window.location.host.includes("127.0.0.1")) return;
22
22
  if (window.location.host.includes("localhost")) return;
23
23
  const { type, data } = require_track_oaiq.mapOAIEvent(name, properties);
24
- if (type === "custom") window.oaiq("measure", "custom", clean(data), {
25
- event_id: eventId,
26
- custom_event_name: name
27
- });
28
- else window.oaiq("measure", type, clean(data), { event_id: eventId });
24
+ if (type === "custom") {
25
+ const custom_event_name = require_track_oaiq.oaiCustomEventName(name);
26
+ if (!custom_event_name) return;
27
+ window.oaiq("measure", "custom", clean(data), {
28
+ event_id: eventId,
29
+ custom_event_name
30
+ });
31
+ } else window.oaiq("measure", type, clean(data), { event_id: eventId });
29
32
  }
30
33
  /**
31
- * Re-initialize the pixel with hashed user identity for better conversion matching. Email and
32
- * external id are hashed client-side; geographic fields are sent raw. Hashing is asynchronous,
33
- * so the `init` call is deferred until the digests resolve.
34
+ * Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,
35
+ * external id and names are normalized as OpenAI documents and hashed client-side; geographic
36
+ * fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests
37
+ * resolve.
34
38
  */
35
39
  function setOpenAIUser(pixelId) {
36
40
  return ({ user_id, user_data }) => {
@@ -39,12 +43,14 @@ function setOpenAIUser(pixelId) {
39
43
  return;
40
44
  }
41
45
  const oaiq = window.oaiq;
42
- const email = require_utils_field.getFirst(user_data?.email)?.trim().toLowerCase();
46
+ const email = require_utils_field.getFirst(user_data?.email)?.trim().toLowerCase() || void 0;
47
+ const phone = require_utils_field.getFirst(user_data?.phone_number);
43
48
  const address = require_utils_field.getFirst(user_data?.address);
44
49
  const base = {
45
50
  country: address?.country?.trim().toUpperCase(),
46
- city: address?.city?.trim().toLowerCase(),
47
- zip_code: address?.postal_code
51
+ city: address?.city?.trim(),
52
+ region: address?.region?.trim(),
53
+ postal_code: address?.postal_code?.trim()
48
54
  };
49
55
  const init = (hashed) => {
50
56
  oaiq("init", {
@@ -55,9 +61,19 @@ function setOpenAIUser(pixelId) {
55
61
  })
56
62
  });
57
63
  };
58
- Promise.all([email ? require_utils_sha256.sha256(email) : void 0, user_id ? require_utils_sha256.sha256(user_id) : void 0]).then(([email_sha256, external_id_sha256]) => init({
64
+ const hash = (value) => value ? require_utils_sha256.sha256(value) : void 0;
65
+ Promise.all([
66
+ hash(email),
67
+ hash(phone ? require_track_oaiq.normalizeOAIPhone(phone) : void 0),
68
+ hash(user_id?.trim()),
69
+ hash(require_track_oaiq.normalizeOAIName(address?.first_name)),
70
+ hash(require_track_oaiq.normalizeOAIName(address?.last_name))
71
+ ]).then(([email_sha256, phone_number_sha256, external_id_sha256, first, last]) => init({
59
72
  email_sha256,
60
- external_id_sha256
73
+ phone_number_sha256,
74
+ external_id_sha256,
75
+ first_name_sha256: first,
76
+ last_name_sha256: last
61
77
  })).catch(() => init({}));
62
78
  };
63
79
  }
@@ -1 +1 @@
1
- {"version":3,"file":"openai-pixel.cjs","names":["NON_AD_EVENTS","mapOAIEvent","getFirst","sha256"],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport { NON_AD_EVENTS, type OAIQ, type OAIQUser, mapOAIEvent } from '../track/oaiq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\n\ndeclare global {\n interface Window {\n /** Undefined until the OpenAI pixel script has loaded. */\n oaiq?: OAIQ['oaiq'];\n }\n}\n\n/** Drop `undefined` fields so the SDK only receives populated values. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\n/**\n * Forward an internal track event to the OpenAI measurement pixel.\n * `eventId` is reused as the OpenAI `event_id` so browser events deduplicate against the\n * Conversions API. https://developers.openai.com/ads/measurement-pixel\n */\nexport function sendOpenAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n if (NON_AD_EVENTS.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const { type, data } = mapOAIEvent(name, properties);\n if (type === 'custom') {\n window.oaiq('measure', 'custom', clean(data), { event_id: eventId, custom_event_name: name });\n } else {\n window.oaiq('measure', type, clean(data), { event_id: eventId });\n }\n}\n\n/**\n * Re-initialize the pixel with hashed user identity for better conversion matching. Email and\n * external id are hashed client-side; geographic fields are sent raw. Hashing is asynchronous,\n * so the `init` call is deferred until the digests resolve.\n */\nexport function setOpenAIUser(pixelId: string) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n // Capture the narrowed reference so the deferred `init` closure keeps it non-optional.\n const oaiq = window.oaiq;\n\n const email = getFirst(user_data?.email)?.trim().toLowerCase();\n const address = getFirst(user_data?.address);\n\n const base: OAIQUser = {\n country: address?.country?.trim().toUpperCase(),\n city: address?.city?.trim().toLowerCase(),\n zip_code: address?.postal_code,\n };\n\n const init = (hashed: Partial<OAIQUser>) => {\n oaiq('init', { pixelId, user: clean({ ...base, ...hashed }) });\n };\n\n Promise.all([email ? sha256(email) : undefined, user_id ? sha256(user_id) : undefined])\n .then(([email_sha256, external_id_sha256]) => init({ email_sha256, external_id_sha256 }))\n .catch(() => init({}));\n };\n}\n"],"mappings":";;;;;;AAeA,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;;;;AAOA,SAAgB,gBACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAIA,mBAAAA,cAAc,SAAS,IAAI,GAAG;CAClC,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,EAAE,MAAM,SAASC,mBAAAA,YAAY,MAAM,UAAU;CACnD,IAAI,SAAS,UACX,OAAO,KAAK,WAAW,UAAU,MAAM,IAAI,GAAG;EAAE,UAAU;EAAS,mBAAmB;CAAK,CAAC;MAE5F,OAAO,KAAK,WAAW,MAAM,MAAM,IAAI,GAAG,EAAE,UAAU,QAAQ,CAAC;AAEnE;;;;;;AAOA,SAAgB,cAAc,SAAiB;CAC7C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;GACjD,QAAQ,KAAK,+BAA+B;GAC5C;EACF;EAEA,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQC,oBAAAA,SAAS,WAAW,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,YAAY;EAC7D,MAAM,UAAUA,oBAAAA,SAAS,WAAW,OAAO;EAE3C,MAAM,OAAiB;GACrB,SAAS,SAAS,SAAS,KAAK,CAAC,CAAC,YAAY;GAC9C,MAAM,SAAS,MAAM,KAAK,CAAC,CAAC,YAAY;GACxC,UAAU,SAAS;EACrB;EAEA,MAAM,QAAQ,WAA8B;GAC1C,KAAK,QAAQ;IAAE;IAAS,MAAM,MAAM;KAAE,GAAG;KAAM,GAAG;IAAO,CAAC;GAAE,CAAC;EAC/D;EAEA,QAAQ,IAAI,CAAC,QAAQC,qBAAAA,OAAO,KAAK,IAAI,KAAA,GAAW,UAAUA,qBAAAA,OAAO,OAAO,IAAI,KAAA,CAAS,CAAC,CAAC,CACpF,MAAM,CAAC,cAAc,wBAAwB,KAAK;GAAE;GAAc;EAAmB,CAAC,CAAC,CAAC,CACxF,YAAY,KAAK,CAAC,CAAC,CAAC;CACzB;AACF"}
1
+ {"version":3,"file":"openai-pixel.cjs","names":["NON_AD_EVENTS","mapOAIEvent","oaiCustomEventName","getFirst","sha256","normalizeOAIPhone","normalizeOAIName"],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport {\n NON_AD_EVENTS,\n type OAIQ,\n type OAIQUser,\n mapOAIEvent,\n normalizeOAIName,\n normalizeOAIPhone,\n oaiCustomEventName,\n} from '../track/oaiq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\n\ndeclare global {\n interface Window {\n /** Undefined until the OpenAI pixel script has loaded. */\n oaiq?: OAIQ['oaiq'];\n }\n}\n\n/** Drop `undefined` fields so the SDK only receives populated values. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\n/**\n * Forward an internal track event to the OpenAI measurement pixel.\n * `eventId` is reused as the OpenAI `event_id` so browser events deduplicate against the\n * Conversions API. https://developers.openai.com/ads/measurement-pixel\n */\nexport function sendOpenAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n if (NON_AD_EVENTS.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const { type, data } = mapOAIEvent(name, properties);\n if (type === 'custom') {\n // Named as the Conversions API names it, so the two deduplicate; skipped when it cannot be.\n const custom_event_name = oaiCustomEventName(name);\n if (!custom_event_name) return;\n window.oaiq('measure', 'custom', clean(data), { event_id: eventId, custom_event_name });\n } else {\n window.oaiq('measure', type, clean(data), { event_id: eventId });\n }\n}\n\n/**\n * Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,\n * external id and names are normalized as OpenAI documents and hashed client-side; geographic\n * fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests\n * resolve.\n */\nexport function setOpenAIUser(pixelId: string) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n // Capture the narrowed reference so the deferred `init` closure keeps it non-optional.\n const oaiq = window.oaiq;\n\n const email = getFirst(user_data?.email)?.trim().toLowerCase() || undefined;\n const phone = getFirst(user_data?.phone_number);\n const address = getFirst(user_data?.address);\n\n const base: OAIQUser = {\n country: address?.country?.trim().toUpperCase(),\n city: address?.city?.trim(),\n region: address?.region?.trim(),\n postal_code: address?.postal_code?.trim(),\n };\n\n const init = (hashed: Partial<OAIQUser>) => {\n oaiq('init', { pixelId, user: clean({ ...base, ...hashed }) });\n };\n\n const hash = (value: string | undefined) => (value ? sha256(value) : undefined);\n Promise.all([\n hash(email),\n hash(phone ? normalizeOAIPhone(phone) : undefined),\n hash(user_id?.trim()),\n hash(normalizeOAIName(address?.first_name)),\n hash(normalizeOAIName(address?.last_name)),\n ])\n .then(([email_sha256, phone_number_sha256, external_id_sha256, first, last]) =>\n init({\n email_sha256,\n phone_number_sha256,\n external_id_sha256,\n first_name_sha256: first,\n last_name_sha256: last,\n })\n )\n .catch(() => init({}));\n };\n}\n"],"mappings":";;;;;;AAuBA,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;;;;AAOA,SAAgB,gBACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAIA,mBAAAA,cAAc,SAAS,IAAI,GAAG;CAClC,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,EAAE,MAAM,SAASC,mBAAAA,YAAY,MAAM,UAAU;CACnD,IAAI,SAAS,UAAU;EAErB,MAAM,oBAAoBC,mBAAAA,mBAAmB,IAAI;EACjD,IAAI,CAAC,mBAAmB;EACxB,OAAO,KAAK,WAAW,UAAU,MAAM,IAAI,GAAG;GAAE,UAAU;GAAS;EAAkB,CAAC;CACxF,OACE,OAAO,KAAK,WAAW,MAAM,MAAM,IAAI,GAAG,EAAE,UAAU,QAAQ,CAAC;AAEnE;;;;;;;AAQA,SAAgB,cAAc,SAAiB;CAC7C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;GACjD,QAAQ,KAAK,+BAA+B;GAC5C;EACF;EAEA,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQC,oBAAAA,SAAS,WAAW,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,YAAY,KAAK,KAAA;EAClE,MAAM,QAAQA,oBAAAA,SAAS,WAAW,YAAY;EAC9C,MAAM,UAAUA,oBAAAA,SAAS,WAAW,OAAO;EAE3C,MAAM,OAAiB;GACrB,SAAS,SAAS,SAAS,KAAK,CAAC,CAAC,YAAY;GAC9C,MAAM,SAAS,MAAM,KAAK;GAC1B,QAAQ,SAAS,QAAQ,KAAK;GAC9B,aAAa,SAAS,aAAa,KAAK;EAC1C;EAEA,MAAM,QAAQ,WAA8B;GAC1C,KAAK,QAAQ;IAAE;IAAS,MAAM,MAAM;KAAE,GAAG;KAAM,GAAG;IAAO,CAAC;GAAE,CAAC;EAC/D;EAEA,MAAM,QAAQ,UAA+B,QAAQC,qBAAAA,OAAO,KAAK,IAAI,KAAA;EACrE,QAAQ,IAAI;GACV,KAAK,KAAK;GACV,KAAK,QAAQC,mBAAAA,kBAAkB,KAAK,IAAI,KAAA,CAAS;GACjD,KAAK,SAAS,KAAK,CAAC;GACpB,KAAKC,mBAAAA,iBAAiB,SAAS,UAAU,CAAC;GAC1C,KAAKA,mBAAAA,iBAAiB,SAAS,SAAS,CAAC;EAC3C,CAAC,CAAC,CACC,MAAM,CAAC,cAAc,qBAAqB,oBAAoB,OAAO,UACpE,KAAK;GACH;GACA;GACA;GACA,mBAAmB;GACnB,kBAAkB;EACpB,CAAC,CACH,CAAC,CACA,YAAY,KAAK,CAAC,CAAC,CAAC;CACzB;AACF"}
@@ -15,9 +15,10 @@ declare global {
15
15
  */
16
16
  export declare function sendOpenAIEvent<T extends EventName>(name: TrackName<T>, properties?: TrackProperties<T>, eventId?: string): void;
17
17
  /**
18
- * Re-initialize the pixel with hashed user identity for better conversion matching. Email and
19
- * external id are hashed client-side; geographic fields are sent raw. Hashing is asynchronous,
20
- * so the `init` call is deferred until the digests resolve.
18
+ * Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,
19
+ * external id and names are normalized as OpenAI documents and hashed client-side; geographic
20
+ * fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests
21
+ * resolve.
21
22
  */
22
23
  export declare function setOpenAIUser(pixelId: string): ({ user_id, user_data }: UpdateVisitorDTO) => void;
23
24
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"openai-pixel.d.cts","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"mappings":";;;;QAMQ;YACI;;IAER,OAAO;;;;;;;;wBAeK,gBAAgB,UAAU,WACxC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;wBAuBc,cAAc,qBAAe,SAAA,aACX"}
1
+ {"version":3,"file":"openai-pixel.d.cts","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"mappings":";;;;QAcQ;YACI;;IAER,OAAO;;;;;;;;wBAeK,gBAAgB,UAAU,WACxC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;;wBA2Bc,cAAc,qBAAe,SAAA,aACX"}
@@ -15,9 +15,10 @@ declare global {
15
15
  */
16
16
  export declare function sendOpenAIEvent<T extends EventName>(name: TrackName<T>, properties?: TrackProperties<T>, eventId?: string): void;
17
17
  /**
18
- * Re-initialize the pixel with hashed user identity for better conversion matching. Email and
19
- * external id are hashed client-side; geographic fields are sent raw. Hashing is asynchronous,
20
- * so the `init` call is deferred until the digests resolve.
18
+ * Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,
19
+ * external id and names are normalized as OpenAI documents and hashed client-side; geographic
20
+ * fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests
21
+ * resolve.
21
22
  */
22
23
  export declare function setOpenAIUser(pixelId: string): ({ user_id, user_data }: UpdateVisitorDTO) => void;
23
24
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"openai-pixel.d.mts","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"mappings":";;;;QAMQ;YACI;;IAER,OAAO;;;;;;;;wBAeK,gBAAgB,UAAU,WACxC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;wBAuBc,cAAc,qBAAe,SAAA,aACX"}
1
+ {"version":3,"file":"openai-pixel.d.mts","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"mappings":";;;;QAcQ;YACI;;IAER,OAAO;;;;;;;;wBAeK,gBAAgB,UAAU,WACxC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;;wBA2Bc,cAAc,qBAAe,SAAA,aACX"}
@@ -1,6 +1,6 @@
1
1
  import { getFirst } from "../utils/field.mjs";
2
2
  import { sha256 } from "../utils/sha256.mjs";
3
- import { NON_AD_EVENTS, mapOAIEvent } from "../track/oaiq.mjs";
3
+ import { NON_AD_EVENTS, mapOAIEvent, normalizeOAIName, normalizeOAIPhone, oaiCustomEventName } from "../track/oaiq.mjs";
4
4
  //#region src/third-parties/openai-pixel.ts
5
5
  /** Drop `undefined` fields so the SDK only receives populated values. */
6
6
  function clean(value) {
@@ -20,16 +20,20 @@ function sendOpenAIEvent(name, properties, eventId) {
20
20
  if (window.location.host.includes("127.0.0.1")) return;
21
21
  if (window.location.host.includes("localhost")) return;
22
22
  const { type, data } = mapOAIEvent(name, properties);
23
- if (type === "custom") window.oaiq("measure", "custom", clean(data), {
24
- event_id: eventId,
25
- custom_event_name: name
26
- });
27
- else window.oaiq("measure", type, clean(data), { event_id: eventId });
23
+ if (type === "custom") {
24
+ const custom_event_name = oaiCustomEventName(name);
25
+ if (!custom_event_name) return;
26
+ window.oaiq("measure", "custom", clean(data), {
27
+ event_id: eventId,
28
+ custom_event_name
29
+ });
30
+ } else window.oaiq("measure", type, clean(data), { event_id: eventId });
28
31
  }
29
32
  /**
30
- * Re-initialize the pixel with hashed user identity for better conversion matching. Email and
31
- * external id are hashed client-side; geographic fields are sent raw. Hashing is asynchronous,
32
- * so the `init` call is deferred until the digests resolve.
33
+ * Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,
34
+ * external id and names are normalized as OpenAI documents and hashed client-side; geographic
35
+ * fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests
36
+ * resolve.
33
37
  */
34
38
  function setOpenAIUser(pixelId) {
35
39
  return ({ user_id, user_data }) => {
@@ -38,12 +42,14 @@ function setOpenAIUser(pixelId) {
38
42
  return;
39
43
  }
40
44
  const oaiq = window.oaiq;
41
- const email = getFirst(user_data?.email)?.trim().toLowerCase();
45
+ const email = getFirst(user_data?.email)?.trim().toLowerCase() || void 0;
46
+ const phone = getFirst(user_data?.phone_number);
42
47
  const address = getFirst(user_data?.address);
43
48
  const base = {
44
49
  country: address?.country?.trim().toUpperCase(),
45
- city: address?.city?.trim().toLowerCase(),
46
- zip_code: address?.postal_code
50
+ city: address?.city?.trim(),
51
+ region: address?.region?.trim(),
52
+ postal_code: address?.postal_code?.trim()
47
53
  };
48
54
  const init = (hashed) => {
49
55
  oaiq("init", {
@@ -54,9 +60,19 @@ function setOpenAIUser(pixelId) {
54
60
  })
55
61
  });
56
62
  };
57
- Promise.all([email ? sha256(email) : void 0, user_id ? sha256(user_id) : void 0]).then(([email_sha256, external_id_sha256]) => init({
63
+ const hash = (value) => value ? sha256(value) : void 0;
64
+ Promise.all([
65
+ hash(email),
66
+ hash(phone ? normalizeOAIPhone(phone) : void 0),
67
+ hash(user_id?.trim()),
68
+ hash(normalizeOAIName(address?.first_name)),
69
+ hash(normalizeOAIName(address?.last_name))
70
+ ]).then(([email_sha256, phone_number_sha256, external_id_sha256, first, last]) => init({
58
71
  email_sha256,
59
- external_id_sha256
72
+ phone_number_sha256,
73
+ external_id_sha256,
74
+ first_name_sha256: first,
75
+ last_name_sha256: last
60
76
  })).catch(() => init({}));
61
77
  };
62
78
  }
@@ -1 +1 @@
1
- {"version":3,"file":"openai-pixel.mjs","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport { NON_AD_EVENTS, type OAIQ, type OAIQUser, mapOAIEvent } from '../track/oaiq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\n\ndeclare global {\n interface Window {\n /** Undefined until the OpenAI pixel script has loaded. */\n oaiq?: OAIQ['oaiq'];\n }\n}\n\n/** Drop `undefined` fields so the SDK only receives populated values. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\n/**\n * Forward an internal track event to the OpenAI measurement pixel.\n * `eventId` is reused as the OpenAI `event_id` so browser events deduplicate against the\n * Conversions API. https://developers.openai.com/ads/measurement-pixel\n */\nexport function sendOpenAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n if (NON_AD_EVENTS.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const { type, data } = mapOAIEvent(name, properties);\n if (type === 'custom') {\n window.oaiq('measure', 'custom', clean(data), { event_id: eventId, custom_event_name: name });\n } else {\n window.oaiq('measure', type, clean(data), { event_id: eventId });\n }\n}\n\n/**\n * Re-initialize the pixel with hashed user identity for better conversion matching. Email and\n * external id are hashed client-side; geographic fields are sent raw. Hashing is asynchronous,\n * so the `init` call is deferred until the digests resolve.\n */\nexport function setOpenAIUser(pixelId: string) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n // Capture the narrowed reference so the deferred `init` closure keeps it non-optional.\n const oaiq = window.oaiq;\n\n const email = getFirst(user_data?.email)?.trim().toLowerCase();\n const address = getFirst(user_data?.address);\n\n const base: OAIQUser = {\n country: address?.country?.trim().toUpperCase(),\n city: address?.city?.trim().toLowerCase(),\n zip_code: address?.postal_code,\n };\n\n const init = (hashed: Partial<OAIQUser>) => {\n oaiq('init', { pixelId, user: clean({ ...base, ...hashed }) });\n };\n\n Promise.all([email ? sha256(email) : undefined, user_id ? sha256(user_id) : undefined])\n .then(([email_sha256, external_id_sha256]) => init({ email_sha256, external_id_sha256 }))\n .catch(() => init({}));\n };\n}\n"],"mappings":";;;;;AAeA,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;;;;AAOA,SAAgB,gBACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,cAAc,SAAS,IAAI,GAAG;CAClC,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,EAAE,MAAM,SAAS,YAAY,MAAM,UAAU;CACnD,IAAI,SAAS,UACX,OAAO,KAAK,WAAW,UAAU,MAAM,IAAI,GAAG;EAAE,UAAU;EAAS,mBAAmB;CAAK,CAAC;MAE5F,OAAO,KAAK,WAAW,MAAM,MAAM,IAAI,GAAG,EAAE,UAAU,QAAQ,CAAC;AAEnE;;;;;;AAOA,SAAgB,cAAc,SAAiB;CAC7C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;GACjD,QAAQ,KAAK,+BAA+B;GAC5C;EACF;EAEA,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQ,SAAS,WAAW,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,YAAY;EAC7D,MAAM,UAAU,SAAS,WAAW,OAAO;EAE3C,MAAM,OAAiB;GACrB,SAAS,SAAS,SAAS,KAAK,CAAC,CAAC,YAAY;GAC9C,MAAM,SAAS,MAAM,KAAK,CAAC,CAAC,YAAY;GACxC,UAAU,SAAS;EACrB;EAEA,MAAM,QAAQ,WAA8B;GAC1C,KAAK,QAAQ;IAAE;IAAS,MAAM,MAAM;KAAE,GAAG;KAAM,GAAG;IAAO,CAAC;GAAE,CAAC;EAC/D;EAEA,QAAQ,IAAI,CAAC,QAAQ,OAAO,KAAK,IAAI,KAAA,GAAW,UAAU,OAAO,OAAO,IAAI,KAAA,CAAS,CAAC,CAAC,CACpF,MAAM,CAAC,cAAc,wBAAwB,KAAK;GAAE;GAAc;EAAmB,CAAC,CAAC,CAAC,CACxF,YAAY,KAAK,CAAC,CAAC,CAAC;CACzB;AACF"}
1
+ {"version":3,"file":"openai-pixel.mjs","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport {\n NON_AD_EVENTS,\n type OAIQ,\n type OAIQUser,\n mapOAIEvent,\n normalizeOAIName,\n normalizeOAIPhone,\n oaiCustomEventName,\n} from '../track/oaiq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\n\ndeclare global {\n interface Window {\n /** Undefined until the OpenAI pixel script has loaded. */\n oaiq?: OAIQ['oaiq'];\n }\n}\n\n/** Drop `undefined` fields so the SDK only receives populated values. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\n/**\n * Forward an internal track event to the OpenAI measurement pixel.\n * `eventId` is reused as the OpenAI `event_id` so browser events deduplicate against the\n * Conversions API. https://developers.openai.com/ads/measurement-pixel\n */\nexport function sendOpenAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n if (NON_AD_EVENTS.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const { type, data } = mapOAIEvent(name, properties);\n if (type === 'custom') {\n // Named as the Conversions API names it, so the two deduplicate; skipped when it cannot be.\n const custom_event_name = oaiCustomEventName(name);\n if (!custom_event_name) return;\n window.oaiq('measure', 'custom', clean(data), { event_id: eventId, custom_event_name });\n } else {\n window.oaiq('measure', type, clean(data), { event_id: eventId });\n }\n}\n\n/**\n * Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,\n * external id and names are normalized as OpenAI documents and hashed client-side; geographic\n * fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests\n * resolve.\n */\nexport function setOpenAIUser(pixelId: string) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n // Capture the narrowed reference so the deferred `init` closure keeps it non-optional.\n const oaiq = window.oaiq;\n\n const email = getFirst(user_data?.email)?.trim().toLowerCase() || undefined;\n const phone = getFirst(user_data?.phone_number);\n const address = getFirst(user_data?.address);\n\n const base: OAIQUser = {\n country: address?.country?.trim().toUpperCase(),\n city: address?.city?.trim(),\n region: address?.region?.trim(),\n postal_code: address?.postal_code?.trim(),\n };\n\n const init = (hashed: Partial<OAIQUser>) => {\n oaiq('init', { pixelId, user: clean({ ...base, ...hashed }) });\n };\n\n const hash = (value: string | undefined) => (value ? sha256(value) : undefined);\n Promise.all([\n hash(email),\n hash(phone ? normalizeOAIPhone(phone) : undefined),\n hash(user_id?.trim()),\n hash(normalizeOAIName(address?.first_name)),\n hash(normalizeOAIName(address?.last_name)),\n ])\n .then(([email_sha256, phone_number_sha256, external_id_sha256, first, last]) =>\n init({\n email_sha256,\n phone_number_sha256,\n external_id_sha256,\n first_name_sha256: first,\n last_name_sha256: last,\n })\n )\n .catch(() => init({}));\n };\n}\n"],"mappings":";;;;;AAuBA,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;;;;AAOA,SAAgB,gBACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,cAAc,SAAS,IAAI,GAAG;CAClC,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,EAAE,MAAM,SAAS,YAAY,MAAM,UAAU;CACnD,IAAI,SAAS,UAAU;EAErB,MAAM,oBAAoB,mBAAmB,IAAI;EACjD,IAAI,CAAC,mBAAmB;EACxB,OAAO,KAAK,WAAW,UAAU,MAAM,IAAI,GAAG;GAAE,UAAU;GAAS;EAAkB,CAAC;CACxF,OACE,OAAO,KAAK,WAAW,MAAM,MAAM,IAAI,GAAG,EAAE,UAAU,QAAQ,CAAC;AAEnE;;;;;;;AAQA,SAAgB,cAAc,SAAiB;CAC7C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;GACjD,QAAQ,KAAK,+BAA+B;GAC5C;EACF;EAEA,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQ,SAAS,WAAW,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,YAAY,KAAK,KAAA;EAClE,MAAM,QAAQ,SAAS,WAAW,YAAY;EAC9C,MAAM,UAAU,SAAS,WAAW,OAAO;EAE3C,MAAM,OAAiB;GACrB,SAAS,SAAS,SAAS,KAAK,CAAC,CAAC,YAAY;GAC9C,MAAM,SAAS,MAAM,KAAK;GAC1B,QAAQ,SAAS,QAAQ,KAAK;GAC9B,aAAa,SAAS,aAAa,KAAK;EAC1C;EAEA,MAAM,QAAQ,WAA8B;GAC1C,KAAK,QAAQ;IAAE;IAAS,MAAM,MAAM;KAAE,GAAG;KAAM,GAAG;IAAO,CAAC;GAAE,CAAC;EAC/D;EAEA,MAAM,QAAQ,UAA+B,QAAQ,OAAO,KAAK,IAAI,KAAA;EACrE,QAAQ,IAAI;GACV,KAAK,KAAK;GACV,KAAK,QAAQ,kBAAkB,KAAK,IAAI,KAAA,CAAS;GACjD,KAAK,SAAS,KAAK,CAAC;GACpB,KAAK,iBAAiB,SAAS,UAAU,CAAC;GAC1C,KAAK,iBAAiB,SAAS,SAAS,CAAC;EAC3C,CAAC,CAAC,CACC,MAAM,CAAC,cAAc,qBAAqB,oBAAoB,OAAO,UACpE,KAAK;GACH;GACA;GACA;GACA,mBAAmB;GACnB,kBAAkB;EACpB,CAAC,CACH,CAAC,CACA,YAAY,KAAK,CAAC,CAAC,CAAC;CACzB;AACF"}
@@ -10,6 +10,43 @@ const NON_AD_EVENTS = [
10
10
  "view_promotion",
11
11
  "select_promotion"
12
12
  ];
13
+ const STANDARD_EVENTS = [
14
+ "appointment_scheduled",
15
+ "checkout_started",
16
+ "contents_viewed",
17
+ "items_added",
18
+ "lead_created",
19
+ "order_created",
20
+ "page_viewed",
21
+ "registration_completed",
22
+ "subscription_created",
23
+ "trial_started"
24
+ ];
25
+ /**
26
+ * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,
27
+ * starting and ending with a letter or digit, not a standard event name; the API lowercases it.
28
+ * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when
29
+ * nothing usable is left — the event is then not sent at all. The pixel and the server both name
30
+ * a custom event through this, so the two still deduplicate.
31
+ * https://developers.openai.com/ads/supported-events
32
+ */
33
+ function oaiCustomEventName(name) {
34
+ const sanitized = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "_").slice(0, 64).replace(/^[_-]+|[_-]+$/g, "");
35
+ if (!sanitized || STANDARD_EVENTS.includes(sanitized)) return void 0;
36
+ return sanitized;
37
+ }
38
+ /**
39
+ * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then
40
+ * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.
41
+ */
42
+ function normalizeOAIPhone(phone) {
43
+ const digits = phone.replace(/[\s().-]/g, "").replace(/^\+/, "").replace(/^0+/, "");
44
+ return /^\d{8,15}$/.test(digits) ? digits : void 0;
45
+ }
46
+ /** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */
47
+ function normalizeOAIName(name) {
48
+ return name?.toLowerCase().replace(/[\s!-/:-@[-`{-~]/g, "") || void 0;
49
+ }
13
50
  /**
14
51
  * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.
15
52
  * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),
@@ -44,24 +81,32 @@ const THREE_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
44
81
  "OMR",
45
82
  "TND"
46
83
  ]);
47
- /** Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). */
84
+ /**
85
+ * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None
86
+ * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every
87
+ * `amount` — an amount alone would be invalid, not just imprecise.
88
+ */
48
89
  function toMinorUnits(value, currency) {
49
90
  if (value === void 0 || value === null || Number.isNaN(value)) return void 0;
50
91
  const code = currency?.toUpperCase();
92
+ if (!code) return void 0;
51
93
  const exponent = code && ZERO_DECIMAL_CURRENCIES.has(code) ? 0 : code && THREE_DECIMAL_CURRENCIES.has(code) ? 3 : 2;
52
94
  return Math.round(value * 10 ** exponent);
53
95
  }
54
96
  function mapContents(items, currency) {
55
97
  if (!items || items.length === 0) return void 0;
56
98
  const code = currency?.toUpperCase();
57
- return items.map((item) => ({
58
- id: item.item_id,
59
- name: item.item_name,
60
- content_type: item.item_category,
61
- quantity: item.quantity,
62
- amount: toMinorUnits(item.price, currency),
63
- currency: item.price !== void 0 ? code : void 0
64
- }));
99
+ return items.map((item) => {
100
+ const amount = toMinorUnits(item.price, currency);
101
+ return {
102
+ id: item.item_id,
103
+ name: item.item_name,
104
+ content_type: item.item_category,
105
+ quantity: item.quantity,
106
+ amount,
107
+ currency: amount !== void 0 ? code : void 0
108
+ };
109
+ });
65
110
  }
66
111
  /** Safely read a property off loosely typed track properties without widening to `any`. */
67
112
  function prop(properties, key) {
@@ -171,6 +216,9 @@ function mapOAIEvent(name, properties) {
171
216
  exports.NON_AD_EVENTS = NON_AD_EVENTS;
172
217
  exports.mapContents = mapContents;
173
218
  exports.mapOAIEvent = mapOAIEvent;
219
+ exports.normalizeOAIName = normalizeOAIName;
220
+ exports.normalizeOAIPhone = normalizeOAIPhone;
221
+ exports.oaiCustomEventName = oaiCustomEventName;
174
222
  exports.toMinorUnits = toMinorUnits;
175
223
 
176
224
  //# sourceMappingURL=oaiq.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"oaiq.cjs","names":[],"sources":["../../src/track/oaiq.ts"],"sourcesContent":["import type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport const NON_AD_EVENTS = [\n // metrics\n 'CLS',\n 'FCP',\n 'FID',\n 'INP',\n 'LCP',\n 'TTFB',\n // promotions\n 'view_promotion',\n 'select_promotion',\n];\n\n/**\n * Content item shared by `contents` and `plan_enrollment` events.\n * https://developers.openai.com/ads/supported-events\n */\nexport type ContentItem = {\n id?: string;\n name?: string;\n content_type?: string;\n quantity?: number;\n /** Per-item monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n};\n\nexport type ContentsData = {\n type: 'contents';\n /** Monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomerActionData = {\n type: 'customer_action';\n amount?: number;\n currency?: string;\n};\n\nexport type PlanEnrollmentData = {\n type: 'plan_enrollment';\n plan_id?: string;\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomEventData = {\n type: 'custom';\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type EventData = ContentsData | CustomerActionData | PlanEnrollmentData | CustomEventData;\n\n/**\n * Standard event names and the `data` shape each one expects.\n * https://developers.openai.com/ads/supported-events\n */\nexport type StandardEvents = {\n appointment_scheduled: CustomerActionData;\n checkout_started: ContentsData;\n contents_viewed: ContentsData;\n items_added: ContentsData;\n lead_created: CustomerActionData;\n order_created: ContentsData;\n page_viewed: ContentsData;\n registration_completed: CustomerActionData;\n subscription_created: PlanEnrollmentData;\n trial_started: PlanEnrollmentData;\n};\n\nexport type StandardEvent = keyof StandardEvents;\n\n/**\n * Identity fields passed to `oaiq(\"init\", ...)` for conversion matching. Email and external id\n * must be pre-hashed as lowercase 64-char SHA-256 hex strings; the rest are sent as raw values.\n * https://developers.openai.com/ads/measurement-pixel\n */\nexport type OAIQUser = {\n email_sha256?: string;\n external_id_sha256?: string;\n /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n country?: string;\n /** Lowercased, max 128 characters. */\n city?: string;\n /** Max 32 characters. */\n zip_code?: string;\n};\n\nexport type InitConfig = {\n pixelId: string;\n /** Log SDK activity to the browser console. */\n debug?: boolean;\n user?: OAIQUser;\n};\n\n/**\n * Fourth argument of `oaiq(\"measure\", ...)`. Required for custom events (carries\n * `custom_event_name`); optional for standard events.\n */\nexport type MeasureOptions = {\n /** Shared with the Conversions API to deduplicate the same conversion across browser and server. */\n event_id?: string;\n /** Required for custom events; 1-64 chars of letters, numbers, underscores, or dashes. */\n custom_event_name?: string;\n /** When true, opts the event out of user-level personalization. */\n opt_out?: boolean;\n};\n\nexport interface OAIQ {\n oaiq(command: 'init', config: InitConfig): void;\n oaiq<T extends StandardEvent>(\n command: 'measure',\n event: T,\n data: StandardEvents[T],\n options?: MeasureOptions\n ): void;\n oaiq(\n command: 'measure',\n event: 'custom',\n data: CustomEventData,\n options: MeasureOptions & { custom_event_name: string }\n ): void;\n}\n\n/**\n * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.\n * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),\n * which can differ from strict ISO 4217 exponents.\n * https://docs.stripe.com/currencies#zero-decimal\n */\nconst ZERO_DECIMAL_CURRENCIES = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'ISK',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n]);\nconst THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']);\n\n/** Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). */\nexport function toMinorUnits(value?: number | null, currency?: string): number | undefined {\n if (value === undefined || value === null || Number.isNaN(value)) return undefined;\n const code = currency?.toUpperCase();\n const exponent =\n code && ZERO_DECIMAL_CURRENCIES.has(code)\n ? 0\n : code && THREE_DECIMAL_CURRENCIES.has(code)\n ? 3\n : 2;\n return Math.round(value * 10 ** exponent);\n}\n\nexport function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined {\n if (!items || items.length === 0) return undefined;\n const code = currency?.toUpperCase();\n return items.map((item) => ({\n id: item.item_id,\n name: item.item_name,\n content_type: item.item_category,\n quantity: item.quantity,\n amount: toMinorUnits(item.price, currency),\n currency: item.price !== undefined ? code : undefined,\n }));\n}\n\n/** Safely read a property off loosely typed track properties without widening to `any`. */\nfunction prop(properties: unknown, key: string): unknown {\n return typeof properties === 'object' && properties !== null\n ? (properties as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction readValue(properties: unknown): number | undefined {\n const value = prop(properties, 'value');\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction readCurrency(properties: unknown): string | undefined {\n const currency = prop(properties, 'currency');\n return typeof currency === 'string' ? currency : undefined;\n}\n\nfunction readItems(properties: unknown): Item[] | undefined {\n const items = prop(properties, 'items');\n return Array.isArray(items) ? (items as Item[]) : undefined;\n}\n\nfunction contentsData(properties: unknown): ContentsData {\n const currency = readCurrency(properties);\n return {\n type: 'contents',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n };\n}\n\nfunction customerActionData(properties: unknown): CustomerActionData {\n const currency = readCurrency(properties);\n return {\n type: 'customer_action',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n };\n}\n\nfunction planEnrollmentData(properties: unknown): PlanEnrollmentData {\n const currency = readCurrency(properties);\n const items = readItems(properties);\n return {\n type: 'plan_enrollment',\n plan_id: items?.[0]?.item_id,\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(items, currency),\n };\n}\n\nexport type MappedOAIEvent = {\n type: StandardEvent | 'custom';\n data: EventData;\n};\n\n/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */\nexport function mapOAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): MappedOAIEvent {\n switch (name) {\n case 'page_view':\n return { type: 'page_viewed', data: contentsData(properties) };\n case 'view_item':\n case 'view_item_list':\n return { type: 'contents_viewed', data: contentsData(properties) };\n case 'begin_checkout':\n return { type: 'checkout_started', data: contentsData(properties) };\n case 'add_to_cart':\n return { type: 'items_added', data: contentsData(properties) };\n case 'purchase':\n return { type: 'order_created', data: contentsData(properties) };\n case 'generate_lead':\n case 'qualify_lead':\n return { type: 'lead_created', data: customerActionData(properties) };\n case 'sign_up':\n case 'login':\n return { type: 'registration_completed', data: customerActionData(properties) };\n case 'schedule':\n return { type: 'appointment_scheduled', data: customerActionData(properties) };\n case 'subscribe':\n return { type: 'subscription_created', data: planEnrollmentData(properties) };\n case 'trial_begin':\n return { type: 'trial_started', data: planEnrollmentData(properties) };\n default: {\n const currency = readCurrency(properties);\n return {\n type: 'custom',\n data: {\n type: 'custom',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n },\n };\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"oaiq.cjs","names":[],"sources":["../../src/track/oaiq.ts"],"sourcesContent":["import type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport const NON_AD_EVENTS = [\n // metrics\n 'CLS',\n 'FCP',\n 'FID',\n 'INP',\n 'LCP',\n 'TTFB',\n // promotions\n 'view_promotion',\n 'select_promotion',\n];\n\n/**\n * Content item shared by `contents` and `plan_enrollment` events.\n * https://developers.openai.com/ads/supported-events\n */\nexport type ContentItem = {\n id?: string;\n name?: string;\n content_type?: string;\n quantity?: number;\n /** Per-item monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n};\n\nexport type ContentsData = {\n type: 'contents';\n /** Monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomerActionData = {\n type: 'customer_action';\n amount?: number;\n currency?: string;\n};\n\nexport type PlanEnrollmentData = {\n type: 'plan_enrollment';\n plan_id?: string;\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomEventData = {\n type: 'custom';\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type EventData = ContentsData | CustomerActionData | PlanEnrollmentData | CustomEventData;\n\n/**\n * Standard event names and the `data` shape each one expects.\n * https://developers.openai.com/ads/supported-events\n */\nexport type StandardEvents = {\n appointment_scheduled: CustomerActionData;\n checkout_started: ContentsData;\n contents_viewed: ContentsData;\n items_added: ContentsData;\n lead_created: CustomerActionData;\n order_created: ContentsData;\n page_viewed: ContentsData;\n registration_completed: CustomerActionData;\n subscription_created: PlanEnrollmentData;\n trial_started: PlanEnrollmentData;\n};\n\nexport type StandardEvent = keyof StandardEvents;\n\nconst STANDARD_EVENTS: readonly string[] = [\n 'appointment_scheduled',\n 'checkout_started',\n 'contents_viewed',\n 'items_added',\n 'lead_created',\n 'order_created',\n 'page_viewed',\n 'registration_completed',\n 'subscription_created',\n 'trial_started',\n] satisfies StandardEvent[];\n\n/**\n * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,\n * starting and ending with a letter or digit, not a standard event name; the API lowercases it.\n * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when\n * nothing usable is left — the event is then not sent at all. The pixel and the server both name\n * a custom event through this, so the two still deduplicate.\n * https://developers.openai.com/ads/supported-events\n */\nexport function oaiCustomEventName(name: string): string | undefined {\n const sanitized = name\n .toLowerCase()\n .replace(/[^a-z0-9_-]+/g, '_')\n .slice(0, 64)\n .replace(/^[_-]+|[_-]+$/g, '');\n if (!sanitized || STANDARD_EVENTS.includes(sanitized)) return undefined;\n return sanitized;\n}\n\n/**\n * Identity fields passed to `oaiq(\"init\", ...)` for conversion matching. Identifiers are\n * normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.\n * https://developers.openai.com/ads/measurement-pixel#send-user-data\n */\nexport type OAIQUser = {\n email_sha256?: string;\n /** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */\n phone_number_sha256?: string;\n external_id_sha256?: string;\n /** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */\n first_name_sha256?: string;\n last_name_sha256?: string;\n /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n country?: string;\n /** Max 128 characters; OpenAI trims and lowercases it. */\n city?: string;\n /** State, province or region, max 128 characters. */\n region?: string;\n postal_code?: string;\n};\n\n/**\n * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then\n * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.\n */\nexport function normalizeOAIPhone(phone: string): string | undefined {\n const digits = phone\n .replace(/[\\s().-]/g, '')\n .replace(/^\\+/, '')\n .replace(/^0+/, '');\n return /^\\d{8,15}$/.test(digits) ? digits : undefined;\n}\n\n/** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */\nexport function normalizeOAIName(name: string | undefined): string | undefined {\n return name?.toLowerCase().replace(/[\\s!-/:-@[-`{-~]/g, '') || undefined;\n}\n\nexport type InitConfig = {\n pixelId: string;\n /** Log SDK activity to the browser console. */\n debug?: boolean;\n user?: OAIQUser;\n};\n\n/**\n * Fourth argument of `oaiq(\"measure\", ...)`. Required for custom events (carries\n * `custom_event_name`); optional for standard events.\n */\nexport type MeasureOptions = {\n /** Shared with the Conversions API to deduplicate the same conversion across browser and server. */\n event_id?: string;\n /** Required for custom events; 1-64 chars of letters, numbers, underscores, or dashes. */\n custom_event_name?: string;\n /** When true, opts the event out of user-level personalization. */\n opt_out?: boolean;\n};\n\nexport interface OAIQ {\n oaiq(command: 'init', config: InitConfig): void;\n oaiq<T extends StandardEvent>(\n command: 'measure',\n event: T,\n data: StandardEvents[T],\n options?: MeasureOptions\n ): void;\n oaiq(\n command: 'measure',\n event: 'custom',\n data: CustomEventData,\n options: MeasureOptions & { custom_event_name: string }\n ): void;\n}\n\n/**\n * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.\n * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),\n * which can differ from strict ISO 4217 exponents.\n * https://docs.stripe.com/currencies#zero-decimal\n */\nconst ZERO_DECIMAL_CURRENCIES = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'ISK',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n]);\nconst THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']);\n\n/**\n * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None\n * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every\n * `amount` — an amount alone would be invalid, not just imprecise.\n */\nexport function toMinorUnits(value?: number | null, currency?: string): number | undefined {\n if (value === undefined || value === null || Number.isNaN(value)) return undefined;\n const code = currency?.toUpperCase();\n if (!code) return undefined;\n const exponent =\n code && ZERO_DECIMAL_CURRENCIES.has(code)\n ? 0\n : code && THREE_DECIMAL_CURRENCIES.has(code)\n ? 3\n : 2;\n return Math.round(value * 10 ** exponent);\n}\n\nexport function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined {\n if (!items || items.length === 0) return undefined;\n const code = currency?.toUpperCase();\n return items.map((item) => {\n const amount = toMinorUnits(item.price, currency);\n return {\n id: item.item_id,\n name: item.item_name,\n content_type: item.item_category,\n quantity: item.quantity,\n amount,\n currency: amount !== undefined ? code : undefined,\n };\n });\n}\n\n/** Safely read a property off loosely typed track properties without widening to `any`. */\nfunction prop(properties: unknown, key: string): unknown {\n return typeof properties === 'object' && properties !== null\n ? (properties as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction readValue(properties: unknown): number | undefined {\n const value = prop(properties, 'value');\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction readCurrency(properties: unknown): string | undefined {\n const currency = prop(properties, 'currency');\n return typeof currency === 'string' ? currency : undefined;\n}\n\nfunction readItems(properties: unknown): Item[] | undefined {\n const items = prop(properties, 'items');\n return Array.isArray(items) ? (items as Item[]) : undefined;\n}\n\nfunction contentsData(properties: unknown): ContentsData {\n const currency = readCurrency(properties);\n return {\n type: 'contents',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n };\n}\n\nfunction customerActionData(properties: unknown): CustomerActionData {\n const currency = readCurrency(properties);\n return {\n type: 'customer_action',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n };\n}\n\nfunction planEnrollmentData(properties: unknown): PlanEnrollmentData {\n const currency = readCurrency(properties);\n const items = readItems(properties);\n return {\n type: 'plan_enrollment',\n plan_id: items?.[0]?.item_id,\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(items, currency),\n };\n}\n\nexport type MappedOAIEvent = {\n type: StandardEvent | 'custom';\n data: EventData;\n};\n\n/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */\nexport function mapOAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): MappedOAIEvent {\n switch (name) {\n case 'page_view':\n return { type: 'page_viewed', data: contentsData(properties) };\n case 'view_item':\n case 'view_item_list':\n return { type: 'contents_viewed', data: contentsData(properties) };\n case 'begin_checkout':\n return { type: 'checkout_started', data: contentsData(properties) };\n case 'add_to_cart':\n return { type: 'items_added', data: contentsData(properties) };\n case 'purchase':\n return { type: 'order_created', data: contentsData(properties) };\n case 'generate_lead':\n case 'qualify_lead':\n return { type: 'lead_created', data: customerActionData(properties) };\n case 'sign_up':\n case 'login':\n return { type: 'registration_completed', data: customerActionData(properties) };\n case 'schedule':\n return { type: 'appointment_scheduled', data: customerActionData(properties) };\n case 'subscribe':\n return { type: 'subscription_created', data: planEnrollmentData(properties) };\n case 'trial_begin':\n return { type: 'trial_started', data: planEnrollmentData(properties) };\n default: {\n const currency = readCurrency(properties);\n return {\n type: 'custom',\n data: {\n type: 'custom',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n },\n };\n }\n }\n}\n"],"mappings":";;AAGA,MAAa,gBAAgB;CAE3B;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;AACF;AAkEA,MAAM,kBAAqC;CACzC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;AAUA,SAAgB,mBAAmB,MAAkC;CACnE,MAAM,YAAY,KACf,YAAY,CAAC,CACb,QAAQ,iBAAiB,GAAG,CAAC,CAC7B,MAAM,GAAG,EAAE,CAAC,CACZ,QAAQ,kBAAkB,EAAE;CAC/B,IAAI,CAAC,aAAa,gBAAgB,SAAS,SAAS,GAAG,OAAO,KAAA;CAC9D,OAAO;AACT;;;;;AA4BA,SAAgB,kBAAkB,OAAmC;CACnE,MAAM,SAAS,MACZ,QAAQ,aAAa,EAAE,CAAC,CACxB,QAAQ,OAAO,EAAE,CAAC,CAClB,QAAQ,OAAO,EAAE;CACpB,OAAO,aAAa,KAAK,MAAM,IAAI,SAAS,KAAA;AAC9C;;AAGA,SAAgB,iBAAiB,MAA8C;CAC7E,OAAO,MAAM,YAAY,CAAC,CAAC,QAAQ,qBAAqB,EAAE,KAAK,KAAA;AACjE;;;;;;;AA4CA,MAAM,0CAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,2CAA2B,IAAI,IAAI;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;CAAO;AAAK,CAAC;;;;;;AAO1F,SAAgB,aAAa,OAAuB,UAAuC;CACzF,IAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,OAAO,MAAM,KAAK,GAAG,OAAO,KAAA;CACzE,MAAM,OAAO,UAAU,YAAY;CACnC,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,MAAM,WACJ,QAAQ,wBAAwB,IAAI,IAAI,IACpC,IACA,QAAQ,yBAAyB,IAAI,IAAI,IACvC,IACA;CACR,OAAO,KAAK,MAAM,QAAQ,MAAM,QAAQ;AAC1C;AAEA,SAAgB,YAAY,OAAgB,UAA8C;CACxF,IAAI,CAAC,SAAS,MAAM,WAAW,GAAG,OAAO,KAAA;CACzC,MAAM,OAAO,UAAU,YAAY;CACnC,OAAO,MAAM,KAAK,SAAS;EACzB,MAAM,SAAS,aAAa,KAAK,OAAO,QAAQ;EAChD,OAAO;GACL,IAAI,KAAK;GACT,MAAM,KAAK;GACX,cAAc,KAAK;GACnB,UAAU,KAAK;GACf;GACA,UAAU,WAAW,KAAA,IAAY,OAAO,KAAA;EAC1C;CACF,CAAC;AACH;;AAGA,SAAS,KAAK,YAAqB,KAAsB;CACvD,OAAO,OAAO,eAAe,YAAY,eAAe,OACnD,WAAuC,OACxC,KAAA;AACN;AAEA,SAAS,UAAU,YAAyC;CAC1D,MAAM,QAAQ,KAAK,YAAY,OAAO;CACtC,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;AAEA,SAAS,aAAa,YAAyC;CAC7D,MAAM,WAAW,KAAK,YAAY,UAAU;CAC5C,OAAO,OAAO,aAAa,WAAW,WAAW,KAAA;AACnD;AAEA,SAAS,UAAU,YAAyC;CAC1D,MAAM,QAAQ,KAAK,YAAY,OAAO;CACtC,OAAO,MAAM,QAAQ,KAAK,IAAK,QAAmB,KAAA;AACpD;AAEA,SAAS,aAAa,YAAmC;CACvD,MAAM,WAAW,aAAa,UAAU;CACxC,OAAO;EACL,MAAM;EACN,QAAQ,aAAa,UAAU,UAAU,GAAG,QAAQ;EACpD,UAAU,UAAU,YAAY;EAChC,UAAU,YAAY,UAAU,UAAU,GAAG,QAAQ;CACvD;AACF;AAEA,SAAS,mBAAmB,YAAyC;CACnE,MAAM,WAAW,aAAa,UAAU;CACxC,OAAO;EACL,MAAM;EACN,QAAQ,aAAa,UAAU,UAAU,GAAG,QAAQ;EACpD,UAAU,UAAU,YAAY;CAClC;AACF;AAEA,SAAS,mBAAmB,YAAyC;CACnE,MAAM,WAAW,aAAa,UAAU;CACxC,MAAM,QAAQ,UAAU,UAAU;CAClC,OAAO;EACL,MAAM;EACN,SAAS,QAAQ,EAAE,EAAE;EACrB,QAAQ,aAAa,UAAU,UAAU,GAAG,QAAQ;EACpD,UAAU,UAAU,YAAY;EAChC,UAAU,YAAY,OAAO,QAAQ;CACvC;AACF;;AAQA,SAAgB,YACd,MACA,YACgB;CAChB,QAAQ,MAAR;EACE,KAAK,aACH,OAAO;GAAE,MAAM;GAAe,MAAM,aAAa,UAAU;EAAE;EAC/D,KAAK;EACL,KAAK,kBACH,OAAO;GAAE,MAAM;GAAmB,MAAM,aAAa,UAAU;EAAE;EACnE,KAAK,kBACH,OAAO;GAAE,MAAM;GAAoB,MAAM,aAAa,UAAU;EAAE;EACpE,KAAK,eACH,OAAO;GAAE,MAAM;GAAe,MAAM,aAAa,UAAU;EAAE;EAC/D,KAAK,YACH,OAAO;GAAE,MAAM;GAAiB,MAAM,aAAa,UAAU;EAAE;EACjE,KAAK;EACL,KAAK,gBACH,OAAO;GAAE,MAAM;GAAgB,MAAM,mBAAmB,UAAU;EAAE;EACtE,KAAK;EACL,KAAK,SACH,OAAO;GAAE,MAAM;GAA0B,MAAM,mBAAmB,UAAU;EAAE;EAChF,KAAK,YACH,OAAO;GAAE,MAAM;GAAyB,MAAM,mBAAmB,UAAU;EAAE;EAC/E,KAAK,aACH,OAAO;GAAE,MAAM;GAAwB,MAAM,mBAAmB,UAAU;EAAE;EAC9E,KAAK,eACH,OAAO;GAAE,MAAM;GAAiB,MAAM,mBAAmB,UAAU;EAAE;EACvE,SAAS;GACP,MAAM,WAAW,aAAa,UAAU;GACxC,OAAO;IACL,MAAM;IACN,MAAM;KACJ,MAAM;KACN,QAAQ,aAAa,UAAU,UAAU,GAAG,QAAQ;KACpD,UAAU,UAAU,YAAY;KAChC,UAAU,YAAY,UAAU,UAAU,GAAG,QAAQ;IACvD;GACF;EACF;CACF;AACF"}
@@ -59,20 +59,42 @@ export type StandardEvents = {
59
59
  };
60
60
  export type StandardEvent = keyof StandardEvents;
61
61
  /**
62
- * Identity fields passed to `oaiq("init", ...)` for conversion matching. Email and external id
63
- * must be pre-hashed as lowercase 64-char SHA-256 hex strings; the rest are sent as raw values.
64
- * https://developers.openai.com/ads/measurement-pixel
62
+ * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,
63
+ * starting and ending with a letter or digit, not a standard event name; the API lowercases it.
64
+ * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when
65
+ * nothing usable is left — the event is then not sent at all. The pixel and the server both name
66
+ * a custom event through this, so the two still deduplicate.
67
+ * https://developers.openai.com/ads/supported-events
68
+ */
69
+ export declare function oaiCustomEventName(name: string): string | undefined;
70
+ /**
71
+ * Identity fields passed to `oaiq("init", ...)` for conversion matching. Identifiers are
72
+ * normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.
73
+ * https://developers.openai.com/ads/measurement-pixel#send-user-data
65
74
  */
66
75
  export type OAIQUser = {
67
76
  email_sha256?: string;
77
+ /** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */
78
+ phone_number_sha256?: string;
68
79
  external_id_sha256?: string;
80
+ /** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */
81
+ first_name_sha256?: string;
82
+ last_name_sha256?: string;
69
83
  /** Two-letter ISO 3166-1 country code (e.g. "US"). */
70
84
  country?: string;
71
- /** Lowercased, max 128 characters. */
85
+ /** Max 128 characters; OpenAI trims and lowercases it. */
72
86
  city?: string;
73
- /** Max 32 characters. */
74
- zip_code?: string;
87
+ /** State, province or region, max 128 characters. */
88
+ region?: string;
89
+ postal_code?: string;
75
90
  };
91
+ /**
92
+ * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then
93
+ * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.
94
+ */
95
+ export declare function normalizeOAIPhone(phone: string): string | undefined;
96
+ /** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */
97
+ export declare function normalizeOAIName(name: string | undefined): string | undefined;
76
98
  export type InitConfig = {
77
99
  pixelId: string;
78
100
  /** Log SDK activity to the browser console. */
@@ -98,7 +120,11 @@ export interface OAIQ {
98
120
  custom_event_name: string;
99
121
  }): void;
100
122
  }
101
- /** Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). */
123
+ /**
124
+ * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None
125
+ * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every
126
+ * `amount` — an amount alone would be invalid, not just imprecise.
127
+ */
102
128
  export declare function toMinorUnits(value?: number | null, currency?: string): number | undefined;
103
129
  export declare function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined;
104
130
  export type MappedOAIEvent = {
@@ -1 +1 @@
1
- {"version":3,"file":"oaiq.d.cts","names":[],"sources":["../../src/track/oaiq.ts"],"mappings":";;;qBAGa;;;;;YAiBD;EACV;EACA;EACA;EACA;;EAEA;EACA;;YAGU;EACV;;EAEA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;EACA,WAAW;;YAGD,YAAY,eAAe,qBAAqB,qBAAqB;;;;;YAMrE;EACV,uBAAuB;EACvB,kBAAkB;EAClB,iBAAiB;EACjB,aAAa;EACb,cAAc;EACd,eAAe;EACf,aAAa;EACb,wBAAwB;EACxB,sBAAsB;EACtB,eAAe;;YAGL,sBAAsB;;;;;;YAOtB;EACV;EACA;;EAEA;;EAEA;;EAEA;;YAGU;EACV;;EAEA;EACA,OAAO;;;;;;YAOG;;EAEV;;EAEA;;EAEA;;iBAGe;EACf,KAAK,iBAAiB,QAAQ;EAC9B,KAAK,UAAU,eACb,oBACA,OAAO,GACP,MAAM,eAAe,IACrB,UAAU;EAEZ,KACE,oBACA,iBACA,MAAM,iBACN,SAAS;IAAmB;;;;wBAgChB,aAAa,uBAAuB;wBAYpC,YAAY,QAAQ,QAAQ,oBAAoB;YAkEpD;EACV,MAAM;EACN,MAAM;;;wBAIQ,YAAY,UAAU,WACpC,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B"}
1
+ {"version":3,"file":"oaiq.d.cts","names":[],"sources":["../../src/track/oaiq.ts"],"mappings":";;;qBAGa;;;;;YAiBD;EACV;EACA;EACA;EACA;;EAEA;EACA;;YAGU;EACV;;EAEA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;EACA,WAAW;;YAGD,YAAY,eAAe,qBAAqB,qBAAqB;;;;;YAMrE;EACV,uBAAuB;EACvB,kBAAkB;EAClB,iBAAiB;EACjB,aAAa;EACb,cAAc;EACd,eAAe;EACf,aAAa;EACb,wBAAwB;EACxB,sBAAsB;EACtB,eAAe;;YAGL,sBAAsB;;;;;;;;;wBAuBlB,mBAAmB;;;;;;YAevB;EACV;;EAEA;EACA;;EAEA;EACA;;EAEA;;EAEA;;EAEA;EACA;;;;;;wBAOc,kBAAkB;;wBASlB,iBAAiB;YAIrB;EACV;;EAEA;EACA,OAAO;;;;;;YAOG;;EAEV;;EAEA;;EAEA;;iBAGe;EACf,KAAK,iBAAiB,QAAQ;EAC9B,KAAK,UAAU,eACb,oBACA,OAAO,GACP,MAAM,eAAe,IACrB,UAAU;EAEZ,KACE,oBACA,iBACA,MAAM,iBACN,SAAS;IAAmB;;;;;;;;wBAoChB,aAAa,uBAAuB;wBAapC,YAAY,QAAQ,QAAQ,oBAAoB;YAqEpD;EACV,MAAM;EACN,MAAM;;;wBAIQ,YAAY,UAAU,WACpC,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B"}
@@ -59,20 +59,42 @@ export type StandardEvents = {
59
59
  };
60
60
  export type StandardEvent = keyof StandardEvents;
61
61
  /**
62
- * Identity fields passed to `oaiq("init", ...)` for conversion matching. Email and external id
63
- * must be pre-hashed as lowercase 64-char SHA-256 hex strings; the rest are sent as raw values.
64
- * https://developers.openai.com/ads/measurement-pixel
62
+ * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,
63
+ * starting and ending with a letter or digit, not a standard event name; the API lowercases it.
64
+ * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when
65
+ * nothing usable is left — the event is then not sent at all. The pixel and the server both name
66
+ * a custom event through this, so the two still deduplicate.
67
+ * https://developers.openai.com/ads/supported-events
68
+ */
69
+ export declare function oaiCustomEventName(name: string): string | undefined;
70
+ /**
71
+ * Identity fields passed to `oaiq("init", ...)` for conversion matching. Identifiers are
72
+ * normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.
73
+ * https://developers.openai.com/ads/measurement-pixel#send-user-data
65
74
  */
66
75
  export type OAIQUser = {
67
76
  email_sha256?: string;
77
+ /** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */
78
+ phone_number_sha256?: string;
68
79
  external_id_sha256?: string;
80
+ /** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */
81
+ first_name_sha256?: string;
82
+ last_name_sha256?: string;
69
83
  /** Two-letter ISO 3166-1 country code (e.g. "US"). */
70
84
  country?: string;
71
- /** Lowercased, max 128 characters. */
85
+ /** Max 128 characters; OpenAI trims and lowercases it. */
72
86
  city?: string;
73
- /** Max 32 characters. */
74
- zip_code?: string;
87
+ /** State, province or region, max 128 characters. */
88
+ region?: string;
89
+ postal_code?: string;
75
90
  };
91
+ /**
92
+ * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then
93
+ * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.
94
+ */
95
+ export declare function normalizeOAIPhone(phone: string): string | undefined;
96
+ /** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */
97
+ export declare function normalizeOAIName(name: string | undefined): string | undefined;
76
98
  export type InitConfig = {
77
99
  pixelId: string;
78
100
  /** Log SDK activity to the browser console. */
@@ -98,7 +120,11 @@ export interface OAIQ {
98
120
  custom_event_name: string;
99
121
  }): void;
100
122
  }
101
- /** Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). */
123
+ /**
124
+ * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None
125
+ * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every
126
+ * `amount` — an amount alone would be invalid, not just imprecise.
127
+ */
102
128
  export declare function toMinorUnits(value?: number | null, currency?: string): number | undefined;
103
129
  export declare function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined;
104
130
  export type MappedOAIEvent = {