@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.
- package/README.md +9 -5
- package/dist/index.cjs +4 -2
- package/dist/index.d.cts +3 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +3 -2
- package/dist/server/openai-conversions-api.cjs +2 -2
- package/dist/server/openai-conversions-api.cjs.map +1 -1
- package/dist/server/openai-conversions-api.d.cts.map +1 -1
- package/dist/server/openai-conversions-api.d.mts.map +1 -1
- package/dist/server/openai-conversions-api.mjs +3 -3
- package/dist/server/openai-conversions-api.mjs.map +1 -1
- package/dist/third-parties/ignored-events.cjs +7 -1
- package/dist/third-parties/ignored-events.cjs.map +1 -1
- package/dist/third-parties/ignored-events.d.cts.map +1 -1
- package/dist/third-parties/ignored-events.d.mts.map +1 -1
- package/dist/third-parties/ignored-events.mjs +7 -1
- package/dist/third-parties/ignored-events.mjs.map +1 -1
- package/dist/third-parties/microsoft-uet.cjs +2 -2
- package/dist/third-parties/microsoft-uet.cjs.map +1 -1
- package/dist/third-parties/microsoft-uet.mjs +1 -1
- package/dist/third-parties/microsoft-uet.mjs.map +1 -1
- package/dist/third-parties/openai-pixel.cjs +1 -0
- package/dist/third-parties/openai-pixel.cjs.map +1 -1
- package/dist/third-parties/openai-pixel.d.cts.map +1 -1
- package/dist/third-parties/openai-pixel.d.mts.map +1 -1
- package/dist/third-parties/openai-pixel.mjs +2 -1
- package/dist/third-parties/openai-pixel.mjs.map +1 -1
- package/dist/track/index.cjs +4 -4
- package/dist/track/index.cjs.map +1 -1
- package/dist/track/index.mjs +1 -1
- package/dist/track/index.mjs.map +1 -1
- package/dist/track/oaiq.cjs +17 -0
- package/dist/track/oaiq.cjs.map +1 -1
- package/dist/track/oaiq.d.cts +8 -0
- package/dist/track/oaiq.d.cts.map +1 -1
- package/dist/track/oaiq.d.mts +8 -0
- package/dist/track/oaiq.d.mts.map +1 -1
- package/dist/track/oaiq.mjs +17 -1
- package/dist/track/oaiq.mjs.map +1 -1
- package/dist/visitor/id.cjs +25 -0
- package/dist/visitor/id.cjs.map +1 -0
- package/dist/visitor/id.d.cts +13 -0
- package/dist/visitor/id.d.cts.map +1 -0
- package/dist/visitor/id.d.mts +13 -0
- package/dist/visitor/id.d.mts.map +1 -0
- package/dist/visitor/id.mjs +24 -0
- package/dist/visitor/id.mjs.map +1 -0
- package/dist/visitor/index.cjs +24 -29
- package/dist/visitor/index.cjs.map +1 -1
- package/dist/visitor/index.d.cts +15 -9
- package/dist/visitor/index.d.cts.map +1 -1
- package/dist/visitor/index.d.mts +15 -9
- package/dist/visitor/index.d.mts.map +1 -1
- package/dist/visitor/index.mjs +23 -29
- package/dist/visitor/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/track/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["TokenBucket","config","cache","getSession","visitorId","fetch","IGNORED_EVENTS"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { visitorId } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n /** `tags` once settled: a page being hidden can await nothing, and sends what it has. */\n settled?: TrackTags;\n /** Decided when the event happens, as GA4 and PostHog decide it, never when it is sent. */\n session_id: string;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\n/**\n * The events not yet sent, and the only place they are: a batch leaves it at the moment it is\n * handed to `fetch` or to a beacon, never earlier. So when the page is hidden or left,\n * everything still unsent is right here for `sendPendingEvents`.\n */\nconst list: Item[] = [];\nconst batchSize = 10;\nconst delay = 2000;\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Queues an event, preceded by a `session_start` when it is the one that opens a session.\n *\n * The session is decided here, when the event happens, as GA4 and PostHog decide it: the\n * `session_start` then sits in the queue right before the event that opened the session, so\n * wherever that event goes — a batch, a beacon, a retry — its announcement has gone ahead of it.\n * It carries that event's tags and time, as GA4 derives it from the hit that started the\n * session: the landing page is where the session began, whatever the page has navigated to by\n * the time the batch goes out.\n */\nfunction enqueue(name: Item['name'], properties: Item['properties'], options: TrackOptions): Item {\n const timestamp = new Date().toISOString();\n const tags = captureTags();\n const { id: session_id, started } = getSession().touch(Date.parse(timestamp));\n const item = (fields: Pick<Item, 'name' | 'properties' | 'options'>): Item => {\n const result: Item = { ...fields, tags, timestamp, session_id };\n void tags.then((settled) => (result.settled = settled));\n return result;\n };\n if (started) {\n const options = { enableThirdPartyTracking: false };\n list.push(item({ name: 'session_start', properties: {}, options }));\n }\n const event = item({ name, properties, options });\n list.push(event);\n return event;\n}\n\n/**\n * Sends the queue. Everything that has to be awaited is awaited first, and only then are the\n * events taken off the queue and handed to `fetch` in the same synchronous step — so there is\n * no moment at which an event is neither queued nor sent.\n */\nasync function flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n\n let events: Item[] = [];\n try {\n await getTokenBucket().removeTokens();\n const visitor_id = visitorId();\n const headers = await config.getHeaders();\n // Settled before the events leave the queue; those queued meanwhile are a microtask away.\n await Promise.all(list.map((event) => event.tags));\n\n events = list.splice(0);\n if (events.length === 0) return;\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const body = JSON.stringify(dto);\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // keepalive lets the request survive the page being unloaded mid-flight, within a 64KB\n // budget shared by every such request of the page; a body that could not fit goes as a\n // plain POST rather than failing outright.\n keepalive: body.length < 60_000,\n headers,\n body,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n events.forEach((event, index) => {\n const eventId = data.at(index)?.id;\n event.options.onSucceed?.(eventId ? { id: eventId } : undefined);\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (event.options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(event.name)) {\n return;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(event.name, event.properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it.\n if (e instanceof Error) console.log(e.message);\n }\n });\n });\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n // `fetch` has already retried the transient failures by the time a batch fails here; what\n // remains is a batch the server rejected outright — one invalid event fails the whole batch\n // — or a network that stayed down past the retries. The other events are reported lost;\n // `session_start` alone goes back on the queue for the next send, because it is the\n // session's only attribution record: a session without one has no channel, and its every\n // later event drops out of an attribution join.\n const starts = events.filter((event) => event.name === 'session_start');\n list.unshift(...starts);\n events\n .filter((event) => event.name !== 'session_start')\n .forEach((event) => event.options.onError?.(e));\n }\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n enqueue(name, properties, options);\n if (list.length >= batchSize) {\n void flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => void flush(), delay);\n}\n\n/**\n * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,\n * when the send could not start (its headers or rate limit threw), at once, the event left queued\n * for the next send. It never rejects: tracking must not fail the step that awaits it.\n */\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n let settle!: () => void;\n const settled = new Promise<void>((resolve) => {\n settle = resolve;\n });\n const event = enqueue(name, properties, {\n ...options,\n onSucceed: (response) => {\n options.onSucceed?.(response);\n settle();\n },\n onError: (error) => {\n options.onError?.(error);\n settle();\n },\n });\n await flush();\n // Still queued: this send failed before taking it. It waits for the next send, but the caller,\n // who may be holding a navigation on this, does not.\n if (list.includes(event)) return;\n await settled;\n}\n\nfunction beacon(dto: CreateTrackEventDTO): boolean {\n // Not every runtime with a `navigator` has it: React Native has neither beacons nor pages.\n if (typeof navigator === 'undefined' || !('sendBeacon' in navigator)) return false;\n // text/plain keeps a cross-origin beacon a CORS simple request: as application/json it needs a\n // preflight, which a page being closed often cannot complete, and the beacon is then dropped.\n // The server reads it as JSON (`zBeaconJson` in @shware/http).\n const blob = new Blob([JSON.stringify(dto)], { type: 'text/plain;charset=UTF-8' });\n return navigator.sendBeacon(`${config.endpoint}/events`, blob);\n}\n\n/**\n * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its\n * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.\n * Call it before the engagement beacon: a new session's `session_start` is in this queue.\n *\n * Nothing can be awaited here, so the events go with the tags they have settled on (the last\n * built ones otherwise), and without the server's ids: their third-party trackers are not fired,\n * as an id-less browser event could not be deduplicated against the server's. The visitor id is\n * local (`visitorId`), so a first visit left within a second is sent like any other; when no\n * beacon can take them — no `sendBeacon`, a refused body — they stay queued for the usual send.\n */\nexport function sendPendingEvents() {\n if (list.length === 0) return;\n const visitor_id = visitorId();\n const dto: CreateTrackEventDTO = list.map((event) => ({\n name: event.name,\n properties: event.properties,\n tags: event.settled ?? cache.tags ?? {},\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }));\n if (!beacon(dto)) {\n void flush();\n return;\n }\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n list.splice(0).forEach((event) => event.options.onSucceed?.(undefined));\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n const visitor_id = visitorId();\n // No stored session means no event was ever queued from this storage, so there is no session\n // to report for — and one started here could never be announced. See `Session.extend`.\n const session_id = getSession().extend();\n if (!session_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n if (beacon(dto)) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["TokenBucket","config","cache","getSession","visitorId","fetch","IGNORED_EVENTS"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { visitorId } from '../visitor/id';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n /** `tags` once settled: a page being hidden can await nothing, and sends what it has. */\n settled?: TrackTags;\n /** Decided when the event happens, as GA4 and PostHog decide it, never when it is sent. */\n session_id: string;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\n/**\n * The events not yet sent, and the only place they are: a batch leaves it at the moment it is\n * handed to `fetch` or to a beacon, never earlier. So when the page is hidden or left,\n * everything still unsent is right here for `sendPendingEvents`.\n */\nconst list: Item[] = [];\nconst batchSize = 10;\nconst delay = 2000;\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Queues an event, preceded by a `session_start` when it is the one that opens a session.\n *\n * The session is decided here, when the event happens, as GA4 and PostHog decide it: the\n * `session_start` then sits in the queue right before the event that opened the session, so\n * wherever that event goes — a batch, a beacon, a retry — its announcement has gone ahead of it.\n * It carries that event's tags and time, as GA4 derives it from the hit that started the\n * session: the landing page is where the session began, whatever the page has navigated to by\n * the time the batch goes out.\n */\nfunction enqueue(name: Item['name'], properties: Item['properties'], options: TrackOptions): Item {\n const timestamp = new Date().toISOString();\n const tags = captureTags();\n const { id: session_id, started } = getSession().touch(Date.parse(timestamp));\n const item = (fields: Pick<Item, 'name' | 'properties' | 'options'>): Item => {\n const result: Item = { ...fields, tags, timestamp, session_id };\n void tags.then((settled) => (result.settled = settled));\n return result;\n };\n if (started) {\n const options = { enableThirdPartyTracking: false };\n list.push(item({ name: 'session_start', properties: {}, options }));\n }\n const event = item({ name, properties, options });\n list.push(event);\n return event;\n}\n\n/**\n * Sends the queue. Everything that has to be awaited is awaited first, and only then are the\n * events taken off the queue and handed to `fetch` in the same synchronous step — so there is\n * no moment at which an event is neither queued nor sent.\n */\nasync function flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n\n let events: Item[] = [];\n try {\n await getTokenBucket().removeTokens();\n const visitor_id = visitorId();\n const headers = await config.getHeaders();\n // Settled before the events leave the queue; those queued meanwhile are a microtask away.\n await Promise.all(list.map((event) => event.tags));\n\n events = list.splice(0);\n if (events.length === 0) return;\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const body = JSON.stringify(dto);\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // keepalive lets the request survive the page being unloaded mid-flight, within a 64KB\n // budget shared by every such request of the page; a body that could not fit goes as a\n // plain POST rather than failing outright.\n keepalive: body.length < 60_000,\n headers,\n body,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n events.forEach((event, index) => {\n const eventId = data.at(index)?.id;\n event.options.onSucceed?.(eventId ? { id: eventId } : undefined);\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (event.options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(event.name)) {\n return;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(event.name, event.properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it.\n if (e instanceof Error) console.log(e.message);\n }\n });\n });\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n // `fetch` has already retried the transient failures by the time a batch fails here; what\n // remains is a batch the server rejected outright — one invalid event fails the whole batch\n // — or a network that stayed down past the retries. The other events are reported lost;\n // `session_start` alone goes back on the queue for the next send, because it is the\n // session's only attribution record: a session without one has no channel, and its every\n // later event drops out of an attribution join.\n const starts = events.filter((event) => event.name === 'session_start');\n list.unshift(...starts);\n events\n .filter((event) => event.name !== 'session_start')\n .forEach((event) => event.options.onError?.(e));\n }\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n enqueue(name, properties, options);\n if (list.length >= batchSize) {\n void flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => void flush(), delay);\n}\n\n/**\n * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,\n * when the send could not start (its headers or rate limit threw), at once, the event left queued\n * for the next send. It never rejects: tracking must not fail the step that awaits it.\n */\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n let settle!: () => void;\n const settled = new Promise<void>((resolve) => {\n settle = resolve;\n });\n const event = enqueue(name, properties, {\n ...options,\n onSucceed: (response) => {\n options.onSucceed?.(response);\n settle();\n },\n onError: (error) => {\n options.onError?.(error);\n settle();\n },\n });\n await flush();\n // Still queued: this send failed before taking it. It waits for the next send, but the caller,\n // who may be holding a navigation on this, does not.\n if (list.includes(event)) return;\n await settled;\n}\n\nfunction beacon(dto: CreateTrackEventDTO): boolean {\n // Not every runtime with a `navigator` has it: React Native has neither beacons nor pages.\n if (typeof navigator === 'undefined' || !('sendBeacon' in navigator)) return false;\n // text/plain keeps a cross-origin beacon a CORS simple request: as application/json it needs a\n // preflight, which a page being closed often cannot complete, and the beacon is then dropped.\n // The server reads it as JSON (`zBeaconJson` in @shware/http).\n const blob = new Blob([JSON.stringify(dto)], { type: 'text/plain;charset=UTF-8' });\n return navigator.sendBeacon(`${config.endpoint}/events`, blob);\n}\n\n/**\n * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its\n * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.\n * Call it before the engagement beacon: a new session's `session_start` is in this queue.\n *\n * Nothing can be awaited here, so the events go with the tags they have settled on (the last\n * built ones otherwise), and without the server's ids: their third-party trackers are not fired,\n * as an id-less browser event could not be deduplicated against the server's. The visitor id is\n * local (`visitorId`), so a first visit left within a second is sent like any other; when no\n * beacon can take them — no `sendBeacon`, a refused body — they stay queued for the usual send.\n */\nexport function sendPendingEvents() {\n if (list.length === 0) return;\n const visitor_id = visitorId();\n const dto: CreateTrackEventDTO = list.map((event) => ({\n name: event.name,\n properties: event.properties,\n tags: event.settled ?? cache.tags ?? {},\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }));\n if (!beacon(dto)) {\n void flush();\n return;\n }\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n list.splice(0).forEach((event) => event.options.onSucceed?.(undefined));\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n const visitor_id = visitorId();\n // No stored session means no event was ever queued from this storage, so there is no session\n // to report for — and one started here could never be announced. See `Session.extend`.\n const session_id = getSession().extend();\n if (!session_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n if (beacon(dto)) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
package/dist/track/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cache, config } from "../setup/index.mjs";
|
|
2
2
|
import { getSession } from "../setup/session.mjs";
|
|
3
3
|
import { IGNORED_EVENTS } from "../third-parties/ignored-events.mjs";
|
|
4
|
-
import { visitorId } from "../visitor/
|
|
4
|
+
import { visitorId } from "../visitor/id.mjs";
|
|
5
5
|
import { TokenBucket, fetch } from "@shware/utils";
|
|
6
6
|
//#region src/track/index.ts
|
|
7
7
|
const defaultOptions = {};
|
package/dist/track/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { visitorId } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n /** `tags` once settled: a page being hidden can await nothing, and sends what it has. */\n settled?: TrackTags;\n /** Decided when the event happens, as GA4 and PostHog decide it, never when it is sent. */\n session_id: string;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\n/**\n * The events not yet sent, and the only place they are: a batch leaves it at the moment it is\n * handed to `fetch` or to a beacon, never earlier. So when the page is hidden or left,\n * everything still unsent is right here for `sendPendingEvents`.\n */\nconst list: Item[] = [];\nconst batchSize = 10;\nconst delay = 2000;\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Queues an event, preceded by a `session_start` when it is the one that opens a session.\n *\n * The session is decided here, when the event happens, as GA4 and PostHog decide it: the\n * `session_start` then sits in the queue right before the event that opened the session, so\n * wherever that event goes — a batch, a beacon, a retry — its announcement has gone ahead of it.\n * It carries that event's tags and time, as GA4 derives it from the hit that started the\n * session: the landing page is where the session began, whatever the page has navigated to by\n * the time the batch goes out.\n */\nfunction enqueue(name: Item['name'], properties: Item['properties'], options: TrackOptions): Item {\n const timestamp = new Date().toISOString();\n const tags = captureTags();\n const { id: session_id, started } = getSession().touch(Date.parse(timestamp));\n const item = (fields: Pick<Item, 'name' | 'properties' | 'options'>): Item => {\n const result: Item = { ...fields, tags, timestamp, session_id };\n void tags.then((settled) => (result.settled = settled));\n return result;\n };\n if (started) {\n const options = { enableThirdPartyTracking: false };\n list.push(item({ name: 'session_start', properties: {}, options }));\n }\n const event = item({ name, properties, options });\n list.push(event);\n return event;\n}\n\n/**\n * Sends the queue. Everything that has to be awaited is awaited first, and only then are the\n * events taken off the queue and handed to `fetch` in the same synchronous step — so there is\n * no moment at which an event is neither queued nor sent.\n */\nasync function flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n\n let events: Item[] = [];\n try {\n await getTokenBucket().removeTokens();\n const visitor_id = visitorId();\n const headers = await config.getHeaders();\n // Settled before the events leave the queue; those queued meanwhile are a microtask away.\n await Promise.all(list.map((event) => event.tags));\n\n events = list.splice(0);\n if (events.length === 0) return;\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const body = JSON.stringify(dto);\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // keepalive lets the request survive the page being unloaded mid-flight, within a 64KB\n // budget shared by every such request of the page; a body that could not fit goes as a\n // plain POST rather than failing outright.\n keepalive: body.length < 60_000,\n headers,\n body,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n events.forEach((event, index) => {\n const eventId = data.at(index)?.id;\n event.options.onSucceed?.(eventId ? { id: eventId } : undefined);\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (event.options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(event.name)) {\n return;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(event.name, event.properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it.\n if (e instanceof Error) console.log(e.message);\n }\n });\n });\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n // `fetch` has already retried the transient failures by the time a batch fails here; what\n // remains is a batch the server rejected outright — one invalid event fails the whole batch\n // — or a network that stayed down past the retries. The other events are reported lost;\n // `session_start` alone goes back on the queue for the next send, because it is the\n // session's only attribution record: a session without one has no channel, and its every\n // later event drops out of an attribution join.\n const starts = events.filter((event) => event.name === 'session_start');\n list.unshift(...starts);\n events\n .filter((event) => event.name !== 'session_start')\n .forEach((event) => event.options.onError?.(e));\n }\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n enqueue(name, properties, options);\n if (list.length >= batchSize) {\n void flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => void flush(), delay);\n}\n\n/**\n * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,\n * when the send could not start (its headers or rate limit threw), at once, the event left queued\n * for the next send. It never rejects: tracking must not fail the step that awaits it.\n */\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n let settle!: () => void;\n const settled = new Promise<void>((resolve) => {\n settle = resolve;\n });\n const event = enqueue(name, properties, {\n ...options,\n onSucceed: (response) => {\n options.onSucceed?.(response);\n settle();\n },\n onError: (error) => {\n options.onError?.(error);\n settle();\n },\n });\n await flush();\n // Still queued: this send failed before taking it. It waits for the next send, but the caller,\n // who may be holding a navigation on this, does not.\n if (list.includes(event)) return;\n await settled;\n}\n\nfunction beacon(dto: CreateTrackEventDTO): boolean {\n // Not every runtime with a `navigator` has it: React Native has neither beacons nor pages.\n if (typeof navigator === 'undefined' || !('sendBeacon' in navigator)) return false;\n // text/plain keeps a cross-origin beacon a CORS simple request: as application/json it needs a\n // preflight, which a page being closed often cannot complete, and the beacon is then dropped.\n // The server reads it as JSON (`zBeaconJson` in @shware/http).\n const blob = new Blob([JSON.stringify(dto)], { type: 'text/plain;charset=UTF-8' });\n return navigator.sendBeacon(`${config.endpoint}/events`, blob);\n}\n\n/**\n * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its\n * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.\n * Call it before the engagement beacon: a new session's `session_start` is in this queue.\n *\n * Nothing can be awaited here, so the events go with the tags they have settled on (the last\n * built ones otherwise), and without the server's ids: their third-party trackers are not fired,\n * as an id-less browser event could not be deduplicated against the server's. The visitor id is\n * local (`visitorId`), so a first visit left within a second is sent like any other; when no\n * beacon can take them — no `sendBeacon`, a refused body — they stay queued for the usual send.\n */\nexport function sendPendingEvents() {\n if (list.length === 0) return;\n const visitor_id = visitorId();\n const dto: CreateTrackEventDTO = list.map((event) => ({\n name: event.name,\n properties: event.properties,\n tags: event.settled ?? cache.tags ?? {},\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }));\n if (!beacon(dto)) {\n void flush();\n return;\n }\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n list.splice(0).forEach((event) => event.options.onSucceed?.(undefined));\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n const visitor_id = visitorId();\n // No stored session means no event was ever queued from this storage, so there is no session\n // to report for — and one started here could never be announced. See `Session.extend`.\n const session_id = getSession().extend();\n if (!session_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n if (beacon(dto)) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { visitorId } from '../visitor/id';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n /** `tags` once settled: a page being hidden can await nothing, and sends what it has. */\n settled?: TrackTags;\n /** Decided when the event happens, as GA4 and PostHog decide it, never when it is sent. */\n session_id: string;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\n/**\n * The events not yet sent, and the only place they are: a batch leaves it at the moment it is\n * handed to `fetch` or to a beacon, never earlier. So when the page is hidden or left,\n * everything still unsent is right here for `sendPendingEvents`.\n */\nconst list: Item[] = [];\nconst batchSize = 10;\nconst delay = 2000;\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Queues an event, preceded by a `session_start` when it is the one that opens a session.\n *\n * The session is decided here, when the event happens, as GA4 and PostHog decide it: the\n * `session_start` then sits in the queue right before the event that opened the session, so\n * wherever that event goes — a batch, a beacon, a retry — its announcement has gone ahead of it.\n * It carries that event's tags and time, as GA4 derives it from the hit that started the\n * session: the landing page is where the session began, whatever the page has navigated to by\n * the time the batch goes out.\n */\nfunction enqueue(name: Item['name'], properties: Item['properties'], options: TrackOptions): Item {\n const timestamp = new Date().toISOString();\n const tags = captureTags();\n const { id: session_id, started } = getSession().touch(Date.parse(timestamp));\n const item = (fields: Pick<Item, 'name' | 'properties' | 'options'>): Item => {\n const result: Item = { ...fields, tags, timestamp, session_id };\n void tags.then((settled) => (result.settled = settled));\n return result;\n };\n if (started) {\n const options = { enableThirdPartyTracking: false };\n list.push(item({ name: 'session_start', properties: {}, options }));\n }\n const event = item({ name, properties, options });\n list.push(event);\n return event;\n}\n\n/**\n * Sends the queue. Everything that has to be awaited is awaited first, and only then are the\n * events taken off the queue and handed to `fetch` in the same synchronous step — so there is\n * no moment at which an event is neither queued nor sent.\n */\nasync function flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n\n let events: Item[] = [];\n try {\n await getTokenBucket().removeTokens();\n const visitor_id = visitorId();\n const headers = await config.getHeaders();\n // Settled before the events leave the queue; those queued meanwhile are a microtask away.\n await Promise.all(list.map((event) => event.tags));\n\n events = list.splice(0);\n if (events.length === 0) return;\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const body = JSON.stringify(dto);\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // keepalive lets the request survive the page being unloaded mid-flight, within a 64KB\n // budget shared by every such request of the page; a body that could not fit goes as a\n // plain POST rather than failing outright.\n keepalive: body.length < 60_000,\n headers,\n body,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n events.forEach((event, index) => {\n const eventId = data.at(index)?.id;\n event.options.onSucceed?.(eventId ? { id: eventId } : undefined);\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (event.options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(event.name)) {\n return;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(event.name, event.properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it.\n if (e instanceof Error) console.log(e.message);\n }\n });\n });\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n // `fetch` has already retried the transient failures by the time a batch fails here; what\n // remains is a batch the server rejected outright — one invalid event fails the whole batch\n // — or a network that stayed down past the retries. The other events are reported lost;\n // `session_start` alone goes back on the queue for the next send, because it is the\n // session's only attribution record: a session without one has no channel, and its every\n // later event drops out of an attribution join.\n const starts = events.filter((event) => event.name === 'session_start');\n list.unshift(...starts);\n events\n .filter((event) => event.name !== 'session_start')\n .forEach((event) => event.options.onError?.(e));\n }\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n enqueue(name, properties, options);\n if (list.length >= batchSize) {\n void flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => void flush(), delay);\n}\n\n/**\n * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,\n * when the send could not start (its headers or rate limit threw), at once, the event left queued\n * for the next send. It never rejects: tracking must not fail the step that awaits it.\n */\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n let settle!: () => void;\n const settled = new Promise<void>((resolve) => {\n settle = resolve;\n });\n const event = enqueue(name, properties, {\n ...options,\n onSucceed: (response) => {\n options.onSucceed?.(response);\n settle();\n },\n onError: (error) => {\n options.onError?.(error);\n settle();\n },\n });\n await flush();\n // Still queued: this send failed before taking it. It waits for the next send, but the caller,\n // who may be holding a navigation on this, does not.\n if (list.includes(event)) return;\n await settled;\n}\n\nfunction beacon(dto: CreateTrackEventDTO): boolean {\n // Not every runtime with a `navigator` has it: React Native has neither beacons nor pages.\n if (typeof navigator === 'undefined' || !('sendBeacon' in navigator)) return false;\n // text/plain keeps a cross-origin beacon a CORS simple request: as application/json it needs a\n // preflight, which a page being closed often cannot complete, and the beacon is then dropped.\n // The server reads it as JSON (`zBeaconJson` in @shware/http).\n const blob = new Blob([JSON.stringify(dto)], { type: 'text/plain;charset=UTF-8' });\n return navigator.sendBeacon(`${config.endpoint}/events`, blob);\n}\n\n/**\n * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its\n * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.\n * Call it before the engagement beacon: a new session's `session_start` is in this queue.\n *\n * Nothing can be awaited here, so the events go with the tags they have settled on (the last\n * built ones otherwise), and without the server's ids: their third-party trackers are not fired,\n * as an id-less browser event could not be deduplicated against the server's. The visitor id is\n * local (`visitorId`), so a first visit left within a second is sent like any other; when no\n * beacon can take them — no `sendBeacon`, a refused body — they stay queued for the usual send.\n */\nexport function sendPendingEvents() {\n if (list.length === 0) return;\n const visitor_id = visitorId();\n const dto: CreateTrackEventDTO = list.map((event) => ({\n name: event.name,\n properties: event.properties,\n tags: event.settled ?? cache.tags ?? {},\n visitor_id,\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }));\n if (!beacon(dto)) {\n void flush();\n return;\n }\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n list.splice(0).forEach((event) => event.options.onSucceed?.(undefined));\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n const visitor_id = visitorId();\n // No stored session means no event was ever queued from this storage, so there is no session\n // to report for — and one started here could never be announced. See `Session.extend`.\n const session_id = getSession().extend();\n if (!session_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n if (beacon(dto)) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
package/dist/track/oaiq.cjs
CHANGED
|
@@ -10,7 +10,15 @@ const NON_AD_EVENTS = [
|
|
|
10
10
|
"view_promotion",
|
|
11
11
|
"select_promotion"
|
|
12
12
|
];
|
|
13
|
+
/**
|
|
14
|
+
* The app events, which OpenAI takes "through the Conversions API only", with `action_source`
|
|
15
|
+
* `mobile_app`: the pixel never sends them, and the server only for an app's events.
|
|
16
|
+
* https://developers.openai.com/ads/supported-events
|
|
17
|
+
*/
|
|
18
|
+
const APP_EVENTS = ["app_installed", "app_opened"];
|
|
13
19
|
const STANDARD_EVENTS = [
|
|
20
|
+
"app_installed",
|
|
21
|
+
"app_opened",
|
|
14
22
|
"appointment_scheduled",
|
|
15
23
|
"checkout_started",
|
|
16
24
|
"contents_viewed",
|
|
@@ -155,6 +163,14 @@ function planEnrollmentData(properties) {
|
|
|
155
163
|
/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */
|
|
156
164
|
function mapOAIEvent(name, properties) {
|
|
157
165
|
switch (name) {
|
|
166
|
+
case "first_open": return {
|
|
167
|
+
type: "app_installed",
|
|
168
|
+
data: customerActionData(properties)
|
|
169
|
+
};
|
|
170
|
+
case "app_open": return {
|
|
171
|
+
type: "app_opened",
|
|
172
|
+
data: customerActionData(properties)
|
|
173
|
+
};
|
|
158
174
|
case "page_view": return {
|
|
159
175
|
type: "page_viewed",
|
|
160
176
|
data: contentsData(properties)
|
|
@@ -213,6 +229,7 @@ function mapOAIEvent(name, properties) {
|
|
|
213
229
|
}
|
|
214
230
|
}
|
|
215
231
|
//#endregion
|
|
232
|
+
exports.APP_EVENTS = APP_EVENTS;
|
|
216
233
|
exports.NON_AD_EVENTS = NON_AD_EVENTS;
|
|
217
234
|
exports.mapContents = mapContents;
|
|
218
235
|
exports.mapOAIEvent = mapOAIEvent;
|
package/dist/track/oaiq.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oaiq.cjs","names":[],"sources":["../../src/track/oaiq.ts"],"sourcesContent":["import type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport const NON_AD_EVENTS = [\n // metrics\n 'CLS',\n 'FCP',\n 'FID',\n 'INP',\n 'LCP',\n 'TTFB',\n // promotions\n 'view_promotion',\n 'select_promotion',\n];\n\n/**\n * Content item shared by `contents` and `plan_enrollment` events.\n * https://developers.openai.com/ads/supported-events\n */\nexport type ContentItem = {\n id?: string;\n name?: string;\n content_type?: string;\n quantity?: number;\n /** Per-item monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n};\n\nexport type ContentsData = {\n type: 'contents';\n /** Monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomerActionData = {\n type: 'customer_action';\n amount?: number;\n currency?: string;\n};\n\nexport type PlanEnrollmentData = {\n type: 'plan_enrollment';\n plan_id?: string;\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomEventData = {\n type: 'custom';\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type EventData = ContentsData | CustomerActionData | PlanEnrollmentData | CustomEventData;\n\n/**\n * Standard event names and the `data` shape each one expects.\n * https://developers.openai.com/ads/supported-events\n */\nexport type StandardEvents = {\n appointment_scheduled: CustomerActionData;\n checkout_started: ContentsData;\n contents_viewed: ContentsData;\n items_added: ContentsData;\n lead_created: CustomerActionData;\n order_created: ContentsData;\n page_viewed: ContentsData;\n registration_completed: CustomerActionData;\n subscription_created: PlanEnrollmentData;\n trial_started: PlanEnrollmentData;\n};\n\nexport type StandardEvent = keyof StandardEvents;\n\nconst STANDARD_EVENTS: readonly string[] = [\n 'appointment_scheduled',\n 'checkout_started',\n 'contents_viewed',\n 'items_added',\n 'lead_created',\n 'order_created',\n 'page_viewed',\n 'registration_completed',\n 'subscription_created',\n 'trial_started',\n] satisfies StandardEvent[];\n\n/**\n * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,\n * starting and ending with a letter or digit, not a standard event name; the API lowercases it.\n * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when\n * nothing usable is left — the event is then not sent at all. The pixel and the server both name\n * a custom event through this, so the two still deduplicate.\n * https://developers.openai.com/ads/supported-events\n */\nexport function oaiCustomEventName(name: string): string | undefined {\n const sanitized = name\n .toLowerCase()\n .replace(/[^a-z0-9_-]+/g, '_')\n .slice(0, 64)\n .replace(/^[_-]+|[_-]+$/g, '');\n if (!sanitized || STANDARD_EVENTS.includes(sanitized)) return undefined;\n return sanitized;\n}\n\n/**\n * Identity fields passed to `oaiq(\"init\", ...)` for conversion matching. Identifiers are\n * normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.\n * https://developers.openai.com/ads/measurement-pixel#send-user-data\n */\nexport type OAIQUser = {\n email_sha256?: string;\n /** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */\n phone_number_sha256?: string;\n external_id_sha256?: string;\n /** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */\n first_name_sha256?: string;\n last_name_sha256?: string;\n /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n country?: string;\n /** Max 128 characters; OpenAI trims and lowercases it. */\n city?: string;\n /** State, province or region, max 128 characters. */\n region?: string;\n postal_code?: string;\n};\n\n/**\n * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then\n * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.\n */\nexport function normalizeOAIPhone(phone: string): string | undefined {\n const digits = phone\n .replace(/[\\s().-]/g, '')\n .replace(/^\\+/, '')\n .replace(/^0+/, '');\n return /^\\d{8,15}$/.test(digits) ? digits : undefined;\n}\n\n/** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */\nexport function normalizeOAIName(name: string | undefined): string | undefined {\n return name?.toLowerCase().replace(/[\\s!-/:-@[-`{-~]/g, '') || undefined;\n}\n\nexport type InitConfig = {\n pixelId: string;\n /** Log SDK activity to the browser console. */\n debug?: boolean;\n user?: OAIQUser;\n};\n\n/**\n * Fourth argument of `oaiq(\"measure\", ...)`. Required for custom events (carries\n * `custom_event_name`); optional for standard events.\n */\nexport type MeasureOptions = {\n /** Shared with the Conversions API to deduplicate the same conversion across browser and server. */\n event_id?: string;\n /** Required for custom events; 1-64 chars of letters, numbers, underscores, or dashes. */\n custom_event_name?: string;\n /** When true, opts the event out of user-level personalization. */\n opt_out?: boolean;\n};\n\nexport interface OAIQ {\n oaiq(command: 'init', config: InitConfig): void;\n oaiq<T extends StandardEvent>(\n command: 'measure',\n event: T,\n data: StandardEvents[T],\n options?: MeasureOptions\n ): void;\n oaiq(\n command: 'measure',\n event: 'custom',\n data: CustomEventData,\n options: MeasureOptions & { custom_event_name: string }\n ): void;\n}\n\n/**\n * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.\n * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),\n * which can differ from strict ISO 4217 exponents.\n * https://docs.stripe.com/currencies#zero-decimal\n */\nconst ZERO_DECIMAL_CURRENCIES = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'ISK',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n]);\nconst THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']);\n\n/**\n * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None\n * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every\n * `amount` — an amount alone would be invalid, not just imprecise.\n */\nexport function toMinorUnits(value?: number | null, currency?: string): number | undefined {\n if (value === undefined || value === null || Number.isNaN(value)) return undefined;\n const code = currency?.toUpperCase();\n if (!code) return undefined;\n const exponent =\n code && ZERO_DECIMAL_CURRENCIES.has(code)\n ? 0\n : code && THREE_DECIMAL_CURRENCIES.has(code)\n ? 3\n : 2;\n return Math.round(value * 10 ** exponent);\n}\n\nexport function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined {\n if (!items || items.length === 0) return undefined;\n const code = currency?.toUpperCase();\n return items.map((item) => {\n const amount = toMinorUnits(item.price, currency);\n return {\n id: item.item_id,\n name: item.item_name,\n content_type: item.item_category,\n quantity: item.quantity,\n amount,\n currency: amount !== undefined ? code : undefined,\n };\n });\n}\n\n/** Safely read a property off loosely typed track properties without widening to `any`. */\nfunction prop(properties: unknown, key: string): unknown {\n return typeof properties === 'object' && properties !== null\n ? (properties as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction readValue(properties: unknown): number | undefined {\n const value = prop(properties, 'value');\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction readCurrency(properties: unknown): string | undefined {\n const currency = prop(properties, 'currency');\n return typeof currency === 'string' ? currency : undefined;\n}\n\nfunction readItems(properties: unknown): Item[] | undefined {\n const items = prop(properties, 'items');\n return Array.isArray(items) ? (items as Item[]) : undefined;\n}\n\nfunction contentsData(properties: unknown): ContentsData {\n const currency = readCurrency(properties);\n return {\n type: 'contents',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n };\n}\n\nfunction customerActionData(properties: unknown): CustomerActionData {\n const currency = readCurrency(properties);\n return {\n type: 'customer_action',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n };\n}\n\nfunction planEnrollmentData(properties: unknown): PlanEnrollmentData {\n const currency = readCurrency(properties);\n const items = readItems(properties);\n return {\n type: 'plan_enrollment',\n plan_id: items?.[0]?.item_id,\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(items, currency),\n };\n}\n\nexport type MappedOAIEvent = {\n type: StandardEvent | 'custom';\n data: EventData;\n};\n\n/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */\nexport function mapOAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): MappedOAIEvent {\n switch (name) {\n case 'page_view':\n return { type: 'page_viewed', data: contentsData(properties) };\n case 'view_item':\n case 'view_item_list':\n return { type: 'contents_viewed', data: contentsData(properties) };\n case 'begin_checkout':\n return { type: 'checkout_started', data: contentsData(properties) };\n case 'add_to_cart':\n return { type: 'items_added', data: contentsData(properties) };\n case 'purchase':\n return { type: 'order_created', data: contentsData(properties) };\n case 'generate_lead':\n case 'qualify_lead':\n return { type: 'lead_created', data: customerActionData(properties) };\n case 'sign_up':\n case 'login':\n return { type: 'registration_completed', data: customerActionData(properties) };\n case 'schedule':\n return { type: 'appointment_scheduled', data: customerActionData(properties) };\n case 'subscribe':\n return { type: 'subscription_created', data: planEnrollmentData(properties) };\n case 'trial_begin':\n return { type: 'trial_started', data: planEnrollmentData(properties) };\n default: {\n const currency = readCurrency(properties);\n return {\n type: 'custom',\n data: {\n type: 'custom',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n },\n };\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oaiq.cjs","names":[],"sources":["../../src/track/oaiq.ts"],"sourcesContent":["import type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport const NON_AD_EVENTS = [\n // metrics\n 'CLS',\n 'FCP',\n 'FID',\n 'INP',\n 'LCP',\n 'TTFB',\n // promotions\n 'view_promotion',\n 'select_promotion',\n];\n\n/**\n * Content item shared by `contents` and `plan_enrollment` events.\n * https://developers.openai.com/ads/supported-events\n */\nexport type ContentItem = {\n id?: string;\n name?: string;\n content_type?: string;\n quantity?: number;\n /** Per-item monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n};\n\nexport type ContentsData = {\n type: 'contents';\n /** Monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomerActionData = {\n type: 'customer_action';\n amount?: number;\n currency?: string;\n};\n\nexport type PlanEnrollmentData = {\n type: 'plan_enrollment';\n plan_id?: string;\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomEventData = {\n type: 'custom';\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type EventData = ContentsData | CustomerActionData | PlanEnrollmentData | CustomEventData;\n\n/**\n * Standard event names and the `data` shape each one expects.\n * https://developers.openai.com/ads/supported-events\n */\nexport type StandardEvents = {\n app_installed: CustomerActionData;\n app_opened: CustomerActionData;\n appointment_scheduled: CustomerActionData;\n checkout_started: ContentsData;\n contents_viewed: ContentsData;\n items_added: ContentsData;\n lead_created: CustomerActionData;\n order_created: ContentsData;\n page_viewed: ContentsData;\n registration_completed: CustomerActionData;\n subscription_created: PlanEnrollmentData;\n trial_started: PlanEnrollmentData;\n};\n\nexport type StandardEvent = keyof StandardEvents;\n\n/**\n * The app events, which OpenAI takes \"through the Conversions API only\", with `action_source`\n * `mobile_app`: the pixel never sends them, and the server only for an app's events.\n * https://developers.openai.com/ads/supported-events\n */\nexport const APP_EVENTS: readonly StandardEvent[] = ['app_installed', 'app_opened'];\n\nconst STANDARD_EVENTS: readonly string[] = [\n 'app_installed',\n 'app_opened',\n 'appointment_scheduled',\n 'checkout_started',\n 'contents_viewed',\n 'items_added',\n 'lead_created',\n 'order_created',\n 'page_viewed',\n 'registration_completed',\n 'subscription_created',\n 'trial_started',\n] satisfies StandardEvent[];\n\n/**\n * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,\n * starting and ending with a letter or digit, not a standard event name; the API lowercases it.\n * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when\n * nothing usable is left — the event is then not sent at all. The pixel and the server both name\n * a custom event through this, so the two still deduplicate.\n * https://developers.openai.com/ads/supported-events\n */\nexport function oaiCustomEventName(name: string): string | undefined {\n const sanitized = name\n .toLowerCase()\n .replace(/[^a-z0-9_-]+/g, '_')\n .slice(0, 64)\n .replace(/^[_-]+|[_-]+$/g, '');\n if (!sanitized || STANDARD_EVENTS.includes(sanitized)) return undefined;\n return sanitized;\n}\n\n/**\n * Identity fields passed to `oaiq(\"init\", ...)` for conversion matching. Identifiers are\n * normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.\n * https://developers.openai.com/ads/measurement-pixel#send-user-data\n */\nexport type OAIQUser = {\n email_sha256?: string;\n /** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */\n phone_number_sha256?: string;\n external_id_sha256?: string;\n /** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */\n first_name_sha256?: string;\n last_name_sha256?: string;\n /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n country?: string;\n /** Max 128 characters; OpenAI trims and lowercases it. */\n city?: string;\n /** State, province or region, max 128 characters. */\n region?: string;\n postal_code?: string;\n};\n\n/**\n * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then\n * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.\n */\nexport function normalizeOAIPhone(phone: string): string | undefined {\n const digits = phone\n .replace(/[\\s().-]/g, '')\n .replace(/^\\+/, '')\n .replace(/^0+/, '');\n return /^\\d{8,15}$/.test(digits) ? digits : undefined;\n}\n\n/** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */\nexport function normalizeOAIName(name: string | undefined): string | undefined {\n return name?.toLowerCase().replace(/[\\s!-/:-@[-`{-~]/g, '') || undefined;\n}\n\nexport type InitConfig = {\n pixelId: string;\n /** Log SDK activity to the browser console. */\n debug?: boolean;\n user?: OAIQUser;\n};\n\n/**\n * Fourth argument of `oaiq(\"measure\", ...)`. Required for custom events (carries\n * `custom_event_name`); optional for standard events.\n */\nexport type MeasureOptions = {\n /** Shared with the Conversions API to deduplicate the same conversion across browser and server. */\n event_id?: string;\n /** Required for custom events; 1-64 chars of letters, numbers, underscores, or dashes. */\n custom_event_name?: string;\n /** When true, opts the event out of user-level personalization. */\n opt_out?: boolean;\n};\n\nexport interface OAIQ {\n oaiq(command: 'init', config: InitConfig): void;\n oaiq<T extends StandardEvent>(\n command: 'measure',\n event: T,\n data: StandardEvents[T],\n options?: MeasureOptions\n ): void;\n oaiq(\n command: 'measure',\n event: 'custom',\n data: CustomEventData,\n options: MeasureOptions & { custom_event_name: string }\n ): void;\n}\n\n/**\n * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.\n * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),\n * which can differ from strict ISO 4217 exponents.\n * https://docs.stripe.com/currencies#zero-decimal\n */\nconst ZERO_DECIMAL_CURRENCIES = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'ISK',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n]);\nconst THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']);\n\n/**\n * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None\n * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every\n * `amount` — an amount alone would be invalid, not just imprecise.\n */\nexport function toMinorUnits(value?: number | null, currency?: string): number | undefined {\n if (value === undefined || value === null || Number.isNaN(value)) return undefined;\n const code = currency?.toUpperCase();\n if (!code) return undefined;\n const exponent =\n code && ZERO_DECIMAL_CURRENCIES.has(code)\n ? 0\n : code && THREE_DECIMAL_CURRENCIES.has(code)\n ? 3\n : 2;\n return Math.round(value * 10 ** exponent);\n}\n\nexport function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined {\n if (!items || items.length === 0) return undefined;\n const code = currency?.toUpperCase();\n return items.map((item) => {\n const amount = toMinorUnits(item.price, currency);\n return {\n id: item.item_id,\n name: item.item_name,\n content_type: item.item_category,\n quantity: item.quantity,\n amount,\n currency: amount !== undefined ? code : undefined,\n };\n });\n}\n\n/** Safely read a property off loosely typed track properties without widening to `any`. */\nfunction prop(properties: unknown, key: string): unknown {\n return typeof properties === 'object' && properties !== null\n ? (properties as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction readValue(properties: unknown): number | undefined {\n const value = prop(properties, 'value');\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction readCurrency(properties: unknown): string | undefined {\n const currency = prop(properties, 'currency');\n return typeof currency === 'string' ? currency : undefined;\n}\n\nfunction readItems(properties: unknown): Item[] | undefined {\n const items = prop(properties, 'items');\n return Array.isArray(items) ? (items as Item[]) : undefined;\n}\n\nfunction contentsData(properties: unknown): ContentsData {\n const currency = readCurrency(properties);\n return {\n type: 'contents',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n };\n}\n\nfunction customerActionData(properties: unknown): CustomerActionData {\n const currency = readCurrency(properties);\n return {\n type: 'customer_action',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n };\n}\n\nfunction planEnrollmentData(properties: unknown): PlanEnrollmentData {\n const currency = readCurrency(properties);\n const items = readItems(properties);\n return {\n type: 'plan_enrollment',\n plan_id: items?.[0]?.item_id,\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(items, currency),\n };\n}\n\nexport type MappedOAIEvent = {\n type: StandardEvent | 'custom';\n data: EventData;\n};\n\n/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */\nexport function mapOAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): MappedOAIEvent {\n switch (name) {\n // The app's first launch, recorded once by the app hook: the closest thing to an install.\n case 'first_open':\n return { type: 'app_installed', data: customerActionData(properties) };\n case 'app_open':\n return { type: 'app_opened', data: customerActionData(properties) };\n case 'page_view':\n return { type: 'page_viewed', data: contentsData(properties) };\n case 'view_item':\n case 'view_item_list':\n return { type: 'contents_viewed', data: contentsData(properties) };\n case 'begin_checkout':\n return { type: 'checkout_started', data: contentsData(properties) };\n case 'add_to_cart':\n return { type: 'items_added', data: contentsData(properties) };\n case 'purchase':\n return { type: 'order_created', data: contentsData(properties) };\n case 'generate_lead':\n case 'qualify_lead':\n return { type: 'lead_created', data: customerActionData(properties) };\n case 'sign_up':\n case 'login':\n return { type: 'registration_completed', data: customerActionData(properties) };\n case 'schedule':\n return { type: 'appointment_scheduled', data: customerActionData(properties) };\n case 'subscribe':\n return { type: 'subscription_created', data: planEnrollmentData(properties) };\n case 'trial_begin':\n return { type: 'trial_started', data: planEnrollmentData(properties) };\n default: {\n const currency = readCurrency(properties);\n return {\n type: 'custom',\n data: {\n type: 'custom',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n },\n };\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/track/oaiq.d.cts
CHANGED
|
@@ -46,6 +46,8 @@ export type EventData = ContentsData | CustomerActionData | PlanEnrollmentData |
|
|
|
46
46
|
* https://developers.openai.com/ads/supported-events
|
|
47
47
|
*/
|
|
48
48
|
export type StandardEvents = {
|
|
49
|
+
app_installed: CustomerActionData;
|
|
50
|
+
app_opened: CustomerActionData;
|
|
49
51
|
appointment_scheduled: CustomerActionData;
|
|
50
52
|
checkout_started: ContentsData;
|
|
51
53
|
contents_viewed: ContentsData;
|
|
@@ -58,6 +60,12 @@ export type StandardEvents = {
|
|
|
58
60
|
trial_started: PlanEnrollmentData;
|
|
59
61
|
};
|
|
60
62
|
export type StandardEvent = keyof StandardEvents;
|
|
63
|
+
/**
|
|
64
|
+
* The app events, which OpenAI takes "through the Conversions API only", with `action_source`
|
|
65
|
+
* `mobile_app`: the pixel never sends them, and the server only for an app's events.
|
|
66
|
+
* https://developers.openai.com/ads/supported-events
|
|
67
|
+
*/
|
|
68
|
+
export declare const APP_EVENTS: readonly StandardEvent[];
|
|
61
69
|
/**
|
|
62
70
|
* A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,
|
|
63
71
|
* starting and ending with a letter or digit, not a standard event name; the API lowercases it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oaiq.d.cts","names":[],"sources":["../../src/track/oaiq.ts"],"mappings":";;;qBAGa;;;;;YAiBD;EACV;EACA;EACA;EACA;;EAEA;EACA;;YAGU;EACV;;EAEA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;EACA,WAAW;;YAGD,YAAY,eAAe,qBAAqB,qBAAqB;;;;;YAMrE;EACV,uBAAuB;EACvB,kBAAkB;EAClB,iBAAiB;EACjB,aAAa;EACb,cAAc;EACd,eAAe;EACf,aAAa;EACb,wBAAwB;EACxB,sBAAsB;EACtB,eAAe;;YAGL,sBAAsB;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"oaiq.d.cts","names":[],"sources":["../../src/track/oaiq.ts"],"mappings":";;;qBAGa;;;;;YAiBD;EACV;EACA;EACA;EACA;;EAEA;EACA;;YAGU;EACV;;EAEA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;EACA,WAAW;;YAGD,YAAY,eAAe,qBAAqB,qBAAqB;;;;;YAMrE;EACV,eAAe;EACf,YAAY;EACZ,uBAAuB;EACvB,kBAAkB;EAClB,iBAAiB;EACjB,aAAa;EACb,cAAc;EACd,eAAe;EACf,aAAa;EACb,wBAAwB;EACxB,sBAAsB;EACtB,eAAe;;YAGL,sBAAsB;;;;;;qBAOrB,qBAAqB;;;;;;;;;wBAyBlB,mBAAmB;;;;;;YAevB;EACV;;EAEA;EACA;;EAEA;EACA;;EAEA;;EAEA;;EAEA;EACA;;;;;;wBAOc,kBAAkB;;wBASlB,iBAAiB;YAIrB;EACV;;EAEA;EACA,OAAO;;;;;;YAOG;;EAEV;;EAEA;;EAEA;;iBAGe;EACf,KAAK,iBAAiB,QAAQ;EAC9B,KAAK,UAAU,eACb,oBACA,OAAO,GACP,MAAM,eAAe,IACrB,UAAU;EAEZ,KACE,oBACA,iBACA,MAAM,iBACN,SAAS;IAAmB;;;;;;;;wBAoChB,aAAa,uBAAuB;wBAapC,YAAY,QAAQ,QAAQ,oBAAoB;YAqEpD;EACV,MAAM;EACN,MAAM;;;wBAIQ,YAAY,UAAU,WACpC,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B"}
|
package/dist/track/oaiq.d.mts
CHANGED
|
@@ -46,6 +46,8 @@ export type EventData = ContentsData | CustomerActionData | PlanEnrollmentData |
|
|
|
46
46
|
* https://developers.openai.com/ads/supported-events
|
|
47
47
|
*/
|
|
48
48
|
export type StandardEvents = {
|
|
49
|
+
app_installed: CustomerActionData;
|
|
50
|
+
app_opened: CustomerActionData;
|
|
49
51
|
appointment_scheduled: CustomerActionData;
|
|
50
52
|
checkout_started: ContentsData;
|
|
51
53
|
contents_viewed: ContentsData;
|
|
@@ -58,6 +60,12 @@ export type StandardEvents = {
|
|
|
58
60
|
trial_started: PlanEnrollmentData;
|
|
59
61
|
};
|
|
60
62
|
export type StandardEvent = keyof StandardEvents;
|
|
63
|
+
/**
|
|
64
|
+
* The app events, which OpenAI takes "through the Conversions API only", with `action_source`
|
|
65
|
+
* `mobile_app`: the pixel never sends them, and the server only for an app's events.
|
|
66
|
+
* https://developers.openai.com/ads/supported-events
|
|
67
|
+
*/
|
|
68
|
+
export declare const APP_EVENTS: readonly StandardEvent[];
|
|
61
69
|
/**
|
|
62
70
|
* A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,
|
|
63
71
|
* starting and ending with a letter or digit, not a standard event name; the API lowercases it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oaiq.d.mts","names":[],"sources":["../../src/track/oaiq.ts"],"mappings":";;;qBAGa;;;;;YAiBD;EACV;EACA;EACA;EACA;;EAEA;EACA;;YAGU;EACV;;EAEA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;EACA,WAAW;;YAGD,YAAY,eAAe,qBAAqB,qBAAqB;;;;;YAMrE;EACV,uBAAuB;EACvB,kBAAkB;EAClB,iBAAiB;EACjB,aAAa;EACb,cAAc;EACd,eAAe;EACf,aAAa;EACb,wBAAwB;EACxB,sBAAsB;EACtB,eAAe;;YAGL,sBAAsB;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"oaiq.d.mts","names":[],"sources":["../../src/track/oaiq.ts"],"mappings":";;;qBAGa;;;;;YAiBD;EACV;EACA;EACA;EACA;;EAEA;EACA;;YAGU;EACV;;EAEA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;EACA,WAAW;;YAGD,YAAY,eAAe,qBAAqB,qBAAqB;;;;;YAMrE;EACV,eAAe;EACf,YAAY;EACZ,uBAAuB;EACvB,kBAAkB;EAClB,iBAAiB;EACjB,aAAa;EACb,cAAc;EACd,eAAe;EACf,aAAa;EACb,wBAAwB;EACxB,sBAAsB;EACtB,eAAe;;YAGL,sBAAsB;;;;;;qBAOrB,qBAAqB;;;;;;;;;wBAyBlB,mBAAmB;;;;;;YAevB;EACV;;EAEA;EACA;;EAEA;EACA;;EAEA;;EAEA;;EAEA;EACA;;;;;;wBAOc,kBAAkB;;wBASlB,iBAAiB;YAIrB;EACV;;EAEA;EACA,OAAO;;;;;;YAOG;;EAEV;;EAEA;;EAEA;;iBAGe;EACf,KAAK,iBAAiB,QAAQ;EAC9B,KAAK,UAAU,eACb,oBACA,OAAO,GACP,MAAM,eAAe,IACrB,UAAU;EAEZ,KACE,oBACA,iBACA,MAAM,iBACN,SAAS;IAAmB;;;;;;;;wBAoChB,aAAa,uBAAuB;wBAapC,YAAY,QAAQ,QAAQ,oBAAoB;YAqEpD;EACV,MAAM;EACN,MAAM;;;wBAIQ,YAAY,UAAU,WACpC,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B"}
|
package/dist/track/oaiq.mjs
CHANGED
|
@@ -9,7 +9,15 @@ const NON_AD_EVENTS = [
|
|
|
9
9
|
"view_promotion",
|
|
10
10
|
"select_promotion"
|
|
11
11
|
];
|
|
12
|
+
/**
|
|
13
|
+
* The app events, which OpenAI takes "through the Conversions API only", with `action_source`
|
|
14
|
+
* `mobile_app`: the pixel never sends them, and the server only for an app's events.
|
|
15
|
+
* https://developers.openai.com/ads/supported-events
|
|
16
|
+
*/
|
|
17
|
+
const APP_EVENTS = ["app_installed", "app_opened"];
|
|
12
18
|
const STANDARD_EVENTS = [
|
|
19
|
+
"app_installed",
|
|
20
|
+
"app_opened",
|
|
13
21
|
"appointment_scheduled",
|
|
14
22
|
"checkout_started",
|
|
15
23
|
"contents_viewed",
|
|
@@ -154,6 +162,14 @@ function planEnrollmentData(properties) {
|
|
|
154
162
|
/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */
|
|
155
163
|
function mapOAIEvent(name, properties) {
|
|
156
164
|
switch (name) {
|
|
165
|
+
case "first_open": return {
|
|
166
|
+
type: "app_installed",
|
|
167
|
+
data: customerActionData(properties)
|
|
168
|
+
};
|
|
169
|
+
case "app_open": return {
|
|
170
|
+
type: "app_opened",
|
|
171
|
+
data: customerActionData(properties)
|
|
172
|
+
};
|
|
157
173
|
case "page_view": return {
|
|
158
174
|
type: "page_viewed",
|
|
159
175
|
data: contentsData(properties)
|
|
@@ -212,6 +228,6 @@ function mapOAIEvent(name, properties) {
|
|
|
212
228
|
}
|
|
213
229
|
}
|
|
214
230
|
//#endregion
|
|
215
|
-
export { NON_AD_EVENTS, mapContents, mapOAIEvent, normalizeOAIName, normalizeOAIPhone, oaiCustomEventName, toMinorUnits };
|
|
231
|
+
export { APP_EVENTS, NON_AD_EVENTS, mapContents, mapOAIEvent, normalizeOAIName, normalizeOAIPhone, oaiCustomEventName, toMinorUnits };
|
|
216
232
|
|
|
217
233
|
//# sourceMappingURL=oaiq.mjs.map
|
package/dist/track/oaiq.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oaiq.mjs","names":[],"sources":["../../src/track/oaiq.ts"],"sourcesContent":["import type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport const NON_AD_EVENTS = [\n // metrics\n 'CLS',\n 'FCP',\n 'FID',\n 'INP',\n 'LCP',\n 'TTFB',\n // promotions\n 'view_promotion',\n 'select_promotion',\n];\n\n/**\n * Content item shared by `contents` and `plan_enrollment` events.\n * https://developers.openai.com/ads/supported-events\n */\nexport type ContentItem = {\n id?: string;\n name?: string;\n content_type?: string;\n quantity?: number;\n /** Per-item monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n};\n\nexport type ContentsData = {\n type: 'contents';\n /** Monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomerActionData = {\n type: 'customer_action';\n amount?: number;\n currency?: string;\n};\n\nexport type PlanEnrollmentData = {\n type: 'plan_enrollment';\n plan_id?: string;\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomEventData = {\n type: 'custom';\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type EventData = ContentsData | CustomerActionData | PlanEnrollmentData | CustomEventData;\n\n/**\n * Standard event names and the `data` shape each one expects.\n * https://developers.openai.com/ads/supported-events\n */\nexport type StandardEvents = {\n appointment_scheduled: CustomerActionData;\n checkout_started: ContentsData;\n contents_viewed: ContentsData;\n items_added: ContentsData;\n lead_created: CustomerActionData;\n order_created: ContentsData;\n page_viewed: ContentsData;\n registration_completed: CustomerActionData;\n subscription_created: PlanEnrollmentData;\n trial_started: PlanEnrollmentData;\n};\n\nexport type StandardEvent = keyof StandardEvents;\n\nconst STANDARD_EVENTS: readonly string[] = [\n 'appointment_scheduled',\n 'checkout_started',\n 'contents_viewed',\n 'items_added',\n 'lead_created',\n 'order_created',\n 'page_viewed',\n 'registration_completed',\n 'subscription_created',\n 'trial_started',\n] satisfies StandardEvent[];\n\n/**\n * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,\n * starting and ending with a letter or digit, not a standard event name; the API lowercases it.\n * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when\n * nothing usable is left — the event is then not sent at all. The pixel and the server both name\n * a custom event through this, so the two still deduplicate.\n * https://developers.openai.com/ads/supported-events\n */\nexport function oaiCustomEventName(name: string): string | undefined {\n const sanitized = name\n .toLowerCase()\n .replace(/[^a-z0-9_-]+/g, '_')\n .slice(0, 64)\n .replace(/^[_-]+|[_-]+$/g, '');\n if (!sanitized || STANDARD_EVENTS.includes(sanitized)) return undefined;\n return sanitized;\n}\n\n/**\n * Identity fields passed to `oaiq(\"init\", ...)` for conversion matching. Identifiers are\n * normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.\n * https://developers.openai.com/ads/measurement-pixel#send-user-data\n */\nexport type OAIQUser = {\n email_sha256?: string;\n /** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */\n phone_number_sha256?: string;\n external_id_sha256?: string;\n /** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */\n first_name_sha256?: string;\n last_name_sha256?: string;\n /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n country?: string;\n /** Max 128 characters; OpenAI trims and lowercases it. */\n city?: string;\n /** State, province or region, max 128 characters. */\n region?: string;\n postal_code?: string;\n};\n\n/**\n * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then\n * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.\n */\nexport function normalizeOAIPhone(phone: string): string | undefined {\n const digits = phone\n .replace(/[\\s().-]/g, '')\n .replace(/^\\+/, '')\n .replace(/^0+/, '');\n return /^\\d{8,15}$/.test(digits) ? digits : undefined;\n}\n\n/** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */\nexport function normalizeOAIName(name: string | undefined): string | undefined {\n return name?.toLowerCase().replace(/[\\s!-/:-@[-`{-~]/g, '') || undefined;\n}\n\nexport type InitConfig = {\n pixelId: string;\n /** Log SDK activity to the browser console. */\n debug?: boolean;\n user?: OAIQUser;\n};\n\n/**\n * Fourth argument of `oaiq(\"measure\", ...)`. Required for custom events (carries\n * `custom_event_name`); optional for standard events.\n */\nexport type MeasureOptions = {\n /** Shared with the Conversions API to deduplicate the same conversion across browser and server. */\n event_id?: string;\n /** Required for custom events; 1-64 chars of letters, numbers, underscores, or dashes. */\n custom_event_name?: string;\n /** When true, opts the event out of user-level personalization. */\n opt_out?: boolean;\n};\n\nexport interface OAIQ {\n oaiq(command: 'init', config: InitConfig): void;\n oaiq<T extends StandardEvent>(\n command: 'measure',\n event: T,\n data: StandardEvents[T],\n options?: MeasureOptions\n ): void;\n oaiq(\n command: 'measure',\n event: 'custom',\n data: CustomEventData,\n options: MeasureOptions & { custom_event_name: string }\n ): void;\n}\n\n/**\n * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.\n * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),\n * which can differ from strict ISO 4217 exponents.\n * https://docs.stripe.com/currencies#zero-decimal\n */\nconst ZERO_DECIMAL_CURRENCIES = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'ISK',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n]);\nconst THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']);\n\n/**\n * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None\n * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every\n * `amount` — an amount alone would be invalid, not just imprecise.\n */\nexport function toMinorUnits(value?: number | null, currency?: string): number | undefined {\n if (value === undefined || value === null || Number.isNaN(value)) return undefined;\n const code = currency?.toUpperCase();\n if (!code) return undefined;\n const exponent =\n code && ZERO_DECIMAL_CURRENCIES.has(code)\n ? 0\n : code && THREE_DECIMAL_CURRENCIES.has(code)\n ? 3\n : 2;\n return Math.round(value * 10 ** exponent);\n}\n\nexport function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined {\n if (!items || items.length === 0) return undefined;\n const code = currency?.toUpperCase();\n return items.map((item) => {\n const amount = toMinorUnits(item.price, currency);\n return {\n id: item.item_id,\n name: item.item_name,\n content_type: item.item_category,\n quantity: item.quantity,\n amount,\n currency: amount !== undefined ? code : undefined,\n };\n });\n}\n\n/** Safely read a property off loosely typed track properties without widening to `any`. */\nfunction prop(properties: unknown, key: string): unknown {\n return typeof properties === 'object' && properties !== null\n ? (properties as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction readValue(properties: unknown): number | undefined {\n const value = prop(properties, 'value');\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction readCurrency(properties: unknown): string | undefined {\n const currency = prop(properties, 'currency');\n return typeof currency === 'string' ? currency : undefined;\n}\n\nfunction readItems(properties: unknown): Item[] | undefined {\n const items = prop(properties, 'items');\n return Array.isArray(items) ? (items as Item[]) : undefined;\n}\n\nfunction contentsData(properties: unknown): ContentsData {\n const currency = readCurrency(properties);\n return {\n type: 'contents',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n };\n}\n\nfunction customerActionData(properties: unknown): CustomerActionData {\n const currency = readCurrency(properties);\n return {\n type: 'customer_action',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n };\n}\n\nfunction planEnrollmentData(properties: unknown): PlanEnrollmentData {\n const currency = readCurrency(properties);\n const items = readItems(properties);\n return {\n type: 'plan_enrollment',\n plan_id: items?.[0]?.item_id,\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(items, currency),\n };\n}\n\nexport type MappedOAIEvent = {\n type: StandardEvent | 'custom';\n data: EventData;\n};\n\n/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */\nexport function mapOAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): MappedOAIEvent {\n switch (name) {\n case 'page_view':\n return { type: 'page_viewed', data: contentsData(properties) };\n case 'view_item':\n case 'view_item_list':\n return { type: 'contents_viewed', data: contentsData(properties) };\n case 'begin_checkout':\n return { type: 'checkout_started', data: contentsData(properties) };\n case 'add_to_cart':\n return { type: 'items_added', data: contentsData(properties) };\n case 'purchase':\n return { type: 'order_created', data: contentsData(properties) };\n case 'generate_lead':\n case 'qualify_lead':\n return { type: 'lead_created', data: customerActionData(properties) };\n case 'sign_up':\n case 'login':\n return { type: 'registration_completed', data: customerActionData(properties) };\n case 'schedule':\n return { type: 'appointment_scheduled', data: customerActionData(properties) };\n case 'subscribe':\n return { type: 'subscription_created', data: planEnrollmentData(properties) };\n case 'trial_begin':\n return { type: 'trial_started', data: planEnrollmentData(properties) };\n default: {\n const currency = readCurrency(properties);\n return {\n type: 'custom',\n data: {\n type: 'custom',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n },\n };\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oaiq.mjs","names":[],"sources":["../../src/track/oaiq.ts"],"sourcesContent":["import type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport const NON_AD_EVENTS = [\n // metrics\n 'CLS',\n 'FCP',\n 'FID',\n 'INP',\n 'LCP',\n 'TTFB',\n // promotions\n 'view_promotion',\n 'select_promotion',\n];\n\n/**\n * Content item shared by `contents` and `plan_enrollment` events.\n * https://developers.openai.com/ads/supported-events\n */\nexport type ContentItem = {\n id?: string;\n name?: string;\n content_type?: string;\n quantity?: number;\n /** Per-item monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n};\n\nexport type ContentsData = {\n type: 'contents';\n /** Monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomerActionData = {\n type: 'customer_action';\n amount?: number;\n currency?: string;\n};\n\nexport type PlanEnrollmentData = {\n type: 'plan_enrollment';\n plan_id?: string;\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomEventData = {\n type: 'custom';\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type EventData = ContentsData | CustomerActionData | PlanEnrollmentData | CustomEventData;\n\n/**\n * Standard event names and the `data` shape each one expects.\n * https://developers.openai.com/ads/supported-events\n */\nexport type StandardEvents = {\n app_installed: CustomerActionData;\n app_opened: CustomerActionData;\n appointment_scheduled: CustomerActionData;\n checkout_started: ContentsData;\n contents_viewed: ContentsData;\n items_added: ContentsData;\n lead_created: CustomerActionData;\n order_created: ContentsData;\n page_viewed: ContentsData;\n registration_completed: CustomerActionData;\n subscription_created: PlanEnrollmentData;\n trial_started: PlanEnrollmentData;\n};\n\nexport type StandardEvent = keyof StandardEvents;\n\n/**\n * The app events, which OpenAI takes \"through the Conversions API only\", with `action_source`\n * `mobile_app`: the pixel never sends them, and the server only for an app's events.\n * https://developers.openai.com/ads/supported-events\n */\nexport const APP_EVENTS: readonly StandardEvent[] = ['app_installed', 'app_opened'];\n\nconst STANDARD_EVENTS: readonly string[] = [\n 'app_installed',\n 'app_opened',\n 'appointment_scheduled',\n 'checkout_started',\n 'contents_viewed',\n 'items_added',\n 'lead_created',\n 'order_created',\n 'page_viewed',\n 'registration_completed',\n 'subscription_created',\n 'trial_started',\n] satisfies StandardEvent[];\n\n/**\n * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,\n * starting and ending with a letter or digit, not a standard event name; the API lowercases it.\n * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when\n * nothing usable is left — the event is then not sent at all. The pixel and the server both name\n * a custom event through this, so the two still deduplicate.\n * https://developers.openai.com/ads/supported-events\n */\nexport function oaiCustomEventName(name: string): string | undefined {\n const sanitized = name\n .toLowerCase()\n .replace(/[^a-z0-9_-]+/g, '_')\n .slice(0, 64)\n .replace(/^[_-]+|[_-]+$/g, '');\n if (!sanitized || STANDARD_EVENTS.includes(sanitized)) return undefined;\n return sanitized;\n}\n\n/**\n * Identity fields passed to `oaiq(\"init\", ...)` for conversion matching. Identifiers are\n * normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.\n * https://developers.openai.com/ads/measurement-pixel#send-user-data\n */\nexport type OAIQUser = {\n email_sha256?: string;\n /** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */\n phone_number_sha256?: string;\n external_id_sha256?: string;\n /** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */\n first_name_sha256?: string;\n last_name_sha256?: string;\n /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n country?: string;\n /** Max 128 characters; OpenAI trims and lowercases it. */\n city?: string;\n /** State, province or region, max 128 characters. */\n region?: string;\n postal_code?: string;\n};\n\n/**\n * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then\n * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.\n */\nexport function normalizeOAIPhone(phone: string): string | undefined {\n const digits = phone\n .replace(/[\\s().-]/g, '')\n .replace(/^\\+/, '')\n .replace(/^0+/, '');\n return /^\\d{8,15}$/.test(digits) ? digits : undefined;\n}\n\n/** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */\nexport function normalizeOAIName(name: string | undefined): string | undefined {\n return name?.toLowerCase().replace(/[\\s!-/:-@[-`{-~]/g, '') || undefined;\n}\n\nexport type InitConfig = {\n pixelId: string;\n /** Log SDK activity to the browser console. */\n debug?: boolean;\n user?: OAIQUser;\n};\n\n/**\n * Fourth argument of `oaiq(\"measure\", ...)`. Required for custom events (carries\n * `custom_event_name`); optional for standard events.\n */\nexport type MeasureOptions = {\n /** Shared with the Conversions API to deduplicate the same conversion across browser and server. */\n event_id?: string;\n /** Required for custom events; 1-64 chars of letters, numbers, underscores, or dashes. */\n custom_event_name?: string;\n /** When true, opts the event out of user-level personalization. */\n opt_out?: boolean;\n};\n\nexport interface OAIQ {\n oaiq(command: 'init', config: InitConfig): void;\n oaiq<T extends StandardEvent>(\n command: 'measure',\n event: T,\n data: StandardEvents[T],\n options?: MeasureOptions\n ): void;\n oaiq(\n command: 'measure',\n event: 'custom',\n data: CustomEventData,\n options: MeasureOptions & { custom_event_name: string }\n ): void;\n}\n\n/**\n * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.\n * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),\n * which can differ from strict ISO 4217 exponents.\n * https://docs.stripe.com/currencies#zero-decimal\n */\nconst ZERO_DECIMAL_CURRENCIES = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'ISK',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n]);\nconst THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']);\n\n/**\n * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None\n * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every\n * `amount` — an amount alone would be invalid, not just imprecise.\n */\nexport function toMinorUnits(value?: number | null, currency?: string): number | undefined {\n if (value === undefined || value === null || Number.isNaN(value)) return undefined;\n const code = currency?.toUpperCase();\n if (!code) return undefined;\n const exponent =\n code && ZERO_DECIMAL_CURRENCIES.has(code)\n ? 0\n : code && THREE_DECIMAL_CURRENCIES.has(code)\n ? 3\n : 2;\n return Math.round(value * 10 ** exponent);\n}\n\nexport function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined {\n if (!items || items.length === 0) return undefined;\n const code = currency?.toUpperCase();\n return items.map((item) => {\n const amount = toMinorUnits(item.price, currency);\n return {\n id: item.item_id,\n name: item.item_name,\n content_type: item.item_category,\n quantity: item.quantity,\n amount,\n currency: amount !== undefined ? code : undefined,\n };\n });\n}\n\n/** Safely read a property off loosely typed track properties without widening to `any`. */\nfunction prop(properties: unknown, key: string): unknown {\n return typeof properties === 'object' && properties !== null\n ? (properties as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction readValue(properties: unknown): number | undefined {\n const value = prop(properties, 'value');\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction readCurrency(properties: unknown): string | undefined {\n const currency = prop(properties, 'currency');\n return typeof currency === 'string' ? currency : undefined;\n}\n\nfunction readItems(properties: unknown): Item[] | undefined {\n const items = prop(properties, 'items');\n return Array.isArray(items) ? (items as Item[]) : undefined;\n}\n\nfunction contentsData(properties: unknown): ContentsData {\n const currency = readCurrency(properties);\n return {\n type: 'contents',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n };\n}\n\nfunction customerActionData(properties: unknown): CustomerActionData {\n const currency = readCurrency(properties);\n return {\n type: 'customer_action',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n };\n}\n\nfunction planEnrollmentData(properties: unknown): PlanEnrollmentData {\n const currency = readCurrency(properties);\n const items = readItems(properties);\n return {\n type: 'plan_enrollment',\n plan_id: items?.[0]?.item_id,\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(items, currency),\n };\n}\n\nexport type MappedOAIEvent = {\n type: StandardEvent | 'custom';\n data: EventData;\n};\n\n/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */\nexport function mapOAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): MappedOAIEvent {\n switch (name) {\n // The app's first launch, recorded once by the app hook: the closest thing to an install.\n case 'first_open':\n return { type: 'app_installed', data: customerActionData(properties) };\n case 'app_open':\n return { type: 'app_opened', data: customerActionData(properties) };\n case 'page_view':\n return { type: 'page_viewed', data: contentsData(properties) };\n case 'view_item':\n case 'view_item_list':\n return { type: 'contents_viewed', data: contentsData(properties) };\n case 'begin_checkout':\n return { type: 'checkout_started', data: contentsData(properties) };\n case 'add_to_cart':\n return { type: 'items_added', data: contentsData(properties) };\n case 'purchase':\n return { type: 'order_created', data: contentsData(properties) };\n case 'generate_lead':\n case 'qualify_lead':\n return { type: 'lead_created', data: customerActionData(properties) };\n case 'sign_up':\n case 'login':\n return { type: 'registration_completed', data: customerActionData(properties) };\n case 'schedule':\n return { type: 'appointment_scheduled', data: customerActionData(properties) };\n case 'subscribe':\n return { type: 'subscription_created', data: planEnrollmentData(properties) };\n case 'trial_begin':\n return { type: 'trial_started', data: planEnrollmentData(properties) };\n default: {\n const currency = readCurrency(properties);\n return {\n type: 'custom',\n data: {\n type: 'custom',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n },\n };\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_setup_index = require("../setup/index.cjs");
|
|
3
|
+
const require_constants_storage = require("../constants/storage.cjs");
|
|
4
|
+
let uuid = require("uuid");
|
|
5
|
+
//#region src/visitor/id.ts
|
|
6
|
+
/**
|
|
7
|
+
* This visitor's id: generated here on the first visit (uuidv7) and kept in `config.storage`, so
|
|
8
|
+
* it exists the moment the page loads. The first events, and the beacon of a visit left within a
|
|
9
|
+
* second, go out without waiting for a round trip; the server creates the visitor from its first
|
|
10
|
+
* events. An id a server issued to an older client is kept as it is: the visitor continues.
|
|
11
|
+
*
|
|
12
|
+
* Needs a server that creates visitors from events (see the README, "Visitors"); one that still
|
|
13
|
+
* only creates them on `POST /visitors` rejects the events of a new visitor.
|
|
14
|
+
*/
|
|
15
|
+
function visitorId() {
|
|
16
|
+
const stored = require_setup_index.config.storage.getItem(require_constants_storage.keys.visitor_id);
|
|
17
|
+
if (stored && stored !== "undefined") return stored;
|
|
18
|
+
const id = (0, uuid.v7)();
|
|
19
|
+
require_setup_index.config.storage.setItem(require_constants_storage.keys.visitor_id, id);
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
exports.visitorId = visitorId;
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=id.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id.cjs","names":["config","keys","uuidv7"],"sources":["../../src/visitor/id.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\n\n/**\n * This visitor's id: generated here on the first visit (uuidv7) and kept in `config.storage`, so\n * it exists the moment the page loads. The first events, and the beacon of a visit left within a\n * second, go out without waiting for a round trip; the server creates the visitor from its first\n * events. An id a server issued to an older client is kept as it is: the visitor continues.\n *\n * Needs a server that creates visitors from events (see the README, \"Visitors\"); one that still\n * only creates them on `POST /visitors` rejects the events of a new visitor.\n */\nexport function visitorId(): string {\n const stored = config.storage.getItem(keys.visitor_id);\n if (stored && stored !== 'undefined') return stored;\n const id = uuidv7();\n config.storage.setItem(keys.visitor_id, id);\n return id;\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,SAAgB,YAAoB;CAClC,MAAM,SAASA,oBAAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,UAAU;CACrD,IAAI,UAAU,WAAW,aAAa,OAAO;CAC7C,MAAM,MAAA,GAAKC,KAAAA,GAAAA,CAAO;CAClB,oBAAA,OAAO,QAAQ,QAAQD,0BAAAA,KAAK,YAAY,EAAE;CAC1C,OAAO;AACT"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/visitor/id.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* This visitor's id: generated here on the first visit (uuidv7) and kept in `config.storage`, so
|
|
4
|
+
* it exists the moment the page loads. The first events, and the beacon of a visit left within a
|
|
5
|
+
* second, go out without waiting for a round trip; the server creates the visitor from its first
|
|
6
|
+
* events. An id a server issued to an older client is kept as it is: the visitor continues.
|
|
7
|
+
*
|
|
8
|
+
* Needs a server that creates visitors from events (see the README, "Visitors"); one that still
|
|
9
|
+
* only creates them on `POST /visitors` rejects the events of a new visitor.
|
|
10
|
+
*/
|
|
11
|
+
export declare function visitorId(): string;
|
|
12
|
+
//#endregion
|
|
13
|
+
//# sourceMappingURL=id.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id.d.cts","names":[],"sources":["../../src/visitor/id.ts"],"mappings":";;;;;;;;;;wBAagB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/visitor/id.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* This visitor's id: generated here on the first visit (uuidv7) and kept in `config.storage`, so
|
|
4
|
+
* it exists the moment the page loads. The first events, and the beacon of a visit left within a
|
|
5
|
+
* second, go out without waiting for a round trip; the server creates the visitor from its first
|
|
6
|
+
* events. An id a server issued to an older client is kept as it is: the visitor continues.
|
|
7
|
+
*
|
|
8
|
+
* Needs a server that creates visitors from events (see the README, "Visitors"); one that still
|
|
9
|
+
* only creates them on `POST /visitors` rejects the events of a new visitor.
|
|
10
|
+
*/
|
|
11
|
+
export declare function visitorId(): string;
|
|
12
|
+
//#endregion
|
|
13
|
+
//# sourceMappingURL=id.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id.d.mts","names":[],"sources":["../../src/visitor/id.ts"],"mappings":";;;;;;;;;;wBAagB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { config } from "../setup/index.mjs";
|
|
2
|
+
import { keys } from "../constants/storage.mjs";
|
|
3
|
+
import { v7 } from "uuid";
|
|
4
|
+
//#region src/visitor/id.ts
|
|
5
|
+
/**
|
|
6
|
+
* This visitor's id: generated here on the first visit (uuidv7) and kept in `config.storage`, so
|
|
7
|
+
* it exists the moment the page loads. The first events, and the beacon of a visit left within a
|
|
8
|
+
* second, go out without waiting for a round trip; the server creates the visitor from its first
|
|
9
|
+
* events. An id a server issued to an older client is kept as it is: the visitor continues.
|
|
10
|
+
*
|
|
11
|
+
* Needs a server that creates visitors from events (see the README, "Visitors"); one that still
|
|
12
|
+
* only creates them on `POST /visitors` rejects the events of a new visitor.
|
|
13
|
+
*/
|
|
14
|
+
function visitorId() {
|
|
15
|
+
const stored = config.storage.getItem(keys.visitor_id);
|
|
16
|
+
if (stored && stored !== "undefined") return stored;
|
|
17
|
+
const id = v7();
|
|
18
|
+
config.storage.setItem(keys.visitor_id, id);
|
|
19
|
+
return id;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { visitorId };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=id.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id.mjs","names":["uuidv7"],"sources":["../../src/visitor/id.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\n\n/**\n * This visitor's id: generated here on the first visit (uuidv7) and kept in `config.storage`, so\n * it exists the moment the page loads. The first events, and the beacon of a visit left within a\n * second, go out without waiting for a round trip; the server creates the visitor from its first\n * events. An id a server issued to an older client is kept as it is: the visitor continues.\n *\n * Needs a server that creates visitors from events (see the README, \"Visitors\"); one that still\n * only creates them on `POST /visitors` rejects the events of a new visitor.\n */\nexport function visitorId(): string {\n const stored = config.storage.getItem(keys.visitor_id);\n if (stored && stored !== 'undefined') return stored;\n const id = uuidv7();\n config.storage.setItem(keys.visitor_id, id);\n return id;\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,SAAgB,YAAoB;CAClC,MAAM,SAAS,OAAO,QAAQ,QAAQ,KAAK,UAAU;CACrD,IAAI,UAAU,WAAW,aAAa,OAAO;CAC7C,MAAM,KAAKA,GAAO;CAClB,OAAO,QAAQ,QAAQ,KAAK,YAAY,EAAE;CAC1C,OAAO;AACT"}
|