@shware/analytics 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/hooks/use-web-analytics.cjs +5 -1
  2. package/dist/hooks/use-web-analytics.cjs.map +1 -1
  3. package/dist/hooks/use-web-analytics.d.cts.map +1 -1
  4. package/dist/hooks/use-web-analytics.d.mts.map +1 -1
  5. package/dist/hooks/use-web-analytics.mjs +6 -2
  6. package/dist/hooks/use-web-analytics.mjs.map +1 -1
  7. package/dist/index.cjs +1 -0
  8. package/dist/index.d.cts +2 -2
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.mjs +2 -2
  11. package/dist/server/google-data-manager.cjs +3 -3
  12. package/dist/server/google-data-manager.cjs.map +1 -1
  13. package/dist/server/google-data-manager.d.cts +15 -2
  14. package/dist/server/google-data-manager.d.cts.map +1 -1
  15. package/dist/server/google-data-manager.d.mts +15 -2
  16. package/dist/server/google-data-manager.d.mts.map +1 -1
  17. package/dist/server/google-data-manager.mjs +3 -3
  18. package/dist/server/google-data-manager.mjs.map +1 -1
  19. package/dist/server/index.d.cts +2 -2
  20. package/dist/server/index.d.mts +2 -2
  21. package/dist/server/linkedin-conversions-api.cjs +6 -1
  22. package/dist/server/linkedin-conversions-api.cjs.map +1 -1
  23. package/dist/server/linkedin-conversions-api.d.cts.map +1 -1
  24. package/dist/server/linkedin-conversions-api.d.mts.map +1 -1
  25. package/dist/server/linkedin-conversions-api.mjs +6 -1
  26. package/dist/server/linkedin-conversions-api.mjs.map +1 -1
  27. package/dist/server/meta-capi.cjs +7 -1
  28. package/dist/server/meta-capi.cjs.map +1 -1
  29. package/dist/server/meta-capi.d.cts.map +1 -1
  30. package/dist/server/meta-capi.d.mts.map +1 -1
  31. package/dist/server/meta-capi.mjs +7 -1
  32. package/dist/server/meta-capi.mjs.map +1 -1
  33. package/dist/server/meta-conversions-api.cjs +3 -1
  34. package/dist/server/meta-conversions-api.cjs.map +1 -1
  35. package/dist/server/meta-conversions-api.d.cts.map +1 -1
  36. package/dist/server/meta-conversions-api.d.mts.map +1 -1
  37. package/dist/server/meta-conversions-api.mjs +3 -1
  38. package/dist/server/meta-conversions-api.mjs.map +1 -1
  39. package/dist/server/microsoft-conversions-api.cjs +3 -1
  40. package/dist/server/microsoft-conversions-api.cjs.map +1 -1
  41. package/dist/server/microsoft-conversions-api.d.cts.map +1 -1
  42. package/dist/server/microsoft-conversions-api.d.mts.map +1 -1
  43. package/dist/server/microsoft-conversions-api.mjs +3 -1
  44. package/dist/server/microsoft-conversions-api.mjs.map +1 -1
  45. package/dist/server/openai-conversions-api.cjs +13 -12
  46. package/dist/server/openai-conversions-api.cjs.map +1 -1
  47. package/dist/server/openai-conversions-api.d.cts.map +1 -1
  48. package/dist/server/openai-conversions-api.d.mts.map +1 -1
  49. package/dist/server/openai-conversions-api.mjs +14 -13
  50. package/dist/server/openai-conversions-api.mjs.map +1 -1
  51. package/dist/server/reddit-conversions-api.cjs +6 -2
  52. package/dist/server/reddit-conversions-api.cjs.map +1 -1
  53. package/dist/server/reddit-conversions-api.d.cts +10 -1
  54. package/dist/server/reddit-conversions-api.d.cts.map +1 -1
  55. package/dist/server/reddit-conversions-api.d.mts +10 -1
  56. package/dist/server/reddit-conversions-api.d.mts.map +1 -1
  57. package/dist/server/reddit-conversions-api.mjs +6 -2
  58. package/dist/server/reddit-conversions-api.mjs.map +1 -1
  59. package/dist/setup/session.cjs +1 -1
  60. package/dist/setup/session.cjs.map +1 -1
  61. package/dist/setup/session.d.cts +6 -5
  62. package/dist/setup/session.d.cts.map +1 -1
  63. package/dist/setup/session.d.mts +6 -5
  64. package/dist/setup/session.d.mts.map +1 -1
  65. package/dist/setup/session.mjs +1 -1
  66. package/dist/setup/session.mjs.map +1 -1
  67. package/dist/third-parties/openai-pixel.cjs +29 -13
  68. package/dist/third-parties/openai-pixel.cjs.map +1 -1
  69. package/dist/third-parties/openai-pixel.d.cts +4 -3
  70. package/dist/third-parties/openai-pixel.d.cts.map +1 -1
  71. package/dist/third-parties/openai-pixel.d.mts +4 -3
  72. package/dist/third-parties/openai-pixel.d.mts.map +1 -1
  73. package/dist/third-parties/openai-pixel.mjs +30 -14
  74. package/dist/third-parties/openai-pixel.mjs.map +1 -1
  75. package/dist/track/index.cjs +156 -89
  76. package/dist/track/index.cjs.map +1 -1
  77. package/dist/track/index.d.cts +17 -0
  78. package/dist/track/index.d.cts.map +1 -1
  79. package/dist/track/index.d.mts +17 -0
  80. package/dist/track/index.d.mts.map +1 -1
  81. package/dist/track/index.mjs +156 -90
  82. package/dist/track/index.mjs.map +1 -1
  83. package/dist/track/oaiq.cjs +57 -9
  84. package/dist/track/oaiq.cjs.map +1 -1
  85. package/dist/track/oaiq.d.cts +33 -7
  86. package/dist/track/oaiq.d.cts.map +1 -1
  87. package/dist/track/oaiq.d.mts +33 -7
  88. package/dist/track/oaiq.d.mts.map +1 -1
  89. package/dist/track/oaiq.mjs +55 -10
  90. package/dist/track/oaiq.mjs.map +1 -1
  91. package/package.json +1 -1
