@shware/analytics 11.1.0 → 11.3.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 (56) hide show
  1. package/README.md +9 -5
  2. package/dist/index.cjs +4 -2
  3. package/dist/index.d.cts +3 -2
  4. package/dist/index.d.mts +3 -2
  5. package/dist/index.mjs +3 -2
  6. package/dist/server/openai-conversions-api.cjs +2 -2
  7. package/dist/server/openai-conversions-api.cjs.map +1 -1
  8. package/dist/server/openai-conversions-api.d.cts.map +1 -1
  9. package/dist/server/openai-conversions-api.d.mts.map +1 -1
  10. package/dist/server/openai-conversions-api.mjs +3 -3
  11. package/dist/server/openai-conversions-api.mjs.map +1 -1
  12. package/dist/third-parties/ignored-events.cjs +7 -1
  13. package/dist/third-parties/ignored-events.cjs.map +1 -1
  14. package/dist/third-parties/ignored-events.d.cts.map +1 -1
  15. package/dist/third-parties/ignored-events.d.mts.map +1 -1
  16. package/dist/third-parties/ignored-events.mjs +7 -1
  17. package/dist/third-parties/ignored-events.mjs.map +1 -1
  18. package/dist/third-parties/microsoft-uet.cjs +2 -2
  19. package/dist/third-parties/microsoft-uet.cjs.map +1 -1
  20. package/dist/third-parties/microsoft-uet.mjs +1 -1
  21. package/dist/third-parties/microsoft-uet.mjs.map +1 -1
  22. package/dist/third-parties/openai-pixel.cjs +1 -0
  23. package/dist/third-parties/openai-pixel.cjs.map +1 -1
  24. package/dist/third-parties/openai-pixel.d.cts.map +1 -1
  25. package/dist/third-parties/openai-pixel.d.mts.map +1 -1
  26. package/dist/third-parties/openai-pixel.mjs +2 -1
  27. package/dist/third-parties/openai-pixel.mjs.map +1 -1
  28. package/dist/track/index.cjs +4 -4
  29. package/dist/track/index.cjs.map +1 -1
  30. package/dist/track/index.mjs +1 -1
  31. package/dist/track/index.mjs.map +1 -1
  32. package/dist/track/oaiq.cjs +17 -0
  33. package/dist/track/oaiq.cjs.map +1 -1
  34. package/dist/track/oaiq.d.cts +8 -0
  35. package/dist/track/oaiq.d.cts.map +1 -1
  36. package/dist/track/oaiq.d.mts +8 -0
  37. package/dist/track/oaiq.d.mts.map +1 -1
  38. package/dist/track/oaiq.mjs +17 -1
  39. package/dist/track/oaiq.mjs.map +1 -1
  40. package/dist/visitor/id.cjs +25 -0
  41. package/dist/visitor/id.cjs.map +1 -0
  42. package/dist/visitor/id.d.cts +13 -0
  43. package/dist/visitor/id.d.cts.map +1 -0
  44. package/dist/visitor/id.d.mts +13 -0
  45. package/dist/visitor/id.d.mts.map +1 -0
  46. package/dist/visitor/id.mjs +24 -0
  47. package/dist/visitor/id.mjs.map +1 -0
  48. package/dist/visitor/index.cjs +24 -29
  49. package/dist/visitor/index.cjs.map +1 -1
  50. package/dist/visitor/index.d.cts +15 -9
  51. package/dist/visitor/index.d.cts.map +1 -1
  52. package/dist/visitor/index.d.mts +15 -9
  53. package/dist/visitor/index.d.mts.map +1 -1
  54. package/dist/visitor/index.mjs +23 -29
  55. package/dist/visitor/index.mjs.map +1 -1
  56. package/package.json +1 -1
package/README.md CHANGED
@@ -85,11 +85,15 @@ the events are the visitor's record. The server:
85
85
  once per session is both enough and the right moment. `setVisitor` sends no tags (11.1): at
86
86
  sign-in the page is the login page, and its tags would overwrite the landing's. Clients before
87
87
  11.1 still send tags on their PATCH; merge them when present.
