@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.
- package/dist/hooks/use-web-analytics.cjs +5 -1
- package/dist/hooks/use-web-analytics.cjs.map +1 -1
- package/dist/hooks/use-web-analytics.d.cts.map +1 -1
- package/dist/hooks/use-web-analytics.d.mts.map +1 -1
- package/dist/hooks/use-web-analytics.mjs +6 -2
- package/dist/hooks/use-web-analytics.mjs.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/server/google-data-manager.cjs +3 -3
- package/dist/server/google-data-manager.cjs.map +1 -1
- package/dist/server/google-data-manager.d.cts +15 -2
- package/dist/server/google-data-manager.d.cts.map +1 -1
- package/dist/server/google-data-manager.d.mts +15 -2
- package/dist/server/google-data-manager.d.mts.map +1 -1
- package/dist/server/google-data-manager.mjs +3 -3
- package/dist/server/google-data-manager.mjs.map +1 -1
- package/dist/server/index.d.cts +2 -2
- package/dist/server/index.d.mts +2 -2
- package/dist/server/linkedin-conversions-api.cjs +6 -1
- package/dist/server/linkedin-conversions-api.cjs.map +1 -1
- package/dist/server/linkedin-conversions-api.d.cts.map +1 -1
- package/dist/server/linkedin-conversions-api.d.mts.map +1 -1
- package/dist/server/linkedin-conversions-api.mjs +6 -1
- package/dist/server/linkedin-conversions-api.mjs.map +1 -1
- package/dist/server/meta-capi.cjs +7 -1
- package/dist/server/meta-capi.cjs.map +1 -1
- package/dist/server/meta-capi.d.cts.map +1 -1
- package/dist/server/meta-capi.d.mts.map +1 -1
- package/dist/server/meta-capi.mjs +7 -1
- package/dist/server/meta-capi.mjs.map +1 -1
- package/dist/server/meta-conversions-api.cjs +3 -1
- package/dist/server/meta-conversions-api.cjs.map +1 -1
- package/dist/server/meta-conversions-api.d.cts.map +1 -1
- package/dist/server/meta-conversions-api.d.mts.map +1 -1
- package/dist/server/meta-conversions-api.mjs +3 -1
- package/dist/server/meta-conversions-api.mjs.map +1 -1
- package/dist/server/microsoft-conversions-api.cjs +3 -1
- package/dist/server/microsoft-conversions-api.cjs.map +1 -1
- package/dist/server/microsoft-conversions-api.d.cts.map +1 -1
- package/dist/server/microsoft-conversions-api.d.mts.map +1 -1
- package/dist/server/microsoft-conversions-api.mjs +3 -1
- package/dist/server/microsoft-conversions-api.mjs.map +1 -1
- package/dist/server/openai-conversions-api.cjs +13 -12
- 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 +14 -13
- package/dist/server/openai-conversions-api.mjs.map +1 -1
- package/dist/server/reddit-conversions-api.cjs +6 -2
- package/dist/server/reddit-conversions-api.cjs.map +1 -1
- package/dist/server/reddit-conversions-api.d.cts +10 -1
- package/dist/server/reddit-conversions-api.d.cts.map +1 -1
- package/dist/server/reddit-conversions-api.d.mts +10 -1
- package/dist/server/reddit-conversions-api.d.mts.map +1 -1
- package/dist/server/reddit-conversions-api.mjs +6 -2
- package/dist/server/reddit-conversions-api.mjs.map +1 -1
- package/dist/setup/session.cjs +1 -1
- package/dist/setup/session.cjs.map +1 -1
- package/dist/setup/session.d.cts +6 -5
- package/dist/setup/session.d.cts.map +1 -1
- package/dist/setup/session.d.mts +6 -5
- package/dist/setup/session.d.mts.map +1 -1
- package/dist/setup/session.mjs +1 -1
- package/dist/setup/session.mjs.map +1 -1
- package/dist/third-parties/openai-pixel.cjs +29 -13
- package/dist/third-parties/openai-pixel.cjs.map +1 -1
- package/dist/third-parties/openai-pixel.d.cts +4 -3
- package/dist/third-parties/openai-pixel.d.cts.map +1 -1
- package/dist/third-parties/openai-pixel.d.mts +4 -3
- package/dist/third-parties/openai-pixel.d.mts.map +1 -1
- package/dist/third-parties/openai-pixel.mjs +30 -14
- package/dist/third-parties/openai-pixel.mjs.map +1 -1
- package/dist/track/index.cjs +156 -89
- package/dist/track/index.cjs.map +1 -1
- package/dist/track/index.d.cts +17 -0
- package/dist/track/index.d.cts.map +1 -1
- package/dist/track/index.d.mts +17 -0
- package/dist/track/index.d.mts.map +1 -1
- package/dist/track/index.mjs +156 -90
- package/dist/track/index.mjs.map +1 -1
- package/dist/track/oaiq.cjs +57 -9
- package/dist/track/oaiq.cjs.map +1 -1
- package/dist/track/oaiq.d.cts +33 -7
- package/dist/track/oaiq.d.cts.map +1 -1
- package/dist/track/oaiq.d.mts +33 -7
- package/dist/track/oaiq.d.mts.map +1 -1
- package/dist/track/oaiq.mjs +55 -10
- package/dist/track/oaiq.mjs.map +1 -1
- package/package.json +1 -1
package/dist/track/index.mjs
CHANGED
|
@@ -38,145 +38,211 @@ async function captureTags() {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
* The
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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:
|
|
88
|
-
headers
|
|
89
|
-
body
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
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 { 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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"}
|
package/dist/track/oaiq.cjs
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
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\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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"}
|
package/dist/track/oaiq.d.cts
CHANGED
|
@@ -59,20 +59,42 @@ export type StandardEvents = {
|
|
|
59
59
|
};
|
|
60
60
|
export type StandardEvent = keyof StandardEvents;
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
-
/**
|
|
85
|
+
/** Max 128 characters; OpenAI trims and lowercases it. */
|
|
72
86
|
city?: string;
|
|
73
|
-
/**
|
|
74
|
-
|
|
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
|
-
/**
|
|
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;;;;;;
|
|
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"}
|