@aranova/tracking-next 0.24.0 → 0.25.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/client.js ADDED
@@ -0,0 +1,3572 @@
1
+ "use strict";
2
+ "use client";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/client.ts
32
+ var client_exports = {};
33
+ __export(client_exports, {
34
+ AdPlatformTracking: () => AdPlatformTracking,
35
+ ConsentBanner: () => ConsentBanner,
36
+ GoogleAdsTracking: () => GoogleAdsTracking,
37
+ PhoneField: () => PhoneField,
38
+ createTracking: () => createTracking,
39
+ useConsent: () => useConsent,
40
+ useConsentState: () => useConsentState,
41
+ useCookiePreferences: () => useCookiePreferences,
42
+ useGclid: () => useGclid,
43
+ usePhoneConfig: () => usePhoneConfig,
44
+ usePhoneField: () => usePhoneField,
45
+ useTrackingParams: () => useTrackingParams
46
+ });
47
+ module.exports = __toCommonJS(client_exports);
48
+
49
+ // src/ConsentBanner.tsx
50
+ var import_react2 = require("react");
51
+
52
+ // src/hooks.ts
53
+ var import_react = require("react");
54
+
55
+ // ../tracking-core/src/phone.ts
56
+ var import_libphonenumber_js = require("libphonenumber-js");
57
+ var DEFAULT_PHONE_COUNTRY = "CA";
58
+ function parsePhone(raw, country) {
59
+ const region = country ?? DEFAULT_PHONE_COUNTRY;
60
+ const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(raw ?? "", region);
61
+ if (!parsed) {
62
+ return { e164: null, national: "", international: "", country: region, isValid: false };
63
+ }
64
+ const isValid = parsed.isValid();
65
+ return {
66
+ // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
67
+ // (e.g. too few digits) still parses but must not be transmitted.
68
+ e164: isValid ? parsed.number : null,
69
+ national: parsed.formatNational(),
70
+ international: parsed.formatInternational(),
71
+ country: parsed.country ?? region,
72
+ isValid
73
+ };
74
+ }
75
+ function toE164(raw, country) {
76
+ return parsePhone(raw, country).e164;
77
+ }
78
+ function formatPhone(value, format = "national", country) {
79
+ const parsed = parsePhone(value, country);
80
+ if (typeof format === "function") return format(parsed);
81
+ switch (format) {
82
+ case "international":
83
+ return parsed.international || value;
84
+ case "e164":
85
+ return parsed.e164 ?? value;
86
+ case "national":
87
+ default:
88
+ return parsed.national || value;
89
+ }
90
+ }
91
+ function formatPhoneAsTyped(raw, country) {
92
+ return new import_libphonenumber_js.AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
93
+ }
94
+
95
+ // ../tracking-core/src/user-data.ts
96
+ var EMAIL_SHAPE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
97
+ var EMAIL_NAME_HINT = /e[-_]?mail/i;
98
+ var PHONE_NAME_HINT = /(^|[^a-z])(phone|tel|mobile|cell)/i;
99
+ var stash = { email: null, phoneNumber: null };
100
+ function normalizeEmail(raw) {
101
+ if (typeof raw !== "string") return null;
102
+ const cleaned = raw.trim().toLowerCase();
103
+ return EMAIL_SHAPE.test(cleaned) ? cleaned : null;
104
+ }
105
+ function fieldText(field, key) {
106
+ const value = field[key];
107
+ return typeof value === "string" ? value : "";
108
+ }
109
+ function extractUserDataFromFormFields(fields, country) {
110
+ const result = { email: null, phoneNumber: null };
111
+ if (!Array.isArray(fields)) return result;
112
+ const passes = [
113
+ (field, _hint, type) => fieldText(field, "type").toLowerCase() === type,
114
+ (field, hint) => hint.test(fieldText(field, "name")) || hint.test(fieldText(field, "label"))
115
+ ];
116
+ for (const matches of passes) {
117
+ for (const raw of fields) {
118
+ if (raw === null || typeof raw !== "object") continue;
119
+ const field = raw;
120
+ if (typeof field.value !== "string" || field.value.length === 0) continue;
121
+ if (result.email === null && matches(field, EMAIL_NAME_HINT, "email")) {
122
+ result.email = normalizeEmail(field.value);
123
+ }
124
+ if (result.phoneNumber === null && matches(field, PHONE_NAME_HINT, "tel")) {
125
+ try {
126
+ result.phoneNumber = toE164(field.value, country);
127
+ } catch {
128
+ }
129
+ }
130
+ }
131
+ if (result.email !== null && result.phoneNumber !== null) break;
132
+ }
133
+ return result;
134
+ }
135
+ function stashUserDataFromFormFields(fields, country) {
136
+ try {
137
+ const extracted = extractUserDataFromFormFields(fields, country);
138
+ stash = {
139
+ email: extracted.email ?? stash.email,
140
+ phoneNumber: extracted.phoneNumber ?? stash.phoneNumber
141
+ };
142
+ } catch {
143
+ }
144
+ }
145
+ function clearStashedUserData() {
146
+ stash = { email: null, phoneNumber: null };
147
+ }
148
+ function applyUserDataForConversion(explicit, country) {
149
+ if (typeof window === "undefined" || typeof window.gtag !== "function") return false;
150
+ let email = stash.email;
151
+ let phoneNumber = stash.phoneNumber;
152
+ if (explicit) {
153
+ const normalizedEmail = normalizeEmail(explicit.email);
154
+ if (normalizedEmail) email = normalizedEmail;
155
+ if (typeof explicit.phone === "string" && explicit.phone.length > 0) {
156
+ try {
157
+ phoneNumber = toE164(explicit.phone, country) ?? phoneNumber;
158
+ } catch {
159
+ }
160
+ }
161
+ }
162
+ if (email === null && phoneNumber === null) return false;
163
+ try {
164
+ window.gtag("set", "user_data", {
165
+ ...email !== null ? { email } : {},
166
+ ...phoneNumber !== null ? { phone_number: phoneNumber } : {}
167
+ });
168
+ return true;
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ // ../tracking-core/src/consent.ts
175
+ var CONSENT_STATE_KEY = "consent_state";
176
+ var CONSENT_TIMESTAMP_KEY = "consent_timestamp";
177
+ var CONSENT_EXPIRES_AT_KEY = "consent_expires_at";
178
+ var DEFAULT_DECLINE_TTL_DAYS = 90;
179
+ var DAY_MS = 864e5;
180
+ var DEFAULT_CHOICE = {
181
+ state: "granted",
182
+ source: "default",
183
+ updatedAt: null,
184
+ expiresAt: null
185
+ };
186
+ var changeListeners = /* @__PURE__ */ new Set();
187
+ function onConsentChange(listener) {
188
+ changeListeners.add(listener);
189
+ return () => {
190
+ changeListeners.delete(listener);
191
+ };
192
+ }
193
+ function notifyConsentChanged(choice) {
194
+ for (const listener of changeListeners) {
195
+ try {
196
+ listener(choice);
197
+ } catch {
198
+ }
199
+ }
200
+ }
201
+ function buildConsentPayload(state) {
202
+ return {
203
+ ad_storage: state,
204
+ ad_user_data: state,
205
+ ad_personalization: state,
206
+ analytics_storage: state
207
+ };
208
+ }
209
+ function getConsentChoice() {
210
+ if (typeof window === "undefined") return DEFAULT_CHOICE;
211
+ try {
212
+ const stored = window.localStorage.getItem(CONSENT_STATE_KEY);
213
+ const updatedAt = window.localStorage.getItem(CONSENT_TIMESTAMP_KEY);
214
+ if (stored === "granted")
215
+ return { state: "granted", source: "explicit", updatedAt, expiresAt: null };
216
+ if (stored === "denied") {
217
+ let expiresAt = window.localStorage.getItem(CONSENT_EXPIRES_AT_KEY);
218
+ if (!expiresAt) {
219
+ expiresAt = new Date(Date.now() + DEFAULT_DECLINE_TTL_DAYS * DAY_MS).toISOString();
220
+ try {
221
+ window.localStorage.setItem(CONSENT_EXPIRES_AT_KEY, expiresAt);
222
+ } catch {
223
+ }
224
+ }
225
+ if (!(Date.parse(expiresAt) <= Date.now()))
226
+ return { state: "denied", source: "explicit", updatedAt, expiresAt };
227
+ }
228
+ } catch {
229
+ }
230
+ return DEFAULT_CHOICE;
231
+ }
232
+ function getConsentState() {
233
+ return getConsentChoice().state;
234
+ }
235
+ function pushConsentToPlatforms(state) {
236
+ if (typeof window === "undefined") return;
237
+ if (typeof window.gtag === "function")
238
+ window.gtag("consent", "update", buildConsentPayload(state));
239
+ if (typeof window.fbq === "function")
240
+ window.fbq("consent", state === "granted" ? "grant" : "revoke");
241
+ }
242
+ function setConsentState(state, options) {
243
+ if (typeof window === "undefined") return;
244
+ const requestedTtl = options?.declineTtlDays;
245
+ const ttlDays = typeof requestedTtl === "number" && Number.isFinite(requestedTtl) && requestedTtl > 0 ? requestedTtl : DEFAULT_DECLINE_TTL_DAYS;
246
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
247
+ const expiresAt = state === "denied" ? new Date(Date.now() + ttlDays * DAY_MS).toISOString() : null;
248
+ try {
249
+ window.localStorage.setItem(CONSENT_STATE_KEY, state);
250
+ window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, updatedAt);
251
+ if (expiresAt !== null) {
252
+ window.localStorage.setItem(CONSENT_EXPIRES_AT_KEY, expiresAt);
253
+ } else {
254
+ window.localStorage.removeItem(CONSENT_EXPIRES_AT_KEY);
255
+ }
256
+ } catch {
257
+ }
258
+ pushConsentToPlatforms(state);
259
+ if (state === "denied") {
260
+ clearStashedUserData();
261
+ try {
262
+ if (typeof window.gtag === "function") window.gtag("set", "user_data", null);
263
+ } catch {
264
+ }
265
+ }
266
+ notifyConsentChanged({ state, source: "explicit", updatedAt, expiresAt });
267
+ }
268
+ function optIn() {
269
+ setConsentState("granted");
270
+ }
271
+ function optOut(options) {
272
+ setConsentState("denied", options);
273
+ }
274
+ function resetConsent() {
275
+ if (typeof window === "undefined") return;
276
+ try {
277
+ window.localStorage.removeItem(CONSENT_STATE_KEY);
278
+ window.localStorage.removeItem(CONSENT_TIMESTAMP_KEY);
279
+ window.localStorage.removeItem(CONSENT_EXPIRES_AT_KEY);
280
+ } catch {
281
+ }
282
+ pushConsentToPlatforms("granted");
283
+ notifyConsentChanged(DEFAULT_CHOICE);
284
+ }
285
+ function createEffectiveConsentSnippet() {
286
+ return `
287
+ var aranovaConsent = (function () {
288
+ try {
289
+ if (window.localStorage.getItem('${CONSENT_STATE_KEY}') === 'denied') {
290
+ var exp = window.localStorage.getItem('${CONSENT_EXPIRES_AT_KEY}');
291
+ var expMs = exp ? Date.parse(exp) : NaN;
292
+ if (!(expMs <= Date.now())) return 'denied';
293
+ }
294
+ } catch (e) {}
295
+ return 'granted';
296
+ })();
297
+ `.trim();
298
+ }
299
+
300
+ // ../tracking-core/src/capabilities.ts
301
+ var TRACKING_CAPABILITIES = [
302
+ "ad_tags_google",
303
+ "ad_tags_meta",
304
+ "base_tracking",
305
+ "blog_rendering",
306
+ "calendar_read",
307
+ "calendar_write",
308
+ "consent_controls",
309
+ "conversion_goals_auto",
310
+ "conversion_goals_manual",
311
+ "cta_click_capture",
312
+ "form_capture",
313
+ "phone_click_capture",
314
+ "phone_fields"
315
+ ];
316
+ var CAPABILITY_DOM_ATTRIBUTE = "data-aranova-capability";
317
+ var registered = /* @__PURE__ */ new Set();
318
+ function registerCapability(capability) {
319
+ registered.add(capability);
320
+ }
321
+ function getRegisteredCapabilities() {
322
+ const all = new Set(registered);
323
+ for (const marker of readDomMarkers()) all.add(marker);
324
+ return Array.from(all).sort();
325
+ }
326
+ function readDomMarkers() {
327
+ if (typeof document === "undefined") return [];
328
+ const known = new Set(TRACKING_CAPABILITIES);
329
+ const found = [];
330
+ for (const node of document.querySelectorAll(`[${CAPABILITY_DOM_ATTRIBUTE}]`)) {
331
+ const value = node.getAttribute(CAPABILITY_DOM_ATTRIBUTE);
332
+ if (value && known.has(value)) found.push(value);
333
+ }
334
+ return found;
335
+ }
336
+
337
+ // ../tracking-core/src/tracking.ts
338
+ var TRACKING_COOKIE_MAX_AGE_SECONDS = 7776e3;
339
+ var TRACKING_PARAM_KEYS = [
340
+ "gclid",
341
+ // Google's iOS/Safari replacement click IDs — issued when privacy features
342
+ // withhold gclid (wbraid: web-to-web, gbraid: app-to-web). First-class
343
+ // citizens: captured, persisted, and attributed exactly like gclid.
344
+ "wbraid",
345
+ "gbraid",
346
+ // Yelp Ads click id (40 hex chars), appended to ad-click URLs.
347
+ "ylpcid",
348
+ "fbclid",
349
+ "utm_source",
350
+ "utm_medium",
351
+ "utm_campaign",
352
+ "utm_term",
353
+ "utm_content"
354
+ ];
355
+ function createEmptyTrackingParams() {
356
+ return {
357
+ gclid: null,
358
+ wbraid: null,
359
+ gbraid: null,
360
+ ylpcid: null,
361
+ fbclid: null,
362
+ utm_source: null,
363
+ utm_medium: null,
364
+ utm_campaign: null,
365
+ utm_term: null,
366
+ utm_content: null
367
+ };
368
+ }
369
+ function normalizeTrackingCookieValue(value) {
370
+ return typeof value === "string" && value.length > 0 ? value : null;
371
+ }
372
+ function getTrackingParamsFromCookieReader(readCookie) {
373
+ return TRACKING_PARAM_KEYS.reduce((params, key) => {
374
+ params[key] = normalizeTrackingCookieValue(readCookie(key));
375
+ return params;
376
+ }, createEmptyTrackingParams());
377
+ }
378
+ function getTrackingQueryValues(searchParams) {
379
+ return TRACKING_PARAM_KEYS.reduce((params, key) => {
380
+ const value = searchParams.get(key);
381
+ if (typeof value === "string" && value.trim().length > 0) {
382
+ params[key] = value;
383
+ }
384
+ return params;
385
+ }, {});
386
+ }
387
+ var FALLBACK_STORAGE_PREFIX = "_aranova_track_";
388
+ function fallbackKey(name) {
389
+ return `${FALLBACK_STORAGE_PREFIX}${name}`;
390
+ }
391
+ function persistCookieValue(name, value, maxAgeSeconds = TRACKING_COOKIE_MAX_AGE_SECONDS) {
392
+ const encoded = encodeURIComponent(value);
393
+ if (typeof document !== "undefined") {
394
+ try {
395
+ document.cookie = `${name}=${encoded}; Max-Age=${maxAgeSeconds}; Path=/; SameSite=Lax`;
396
+ } catch {
397
+ }
398
+ }
399
+ if (typeof window !== "undefined") {
400
+ try {
401
+ window.localStorage.setItem(fallbackKey(name), encoded);
402
+ } catch {
403
+ }
404
+ }
405
+ }
406
+ function readCookieValue(name) {
407
+ if (typeof document !== "undefined") {
408
+ const cookies = document.cookie ? document.cookie.split("; ") : [];
409
+ const match = cookies.find((cookie) => cookie.startsWith(`${name}=`));
410
+ if (match) {
411
+ const [, rawValue = ""] = match.split("=");
412
+ return normalizeTrackingCookieValue(decodeURIComponent(rawValue));
413
+ }
414
+ }
415
+ if (typeof window !== "undefined") {
416
+ try {
417
+ const stored = window.localStorage.getItem(fallbackKey(name));
418
+ if (stored) return normalizeTrackingCookieValue(decodeURIComponent(stored));
419
+ } catch {
420
+ }
421
+ }
422
+ return null;
423
+ }
424
+ function getCookieValueFromDocument(key) {
425
+ return readCookieValue(key);
426
+ }
427
+ function setTrackingCookie(key, value, maxAgeSeconds = TRACKING_COOKIE_MAX_AGE_SECONDS) {
428
+ persistCookieValue(key, value, maxAgeSeconds);
429
+ }
430
+ function mergeTrackingParams(primary, fallback) {
431
+ return TRACKING_PARAM_KEYS.reduce((merged, key) => {
432
+ merged[key] = primary[key] ?? fallback[key];
433
+ return merged;
434
+ }, createEmptyTrackingParams());
435
+ }
436
+ function persistTrackingParamsFromSearchParams(searchParams, maxAgeSeconds = TRACKING_COOKIE_MAX_AGE_SECONDS) {
437
+ const trackingValues = getTrackingQueryValues(searchParams);
438
+ Object.entries(trackingValues).forEach(([key, value]) => {
439
+ setTrackingCookie(key, value, maxAgeSeconds);
440
+ });
441
+ return trackingValues;
442
+ }
443
+ function captureTrackingParamsFromLocation(url = typeof window === "undefined" ? "" : window.location.href, maxAgeSeconds = TRACKING_COOKIE_MAX_AGE_SECONDS) {
444
+ const resolvedUrl = typeof window === "undefined" ? new URL(url || "https://example.invalid") : new URL(url, window.location.origin);
445
+ persistTrackingParamsFromSearchParams(resolvedUrl.searchParams, maxAgeSeconds);
446
+ return getTrackingParamsFromCookieReader(getCookieValueFromDocument);
447
+ }
448
+
449
+ // ../tracking-core/src/gtag.ts
450
+ var GTAG_SCRIPT_HOST = "https://www.googletagmanager.com/gtag/js";
451
+ var TRACKING_SCRIPT_ATTRIBUTE = "data-aranova-tracking";
452
+ function getScriptMarker(id) {
453
+ return `aranova-${id}`;
454
+ }
455
+ var GTAG_ID_PATTERN = /^[A-Z]{1,3}-[A-Za-z0-9_-]+$/;
456
+ function isValidGtagId(id) {
457
+ return GTAG_ID_PATTERN.test(id);
458
+ }
459
+ function createConsentDefaultScript() {
460
+ return `
461
+ window.dataLayer = window.dataLayer || [];
462
+ function gtag(){dataLayer.push(arguments);}
463
+ window.gtag = gtag;
464
+ ${createEffectiveConsentSnippet()}
465
+ gtag('consent', 'default', {
466
+ ad_storage: aranovaConsent,
467
+ ad_user_data: aranovaConsent,
468
+ ad_personalization: aranovaConsent,
469
+ analytics_storage: aranovaConsent
470
+ });
471
+ `.trim();
472
+ }
473
+ function createGtagInitScript(gtagId) {
474
+ if (!isValidGtagId(gtagId)) return "";
475
+ return `
476
+ window.dataLayer = window.dataLayer || [];
477
+ function gtag(){dataLayer.push(arguments);}
478
+ window.gtag = gtag;
479
+ gtag('js', new Date());
480
+ gtag('config', '${gtagId}');
481
+ `.trim();
482
+ }
483
+ function ensureGtagFunction() {
484
+ window.dataLayer = window.dataLayer || [];
485
+ if (typeof window.gtag === "function") return window.gtag;
486
+ function gtag() {
487
+ window.dataLayer?.push(arguments);
488
+ }
489
+ window.gtag = gtag;
490
+ return window.gtag;
491
+ }
492
+ var SEND_TO_RE = /^AW-[A-Za-z0-9]+\/[A-Za-z0-9_-]+$/;
493
+ function isValidSendTo(sendTo) {
494
+ return SEND_TO_RE.test(sendTo);
495
+ }
496
+ function fireGtagConversion(input) {
497
+ if (typeof window === "undefined" || typeof window.gtag !== "function") return false;
498
+ if (!isValidSendTo(input.sendTo)) return false;
499
+ const params = { send_to: input.sendTo };
500
+ if (input.value != null) params.value = input.value;
501
+ if (input.currency) params.currency = input.currency;
502
+ if (input.transactionId) params.transaction_id = input.transactionId;
503
+ try {
504
+ window.gtag("event", "conversion", params);
505
+ return true;
506
+ } catch {
507
+ return false;
508
+ }
509
+ }
510
+ function applyDefaultConsentState() {
511
+ const gtag = ensureGtagFunction();
512
+ gtag(
513
+ "consent",
514
+ "default",
515
+ buildConsentPayload(getConsentState() === "denied" ? "denied" : "granted")
516
+ );
517
+ }
518
+ function loadGtagScript(gtagId) {
519
+ if (typeof document === "undefined") return;
520
+ const marker = getScriptMarker("gtag-loader");
521
+ const existingScript = document.querySelector(
522
+ `script[${TRACKING_SCRIPT_ATTRIBUTE}="${marker}"]`
523
+ );
524
+ if (existingScript) return;
525
+ const script = document.createElement("script");
526
+ script.async = true;
527
+ script.src = `${GTAG_SCRIPT_HOST}?id=${encodeURIComponent(gtagId)}`;
528
+ script.setAttribute(TRACKING_SCRIPT_ATTRIBUTE, marker);
529
+ document.head.append(script);
530
+ }
531
+ function createGtagInitScriptMulti(gtagIds) {
532
+ const safeIds = gtagIds.filter(isValidGtagId);
533
+ const configs = safeIds.map((id) => `gtag('config', '${id}');`).join("\n");
534
+ return `
535
+ window.dataLayer = window.dataLayer || [];
536
+ function gtag(){dataLayer.push(arguments);}
537
+ window.gtag = gtag;
538
+ gtag('js', new Date());
539
+ ${configs}
540
+ `.trim();
541
+ }
542
+
543
+ // ../tracking-core/src/fbq.ts
544
+ var FB_EVENTS_SCRIPT_HOST = "https://connect.facebook.net/en_US/fbevents.js";
545
+ var FBC_COOKIE = "_fbc";
546
+ var FBP_COOKIE = "_fbp";
547
+ var META_PIXEL_ID_PATTERN = /^\d{15,16}$/;
548
+ function isValidMetaPixelId(id) {
549
+ return META_PIXEL_ID_PATTERN.test(id);
550
+ }
551
+ function computeFbSubdomainIndex(hostname) {
552
+ const labels = hostname.split(".").filter(Boolean);
553
+ return Math.max(0, labels.length - 1);
554
+ }
555
+ function buildFbc(fbclid, now, hostname) {
556
+ const host = hostname ?? (typeof window === "undefined" ? "" : window.location.hostname);
557
+ return `fb.${computeFbSubdomainIndex(host)}.${now}.${fbclid}`;
558
+ }
559
+ function getFbcCookie() {
560
+ return readCookieValue(FBC_COOKIE);
561
+ }
562
+ function getFbpCookie() {
563
+ return readCookieValue(FBP_COOKIE);
564
+ }
565
+ function readFbclidFromUrl() {
566
+ if (typeof window === "undefined") return null;
567
+ try {
568
+ const value = new URL(window.location.href).searchParams.get("fbclid");
569
+ return value && value.trim().length > 0 ? value : null;
570
+ } catch {
571
+ return null;
572
+ }
573
+ }
574
+ function captureFbc(now = typeof Date === "undefined" ? 0 : Date.now()) {
575
+ if (typeof window === "undefined") return;
576
+ if (getFbcCookie()) return;
577
+ const fbclid = readFbclidFromUrl() ?? readCookieValue("fbclid");
578
+ if (!fbclid) return;
579
+ persistCookieValue(FBC_COOKIE, buildFbc(fbclid, now), TRACKING_COOKIE_MAX_AGE_SECONDS);
580
+ }
581
+ function fbqStubScript() {
582
+ return `
583
+ !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
584
+ n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
585
+ n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
586
+ t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
587
+ document,'script','${FB_EVENTS_SCRIPT_HOST}');`.trim();
588
+ }
589
+ function createMetaConsentDefaultScript() {
590
+ return `${fbqStubScript()}
591
+ ${createEffectiveConsentSnippet()}
592
+ fbq('consent', aranovaConsent === 'denied' ? 'revoke' : 'grant');`;
593
+ }
594
+ function createMetaPixelInitScript(pixelId) {
595
+ if (!isValidMetaPixelId(pixelId)) return "";
596
+ return `fbq('init', '${pixelId}');
597
+ fbq('track', 'PageView');`;
598
+ }
599
+ function createMetaPixelInitScriptMulti(pixelIds) {
600
+ const inits = pixelIds.filter(isValidMetaPixelId).map((id) => `fbq('init', '${id}');`);
601
+ if (inits.length === 0) return "";
602
+ return `${inits.join("\n")}
603
+ fbq('track', 'PageView');`;
604
+ }
605
+
606
+ // ../tracking-core/src/landing.ts
607
+ var LANDING_STORAGE_KEY = "_aranova_track_landing";
608
+ var memoryRecord = null;
609
+ function sanitizeParams(value) {
610
+ if (typeof value !== "object" || value === null) return {};
611
+ const source = value;
612
+ return TRACKING_PARAM_KEYS.reduce((params, key) => {
613
+ const entry = source[key];
614
+ if (typeof entry === "string" && entry.length > 0) params[key] = entry;
615
+ return params;
616
+ }, {});
617
+ }
618
+ function readStoredRecord() {
619
+ try {
620
+ const raw = window.localStorage.getItem(LANDING_STORAGE_KEY);
621
+ if (!raw) return null;
622
+ const parsed = JSON.parse(raw);
623
+ if (typeof parsed.session_id !== "string" || parsed.session_id.length === 0) return null;
624
+ return { session_id: parsed.session_id, params: sanitizeParams(parsed.params) };
625
+ } catch {
626
+ return null;
627
+ }
628
+ }
629
+ function writeRecord(record) {
630
+ memoryRecord = record;
631
+ try {
632
+ window.localStorage.setItem(LANDING_STORAGE_KEY, JSON.stringify(record));
633
+ } catch {
634
+ }
635
+ }
636
+ function captureFromUrl(url) {
637
+ try {
638
+ const resolved = new URL(url ?? window.location.href, window.location.origin);
639
+ return getTrackingQueryValues(resolved.searchParams);
640
+ } catch {
641
+ return {};
642
+ }
643
+ }
644
+ function getOrCaptureLandingParams(sessionId, url) {
645
+ if (typeof window === "undefined") return {};
646
+ const stored = readStoredRecord();
647
+ if (stored && stored.session_id === sessionId) {
648
+ memoryRecord = stored;
649
+ return stored.params;
650
+ }
651
+ if (memoryRecord && memoryRecord.session_id === sessionId) {
652
+ return memoryRecord.params;
653
+ }
654
+ const record = { session_id: sessionId, params: captureFromUrl(url) };
655
+ writeRecord(record);
656
+ return record.params;
657
+ }
658
+ function buildLandingPayloadFields(sessionId, override) {
659
+ const params = override ?? (typeof window === "undefined" ? null : getOrCaptureLandingParams(sessionId));
660
+ if (params === null) return {};
661
+ return {
662
+ landing_gclid: params.gclid ?? null,
663
+ landing_wbraid: params.wbraid ?? null,
664
+ landing_gbraid: params.gbraid ?? null,
665
+ landing_ylpcid: params.ylpcid ?? null,
666
+ landing_fbclid: params.fbclid ?? null,
667
+ landing_utm_source: params.utm_source ?? null,
668
+ landing_utm_medium: params.utm_medium ?? null,
669
+ landing_utm_campaign: params.utm_campaign ?? null,
670
+ landing_utm_term: params.utm_term ?? null,
671
+ landing_utm_content: params.utm_content ?? null
672
+ };
673
+ }
674
+
675
+ // ../tracking-core/src/resources/conversion-firing.ts
676
+ var DEDUP_PREFIX = "_aranova_conv_";
677
+ function dedupKey(input) {
678
+ return `${DEDUP_PREFIX}${input.transactionId ?? ""}:${input.sendTo}`;
679
+ }
680
+ function alreadyFired(input) {
681
+ if (!input.transactionId || typeof window === "undefined") return false;
682
+ try {
683
+ return window.sessionStorage.getItem(dedupKey(input)) !== null;
684
+ } catch {
685
+ return false;
686
+ }
687
+ }
688
+ function markFired(input) {
689
+ if (!input.transactionId || typeof window === "undefined") return;
690
+ try {
691
+ window.sessionStorage.setItem(dedupKey(input), "1");
692
+ } catch {
693
+ }
694
+ }
695
+ function fireConversionWithConsent(input, options) {
696
+ if (getConsentState() === "denied") return "denied";
697
+ if (alreadyFired(input)) return "duplicate";
698
+ if (!isValidSendTo(input.sendTo)) return "invalid";
699
+ try {
700
+ applyUserDataForConversion(options?.userData);
701
+ if (!fireGtagConversion(input)) return "retryable";
702
+ markFired(input);
703
+ return "fired";
704
+ } catch {
705
+ return "retryable";
706
+ }
707
+ }
708
+
709
+ // ../tracking-core/src/resources/conversion-config.ts
710
+ function isStringMap(value) {
711
+ return typeof value === "object" && value !== null && Object.values(value).every((v) => typeof v === "string");
712
+ }
713
+ function parseFiring(value) {
714
+ if (!value || typeof value !== "object") return null;
715
+ const f = value;
716
+ if (typeof f.send_to !== "string") return null;
717
+ return {
718
+ send_to: f.send_to,
719
+ value_cents: typeof f.value_cents === "number" ? f.value_cents : null,
720
+ currency: typeof f.currency === "string" ? f.currency : null
721
+ };
722
+ }
723
+ function parseTrigger(value) {
724
+ if (!value || typeof value !== "object") return null;
725
+ const t = value;
726
+ if (typeof t.event_type !== "string") return null;
727
+ const spec = { event_type: t.event_type };
728
+ if (typeof t.threshold_percent === "number") spec.threshold_percent = t.threshold_percent;
729
+ if (typeof t.threshold_seconds === "number") spec.threshold_seconds = t.threshold_seconds;
730
+ if (typeof t.page_threshold === "number") spec.page_threshold = t.page_threshold;
731
+ if (typeof t.page_name === "string") spec.page_name = t.page_name;
732
+ return spec;
733
+ }
734
+ function parseConversionConfig(raw) {
735
+ if (!raw || typeof raw !== "object") return null;
736
+ const obj = raw;
737
+ const servicesRaw = Array.isArray(obj.services) ? obj.services : [];
738
+ const services = servicesRaw.flatMap((entry) => {
739
+ if (!entry || typeof entry !== "object") return [];
740
+ const s = entry;
741
+ if (typeof s.key !== "string") return [];
742
+ return [
743
+ {
744
+ key: s.key,
745
+ label: typeof s.label === "string" ? s.label : void 0,
746
+ firing: parseFiring(s.firing)
747
+ }
748
+ ];
749
+ });
750
+ const goalsRaw = Array.isArray(obj.goals) ? obj.goals : null;
751
+ const goals = goalsRaw ? goalsRaw.flatMap((entry) => {
752
+ if (!entry || typeof entry !== "object") return [];
753
+ const g = entry;
754
+ if (typeof g.key !== "string") return [];
755
+ return [
756
+ {
757
+ key: g.key,
758
+ label: typeof g.label === "string" ? g.label : void 0,
759
+ kind: g.kind === "event" ? "event" : "sale",
760
+ trigger: parseTrigger(g.trigger),
761
+ firing: parseFiring(g.firing)
762
+ }
763
+ ];
764
+ }) : services.map((s) => ({
765
+ key: s.key,
766
+ label: s.label,
767
+ kind: "sale",
768
+ trigger: null,
769
+ firing: s.firing
770
+ }));
771
+ return {
772
+ schema_version: typeof obj.schema_version === "number" ? obj.schema_version : 1,
773
+ config_version: typeof obj.config_version === "number" ? obj.config_version : 0,
774
+ business_id: typeof obj.business_id === "string" ? obj.business_id : void 0,
775
+ customer_id: typeof obj.customer_id === "string" ? obj.customer_id : null,
776
+ environment: typeof obj.environment === "string" ? obj.environment : void 0,
777
+ google_tracking_state: obj.google_tracking_state === "active" ? "active" : "disabled",
778
+ gtag_ids: isStringMap(obj.gtag_ids) ? obj.gtag_ids : {},
779
+ meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
780
+ services,
781
+ goals
782
+ };
783
+ }
784
+ var CACHE_PREFIX = "_aranova_cfg_";
785
+ function cacheKey(url) {
786
+ return `${CACHE_PREFIX}${url}`;
787
+ }
788
+ function readCache(url) {
789
+ if (typeof window === "undefined") return null;
790
+ try {
791
+ const raw = window.sessionStorage.getItem(cacheKey(url));
792
+ if (!raw) return null;
793
+ const parsed = JSON.parse(raw);
794
+ const config = parseConversionConfig(parsed.config);
795
+ if (!config) return null;
796
+ return {
797
+ etag: typeof parsed.etag === "string" ? parsed.etag : null,
798
+ config
799
+ };
800
+ } catch {
801
+ return null;
802
+ }
803
+ }
804
+ function writeCache(url, entry) {
805
+ if (typeof window === "undefined") return;
806
+ try {
807
+ window.sessionStorage.setItem(cacheKey(url), JSON.stringify(entry));
808
+ } catch {
809
+ }
810
+ }
811
+ function resolveConversionConfig(options) {
812
+ const cached = readCache(options.cdnUrl);
813
+ let current = cached?.config ?? options.baked ?? null;
814
+ let etag = cached?.etag ?? null;
815
+ const goalsByKey = /* @__PURE__ */ new Map();
816
+ const resolveListeners = /* @__PURE__ */ new Set();
817
+ function rebuildIndex() {
818
+ goalsByKey.clear();
819
+ for (const goal of current?.goals ?? []) {
820
+ goalsByKey.set(goal.key, goal);
821
+ }
822
+ }
823
+ function notifyResolved() {
824
+ const listeners2 = [...resolveListeners];
825
+ resolveListeners.clear();
826
+ for (const listener of listeners2) {
827
+ try {
828
+ listener();
829
+ } catch {
830
+ }
831
+ }
832
+ }
833
+ function adopt(next, nextEtag) {
834
+ if (!next) return;
835
+ if (current && next.config_version <= current.config_version) return;
836
+ const wasEmpty = current === null;
837
+ current = next;
838
+ etag = nextEtag;
839
+ rebuildIndex();
840
+ writeCache(options.cdnUrl, { etag, config: next });
841
+ if (wasEmpty) notifyResolved();
842
+ }
843
+ async function revalidate() {
844
+ if (typeof window === "undefined") return;
845
+ try {
846
+ const doFetch = options.fetchImpl ?? globalThis.fetch;
847
+ if (!doFetch) return;
848
+ const headers = {};
849
+ if (etag) headers["If-None-Match"] = etag;
850
+ const response = await doFetch(options.cdnUrl, {
851
+ method: "GET",
852
+ headers
853
+ });
854
+ if (response.status === 304 || !response.ok) return;
855
+ adopt(parseConversionConfig(await response.json()), response.headers.get("ETag"));
856
+ } catch {
857
+ }
858
+ }
859
+ rebuildIndex();
860
+ void revalidate();
861
+ return {
862
+ getFiring: (key) => goalsByKey.get(key)?.firing ?? null,
863
+ getGoal: (key) => goalsByKey.get(key) ?? null,
864
+ listGoals: () => [...goalsByKey.values()],
865
+ current: () => current,
866
+ isReady: () => current !== null,
867
+ onResolve: (listener) => {
868
+ if (current !== null) {
869
+ listener();
870
+ return () => {
871
+ };
872
+ }
873
+ resolveListeners.add(listener);
874
+ return () => resolveListeners.delete(listener);
875
+ },
876
+ revalidate
877
+ };
878
+ }
879
+
880
+ // ../tracking-core/src/resources/automatic-transaction.ts
881
+ var STORAGE_KEY = "_aranova_auto_txn_map";
882
+ var transactionIds = /* @__PURE__ */ new Map();
883
+ var legacyCounter = 0;
884
+ function scopeKey(sessionId, goalKey, path) {
885
+ return JSON.stringify([sessionId, goalKey, path]);
886
+ }
887
+ function randomId() {
888
+ const cryptoApi = globalThis.crypto;
889
+ if (typeof cryptoApi?.randomUUID === "function") return cryptoApi.randomUUID();
890
+ if (typeof cryptoApi?.getRandomValues === "function") {
891
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(16));
892
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
893
+ }
894
+ legacyCounter = (legacyCounter + 1) % 4294967296;
895
+ const timestamp = Date.now().toString(16).padStart(12, "0");
896
+ const counter = legacyCounter.toString(16).padStart(8, "0");
897
+ const random = Math.floor(Math.random() * 281474976710655).toString(16).padStart(12, "0");
898
+ return `${timestamp}${counter}${random}`.slice(0, 32);
899
+ }
900
+ function isValidTransactionId(value) {
901
+ return typeof value === "string" && /^auto:(?:[0-9a-f]{32}|[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(
902
+ value
903
+ ) && value.length <= 64;
904
+ }
905
+ function readStoredMap(sessionId) {
906
+ if (typeof window === "undefined") return { sessionId, entries: {} };
907
+ try {
908
+ const raw = window.localStorage.getItem(STORAGE_KEY);
909
+ if (!raw) return { sessionId, entries: {} };
910
+ const parsed = JSON.parse(raw);
911
+ if (parsed.sessionId !== sessionId || !parsed.entries || typeof parsed.entries !== "object" || Array.isArray(parsed.entries)) {
912
+ return { sessionId, entries: {} };
913
+ }
914
+ return { sessionId, entries: parsed.entries };
915
+ } catch {
916
+ return { sessionId, entries: {} };
917
+ }
918
+ }
919
+ function writeStoredMap(stored) {
920
+ if (typeof window === "undefined") return;
921
+ try {
922
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
923
+ } catch {
924
+ }
925
+ }
926
+ function getAutomaticTransactionId(sessionId, goalKey, path) {
927
+ const key = scopeKey(sessionId, goalKey, path);
928
+ const existing = transactionIds.get(key);
929
+ if (existing) return existing;
930
+ const stored = readStoredMap(sessionId);
931
+ const storedId = stored.entries[key];
932
+ if (isValidTransactionId(storedId)) {
933
+ transactionIds.set(key, storedId);
934
+ return storedId;
935
+ }
936
+ const transactionId = `auto:${randomId()}`;
937
+ transactionIds.set(key, transactionId);
938
+ const latest = readStoredMap(sessionId);
939
+ const concurrentId = latest.entries[key];
940
+ if (isValidTransactionId(concurrentId)) {
941
+ transactionIds.set(key, concurrentId);
942
+ return concurrentId;
943
+ }
944
+ latest.entries[key] = transactionId;
945
+ writeStoredMap(latest);
946
+ return transactionId;
947
+ }
948
+
949
+ // ../tracking-core/src/resources/automatic-trigger.ts
950
+ function hasNumberField(metadata, key) {
951
+ return typeof metadata[key] === "number";
952
+ }
953
+ function hasStringField(metadata, key) {
954
+ return typeof metadata[key] === "string";
955
+ }
956
+ function automaticThresholdMet(goal, eventType, metadata) {
957
+ const trigger = goal.trigger;
958
+ if (!trigger || trigger.event_type !== eventType) return false;
959
+ switch (eventType) {
960
+ case "scroll_depth":
961
+ return hasNumberField(metadata, "depth_percent") && trigger.threshold_percent != null && metadata.depth_percent >= trigger.threshold_percent;
962
+ case "time_on_site":
963
+ return hasNumberField(metadata, "duration_ms") && trigger.threshold_seconds != null && metadata.duration_ms >= trigger.threshold_seconds * 1e3;
964
+ case "multi_page_session":
965
+ return hasNumberField(metadata, "page_count") && trigger.page_threshold != null && metadata.page_count >= trigger.page_threshold;
966
+ case "specific_page_visit":
967
+ return hasStringField(metadata, "page_name") && metadata.page_name === trigger.page_name;
968
+ case "page_view":
969
+ case "form_start":
970
+ case "phone_click":
971
+ return true;
972
+ default:
973
+ return false;
974
+ }
975
+ }
976
+
977
+ // ../tracking-core/src/resources/sales/money.ts
978
+ var MINOR_UNIT_EXPONENT = {
979
+ USD: 2,
980
+ CAD: 2
981
+ };
982
+ function exponentFor(currency) {
983
+ return MINOR_UNIT_EXPONENT[currency] ?? 2;
984
+ }
985
+ function fromMinor(cents, currency) {
986
+ return cents / 10 ** exponentFor(currency);
987
+ }
988
+
989
+ // ../tracking-core/src/resources/tracking-config-runtime.ts
990
+ var DEFAULT_CDN_BASE_URL = "https://demos.aranova.io";
991
+ function trackingConfigKey(businessId, environment) {
992
+ return `tracking-config/v1/${businessId}-${environment}.json`;
993
+ }
994
+ function resolveTrackingConfigUrl(ref) {
995
+ if (ref.cdnUrl) return ref.cdnUrl;
996
+ const configured = ref.cdnBaseUrl?.trim();
997
+ const base = (configured || DEFAULT_CDN_BASE_URL).replace(/\/+$/, "");
998
+ return `${base}/${trackingConfigKey(ref.businessId, ref.environment)}`;
999
+ }
1000
+ var CACHE_PREFIX2 = "_aranova_cfg_runtime_";
1001
+ var AUTHORITY_TTL_MS = 6e4;
1002
+ var MAX_QUEUE_LENGTH = 50;
1003
+ var RETRY_BASE_MS = 5e3;
1004
+ var RETRY_MAX_MS = 5 * 6e4;
1005
+ var runtimes = /* @__PURE__ */ new Map();
1006
+ var configuredIds = /* @__PURE__ */ new Set();
1007
+ var scriptLoad = null;
1008
+ var jsInitialized = false;
1009
+ function cacheKey2(url) {
1010
+ return `${CACHE_PREFIX2}${url}`;
1011
+ }
1012
+ function pushBounded(queue, item) {
1013
+ queue.push(item);
1014
+ if (queue.length > MAX_QUEUE_LENGTH) queue.splice(0, queue.length - MAX_QUEUE_LENGTH);
1015
+ }
1016
+ function readCache2(url) {
1017
+ if (typeof window === "undefined") return null;
1018
+ try {
1019
+ const raw = window.sessionStorage.getItem(cacheKey2(url));
1020
+ if (!raw) return null;
1021
+ const parsed = JSON.parse(raw);
1022
+ const config = parseConversionConfig(parsed.config);
1023
+ if (!config) return null;
1024
+ return {
1025
+ etag: typeof parsed.etag === "string" ? parsed.etag : null,
1026
+ config
1027
+ };
1028
+ } catch {
1029
+ return null;
1030
+ }
1031
+ }
1032
+ function writeCache2(url, entry) {
1033
+ if (typeof window === "undefined") return;
1034
+ try {
1035
+ window.sessionStorage.setItem(cacheKey2(url), JSON.stringify(entry));
1036
+ } catch {
1037
+ }
1038
+ }
1039
+ function isTombstone(config) {
1040
+ return config.google_tracking_state === "disabled" || Object.keys(config.gtag_ids).length === 0;
1041
+ }
1042
+ function validateConfig(config, ref) {
1043
+ return config.business_id === ref.businessId && config.environment === ref.environment && (config.google_tracking_state === "active" || config.google_tracking_state === "disabled");
1044
+ }
1045
+ function pageViewSnapshot() {
1046
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
1047
+ return {
1048
+ href: window.location.href,
1049
+ title: document.title || null,
1050
+ referrer: document.referrer || null
1051
+ };
1052
+ }
1053
+ function ensureScript(gtagId) {
1054
+ if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
1055
+ applyDefaultConsentState();
1056
+ loadGtagScript(gtagId);
1057
+ if (scriptLoad) return scriptLoad;
1058
+ scriptLoad = new Promise((resolve) => {
1059
+ const script = document.querySelector(
1060
+ 'script[data-aranova-tracking="aranova-gtag-loader"]'
1061
+ );
1062
+ if (!script) {
1063
+ resolve();
1064
+ return;
1065
+ }
1066
+ if (script.dataset.loaded === "true") {
1067
+ resolve();
1068
+ return;
1069
+ }
1070
+ window.setTimeout(resolve, 0);
1071
+ script.addEventListener(
1072
+ "load",
1073
+ () => {
1074
+ script.dataset.loaded = "true";
1075
+ resolve();
1076
+ },
1077
+ { once: true }
1078
+ );
1079
+ script.addEventListener("error", () => resolve(), { once: true });
1080
+ });
1081
+ return scriptLoad;
1082
+ }
1083
+ var TrackingConfigRuntime = class {
1084
+ constructor(ref, fetchImpl = globalThis.fetch) {
1085
+ this.ref = ref;
1086
+ this.current = null;
1087
+ this.etag = null;
1088
+ this.stateValue = "unconfirmed";
1089
+ this.confirmedAt = 0;
1090
+ this.authorityGeneration = 0;
1091
+ this.inFlight = null;
1092
+ this.flushInFlight = null;
1093
+ this.flushRequested = false;
1094
+ this.retryTimer = null;
1095
+ this.started = false;
1096
+ /** Consecutive failed authority attempts — drives the revalidate backoff. */
1097
+ this.authorityFailures = 0;
1098
+ /** Epoch ms before which `ensureAuthority` must not issue another request. */
1099
+ this.nextAuthorityAttemptAt = 0;
1100
+ this.conversionQueue = [];
1101
+ this.automaticQueue = [];
1102
+ this.pageQueue = [];
1103
+ this.listeners = /* @__PURE__ */ new Set();
1104
+ this.fetchImpl = fetchImpl.bind(globalThis);
1105
+ this.url = resolveTrackingConfigUrl(ref);
1106
+ const cached = readCache2(this.url);
1107
+ this.current = cached?.config ?? null;
1108
+ this.etag = cached?.etag ?? null;
1109
+ this.start();
1110
+ }
1111
+ /** The URL this runtime actually fetches (composed or explicit). */
1112
+ configUrl() {
1113
+ return this.url;
1114
+ }
1115
+ /**
1116
+ * Explicitly start authority resolution and Google-tag bootstrap.
1117
+ *
1118
+ * Idempotent so framework effects can call it after hydration without
1119
+ * depending on constructor timing.
1120
+ */
1121
+ start() {
1122
+ if (this.started) {
1123
+ void this.flush();
1124
+ return;
1125
+ }
1126
+ this.started = true;
1127
+ void this.revalidate();
1128
+ if (typeof window !== "undefined") {
1129
+ window.addEventListener("visibilitychange", () => {
1130
+ if (document.visibilityState === "visible") void this.revalidate();
1131
+ });
1132
+ window.addEventListener("focus", () => void this.revalidate());
1133
+ }
1134
+ }
1135
+ state() {
1136
+ return this.stateValue;
1137
+ }
1138
+ config() {
1139
+ return this.stateValue === "active" || this.stateValue === "tombstone" ? this.current : null;
1140
+ }
1141
+ __unsafeExpireAuthorityForTests() {
1142
+ this.confirmedAt = Number.NEGATIVE_INFINITY;
1143
+ this.nextAuthorityAttemptAt = 0;
1144
+ }
1145
+ /** Queue depths — asserted by tests to pin the bound. */
1146
+ __queueDepthsForTests() {
1147
+ return {
1148
+ conversions: this.conversionQueue.length,
1149
+ automatic: this.automaticQueue.length,
1150
+ pages: this.pageQueue.length
1151
+ };
1152
+ }
1153
+ subscribe(listener) {
1154
+ this.listeners.add(listener);
1155
+ return () => this.listeners.delete(listener);
1156
+ }
1157
+ async ensureAuthority() {
1158
+ if (this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {
1159
+ return true;
1160
+ }
1161
+ if (Date.now() < this.nextAuthorityAttemptAt) return false;
1162
+ await this.revalidateAuthority();
1163
+ return this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;
1164
+ }
1165
+ async revalidate() {
1166
+ this.nextAuthorityAttemptAt = 0;
1167
+ await this.revalidateAuthority();
1168
+ await this.flush();
1169
+ }
1170
+ async revalidateAuthority() {
1171
+ if (typeof window === "undefined" || !this.fetchImpl) return;
1172
+ if (this.inFlight) return this.inFlight;
1173
+ this.inFlight = this.revalidateNow().finally(() => {
1174
+ this.inFlight = null;
1175
+ });
1176
+ return this.inFlight;
1177
+ }
1178
+ queuePageView(snapshot = pageViewSnapshot()) {
1179
+ if (!snapshot) return;
1180
+ pushBounded(this.pageQueue, snapshot);
1181
+ void this.flush();
1182
+ }
1183
+ fireConversion(key, options) {
1184
+ pushBounded(this.conversionQueue, { key, ...options });
1185
+ void this.flush();
1186
+ }
1187
+ queueAutomaticEvent(eventType, metadata, transactionPath, transactionScope) {
1188
+ pushBounded(this.automaticQueue, {
1189
+ eventType,
1190
+ metadata,
1191
+ transactionPath,
1192
+ transactionScope
1193
+ });
1194
+ void this.flush();
1195
+ }
1196
+ listGoals() {
1197
+ return this.current?.goals ?? [];
1198
+ }
1199
+ async revalidateNow() {
1200
+ try {
1201
+ const headers = {};
1202
+ if (this.etag) headers["If-None-Match"] = this.etag;
1203
+ const response = await this.fetchImpl(this.url, {
1204
+ method: "GET",
1205
+ headers,
1206
+ cache: "no-cache"
1207
+ });
1208
+ if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {
1209
+ this.confirm(this.current, this.etag);
1210
+ return;
1211
+ }
1212
+ if (!response.ok) {
1213
+ this.expireAuthority();
1214
+ return;
1215
+ }
1216
+ const next = parseConversionConfig(await response.json());
1217
+ if (!next || !validateConfig(next, this.ref)) {
1218
+ this.expireAuthority();
1219
+ return;
1220
+ }
1221
+ if (this.current && next.config_version < this.current.config_version) {
1222
+ this.expireAuthority();
1223
+ return;
1224
+ }
1225
+ this.confirm(next, response.headers.get("ETag"));
1226
+ } catch {
1227
+ this.expireAuthority();
1228
+ }
1229
+ }
1230
+ expireAuthority() {
1231
+ this.authorityGeneration += 1;
1232
+ if (this.stateValue !== "unconfirmed") this.stateValue = "unconfirmed";
1233
+ this.authorityFailures += 1;
1234
+ const delay = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** (this.authorityFailures - 1));
1235
+ this.nextAuthorityAttemptAt = Date.now() + delay;
1236
+ }
1237
+ confirm(config, etag) {
1238
+ this.authorityGeneration += 1;
1239
+ this.current = config;
1240
+ this.etag = etag;
1241
+ this.confirmedAt = Date.now();
1242
+ this.authorityFailures = 0;
1243
+ this.nextAuthorityAttemptAt = 0;
1244
+ this.stateValue = isTombstone(config) ? "tombstone" : "active";
1245
+ writeCache2(this.url, { etag, config });
1246
+ if (this.stateValue === "tombstone") {
1247
+ if (this.retryTimer !== null) {
1248
+ window.clearTimeout(this.retryTimer);
1249
+ this.retryTimer = null;
1250
+ }
1251
+ this.conversionQueue.length = 0;
1252
+ this.automaticQueue.length = 0;
1253
+ this.pageQueue.length = 0;
1254
+ }
1255
+ for (const listener of this.listeners) listener();
1256
+ }
1257
+ async flush() {
1258
+ if (this.flushInFlight) {
1259
+ this.flushRequested = true;
1260
+ return this.flushInFlight;
1261
+ }
1262
+ this.flushRequested = false;
1263
+ this.flushInFlight = this.flushNow().finally(() => {
1264
+ this.flushInFlight = null;
1265
+ if (this.flushRequested && this.stateValue === "active") void this.flush();
1266
+ });
1267
+ return this.flushInFlight;
1268
+ }
1269
+ scheduleRetry() {
1270
+ if (this.retryTimer !== null || typeof window === "undefined") return;
1271
+ this.retryTimer = window.setTimeout(() => {
1272
+ this.retryTimer = null;
1273
+ void this.flush();
1274
+ }, 1e3);
1275
+ }
1276
+ async flushNow() {
1277
+ if (!await this.ensureAuthority()) return;
1278
+ if (this.stateValue !== "active" || !this.current) return;
1279
+ const config = this.current;
1280
+ const generation = this.authorityGeneration;
1281
+ const ids = Object.values(config.gtag_ids).filter(
1282
+ (id) => typeof id === "string" && isValidGtagId(id)
1283
+ );
1284
+ if (ids.length === 0) return;
1285
+ await ensureScript(ids[0]);
1286
+ if (this.authorityGeneration !== generation || this.stateValue !== "active" || this.current !== config) {
1287
+ return;
1288
+ }
1289
+ const gtag = window.gtag;
1290
+ if (typeof gtag !== "function") return;
1291
+ try {
1292
+ if (!jsInitialized) {
1293
+ gtag("js", /* @__PURE__ */ new Date());
1294
+ jsInitialized = true;
1295
+ }
1296
+ for (const id of ids) {
1297
+ if (configuredIds.has(id)) continue;
1298
+ gtag("config", id, { send_page_view: false });
1299
+ configuredIds.add(id);
1300
+ }
1301
+ } catch {
1302
+ this.scheduleRetry();
1303
+ return;
1304
+ }
1305
+ while (this.pageQueue.length > 0) {
1306
+ const page = this.pageQueue[0];
1307
+ try {
1308
+ gtag("event", "page_view", {
1309
+ page_location: page.href,
1310
+ page_title: page.title ?? void 0,
1311
+ page_referrer: page.referrer ?? void 0
1312
+ });
1313
+ this.pageQueue.shift();
1314
+ } catch {
1315
+ this.scheduleRetry();
1316
+ return;
1317
+ }
1318
+ }
1319
+ while (this.automaticQueue.length > 0) {
1320
+ const event = this.automaticQueue[0];
1321
+ for (const goal of config.goals) {
1322
+ if (goal.kind !== "event" || !goal.firing) continue;
1323
+ if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;
1324
+ pushBounded(this.conversionQueue, {
1325
+ key: goal.key,
1326
+ transactionId: getAutomaticTransactionId(
1327
+ event.transactionScope,
1328
+ goal.key,
1329
+ event.transactionPath
1330
+ )
1331
+ });
1332
+ }
1333
+ this.automaticQueue.shift();
1334
+ }
1335
+ while (this.conversionQueue.length > 0) {
1336
+ const item = this.conversionQueue[0];
1337
+ const goal = config.goals.find((g) => g.key === item.key);
1338
+ const firing = goal?.firing;
1339
+ if (!firing) {
1340
+ this.conversionQueue.shift();
1341
+ continue;
1342
+ }
1343
+ const currency = item.currency ?? firing.currency ?? null;
1344
+ const value = item.value ?? (firing.value_cents != null && currency ? fromMinor(firing.value_cents, currency) : null);
1345
+ const outcome = fireConversionWithConsent({
1346
+ sendTo: firing.send_to,
1347
+ value,
1348
+ currency,
1349
+ transactionId: item.transactionId ?? null
1350
+ });
1351
+ if (outcome === "retryable") {
1352
+ this.scheduleRetry();
1353
+ return;
1354
+ }
1355
+ this.conversionQueue.shift();
1356
+ }
1357
+ }
1358
+ };
1359
+ function getTrackingConfigRuntime(ref, fetchImpl) {
1360
+ const key = `${resolveTrackingConfigUrl(ref)}|${ref.businessId}|${ref.environment}`;
1361
+ const existing = runtimes.get(key);
1362
+ if (existing) return existing;
1363
+ const runtime = new TrackingConfigRuntime(ref, fetchImpl ?? globalThis.fetch);
1364
+ runtimes.set(key, runtime);
1365
+ return runtime;
1366
+ }
1367
+
1368
+ // ../tracking-core/src/resources/conversion-autofire.ts
1369
+ function currentPath() {
1370
+ return typeof window === "undefined" ? "" : window.location.pathname;
1371
+ }
1372
+ var MAX_BUFFERED_EVENTS = 50;
1373
+ function createConversionAutoFire(store) {
1374
+ const pending = [];
1375
+ let subscribed = false;
1376
+ function fireMatching(eventType, metadata, transactionScope) {
1377
+ for (const goal of store.listGoals()) {
1378
+ if (goal.kind !== "event" || !goal.firing) continue;
1379
+ if (!automaticThresholdMet(goal, eventType, metadata)) continue;
1380
+ const transactionId = getAutomaticTransactionId(transactionScope, goal.key, currentPath());
1381
+ if ("queueAutomaticEvent" in store) {
1382
+ store.fireConversion(goal.key, {
1383
+ transactionId
1384
+ });
1385
+ continue;
1386
+ }
1387
+ const firing = goal.firing;
1388
+ const cents = firing.value_cents ?? null;
1389
+ const currency = firing.currency ?? null;
1390
+ fireConversionWithConsent({
1391
+ sendTo: firing.send_to,
1392
+ value: cents != null && currency ? fromMinor(cents, currency) : null,
1393
+ currency,
1394
+ transactionId
1395
+ });
1396
+ }
1397
+ }
1398
+ return {
1399
+ onAutomaticEvent(eventType, metadata, transactionScope) {
1400
+ if ("queueAutomaticEvent" in store) {
1401
+ store.queueAutomaticEvent(eventType, metadata, currentPath(), transactionScope);
1402
+ return;
1403
+ }
1404
+ if (store.isReady()) {
1405
+ fireMatching(eventType, metadata, transactionScope);
1406
+ return;
1407
+ }
1408
+ if (pending.length < MAX_BUFFERED_EVENTS) {
1409
+ pending.push({ eventType, metadata, transactionScope });
1410
+ }
1411
+ if (!subscribed) {
1412
+ subscribed = true;
1413
+ store.onResolve(() => {
1414
+ const buffered = pending.splice(0);
1415
+ for (const event of buffered) {
1416
+ fireMatching(event.eventType, event.metadata, event.transactionScope);
1417
+ }
1418
+ });
1419
+ }
1420
+ }
1421
+ };
1422
+ }
1423
+ function withConversionAutoFire(client, autoFire) {
1424
+ return {
1425
+ ...client,
1426
+ trackEvent: (input) => {
1427
+ client.trackEvent(input);
1428
+ try {
1429
+ autoFire.onAutomaticEvent(input.eventType, input.metadata ?? {}, client.getSessionId());
1430
+ } catch {
1431
+ }
1432
+ }
1433
+ };
1434
+ }
1435
+
1436
+ // ../tracking-core/src/session.ts
1437
+ var VISITOR_STORAGE_KEY = "aranova_tracking_visitor";
1438
+ var SESSION_STORAGE_KEY = "aranova_tracking_session";
1439
+ var SESSION_IDLE_MS = 30 * 60 * 1e3;
1440
+ function safeUuid() {
1441
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function")
1442
+ return crypto.randomUUID();
1443
+ return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;
1444
+ }
1445
+ function readLocalStorage(key) {
1446
+ try {
1447
+ return window.localStorage.getItem(key);
1448
+ } catch {
1449
+ return null;
1450
+ }
1451
+ }
1452
+ function writeLocalStorage(key, value) {
1453
+ try {
1454
+ window.localStorage.setItem(key, value);
1455
+ } catch {
1456
+ }
1457
+ }
1458
+ function getVisitorId() {
1459
+ if (typeof window === "undefined") return safeUuid();
1460
+ const existing = readLocalStorage(VISITOR_STORAGE_KEY);
1461
+ if (existing && existing.length > 0) return existing;
1462
+ const fresh = safeUuid();
1463
+ writeLocalStorage(VISITOR_STORAGE_KEY, fresh);
1464
+ return fresh;
1465
+ }
1466
+ function getOrRotateSessionId(now = Date.now()) {
1467
+ if (typeof window === "undefined") return { id: safeUuid(), isNew: true };
1468
+ const raw = readLocalStorage(SESSION_STORAGE_KEY);
1469
+ if (raw) {
1470
+ try {
1471
+ const parsed = JSON.parse(raw);
1472
+ if (typeof parsed.id === "string" && typeof parsed.last_event_at === "number") {
1473
+ if (now - parsed.last_event_at <= SESSION_IDLE_MS) {
1474
+ const refreshed = { id: parsed.id, last_event_at: now };
1475
+ writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(refreshed));
1476
+ return { id: parsed.id, isNew: false };
1477
+ }
1478
+ }
1479
+ } catch {
1480
+ }
1481
+ }
1482
+ const fresh = { id: safeUuid(), last_event_at: now };
1483
+ writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(fresh));
1484
+ return { id: fresh.id, isNew: true };
1485
+ }
1486
+
1487
+ // ../tracking-core/src/events/page-view.ts
1488
+ var import_zod = require("zod");
1489
+ var pageViewMetadataSchema = import_zod.z.object({
1490
+ page: import_zod.z.object({
1491
+ title: import_zod.z.string().nullable(),
1492
+ path: import_zod.z.string(),
1493
+ search: import_zod.z.string(),
1494
+ hash: import_zod.z.string()
1495
+ }).strict(),
1496
+ referrer: import_zod.z.string().nullable(),
1497
+ // `.nullable().optional()` — absent (undefined) OR explicit null OR a
1498
+ // real viewport object. Mirrors Pydantic's `_Viewport | None = None`
1499
+ // on the backend side so the drift test stays clean.
1500
+ viewport: import_zod.z.object({
1501
+ w: import_zod.z.number(),
1502
+ h: import_zod.z.number()
1503
+ }).strict().nullable().optional()
1504
+ }).strict();
1505
+ var pageViewConfigSchema = import_zod.z.object({}).strict();
1506
+
1507
+ // ../tracking-core/src/page-view.ts
1508
+ var LAST_FIRED_URL_STORAGE_KEY = "aranova_tracking_last_fired_url";
1509
+ var lastFiredUrl = null;
1510
+ var lastFiredUrlHydrated = false;
1511
+ function readSessionStorage(key) {
1512
+ try {
1513
+ if (typeof window === "undefined") return null;
1514
+ return window.sessionStorage.getItem(key);
1515
+ } catch {
1516
+ return null;
1517
+ }
1518
+ }
1519
+ function writeSessionStorage(key, value) {
1520
+ try {
1521
+ if (typeof window === "undefined") return;
1522
+ window.sessionStorage.setItem(key, value);
1523
+ } catch {
1524
+ }
1525
+ }
1526
+ function getLastFiredUrl() {
1527
+ if (!lastFiredUrlHydrated) {
1528
+ lastFiredUrlHydrated = true;
1529
+ const stored = readSessionStorage(LAST_FIRED_URL_STORAGE_KEY);
1530
+ if (stored !== null) lastFiredUrl = stored;
1531
+ }
1532
+ return lastFiredUrl;
1533
+ }
1534
+ function setLastFiredUrl(url) {
1535
+ lastFiredUrl = url;
1536
+ lastFiredUrlHydrated = true;
1537
+ writeSessionStorage(LAST_FIRED_URL_STORAGE_KEY, url);
1538
+ }
1539
+ function buildPageViewMetadata(referrerOverride) {
1540
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
1541
+ return pageViewMetadataSchema.parse({
1542
+ page: {
1543
+ title: document.title || null,
1544
+ path: window.location.pathname,
1545
+ search: window.location.search,
1546
+ hash: window.location.hash
1547
+ },
1548
+ referrer: referrerOverride !== void 0 ? referrerOverride : document.referrer || null,
1549
+ viewport: { w: window.innerWidth, h: window.innerHeight }
1550
+ });
1551
+ }
1552
+ function fireManualPageView(client) {
1553
+ if (typeof window === "undefined") return;
1554
+ const currentHref = window.location.href;
1555
+ const previousFiredUrl = getLastFiredUrl();
1556
+ const internalReferrer = previousFiredUrl !== null && previousFiredUrl !== currentHref ? previousFiredUrl : null;
1557
+ const externalReferrer = typeof document !== "undefined" ? document.referrer || null : null;
1558
+ const referrer = internalReferrer ?? externalReferrer;
1559
+ const metadata = buildPageViewMetadata(referrer);
1560
+ client.trackEvent({
1561
+ eventType: "page_view",
1562
+ pageUrl: currentHref,
1563
+ metadata
1564
+ });
1565
+ if (currentHref !== previousFiredUrl) {
1566
+ setLastFiredUrl(currentHref);
1567
+ }
1568
+ }
1569
+ function attachBfcacheRestore(client) {
1570
+ if (typeof window === "undefined") return () => {
1571
+ };
1572
+ function handlePageShow(event) {
1573
+ if (!event.persisted) return;
1574
+ fireManualPageView(client);
1575
+ }
1576
+ window.addEventListener("pageshow", handlePageShow);
1577
+ return () => {
1578
+ window.removeEventListener("pageshow", handlePageShow);
1579
+ };
1580
+ }
1581
+
1582
+ // ../tracking-core/src/heartbeat.ts
1583
+ function serializeValue(value) {
1584
+ if (value instanceof RegExp) return value.source;
1585
+ if (Array.isArray(value)) return value.map(serializeValue);
1586
+ if (value !== null && typeof value === "object") {
1587
+ const out = {};
1588
+ for (const [k, v] of Object.entries(value)) {
1589
+ out[k] = serializeValue(v);
1590
+ }
1591
+ return out;
1592
+ }
1593
+ return value;
1594
+ }
1595
+ function buildHeartbeatMetadata(surface, sdkVersion, packageName, triggers, gtagIds = null) {
1596
+ const automaticNames = triggers ? Object.keys(triggers.automatic) : [];
1597
+ const manualNames = triggers?.manual ? Object.keys(triggers.manual) : [];
1598
+ let triggerConfig = null;
1599
+ if (triggers) {
1600
+ const cfg = {};
1601
+ for (const [name, config] of Object.entries(triggers.automatic)) {
1602
+ const serialized = serializeValue(config);
1603
+ if (Object.keys(serialized).length > 0) {
1604
+ cfg[name] = serialized;
1605
+ }
1606
+ }
1607
+ if (triggers.manual) {
1608
+ for (const [name, config] of Object.entries(triggers.manual)) {
1609
+ if (config === void 0) continue;
1610
+ const serialized = serializeValue(config);
1611
+ if (Object.keys(serialized).length > 0) {
1612
+ cfg[name] = serialized;
1613
+ }
1614
+ }
1615
+ }
1616
+ if (Object.keys(cfg).length > 0) {
1617
+ triggerConfig = cfg;
1618
+ }
1619
+ }
1620
+ return {
1621
+ sdk_version: sdkVersion ?? "unknown",
1622
+ package_name: packageName,
1623
+ surface,
1624
+ triggers: {
1625
+ automatic: automaticNames,
1626
+ manual: manualNames
1627
+ },
1628
+ trigger_config: triggerConfig,
1629
+ configured_gtag_ids: gtagIds
1630
+ };
1631
+ }
1632
+
1633
+ // ../tracking-core/src/ingest.ts
1634
+ var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
1635
+ var DEFAULT_MAX_QUEUE_SIZE = 10;
1636
+ var HARD_MAX_BATCH = 50;
1637
+ var MAX_RETRY_BACKOFF_MS = 6e4;
1638
+ var MAX_BUFFERED_EVENTS2 = 200;
1639
+ var RETRY_BUFFER_KEY_PREFIX = "aranova_tracking_pending_v1";
1640
+ var API_KEY_HEADER = "X-Aranova-Api-Key";
1641
+ var SDK_VERSION_HEADER = "X-Aranova-Sdk-Version";
1642
+ var SDK_PACKAGE_HEADER = "X-Aranova-Sdk-Package";
1643
+ var SDK_SURFACE_HEADER = "X-Aranova-Sdk-Surface";
1644
+ var SDK_ENVIRONMENT_HEADER = "X-Aranova-Sdk-Environment";
1645
+ function buildContext(surface, sdkVersion, packageName, environment, activeGtagIds) {
1646
+ return {
1647
+ surface,
1648
+ sdk_version: sdkVersion,
1649
+ package_name: packageName,
1650
+ site_origin: typeof window === "undefined" ? null : window.location.origin,
1651
+ page_title: typeof document === "undefined" ? null : document.title || null,
1652
+ referrer: typeof document === "undefined" ? null : document.referrer || null,
1653
+ environment,
1654
+ active_gtag_ids: activeGtagIds,
1655
+ // Rebuilt per flush (this runs inside the payload builder), so a client
1656
+ // constructed later on a deeper route still gets reported.
1657
+ capabilities: getRegisteredCapabilities()
1658
+ };
1659
+ }
1660
+ function readTrackingParams() {
1661
+ if (typeof window === "undefined") return createEmptyTrackingParams();
1662
+ try {
1663
+ captureTrackingParamsFromLocation();
1664
+ } catch {
1665
+ }
1666
+ return getTrackingParamsFromCookieReader(getCookieValueFromDocument);
1667
+ }
1668
+ function consentSnapshot() {
1669
+ try {
1670
+ const choice = getConsentChoice();
1671
+ return {
1672
+ state: choice.state,
1673
+ source: choice.source,
1674
+ updated_at: choice.updatedAt,
1675
+ expires_at: choice.expiresAt
1676
+ };
1677
+ } catch {
1678
+ return null;
1679
+ }
1680
+ }
1681
+ async function postWithFetch(url, body, apiKey, identity, keepalive) {
1682
+ if (typeof fetch !== "function") return "drop";
1683
+ try {
1684
+ const response = await fetch(url, {
1685
+ method: "POST",
1686
+ headers: {
1687
+ "Content-Type": "application/json",
1688
+ [API_KEY_HEADER]: apiKey,
1689
+ [SDK_VERSION_HEADER]: identity.sdkVersion,
1690
+ [SDK_PACKAGE_HEADER]: identity.packageName,
1691
+ [SDK_SURFACE_HEADER]: identity.surface,
1692
+ [SDK_ENVIRONMENT_HEADER]: identity.environment
1693
+ },
1694
+ body,
1695
+ keepalive,
1696
+ // CORS is open on the tracking endpoint; never send cookies.
1697
+ credentials: "omit",
1698
+ mode: "cors"
1699
+ });
1700
+ if (response.ok) return "ok";
1701
+ if (response.status === 408 || response.status === 429 || response.status >= 500) {
1702
+ return "retry";
1703
+ }
1704
+ return "drop";
1705
+ } catch {
1706
+ return "retry";
1707
+ }
1708
+ }
1709
+ function retryBufferKey(apiKey, endpoint) {
1710
+ return `${RETRY_BUFFER_KEY_PREFIX}:${apiKey}:${endpoint}`;
1711
+ }
1712
+ function readPendingBatches(key) {
1713
+ if (typeof window === "undefined") return [];
1714
+ try {
1715
+ const raw = window.localStorage.getItem(key);
1716
+ if (!raw) return [];
1717
+ const parsed = JSON.parse(raw);
1718
+ if (!Array.isArray(parsed)) return [];
1719
+ return parsed.filter(
1720
+ (entry) => typeof entry === "object" && entry !== null && "session" in entry && Array.isArray(entry.events)
1721
+ );
1722
+ } catch {
1723
+ return [];
1724
+ }
1725
+ }
1726
+ function writePendingBatches(key, batches) {
1727
+ if (typeof window === "undefined") return;
1728
+ try {
1729
+ if (batches.length === 0) window.localStorage.removeItem(key);
1730
+ else window.localStorage.setItem(key, JSON.stringify(batches));
1731
+ } catch {
1732
+ }
1733
+ }
1734
+ function trimToBufferCap(batches) {
1735
+ let total = batches.reduce((sum, batch) => sum + batch.events.length, 0);
1736
+ const trimmed = batches.slice();
1737
+ while (total > MAX_BUFFERED_EVENTS2 && trimmed.length > 1) {
1738
+ const dropped = trimmed.shift();
1739
+ total -= dropped ? dropped.events.length : 0;
1740
+ }
1741
+ return trimmed;
1742
+ }
1743
+ var globalClient = null;
1744
+ var globalClientKey = null;
1745
+ function clientConfigKey(config) {
1746
+ return `${config.apiKey}@${config.endpoint}#${config.surface}`;
1747
+ }
1748
+ function getOrCreateTrackingClient(config) {
1749
+ const key = clientConfigKey(config);
1750
+ if (globalClient !== null && globalClientKey === key) {
1751
+ return globalClient;
1752
+ }
1753
+ if (globalClient !== null) {
1754
+ globalClient.destroy();
1755
+ }
1756
+ globalClient = createTrackingClient(config);
1757
+ globalClientKey = key;
1758
+ return globalClient;
1759
+ }
1760
+ var clientCaptureRegistry = /* @__PURE__ */ new WeakMap();
1761
+ function attachClientCapturesOnce(client, build) {
1762
+ let entry = clientCaptureRegistry.get(client);
1763
+ if (entry === void 0) {
1764
+ entry = { detach: build(), refCount: 0 };
1765
+ clientCaptureRegistry.set(client, entry);
1766
+ }
1767
+ entry.refCount += 1;
1768
+ let released = false;
1769
+ return () => {
1770
+ if (released) return;
1771
+ released = true;
1772
+ const current = clientCaptureRegistry.get(client);
1773
+ if (current === void 0) return;
1774
+ current.refCount -= 1;
1775
+ if (current.refCount <= 0) {
1776
+ current.detach();
1777
+ clientCaptureRegistry.delete(client);
1778
+ }
1779
+ };
1780
+ }
1781
+ function createTrackingClient(config) {
1782
+ const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
1783
+ const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);
1784
+ const sdkVersion = config.sdkVersion ?? null;
1785
+ const packageName = config.packageName ?? null;
1786
+ const environment = config.environment ?? "production";
1787
+ const activeGtagIds = config.activeGtagIds ?? null;
1788
+ const endpointBase = config.endpoint.replace(/\/$/, "");
1789
+ const eventsUrl = `${endpointBase}/events`;
1790
+ const identityHeaders = {
1791
+ sdkVersion: sdkVersion ?? "",
1792
+ packageName: packageName ?? "",
1793
+ surface: config.surface,
1794
+ environment
1795
+ };
1796
+ let queue = [];
1797
+ let flushTimer = null;
1798
+ const bufferKey = retryBufferKey(config.apiKey, endpointBase);
1799
+ let pending = readPendingBatches(bufferKey);
1800
+ let flushChain = Promise.resolve();
1801
+ let retryAttempt = 0;
1802
+ let firstPage = null;
1803
+ let initialParams = createEmptyTrackingParams();
1804
+ let destroyed = false;
1805
+ const visitorId = getVisitorId();
1806
+ const initialSession = getOrRotateSessionId();
1807
+ let sessionId = initialSession.id;
1808
+ if (typeof window !== "undefined") {
1809
+ firstPage = window.location.href;
1810
+ try {
1811
+ initialParams = captureTrackingParamsFromLocation();
1812
+ } catch {
1813
+ }
1814
+ getOrCaptureLandingParams(sessionId);
1815
+ try {
1816
+ captureFbc();
1817
+ } catch {
1818
+ }
1819
+ }
1820
+ function enqueueHeartbeat() {
1821
+ const metadata = buildHeartbeatMetadata(
1822
+ config.surface,
1823
+ sdkVersion,
1824
+ packageName,
1825
+ config.triggers ?? null,
1826
+ activeGtagIds
1827
+ );
1828
+ queue.push({
1829
+ event_type: "sdk_heartbeat",
1830
+ page_url: typeof window === "undefined" ? null : window.location.href,
1831
+ metadata,
1832
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString()
1833
+ });
1834
+ }
1835
+ if (initialSession.isNew) {
1836
+ enqueueHeartbeat();
1837
+ }
1838
+ if (pending.length > 0) {
1839
+ scheduleFlush();
1840
+ }
1841
+ function buildSessionPayload() {
1842
+ const rotated = getOrRotateSessionId();
1843
+ if (rotated.isNew && rotated.id !== sessionId) {
1844
+ enqueueHeartbeat();
1845
+ }
1846
+ sessionId = rotated.id;
1847
+ const params = mergeTrackingParams(readTrackingParams(), initialParams);
1848
+ const context = buildContext(
1849
+ config.surface,
1850
+ sdkVersion,
1851
+ packageName,
1852
+ environment,
1853
+ activeGtagIds
1854
+ );
1855
+ return {
1856
+ session_id: sessionId,
1857
+ visitor_id: visitorId,
1858
+ gclid: params.gclid,
1859
+ wbraid: params.wbraid,
1860
+ gbraid: params.gbraid,
1861
+ ylpcid: params.ylpcid,
1862
+ fbclid: params.fbclid,
1863
+ fbc: getFbcCookie(),
1864
+ fbp: getFbpCookie(),
1865
+ utm_source: params.utm_source,
1866
+ utm_medium: params.utm_medium,
1867
+ utm_campaign: params.utm_campaign,
1868
+ utm_term: params.utm_term,
1869
+ utm_content: params.utm_content,
1870
+ // Landing params for the CURRENT session id — captured on the spot when
1871
+ // the session just rotated (the current URL is the rotated session's
1872
+ // landing), reused from the stored record otherwise. Keys are omitted
1873
+ // entirely when the landing isn't observable (SSR).
1874
+ ...buildLandingPayloadFields(sessionId),
1875
+ first_page: firstPage,
1876
+ consent_state: consentSnapshot(),
1877
+ context
1878
+ };
1879
+ }
1880
+ function scheduleFlush(delayMs = flushIntervalMs) {
1881
+ if (flushTimer !== null || destroyed) return;
1882
+ flushTimer = setTimeout(() => {
1883
+ flushTimer = null;
1884
+ void flush();
1885
+ }, delayMs);
1886
+ }
1887
+ function clearScheduledFlush() {
1888
+ if (flushTimer !== null) {
1889
+ clearTimeout(flushTimer);
1890
+ flushTimer = null;
1891
+ }
1892
+ }
1893
+ function bufferQueued() {
1894
+ if (queue.length === 0) return;
1895
+ const events = queue.slice(0, HARD_MAX_BATCH);
1896
+ queue = queue.slice(events.length);
1897
+ pending = trimToBufferCap([...pending, { session: buildSessionPayload(), events }]);
1898
+ writePendingBatches(bufferKey, pending);
1899
+ }
1900
+ function flush() {
1901
+ flushChain = flushChain.then(runFlush).catch(() => {
1902
+ });
1903
+ return flushChain;
1904
+ }
1905
+ async function runFlush() {
1906
+ if (destroyed) return;
1907
+ clearScheduledFlush();
1908
+ bufferQueued();
1909
+ if (pending.length === 0) return;
1910
+ try {
1911
+ while (pending.length > 0) {
1912
+ const batch = pending[0];
1913
+ const body = { session: batch.session, events: batch.events };
1914
+ const outcome = await postWithFetch(
1915
+ eventsUrl,
1916
+ JSON.stringify(body),
1917
+ config.apiKey,
1918
+ identityHeaders,
1919
+ false
1920
+ );
1921
+ if (outcome === "retry") {
1922
+ retryAttempt += 1;
1923
+ return;
1924
+ }
1925
+ pending = pending.slice(1);
1926
+ writePendingBatches(bufferKey, pending);
1927
+ retryAttempt = 0;
1928
+ }
1929
+ } finally {
1930
+ if (pending.length > 0) {
1931
+ const backoff = Math.min(flushIntervalMs * 2 ** retryAttempt, MAX_RETRY_BACKOFF_MS);
1932
+ scheduleFlush(backoff);
1933
+ } else if (queue.length > 0) {
1934
+ scheduleFlush();
1935
+ }
1936
+ }
1937
+ }
1938
+ function trackEvent(input) {
1939
+ if (destroyed) return;
1940
+ if (!input || typeof input.eventType !== "string" || input.eventType.length === 0) return;
1941
+ if (input.eventType === "form_submit" && getConsentState() !== "denied") {
1942
+ try {
1943
+ const fields = input.metadata?.form?.fields;
1944
+ if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);
1945
+ } catch {
1946
+ }
1947
+ }
1948
+ const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : (/* @__PURE__ */ new Date()).toISOString();
1949
+ queue.push({
1950
+ event_type: input.eventType,
1951
+ page_url: input.pageUrl ?? (typeof window === "undefined" ? null : window.location.href),
1952
+ metadata: input.metadata ?? null,
1953
+ occurred_at: occurredAt
1954
+ });
1955
+ if (queue.length >= maxQueueSize) {
1956
+ void flush();
1957
+ } else {
1958
+ scheduleFlush();
1959
+ }
1960
+ }
1961
+ function flushOnUnload() {
1962
+ bufferQueued();
1963
+ clearScheduledFlush();
1964
+ if (pending.length === 0) return;
1965
+ const batch = pending[0];
1966
+ pending = pending.slice(1);
1967
+ writePendingBatches(bufferKey, pending);
1968
+ const body = { session: batch.session, events: batch.events };
1969
+ void postWithFetch(eventsUrl, JSON.stringify(body), config.apiKey, identityHeaders, true);
1970
+ }
1971
+ if (typeof window !== "undefined") {
1972
+ window.addEventListener("pagehide", flushOnUnload);
1973
+ window.addEventListener("visibilitychange", () => {
1974
+ if (document.visibilityState === "hidden") flushOnUnload();
1975
+ });
1976
+ }
1977
+ return {
1978
+ trackEvent,
1979
+ flush,
1980
+ flushBeacon: flushOnUnload,
1981
+ getSessionId: () => sessionId,
1982
+ getVisitorId: () => visitorId,
1983
+ destroy: () => {
1984
+ destroyed = true;
1985
+ if (queue.length > 0) {
1986
+ flushOnUnload();
1987
+ }
1988
+ clearScheduledFlush();
1989
+ queue = [];
1990
+ if (typeof window !== "undefined") {
1991
+ window.removeEventListener("pagehide", flushOnUnload);
1992
+ }
1993
+ }
1994
+ };
1995
+ }
1996
+
1997
+ // ../tracking-core/src/events/cta-click.ts
1998
+ var import_zod2 = require("zod");
1999
+ var ctaClickMetadataSchema = import_zod2.z.object({
2000
+ cta_name: import_zod2.z.string(),
2001
+ page: import_zod2.z.object({
2002
+ path: import_zod2.z.string()
2003
+ }).strict(),
2004
+ section: import_zod2.z.string().nullable().optional(),
2005
+ destination_url: import_zod2.z.string().nullable().optional(),
2006
+ // Set by auto-capture (and available to manual callers): the link target
2007
+ // and a short element descriptor (tag#id) for tying clicks to specific UI.
2008
+ href: import_zod2.z.string().nullable().optional(),
2009
+ element: import_zod2.z.string().nullable().optional()
2010
+ }).strict();
2011
+ var ctaClickConfigSchema = import_zod2.z.object({
2012
+ autoCapture: import_zod2.z.object({
2013
+ selector: import_zod2.z.string().optional()
2014
+ }).strict().optional()
2015
+ }).strict();
2016
+
2017
+ // ../tracking-core/src/events/sdk-heartbeat.ts
2018
+ var import_zod3 = require("zod");
2019
+ var sdkHeartbeatTriggersSchema = import_zod3.z.object({
2020
+ automatic: import_zod3.z.array(import_zod3.z.string()),
2021
+ manual: import_zod3.z.array(import_zod3.z.string())
2022
+ }).strict();
2023
+ var sdkHeartbeatMetadataSchema = import_zod3.z.object({
2024
+ sdk_version: import_zod3.z.string(),
2025
+ package_name: import_zod3.z.string().nullable(),
2026
+ surface: import_zod3.z.enum(["next", "react", "script"]),
2027
+ triggers: sdkHeartbeatTriggersSchema,
2028
+ trigger_config: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown())).nullable().optional(),
2029
+ configured_gtag_ids: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string()).nullable().optional()
2030
+ }).strict();
2031
+ var sdkHeartbeatConfigSchema = import_zod3.z.object({}).strict();
2032
+
2033
+ // ../tracking-core/src/events/form-start.ts
2034
+ var import_zod4 = require("zod");
2035
+ var formStartMetadataSchema = import_zod4.z.object({
2036
+ form: import_zod4.z.object({
2037
+ id: import_zod4.z.string(),
2038
+ action: import_zod4.z.string().nullable()
2039
+ }).strict(),
2040
+ page: import_zod4.z.object({
2041
+ path: import_zod4.z.string()
2042
+ }).strict()
2043
+ }).strict();
2044
+ var formStartConfigSchema = import_zod4.z.object({
2045
+ selector: import_zod4.z.string().optional()
2046
+ }).strict();
2047
+
2048
+ // ../tracking-core/src/events/form-submit.ts
2049
+ var import_zod5 = require("zod");
2050
+ var jsonValueSchema = import_zod5.z.lazy(
2051
+ () => import_zod5.z.union([
2052
+ import_zod5.z.string(),
2053
+ import_zod5.z.number().finite(),
2054
+ import_zod5.z.boolean(),
2055
+ import_zod5.z.null(),
2056
+ import_zod5.z.array(jsonValueSchema),
2057
+ import_zod5.z.record(jsonValueSchema)
2058
+ ])
2059
+ );
2060
+ var formSubmitMetadataSchema = import_zod5.z.object({
2061
+ form: import_zod5.z.object({
2062
+ id: import_zod5.z.string(),
2063
+ action: import_zod5.z.string().nullable(),
2064
+ fields: import_zod5.z.array(
2065
+ import_zod5.z.object({
2066
+ name: import_zod5.z.string(),
2067
+ type: import_zod5.z.string(),
2068
+ label: import_zod5.z.string().nullable(),
2069
+ value: jsonValueSchema
2070
+ }).strict()
2071
+ ).optional()
2072
+ }).strict(),
2073
+ page: import_zod5.z.object({
2074
+ path: import_zod5.z.string()
2075
+ }).strict()
2076
+ }).strict();
2077
+ var formSubmitConfigSchema = import_zod5.z.object({}).strict();
2078
+
2079
+ // ../tracking-core/src/events/multi-page-session.ts
2080
+ var import_zod6 = require("zod");
2081
+ var multiPageSessionMetadataSchema = import_zod6.z.object({
2082
+ page_count: import_zod6.z.number().int().min(2),
2083
+ page: import_zod6.z.object({
2084
+ path: import_zod6.z.string()
2085
+ }).strict()
2086
+ }).strict();
2087
+ var multiPageSessionConfigSchema = import_zod6.z.object({
2088
+ pageThreshold: import_zod6.z.number().int().min(2)
2089
+ }).strict();
2090
+
2091
+ // ../tracking-core/src/events/page-exit.ts
2092
+ var import_zod7 = require("zod");
2093
+ var pageExitMetadataSchema = import_zod7.z.object({
2094
+ dwell_ms: import_zod7.z.number().int().min(0),
2095
+ // null = left without any scroll signal; floor is 0 so a valid 0% is never
2096
+ // rejected (a single bad field 422s the whole keepalive beacon batch).
2097
+ max_scroll_percent: import_zod7.z.number().int().min(0).max(100).nullable(),
2098
+ // The gating baseline: the fraction of the page visible at load with
2099
+ // zero scrolling — or, for pages that grew after the post-paint snapshot
2100
+ // (skeleton/streaming renders), the first-scroll position that
2101
+ // established it. null = page had no scrollable range, or the segment
2102
+ // ended before the snapshot. `.optional()` is load-bearing: SDK builds
2103
+ // predating this field keep POSTing page_exit without the key —
2104
+ // requiring it would 422 whole keepalive beacon batches.
2105
+ scroll_baseline_percent: import_zod7.z.number().int().min(0).max(100).nullable().optional(),
2106
+ page: import_zod7.z.object({
2107
+ path: import_zod7.z.string()
2108
+ }).strict()
2109
+ }).strict();
2110
+ var pageExitConfigSchema = import_zod7.z.object({}).strict();
2111
+
2112
+ // ../tracking-core/src/events/phone-click.ts
2113
+ var import_zod8 = require("zod");
2114
+ var phoneClickMetadataSchema = import_zod8.z.object({
2115
+ phone_number: import_zod8.z.string(),
2116
+ page: import_zod8.z.object({
2117
+ path: import_zod8.z.string()
2118
+ }).strict(),
2119
+ section: import_zod8.z.string().nullable().optional()
2120
+ }).strict();
2121
+ var phoneClickConfigSchema = import_zod8.z.object({
2122
+ autoCapture: import_zod8.z.object({
2123
+ selector: import_zod8.z.string().optional()
2124
+ }).strict().optional()
2125
+ }).strict();
2126
+
2127
+ // ../tracking-core/src/events/scroll-depth.ts
2128
+ var import_zod9 = require("zod");
2129
+ var scrollDepthMetadataSchema = import_zod9.z.object({
2130
+ depth_percent: import_zod9.z.number().int().min(1).max(100),
2131
+ page: import_zod9.z.object({
2132
+ path: import_zod9.z.string()
2133
+ }).strict()
2134
+ }).strict();
2135
+ var scrollDepthConfigSchema = import_zod9.z.object({
2136
+ thresholds: import_zod9.z.array(import_zod9.z.number().int().min(1).max(100)).min(1)
2137
+ }).strict();
2138
+
2139
+ // ../tracking-core/src/events/specific-page-visit.ts
2140
+ var import_zod10 = require("zod");
2141
+ var PAGE_IDENTITY_MAX_LENGTH = 64;
2142
+ var PAGE_IDENTITY_PATTERN = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
2143
+ var pageIdentitySchema = import_zod10.z.string().max(PAGE_IDENTITY_MAX_LENGTH).regex(PAGE_IDENTITY_PATTERN, {
2144
+ message: "page identity must be lowercase snake_case, start with a letter, and contain at most 64 characters"
2145
+ });
2146
+ var specificPageNameSchema = pageIdentitySchema;
2147
+ var specificPageVisitMetadataSchema = import_zod10.z.object({
2148
+ page_name: specificPageNameSchema,
2149
+ page: import_zod10.z.object({
2150
+ path: import_zod10.z.string()
2151
+ }).strict()
2152
+ }).strict();
2153
+ var specificPageVisitConfigSchema = import_zod10.z.object({
2154
+ pages: import_zod10.z.array(
2155
+ import_zod10.z.object({
2156
+ name: specificPageNameSchema,
2157
+ pathPattern: import_zod10.z.custom((value) => value instanceof RegExp, {
2158
+ message: "pathPattern must be a RegExp"
2159
+ })
2160
+ }).strict()
2161
+ ).min(1)
2162
+ }).strict();
2163
+
2164
+ // ../tracking-core/src/events/time-on-site.ts
2165
+ var import_zod11 = require("zod");
2166
+ var timeOnSiteMetadataSchema = import_zod11.z.object({
2167
+ duration_ms: import_zod11.z.number().int().nonnegative(),
2168
+ page: import_zod11.z.object({
2169
+ path: import_zod11.z.string()
2170
+ }).strict()
2171
+ }).strict();
2172
+ var timeOnSiteConfigSchema = import_zod11.z.object({
2173
+ thresholdSeconds: import_zod11.z.number().int().positive()
2174
+ }).strict();
2175
+
2176
+ // ../tracking-core/src/events/semantics.ts
2177
+ var EVENT_SEMANTICS = {
2178
+ page_view: {
2179
+ label: "Page view",
2180
+ category: "page",
2181
+ outcomeRole: "navigation",
2182
+ clientVisibility: "simple"
2183
+ },
2184
+ time_on_site: {
2185
+ label: "Time on site",
2186
+ category: "engagement",
2187
+ outcomeRole: "engagement",
2188
+ clientVisibility: "detailed"
2189
+ },
2190
+ specific_page_visit: {
2191
+ label: "Key page visit",
2192
+ category: "engagement",
2193
+ outcomeRole: "engagement",
2194
+ clientVisibility: "detailed"
2195
+ },
2196
+ scroll_depth: {
2197
+ label: "Scroll depth",
2198
+ category: "engagement",
2199
+ outcomeRole: "engagement",
2200
+ clientVisibility: "detailed"
2201
+ },
2202
+ multi_page_session: {
2203
+ label: "Multi-page session",
2204
+ category: "engagement",
2205
+ outcomeRole: "engagement",
2206
+ clientVisibility: "detailed"
2207
+ },
2208
+ form_start: {
2209
+ label: "Form started",
2210
+ category: "engagement",
2211
+ outcomeRole: "engagement",
2212
+ clientVisibility: "simple"
2213
+ },
2214
+ sdk_heartbeat: {
2215
+ label: "SDK heartbeat",
2216
+ category: "system",
2217
+ outcomeRole: "diagnostic",
2218
+ clientVisibility: "hidden"
2219
+ },
2220
+ page_exit: {
2221
+ label: "Page exit",
2222
+ category: "engagement",
2223
+ outcomeRole: "diagnostic",
2224
+ clientVisibility: "detailed"
2225
+ },
2226
+ form_submit: {
2227
+ label: "Form submitted",
2228
+ category: "lead",
2229
+ outcomeRole: "lead",
2230
+ clientVisibility: "simple"
2231
+ },
2232
+ phone_click: {
2233
+ label: "Phone click",
2234
+ category: "lead",
2235
+ outcomeRole: "lead",
2236
+ clientVisibility: "simple"
2237
+ },
2238
+ cta_click: {
2239
+ label: "CTA click",
2240
+ category: "engagement",
2241
+ outcomeRole: "engagement",
2242
+ clientVisibility: "simple"
2243
+ }
2244
+ };
2245
+
2246
+ // ../tracking-core/src/events/registry.ts
2247
+ var EVENT_REGISTRY = {
2248
+ // --- automatic triggers ---
2249
+ page_view: {
2250
+ kind: "automatic",
2251
+ semantics: EVENT_SEMANTICS.page_view,
2252
+ metadataSchema: pageViewMetadataSchema,
2253
+ configSchema: pageViewConfigSchema
2254
+ },
2255
+ time_on_site: {
2256
+ kind: "automatic",
2257
+ semantics: EVENT_SEMANTICS.time_on_site,
2258
+ metadataSchema: timeOnSiteMetadataSchema,
2259
+ configSchema: timeOnSiteConfigSchema
2260
+ },
2261
+ specific_page_visit: {
2262
+ kind: "automatic",
2263
+ semantics: EVENT_SEMANTICS.specific_page_visit,
2264
+ metadataSchema: specificPageVisitMetadataSchema,
2265
+ configSchema: specificPageVisitConfigSchema
2266
+ },
2267
+ scroll_depth: {
2268
+ kind: "automatic",
2269
+ semantics: EVENT_SEMANTICS.scroll_depth,
2270
+ metadataSchema: scrollDepthMetadataSchema,
2271
+ configSchema: scrollDepthConfigSchema
2272
+ },
2273
+ multi_page_session: {
2274
+ kind: "automatic",
2275
+ semantics: EVENT_SEMANTICS.multi_page_session,
2276
+ metadataSchema: multiPageSessionMetadataSchema,
2277
+ configSchema: multiPageSessionConfigSchema
2278
+ },
2279
+ form_start: {
2280
+ kind: "automatic",
2281
+ semantics: EVENT_SEMANTICS.form_start,
2282
+ metadataSchema: formStartMetadataSchema,
2283
+ configSchema: formStartConfigSchema
2284
+ },
2285
+ // --- SDK-internal automatic (not consumer-configurable) ---
2286
+ sdk_heartbeat: {
2287
+ kind: "automatic",
2288
+ semantics: EVENT_SEMANTICS.sdk_heartbeat,
2289
+ metadataSchema: sdkHeartbeatMetadataSchema,
2290
+ configSchema: sdkHeartbeatConfigSchema
2291
+ },
2292
+ page_exit: {
2293
+ kind: "automatic",
2294
+ semantics: EVENT_SEMANTICS.page_exit,
2295
+ metadataSchema: pageExitMetadataSchema,
2296
+ configSchema: pageExitConfigSchema
2297
+ },
2298
+ // --- manual triggers ---
2299
+ form_submit: {
2300
+ kind: "manual",
2301
+ semantics: EVENT_SEMANTICS.form_submit,
2302
+ metadataSchema: formSubmitMetadataSchema,
2303
+ configSchema: formSubmitConfigSchema
2304
+ },
2305
+ phone_click: {
2306
+ kind: "manual",
2307
+ semantics: EVENT_SEMANTICS.phone_click,
2308
+ metadataSchema: phoneClickMetadataSchema,
2309
+ configSchema: phoneClickConfigSchema
2310
+ },
2311
+ cta_click: {
2312
+ kind: "manual",
2313
+ semantics: EVENT_SEMANTICS.cta_click,
2314
+ metadataSchema: ctaClickMetadataSchema,
2315
+ configSchema: ctaClickConfigSchema
2316
+ }
2317
+ };
2318
+ var ALL_AUTOMATIC_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "automatic").map(([name]) => name);
2319
+ var ALL_MANUAL_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "manual").map(([name]) => name);
2320
+ var ALL_LEAD_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.semantics.outcomeRole === "lead").map(([name]) => name);
2321
+ function getEventDefinition(name) {
2322
+ return EVENT_REGISTRY[name];
2323
+ }
2324
+
2325
+ // ../tracking-core/src/ingest-typed.ts
2326
+ function createTypedClient(raw, registry, options = {}) {
2327
+ const debug = options.debug ?? false;
2328
+ if (registry.manual?.form_submit) registerCapability("form_capture");
2329
+ return {
2330
+ trackEvent(eventType, metadata, opts) {
2331
+ if (debug) {
2332
+ const def = getEventDefinition(eventType);
2333
+ def.metadataSchema.parse(metadata);
2334
+ }
2335
+ raw.trackEvent({
2336
+ eventType,
2337
+ metadata,
2338
+ pageUrl: opts?.pageUrl ?? null,
2339
+ occurredAt: opts?.occurredAt ?? null
2340
+ });
2341
+ },
2342
+ flush: raw.flush.bind(raw),
2343
+ getSessionId: raw.getSessionId.bind(raw),
2344
+ getVisitorId: raw.getVisitorId.bind(raw)
2345
+ };
2346
+ }
2347
+
2348
+ // ../tracking-core/src/triggers/time-on-site.ts
2349
+ function attachTimeOnSite(client, config) {
2350
+ if (typeof window === "undefined" || typeof document === "undefined") {
2351
+ return () => {
2352
+ };
2353
+ }
2354
+ const thresholdMs = config.thresholdSeconds * 1e3;
2355
+ let accumulatedMs = 0;
2356
+ let activeSince = document.visibilityState === "visible" ? Date.now() : null;
2357
+ let timer = null;
2358
+ let fired = false;
2359
+ function fire() {
2360
+ if (fired) return;
2361
+ fired = true;
2362
+ client.trackEvent({
2363
+ eventType: "time_on_site",
2364
+ metadata: {
2365
+ duration_ms: thresholdMs,
2366
+ page: { path: window.location.pathname }
2367
+ },
2368
+ pageUrl: window.location.href,
2369
+ occurredAt: null
2370
+ });
2371
+ }
2372
+ function scheduleNext() {
2373
+ if (fired || activeSince === null) return;
2374
+ const remaining = thresholdMs - accumulatedMs;
2375
+ if (remaining <= 0) {
2376
+ fire();
2377
+ return;
2378
+ }
2379
+ timer = setTimeout(fire, remaining);
2380
+ }
2381
+ function clearTimer() {
2382
+ if (timer !== null) {
2383
+ clearTimeout(timer);
2384
+ timer = null;
2385
+ }
2386
+ }
2387
+ function onVisibilityChange() {
2388
+ if (fired) return;
2389
+ if (document.visibilityState === "hidden") {
2390
+ if (activeSince !== null) {
2391
+ accumulatedMs += Date.now() - activeSince;
2392
+ activeSince = null;
2393
+ }
2394
+ clearTimer();
2395
+ } else {
2396
+ activeSince = Date.now();
2397
+ scheduleNext();
2398
+ }
2399
+ }
2400
+ document.addEventListener("visibilitychange", onVisibilityChange);
2401
+ scheduleNext();
2402
+ return () => {
2403
+ clearTimer();
2404
+ document.removeEventListener("visibilitychange", onVisibilityChange);
2405
+ };
2406
+ }
2407
+
2408
+ // ../tracking-core/src/triggers/navigation.ts
2409
+ var listeners = /* @__PURE__ */ new Set();
2410
+ var restorePatch = null;
2411
+ function notify() {
2412
+ for (const listener of listeners) listener();
2413
+ }
2414
+ function notifyDeferred() {
2415
+ setTimeout(notify, 0);
2416
+ }
2417
+ function installPatch() {
2418
+ const originalPushState = history.pushState;
2419
+ const originalReplaceState = history.replaceState;
2420
+ function patchedPushState(...args) {
2421
+ originalPushState.apply(this, args);
2422
+ notifyDeferred();
2423
+ }
2424
+ function patchedReplaceState(...args) {
2425
+ originalReplaceState.apply(this, args);
2426
+ notifyDeferred();
2427
+ }
2428
+ history.pushState = patchedPushState;
2429
+ history.replaceState = patchedReplaceState;
2430
+ window.addEventListener("popstate", notify);
2431
+ restorePatch = () => {
2432
+ history.pushState = originalPushState;
2433
+ history.replaceState = originalReplaceState;
2434
+ window.removeEventListener("popstate", notify);
2435
+ restorePatch = null;
2436
+ };
2437
+ }
2438
+ function onHistoryChange(listener) {
2439
+ if (listeners.size === 0) installPatch();
2440
+ listeners.add(listener);
2441
+ return () => {
2442
+ if (!listeners.delete(listener)) return;
2443
+ if (listeners.size === 0) restorePatch?.();
2444
+ };
2445
+ }
2446
+
2447
+ // ../tracking-core/src/triggers/scroll-measurement.ts
2448
+ var BOTTOM_EPSILON_PX = 2;
2449
+ function measureScrollPercent() {
2450
+ const root = document.scrollingElement ?? document.documentElement;
2451
+ const scrollHeight = root.scrollHeight;
2452
+ const clientHeight = root.clientHeight;
2453
+ if (scrollHeight <= 0 || clientHeight <= 0) return null;
2454
+ if (scrollHeight <= clientHeight + BOTTOM_EPSILON_PX) return null;
2455
+ const maxTop = scrollHeight - clientHeight;
2456
+ const scrollTop = Math.min(Math.max(root.scrollTop, 0), maxTop);
2457
+ if (scrollTop + clientHeight >= scrollHeight - BOTTOM_EPSILON_PX) return 100;
2458
+ return Math.max(1, Math.min(100, Math.round((scrollTop + clientHeight) / scrollHeight * 100)));
2459
+ }
2460
+ function scheduleBaselineSnapshot(onSnapshot) {
2461
+ let rafId = requestAnimationFrame(() => {
2462
+ rafId = requestAnimationFrame(() => {
2463
+ onSnapshot(measureScrollPercent());
2464
+ });
2465
+ });
2466
+ return () => cancelAnimationFrame(rafId);
2467
+ }
2468
+ function createBaselineGate() {
2469
+ let baselinePercent = null;
2470
+ let ready = false;
2471
+ let cancelSnapshot = null;
2472
+ return {
2473
+ rebaseline() {
2474
+ cancelSnapshot?.();
2475
+ ready = false;
2476
+ baselinePercent = null;
2477
+ cancelSnapshot = scheduleBaselineSnapshot((b) => {
2478
+ baselinePercent = b;
2479
+ ready = true;
2480
+ });
2481
+ },
2482
+ cancel() {
2483
+ cancelSnapshot?.();
2484
+ },
2485
+ baseline() {
2486
+ return ready ? baselinePercent : null;
2487
+ },
2488
+ sample() {
2489
+ if (!ready) return null;
2490
+ if (baselinePercent === null) {
2491
+ baselinePercent = measureScrollPercent();
2492
+ return null;
2493
+ }
2494
+ return measureScrollPercent();
2495
+ }
2496
+ };
2497
+ }
2498
+
2499
+ // ../tracking-core/src/triggers/scroll-depth.ts
2500
+ function attachScrollDepth(client, config) {
2501
+ if (typeof window === "undefined" || typeof document === "undefined") {
2502
+ return () => {
2503
+ };
2504
+ }
2505
+ const thresholds = new Set(config.thresholds);
2506
+ let firedForPath = /* @__PURE__ */ new Set();
2507
+ let currentPath2 = window.location.pathname;
2508
+ let rafId = null;
2509
+ const gate = createBaselineGate();
2510
+ function checkThresholds() {
2511
+ const percent = gate.sample();
2512
+ if (percent === null) return;
2513
+ const baseline = gate.baseline();
2514
+ if (baseline === null) return;
2515
+ for (const threshold of thresholds) {
2516
+ if (threshold <= baseline) continue;
2517
+ if (percent >= threshold && !firedForPath.has(threshold)) {
2518
+ firedForPath.add(threshold);
2519
+ client.trackEvent({
2520
+ eventType: "scroll_depth",
2521
+ metadata: {
2522
+ depth_percent: threshold,
2523
+ page: { path: currentPath2 }
2524
+ },
2525
+ pageUrl: window.location.href,
2526
+ occurredAt: null
2527
+ });
2528
+ }
2529
+ }
2530
+ }
2531
+ function onScroll() {
2532
+ if (rafId !== null) return;
2533
+ rafId = requestAnimationFrame(() => {
2534
+ rafId = null;
2535
+ checkThresholds();
2536
+ });
2537
+ }
2538
+ function resetIfPathChanged() {
2539
+ const newPath = window.location.pathname;
2540
+ if (newPath === currentPath2) return;
2541
+ currentPath2 = newPath;
2542
+ firedForPath = /* @__PURE__ */ new Set();
2543
+ gate.rebaseline();
2544
+ }
2545
+ const unsubscribeNav = onHistoryChange(resetIfPathChanged);
2546
+ window.addEventListener("scroll", onScroll, { passive: true });
2547
+ gate.rebaseline();
2548
+ return () => {
2549
+ if (rafId !== null) cancelAnimationFrame(rafId);
2550
+ gate.cancel();
2551
+ unsubscribeNav();
2552
+ window.removeEventListener("scroll", onScroll);
2553
+ };
2554
+ }
2555
+
2556
+ // ../tracking-core/src/triggers/form-start.ts
2557
+ function attachFormStart(client, config) {
2558
+ if (typeof window === "undefined" || typeof document === "undefined") {
2559
+ return () => {
2560
+ };
2561
+ }
2562
+ const selector = config.selector ?? "form";
2563
+ let firedForms = /* @__PURE__ */ new Set();
2564
+ let currentPath2 = window.location.pathname;
2565
+ function getFormKey(form) {
2566
+ if (form.id) return `id:${form.id}`;
2567
+ const explicitAction = form.getAttribute("action");
2568
+ if (explicitAction) return `action:${explicitAction}`;
2569
+ const forms = Array.from(document.querySelectorAll(selector));
2570
+ return `index:${forms.indexOf(form)}`;
2571
+ }
2572
+ function onFocusIn(event) {
2573
+ const target = event.target;
2574
+ if (!(target instanceof HTMLElement)) return;
2575
+ const form = target.closest(selector);
2576
+ if (!form || form.tagName !== "FORM") return;
2577
+ const key = getFormKey(form);
2578
+ if (firedForms.has(key)) return;
2579
+ firedForms.add(key);
2580
+ client.trackEvent({
2581
+ eventType: "form_start",
2582
+ metadata: {
2583
+ form: {
2584
+ id: form.id || "",
2585
+ action: form.getAttribute("action") ?? null
2586
+ },
2587
+ page: { path: window.location.pathname }
2588
+ },
2589
+ pageUrl: window.location.href,
2590
+ occurredAt: null
2591
+ });
2592
+ }
2593
+ function resetIfPathChanged() {
2594
+ const newPath = window.location.pathname;
2595
+ if (newPath === currentPath2) return;
2596
+ currentPath2 = newPath;
2597
+ firedForms = /* @__PURE__ */ new Set();
2598
+ }
2599
+ const originalPushState = history.pushState.bind(history);
2600
+ const originalReplaceState = history.replaceState.bind(history);
2601
+ function patchedPushState(...args) {
2602
+ originalPushState(...args);
2603
+ setTimeout(resetIfPathChanged, 0);
2604
+ }
2605
+ function patchedReplaceState(...args) {
2606
+ originalReplaceState(...args);
2607
+ setTimeout(resetIfPathChanged, 0);
2608
+ }
2609
+ history.pushState = patchedPushState;
2610
+ history.replaceState = patchedReplaceState;
2611
+ window.addEventListener("popstate", resetIfPathChanged);
2612
+ document.addEventListener("focusin", onFocusIn);
2613
+ return () => {
2614
+ history.pushState = originalPushState;
2615
+ history.replaceState = originalReplaceState;
2616
+ window.removeEventListener("popstate", resetIfPathChanged);
2617
+ document.removeEventListener("focusin", onFocusIn);
2618
+ };
2619
+ }
2620
+
2621
+ // ../tracking-core/src/triggers/page-exit.ts
2622
+ var MIN_SEGMENT_MS = 50;
2623
+ function attachPageExit(client) {
2624
+ if (typeof window === "undefined" || typeof document === "undefined") {
2625
+ return () => {
2626
+ };
2627
+ }
2628
+ let currentPath2 = window.location.pathname;
2629
+ let activeSince = document.visibilityState === "visible" ? Date.now() : null;
2630
+ let accumulatedMs = 0;
2631
+ let maxScrollPercent = null;
2632
+ let rafId = null;
2633
+ const gate = createBaselineGate();
2634
+ function onScroll() {
2635
+ if (rafId !== null) return;
2636
+ rafId = requestAnimationFrame(() => {
2637
+ rafId = null;
2638
+ const percent = gate.sample();
2639
+ if (percent !== null && (maxScrollPercent === null || percent > maxScrollPercent)) {
2640
+ maxScrollPercent = percent;
2641
+ }
2642
+ });
2643
+ }
2644
+ function settledDwellMs() {
2645
+ let total = accumulatedMs;
2646
+ if (activeSince !== null) {
2647
+ total += Date.now() - activeSince;
2648
+ }
2649
+ return Math.max(0, Math.round(total));
2650
+ }
2651
+ function emitSegment(path, flush) {
2652
+ const dwell = settledDwellMs();
2653
+ if (dwell < MIN_SEGMENT_MS) return;
2654
+ client.trackEvent({
2655
+ eventType: "page_exit",
2656
+ metadata: {
2657
+ dwell_ms: dwell,
2658
+ max_scroll_percent: maxScrollPercent,
2659
+ // Lets the backend tell "scrolled to the bottom" apart from "the page
2660
+ // was barely scrollable". null = unscrollable page or the segment
2661
+ // ended before the post-paint snapshot landed. For pages that grew
2662
+ // after the snapshot this is the first-scroll position, not the
2663
+ // at-load fraction (see BaselineGate.baseline).
2664
+ scroll_baseline_percent: gate.baseline(),
2665
+ page: { path }
2666
+ },
2667
+ pageUrl: window.location.href,
2668
+ occurredAt: null
2669
+ });
2670
+ if (flush) {
2671
+ client.flushBeacon();
2672
+ }
2673
+ accumulatedMs = 0;
2674
+ activeSince = null;
2675
+ }
2676
+ function onNavigate() {
2677
+ const newPath = window.location.pathname;
2678
+ if (newPath === currentPath2) return;
2679
+ emitSegment(currentPath2, false);
2680
+ currentPath2 = newPath;
2681
+ maxScrollPercent = null;
2682
+ gate.rebaseline();
2683
+ accumulatedMs = 0;
2684
+ activeSince = document.visibilityState === "visible" ? Date.now() : null;
2685
+ }
2686
+ function onVisibilityChange() {
2687
+ if (document.visibilityState === "hidden") {
2688
+ emitSegment(currentPath2, true);
2689
+ } else {
2690
+ activeSince = Date.now();
2691
+ }
2692
+ }
2693
+ function onPageHide() {
2694
+ emitSegment(currentPath2, true);
2695
+ }
2696
+ const unsubscribeNav = onHistoryChange(onNavigate);
2697
+ window.addEventListener("scroll", onScroll, { passive: true });
2698
+ document.addEventListener("visibilitychange", onVisibilityChange);
2699
+ window.addEventListener("pagehide", onPageHide);
2700
+ gate.rebaseline();
2701
+ return () => {
2702
+ if (rafId !== null) cancelAnimationFrame(rafId);
2703
+ gate.cancel();
2704
+ unsubscribeNav();
2705
+ window.removeEventListener("scroll", onScroll);
2706
+ document.removeEventListener("visibilitychange", onVisibilityChange);
2707
+ window.removeEventListener("pagehide", onPageHide);
2708
+ };
2709
+ }
2710
+
2711
+ // ../tracking-core/src/triggers/cta-click-capture.ts
2712
+ var DEFAULT_CTA_SELECTOR = "[data-aranova-cta]";
2713
+ var CTA_NAME_MAX_LENGTH = 120;
2714
+ function describeElement(el) {
2715
+ const tag = el.tagName.toLowerCase();
2716
+ return el.id ? `${tag}#${el.id}` : tag;
2717
+ }
2718
+ function resolveCtaName(el) {
2719
+ const explicit = el.getAttribute("data-aranova-cta");
2720
+ if (explicit && explicit.trim().length > 0) return explicit.trim();
2721
+ const text = (el.textContent ?? "").trim().replaceAll(/\s+/g, " ");
2722
+ if (text.length > 0) return text.slice(0, CTA_NAME_MAX_LENGTH);
2723
+ return describeElement(el);
2724
+ }
2725
+ function attachCtaClickCapture(client, config) {
2726
+ if (typeof window === "undefined" || typeof document === "undefined") {
2727
+ return () => {
2728
+ };
2729
+ }
2730
+ const autoCapture = config.autoCapture;
2731
+ if (!autoCapture) {
2732
+ return () => {
2733
+ };
2734
+ }
2735
+ registerCapability("cta_click_capture");
2736
+ const selector = autoCapture.selector ?? DEFAULT_CTA_SELECTOR;
2737
+ function onClick(event) {
2738
+ const target = event.target;
2739
+ if (!(target instanceof Element)) return;
2740
+ let matched = null;
2741
+ try {
2742
+ matched = target.closest(selector);
2743
+ } catch {
2744
+ return;
2745
+ }
2746
+ if (matched === null) return;
2747
+ const href = matched instanceof HTMLAnchorElement ? matched.href || null : matched.getAttribute("href");
2748
+ client.trackEvent({
2749
+ eventType: "cta_click",
2750
+ metadata: {
2751
+ cta_name: resolveCtaName(matched),
2752
+ page: { path: window.location.pathname },
2753
+ section: matched.getAttribute("data-aranova-section"),
2754
+ destination_url: href,
2755
+ href,
2756
+ element: describeElement(matched)
2757
+ },
2758
+ pageUrl: window.location.href,
2759
+ occurredAt: null
2760
+ });
2761
+ }
2762
+ document.addEventListener("click", onClick, true);
2763
+ return () => {
2764
+ document.removeEventListener("click", onClick, true);
2765
+ };
2766
+ }
2767
+
2768
+ // ../tracking-core/src/triggers/phone-click-capture.ts
2769
+ var DEFAULT_TEL_SELECTOR = 'a[href^="tel:"]';
2770
+ function safeDecodeURIComponent(value) {
2771
+ try {
2772
+ return decodeURIComponent(value);
2773
+ } catch {
2774
+ return value;
2775
+ }
2776
+ }
2777
+ function resolvePhoneNumber(el) {
2778
+ const href = el instanceof HTMLAnchorElement ? el.href : el.getAttribute("href") ?? "";
2779
+ const raw = safeDecodeURIComponent(href.replace(/^tel:/i, "").split(";")[0]).trim();
2780
+ return toE164(raw) ?? raw;
2781
+ }
2782
+ function attachPhoneClickCapture(client, config) {
2783
+ if (typeof window === "undefined" || typeof document === "undefined") {
2784
+ return () => {
2785
+ };
2786
+ }
2787
+ const autoCapture = config.autoCapture;
2788
+ if (!autoCapture) {
2789
+ return () => {
2790
+ };
2791
+ }
2792
+ registerCapability("phone_click_capture");
2793
+ const selector = autoCapture.selector ?? DEFAULT_TEL_SELECTOR;
2794
+ function onClick(event) {
2795
+ const target = event.target;
2796
+ if (!(target instanceof Element)) return;
2797
+ let matched = null;
2798
+ try {
2799
+ matched = target.closest(selector);
2800
+ } catch {
2801
+ return;
2802
+ }
2803
+ if (matched === null) return;
2804
+ const metadata = {
2805
+ phone_number: resolvePhoneNumber(matched),
2806
+ page: { path: window.location.pathname },
2807
+ section: matched.getAttribute("data-aranova-section")
2808
+ };
2809
+ client.trackEvent({
2810
+ eventType: "phone_click",
2811
+ metadata,
2812
+ pageUrl: window.location.href,
2813
+ occurredAt: null
2814
+ });
2815
+ }
2816
+ document.addEventListener("click", onClick, true);
2817
+ return () => {
2818
+ document.removeEventListener("click", onClick, true);
2819
+ };
2820
+ }
2821
+
2822
+ // ../tracking-core/src/resources/sales/schema.ts
2823
+ var import_zod12 = require("zod");
2824
+ var SUPPORTED_CURRENCIES = ["USD", "CAD"];
2825
+ var TRACKING_ENVIRONMENTS = ["production", "development"];
2826
+ var currencySchema = import_zod12.z.enum(SUPPORTED_CURRENCIES);
2827
+ var centsSchema = import_zod12.z.number().int().nonnegative();
2828
+ var quantitySchema = import_zod12.z.string().regex(/^\d+(\.\d{1,3})?$/);
2829
+ var metadataSchema = import_zod12.z.record(import_zod12.z.unknown());
2830
+ var saleItemSchema = import_zod12.z.object({
2831
+ external_item_id: import_zod12.z.string().nullable().optional(),
2832
+ name: import_zod12.z.string().nullable().optional(),
2833
+ category: import_zod12.z.string().nullable().optional(),
2834
+ quantity: quantitySchema,
2835
+ unit_price_cents: centsSchema,
2836
+ // Non-negativity validated on the wire — same contract as the other cents
2837
+ // fields — and backstopped by the DB CHECK.
2838
+ unit_cost_cents: centsSchema.nullable().optional()
2839
+ }).strict();
2840
+ var saleServiceSchema = import_zod12.z.object({
2841
+ service: import_zod12.z.string(),
2842
+ amount_cents: centsSchema
2843
+ }).strict();
2844
+ var customerNameSchema = import_zod12.z.string().max(200);
2845
+ var customerPhoneSchema = import_zod12.z.string().max(64);
2846
+ var customerEmailSchema = import_zod12.z.string().max(320).email();
2847
+ function refineServiceXor(val, ctx, { requireAmount }) {
2848
+ if (val.services != null) {
2849
+ if (val.service != null) {
2850
+ ctx.addIssue({
2851
+ code: import_zod12.z.ZodIssueCode.custom,
2852
+ message: "pass either `service` or `services`, not both",
2853
+ path: ["services"]
2854
+ });
2855
+ }
2856
+ if (val.services.length === 0) {
2857
+ ctx.addIssue({
2858
+ code: import_zod12.z.ZodIssueCode.custom,
2859
+ message: "`services` must not be empty",
2860
+ path: ["services"]
2861
+ });
2862
+ }
2863
+ const keys = val.services.map((s) => s.service);
2864
+ if (new Set(keys).size !== keys.length) {
2865
+ ctx.addIssue({
2866
+ code: import_zod12.z.ZodIssueCode.custom,
2867
+ message: "`services` must not list the same service more than once",
2868
+ path: ["services"]
2869
+ });
2870
+ }
2871
+ if (val.amount_total_cents != null) {
2872
+ const sum = val.services.reduce((acc, s) => acc + s.amount_cents, 0);
2873
+ if (val.amount_total_cents !== sum) {
2874
+ ctx.addIssue({
2875
+ code: import_zod12.z.ZodIssueCode.custom,
2876
+ message: "amount_total_cents must equal the sum of the services amounts (omit it to derive it automatically)",
2877
+ path: ["amount_total_cents"]
2878
+ });
2879
+ }
2880
+ }
2881
+ } else if (requireAmount && val.amount_total_cents == null) {
2882
+ ctx.addIssue({
2883
+ code: import_zod12.z.ZodIssueCode.custom,
2884
+ message: "amount_total_cents is required unless `services` is provided",
2885
+ path: ["amount_total_cents"]
2886
+ });
2887
+ }
2888
+ }
2889
+ var saleCreateSchema = import_zod12.z.object({
2890
+ external_id: import_zod12.z.string().nullable().optional(),
2891
+ description: import_zod12.z.string().nullable().optional(),
2892
+ service: import_zod12.z.string().nullable().optional(),
2893
+ services: import_zod12.z.array(saleServiceSchema).nullable().optional(),
2894
+ currency: currencySchema,
2895
+ // Optional only because the plural `services` form derives it from the sum
2896
+ // (see refineServiceXor); the singular/serviceless path still requires it.
2897
+ amount_total_cents: centsSchema.nullable().optional(),
2898
+ occurred_at: import_zod12.z.string().datetime(),
2899
+ environment: import_zod12.z.enum(TRACKING_ENVIRONMENTS).default("production"),
2900
+ items: import_zod12.z.array(saleItemSchema).default([]),
2901
+ metadata: metadataSchema.nullable().optional(),
2902
+ customer_name: customerNameSchema.nullable().optional(),
2903
+ customer_phone: customerPhoneSchema.nullable().optional(),
2904
+ customer_email: customerEmailSchema.nullable().optional(),
2905
+ // CASL consent attestation: the customer agreed to receive SMS. Recorded
2906
+ // with a timestamp server-side; every SMS send path gates on it.
2907
+ //
2908
+ // Tri-state: omit it (or send null) to let the server apply the business's
2909
+ // configured default-opt-in policy, honouring a returning customer's
2910
+ // remembered preference. Send an explicit boolean to assert consent state
2911
+ // yourself — `false` records a deliberate opt-out.
2912
+ sms_consent: import_zod12.z.boolean().nullable().optional()
2913
+ }).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));
2914
+ var saleUpdateSchema = import_zod12.z.object({
2915
+ description: import_zod12.z.string().nullable().optional(),
2916
+ service: import_zod12.z.string().nullable().optional(),
2917
+ services: import_zod12.z.array(saleServiceSchema).nullable().optional(),
2918
+ currency: currencySchema.optional(),
2919
+ amount_total_cents: centsSchema.optional(),
2920
+ occurred_at: import_zod12.z.string().datetime().optional(),
2921
+ items: import_zod12.z.array(saleItemSchema).optional(),
2922
+ metadata: metadataSchema.nullable().optional(),
2923
+ customer_name: customerNameSchema.nullable().optional(),
2924
+ // Re-attest when changing a filler-looking name; the server clears the
2925
+ // prior attestation whenever `customer_name` changes.
2926
+ customer_name_placeholder_confirmed: import_zod12.z.boolean().optional(),
2927
+ customer_phone: customerPhoneSchema.nullable().optional(),
2928
+ customer_email: customerEmailSchema.nullable().optional(),
2929
+ // NOT NULL server-side: omit to leave unchanged (explicit null is rejected).
2930
+ sms_consent: import_zod12.z.boolean().optional()
2931
+ }).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: false }));
2932
+
2933
+ // src/hooks.ts
2934
+ function useGclid() {
2935
+ const [gclid, setGclid] = (0, import_react.useState)(null);
2936
+ (0, import_react.useEffect)(() => {
2937
+ setGclid(getCookieValueFromDocument("gclid"));
2938
+ }, []);
2939
+ return gclid;
2940
+ }
2941
+ function useTrackingParams() {
2942
+ const [trackingParams, setTrackingParams] = (0, import_react.useState)(createEmptyTrackingParams());
2943
+ (0, import_react.useEffect)(() => {
2944
+ setTrackingParams(getTrackingParamsFromCookieReader(getCookieValueFromDocument));
2945
+ }, []);
2946
+ return trackingParams;
2947
+ }
2948
+ var DEFAULT_CHOICE2 = {
2949
+ state: "granted",
2950
+ source: "default",
2951
+ updatedAt: null,
2952
+ expiresAt: null
2953
+ };
2954
+ function useCookiePreferences(options) {
2955
+ registerCapability("consent_controls");
2956
+ const [choice, setChoice] = (0, import_react.useState)(DEFAULT_CHOICE2);
2957
+ const ttlDays = options?.declineTtlDays;
2958
+ (0, import_react.useEffect)(() => {
2959
+ const sync = () => setChoice(getConsentChoice());
2960
+ sync();
2961
+ const handleStorage = (event) => {
2962
+ if (event.key === null || event.key === CONSENT_STATE_KEY || event.key === CONSENT_EXPIRES_AT_KEY || event.key === CONSENT_TIMESTAMP_KEY)
2963
+ sync();
2964
+ };
2965
+ window.addEventListener("storage", handleStorage);
2966
+ const unsubscribe = onConsentChange(sync);
2967
+ return () => {
2968
+ window.removeEventListener("storage", handleStorage);
2969
+ unsubscribe();
2970
+ };
2971
+ }, []);
2972
+ const optOutAction = (0, import_react.useCallback)(() => {
2973
+ optOut(ttlDays != null ? { declineTtlDays: ttlDays } : void 0);
2974
+ }, [ttlDays]);
2975
+ const optInAction = (0, import_react.useCallback)(() => {
2976
+ optIn();
2977
+ }, []);
2978
+ const reset = (0, import_react.useCallback)(() => {
2979
+ resetConsent();
2980
+ }, []);
2981
+ return {
2982
+ state: choice.state,
2983
+ source: choice.source,
2984
+ isDefault: choice.source === "default",
2985
+ isGranted: choice.state === "granted",
2986
+ isDenied: choice.state === "denied",
2987
+ updatedAt: choice.updatedAt,
2988
+ expiresAt: choice.expiresAt,
2989
+ optOut: optOutAction,
2990
+ optIn: optInAction,
2991
+ reset
2992
+ };
2993
+ }
2994
+ function useConsentState() {
2995
+ return useConsent().state;
2996
+ }
2997
+ function useConsent() {
2998
+ const {
2999
+ state,
3000
+ isGranted,
3001
+ isDenied,
3002
+ optIn: accept,
3003
+ optOut: decline,
3004
+ reset
3005
+ } = useCookiePreferences();
3006
+ return {
3007
+ state,
3008
+ isPending: false,
3009
+ isGranted,
3010
+ isDenied,
3011
+ accept,
3012
+ decline,
3013
+ reset
3014
+ };
3015
+ }
3016
+
3017
+ // src/ConsentBanner.tsx
3018
+ var import_jsx_runtime = require("react/jsx-runtime");
3019
+ var LIGHT_THEME = {
3020
+ background: "#ffffff",
3021
+ border: "#e5e7eb",
3022
+ text: "#111827",
3023
+ mutedText: "#4b5563",
3024
+ acceptBg: "#111827",
3025
+ acceptText: "#ffffff",
3026
+ declineBg: "transparent",
3027
+ declineText: "#111827",
3028
+ declineBorder: "#d1d5db",
3029
+ shadow: "0 -4px 16px -2px rgba(15, 23, 42, 0.08), 0 -2px 6px -1px rgba(15, 23, 42, 0.04)",
3030
+ linkColor: "#1f2937"
3031
+ };
3032
+ var DARK_THEME = {
3033
+ background: "#0f172a",
3034
+ border: "#1e293b",
3035
+ text: "#f1f5f9",
3036
+ mutedText: "#cbd5e1",
3037
+ acceptBg: "#f1f5f9",
3038
+ acceptText: "#0f172a",
3039
+ declineBg: "transparent",
3040
+ declineText: "#f1f5f9",
3041
+ declineBorder: "#334155",
3042
+ shadow: "0 -4px 16px -2px rgba(0, 0, 0, 0.5), 0 -2px 6px -1px rgba(0, 0, 0, 0.3)",
3043
+ linkColor: "#e2e8f0"
3044
+ };
3045
+ function useResolvedTheme(theme) {
3046
+ const [prefersDark, setPrefersDark] = (0, import_react2.useState)(false);
3047
+ (0, import_react2.useEffect)(() => {
3048
+ if (theme !== "auto" || typeof window === "undefined" || !window.matchMedia) return;
3049
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
3050
+ setPrefersDark(mql.matches);
3051
+ const onChange = (e) => setPrefersDark(e.matches);
3052
+ mql.addEventListener("change", onChange);
3053
+ return () => mql.removeEventListener("change", onChange);
3054
+ }, [theme]);
3055
+ if (theme === "dark") return DARK_THEME;
3056
+ if (theme === "auto" && prefersDark) return DARK_THEME;
3057
+ return LIGHT_THEME;
3058
+ }
3059
+ var DEFAULT_MESSAGE = "We use cookies to understand ad performance and improve how our marketing works across visits. You can accept or decline this tracking.";
3060
+ function ConsentBanner({
3061
+ message,
3062
+ title,
3063
+ acceptLabel = "Accept",
3064
+ declineLabel = "Decline",
3065
+ policyHref,
3066
+ policyLabel = "Learn more",
3067
+ onAccept,
3068
+ onDecline,
3069
+ position = "bottom",
3070
+ theme = "light",
3071
+ className,
3072
+ style
3073
+ } = {}) {
3074
+ const { isPending, accept, decline } = useConsent();
3075
+ const tokens = useResolvedTheme(theme);
3076
+ const [hasMounted, setHasMounted] = (0, import_react2.useState)(false);
3077
+ (0, import_react2.useEffect)(() => {
3078
+ setHasMounted(true);
3079
+ }, []);
3080
+ if (!hasMounted || !isPending) return null;
3081
+ const wrapperStyle = {
3082
+ position: "fixed",
3083
+ left: 0,
3084
+ right: 0,
3085
+ [position]: 0,
3086
+ zIndex: 2147483640,
3087
+ background: tokens.background,
3088
+ color: tokens.text,
3089
+ borderTop: position === "bottom" ? `1px solid ${tokens.border}` : "none",
3090
+ borderBottom: position === "top" ? `1px solid ${tokens.border}` : "none",
3091
+ boxShadow: tokens.shadow,
3092
+ padding: "16px 20px",
3093
+ boxSizing: "border-box",
3094
+ animation: `${ANIMATION_NAME}-${position} 200ms ease-out`,
3095
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
3096
+ ...style
3097
+ };
3098
+ const innerStyle = {
3099
+ maxWidth: 1100,
3100
+ margin: "0 auto",
3101
+ display: "flex",
3102
+ flexWrap: "wrap",
3103
+ gap: 16,
3104
+ alignItems: "center",
3105
+ justifyContent: "space-between"
3106
+ };
3107
+ const messageStyle = {
3108
+ flex: "1 1 320px",
3109
+ margin: 0,
3110
+ fontSize: 14,
3111
+ lineHeight: 1.5,
3112
+ color: tokens.mutedText
3113
+ };
3114
+ const titleStyle = {
3115
+ margin: "0 0 4px 0",
3116
+ fontSize: 14,
3117
+ fontWeight: 600,
3118
+ color: tokens.text
3119
+ };
3120
+ const actionsStyle = {
3121
+ display: "flex",
3122
+ gap: 8,
3123
+ flexShrink: 0
3124
+ };
3125
+ const buttonBase = {
3126
+ appearance: "none",
3127
+ fontFamily: "inherit",
3128
+ fontSize: 14,
3129
+ fontWeight: 500,
3130
+ padding: "8px 16px",
3131
+ borderRadius: 6,
3132
+ cursor: "pointer",
3133
+ border: "1px solid transparent",
3134
+ transition: "opacity 120ms ease"
3135
+ };
3136
+ const declineStyle = {
3137
+ ...buttonBase,
3138
+ background: tokens.declineBg,
3139
+ color: tokens.declineText,
3140
+ borderColor: tokens.declineBorder
3141
+ };
3142
+ const acceptStyle = {
3143
+ ...buttonBase,
3144
+ background: tokens.acceptBg,
3145
+ color: tokens.acceptText
3146
+ };
3147
+ const linkStyle = {
3148
+ color: tokens.linkColor,
3149
+ textDecoration: "underline",
3150
+ textUnderlineOffset: 2
3151
+ };
3152
+ const handleAccept = () => {
3153
+ accept();
3154
+ onAccept?.();
3155
+ };
3156
+ const handleDecline = () => {
3157
+ decline();
3158
+ onDecline?.();
3159
+ };
3160
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
3161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: ANIMATION_KEYFRAMES }),
3162
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3163
+ "div",
3164
+ {
3165
+ role: "dialog",
3166
+ "aria-live": "polite",
3167
+ "aria-label": "Cookie consent",
3168
+ className,
3169
+ style: wrapperStyle,
3170
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: innerStyle, children: [
3171
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: "1 1 320px" }, children: [
3172
+ title ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: titleStyle, children: title }) : null,
3173
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: messageStyle, children: [
3174
+ message ?? DEFAULT_MESSAGE,
3175
+ policyHref ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
3176
+ " ",
3177
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: policyHref, style: linkStyle, children: policyLabel })
3178
+ ] }) : null
3179
+ ] })
3180
+ ] }),
3181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: actionsStyle, children: [
3182
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: handleDecline, style: declineStyle, children: declineLabel }),
3183
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: handleAccept, style: acceptStyle, children: acceptLabel })
3184
+ ] })
3185
+ ] })
3186
+ }
3187
+ )
3188
+ ] });
3189
+ }
3190
+ var ANIMATION_NAME = "aranova-consent-banner";
3191
+ var ANIMATION_KEYFRAMES = `
3192
+ @keyframes ${ANIMATION_NAME}-bottom {
3193
+ from { transform: translateY(100%); opacity: 0; }
3194
+ to { transform: translateY(0); opacity: 1; }
3195
+ }
3196
+ @keyframes ${ANIMATION_NAME}-top {
3197
+ from { transform: translateY(-100%); opacity: 0; }
3198
+ to { transform: translateY(0); opacity: 1; }
3199
+ }
3200
+ `;
3201
+
3202
+ // src/AdPlatformTracking.tsx
3203
+ var import_script = __toESM(require("next/script"));
3204
+ var import_react3 = require("react");
3205
+ var import_jsx_runtime2 = require("react/jsx-runtime");
3206
+ function AdPlatformTracking({
3207
+ gtagId,
3208
+ gtagIds,
3209
+ trackingConfig,
3210
+ standalonePageView = false,
3211
+ metaPixelId,
3212
+ metaPixelIds
3213
+ }) {
3214
+ const trackingConfigKey2 = trackingConfig ? `${resolveTrackingConfigUrl(trackingConfig)}:${trackingConfig.businessId}:${trackingConfig.environment}` : "";
3215
+ (0, import_react3.useEffect)(() => {
3216
+ if (!trackingConfig) return;
3217
+ const runtime = getTrackingConfigRuntime(trackingConfig);
3218
+ if (standalonePageView) runtime.queuePageView();
3219
+ else runtime.start();
3220
+ }, [trackingConfigKey2, standalonePageView]);
3221
+ const gtagAll = trackingConfig ? [] : gtagIds ? Object.values(gtagIds).filter((id) => id != null) : gtagId ? [gtagId] : [];
3222
+ const metaAll = metaPixelIds ? Object.values(metaPixelIds).filter((id) => id != null) : metaPixelId ? [metaPixelId] : [];
3223
+ if (trackingConfig || gtagAll.length > 0) registerCapability("ad_tags_google");
3224
+ if (metaAll.length > 0) registerCapability("ad_tags_meta");
3225
+ const gtagLoaderId = gtagAll[0];
3226
+ const gtagInitScript = gtagAll.length === 1 ? createGtagInitScript(gtagAll[0]) : createGtagInitScriptMulti(gtagAll);
3227
+ const metaInitScript = metaAll.length === 1 ? createMetaPixelInitScript(metaAll[0]) : createMetaPixelInitScriptMulti(metaAll);
3228
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
3229
+ gtagLoaderId ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
3230
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "consent-default", strategy: "beforeInteractive", children: createConsentDefaultScript() }),
3231
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3232
+ import_script.default,
3233
+ {
3234
+ src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(gtagLoaderId)}`,
3235
+ strategy: "afterInteractive"
3236
+ }
3237
+ ),
3238
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "gtag-init", strategy: "afterInteractive", children: gtagInitScript })
3239
+ ] }) : null,
3240
+ metaAll.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
3241
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "meta-pixel-base", strategy: "afterInteractive", children: createMetaConsentDefaultScript() }),
3242
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "meta-pixel-init", strategy: "afterInteractive", children: metaInitScript })
3243
+ ] }) : null
3244
+ ] });
3245
+ }
3246
+
3247
+ // src/GoogleAdsTracking.tsx
3248
+ var import_script2 = __toESM(require("next/script"));
3249
+ var import_jsx_runtime3 = require("react/jsx-runtime");
3250
+ function GoogleAdsTracking(props) {
3251
+ const { gtagId, gtagIds } = props;
3252
+ const allIds = gtagIds ? Object.values(gtagIds).filter((id) => id != null) : gtagId ? [gtagId] : [];
3253
+ if (allIds.length === 0) return null;
3254
+ const loaderId = allIds[0];
3255
+ const initScript = allIds.length === 1 ? createGtagInitScript(loaderId) : createGtagInitScriptMulti(allIds);
3256
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3257
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_script2.default, { id: "consent-default", strategy: "beforeInteractive", children: createConsentDefaultScript() }),
3258
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3259
+ import_script2.default,
3260
+ {
3261
+ src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(loaderId)}`,
3262
+ strategy: "afterInteractive"
3263
+ }
3264
+ ),
3265
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_script2.default, { id: "gtag-init", strategy: "afterInteractive", children: initScript })
3266
+ ] });
3267
+ }
3268
+
3269
+ // src/factory.tsx
3270
+ var import_navigation3 = require("next/navigation");
3271
+
3272
+ // package.json
3273
+ var version = "0.25.0";
3274
+
3275
+ // src/factory.tsx
3276
+ var import_react5 = require("react");
3277
+
3278
+ // ../tracking-core/src/phone-react.tsx
3279
+ var import_react4 = require("react");
3280
+ var import_jsx_runtime4 = require("react/jsx-runtime");
3281
+ var _phoneConfigContext;
3282
+ function phoneConfigContext() {
3283
+ return _phoneConfigContext ?? (_phoneConfigContext = (0, import_react4.createContext)(null));
3284
+ }
3285
+ function PhoneConfigProvider({
3286
+ value,
3287
+ children
3288
+ }) {
3289
+ const Ctx = phoneConfigContext();
3290
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Ctx.Provider, { value, children });
3291
+ }
3292
+ function usePhoneConfig() {
3293
+ const ctx = (0, import_react4.useContext)(phoneConfigContext());
3294
+ return {
3295
+ defaultCountry: ctx?.defaultCountry ?? DEFAULT_PHONE_COUNTRY,
3296
+ display: ctx?.display ?? "national"
3297
+ };
3298
+ }
3299
+ function usePhoneField(opts = {}) {
3300
+ registerCapability("phone_fields");
3301
+ const cfg = usePhoneConfig();
3302
+ const country = opts.country ?? cfg.defaultCountry;
3303
+ const display = opts.display ?? cfg.display;
3304
+ const { onValueChange } = opts;
3305
+ const [value, setValue] = (0, import_react4.useState)(() => formatPhoneAsTyped(opts.defaultValue ?? "", country));
3306
+ const [touched, setTouched] = (0, import_react4.useState)(false);
3307
+ const parsed = (0, import_react4.useMemo)(() => parsePhone(value, country), [value, country]);
3308
+ const onChange = (0, import_react4.useCallback)(
3309
+ (event) => {
3310
+ const next = formatPhoneAsTyped(event.target.value, country);
3311
+ setValue(next);
3312
+ onValueChange?.(parsePhone(next, country).e164);
3313
+ },
3314
+ [country, onValueChange]
3315
+ );
3316
+ const onBlur = (0, import_react4.useCallback)(
3317
+ (_event) => {
3318
+ setTouched(true);
3319
+ setValue((current) => {
3320
+ const p = parsePhone(current, country);
3321
+ return p.isValid ? formatPhone(current, display, country) : current;
3322
+ });
3323
+ },
3324
+ [country, display]
3325
+ );
3326
+ const error = touched && value.length > 0 && !parsed.isValid ? "Enter a valid phone number" : null;
3327
+ return {
3328
+ value,
3329
+ e164: parsed.e164,
3330
+ isValid: parsed.isValid,
3331
+ error,
3332
+ parsed,
3333
+ inputProps: { value, onChange, onBlur, type: "tel", inputMode: "tel", autoComplete: "tel" }
3334
+ };
3335
+ }
3336
+ var PhoneField = (0, import_react4.forwardRef)(function PhoneField2({ country, value, defaultValue, onChange, onE164Change, ...rest }, ref) {
3337
+ const cfg = usePhoneConfig();
3338
+ const resolvedCountry = country ?? cfg.defaultCountry;
3339
+ const isControlled = value !== void 0;
3340
+ const [internal, setInternal] = (0, import_react4.useState)(
3341
+ () => formatPhoneAsTyped(defaultValue ?? "", resolvedCountry)
3342
+ );
3343
+ const handleChange = (event) => {
3344
+ const formatted = formatPhoneAsTyped(event.target.value, resolvedCountry);
3345
+ event.target.value = formatted;
3346
+ onE164Change?.(parsePhone(formatted, resolvedCountry).e164);
3347
+ if (!isControlled) setInternal(formatted);
3348
+ onChange?.(event);
3349
+ };
3350
+ const shown = isControlled ? formatPhoneAsTyped(value, resolvedCountry) : internal;
3351
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3352
+ "input",
3353
+ {
3354
+ ...rest,
3355
+ ref,
3356
+ type: "tel",
3357
+ inputMode: "tel",
3358
+ autoComplete: "tel",
3359
+ value: shown,
3360
+ onChange: handleChange
3361
+ }
3362
+ );
3363
+ });
3364
+
3365
+ // src/factory.tsx
3366
+ var import_jsx_runtime5 = require("react/jsx-runtime");
3367
+ function PageViewTracker({ fire }) {
3368
+ const pathname = (0, import_navigation3.usePathname)();
3369
+ const searchParams = (0, import_navigation3.useSearchParams)();
3370
+ (0, import_react5.useEffect)(() => {
3371
+ fire();
3372
+ }, [pathname, searchParams, fire]);
3373
+ return null;
3374
+ }
3375
+ function createTracking(options) {
3376
+ const {
3377
+ apiKey,
3378
+ endpoint,
3379
+ triggers,
3380
+ environment,
3381
+ gtagIds,
3382
+ debug,
3383
+ phone,
3384
+ conversionConfig,
3385
+ trackingConfig
3386
+ } = options;
3387
+ registerCapability("base_tracking");
3388
+ if (trackingConfig) registerCapability("conversion_goals_auto");
3389
+ if (!apiKey || !endpoint) {
3390
+ if (apiKey || endpoint) {
3391
+ console.warn(
3392
+ "[AranovaTracking] createTracking() requires both `apiKey` and `endpoint`. Tracking is disabled for this session."
3393
+ );
3394
+ }
3395
+ const noopClient = {
3396
+ trackEvent: () => {
3397
+ },
3398
+ flush: async () => {
3399
+ },
3400
+ getSessionId: () => "",
3401
+ getVisitorId: () => ""
3402
+ };
3403
+ return {
3404
+ TrackingProvider: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PhoneConfigProvider, { value: phone ?? null, children }),
3405
+ useTracking: () => noopClient
3406
+ };
3407
+ }
3408
+ const TrackingContext = (0, import_react5.createContext)(null);
3409
+ const specificPageVisitConfig = triggers.automatic.specific_page_visit ?? null;
3410
+ const timeOnSiteConfig = triggers.automatic.time_on_site ?? null;
3411
+ function TrackingProvider({ children }) {
3412
+ const pathname = (0, import_navigation3.usePathname)();
3413
+ const rawClient = (0, import_react5.useMemo)(
3414
+ () => getOrCreateTrackingClient({
3415
+ apiKey,
3416
+ endpoint,
3417
+ surface: "next",
3418
+ packageName: "@aranova/tracking-next",
3419
+ sdkVersion: version,
3420
+ triggers,
3421
+ environment,
3422
+ activeGtagIds: gtagIds,
3423
+ debug
3424
+ }),
3425
+ []
3426
+ );
3427
+ const conversionStore = (0, import_react5.useMemo)(
3428
+ () => trackingConfig ? getTrackingConfigRuntime(trackingConfig) : conversionConfig ? resolveConversionConfig({
3429
+ cdnUrl: conversionConfig.cdnUrl,
3430
+ baked: conversionConfig.baked
3431
+ }) : null,
3432
+ []
3433
+ );
3434
+ const wrapClient = (0, import_react5.useMemo)(() => {
3435
+ const autoFire = conversionStore ? createConversionAutoFire(conversionStore) : null;
3436
+ return (c) => autoFire ? withConversionAutoFire(c, autoFire) : c;
3437
+ }, [conversionStore]);
3438
+ const client = (0, import_react5.useMemo)(
3439
+ () => createTypedClient(wrapClient(rawClient), triggers, { debug }),
3440
+ [rawClient, wrapClient]
3441
+ );
3442
+ (0, import_react5.useEffect)(() => {
3443
+ if (trackingConfig && conversionStore && "start" in conversionStore) {
3444
+ conversionStore.start();
3445
+ }
3446
+ }, [conversionStore]);
3447
+ const firePageView = (0, import_react5.useCallback)(() => {
3448
+ fireManualPageView(wrapClient(rawClient));
3449
+ if (trackingConfig) getTrackingConfigRuntime(trackingConfig).queuePageView();
3450
+ }, [rawClient, wrapClient]);
3451
+ const firedPagesRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
3452
+ (0, import_react5.useEffect)(() => {
3453
+ if (!specificPageVisitConfig || pathname === null) return;
3454
+ const detectorClient = wrapClient(rawClient);
3455
+ for (const { name, pathPattern } of specificPageVisitConfig.pages) {
3456
+ if (!pathPattern.test(pathname)) continue;
3457
+ const key = `${name}:${pathname}`;
3458
+ if (firedPagesRef.current.has(key)) continue;
3459
+ firedPagesRef.current.add(key);
3460
+ detectorClient.trackEvent({
3461
+ eventType: "specific_page_visit",
3462
+ metadata: {
3463
+ page_name: name,
3464
+ page: { path: pathname }
3465
+ },
3466
+ pageUrl: null,
3467
+ occurredAt: null
3468
+ });
3469
+ }
3470
+ }, [pathname, rawClient, wrapClient]);
3471
+ const multiPageSessionConfig = triggers.automatic.multi_page_session ?? null;
3472
+ const mpsLastPathRef = (0, import_react5.useRef)(null);
3473
+ (0, import_react5.useEffect)(() => {
3474
+ if (!multiPageSessionConfig || pathname === null) return;
3475
+ if (pathname === mpsLastPathRef.current) return;
3476
+ mpsLastPathRef.current = pathname;
3477
+ const MPS_PATHS_KEY = "aranova_tracking_mps_paths";
3478
+ const MPS_SESSION_KEY = "aranova_tracking_mps_session";
3479
+ const MPS_FIRED_KEY = "aranova_tracking_mps_fired";
3480
+ try {
3481
+ const store = window.sessionStorage;
3482
+ const currentSession = getOrRotateSessionId().id;
3483
+ const storedSession = store.getItem(MPS_SESSION_KEY);
3484
+ if (storedSession !== currentSession) {
3485
+ store.setItem(MPS_SESSION_KEY, currentSession);
3486
+ store.removeItem(MPS_PATHS_KEY);
3487
+ store.removeItem(MPS_FIRED_KEY);
3488
+ }
3489
+ if (store.getItem(MPS_FIRED_KEY) === "1") return;
3490
+ const raw = store.getItem(MPS_PATHS_KEY);
3491
+ const paths = raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
3492
+ paths.add(pathname);
3493
+ store.setItem(MPS_PATHS_KEY, JSON.stringify([...paths]));
3494
+ if (paths.size >= multiPageSessionConfig.pageThreshold) {
3495
+ store.setItem(MPS_FIRED_KEY, "1");
3496
+ wrapClient(rawClient).trackEvent({
3497
+ eventType: "multi_page_session",
3498
+ metadata: {
3499
+ page_count: paths.size,
3500
+ page: { path: pathname }
3501
+ },
3502
+ pageUrl: null,
3503
+ occurredAt: null
3504
+ });
3505
+ }
3506
+ } catch {
3507
+ }
3508
+ }, [multiPageSessionConfig, pathname, rawClient, wrapClient]);
3509
+ (0, import_react5.useEffect)(() => {
3510
+ return attachClientCapturesOnce(rawClient, () => {
3511
+ const detachers = [];
3512
+ const detectorClient = wrapClient(rawClient);
3513
+ detachers.push(attachPageExit(detectorClient));
3514
+ detachers.push(attachBfcacheRestore(detectorClient));
3515
+ if (timeOnSiteConfig) {
3516
+ detachers.push(attachTimeOnSite(detectorClient, timeOnSiteConfig));
3517
+ }
3518
+ const scrollDepth = triggers.automatic.scroll_depth;
3519
+ if (scrollDepth) {
3520
+ detachers.push(attachScrollDepth(detectorClient, scrollDepth));
3521
+ }
3522
+ const formStart = triggers.automatic.form_start;
3523
+ if (formStart) {
3524
+ detachers.push(attachFormStart(detectorClient, formStart));
3525
+ }
3526
+ const ctaClick = triggers.manual?.cta_click;
3527
+ if (ctaClick) {
3528
+ detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
3529
+ }
3530
+ const phoneClick = triggers.manual?.phone_click;
3531
+ if (phoneClick) {
3532
+ detachers.push(attachPhoneClickCapture(detectorClient, phoneClick));
3533
+ }
3534
+ return () => {
3535
+ for (let i = detachers.length - 1; i >= 0; i--) {
3536
+ detachers[i]();
3537
+ }
3538
+ };
3539
+ });
3540
+ }, [rawClient, wrapClient]);
3541
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(TrackingContext.Provider, { value: client, children: [
3542
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react5.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PageViewTracker, { fire: firePageView }) }),
3543
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PhoneConfigProvider, { value: phone ?? null, children })
3544
+ ] });
3545
+ }
3546
+ function useTracking() {
3547
+ const client = (0, import_react5.useContext)(TrackingContext);
3548
+ if (client === null) {
3549
+ throw new Error(
3550
+ "useTracking must be called inside a <TrackingProvider> returned by createTracking()"
3551
+ );
3552
+ }
3553
+ return client;
3554
+ }
3555
+ return { TrackingProvider, useTracking };
3556
+ }
3557
+ // Annotate the CommonJS export names for ESM import in node:
3558
+ 0 && (module.exports = {
3559
+ AdPlatformTracking,
3560
+ ConsentBanner,
3561
+ GoogleAdsTracking,
3562
+ PhoneField,
3563
+ createTracking,
3564
+ useConsent,
3565
+ useConsentState,
3566
+ useCookiePreferences,
3567
+ useGclid,
3568
+ usePhoneConfig,
3569
+ usePhoneField,
3570
+ useTrackingParams
3571
+ });
3572
+ //# sourceMappingURL=client.js.map