@hellyeah/x-ray 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -0
- package/dist/astro/index.cjs +48 -0
- package/dist/astro/index.d.cts +36 -0
- package/dist/astro/index.d.ts +36 -0
- package/dist/astro/index.js +6 -0
- package/dist/index.cjs +49 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +7 -0
- package/dist/next/index.cjs +145 -0
- package/dist/next/index.d.cts +32 -0
- package/dist/next/index.d.ts +32 -0
- package/dist/next/index.js +105 -0
- package/dist/nuxt/index.cjs +140 -0
- package/dist/nuxt/index.d.cts +33 -0
- package/dist/nuxt/index.d.ts +33 -0
- package/dist/nuxt/index.js +97 -0
- package/dist/react/index.cjs +129 -0
- package/dist/react/index.d.cts +31 -0
- package/dist/react/index.d.ts +31 -0
- package/dist/react/index.js +89 -0
- package/dist/remix/index.cjs +137 -0
- package/dist/remix/index.d.cts +32 -0
- package/dist/remix/index.d.ts +32 -0
- package/dist/remix/index.js +95 -0
- package/dist/server/index.cjs +519 -0
- package/dist/server/index.d.cts +234 -0
- package/dist/server/index.d.ts +234 -0
- package/dist/server/index.js +476 -0
- package/dist/svelte/index.cjs +120 -0
- package/dist/svelte/index.d.cts +31 -0
- package/dist/svelte/index.d.ts +31 -0
- package/dist/svelte/index.js +77 -0
- package/dist/vue/index.cjs +136 -0
- package/dist/vue/index.d.cts +32 -0
- package/dist/vue/index.d.ts +32 -0
- package/dist/vue/index.js +95 -0
- package/package.json +255 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
var import_node_module = require("node:module");
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
function __accessProp(key) {
|
|
7
|
+
return this[key];
|
|
8
|
+
}
|
|
9
|
+
var __toCommonJS = (from) => {
|
|
10
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
11
|
+
if (entry)
|
|
12
|
+
return entry;
|
|
13
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (var key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(entry, key))
|
|
17
|
+
__defProp(entry, key, {
|
|
18
|
+
get: __accessProp.bind(from, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
__moduleCache.set(from, entry);
|
|
23
|
+
return entry;
|
|
24
|
+
};
|
|
25
|
+
var __moduleCache;
|
|
26
|
+
var __returnValue = (v) => v;
|
|
27
|
+
function __exportSetter(name, newValue) {
|
|
28
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
29
|
+
}
|
|
30
|
+
var __export = (target, all) => {
|
|
31
|
+
for (var name in all)
|
|
32
|
+
__defProp(target, name, {
|
|
33
|
+
get: all[name],
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
set: __exportSetter.bind(all, name)
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/server/index.ts
|
|
41
|
+
var exports_server = {};
|
|
42
|
+
__export(exports_server, {
|
|
43
|
+
cv: () => cv,
|
|
44
|
+
createXRay: () => createXRay,
|
|
45
|
+
XRay: () => XRay
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(exports_server);
|
|
48
|
+
|
|
49
|
+
// src/constants.ts
|
|
50
|
+
var cv = {
|
|
51
|
+
purchase: "cv_purchase",
|
|
52
|
+
subscribe: "cv_subscribe",
|
|
53
|
+
startTrial: "cv_start_trial",
|
|
54
|
+
leadSubmit: "cv_lead_submit",
|
|
55
|
+
registrationComplete: "cv_registration_complete",
|
|
56
|
+
bookAppointment: "cv_book_appointment",
|
|
57
|
+
contact: "cv_contact",
|
|
58
|
+
submitApplication: "cv_submit_application",
|
|
59
|
+
addToCart: "cv_add_to_cart",
|
|
60
|
+
beginCheckout: "cv_begin_checkout",
|
|
61
|
+
addPaymentInfo: "cv_add_payment_info",
|
|
62
|
+
viewContent: "cv_view_content",
|
|
63
|
+
search: "cv_search"
|
|
64
|
+
};
|
|
65
|
+
// src/server/types.ts
|
|
66
|
+
var isUnrefable = (value) => typeof value === "object" && value !== null && ("unref" in value);
|
|
67
|
+
|
|
68
|
+
// src/server/index.ts
|
|
69
|
+
var DEFAULTS = {
|
|
70
|
+
flushAt: 20,
|
|
71
|
+
flushInterval: 1e4,
|
|
72
|
+
maxQueueSize: 1000,
|
|
73
|
+
requestTimeout: 1e4,
|
|
74
|
+
retryDelay: 3000
|
|
75
|
+
};
|
|
76
|
+
var PROPERTY_LIMITS = {
|
|
77
|
+
maxProperties: 20,
|
|
78
|
+
maxKeyLength: 100,
|
|
79
|
+
maxStringValueLength: 500
|
|
80
|
+
};
|
|
81
|
+
var FLAT_TYPES = new Set(["string", "number", "boolean"]);
|
|
82
|
+
var RETRY_ATTEMPTS = 3;
|
|
83
|
+
var REVENUE_WARNING_THRESHOLD = 1e4;
|
|
84
|
+
var CURRENCY_PATTERN = /^[A-Z]{3}$/;
|
|
85
|
+
var TRAILING_SLASH = /\/$/;
|
|
86
|
+
var SERVERLESS_ENV_KEYS = [
|
|
87
|
+
"VERCEL",
|
|
88
|
+
"AWS_LAMBDA_FUNCTION_NAME",
|
|
89
|
+
"NETLIFY",
|
|
90
|
+
"CLOUDFLARE_WORKER"
|
|
91
|
+
];
|
|
92
|
+
var DATA_KEYS = ["currency", "tag", "url", "hostname"];
|
|
93
|
+
var ATTRIBUTION_KEYS = [
|
|
94
|
+
"utm_source",
|
|
95
|
+
"utm_medium",
|
|
96
|
+
"utm_campaign",
|
|
97
|
+
"utm_content",
|
|
98
|
+
"utm_term",
|
|
99
|
+
"gclid",
|
|
100
|
+
"fbclid",
|
|
101
|
+
"msclkid",
|
|
102
|
+
"ttclid",
|
|
103
|
+
"li_fat_id",
|
|
104
|
+
"twclid"
|
|
105
|
+
];
|
|
106
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
107
|
+
var WELL_KNOWN_CONTEXT_SYMBOLS = [
|
|
108
|
+
Symbol.for("@vercel/request-context"),
|
|
109
|
+
Symbol.for("@next/request-context")
|
|
110
|
+
];
|
|
111
|
+
var discoverWaitUntil = () => {
|
|
112
|
+
const g = globalThis;
|
|
113
|
+
for (const sym of WELL_KNOWN_CONTEXT_SYMBOLS) {
|
|
114
|
+
const waitUntil = g[sym]?.get?.()?.waitUntil;
|
|
115
|
+
if (waitUntil)
|
|
116
|
+
return waitUntil;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
class XRay {
|
|
122
|
+
queue = [];
|
|
123
|
+
timer = null;
|
|
124
|
+
defaultDistinctId;
|
|
125
|
+
logFn;
|
|
126
|
+
flushing = false;
|
|
127
|
+
fetchFn;
|
|
128
|
+
shutdownCalled = false;
|
|
129
|
+
cfg;
|
|
130
|
+
constructor(websiteId, options) {
|
|
131
|
+
const resolvedWebsiteId = websiteId ?? process.env.XRAY_WEBSITE_ID ?? "";
|
|
132
|
+
const resolvedHost = options?.host ?? process.env.XRAY_HOST ?? "https://xray-staging.hellyeahai.com";
|
|
133
|
+
if (!resolvedWebsiteId) {
|
|
134
|
+
console.warn("[x-ray] websiteId is missing. Pass it to the constructor or set XRAY_WEBSITE_ID env var.");
|
|
135
|
+
}
|
|
136
|
+
this.cfg = {
|
|
137
|
+
websiteId: resolvedWebsiteId,
|
|
138
|
+
host: resolvedHost.replace(TRAILING_SLASH, ""),
|
|
139
|
+
flushAt: options?.flushAt ?? DEFAULTS.flushAt,
|
|
140
|
+
flushInterval: options?.flushInterval ?? DEFAULTS.flushInterval,
|
|
141
|
+
maxQueueSize: options?.maxQueueSize ?? DEFAULTS.maxQueueSize,
|
|
142
|
+
requestTimeout: options?.requestTimeout ?? DEFAULTS.requestTimeout,
|
|
143
|
+
disabled: options?.disabled ?? false,
|
|
144
|
+
retryDelay: options?.retryDelay ?? DEFAULTS.retryDelay,
|
|
145
|
+
context: options?.context ?? {},
|
|
146
|
+
waitUntil: options?.waitUntil ?? null
|
|
147
|
+
};
|
|
148
|
+
this.fetchFn = options?.fetch ?? globalThis.fetch;
|
|
149
|
+
this.logFn = options?.logger ?? null;
|
|
150
|
+
if (!this.cfg.disabled) {
|
|
151
|
+
this.timer = setInterval(() => {
|
|
152
|
+
const p = this.flush().catch(() => {});
|
|
153
|
+
const wU = this.resolveWaitUntil();
|
|
154
|
+
if (wU)
|
|
155
|
+
wU(p);
|
|
156
|
+
}, this.cfg.flushInterval);
|
|
157
|
+
if (isUnrefable(this.timer)) {
|
|
158
|
+
this.timer.unref();
|
|
159
|
+
}
|
|
160
|
+
if (!(this.cfg.waitUntil || discoverWaitUntil())) {
|
|
161
|
+
const isServerless = SERVERLESS_ENV_KEYS.some((k) => typeof process !== "undefined" && (k in process.env));
|
|
162
|
+
if (isServerless) {
|
|
163
|
+
this.log({
|
|
164
|
+
action: "serverless_detect",
|
|
165
|
+
outcome: "warning",
|
|
166
|
+
message: "Serverless environment detected without waitUntil — batched track() events may be lost. Pass waitUntil in options or use trackImmediate()."
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
track(event, data) {
|
|
173
|
+
try {
|
|
174
|
+
if (this.cfg.disabled) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const distinctId = data?.distinctId ?? this.defaultDistinctId;
|
|
178
|
+
if (!(event && distinctId)) {
|
|
179
|
+
this.log({
|
|
180
|
+
action: "track",
|
|
181
|
+
outcome: "drop",
|
|
182
|
+
message: "missing event name or distinctId",
|
|
183
|
+
event: event || undefined
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const item = this.buildItem(event, distinctId, data);
|
|
188
|
+
this.enqueue(item);
|
|
189
|
+
this.log({
|
|
190
|
+
action: "track",
|
|
191
|
+
outcome: "success",
|
|
192
|
+
message: "queued",
|
|
193
|
+
event
|
|
194
|
+
});
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this.log({
|
|
197
|
+
action: "track",
|
|
198
|
+
outcome: "error",
|
|
199
|
+
message: "track error",
|
|
200
|
+
error: err
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async trackImmediate(event, data) {
|
|
205
|
+
try {
|
|
206
|
+
if (this.cfg.disabled) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const distinctId = data?.distinctId ?? this.defaultDistinctId;
|
|
210
|
+
if (!(event && distinctId)) {
|
|
211
|
+
this.log({
|
|
212
|
+
action: "track_immediate",
|
|
213
|
+
outcome: "drop",
|
|
214
|
+
message: "missing event name or distinctId",
|
|
215
|
+
event: event || undefined
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const item = this.buildItem(event, distinctId, data);
|
|
220
|
+
await this.sendBatch([item]);
|
|
221
|
+
this.log({
|
|
222
|
+
action: "track_immediate",
|
|
223
|
+
outcome: "success",
|
|
224
|
+
message: "sent immediate",
|
|
225
|
+
event
|
|
226
|
+
});
|
|
227
|
+
} catch (err) {
|
|
228
|
+
this.log({
|
|
229
|
+
action: "track_immediate",
|
|
230
|
+
outcome: "error",
|
|
231
|
+
message: "trackImmediate error",
|
|
232
|
+
error: err
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
identify(params) {
|
|
237
|
+
try {
|
|
238
|
+
this.defaultDistinctId = params.distinctId;
|
|
239
|
+
this.log({
|
|
240
|
+
action: "identify",
|
|
241
|
+
outcome: "success",
|
|
242
|
+
message: "identified"
|
|
243
|
+
});
|
|
244
|
+
} catch (err) {
|
|
245
|
+
this.log({
|
|
246
|
+
action: "identify",
|
|
247
|
+
outcome: "error",
|
|
248
|
+
message: "identify error",
|
|
249
|
+
error: err
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async flush() {
|
|
254
|
+
try {
|
|
255
|
+
if (this.cfg.disabled)
|
|
256
|
+
return;
|
|
257
|
+
if (this.flushing || this.queue.length === 0)
|
|
258
|
+
return;
|
|
259
|
+
this.flushing = true;
|
|
260
|
+
while (this.queue.length > 0) {
|
|
261
|
+
const items = this.queue.splice(0, this.cfg.flushAt);
|
|
262
|
+
try {
|
|
263
|
+
await this.sendBatch(items);
|
|
264
|
+
this.log({
|
|
265
|
+
action: "flush",
|
|
266
|
+
outcome: "success",
|
|
267
|
+
message: "flushed",
|
|
268
|
+
count: items.length
|
|
269
|
+
});
|
|
270
|
+
} catch (err) {
|
|
271
|
+
this.queue.unshift(...items);
|
|
272
|
+
this.log({
|
|
273
|
+
action: "flush",
|
|
274
|
+
outcome: "error",
|
|
275
|
+
message: "re-queued events after send failure",
|
|
276
|
+
count: items.length,
|
|
277
|
+
error: err
|
|
278
|
+
});
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
this.flushing = false;
|
|
283
|
+
} catch (err) {
|
|
284
|
+
this.flushing = false;
|
|
285
|
+
this.log({
|
|
286
|
+
action: "flush",
|
|
287
|
+
outcome: "error",
|
|
288
|
+
message: "flush error",
|
|
289
|
+
error: err
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async shutdown(timeoutMs = 5000) {
|
|
294
|
+
try {
|
|
295
|
+
if (this.shutdownCalled) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.shutdownCalled = true;
|
|
299
|
+
if (this.timer) {
|
|
300
|
+
clearInterval(this.timer);
|
|
301
|
+
this.timer = null;
|
|
302
|
+
}
|
|
303
|
+
await Promise.race([this.flush(), sleep(timeoutMs)]);
|
|
304
|
+
this.log({
|
|
305
|
+
action: "shutdown",
|
|
306
|
+
outcome: "success",
|
|
307
|
+
message: "shutdown complete"
|
|
308
|
+
});
|
|
309
|
+
} catch (err) {
|
|
310
|
+
this.log({
|
|
311
|
+
action: "shutdown",
|
|
312
|
+
outcome: "error",
|
|
313
|
+
message: "shutdown error",
|
|
314
|
+
error: err
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
debug(enabled) {
|
|
319
|
+
if (enabled) {
|
|
320
|
+
this.logFn = (entry) => console.log("[x-ray]", JSON.stringify(entry));
|
|
321
|
+
} else {
|
|
322
|
+
this.logFn = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
enqueue(item) {
|
|
326
|
+
if (this.queue.length >= this.cfg.maxQueueSize) {
|
|
327
|
+
this.queue.shift();
|
|
328
|
+
this.log({
|
|
329
|
+
action: "enqueue",
|
|
330
|
+
outcome: "overflow",
|
|
331
|
+
message: "queue full, dropped oldest event",
|
|
332
|
+
count: this.queue.length
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
this.queue.push(item);
|
|
336
|
+
if (this.queue.length >= this.cfg.flushAt) {
|
|
337
|
+
const trigger = () => {
|
|
338
|
+
const p = this.flush().catch(() => {});
|
|
339
|
+
const wU = this.resolveWaitUntil();
|
|
340
|
+
if (wU)
|
|
341
|
+
wU(p);
|
|
342
|
+
};
|
|
343
|
+
queueMicrotask(trigger);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async sendBatch(items) {
|
|
347
|
+
const payload = items.map((item) => ({
|
|
348
|
+
type: "event",
|
|
349
|
+
payload: item
|
|
350
|
+
}));
|
|
351
|
+
for (let attempt = 0;attempt < RETRY_ATTEMPTS; attempt++) {
|
|
352
|
+
const controller = new AbortController;
|
|
353
|
+
const timeout = setTimeout(() => controller.abort(), this.cfg.requestTimeout);
|
|
354
|
+
if (isUnrefable(timeout))
|
|
355
|
+
timeout.unref();
|
|
356
|
+
try {
|
|
357
|
+
const res = await this.fetchFn(`${this.cfg.host}/api/batch`, {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: {
|
|
360
|
+
"Content-Type": "application/json",
|
|
361
|
+
"X-HY-Source": "server-sdk"
|
|
362
|
+
},
|
|
363
|
+
body: JSON.stringify(payload),
|
|
364
|
+
signal: controller.signal
|
|
365
|
+
});
|
|
366
|
+
clearTimeout(timeout);
|
|
367
|
+
if (res.ok) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (res.status >= 500) {
|
|
371
|
+
this.log({
|
|
372
|
+
action: "send_batch",
|
|
373
|
+
outcome: "retry",
|
|
374
|
+
message: "server error, retrying",
|
|
375
|
+
statusCode: res.status,
|
|
376
|
+
attempt: attempt + 1,
|
|
377
|
+
maxAttempts: RETRY_ATTEMPTS
|
|
378
|
+
});
|
|
379
|
+
if (attempt < RETRY_ATTEMPTS - 1) {
|
|
380
|
+
await sleep(this.cfg.retryDelay);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
this.log({
|
|
384
|
+
action: "send_batch",
|
|
385
|
+
outcome: "drop",
|
|
386
|
+
message: "dropping batch after retries exhausted",
|
|
387
|
+
statusCode: res.status,
|
|
388
|
+
attempt: attempt + 1,
|
|
389
|
+
maxAttempts: RETRY_ATTEMPTS
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
this.log({
|
|
394
|
+
action: "send_batch",
|
|
395
|
+
outcome: "drop",
|
|
396
|
+
message: "client error, dropping batch",
|
|
397
|
+
statusCode: res.status
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
} catch (err) {
|
|
401
|
+
clearTimeout(timeout);
|
|
402
|
+
if (attempt < RETRY_ATTEMPTS - 1 && !controller.signal.aborted) {
|
|
403
|
+
this.log({
|
|
404
|
+
action: "send_batch",
|
|
405
|
+
outcome: "retry",
|
|
406
|
+
message: "network error, retrying",
|
|
407
|
+
attempt: attempt + 1,
|
|
408
|
+
maxAttempts: RETRY_ATTEMPTS,
|
|
409
|
+
error: err
|
|
410
|
+
});
|
|
411
|
+
await sleep(this.cfg.retryDelay);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
this.log({
|
|
415
|
+
action: "send_batch",
|
|
416
|
+
outcome: "drop",
|
|
417
|
+
message: "dropping batch",
|
|
418
|
+
error: err
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
buildItem(event, distinctId, data) {
|
|
425
|
+
const item = {
|
|
426
|
+
website: this.cfg.websiteId,
|
|
427
|
+
name: event,
|
|
428
|
+
id: distinctId,
|
|
429
|
+
hy_event_id: crypto.randomUUID()
|
|
430
|
+
};
|
|
431
|
+
if (data) {
|
|
432
|
+
if (data.revenue !== undefined) {
|
|
433
|
+
item.revenue = String(data.revenue);
|
|
434
|
+
if (data.revenue > REVENUE_WARNING_THRESHOLD) {
|
|
435
|
+
this.log({
|
|
436
|
+
action: "track",
|
|
437
|
+
outcome: "warning",
|
|
438
|
+
message: `Revenue value ${data.revenue} looks unusually high — X-Ray expects dollars (e.g., 49.99), not cents. Divide by 100 if your payment provider returns cents.`,
|
|
439
|
+
event
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (data.currency !== undefined && !CURRENCY_PATTERN.test(data.currency)) {
|
|
444
|
+
this.log({
|
|
445
|
+
action: "track",
|
|
446
|
+
outcome: "warning",
|
|
447
|
+
message: `Currency "${data.currency}" does not match ISO 4217 format (expected 3 uppercase letters, e.g., "USD").`,
|
|
448
|
+
event
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
for (const key of DATA_KEYS) {
|
|
452
|
+
if (data[key] !== undefined)
|
|
453
|
+
item[key] = data[key];
|
|
454
|
+
}
|
|
455
|
+
if (data.metadata) {
|
|
456
|
+
const { data: sanitized, warnings } = this.sanitizeProperties(data.metadata);
|
|
457
|
+
if (sanitized)
|
|
458
|
+
item.data = sanitized;
|
|
459
|
+
for (const w of warnings) {
|
|
460
|
+
this.log({ action: "track", outcome: "drop", message: w, event });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for (const key of ATTRIBUTION_KEYS) {
|
|
465
|
+
const value = this.cfg.context[key];
|
|
466
|
+
if (value !== undefined)
|
|
467
|
+
item[key] = value;
|
|
468
|
+
}
|
|
469
|
+
return item;
|
|
470
|
+
}
|
|
471
|
+
sanitizeProperties(properties) {
|
|
472
|
+
const warnings = [];
|
|
473
|
+
const result = {};
|
|
474
|
+
let count = 0;
|
|
475
|
+
for (const [rawKey, rawValue] of Object.entries(properties)) {
|
|
476
|
+
if (count >= PROPERTY_LIMITS.maxProperties) {
|
|
477
|
+
warnings.push(`Exceeded ${PROPERTY_LIMITS.maxProperties} properties, remaining dropped`);
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
const needsTruncation = rawKey.length > PROPERTY_LIMITS.maxKeyLength;
|
|
481
|
+
const key = needsTruncation ? rawKey.slice(0, PROPERTY_LIMITS.maxKeyLength) : rawKey;
|
|
482
|
+
if (needsTruncation) {
|
|
483
|
+
warnings.push(`Property key "${key}…" truncated to ${PROPERTY_LIMITS.maxKeyLength} chars`);
|
|
484
|
+
}
|
|
485
|
+
const value = rawValue ?? null;
|
|
486
|
+
if (value !== null && !FLAT_TYPES.has(typeof value)) {
|
|
487
|
+
warnings.push(`Property "${key}" has non-flat value (${typeof value}), dropped`);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (typeof value === "string" && value.length > PROPERTY_LIMITS.maxStringValueLength) {
|
|
491
|
+
result[key] = value.slice(0, PROPERTY_LIMITS.maxStringValueLength);
|
|
492
|
+
warnings.push(`Property "${key}" value truncated to ${PROPERTY_LIMITS.maxStringValueLength} chars`);
|
|
493
|
+
} else
|
|
494
|
+
result[key] = value;
|
|
495
|
+
count++;
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
data: count > 0 ? result : undefined,
|
|
499
|
+
warnings
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
resolveWaitUntil() {
|
|
503
|
+
return this.cfg.waitUntil ?? discoverWaitUntil();
|
|
504
|
+
}
|
|
505
|
+
log(entry) {
|
|
506
|
+
if (this.logFn)
|
|
507
|
+
this.logFn(entry);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
var GLOBAL_KEY = Symbol.for("hellyeah.xray.instance");
|
|
511
|
+
var createXRay = (websiteId, options) => {
|
|
512
|
+
const g = globalThis;
|
|
513
|
+
const existing = g[GLOBAL_KEY];
|
|
514
|
+
if (existing)
|
|
515
|
+
return existing;
|
|
516
|
+
const instance = new XRay(websiteId, options);
|
|
517
|
+
g[GLOBAL_KEY] = instance;
|
|
518
|
+
return instance;
|
|
519
|
+
};
|