88
- - **does not create it on `PATCH /visitors/:id`** (`setVisitor`): that is a sign-in handing the
89
- user to the ad platforms' pixels and gtag, and by then the visitor's events have created it. On
90
- the rare page where a signed-in user's sign-in lands before the first batch of a new visitor id
91
- (storage cleared, the session kept), answer 404: `setVisitor` rejects, and the next page load
92
- sets the user.
88
+ - **binds it to the user from an `identify` event** (`IDENTIFY_EVENT`): `setVisitor` (11.2) sends no
89
+ request of its own — it hands the user to gtag and the pixels at once, and queues
90
+ `identify` with `{ user_id }` when the user differs from the one the page last identified. In
91
+ the batch's transaction, after creating the visitor, set its `user_id` and `distinct_id` to that
92
+ user (taking only a valid uuid, so a host's own event of that name binds nothing). Creation and
93
+ binding then travel in one request, in order: the binding can never arrive before the visitor.
94
+ `identify` is the SDK's: never forward it to the ad platforms or a CRM.
95
+ - **keeps `PATCH /visitors/:id`** for clients before 11.2, which bind the user there — answer 404
96
+ for a visitor no event has created, and merge the `tags` of clients before 11.1 when present.
93
97
  - **keeps `POST /visitors`** (`createVisitorSchema`) while clients before 11.0 are in use.
94
98
 
95
99
  Deploy the server first: a server that only creates visitors on `POST /visitors` rejects the
package/dist/index.cjs CHANGED
@@ -1,7 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_setup_index = require("./setup/index.cjs");
3
- const require_visitor_index = require("./visitor/index.cjs");
3
+ const require_visitor_id = require("./visitor/id.cjs");
4
4
  const require_track_index = require("./track/index.cjs");
5
+ const require_visitor_index = require("./visitor/index.cjs");
5
6
  const require_feedback_index = require("./feedback/index.cjs");
6
7
  const require_link_index = require("./link/index.cjs");
7
8
  const require_schema_index = require("./schema/index.cjs");
@@ -11,6 +12,7 @@ const require_hooks_use_track_impression = require("./hooks/use-track-impression
11
12
  exports.ALL_ENVIRONMENTS = require_schema_index.ALL_ENVIRONMENTS;
12
13
  exports.ALL_PLATFORMS = require_schema_index.ALL_PLATFORMS;
13
14
  exports.BOT_CATEGORIES = require_bot_index.BOT_CATEGORIES;
15
+ exports.IDENTIFY_EVENT = require_visitor_index.IDENTIFY_EVENT;
14
16
  exports.NOT_A_BOT = require_bot_index.NOT_A_BOT;
15
17
  exports.botOf = require_bot_index.botOf;
16
18
  exports.createFeedbackSchema = require_schema_index.createFeedbackSchema;
@@ -29,4 +31,4 @@ exports.track = require_track_index.track;
29
31
  exports.trackAsync = require_track_index.trackAsync;
30
32
  exports.updateVisitorSchema = require_schema_index.updateVisitorSchema;
31
33
  exports.useTrackImpression = require_hooks_use_track_impression.useTrackImpression;
32
- exports.visitorId = require_visitor_index.visitorId;
34
+ exports.visitorId = require_visitor_id.visitorId;
package/dist/index.d.cts CHANGED
@@ -6,7 +6,8 @@ import { useTrackImpression } from "./hooks/use-track-impression.cjs";
6
6
  import { VisitorProperties } from "./visitor/types.cjs";
7
7
  import { setupAnalytics } from "./setup/index.cjs";
8
8
  import { sendBeacon, sendPendingEvents, track, trackAsync } from "./track/index.cjs";
9
- import { setVisitor, visitorId } from "./visitor/index.cjs";
9
+ import { visitorId } from "./visitor/id.cjs";
10
+ import { IDENTIFY_EVENT, setVisitor } from "./visitor/index.cjs";
10
11
  import { Link, createLink, getLink } from "./link/index.cjs";
11
12
  import { stripeMinorUnits } from "./utils/stripe.cjs";
12
- export { ALL_ENVIRONMENTS, ALL_PLATFORMS, type AllowedPropertyValues, BOT_CATEGORIES, type Bot, type BotCategory, type CreateFeedbackDTO, type CreateLinkDTO, type CreateTrackEventDTO, type CreateVisitorDTO, type CustomEventProperties, type Environment, type Link, NOT_A_BOT, type Platform, type TrackProperties, type TrackTags, type UTMParams, type UpdateVisitorDTO, type UserProvidedData, type VisitorProperties, botOf, createFeedbackSchema, createLink, createLinkSchema, createTrackEventSchema, createVisitorSchema, getLink, sendBeacon, sendFeedback, sendPendingEvents, setVisitor, setupAnalytics, stripeMinorUnits, track, trackAsync, updateVisitorSchema, useTrackImpression, visitorId };
13
+ export { ALL_ENVIRONMENTS, ALL_PLATFORMS, type AllowedPropertyValues, BOT_CATEGORIES, type Bot, type BotCategory, type CreateFeedbackDTO, type CreateLinkDTO, type CreateTrackEventDTO, type CreateVisitorDTO, type CustomEventProperties, type Environment, IDENTIFY_EVENT, type Link, NOT_A_BOT, type Platform, type TrackProperties, type TrackTags, type UTMParams, type UpdateVisitorDTO, type UserProvidedData, type VisitorProperties, botOf, createFeedbackSchema, createLink, createLinkSchema, createTrackEventSchema, createVisitorSchema, getLink, sendBeacon, sendFeedback, sendPendingEvents, setVisitor, setupAnalytics, stripeMinorUnits, track, trackAsync, updateVisitorSchema, useTrackImpression, visitorId };
package/dist/index.d.mts CHANGED
@@ -6,7 +6,8 @@ import { useTrackImpression } from "./hooks/use-track-impression.mjs";
6
6
  import { VisitorProperties } from "./visitor/types.mjs";
7
7
  import { setupAnalytics } from "./setup/index.mjs";
8
8
  import { sendBeacon, sendPendingEvents, track, trackAsync } from "./track/index.mjs";
9
- import { setVisitor, visitorId } from "./visitor/index.mjs";
9
+ import { visitorId } from "./visitor/id.mjs";
10
+ import { IDENTIFY_EVENT, setVisitor } from "./visitor/index.mjs";
10
11
  import { Link, createLink, getLink } from "./link/index.mjs";
11
12
  import { stripeMinorUnits } from "./utils/stripe.mjs";
12
- export { ALL_ENVIRONMENTS, ALL_PLATFORMS, type AllowedPropertyValues, BOT_CATEGORIES, type Bot, type BotCategory, type CreateFeedbackDTO, type CreateLinkDTO, type CreateTrackEventDTO, type CreateVisitorDTO, type CustomEventProperties, type Environment, type Link, NOT_A_BOT, type Platform, type TrackProperties, type TrackTags, type UTMParams, type UpdateVisitorDTO, type UserProvidedData, type VisitorProperties, botOf, createFeedbackSchema, createLink, createLinkSchema, createTrackEventSchema, createVisitorSchema, getLink, sendBeacon, sendFeedback, sendPendingEvents, setVisitor, setupAnalytics, stripeMinorUnits, track, trackAsync, updateVisitorSchema, useTrackImpression, visitorId };
13
+ export { ALL_ENVIRONMENTS, ALL_PLATFORMS, type AllowedPropertyValues, BOT_CATEGORIES, type Bot, type BotCategory, type CreateFeedbackDTO, type CreateLinkDTO, type CreateTrackEventDTO, type CreateVisitorDTO, type CustomEventProperties, type Environment, IDENTIFY_EVENT, type Link, NOT_A_BOT, type Platform, type TrackProperties, type TrackTags, type UTMParams, type UpdateVisitorDTO, type UserProvidedData, type VisitorProperties, botOf, createFeedbackSchema, createLink, createLinkSchema, createTrackEventSchema, createVisitorSchema, getLink, sendBeacon, sendFeedback, sendPendingEvents, setVisitor, setupAnalytics, stripeMinorUnits, track, trackAsync, updateVisitorSchema, useTrackImpression, visitorId };
package/dist/index.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  import { setupAnalytics } from "./setup/index.mjs";
2
- import { setVisitor, visitorId } from "./visitor/index.mjs";
2
+ import { visitorId } from "./visitor/id.mjs";
3
3
  import { sendBeacon, sendPendingEvents, track, trackAsync } from "./track/index.mjs";
4
+ import { IDENTIFY_EVENT, setVisitor } from "./visitor/index.mjs";
4
5
  import { sendFeedback } from "./feedback/index.mjs";
5
6
  import { createLink, getLink } from "./link/index.mjs";
6
7
  import { ALL_ENVIRONMENTS, ALL_PLATFORMS, createFeedbackSchema, createLinkSchema, createTrackEventSchema, createVisitorSchema, updateVisitorSchema } from "./schema/index.mjs";
7
8
  import { BOT_CATEGORIES, NOT_A_BOT, botOf } from "./bot/index.mjs";
8
9
  import { stripeMinorUnits } from "./utils/stripe.mjs";
9
10
  import { useTrackImpression } from "./hooks/use-track-impression.mjs";
10
- export { ALL_ENVIRONMENTS, ALL_PLATFORMS, BOT_CATEGORIES, NOT_A_BOT, botOf, createFeedbackSchema, createLink, createLinkSchema, createTrackEventSchema, createVisitorSchema, getLink, sendBeacon, sendFeedback, sendPendingEvents, setVisitor, setupAnalytics, stripeMinorUnits, track, trackAsync, updateVisitorSchema, useTrackImpression, visitorId };
11
+ export { ALL_ENVIRONMENTS, ALL_PLATFORMS, BOT_CATEGORIES, IDENTIFY_EVENT, NOT_A_BOT, botOf, createFeedbackSchema, createLink, createLinkSchema, createTrackEventSchema, createVisitorSchema, getLink, sendBeacon, sendFeedback, sendPendingEvents, setVisitor, setupAnalytics, stripeMinorUnits, track, trackAsync, updateVisitorSchema, useTrackImpression, visitorId };
@@ -75,10 +75,10 @@ const MAX_EVENT_AGE_MS = 60474e4;
75
75
  async function sendEvents(apiKey, pixelId, events, data = {}, validateOnly = false, actionSource) {
76
76
  const dto = {
77
77
  validate_only: validateOnly,
78
- events: events.filter((event) => !require_third_parties_ignored_events.IGNORED_EVENTS.includes(event.name)).filter((event) => !require_track_oaiq.NON_AD_EVENTS.includes(event.name)).filter((event) => {
78
+ events: events.filter((event) => event.name === "first_open" || !require_third_parties_ignored_events.IGNORED_EVENTS.includes(event.name)).filter((event) => !require_track_oaiq.NON_AD_EVENTS.includes(event.name)).filter((event) => {
79
79
  const age = Date.now() - Date.parse(event.created_at);
80
80
  return age <= MAX_EVENT_AGE_MS && age >= -6e5;
81
- }).map((event) => getServerEvent(event, data, actionSource)).filter((event) => event.type !== "custom" || event.custom_event_name !== void 0)
81
+ }).map((event) => getServerEvent(event, data, actionSource)).filter((event) => event.type !== "custom" || event.custom_event_name !== void 0).filter((event) => !require_track_oaiq.APP_EVENTS.includes(event.type) || event.action_source === "mobile_app")
82
82
  };
83
83
  if (dto.events.length === 0) return;
84
84
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"openai-conversions-api.cjs","names":["createHash","resolveActionSource","normalizeOAIPhone","normalizeOAIName","mapOAIEvent","oaiCustomEventName","pageLocation","openaiOppref","IGNORED_EVENTS","NON_AD_EVENTS","fetch"],"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 'node:crypto';\nimport { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport {\n type EventData,\n NON_AD_EVENTS,\n mapOAIEvent,\n normalizeOAIName,\n normalizeOAIPhone,\n oaiCustomEventName,\n} from '../track/oaiq';\nimport type { Platform, TrackEvent, UserProvidedData } from '../track/types';\nimport { type EventActionSource, resolveActionSource } from './action-source';\nimport { openaiOppref } from './click-ids';\nimport { pageLocation } from './page-location';\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 * Conversion-matching fields, all optional. Identifiers are normalized, then sent as lowercase\n * 64-char SHA-256 hex strings; geographic values are raw. Of each list the API uses the first\n * three valid, unique values.\n * ref: https://developers.openai.com/ads/conversions-api#send-user-data\n */\nexport interface OpenAIUser {\n /** The pixel's `__obref` cookie, unhashed; ties the server event to the pixel's browser. */\n obref?: string;\n emails_sha256?: string[];\n /** 8–15 digits with the country code, no leading `+` or zeroes. */\n phone_numbers_sha256?: string[];\n external_ids_sha256?: string[];\n first_names_sha256?: string[];\n last_names_sha256?: string[];\n regions?: string[];\n postal_codes?: string[];\n cities?: string[];\n /** Two-letter ISO 3166-1 country codes (e.g. \"US\"). */\n countries?: string[];\n /** Android GAID only; IDFA is not supported. */\n android_advertising_id?: 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(\n platform: Platform,\n actionSource?: EventActionSource\n): ActionSource | undefined {\n switch (resolveActionSource(platform, actionSource)) {\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 list<T>(field: T | T[] | undefined): T[] {\n if (!field) return [];\n return Array.isArray(field) ? field : [field];\n}\n\n/** The distinct non-empty values, or undefined when there are none. */\nfunction values(items: (string | undefined)[]): string[] | undefined {\n const result = [...new Set(items.filter((item): item is string => !!item))];\n return result.length > 0 ? result : undefined;\n}\n\nfunction getUser(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData\n): OpenAIUser | undefined {\n const hashed = (items: (string | undefined)[]) => values(items)?.map(sha256);\n const addresses = list(data.address);\n\n const user: OpenAIUser = {\n obref: event.tags.__obref,\n emails_sha256: hashed(list(data.email).map((email) => email.trim().toLowerCase())),\n phone_numbers_sha256: hashed(list(data.phone_number).map(normalizeOAIPhone)),\n external_ids_sha256: hashed([data.user_id?.trim()]),\n first_names_sha256: hashed(addresses.map((a) => normalizeOAIName(a.first_name))),\n last_names_sha256: hashed(addresses.map((a) => normalizeOAIName(a.last_name))),\n regions: values(addresses.map((a) => a.region?.trim())),\n postal_codes: values(addresses.map((a) => a.postal_code?.trim())),\n cities: values(addresses.map((a) => a.city?.trim())),\n countries: values(addresses.map((a) => a.country?.trim().toUpperCase())),\n android_advertising_id:\n event.platform === 'android' ? event.tags.advertising_id || undefined : undefined,\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 actionSource?: EventActionSource\n): OpenAIEvent {\n const { type, data: eventData } = mapOAIEvent(event.name, event.properties);\n\n return {\n id: event.tags.idempotency_key ?? event.id,\n type,\n // For custom events the track name, made valid (`oaiCustomEventName`); the browser pixel\n // names it the same way, so the two deduplicate. Standard events omit it.\n custom_event_name: type === 'custom' ? oaiCustomEventName(event.name) : undefined,\n timestamp_ms: new Date(event.created_at).getTime(),\n source_url: pageLocation(event.tags),\n // The click id OpenAI appends to an ad's landing URL; what ties the conversion to the click.\n oppref: openaiOppref(event.tags),\n action_source: mapActionSource(event.platform, actionSource),\n user: getUser(event, data),\n data: eventData,\n };\n}\n\n/**\n * `timestamp_ms` \"must be within the last 7 days and no more than 10 minutes in the future\", or\n * the request fails; such an event is left out, with a minute's margin for the trip.\n */\nconst MAX_EVENT_AGE_MS = 7 * 24 * 60 * 60 * 1000 - 60 * 1000;\nconst MAX_EVENT_AHEAD_MS = 10 * 60 * 1000;\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 actionSource?: EventActionSource\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 .filter((event) => {\n const age = Date.now() - Date.parse(event.created_at);\n return age <= MAX_EVENT_AGE_MS && age >= -MAX_EVENT_AHEAD_MS;\n })\n .map((event) => getServerEvent(event, data, actionSource))\n // A custom event whose name cannot be made valid would fail the whole request.\n .filter((event) => event.type !== 'custom' || event.custom_event_name !== undefined),\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":"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"}
1
+ {"version":3,"file":"openai-conversions-api.cjs","names":["createHash","resolveActionSource","normalizeOAIPhone","normalizeOAIName","mapOAIEvent","oaiCustomEventName","pageLocation","openaiOppref","IGNORED_EVENTS","NON_AD_EVENTS","APP_EVENTS","fetch"],"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 'node:crypto';\nimport { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport {\n APP_EVENTS,\n type EventData,\n NON_AD_EVENTS,\n mapOAIEvent,\n normalizeOAIName,\n normalizeOAIPhone,\n oaiCustomEventName,\n} from '../track/oaiq';\nimport type { Platform, TrackEvent, UserProvidedData } from '../track/types';\nimport { type EventActionSource, resolveActionSource } from './action-source';\nimport { openaiOppref } from './click-ids';\nimport { pageLocation } from './page-location';\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 * Conversion-matching fields, all optional. Identifiers are normalized, then sent as lowercase\n * 64-char SHA-256 hex strings; geographic values are raw. Of each list the API uses the first\n * three valid, unique values.\n * ref: https://developers.openai.com/ads/conversions-api#send-user-data\n */\nexport interface OpenAIUser {\n /** The pixel's `__obref` cookie, unhashed; ties the server event to the pixel's browser. */\n obref?: string;\n emails_sha256?: string[];\n /** 8–15 digits with the country code, no leading `+` or zeroes. */\n phone_numbers_sha256?: string[];\n external_ids_sha256?: string[];\n first_names_sha256?: string[];\n last_names_sha256?: string[];\n regions?: string[];\n postal_codes?: string[];\n cities?: string[];\n /** Two-letter ISO 3166-1 country codes (e.g. \"US\"). */\n countries?: string[];\n /** Android GAID only; IDFA is not supported. */\n android_advertising_id?: 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(\n platform: Platform,\n actionSource?: EventActionSource\n): ActionSource | undefined {\n switch (resolveActionSource(platform, actionSource)) {\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 list<T>(field: T | T[] | undefined): T[] {\n if (!field) return [];\n return Array.isArray(field) ? field : [field];\n}\n\n/** The distinct non-empty values, or undefined when there are none. */\nfunction values(items: (string | undefined)[]): string[] | undefined {\n const result = [...new Set(items.filter((item): item is string => !!item))];\n return result.length > 0 ? result : undefined;\n}\n\nfunction getUser(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData\n): OpenAIUser | undefined {\n const hashed = (items: (string | undefined)[]) => values(items)?.map(sha256);\n const addresses = list(data.address);\n\n const user: OpenAIUser = {\n obref: event.tags.__obref,\n emails_sha256: hashed(list(data.email).map((email) => email.trim().toLowerCase())),\n phone_numbers_sha256: hashed(list(data.phone_number).map(normalizeOAIPhone)),\n external_ids_sha256: hashed([data.user_id?.trim()]),\n first_names_sha256: hashed(addresses.map((a) => normalizeOAIName(a.first_name))),\n last_names_sha256: hashed(addresses.map((a) => normalizeOAIName(a.last_name))),\n regions: values(addresses.map((a) => a.region?.trim())),\n postal_codes: values(addresses.map((a) => a.postal_code?.trim())),\n cities: values(addresses.map((a) => a.city?.trim())),\n countries: values(addresses.map((a) => a.country?.trim().toUpperCase())),\n android_advertising_id:\n event.platform === 'android' ? event.tags.advertising_id || undefined : undefined,\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 actionSource?: EventActionSource\n): OpenAIEvent {\n const { type, data: eventData } = mapOAIEvent(event.name, event.properties);\n\n return {\n id: event.tags.idempotency_key ?? event.id,\n type,\n // For custom events the track name, made valid (`oaiCustomEventName`); the browser pixel\n // names it the same way, so the two deduplicate. Standard events omit it.\n custom_event_name: type === 'custom' ? oaiCustomEventName(event.name) : undefined,\n timestamp_ms: new Date(event.created_at).getTime(),\n source_url: pageLocation(event.tags),\n // The click id OpenAI appends to an ad's landing URL; what ties the conversion to the click.\n oppref: openaiOppref(event.tags),\n action_source: mapActionSource(event.platform, actionSource),\n user: getUser(event, data),\n data: eventData,\n };\n}\n\n/**\n * `timestamp_ms` \"must be within the last 7 days and no more than 10 minutes in the future\", or\n * the request fails; such an event is left out, with a minute's margin for the trip.\n */\nconst MAX_EVENT_AGE_MS = 7 * 24 * 60 * 60 * 1000 - 60 * 1000;\nconst MAX_EVENT_AHEAD_MS = 10 * 60 * 1000;\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 actionSource?: EventActionSource\n) {\n const dto: CreateOpenAIEventsDTO = {\n validate_only: validateOnly,\n events: events\n // `first_open` is the install: left out of what GA collects itself, not of what OpenAI takes.\n .filter((event) => event.name === 'first_open' || !IGNORED_EVENTS.includes(event.name))\n .filter((event) => !NON_AD_EVENTS.includes(event.name))\n .filter((event) => {\n const age = Date.now() - Date.parse(event.created_at);\n return age <= MAX_EVENT_AGE_MS && age >= -MAX_EVENT_AHEAD_MS;\n })\n .map((event) => getServerEvent(event, data, actionSource))\n // A custom event whose name cannot be made valid would fail the whole request.\n .filter((event) => event.type !== 'custom' || event.custom_event_name !== undefined)\n // The app events count only from an app: OpenAI takes them with `mobile_app` alone.\n .filter(\n (event) => !APP_EVENTS.includes(event.type as never) || event.action_source === 'mobile_app'\n ),\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":";;;;;;;;;;;;;;AAsBA,MAAM,WAAW;AA+DjB,SAAS,OAAO,OAAuB;CACrC,QAAA,GAAOA,YAAAA,WAAAA,CAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK;AACxD;AAEA,SAAS,gBACP,UACA,cAC0B;CAC1B,QAAQC,6BAAAA,oBAAoB,UAAU,YAAY,GAAlD;EACE,KAAK,OACH,OAAO;EACT,KAAK,OACH,OAAO;EACT,KAAK,WACH,OAAO;EACT,SACE;CACJ;AACF;AAEA,SAAS,KAAQ,OAAiC;CAChD,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AAC9C;;AAGA,SAAS,OAAO,OAAqD;CACnE,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,MAAM,QAAQ,SAAyB,CAAC,CAAC,IAAI,CAAC,CAAC;CAC1E,OAAO,OAAO,SAAS,IAAI,SAAS,KAAA;AACtC;AAEA,SAAS,QAEP,OACA,MACwB;CACxB,MAAM,UAAU,UAAkC,OAAO,KAAK,CAAC,EAAE,IAAI,MAAM;CAC3E,MAAM,YAAY,KAAK,KAAK,OAAO;CAEnC,MAAM,OAAmB;EACvB,OAAO,MAAM,KAAK;EAClB,eAAe,OAAO,KAAK,KAAK,KAAK,CAAC,CAAC,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC;EACjF,sBAAsB,OAAO,KAAK,KAAK,YAAY,CAAC,CAAC,IAAIC,mBAAAA,iBAAiB,CAAC;EAC3E,qBAAqB,OAAO,CAAC,KAAK,SAAS,KAAK,CAAC,CAAC;EAClD,oBAAoB,OAAO,UAAU,KAAK,MAAMC,mBAAAA,iBAAiB,EAAE,UAAU,CAAC,CAAC;EAC/E,mBAAmB,OAAO,UAAU,KAAK,MAAMA,mBAAAA,iBAAiB,EAAE,SAAS,CAAC,CAAC;EAC7E,SAAS,OAAO,UAAU,KAAK,MAAM,EAAE,QAAQ,KAAK,CAAC,CAAC;EACtD,cAAc,OAAO,UAAU,KAAK,MAAM,EAAE,aAAa,KAAK,CAAC,CAAC;EAChE,QAAQ,OAAO,UAAU,KAAK,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC;EACnD,WAAW,OAAO,UAAU,KAAK,MAAM,EAAE,SAAS,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC;EACvE,wBACE,MAAM,aAAa,YAAY,MAAM,KAAK,kBAAkB,KAAA,IAAY,KAAA;EAC1E,YAAY,KAAK;EACjB,YAAY,KAAK;CACnB;CAEA,OAAO,OAAO,OAAO,IAAI,CAAC,CAAC,MAAM,UAAU,UAAU,KAAA,CAAS,IAAI,OAAO,KAAA;AAC3E;AAEA,SAAgB,eAEd,OACA,MACA,cACa;CACb,MAAM,EAAE,MAAM,MAAM,cAAcC,mBAAAA,YAAY,MAAM,MAAM,MAAM,UAAU;CAE1E,OAAO;EACL,IAAI,MAAM,KAAK,mBAAmB,MAAM;EACxC;EAGA,mBAAmB,SAAS,WAAWC,mBAAAA,mBAAmB,MAAM,IAAI,IAAI,KAAA;EACxE,cAAc,IAAI,KAAK,MAAM,UAAU,CAAC,CAAC,QAAQ;EACjD,YAAYC,6BAAAA,aAAa,MAAM,IAAI;EAEnC,QAAQC,yBAAAA,aAAa,MAAM,IAAI;EAC/B,eAAe,gBAAgB,MAAM,UAAU,YAAY;EAC3D,MAAM,QAAQ,OAAO,IAAI;EACzB,MAAM;CACR;AACF;;;;;AAMA,MAAM,mBAAmB;AAGzB,eAAsB,WACpB,QACA,SAEA,QACA,OAAyB,CAAC,GAC1B,eAAe,OACf,cACA;CACA,MAAM,MAA6B;EACjC,eAAe;EACf,QAAQ,OAEL,QAAQ,UAAU,MAAM,SAAS,gBAAgB,CAACC,qCAAAA,eAAe,SAAS,MAAM,IAAI,CAAC,CAAC,CACtF,QAAQ,UAAU,CAACC,mBAAAA,cAAc,SAAS,MAAM,IAAI,CAAC,CAAC,CACtD,QAAQ,UAAU;GACjB,MAAM,MAAM,KAAK,IAAI,IAAI,KAAK,MAAM,MAAM,UAAU;GACpD,OAAO,OAAO,oBAAoB,OAAO;EAC3C,CAAC,CAAC,CACD,KAAK,UAAU,eAAe,OAAO,MAAM,YAAY,CAAC,CAAC,CAEzD,QAAQ,UAAU,MAAM,SAAS,YAAY,MAAM,sBAAsB,KAAA,CAAS,CAAC,CAEnF,QACE,UAAU,CAACC,mBAAAA,WAAW,SAAS,MAAM,IAAa,KAAK,MAAM,kBAAkB,YAClF;CACJ;CAEA,IAAI,IAAI,OAAO,WAAW,GAAG;CAE7B,IAAI;EACF,MAAM,WAAW,OAAA,GAAMC,cAAAA,MAAAA,CAAM,GAAG,SAAS,OAAO,WAAW;GACzD,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,QAAQ;IACR,eAAe,UAAU;GAC3B;GACA,MAAM,KAAK,UAAU,GAAG;EAC1B,CAAC;EAED,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 +1 @@
1
- {"version":3,"file":"openai-conversions-api.d.cts","names":[],"sources":["../../src/server/openai-conversions-api.ts"],"mappings":";;;;KAuBK;;;;;;;iBAeY;;EAEf;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;EACA;EACA;;iBAGe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;EACA,gBAAgB;;EAEhB;;EAEA;EACA,OAAO;EACP,MAAM;;iBAGS;;EAEf;EACA,QAAQ;;wBA8DM,eAEd,OAAO,iBACP,MAAM,kBACN,eAAe,oBACd;wBA0BmB,WACpB,gBACA,iBAEA,QAAQ,mBACR,OAAM,kBACN,wBACA,eAAe,oBAAiB"}
1
+ {"version":3,"file":"openai-conversions-api.d.cts","names":[],"sources":["../../src/server/openai-conversions-api.ts"],"mappings":";;;;KAwBK;;;;;;;iBAeY;;EAEf;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;EACA;EACA;;iBAGe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;EACA,gBAAgB;;EAEhB;;EAEA;EACA,OAAO;EACP,MAAM;;iBAGS;;EAEf;EACA,QAAQ;;wBA8DM,eAEd,OAAO,iBACP,MAAM,kBACN,eAAe,oBACd;wBA0BmB,WACpB,gBACA,iBAEA,QAAQ,mBACR,OAAM,kBACN,wBACA,eAAe,oBAAiB"}
@@ -1 +1 @@
1
- {"version":3,"file":"openai-conversions-api.d.mts","names":[],"sources":["../../src/server/openai-conversions-api.ts"],"mappings":";;;;KAuBK;;;;;;;iBAeY;;EAEf;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;EACA;EACA;;iBAGe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;EACA,gBAAgB;;EAEhB;;EAEA;EACA,OAAO;EACP,MAAM;;iBAGS;;EAEf;EACA,QAAQ;;wBA8DM,eAEd,OAAO,iBACP,MAAM,kBACN,eAAe,oBACd;wBA0BmB,WACpB,gBACA,iBAEA,QAAQ,mBACR,OAAM,kBACN,wBACA,eAAe,oBAAiB"}
1
+ {"version":3,"file":"openai-conversions-api.d.mts","names":[],"sources":["../../src/server/openai-conversions-api.ts"],"mappings":";;;;KAwBK;;;;;;;iBAeY;;EAEf;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;EACA;EACA;;iBAGe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;EACA,gBAAgB;;EAEhB;;EAEA;EACA,OAAO;EACP,MAAM;;iBAGS;;EAEf;EACA,QAAQ;;wBA8DM,eAEd,OAAO,iBACP,MAAM,kBACN,eAAe,oBACd;wBA0BmB,WACpB,gBACA,iBAEA,QAAQ,mBACR,OAAM,kBACN,wBACA,eAAe,oBAAiB"}
@@ -2,7 +2,7 @@ import { IGNORED_EVENTS } from "../third-parties/ignored-events.mjs";
2
2
  import { resolveActionSource } from "./action-source.mjs";
3
3
  import { openaiOppref } from "./click-ids.mjs";
4
4
  import { pageLocation } from "./page-location.mjs";
5
- import { NON_AD_EVENTS, mapOAIEvent, normalizeOAIName, normalizeOAIPhone, oaiCustomEventName } from "../track/oaiq.mjs";
5
+ import { APP_EVENTS, NON_AD_EVENTS, mapOAIEvent, normalizeOAIName, normalizeOAIPhone, oaiCustomEventName } from "../track/oaiq.mjs";
6
6
  import { fetch } from "@shware/utils";
7
7
  import { createHash } from "node:crypto";
8
8
  //#region src/server/openai-conversions-api.ts
@@ -74,10 +74,10 @@ const MAX_EVENT_AGE_MS = 60474e4;
74
74
  async function sendEvents(apiKey, pixelId, events, data = {}, validateOnly = false, actionSource) {
75
75
  const dto = {
76
76
  validate_only: validateOnly,
77
- events: events.filter((event) => !IGNORED_EVENTS.includes(event.name)).filter((event) => !NON_AD_EVENTS.includes(event.name)).filter((event) => {
77
+ events: events.filter((event) => event.name === "first_open" || !IGNORED_EVENTS.includes(event.name)).filter((event) => !NON_AD_EVENTS.includes(event.name)).filter((event) => {
78
78
  const age = Date.now() - Date.parse(event.created_at);
79
79
  return age <= MAX_EVENT_AGE_MS && age >= -6e5;
80
- }).map((event) => getServerEvent(event, data, actionSource)).filter((event) => event.type !== "custom" || event.custom_event_name !== void 0)
80
+ }).map((event) => getServerEvent(event, data, actionSource)).filter((event) => event.type !== "custom" || event.custom_event_name !== void 0).filter((event) => !APP_EVENTS.includes(event.type) || event.action_source === "mobile_app")
81
81
  };
82
82
  if (dto.events.length === 0) return;
83
83
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"openai-conversions-api.mjs","names":[],"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 'node:crypto';\nimport { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport {\n type EventData,\n NON_AD_EVENTS,\n mapOAIEvent,\n normalizeOAIName,\n normalizeOAIPhone,\n oaiCustomEventName,\n} from '../track/oaiq';\nimport type { Platform, TrackEvent, UserProvidedData } from '../track/types';\nimport { type EventActionSource, resolveActionSource } from './action-source';\nimport { openaiOppref } from './click-ids';\nimport { pageLocation } from './page-location';\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 * Conversion-matching fields, all optional. Identifiers are normalized, then sent as lowercase\n * 64-char SHA-256 hex strings; geographic values are raw. Of each list the API uses the first\n * three valid, unique values.\n * ref: https://developers.openai.com/ads/conversions-api#send-user-data\n */\nexport interface OpenAIUser {\n /** The pixel's `__obref` cookie, unhashed; ties the server event to the pixel's browser. */\n obref?: string;\n emails_sha256?: string[];\n /** 8–15 digits with the country code, no leading `+` or zeroes. */\n phone_numbers_sha256?: string[];\n external_ids_sha256?: string[];\n first_names_sha256?: string[];\n last_names_sha256?: string[];\n regions?: string[];\n postal_codes?: string[];\n cities?: string[];\n /** Two-letter ISO 3166-1 country codes (e.g. \"US\"). */\n countries?: string[];\n /** Android GAID only; IDFA is not supported. */\n android_advertising_id?: 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(\n platform: Platform,\n actionSource?: EventActionSource\n): ActionSource | undefined {\n switch (resolveActionSource(platform, actionSource)) {\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 list<T>(field: T | T[] | undefined): T[] {\n if (!field) return [];\n return Array.isArray(field) ? field : [field];\n}\n\n/** The distinct non-empty values, or undefined when there are none. */\nfunction values(items: (string | undefined)[]): string[] | undefined {\n const result = [...new Set(items.filter((item): item is string => !!item))];\n return result.length > 0 ? result : undefined;\n}\n\nfunction getUser(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData\n): OpenAIUser | undefined {\n const hashed = (items: (string | undefined)[]) => values(items)?.map(sha256);\n const addresses = list(data.address);\n\n const user: OpenAIUser = {\n obref: event.tags.__obref,\n emails_sha256: hashed(list(data.email).map((email) => email.trim().toLowerCase())),\n phone_numbers_sha256: hashed(list(data.phone_number).map(normalizeOAIPhone)),\n external_ids_sha256: hashed([data.user_id?.trim()]),\n first_names_sha256: hashed(addresses.map((a) => normalizeOAIName(a.first_name))),\n last_names_sha256: hashed(addresses.map((a) => normalizeOAIName(a.last_name))),\n regions: values(addresses.map((a) => a.region?.trim())),\n postal_codes: values(addresses.map((a) => a.postal_code?.trim())),\n cities: values(addresses.map((a) => a.city?.trim())),\n countries: values(addresses.map((a) => a.country?.trim().toUpperCase())),\n android_advertising_id:\n event.platform === 'android' ? event.tags.advertising_id || undefined : undefined,\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 actionSource?: EventActionSource\n): OpenAIEvent {\n const { type, data: eventData } = mapOAIEvent(event.name, event.properties);\n\n return {\n id: event.tags.idempotency_key ?? event.id,\n type,\n // For custom events the track name, made valid (`oaiCustomEventName`); the browser pixel\n // names it the same way, so the two deduplicate. Standard events omit it.\n custom_event_name: type === 'custom' ? oaiCustomEventName(event.name) : undefined,\n timestamp_ms: new Date(event.created_at).getTime(),\n source_url: pageLocation(event.tags),\n // The click id OpenAI appends to an ad's landing URL; what ties the conversion to the click.\n oppref: openaiOppref(event.tags),\n action_source: mapActionSource(event.platform, actionSource),\n user: getUser(event, data),\n data: eventData,\n };\n}\n\n/**\n * `timestamp_ms` \"must be within the last 7 days and no more than 10 minutes in the future\", or\n * the request fails; such an event is left out, with a minute's margin for the trip.\n */\nconst MAX_EVENT_AGE_MS = 7 * 24 * 60 * 60 * 1000 - 60 * 1000;\nconst MAX_EVENT_AHEAD_MS = 10 * 60 * 1000;\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 actionSource?: EventActionSource\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 .filter((event) => {\n const age = Date.now() - Date.parse(event.created_at);\n return age <= MAX_EVENT_AGE_MS && age >= -MAX_EVENT_AHEAD_MS;\n })\n .map((event) => getServerEvent(event, data, actionSource))\n // A custom event whose name cannot be made valid would fail the whole request.\n .filter((event) => event.type !== 'custom' || event.custom_event_name !== undefined),\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":"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"}
1
+ {"version":3,"file":"openai-conversions-api.mjs","names":[],"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 'node:crypto';\nimport { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport {\n APP_EVENTS,\n type EventData,\n NON_AD_EVENTS,\n mapOAIEvent,\n normalizeOAIName,\n normalizeOAIPhone,\n oaiCustomEventName,\n} from '../track/oaiq';\nimport type { Platform, TrackEvent, UserProvidedData } from '../track/types';\nimport { type EventActionSource, resolveActionSource } from './action-source';\nimport { openaiOppref } from './click-ids';\nimport { pageLocation } from './page-location';\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 * Conversion-matching fields, all optional. Identifiers are normalized, then sent as lowercase\n * 64-char SHA-256 hex strings; geographic values are raw. Of each list the API uses the first\n * three valid, unique values.\n * ref: https://developers.openai.com/ads/conversions-api#send-user-data\n */\nexport interface OpenAIUser {\n /** The pixel's `__obref` cookie, unhashed; ties the server event to the pixel's browser. */\n obref?: string;\n emails_sha256?: string[];\n /** 8–15 digits with the country code, no leading `+` or zeroes. */\n phone_numbers_sha256?: string[];\n external_ids_sha256?: string[];\n first_names_sha256?: string[];\n last_names_sha256?: string[];\n regions?: string[];\n postal_codes?: string[];\n cities?: string[];\n /** Two-letter ISO 3166-1 country codes (e.g. \"US\"). */\n countries?: string[];\n /** Android GAID only; IDFA is not supported. */\n android_advertising_id?: 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(\n platform: Platform,\n actionSource?: EventActionSource\n): ActionSource | undefined {\n switch (resolveActionSource(platform, actionSource)) {\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 list<T>(field: T | T[] | undefined): T[] {\n if (!field) return [];\n return Array.isArray(field) ? field : [field];\n}\n\n/** The distinct non-empty values, or undefined when there are none. */\nfunction values(items: (string | undefined)[]): string[] | undefined {\n const result = [...new Set(items.filter((item): item is string => !!item))];\n return result.length > 0 ? result : undefined;\n}\n\nfunction getUser(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData\n): OpenAIUser | undefined {\n const hashed = (items: (string | undefined)[]) => values(items)?.map(sha256);\n const addresses = list(data.address);\n\n const user: OpenAIUser = {\n obref: event.tags.__obref,\n emails_sha256: hashed(list(data.email).map((email) => email.trim().toLowerCase())),\n phone_numbers_sha256: hashed(list(data.phone_number).map(normalizeOAIPhone)),\n external_ids_sha256: hashed([data.user_id?.trim()]),\n first_names_sha256: hashed(addresses.map((a) => normalizeOAIName(a.first_name))),\n last_names_sha256: hashed(addresses.map((a) => normalizeOAIName(a.last_name))),\n regions: values(addresses.map((a) => a.region?.trim())),\n postal_codes: values(addresses.map((a) => a.postal_code?.trim())),\n cities: values(addresses.map((a) => a.city?.trim())),\n countries: values(addresses.map((a) => a.country?.trim().toUpperCase())),\n android_advertising_id:\n event.platform === 'android' ? event.tags.advertising_id || undefined : undefined,\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 actionSource?: EventActionSource\n): OpenAIEvent {\n const { type, data: eventData } = mapOAIEvent(event.name, event.properties);\n\n return {\n id: event.tags.idempotency_key ?? event.id,\n type,\n // For custom events the track name, made valid (`oaiCustomEventName`); the browser pixel\n // names it the same way, so the two deduplicate. Standard events omit it.\n custom_event_name: type === 'custom' ? oaiCustomEventName(event.name) : undefined,\n timestamp_ms: new Date(event.created_at).getTime(),\n source_url: pageLocation(event.tags),\n // The click id OpenAI appends to an ad's landing URL; what ties the conversion to the click.\n oppref: openaiOppref(event.tags),\n action_source: mapActionSource(event.platform, actionSource),\n user: getUser(event, data),\n data: eventData,\n };\n}\n\n/**\n * `timestamp_ms` \"must be within the last 7 days and no more than 10 minutes in the future\", or\n * the request fails; such an event is left out, with a minute's margin for the trip.\n */\nconst MAX_EVENT_AGE_MS = 7 * 24 * 60 * 60 * 1000 - 60 * 1000;\nconst MAX_EVENT_AHEAD_MS = 10 * 60 * 1000;\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 actionSource?: EventActionSource\n) {\n const dto: CreateOpenAIEventsDTO = {\n validate_only: validateOnly,\n events: events\n // `first_open` is the install: left out of what GA collects itself, not of what OpenAI takes.\n .filter((event) => event.name === 'first_open' || !IGNORED_EVENTS.includes(event.name))\n .filter((event) => !NON_AD_EVENTS.includes(event.name))\n .filter((event) => {\n const age = Date.now() - Date.parse(event.created_at);\n return age <= MAX_EVENT_AGE_MS && age >= -MAX_EVENT_AHEAD_MS;\n })\n .map((event) => getServerEvent(event, data, actionSource))\n // A custom event whose name cannot be made valid would fail the whole request.\n .filter((event) => event.type !== 'custom' || event.custom_event_name !== undefined)\n // The app events count only from an app: OpenAI takes them with `mobile_app` alone.\n .filter(\n (event) => !APP_EVENTS.includes(event.type as never) || event.action_source === 'mobile_app'\n ),\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":"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"}
@@ -31,7 +31,13 @@ const AUTOMATICALLY_COLLECTED_EVENTS = [
31
31
  "session_start",
32
32
  "user_engagement"
33
33
  ];
34
- const IGNORED_EVENTS = [...METRICS_EVENTS, ...AUTOMATICALLY_COLLECTED_EVENTS];
34
+ /** The SDK's own: the server binds the visitor to the user from it; no third party has a use for it. */
35
+ const SDK_EVENTS = ["identify"];
36
+ const IGNORED_EVENTS = [
37
+ ...METRICS_EVENTS,
38
+ ...AUTOMATICALLY_COLLECTED_EVENTS,
39
+ ...SDK_EVENTS
40
+ ];
35
41
  //#endregion
36
42
  exports.IGNORED_EVENTS = IGNORED_EVENTS;
37
43
 
@@ -1 +1 @@
1
- {"version":3,"file":"ignored-events.cjs","names":[],"sources":["../../src/third-parties/ignored-events.ts"],"sourcesContent":["const METRICS_EVENTS = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/**\n * Automatically collected events: https://support.google.com/analytics/answer/9234069\n * Enhanced measurement events: https://support.google.com/analytics/answer/9216061\n */\nconst ENHANCED_MEASUREMENT_EVENTS = [\n // Page views\n 'page_view',\n // Scrolls: the first time a user reaches the bottom of each page\n // (i.e., when a 90% vertical depth becomes visible)\n 'scroll',\n // Outbound clicks\n 'click',\n // Site search\n 'view_search_results',\n // Video engagement\n 'video_start',\n 'video_progress',\n 'video_complete',\n // File downloads\n 'file_download',\n // Form interactions\n 'form_start',\n 'form_submit',\n];\n\nconst AUTOMATICALLY_COLLECTED_EVENTS = [\n ...ENHANCED_MEASUREMENT_EVENTS,\n 'first_visit',\n 'first_open',\n // ref: https://support.google.com/analytics/answer/9234069\n // 'in_app_purchase',\n 'notification_dismiss',\n 'notification_foreground',\n 'notification_open',\n 'notification_receive',\n 'notification_send',\n /**\n * Note: The 'screen_view' event is automatically sent only when using Android or iOS native SDKs.\n * If you are using React Native, you need to send this event manually; it should not be ignored.\n * */\n // 'screen_view',\n 'session_start',\n 'user_engagement',\n];\n\nexport const IGNORED_EVENTS = [...METRICS_EVENTS, ...AUTOMATICALLY_COLLECTED_EVENTS];\n"],"mappings":";;AAAA,MAAM,iBAAiB;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;AA2BjE,MAAM,iCAAiC;CACrC,GAAG;EApBH;EAGA;EAEA;EAEA;EAEA;EACA;EACA;EAEA;EAEA;EACA;CAI6B;CAC7B;CACA;CAGA;CACA;CACA;CACA;CACA;CAMA;CACA;AACF;AAEA,MAAa,iBAAiB,CAAC,GAAG,gBAAgB,GAAG,8BAA8B"}
1
+ {"version":3,"file":"ignored-events.cjs","names":[],"sources":["../../src/third-parties/ignored-events.ts"],"sourcesContent":["const METRICS_EVENTS = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/**\n * Automatically collected events: https://support.google.com/analytics/answer/9234069\n * Enhanced measurement events: https://support.google.com/analytics/answer/9216061\n */\nconst ENHANCED_MEASUREMENT_EVENTS = [\n // Page views\n 'page_view',\n // Scrolls: the first time a user reaches the bottom of each page\n // (i.e., when a 90% vertical depth becomes visible)\n 'scroll',\n // Outbound clicks\n 'click',\n // Site search\n 'view_search_results',\n // Video engagement\n 'video_start',\n 'video_progress',\n 'video_complete',\n // File downloads\n 'file_download',\n // Form interactions\n 'form_start',\n 'form_submit',\n];\n\nconst AUTOMATICALLY_COLLECTED_EVENTS = [\n ...ENHANCED_MEASUREMENT_EVENTS,\n 'first_visit',\n 'first_open',\n // ref: https://support.google.com/analytics/answer/9234069\n // 'in_app_purchase',\n 'notification_dismiss',\n 'notification_foreground',\n 'notification_open',\n 'notification_receive',\n 'notification_send',\n /**\n * Note: The 'screen_view' event is automatically sent only when using Android or iOS native SDKs.\n * If you are using React Native, you need to send this event manually; it should not be ignored.\n * */\n // 'screen_view',\n 'session_start',\n 'user_engagement',\n];\n\n/** The SDK's own: the server binds the visitor to the user from it; no third party has a use for it. */\nconst SDK_EVENTS = ['identify'];\n\nexport const IGNORED_EVENTS = [...METRICS_EVENTS, ...AUTOMATICALLY_COLLECTED_EVENTS, ...SDK_EVENTS];\n"],"mappings":";;AAAA,MAAM,iBAAiB;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;AA2BjE,MAAM,iCAAiC;CACrC,GAAG;EApBH;EAGA;EAEA;EAEA;EAEA;EACA;EACA;EAEA;EAEA;EACA;CAI6B;CAC7B;CACA;CAGA;CACA;CACA;CACA;CACA;CAMA;CACA;AACF;;AAGA,MAAM,aAAa,CAAC,UAAU;AAE9B,MAAa,iBAAiB;CAAC,GAAG;CAAgB,GAAG;CAAgC,GAAG;AAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"ignored-events.d.cts","names":[],"sources":["../../src/third-parties/ignored-events.ts"],"mappings":";qBA+Ca"}
1
+ {"version":3,"file":"ignored-events.d.cts","names":[],"sources":["../../src/third-parties/ignored-events.ts"],"mappings":";qBAkDa"}
@@ -1 +1 @@
1
- {"version":3,"file":"ignored-events.d.mts","names":[],"sources":["../../src/third-parties/ignored-events.ts"],"mappings":";qBA+Ca"}
1
+ {"version":3,"file":"ignored-events.d.mts","names":[],"sources":["../../src/third-parties/ignored-events.ts"],"mappings":";qBAkDa"}
@@ -30,7 +30,13 @@ const AUTOMATICALLY_COLLECTED_EVENTS = [
30
30
  "session_start",
31
31
  "user_engagement"
32
32
  ];
33
- const IGNORED_EVENTS = [...METRICS_EVENTS, ...AUTOMATICALLY_COLLECTED_EVENTS];
33
+ /** The SDK's own: the server binds the visitor to the user from it; no third party has a use for it. */
34
+ const SDK_EVENTS = ["identify"];
35
+ const IGNORED_EVENTS = [
36
+ ...METRICS_EVENTS,
37
+ ...AUTOMATICALLY_COLLECTED_EVENTS,
38
+ ...SDK_EVENTS
39
+ ];
34
40
  //#endregion
35
41
  export { IGNORED_EVENTS };
36
42
 
@@ -1 +1 @@
1
- {"version":3,"file":"ignored-events.mjs","names":[],"sources":["../../src/third-parties/ignored-events.ts"],"sourcesContent":["const METRICS_EVENTS = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/**\n * Automatically collected events: https://support.google.com/analytics/answer/9234069\n * Enhanced measurement events: https://support.google.com/analytics/answer/9216061\n */\nconst ENHANCED_MEASUREMENT_EVENTS = [\n // Page views\n 'page_view',\n // Scrolls: the first time a user reaches the bottom of each page\n // (i.e., when a 90% vertical depth becomes visible)\n 'scroll',\n // Outbound clicks\n 'click',\n // Site search\n 'view_search_results',\n // Video engagement\n 'video_start',\n 'video_progress',\n 'video_complete',\n // File downloads\n 'file_download',\n // Form interactions\n 'form_start',\n 'form_submit',\n];\n\nconst AUTOMATICALLY_COLLECTED_EVENTS = [\n ...ENHANCED_MEASUREMENT_EVENTS,\n 'first_visit',\n 'first_open',\n // ref: https://support.google.com/analytics/answer/9234069\n // 'in_app_purchase',\n 'notification_dismiss',\n 'notification_foreground',\n 'notification_open',\n 'notification_receive',\n 'notification_send',\n /**\n * Note: The 'screen_view' event is automatically sent only when using Android or iOS native SDKs.\n * If you are using React Native, you need to send this event manually; it should not be ignored.\n * */\n // 'screen_view',\n 'session_start',\n 'user_engagement',\n];\n\nexport const IGNORED_EVENTS = [...METRICS_EVENTS, ...AUTOMATICALLY_COLLECTED_EVENTS];\n"],"mappings":";AAAA,MAAM,iBAAiB;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;AA2BjE,MAAM,iCAAiC;CACrC,GAAG;EApBH;EAGA;EAEA;EAEA;EAEA;EACA;EACA;EAEA;EAEA;EACA;CAI6B;CAC7B;CACA;CAGA;CACA;CACA;CACA;CACA;CAMA;CACA;AACF;AAEA,MAAa,iBAAiB,CAAC,GAAG,gBAAgB,GAAG,8BAA8B"}
1
+ {"version":3,"file":"ignored-events.mjs","names":[],"sources":["../../src/third-parties/ignored-events.ts"],"sourcesContent":["const METRICS_EVENTS = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/**\n * Automatically collected events: https://support.google.com/analytics/answer/9234069\n * Enhanced measurement events: https://support.google.com/analytics/answer/9216061\n */\nconst ENHANCED_MEASUREMENT_EVENTS = [\n // Page views\n 'page_view',\n // Scrolls: the first time a user reaches the bottom of each page\n // (i.e., when a 90% vertical depth becomes visible)\n 'scroll',\n // Outbound clicks\n 'click',\n // Site search\n 'view_search_results',\n // Video engagement\n 'video_start',\n 'video_progress',\n 'video_complete',\n // File downloads\n 'file_download',\n // Form interactions\n 'form_start',\n 'form_submit',\n];\n\nconst AUTOMATICALLY_COLLECTED_EVENTS = [\n ...ENHANCED_MEASUREMENT_EVENTS,\n 'first_visit',\n 'first_open',\n // ref: https://support.google.com/analytics/answer/9234069\n // 'in_app_purchase',\n 'notification_dismiss',\n 'notification_foreground',\n 'notification_open',\n 'notification_receive',\n 'notification_send',\n /**\n * Note: The 'screen_view' event is automatically sent only when using Android or iOS native SDKs.\n * If you are using React Native, you need to send this event manually; it should not be ignored.\n * */\n // 'screen_view',\n 'session_start',\n 'user_engagement',\n];\n\n/** The SDK's own: the server binds the visitor to the user from it; no third party has a use for it. */\nconst SDK_EVENTS = ['identify'];\n\nexport const IGNORED_EVENTS = [...METRICS_EVENTS, ...AUTOMATICALLY_COLLECTED_EVENTS, ...SDK_EVENTS];\n"],"mappings":";AAAA,MAAM,iBAAiB;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;AA2BjE,MAAM,iCAAiC;CACrC,GAAG;EApBH;EAGA;EAEA;EAEA;EAEA;EACA;EACA;EAEA;EAEA;EACA;CAI6B;CAC7B;CACA;CAGA;CACA;CACA;CACA;CACA;CAMA;CACA;AACF;;AAGA,MAAM,aAAa,CAAC,UAAU;AAE9B,MAAa,iBAAiB;CAAC,GAAG;CAAgB,GAAG;CAAgC,GAAG;AAAU"}
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_visitor_index = require("../visitor/index.cjs");
2
+ const require_visitor_id = require("../visitor/id.cjs");
3
3
  const require_track_uetq = require("../track/uetq.cjs");
4
4
  const require_utils_field = require("../utils/field.cjs");
5
5
  const require_utils_sha256 = require("../utils/sha256.cjs");
@@ -112,7 +112,7 @@ async function syncUETVisitor(userId) {
112
112
  if (!uetCustomerId) return;
113
113
  sendUETIdSync({
114
114
  customerId: uetCustomerId,
115
- visitorId: require_visitor_index.visitorId(),
115
+ visitorId: require_visitor_id.visitorId(),
116
116
  userId
117
117
  });
118
118
  }
@@ -1 +1 @@
1
- {"version":3,"file":"microsoft-uet.cjs","names":["mapUETEvent","getFirst","visitorId"],"sources":["../../src/third-parties/microsoft-uet.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { type UETConsent, type UETQ, mapUETEvent } from '../track/uetq';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\nimport { visitorId } from '../visitor/index';\n\ndeclare global {\n interface Window {\n /**\n * Defined by the UET snippet before `bat.js` loads (as a plain array), so pushes made early\n * are queued rather than lost. Undefined only where the snippet was never installed.\n */\n uetq?: UETQ;\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/** Drop `undefined` fields: bat.js validates every key it receives, and `undefined` is a value. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction isLocal(): boolean {\n return window.location.host.includes('127.0.0.1') || window.location.host.includes('localhost');\n}\n\n/**\n * Forward an internal track event to the Microsoft Advertising UET tag.\n *\n * `eventId` is reused as UET's `event_id` so the browser event deduplicates against the same\n * conversion sent through the Conversions API (`sendMicrosoftEvents`). The action name is the\n * internal name on both channels — see `mapUETEvent`.\n *\n * `page_view` is not forwarded: bat.js turns that action into its page-load beacon, which the\n * snippet already fires on load and, with `enableAutoSpaTracking: true` (the setting in\n * Microsoft's generated snippet), on every history change — forwarding the SDK's own page views\n * would report each page twice. The tag owns page loads; this tracker owns everything else.\n */\nexport function sendUETEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n if (metrics.includes(name) || name === 'page_view') return;\n if (isLocal()) return;\n\n const [action, params] = mapUETEvent(name, properties, eventId);\n window.uetq.push('event', action, clean(params));\n}\n\n/**\n * Enhanced conversions: hand the tag the visitor's email and phone so Microsoft can match a\n * conversion to an ad click without a third-party cookie. A `set` applies to every event fired\n * from the page afterwards, `pageLoad` included, and the tag fires a `pid` beacon on its own\n * when the page load already went out. Values are sent raw — the tag normalizes and SHA-256\n * hashes them in the browser (bat.js `validatePid`) before anything leaves the page.\n *\n * A setter itself, like `setGAUser`, rather than a factory like `setFBUser(pixelId)`: UET is\n * bound to its tag by the snippet and `set` addresses whichever tag drains `uetq`, so there is\n * nothing to bind first.\n */\nexport function setUETUser({ user_id, user_data }: UpdateVisitorDTO) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n\n // A signed-in user is also the moment the ID Sync pixel can carry a `UID` — see `syncUETVisitor`.\n if (user_id) void syncUETVisitor(user_id);\n\n const em = getFirst(user_data?.email);\n const ph = getFirst(user_data?.phone_number);\n if (!em && !ph) return;\n\n window.uetq.push('set', { pid: clean({ em, ph }) });\n}\n\n/**\n * UET consent mode. Push `default` before the tag loads (the tag processes `consent` early, ahead\n * of the queued events) and `update` when the visitor decides. With `ad_storage: 'denied'` the\n * tag neither writes its cookies nor reads the `msclkid` from the URL.\n * https://help.ads.microsoft.com/#apex/ads/en/60119/1\n */\nexport function setUETConsent(mode: 'default' | 'update', consent: UETConsent) {\n if (typeof window === 'undefined') return;\n window.uetq ??= [] as unknown as UETQ;\n window.uetq.push('consent', mode, consent);\n}\n\n/**\n * The Conversions API's client-side ID Sync pixel: ties the visitor id sent server-side as\n * `anonymousId` to Microsoft's own identifier, so server events can be attributed across\n * contexts (view-through, audiences). Microsoft requires it for remarketing built from CAPI\n * events and recommends it for measurement quality; fire it at least once per session, ideally\n * on the first page view. A pixel rather than a fetch on purpose: Microsoft must observe the\n * browser context of the sync, which a server-side call cannot carry.\n *\n * `customerId` is the Microsoft Advertising customer id (`cid` in the UI's URLs) — not the UET\n * tag id. `visitorId` is the SDK visitor id (`visitorId()`), which is what\n * `sendMicrosoftEvents` sends as `anonymousId`; Microsoft requires the two to be equal.\n * `userId` is the raw signed-in user id: it is SHA-256 hashed here, exactly as the server sender\n * hashes it into `externalId`, so the pair matches as well. Hashing is asynchronous, so the\n * pixel fires once the digest resolves; where SubtleCrypto is unavailable (an insecure context)\n * the pixel goes out without `UID` rather than with a raw id.\n * https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#id-sync-and-why-it-matters\n */\n/**\n * The Microsoft Advertising customer id the ID Sync pixel reports to, set once by the framework\n * `Analytics` component's `uetCustomerId` prop (or `configureUET`). Unset, nothing syncs.\n */\nlet uetCustomerId: string | undefined;\n\nexport function configureUET(options: { customerId?: string | number }) {\n uetCustomerId = options.customerId === undefined ? undefined : String(options.customerId);\n}\n\n/**\n * Sync the SDK visitor — the `anonymousId` of the events the server sends to the Conversions\n * API — with Microsoft, plus the user's `UID` when one is known. The `Analytics` components run\n * it on mount so every visit syncs at least once, anonymous visitors included (they are who\n * remarketing audiences are built from), and `setUETUser` runs it again on sign-in.\n */\nexport async function syncUETVisitor(userId?: string): Promise<void> {\n if (!uetCustomerId) return;\n sendUETIdSync({ customerId: uetCustomerId, visitorId: visitorId(), userId });\n}\n\nexport function sendUETIdSync(options: {\n customerId: string | number;\n visitorId: string;\n /** The signed-in user's id, raw; hashed before it leaves the page. */\n userId?: string;\n}) {\n if (typeof window === 'undefined' || typeof Image === 'undefined') return;\n if (isLocal()) return;\n\n const fire = (uid?: string) => {\n const params = new URLSearchParams({\n Red3: `BACID_${options.customerId}`,\n VID: options.visitorId,\n });\n if (uid) params.set('UID', uid);\n new Image().src = `https://c.bing.com/c.gif?${params.toString()}`;\n };\n\n if (!options.userId) {\n fire();\n return;\n }\n sha256(options.userId).then(fire, () => fire());\n}\n"],"mappings":";;;;;;AAiBA,MAAM,UAAU;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;;AAI1D,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;AAEA,SAAS,UAAmB;CAC1B,OAAO,OAAO,SAAS,KAAK,SAAS,WAAW,KAAK,OAAO,SAAS,KAAK,SAAS,WAAW;AAChG;;;;;;;;;;;;;AAcA,SAAgB,aACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,QAAQ,SAAS,IAAI,KAAK,SAAS,aAAa;CACpD,IAAI,QAAQ,GAAG;CAEf,MAAM,CAAC,QAAQ,UAAUA,mBAAAA,YAAY,MAAM,YAAY,OAAO;CAC9D,OAAO,KAAK,KAAK,SAAS,QAAQ,MAAM,MAAM,CAAC;AACjD;;;;;;;;;;;;AAaA,SAAgB,WAAW,EAAE,SAAS,aAA+B;CACnE,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CAGA,IAAI,SAAS,eAAoB,OAAO;CAExC,MAAM,KAAKC,oBAAAA,SAAS,WAAW,KAAK;CACpC,MAAM,KAAKA,oBAAAA,SAAS,WAAW,YAAY;CAC3C,IAAI,CAAC,MAAM,CAAC,IAAI;CAEhB,OAAO,KAAK,KAAK,OAAO,EAAE,KAAK,MAAM;EAAE;EAAI;CAAG,CAAC,EAAE,CAAC;AACpD;;;;;;;AAQA,SAAgB,cAAc,MAA4B,SAAqB;CAC7E,IAAI,OAAO,WAAW,aAAa;CACnC,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,KAAK,WAAW,MAAM,OAAO;AAC3C;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAI;AAEJ,SAAgB,aAAa,SAA2C;CACtE,gBAAgB,QAAQ,eAAe,KAAA,IAAY,KAAA,IAAY,OAAO,QAAQ,UAAU;AAC1F;;;;;;;AAQA,eAAsB,eAAe,QAAgC;CACnE,IAAI,CAAC,eAAe;CACpB,cAAc;EAAE,YAAY;EAAe,WAAWC,sBAAAA,UAAU;EAAG;CAAO,CAAC;AAC7E;AAEA,SAAgB,cAAc,SAK3B;CACD,IAAI,OAAO,WAAW,eAAe,OAAO,UAAU,aAAa;CACnE,IAAI,QAAQ,GAAG;CAEf,MAAM,QAAQ,QAAiB;EAC7B,MAAM,SAAS,IAAI,gBAAgB;GACjC,MAAM,SAAS,QAAQ;GACvB,KAAK,QAAQ;EACf,CAAC;EACD,IAAI,KAAK,OAAO,IAAI,OAAO,GAAG;EAC9B,IAAI,MAAM,CAAC,CAAC,MAAM,4BAA4B,OAAO,SAAS;CAChE;CAEA,IAAI,CAAC,QAAQ,QAAQ;EACnB,KAAK;EACL;CACF;CACA,qBAAA,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;AAChD"}
1
+ {"version":3,"file":"microsoft-uet.cjs","names":["mapUETEvent","getFirst","visitorId"],"sources":["../../src/third-parties/microsoft-uet.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { type UETConsent, type UETQ, mapUETEvent } from '../track/uetq';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\nimport { visitorId } from '../visitor/id';\n\ndeclare global {\n interface Window {\n /**\n * Defined by the UET snippet before `bat.js` loads (as a plain array), so pushes made early\n * are queued rather than lost. Undefined only where the snippet was never installed.\n */\n uetq?: UETQ;\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/** Drop `undefined` fields: bat.js validates every key it receives, and `undefined` is a value. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction isLocal(): boolean {\n return window.location.host.includes('127.0.0.1') || window.location.host.includes('localhost');\n}\n\n/**\n * Forward an internal track event to the Microsoft Advertising UET tag.\n *\n * `eventId` is reused as UET's `event_id` so the browser event deduplicates against the same\n * conversion sent through the Conversions API (`sendMicrosoftEvents`). The action name is the\n * internal name on both channels — see `mapUETEvent`.\n *\n * `page_view` is not forwarded: bat.js turns that action into its page-load beacon, which the\n * snippet already fires on load and, with `enableAutoSpaTracking: true` (the setting in\n * Microsoft's generated snippet), on every history change — forwarding the SDK's own page views\n * would report each page twice. The tag owns page loads; this tracker owns everything else.\n */\nexport function sendUETEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n if (metrics.includes(name) || name === 'page_view') return;\n if (isLocal()) return;\n\n const [action, params] = mapUETEvent(name, properties, eventId);\n window.uetq.push('event', action, clean(params));\n}\n\n/**\n * Enhanced conversions: hand the tag the visitor's email and phone so Microsoft can match a\n * conversion to an ad click without a third-party cookie. A `set` applies to every event fired\n * from the page afterwards, `pageLoad` included, and the tag fires a `pid` beacon on its own\n * when the page load already went out. Values are sent raw — the tag normalizes and SHA-256\n * hashes them in the browser (bat.js `validatePid`) before anything leaves the page.\n *\n * A setter itself, like `setGAUser`, rather than a factory like `setFBUser(pixelId)`: UET is\n * bound to its tag by the snippet and `set` addresses whichever tag drains `uetq`, so there is\n * nothing to bind first.\n */\nexport function setUETUser({ user_id, user_data }: UpdateVisitorDTO) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n\n // A signed-in user is also the moment the ID Sync pixel can carry a `UID` — see `syncUETVisitor`.\n if (user_id) void syncUETVisitor(user_id);\n\n const em = getFirst(user_data?.email);\n const ph = getFirst(user_data?.phone_number);\n if (!em && !ph) return;\n\n window.uetq.push('set', { pid: clean({ em, ph }) });\n}\n\n/**\n * UET consent mode. Push `default` before the tag loads (the tag processes `consent` early, ahead\n * of the queued events) and `update` when the visitor decides. With `ad_storage: 'denied'` the\n * tag neither writes its cookies nor reads the `msclkid` from the URL.\n * https://help.ads.microsoft.com/#apex/ads/en/60119/1\n */\nexport function setUETConsent(mode: 'default' | 'update', consent: UETConsent) {\n if (typeof window === 'undefined') return;\n window.uetq ??= [] as unknown as UETQ;\n window.uetq.push('consent', mode, consent);\n}\n\n/**\n * The Conversions API's client-side ID Sync pixel: ties the visitor id sent server-side as\n * `anonymousId` to Microsoft's own identifier, so server events can be attributed across\n * contexts (view-through, audiences). Microsoft requires it for remarketing built from CAPI\n * events and recommends it for measurement quality; fire it at least once per session, ideally\n * on the first page view. A pixel rather than a fetch on purpose: Microsoft must observe the\n * browser context of the sync, which a server-side call cannot carry.\n *\n * `customerId` is the Microsoft Advertising customer id (`cid` in the UI's URLs) — not the UET\n * tag id. `visitorId` is the SDK visitor id (`visitorId()`), which is what\n * `sendMicrosoftEvents` sends as `anonymousId`; Microsoft requires the two to be equal.\n * `userId` is the raw signed-in user id: it is SHA-256 hashed here, exactly as the server sender\n * hashes it into `externalId`, so the pair matches as well. Hashing is asynchronous, so the\n * pixel fires once the digest resolves; where SubtleCrypto is unavailable (an insecure context)\n * the pixel goes out without `UID` rather than with a raw id.\n * https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#id-sync-and-why-it-matters\n */\n/**\n * The Microsoft Advertising customer id the ID Sync pixel reports to, set once by the framework\n * `Analytics` component's `uetCustomerId` prop (or `configureUET`). Unset, nothing syncs.\n */\nlet uetCustomerId: string | undefined;\n\nexport function configureUET(options: { customerId?: string | number }) {\n uetCustomerId = options.customerId === undefined ? undefined : String(options.customerId);\n}\n\n/**\n * Sync the SDK visitor — the `anonymousId` of the events the server sends to the Conversions\n * API — with Microsoft, plus the user's `UID` when one is known. The `Analytics` components run\n * it on mount so every visit syncs at least once, anonymous visitors included (they are who\n * remarketing audiences are built from), and `setUETUser` runs it again on sign-in.\n */\nexport async function syncUETVisitor(userId?: string): Promise<void> {\n if (!uetCustomerId) return;\n sendUETIdSync({ customerId: uetCustomerId, visitorId: visitorId(), userId });\n}\n\nexport function sendUETIdSync(options: {\n customerId: string | number;\n visitorId: string;\n /** The signed-in user's id, raw; hashed before it leaves the page. */\n userId?: string;\n}) {\n if (typeof window === 'undefined' || typeof Image === 'undefined') return;\n if (isLocal()) return;\n\n const fire = (uid?: string) => {\n const params = new URLSearchParams({\n Red3: `BACID_${options.customerId}`,\n VID: options.visitorId,\n });\n if (uid) params.set('UID', uid);\n new Image().src = `https://c.bing.com/c.gif?${params.toString()}`;\n };\n\n if (!options.userId) {\n fire();\n return;\n }\n sha256(options.userId).then(fire, () => fire());\n}\n"],"mappings":";;;;;;AAiBA,MAAM,UAAU;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;;AAI1D,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;AAEA,SAAS,UAAmB;CAC1B,OAAO,OAAO,SAAS,KAAK,SAAS,WAAW,KAAK,OAAO,SAAS,KAAK,SAAS,WAAW;AAChG;;;;;;;;;;;;;AAcA,SAAgB,aACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,QAAQ,SAAS,IAAI,KAAK,SAAS,aAAa;CACpD,IAAI,QAAQ,GAAG;CAEf,MAAM,CAAC,QAAQ,UAAUA,mBAAAA,YAAY,MAAM,YAAY,OAAO;CAC9D,OAAO,KAAK,KAAK,SAAS,QAAQ,MAAM,MAAM,CAAC;AACjD;;;;;;;;;;;;AAaA,SAAgB,WAAW,EAAE,SAAS,aAA+B;CACnE,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CAGA,IAAI,SAAS,eAAoB,OAAO;CAExC,MAAM,KAAKC,oBAAAA,SAAS,WAAW,KAAK;CACpC,MAAM,KAAKA,oBAAAA,SAAS,WAAW,YAAY;CAC3C,IAAI,CAAC,MAAM,CAAC,IAAI;CAEhB,OAAO,KAAK,KAAK,OAAO,EAAE,KAAK,MAAM;EAAE;EAAI;CAAG,CAAC,EAAE,CAAC;AACpD;;;;;;;AAQA,SAAgB,cAAc,MAA4B,SAAqB;CAC7E,IAAI,OAAO,WAAW,aAAa;CACnC,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,KAAK,WAAW,MAAM,OAAO;AAC3C;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAI;AAEJ,SAAgB,aAAa,SAA2C;CACtE,gBAAgB,QAAQ,eAAe,KAAA,IAAY,KAAA,IAAY,OAAO,QAAQ,UAAU;AAC1F;;;;;;;AAQA,eAAsB,eAAe,QAAgC;CACnE,IAAI,CAAC,eAAe;CACpB,cAAc;EAAE,YAAY;EAAe,WAAWC,mBAAAA,UAAU;EAAG;CAAO,CAAC;AAC7E;AAEA,SAAgB,cAAc,SAK3B;CACD,IAAI,OAAO,WAAW,eAAe,OAAO,UAAU,aAAa;CACnE,IAAI,QAAQ,GAAG;CAEf,MAAM,QAAQ,QAAiB;EAC7B,MAAM,SAAS,IAAI,gBAAgB;GACjC,MAAM,SAAS,QAAQ;GACvB,KAAK,QAAQ;EACf,CAAC;EACD,IAAI,KAAK,OAAO,IAAI,OAAO,GAAG;EAC9B,IAAI,MAAM,CAAC,CAAC,MAAM,4BAA4B,OAAO,SAAS;CAChE;CAEA,IAAI,CAAC,QAAQ,QAAQ;EACnB,KAAK;EACL;CACF;CACA,qBAAA,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;AAChD"}
@@ -1,4 +1,4 @@
1
- import { visitorId } from "../visitor/index.mjs";
1
+ import { visitorId } from "../visitor/id.mjs";
2
2
  import { mapUETEvent } from "../track/uetq.mjs";
3
3
  import { getFirst } from "../utils/field.mjs";
4
4
  import { sha256 } from "../utils/sha256.mjs";
@@ -1 +1 @@
1
- {"version":3,"file":"microsoft-uet.mjs","names":[],"sources":["../../src/third-parties/microsoft-uet.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { type UETConsent, type UETQ, mapUETEvent } from '../track/uetq';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\nimport { visitorId } from '../visitor/index';\n\ndeclare global {\n interface Window {\n /**\n * Defined by the UET snippet before `bat.js` loads (as a plain array), so pushes made early\n * are queued rather than lost. Undefined only where the snippet was never installed.\n */\n uetq?: UETQ;\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/** Drop `undefined` fields: bat.js validates every key it receives, and `undefined` is a value. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction isLocal(): boolean {\n return window.location.host.includes('127.0.0.1') || window.location.host.includes('localhost');\n}\n\n/**\n * Forward an internal track event to the Microsoft Advertising UET tag.\n *\n * `eventId` is reused as UET's `event_id` so the browser event deduplicates against the same\n * conversion sent through the Conversions API (`sendMicrosoftEvents`). The action name is the\n * internal name on both channels — see `mapUETEvent`.\n *\n * `page_view` is not forwarded: bat.js turns that action into its page-load beacon, which the\n * snippet already fires on load and, with `enableAutoSpaTracking: true` (the setting in\n * Microsoft's generated snippet), on every history change — forwarding the SDK's own page views\n * would report each page twice. The tag owns page loads; this tracker owns everything else.\n */\nexport function sendUETEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n if (metrics.includes(name) || name === 'page_view') return;\n if (isLocal()) return;\n\n const [action, params] = mapUETEvent(name, properties, eventId);\n window.uetq.push('event', action, clean(params));\n}\n\n/**\n * Enhanced conversions: hand the tag the visitor's email and phone so Microsoft can match a\n * conversion to an ad click without a third-party cookie. A `set` applies to every event fired\n * from the page afterwards, `pageLoad` included, and the tag fires a `pid` beacon on its own\n * when the page load already went out. Values are sent raw — the tag normalizes and SHA-256\n * hashes them in the browser (bat.js `validatePid`) before anything leaves the page.\n *\n * A setter itself, like `setGAUser`, rather than a factory like `setFBUser(pixelId)`: UET is\n * bound to its tag by the snippet and `set` addresses whichever tag drains `uetq`, so there is\n * nothing to bind first.\n */\nexport function setUETUser({ user_id, user_data }: UpdateVisitorDTO) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n\n // A signed-in user is also the moment the ID Sync pixel can carry a `UID` — see `syncUETVisitor`.\n if (user_id) void syncUETVisitor(user_id);\n\n const em = getFirst(user_data?.email);\n const ph = getFirst(user_data?.phone_number);\n if (!em && !ph) return;\n\n window.uetq.push('set', { pid: clean({ em, ph }) });\n}\n\n/**\n * UET consent mode. Push `default` before the tag loads (the tag processes `consent` early, ahead\n * of the queued events) and `update` when the visitor decides. With `ad_storage: 'denied'` the\n * tag neither writes its cookies nor reads the `msclkid` from the URL.\n * https://help.ads.microsoft.com/#apex/ads/en/60119/1\n */\nexport function setUETConsent(mode: 'default' | 'update', consent: UETConsent) {\n if (typeof window === 'undefined') return;\n window.uetq ??= [] as unknown as UETQ;\n window.uetq.push('consent', mode, consent);\n}\n\n/**\n * The Conversions API's client-side ID Sync pixel: ties the visitor id sent server-side as\n * `anonymousId` to Microsoft's own identifier, so server events can be attributed across\n * contexts (view-through, audiences). Microsoft requires it for remarketing built from CAPI\n * events and recommends it for measurement quality; fire it at least once per session, ideally\n * on the first page view. A pixel rather than a fetch on purpose: Microsoft must observe the\n * browser context of the sync, which a server-side call cannot carry.\n *\n * `customerId` is the Microsoft Advertising customer id (`cid` in the UI's URLs) — not the UET\n * tag id. `visitorId` is the SDK visitor id (`visitorId()`), which is what\n * `sendMicrosoftEvents` sends as `anonymousId`; Microsoft requires the two to be equal.\n * `userId` is the raw signed-in user id: it is SHA-256 hashed here, exactly as the server sender\n * hashes it into `externalId`, so the pair matches as well. Hashing is asynchronous, so the\n * pixel fires once the digest resolves; where SubtleCrypto is unavailable (an insecure context)\n * the pixel goes out without `UID` rather than with a raw id.\n * https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#id-sync-and-why-it-matters\n */\n/**\n * The Microsoft Advertising customer id the ID Sync pixel reports to, set once by the framework\n * `Analytics` component's `uetCustomerId` prop (or `configureUET`). Unset, nothing syncs.\n */\nlet uetCustomerId: string | undefined;\n\nexport function configureUET(options: { customerId?: string | number }) {\n uetCustomerId = options.customerId === undefined ? undefined : String(options.customerId);\n}\n\n/**\n * Sync the SDK visitor — the `anonymousId` of the events the server sends to the Conversions\n * API — with Microsoft, plus the user's `UID` when one is known. The `Analytics` components run\n * it on mount so every visit syncs at least once, anonymous visitors included (they are who\n * remarketing audiences are built from), and `setUETUser` runs it again on sign-in.\n */\nexport async function syncUETVisitor(userId?: string): Promise<void> {\n if (!uetCustomerId) return;\n sendUETIdSync({ customerId: uetCustomerId, visitorId: visitorId(), userId });\n}\n\nexport function sendUETIdSync(options: {\n customerId: string | number;\n visitorId: string;\n /** The signed-in user's id, raw; hashed before it leaves the page. */\n userId?: string;\n}) {\n if (typeof window === 'undefined' || typeof Image === 'undefined') return;\n if (isLocal()) return;\n\n const fire = (uid?: string) => {\n const params = new URLSearchParams({\n Red3: `BACID_${options.customerId}`,\n VID: options.visitorId,\n });\n if (uid) params.set('UID', uid);\n new Image().src = `https://c.bing.com/c.gif?${params.toString()}`;\n };\n\n if (!options.userId) {\n fire();\n return;\n }\n sha256(options.userId).then(fire, () => fire());\n}\n"],"mappings":";;;;;AAiBA,MAAM,UAAU;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;;AAI1D,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;AAEA,SAAS,UAAmB;CAC1B,OAAO,OAAO,SAAS,KAAK,SAAS,WAAW,KAAK,OAAO,SAAS,KAAK,SAAS,WAAW;AAChG;;;;;;;;;;;;;AAcA,SAAgB,aACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,QAAQ,SAAS,IAAI,KAAK,SAAS,aAAa;CACpD,IAAI,QAAQ,GAAG;CAEf,MAAM,CAAC,QAAQ,UAAU,YAAY,MAAM,YAAY,OAAO;CAC9D,OAAO,KAAK,KAAK,SAAS,QAAQ,MAAM,MAAM,CAAC;AACjD;;;;;;;;;;;;AAaA,SAAgB,WAAW,EAAE,SAAS,aAA+B;CACnE,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CAGA,IAAI,SAAS,eAAoB,OAAO;CAExC,MAAM,KAAK,SAAS,WAAW,KAAK;CACpC,MAAM,KAAK,SAAS,WAAW,YAAY;CAC3C,IAAI,CAAC,MAAM,CAAC,IAAI;CAEhB,OAAO,KAAK,KAAK,OAAO,EAAE,KAAK,MAAM;EAAE;EAAI;CAAG,CAAC,EAAE,CAAC;AACpD;;;;;;;AAQA,SAAgB,cAAc,MAA4B,SAAqB;CAC7E,IAAI,OAAO,WAAW,aAAa;CACnC,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,KAAK,WAAW,MAAM,OAAO;AAC3C;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAI;AAEJ,SAAgB,aAAa,SAA2C;CACtE,gBAAgB,QAAQ,eAAe,KAAA,IAAY,KAAA,IAAY,OAAO,QAAQ,UAAU;AAC1F;;;;;;;AAQA,eAAsB,eAAe,QAAgC;CACnE,IAAI,CAAC,eAAe;CACpB,cAAc;EAAE,YAAY;EAAe,WAAW,UAAU;EAAG;CAAO,CAAC;AAC7E;AAEA,SAAgB,cAAc,SAK3B;CACD,IAAI,OAAO,WAAW,eAAe,OAAO,UAAU,aAAa;CACnE,IAAI,QAAQ,GAAG;CAEf,MAAM,QAAQ,QAAiB;EAC7B,MAAM,SAAS,IAAI,gBAAgB;GACjC,MAAM,SAAS,QAAQ;GACvB,KAAK,QAAQ;EACf,CAAC;EACD,IAAI,KAAK,OAAO,IAAI,OAAO,GAAG;EAC9B,IAAI,MAAM,CAAC,CAAC,MAAM,4BAA4B,OAAO,SAAS;CAChE;CAEA,IAAI,CAAC,QAAQ,QAAQ;EACnB,KAAK;EACL;CACF;CACA,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;AAChD"}
1
+ {"version":3,"file":"microsoft-uet.mjs","names":[],"sources":["../../src/third-parties/microsoft-uet.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { type UETConsent, type UETQ, mapUETEvent } from '../track/uetq';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\nimport { visitorId } from '../visitor/id';\n\ndeclare global {\n interface Window {\n /**\n * Defined by the UET snippet before `bat.js` loads (as a plain array), so pushes made early\n * are queued rather than lost. Undefined only where the snippet was never installed.\n */\n uetq?: UETQ;\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/** Drop `undefined` fields: bat.js validates every key it receives, and `undefined` is a value. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction isLocal(): boolean {\n return window.location.host.includes('127.0.0.1') || window.location.host.includes('localhost');\n}\n\n/**\n * Forward an internal track event to the Microsoft Advertising UET tag.\n *\n * `eventId` is reused as UET's `event_id` so the browser event deduplicates against the same\n * conversion sent through the Conversions API (`sendMicrosoftEvents`). The action name is the\n * internal name on both channels — see `mapUETEvent`.\n *\n * `page_view` is not forwarded: bat.js turns that action into its page-load beacon, which the\n * snippet already fires on load and, with `enableAutoSpaTracking: true` (the setting in\n * Microsoft's generated snippet), on every history change — forwarding the SDK's own page views\n * would report each page twice. The tag owns page loads; this tracker owns everything else.\n */\nexport function sendUETEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n if (metrics.includes(name) || name === 'page_view') return;\n if (isLocal()) return;\n\n const [action, params] = mapUETEvent(name, properties, eventId);\n window.uetq.push('event', action, clean(params));\n}\n\n/**\n * Enhanced conversions: hand the tag the visitor's email and phone so Microsoft can match a\n * conversion to an ad click without a third-party cookie. A `set` applies to every event fired\n * from the page afterwards, `pageLoad` included, and the tag fires a `pid` beacon on its own\n * when the page load already went out. Values are sent raw — the tag normalizes and SHA-256\n * hashes them in the browser (bat.js `validatePid`) before anything leaves the page.\n *\n * A setter itself, like `setGAUser`, rather than a factory like `setFBUser(pixelId)`: UET is\n * bound to its tag by the snippet and `set` addresses whichever tag drains `uetq`, so there is\n * nothing to bind first.\n */\nexport function setUETUser({ user_id, user_data }: UpdateVisitorDTO) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n\n // A signed-in user is also the moment the ID Sync pixel can carry a `UID` — see `syncUETVisitor`.\n if (user_id) void syncUETVisitor(user_id);\n\n const em = getFirst(user_data?.email);\n const ph = getFirst(user_data?.phone_number);\n if (!em && !ph) return;\n\n window.uetq.push('set', { pid: clean({ em, ph }) });\n}\n\n/**\n * UET consent mode. Push `default` before the tag loads (the tag processes `consent` early, ahead\n * of the queued events) and `update` when the visitor decides. With `ad_storage: 'denied'` the\n * tag neither writes its cookies nor reads the `msclkid` from the URL.\n * https://help.ads.microsoft.com/#apex/ads/en/60119/1\n */\nexport function setUETConsent(mode: 'default' | 'update', consent: UETConsent) {\n if (typeof window === 'undefined') return;\n window.uetq ??= [] as unknown as UETQ;\n window.uetq.push('consent', mode, consent);\n}\n\n/**\n * The Conversions API's client-side ID Sync pixel: ties the visitor id sent server-side as\n * `anonymousId` to Microsoft's own identifier, so server events can be attributed across\n * contexts (view-through, audiences). Microsoft requires it for remarketing built from CAPI\n * events and recommends it for measurement quality; fire it at least once per session, ideally\n * on the first page view. A pixel rather than a fetch on purpose: Microsoft must observe the\n * browser context of the sync, which a server-side call cannot carry.\n *\n * `customerId` is the Microsoft Advertising customer id (`cid` in the UI's URLs) — not the UET\n * tag id. `visitorId` is the SDK visitor id (`visitorId()`), which is what\n * `sendMicrosoftEvents` sends as `anonymousId`; Microsoft requires the two to be equal.\n * `userId` is the raw signed-in user id: it is SHA-256 hashed here, exactly as the server sender\n * hashes it into `externalId`, so the pair matches as well. Hashing is asynchronous, so the\n * pixel fires once the digest resolves; where SubtleCrypto is unavailable (an insecure context)\n * the pixel goes out without `UID` rather than with a raw id.\n * https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#id-sync-and-why-it-matters\n */\n/**\n * The Microsoft Advertising customer id the ID Sync pixel reports to, set once by the framework\n * `Analytics` component's `uetCustomerId` prop (or `configureUET`). Unset, nothing syncs.\n */\nlet uetCustomerId: string | undefined;\n\nexport function configureUET(options: { customerId?: string | number }) {\n uetCustomerId = options.customerId === undefined ? undefined : String(options.customerId);\n}\n\n/**\n * Sync the SDK visitor — the `anonymousId` of the events the server sends to the Conversions\n * API — with Microsoft, plus the user's `UID` when one is known. The `Analytics` components run\n * it on mount so every visit syncs at least once, anonymous visitors included (they are who\n * remarketing audiences are built from), and `setUETUser` runs it again on sign-in.\n */\nexport async function syncUETVisitor(userId?: string): Promise<void> {\n if (!uetCustomerId) return;\n sendUETIdSync({ customerId: uetCustomerId, visitorId: visitorId(), userId });\n}\n\nexport function sendUETIdSync(options: {\n customerId: string | number;\n visitorId: string;\n /** The signed-in user's id, raw; hashed before it leaves the page. */\n userId?: string;\n}) {\n if (typeof window === 'undefined' || typeof Image === 'undefined') return;\n if (isLocal()) return;\n\n const fire = (uid?: string) => {\n const params = new URLSearchParams({\n Red3: `BACID_${options.customerId}`,\n VID: options.visitorId,\n });\n if (uid) params.set('UID', uid);\n new Image().src = `https://c.bing.com/c.gif?${params.toString()}`;\n };\n\n if (!options.userId) {\n fire();\n return;\n }\n sha256(options.userId).then(fire, () => fire());\n}\n"],"mappings":";;;;;AAiBA,MAAM,UAAU;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;;AAI1D,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;AAEA,SAAS,UAAmB;CAC1B,OAAO,OAAO,SAAS,KAAK,SAAS,WAAW,KAAK,OAAO,SAAS,KAAK,SAAS,WAAW;AAChG;;;;;;;;;;;;;AAcA,SAAgB,aACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,QAAQ,SAAS,IAAI,KAAK,SAAS,aAAa;CACpD,IAAI,QAAQ,GAAG;CAEf,MAAM,CAAC,QAAQ,UAAU,YAAY,MAAM,YAAY,OAAO;CAC9D,OAAO,KAAK,KAAK,SAAS,QAAQ,MAAM,MAAM,CAAC;AACjD;;;;;;;;;;;;AAaA,SAAgB,WAAW,EAAE,SAAS,aAA+B;CACnE,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CAGA,IAAI,SAAS,eAAoB,OAAO;CAExC,MAAM,KAAK,SAAS,WAAW,KAAK;CACpC,MAAM,KAAK,SAAS,WAAW,YAAY;CAC3C,IAAI,CAAC,MAAM,CAAC,IAAI;CAEhB,OAAO,KAAK,KAAK,OAAO,EAAE,KAAK,MAAM;EAAE;EAAI;CAAG,CAAC,EAAE,CAAC;AACpD;;;;;;;AAQA,SAAgB,cAAc,MAA4B,SAAqB;CAC7E,IAAI,OAAO,WAAW,aAAa;CACnC,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,KAAK,WAAW,MAAM,OAAO;AAC3C;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAI;AAEJ,SAAgB,aAAa,SAA2C;CACtE,gBAAgB,QAAQ,eAAe,KAAA,IAAY,KAAA,IAAY,OAAO,QAAQ,UAAU;AAC1F;;;;;;;AAQA,eAAsB,eAAe,QAAgC;CACnE,IAAI,CAAC,eAAe;CACpB,cAAc;EAAE,YAAY;EAAe,WAAW,UAAU;EAAG;CAAO,CAAC;AAC7E;AAEA,SAAgB,cAAc,SAK3B;CACD,IAAI,OAAO,WAAW,eAAe,OAAO,UAAU,aAAa;CACnE,IAAI,QAAQ,GAAG;CAEf,MAAM,QAAQ,QAAiB;EAC7B,MAAM,SAAS,IAAI,gBAAgB;GACjC,MAAM,SAAS,QAAQ;GACvB,KAAK,QAAQ;EACf,CAAC;EACD,IAAI,KAAK,OAAO,IAAI,OAAO,GAAG;EAC9B,IAAI,MAAM,CAAC,CAAC,MAAM,4BAA4B,OAAO,SAAS;CAChE;CAEA,IAAI,CAAC,QAAQ,QAAQ;EACnB,KAAK;EACL;CACF;CACA,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;AAChD"}
@@ -21,6 +21,7 @@ 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 (require_track_oaiq.APP_EVENTS.includes(type)) return;
24
25
  if (type === "custom") {
25
26
  const custom_event_name = require_track_oaiq.oaiCustomEventName(name);
26
27
  if (!custom_event_name) return;
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"openai-pixel.cjs","names":["NON_AD_EVENTS","mapOAIEvent","APP_EVENTS","oaiCustomEventName","getFirst","sha256","normalizeOAIPhone","normalizeOAIName"],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport {\n APP_EVENTS,\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 // OpenAI takes the app events from the Conversions API only.\n if (APP_EVENTS.includes(type as never)) return;\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":";;;;;;AAwBA,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;CAEnD,IAAIC,mBAAAA,WAAW,SAAS,IAAa,GAAG;CACxC,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"}
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"openai-pixel.d.cts","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"mappings":";;;;QAeQ;YACI;;IAER,OAAO;;;;;;;;wBAeK,gBAAgB,UAAU,WACxC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;;wBA6Bc,cAAc,qBAAe,SAAA,aACX"}
@@ -1 +1 @@
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
+ {"version":3,"file":"openai-pixel.d.mts","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"mappings":";;;;QAeQ;YACI;;IAER,OAAO;;;;;;;;wBAeK,gBAAgB,UAAU,WACxC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;;wBA6Bc,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, normalizeOAIName, normalizeOAIPhone, oaiCustomEventName } from "../track/oaiq.mjs";
3
+ import { APP_EVENTS, 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,6 +20,7 @@ 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 (APP_EVENTS.includes(type)) return;
23
24
  if (type === "custom") {
24
25
  const custom_event_name = oaiCustomEventName(name);
25
26
  if (!custom_event_name) return;
@@ -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 {\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"}
1
+ {"version":3,"file":"openai-pixel.mjs","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport {\n APP_EVENTS,\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 // OpenAI takes the app events from the Conversions API only.\n if (APP_EVENTS.includes(type as never)) return;\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":";;;;;AAwBA,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;CAEnD,IAAI,WAAW,SAAS,IAAa,GAAG;CACxC,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"}
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_setup_index = require("../setup/index.cjs");
3
3
  const require_setup_session = require("../setup/session.cjs");
4
4
  const require_third_parties_ignored_events = require("../third-parties/ignored-events.cjs");
5
- const require_visitor_index = require("../visitor/index.cjs");
5
+ const require_visitor_id = require("../visitor/id.cjs");
6
6
  let _shware_utils = require("@shware/utils");
7
7
  //#region src/track/index.ts
8
8
  const defaultOptions = {};
@@ -97,7 +97,7 @@ async function flush() {
97
97
  let events = [];
98
98
  try {
99
99
  await getTokenBucket().removeTokens();
100
- const visitor_id = require_visitor_index.visitorId();
100
+ const visitor_id = require_visitor_id.visitorId();
101
101
  const headers = await require_setup_index.config.getHeaders();
102
102
  await Promise.all(list.map((event) => event.tags));
103
103
  events = list.splice(0);
@@ -193,7 +193,7 @@ function beacon(dto) {
193
193
  */
194
194
  function sendPendingEvents() {
195
195
  if (list.length === 0) return;
196
- const visitor_id = require_visitor_index.visitorId();
196
+ const visitor_id = require_visitor_id.visitorId();
197
197
  if (!beacon(list.map((event) => ({
198
198
  name: event.name,
199
199
  properties: event.properties,
@@ -214,7 +214,7 @@ function sendPendingEvents() {
214
214
  list.splice(0).forEach((event) => event.options.onSucceed?.(void 0));
215
215
  }
216
216
  function sendBeacon(name, properties) {
217
- const visitor_id = require_visitor_index.visitorId();
217
+ const visitor_id = require_visitor_id.visitorId();
218
218
  const session_id = require_setup_session.getSession().extend();
219
219
  if (!session_id) return;
220
220
  if (beacon([{