@@ -38,145 +38,211 @@ async function captureTags() {
38
38
  }
39
39
  }
40
40
  /**
41
- * The `session_start` a failed batch took down with it, kept for the next batch of the same
42
- * session. `fetch` has already retried the transient failures by the time a batch fails here;
43
- * what remains is a batch the server rejected outright — one invalid event fails the whole batch,
44
- * and `session_start` with it — or a network that stayed down past the retries. Neither is a
45
- * reason to lose the session's one attribution record, so it goes out again, with the tags and
46
- * timestamp of the moment the session actually began, under the same session id. Held in memory
47
- * only: a reload loses it, and with it the session_start — the same as before this existed.
41
+ * The events not yet sent, and the only place they are: a batch leaves it at the moment it is
42
+ * handed to `fetch` or to a beacon, never earlier. So when the page is hidden or left,
43
+ * everything still unsent is right here for `sendPendingEvents`.
48
44
  */
49
- let pendingStart;
50
- async function sendEvents(events) {
51
- /** The announcement this batch carries, if any, so the catch below can hold it back. */
52
- let announcement;
45
+ const list = [];
46
+ const batchSize = 10;
47
+ const delay = 2e3;
48
+ let timer = null;
49
+ /**
50
+ * Queues an event, preceded by a `session_start` when it is the one that opens a session.
51
+ *
52
+ * The session is decided here, when the event happens, as GA4 and PostHog decide it: the
53
+ * `session_start` then sits in the queue right before the event that opened the session, so
54
+ * wherever that event goes — a batch, a beacon, a retry — its announcement has gone ahead of it.
55
+ * It carries that event's tags and time, as GA4 derives it from the hit that started the
56
+ * session: the landing page is where the session began, whatever the page has navigated to by
57
+ * the time the batch goes out.
58
+ */
59
+ function enqueue(name, properties, options) {
60
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
61
+ const tags = captureTags();
62
+ const { id: session_id, started } = getSession().touch(Date.parse(timestamp));
63
+ const item = (fields) => {
64
+ const result = {
65
+ ...fields,
66
+ tags,
67
+ timestamp,
68
+ session_id
69
+ };
70
+ tags.then((settled) => result.settled = settled);
71
+ return result;
72
+ };
73
+ if (started) list.push(item({
74
+ name: "session_start",
75
+ properties: {},
76
+ options: { enableThirdPartyTracking: false }
77
+ }));
78
+ const event = item({
79
+ name,
80
+ properties,
81
+ options
82
+ });
83
+ list.push(event);
84
+ return event;
85
+ }
86
+ /**
87
+ * Sends the queue. Everything that has to be awaited is awaited first, and only then are the
88
+ * events taken off the queue and handed to `fetch` in the same synchronous step — so there is
89
+ * no moment at which an event is neither queued nor sent.
90
+ */
91
+ async function flush() {
92
+ if (timer) {
93
+ clearTimeout(timer);
94
+ timer = null;
95
+ }
96
+ if (list.length === 0) return;
97
+ let events = [];
53
98
  try {
54
- if (events.length === 0) return;
55
- const opening = events[0];
56
- const { id: session_id, started } = getSession().touch(Date.parse(opening.timestamp), Date.parse(events[events.length - 1].timestamp));
57
- if (started || pendingStart?.session_id === session_id) {
58
- const start = pendingStart?.session_id === session_id ? pendingStart.item : {
59
- name: "session_start",
60
- properties: {},
61
- options: { enableThirdPartyTracking: false },
62
- tags: opening.tags,
63
- timestamp: opening.timestamp
64
- };
65
- pendingStart = void 0;
66
- announcement = {
67
- session_id,
68
- item: start
69
- };
70
- events.unshift(start);
71
- }
72
99
  await getTokenBucket().removeTokens();
73
100
  const visitor_id = (await getVisitor()).id;
101
+ const headers = await config.getHeaders();
102
+ await Promise.all(list.map((event) => event.tags));
103
+ events = list.splice(0);
104
+ if (events.length === 0) return;
74
105
  const dto = await Promise.all(events.map(async (event) => ({
75
106
  name: event.name,
76
107
  properties: event.properties,
77
108
  tags: await event.tags,
78
109
  visitor_id,
79
- session_id,
110
+ session_id: event.session_id,
80
111
  platform: config.platform,
81
112
  environment: config.environment,
82
113
  timestamp: event.timestamp
83
114
  })));
115
+ const body = JSON.stringify(dto);
84
116
  const response = await fetch(`${config.endpoint}/events`, {
85
117
  method: "POST",
86
118
  credentials: "include",
87
- keepalive: true,
88
- headers: await config.getHeaders(),
89
- body: JSON.stringify(dto)
119
+ keepalive: body.length < 6e4,
120
+ headers,
121
+ body
90
122
  });
91
123
  if (!response.ok) throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);
92
124
  const data = await response.json();
93
- let index = 0;
94
- while (events.length > 0) {
95
- const event = events.shift();
96
- if (!event) {
97
- index++;
98
- continue;
99
- }
100
- const { options, name, properties } = event;
125
+ events.forEach((event, index) => {
101
126
  const eventId = data.at(index)?.id;
102
- options.onSucceed?.(eventId ? { id: eventId } : void 0);
103
- index++;
104
- if (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) continue;
127
+ event.options.onSucceed?.(eventId ? { id: eventId } : void 0);
128
+ if (event.options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(event.name)) return;
105
129
  config.thirdPartyTrackers.forEach((tracker) => {
106
130
  try {
107
- tracker(name, properties, eventId);
131
+ tracker(event.name, event.properties, eventId);
108
132
  } catch (e) {
109
133
  if (e instanceof Error) console.log(e.message);
110
134
  }
111
135
  });
112
- }
136
+ });
113
137
  } catch (e) {
114
138
  if (e instanceof Error) console.log(e.message);
115
- if (announcement) pendingStart = announcement;
116
- events.forEach((event) => event.options.onError?.(e));
117
- }
118
- }
119
- const batch = 10;
120
- const delay = 2e3;
121
- const list = [];
122
- let timer = null;
123
- /**
124
- * Both paths into a send go through here, so a batch that fills up cancels the timer the
125
- * previous push armed rather than leaving it to wake up on its own with nothing to send.
126
- */
127
- function flush() {
128
- if (timer) {
129
- clearTimeout(timer);
130
- timer = null;
139
+ const starts = events.filter((event) => event.name === "session_start");
140
+ list.unshift(...starts);
141
+ events.filter((event) => event.name !== "session_start").forEach((event) => event.options.onError?.(e));
131
142
  }
132
- if (list.length === 0) return;
133
- const copy = [...list];
134
- list.length = 0;
135
- sendEvents(copy);
136
143
  }
