@active-reach/web-sdk 1.43.1 → 1.43.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aegis-push.min.js +2 -0
- package/dist/aegis-push.min.js.map +1 -0
- package/dist/aegis-sw.js +1 -0
- package/dist/aegis.min.js +2 -0
- package/dist/aegis.min.js.map +1 -0
- package/dist/analytics-C-Go3Pm5.mjs +3509 -0
- package/dist/analytics-C-Go3Pm5.mjs.map +1 -0
- package/dist/cdn.d.ts +26 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/chat/AegisChat.d.ts +180 -0
- package/dist/chat/AegisChat.d.ts.map +1 -0
- package/dist/chat/index.d.ts +3 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/core/analytics.d.ts +320 -0
- package/dist/core/analytics.d.ts.map +1 -0
- package/dist/core/bootstrap.d.ts +96 -0
- package/dist/core/bootstrap.d.ts.map +1 -0
- package/dist/core/prefetch-bundle-client.d.ts +175 -0
- package/dist/core/prefetch-bundle-client.d.ts.map +1 -0
- package/dist/core/queue.d.ts +35 -0
- package/dist/core/queue.d.ts.map +1 -0
- package/dist/core/rate-limiter.d.ts +45 -0
- package/dist/core/rate-limiter.d.ts.map +1 -0
- package/dist/core/sdk-config-poller.d.ts +59 -0
- package/dist/core/sdk-config-poller.d.ts.map +1 -0
- package/dist/core/session.d.ts +52 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/transport.d.ts +50 -0
- package/dist/core/transport.d.ts.map +1 -0
- package/dist/core/user-namespace.d.ts +48 -0
- package/dist/core/user-namespace.d.ts.map +1 -0
- package/dist/ecommerce/index.d.ts +94 -0
- package/dist/ecommerce/index.d.ts.map +1 -0
- package/dist/ecommerce/types.d.ts +81 -0
- package/dist/ecommerce/types.d.ts.map +1 -0
- package/dist/governance/bloom-filter.d.ts +47 -0
- package/dist/governance/bloom-filter.d.ts.map +1 -0
- package/dist/governance/index.d.ts +8 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/murmur3.d.ts +43 -0
- package/dist/governance/murmur3.d.ts.map +1 -0
- package/dist/governance/name-governor.d.ts +98 -0
- package/dist/governance/name-governor.d.ts.map +1 -0
- package/dist/governance/trait-governor.d.ts +63 -0
- package/dist/governance/trait-governor.d.ts.map +1 -0
- package/dist/inapp/AegisInAppManager.d.ts +894 -0
- package/dist/inapp/AegisInAppManager.d.ts.map +1 -0
- package/dist/inapp/actions.d.ts +91 -0
- package/dist/inapp/actions.d.ts.map +1 -0
- package/dist/inapp/devicePairing.d.ts +70 -0
- package/dist/inapp/devicePairing.d.ts.map +1 -0
- package/dist/inapp/eval-throttle.d.ts +33 -0
- package/dist/inapp/eval-throttle.d.ts.map +1 -0
- package/dist/inapp/font.d.ts +16 -0
- package/dist/inapp/font.d.ts.map +1 -0
- package/dist/inapp/index.d.ts +8 -0
- package/dist/inapp/index.d.ts.map +1 -0
- package/dist/inapp/lucide-svg.d.ts +46 -0
- package/dist/inapp/lucide-svg.d.ts.map +1 -0
- package/dist/inapp/renderPreview.d.ts +40 -0
- package/dist/inapp/renderPreview.d.ts.map +1 -0
- package/dist/inapp/renderers/active-web-chat.d.ts +45 -0
- package/dist/inapp/renderers/active-web-chat.d.ts.map +1 -0
- package/dist/inapp/renderers/card-style.d.ts +31 -0
- package/dist/inapp/renderers/card-style.d.ts.map +1 -0
- package/dist/inapp/renderers/card-template.d.ts +233 -0
- package/dist/inapp/renderers/card-template.d.ts.map +1 -0
- package/dist/inapp/renderers/carousel-cards.d.ts +15 -0
- package/dist/inapp/renderers/carousel-cards.d.ts.map +1 -0
- package/dist/inapp/renderers/coachmark-tour.d.ts +24 -0
- package/dist/inapp/renderers/coachmark-tour.d.ts.map +1 -0
- package/dist/inapp/renderers/games.d.ts +27 -0
- package/dist/inapp/renderers/games.d.ts.map +1 -0
- package/dist/inapp/renderers/hero.d.ts +23 -0
- package/dist/inapp/renderers/hero.d.ts.map +1 -0
- package/dist/inapp/renderers/index.d.ts +22 -0
- package/dist/inapp/renderers/index.d.ts.map +1 -0
- package/dist/inapp/renderers/interactive-contract.d.ts +48 -0
- package/dist/inapp/renderers/interactive-contract.d.ts.map +1 -0
- package/dist/inapp/renderers/product-recommendation.d.ts +37 -0
- package/dist/inapp/renderers/product-recommendation.d.ts.map +1 -0
- package/dist/inapp/renderers/progress-bar.d.ts +38 -0
- package/dist/inapp/renderers/progress-bar.d.ts.map +1 -0
- package/dist/inapp/renderers/progress-core.d.ts +80 -0
- package/dist/inapp/renderers/progress-core.d.ts.map +1 -0
- package/dist/inapp/renderers/reviews.d.ts +38 -0
- package/dist/inapp/renderers/reviews.d.ts.map +1 -0
- package/dist/inapp/renderers/scratch-engine.d.ts +66 -0
- package/dist/inapp/renderers/scratch-engine.d.ts.map +1 -0
- package/dist/inapp/renderers/sticky-bar.d.ts +14 -0
- package/dist/inapp/renderers/sticky-bar.d.ts.map +1 -0
- package/dist/inapp/renderers/stories.d.ts +31 -0
- package/dist/inapp/renderers/stories.d.ts.map +1 -0
- package/dist/inapp/renderers/types.d.ts +152 -0
- package/dist/inapp/renderers/types.d.ts.map +1 -0
- package/dist/inapp/renderers/video.d.ts +20 -0
- package/dist/inapp/renderers/video.d.ts.map +1 -0
- package/dist/inbox/AegisInbox.d.ts +106 -0
- package/dist/inbox/AegisInbox.d.ts.map +1 -0
- package/dist/inbox/index.d.ts +3 -0
- package/dist/inbox/index.d.ts.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15546 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/react.d.ts +19 -0
- package/dist/integrations/react.d.ts.map +1 -0
- package/dist/loyalty/AegisLoyaltyManager.d.ts +120 -0
- package/dist/loyalty/AegisLoyaltyManager.d.ts.map +1 -0
- package/dist/loyalty/index.d.ts +3 -0
- package/dist/loyalty/index.d.ts.map +1 -0
- package/dist/plugins/registry.d.ts +13 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/push/AegisWebPush.d.ts +199 -0
- package/dist/push/AegisWebPush.d.ts.map +1 -0
- package/dist/push/AegisWebPush.js +542 -0
- package/dist/push/AegisWebPush.js.map +1 -0
- package/dist/push/cdn-push.d.ts +21 -0
- package/dist/push/cdn-push.d.ts.map +1 -0
- package/dist/react.js +70 -0
- package/dist/react.js.map +1 -0
- package/dist/runtime/AegisMessageRuntime.d.ts +276 -0
- package/dist/runtime/AegisMessageRuntime.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/snippet.min.js +1 -0
- package/dist/state/intent_ledger.d.ts +136 -0
- package/dist/state/intent_ledger.d.ts.map +1 -0
- package/dist/triggers/ContactScoresFetcher.d.ts +113 -0
- package/dist/triggers/ContactScoresFetcher.d.ts.map +1 -0
- package/dist/triggers/IntentRuleEvaluator.d.ts +213 -0
- package/dist/triggers/IntentRuleEvaluator.d.ts.map +1 -0
- package/dist/triggers/IntentSnapshotCollector.d.ts +83 -0
- package/dist/triggers/IntentSnapshotCollector.d.ts.map +1 -0
- package/dist/triggers/SelectorBinder.d.ts +118 -0
- package/dist/triggers/SelectorBinder.d.ts.map +1 -0
- package/dist/triggers/TriggerEngine.d.ts +296 -0
- package/dist/triggers/TriggerEngine.d.ts.map +1 -0
- package/dist/triggers/behaviouralTriggers.d.ts +44 -0
- package/dist/triggers/behaviouralTriggers.d.ts.map +1 -0
- package/dist/triggers/index.d.ts +11 -0
- package/dist/triggers/index.d.ts.map +1 -0
- package/dist/triggers/microIntentTriggers.d.ts +80 -0
- package/dist/triggers/microIntentTriggers.d.ts.map +1 -0
- package/dist/types/config.d.ts +77 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/events.d.ts +147 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/plugin.d.ts +21 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/type-safe-events.d.ts +14 -0
- package/dist/types/type-safe-events.d.ts.map +1 -0
- package/dist/utils/canonical-identity.d.ts +29 -0
- package/dist/utils/canonical-identity.d.ts.map +1 -0
- package/dist/utils/consent.d.ts +40 -0
- package/dist/utils/consent.d.ts.map +1 -0
- package/dist/utils/debounce.d.ts +3 -0
- package/dist/utils/debounce.d.ts.map +1 -0
- package/dist/utils/device.d.ts +6 -0
- package/dist/utils/device.d.ts.map +1 -0
- package/dist/utils/identity.d.ts +27 -0
- package/dist/utils/identity.d.ts.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +15 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/meta-cookies.d.ts +65 -0
- package/dist/utils/meta-cookies.d.ts.map +1 -0
- package/dist/utils/redact-url.d.ts +56 -0
- package/dist/utils/redact-url.d.ts.map +1 -0
- package/dist/utils/storage.d.ts +20 -0
- package/dist/utils/storage.d.ts.map +1 -0
- package/dist/utils/url-parser.d.ts +61 -0
- package/dist/utils/url-parser.d.ts.map +1 -0
- package/dist/utils/uuid.d.ts +4 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/dist/utils/visitor.d.ts +43 -0
- package/dist/utils/visitor.d.ts.map +1 -0
- package/dist/widgets/AegisWidgetManager.d.ts +209 -0
- package/dist/widgets/AegisWidgetManager.d.ts.map +1 -0
- package/dist/widgets/index.d.ts +3 -0
- package/dist/widgets/index.d.ts.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,3509 @@
|
|
|
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
|