@active-reach/web-sdk 1.42.0 → 1.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/package.json +1 -1
  2. package/dist/aegis-push.min.js +0 -2
  3. package/dist/aegis-push.min.js.map +0 -1
  4. package/dist/aegis-sw.js +0 -1
  5. package/dist/aegis.min.js +0 -2
  6. package/dist/aegis.min.js.map +0 -1
  7. package/dist/analytics-C-Go3Pm5.mjs +0 -3509
  8. package/dist/analytics-C-Go3Pm5.mjs.map +0 -1
  9. package/dist/cdn.d.ts +0 -26
  10. package/dist/cdn.d.ts.map +0 -1
  11. package/dist/chat/AegisChat.d.ts +0 -169
  12. package/dist/chat/AegisChat.d.ts.map +0 -1
  13. package/dist/chat/index.d.ts +0 -3
  14. package/dist/chat/index.d.ts.map +0 -1
  15. package/dist/core/analytics.d.ts +0 -320
  16. package/dist/core/analytics.d.ts.map +0 -1
  17. package/dist/core/bootstrap.d.ts +0 -96
  18. package/dist/core/bootstrap.d.ts.map +0 -1
  19. package/dist/core/prefetch-bundle-client.d.ts +0 -175
  20. package/dist/core/prefetch-bundle-client.d.ts.map +0 -1
  21. package/dist/core/queue.d.ts +0 -35
  22. package/dist/core/queue.d.ts.map +0 -1
  23. package/dist/core/rate-limiter.d.ts +0 -45
  24. package/dist/core/rate-limiter.d.ts.map +0 -1
  25. package/dist/core/sdk-config-poller.d.ts +0 -59
  26. package/dist/core/sdk-config-poller.d.ts.map +0 -1
  27. package/dist/core/session.d.ts +0 -52
  28. package/dist/core/session.d.ts.map +0 -1
  29. package/dist/core/transport.d.ts +0 -50
  30. package/dist/core/transport.d.ts.map +0 -1
  31. package/dist/core/user-namespace.d.ts +0 -48
  32. package/dist/core/user-namespace.d.ts.map +0 -1
  33. package/dist/ecommerce/index.d.ts +0 -94
  34. package/dist/ecommerce/index.d.ts.map +0 -1
  35. package/dist/ecommerce/types.d.ts +0 -81
  36. package/dist/ecommerce/types.d.ts.map +0 -1
  37. package/dist/governance/bloom-filter.d.ts +0 -47
  38. package/dist/governance/bloom-filter.d.ts.map +0 -1
  39. package/dist/governance/index.d.ts +0 -8
  40. package/dist/governance/index.d.ts.map +0 -1
  41. package/dist/governance/murmur3.d.ts +0 -43
  42. package/dist/governance/murmur3.d.ts.map +0 -1
  43. package/dist/governance/name-governor.d.ts +0 -98
  44. package/dist/governance/name-governor.d.ts.map +0 -1
  45. package/dist/governance/trait-governor.d.ts +0 -63
  46. package/dist/governance/trait-governor.d.ts.map +0 -1
  47. package/dist/inapp/AegisInAppManager.d.ts +0 -888
  48. package/dist/inapp/AegisInAppManager.d.ts.map +0 -1
  49. package/dist/inapp/actions.d.ts +0 -91
  50. package/dist/inapp/actions.d.ts.map +0 -1
  51. package/dist/inapp/devicePairing.d.ts +0 -70
  52. package/dist/inapp/devicePairing.d.ts.map +0 -1
  53. package/dist/inapp/eval-throttle.d.ts +0 -33
  54. package/dist/inapp/eval-throttle.d.ts.map +0 -1
  55. package/dist/inapp/font.d.ts +0 -16
  56. package/dist/inapp/font.d.ts.map +0 -1
  57. package/dist/inapp/index.d.ts +0 -8
  58. package/dist/inapp/index.d.ts.map +0 -1
  59. package/dist/inapp/lucide-svg.d.ts +0 -46
  60. package/dist/inapp/lucide-svg.d.ts.map +0 -1
  61. package/dist/inapp/renderPreview.d.ts +0 -40
  62. package/dist/inapp/renderPreview.d.ts.map +0 -1
  63. package/dist/inapp/renderers/active-web-chat.d.ts +0 -45
  64. package/dist/inapp/renderers/active-web-chat.d.ts.map +0 -1
  65. package/dist/inapp/renderers/card-style.d.ts +0 -31
  66. package/dist/inapp/renderers/card-style.d.ts.map +0 -1
  67. package/dist/inapp/renderers/card-template.d.ts +0 -233
  68. package/dist/inapp/renderers/card-template.d.ts.map +0 -1
  69. package/dist/inapp/renderers/carousel-cards.d.ts +0 -15
  70. package/dist/inapp/renderers/carousel-cards.d.ts.map +0 -1
  71. package/dist/inapp/renderers/coachmark-tour.d.ts +0 -24
  72. package/dist/inapp/renderers/coachmark-tour.d.ts.map +0 -1
  73. package/dist/inapp/renderers/games.d.ts +0 -27
  74. package/dist/inapp/renderers/games.d.ts.map +0 -1
  75. package/dist/inapp/renderers/hero.d.ts +0 -23
  76. package/dist/inapp/renderers/hero.d.ts.map +0 -1
  77. package/dist/inapp/renderers/index.d.ts +0 -22
  78. package/dist/inapp/renderers/index.d.ts.map +0 -1
  79. package/dist/inapp/renderers/interactive-contract.d.ts +0 -48
  80. package/dist/inapp/renderers/interactive-contract.d.ts.map +0 -1
  81. package/dist/inapp/renderers/product-recommendation.d.ts +0 -37
  82. package/dist/inapp/renderers/product-recommendation.d.ts.map +0 -1
  83. package/dist/inapp/renderers/progress-bar.d.ts +0 -38
  84. package/dist/inapp/renderers/progress-bar.d.ts.map +0 -1
  85. package/dist/inapp/renderers/progress-core.d.ts +0 -80
  86. package/dist/inapp/renderers/progress-core.d.ts.map +0 -1
  87. package/dist/inapp/renderers/reviews.d.ts +0 -38
  88. package/dist/inapp/renderers/reviews.d.ts.map +0 -1
  89. package/dist/inapp/renderers/sticky-bar.d.ts +0 -14
  90. package/dist/inapp/renderers/sticky-bar.d.ts.map +0 -1
  91. package/dist/inapp/renderers/stories.d.ts +0 -31
  92. package/dist/inapp/renderers/stories.d.ts.map +0 -1
  93. package/dist/inapp/renderers/types.d.ts +0 -147
  94. package/dist/inapp/renderers/types.d.ts.map +0 -1
  95. package/dist/inapp/renderers/video.d.ts +0 -20
  96. package/dist/inapp/renderers/video.d.ts.map +0 -1
  97. package/dist/inbox/AegisInbox.d.ts +0 -106
  98. package/dist/inbox/AegisInbox.d.ts.map +0 -1
  99. package/dist/inbox/index.d.ts +0 -3
  100. package/dist/inbox/index.d.ts.map +0 -1
  101. package/dist/index.d.ts +0 -40
  102. package/dist/index.d.ts.map +0 -1
  103. package/dist/index.js +0 -15311
  104. package/dist/index.js.map +0 -1
  105. package/dist/integrations/react.d.ts +0 -19
  106. package/dist/integrations/react.d.ts.map +0 -1
  107. package/dist/loyalty/AegisLoyaltyManager.d.ts +0 -120
  108. package/dist/loyalty/AegisLoyaltyManager.d.ts.map +0 -1
  109. package/dist/loyalty/index.d.ts +0 -3
  110. package/dist/loyalty/index.d.ts.map +0 -1
  111. package/dist/plugins/registry.d.ts +0 -13
  112. package/dist/plugins/registry.d.ts.map +0 -1
  113. package/dist/push/AegisWebPush.d.ts +0 -199
  114. package/dist/push/AegisWebPush.d.ts.map +0 -1
  115. package/dist/push/AegisWebPush.js +0 -542
  116. package/dist/push/AegisWebPush.js.map +0 -1
  117. package/dist/push/cdn-push.d.ts +0 -21
  118. package/dist/push/cdn-push.d.ts.map +0 -1
  119. package/dist/react.js +0 -70
  120. package/dist/react.js.map +0 -1
  121. package/dist/runtime/AegisMessageRuntime.d.ts +0 -276
  122. package/dist/runtime/AegisMessageRuntime.d.ts.map +0 -1
  123. package/dist/runtime/index.d.ts +0 -3
  124. package/dist/runtime/index.d.ts.map +0 -1
  125. package/dist/snippet.min.js +0 -1
  126. package/dist/state/intent_ledger.d.ts +0 -136
  127. package/dist/state/intent_ledger.d.ts.map +0 -1
  128. package/dist/triggers/ContactScoresFetcher.d.ts +0 -113
  129. package/dist/triggers/ContactScoresFetcher.d.ts.map +0 -1
  130. package/dist/triggers/IntentRuleEvaluator.d.ts +0 -213
  131. package/dist/triggers/IntentRuleEvaluator.d.ts.map +0 -1
  132. package/dist/triggers/IntentSnapshotCollector.d.ts +0 -83
  133. package/dist/triggers/IntentSnapshotCollector.d.ts.map +0 -1
  134. package/dist/triggers/SelectorBinder.d.ts +0 -118
  135. package/dist/triggers/SelectorBinder.d.ts.map +0 -1
  136. package/dist/triggers/TriggerEngine.d.ts +0 -296
  137. package/dist/triggers/TriggerEngine.d.ts.map +0 -1
  138. package/dist/triggers/behaviouralTriggers.d.ts +0 -44
  139. package/dist/triggers/behaviouralTriggers.d.ts.map +0 -1
  140. package/dist/triggers/index.d.ts +0 -11
  141. package/dist/triggers/index.d.ts.map +0 -1
  142. package/dist/triggers/microIntentTriggers.d.ts +0 -80
  143. package/dist/triggers/microIntentTriggers.d.ts.map +0 -1
  144. package/dist/types/config.d.ts +0 -77
  145. package/dist/types/config.d.ts.map +0 -1
  146. package/dist/types/events.d.ts +0 -147
  147. package/dist/types/events.d.ts.map +0 -1
  148. package/dist/types/index.d.ts +0 -5
  149. package/dist/types/index.d.ts.map +0 -1
  150. package/dist/types/plugin.d.ts +0 -21
  151. package/dist/types/plugin.d.ts.map +0 -1
  152. package/dist/types/type-safe-events.d.ts +0 -14
  153. package/dist/types/type-safe-events.d.ts.map +0 -1
  154. package/dist/utils/canonical-identity.d.ts +0 -29
  155. package/dist/utils/canonical-identity.d.ts.map +0 -1
  156. package/dist/utils/consent.d.ts +0 -40
  157. package/dist/utils/consent.d.ts.map +0 -1
  158. package/dist/utils/debounce.d.ts +0 -3
  159. package/dist/utils/debounce.d.ts.map +0 -1
  160. package/dist/utils/device.d.ts +0 -6
  161. package/dist/utils/device.d.ts.map +0 -1
  162. package/dist/utils/identity.d.ts +0 -27
  163. package/dist/utils/identity.d.ts.map +0 -1
  164. package/dist/utils/index.d.ts +0 -7
  165. package/dist/utils/index.d.ts.map +0 -1
  166. package/dist/utils/logger.d.ts +0 -15
  167. package/dist/utils/logger.d.ts.map +0 -1
  168. package/dist/utils/meta-cookies.d.ts +0 -65
  169. package/dist/utils/meta-cookies.d.ts.map +0 -1
  170. package/dist/utils/redact-url.d.ts +0 -56
  171. package/dist/utils/redact-url.d.ts.map +0 -1
  172. package/dist/utils/storage.d.ts +0 -20
  173. package/dist/utils/storage.d.ts.map +0 -1
  174. package/dist/utils/url-parser.d.ts +0 -61
  175. package/dist/utils/url-parser.d.ts.map +0 -1
  176. package/dist/utils/uuid.d.ts +0 -4
  177. package/dist/utils/uuid.d.ts.map +0 -1
  178. package/dist/utils/visitor.d.ts +0 -43
  179. package/dist/utils/visitor.d.ts.map +0 -1
  180. package/dist/widgets/AegisWidgetManager.d.ts +0 -209
  181. package/dist/widgets/AegisWidgetManager.d.ts.map +0 -1
  182. package/dist/widgets/index.d.ts +0 -3
  183. package/dist/widgets/index.d.ts.map +0 -1