137
144
  function track(name, properties, options = defaultOptions) {
138
- list.push({
139
- name,
140
- properties,
141
- options,
142
- tags: captureTags(),
143
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
144
- });
145
- if (list.length >= batch) {
145
+ enqueue(name, properties, options);
146
+ if (list.length >= batchSize) {
146
147
  flush();
147
148
  return;
148
149
  }
149
150
  if (timer) clearTimeout(timer);
150
- timer = setTimeout(flush, delay);
151
+ timer = setTimeout(() => void flush(), delay);
151
152
  }
153
+ /**
154
+ * Sends the event now, with whatever is queued, and resolves once it has been sent or lost — or,
155
+ * when no visitor could be created to send it with, at once, the event left queued for the next
156
+ * send. It never rejects: tracking must not fail the step that awaits it.
157
+ */
152
158
  async function trackAsync(name, properties, options = defaultOptions) {
153
- await sendEvents([{
154
- name,
155
- properties,
156
- options,
157
- tags: captureTags(),
158
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
159
- }]);
159
+ let settle;
160
+ const settled = new Promise((resolve) => {
161
+ settle = resolve;
162
+ });
163
+ const event = enqueue(name, properties, {
164
+ ...options,
165
+ onSucceed: (response) => {
166
+ options.onSucceed?.(response);
167
+ settle();
168
+ },
169
+ onError: (error) => {
170
+ options.onError?.(error);
171
+ settle();
172
+ }
173
+ });
174
+ await flush();
175
+ if (list.includes(event)) return;
176
+ await settled;
160
177
  }
161
- function sendBeacon(name, properties) {
178
+ /**
179
+ * The visitor id a beacon can carry. It is persisted, so a returning visitor already has one
180
+ * before `getVisitor` has finished its round trip for this page. Requiring the in-memory copy
181
+ * threw away exactly the events a beacon exists for: everything a visit accrues before its first
182
+ * batch comes back, which for a short visit is the whole of it. A visitor the server has never
183
+ * seen has none: the server would reject its events.
184
+ */
185
+ function beaconVisitorId() {
162
186
  const stored = config.storage.getItem(keys.visitor_id);
163
- const visitor_id = cache.visitor?.id ?? (stored && stored !== "undefined" ? stored : void 0);
187
+ return cache.visitor?.id ?? (stored && stored !== "undefined" ? stored : void 0);
188
+ }
189
+ function beacon(dto) {
190
+ if (typeof navigator === "undefined" || !("sendBeacon" in navigator)) return false;
191
+ const blob = new Blob([JSON.stringify(dto)], { type: "text/plain;charset=UTF-8" });
192
+ return navigator.sendBeacon(`${config.endpoint}/events`, blob);
193
+ }
194
+ /**
195
+ * Sends the queue by beacon, now: for when the page is hidden or left, the last moment its
196
+ * script is sure to run — what PostHog's `requestQueue.unload()` and GA4 do on the same events.
197
+ * Call it before the engagement beacon: a new session's `session_start` is in this queue.
198
+ *
199
+ * Nothing can be awaited here, so the events go with the tags they have settled on (the last
200
+ * built ones otherwise), and without the server's ids: their third-party trackers are not fired,
201
+ * as an id-less browser event could not be deduplicated against the server's. When no beacon can
202
+ * take them — a visitor the server does not know yet, no `sendBeacon`, a refused body — they stay
203
+ * queued for the usual send, which also creates the visitor.
204
+ */
205
+ function sendPendingEvents() {
206
+ if (list.length === 0) return;
207
+ const visitor_id = beaconVisitorId();
208
+ const dto = list.map((event) => ({
209
+ name: event.name,
210
+ properties: event.properties,
211
+ tags: event.settled ?? cache.tags ?? {},
212
+ visitor_id: visitor_id ?? "",
213
+ session_id: event.session_id,
214
+ platform: config.platform,
215
+ environment: config.environment,
216
+ timestamp: event.timestamp
217
+ }));
218
+ if (!visitor_id || !beacon(dto)) {
219
+ flush();
220
+ return;
221
+ }
222
+ if (timer) {
223
+ clearTimeout(timer);
224
+ timer = null;
225
+ }
226
+ list.splice(0).forEach((event) => event.options.onSucceed?.(void 0));
227
+ }
228
+ function sendBeacon(name, properties) {
229
+ const visitor_id = beaconVisitorId();
164
230
  if (!visitor_id) return;
165
- const dto = [{
231
+ const session_id = getSession().extend();
232
+ if (!session_id) return;
233
+ if (beacon([{
166
234
  name,
167
235
  properties,
168
236
  tags: cache.tags ?? {},
169
237
  visitor_id,
170
- session_id: getSession().extend(),
238
+ session_id,
171
239
  platform: config.platform,
172
240
  environment: config.environment,
173
241
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
174
- }];
175
- const blob = new Blob([JSON.stringify(dto)], { type: "application/json" });
176
- if (navigator.sendBeacon(`${config.endpoint}/events`, blob)) return;
242
+ }])) return;
177
243
  console.warn("Failed to send beacon", name, properties);
178
244
  }
179
245
  //#endregion
180
- export { sendBeacon, track, trackAsync };
246
+ export { sendBeacon, sendPendingEvents, track, trackAsync };
181
247
 
182
248
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\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 { getVisitor } 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 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 `session_start` a failed batch took down with it, kept for the next batch of the same\n * session. `fetch` has already retried the transient failures by the time a batch fails here;\n * what remains is a batch the server rejected outright — one invalid event fails the whole batch,\n * and `session_start` with it — or a network that stayed down past the retries. Neither is a\n * reason to lose the session's one attribution record, so it goes out again, with the tags and\n * timestamp of the moment the session actually began, under the same session id. Held in memory\n * only: a reload loses it, and with it the session_start — the same as before this existed.\n */\nlet pendingStart: { session_id: string; item: Item } | undefined;\n\nasync function sendEvents(events: Item[]) {\n /** The announcement this batch carries, if any, so the catch below can hold it back. */\n let announcement: { session_id: string; item: Item } | undefined;\n try {\n if (events.length === 0) return;\n\n // One read-modify-write of the stored session for the whole batch: it answers which session\n // these events belong to and whether this batch is the one that started it. Timed by the\n // events themselves rather than by this moment — a tab frozen in the background can hold a\n // batch for far longer than `delay`, and those events belong to the session they happened in.\n const opening = events[0];\n const { id: session_id, started } = getSession().touch(\n Date.parse(opening.timestamp),\n Date.parse(events[events.length - 1].timestamp)\n );\n if (started || pendingStart?.session_id === session_id) {\n // `session_start` is derived from the event that opened the session, as GA4 derives it\n // from the hit that did: when that event happened is when the session began, and the page\n // it was captured on is where. Neither is read now — this batch goes out up to `delay` ms\n // later, a batch held in a frozen tab far later still, and a landing page that redirects\n // inside that window would stamp the session's one attribution record with the URL the utm\n // parameters were already stripped from.\n const start: Item =\n pendingStart?.session_id === session_id\n ? pendingStart.item\n : {\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: opening.tags,\n timestamp: opening.timestamp,\n };\n pendingStart = undefined;\n announcement = { session_id, item: start };\n events.unshift(start);\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\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,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // Survive the page being unloaded mid-flight: a batch waits up to `delay` ms, so closing\n // the tab inside that window would otherwise abort the request and lose every event in it.\n // The body stays far under keepalive's 64KB in-flight budget at 100 events per batch.\n keepalive: true,\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\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 let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\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 (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(name, properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it. This loop is\n // still draining `events` with `shift`, so a throw would escape to the catch below and\n // report failure to whatever is left in the queue — for events the server has already\n // accepted, and after the ones ahead of them were told they succeeded.\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 // The other events are reported lost and stay lost; `session_start` alone is worth carrying\n // over, because it is the session's only attribution record — a session without one has no\n // channel, and its every later event drops out of an attribution join.\n if (announcement) pendingStart = announcement;\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n // The visitor id is persisted, so a returning visitor already has one before `getVisitor` has\n // finished its round trip for this page. Requiring the in-memory copy threw away exactly the\n // events this function exists for: everything a visit accrues before its first batch comes\n // back, which for a short visit is the whole of it.\n const stored = config.storage.getItem(keys.visitor_id);\n const visitor_id = cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n if (!visitor_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: getSession().extend(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) 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 { keys } from '../constants/storage';\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 { getVisitor } 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 = (await getVisitor()).id;\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 no visitor could be created to send it with, at once, the event left queued for the next\n * 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 could not take it — no visitor id to send it with yet. It waits for\n // the next send, but the caller, who may be holding a navigation on this, does not.\n if (list.includes(event)) return;\n await settled;\n}\n\n/**\n * The visitor id a beacon can carry. It is persisted, so a returning visitor already has one\n * before `getVisitor` has finished its round trip for this page. Requiring the in-memory copy\n * threw away exactly the events a beacon exists for: everything a visit accrues before its first\n * batch comes back, which for a short visit is the whole of it. A visitor the server has never\n * seen has none: the server would reject its events.\n */\nfunction beaconVisitorId(): string | undefined {\n const stored = config.storage.getItem(keys.visitor_id);\n return cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\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. When no beacon can\n * take them — a visitor the server does not know yet, no `sendBeacon`, a refused body — they stay\n * queued for the usual send, which also creates the visitor.\n */\nexport function sendPendingEvents() {\n if (list.length === 0) return;\n const visitor_id = beaconVisitorId();\n const dto: CreateTrackEventDTO = list.map((event) => ({\n name: event.name,\n properties: event.properties,\n tags: event.settled ?? cache.tags ?? {},\n visitor_id: visitor_id ?? '',\n session_id: event.session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }));\n if (!visitor_id || !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 = beaconVisitorId();\n if (!visitor_id) return;\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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"}
@@ -10,6 +10,43 @@ const NON_AD_EVENTS = [
10
10
  "view_promotion",
11
11
  "select_promotion"
12
12
  ];
13
+ const STANDARD_EVENTS = [
14
+ "appointment_scheduled",
15
+ "checkout_started",
16
+ "contents_viewed",
17
+ "items_added",
18
+ "lead_created",
19
+ "order_created",
20
+ "page_viewed",
21
+ "registration_completed",
22
+ "subscription_created",
23
+ "trial_started"
24
+ ];
25
+ /**
26
+ * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,
27
+ * starting and ending with a letter or digit, not a standard event name; the API lowercases it.
28
+ * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when
29
+ * nothing usable is left — the event is then not sent at all. The pixel and the server both name
30
+ * a custom event through this, so the two still deduplicate.
31
+ * https://developers.openai.com/ads/supported-events
32
+ */
33
+ function oaiCustomEventName(name) {
34
+ const sanitized = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "_").slice(0, 64).replace(/^[_-]+|[_-]+$/g, "");
35
+ if (!sanitized || STANDARD_EVENTS.includes(sanitized)) return void 0;
36
+ return sanitized;
37
+ }
38
+ /**
39
+ * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then
40
+ * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.
41
+ */
42
+ function normalizeOAIPhone(phone) {
43
+ const digits = phone.replace(/[\s().-]/g, "").replace(/^\+/, "").replace(/^0+/, "");
44
+ return /^\d{8,15}$/.test(digits) ? digits : void 0;
45
+ }
46
+ /** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */
47
+ function normalizeOAIName(name) {
48
+ return name?.toLowerCase().replace(/[\s!-/:-@[-`{-~]/g, "") || void 0;
49
+ }
13
50
  /**
14
51
  * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.
15
52
  * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),
@@ -44,24 +81,32 @@ const THREE_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
44
81
  "OMR",
45
82
  "TND"
46
83
  ]);
47
- /** Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). */
84
+ /**
85
+ * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None
86
+ * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every
87
+ * `amount` — an amount alone would be invalid, not just imprecise.
88
+ */
48
89
  function toMinorUnits(value, currency) {
49
90
  if (value === void 0 || value === null || Number.isNaN(value)) return void 0;
50
91
  const code = currency?.toUpperCase();
92
+ if (!code) return void 0;
51
93
  const exponent = code && ZERO_DECIMAL_CURRENCIES.has(code) ? 0 : code && THREE_DECIMAL_CURRENCIES.has(code) ? 3 : 2;
52
94
  return Math.round(value * 10 ** exponent);
53
95
  }
54
96
  function mapContents(items, currency) {
55
97
  if (!items || items.length === 0) return void 0;
56
98
  const code = currency?.toUpperCase();
57
- return items.map((item) => ({
58
- id: item.item_id,
59
- name: item.item_name,
60
- content_type: item.item_category,
61
- quantity: item.quantity,
62
- amount: toMinorUnits(item.price, currency),
63
- currency: item.price !== void 0 ? code : void 0
64
- }));
99
+ return items.map((item) => {
100
+ const amount = toMinorUnits(item.price, currency);
101
+ return {
102
+ id: item.item_id,
103
+ name: item.item_name,
104
+ content_type: item.item_category,
105
+ quantity: item.quantity,
106
+ amount,
107
+ currency: amount !== void 0 ? code : void 0
108
+ };
109
+ });
65
110
  }
66
111
  /** Safely read a property off loosely typed track properties without widening to `any`. */
67
112
  function prop(properties, key) {
@@ -171,6 +216,9 @@ function mapOAIEvent(name, properties) {
171
216
  exports.NON_AD_EVENTS = NON_AD_EVENTS;
172
217
  exports.mapContents = mapContents;
173
218
  exports.mapOAIEvent = mapOAIEvent;
219
+ exports.normalizeOAIName = normalizeOAIName;
220
+ exports.normalizeOAIPhone = normalizeOAIPhone;
221
+ exports.oaiCustomEventName = oaiCustomEventName;
174
222
  exports.toMinorUnits = toMinorUnits;
175
223
 
176
224
  //# sourceMappingURL=oaiq.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"oaiq.cjs","names":[],"sources":["../../src/track/oaiq.ts"],"sourcesContent":["import type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport const NON_AD_EVENTS = [\n // metrics\n 'CLS',\n 'FCP',\n 'FID',\n 'INP',\n 'LCP',\n 'TTFB',\n // promotions\n 'view_promotion',\n 'select_promotion',\n];\n\n/**\n * Content item shared by `contents` and `plan_enrollment` events.\n * https://developers.openai.com/ads/supported-events\n */\nexport type ContentItem = {\n id?: string;\n name?: string;\n content_type?: string;\n quantity?: number;\n /** Per-item monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n};\n\nexport type ContentsData = {\n type: 'contents';\n /** Monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomerActionData = {\n type: 'customer_action';\n amount?: number;\n currency?: string;\n};\n\nexport type PlanEnrollmentData = {\n type: 'plan_enrollment';\n plan_id?: string;\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomEventData = {\n type: 'custom';\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type EventData = ContentsData | CustomerActionData | PlanEnrollmentData | CustomEventData;\n\n/**\n * Standard event names and the `data` shape each one expects.\n * https://developers.openai.com/ads/supported-events\n */\nexport type StandardEvents = {\n appointment_scheduled: CustomerActionData;\n checkout_started: ContentsData;\n contents_viewed: ContentsData;\n items_added: ContentsData;\n lead_created: CustomerActionData;\n order_created: ContentsData;\n page_viewed: ContentsData;\n registration_completed: CustomerActionData;\n subscription_created: PlanEnrollmentData;\n trial_started: PlanEnrollmentData;\n};\n\nexport type StandardEvent = keyof StandardEvents;\n\n/**\n * Identity fields passed to `oaiq(\"init\", ...)` for conversion matching. Email and external id\n * must be pre-hashed as lowercase 64-char SHA-256 hex strings; the rest are sent as raw values.\n * https://developers.openai.com/ads/measurement-pixel\n */\nexport type OAIQUser = {\n email_sha256?: string;\n external_id_sha256?: string;\n /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n country?: string;\n /** Lowercased, max 128 characters. */\n city?: string;\n /** Max 32 characters. */\n zip_code?: string;\n};\n\nexport type InitConfig = {\n pixelId: string;\n /** Log SDK activity to the browser console. */\n debug?: boolean;\n user?: OAIQUser;\n};\n\n/**\n * Fourth argument of `oaiq(\"measure\", ...)`. Required for custom events (carries\n * `custom_event_name`); optional for standard events.\n */\nexport type MeasureOptions = {\n /** Shared with the Conversions API to deduplicate the same conversion across browser and server. */\n event_id?: string;\n /** Required for custom events; 1-64 chars of letters, numbers, underscores, or dashes. */\n custom_event_name?: string;\n /** When true, opts the event out of user-level personalization. */\n opt_out?: boolean;\n};\n\nexport interface OAIQ {\n oaiq(command: 'init', config: InitConfig): void;\n oaiq<T extends StandardEvent>(\n command: 'measure',\n event: T,\n data: StandardEvents[T],\n options?: MeasureOptions\n ): void;\n oaiq(\n command: 'measure',\n event: 'custom',\n data: CustomEventData,\n options: MeasureOptions & { custom_event_name: string }\n ): void;\n}\n\n/**\n * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.\n * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),\n * which can differ from strict ISO 4217 exponents.\n * https://docs.stripe.com/currencies#zero-decimal\n */\nconst ZERO_DECIMAL_CURRENCIES = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'ISK',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n]);\nconst THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']);\n\n/** Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). */\nexport function toMinorUnits(value?: number | null, currency?: string): number | undefined {\n if (value === undefined || value === null || Number.isNaN(value)) return undefined;\n const code = currency?.toUpperCase();\n const exponent =\n code && ZERO_DECIMAL_CURRENCIES.has(code)\n ? 0\n : code && THREE_DECIMAL_CURRENCIES.has(code)\n ? 3\n : 2;\n return Math.round(value * 10 ** exponent);\n}\n\nexport function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined {\n if (!items || items.length === 0) return undefined;\n const code = currency?.toUpperCase();\n return items.map((item) => ({\n id: item.item_id,\n name: item.item_name,\n content_type: item.item_category,\n quantity: item.quantity,\n amount: toMinorUnits(item.price, currency),\n currency: item.price !== undefined ? code : undefined,\n }));\n}\n\n/** Safely read a property off loosely typed track properties without widening to `any`. */\nfunction prop(properties: unknown, key: string): unknown {\n return typeof properties === 'object' && properties !== null\n ? (properties as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction readValue(properties: unknown): number | undefined {\n const value = prop(properties, 'value');\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction readCurrency(properties: unknown): string | undefined {\n const currency = prop(properties, 'currency');\n return typeof currency === 'string' ? currency : undefined;\n}\n\nfunction readItems(properties: unknown): Item[] | undefined {\n const items = prop(properties, 'items');\n return Array.isArray(items) ? (items as Item[]) : undefined;\n}\n\nfunction contentsData(properties: unknown): ContentsData {\n const currency = readCurrency(properties);\n return {\n type: 'contents',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n };\n}\n\nfunction customerActionData(properties: unknown): CustomerActionData {\n const currency = readCurrency(properties);\n return {\n type: 'customer_action',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n };\n}\n\nfunction planEnrollmentData(properties: unknown): PlanEnrollmentData {\n const currency = readCurrency(properties);\n const items = readItems(properties);\n return {\n type: 'plan_enrollment',\n plan_id: items?.[0]?.item_id,\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(items, currency),\n };\n}\n\nexport type MappedOAIEvent = {\n type: StandardEvent | 'custom';\n data: EventData;\n};\n\n/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */\nexport function mapOAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): MappedOAIEvent {\n switch (name) {\n case 'page_view':\n return { type: 'page_viewed', data: contentsData(properties) };\n case 'view_item':\n case 'view_item_list':\n return { type: 'contents_viewed', data: contentsData(properties) };\n case 'begin_checkout':\n return { type: 'checkout_started', data: contentsData(properties) };\n case 'add_to_cart':\n return { type: 'items_added', data: contentsData(properties) };\n case 'purchase':\n return { type: 'order_created', data: contentsData(properties) };\n case 'generate_lead':\n case 'qualify_lead':\n return { type: 'lead_created', data: customerActionData(properties) };\n case 'sign_up':\n case 'login':\n return { type: 'registration_completed', data: customerActionData(properties) };\n case 'schedule':\n return { type: 'appointment_scheduled', data: customerActionData(properties) };\n case 'subscribe':\n return { type: 'subscription_created', data: planEnrollmentData(properties) };\n case 'trial_begin':\n return { type: 'trial_started', data: planEnrollmentData(properties) };\n default: {\n const currency = readCurrency(properties);\n return {\n type: 'custom',\n data: {\n type: 'custom',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n },\n };\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"oaiq.cjs","names":[],"sources":["../../src/track/oaiq.ts"],"sourcesContent":["import type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport const NON_AD_EVENTS = [\n // metrics\n 'CLS',\n 'FCP',\n 'FID',\n 'INP',\n 'LCP',\n 'TTFB',\n // promotions\n 'view_promotion',\n 'select_promotion',\n];\n\n/**\n * Content item shared by `contents` and `plan_enrollment` events.\n * https://developers.openai.com/ads/supported-events\n */\nexport type ContentItem = {\n id?: string;\n name?: string;\n content_type?: string;\n quantity?: number;\n /** Per-item monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n};\n\nexport type ContentsData = {\n type: 'contents';\n /** Monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomerActionData = {\n type: 'customer_action';\n amount?: number;\n currency?: string;\n};\n\nexport type PlanEnrollmentData = {\n type: 'plan_enrollment';\n plan_id?: string;\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomEventData = {\n type: 'custom';\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type EventData = ContentsData | CustomerActionData | PlanEnrollmentData | CustomEventData;\n\n/**\n * Standard event names and the `data` shape each one expects.\n * https://developers.openai.com/ads/supported-events\n */\nexport type StandardEvents = {\n appointment_scheduled: CustomerActionData;\n checkout_started: ContentsData;\n contents_viewed: ContentsData;\n items_added: ContentsData;\n lead_created: CustomerActionData;\n order_created: ContentsData;\n page_viewed: ContentsData;\n registration_completed: CustomerActionData;\n subscription_created: PlanEnrollmentData;\n trial_started: PlanEnrollmentData;\n};\n\nexport type StandardEvent = keyof StandardEvents;\n\nconst STANDARD_EVENTS: readonly string[] = [\n 'appointment_scheduled',\n 'checkout_started',\n 'contents_viewed',\n 'items_added',\n 'lead_created',\n 'order_created',\n 'page_viewed',\n 'registration_completed',\n 'subscription_created',\n 'trial_started',\n] satisfies StandardEvent[];\n\n/**\n * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,\n * starting and ending with a letter or digit, not a standard event name; the API lowercases it.\n * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when\n * nothing usable is left — the event is then not sent at all. The pixel and the server both name\n * a custom event through this, so the two still deduplicate.\n * https://developers.openai.com/ads/supported-events\n */\nexport function oaiCustomEventName(name: string): string | undefined {\n const sanitized = name\n .toLowerCase()\n .replace(/[^a-z0-9_-]+/g, '_')\n .slice(0, 64)\n .replace(/^[_-]+|[_-]+$/g, '');\n if (!sanitized || STANDARD_EVENTS.includes(sanitized)) return undefined;\n return sanitized;\n}\n\n/**\n * Identity fields passed to `oaiq(\"init\", ...)` for conversion matching. Identifiers are\n * normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.\n * https://developers.openai.com/ads/measurement-pixel#send-user-data\n */\nexport type OAIQUser = {\n email_sha256?: string;\n /** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */\n phone_number_sha256?: string;\n external_id_sha256?: string;\n /** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */\n first_name_sha256?: string;\n last_name_sha256?: string;\n /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n country?: string;\n /** Max 128 characters; OpenAI trims and lowercases it. */\n city?: string;\n /** State, province or region, max 128 characters. */\n region?: string;\n postal_code?: string;\n};\n\n/**\n * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then\n * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.\n */\nexport function normalizeOAIPhone(phone: string): string | undefined {\n const digits = phone\n .replace(/[\\s().-]/g, '')\n .replace(/^\\+/, '')\n .replace(/^0+/, '');\n return /^\\d{8,15}$/.test(digits) ? digits : undefined;\n}\n\n/** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */\nexport function normalizeOAIName(name: string | undefined): string | undefined {\n return name?.toLowerCase().replace(/[\\s!-/:-@[-`{-~]/g, '') || undefined;\n}\n\nexport type InitConfig = {\n pixelId: string;\n /** Log SDK activity to the browser console. */\n debug?: boolean;\n user?: OAIQUser;\n};\n\n/**\n * Fourth argument of `oaiq(\"measure\", ...)`. Required for custom events (carries\n * `custom_event_name`); optional for standard events.\n */\nexport type MeasureOptions = {\n /** Shared with the Conversions API to deduplicate the same conversion across browser and server. */\n event_id?: string;\n /** Required for custom events; 1-64 chars of letters, numbers, underscores, or dashes. */\n custom_event_name?: string;\n /** When true, opts the event out of user-level personalization. */\n opt_out?: boolean;\n};\n\nexport interface OAIQ {\n oaiq(command: 'init', config: InitConfig): void;\n oaiq<T extends StandardEvent>(\n command: 'measure',\n event: T,\n data: StandardEvents[T],\n options?: MeasureOptions\n ): void;\n oaiq(\n command: 'measure',\n event: 'custom',\n data: CustomEventData,\n options: MeasureOptions & { custom_event_name: string }\n ): void;\n}\n\n/**\n * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.\n * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),\n * which can differ from strict ISO 4217 exponents.\n * https://docs.stripe.com/currencies#zero-decimal\n */\nconst ZERO_DECIMAL_CURRENCIES = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'ISK',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n]);\nconst THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']);\n\n/**\n * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None\n * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every\n * `amount` — an amount alone would be invalid, not just imprecise.\n */\nexport function toMinorUnits(value?: number | null, currency?: string): number | undefined {\n if (value === undefined || value === null || Number.isNaN(value)) return undefined;\n const code = currency?.toUpperCase();\n if (!code) return undefined;\n const exponent =\n code && ZERO_DECIMAL_CURRENCIES.has(code)\n ? 0\n : code && THREE_DECIMAL_CURRENCIES.has(code)\n ? 3\n : 2;\n return Math.round(value * 10 ** exponent);\n}\n\nexport function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined {\n if (!items || items.length === 0) return undefined;\n const code = currency?.toUpperCase();\n return items.map((item) => {\n const amount = toMinorUnits(item.price, currency);\n return {\n id: item.item_id,\n name: item.item_name,\n content_type: item.item_category,\n quantity: item.quantity,\n amount,\n currency: amount !== undefined ? code : undefined,\n };\n });\n}\n\n/** Safely read a property off loosely typed track properties without widening to `any`. */\nfunction prop(properties: unknown, key: string): unknown {\n return typeof properties === 'object' && properties !== null\n ? (properties as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction readValue(properties: unknown): number | undefined {\n const value = prop(properties, 'value');\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction readCurrency(properties: unknown): string | undefined {\n const currency = prop(properties, 'currency');\n return typeof currency === 'string' ? currency : undefined;\n}\n\nfunction readItems(properties: unknown): Item[] | undefined {\n const items = prop(properties, 'items');\n return Array.isArray(items) ? (items as Item[]) : undefined;\n}\n\nfunction contentsData(properties: unknown): ContentsData {\n const currency = readCurrency(properties);\n return {\n type: 'contents',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n };\n}\n\nfunction customerActionData(properties: unknown): CustomerActionData {\n const currency = readCurrency(properties);\n return {\n type: 'customer_action',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n };\n}\n\nfunction planEnrollmentData(properties: unknown): PlanEnrollmentData {\n const currency = readCurrency(properties);\n const items = readItems(properties);\n return {\n type: 'plan_enrollment',\n plan_id: items?.[0]?.item_id,\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(items, currency),\n };\n}\n\nexport type MappedOAIEvent = {\n type: StandardEvent | 'custom';\n data: EventData;\n};\n\n/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */\nexport function mapOAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): MappedOAIEvent {\n switch (name) {\n case 'page_view':\n return { type: 'page_viewed', data: contentsData(properties) };\n case 'view_item':\n case 'view_item_list':\n return { type: 'contents_viewed', data: contentsData(properties) };\n case 'begin_checkout':\n return { type: 'checkout_started', data: contentsData(properties) };\n case 'add_to_cart':\n return { type: 'items_added', data: contentsData(properties) };\n case 'purchase':\n return { type: 'order_created', data: contentsData(properties) };\n case 'generate_lead':\n case 'qualify_lead':\n return { type: 'lead_created', data: customerActionData(properties) };\n case 'sign_up':\n case 'login':\n return { type: 'registration_completed', data: customerActionData(properties) };\n case 'schedule':\n return { type: 'appointment_scheduled', data: customerActionData(properties) };\n case 'subscribe':\n return { type: 'subscription_created', data: planEnrollmentData(properties) };\n case 'trial_begin':\n return { type: 'trial_started', data: planEnrollmentData(properties) };\n default: {\n const currency = readCurrency(properties);\n return {\n type: 'custom',\n data: {\n type: 'custom',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n },\n };\n }\n }\n}\n"],"mappings":"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"}
@@ -59,20 +59,42 @@ export type StandardEvents = {
59
59
  };
60
60
  export type StandardEvent = keyof StandardEvents;
61
61
  /**
62
- * Identity fields passed to `oaiq("init", ...)` for conversion matching. Email and external id
63
- * must be pre-hashed as lowercase 64-char SHA-256 hex strings; the rest are sent as raw values.
64
- * https://developers.openai.com/ads/measurement-pixel
62
+ * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,
63
+ * starting and ending with a letter or digit, not a standard event name; the API lowercases it.
64
+ * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when
65
+ * nothing usable is left — the event is then not sent at all. The pixel and the server both name
66
+ * a custom event through this, so the two still deduplicate.
67
+ * https://developers.openai.com/ads/supported-events
68
+ */
69
+ export declare function oaiCustomEventName(name: string): string | undefined;
70
+ /**
71
+ * Identity fields passed to `oaiq("init", ...)` for conversion matching. Identifiers are
72
+ * normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.
73
+ * https://developers.openai.com/ads/measurement-pixel#send-user-data
65
74
  */
66
75
  export type OAIQUser = {
67
76
  email_sha256?: string;
77
+ /** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */
78
+ phone_number_sha256?: string;
68
79
  external_id_sha256?: string;
80
+ /** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */
81
+ first_name_sha256?: string;
82
+ last_name_sha256?: string;
69
83
  /** Two-letter ISO 3166-1 country code (e.g. "US"). */
70
84
  country?: string;
71
- /** Lowercased, max 128 characters. */
85
+ /** Max 128 characters; OpenAI trims and lowercases it. */
72
86
  city?: string;
73
- /** Max 32 characters. */
74
- zip_code?: string;
87
+ /** State, province or region, max 128 characters. */
88
+ region?: string;
89
+ postal_code?: string;
75
90
  };
91
+ /**
92
+ * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then
93
+ * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.
94
+ */
95
+ export declare function normalizeOAIPhone(phone: string): string | undefined;
96
+ /** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */
97
+ export declare function normalizeOAIName(name: string | undefined): string | undefined;
76
98
  export type InitConfig = {
77
99
  pixelId: string;
78
100
  /** Log SDK activity to the browser console. */
@@ -98,7 +120,11 @@ export interface OAIQ {
98
120
  custom_event_name: string;
99
121
  }): void;
100
122
  }
101
- /** Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). */
123
+ /**
124
+ * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None
125
+ * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every
126
+ * `amount` — an amount alone would be invalid, not just imprecise.
127
+ */
102
128
  export declare function toMinorUnits(value?: number | null, currency?: string): number | undefined;
103
129
  export declare function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined;
104
130
  export type MappedOAIEvent = {
@@ -1 +1 @@
1
- {"version":3,"file":"oaiq.d.cts","names":[],"sources":["../../src/track/oaiq.ts"],"mappings":";;;qBAGa;;;;;YAiBD;EACV;EACA;EACA;EACA;;EAEA;EACA;;YAGU;EACV;;EAEA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;EACA,WAAW;;YAGD,YAAY,eAAe,qBAAqB,qBAAqB;;;;;YAMrE;EACV,uBAAuB;EACvB,kBAAkB;EAClB,iBAAiB;EACjB,aAAa;EACb,cAAc;EACd,eAAe;EACf,aAAa;EACb,wBAAwB;EACxB,sBAAsB;EACtB,eAAe;;YAGL,sBAAsB;;;;;;YAOtB;EACV;EACA;;EAEA;;EAEA;;EAEA;;YAGU;EACV;;EAEA;EACA,OAAO;;;;;;YAOG;;EAEV;;EAEA;;EAEA;;iBAGe;EACf,KAAK,iBAAiB,QAAQ;EAC9B,KAAK,UAAU,eACb,oBACA,OAAO,GACP,MAAM,eAAe,IACrB,UAAU;EAEZ,KACE,oBACA,iBACA,MAAM,iBACN,SAAS;IAAmB;;;;wBAgChB,aAAa,uBAAuB;wBAYpC,YAAY,QAAQ,QAAQ,oBAAoB;YAkEpD;EACV,MAAM;EACN,MAAM;;;wBAIQ,YAAY,UAAU,WACpC,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B"}
1
+ {"version":3,"file":"oaiq.d.cts","names":[],"sources":["../../src/track/oaiq.ts"],"mappings":";;;qBAGa;;;;;YAiBD;EACV;EACA;EACA;EACA;;EAEA;EACA;;YAGU;EACV;;EAEA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;EACA,WAAW;;YAGD,YAAY,eAAe,qBAAqB,qBAAqB;;;;;YAMrE;EACV,uBAAuB;EACvB,kBAAkB;EAClB,iBAAiB;EACjB,aAAa;EACb,cAAc;EACd,eAAe;EACf,aAAa;EACb,wBAAwB;EACxB,sBAAsB;EACtB,eAAe;;YAGL,sBAAsB;;;;;;;;;wBAuBlB,mBAAmB;;;;;;YAevB;EACV;;EAEA;EACA;;EAEA;EACA;;EAEA;;EAEA;;EAEA;EACA;;;;;;wBAOc,kBAAkB;;wBASlB,iBAAiB;YAIrB;EACV;;EAEA;EACA,OAAO;;;;;;YAOG;;EAEV;;EAEA;;EAEA;;iBAGe;EACf,KAAK,iBAAiB,QAAQ;EAC9B,KAAK,UAAU,eACb,oBACA,OAAO,GACP,MAAM,eAAe,IACrB,UAAU;EAEZ,KACE,oBACA,iBACA,MAAM,iBACN,SAAS;IAAmB;;;;;;;;wBAoChB,aAAa,uBAAuB;wBAapC,YAAY,QAAQ,QAAQ,oBAAoB;YAqEpD;EACV,MAAM;EACN,MAAM;;;wBAIQ,YAAY,UAAU,WACpC,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B"}