@crelora/mark 0.0.18 → 0.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.
@@ -1,4 +1,153 @@
1
- const w = "https://ingest.onelence.com", v = /* @__PURE__ */ new Set(["event_name", "user_id", "consent_state", "source", "is_conversion"]), A = /* @__PURE__ */ new Set([
1
+ const S = "https://ingest.onelence.com";
2
+ class h extends Error {
3
+ status;
4
+ retryAfterMs;
5
+ constructor(t, e = {}) {
6
+ super(t), this.name = "TransportError", this.status = e.status, this.retryAfterMs = e.retryAfterMs;
7
+ }
8
+ }
9
+ function I(c) {
10
+ return !(typeof c != "number" || c < 400 || c >= 500 || c === 408 || c === 429);
11
+ }
12
+ function E(c) {
13
+ if (!c) return;
14
+ const t = c.trim();
15
+ if (!t) return;
16
+ const e = Number(t);
17
+ if (Number.isFinite(e) && e >= 0)
18
+ return Math.floor(e * 1e3);
19
+ const i = Date.parse(t);
20
+ if (Number.isFinite(i)) {
21
+ const s = i - Date.now();
22
+ return s > 0 ? s : 0;
23
+ }
24
+ }
25
+ const x = 5, q = 300, T = 15e3, k = 2880 * 60 * 1e3;
26
+ class D {
27
+ constructor(t, e = {}) {
28
+ this.transport = t, this.maxAttempts = e.maxAttempts ?? x, this.baseBackoffMs = e.baseBackoffMs ?? q, this.maxBackoffMs = e.maxBackoffMs ?? T, this.maxItemAgeMs = e.maxItemAgeMs ?? k, this.debug = e.debug ?? !1, this.loadPersisted = e.loadPersisted, this.savePersisted = e.savePersisted, this.onError = e.onError;
29
+ const i = this.loadPersisted?.() ?? [];
30
+ if (i.length > 0) {
31
+ const s = Date.now();
32
+ for (const r of i) {
33
+ const o = r.enqueuedAt ?? s;
34
+ if (s - o > this.maxItemAgeMs) {
35
+ this.dropped += 1;
36
+ continue;
37
+ }
38
+ this.queue.push({ ...r, enqueuedAt: o });
39
+ }
40
+ this.persist();
41
+ }
42
+ }
43
+ queue = [];
44
+ flushing = !1;
45
+ sent = 0;
46
+ failed = 0;
47
+ dropped = 0;
48
+ maxAttempts;
49
+ baseBackoffMs;
50
+ maxBackoffMs;
51
+ maxItemAgeMs;
52
+ debug;
53
+ loadPersisted;
54
+ savePersisted;
55
+ onError;
56
+ enqueue(t, e) {
57
+ this.queue.push({
58
+ path: t,
59
+ data: e,
60
+ attempts: 0,
61
+ nextAttemptAt: Date.now(),
62
+ enqueuedAt: Date.now()
63
+ }), this.persist(), this.process();
64
+ }
65
+ async flush() {
66
+ await this.process(!0), await this.transport.flush?.();
67
+ }
68
+ /**
69
+ * Best-effort synchronous drain using sendBeacon. Intended for page unload;
70
+ * errors are swallowed because the tab is going away.
71
+ */
72
+ drainViaBeacon() {
73
+ if (this.queue.length === 0) return;
74
+ const t = this.queue.splice(0, this.queue.length);
75
+ this.persist();
76
+ for (const e of t) {
77
+ const i = { ...e.data };
78
+ delete i.__prefer_beacon;
79
+ try {
80
+ this.transport.send(e.path, i, { preferBeacon: !0 });
81
+ } catch {
82
+ }
83
+ }
84
+ }
85
+ getStats() {
86
+ return {
87
+ queued: this.queue.length,
88
+ sent: this.sent,
89
+ failed: this.failed,
90
+ dropped: this.dropped
91
+ };
92
+ }
93
+ clear() {
94
+ this.queue.splice(0, this.queue.length), this.persist();
95
+ }
96
+ persist() {
97
+ this.savePersisted?.(this.queue);
98
+ }
99
+ evictExpired() {
100
+ if (this.queue.length === 0) return;
101
+ const e = Date.now() - this.maxItemAgeMs;
102
+ let i = 0;
103
+ for (let s = this.queue.length - 1; s >= 0; s -= 1)
104
+ this.queue[s].enqueuedAt <= e && (this.queue.splice(s, 1), this.dropped += 1, i += 1);
105
+ i > 0 && this.persist();
106
+ }
107
+ async process(t = !1) {
108
+ if (!this.flushing) {
109
+ this.flushing = !0;
110
+ try {
111
+ for (this.evictExpired(); this.queue.length > 0; ) {
112
+ const e = this.queue[0];
113
+ if (!t && e.nextAttemptAt > Date.now())
114
+ break;
115
+ try {
116
+ const i = { ...e.data }, s = i.__prefer_beacon === !0;
117
+ delete i.__prefer_beacon, await this.transport.send(e.path, i, { preferBeacon: s }), this.queue.shift(), this.sent += 1, this.persist();
118
+ } catch (i) {
119
+ this.failed += 1, this.onError?.(i, e.data);
120
+ const s = i instanceof h ? i.status : void 0;
121
+ if (I(s)) {
122
+ this.queue.shift(), this.dropped += 1, this.persist(), this.debug && console.error("[Mark] Dropping event after non-retriable status", s, e.path);
123
+ continue;
124
+ }
125
+ if (e.attempts += 1, e.attempts >= this.maxAttempts) {
126
+ this.queue.shift(), this.dropped += 1, this.persist(), this.debug && console.error("[Mark] Dropping event after max retries", e.path, i);
127
+ continue;
128
+ }
129
+ const r = i instanceof h ? i.retryAfterMs : void 0;
130
+ let o;
131
+ if (typeof r == "number")
132
+ o = Math.min(this.maxBackoffMs, Math.max(0, r));
133
+ else {
134
+ const a = Math.random() * this.baseBackoffMs;
135
+ o = Math.min(
136
+ this.maxBackoffMs,
137
+ this.baseBackoffMs * 2 ** (e.attempts - 1) + a
138
+ );
139
+ }
140
+ e.nextAttemptAt = Date.now() + o, this.persist();
141
+ break;
142
+ }
143
+ }
144
+ } finally {
145
+ this.flushing = !1;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ const C = /* @__PURE__ */ new Set(["event_name", "user_id", "consent_state", "source", "is_conversion"]), U = /* @__PURE__ */ new Set([
2
151
  "user_id",
3
152
  "visitor_id",
4
153
  "click_id",
@@ -7,56 +156,94 @@ const w = "https://ingest.onelence.com", v = /* @__PURE__ */ new Set(["event_nam
7
156
  "consent_state",
8
157
  "source"
9
158
  ]);
10
- class I {
159
+ class L {
11
160
  constructor(t, e) {
12
161
  this.deps = e, this.validateConfig(t), this.config = {
13
- endpoint: t.endpoint ?? w,
162
+ endpoint: t.endpoint ?? S,
14
163
  ...t,
15
164
  include_page_context: t.include_page_context ?? !0
16
- }, this.consentRequirement = t.require_consent ?? !1, this.siteId = t.site_id, this.siteHost = t.site_host;
165
+ }, this.consentRequirement = t.require_consent ?? !1, this.siteId = t.site_id, this.siteHost = t.site_host, this.sessionTimeoutMs = t.session_timeout_ms ?? 1800 * 1e3, this.queue = new D(this.deps.transport, {
166
+ debug: this.config.debug,
167
+ loadPersisted: () => this.deps.storage.getOutbox?.() ?? [],
168
+ savePersisted: (i) => this.deps.storage.setOutbox?.(i),
169
+ onError: (i, s) => this.config.on_error?.(i, s)
170
+ }), this.warnMisconfiguredSiteHost(), this.subscribeTcf();
17
171
  }
18
172
  config;
19
173
  consentRequirement;
20
174
  siteId;
21
175
  siteHost;
176
+ queue;
177
+ sessionTimeoutMs;
178
+ batchTimer = null;
179
+ batchedEvents = [];
180
+ tcfCachedAllowed = !1;
181
+ /**
182
+ * Best-effort synchronous drain that dispatches all queued events via
183
+ * sendBeacon. Intended for use on page unload (visibilitychange=hidden,
184
+ * pagehide) where async fetch may be cancelled by the browser.
185
+ */
186
+ drainViaBeacon() {
187
+ this.flushBatch(), this.queue.drainViaBeacon();
188
+ }
189
+ /**
190
+ * Kicks the queue to retry any pending items now. Safe to call repeatedly;
191
+ * used by the browser wrapper in response to `online` events or periodic
192
+ * timers.
193
+ */
194
+ kickQueue() {
195
+ this.queue.flush();
196
+ }
22
197
  track(t, e = {}) {
23
198
  return this.trackInternal(t, e, !1);
24
199
  }
25
- trackInternal(t, e = {}, i = !1) {
200
+ trackInternal(t, e = {}, i = !1, s) {
26
201
  if (!t)
27
202
  return this.config.debug && console.warn("[Mark] track called without event name"), !1;
28
- if (!this.hasConsent())
203
+ if (!this.hasConsent() || this.isDntBlocked())
29
204
  return this.config.debug && console.warn("[Mark] Tracking blocked due to consent requirement."), !1;
30
- const s = this.sanitizeTrackData(e), r = { ...s };
31
- "query" in r && delete r.query, "site_id" in r && delete r.site_id, "site_host" in r && delete r.site_host;
32
- const o = {
205
+ if (!i && !this.shouldSampleTrack())
206
+ return !0;
207
+ const r = this.sanitizeTrackData(e), o = { ...r };
208
+ "query" in o && delete o.query, "site_id" in o && delete o.site_id, "site_host" in o && delete o.site_host;
209
+ const a = {
33
210
  event_name: t,
34
- ...this.getIdentityFields(s),
35
- ...r
211
+ message_id: this.createMessageId(),
212
+ ...this.getIdentityFields(r),
213
+ ...o
36
214
  };
37
- i && (o.is_conversion = !0);
38
- const a = s.site_id ?? this.siteId, c = s.site_host ?? this.siteHost;
39
- return a && (o.site_id = a), c && (o.site_host = c), this.config.include_page_context && typeof window < "u" && (this.applyPageContext(o), !c && o.site && (o.site_host = o.site)), this.deps.transport.send("/event", o), !0;
215
+ i && (a.is_conversion = !0);
216
+ const l = r.site_id ?? this.siteId, f = r.site_host ?? this.siteHost;
217
+ l && (a.site_id = l), f && (a.site_host = f), this.config.include_page_context && typeof window < "u" && (this.applyPageContext(a), !f && a.site && (a.site_host = a.site));
218
+ const d = this.config.before_send ? this.config.before_send(a) : a;
219
+ return d ? (this.ensureSession(), this.config.batching?.enabled && !i && !s?.preferBeacon ? (this.enqueueBatch(d), !0) : (this.queue.enqueue("/event", { ...d, __prefer_beacon: s?.preferBeacon === !0 }), !0)) : !0;
40
220
  }
41
221
  identify(t, e = {}) {
42
222
  if (!t) {
43
223
  this.config.debug && console.warn("[Mark] identify called without userId");
44
224
  return;
45
225
  }
46
- if (!this.hasConsent()) {
226
+ if (!this.hasConsent() || this.isDntBlocked()) {
47
227
  this.config.debug && console.warn("[Mark] Identify blocked due to consent requirement.");
48
228
  return;
49
229
  }
230
+ this.deps.storage.update({ user_id: t });
50
231
  const i = {
51
232
  user_id: t,
233
+ message_id: this.createMessageId(),
52
234
  ...this.sanitizeIdentifyTraits(e),
53
235
  ...this.getIdentityFields()
54
236
  };
55
- this.siteId && (i.site_id = this.siteId), this.siteHost && (i.site_host = this.siteHost), this.deps.transport.send("/identify", i);
237
+ this.siteId && (i.site_id = this.siteId), this.siteHost && (i.site_host = this.siteHost);
238
+ const s = this.config.before_send ? this.config.before_send(i) : i;
239
+ s && this.queue.enqueue("/identify", s);
56
240
  }
57
241
  conversion(t, e = {}) {
58
242
  return this.trackInternal(t, e, !0);
59
243
  }
244
+ trackWithOptions(t, e = {}, i) {
245
+ return this.trackInternal(t, e, !1, i);
246
+ }
60
247
  /**
61
248
  * Returns the current visitor ID from storage, if any.
62
249
  * Used by browser/Node wrappers to expose a stable pseudonymous ID for server-side attribution.
@@ -65,19 +252,42 @@ class I {
65
252
  return this.deps.storage.getVisitorId();
66
253
  }
67
254
  setConsent(t) {
68
- this.deps.storage.setConsentStatus(t);
69
- const e = {
255
+ const e = this.deps.storage.getConsentStatus();
256
+ this.deps.storage.setConsentStatus(t), t === "denied" ? (this.deps.storage.clearAttribution?.(), this.deps.storage.clearCookieVisitorId?.()) : t === "granted" && e === "denied" && this.config.rotate_visitor_on_consent_change && this.deps.storage.rotateVisitorId?.();
257
+ const i = {
70
258
  visitor_id: this.deps.storage.getVisitorId(),
71
259
  consent_state: t,
72
- source: "sdk"
260
+ source: "sdk",
261
+ message_id: this.createMessageId()
73
262
  };
74
- this.siteId && (e.site_id = this.siteId), this.siteHost && (e.site_host = this.siteHost), this.deps.transport.send("/consent", e);
263
+ this.siteId && (i.site_id = this.siteId), this.siteHost && (i.site_host = this.siteHost);
264
+ const s = this.config.before_send ? this.config.before_send(i) : i;
265
+ s && this.queue.enqueue("/consent", s);
266
+ }
267
+ reset() {
268
+ this.deps.storage.update({
269
+ user_id: void 0,
270
+ last_click_id: void 0,
271
+ campaign_id: void 0,
272
+ query_params: void 0,
273
+ session_id: void 0,
274
+ session_started_at: void 0,
275
+ last_activity_at: void 0
276
+ }), this.deps.storage.rotateVisitorId?.();
277
+ }
278
+ flush() {
279
+ return this.flushBatch(), this.queue.flush();
280
+ }
281
+ getStats() {
282
+ return this.queue.getStats();
75
283
  }
76
284
  getIdentityFields(t) {
77
- const e = t?.visitor_id ?? this.deps.storage.getVisitorId(), i = t?.click_id ?? this.deps.storage.getLastClickId(), s = t?.campaign_id ?? this.deps.storage.getCampaignId(), r = this.deps.storage.getQueryParams() ?? {}, o = t?.query ?? {}, a = { ...r, ...o }, c = {};
78
- return e && (c.visitor_id = e), i && (c.click_id = i), s && (c.campaign_id = s), Object.keys(a).length > 0 && (c.query = a), c;
285
+ const e = t?.visitor_id ?? this.deps.storage.getVisitorId(), i = t?.user_id ?? this.deps.storage.getUserId?.(), s = t?.click_id ?? this.deps.storage.getLastClickId(), r = t?.campaign_id ?? this.deps.storage.getCampaignId(), o = t?.session_id ?? this.deps.storage.getSessionId?.(), a = this.deps.storage.getQueryParams() ?? {}, l = t?.query ?? {}, f = { ...a, ...l }, d = {};
286
+ return e && (d.visitor_id = e), i && (d.user_id = i), s && (d.click_id = s), r && (d.campaign_id = r), o && (d.session_id = o), Object.keys(f).length > 0 && (d.query = f), d;
79
287
  }
80
288
  hasConsent() {
289
+ if (this.config.consent_source?.type === "tcf" && typeof window < "u" && !this.tcfCachedAllowed)
290
+ return !1;
81
291
  if (!this.consentRequirement)
82
292
  return !0;
83
293
  const e = this.deps.storage.getConsentStatus();
@@ -86,13 +296,13 @@ class I {
86
296
  sanitizeTrackData(t) {
87
297
  const e = {};
88
298
  for (const [i, s] of Object.entries(t))
89
- v.has(i) || (e[i] = s);
299
+ C.has(i) || (e[i] = s);
90
300
  return e;
91
301
  }
92
302
  sanitizeIdentifyTraits(t) {
93
303
  const e = {};
94
304
  for (const [i, s] of Object.entries(t))
95
- A.has(i) || (e[i] = s);
305
+ U.has(i) || (e[i] = s);
96
306
  return e;
97
307
  }
98
308
  validateConfig(t) {
@@ -110,42 +320,175 @@ class I {
110
320
  throw new Error("[Mark] `site_host` cannot be an empty string.");
111
321
  }
112
322
  applyPageContext(t) {
113
- t.page || t.title || t.referrer || t.site || (t.site = window.location.host, t.page = window.location.pathname, t.title = document.title, document.referrer && (t.referrer = document.referrer));
323
+ typeof document > "u" || (t.site || (t.site = window.location.host), t.page || (t.page = window.location.pathname), t.title || (t.title = document.title), !t.referrer && document.referrer && (t.referrer = this.scrubReferrer(document.referrer)));
324
+ }
325
+ enqueueBatch(t) {
326
+ this.batchedEvents.push(t);
327
+ const e = this.config.batching?.max_size ?? 20;
328
+ if (this.batchedEvents.length >= e) {
329
+ this.flushBatch();
330
+ return;
331
+ }
332
+ if (!this.batchTimer) {
333
+ const i = this.config.batching?.flush_interval_ms ?? 2e3;
334
+ this.batchTimer = setTimeout(() => {
335
+ this.batchTimer = null, this.flushBatch();
336
+ }, i);
337
+ }
338
+ }
339
+ flushBatch() {
340
+ if (this.batchedEvents.length === 0) return;
341
+ const t = this.config.batching?.endpoint_path ?? "/events", e = this.batchedEvents.splice(0, this.batchedEvents.length);
342
+ this.queue.enqueue(t, { events: e, message_id: this.createMessageId() });
343
+ }
344
+ createMessageId() {
345
+ return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`;
346
+ }
347
+ ensureSession() {
348
+ const t = Date.now(), e = this.deps.storage.getSessionId?.(), i = this.deps.storage.getLastActivityAt?.(), s = i ? Date.parse(i) : 0;
349
+ if (!e || !s || t - s >= this.sessionTimeoutMs || this.crossedUtcDay(s, t)) {
350
+ const o = this.createMessageId(), a = new Date(t).toISOString();
351
+ this.deps.storage.update({
352
+ session_id: o,
353
+ session_started_at: a,
354
+ last_activity_at: a
355
+ });
356
+ return;
357
+ }
358
+ this.deps.storage.update({ last_activity_at: new Date(t).toISOString() });
359
+ }
360
+ crossedUtcDay(t, e) {
361
+ const i = new Date(t), s = new Date(e);
362
+ return i.getUTCFullYear() !== s.getUTCFullYear() || i.getUTCMonth() !== s.getUTCMonth() || i.getUTCDate() !== s.getUTCDate();
363
+ }
364
+ shouldSampleTrack() {
365
+ return typeof this.config.sample_rate != "number" ? !0 : this.config.sample_rate <= 0 ? !1 : this.config.sample_rate >= 1 ? !0 : Math.random() <= this.config.sample_rate;
366
+ }
367
+ isDntBlocked() {
368
+ if (!this.config.honor_dnt || typeof navigator > "u")
369
+ return !1;
370
+ const t = navigator.doNotTrack, e = navigator.globalPrivacyControl;
371
+ return t === "1" || e === !0;
372
+ }
373
+ scrubReferrer(t) {
374
+ try {
375
+ const e = new URL(t);
376
+ if (typeof window > "u") return t;
377
+ const i = new URL(window.location.href);
378
+ return e.origin !== i.origin ? (e.search = "", e.toString()) : t;
379
+ } catch {
380
+ return t;
381
+ }
382
+ }
383
+ warnMisconfiguredSiteHost() {
384
+ !this.config.debug || !this.siteHost || typeof window > "u" || (window.location.host !== this.siteHost && console.warn("[Mark] config.site_host does not match current host", {
385
+ expected: this.siteHost,
386
+ actual: window.location.host
387
+ }), this.siteId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(this.siteId) && console.warn("[Mark] config.site_id does not look like UUID v4", this.siteId));
388
+ }
389
+ /**
390
+ * Subscribes to the IAB TCF v2 CMP via `__tcfapi('addEventListener', ...)`.
391
+ * Result is cached in `tcfCachedAllowed` so the synchronous `hasConsent()`
392
+ * path does not need to await the CMP. If the CMP is not yet present, we
393
+ * poll briefly (CMPs commonly load asynchronously) and give up after ~2s.
394
+ */
395
+ subscribeTcf() {
396
+ const t = this.config.consent_source;
397
+ if (t?.type !== "tcf" || typeof window > "u") return;
398
+ const e = t.purposes, i = (s) => {
399
+ try {
400
+ const r = window;
401
+ if (typeof r.__tcfapi != "function") {
402
+ s > 0 && setTimeout(() => i(s - 1), 200);
403
+ return;
404
+ }
405
+ r.__tcfapi("addEventListener", 2, (o, a) => {
406
+ if (!a || !o) {
407
+ this.tcfCachedAllowed = !1;
408
+ return;
409
+ }
410
+ if (o.gdprApplies === !1) {
411
+ this.tcfCachedAllowed = !0;
412
+ return;
413
+ }
414
+ const l = o.purpose?.consents ?? {};
415
+ this.tcfCachedAllowed = e.every((f) => l[String(f)] === !0);
416
+ });
417
+ } catch {
418
+ this.tcfCachedAllowed = !1;
419
+ }
420
+ };
421
+ i(10);
114
422
  }
115
423
  }
116
- class E {
424
+ class P {
117
425
  config;
118
426
  endpoint;
427
+ pending = /* @__PURE__ */ new Set();
119
428
  constructor(t) {
120
- this.validateConfig(t), this.config = t, this.endpoint = t.endpoint ?? w;
429
+ this.config = t, this.endpoint = t.endpoint ?? S;
121
430
  }
122
- async send(t, e) {
123
- const i = this.joinUrl(this.endpoint, t), s = this.config.key, r = {
431
+ async send(t, e, i) {
432
+ const s = this.sendInternal(t, e, i);
433
+ this.pending.add(s);
434
+ try {
435
+ await s;
436
+ } finally {
437
+ this.pending.delete(s);
438
+ }
439
+ }
440
+ async flush() {
441
+ this.pending.size !== 0 && await Promise.allSettled(Array.from(this.pending));
442
+ }
443
+ async sendInternal(t, e, i) {
444
+ const s = this.joinUrl(this.endpoint, t), r = this.config.key, o = {
124
445
  "Content-Type": "application/json",
125
- [s.startsWith("sk_") ? "x-secret-key" : "x-publishable-key"]: s
446
+ [r.startsWith("sk_") ? "x-secret-key" : "x-publishable-key"]: r
126
447
  };
127
- if (this.config.debug && console.log("[Mark] Sending", i, e), typeof fetch != "function") {
128
- this.config.debug && console.error("[Mark] Global fetch is not available in this runtime.");
129
- return;
448
+ this.config.debug && console.log("[Mark] Sending", s, e);
449
+ const a = JSON.stringify(e);
450
+ if (i?.preferBeacon && typeof navigator < "u" && typeof navigator.sendBeacon == "function") {
451
+ const u = new Blob([a], { type: "application/json" });
452
+ if (navigator.sendBeacon(s, u))
453
+ return;
130
454
  }
455
+ if (typeof fetch != "function")
456
+ throw this.config.debug && console.error("[Mark] Global fetch is not available in this runtime."), new h("[Mark] Global fetch is not available in this runtime.");
457
+ const l = this.config.request_timeout_ms ?? 1e4, f = new AbortController();
458
+ let d = !1;
459
+ const p = setTimeout(() => {
460
+ d = !0, f.abort();
461
+ }, l);
131
462
  try {
132
- const o = await fetch(i, {
463
+ const u = await fetch(s, {
133
464
  method: "POST",
134
- headers: r,
135
- body: JSON.stringify(e),
136
- keepalive: !0
465
+ headers: o,
466
+ body: a,
467
+ keepalive: !0,
468
+ signal: f.signal
137
469
  });
138
- if (!o.ok && this.config.debug) {
139
- const a = await this.readErrorSnippet(o);
140
- console.error("[Mark] Request rejected", {
141
- url: i,
142
- status: o.status,
143
- statusText: o.statusText,
144
- body: a
145
- });
470
+ if (!u.ok) {
471
+ const g = await this.readErrorSnippet(u), y = E(u.headers.get("Retry-After"));
472
+ throw this.config.debug && console.error("[Mark] Request rejected", {
473
+ url: s,
474
+ status: u.status,
475
+ statusText: u.statusText,
476
+ body: g,
477
+ retryAfterMs: y
478
+ }), new h(
479
+ `[Mark] Request rejected with status ${u.status}: ${g}`,
480
+ { status: u.status, retryAfterMs: y }
481
+ );
146
482
  }
147
- } catch (o) {
148
- this.config.debug && console.error("[Mark] Failed to send", i, o);
483
+ } catch (u) {
484
+ if (this.config.debug && console.error("[Mark] Failed to send", s, u), u instanceof h)
485
+ throw u;
486
+ if (d)
487
+ throw new h(`[Mark] Request timed out after ${l}ms`, { status: 408 });
488
+ const g = u instanceof Error ? u.message : String(u);
489
+ throw new h(`[Mark] Network error: ${g}`);
490
+ } finally {
491
+ clearTimeout(p);
149
492
  }
150
493
  }
151
494
  joinUrl(t, e) {
@@ -159,27 +502,22 @@ class E {
159
502
  return "";
160
503
  }
161
504
  }
162
- validateConfig(t) {
163
- if (!t.key || !t.key.trim())
164
- throw new Error("[Mark] `key` must be a non-empty string.");
165
- if (t.endpoint)
166
- try {
167
- new URL(t.endpoint);
168
- } catch {
169
- throw new Error("[Mark] `endpoint` must be a valid absolute URL.");
170
- }
171
- }
172
505
  }
173
- const k = "crelora_mark_data", g = "crelora_mark_vid";
174
- class x {
506
+ const R = "crelora_mark_data", B = "crelora_mark_outbox", w = "crelora_mark_vid";
507
+ class F {
175
508
  data;
176
509
  storageKey;
510
+ outboxKey;
177
511
  options;
178
512
  bridgeFrame;
179
513
  bridgeReady = !1;
180
514
  bridgeOrigin;
181
515
  constructor(t) {
182
- this.options = t ?? {}, this.storageKey = this.options.storageKey ?? k, this.data = this.load(), this.data.visitor_id || (this.data.visitor_id = this.generateUUID(), this.save()), typeof window < "u" && this.options.bridge?.url && this.setupBridge(this.options.bridge);
516
+ if (this.options = t ?? {}, this.storageKey = this.options.storageKey ?? R, this.outboxKey = this.options.outboxKey ?? B, typeof window > "u") {
517
+ this.data = {};
518
+ return;
519
+ }
520
+ this.data = this.load(), this.data.visitor_id || (this.data.visitor_id = this.generateUUID(), this.save()), typeof window < "u" && this.options.bridge?.url && this.setupBridge(this.options.bridge), window.addEventListener("storage", this.handleStorageEvent);
183
521
  }
184
522
  getVisitorId() {
185
523
  return this.data.visitor_id;
@@ -196,33 +534,82 @@ class x {
196
534
  getConsentStatus() {
197
535
  return this.data.consent_status;
198
536
  }
537
+ getUserId() {
538
+ return this.data.user_id;
539
+ }
540
+ getSessionId() {
541
+ return this.data.session_id;
542
+ }
543
+ getSessionStartedAt() {
544
+ return this.data.session_started_at;
545
+ }
546
+ getLastActivityAt() {
547
+ return this.data.last_activity_at;
548
+ }
199
549
  update(t) {
200
550
  this.data = { ...this.data, ...t }, this.save();
201
551
  }
202
552
  setConsentStatus(t) {
203
553
  this.data.consent_status = t, this.save();
204
554
  }
555
+ clearAttribution() {
556
+ this.update({
557
+ last_click_id: void 0,
558
+ campaign_id: void 0,
559
+ query_params: void 0
560
+ });
561
+ }
562
+ clearCookieVisitorId() {
563
+ this.setCookie(w, "", -1);
564
+ }
565
+ rotateVisitorId() {
566
+ this.update({ visitor_id: this.generateUUID() });
567
+ }
568
+ getOutbox() {
569
+ if (typeof window > "u") return [];
570
+ try {
571
+ const t = localStorage.getItem(this.outboxKey);
572
+ if (!t) return [];
573
+ const e = JSON.parse(t);
574
+ return Array.isArray(e) ? e : [];
575
+ } catch {
576
+ return [];
577
+ }
578
+ }
579
+ setOutbox(t) {
580
+ if (!(typeof window > "u"))
581
+ try {
582
+ localStorage.setItem(this.outboxKey, JSON.stringify(t.slice(-200)));
583
+ } catch {
584
+ }
585
+ }
205
586
  load() {
587
+ if (typeof window > "u")
588
+ return {};
206
589
  try {
207
590
  const e = localStorage.getItem(this.storageKey);
208
591
  if (e)
209
592
  return JSON.parse(e);
210
593
  } catch {
211
594
  }
212
- const t = this.getCookie(g);
595
+ const t = this.getCookie(w);
213
596
  return t ? { visitor_id: t } : {};
214
597
  }
215
598
  save() {
216
- try {
217
- localStorage.setItem(this.storageKey, JSON.stringify(this.data));
218
- } catch {
599
+ if (!(typeof window > "u")) {
600
+ try {
601
+ localStorage.setItem(this.storageKey, JSON.stringify(this.data));
602
+ } catch {
603
+ }
604
+ this.data.visitor_id && this.isCookieEnabled() && (this.setCookie(w, this.data.visitor_id, 365), this.options.bridge?.url && this.bridgeReady && this.postBridgeMessage({
605
+ type: "MARK_SYNC_UPDATE",
606
+ visitorId: this.data.visitor_id
607
+ }));
219
608
  }
220
- this.data.visitor_id && this.isCookieEnabled() && (this.setCookie(g, this.data.visitor_id, 365), this.options.bridge?.url && this.bridgeReady && this.postBridgeMessage({
221
- type: "MARK_SYNC_UPDATE",
222
- visitorId: this.data.visitor_id
223
- }));
224
609
  }
225
610
  getCookie(t) {
611
+ if (typeof document > "u")
612
+ return null;
226
613
  try {
227
614
  const e = document.cookie.match(new RegExp(`(^| )${t}=([^;]+)`));
228
615
  if (e) return e[2];
@@ -231,15 +618,24 @@ class x {
231
618
  return null;
232
619
  }
233
620
  setCookie(t, e, i) {
234
- try {
235
- const s = new Date(Date.now() + i * 24 * 60 * 60 * 1e3), r = this.options.cookie_domain ? `;domain=${this.options.cookie_domain}` : "";
236
- document.cookie = `${t}=${e};expires=${s.toUTCString()};path=/;SameSite=Lax${r}`;
237
- } catch {
238
- }
621
+ if (!(typeof document > "u"))
622
+ try {
623
+ const s = new Date(Date.now() + i * 24 * 60 * 60 * 1e3), r = this.options.cookie_domain ? `;domain=${this.options.cookie_domain}` : "";
624
+ document.cookie = `${t}=${e};expires=${s.toUTCString()};path=/;SameSite=Lax${r}`;
625
+ } catch {
626
+ }
239
627
  }
240
628
  generateUUID() {
241
- return typeof crypto < "u" && crypto.randomUUID ? crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (t) => {
242
- const e = Math.random() * 16 | 0;
629
+ if (typeof crypto < "u" && crypto.randomUUID)
630
+ return crypto.randomUUID();
631
+ if (typeof crypto < "u" && typeof crypto.getRandomValues == "function") {
632
+ const t = crypto.getRandomValues(new Uint8Array(16));
633
+ t[6] = t[6] & 15 | 64, t[8] = t[8] & 63 | 128;
634
+ const e = Array.from(t, (i) => i.toString(16).padStart(2, "0")).join("");
635
+ return `${e.slice(0, 8)}-${e.slice(8, 12)}-${e.slice(12, 16)}-${e.slice(16, 20)}-${e.slice(20)}`;
636
+ }
637
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (t) => {
638
+ const e = (Date.now() + Math.random() * 16) % 16 | 0;
243
639
  return (t === "x" ? e : e & 3 | 8).toString(16);
244
640
  });
245
641
  }
@@ -293,8 +689,15 @@ class x {
293
689
  return !1;
294
690
  }
295
691
  }
692
+ handleStorageEvent = (t) => {
693
+ if (!(t.key !== this.storageKey || typeof t.newValue != "string"))
694
+ try {
695
+ this.data = JSON.parse(t.newValue);
696
+ } catch {
697
+ }
698
+ };
296
699
  }
297
- const S = [
700
+ const H = [
298
701
  "click_id",
299
702
  "ch_click_id",
300
703
  "gclid",
@@ -306,7 +709,7 @@ const S = [
306
709
  "ttclid",
307
710
  "twclid",
308
711
  "li_fat_id"
309
- ], C = ["cid", "campaign_id"], R = [
712
+ ], O = ["cid", "campaign_id"], N = [
310
713
  "utm_source",
311
714
  "utm_medium",
312
715
  "utm_campaign",
@@ -328,74 +731,74 @@ const S = [
328
731
  "ttclid",
329
732
  "twclid",
330
733
  "li_fat_id"
331
- ], U = {
734
+ ], K = {
332
735
  referral: "ref",
333
736
  affiliate_id: "ref",
334
737
  ch_click_id: "click_id",
335
738
  cid: "campaign_id"
336
- }, P = ["email", "phone", "token", "auth", "password", "code"], T = 30, q = 256;
337
- function p(d, t = {}) {
338
- const e = new URLSearchParams(d), i = L(e), s = m(i, S), r = m(i, C), o = D(e, t), a = {};
739
+ }, V = ["email", "phone", "token", "auth", "password", "code"], z = 30, $ = 256;
740
+ function b(c, t = {}) {
741
+ const e = new URLSearchParams(c), i = Y(e), s = v(i, H), r = v(i, O), o = j(e, t), a = {};
339
742
  return s && (a.last_click_id = s), r && (a.campaign_id = r), Object.keys(o).length > 0 && (a.query_params = o), a;
340
743
  }
341
- function D(d, t) {
342
- const e = {}, i = new Set(y(t.query_param_denylist, P)), s = _(
744
+ function j(c, t) {
745
+ const e = {}, i = new Set(_(t.query_param_denylist, V)), s = A(
343
746
  t.max_captured_query_params,
344
- T
345
- ), r = _(
747
+ z
748
+ ), r = A(
346
749
  t.max_query_param_value_length,
347
- q
750
+ $
348
751
  ), a = t.capture_all_query_params ?? !1 ? null : /* @__PURE__ */ new Set([
349
- ...R.map(l),
350
- ...y(t.capture_query_params, [])
752
+ ...N.map(m),
753
+ ..._(t.capture_query_params, [])
351
754
  ]);
352
- for (const [c, b] of d.entries()) {
353
- const u = l(c);
354
- if (!u)
755
+ for (const [l, f] of c.entries()) {
756
+ const d = m(l);
757
+ if (!d)
355
758
  continue;
356
- const h = U[u] ?? u;
357
- if (i.has(u) || i.has(h) || a && !a.has(u))
759
+ const p = K[d] ?? d;
760
+ if (i.has(d) || i.has(p) || a && !a.has(d))
358
761
  continue;
359
- const f = b.trim();
360
- if (f) {
361
- if (!(h in e) && Object.keys(e).length >= s)
762
+ const u = f.trim();
763
+ if (u) {
764
+ if (!(p in e) && Object.keys(e).length >= s)
362
765
  break;
363
- e[h] = f.slice(0, r);
766
+ e[p] = u.slice(0, r);
364
767
  }
365
768
  }
366
769
  return e;
367
770
  }
368
- function m(d, t) {
771
+ function v(c, t) {
369
772
  for (const e of t) {
370
- const i = l(e), s = d[i]?.trim();
773
+ const i = m(e), s = c[i]?.trim();
371
774
  if (s)
372
775
  return s;
373
776
  }
374
777
  }
375
- function L(d) {
778
+ function Y(c) {
376
779
  const t = {};
377
- for (const [e, i] of d.entries()) {
378
- const s = l(e);
780
+ for (const [e, i] of c.entries()) {
781
+ const s = m(e);
379
782
  !s || s in t || (t[s] = i);
380
783
  }
381
784
  return t;
382
785
  }
383
- function l(d) {
384
- return d.trim().toLowerCase();
786
+ function m(c) {
787
+ return c.trim().toLowerCase();
385
788
  }
386
- function y(d, t) {
387
- const e = d ?? t, i = /* @__PURE__ */ new Set();
789
+ function _(c, t) {
790
+ const e = c ?? t, i = /* @__PURE__ */ new Set();
388
791
  for (const s of e) {
389
792
  if (typeof s != "string") continue;
390
- const r = l(s);
793
+ const r = m(s);
391
794
  r && i.add(r);
392
795
  }
393
796
  return Array.from(i);
394
797
  }
395
- function _(d, t) {
396
- if (typeof d != "number" || !Number.isFinite(d))
798
+ function A(c, t) {
799
+ if (typeof c != "number" || !Number.isFinite(c))
397
800
  return t;
398
- const e = Math.floor(d);
801
+ const e = Math.floor(c);
399
802
  return e < 0 ? t : e;
400
803
  }
401
804
  class n {
@@ -406,37 +809,57 @@ class n {
406
809
  static lastPageviewHref = null;
407
810
  static emitAutoPageview = null;
408
811
  static pendingAttribution = {};
812
+ static originalPushState = null;
813
+ static originalReplaceState = null;
814
+ static popstateHandler = null;
815
+ static beaconDrainHandler = null;
816
+ static visibilityHandler = null;
817
+ static onlineHandler = null;
818
+ static kickTimer = null;
409
819
  static init(t) {
410
820
  if (!n.client) {
411
- const e = new x(t.cross_domain), i = new E(t);
412
- n.client = new I(t, { storage: e, transport: i }), n.storage = e, n.config = t, n.refreshAttribution(e, t), typeof window < "u" && t.autocapture?.pageview && n.installPageviewTracking(t, e);
821
+ const e = new F(t.cross_domain), i = new P(t);
822
+ n.client = new L(t, { storage: e, transport: i }), n.storage = e, n.config = t, n.refreshAttribution(e, t), typeof window < "u" && t.autocapture?.pageview && n.installPageviewTracking(t, e), typeof window < "u" && (n.installExtendedAutocapture(t), n.installLifecycleFlushing());
413
823
  }
414
824
  return n.client;
415
825
  }
826
+ static installLifecycleFlushing() {
827
+ if (typeof window > "u" || n.beaconDrainHandler) return;
828
+ const t = () => {
829
+ n.client?.drainViaBeacon();
830
+ }, e = () => {
831
+ typeof document < "u" && document.visibilityState === "hidden" && t();
832
+ }, i = () => {
833
+ n.client?.kickQueue();
834
+ };
835
+ n.beaconDrainHandler = t, n.visibilityHandler = e, n.onlineHandler = i, typeof document < "u" && typeof document.addEventListener == "function" && document.addEventListener("visibilitychange", e), typeof window.addEventListener == "function" && (window.addEventListener("pagehide", t), window.addEventListener("beforeunload", t), window.addEventListener("online", i)), typeof setInterval == "function" && (n.kickTimer = setInterval(() => {
836
+ n.client?.kickQueue();
837
+ }, 3e4));
838
+ }
416
839
  static track(t, e = {}) {
417
840
  if (!n.client) {
418
- console.warn("[Mark] Not initialized. Call init() first.");
841
+ n.config?.debug && console.warn("[Mark] Not initialized. Call init() first.");
419
842
  return;
420
843
  }
421
844
  n.client.track(t, e);
422
845
  }
423
846
  static identify(t, e = {}) {
424
847
  if (!n.client) {
425
- console.warn("[Mark] Not initialized. Call init() first.");
848
+ n.config?.debug && console.warn("[Mark] Not initialized. Call init() first.");
426
849
  return;
427
850
  }
428
851
  n.client.identify(t, e);
429
852
  }
430
853
  static conversion(t, e = {}) {
431
854
  if (!n.client) {
432
- console.warn("[Mark] Not initialized. Call init() first.");
855
+ n.config?.debug && console.warn("[Mark] Not initialized. Call init() first.");
433
856
  return;
434
857
  }
435
858
  n.client.conversion(t, e);
436
859
  }
437
860
  static setConsent(t) {
438
861
  if (!n.client) {
439
- console.warn("[Mark] Not initialized. Call init() first.");
862
+ n.config?.debug && console.warn("[Mark] Not initialized. Call init() first.");
440
863
  return;
441
864
  }
442
865
  n.client.setConsent(t), t === "granted" && (n.flushPendingAttribution(), n.emitAutoPageview?.());
@@ -451,6 +874,18 @@ class n {
451
874
  if (!(!n.client || !n.storage || !n.config || (n.config.require_consent ?? !1) && n.storage.getConsentStatus() !== "granted"))
452
875
  return n.client.getVisitorId();
453
876
  }
877
+ static flush() {
878
+ return n.client ? n.client.flush() : Promise.resolve();
879
+ }
880
+ static reset() {
881
+ n.client?.reset(), n.pendingAttribution = {}, n.lastPageviewHref = null;
882
+ }
883
+ static getStats() {
884
+ return n.client?.getStats() ?? { queued: 0, sent: 0, failed: 0, dropped: 0 };
885
+ }
886
+ static destroy() {
887
+ typeof window > "u" || (n.originalPushState && (history.pushState = n.originalPushState), n.originalReplaceState && (history.replaceState = n.originalReplaceState), n.popstateHandler && window.removeEventListener("popstate", n.popstateHandler), n.beaconDrainHandler && (window.removeEventListener("pagehide", n.beaconDrainHandler), window.removeEventListener("beforeunload", n.beaconDrainHandler)), n.visibilityHandler && typeof document < "u" && document.removeEventListener("visibilitychange", n.visibilityHandler), n.onlineHandler && window.removeEventListener("online", n.onlineHandler), n.kickTimer && clearInterval(n.kickTimer), n.originalPushState = null, n.originalReplaceState = null, n.popstateHandler = null, n.beaconDrainHandler = null, n.visibilityHandler = null, n.onlineHandler = null, n.kickTimer = null, n.pageviewTrackerInstalled = !1);
888
+ }
454
889
  static installPageviewTracking(t, e) {
455
890
  if (n.pageviewTrackerInstalled) return;
456
891
  n.pageviewTrackerInstalled = !0;
@@ -468,18 +903,18 @@ class n {
468
903
  };
469
904
  if (n.emitAutoPageview = i, i(), !(t.track_route_changes ?? !0)) return;
470
905
  const r = history.pushState, o = history.replaceState;
471
- history.pushState = function(...a) {
472
- const c = r.apply(this, a);
473
- return i(), c;
906
+ n.originalPushState = r, n.originalReplaceState = o, history.pushState = function(...a) {
907
+ const l = r.apply(this, a);
908
+ return i(), l;
474
909
  }, history.replaceState = function(...a) {
475
- const c = o.apply(this, a);
476
- return i(), c;
477
- }, window.addEventListener("popstate", i);
910
+ const l = o.apply(this, a);
911
+ return i(), l;
912
+ }, n.popstateHandler = i, window.addEventListener("popstate", i);
478
913
  }
479
914
  static refreshAttribution(t, e) {
480
915
  if (typeof window > "u")
481
916
  return;
482
- const i = p(window.location.search, e);
917
+ const i = b(window.location.search, e);
483
918
  if (Object.keys(i).length !== 0) {
484
919
  if (n.shouldPersistAttribution(t, e)) {
485
920
  const s = n.mergeAttributionUpdates(n.pendingAttribution, i);
@@ -493,7 +928,7 @@ class n {
493
928
  const t = n.storage, e = n.config;
494
929
  if (!t || !e || typeof window > "u")
495
930
  return;
496
- const i = p(window.location.search, e), s = n.mergeAttributionUpdates(n.pendingAttribution, i);
931
+ const i = b(window.location.search, e), s = n.mergeAttributionUpdates(n.pendingAttribution, i);
497
932
  Object.keys(s).length > 0 && t.update(s), n.pendingAttribution = {};
498
933
  }
499
934
  static shouldPersistAttribution(t, e) {
@@ -509,9 +944,68 @@ class n {
509
944
  }
510
945
  };
511
946
  }
947
+ static installExtendedAutocapture(t) {
948
+ if (!n.client || typeof document > "u") return;
949
+ const e = t.autocapture;
950
+ if (e) {
951
+ if (e.click && document.addEventListener("click", (i) => {
952
+ const s = i.target;
953
+ if (!s) return;
954
+ const r = typeof e.click == "object" ? e.click.selector : void 0, o = r ? s.closest(r) : s.closest("[data-mark-event]");
955
+ if (!o) return;
956
+ const a = o.getAttribute("data-mark-event") || "click";
957
+ n.client?.track(a, {
958
+ element_id: o.id || void 0,
959
+ element_classes: o.className || void 0,
960
+ text: o.textContent?.trim().slice(0, 120) || void 0,
961
+ href: o.href || void 0
962
+ });
963
+ }), e.form_submit && document.addEventListener("submit", (i) => {
964
+ const s = i.target;
965
+ s && n.client?.track("form_submit", {
966
+ form_id: s.id || void 0,
967
+ form_name: s.name || void 0,
968
+ action: s.action || void 0
969
+ });
970
+ }), e.outbound_link && document.addEventListener("click", (i) => {
971
+ const s = i.target?.closest("a[href]");
972
+ if (!(!s || !s.href))
973
+ try {
974
+ const r = new URL(s.href, window.location.href), o = new URL(window.location.href);
975
+ r.origin !== o.origin && n.client?.trackWithOptions("outbound_link_click", { href: r.toString() }, { preferBeacon: !0 });
976
+ } catch {
977
+ }
978
+ }), e.scroll_depth) {
979
+ const i = [25, 50, 75, 100], s = /* @__PURE__ */ new Set();
980
+ window.addEventListener(
981
+ "scroll",
982
+ () => {
983
+ const r = document.documentElement, o = window.scrollY || r.scrollTop, a = Math.max(1, r.scrollHeight - window.innerHeight), l = Math.min(100, Math.round(o / a * 100));
984
+ for (const f of i)
985
+ l >= f && !s.has(f) && (s.add(f), n.client?.track("scroll_depth", { percent: f }));
986
+ },
987
+ { passive: !0 }
988
+ );
989
+ }
990
+ e.web_vitals && import("./web-vitals-CrnTllyu.js").then((i) => {
991
+ const s = (r) => {
992
+ n.client?.track("web_vital", {
993
+ metric: r.name,
994
+ value: r.value,
995
+ rating: r.rating,
996
+ metric_id: r.id
997
+ });
998
+ };
999
+ i.onLCP(s), i.onCLS(s), i.onINP(s), i.onTTFB(s);
1000
+ }).catch(() => {
1001
+ t.debug && console.warn("[Mark] web-vitals package not available; skipping autocapture.web_vitals.");
1002
+ });
1003
+ }
1004
+ }
512
1005
  }
513
1006
  typeof window < "u" && (window.Mark = n);
514
1007
  export {
515
1008
  n as Mark,
516
1009
  n as default
517
1010
  };
1011
+ //# sourceMappingURL=browser.es.js.map