@analyticscli/sdk 0.1.0-preview.10
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/LICENSE +21 -0
- package/README.md +239 -0
- package/dist/browser.cjs +1978 -0
- package/dist/browser.d.cts +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +36 -0
- package/dist/chunk-6EPJZLLK.js +1937 -0
- package/dist/index.cjs +1978 -0
- package/dist/index.d.cts +587 -0
- package/dist/index.d.ts +587 -0
- package/dist/index.js +36 -0
- package/dist/react-native.cjs +1978 -0
- package/dist/react-native.d.cts +1 -0
- package/dist/react-native.d.ts +1 -0
- package/dist/react-native.js +36 -0
- package/package.json +58 -0
package/dist/browser.cjs
ADDED
|
@@ -0,0 +1,1978 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/browser.ts
|
|
21
|
+
var browser_exports = {};
|
|
22
|
+
__export(browser_exports, {
|
|
23
|
+
AnalyticsClient: () => AnalyticsClient,
|
|
24
|
+
ONBOARDING_EVENTS: () => ONBOARDING_EVENTS,
|
|
25
|
+
ONBOARDING_PROGRESS_EVENT_ORDER: () => ONBOARDING_PROGRESS_EVENT_ORDER,
|
|
26
|
+
ONBOARDING_SCREEN_EVENT_PREFIXES: () => ONBOARDING_SCREEN_EVENT_PREFIXES,
|
|
27
|
+
ONBOARDING_SURVEY_EVENTS: () => ONBOARDING_SURVEY_EVENTS,
|
|
28
|
+
PAYWALL_ANCHOR_EVENT_CANDIDATES: () => PAYWALL_ANCHOR_EVENT_CANDIDATES,
|
|
29
|
+
PAYWALL_EVENTS: () => PAYWALL_EVENTS,
|
|
30
|
+
PAYWALL_JOURNEY_EVENT_ORDER: () => PAYWALL_JOURNEY_EVENT_ORDER,
|
|
31
|
+
PAYWALL_SKIP_EVENT_CANDIDATES: () => PAYWALL_SKIP_EVENT_CANDIDATES,
|
|
32
|
+
PURCHASE_EVENTS: () => PURCHASE_EVENTS,
|
|
33
|
+
PURCHASE_SUCCESS_EVENT_CANDIDATES: () => PURCHASE_SUCCESS_EVENT_CANDIDATES,
|
|
34
|
+
createAnalyticsContext: () => createAnalyticsContext,
|
|
35
|
+
init: () => init,
|
|
36
|
+
initAsync: () => initAsync,
|
|
37
|
+
initConsentFirst: () => initConsentFirst,
|
|
38
|
+
initConsentFirstAsync: () => initConsentFirstAsync
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(browser_exports);
|
|
41
|
+
|
|
42
|
+
// src/sdk-contract.ts
|
|
43
|
+
var DEFAULT_INGEST_LIMITS = {
|
|
44
|
+
maxBatchSize: 50,
|
|
45
|
+
maxPayloadBytes: 128 * 1024
|
|
46
|
+
};
|
|
47
|
+
var ONBOARDING_EVENTS = {
|
|
48
|
+
START: "onboarding:start",
|
|
49
|
+
STEP_VIEW: "onboarding:step_view",
|
|
50
|
+
STEP_COMPLETE: "onboarding:step_complete",
|
|
51
|
+
COMPLETE: "onboarding:complete",
|
|
52
|
+
SKIP: "onboarding:skip"
|
|
53
|
+
};
|
|
54
|
+
var PAYWALL_EVENTS = {
|
|
55
|
+
SHOWN: "paywall:shown",
|
|
56
|
+
SKIP: "paywall:skip"
|
|
57
|
+
};
|
|
58
|
+
var PURCHASE_EVENTS = {
|
|
59
|
+
STARTED: "purchase:started",
|
|
60
|
+
SUCCESS: "purchase:success",
|
|
61
|
+
FAILED: "purchase:failed",
|
|
62
|
+
CANCEL: "purchase:cancel"
|
|
63
|
+
};
|
|
64
|
+
var ONBOARDING_SURVEY_EVENTS = {
|
|
65
|
+
RESPONSE: "onboarding:survey_response"
|
|
66
|
+
};
|
|
67
|
+
var ONBOARDING_PROGRESS_EVENT_ORDER = [
|
|
68
|
+
ONBOARDING_EVENTS.COMPLETE,
|
|
69
|
+
ONBOARDING_EVENTS.SKIP
|
|
70
|
+
];
|
|
71
|
+
var PAYWALL_JOURNEY_EVENT_ORDER = [
|
|
72
|
+
PAYWALL_EVENTS.SHOWN,
|
|
73
|
+
PAYWALL_EVENTS.SKIP,
|
|
74
|
+
PURCHASE_EVENTS.SUCCESS,
|
|
75
|
+
PURCHASE_EVENTS.FAILED
|
|
76
|
+
];
|
|
77
|
+
var ONBOARDING_SCREEN_EVENT_PREFIXES = ["screen:onboarding", "screen:onboarding_"];
|
|
78
|
+
var PAYWALL_ANCHOR_EVENT_CANDIDATES = [PAYWALL_EVENTS.SHOWN];
|
|
79
|
+
var PAYWALL_SKIP_EVENT_CANDIDATES = [PAYWALL_EVENTS.SKIP];
|
|
80
|
+
var PURCHASE_SUCCESS_EVENT_CANDIDATES = [PURCHASE_EVENTS.SUCCESS];
|
|
81
|
+
var RESERVED_PII_KEYS = /* @__PURE__ */ new Set([
|
|
82
|
+
"email",
|
|
83
|
+
"phone",
|
|
84
|
+
"firstName",
|
|
85
|
+
"lastName",
|
|
86
|
+
"fullName",
|
|
87
|
+
"address",
|
|
88
|
+
"ssn",
|
|
89
|
+
"creditCard"
|
|
90
|
+
]);
|
|
91
|
+
var EVENT_NAME_REGEX = /^[a-zA-Z0-9_:\-.]{1,100}$/;
|
|
92
|
+
|
|
93
|
+
// src/ingest-validation.ts
|
|
94
|
+
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
95
|
+
var ISO_DATETIME_WITH_OFFSET_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
96
|
+
var TYPE_VALUES = /* @__PURE__ */ new Set(["track", "screen", "identify", "feedback"]);
|
|
97
|
+
var isRecord = (value) => {
|
|
98
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
99
|
+
};
|
|
100
|
+
var isStringBetween = (value, min, max) => {
|
|
101
|
+
return typeof value === "string" && value.length >= min && value.length <= max;
|
|
102
|
+
};
|
|
103
|
+
var isOptionalStringMax = (value, max) => {
|
|
104
|
+
return value === void 0 || typeof value === "string" && value.length <= max;
|
|
105
|
+
};
|
|
106
|
+
var isNullableOptionalStringBetween = (value, min, max) => {
|
|
107
|
+
return value === void 0 || value === null || isStringBetween(value, min, max);
|
|
108
|
+
};
|
|
109
|
+
var isIsoDatetimeWithOffset = (value) => {
|
|
110
|
+
if (typeof value !== "string" || !ISO_DATETIME_WITH_OFFSET_REGEX.test(value)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return Number.isFinite(Date.parse(value));
|
|
114
|
+
};
|
|
115
|
+
var validateEvent = (event, index) => {
|
|
116
|
+
if (!isRecord(event)) {
|
|
117
|
+
return { success: false, reason: `events[${index}] is not an object` };
|
|
118
|
+
}
|
|
119
|
+
if (!isStringBetween(event.eventName, 1, 100) || !EVENT_NAME_REGEX.test(event.eventName)) {
|
|
120
|
+
return { success: false, reason: `events[${index}].eventName is invalid` };
|
|
121
|
+
}
|
|
122
|
+
if (!isStringBetween(event.sessionId, 1, 128)) {
|
|
123
|
+
return { success: false, reason: `events[${index}].sessionId is invalid` };
|
|
124
|
+
}
|
|
125
|
+
if (!isStringBetween(event.anonId, 1, 128)) {
|
|
126
|
+
return { success: false, reason: `events[${index}].anonId is invalid` };
|
|
127
|
+
}
|
|
128
|
+
if (!isNullableOptionalStringBetween(event.userId, 1, 128)) {
|
|
129
|
+
return { success: false, reason: `events[${index}].userId is invalid` };
|
|
130
|
+
}
|
|
131
|
+
if (!isRecord(event.properties)) {
|
|
132
|
+
return { success: false, reason: `events[${index}].properties is invalid` };
|
|
133
|
+
}
|
|
134
|
+
if (!isOptionalStringMax(event.platform, 64)) {
|
|
135
|
+
return { success: false, reason: `events[${index}].platform is invalid` };
|
|
136
|
+
}
|
|
137
|
+
if (!isOptionalStringMax(event.projectSurface, 64)) {
|
|
138
|
+
return { success: false, reason: `events[${index}].projectSurface is invalid` };
|
|
139
|
+
}
|
|
140
|
+
if (!isOptionalStringMax(event.appVersion, 64)) {
|
|
141
|
+
return { success: false, reason: `events[${index}].appVersion is invalid` };
|
|
142
|
+
}
|
|
143
|
+
if (!isOptionalStringMax(event.appBuild, 64)) {
|
|
144
|
+
return { success: false, reason: `events[${index}].appBuild is invalid` };
|
|
145
|
+
}
|
|
146
|
+
if (!isOptionalStringMax(event.osName, 64)) {
|
|
147
|
+
return { success: false, reason: `events[${index}].osName is invalid` };
|
|
148
|
+
}
|
|
149
|
+
if (!isOptionalStringMax(event.osVersion, 64)) {
|
|
150
|
+
return { success: false, reason: `events[${index}].osVersion is invalid` };
|
|
151
|
+
}
|
|
152
|
+
if (!isOptionalStringMax(event.deviceModel, 128)) {
|
|
153
|
+
return { success: false, reason: `events[${index}].deviceModel is invalid` };
|
|
154
|
+
}
|
|
155
|
+
if (!isOptionalStringMax(event.deviceManufacturer, 128)) {
|
|
156
|
+
return { success: false, reason: `events[${index}].deviceManufacturer is invalid` };
|
|
157
|
+
}
|
|
158
|
+
if (!isOptionalStringMax(event.deviceType, 32)) {
|
|
159
|
+
return { success: false, reason: `events[${index}].deviceType is invalid` };
|
|
160
|
+
}
|
|
161
|
+
if (!isOptionalStringMax(event.locale, 32)) {
|
|
162
|
+
return { success: false, reason: `events[${index}].locale is invalid` };
|
|
163
|
+
}
|
|
164
|
+
if (!isOptionalStringMax(event.country, 8)) {
|
|
165
|
+
return { success: false, reason: `events[${index}].country is invalid` };
|
|
166
|
+
}
|
|
167
|
+
if (!isOptionalStringMax(event.region, 96)) {
|
|
168
|
+
return { success: false, reason: `events[${index}].region is invalid` };
|
|
169
|
+
}
|
|
170
|
+
if (!isOptionalStringMax(event.city, 96)) {
|
|
171
|
+
return { success: false, reason: `events[${index}].city is invalid` };
|
|
172
|
+
}
|
|
173
|
+
if (!isOptionalStringMax(event.timezone, 64)) {
|
|
174
|
+
return { success: false, reason: `events[${index}].timezone is invalid` };
|
|
175
|
+
}
|
|
176
|
+
if (!isOptionalStringMax(event.networkType, 32)) {
|
|
177
|
+
return { success: false, reason: `events[${index}].networkType is invalid` };
|
|
178
|
+
}
|
|
179
|
+
if (!isOptionalStringMax(event.carrier, 64)) {
|
|
180
|
+
return { success: false, reason: `events[${index}].carrier is invalid` };
|
|
181
|
+
}
|
|
182
|
+
if (!isOptionalStringMax(event.installSource, 64)) {
|
|
183
|
+
return { success: false, reason: `events[${index}].installSource is invalid` };
|
|
184
|
+
}
|
|
185
|
+
if (event.eventId !== void 0 && (typeof event.eventId !== "string" || !UUID_REGEX.test(event.eventId))) {
|
|
186
|
+
return { success: false, reason: `events[${index}].eventId is invalid` };
|
|
187
|
+
}
|
|
188
|
+
if (event.ts !== void 0 && !isIsoDatetimeWithOffset(event.ts)) {
|
|
189
|
+
return { success: false, reason: `events[${index}].ts is invalid` };
|
|
190
|
+
}
|
|
191
|
+
if (event.type !== void 0 && (typeof event.type !== "string" || !TYPE_VALUES.has(event.type))) {
|
|
192
|
+
return { success: false, reason: `events[${index}].type is invalid` };
|
|
193
|
+
}
|
|
194
|
+
return { success: true };
|
|
195
|
+
};
|
|
196
|
+
var validateIngestBatch = (batch) => {
|
|
197
|
+
if (batch.sentAt !== void 0 && !isIsoDatetimeWithOffset(batch.sentAt)) {
|
|
198
|
+
return { success: false, reason: "sentAt is invalid" };
|
|
199
|
+
}
|
|
200
|
+
if (!Array.isArray(batch.events)) {
|
|
201
|
+
return { success: false, reason: "events is invalid" };
|
|
202
|
+
}
|
|
203
|
+
if (batch.events.length === 0 || batch.events.length > DEFAULT_INGEST_LIMITS.maxBatchSize) {
|
|
204
|
+
return { success: false, reason: "events length is out of bounds" };
|
|
205
|
+
}
|
|
206
|
+
for (let index = 0; index < batch.events.length; index += 1) {
|
|
207
|
+
const result = validateEvent(batch.events[index], index);
|
|
208
|
+
if (!result.success) {
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { success: true };
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/constants.ts
|
|
216
|
+
var DEVICE_ID_KEY = "pi_device_id";
|
|
217
|
+
var SESSION_ID_KEY = "pi_session_id";
|
|
218
|
+
var LAST_SEEN_KEY = "pi_last_seen";
|
|
219
|
+
var SESSION_EVENT_SEQ_PREFIX = "pi_session_event_seq:";
|
|
220
|
+
var ONBOARDING_STEP_VIEW_STATE_KEY = "pi_onboarding_step_views";
|
|
221
|
+
var DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
222
|
+
var DEFAULT_SCREEN_VIEW_DEDUPE_WINDOW_MS = 1200;
|
|
223
|
+
var DEFAULT_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 400;
|
|
224
|
+
var DEFAULT_COLLECTOR_ENDPOINT = "https://collector.analyticscli.com";
|
|
225
|
+
|
|
226
|
+
// src/helpers.ts
|
|
227
|
+
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
228
|
+
var isPromiseLike = (value) => {
|
|
229
|
+
return typeof value === "object" && value !== null && "then" in value;
|
|
230
|
+
};
|
|
231
|
+
var normalizeStoredValue = (value) => {
|
|
232
|
+
if (typeof value === "string" || value === null) {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
};
|
|
237
|
+
var randomId = () => {
|
|
238
|
+
if (globalThis.crypto?.randomUUID) {
|
|
239
|
+
return globalThis.crypto.randomUUID();
|
|
240
|
+
}
|
|
241
|
+
const bytes = new Uint8Array(16);
|
|
242
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
243
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
244
|
+
} else {
|
|
245
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
246
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const byte6 = bytes[6] ?? 0;
|
|
250
|
+
const byte8 = bytes[8] ?? 0;
|
|
251
|
+
bytes[6] = byte6 & 15 | 64;
|
|
252
|
+
bytes[8] = byte8 & 63 | 128;
|
|
253
|
+
let output = "";
|
|
254
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
255
|
+
const byte = bytes[index] ?? 0;
|
|
256
|
+
const hex = byte.toString(16).padStart(2, "0");
|
|
257
|
+
output += hex;
|
|
258
|
+
if (index === 3 || index === 5 || index === 7 || index === 9) {
|
|
259
|
+
output += "-";
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return output;
|
|
263
|
+
};
|
|
264
|
+
var readStorageSync = (storage, key) => {
|
|
265
|
+
if (!storage) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const value = storage.getItem(key);
|
|
270
|
+
if (isPromiseLike(value)) {
|
|
271
|
+
void value.catch(() => {
|
|
272
|
+
});
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return normalizeStoredValue(value);
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var readStorageAsync = async (storage, key) => {
|
|
281
|
+
if (!storage) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const value = storage.getItem(key);
|
|
286
|
+
if (isPromiseLike(value)) {
|
|
287
|
+
return normalizeStoredValue(await value.catch(() => null));
|
|
288
|
+
}
|
|
289
|
+
return normalizeStoredValue(value);
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
var writeStorageSync = (storage, key, value) => {
|
|
295
|
+
if (!storage) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const result = storage.setItem(key, value);
|
|
300
|
+
if (isPromiseLike(result)) {
|
|
301
|
+
void result.catch(() => {
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
var readTrimmedString = (value) => {
|
|
308
|
+
if (typeof value !== "string") {
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
const trimmed = value.trim();
|
|
312
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
313
|
+
};
|
|
314
|
+
var normalizePlatformValue = (value) => {
|
|
315
|
+
const normalized = readTrimmedString(value)?.toLowerCase();
|
|
316
|
+
if (normalized === "web" || normalized === "ios" || normalized === "android" || normalized === "mac" || normalized === "windows") {
|
|
317
|
+
return normalized;
|
|
318
|
+
}
|
|
319
|
+
if (normalized === "macos" || normalized === "osx" || normalized === "darwin") {
|
|
320
|
+
return "mac";
|
|
321
|
+
}
|
|
322
|
+
if (normalized === "win32") {
|
|
323
|
+
return "windows";
|
|
324
|
+
}
|
|
325
|
+
return void 0;
|
|
326
|
+
};
|
|
327
|
+
var readGlobalPlatformOs = () => {
|
|
328
|
+
const withPlatform = globalThis;
|
|
329
|
+
const fromPlatform = normalizePlatformValue(withPlatform.Platform?.OS);
|
|
330
|
+
if (fromPlatform) {
|
|
331
|
+
return fromPlatform;
|
|
332
|
+
}
|
|
333
|
+
const withExpoPlatform = globalThis;
|
|
334
|
+
return normalizePlatformValue(withExpoPlatform.expo?.modules?.ExpoPlatform?.osName);
|
|
335
|
+
};
|
|
336
|
+
var detectDefaultPlatform = () => {
|
|
337
|
+
const detectedNativePlatform = readGlobalPlatformOs();
|
|
338
|
+
if (detectedNativePlatform) {
|
|
339
|
+
return detectedNativePlatform;
|
|
340
|
+
}
|
|
341
|
+
if (typeof navigator === "undefined") {
|
|
342
|
+
return void 0;
|
|
343
|
+
}
|
|
344
|
+
if (navigator.product === "ReactNative") {
|
|
345
|
+
return void 0;
|
|
346
|
+
}
|
|
347
|
+
return "web";
|
|
348
|
+
};
|
|
349
|
+
var detectDefaultAppVersion = () => {
|
|
350
|
+
const withExpoModules = globalThis;
|
|
351
|
+
const expoApp = withExpoModules.expo?.modules?.ExpoApplication;
|
|
352
|
+
const candidates = [
|
|
353
|
+
expoApp?.nativeApplicationVersion,
|
|
354
|
+
expoApp?.applicationVersion,
|
|
355
|
+
expoApp?.version
|
|
356
|
+
];
|
|
357
|
+
for (const candidate of candidates) {
|
|
358
|
+
const value = readTrimmedString(candidate);
|
|
359
|
+
if (value) {
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return void 0;
|
|
364
|
+
};
|
|
365
|
+
var detectRuntimeEnv = () => {
|
|
366
|
+
const globalWithDevFlag = globalThis;
|
|
367
|
+
if (typeof globalWithDevFlag.__DEV__ === "boolean") {
|
|
368
|
+
return globalWithDevFlag.__DEV__ ? "development" : "production";
|
|
369
|
+
}
|
|
370
|
+
const globalWithProcess = globalThis;
|
|
371
|
+
const nodeEnv = globalWithProcess.process?.env?.NODE_ENV;
|
|
372
|
+
if (nodeEnv) {
|
|
373
|
+
return nodeEnv === "production" ? "production" : "development";
|
|
374
|
+
}
|
|
375
|
+
if (typeof window !== "undefined" && typeof window.location?.hostname === "string") {
|
|
376
|
+
const hostname = window.location.hostname.toLowerCase();
|
|
377
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname.endsWith(".local") || hostname.endsWith(".test") || hostname.includes("dev") || hostname.includes("staging") || hostname.includes("preview")) {
|
|
378
|
+
return "development";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return "production";
|
|
382
|
+
};
|
|
383
|
+
var decodeComponentSafe = (value) => {
|
|
384
|
+
try {
|
|
385
|
+
return decodeURIComponent(value);
|
|
386
|
+
} catch {
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
var resolveCookieStorageAdapter = (enabled, cookieDomain, cookieMaxAgeSeconds) => {
|
|
391
|
+
if (!enabled || typeof document === "undefined") {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const normalizedDomain = cookieDomain?.trim();
|
|
395
|
+
const getCookie = (key) => {
|
|
396
|
+
const encodedKey = encodeURIComponent(key);
|
|
397
|
+
const cookies = document.cookie ? document.cookie.split(";") : [];
|
|
398
|
+
for (const rawCookie of cookies) {
|
|
399
|
+
const cookie = rawCookie.trim();
|
|
400
|
+
if (!cookie.startsWith(`${encodedKey}=`)) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const rawValue = cookie.slice(encodedKey.length + 1);
|
|
404
|
+
return decodeComponentSafe(rawValue);
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
};
|
|
408
|
+
const setCookie = (key, value) => {
|
|
409
|
+
const attributes = [
|
|
410
|
+
"Path=/",
|
|
411
|
+
"SameSite=Lax",
|
|
412
|
+
`Max-Age=${cookieMaxAgeSeconds}`,
|
|
413
|
+
...normalizedDomain ? [`Domain=${normalizedDomain}`] : [],
|
|
414
|
+
...typeof location !== "undefined" && location.protocol === "https:" ? ["Secure"] : []
|
|
415
|
+
];
|
|
416
|
+
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}; ${attributes.join("; ")}`;
|
|
417
|
+
};
|
|
418
|
+
const removeCookie = (key) => {
|
|
419
|
+
const attributes = [
|
|
420
|
+
"Path=/",
|
|
421
|
+
"SameSite=Lax",
|
|
422
|
+
"Max-Age=0",
|
|
423
|
+
...normalizedDomain ? [`Domain=${normalizedDomain}`] : [],
|
|
424
|
+
...typeof location !== "undefined" && location.protocol === "https:" ? ["Secure"] : []
|
|
425
|
+
];
|
|
426
|
+
document.cookie = `${encodeURIComponent(key)}=; ${attributes.join("; ")}`;
|
|
427
|
+
};
|
|
428
|
+
return {
|
|
429
|
+
getItem: (key) => getCookie(key),
|
|
430
|
+
setItem: (key, value) => setCookie(key, value),
|
|
431
|
+
removeItem: (key) => removeCookie(key)
|
|
432
|
+
};
|
|
433
|
+
};
|
|
434
|
+
var resolveBrowserStorageAdapter = () => {
|
|
435
|
+
if (typeof window === "undefined") {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
let storage;
|
|
439
|
+
try {
|
|
440
|
+
storage = window.localStorage;
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
if (!storage) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
getItem: (key) => storage.getItem(key),
|
|
449
|
+
setItem: (key, value) => storage.setItem(key, value),
|
|
450
|
+
removeItem: (key) => storage.removeItem(key)
|
|
451
|
+
};
|
|
452
|
+
};
|
|
453
|
+
var combineStorageAdapters = (primary, secondary) => {
|
|
454
|
+
return {
|
|
455
|
+
getItem: (key) => {
|
|
456
|
+
const primaryValue = primary.getItem(key);
|
|
457
|
+
if (typeof primaryValue === "string") {
|
|
458
|
+
return primaryValue;
|
|
459
|
+
}
|
|
460
|
+
if (primaryValue === null) {
|
|
461
|
+
const secondaryValue = secondary.getItem(key);
|
|
462
|
+
if (typeof secondaryValue === "string") {
|
|
463
|
+
return secondaryValue;
|
|
464
|
+
}
|
|
465
|
+
return secondaryValue === null ? null : null;
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
},
|
|
469
|
+
setItem: (key, value) => {
|
|
470
|
+
primary.setItem(key, value);
|
|
471
|
+
secondary.setItem(key, value);
|
|
472
|
+
},
|
|
473
|
+
removeItem: (key) => {
|
|
474
|
+
primary.removeItem?.(key);
|
|
475
|
+
secondary.removeItem?.(key);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
};
|
|
479
|
+
var sanitizeProperties = (properties) => {
|
|
480
|
+
if (!properties) {
|
|
481
|
+
return {};
|
|
482
|
+
}
|
|
483
|
+
const entries = Object.entries(properties).filter(([key]) => !RESERVED_PII_KEYS.has(key));
|
|
484
|
+
return Object.fromEntries(entries);
|
|
485
|
+
};
|
|
486
|
+
var toStableKey = (value) => {
|
|
487
|
+
if (typeof value !== "string") return void 0;
|
|
488
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_\-:.]/g, "_");
|
|
489
|
+
if (!normalized) return void 0;
|
|
490
|
+
return normalized.slice(0, 80);
|
|
491
|
+
};
|
|
492
|
+
var toNumericBucket = (value) => {
|
|
493
|
+
if (!Number.isFinite(value)) return "nan";
|
|
494
|
+
if (value < 0) return "lt_0";
|
|
495
|
+
if (value <= 10) return "0_10";
|
|
496
|
+
if (value <= 20) return "11_20";
|
|
497
|
+
if (value <= 30) return "21_30";
|
|
498
|
+
if (value <= 40) return "31_40";
|
|
499
|
+
if (value <= 50) return "41_50";
|
|
500
|
+
if (value <= 100) return "51_100";
|
|
501
|
+
return "gt_100";
|
|
502
|
+
};
|
|
503
|
+
var toTextLengthBucket = (value) => {
|
|
504
|
+
const length = value.trim().length;
|
|
505
|
+
if (length === 0) return "empty";
|
|
506
|
+
if (length <= 10) return "1_10";
|
|
507
|
+
if (length <= 30) return "11_30";
|
|
508
|
+
if (length <= 80) return "31_80";
|
|
509
|
+
return "gt_80";
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// src/survey.ts
|
|
513
|
+
var sanitizeSurveyResponseInput = (input) => {
|
|
514
|
+
const surveyKey = toStableKey(input.surveyKey);
|
|
515
|
+
const questionKey = toStableKey(input.questionKey);
|
|
516
|
+
if (!surveyKey || !questionKey) {
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
const baseProperties = {
|
|
520
|
+
surveyKey,
|
|
521
|
+
questionKey,
|
|
522
|
+
answerType: input.answerType,
|
|
523
|
+
responseProvided: true,
|
|
524
|
+
...input.appVersion ? { appVersion: input.appVersion } : {},
|
|
525
|
+
...input.isNewUser !== void 0 ? { isNewUser: input.isNewUser } : {},
|
|
526
|
+
...input.onboardingFlowId ? { onboardingFlowId: input.onboardingFlowId } : {},
|
|
527
|
+
...input.onboardingFlowVersion !== void 0 ? { onboardingFlowVersion: input.onboardingFlowVersion } : {},
|
|
528
|
+
...input.stepKey ? { stepKey: input.stepKey } : {},
|
|
529
|
+
...input.stepIndex !== void 0 ? { stepIndex: input.stepIndex } : {},
|
|
530
|
+
...input.stepCount !== void 0 ? { stepCount: input.stepCount } : {},
|
|
531
|
+
...input.experimentVariant ? { experimentVariant: input.experimentVariant } : {},
|
|
532
|
+
...input.paywallId ? { paywallId: input.paywallId } : {},
|
|
533
|
+
...sanitizeProperties(input.properties)
|
|
534
|
+
};
|
|
535
|
+
if (input.answerType === "multiple_choice") {
|
|
536
|
+
const keys = (input.responseKeys ?? []).map((value) => toStableKey(value)).filter((value) => Boolean(value)).slice(0, 20);
|
|
537
|
+
return keys.map((responseKey2) => ({
|
|
538
|
+
...baseProperties,
|
|
539
|
+
responseKey: responseKey2
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
if (input.answerType === "single_choice") {
|
|
543
|
+
const responseKey2 = toStableKey(input.responseKey);
|
|
544
|
+
if (!responseKey2) return [];
|
|
545
|
+
return [
|
|
546
|
+
{
|
|
547
|
+
...baseProperties,
|
|
548
|
+
responseKey: responseKey2
|
|
549
|
+
}
|
|
550
|
+
];
|
|
551
|
+
}
|
|
552
|
+
if (input.answerType === "boolean") {
|
|
553
|
+
if (typeof input.responseBoolean !== "boolean") return [];
|
|
554
|
+
return [
|
|
555
|
+
{
|
|
556
|
+
...baseProperties,
|
|
557
|
+
responseKey: input.responseBoolean ? "true" : "false"
|
|
558
|
+
}
|
|
559
|
+
];
|
|
560
|
+
}
|
|
561
|
+
if (input.answerType === "numeric") {
|
|
562
|
+
if (typeof input.responseNumber !== "number") return [];
|
|
563
|
+
return [
|
|
564
|
+
{
|
|
565
|
+
...baseProperties,
|
|
566
|
+
responseKey: toNumericBucket(input.responseNumber)
|
|
567
|
+
}
|
|
568
|
+
];
|
|
569
|
+
}
|
|
570
|
+
if (input.answerType === "text") {
|
|
571
|
+
if (typeof input.responseText !== "string") return [];
|
|
572
|
+
return [
|
|
573
|
+
{
|
|
574
|
+
...baseProperties,
|
|
575
|
+
responseKey: `text_len:${toTextLengthBucket(input.responseText)}`
|
|
576
|
+
}
|
|
577
|
+
];
|
|
578
|
+
}
|
|
579
|
+
const responseKey = toStableKey(input.responseKey);
|
|
580
|
+
if (!responseKey) return [];
|
|
581
|
+
return [
|
|
582
|
+
{
|
|
583
|
+
...baseProperties,
|
|
584
|
+
responseKey
|
|
585
|
+
}
|
|
586
|
+
];
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// src/analytics-client.ts
|
|
590
|
+
var DEFAULT_CONSENT_STORAGE_KEY = "analyticscli:consent:v1";
|
|
591
|
+
var AUTH_FAILURE_FLUSH_PAUSE_MS = 6e4;
|
|
592
|
+
var resolveDefaultOsNameFromPlatform = (platform) => {
|
|
593
|
+
if (!platform) {
|
|
594
|
+
return void 0;
|
|
595
|
+
}
|
|
596
|
+
if (platform === "ios") {
|
|
597
|
+
return "iOS";
|
|
598
|
+
}
|
|
599
|
+
if (platform === "android") {
|
|
600
|
+
return "Android";
|
|
601
|
+
}
|
|
602
|
+
if (platform === "web") {
|
|
603
|
+
return "Web";
|
|
604
|
+
}
|
|
605
|
+
if (platform === "mac") {
|
|
606
|
+
return "macOS";
|
|
607
|
+
}
|
|
608
|
+
if (platform === "windows") {
|
|
609
|
+
return "Windows";
|
|
610
|
+
}
|
|
611
|
+
return void 0;
|
|
612
|
+
};
|
|
613
|
+
var IngestSendError = class extends Error {
|
|
614
|
+
retryable;
|
|
615
|
+
attempts;
|
|
616
|
+
status;
|
|
617
|
+
errorCode;
|
|
618
|
+
serverMessage;
|
|
619
|
+
requestId;
|
|
620
|
+
constructor(input) {
|
|
621
|
+
super(input.message);
|
|
622
|
+
this.name = "IngestSendError";
|
|
623
|
+
this.retryable = input.retryable;
|
|
624
|
+
this.attempts = input.attempts;
|
|
625
|
+
this.status = input.status;
|
|
626
|
+
this.errorCode = input.errorCode;
|
|
627
|
+
this.serverMessage = input.serverMessage;
|
|
628
|
+
this.requestId = input.requestId;
|
|
629
|
+
if (input.cause !== void 0) {
|
|
630
|
+
this.cause = input.cause;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
var AnalyticsClient = class {
|
|
635
|
+
apiKey;
|
|
636
|
+
hasIngestConfig;
|
|
637
|
+
endpoint;
|
|
638
|
+
batchSize;
|
|
639
|
+
flushIntervalMs;
|
|
640
|
+
maxRetries;
|
|
641
|
+
debug;
|
|
642
|
+
onIngestError;
|
|
643
|
+
platform;
|
|
644
|
+
projectSurface;
|
|
645
|
+
appVersion;
|
|
646
|
+
identityTrackingMode;
|
|
647
|
+
context;
|
|
648
|
+
configuredStorage;
|
|
649
|
+
storage;
|
|
650
|
+
storageReadsAreAsync;
|
|
651
|
+
persistConsentState;
|
|
652
|
+
consentStorageKey;
|
|
653
|
+
hasExplicitInitialConsent;
|
|
654
|
+
hasExplicitInitialFullTrackingConsent;
|
|
655
|
+
sessionTimeoutMs;
|
|
656
|
+
dedupeOnboardingStepViewsPerSession;
|
|
657
|
+
dedupeScreenViewsPerSession;
|
|
658
|
+
screenViewDedupeWindowMs;
|
|
659
|
+
runtimeEnv;
|
|
660
|
+
hasExplicitAnonId;
|
|
661
|
+
hasExplicitSessionId;
|
|
662
|
+
hydrationPromise;
|
|
663
|
+
queue = [];
|
|
664
|
+
flushTimer = null;
|
|
665
|
+
isFlushing = false;
|
|
666
|
+
consentGranted = true;
|
|
667
|
+
fullTrackingConsentGranted = false;
|
|
668
|
+
userId = null;
|
|
669
|
+
anonId;
|
|
670
|
+
sessionId;
|
|
671
|
+
sessionEventSeq = 0;
|
|
672
|
+
inMemoryLastSeenMs = Date.now();
|
|
673
|
+
hydrationCompleted = false;
|
|
674
|
+
deferredEventsBeforeHydration = [];
|
|
675
|
+
onboardingStepViewStateSessionId = null;
|
|
676
|
+
onboardingStepViewsSeen = /* @__PURE__ */ new Set();
|
|
677
|
+
lastScreenViewDedupeSessionId = null;
|
|
678
|
+
lastScreenViewDedupeKey = null;
|
|
679
|
+
lastScreenViewDedupeTsMs = 0;
|
|
680
|
+
flushPausedUntilMs = 0;
|
|
681
|
+
constructor(options) {
|
|
682
|
+
const normalizedOptions = this.normalizeOptions(options);
|
|
683
|
+
this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
|
|
684
|
+
this.hasIngestConfig = Boolean(this.apiKey);
|
|
685
|
+
if (!this.hasIngestConfig) {
|
|
686
|
+
this.reportMissingApiKey();
|
|
687
|
+
}
|
|
688
|
+
this.endpoint = (this.readRequiredStringOption(normalizedOptions.endpoint) || DEFAULT_COLLECTOR_ENDPOINT).replace(/\/$/, "");
|
|
689
|
+
this.batchSize = Math.min(normalizedOptions.batchSize ?? 20, DEFAULT_INGEST_LIMITS.maxBatchSize);
|
|
690
|
+
this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
|
|
691
|
+
this.maxRetries = normalizedOptions.maxRetries ?? 4;
|
|
692
|
+
this.debug = normalizedOptions.debug ?? false;
|
|
693
|
+
this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
|
|
694
|
+
this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
|
|
695
|
+
this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
|
|
696
|
+
this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
|
|
697
|
+
this.identityTrackingMode = this.resolveIdentityTrackingModeOption(normalizedOptions);
|
|
698
|
+
const initialContext = { ...normalizedOptions.context ?? {} };
|
|
699
|
+
const hasExplicitOsName = this.readRequiredStringOption(initialContext.osName).length > 0;
|
|
700
|
+
this.context = {
|
|
701
|
+
...initialContext,
|
|
702
|
+
osName: hasExplicitOsName ? initialContext.osName : resolveDefaultOsNameFromPlatform(this.platform) ?? initialContext.osName
|
|
703
|
+
};
|
|
704
|
+
this.runtimeEnv = detectRuntimeEnv();
|
|
705
|
+
this.persistConsentState = normalizedOptions.persistConsentState ?? false;
|
|
706
|
+
this.consentStorageKey = this.readRequiredStringOption(normalizedOptions.consentStorageKey) || DEFAULT_CONSENT_STORAGE_KEY;
|
|
707
|
+
this.hasExplicitInitialConsent = typeof normalizedOptions.initialConsentGranted === "boolean";
|
|
708
|
+
this.hasExplicitInitialFullTrackingConsent = typeof normalizedOptions.initialFullTrackingConsentGranted === "boolean";
|
|
709
|
+
this.sessionTimeoutMs = normalizedOptions.sessionTimeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
|
|
710
|
+
this.dedupeOnboardingStepViewsPerSession = normalizedOptions.dedupeOnboardingStepViewsPerSession ?? true;
|
|
711
|
+
this.dedupeScreenViewsPerSession = normalizedOptions.dedupeScreenViewsPerSession ?? true;
|
|
712
|
+
this.screenViewDedupeWindowMs = this.normalizeScreenViewDedupeWindowMs(
|
|
713
|
+
normalizedOptions.screenViewDedupeWindowMs
|
|
714
|
+
);
|
|
715
|
+
this.configuredStorage = this.resolveConfiguredStorage(normalizedOptions);
|
|
716
|
+
const persistedFullTrackingConsent = this.readPersistedConsentSync(this.configuredStorage);
|
|
717
|
+
const configuredFullTrackingConsent = normalizedOptions.initialFullTrackingConsentGranted;
|
|
718
|
+
const initialFullTrackingConsentGranted = typeof configuredFullTrackingConsent === "boolean" ? configuredFullTrackingConsent : persistedFullTrackingConsent ?? false;
|
|
719
|
+
this.fullTrackingConsentGranted = this.identityTrackingMode === "always_on" || initialFullTrackingConsentGranted;
|
|
720
|
+
this.storage = this.isFullTrackingActive() ? this.configuredStorage : null;
|
|
721
|
+
this.storageReadsAreAsync = this.detectAsyncStorageReads();
|
|
722
|
+
const providedAnonId = this.isFullTrackingActive() ? this.readRequiredStringOption(normalizedOptions.anonId) : "";
|
|
723
|
+
const providedSessionId = this.isFullTrackingActive() ? this.readRequiredStringOption(normalizedOptions.sessionId) : "";
|
|
724
|
+
this.hasExplicitAnonId = Boolean(providedAnonId);
|
|
725
|
+
this.hasExplicitSessionId = Boolean(providedSessionId);
|
|
726
|
+
this.anonId = providedAnonId || this.ensureDeviceId();
|
|
727
|
+
this.sessionId = providedSessionId || this.ensureSessionId();
|
|
728
|
+
this.sessionEventSeq = this.readSessionEventSeq(this.sessionId);
|
|
729
|
+
const persistedConsent = this.readPersistedConsentSync(this.storage);
|
|
730
|
+
const configuredConsent = normalizedOptions.initialConsentGranted;
|
|
731
|
+
const initialConsentGranted = typeof configuredConsent === "boolean" ? configuredConsent : persistedConsent ?? this.hasIngestConfig;
|
|
732
|
+
this.consentGranted = this.hasIngestConfig && initialConsentGranted;
|
|
733
|
+
if (this.hasExplicitInitialConsent && this.persistConsentState) {
|
|
734
|
+
this.writePersistedConsent(this.storage, this.consentGranted);
|
|
735
|
+
}
|
|
736
|
+
if (this.hasExplicitInitialFullTrackingConsent && this.persistConsentState) {
|
|
737
|
+
this.writePersistedConsent(this.configuredStorage, this.fullTrackingConsentGranted);
|
|
738
|
+
}
|
|
739
|
+
this.hydrationPromise = this.hydrateIdentityFromStorage();
|
|
740
|
+
this.enqueueInitialSessionStart();
|
|
741
|
+
this.startAutoFlush();
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Resolves once client initialization work completes.
|
|
745
|
+
*/
|
|
746
|
+
async ready() {
|
|
747
|
+
await this.hydrationPromise;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Enables or disables event collection.
|
|
751
|
+
* When disabled, queued events are dropped immediately.
|
|
752
|
+
*/
|
|
753
|
+
setConsent(granted, options = {}) {
|
|
754
|
+
if (granted && !this.hasIngestConfig) {
|
|
755
|
+
this.log("Ignoring consent opt-in because `apiKey` is missing");
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
this.consentGranted = granted;
|
|
759
|
+
if ((options.persist ?? true) && this.persistConsentState) {
|
|
760
|
+
this.writePersistedConsent(this.storage, granted);
|
|
761
|
+
}
|
|
762
|
+
if (this.identityTrackingMode === "consent_gated") {
|
|
763
|
+
this.setFullTrackingConsent(granted, options);
|
|
764
|
+
}
|
|
765
|
+
if (!granted) {
|
|
766
|
+
this.queue = [];
|
|
767
|
+
this.deferredEventsBeforeHydration = [];
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
optIn(options) {
|
|
771
|
+
this.setConsent(true, options);
|
|
772
|
+
}
|
|
773
|
+
optOut(options) {
|
|
774
|
+
this.setConsent(false, options);
|
|
775
|
+
}
|
|
776
|
+
getConsent() {
|
|
777
|
+
return this.consentGranted;
|
|
778
|
+
}
|
|
779
|
+
getConsentState() {
|
|
780
|
+
const persisted = this.readPersistedConsentSync(this.storage);
|
|
781
|
+
if (persisted === true) {
|
|
782
|
+
return "granted";
|
|
783
|
+
}
|
|
784
|
+
if (persisted === false) {
|
|
785
|
+
return "denied";
|
|
786
|
+
}
|
|
787
|
+
return this.consentGranted ? "granted" : "unknown";
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Sets or updates shared event context fields (useful for mobile device/app metadata).
|
|
791
|
+
*/
|
|
792
|
+
setContext(context) {
|
|
793
|
+
this.context = {
|
|
794
|
+
...this.context,
|
|
795
|
+
...context
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
enqueueInitialSessionStart() {
|
|
799
|
+
if (!this.consentGranted) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (this.shouldDeferEventsUntilHydrated()) {
|
|
803
|
+
this.deferEventUntilHydrated(() => {
|
|
804
|
+
this.enqueueInitialSessionStart();
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const sessionId = this.getSessionId();
|
|
809
|
+
this.enqueue({
|
|
810
|
+
eventId: randomId(),
|
|
811
|
+
eventName: "session_start",
|
|
812
|
+
ts: nowIso(),
|
|
813
|
+
sessionId,
|
|
814
|
+
anonId: this.anonId,
|
|
815
|
+
userId: this.getEventUserId(),
|
|
816
|
+
properties: this.withRuntimeMetadata({ source: "sdk_mount" }, sessionId),
|
|
817
|
+
platform: this.platform,
|
|
818
|
+
projectSurface: this.projectSurface,
|
|
819
|
+
appVersion: this.appVersion,
|
|
820
|
+
...this.withEventContext(),
|
|
821
|
+
type: "track"
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Associates following events with a known user id.
|
|
826
|
+
* Anonymous history remains linked by anonId/sessionId.
|
|
827
|
+
*/
|
|
828
|
+
identify(userId, traits) {
|
|
829
|
+
const normalizedUserId = this.readRequiredStringOption(userId);
|
|
830
|
+
if (!normalizedUserId) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (!this.isFullTrackingActive()) {
|
|
834
|
+
this.log("Ignoring identify() because identity persistence is not enabled");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
this.userId = normalizedUserId;
|
|
838
|
+
if (!this.consentGranted) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const normalizedTraits = this.cloneProperties(traits);
|
|
842
|
+
if (this.shouldDeferEventsUntilHydrated()) {
|
|
843
|
+
this.deferEventUntilHydrated(() => {
|
|
844
|
+
this.identify(normalizedUserId, normalizedTraits);
|
|
845
|
+
});
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const sessionId = this.getSessionId();
|
|
849
|
+
this.enqueue({
|
|
850
|
+
eventId: randomId(),
|
|
851
|
+
eventName: "identify",
|
|
852
|
+
ts: nowIso(),
|
|
853
|
+
sessionId,
|
|
854
|
+
anonId: this.anonId,
|
|
855
|
+
userId: normalizedUserId,
|
|
856
|
+
properties: this.withRuntimeMetadata(normalizedTraits, sessionId),
|
|
857
|
+
platform: this.platform,
|
|
858
|
+
projectSurface: this.projectSurface,
|
|
859
|
+
appVersion: this.appVersion,
|
|
860
|
+
...this.withEventContext(),
|
|
861
|
+
type: "identify"
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Convenience helper for login/logout boundaries.
|
|
866
|
+
* - pass a non-empty user id to emit an identify event
|
|
867
|
+
* - pass null/undefined/empty string to clear user linkage
|
|
868
|
+
*/
|
|
869
|
+
setUser(userId, traits) {
|
|
870
|
+
const normalizedUserId = this.readRequiredStringOption(userId);
|
|
871
|
+
if (!normalizedUserId) {
|
|
872
|
+
this.clearUser();
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
this.identify(normalizedUserId, traits);
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Sets consent specifically for persistent identity tracking.
|
|
879
|
+
* In `consent_gated` mode this toggles strict-vs-full identity behavior while generic event tracking can stay enabled.
|
|
880
|
+
*/
|
|
881
|
+
setFullTrackingConsent(granted, options = {}) {
|
|
882
|
+
if (this.identityTrackingMode === "strict") {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (this.identityTrackingMode === "always_on") {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
this.fullTrackingConsentGranted = granted;
|
|
889
|
+
if ((options.persist ?? true) && this.persistConsentState) {
|
|
890
|
+
this.writePersistedConsent(this.configuredStorage, granted);
|
|
891
|
+
}
|
|
892
|
+
this.applyIdentityTrackingState();
|
|
893
|
+
}
|
|
894
|
+
optInFullTracking(options) {
|
|
895
|
+
this.setFullTrackingConsent(true, options);
|
|
896
|
+
}
|
|
897
|
+
optOutFullTracking(options) {
|
|
898
|
+
this.setFullTrackingConsent(false, options);
|
|
899
|
+
}
|
|
900
|
+
isFullTrackingEnabled() {
|
|
901
|
+
return this.isFullTrackingActive();
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Clears the current identified user from in-memory SDK state.
|
|
905
|
+
*/
|
|
906
|
+
clearUser() {
|
|
907
|
+
this.userId = null;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Sends a generic product event.
|
|
911
|
+
*/
|
|
912
|
+
track(eventName, properties) {
|
|
913
|
+
if (!this.consentGranted) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (this.shouldDeferEventsUntilHydrated()) {
|
|
917
|
+
const deferredProperties = this.cloneProperties(properties);
|
|
918
|
+
this.deferEventUntilHydrated(() => {
|
|
919
|
+
this.track(eventName, deferredProperties);
|
|
920
|
+
});
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const sessionId = this.getSessionId();
|
|
924
|
+
if (this.shouldDropOnboardingStepView(eventName, properties, sessionId)) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
this.enqueue({
|
|
928
|
+
eventId: randomId(),
|
|
929
|
+
eventName,
|
|
930
|
+
ts: nowIso(),
|
|
931
|
+
sessionId,
|
|
932
|
+
anonId: this.anonId,
|
|
933
|
+
userId: this.getEventUserId(),
|
|
934
|
+
properties: this.withRuntimeMetadata(properties, sessionId),
|
|
935
|
+
platform: this.platform,
|
|
936
|
+
projectSurface: this.projectSurface,
|
|
937
|
+
appVersion: this.appVersion,
|
|
938
|
+
...this.withEventContext(),
|
|
939
|
+
type: "track"
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Sends a typed onboarding event with conventional onboarding metadata.
|
|
944
|
+
*/
|
|
945
|
+
trackOnboardingEvent(eventName, properties) {
|
|
946
|
+
this.track(eventName, properties);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Creates a scoped onboarding tracker that applies shared flow properties to every onboarding event.
|
|
950
|
+
* This reduces app-side boilerplate while keeping each emitted event fully self-describing.
|
|
951
|
+
*/
|
|
952
|
+
createOnboardingTracker(defaults) {
|
|
953
|
+
const {
|
|
954
|
+
surveyKey: rawDefaultSurveyKey,
|
|
955
|
+
appVersion: rawDefaultAppVersion,
|
|
956
|
+
isNewUser: rawDefaultIsNewUser,
|
|
957
|
+
onboardingFlowId: rawDefaultFlowId,
|
|
958
|
+
onboardingFlowVersion: rawDefaultFlowVersion,
|
|
959
|
+
stepKey: rawDefaultStepKey,
|
|
960
|
+
stepIndex: rawDefaultStepIndex,
|
|
961
|
+
stepCount: rawDefaultStepCount,
|
|
962
|
+
...defaultExtraProperties
|
|
963
|
+
} = defaults;
|
|
964
|
+
const defaultSurveyKey = this.readPropertyAsString(rawDefaultSurveyKey);
|
|
965
|
+
const defaultAppVersion = this.readPropertyAsString(rawDefaultAppVersion);
|
|
966
|
+
const defaultIsNewUser = typeof rawDefaultIsNewUser === "boolean" ? rawDefaultIsNewUser : void 0;
|
|
967
|
+
const defaultFlowId = this.readPropertyAsString(rawDefaultFlowId);
|
|
968
|
+
const defaultFlowVersion = typeof rawDefaultFlowVersion === "string" || typeof rawDefaultFlowVersion === "number" ? rawDefaultFlowVersion : void 0;
|
|
969
|
+
const defaultStepKey = this.readPropertyAsString(rawDefaultStepKey);
|
|
970
|
+
const defaultStepIndex = this.readPropertyAsStepIndex(rawDefaultStepIndex);
|
|
971
|
+
const defaultStepCount = this.readPropertyAsStepIndex(rawDefaultStepCount);
|
|
972
|
+
const mergeEventProperties = (properties) => ({
|
|
973
|
+
...defaultExtraProperties,
|
|
974
|
+
appVersion: defaultAppVersion,
|
|
975
|
+
isNewUser: defaultIsNewUser,
|
|
976
|
+
onboardingFlowId: defaultFlowId,
|
|
977
|
+
onboardingFlowVersion: defaultFlowVersion,
|
|
978
|
+
stepKey: defaultStepKey,
|
|
979
|
+
stepIndex: defaultStepIndex,
|
|
980
|
+
stepCount: defaultStepCount,
|
|
981
|
+
...properties ?? {}
|
|
982
|
+
});
|
|
983
|
+
const track = (eventName, properties) => {
|
|
984
|
+
this.trackOnboardingEvent(eventName, mergeEventProperties(properties));
|
|
985
|
+
};
|
|
986
|
+
const surveyResponse = (input) => {
|
|
987
|
+
this.trackOnboardingSurveyResponse({
|
|
988
|
+
...input,
|
|
989
|
+
surveyKey: input.surveyKey ?? defaultSurveyKey ?? defaultFlowId ?? "onboarding",
|
|
990
|
+
appVersion: input.appVersion ?? defaultAppVersion,
|
|
991
|
+
isNewUser: input.isNewUser ?? defaultIsNewUser,
|
|
992
|
+
onboardingFlowId: input.onboardingFlowId ?? defaultFlowId,
|
|
993
|
+
onboardingFlowVersion: input.onboardingFlowVersion ?? defaultFlowVersion,
|
|
994
|
+
stepKey: input.stepKey ?? defaultStepKey,
|
|
995
|
+
stepIndex: input.stepIndex ?? defaultStepIndex,
|
|
996
|
+
stepCount: input.stepCount ?? defaultStepCount,
|
|
997
|
+
properties: {
|
|
998
|
+
...defaultExtraProperties,
|
|
999
|
+
...input.properties ?? {}
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
};
|
|
1003
|
+
const step = (stepKey, stepIndex, properties) => {
|
|
1004
|
+
const stepProps = {
|
|
1005
|
+
...properties ?? {},
|
|
1006
|
+
stepKey,
|
|
1007
|
+
stepIndex
|
|
1008
|
+
};
|
|
1009
|
+
return {
|
|
1010
|
+
view: (overrides) => track(ONBOARDING_EVENTS.STEP_VIEW, { ...stepProps, ...overrides ?? {} }),
|
|
1011
|
+
complete: (overrides) => track(ONBOARDING_EVENTS.STEP_COMPLETE, { ...stepProps, ...overrides ?? {} }),
|
|
1012
|
+
surveyResponse: (input) => surveyResponse({
|
|
1013
|
+
...input,
|
|
1014
|
+
stepKey,
|
|
1015
|
+
stepIndex
|
|
1016
|
+
})
|
|
1017
|
+
};
|
|
1018
|
+
};
|
|
1019
|
+
return {
|
|
1020
|
+
track,
|
|
1021
|
+
start: (properties) => track(ONBOARDING_EVENTS.START, properties),
|
|
1022
|
+
stepView: (properties) => track(ONBOARDING_EVENTS.STEP_VIEW, properties),
|
|
1023
|
+
stepComplete: (properties) => track(ONBOARDING_EVENTS.STEP_COMPLETE, properties),
|
|
1024
|
+
complete: (properties) => track(ONBOARDING_EVENTS.COMPLETE, properties),
|
|
1025
|
+
skip: (properties) => track(ONBOARDING_EVENTS.SKIP, properties),
|
|
1026
|
+
surveyResponse,
|
|
1027
|
+
step
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Creates a scoped paywall tracker that applies shared paywall defaults to every journey event.
|
|
1032
|
+
* Useful when a flow has a stable `source`, `paywallId`, `offering`, or experiment metadata.
|
|
1033
|
+
* Reuse the returned tracker for that flow context; creating a new tracker per event resets
|
|
1034
|
+
* paywall entry correlation.
|
|
1035
|
+
*/
|
|
1036
|
+
createPaywallTracker(defaults) {
|
|
1037
|
+
const { source: rawDefaultSource, ...defaultProperties } = defaults;
|
|
1038
|
+
const defaultSource = this.readRequiredStringOption(rawDefaultSource);
|
|
1039
|
+
let currentPaywallEntryId;
|
|
1040
|
+
if (!defaultSource) {
|
|
1041
|
+
this.log("createPaywallTracker() called without a valid default `source`");
|
|
1042
|
+
}
|
|
1043
|
+
const mergeProperties = (properties) => {
|
|
1044
|
+
const mergedSource = this.readRequiredStringOption(
|
|
1045
|
+
this.readPropertyAsString(properties?.source) ?? defaultSource
|
|
1046
|
+
);
|
|
1047
|
+
return {
|
|
1048
|
+
...defaultProperties,
|
|
1049
|
+
...properties ?? {},
|
|
1050
|
+
source: mergedSource
|
|
1051
|
+
};
|
|
1052
|
+
};
|
|
1053
|
+
const track = (eventName, properties) => {
|
|
1054
|
+
const mergedProperties = mergeProperties(properties);
|
|
1055
|
+
delete mergedProperties.paywallEntryId;
|
|
1056
|
+
if (eventName === PAYWALL_EVENTS.SHOWN) {
|
|
1057
|
+
currentPaywallEntryId = randomId();
|
|
1058
|
+
mergedProperties.paywallEntryId = currentPaywallEntryId;
|
|
1059
|
+
} else {
|
|
1060
|
+
if (currentPaywallEntryId) {
|
|
1061
|
+
mergedProperties.paywallEntryId = currentPaywallEntryId;
|
|
1062
|
+
}
|
|
1063
|
+
if (properties?.offering === void 0) {
|
|
1064
|
+
delete mergedProperties.offering;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
this.sendPaywallEvent(eventName, mergedProperties, {
|
|
1068
|
+
allowPaywallEntryId: true
|
|
1069
|
+
});
|
|
1070
|
+
};
|
|
1071
|
+
return {
|
|
1072
|
+
track,
|
|
1073
|
+
shown: (properties) => track(PAYWALL_EVENTS.SHOWN, properties),
|
|
1074
|
+
skip: (properties) => track(PAYWALL_EVENTS.SKIP, properties),
|
|
1075
|
+
purchaseStarted: (properties) => track(PURCHASE_EVENTS.STARTED, properties),
|
|
1076
|
+
purchaseSuccess: (properties) => track(PURCHASE_EVENTS.SUCCESS, properties),
|
|
1077
|
+
purchaseFailed: (properties) => track(PURCHASE_EVENTS.FAILED, properties),
|
|
1078
|
+
purchaseCancel: (properties) => track(PURCHASE_EVENTS.CANCEL, properties)
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Sends a typed paywall/purchase journey event.
|
|
1083
|
+
* Direct calls ignore `paywallEntryId`; use `createPaywallTracker(...)` for entry correlation.
|
|
1084
|
+
*/
|
|
1085
|
+
trackPaywallEvent(eventName, properties) {
|
|
1086
|
+
this.sendPaywallEvent(eventName, properties, {
|
|
1087
|
+
allowPaywallEntryId: false
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
sendPaywallEvent(eventName, properties, options) {
|
|
1091
|
+
if (typeof properties?.source !== "string" || properties.source.trim().length === 0) {
|
|
1092
|
+
this.log("Dropping paywall event without required `source` property", { eventName });
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const normalizedProperties = {
|
|
1096
|
+
...properties
|
|
1097
|
+
};
|
|
1098
|
+
if (!options.allowPaywallEntryId && normalizedProperties.paywallEntryId !== void 0) {
|
|
1099
|
+
this.log(
|
|
1100
|
+
"Ignoring `paywallEntryId` in direct trackPaywallEvent(); use createPaywallTracker()",
|
|
1101
|
+
{ eventName }
|
|
1102
|
+
);
|
|
1103
|
+
delete normalizedProperties.paywallEntryId;
|
|
1104
|
+
}
|
|
1105
|
+
this.track(eventName, normalizedProperties);
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Sends anonymized onboarding survey responses using canonical event naming.
|
|
1109
|
+
* Free text and raw numeric values are reduced to coarse buckets.
|
|
1110
|
+
*/
|
|
1111
|
+
trackOnboardingSurveyResponse(input, eventName = ONBOARDING_SURVEY_EVENTS.RESPONSE) {
|
|
1112
|
+
const rows = sanitizeSurveyResponseInput(input);
|
|
1113
|
+
for (const properties of rows) {
|
|
1114
|
+
this.track(eventName, properties);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Sends a screen-view style event using the `screen:<name>` convention.
|
|
1119
|
+
*/
|
|
1120
|
+
screen(name, properties) {
|
|
1121
|
+
if (!this.consentGranted) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (this.shouldDeferEventsUntilHydrated()) {
|
|
1125
|
+
const deferredProperties = this.cloneProperties(properties);
|
|
1126
|
+
this.deferEventUntilHydrated(() => {
|
|
1127
|
+
this.screen(name, deferredProperties);
|
|
1128
|
+
});
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
const sessionId = this.getSessionId();
|
|
1132
|
+
if (this.shouldDropScreenView(name, properties, sessionId)) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
this.enqueue({
|
|
1136
|
+
eventId: randomId(),
|
|
1137
|
+
eventName: `screen:${name}`,
|
|
1138
|
+
ts: nowIso(),
|
|
1139
|
+
sessionId,
|
|
1140
|
+
anonId: this.anonId,
|
|
1141
|
+
userId: this.getEventUserId(),
|
|
1142
|
+
properties: this.withRuntimeMetadata(properties, sessionId),
|
|
1143
|
+
platform: this.platform,
|
|
1144
|
+
projectSurface: this.projectSurface,
|
|
1145
|
+
appVersion: this.appVersion,
|
|
1146
|
+
...this.withEventContext(),
|
|
1147
|
+
type: "screen"
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Alias of `screen(...)` for web-style naming.
|
|
1152
|
+
*/
|
|
1153
|
+
page(name, properties) {
|
|
1154
|
+
this.screen(name, properties);
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Sends a feedback event.
|
|
1158
|
+
*/
|
|
1159
|
+
feedback(message, rating, properties) {
|
|
1160
|
+
if (!this.consentGranted) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (this.shouldDeferEventsUntilHydrated()) {
|
|
1164
|
+
const deferredProperties = this.cloneProperties(properties);
|
|
1165
|
+
this.deferEventUntilHydrated(() => {
|
|
1166
|
+
this.feedback(message, rating, deferredProperties);
|
|
1167
|
+
});
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const sessionId = this.getSessionId();
|
|
1171
|
+
this.enqueue({
|
|
1172
|
+
eventId: randomId(),
|
|
1173
|
+
eventName: "feedback_submitted",
|
|
1174
|
+
ts: nowIso(),
|
|
1175
|
+
sessionId,
|
|
1176
|
+
anonId: this.anonId,
|
|
1177
|
+
userId: this.getEventUserId(),
|
|
1178
|
+
properties: this.withRuntimeMetadata({ message, rating, ...properties }, sessionId),
|
|
1179
|
+
platform: this.platform,
|
|
1180
|
+
projectSurface: this.projectSurface,
|
|
1181
|
+
appVersion: this.appVersion,
|
|
1182
|
+
...this.withEventContext(),
|
|
1183
|
+
type: "feedback"
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Flushes current event queue to the ingest endpoint.
|
|
1188
|
+
*/
|
|
1189
|
+
async flush() {
|
|
1190
|
+
if (!this.hydrationCompleted && this.deferredEventsBeforeHydration.length > 0) {
|
|
1191
|
+
await this.hydrationPromise;
|
|
1192
|
+
}
|
|
1193
|
+
if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (Date.now() < this.flushPausedUntilMs) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
this.isFlushing = true;
|
|
1200
|
+
const batch = this.queue.splice(0, this.batchSize);
|
|
1201
|
+
const payload = {
|
|
1202
|
+
sentAt: nowIso(),
|
|
1203
|
+
events: batch
|
|
1204
|
+
};
|
|
1205
|
+
const validation = validateIngestBatch(payload);
|
|
1206
|
+
if (!validation.success) {
|
|
1207
|
+
this.log("Validation failed, dropping batch", validation.reason);
|
|
1208
|
+
this.isFlushing = false;
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
await this.sendWithRetry(payload);
|
|
1213
|
+
this.flushPausedUntilMs = 0;
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
this.queue = [...batch, ...this.queue];
|
|
1216
|
+
const ingestError = this.toIngestSendError(error);
|
|
1217
|
+
const diagnostics = this.createIngestDiagnostics(ingestError, batch.length, this.queue.length);
|
|
1218
|
+
if (ingestError.status === 401 || ingestError.status === 403) {
|
|
1219
|
+
this.flushPausedUntilMs = Date.now() + AUTH_FAILURE_FLUSH_PAUSE_MS;
|
|
1220
|
+
this.log("Pausing ingest flush after auth failure", {
|
|
1221
|
+
status: ingestError.status,
|
|
1222
|
+
retryAfterMs: AUTH_FAILURE_FLUSH_PAUSE_MS
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
this.log("Send failed permanently, requeueing batch", diagnostics);
|
|
1226
|
+
this.reportIngestError(diagnostics);
|
|
1227
|
+
} finally {
|
|
1228
|
+
this.isFlushing = false;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Stops internal timers and unload handlers.
|
|
1233
|
+
*/
|
|
1234
|
+
shutdown() {
|
|
1235
|
+
if (this.flushTimer) {
|
|
1236
|
+
clearInterval(this.flushTimer);
|
|
1237
|
+
this.flushTimer = null;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
enqueue(event) {
|
|
1241
|
+
if (!this.consentGranted) {
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
this.queue.push(event);
|
|
1245
|
+
if (this.queue.length >= this.batchSize) {
|
|
1246
|
+
this.scheduleFlush();
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
scheduleFlush() {
|
|
1250
|
+
void this.flush().catch((error) => {
|
|
1251
|
+
this.log("Unexpected flush failure", error);
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
async sendWithRetry(payload) {
|
|
1255
|
+
let delay = 250;
|
|
1256
|
+
for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
|
|
1257
|
+
try {
|
|
1258
|
+
const response = await fetch(`${this.endpoint}/v1/collect`, {
|
|
1259
|
+
method: "POST",
|
|
1260
|
+
headers: {
|
|
1261
|
+
"content-type": "application/json",
|
|
1262
|
+
"x-api-key": this.apiKey
|
|
1263
|
+
},
|
|
1264
|
+
body: JSON.stringify(payload),
|
|
1265
|
+
keepalive: true
|
|
1266
|
+
});
|
|
1267
|
+
if (!response.ok) {
|
|
1268
|
+
throw await this.createHttpIngestSendError(response, attempt);
|
|
1269
|
+
}
|
|
1270
|
+
return;
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
const normalized = this.toIngestSendError(error, attempt);
|
|
1273
|
+
const finalAttempt = attempt >= this.maxRetries + 1;
|
|
1274
|
+
this.log("Ingest attempt failed", {
|
|
1275
|
+
attempt: normalized.attempts,
|
|
1276
|
+
maxRetries: this.maxRetries,
|
|
1277
|
+
retryable: normalized.retryable,
|
|
1278
|
+
status: normalized.status,
|
|
1279
|
+
errorCode: normalized.errorCode,
|
|
1280
|
+
requestId: normalized.requestId,
|
|
1281
|
+
nextRetryInMs: !finalAttempt && normalized.retryable ? delay : null
|
|
1282
|
+
});
|
|
1283
|
+
if (finalAttempt || !normalized.retryable) {
|
|
1284
|
+
throw normalized;
|
|
1285
|
+
}
|
|
1286
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1287
|
+
delay *= 2;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
async createHttpIngestSendError(response, attempts) {
|
|
1292
|
+
const requestId = response.headers.get("x-request-id") ?? response.headers.get("cf-ray") ?? void 0;
|
|
1293
|
+
let errorCode;
|
|
1294
|
+
let serverMessage;
|
|
1295
|
+
try {
|
|
1296
|
+
const parsed = await response.json();
|
|
1297
|
+
const errorBody = parsed && typeof parsed === "object" && parsed.error && typeof parsed.error === "object" ? parsed.error : void 0;
|
|
1298
|
+
if (typeof errorBody?.code === "string") {
|
|
1299
|
+
errorCode = errorBody.code;
|
|
1300
|
+
}
|
|
1301
|
+
if (typeof errorBody?.message === "string") {
|
|
1302
|
+
serverMessage = errorBody.message;
|
|
1303
|
+
}
|
|
1304
|
+
} catch {
|
|
1305
|
+
}
|
|
1306
|
+
const retryable = this.shouldRetryHttpStatus(response.status);
|
|
1307
|
+
const statusSuffix = errorCode ? ` ${errorCode}` : "";
|
|
1308
|
+
const message = `ingest status=${response.status}${statusSuffix}`;
|
|
1309
|
+
return new IngestSendError({
|
|
1310
|
+
message,
|
|
1311
|
+
retryable,
|
|
1312
|
+
attempts,
|
|
1313
|
+
status: response.status,
|
|
1314
|
+
errorCode,
|
|
1315
|
+
serverMessage,
|
|
1316
|
+
requestId
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
shouldRetryHttpStatus(status) {
|
|
1320
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
1321
|
+
}
|
|
1322
|
+
toIngestSendError(error, attempts) {
|
|
1323
|
+
if (error instanceof IngestSendError) {
|
|
1324
|
+
const resolvedAttempts = attempts ?? error.attempts;
|
|
1325
|
+
return new IngestSendError({
|
|
1326
|
+
message: error.message,
|
|
1327
|
+
retryable: error.retryable,
|
|
1328
|
+
attempts: resolvedAttempts,
|
|
1329
|
+
status: error.status,
|
|
1330
|
+
errorCode: error.errorCode,
|
|
1331
|
+
serverMessage: error.serverMessage,
|
|
1332
|
+
requestId: error.requestId,
|
|
1333
|
+
cause: error.cause
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
const fallbackMessage = error instanceof Error ? error.message : "ingest request failed";
|
|
1337
|
+
return new IngestSendError({
|
|
1338
|
+
message: fallbackMessage,
|
|
1339
|
+
retryable: true,
|
|
1340
|
+
attempts: attempts ?? 1,
|
|
1341
|
+
cause: error
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
createIngestDiagnostics(error, batchSize, queueSize) {
|
|
1345
|
+
return {
|
|
1346
|
+
name: "AnalyticsIngestError",
|
|
1347
|
+
message: error.message,
|
|
1348
|
+
endpoint: this.endpoint,
|
|
1349
|
+
path: "/v1/collect",
|
|
1350
|
+
status: error.status,
|
|
1351
|
+
errorCode: error.errorCode,
|
|
1352
|
+
serverMessage: error.serverMessage,
|
|
1353
|
+
requestId: error.requestId,
|
|
1354
|
+
retryable: error.retryable,
|
|
1355
|
+
attempts: error.attempts,
|
|
1356
|
+
maxRetries: this.maxRetries,
|
|
1357
|
+
batchSize,
|
|
1358
|
+
queueSize,
|
|
1359
|
+
timestamp: nowIso()
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
reportIngestError(error) {
|
|
1363
|
+
if (!this.onIngestError) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
try {
|
|
1367
|
+
this.onIngestError(error);
|
|
1368
|
+
} catch (callbackError) {
|
|
1369
|
+
this.log("onIngestError callback threw", callbackError);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
parsePersistedConsent(raw) {
|
|
1373
|
+
if (raw === "granted") {
|
|
1374
|
+
return true;
|
|
1375
|
+
}
|
|
1376
|
+
if (raw === "denied") {
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
readPersistedConsentSync(storage) {
|
|
1382
|
+
if (!this.persistConsentState) {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
if (storage === this.storage && this.storageReadsAreAsync) {
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
return this.parsePersistedConsent(readStorageSync(storage, this.consentStorageKey));
|
|
1389
|
+
}
|
|
1390
|
+
async readPersistedConsentAsync(storage) {
|
|
1391
|
+
if (!this.persistConsentState) {
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
return this.parsePersistedConsent(await readStorageAsync(storage, this.consentStorageKey));
|
|
1395
|
+
}
|
|
1396
|
+
writePersistedConsent(storage, granted) {
|
|
1397
|
+
if (!this.persistConsentState) {
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
writeStorageSync(storage, this.consentStorageKey, granted ? "granted" : "denied");
|
|
1401
|
+
}
|
|
1402
|
+
startAutoFlush() {
|
|
1403
|
+
if (!this.hasIngestConfig) {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
this.flushTimer = setInterval(() => {
|
|
1407
|
+
this.scheduleFlush();
|
|
1408
|
+
}, this.flushIntervalMs);
|
|
1409
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
1410
|
+
window.addEventListener("beforeunload", () => {
|
|
1411
|
+
this.scheduleFlush();
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
ensureDeviceId() {
|
|
1416
|
+
if (this.storageReadsAreAsync) {
|
|
1417
|
+
return randomId();
|
|
1418
|
+
}
|
|
1419
|
+
const existing = readStorageSync(this.storage, DEVICE_ID_KEY);
|
|
1420
|
+
if (existing) {
|
|
1421
|
+
return existing;
|
|
1422
|
+
}
|
|
1423
|
+
const value = randomId();
|
|
1424
|
+
writeStorageSync(this.storage, DEVICE_ID_KEY, value);
|
|
1425
|
+
return value;
|
|
1426
|
+
}
|
|
1427
|
+
ensureSessionId() {
|
|
1428
|
+
const now = Date.now();
|
|
1429
|
+
if (this.sessionId && now - this.inMemoryLastSeenMs < this.sessionTimeoutMs) {
|
|
1430
|
+
this.inMemoryLastSeenMs = now;
|
|
1431
|
+
if (!this.storageReadsAreAsync || this.hydrationCompleted) {
|
|
1432
|
+
writeStorageSync(this.storage, SESSION_ID_KEY, this.sessionId);
|
|
1433
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(now));
|
|
1434
|
+
}
|
|
1435
|
+
return this.sessionId;
|
|
1436
|
+
}
|
|
1437
|
+
if (this.storageReadsAreAsync) {
|
|
1438
|
+
this.inMemoryLastSeenMs = now;
|
|
1439
|
+
const next2 = randomId();
|
|
1440
|
+
if (this.hydrationCompleted) {
|
|
1441
|
+
writeStorageSync(this.storage, SESSION_ID_KEY, next2);
|
|
1442
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(now));
|
|
1443
|
+
}
|
|
1444
|
+
return next2;
|
|
1445
|
+
}
|
|
1446
|
+
const existing = readStorageSync(this.storage, SESSION_ID_KEY);
|
|
1447
|
+
const lastSeenRaw = readStorageSync(this.storage, LAST_SEEN_KEY);
|
|
1448
|
+
const lastSeen = lastSeenRaw ? Number(lastSeenRaw) : NaN;
|
|
1449
|
+
if (existing && Number.isFinite(lastSeen) && now - lastSeen < this.sessionTimeoutMs) {
|
|
1450
|
+
this.inMemoryLastSeenMs = now;
|
|
1451
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(now));
|
|
1452
|
+
return existing;
|
|
1453
|
+
}
|
|
1454
|
+
this.inMemoryLastSeenMs = now;
|
|
1455
|
+
const next = randomId();
|
|
1456
|
+
writeStorageSync(this.storage, SESSION_ID_KEY, next);
|
|
1457
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(now));
|
|
1458
|
+
return next;
|
|
1459
|
+
}
|
|
1460
|
+
getSessionId() {
|
|
1461
|
+
const resolvedSessionId = this.ensureSessionId();
|
|
1462
|
+
if (resolvedSessionId !== this.sessionId) {
|
|
1463
|
+
this.sessionId = resolvedSessionId;
|
|
1464
|
+
this.sessionEventSeq = this.readSessionEventSeq(resolvedSessionId);
|
|
1465
|
+
}
|
|
1466
|
+
return this.sessionId;
|
|
1467
|
+
}
|
|
1468
|
+
readSessionEventSeq(sessionId) {
|
|
1469
|
+
const raw = readStorageSync(this.storage, `${SESSION_EVENT_SEQ_PREFIX}${sessionId}`);
|
|
1470
|
+
return this.parseSessionEventSeq(raw);
|
|
1471
|
+
}
|
|
1472
|
+
async readSessionEventSeqAsync(sessionId) {
|
|
1473
|
+
const raw = await readStorageAsync(this.storage, `${SESSION_EVENT_SEQ_PREFIX}${sessionId}`);
|
|
1474
|
+
return this.parseSessionEventSeq(raw);
|
|
1475
|
+
}
|
|
1476
|
+
parseSessionEventSeq(raw) {
|
|
1477
|
+
const parsed = raw ? Number(raw) : Number.NaN;
|
|
1478
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
1479
|
+
return 0;
|
|
1480
|
+
}
|
|
1481
|
+
return Math.floor(parsed);
|
|
1482
|
+
}
|
|
1483
|
+
writeSessionEventSeq(sessionId, value) {
|
|
1484
|
+
writeStorageSync(this.storage, `${SESSION_EVENT_SEQ_PREFIX}${sessionId}`, String(value));
|
|
1485
|
+
}
|
|
1486
|
+
async hydrateIdentityFromStorage() {
|
|
1487
|
+
if (!this.storage) {
|
|
1488
|
+
this.onboardingStepViewStateSessionId = this.sessionId;
|
|
1489
|
+
this.hydrationCompleted = true;
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
const [storedAnonId, storedSessionId, storedLastSeen, storedConsent] = await Promise.all([
|
|
1494
|
+
readStorageAsync(this.storage, DEVICE_ID_KEY),
|
|
1495
|
+
readStorageAsync(this.storage, SESSION_ID_KEY),
|
|
1496
|
+
readStorageAsync(this.storage, LAST_SEEN_KEY),
|
|
1497
|
+
this.readPersistedConsentAsync(this.storage)
|
|
1498
|
+
]);
|
|
1499
|
+
if (!this.hasExplicitAnonId && storedAnonId) {
|
|
1500
|
+
this.anonId = storedAnonId;
|
|
1501
|
+
}
|
|
1502
|
+
if (!this.hasExplicitSessionId && storedSessionId) {
|
|
1503
|
+
const lastSeenMs = storedLastSeen ? Number(storedLastSeen) : Number.NaN;
|
|
1504
|
+
if (Number.isFinite(lastSeenMs) && Date.now() - lastSeenMs < this.sessionTimeoutMs) {
|
|
1505
|
+
this.sessionId = storedSessionId;
|
|
1506
|
+
this.inMemoryLastSeenMs = Date.now();
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (!this.hasExplicitInitialConsent && typeof storedConsent === "boolean") {
|
|
1510
|
+
this.consentGranted = this.hasIngestConfig && storedConsent;
|
|
1511
|
+
if (!this.consentGranted) {
|
|
1512
|
+
this.queue = [];
|
|
1513
|
+
this.deferredEventsBeforeHydration = [];
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
this.sessionEventSeq = await this.readSessionEventSeqAsync(this.sessionId);
|
|
1517
|
+
await this.hydrateOnboardingStepViewState(this.sessionId);
|
|
1518
|
+
writeStorageSync(this.storage, DEVICE_ID_KEY, this.anonId);
|
|
1519
|
+
writeStorageSync(this.storage, SESSION_ID_KEY, this.sessionId);
|
|
1520
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(this.inMemoryLastSeenMs));
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
this.log("Storage hydration failed; continuing with in-memory identity", error);
|
|
1523
|
+
} finally {
|
|
1524
|
+
this.hydrationCompleted = true;
|
|
1525
|
+
this.drainDeferredEventsAfterHydration();
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
shouldDeferEventsUntilHydrated() {
|
|
1529
|
+
return this.storageReadsAreAsync && !this.hydrationCompleted && (!this.hasExplicitAnonId || !this.hasExplicitSessionId);
|
|
1530
|
+
}
|
|
1531
|
+
deferEventUntilHydrated(action) {
|
|
1532
|
+
const maxDeferredEvents = 1e3;
|
|
1533
|
+
if (this.deferredEventsBeforeHydration.length >= maxDeferredEvents) {
|
|
1534
|
+
this.deferredEventsBeforeHydration.shift();
|
|
1535
|
+
this.log("Dropping oldest deferred pre-hydration event to cap memory usage");
|
|
1536
|
+
}
|
|
1537
|
+
this.deferredEventsBeforeHydration.push(action);
|
|
1538
|
+
}
|
|
1539
|
+
drainDeferredEventsAfterHydration() {
|
|
1540
|
+
if (this.deferredEventsBeforeHydration.length === 0) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const deferred = this.deferredEventsBeforeHydration;
|
|
1544
|
+
this.deferredEventsBeforeHydration = [];
|
|
1545
|
+
for (const action of deferred) {
|
|
1546
|
+
try {
|
|
1547
|
+
action();
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
this.log("Failed to emit deferred pre-hydration event", error);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
cloneProperties(properties) {
|
|
1554
|
+
if (!properties) {
|
|
1555
|
+
return void 0;
|
|
1556
|
+
}
|
|
1557
|
+
return { ...properties };
|
|
1558
|
+
}
|
|
1559
|
+
detectAsyncStorageReads() {
|
|
1560
|
+
if (!this.storage) {
|
|
1561
|
+
return false;
|
|
1562
|
+
}
|
|
1563
|
+
try {
|
|
1564
|
+
const value = this.storage.getItem(DEVICE_ID_KEY);
|
|
1565
|
+
if (typeof value === "object" && value !== null && "then" in value) {
|
|
1566
|
+
void value.catch(() => {
|
|
1567
|
+
});
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
return false;
|
|
1571
|
+
} catch {
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
withRuntimeMetadata(properties, sessionId) {
|
|
1576
|
+
const sanitized = sanitizeProperties(properties);
|
|
1577
|
+
const nextEventIndex = this.sessionEventSeq + 1;
|
|
1578
|
+
this.sessionEventSeq = nextEventIndex;
|
|
1579
|
+
this.writeSessionEventSeq(sessionId, nextEventIndex);
|
|
1580
|
+
if (typeof sanitized.runtimeEnv !== "string") {
|
|
1581
|
+
sanitized.runtimeEnv = this.runtimeEnv;
|
|
1582
|
+
}
|
|
1583
|
+
if (typeof sanitized.sessionEventIndex !== "number") {
|
|
1584
|
+
sanitized.sessionEventIndex = nextEventIndex;
|
|
1585
|
+
}
|
|
1586
|
+
return sanitized;
|
|
1587
|
+
}
|
|
1588
|
+
shouldDropOnboardingStepView(eventName, properties, sessionId) {
|
|
1589
|
+
if (!this.dedupeOnboardingStepViewsPerSession || eventName !== ONBOARDING_EVENTS.STEP_VIEW) {
|
|
1590
|
+
return false;
|
|
1591
|
+
}
|
|
1592
|
+
const dedupeKey = this.getOnboardingStepViewDedupeKey(properties);
|
|
1593
|
+
if (!dedupeKey) {
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
this.syncOnboardingStepViewState(sessionId);
|
|
1597
|
+
if (this.onboardingStepViewsSeen.has(dedupeKey)) {
|
|
1598
|
+
this.log("Dropping duplicate onboarding step view for session", { sessionId, dedupeKey });
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1601
|
+
this.onboardingStepViewsSeen.add(dedupeKey);
|
|
1602
|
+
this.persistOnboardingStepViewState(sessionId);
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
shouldDropScreenView(name, properties, sessionId) {
|
|
1606
|
+
if (!this.dedupeScreenViewsPerSession) {
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
const dedupeKey = this.getScreenViewDedupeKey(name, properties);
|
|
1610
|
+
if (!dedupeKey) {
|
|
1611
|
+
return false;
|
|
1612
|
+
}
|
|
1613
|
+
const nowMs = Date.now();
|
|
1614
|
+
if (this.lastScreenViewDedupeSessionId !== sessionId) {
|
|
1615
|
+
this.lastScreenViewDedupeSessionId = sessionId;
|
|
1616
|
+
this.lastScreenViewDedupeKey = null;
|
|
1617
|
+
this.lastScreenViewDedupeTsMs = 0;
|
|
1618
|
+
}
|
|
1619
|
+
const withinWindow = this.lastScreenViewDedupeKey === dedupeKey && nowMs - this.lastScreenViewDedupeTsMs <= this.screenViewDedupeWindowMs;
|
|
1620
|
+
if (withinWindow) {
|
|
1621
|
+
this.log("Dropping duplicate screen view for session", {
|
|
1622
|
+
sessionId,
|
|
1623
|
+
dedupeKey,
|
|
1624
|
+
windowMs: this.screenViewDedupeWindowMs
|
|
1625
|
+
});
|
|
1626
|
+
return true;
|
|
1627
|
+
}
|
|
1628
|
+
this.lastScreenViewDedupeSessionId = sessionId;
|
|
1629
|
+
this.lastScreenViewDedupeKey = dedupeKey;
|
|
1630
|
+
this.lastScreenViewDedupeTsMs = nowMs;
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
getScreenViewDedupeKey(name, properties) {
|
|
1634
|
+
const normalizedName = toStableKey(name);
|
|
1635
|
+
if (!normalizedName) {
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
const screenClass = properties && typeof properties === "object" ? toStableKey(this.readPropertyAsString(properties.screen_class)) : null;
|
|
1639
|
+
const resolvedScreenClass = screenClass ?? normalizedName;
|
|
1640
|
+
return `${normalizedName}|${resolvedScreenClass}`;
|
|
1641
|
+
}
|
|
1642
|
+
getOnboardingStepViewDedupeKey(properties) {
|
|
1643
|
+
if (!properties) {
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
const flowId = toStableKey(this.readPropertyAsString(properties.onboardingFlowId)) ?? "unknown_flow";
|
|
1647
|
+
const flowVersion = toStableKey(this.readPropertyAsString(properties.onboardingFlowVersion)) ?? "unknown_version";
|
|
1648
|
+
const stepKey = toStableKey(this.readPropertyAsString(properties.stepKey));
|
|
1649
|
+
const stepIndex = this.readPropertyAsStepIndex(properties.stepIndex);
|
|
1650
|
+
if (!stepKey && stepIndex === void 0) {
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
return `${flowId}|${flowVersion}|${stepKey ?? "unknown_step"}|${stepIndex ?? "unknown_index"}`;
|
|
1654
|
+
}
|
|
1655
|
+
readPropertyAsString(value) {
|
|
1656
|
+
if (typeof value === "string") {
|
|
1657
|
+
return value;
|
|
1658
|
+
}
|
|
1659
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1660
|
+
return String(value);
|
|
1661
|
+
}
|
|
1662
|
+
return void 0;
|
|
1663
|
+
}
|
|
1664
|
+
readPropertyAsStepIndex(value) {
|
|
1665
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1666
|
+
return void 0;
|
|
1667
|
+
}
|
|
1668
|
+
return Math.max(0, Math.floor(value));
|
|
1669
|
+
}
|
|
1670
|
+
syncOnboardingStepViewState(sessionId) {
|
|
1671
|
+
if (this.onboardingStepViewStateSessionId === sessionId) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const persisted = this.parseOnboardingStepViewState(
|
|
1675
|
+
readStorageSync(this.storage, ONBOARDING_STEP_VIEW_STATE_KEY)
|
|
1676
|
+
);
|
|
1677
|
+
this.onboardingStepViewStateSessionId = sessionId;
|
|
1678
|
+
this.onboardingStepViewsSeen = persisted?.sessionId === sessionId ? new Set(persisted.keys) : /* @__PURE__ */ new Set();
|
|
1679
|
+
}
|
|
1680
|
+
async hydrateOnboardingStepViewState(sessionId) {
|
|
1681
|
+
if (!this.dedupeOnboardingStepViewsPerSession) {
|
|
1682
|
+
this.onboardingStepViewStateSessionId = sessionId;
|
|
1683
|
+
this.onboardingStepViewsSeen = /* @__PURE__ */ new Set();
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const persisted = this.parseOnboardingStepViewState(
|
|
1687
|
+
await readStorageAsync(this.storage, ONBOARDING_STEP_VIEW_STATE_KEY)
|
|
1688
|
+
);
|
|
1689
|
+
this.onboardingStepViewStateSessionId = sessionId;
|
|
1690
|
+
this.onboardingStepViewsSeen = persisted?.sessionId === sessionId ? /* @__PURE__ */ new Set([...persisted.keys, ...this.onboardingStepViewsSeen]) : new Set(this.onboardingStepViewsSeen);
|
|
1691
|
+
}
|
|
1692
|
+
persistOnboardingStepViewState(sessionId) {
|
|
1693
|
+
this.onboardingStepViewStateSessionId = sessionId;
|
|
1694
|
+
writeStorageSync(
|
|
1695
|
+
this.storage,
|
|
1696
|
+
ONBOARDING_STEP_VIEW_STATE_KEY,
|
|
1697
|
+
JSON.stringify({
|
|
1698
|
+
sessionId,
|
|
1699
|
+
keys: Array.from(this.onboardingStepViewsSeen)
|
|
1700
|
+
})
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
parseOnboardingStepViewState(raw) {
|
|
1704
|
+
if (!raw) {
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
try {
|
|
1708
|
+
const parsed = JSON.parse(raw);
|
|
1709
|
+
if (typeof parsed.sessionId !== "string" || !Array.isArray(parsed.keys)) {
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
const keys = parsed.keys.filter((value) => typeof value === "string");
|
|
1713
|
+
return {
|
|
1714
|
+
sessionId: parsed.sessionId,
|
|
1715
|
+
keys
|
|
1716
|
+
};
|
|
1717
|
+
} catch {
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
resolveIdentityTrackingModeOption(options) {
|
|
1722
|
+
const explicitMode = this.readRequiredStringOption(options.identityTrackingMode).toLowerCase();
|
|
1723
|
+
if (explicitMode === "strict") {
|
|
1724
|
+
return "strict";
|
|
1725
|
+
}
|
|
1726
|
+
if (explicitMode === "consent_gated") {
|
|
1727
|
+
return "consent_gated";
|
|
1728
|
+
}
|
|
1729
|
+
if (explicitMode === "always_on") {
|
|
1730
|
+
return "always_on";
|
|
1731
|
+
}
|
|
1732
|
+
if (options.enableFullTrackingWithoutConsent === true) {
|
|
1733
|
+
return "always_on";
|
|
1734
|
+
}
|
|
1735
|
+
return "consent_gated";
|
|
1736
|
+
}
|
|
1737
|
+
resolveConfiguredStorage(options) {
|
|
1738
|
+
if (this.identityTrackingMode === "strict") {
|
|
1739
|
+
if (options.storage || options.useCookieStorage || options.cookieDomain) {
|
|
1740
|
+
this.log("Ignoring storage/cookie configuration because identityTrackingMode=strict");
|
|
1741
|
+
}
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
const customStorage = options.storage ?? null;
|
|
1745
|
+
const browserStorage = resolveBrowserStorageAdapter();
|
|
1746
|
+
const primaryStorage = customStorage ?? browserStorage;
|
|
1747
|
+
const cookieStorage = resolveCookieStorageAdapter(
|
|
1748
|
+
options.useCookieStorage === true,
|
|
1749
|
+
this.readRequiredStringOption(options.cookieDomain) || void 0,
|
|
1750
|
+
this.normalizeCookieMaxAgeSeconds(options.cookieMaxAgeSeconds)
|
|
1751
|
+
);
|
|
1752
|
+
if (primaryStorage && cookieStorage) {
|
|
1753
|
+
return combineStorageAdapters(primaryStorage, cookieStorage);
|
|
1754
|
+
}
|
|
1755
|
+
return primaryStorage ?? cookieStorage;
|
|
1756
|
+
}
|
|
1757
|
+
normalizeCookieMaxAgeSeconds(value) {
|
|
1758
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
1759
|
+
return DEFAULT_COOKIE_MAX_AGE_SECONDS;
|
|
1760
|
+
}
|
|
1761
|
+
return Math.floor(value);
|
|
1762
|
+
}
|
|
1763
|
+
normalizeScreenViewDedupeWindowMs(value) {
|
|
1764
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
1765
|
+
return DEFAULT_SCREEN_VIEW_DEDUPE_WINDOW_MS;
|
|
1766
|
+
}
|
|
1767
|
+
return Math.floor(value);
|
|
1768
|
+
}
|
|
1769
|
+
isFullTrackingActive() {
|
|
1770
|
+
if (!this.hasIngestConfig) {
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
if (this.identityTrackingMode === "always_on") {
|
|
1774
|
+
return true;
|
|
1775
|
+
}
|
|
1776
|
+
if (this.identityTrackingMode === "strict") {
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
return this.fullTrackingConsentGranted;
|
|
1780
|
+
}
|
|
1781
|
+
applyIdentityTrackingState() {
|
|
1782
|
+
if (!this.isFullTrackingActive()) {
|
|
1783
|
+
this.storage = null;
|
|
1784
|
+
this.storageReadsAreAsync = false;
|
|
1785
|
+
this.userId = null;
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
this.storage = this.configuredStorage;
|
|
1789
|
+
this.storageReadsAreAsync = this.detectAsyncStorageReads();
|
|
1790
|
+
this.sessionId = this.ensureSessionId();
|
|
1791
|
+
this.sessionEventSeq = this.readSessionEventSeq(this.sessionId);
|
|
1792
|
+
writeStorageSync(this.storage, DEVICE_ID_KEY, this.anonId);
|
|
1793
|
+
writeStorageSync(this.storage, SESSION_ID_KEY, this.sessionId);
|
|
1794
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(this.inMemoryLastSeenMs));
|
|
1795
|
+
}
|
|
1796
|
+
getEventUserId() {
|
|
1797
|
+
if (!this.isFullTrackingActive()) {
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
return this.userId;
|
|
1801
|
+
}
|
|
1802
|
+
withEventContext() {
|
|
1803
|
+
return {
|
|
1804
|
+
appBuild: this.context.appBuild,
|
|
1805
|
+
osName: this.context.osName,
|
|
1806
|
+
osVersion: this.context.osVersion,
|
|
1807
|
+
country: this.context.country,
|
|
1808
|
+
region: this.context.region,
|
|
1809
|
+
city: this.context.city
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
normalizeOptions(options) {
|
|
1813
|
+
if (typeof options !== "object" || options === null) {
|
|
1814
|
+
return {};
|
|
1815
|
+
}
|
|
1816
|
+
return options;
|
|
1817
|
+
}
|
|
1818
|
+
readRequiredStringOption(value) {
|
|
1819
|
+
if (typeof value !== "string") {
|
|
1820
|
+
return "";
|
|
1821
|
+
}
|
|
1822
|
+
return value.trim();
|
|
1823
|
+
}
|
|
1824
|
+
normalizePlatformOption(value) {
|
|
1825
|
+
const normalized = this.readRequiredStringOption(value).toLowerCase();
|
|
1826
|
+
if (normalized === "web" || normalized === "ios" || normalized === "android" || normalized === "mac" || normalized === "windows") {
|
|
1827
|
+
return normalized;
|
|
1828
|
+
}
|
|
1829
|
+
if (normalized === "macos" || normalized === "osx" || normalized === "darwin") {
|
|
1830
|
+
return "mac";
|
|
1831
|
+
}
|
|
1832
|
+
if (normalized === "win32") {
|
|
1833
|
+
return "windows";
|
|
1834
|
+
}
|
|
1835
|
+
return void 0;
|
|
1836
|
+
}
|
|
1837
|
+
normalizeProjectSurfaceOption(value) {
|
|
1838
|
+
const normalized = this.readRequiredStringOption(value).toLowerCase();
|
|
1839
|
+
if (!normalized) {
|
|
1840
|
+
return void 0;
|
|
1841
|
+
}
|
|
1842
|
+
if (normalized.length > 64) {
|
|
1843
|
+
return normalized.slice(0, 64);
|
|
1844
|
+
}
|
|
1845
|
+
return normalized;
|
|
1846
|
+
}
|
|
1847
|
+
log(message, data) {
|
|
1848
|
+
if (!this.debug) {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
console.debug("[analyticscli-sdk]", message, data);
|
|
1852
|
+
}
|
|
1853
|
+
reportMissingApiKey() {
|
|
1854
|
+
console.error(
|
|
1855
|
+
"[analyticscli-sdk] Missing required `apiKey`. Tracking is disabled (safe no-op). Pass your publishable API key."
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
// src/context.ts
|
|
1861
|
+
var normalizeInitInput = (input) => {
|
|
1862
|
+
if (typeof input === "string") {
|
|
1863
|
+
return { apiKey: input };
|
|
1864
|
+
}
|
|
1865
|
+
if (input === null || input === void 0) {
|
|
1866
|
+
return {};
|
|
1867
|
+
}
|
|
1868
|
+
return input;
|
|
1869
|
+
};
|
|
1870
|
+
var resolveClient = (input) => {
|
|
1871
|
+
if (input instanceof AnalyticsClient) {
|
|
1872
|
+
return input;
|
|
1873
|
+
}
|
|
1874
|
+
return new AnalyticsClient(normalizeInitInput(input ?? {}));
|
|
1875
|
+
};
|
|
1876
|
+
var createAnalyticsContext = (options = {}) => {
|
|
1877
|
+
const client = resolveClient(options.client);
|
|
1878
|
+
let onboardingTracker = client.createOnboardingTracker(options.onboarding ?? {});
|
|
1879
|
+
let paywallTracker = options.paywall ? client.createPaywallTracker(options.paywall) : null;
|
|
1880
|
+
const consent = {
|
|
1881
|
+
get: () => client.getConsent(),
|
|
1882
|
+
getState: () => client.getConsentState(),
|
|
1883
|
+
set: (granted, setOptions) => client.setConsent(granted, setOptions),
|
|
1884
|
+
optIn: (setOptions) => client.optIn(setOptions),
|
|
1885
|
+
optOut: (setOptions) => client.optOut(setOptions),
|
|
1886
|
+
setFullTracking: (granted, setOptions) => client.setFullTrackingConsent(granted, setOptions),
|
|
1887
|
+
optInFullTracking: (setOptions) => client.optInFullTracking(setOptions),
|
|
1888
|
+
optOutFullTracking: (setOptions) => client.optOutFullTracking(setOptions),
|
|
1889
|
+
isFullTrackingEnabled: () => client.isFullTrackingEnabled()
|
|
1890
|
+
};
|
|
1891
|
+
const user = {
|
|
1892
|
+
identify: (userId, traits) => client.identify(userId, traits),
|
|
1893
|
+
set: (userId, traits) => client.setUser(userId, traits),
|
|
1894
|
+
clear: () => client.clearUser()
|
|
1895
|
+
};
|
|
1896
|
+
return {
|
|
1897
|
+
client,
|
|
1898
|
+
get onboarding() {
|
|
1899
|
+
return onboardingTracker;
|
|
1900
|
+
},
|
|
1901
|
+
get paywall() {
|
|
1902
|
+
return paywallTracker;
|
|
1903
|
+
},
|
|
1904
|
+
consent,
|
|
1905
|
+
user,
|
|
1906
|
+
track: (eventName, properties) => client.track(eventName, properties),
|
|
1907
|
+
trackOnboardingEvent: (eventName, properties) => client.trackOnboardingEvent(eventName, properties),
|
|
1908
|
+
trackOnboardingSurveyResponse: (input, eventName) => client.trackOnboardingSurveyResponse(input, eventName),
|
|
1909
|
+
trackPaywallEvent: (eventName, properties) => client.trackPaywallEvent(eventName, properties),
|
|
1910
|
+
screen: (name, properties) => client.screen(name, properties),
|
|
1911
|
+
page: (name, properties) => client.page(name, properties),
|
|
1912
|
+
feedback: (message, rating, properties) => client.feedback(message, rating, properties),
|
|
1913
|
+
setContext: (context) => client.setContext(context),
|
|
1914
|
+
createOnboarding: (defaults) => client.createOnboardingTracker(defaults),
|
|
1915
|
+
createPaywall: (defaults) => client.createPaywallTracker(defaults),
|
|
1916
|
+
configureOnboarding: (defaults) => {
|
|
1917
|
+
onboardingTracker = client.createOnboardingTracker(defaults);
|
|
1918
|
+
return onboardingTracker;
|
|
1919
|
+
},
|
|
1920
|
+
configurePaywall: (defaults) => {
|
|
1921
|
+
paywallTracker = client.createPaywallTracker(defaults);
|
|
1922
|
+
return paywallTracker;
|
|
1923
|
+
},
|
|
1924
|
+
ready: () => client.ready(),
|
|
1925
|
+
flush: () => client.flush(),
|
|
1926
|
+
shutdown: () => client.shutdown()
|
|
1927
|
+
};
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
// src/index.ts
|
|
1931
|
+
var normalizeInitInput2 = (input) => {
|
|
1932
|
+
if (typeof input === "string") {
|
|
1933
|
+
return { apiKey: input };
|
|
1934
|
+
}
|
|
1935
|
+
if (input === null || input === void 0) {
|
|
1936
|
+
return {};
|
|
1937
|
+
}
|
|
1938
|
+
return input;
|
|
1939
|
+
};
|
|
1940
|
+
var init = (input = {}) => {
|
|
1941
|
+
return new AnalyticsClient(normalizeInitInput2(input));
|
|
1942
|
+
};
|
|
1943
|
+
var initConsentFirst = (input = {}) => {
|
|
1944
|
+
const normalized = normalizeInitInput2(input);
|
|
1945
|
+
return new AnalyticsClient({
|
|
1946
|
+
...normalized,
|
|
1947
|
+
initialConsentGranted: false
|
|
1948
|
+
});
|
|
1949
|
+
};
|
|
1950
|
+
var initAsync = async (input = {}) => {
|
|
1951
|
+
const client = new AnalyticsClient(normalizeInitInput2(input));
|
|
1952
|
+
await client.ready();
|
|
1953
|
+
return client;
|
|
1954
|
+
};
|
|
1955
|
+
var initConsentFirstAsync = async (input = {}) => {
|
|
1956
|
+
const client = initConsentFirst(input);
|
|
1957
|
+
await client.ready();
|
|
1958
|
+
return client;
|
|
1959
|
+
};
|
|
1960
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1961
|
+
0 && (module.exports = {
|
|
1962
|
+
AnalyticsClient,
|
|
1963
|
+
ONBOARDING_EVENTS,
|
|
1964
|
+
ONBOARDING_PROGRESS_EVENT_ORDER,
|
|
1965
|
+
ONBOARDING_SCREEN_EVENT_PREFIXES,
|
|
1966
|
+
ONBOARDING_SURVEY_EVENTS,
|
|
1967
|
+
PAYWALL_ANCHOR_EVENT_CANDIDATES,
|
|
1968
|
+
PAYWALL_EVENTS,
|
|
1969
|
+
PAYWALL_JOURNEY_EVENT_ORDER,
|
|
1970
|
+
PAYWALL_SKIP_EVENT_CANDIDATES,
|
|
1971
|
+
PURCHASE_EVENTS,
|
|
1972
|
+
PURCHASE_SUCCESS_EVENT_CANDIDATES,
|
|
1973
|
+
createAnalyticsContext,
|
|
1974
|
+
init,
|
|
1975
|
+
initAsync,
|
|
1976
|
+
initConsentFirst,
|
|
1977
|
+
initConsentFirstAsync
|
|
1978
|
+
});
|