@@ -1,3509 +0,0 @@
1
- const DEFAULT_CONFIG = {
2
- workspace_id: null,
3
- batch_size: 10,
4
- batch_interval: 1e3,
5
- capture_utm: true,
6
- capture_referrer: true,
7
- auto_page_view: false,
8
- session_timeout: 30,
9
- debug: false,
10
- respect_dnt: true,
11
- cross_domain_tracking: false,
12
- secure_cookie: true,
13
- enable_offline_mode: true,
14
- max_offline_events: 100,
15
- retry_failed_requests: true,
16
- max_retries: 3,
17
- retry_backoff_multiplier: 2,
18
- request_timeout: 5e3,
19
- rate_limit_burst: 100,
20
- rate_limit_per_second: 20,
21
- auto_region_detection: true,
22
- wait_for_consent: false,
23
- enable_consent_mode: false,
24
- integrate_onetrust: false,
25
- integrate_cookiebot: false,
26
- integrate_google_consent_mode: false,
27
- default_consent: {
28
- analytics: false,
29
- marketing: false,
30
- functional: true
31
- },
32
- initialized: false,
33
- api_host: null,
34
- cell_endpoints: [],
35
- preferred_region: null,
36
- cookie_domain: null,
37
- plugins: [],
38
- active_cell: null
39
- };
40
- function generateUUID() {
41
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
42
- return crypto.randomUUID();
43
- }
44
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
45
- const r = Math.random() * 16 | 0;
46
- const v = c === "x" ? r : r & 3 | 8;
47
- return v.toString(16);
48
- });
49
- }
50
- function generateSessionId() {
51
- return `sess_${Date.now()}_${generateUUID().slice(0, 8)}`;
52
- }
53
- function generateMessageId() {
54
- return `msg_${Date.now()}_${generateUUID().slice(0, 8)}`;
55
- }
56
- class Logger {
57
- constructor() {
58
- this.enabled = false;
59
- this.prefix = "[Active Reach SDK]";
60
- }
61
- enable() {
62
- this.enabled = true;
63
- }
64
- disable() {
65
- this.enabled = false;
66
- }
67
- isEnabled() {
68
- return this.enabled;
69
- }
70
- debug(message, ...args) {
71
- if (!this.enabled) return;
72
- console.debug(`${this.prefix} ${message}`, ...args);
73
- }
74
- info(message, ...args) {
75
- if (!this.enabled) return;
76
- console.info(`${this.prefix} ${message}`, ...args);
77
- }
78
- warn(message, ...args) {
79
- console.warn(`${this.prefix} ${message}`, ...args);
80
- }
81
- error(message, ...args) {
82
- console.error(`${this.prefix} ${message}`, ...args);
83
- }
84
- }
85
- const logger = new Logger();
86
- class Identity {
87
- constructor(storage) {
88
- this.userId = null;
89
- this.traits = {};
90
- this.storage = storage;
91
- this.anonymousId = this.getOrCreateAnonymousId();
92
- this.loadUserIdentity();
93
- }
94
- getOrCreateAnonymousId() {
95
- let id = this.storage.get("anon_id");
96
- if (!id) {
97
- id = generateUUID();
98
- this.storage.set("anon_id", id, 365);
99
- logger.debug("Created new anonymous ID:", id);
100
- } else {
101
- logger.debug("Loaded existing anonymous ID:", id);
102
- }
103
- return id;
104
- }
105
- loadUserIdentity() {
106
- const storedUserId = this.storage.get("user_id");
107
- const storedTraits = this.storage.get("user_traits");
108
- if (storedUserId) {
109
- this.userId = storedUserId;
110
- logger.debug("Loaded user ID:", storedUserId);
111
- }
112
- if (storedTraits) {
113
- try {
114
- this.traits = JSON.parse(storedTraits);
115
- logger.debug("Loaded user traits:", this.traits);
116
- } catch (error) {
117
- logger.warn("Failed to parse stored traits:", error);
118
- this.traits = {};
119
- }
120
- }
121
- }
122
- getAnonymousId() {
123
- return this.anonymousId;
124
- }
125
- setUserId(userId, traits) {
126
- this.userId = userId;
127
- this.storage.set("user_id", userId, 365);
128
- logger.info("User identified:", userId);
129
- if (traits) {
130
- this.setTraits(traits);
131
- }
132
- }
133
- getUserId() {
134
- return this.userId;
135
- }
136
- setTraits(traits) {
137
- this.traits = { ...this.traits, ...traits };
138
- this.storage.set("user_traits", JSON.stringify(this.traits), 365);
139
- logger.debug("User traits updated:", this.traits);
140
- }
141
- getTraits() {
142
- return { ...this.traits };
143
- }
144
- reset() {
145
- this.userId = null;
146
- this.traits = {};
147
- this.anonymousId = generateUUID();
148
- this.storage.remove("user_id");
149
- this.storage.remove("user_traits");
150
- this.storage.set("anon_id", this.anonymousId, 365);
151
- logger.info("Identity reset, new anonymous ID:", this.anonymousId);
152
- }
153
- alias(newUserId) {
154
- const previousId = this.userId || this.anonymousId;
155
- this.setUserId(newUserId);
156
- logger.info("User aliased:", { previousId, newUserId });
157
- return { previousId, newUserId };
158
- }
159
- getState() {
160
- return {
161
- anonymousId: this.anonymousId,
162
- userId: this.userId,
163
- traits: this.getTraits()
164
- };
165
- }
166
- }
167
- const LINK_ATTRIBUTION_DURABLE_DAYS = 30;
168
- class SessionManager {
169
- constructor(storage, sessionTimeoutMinutes = 30) {
170
- this.eventCount = 0;
171
- this.checkInterval = null;
172
- this.activityThrottle = 1e3;
173
- this.lastActivityUpdate = 0;
174
- this.adClickIDs = {};
175
- this.linkAttribution = {};
176
- this.storage = storage;
177
- this.sessionTimeout = sessionTimeoutMinutes * 60 * 1e3;
178
- this.lastActivityTime = Date.now();
179
- this.sessionStartTime = Date.now();
180
- const session = this.loadSession();
181
- if (session) {
182
- this.sessionId = session.sessionId;
183
- this.sessionStartTime = session.startTime;
184
- this.lastActivityTime = session.lastActivityTime;
185
- this.eventCount = session.eventCount;
186
- this.adClickIDs = session.adClickIDs || {};
187
- this.landingPage = session.landingPage;
188
- this.linkAttribution = session.linkAttribution || {};
189
- logger.debug("Session loaded:", session);
190
- } else {
191
- this.sessionId = this.createNewSession();
192
- }
193
- this.startActivityTracking();
194
- this.startExpiryCheck();
195
- }
196
- loadSession() {
197
- const sessionData = this.storage.get("session");
198
- if (!sessionData) {
199
- return null;
200
- }
201
- try {
202
- const session = JSON.parse(sessionData);
203
- const elapsed = Date.now() - session.lastActivityTime;
204
- if (elapsed < this.sessionTimeout) {
205
- return session;
206
- } else {
207
- logger.debug("Session expired, creating new session");
208
- this.storage.remove("session");
209
- return null;
210
- }
211
- } catch (error) {
212
- logger.warn("Failed to parse session data:", error);
213
- this.storage.remove("session");
214
- return null;
215
- }
216
- }
217
- createNewSession() {
218
- const newSessionId = generateSessionId();
219
- this.sessionStartTime = Date.now();
220
- this.lastActivityTime = Date.now();
221
- this.eventCount = 0;
222
- this.sessionId = newSessionId;
223
- this.persistSession();
224
- logger.info("New session created:", newSessionId);
225
- return newSessionId;
226
- }
227
- persistSession() {
228
- const sessionData = {
229
- sessionId: this.sessionId,
230
- startTime: this.sessionStartTime,
231
- lastActivityTime: this.lastActivityTime,
232
- eventCount: this.eventCount,
233
- adClickIDs: this.adClickIDs,
234
- landingPage: this.landingPage,
235
- linkAttribution: this.linkAttribution
236
- };
237
- this.storage.set("session", JSON.stringify(sessionData));
238
- }
239
- startActivityTracking() {
240
- const events = ["click", "scroll", "keypress", "mousemove", "touchstart"];
241
- const activityHandler = () => this.onActivity();
242
- events.forEach((event) => {
243
- window.addEventListener(event, activityHandler, { passive: true });
244
- });
245
- window.addEventListener("visibilitychange", () => {
246
- if (document.visibilityState === "visible") {
247
- this.onActivity();
248
- }
249
- });
250
- }
251
- startExpiryCheck() {
252
- this.checkInterval = window.setInterval(() => {
253
- this.checkSessionExpiry();
254
- }, 6e4);
255
- }
256
- onActivity() {
257
- const now = Date.now();
258
- const timeSinceLastUpdate = now - this.lastActivityUpdate;
259
- if (timeSinceLastUpdate < this.activityThrottle) {
260
- return;
261
- }
262
- this.lastActivityUpdate = now;
263
- const timeSinceLastActivity = now - this.lastActivityTime;
264
- if (timeSinceLastActivity > this.sessionTimeout) {
265
- logger.info("Session expired due to inactivity, creating new session");
266
- this.sessionId = this.createNewSession();
267
- } else {
268
- this.lastActivityTime = now;
269
- this.persistSession();
270
- }
271
- }
272
- checkSessionExpiry() {
273
- const elapsed = Date.now() - this.lastActivityTime;
274
- if (elapsed > this.sessionTimeout) {
275
- logger.info("Session expired during check, creating new session");
276
- this.sessionId = this.createNewSession();
277
- }
278
- }
279
- getSessionId() {
280
- return this.sessionId;
281
- }
282
- incrementEventCount() {
283
- this.eventCount++;
284
- this.lastActivityTime = Date.now();
285
- this.persistSession();
286
- }
287
- getSessionDuration() {
288
- return Date.now() - this.sessionStartTime;
289
- }
290
- getEventCount() {
291
- return this.eventCount;
292
- }
293
- getState() {
294
- return {
295
- sessionId: this.sessionId,
296
- startTime: this.sessionStartTime,
297
- lastActivityTime: this.lastActivityTime,
298
- eventCount: this.eventCount,
299
- adClickIDs: this.adClickIDs,
300
- landingPage: this.landingPage,
301
- linkAttribution: this.linkAttribution
302
- };
303
- }
304
- setAdClickIDs(adClickIDs, landingPage) {
305
- if (Object.keys(adClickIDs).length > 0) {
306
- this.adClickIDs = { ...this.adClickIDs, ...adClickIDs };
307
- if (landingPage) {
308
- this.landingPage = landingPage;
309
- }
310
- this.persistSession();
311
- try {
312
- this.storage.set("ad_click_ids", JSON.stringify(this.adClickIDs), 365);
313
- if (this.landingPage) {
314
- this.storage.set("ad_landing_page", this.landingPage, 365);
315
- }
316
- } catch {
317
- }
318
- logger.info("Ad click IDs stored (session + durable):", this.adClickIDs);
319
- }
320
- }
321
- getAdClickIDs() {
322
- if (Object.keys(this.adClickIDs).length === 0) {
323
- try {
324
- const durable = this.storage.get("ad_click_ids");
325
- if (durable) {
326
- const parsed = JSON.parse(durable);
327
- if (parsed && Object.keys(parsed).length > 0) {
328
- this.adClickIDs = parsed;
329
- if (!this.landingPage) {
330
- this.landingPage = this.storage.get("ad_landing_page") || void 0;
331
- }
332
- }
333
- }
334
- } catch {
335
- }
336
- }
337
- return this.adClickIDs;
338
- }
339
- getLandingPage() {
340
- return this.landingPage;
341
- }
342
- /**
343
- * Record the landing's Aegis link attribution. LAST touch: a new `aegis_code`
344
- * replaces the previous set whole — never merged, because the fields are the
345
- * attributes of ONE link and a mix of two links describes neither.
346
- * Session + durable (30d, the server's short-link attribution window) so a
347
- * checkout in a later session can still name the send that brought them.
348
- */
349
- setLinkAttribution(attribution) {
350
- if (!attribution.aegis_code) return;
351
- this.linkAttribution = { ...attribution };
352
- this.persistSession();
353
- try {
354
- this.storage.set(
355
- "link_attribution",
356
- JSON.stringify(this.linkAttribution),
357
- LINK_ATTRIBUTION_DURABLE_DAYS
358
- );
359
- } catch {
360
- }
361
- }
362
- getLinkAttribution() {
363
- if (!this.linkAttribution.aegis_code) {
364
- try {
365
- const durable = this.storage.get("link_attribution");
366
- if (durable) {
367
- const parsed = JSON.parse(durable);
368
- if (parsed && parsed.aegis_code) {
369
- this.linkAttribution = parsed;
370
- }
371
- }
372
- } catch {
373
- }
374
- }
375
- return this.linkAttribution;
376
- }
377
- destroy() {
378
- if (this.checkInterval) {
379
- clearInterval(this.checkInterval);
380
- this.checkInterval = null;
381
- }
382
- }
383
- }
384
- const FBP_COOKIE = "_fbp";
385
- const FBC_COOKIE = "_fbc";
386
- const COOKIE_LIFETIME_DAYS = 90;
387
- function readCookie(name) {
388
- if (typeof document === "undefined") return null;
389
- try {
390
- const target = `${name}=`;
391
- const parts = document.cookie.split(";");
392
- for (let i = 0; i < parts.length; i++) {
393
- let c = parts[i];
394
- while (c.charAt(0) === " ") c = c.substring(1);
395
- if (c.indexOf(target) === 0) {
396
- return decodeURIComponent(c.substring(target.length));
397
- }
398
- }
399
- } catch {
400
- }
401
- return null;
402
- }
403
- function writeCookie(name, value, days) {
404
- if (typeof document === "undefined" || typeof window === "undefined") return;
405
- try {
406
- const expiry = new Date(Date.now() + days * 24 * 60 * 60 * 1e3).toUTCString();
407
- const domain = getRootDomain();
408
- const isHttps = window.location.protocol === "https:";
409
- let cookieStr = `${name}=${encodeURIComponent(value)};expires=${expiry};path=/;SameSite=Lax`;
410
- if (isHttps) cookieStr += ";Secure";
411
- if (domain) cookieStr += `;domain=${domain}`;
412
- document.cookie = cookieStr;
413
- } catch {
414
- }
415
- }
416
- function getRootDomain() {
417
- if (typeof window === "undefined") return null;
418
- const host = window.location.hostname;
419
- if (!host || host === "localhost") return null;
420
- if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) return null;
421
- const parts = host.split(".");
422
- if (parts.length < 2) return null;
423
- return "." + parts.slice(-2).join(".");
424
- }
425
- function subdomainIndex() {
426
- if (typeof window === "undefined") return 1;
427
- const host = window.location.hostname;
428
- if (!host || host === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(host)) return 1;
429
- const parts = host.split(".");
430
- return Math.max(1, parts.length - 1);
431
- }
432
- function randomToken() {
433
- const n = Math.floor(Math.random() * 1e10);
434
- return n.toString().padStart(10, "0");
435
- }
436
- function ensureFbp() {
437
- const existing = readCookie(FBP_COOKIE);
438
- if (existing) return existing;
439
- const fbp = `fb.${subdomainIndex()}.${Date.now()}.${randomToken()}`;
440
- writeCookie(FBP_COOKIE, fbp, COOKIE_LIFETIME_DAYS);
441
- return fbp;
442
- }
443
- function ensureFbc() {
444
- const existing = readCookie(FBC_COOKIE);
445
- if (existing) return existing;
446
- const fbclid = readFbclidFromUrl();
447
- if (!fbclid) return null;
448
- const fbc = `fb.${subdomainIndex()}.${Date.now()}.${fbclid}`;
449
- writeCookie(FBC_COOKIE, fbc, COOKIE_LIFETIME_DAYS);
450
- return fbc;
451
- }
452
- function readFbclidFromUrl() {
453
- if (typeof window === "undefined") return null;
454
- try {
455
- return new URLSearchParams(window.location.search).get("fbclid");
456
- } catch {
457
- return null;
458
- }
459
- }
460
- function captureMetaCookies() {
461
- return {
462
- fbp: ensureFbp(),
463
- fbc: ensureFbc(),
464
- fbclid: readFbclidFromUrl()
465
- };
466
- }
467
- function redactMetaCookies(event) {
468
- const props = event == null ? void 0 : event.properties;
469
- if (!props || props.fbc === void 0 && props.fbp === void 0) return event;
470
- const masked = { ...props };
471
- if (masked.fbc !== void 0) masked.fbc = "[redacted]";
472
- if (masked.fbp !== void 0) masked.fbp = "[redacted]";
473
- return { ...event, properties: masked };
474
- }
475
- class EventQueue {
476
- constructor(config, transport, storage) {
477
- this.buffer = [];
478
- this.flushInterval = null;
479
- this.isFlushing = false;
480
- this.offlineQueue = [];
481
- this.isOnline = navigator.onLine;
482
- this.config = config;
483
- this.transport = transport;
484
- this.storage = storage;
485
- if (config.enableOfflineMode) {
486
- this.loadOfflineQueue();
487
- this.setupOnlineListener();
488
- }
489
- this.startFlushTimer();
490
- this.setupUnloadHandlers();
491
- }
492
- loadOfflineQueue() {
493
- const offlineData = this.storage.get("offline_queue");
494
- if (!offlineData) {
495
- return;
496
- }
497
- try {
498
- const events = JSON.parse(offlineData);
499
- if (Array.isArray(events) && events.length > 0) {
500
- this.offlineQueue = events.slice(0, this.config.maxOfflineEvents);
501
- logger.info(`Loaded ${this.offlineQueue.length} offline events`);
502
- if (this.isOnline) {
503
- this.flushOfflineQueue();
504
- }
505
- }
506
- } catch (error) {
507
- logger.warn("Failed to load offline queue:", error);
508
- this.storage.remove("offline_queue");
509
- }
510
- }
511
- saveOfflineQueue() {
512
- if (this.offlineQueue.length === 0) {
513
- this.storage.remove("offline_queue");
514
- return;
515
- }
516
- try {
517
- const data = JSON.stringify(this.offlineQueue);
518
- this.storage.set("offline_queue", data);
519
- logger.debug(`Saved ${this.offlineQueue.length} events to offline queue`);
520
- } catch (error) {
521
- logger.warn("Failed to save offline queue:", error);
522
- }
523
- }
524
- setupOnlineListener() {
525
- window.addEventListener("online", () => {
526
- logger.info("Connection restored");
527
- this.isOnline = true;
528
- this.flushOfflineQueue();
529
- this.flush();
530
- });
531
- window.addEventListener("offline", () => {
532
- logger.warn("Connection lost, entering offline mode");
533
- this.isOnline = false;
534
- });
535
- }
536
- async flushOfflineQueue() {
537
- if (this.offlineQueue.length === 0) {
538
- return;
539
- }
540
- logger.info(`Flushing ${this.offlineQueue.length} offline events`);
541
- const events = [...this.offlineQueue];
542
- this.offlineQueue = [];
543
- this.storage.remove("offline_queue");
544
- const result = await this.transport.send(events);
545
- if (!result.success) {
546
- logger.warn("Failed to send offline events, re-queueing");
547
- this.offlineQueue = [...events, ...this.offlineQueue].slice(
548
- 0,
549
- this.config.maxOfflineEvents
550
- );
551
- this.saveOfflineQueue();
552
- } else {
553
- logger.info("Offline events sent successfully");
554
- }
555
- }
556
- push(event) {
557
- if (!this.isOnline && this.config.enableOfflineMode) {
558
- this.addToOfflineQueue(event);
559
- return;
560
- }
561
- this.buffer.push(event);
562
- logger.debug("Event queued:", event.type, redactMetaCookies(event));
563
- if (this.buffer.length >= this.config.batchSize) {
564
- this.flush();
565
- }
566
- }
567
- addToOfflineQueue(event) {
568
- if (this.offlineQueue.length >= this.config.maxOfflineEvents) {
569
- logger.warn("Offline queue full, dropping oldest event");
570
- this.offlineQueue.shift();
571
- }
572
- this.offlineQueue.push(event);
573
- this.saveOfflineQueue();
574
- logger.debug("Event added to offline queue");
575
- }
576
- async flush() {
577
- if (this.isFlushing) {
578
- logger.debug("Flush already in progress, skipping");
579
- return;
580
- }
581
- if (this.buffer.length === 0) {
582
- return;
583
- }
584
- this.isFlushing = true;
585
- const events = [...this.buffer];
586
- this.buffer = [];
587
- logger.info(`Flushing ${events.length} events`);
588
- try {
589
- const result = await this.transport.send(events);
590
- if (!result.success) {
591
- logger.error("Failed to send events:", result.error);
592
- if (this.config.enableOfflineMode) {
593
- this.offlineQueue = [...this.offlineQueue, ...events].slice(
594
- 0,
595
- this.config.maxOfflineEvents
596
- );
597
- this.saveOfflineQueue();
598
- logger.info("Events saved to offline queue");
599
- } else {
600
- this.buffer.unshift(...events);
601
- logger.warn("Events re-queued for retry");
602
- }
603
- } else {
604
- logger.info("Events sent successfully");
605
- }
606
- } catch (error) {
607
- logger.error("Flush error:", error);
608
- if (this.config.enableOfflineMode) {
609
- this.offlineQueue = [...this.offlineQueue, ...events].slice(
610
- 0,
611
- this.config.maxOfflineEvents
612
- );
613
- this.saveOfflineQueue();
614
- }
615
- } finally {
616
- this.isFlushing = false;
617
- }
618
- }
619
- startFlushTimer() {
620
- if (this.flushInterval) {
621
- clearInterval(this.flushInterval);
622
- }
623
- this.flushInterval = window.setInterval(() => {
624
- this.flush();
625
- }, this.config.batchInterval);
626
- logger.debug(
627
- `Flush timer started with interval: ${this.config.batchInterval}ms`
628
- );
629
- }
630
- setupUnloadHandlers() {
631
- const flushBeforeUnload = () => {
632
- if (this.buffer.length > 0 || this.offlineQueue.length > 0) {
633
- this.persistBufferToOfflineQueue();
634
- this.flush();
635
- }
636
- };
637
- document.addEventListener("visibilitychange", () => {
638
- if (document.visibilityState === "hidden") {
639
- flushBeforeUnload();
640
- }
641
- });
642
- window.addEventListener("beforeunload", flushBeforeUnload);
643
- window.addEventListener("pagehide", flushBeforeUnload);
644
- }
645
- persistBufferToOfflineQueue() {
646
- if (this.buffer.length === 0) {
647
- return;
648
- }
649
- if (this.config.enableOfflineMode) {
650
- this.offlineQueue = [...this.offlineQueue, ...this.buffer].slice(
651
- 0,
652
- this.config.maxOfflineEvents
653
- );
654
- this.saveOfflineQueue();
655
- logger.debug("Buffer persisted to offline queue before unload");
656
- }
657
- }
658
- getQueueSize() {
659
- return this.buffer.length;
660
- }
661
- getOfflineQueueSize() {
662
- return this.offlineQueue.length;
663
- }
664
- clear() {
665
- this.buffer = [];
666
- this.offlineQueue = [];
667
- this.storage.remove("offline_queue");
668
- logger.info("Queue cleared");
669
- }
670
- destroy() {
671
- if (this.flushInterval) {
672
- clearInterval(this.flushInterval);
673
- this.flushInterval = null;
674
- }
675
- this.flush();
676
- }
677
- }
678
- class Transport {
679
- constructor(config, storage) {
680
- this.activeCell = null;
681
- this.healthCheckInterval = null;
682
- this.regionLatencyMap = /* @__PURE__ */ new Map();
683
- this.config = config;
684
- this.storage = storage;
685
- if (config.cellEndpoints.length > 0) {
686
- this.initializeCellularArchitecture();
687
- }
688
- }
689
- async initializeCellularArchitecture() {
690
- const cachedCell = this.loadCachedCell();
691
- if (cachedCell && this.isCellHealthy(cachedCell)) {
692
- this.activeCell = cachedCell;
693
- logger.info("Using cached cell:", cachedCell);
694
- } else {
695
- await this.selectOptimalCell();
696
- }
697
- this.startHealthChecks();
698
- }
699
- loadCachedCell() {
700
- const cached = this.storage.get("active_cell");
701
- if (!cached) {
702
- return null;
703
- }
704
- try {
705
- return JSON.parse(cached);
706
- } catch (error) {
707
- logger.warn("Failed to parse cached cell:", error);
708
- return null;
709
- }
710
- }
711
- saveCachedCell(cell) {
712
- this.storage.set("active_cell", JSON.stringify(cell), 7);
713
- }
714
- isCellHealthy(cell) {
715
- return cell.healthy && this.config.cellEndpoints.some(
716
- (c) => c.region === cell.region && c.url === cell.url
717
- );
718
- }
719
- async selectOptimalCell() {
720
- logger.info("Selecting optimal cell...");
721
- if (this.config.preferredRegion) {
722
- const preferredCell = this.config.cellEndpoints.find(
723
- (cell) => cell.region === this.config.preferredRegion && cell.healthy
724
- );
725
- if (preferredCell) {
726
- this.activeCell = preferredCell;
727
- this.saveCachedCell(preferredCell);
728
- logger.info("Using preferred region cell:", preferredCell);
729
- return;
730
- }
731
- }
732
- if (this.config.autoRegionDetection) {
733
- await this.detectOptimalRegion();
734
- } else {
735
- this.selectCellByPriority();
736
- }
737
- }
738
- async detectOptimalRegion() {
739
- const healthyCells = this.config.cellEndpoints.filter((cell) => cell.healthy);
740
- if (healthyCells.length === 0) {
741
- logger.error("No healthy cells available");
742
- return;
743
- }
744
- const latencyPromises = healthyCells.map(async (cell) => {
745
- const latency = await this.measureLatency(cell);
746
- this.regionLatencyMap.set(cell.region, latency);
747
- return { cell, latency };
748
- });
749
- const results = await Promise.all(latencyPromises);
750
- results.sort((a, b) => a.latency - b.latency);
751
- const optimalCell = results[0].cell;
752
- this.activeCell = optimalCell;
753
- this.saveCachedCell(optimalCell);
754
- logger.info("Optimal cell selected:", {
755
- region: optimalCell.region,
756
- latency: results[0].latency
757
- });
758
- }
759
- async measureLatency(cell) {
760
- const startTime = performance.now();
761
- try {
762
- const controller = new AbortController();
763
- const timeoutId = setTimeout(() => controller.abort(), 2e3);
764
- const response = await fetch(`${cell.url}/health`, {
765
- method: "GET",
766
- signal: controller.signal
767
- });
768
- clearTimeout(timeoutId);
769
- if (response.ok) {
770
- return performance.now() - startTime;
771
- } else {
772
- return Infinity;
773
- }
774
- } catch (error) {
775
- logger.warn(`Health check failed for ${cell.region}:`, error);
776
- return Infinity;
777
- }
778
- }
779
- selectCellByPriority() {
780
- const sortedCells = [...this.config.cellEndpoints].filter((cell) => cell.healthy).sort((a, b) => a.priority - b.priority);
781
- if (sortedCells.length > 0) {
782
- this.activeCell = sortedCells[0];
783
- this.saveCachedCell(sortedCells[0]);
784
- logger.info("Cell selected by priority:", sortedCells[0]);
785
- } else {
786
- logger.error("No healthy cells available");
787
- }
788
- }
789
- startHealthChecks() {
790
- this.healthCheckInterval = window.setInterval(() => {
791
- this.performHealthChecks();
792
- }, 6e4);
793
- }
794
- async performHealthChecks() {
795
- const healthPromises = this.config.cellEndpoints.map(async (cell) => {
796
- const isHealthy = await this.checkCellHealth(cell);
797
- cell.healthy = isHealthy;
798
- return { cell, isHealthy };
799
- });
800
- const results = await Promise.all(healthPromises);
801
- if (this.activeCell && !this.activeCell.healthy) {
802
- logger.warn("Active cell unhealthy, selecting new cell");
803
- await this.selectOptimalCell();
804
- }
805
- logger.debug("Health check results:", results);
806
- }
807
- async checkCellHealth(cell) {
808
- try {
809
- const controller = new AbortController();
810
- const timeoutId = setTimeout(() => controller.abort(), 3e3);
811
- const response = await fetch(`${cell.url}/health`, {
812
- method: "GET",
813
- signal: controller.signal
814
- });
815
- clearTimeout(timeoutId);
816
- return response.ok;
817
- } catch (error) {
818
- return false;
819
- }
820
- }
821
- async send(events) {
822
- if (events.length === 0) {
823
- return { success: true };
824
- }
825
- const endpoint = this.getActiveEndpoint();
826
- const payload = this.buildPayload(events);
827
- logger.debug("Sending batch:", { endpoint, eventCount: events.length });
828
- if (document.visibilityState === "hidden" && typeof navigator.sendBeacon === "function") {
829
- const result = this.sendViaBeacon(endpoint, payload);
830
- return Promise.resolve(result);
831
- } else {
832
- return this.sendViaFetch(endpoint, payload);
833
- }
834
- }
835
- getActiveEndpoint() {
836
- if (this.activeCell) {
837
- return `${this.activeCell.url}/v1/batch`;
838
- }
839
- if (this.config.apiHost) {
840
- return `${this.config.apiHost}/v1/batch`;
841
- }
842
- throw new Error("No active endpoint available");
843
- }
844
- buildPayload(events) {
845
- return {
846
- batch: events,
847
- sentAt: (/* @__PURE__ */ new Date()).toISOString(),
848
- writeKey: this.config.writeKey,
849
- context: this.activeCell ? {
850
- cell: {
851
- region: this.activeCell.region,
852
- endpoint: this.activeCell.url
853
- }
854
- } : void 0
855
- };
856
- }
857
- sendViaBeacon(endpoint, payload) {
858
- try {
859
- const blob = new Blob([JSON.stringify(payload)], {
860
- type: "application/json"
861
- });
862
- const sent = navigator.sendBeacon(endpoint, blob);
863
- if (sent) {
864
- logger.debug("Batch sent via Beacon");
865
- return { success: true, endpoint };
866
- } else {
867
- logger.warn("Beacon send failed, falling back to fetch");
868
- return this.sendViaFetch(endpoint, payload);
869
- }
870
- } catch (error) {
871
- logger.error("Beacon send error:", error);
872
- return {
873
- success: false,
874
- error,
875
- endpoint
876
- };
877
- }
878
- }
879
- async sendViaFetch(endpoint, payload, attempt = 1) {
880
- try {
881
- const controller = new AbortController();
882
- const timeoutId = setTimeout(
883
- () => controller.abort(),
884
- this.config.requestTimeout
885
- );
886
- const response = await fetch(endpoint, {
887
- method: "POST",
888
- headers: {
889
- "Content-Type": "application/json",
890
- Authorization: `Bearer ${this.config.writeKey}`
891
- },
892
- body: JSON.stringify(payload),
893
- keepalive: true,
894
- signal: controller.signal
895
- });
896
- clearTimeout(timeoutId);
897
- if (response.ok) {
898
- const result = await response.json();
899
- logger.debug("Batch sent successfully:", result);
900
- return { success: true, response: result, endpoint };
901
- } else {
902
- const errorText = await response.text();
903
- const error = new Error(
904
- `HTTP ${response.status}: ${errorText || response.statusText}`
905
- );
906
- logger.error("Batch send failed:", error);
907
- if (this.shouldRetry(response.status, attempt)) {
908
- return this.retryRequest(endpoint, payload, attempt);
909
- }
910
- return { success: false, error, endpoint };
911
- }
912
- } catch (error) {
913
- logger.error("Fetch error:", error);
914
- if (this.shouldRetry(0, attempt)) {
915
- return this.retryRequest(endpoint, payload, attempt);
916
- }
917
- return {
918
- success: false,
919
- error,
920
- endpoint
921
- };
922
- }
923
- }
924
- shouldRetry(statusCode, attempt) {
925
- if (!this.config.retryFailedRequests) {
926
- return false;
927
- }
928
- if (attempt >= this.config.maxRetries) {
929
- return false;
930
- }
931
- if (statusCode === 0) {
932
- return true;
933
- }
934
- return statusCode >= 500 || statusCode === 429;
935
- }
936
- async retryRequest(endpoint, payload, attempt) {
937
- const delay = Math.pow(this.config.retryBackoffMultiplier, attempt) * 1e3;
938
- logger.info(`Retrying request in ${delay}ms (attempt ${attempt + 1})`);
939
- await new Promise((resolve) => setTimeout(resolve, delay));
940
- return this.sendViaFetch(endpoint, payload, attempt + 1);
941
- }
942
- getActiveCell() {
943
- return this.activeCell;
944
- }
945
- getRegionLatencies() {
946
- return new Map(this.regionLatencyMap);
947
- }
948
- destroy() {
949
- if (this.healthCheckInterval) {
950
- clearInterval(this.healthCheckInterval);
951
- this.healthCheckInterval = null;
952
- }
953
- }
954
- }
955
- const STORAGE_PREFIX = "aegis_";
956
- class Storage {
957
- constructor(options = {}) {
958
- this.options = options;
959
- this.useLocalStorage = this.isLocalStorageAvailable();
960
- }
961
- isLocalStorageAvailable() {
962
- try {
963
- const test = "__aegis_test__";
964
- localStorage.setItem(test, test);
965
- localStorage.removeItem(test);
966
- return true;
967
- } catch {
968
- return false;
969
- }
970
- }
971
- set(key, value, expiryDays = 365) {
972
- const fullKey = STORAGE_PREFIX + key;
973
- if (this.useLocalStorage) {
974
- try {
975
- const item = {
976
- value,
977
- expiry: Date.now() + expiryDays * 24 * 60 * 60 * 1e3
978
- };
979
- localStorage.setItem(fullKey, JSON.stringify(item));
980
- } catch (error) {
981
- console.warn("[Active Reach Storage] localStorage.setItem failed:", error);
982
- }
983
- }
984
- this.setCookie(fullKey, value, expiryDays);
985
- }
986
- get(key) {
987
- const fullKey = STORAGE_PREFIX + key;
988
- if (this.useLocalStorage) {
989
- try {
990
- const itemStr = localStorage.getItem(fullKey);
991
- if (itemStr) {
992
- const item = JSON.parse(itemStr);
993
- if (Date.now() < item.expiry) {
994
- return item.value;
995
- } else {
996
- localStorage.removeItem(fullKey);
997
- }
998
- }
999
- } catch (error) {
1000
- console.warn("[Active Reach Storage] localStorage.getItem failed:", error);
1001
- }
1002
- }
1003
- return this.getCookie(fullKey);
1004
- }
1005
- remove(key) {
1006
- const fullKey = STORAGE_PREFIX + key;
1007
- if (this.useLocalStorage) {
1008
- try {
1009
- localStorage.removeItem(fullKey);
1010
- } catch (error) {
1011
- console.warn("[Active Reach Storage] localStorage.removeItem failed:", error);
1012
- }
1013
- }
1014
- this.deleteCookie(fullKey);
1015
- }
1016
- clear() {
1017
- if (this.useLocalStorage) {
1018
- try {
1019
- const keys = Object.keys(localStorage);
1020
- keys.forEach((key) => {
1021
- if (key.startsWith(STORAGE_PREFIX)) {
1022
- localStorage.removeItem(key);
1023
- }
1024
- });
1025
- } catch (error) {
1026
- console.warn("[Active Reach Storage] localStorage.clear failed:", error);
1027
- }
1028
- }
1029
- const cookies = document.cookie.split(";");
1030
- cookies.forEach((cookie) => {
1031
- const key = cookie.split("=")[0].trim();
1032
- if (key.startsWith(STORAGE_PREFIX)) {
1033
- this.deleteCookie(key);
1034
- }
1035
- });
1036
- }
1037
- setCookie(name, value, days) {
1038
- try {
1039
- const date = /* @__PURE__ */ new Date();
1040
- date.setTime(date.getTime() + days * 24 * 60 * 60 * 1e3);
1041
- const expires = `expires=${date.toUTCString()}`;
1042
- let cookieStr = `${name}=${value};${expires};path=/`;
1043
- if (this.options.secureCookie && window.location.protocol === "https:") {
1044
- cookieStr += ";Secure";
1045
- }
1046
- cookieStr += ";SameSite=Lax";
1047
- if (this.options.cookieDomain) {
1048
- cookieStr += `;domain=${this.options.cookieDomain}`;
1049
- } else if (this.options.crossDomain) {
1050
- const domain = this.getRootDomain();
1051
- if (domain) {
1052
- cookieStr += `;domain=${domain}`;
1053
- }
1054
- }
1055
- document.cookie = cookieStr;
1056
- } catch (error) {
1057
- console.warn("[Active Reach Storage] setCookie failed:", error);
1058
- }
1059
- }
1060
- getCookie(name) {
1061
- try {
1062
- const nameEQ = name + "=";
1063
- const ca = document.cookie.split(";");
1064
- for (let i = 0; i < ca.length; i++) {
1065
- let c = ca[i];
1066
- while (c.charAt(0) === " ") {
1067
- c = c.substring(1, c.length);
1068
- }
1069
- if (c.indexOf(nameEQ) === 0) {
1070
- return c.substring(nameEQ.length, c.length);
1071
- }
1072
- }
1073
- } catch (error) {
1074
- console.warn("[Active Reach Storage] getCookie failed:", error);
1075
- }
1076
- return null;
1077
- }
1078
- deleteCookie(name) {
1079
- try {
1080
- let cookieStr = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/`;
1081
- if (this.options.cookieDomain) {
1082
- cookieStr += `;domain=${this.options.cookieDomain}`;
1083
- } else if (this.options.crossDomain) {
1084
- const domain = this.getRootDomain();
1085
- if (domain) {
1086
- cookieStr += `;domain=${domain}`;
1087
- }
1088
- }
1089
- document.cookie = cookieStr;
1090
- } catch (error) {
1091
- console.warn("[Active Reach Storage] deleteCookie failed:", error);
1092
- }
1093
- }
1094
- getRootDomain() {
1095
- try {
1096
- const hostname = window.location.hostname;
1097
- if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
1098
- return null;
1099
- }
1100
- if (hostname === "localhost") {
1101
- return null;
1102
- }
1103
- const parts = hostname.split(".");
1104
- if (parts.length <= 2) {
1105
- return `.${hostname}`;
1106
- }
1107
- return `.${parts.slice(-2).join(".")}`;
1108
- } catch (error) {
1109
- console.warn("[Active Reach Storage] getRootDomain failed:", error);
1110
- return null;
1111
- }
1112
- }
1113
- }
1114
- const CREDENTIAL_URL_PARAMS = [
1115
- "t",
1116
- "k",
1117
- "code",
1118
- "session_code",
1119
- "aegis_pair",
1120
- "u",
1121
- // The Shopify CarrierService callback token (2026-09-18). Server-to-server
1122
- // today, so the SDK never sees one — it is here because the two lists are
1123
- // ONE rule, and a list that is only correct for today's callers is not.
1124
- "carrier_token"
1125
- ];
1126
- const REDACTED = "REDACTED";
1127
- const CREDENTIAL_PARAM_PATTERN = new RegExp(
1128
- `([?&])(${CREDENTIAL_URL_PARAMS.join("|")})=([^&#\\s"'<>]+)`,
1129
- "g"
1130
- );
1131
- function redactUrl(value) {
1132
- if (typeof value !== "string" || value.length === 0) return value;
1133
- try {
1134
- return value.replace(CREDENTIAL_PARAM_PATTERN, `$1$2=${REDACTED}`);
1135
- } catch {
1136
- return value;
1137
- }
1138
- }
1139
- function parseUTMParameters(url) {
1140
- try {
1141
- const searchParams = url ? new URL(url).searchParams : new URLSearchParams(window.location.search);
1142
- const utm = {};
1143
- const utmKeys = [
1144
- "source",
1145
- "medium",
1146
- "campaign",
1147
- "term",
1148
- "content",
1149
- "name"
1150
- ];
1151
- utmKeys.forEach((key) => {
1152
- const value = searchParams.get(`utm_${key}`);
1153
- if (value) {
1154
- utm[key] = value;
1155
- }
1156
- });
1157
- return utm;
1158
- } catch (error) {
1159
- console.warn("[Active Reach URL Parser] parseUTMParameters failed:", error);
1160
- return {};
1161
- }
1162
- }
1163
- function parseAdClickIDs(url) {
1164
- try {
1165
- const searchParams = url ? new URL(url).searchParams : new URLSearchParams(window.location.search);
1166
- const adClickIDs = {};
1167
- const clickIDKeys = [
1168
- "gclid",
1169
- "gbraid",
1170
- "wbraid",
1171
- "fbclid",
1172
- "msclkid",
1173
- "ctwa_clid",
1174
- "ttclid",
1175
- "li_fat_id"
1176
- ];
1177
- clickIDKeys.forEach((key) => {
1178
- const value = searchParams.get(key);
1179
- if (value) {
1180
- adClickIDs[key] = value;
1181
- }
1182
- });
1183
- return adClickIDs;
1184
- } catch (error) {
1185
- console.warn("[Active Reach URL Parser] parseAdClickIDs failed:", error);
1186
- return {};
1187
- }
1188
- }
1189
- function hasAdClickIDs(adClickIDs) {
1190
- return Object.keys(adClickIDs).length > 0;
1191
- }
1192
- const LINK_ATTRIBUTION_KEYS = [
1193
- "aegis_code",
1194
- "aegis_campaign",
1195
- "aegis_journey",
1196
- "aegis_playbook",
1197
- "aegis_button"
1198
- ];
1199
- function parseLinkAttribution(url) {
1200
- try {
1201
- const searchParams = url ? new URL(url).searchParams : new URLSearchParams(window.location.search);
1202
- if (!searchParams.get("aegis_code")) return {};
1203
- const attribution = {};
1204
- LINK_ATTRIBUTION_KEYS.forEach((key) => {
1205
- const value = searchParams.get(key);
1206
- if (value) {
1207
- attribution[key] = value;
1208
- }
1209
- });
1210
- return attribution;
1211
- } catch (error) {
1212
- console.warn("[Active Reach URL Parser] parseLinkAttribution failed:", error);
1213
- return {};
1214
- }
1215
- }
1216
- function buildContext(config, session) {
1217
- const utm = config.capture_utm ? parseUTMParameters() : void 0;
1218
- const device = detectDevice();
1219
- const os = detectOS();
1220
- const browser = detectBrowser();
1221
- const network = detectNetworkInfo();
1222
- const adClickIDs = session == null ? void 0 : session.getAdClickIDs();
1223
- const storedLandingPage = session == null ? void 0 : session.getLandingPage();
1224
- const landingPage = storedLandingPage ? redactUrl(storedLandingPage) : void 0;
1225
- return {
1226
- library: {
1227
- name: "@active-reach/web-sdk",
1228
- version: "1.0.0"
1229
- },
1230
- // Credential params (`k`, `t`, `code`, ...) keep their key and lose their
1231
- // value here, where the URL enters the page object. The referrer is
1232
- // redacted too: the previous page can be our own order/landing link.
1233
- page: {
1234
- path: window.location.pathname,
1235
- referrer: config.capture_referrer ? redactUrl(document.referrer) : "",
1236
- search: redactUrl(window.location.search),
1237
- title: document.title,
1238
- url: redactUrl(window.location.href),
1239
- hash: window.location.hash
1240
- },
1241
- userAgent: navigator.userAgent,
1242
- locale: navigator.language,
1243
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1244
- screen: {
1245
- width: window.screen.width,
1246
- height: window.screen.height,
1247
- density: window.devicePixelRatio || 1
1248
- },
1249
- viewport: {
1250
- width: window.innerWidth,
1251
- height: window.innerHeight
1252
- },
1253
- ...utm && Object.keys(utm).length > 0 && { campaign: utm },
1254
- ...adClickIDs && Object.keys(adClickIDs).length > 0 && { adClickIDs },
1255
- ...landingPage && { landingPage },
1256
- ...network && { network },
1257
- ...device && { device },
1258
- ...os && { os },
1259
- ...browser && { browser },
1260
- ...config.active_cell && {
1261
- cell: {
1262
- region: config.active_cell.region,
1263
- endpoint: config.active_cell.url
1264
- }
1265
- }
1266
- };
1267
- }
1268
- function detectDevice() {
1269
- const ua = navigator.userAgent;
1270
- if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) {
1271
- return { type: "tablet" };
1272
- }
1273
- if (/Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(ua)) {
1274
- return { type: "mobile" };
1275
- }
1276
- return { type: "desktop" };
1277
- }
1278
- function detectOS() {
1279
- const ua = navigator.userAgent;
1280
- if (/Windows NT 10/i.test(ua)) return { name: "Windows", version: "10" };
1281
- if (/Windows NT 6.3/i.test(ua)) return { name: "Windows", version: "8.1" };
1282
- if (/Windows NT 6.2/i.test(ua)) return { name: "Windows", version: "8" };
1283
- if (/Windows NT 6.1/i.test(ua)) return { name: "Windows", version: "7" };
1284
- if (/Windows/i.test(ua)) return { name: "Windows", version: "Unknown" };
1285
- if (/Mac OS X (\d+)[._](\d+)/.test(ua)) {
1286
- const match = ua.match(/Mac OS X (\d+)[._](\d+)/);
1287
- return { name: "macOS", version: `${match[1]}.${match[2]}` };
1288
- }
1289
- if (/Mac/i.test(ua)) return { name: "macOS", version: "Unknown" };
1290
- if (/Android (\d+\.\d+)/.test(ua)) {
1291
- const match = ua.match(/Android (\d+\.\d+)/);
1292
- return { name: "Android", version: match[1] };
1293
- }
1294
- if (/Android/i.test(ua)) return { name: "Android", version: "Unknown" };
1295
- if (/iPhone OS (\d+)_(\d+)/.test(ua)) {
1296
- const match = ua.match(/iPhone OS (\d+)_(\d+)/);
1297
- return { name: "iOS", version: `${match[1]}.${match[2]}` };
1298
- }
1299
- if (/iPad.*OS (\d+)_(\d+)/.test(ua)) {
1300
- const match = ua.match(/iPad.*OS (\d+)_(\d+)/);
1301
- return { name: "iOS", version: `${match[1]}.${match[2]}` };
1302
- }
1303
- if (/iPhone|iPad/i.test(ua)) return { name: "iOS", version: "Unknown" };
1304
- if (/Linux/i.test(ua)) return { name: "Linux", version: "Unknown" };
1305
- return { name: "Unknown", version: "Unknown" };
1306
- }
1307
- function detectBrowser() {
1308
- const ua = navigator.userAgent;
1309
- if (/Edg\/(\d+\.\d+)/.test(ua)) {
1310
- const match = ua.match(/Edg\/(\d+\.\d+)/);
1311
- return { name: "Edge", version: match[1] };
1312
- }
1313
- if (/Chrome\/(\d+\.\d+)/.test(ua) && !/Edg/.test(ua)) {
1314
- const match = ua.match(/Chrome\/(\d+\.\d+)/);
1315
- return { name: "Chrome", version: match[1] };
1316
- }
1317
- if (/Firefox\/(\d+\.\d+)/.test(ua)) {
1318
- const match = ua.match(/Firefox\/(\d+\.\d+)/);
1319
- return { name: "Firefox", version: match[1] };
1320
- }
1321
- if (/Safari\/(\d+\.\d+)/.test(ua) && !/Chrome/.test(ua)) {
1322
- const match = ua.match(/Version\/(\d+\.\d+)/);
1323
- return { name: "Safari", version: match ? match[1] : "Unknown" };
1324
- }
1325
- if (/MSIE (\d+\.\d+)/.test(ua) || /Trident\//.test(ua)) {
1326
- const match = ua.match(/MSIE (\d+\.\d+)/) || ua.match(/rv:(\d+\.\d+)/);
1327
- return { name: "Internet Explorer", version: match ? match[1] : "Unknown" };
1328
- }
1329
- return { name: "Unknown", version: "Unknown" };
1330
- }
1331
- function detectNetworkInfo() {
1332
- try {
1333
- const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
1334
- if (connection) {
1335
- return {
1336
- effectiveType: connection.effectiveType,
1337
- downlink: connection.downlink,
1338
- rtt: connection.rtt
1339
- };
1340
- }
1341
- } catch (error) {
1342
- console.warn("[Active Reach Device] detectNetworkInfo failed:", error);
1343
- }
1344
- return void 0;
1345
- }
1346
- function isBot() {
1347
- const ua = navigator.userAgent.toLowerCase();
1348
- const botPatterns = [
1349
- "bot",
1350
- "crawl",
1351
- "spider",
1352
- "slurp",
1353
- "mediapartners",
1354
- "googlebot",
1355
- "bingbot",
1356
- "yahoo",
1357
- "duckduckbot",
1358
- "baiduspider",
1359
- "yandex",
1360
- "facebookexternalhit",
1361
- "linkedinbot",
1362
- "twitterbot",
1363
- "slackbot",
1364
- "telegrambot",
1365
- "whatsapp",
1366
- "pingdom",
1367
- "uptimerobot"
1368
- ];
1369
- return botPatterns.some((pattern) => ua.includes(pattern));
1370
- }
1371
- class PluginRegistry {
1372
- constructor() {
1373
- this.plugins = /* @__PURE__ */ new Map();
1374
- }
1375
- register(plugin) {
1376
- if (this.plugins.has(plugin.name)) {
1377
- logger.warn(`Plugin "${plugin.name}" already registered, skipping`);
1378
- return;
1379
- }
1380
- this.plugins.set(plugin.name, plugin);
1381
- logger.info(`Plugin registered: ${plugin.name} v${plugin.version}`);
1382
- }
1383
- unregister(pluginName) {
1384
- const plugin = this.plugins.get(pluginName);
1385
- if (!plugin) {
1386
- logger.warn(`Plugin "${pluginName}" not found`);
1387
- return;
1388
- }
1389
- if (plugin.destroy) {
1390
- try {
1391
- plugin.destroy();
1392
- } catch (error) {
1393
- logger.error(`Error destroying plugin "${pluginName}":`, error);
1394
- }
1395
- }
1396
- this.plugins.delete(pluginName);
1397
- logger.info(`Plugin unregistered: ${pluginName}`);
1398
- }
1399
- get(pluginName) {
1400
- return this.plugins.get(pluginName);
1401
- }
1402
- getAll() {
1403
- return Array.from(this.plugins.values());
1404
- }
1405
- async executeHook(hookName, ...args) {
1406
- for (const plugin of this.plugins.values()) {
1407
- const hook = plugin[hookName];
1408
- if (typeof hook === "function") {
1409
- try {
1410
- const result = await hook.apply(plugin, args);
1411
- if (result !== void 0) {
1412
- return result;
1413
- }
1414
- } catch (error) {
1415
- logger.error(
1416
- `Error executing ${String(hookName)} in plugin "${plugin.name}":`,
1417
- error
1418
- );
1419
- if (plugin.onError) {
1420
- try {
1421
- plugin.onError(error, { hookName, args });
1422
- } catch (onErrorError) {
1423
- logger.error(
1424
- `Error in onError handler for plugin "${plugin.name}":`,
1425
- onErrorError
1426
- );
1427
- }
1428
- }
1429
- }
1430
- }
1431
- }
1432
- return void 0;
1433
- }
1434
- async executeHookChain(hookName, initialValue, ...additionalArgs) {
1435
- let value = initialValue;
1436
- for (const plugin of this.plugins.values()) {
1437
- const hook = plugin[hookName];
1438
- if (typeof hook === "function") {
1439
- try {
1440
- const result = await hook.apply(plugin, [
1441
- value,
1442
- ...additionalArgs
1443
- ]);
1444
- if (result !== void 0 && result !== null) {
1445
- value = result;
1446
- }
1447
- } catch (error) {
1448
- logger.error(
1449
- `Error executing ${String(hookName)} in plugin "${plugin.name}":`,
1450
- error
1451
- );
1452
- if (plugin.onError) {
1453
- try {
1454
- plugin.onError(error, { hookName, value, additionalArgs });
1455
- } catch (onErrorError) {
1456
- logger.error(
1457
- `Error in onError handler for plugin "${plugin.name}":`,
1458
- onErrorError
1459
- );
1460
- }
1461
- }
1462
- }
1463
- }
1464
- }
1465
- return value;
1466
- }
1467
- async executeHookParallel(hookName, ...args) {
1468
- const promises = [];
1469
- for (const plugin of this.plugins.values()) {
1470
- const hook = plugin[hookName];
1471
- if (typeof hook === "function") {
1472
- const promise = (async () => {
1473
- try {
1474
- return await hook.apply(plugin, args);
1475
- } catch (error) {
1476
- logger.error(
1477
- `Error executing ${String(hookName)} in plugin "${plugin.name}":`,
1478
- error
1479
- );
1480
- if (plugin.onError) {
1481
- try {
1482
- plugin.onError(error, { hookName, args });
1483
- } catch (onErrorError) {
1484
- logger.error(
1485
- `Error in onError handler for plugin "${plugin.name}":`,
1486
- onErrorError
1487
- );
1488
- }
1489
- }
1490
- return void 0;
1491
- }
1492
- })();
1493
- promises.push(promise);
1494
- }
1495
- }
1496
- const results = await Promise.all(promises);
1497
- return results.filter((r) => r !== void 0);
1498
- }
1499
- clear() {
1500
- for (const plugin of this.plugins.values()) {
1501
- if (plugin.destroy) {
1502
- try {
1503
- plugin.destroy();
1504
- } catch (error) {
1505
- logger.error(`Error destroying plugin "${plugin.name}":`, error);
1506
- }
1507
- }
1508
- }
1509
- this.plugins.clear();
1510
- logger.info("All plugins cleared");
1511
- }
1512
- }
1513
- class ConsentManager {
1514
- constructor(storage, config = {}) {
1515
- this.listeners = [];
1516
- this.storage = storage;
1517
- this.config = {
1518
- defaultConsent: config.defaultConsent || {},
1519
- waitForConsent: config.waitForConsent ?? false,
1520
- consentStorageKey: config.consentStorageKey || "aegis_consent"
1521
- };
1522
- this.preferences = this.loadPreferences();
1523
- }
1524
- loadPreferences() {
1525
- const stored = this.storage.get(this.config.consentStorageKey);
1526
- if (stored) {
1527
- try {
1528
- const parsed = JSON.parse(stored);
1529
- logger.info("Loaded consent preferences:", parsed);
1530
- return this.mergeWithDefaults(parsed);
1531
- } catch (error) {
1532
- logger.warn("Failed to parse consent preferences:", error);
1533
- }
1534
- }
1535
- return this.getDefaultPreferences();
1536
- }
1537
- getDefaultPreferences() {
1538
- return {
1539
- analytics: this.config.defaultConsent.analytics ?? false,
1540
- marketing: this.config.defaultConsent.marketing ?? false,
1541
- functional: this.config.defaultConsent.functional ?? true,
1542
- necessary: true
1543
- };
1544
- }
1545
- mergeWithDefaults(preferences) {
1546
- return {
1547
- necessary: true,
1548
- analytics: preferences.analytics ?? this.config.defaultConsent.analytics ?? false,
1549
- marketing: preferences.marketing ?? this.config.defaultConsent.marketing ?? false,
1550
- functional: preferences.functional ?? this.config.defaultConsent.functional ?? true
1551
- };
1552
- }
1553
- savePreferences() {
1554
- try {
1555
- this.storage.set(
1556
- this.config.consentStorageKey,
1557
- JSON.stringify(this.preferences),
1558
- 365
1559
- );
1560
- logger.info("Saved consent preferences:", this.preferences);
1561
- } catch (error) {
1562
- logger.error("Failed to save consent preferences:", error);
1563
- }
1564
- }
1565
- setConsent(preferences) {
1566
- const updated = { ...this.preferences, ...preferences };
1567
- updated.necessary = true;
1568
- this.preferences = updated;
1569
- this.savePreferences();
1570
- this.notifyListeners();
1571
- logger.info("Consent updated:", this.preferences);
1572
- }
1573
- grantAll() {
1574
- this.setConsent({
1575
- analytics: true,
1576
- marketing: true,
1577
- functional: true,
1578
- necessary: true
1579
- });
1580
- }
1581
- denyAll() {
1582
- this.setConsent({
1583
- analytics: false,
1584
- marketing: false,
1585
- functional: false,
1586
- necessary: true
1587
- });
1588
- }
1589
- hasConsent(category) {
1590
- return this.preferences[category];
1591
- }
1592
- getPreferences() {
1593
- return { ...this.preferences };
1594
- }
1595
- getStatus(category) {
1596
- if (this.hasConsent(category)) {
1597
- return "granted";
1598
- }
1599
- const stored = this.storage.get(this.config.consentStorageKey);
1600
- if (!stored && this.config.waitForConsent) {
1601
- return "pending";
1602
- }
1603
- return "denied";
1604
- }
1605
- isAnalyticsEnabled() {
1606
- return this.hasConsent("analytics");
1607
- }
1608
- isMarketingEnabled() {
1609
- return this.hasConsent("marketing");
1610
- }
1611
- onChange(callback) {
1612
- this.listeners.push(callback);
1613
- return () => {
1614
- const index = this.listeners.indexOf(callback);
1615
- if (index > -1) {
1616
- this.listeners.splice(index, 1);
1617
- }
1618
- };
1619
- }
1620
- notifyListeners() {
1621
- this.listeners.forEach((listener) => {
1622
- try {
1623
- listener(this.getPreferences());
1624
- } catch (error) {
1625
- logger.error("Consent listener error:", error);
1626
- }
1627
- });
1628
- }
1629
- reset() {
1630
- this.storage.remove(this.config.consentStorageKey);
1631
- this.preferences = this.getDefaultPreferences();
1632
- this.notifyListeners();
1633
- logger.info("Consent preferences reset");
1634
- }
1635
- integrateOneTrust() {
1636
- if (typeof window === "undefined") return;
1637
- const oneTrust = window.OneTrust;
1638
- if (!oneTrust) {
1639
- logger.warn("OneTrust not detected");
1640
- return;
1641
- }
1642
- const syncWithOneTrust = () => {
1643
- const activeGroups = window.OnetrustActiveGroups || "";
1644
- this.setConsent({
1645
- necessary: true,
1646
- functional: activeGroups.includes("C0003"),
1647
- analytics: activeGroups.includes("C0002"),
1648
- marketing: activeGroups.includes("C0004")
1649
- });
1650
- logger.info("Synced with OneTrust consent");
1651
- };
1652
- if (window.OptanonWrapper) {
1653
- const originalWrapper = window.OptanonWrapper;
1654
- window.OptanonWrapper = function() {
1655
- originalWrapper();
1656
- syncWithOneTrust();
1657
- };
1658
- } else {
1659
- window.OptanonWrapper = syncWithOneTrust;
1660
- }
1661
- syncWithOneTrust();
1662
- }
1663
- integrateCookiebot() {
1664
- if (typeof window === "undefined") return;
1665
- const cookiebot = window.Cookiebot;
1666
- if (!cookiebot) {
1667
- logger.warn("Cookiebot not detected");
1668
- return;
1669
- }
1670
- const syncWithCookiebot = () => {
1671
- const consent = cookiebot.consent || {};
1672
- this.setConsent({
1673
- necessary: true,
1674
- functional: consent.preferences || false,
1675
- analytics: consent.statistics || false,
1676
- marketing: consent.marketing || false
1677
- });
1678
- logger.info("Synced with Cookiebot consent");
1679
- };
1680
- window.addEventListener("CookiebotOnAccept", syncWithCookiebot);
1681
- window.addEventListener("CookiebotOnDecline", syncWithCookiebot);
1682
- if (cookiebot.consent) {
1683
- syncWithCookiebot();
1684
- }
1685
- }
1686
- integrateGoogleConsentMode() {
1687
- if (typeof window === "undefined") return;
1688
- const w = window;
1689
- if (typeof w.gtag !== "function") {
1690
- w.dataLayer = w.dataLayer ?? [];
1691
- w.gtag = (...args) => {
1692
- var _a;
1693
- (_a = w.dataLayer) == null ? void 0 : _a.push(args);
1694
- };
1695
- logger.debug("Google tag not yet loaded — queuing consent via dataLayer");
1696
- }
1697
- const gtag = w.gtag;
1698
- const updateGoogleConsent = () => {
1699
- gtag("consent", "update", {
1700
- ad_storage: this.preferences.marketing ? "granted" : "denied",
1701
- analytics_storage: this.preferences.analytics ? "granted" : "denied",
1702
- ad_user_data: this.preferences.marketing ? "granted" : "denied",
1703
- ad_personalization: this.preferences.marketing ? "granted" : "denied",
1704
- functionality_storage: this.preferences.functional ? "granted" : "denied",
1705
- personalization_storage: this.preferences.functional ? "granted" : "denied",
1706
- security_storage: "granted"
1707
- });
1708
- logger.debug("Updated Google Consent Mode");
1709
- };
1710
- this.onChange(updateGoogleConsent);
1711
- updateGoogleConsent();
1712
- }
1713
- }
1714
- function sumItems(products) {
1715
- return products.reduce((sum, p) => sum + (p.quantity ?? 1), 0);
1716
- }
1717
- function mapProducts(products) {
1718
- return products.map((p) => ({
1719
- product_id: p.product_id,
1720
- sku: p.sku,
1721
- name: p.name,
1722
- price: p.price,
1723
- quantity: p.quantity ?? 1,
1724
- currency: p.currency,
1725
- category: p.category,
1726
- brand: p.brand,
1727
- variant_id: p.variant_id,
1728
- variant_label: p.variant_label,
1729
- position: p.position
1730
- }));
1731
- }
1732
- class EcommerceTracker {
1733
- constructor(aegis) {
1734
- this.aegis = aegis;
1735
- this._cart = {
1736
- total: 0,
1737
- itemCount: 0,
1738
- currency: "INR"
1739
- };
1740
- this._cartSink = null;
1741
- }
1742
- /** Register a sink that receives the derived cart on every cart event.
1743
- * Wired once by `AegisMessageRuntime.bindEcommerce(...)`. Emits the current
1744
- * snapshot immediately so a late binding still gets the latest cart. */
1745
- onCartChange(sink) {
1746
- this._cartSink = sink;
1747
- sink({ ...this._cart });
1748
- }
1749
- _emitCart() {
1750
- var _a;
1751
- if (this._cart.total < 0) this._cart.total = 0;
1752
- if (this._cart.itemCount < 0) this._cart.itemCount = 0;
1753
- (_a = this._cartSink) == null ? void 0 : _a.call(this, { ...this._cart });
1754
- }
1755
- // -- Product Discovery --
1756
- productViewed(product) {
1757
- this.aegis.track("product_viewed", {
1758
- product_id: product.product_id,
1759
- sku: product.sku,
1760
- name: product.name,
1761
- price: product.price,
1762
- currency: product.currency ?? "INR",
1763
- category: product.category,
1764
- brand: product.brand,
1765
- variant_id: product.variant_id,
1766
- variant_label: product.variant_label,
1767
- image_url: product.image_url,
1768
- url: product.url
1769
- });
1770
- }
1771
- productListViewed(list) {
1772
- const products = mapProducts(list.products);
1773
- this.aegis.track("product_list_viewed", {
1774
- list_id: list.list_id,
1775
- list_name: list.list_name,
1776
- category: list.category,
1777
- products,
1778
- // `items` as well as `products` — a THIRD spelling of the same fact was
1779
- // one spelling too many. Every product reader in the platform looks for
1780
- // `items` (or `line_items`, the Shopify shape); this event alone said
1781
- // `products`, so a list view showed N products and contributed nothing to
1782
- // any of them. On ST that was 4 views recorded as `not measured`, and it
1783
- // is the same defect that made every Shopify order invisible.
1784
- //
1785
- // `products` is kept so nothing already reading it breaks. mapProducts
1786
- // already emits product_id / sku / name / price / quantity, which is
1787
- // exactly the canonical item shape — no second mapping to drift.
1788
- items: products
1789
- });
1790
- }
1791
- productClicked(product, source) {
1792
- this.aegis.track("product_clicked", {
1793
- product_id: product.product_id,
1794
- sku: product.sku,
1795
- name: product.name,
1796
- price: product.price,
1797
- currency: product.currency ?? "INR",
1798
- category: product.category,
1799
- brand: product.brand,
1800
- variant_id: product.variant_id,
1801
- list_id: source == null ? void 0 : source.list_id,
1802
- position: (source == null ? void 0 : source.position) ?? product.position,
1803
- section: source == null ? void 0 : source.section
1804
- });
1805
- }
1806
- productImpressed(product, source) {
1807
- this.aegis.track("product_impression", {
1808
- product_id: product.product_id,
1809
- sku: product.sku,
1810
- name: product.name,
1811
- price: product.price,
1812
- currency: product.currency ?? "INR",
1813
- category: product.category,
1814
- list_id: source == null ? void 0 : source.list_id,
1815
- position: (source == null ? void 0 : source.position) ?? product.position,
1816
- section: source == null ? void 0 : source.section
1817
- });
1818
- }
1819
- categoryFiltered(category, options) {
1820
- this.aegis.track("category_filtered", {
1821
- category,
1822
- previous_category: options == null ? void 0 : options.previous_category,
1823
- result_count: options == null ? void 0 : options.result_count
1824
- });
1825
- }
1826
- searchPerformed(search) {
1827
- this.aegis.track("search_performed", {
1828
- query: search.query,
1829
- results_count: search.results_count,
1830
- filters: search.filters
1831
- });
1832
- }
1833
- // -- Cart --
1834
- addToCart(product) {
1835
- const qty = product.quantity ?? 1;
1836
- this.aegis.track("cart_item_added", {
1837
- product_id: product.product_id,
1838
- sku: product.sku,
1839
- name: product.name,
1840
- price: product.price,
1841
- quantity: qty,
1842
- currency: product.currency ?? "INR",
1843
- category: product.category,
1844
- brand: product.brand,
1845
- variant_id: product.variant_id,
1846
- variant_label: product.variant_label
1847
- });
1848
- this._cart.total += (product.price || 0) * qty;
1849
- this._cart.itemCount += qty;
1850
- if (product.currency) this._cart.currency = product.currency;
1851
- this._emitCart();
1852
- }
1853
- removeFromCart(product) {
1854
- const qty = product.quantity ?? 1;
1855
- this.aegis.track("cart_item_removed", {
1856
- product_id: product.product_id,
1857
- sku: product.sku,
1858
- name: product.name,
1859
- price: product.price,
1860
- quantity: qty,
1861
- currency: product.currency ?? "INR",
1862
- variant_id: product.variant_id
1863
- });
1864
- this._cart.total -= (product.price || 0) * qty;
1865
- this._cart.itemCount -= qty;
1866
- this._emitCart();
1867
- }
1868
- cartViewed(cart) {
1869
- this.aegis.track("cart_viewed", {
1870
- cart_id: cart.cart_id,
1871
- value: cart.value,
1872
- currency: cart.currency ?? "INR",
1873
- num_items: sumItems(cart.products),
1874
- products: mapProducts(cart.products)
1875
- });
1876
- this._cart = {
1877
- total: cart.value || 0,
1878
- itemCount: sumItems(cart.products),
1879
- currency: cart.currency ?? this._cart.currency ?? "INR"
1880
- };
1881
- this._emitCart();
1882
- }
1883
- // -- Checkout --
1884
- checkoutStarted(checkout) {
1885
- this.aegis.track("checkout_started", {
1886
- checkout_id: checkout.checkout_id,
1887
- value: checkout.value,
1888
- currency: checkout.currency ?? "INR",
1889
- num_items: sumItems(checkout.products),
1890
- coupon: checkout.coupon,
1891
- shipping: checkout.shipping,
1892
- tax: checkout.tax,
1893
- products: mapProducts(checkout.products)
1894
- });
1895
- }
1896
- checkoutStep(step, options) {
1897
- this.aegis.track("checkout_step", {
1898
- step,
1899
- ...options
1900
- });
1901
- }
1902
- checkoutOption(option) {
1903
- this.aegis.track("checkout_option", { ...option });
1904
- }
1905
- // -- Order --
1906
- orderCompleted(order) {
1907
- this.aegis.track("order_completed", {
1908
- order_id: order.order_id,
1909
- value: order.value,
1910
- revenue: order.revenue ?? order.value,
1911
- currency: order.currency ?? "INR",
1912
- num_items: sumItems(order.products),
1913
- coupon: order.coupon,
1914
- shipping: order.shipping,
1915
- tax: order.tax,
1916
- discount: order.discount,
1917
- payment_method: order.payment_method,
1918
- products: mapProducts(order.products)
1919
- });
1920
- }
1921
- orderRefunded(orderId, value, products) {
1922
- this.aegis.track("order_refunded", {
1923
- order_id: orderId,
1924
- value,
1925
- products: products ? mapProducts(products) : void 0
1926
- });
1927
- }
1928
- orderShipped(orderId, tracking) {
1929
- this.aegis.track("order_shipped", {
1930
- order_id: orderId,
1931
- carrier: tracking == null ? void 0 : tracking.carrier,
1932
- tracking_code: tracking == null ? void 0 : tracking.tracking_code,
1933
- tracking_url: tracking == null ? void 0 : tracking.tracking_url
1934
- });
1935
- }
1936
- // -- Coupons --
1937
- couponApplied(coupon) {
1938
- this.aegis.track("coupon_applied", { ...coupon });
1939
- }
1940
- couponRemoved(coupon) {
1941
- this.aegis.track("coupon_removed", { ...coupon });
1942
- }
1943
- couponDenied(coupon, reason) {
1944
- this.aegis.track("coupon_denied", { ...coupon, reason });
1945
- }
1946
- // -- Wishlist --
1947
- wishlistItemAdded(wishlist) {
1948
- this.aegis.track("wishlist_item_added", {
1949
- wishlist_id: wishlist.wishlist_id,
1950
- wishlist_name: wishlist.wishlist_name,
1951
- product_id: wishlist.product.product_id,
1952
- sku: wishlist.product.sku,
1953
- name: wishlist.product.name,
1954
- price: wishlist.product.price,
1955
- variant_id: wishlist.product.variant_id
1956
- });
1957
- }
1958
- productRemovedFromWishlist(wishlist) {
1959
- this.aegis.track("wishlist_item_removed", {
1960
- wishlist_id: wishlist.wishlist_id,
1961
- wishlist_name: wishlist.wishlist_name,
1962
- product_id: wishlist.product.product_id,
1963
- sku: wishlist.product.sku,
1964
- name: wishlist.product.name,
1965
- price: wishlist.product.price,
1966
- variant_id: wishlist.product.variant_id
1967
- });
1968
- }
1969
- // -- Back-in-stock waitlist --
1970
- //
1971
- // Server-side substrate: contact_events row keyed on
1972
- // (organization_id, contact_id, event_name='product_waitlisted',
1973
- // event_properties['product_id']). Resolved by
1974
- // product_event_trigger_service._get_waitlisted_contacts and fanned out via
1975
- // catalog.back_in_stock journey trigger when stock_event_handler_worker
1976
- // detects the SKU flipping back into stock.
1977
- productWaitlisted(waitlist) {
1978
- this.aegis.track("product_waitlisted", {
1979
- product_id: waitlist.product.product_id,
1980
- sku: waitlist.product.sku,
1981
- variant_id: waitlist.product.variant_id,
1982
- name: waitlist.product.name,
1983
- price: waitlist.product.price,
1984
- channels: waitlist.channels
1985
- });
1986
- }
1987
- // -- Promotions --
1988
- promotionViewed(promo) {
1989
- this.aegis.track("promotion_viewed", { ...promo });
1990
- }
1991
- promotionClicked(promo) {
1992
- this.aegis.track("promotion_clicked", { ...promo });
1993
- }
1994
- }
1995
- class RateLimiter {
1996
- constructor(options = {}) {
1997
- this.droppedThisWindow = 0;
1998
- this.firstDroppedName = null;
1999
- this.windowFlushTimer = null;
2000
- this.capacity = Math.max(1, options.burstCapacity ?? 100);
2001
- this.refillPerSecond = Math.max(1, options.refillPerSecond ?? 20);
2002
- this.windowMs = options.dropCoalesceWindowMs ?? 1e3;
2003
- this.onDropBatch = options.onDropBatch;
2004
- this.tokens = this.capacity;
2005
- this.lastRefillMs = Date.now();
2006
- }
2007
- /**
2008
- * Try to consume 1 token. Returns true if allowed, false if throttled.
2009
- * Throttled events are coalesced into a single onDropBatch callback per window.
2010
- */
2011
- tryConsume(eventName) {
2012
- this.refill();
2013
- if (this.tokens >= 1) {
2014
- this.tokens -= 1;
2015
- return true;
2016
- }
2017
- this.droppedThisWindow += 1;
2018
- if (this.firstDroppedName === null) {
2019
- this.firstDroppedName = eventName;
2020
- logger.warn(
2021
- `[Aegis] Rate limit reached — dropping event "${eventName}" and any further events for ${this.windowMs}ms. Burst=${this.capacity}, refill=${this.refillPerSecond}/s.`
2022
- );
2023
- }
2024
- if (!this.windowFlushTimer) {
2025
- this.windowFlushTimer = setTimeout(() => this.flushWindow(), this.windowMs);
2026
- }
2027
- return false;
2028
- }
2029
- /** Diagnostics — current token count (rounded down). */
2030
- getAvailableTokens() {
2031
- this.refill();
2032
- return Math.floor(this.tokens);
2033
- }
2034
- /** Cleanup. Idempotent. */
2035
- destroy() {
2036
- if (this.windowFlushTimer) {
2037
- clearTimeout(this.windowFlushTimer);
2038
- this.windowFlushTimer = null;
2039
- }
2040
- this.flushWindow();
2041
- }
2042
- // ---------------------------------------------------------------------
2043
- refill() {
2044
- const now = Date.now();
2045
- const elapsedSec = (now - this.lastRefillMs) / 1e3;
2046
- if (elapsedSec <= 0) return;
2047
- const newTokens = elapsedSec * this.refillPerSecond;
2048
- this.tokens = Math.min(this.capacity, this.tokens + newTokens);
2049
- this.lastRefillMs = now;
2050
- }
2051
- flushWindow() {
2052
- if (this.windowFlushTimer) {
2053
- clearTimeout(this.windowFlushTimer);
2054
- this.windowFlushTimer = null;
2055
- }
2056
- if (this.droppedThisWindow > 0 && this.onDropBatch) {
2057
- try {
2058
- this.onDropBatch(this.droppedThisWindow, this.firstDroppedName);
2059
- } catch (err) {
2060
- logger.warn("[Active Reach] RateLimiter.onDropBatch threw:", err);
2061
- }
2062
- }
2063
- this.droppedThisWindow = 0;
2064
- this.firstDroppedName = null;
2065
- }
2066
- }
2067
- const C1 = 3432918353;
2068
- const C2 = 461845907;
2069
- function murmurhash3_x86_32(input, seed = 0) {
2070
- const bytes = new TextEncoder().encode(input);
2071
- return murmurhash3_bytes(bytes, seed);
2072
- }
2073
- function murmurhash3_bytes(bytes, seed = 0) {
2074
- const len = bytes.length;
2075
- const nBlocks = Math.floor(len / 4);
2076
- let h1 = seed >>> 0;
2077
- for (let i = 0; i < nBlocks; i++) {
2078
- const offset = i * 4;
2079
- let k12 = bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24;
2080
- k12 = Math.imul(k12, C1);
2081
- k12 = k12 << 15 | k12 >>> 17;
2082
- k12 = Math.imul(k12, C2);
2083
- h1 ^= k12;
2084
- h1 = h1 << 13 | h1 >>> 19;
2085
- h1 = Math.imul(h1, 5) + 3864292196 >>> 0;
2086
- }
2087
- const tailStart = nBlocks * 4;
2088
- let k1 = 0;
2089
- const tailLen = len - tailStart;
2090
- if (tailLen === 3) k1 ^= bytes[tailStart + 2] << 16;
2091
- if (tailLen >= 2) k1 ^= bytes[tailStart + 1] << 8;
2092
- if (tailLen >= 1) {
2093
- k1 ^= bytes[tailStart];
2094
- k1 = Math.imul(k1, C1);
2095
- k1 = k1 << 15 | k1 >>> 17;
2096
- k1 = Math.imul(k1, C2);
2097
- h1 ^= k1;
2098
- }
2099
- h1 ^= len;
2100
- h1 ^= h1 >>> 16;
2101
- h1 = Math.imul(h1, 2246822507);
2102
- h1 ^= h1 >>> 13;
2103
- h1 = Math.imul(h1, 3266489909);
2104
- h1 ^= h1 >>> 16;
2105
- return h1 >>> 0;
2106
- }
2107
- function base64ToBytes(b64) {
2108
- if (typeof atob !== "undefined") {
2109
- const bin = atob(b64);
2110
- const out = new Uint8Array(bin.length);
2111
- for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
2112
- return out;
2113
- }
2114
- const B = globalThis.Buffer;
2115
- if (B && typeof B.from === "function") {
2116
- const buf = B.from(b64, "base64");
2117
- return new Uint8Array(buf);
2118
- }
2119
- throw new Error("No base64 decoder available (neither atob nor Buffer)");
2120
- }
2121
- class BloomFilter {
2122
- constructor(buf, params) {
2123
- this.params = params;
2124
- if ((params.m & params.m - 1) !== 0) {
2125
- throw new Error(`Bloom filter m must be a power of 2, got ${params.m}`);
2126
- }
2127
- if (buf.length !== params.m >> 3) {
2128
- throw new Error(
2129
- `Bloom filter buffer size mismatch: expected ${params.m >> 3} bytes, got ${buf.length}`
2130
- );
2131
- }
2132
- this.buf = buf;
2133
- this.mask = params.m - 1;
2134
- }
2135
- /** Build from the wire format (base64 bytes + explicit params). */
2136
- static fromBase64(bloomB64, params) {
2137
- const bytes = base64ToBytes(bloomB64);
2138
- return new BloomFilter(bytes, params);
2139
- }
2140
- /**
2141
- * Returns true if `name` is probably in the set — possibly with the
2142
- * filter's configured false-positive rate. FALSE is always authoritative.
2143
- *
2144
- * FP here means: SDK thinks a name is already registered when it isn't.
2145
- * That costs one wasted server round-trip (gateway does the exact check
2146
- * and catches it) — strictly safer than a false-negative, which could
2147
- * leak a novel name past the SDK.
2148
- */
2149
- has(name) {
2150
- const h1 = murmurhash3_x86_32(name, this.params.seedA);
2151
- const h2 = murmurhash3_x86_32(name, this.params.seedB);
2152
- for (let i = 0; i < this.params.k; i++) {
2153
- const combined = h1 + Math.imul(i, h2) >>> 0;
2154
- const idx = combined & this.mask;
2155
- const bit = this.buf[idx >> 3] & 1 << (idx & 7);
2156
- if (bit === 0) return false;
2157
- }
2158
- return true;
2159
- }
2160
- }
2161
- const SUPPORTED_ALGO = "mmh3_x86_32_km";
2162
- function warnOncePerSession(message) {
2163
- if (typeof console === "undefined" || typeof console.warn !== "function") return;
2164
- console.warn(message);
2165
- }
2166
- class NameGovernor {
2167
- constructor() {
2168
- this.bloom = null;
2169
- this.remainingNewNames = Infinity;
2170
- this.graceActive = false;
2171
- this.localNovelNames = /* @__PURE__ */ new Set();
2172
- this.droppedSinceLastReport = /* @__PURE__ */ new Map();
2173
- this.reportWindowStart = Date.now();
2174
- this.hasWarnedThisSession = false;
2175
- }
2176
- /**
2177
- * Ingest a freshly-bootstrapped hint. Call on every successful bootstrap.
2178
- * Passing `null` disables governance (fail-open).
2179
- */
2180
- ingestHint(hint) {
2181
- if (!hint || hint.bloom_algo !== SUPPORTED_ALGO) {
2182
- this.bloom = null;
2183
- this.remainingNewNames = Infinity;
2184
- this.graceActive = false;
2185
- this.localNovelNames.clear();
2186
- return;
2187
- }
2188
- try {
2189
- this.bloom = BloomFilter.fromBase64(hint.bloom_b64, {
2190
- m: hint.m,
2191
- k: hint.k,
2192
- seedA: hint.seed_a,
2193
- seedB: hint.seed_b
2194
- });
2195
- } catch {
2196
- this.bloom = null;
2197
- }
2198
- this.remainingNewNames = hint.remaining_new_names ?? Infinity;
2199
- this.graceActive = hint.grace_active === true;
2200
- this.localNovelNames.clear();
2201
- }
2202
- /**
2203
- * Decide whether a `track()` call should proceed.
2204
- *
2205
- * Returns true = send to network (rate-limiter still runs after).
2206
- * Returns false = drop locally; caller should return early.
2207
- */
2208
- shouldSend(eventName) {
2209
- if (!this.bloom) return true;
2210
- if (this.graceActive) return true;
2211
- if (this.bloom.has(eventName)) return true;
2212
- if (this.localNovelNames.has(eventName)) return true;
2213
- if (this.remainingNewNames > 0) {
2214
- this.localNovelNames.add(eventName);
2215
- this.remainingNewNames -= 1;
2216
- return true;
2217
- }
2218
- const prev = this.droppedSinceLastReport.get(eventName) ?? 0;
2219
- this.droppedSinceLastReport.set(eventName, prev + 1);
2220
- if (!this.hasWarnedThisSession) {
2221
- this.hasWarnedThisSession = true;
2222
- warnOncePerSession(
2223
- `[aegis] Event-name cap reached — "${eventName}" dropped locally. Upgrade your plan or remove dynamically-generated event names.`
2224
- );
2225
- }
2226
- return false;
2227
- }
2228
- /**
2229
- * Snapshot + reset the dropped-names counter. Called by the telemetry
2230
- * beacon on batch flush so the gateway gets visibility into client-side
2231
- * drops for ops dashboards.
2232
- */
2233
- drainDropReport() {
2234
- if (this.droppedSinceLastReport.size === 0) return null;
2235
- const events = {};
2236
- let total = 0;
2237
- for (const [name, count] of this.droppedSinceLastReport) {
2238
- events[name] = count;
2239
- total += count;
2240
- }
2241
- const since = this.reportWindowStart;
2242
- this.droppedSinceLastReport.clear();
2243
- this.reportWindowStart = Date.now();
2244
- return { events, total, since };
2245
- }
2246
- // Test-only accessors — not part of public API but easier than reflection.
2247
- /** @internal */
2248
- _debugState() {
2249
- return {
2250
- hasBloom: this.bloom !== null,
2251
- remaining: this.remainingNewNames,
2252
- localNovel: this.localNovelNames.size,
2253
- graceActive: this.graceActive
2254
- };
2255
- }
2256
- }
2257
- const RESERVED_PREFIXES = [
2258
- "system.",
2259
- "user.",
2260
- "loyalty.",
2261
- "review.",
2262
- "cart.",
2263
- "checkout.",
2264
- "product.",
2265
- "pos.",
2266
- "bill.",
2267
- "feedback.",
2268
- "chat.",
2269
- "delivery.",
2270
- "event.",
2271
- "$",
2272
- "_"
2273
- ];
2274
- const KEY_SOFT_VALUE_CAP = 512;
2275
- const KEY_HARD_VALUE_CAP = 1e4;
2276
- const CAMEL_BOUNDARY_1 = /(.)([A-Z][a-z]+)/g;
2277
- const CAMEL_BOUNDARY_2 = /([a-z0-9])([A-Z])/g;
2278
- const NON_SNAKE = /[\s\-.]+/g;
2279
- const DUPE_UNDERSCORE = /_+/g;
2280
- const ISO_8601 = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
2281
- const DATE_KEY_HINTS = /* @__PURE__ */ new Set([
2282
- "at",
2283
- "on",
2284
- "date",
2285
- "time",
2286
- "timestamp",
2287
- "dob",
2288
- "birthday",
2289
- "joined",
2290
- "expired",
2291
- "expires",
2292
- "created",
2293
- "updated",
2294
- "started",
2295
- "ended"
2296
- ]);
2297
- function normalizeKey(key) {
2298
- if (!key) return null;
2299
- const stage1 = key.replace(CAMEL_BOUNDARY_1, "$1_$2").replace(CAMEL_BOUNDARY_2, "$1_$2");
2300
- const stage2 = stage1.replace(NON_SNAKE, "_").toLowerCase();
2301
- const result = stage2.replace(DUPE_UNDERSCORE, "_").replace(/^_+|_+$/g, "");
2302
- return result || null;
2303
- }
2304
- function startsWithReserved(key) {
2305
- const lower = key.toLowerCase();
2306
- for (const prefix of RESERVED_PREFIXES) {
2307
- if (lower.startsWith(prefix)) return prefix;
2308
- }
2309
- return null;
2310
- }
2311
- function looksLikeDateKey(key) {
2312
- for (const part of key.toLowerCase().split(/[_\-.\s]+/)) {
2313
- if (DATE_KEY_HINTS.has(part)) return true;
2314
- }
2315
- return false;
2316
- }
2317
- function parseDateValue(value) {
2318
- if (typeof value === "number" && Number.isFinite(value)) {
2319
- return value < 1e11 ? Math.floor(value * 1e3) : Math.floor(value);
2320
- }
2321
- if (typeof value === "string" && value) {
2322
- const s = value.trim();
2323
- if (ISO_8601.test(s)) {
2324
- const ms = Date.parse(s);
2325
- if (!Number.isNaN(ms)) return ms;
2326
- }
2327
- const asNum = Number(s);
2328
- if (Number.isFinite(asNum)) {
2329
- return asNum < 1e11 ? Math.floor(asNum * 1e3) : Math.floor(asNum);
2330
- }
2331
- }
2332
- return null;
2333
- }
2334
- function warnOnConsole(message) {
2335
- if (typeof console === "undefined" || typeof console.warn !== "function") return;
2336
- console.warn(message);
2337
- }
2338
- const _TraitGovernor = class _TraitGovernor {
2339
- constructor() {
2340
- this.warnCounts = /* @__PURE__ */ new Map();
2341
- }
2342
- /**
2343
- * Apply all guards to a traits object. Returns the sanitized payload
2344
- * to send + a list of drops/modifications.
2345
- *
2346
- * Pass `workspace_id` so warning rate-limits scope per workspace; an
2347
- * agency user switching workspaces won't have their warning budget
2348
- * consumed cross-workspace.
2349
- */
2350
- process(traits, workspace_id) {
2351
- const sanitized = {};
2352
- const drops = [];
2353
- if (!traits || typeof traits !== "object") {
2354
- return { sanitized, drops };
2355
- }
2356
- for (const rawKey of Object.keys(traits)) {
2357
- const prefix = startsWithReserved(rawKey);
2358
- if (prefix !== null) {
2359
- drops.push({
2360
- originalKey: rawKey,
2361
- verdict: "reserved_prefix",
2362
- reason: `key uses reserved namespace ${JSON.stringify(prefix)}`
2363
- });
2364
- continue;
2365
- }
2366
- const normalizedKey = normalizeKey(rawKey);
2367
- if (normalizedKey === null) {
2368
- drops.push({
2369
- originalKey: rawKey,
2370
- verdict: "bad_key_format",
2371
- reason: `key reduced to empty after normalization`
2372
- });
2373
- continue;
2374
- }
2375
- if (startsWithReserved(normalizedKey) !== null) {
2376
- drops.push({
2377
- originalKey: rawKey,
2378
- verdict: "reserved_prefix",
2379
- reason: `normalized key ${JSON.stringify(normalizedKey)} still uses a reserved namespace`
2380
- });
2381
- continue;
2382
- }
2383
- let value = traits[rawKey];
2384
- if (typeof value === "string") {
2385
- if (value.length > KEY_HARD_VALUE_CAP) {
2386
- drops.push({
2387
- originalKey: rawKey,
2388
- verdict: "value_too_long",
2389
- reason: `value length ${value.length} exceeds hard cap ${KEY_HARD_VALUE_CAP}`
2390
- });
2391
- continue;
2392
- }
2393
- if (value.length > KEY_SOFT_VALUE_CAP) {
2394
- drops.push({
2395
- originalKey: rawKey,
2396
- verdict: "value_too_long",
2397
- reason: `value truncated from ${value.length} to ${KEY_SOFT_VALUE_CAP} chars`
2398
- });
2399
- value = value.slice(0, KEY_SOFT_VALUE_CAP);
2400
- }
2401
- }
2402
- if (looksLikeDateKey(normalizedKey) && typeof value === "string") {
2403
- const parsed = parseDateValue(value);
2404
- if (parsed === null) {
2405
- drops.push({
2406
- originalKey: rawKey,
2407
- verdict: "bad_date_format",
2408
- reason: `value ${JSON.stringify(value)} on date-keyed field ${JSON.stringify(normalizedKey)} didn't parse as ISO-8601 / epoch`
2409
- });
2410
- continue;
2411
- }
2412
- value = parsed;
2413
- }
2414
- sanitized[normalizedKey] = value;
2415
- }
2416
- for (const drop of drops) {
2417
- this.maybeWarn(workspace_id ?? null, drop);
2418
- }
2419
- return { sanitized, drops };
2420
- }
2421
- maybeWarn(workspace_id, drop) {
2422
- const ws = workspace_id ?? "__no_workspace__";
2423
- let perVerdict = this.warnCounts.get(ws);
2424
- if (!perVerdict) {
2425
- perVerdict = /* @__PURE__ */ new Map();
2426
- this.warnCounts.set(ws, perVerdict);
2427
- }
2428
- const count = perVerdict.get(drop.verdict) ?? 0;
2429
- if (count >= _TraitGovernor.WARN_CAP) return;
2430
- perVerdict.set(drop.verdict, count + 1);
2431
- warnOnConsole(
2432
- `[Active Reach SDK] trait ${drop.verdict}: ${drop.reason} (original key: ${JSON.stringify(drop.originalKey)}). Backend will reject; fix the SDK call to silence this warning.`
2433
- );
2434
- }
2435
- };
2436
- _TraitGovernor.WARN_CAP = 3;
2437
- let TraitGovernor = _TraitGovernor;
2438
- const VALID_CHANNELS = /* @__PURE__ */ new Set([
2439
- "email",
2440
- "sms",
2441
- "push",
2442
- "webpush",
2443
- "whatsapp",
2444
- "rcs",
2445
- "inapp"
2446
- ]);
2447
- const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2448
- const PHONE_RE = /^\+[1-9]\d{7,14}$/;
2449
- const SHA256_HEX_RE = /^[a-f0-9]{64}$/i;
2450
- const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
2451
- class UserNamespace {
2452
- constructor(aegis) {
2453
- this.pendingTraits = {};
2454
- this.aegis = aegis;
2455
- }
2456
- /**
2457
- * Authoritative identity write. Flushes any pending pre-login traits
2458
- * along with the supplied `traits` argument as a single identify call.
2459
- */
2460
- login(userId, traits) {
2461
- if (typeof userId !== "string" || userId.length === 0) {
2462
- logger.warn("[user.login] userId must be a non-empty string");
2463
- return;
2464
- }
2465
- const merged = { ...this.pendingTraits, ...traits || {} };
2466
- this.pendingTraits = {};
2467
- this.aegis.identify(userId, merged);
2468
- }
2469
- /**
2470
- * Drops the current userId and any pending pre-login traits. Does NOT
2471
- * fire a server-side logout event — that's a separate explicit `track`.
2472
- */
2473
- logout() {
2474
- this.pendingTraits = {};
2475
- this.aegis.reset();
2476
- }
2477
- setAttribute(key, value) {
2478
- if (typeof key !== "string" || key.length === 0) {
2479
- logger.warn("[user.setAttribute] key must be a non-empty string");
2480
- return;
2481
- }
2482
- this.writeTraits({ [key]: value });
2483
- }
2484
- setAttributes(map) {
2485
- if (!map || typeof map !== "object") {
2486
- logger.warn("[user.setAttributes] map must be an object");
2487
- return;
2488
- }
2489
- this.writeTraits(map);
2490
- }
2491
- setEmail(email) {
2492
- if (!EMAIL_RE.test(email)) {
2493
- logger.warn("[user.setEmail] invalid email format");
2494
- return;
2495
- }
2496
- this.writeTraits({ email: email.toLowerCase() });
2497
- }
2498
- setPhone(phone) {
2499
- if (!PHONE_RE.test(phone)) {
2500
- logger.warn("[user.setPhone] phone must be E.164 (e.g. +15551234567)");
2501
- return;
2502
- }
2503
- this.writeTraits({ phone });
2504
- }
2505
- setHashedEmail(sha256Hex) {
2506
- if (!SHA256_HEX_RE.test(sha256Hex)) {
2507
- logger.warn("[user.setHashedEmail] expected 64-char hex SHA-256");
2508
- return;
2509
- }
2510
- this.writeTraits({ email_sha256: sha256Hex.toLowerCase() });
2511
- }
2512
- setHashedPhone(sha256Hex) {
2513
- if (!SHA256_HEX_RE.test(sha256Hex)) {
2514
- logger.warn("[user.setHashedPhone] expected 64-char hex SHA-256");
2515
- return;
2516
- }
2517
- this.writeTraits({ phone_sha256: sha256Hex.toLowerCase() });
2518
- }
2519
- setBirthDate(iso) {
2520
- if (!ISO_DATE_RE.test(iso)) {
2521
- logger.warn("[user.setBirthDate] expected YYYY-MM-DD");
2522
- return;
2523
- }
2524
- this.writeTraits({ birth_date: iso });
2525
- }
2526
- setOptIn(channel, granted) {
2527
- if (!VALID_CHANNELS.has(channel)) {
2528
- logger.warn(`[user.setOptIn] unknown channel: ${channel}`);
2529
- return;
2530
- }
2531
- if (typeof granted !== "boolean") {
2532
- logger.warn("[user.setOptIn] granted must be a boolean");
2533
- return;
2534
- }
2535
- this.writeTraits({ [`opt_in_${channel}`]: granted });
2536
- }
2537
- /**
2538
- * HMAC-signed identity token. Ingress-side verification not yet wired —
2539
- * the trait is forwarded as-is and persisted on the contact record
2540
- * until that lands.
2541
- */
2542
- setSecureToken(token) {
2543
- if (typeof token !== "string" || token.length === 0) {
2544
- logger.warn("[user.setSecureToken] token must be a non-empty string");
2545
- return;
2546
- }
2547
- this.writeTraits({ _secure_token: token });
2548
- }
2549
- /**
2550
- * Test/inspection hook — returns a copy of the pending traits buffer.
2551
- * Used by the e2e smoke test to assert pre-login accumulation.
2552
- */
2553
- _getPendingTraits() {
2554
- return { ...this.pendingTraits };
2555
- }
2556
- // ---------------------------------------------------------------------------
2557
- writeTraits(traits) {
2558
- const userId = this.aegis.getUserId();
2559
- if (userId) {
2560
- this.aegis.identify(userId, traits);
2561
- return;
2562
- }
2563
- Object.assign(this.pendingTraits, traits);
2564
- }
2565
- }
2566
- class Aegis {
2567
- constructor() {
2568
- this.config = null;
2569
- this.session = null;
2570
- this.queue = null;
2571
- this.transport = null;
2572
- this.plugins = new PluginRegistry();
2573
- this.initPromise = null;
2574
- this.consent = null;
2575
- this._ecommerce = null;
2576
- this._user = null;
2577
- this.rateLimiter = null;
2578
- this.nameGovernor = new NameGovernor();
2579
- this.traitGovernor = new TraitGovernor();
2580
- this._lastPageUrl = null;
2581
- this._popstateHandler = null;
2582
- this._originalPushState = null;
2583
- this._originalReplaceState = null;
2584
- this._lastEventIds = /* @__PURE__ */ new Map();
2585
- this._workspaceCodes = /* @__PURE__ */ new Set();
2586
- this._runtimeWorkspace = null;
2587
- this._runtimeLocation = null;
2588
- }
2589
- async init(writeKey, config) {
2590
- if (this.initPromise) {
2591
- return this.initPromise;
2592
- }
2593
- this.initPromise = this._init(writeKey, config);
2594
- return this.initPromise;
2595
- }
2596
- async _init(writeKey, config) {
2597
- var _a;
2598
- if ((_a = this.config) == null ? void 0 : _a.initialized) {
2599
- logger.warn("SDK already initialized");
2600
- return;
2601
- }
2602
- if (this.shouldRespectDNT(config)) {
2603
- logger.info("Do Not Track is enabled, SDK disabled");
2604
- return;
2605
- }
2606
- if (isBot()) {
2607
- logger.info("Bot detected, SDK disabled");
2608
- return;
2609
- }
2610
- this.config = this.buildConfig(writeKey, config);
2611
- if (this.config.debug) {
2612
- logger.enable();
2613
- }
2614
- logger.info("Initializing Active Reach SDK...", this.config);
2615
- this.storage = new Storage({
2616
- cookieDomain: this.config.cookie_domain,
2617
- secureCookie: this.config.secure_cookie,
2618
- crossDomain: this.config.cross_domain_tracking
2619
- });
2620
- this.identity = new Identity(this.storage);
2621
- this.consent = new ConsentManager(this.storage, {
2622
- defaultConsent: this.config.default_consent,
2623
- waitForConsent: this.config.wait_for_consent
2624
- });
2625
- if (this.config.integrate_onetrust) {
2626
- this.consent.integrateOneTrust();
2627
- }
2628
- if (this.config.integrate_cookiebot) {
2629
- this.consent.integrateCookiebot();
2630
- }
2631
- if (this.config.integrate_google_consent_mode) {
2632
- this.consent.integrateGoogleConsentMode();
2633
- }
2634
- this.session = new SessionManager(
2635
- this.storage,
2636
- this.config.session_timeout
2637
- );
2638
- this.captureAdClickIDsOnInit();
2639
- this.transport = new Transport(
2640
- {
2641
- writeKey: this.config.write_key,
2642
- apiHost: this.config.api_host,
2643
- cellEndpoints: this.config.cell_endpoints,
2644
- preferredRegion: this.config.preferred_region,
2645
- autoRegionDetection: this.config.auto_region_detection,
2646
- requestTimeout: this.config.request_timeout,
2647
- retryFailedRequests: this.config.retry_failed_requests,
2648
- maxRetries: this.config.max_retries,
2649
- retryBackoffMultiplier: this.config.retry_backoff_multiplier
2650
- },
2651
- this.storage
2652
- );
2653
- this.config.active_cell = this.transport.getActiveCell();
2654
- this.queue = new EventQueue(
2655
- {
2656
- batchSize: this.config.batch_size,
2657
- batchInterval: this.config.batch_interval,
2658
- enableOfflineMode: this.config.enable_offline_mode,
2659
- maxOfflineEvents: this.config.max_offline_events
2660
- },
2661
- this.transport,
2662
- this.storage
2663
- );
2664
- this.rateLimiter = new RateLimiter({
2665
- burstCapacity: this.config.rate_limit_burst,
2666
- refillPerSecond: this.config.rate_limit_per_second,
2667
- onDropBatch: (count, sampleName) => this.emitRateLimitMeta(count, sampleName)
2668
- });
2669
- await this.initializePlugins();
2670
- if (this.config.auto_page_view) {
2671
- this.page();
2672
- this.startSPATracking();
2673
- }
2674
- logger.info("Active Reach SDK initialized successfully");
2675
- }
2676
- /**
2677
- * SPA navigation tracking — monkey-patches pushState/replaceState and
2678
- * listens for popstate to fire page() on every client-side route change.
2679
- */
2680
- startSPATracking() {
2681
- this._lastPageUrl = window.location.href;
2682
- const onRouteChange = () => {
2683
- const currentUrl = window.location.href;
2684
- if (currentUrl === this._lastPageUrl) return;
2685
- this._lastPageUrl = currentUrl;
2686
- setTimeout(() => {
2687
- if (!this._popstateHandler) return;
2688
- this.page();
2689
- }, 50);
2690
- };
2691
- this._popstateHandler = onRouteChange;
2692
- window.addEventListener("popstate", this._popstateHandler);
2693
- this._originalPushState = history.pushState.bind(history);
2694
- this._originalReplaceState = history.replaceState.bind(history);
2695
- history.pushState = (...args) => {
2696
- this._originalPushState(...args);
2697
- onRouteChange();
2698
- };
2699
- history.replaceState = (...args) => {
2700
- this._originalReplaceState(...args);
2701
- onRouteChange();
2702
- };
2703
- }
2704
- stopSPATracking() {
2705
- if (this._popstateHandler) {
2706
- window.removeEventListener("popstate", this._popstateHandler);
2707
- this._popstateHandler = null;
2708
- }
2709
- if (this._originalPushState) {
2710
- history.pushState = this._originalPushState;
2711
- this._originalPushState = null;
2712
- }
2713
- if (this._originalReplaceState) {
2714
- history.replaceState = this._originalReplaceState;
2715
- this._originalReplaceState = null;
2716
- }
2717
- }
2718
- buildConfig(writeKey, config) {
2719
- return {
2720
- ...DEFAULT_CONFIG,
2721
- ...config,
2722
- write_key: writeKey,
2723
- initialized: true
2724
- };
2725
- }
2726
- shouldRespectDNT(config) {
2727
- const respectDNT = (config == null ? void 0 : config.respect_dnt) !== false;
2728
- if (!respectDNT) return false;
2729
- const dnt = navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack;
2730
- return dnt === "1" || dnt === "yes";
2731
- }
2732
- captureAdClickIDsOnInit() {
2733
- if (!this.session) return;
2734
- const adClickIDs = parseAdClickIDs();
2735
- if (hasAdClickIDs(adClickIDs)) {
2736
- this.session.setAdClickIDs(adClickIDs, redactUrl(window.location.href));
2737
- logger.info("Ad click IDs captured on page load:", adClickIDs);
2738
- }
2739
- const linkAttribution = parseLinkAttribution();
2740
- if (linkAttribution.aegis_code) {
2741
- this.session.setLinkAttribution(linkAttribution);
2742
- }
2743
- }
2744
- async initializePlugins() {
2745
- if (!this.config) return;
2746
- for (const pluginName of this.config.plugins) {
2747
- logger.debug(`Initializing plugin: ${pluginName}`);
2748
- }
2749
- await this.plugins.executeHook("init", this.config);
2750
- }
2751
- use(plugin) {
2752
- var _a;
2753
- this.plugins.register(plugin);
2754
- if (((_a = this.config) == null ? void 0 : _a.initialized) && plugin.init) {
2755
- Promise.resolve(plugin.init(this.config)).catch((error) => {
2756
- logger.error(`Failed to initialize plugin "${plugin.name}":`, error);
2757
- });
2758
- }
2759
- }
2760
- /**
2761
- * Symmetric counterpart to `use(plugin)`. Calls the plugin's `destroy()`
2762
- * hook (if defined) and removes it from the registry so its hooks no
2763
- * longer fire on subsequent events. Safe to call with a name that
2764
- * isn't registered — logs a warning and returns.
2765
- *
2766
- * Primary use case: React components that register a plugin in
2767
- * `useEffect` and need to unregister in the cleanup function so HMR /
2768
- * provider remount doesn't leak listeners. See cashier-portal's
2769
- * MetaPixelInjector for an example.
2770
- */
2771
- removePlugin(name) {
2772
- this.plugins.unregister(name);
2773
- }
2774
- /**
2775
- * Plugin-handshake P1 — ingest the workspace_code allowlist from the
2776
- * bootstrap response. Pass `result.workspaceCodes` from `bootstrap()`
2777
- * here so the SDK can do path-segment workspace detection on
2778
- * customer-facing pages like `/south/rewards`, `/ghatkopar/feedback`.
2779
- *
2780
- * Empty array is the right answer for single-outlet tenants — SDK
2781
- * will skip the path cascade and fall through to query param /
2782
- * setWorkspace() / gateway origin lookup.
2783
- *
2784
- * Safe to call before init() — the set is consulted at event-fire time.
2785
- */
2786
- ingestWorkspaceCodes(codes) {
2787
- this._workspaceCodes = new Set(
2788
- (codes ?? []).filter((c) => typeof c === "string" && c.length > 0)
2789
- );
2790
- logger.debug("Workspace codes ingested", { count: this._workspaceCodes.size });
2791
- }
2792
- /**
2793
- * Plugin-handshake P1 (Track E) — explicitly set the workspace for
2794
- * subsequent events. Used by:
2795
- * - SPAs where workspace context changes via in-app routing
2796
- * - Mobile / RN apps that have no URL
2797
- * - Custom integrations that resolve workspace from their own state
2798
- *
2799
- * Pass null to clear (or call clearWorkspace()).
2800
- *
2801
- * Persisted to sessionStorage so SPA reloads / page transitions on the
2802
- * same outlet inherit it without re-calling.
2803
- */
2804
- setWorkspace(codeOrId) {
2805
- this._runtimeWorkspace = codeOrId;
2806
- if (typeof window !== "undefined" && window.sessionStorage) {
2807
- try {
2808
- if (codeOrId) {
2809
- window.sessionStorage.setItem("aegis_runtime_ws", codeOrId);
2810
- } else {
2811
- window.sessionStorage.removeItem("aegis_runtime_ws");
2812
- }
2813
- } catch {
2814
- }
2815
- }
2816
- logger.debug("Runtime workspace set", { value: codeOrId });
2817
- }
2818
- /** Symmetric helper to clear the runtime workspace. */
2819
- clearWorkspace() {
2820
- this.setWorkspace(null);
2821
- }
2822
- // ── P15.1 — location (outlet) facade ─────────────────────────
2823
- //
2824
- // Under the post-2026-05-26 canonical hierarchy:
2825
- // workspace_id = brand-tenant (one per brand)
2826
- // location_id = outlet within a brand
2827
- // The two are distinct dimensions. Both can be stamped on the
2828
- // same event. These methods mirror the workspace facade but
2829
- // resolve the outlet specifically.
2830
- /**
2831
- * P15.1 — ingest the outlet-code allowlist from bootstrap. The
2832
- * SDK consults this set when reading the URL path segment to
2833
- * decide whether `location.pathname.split('/')[1]` is a valid
2834
- * outlet code vs a generic route.
2835
- *
2836
- * Empty array = single-outlet tenant; SDK skips the path-segment
2837
- * cascade for location resolution.
2838
- *
2839
- * Safe to call before init(); the set is consulted at event-
2840
- * fire time.
2841
- */
2842
- ingestLocationCodes(codes) {
2843
- this._workspaceCodes = new Set(
2844
- (codes ?? []).filter((c) => typeof c === "string" && c.length > 0)
2845
- );
2846
- logger.debug("Location codes ingested", { count: this._workspaceCodes.size });
2847
- }
2848
- /**
2849
- * P15.1 — explicitly set the active outlet for subsequent events.
2850
- * Used by:
2851
- * - Storefronts where outlet picker fires `aegis.setLocation()`
2852
- * on selection (so events that follow carry that outlet's
2853
- * location_id without the SDK having to re-parse the URL).
2854
- * - SPAs where outlet context changes via in-app routing.
2855
- * - Mobile/RN apps that have no URL.
2856
- *
2857
- * Pass null to clear (or call clearLocation()).
2858
- *
2859
- * Persisted to sessionStorage so SPA reloads + route transitions
2860
- * on the same outlet inherit it without re-calling.
2861
- */
2862
- setLocation(codeOrId) {
2863
- this._runtimeLocation = codeOrId;
2864
- if (typeof window !== "undefined" && window.sessionStorage) {
2865
- try {
2866
- if (codeOrId) {
2867
- window.sessionStorage.setItem("aegis_runtime_location", codeOrId);
2868
- } else {
2869
- window.sessionStorage.removeItem("aegis_runtime_location");
2870
- }
2871
- } catch {
2872
- }
2873
- }
2874
- logger.debug("Runtime location set", { value: codeOrId });
2875
- }
2876
- /** Symmetric helper to clear the runtime location. */
2877
- clearLocation() {
2878
- this.setLocation(null);
2879
- }
2880
- /**
2881
- * P15.1 — same logic as `getPathWorkspaceCode()` but expresses
2882
- * the semantic correctly: under the new hierarchy, the first
2883
- * path segment after the brand subdomain IS the outlet code.
2884
- * Returns `undefined` when no outlet code matches the allowlist.
2885
- */
2886
- getPathLocationCode() {
2887
- return this.getPathWorkspaceCode();
2888
- }
2889
- /**
2890
- * P15.1 — resolve the effective `location_id` for the next event.
2891
- * Independent of `getEffectiveWorkspaceId()`: both run on every
2892
- * event so workspace_id (brand) and location_id (outlet) land
2893
- * on the same event when both are resolvable.
2894
- *
2895
- * Cascade (highest precedence first):
2896
- * 1. `this.config.location_id` — explicit operator config.
2897
- * 2. `?location=` query param (also accepts `?ws=` for back-
2898
- * compat with storefronts that pass outlet via the old
2899
- * param name — the value is the same; param naming only).
2900
- * 3. URL path segment matching the outlet-code allowlist.
2901
- * 4. Runtime `aegis.setLocation()` (in-memory).
2902
- * 5. sessionStorage `aegis_runtime_location`.
2903
- * 6. `undefined` — event-ingress treats as brand-tier
2904
- * (no outlet attribution).
2905
- */
2906
- getEffectiveLocationId() {
2907
- var _a;
2908
- const configured = (_a = this.config) == null ? void 0 : _a.location_id;
2909
- if (configured) return configured;
2910
- if (typeof window === "undefined") return void 0;
2911
- try {
2912
- const search = new URLSearchParams(window.location.search);
2913
- const loc = search.get("location") || search.get("ws");
2914
- if (loc && loc.length > 0) return loc;
2915
- } catch {
2916
- }
2917
- const pathLoc = this.getPathLocationCode();
2918
- if (pathLoc) return pathLoc;
2919
- if (this._runtimeLocation) return this._runtimeLocation;
2920
- try {
2921
- if (window.sessionStorage) {
2922
- const cached = window.sessionStorage.getItem("aegis_runtime_location");
2923
- if (cached && cached.length > 0) return cached;
2924
- }
2925
- } catch {
2926
- }
2927
- return void 0;
2928
- }
2929
- /**
2930
- * Inspect the URL's first path segment and return it if it's in the
2931
- * org's workspace_code allowlist. Skips well-known non-workspace prefixes
2932
- * (e.g. `b` for bill short codes; `s` is the storefront app slug).
2933
- *
2934
- * Returns undefined when:
2935
- * - window is undefined (SSR / Node)
2936
- * - path is empty / root
2937
- * - first segment isn't in `_workspaceCodes` (e.g. /products, /cart)
2938
- *
2939
- * The returned value is a workspace CODE (slug like "south"), not a
2940
- * UUID. The gateway normalizes code → UUID server-side via
2941
- * workspace_subaccounts lookup with Redis cache.
2942
- */
2943
- getPathWorkspaceCode() {
2944
- if (typeof window === "undefined") return void 0;
2945
- if (this._workspaceCodes.size === 0) return void 0;
2946
- try {
2947
- const segments = window.location.pathname.split("/").filter(Boolean);
2948
- if (segments.length === 0) return void 0;
2949
- const first = segments[0].toLowerCase();
2950
- return this._workspaceCodes.has(first) ? first : void 0;
2951
- } catch {
2952
- return void 0;
2953
- }
2954
- }
2955
- /**
2956
- * Resolve the effective `workspace_id` for the next event.
2957
- *
2958
- * Cascade (highest precedence first):
2959
- * 1. `this.config.workspace_id` — explicit operator config (headless
2960
- * SDK usage, server-rendered apps, native shells).
2961
- * 2. `?ws=` query param on the current URL — the P3 storefront URL
2962
- * contract. The outlet picker writes this via `router.replace`.
2963
- * 3. URL path segment (P1 Track A) — `/south/rewards` → "south" when
2964
- * "south" is in the org's workspace_code allowlist.
2965
- * 4. `aegis.setWorkspace()` runtime override (P1 Track E) — SPAs +
2966
- * mobile + custom integrations.
2967
- * 5. sessionStorage `aegis_runtime_ws` — persists setWorkspace across
2968
- * reloads/SPA route changes.
2969
- * 6. `undefined` — gateway falls back to `resolveByOrigin` lookup
2970
- * against the property's allowed_origins.
2971
- *
2972
- * The returned value can be EITHER a UUID (from config / ?ws=) or a
2973
- * workspace CODE slug (from path / setWorkspace). The gateway
2974
- * normalizes slug → UUID server-side; analytics layer never needs to
2975
- * worry about the difference.
2976
- *
2977
- * See docs/architecture/MULTI_OUTLET_URL_STRATEGY.md §2.3 and
2978
- * docs/architecture/PLUGIN_HANDSHAKE_AUTOMATION.md §2.
2979
- */
2980
- /**
2981
- * Public so peer SDK surfaces (AegisMessageRuntime / AegisInAppManager /
2982
- * AegisWidgetManager) can plumb the same
2983
- * resolved workspace into their own gateway POSTs. Each manager calls
2984
- * the gateway on its own endpoint (`/v1/in-app/events`,
2985
- * `/v1/widgets/track-event`); without this,
2986
- * those events arrive at event-ingress with no workspace_id stamped
2987
- * → impressions/clicks fall back to the org's primary workspace and
2988
- * cross-outlet attribution breaks.
2989
- */
2990
- getEffectiveWorkspaceId() {
2991
- var _a;
2992
- const configured = (_a = this.config) == null ? void 0 : _a.workspace_id;
2993
- if (configured) return configured;
2994
- if (typeof window === "undefined") return void 0;
2995
- try {
2996
- const ws = new URLSearchParams(window.location.search).get("ws");
2997
- if (ws && ws.length > 0) return ws;
2998
- } catch {
2999
- }
3000
- const pathWs = this.getPathWorkspaceCode();
3001
- if (pathWs) return pathWs;
3002
- if (this._runtimeWorkspace) return this._runtimeWorkspace;
3003
- try {
3004
- if (window.sessionStorage) {
3005
- const cached = window.sessionStorage.getItem("aegis_runtime_ws");
3006
- if (cached && cached.length > 0) return cached;
3007
- }
3008
- } catch {
3009
- }
3010
- return void 0;
3011
- }
3012
- track(eventName, properties) {
3013
- if (!this.assertInitialized()) return;
3014
- const messageId = generateMessageId();
3015
- const event = {
3016
- type: "track",
3017
- event: eventName,
3018
- properties: properties || {},
3019
- messageId,
3020
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3021
- anonymousId: this.identity.getAnonymousId(),
3022
- userId: this.identity.getUserId() || void 0,
3023
- sessionId: this.session.getSessionId(),
3024
- workspace_id: this.getEffectiveWorkspaceId(),
3025
- location_id: this.getEffectiveLocationId(),
3026
- context: buildContext(this.config, this.session)
3027
- };
3028
- this._lastEventIds.set(eventName, messageId);
3029
- this.captureEvent(event);
3030
- }
3031
- identify(userId, traits) {
3032
- if (!this.assertInitialized()) return;
3033
- const wsForGovernor = this.getEffectiveWorkspaceId() || null;
3034
- const { sanitized: governedTraits } = this.traitGovernor.process(traits, wsForGovernor);
3035
- if (this.identity.getUserId() !== userId) {
3036
- this.contactToken = void 0;
3037
- }
3038
- this.identity.setUserId(userId, governedTraits);
3039
- const event = {
3040
- type: "identify",
3041
- traits: governedTraits,
3042
- messageId: generateMessageId(),
3043
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3044
- anonymousId: this.identity.getAnonymousId(),
3045
- userId,
3046
- sessionId: this.session.getSessionId(),
3047
- workspace_id: this.getEffectiveWorkspaceId(),
3048
- location_id: this.getEffectiveLocationId(),
3049
- context: buildContext(this.config, this.session)
3050
- };
3051
- this.captureEvent(event);
3052
- }
3053
- page(name, properties) {
3054
- if (!this.assertInitialized()) return;
3055
- const event = {
3056
- type: "page",
3057
- name: name || document.title,
3058
- properties: properties || {},
3059
- messageId: generateMessageId(),
3060
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3061
- anonymousId: this.identity.getAnonymousId(),
3062
- userId: this.identity.getUserId() || void 0,
3063
- sessionId: this.session.getSessionId(),
3064
- workspace_id: this.getEffectiveWorkspaceId(),
3065
- location_id: this.getEffectiveLocationId(),
3066
- context: buildContext(this.config, this.session)
3067
- };
3068
- this.captureEvent(event);
3069
- }
3070
- group(groupId, traits) {
3071
- if (!this.assertInitialized()) return;
3072
- const wsForGovernor = this.getEffectiveWorkspaceId() || null;
3073
- const { sanitized: governedTraits } = this.traitGovernor.process(traits, wsForGovernor);
3074
- const event = {
3075
- type: "group",
3076
- groupId,
3077
- traits: governedTraits,
3078
- messageId: generateMessageId(),
3079
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3080
- anonymousId: this.identity.getAnonymousId(),
3081
- userId: this.identity.getUserId() || void 0,
3082
- sessionId: this.session.getSessionId(),
3083
- context: buildContext(this.config, this.session)
3084
- };
3085
- this.captureEvent(event);
3086
- }
3087
- alias(newUserId) {
3088
- if (!this.assertInitialized()) return;
3089
- const { previousId } = this.identity.alias(newUserId);
3090
- const event = {
3091
- type: "alias",
3092
- previousId,
3093
- messageId: generateMessageId(),
3094
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3095
- anonymousId: this.identity.getAnonymousId(),
3096
- userId: newUserId,
3097
- sessionId: this.session.getSessionId(),
3098
- context: buildContext(this.config, this.session)
3099
- };
3100
- this.captureEvent(event);
3101
- }
3102
- /**
3103
- * SPA-logical screen view. Distinct from `page()` (URL-bound) — fires
3104
- * when the user enters a logical surface like cart / checkout / drawer
3105
- * without a URL change. Gateway maps `screen` → `engagement.screen_view`.
3106
- */
3107
- screen(name, properties) {
3108
- if (!this.assertInitialized()) return;
3109
- if (typeof name !== "string" || name.length === 0) {
3110
- logger.warn("aegis.screen: name must be a non-empty string");
3111
- return;
3112
- }
3113
- const event = {
3114
- type: "screen",
3115
- name,
3116
- properties: properties || {},
3117
- messageId: generateMessageId(),
3118
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3119
- anonymousId: this.identity.getAnonymousId(),
3120
- userId: this.identity.getUserId() || void 0,
3121
- sessionId: this.session.getSessionId(),
3122
- workspace_id: this.getEffectiveWorkspaceId(),
3123
- location_id: this.getEffectiveLocationId(),
3124
- context: buildContext(this.config, this.session)
3125
- };
3126
- this.captureEvent(event);
3127
- }
3128
- async captureEvent(event) {
3129
- var _a;
3130
- if (((_a = this.config) == null ? void 0 : _a.enable_consent_mode) && this.consent) {
3131
- if (event.type === "track" || event.type === "page") {
3132
- if (!this.consent.hasConsent("analytics")) {
3133
- logger.debug("Event blocked: analytics consent not granted");
3134
- return;
3135
- }
3136
- }
3137
- }
3138
- if (event.type === "track") {
3139
- const trackEvent = event;
3140
- const isMetaEvent = typeof trackEvent.event === "string" && trackEvent.event.startsWith("aegis.client.");
3141
- if (!isMetaEvent && !this.nameGovernor.shouldSend(trackEvent.event)) {
3142
- return;
3143
- }
3144
- }
3145
- if (this.rateLimiter) {
3146
- const eventLabel = event.type === "track" && event.event ? event.event : event.type;
3147
- const isMetaEvent = event.type === "track" && typeof event.event === "string" && event.event.startsWith("aegis.client.");
3148
- if (!isMetaEvent && !this.rateLimiter.tryConsume(eventLabel)) {
3149
- return;
3150
- }
3151
- }
3152
- const ctx = event.context;
3153
- const props = event.properties;
3154
- if ((ctx == null ? void 0 : ctx.campaign) && props) {
3155
- const utm = ctx.campaign;
3156
- if (utm.source && !props.utm_source) props.utm_source = utm.source;
3157
- if (utm.medium && !props.utm_medium) props.utm_medium = utm.medium;
3158
- if (utm.campaign && !props.utm_campaign) props.utm_campaign = utm.campaign;
3159
- if (utm.content && !props.utm_content) props.utm_content = utm.content;
3160
- if (utm.term && !props.utm_term) props.utm_term = utm.term;
3161
- }
3162
- if ((ctx == null ? void 0 : ctx.adClickIDs) && props) {
3163
- const clicks = ctx.adClickIDs;
3164
- if (clicks.gclid && !props.gclid) props.gclid = clicks.gclid;
3165
- if (clicks.gbraid && !props.gbraid) props.gbraid = clicks.gbraid;
3166
- if (clicks.wbraid && !props.wbraid) props.wbraid = clicks.wbraid;
3167
- if (clicks.fbclid && !props.fbclid) props.fbclid = clicks.fbclid;
3168
- const clickId = clicks.gclid || clicks.fbclid || clicks.ctwa_clid || clicks.msclkid || clicks.ttclid;
3169
- if (clickId && !props.campaign_click_id) props.campaign_click_id = clickId;
3170
- }
3171
- if (props && this.session) {
3172
- const link = this.session.getLinkAttribution();
3173
- if (link.aegis_code) {
3174
- for (const [key, value] of Object.entries(link)) {
3175
- if (value && !props[key]) props[key] = value;
3176
- }
3177
- }
3178
- }
3179
- if (props) {
3180
- const meta = captureMetaCookies();
3181
- if (meta.fbp && !props.fbp) props.fbp = meta.fbp;
3182
- if (meta.fbc && !props.fbc) props.fbc = meta.fbc;
3183
- if (meta.fbclid && !props.fbclid) props.fbclid = meta.fbclid;
3184
- }
3185
- logger.debug("Capturing event:", redactMetaCookies(event));
3186
- const transformedEvent = await this.plugins.executeHookChain(
3187
- "beforeEventCapture",
3188
- event
3189
- );
3190
- if (!transformedEvent) {
3191
- logger.debug("Event cancelled by plugin");
3192
- return;
3193
- }
3194
- this.queue.push(transformedEvent);
3195
- this.session.incrementEventCount();
3196
- await this.plugins.executeHook("afterEventCapture", transformedEvent);
3197
- }
3198
- /**
3199
- * Emit a coalesced meta-event reporting client-side rate-limit drops.
3200
- * Bypasses the limiter (see captureEvent guard).
3201
- */
3202
- emitRateLimitMeta(count, sampleEventName) {
3203
- var _a, _b;
3204
- if (!((_a = this.config) == null ? void 0 : _a.initialized) || !this.session || !this.identity) return;
3205
- const event = {
3206
- type: "track",
3207
- event: "aegis.client.rate_limited",
3208
- properties: {
3209
- dropped_count: count,
3210
- sample_event_name: sampleEventName,
3211
- burst_capacity: this.config.rate_limit_burst,
3212
- refill_per_second: this.config.rate_limit_per_second
3213
- },
3214
- messageId: generateMessageId(),
3215
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3216
- anonymousId: this.identity.getAnonymousId(),
3217
- userId: this.identity.getUserId() || void 0,
3218
- sessionId: this.session.getSessionId(),
3219
- context: buildContext(this.config, this.session)
3220
- };
3221
- (_b = this.queue) == null ? void 0 : _b.push(event);
3222
- }
3223
- reset() {
3224
- if (!this.assertInitialized()) return;
3225
- this.identity.reset();
3226
- this._user = null;
3227
- logger.info("User identity reset");
3228
- }
3229
- /**
3230
- * Ingest the event-governance hint returned from `/v1/sdk/bootstrap`.
3231
- *
3232
- * Callers (Shopify pixel, snippet, react integration) run `bootstrap()`
3233
- * themselves and pass the resulting `eventGovernance` field here so the
3234
- * SDK can self-throttle novel event names before they hit the gateway.
3235
- *
3236
- * Passing null/undefined disables governance (Enterprise plan / outage
3237
- * fail-open). Safe to call before `init()` — the hint is stored and
3238
- * applied as soon as init completes.
3239
- */
3240
- ingestGovernanceHint(hint) {
3241
- this.nameGovernor.ingestHint(hint ?? null);
3242
- logger.debug("Governance hint ingested", hint ? {
3243
- k: hint.k,
3244
- m: hint.m,
3245
- remaining: hint.remaining_new_names
3246
- } : "disabled");
3247
- }
3248
- /**
3249
- * Drain the client-side drop counter and emit a meta-event. Called
3250
- * periodically by the queue flush path so ops dashboards see novel-name
3251
- * amplification patterns in near-real-time.
3252
- */
3253
- emitGovernanceDropMeta() {
3254
- var _a, _b;
3255
- if (!((_a = this.config) == null ? void 0 : _a.initialized) || !this.session || !this.identity) return;
3256
- const report = this.nameGovernor.drainDropReport();
3257
- if (!report) return;
3258
- const event = {
3259
- type: "track",
3260
- event: "aegis.client.name_governor_dropped",
3261
- properties: {
3262
- dropped_count: report.total,
3263
- distinct_names: Object.keys(report.events).length,
3264
- // Cap the payload — a runaway loop could produce thousands of names;
3265
- // we ship the top 10 for diagnostics and the total for counting.
3266
- sample_names: Object.entries(report.events).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([name, count]) => ({ name, count })),
3267
- window_start: report.since,
3268
- window_end: Date.now()
3269
- },
3270
- messageId: generateMessageId(),
3271
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3272
- anonymousId: this.identity.getAnonymousId(),
3273
- userId: this.identity.getUserId() || void 0,
3274
- sessionId: this.session.getSessionId(),
3275
- context: buildContext(this.config, this.session)
3276
- };
3277
- (_b = this.queue) == null ? void 0 : _b.push(event);
3278
- }
3279
- async flush() {
3280
- if (!this.assertInitialized()) return;
3281
- this.emitGovernanceDropMeta();
3282
- await this.queue.flush();
3283
- }
3284
- getAnonymousId() {
3285
- if (!this.identity) return null;
3286
- return this.identity.getAnonymousId();
3287
- }
3288
- getUserId() {
3289
- if (!this.identity) return null;
3290
- return this.identity.getUserId();
3291
- }
3292
- /**
3293
- * C1b: register the server-minted proof for the CURRENT contact (as
3294
- * returned by `getUserId()`). A host wiring `AegisMessageRuntime` calls
3295
- * this alongside `runtime.updateContactId(id, token)` so `getRewards()`
3296
- * can also present `X-Contact-Token`. Pass `undefined` to clear it (e.g.
3297
- * on logout, or when the id changes without a fresh token).
3298
- */
3299
- setContactToken(token) {
3300
- this.contactToken = token;
3301
- }
3302
- /**
3303
- * The Aegis short-link attribution captured from this visitor's landing URL
3304
- * (`aegis_code` + the link row's `aegis_campaign` / `aegis_journey` /
3305
- * `aegis_playbook` / `aegis_button`), or `{}` when they did not arrive
3306
- * through one. Hosts forward `aegis_code` with an order so the order can be
3307
- * joined to the send that brought the shopper. Never contains `utm_*`, `k`,
3308
- * `t` or `u`.
3309
- */
3310
- getLinkAttribution() {
3311
- if (!this.session) return {};
3312
- return { ...this.session.getLinkAttribution() };
3313
- }
3314
- /**
3315
- * Fetch the identified contact's REDEEMABLE rewards — coupon codes the
3316
- * storefront can auto-apply at checkout + loyalty points balance/tier.
3317
- * Mirrors `GET /api/v1/sdk/entitlements`. Returns `null` when the SDK is
3318
- * uninitialised, the contact is anonymous, the endpoint returns 204 (nothing
3319
- * to show), or on any network/HTTP error — callers degrade gracefully.
3320
- *
3321
- * Scoped to the identified contact (own grants only) and, when an outlet is
3322
- * resolvable (`getEffectiveLocationId()`), to org-wide + that outlet's grants.
3323
- */
3324
- async getRewards() {
3325
- const config = this.config;
3326
- if (!config || !config.api_host || !this.identity) return null;
3327
- const contactId = this.identity.getUserId();
3328
- if (!contactId) return null;
3329
- const apiHost = config.api_host.replace(/\/$/, "");
3330
- const headers = {
3331
- "X-Aegis-Write-Key": config.write_key,
3332
- "X-Contact-ID": contactId,
3333
- "X-User-ID": contactId
3334
- };
3335
- if (this.contactToken) headers["X-Contact-Token"] = this.contactToken;
3336
- const orgId = config.organization_id;
3337
- if (orgId) headers["X-Organization-ID"] = orgId;
3338
- const location = this.getEffectiveLocationId();
3339
- if (location) headers["X-Location-Id"] = location;
3340
- try {
3341
- const resp = await fetch(`${apiHost}/api/v1/sdk/entitlements`, {
3342
- method: "GET",
3343
- headers,
3344
- credentials: "include"
3345
- });
3346
- if (resp.status === 204 || !resp.ok) return null;
3347
- return await resp.json();
3348
- } catch (err) {
3349
- logger.warn("getRewards() failed", err);
3350
- return null;
3351
- }
3352
- }
3353
- getSessionId() {
3354
- if (!this.session) return null;
3355
- return this.session.getSessionId();
3356
- }
3357
- /**
3358
- * B1.2 — Tenant-Pixel coexistence handoff. Returns the messageId of the
3359
- * most recent `track('<eventName>', ...)` call, or null if no event of
3360
- * that name has been tracked since SDK init.
3361
- *
3362
- * Use this from a tenant's own Meta Pixel script (e.g. via GTM) to pass
3363
- * the same event_id to `fbq('track', '<eventName>', payload, {eventID: id})`.
3364
- * Meta dedups on (pixel_id, event_name, event_id) so the browser-fired
3365
- * Pixel event and our server-side CAPI event collapse to one — preventing
3366
- * double-counting.
3367
- *
3368
- * Event names are case-sensitive ("Purchase" ≠ "purchase") — Meta's dedup
3369
- * is also case-sensitive, so be exact.
3370
- *
3371
- * See docs/integrations/META_PIXEL_COEXISTENCE.md.
3372
- */
3373
- lastEventId(eventName) {
3374
- return this._lastEventIds.get(eventName) ?? null;
3375
- }
3376
- debug(enable = true) {
3377
- if (enable) {
3378
- logger.enable();
3379
- } else {
3380
- logger.disable();
3381
- }
3382
- if (this.config) {
3383
- this.config.debug = enable;
3384
- }
3385
- }
3386
- setCell(cellEndpoint) {
3387
- if (!this.config) {
3388
- logger.warn("SDK not initialized");
3389
- return;
3390
- }
3391
- const existingCell = this.config.cell_endpoints.find(
3392
- (c) => c.region === cellEndpoint.region
3393
- );
3394
- if (existingCell) {
3395
- Object.assign(existingCell, cellEndpoint);
3396
- } else {
3397
- this.config.cell_endpoints.push(cellEndpoint);
3398
- }
3399
- logger.info("Cell endpoint configured:", cellEndpoint);
3400
- }
3401
- getCellInfo() {
3402
- if (!this.transport) return null;
3403
- return {
3404
- activeCell: this.transport.getActiveCell(),
3405
- latencies: this.transport.getRegionLatencies()
3406
- };
3407
- }
3408
- setConsent(preferences) {
3409
- if (!this.consent) {
3410
- logger.warn("Consent manager not initialized");
3411
- return;
3412
- }
3413
- this.consent.setConsent(preferences);
3414
- logger.info("Consent preferences updated");
3415
- }
3416
- grantConsent(category) {
3417
- if (!this.consent) {
3418
- logger.warn("Consent manager not initialized");
3419
- return;
3420
- }
3421
- if (category) {
3422
- this.consent.setConsent({ [category]: true });
3423
- } else {
3424
- this.consent.grantAll();
3425
- }
3426
- }
3427
- denyConsent(category) {
3428
- if (!this.consent) {
3429
- logger.warn("Consent manager not initialized");
3430
- return;
3431
- }
3432
- if (category) {
3433
- this.consent.setConsent({ [category]: false });
3434
- } else {
3435
- this.consent.denyAll();
3436
- }
3437
- }
3438
- hasConsent(category) {
3439
- if (!this.consent) {
3440
- return true;
3441
- }
3442
- return this.consent.hasConsent(category);
3443
- }
3444
- getConsentPreferences() {
3445
- if (!this.consent) {
3446
- return null;
3447
- }
3448
- return this.consent.getPreferences();
3449
- }
3450
- onConsentChange(callback) {
3451
- if (!this.consent) {
3452
- logger.warn("Consent manager not initialized");
3453
- return () => {
3454
- };
3455
- }
3456
- return this.consent.onChange(callback);
3457
- }
3458
- assertInitialized() {
3459
- var _a;
3460
- if (!((_a = this.config) == null ? void 0 : _a.initialized)) {
3461
- logger.warn("SDK not initialized. Call aegis.init() first.");
3462
- return false;
3463
- }
3464
- return true;
3465
- }
3466
- get ecommerce() {
3467
- if (!this._ecommerce) {
3468
- this._ecommerce = new EcommerceTracker(this);
3469
- }
3470
- return this._ecommerce;
3471
- }
3472
- get user() {
3473
- if (!this._user) {
3474
- this._user = new UserNamespace(this);
3475
- }
3476
- return this._user;
3477
- }
3478
- destroy() {
3479
- this.stopSPATracking();
3480
- if (this.queue) {
3481
- this.queue.destroy();
3482
- }
3483
- if (this.session) {
3484
- this.session.destroy();
3485
- }
3486
- if (this.transport) {
3487
- this.transport.destroy();
3488
- }
3489
- if (this.rateLimiter) {
3490
- this.rateLimiter.destroy();
3491
- this.rateLimiter = null;
3492
- }
3493
- this.plugins.clear();
3494
- logger.info("SDK destroyed");
3495
- }
3496
- }
3497
- export {
3498
- Aegis as A,
3499
- BloomFilter as B,
3500
- EcommerceTracker as E,
3501
- NameGovernor as N,
3502
- RateLimiter as R,
3503
- Storage as S,
3504
- UserNamespace as U,
3505
- logger as l,
3506
- murmurhash3_x86_32 as m,
3507
- redactUrl as r
3508
- };
3509
- //# sourceMappingURL=analytics-C-Go3Pm5.mjs.map