@analyticscli/sdk 0.1.0-preview.4
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 +116 -0
- package/dist/browser.cjs +1531 -0
- package/dist/browser.d.cts +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +42 -0
- package/dist/chunk-CY4KHPLT.js +1455 -0
- package/dist/chunk-NC6ANZ4H.js +1455 -0
- package/dist/chunk-UILQQPVJ.js +1487 -0
- package/dist/chunk-W4RJPJLF.js +1426 -0
- package/dist/chunk-ZIOGPQNP.js +1478 -0
- package/dist/index.cjs +1531 -0
- package/dist/index.d.cts +402 -0
- package/dist/index.d.ts +402 -0
- package/dist/index.js +42 -0
- package/dist/react-native.cjs +1531 -0
- package/dist/react-native.d.cts +1 -0
- package/dist/react-native.d.ts +1 -0
- package/dist/react-native.js +42 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1487 @@
|
|
|
1
|
+
// src/sdk-contract.ts
|
|
2
|
+
var DEFAULT_INGEST_LIMITS = {
|
|
3
|
+
maxBatchSize: 50,
|
|
4
|
+
maxPayloadBytes: 128 * 1024
|
|
5
|
+
};
|
|
6
|
+
var ONBOARDING_EVENTS = {
|
|
7
|
+
START: "onboarding:start",
|
|
8
|
+
STEP_VIEW: "onboarding:step_view",
|
|
9
|
+
STEP_COMPLETE: "onboarding:step_complete",
|
|
10
|
+
COMPLETE: "onboarding:complete",
|
|
11
|
+
SKIP: "onboarding:skip"
|
|
12
|
+
};
|
|
13
|
+
var PAYWALL_EVENTS = {
|
|
14
|
+
SHOWN: "paywall:shown",
|
|
15
|
+
SKIP: "paywall:skip"
|
|
16
|
+
};
|
|
17
|
+
var PURCHASE_EVENTS = {
|
|
18
|
+
STARTED: "purchase:started",
|
|
19
|
+
SUCCESS: "purchase:success",
|
|
20
|
+
FAILED: "purchase:failed",
|
|
21
|
+
CANCEL: "purchase:cancel"
|
|
22
|
+
};
|
|
23
|
+
var ONBOARDING_SURVEY_EVENTS = {
|
|
24
|
+
RESPONSE: "onboarding:survey_response"
|
|
25
|
+
};
|
|
26
|
+
var ONBOARDING_PROGRESS_EVENT_ORDER = [
|
|
27
|
+
ONBOARDING_EVENTS.COMPLETE,
|
|
28
|
+
ONBOARDING_EVENTS.SKIP
|
|
29
|
+
];
|
|
30
|
+
var PAYWALL_JOURNEY_EVENT_ORDER = [
|
|
31
|
+
PAYWALL_EVENTS.SHOWN,
|
|
32
|
+
PAYWALL_EVENTS.SKIP,
|
|
33
|
+
PURCHASE_EVENTS.SUCCESS,
|
|
34
|
+
PURCHASE_EVENTS.FAILED
|
|
35
|
+
];
|
|
36
|
+
var ONBOARDING_SCREEN_EVENT_PREFIXES = ["screen:onboarding", "screen:onboarding_"];
|
|
37
|
+
var PAYWALL_ANCHOR_EVENT_CANDIDATES = [PAYWALL_EVENTS.SHOWN];
|
|
38
|
+
var PAYWALL_SKIP_EVENT_CANDIDATES = [PAYWALL_EVENTS.SKIP];
|
|
39
|
+
var PURCHASE_SUCCESS_EVENT_CANDIDATES = [PURCHASE_EVENTS.SUCCESS];
|
|
40
|
+
var RESERVED_PII_KEYS = /* @__PURE__ */ new Set([
|
|
41
|
+
"email",
|
|
42
|
+
"phone",
|
|
43
|
+
"firstName",
|
|
44
|
+
"lastName",
|
|
45
|
+
"fullName",
|
|
46
|
+
"address",
|
|
47
|
+
"ssn",
|
|
48
|
+
"creditCard"
|
|
49
|
+
]);
|
|
50
|
+
var EVENT_NAME_REGEX = /^[a-zA-Z0-9_:\-.]{1,100}$/;
|
|
51
|
+
|
|
52
|
+
// src/ingest-validation.ts
|
|
53
|
+
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;
|
|
54
|
+
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})$/;
|
|
55
|
+
var TYPE_VALUES = /* @__PURE__ */ new Set(["track", "screen", "identify", "feedback"]);
|
|
56
|
+
var isRecord = (value) => {
|
|
57
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
|
+
};
|
|
59
|
+
var isStringBetween = (value, min, max) => {
|
|
60
|
+
return typeof value === "string" && value.length >= min && value.length <= max;
|
|
61
|
+
};
|
|
62
|
+
var isOptionalStringMax = (value, max) => {
|
|
63
|
+
return value === void 0 || typeof value === "string" && value.length <= max;
|
|
64
|
+
};
|
|
65
|
+
var isNullableOptionalStringBetween = (value, min, max) => {
|
|
66
|
+
return value === void 0 || value === null || isStringBetween(value, min, max);
|
|
67
|
+
};
|
|
68
|
+
var isIsoDatetimeWithOffset = (value) => {
|
|
69
|
+
if (typeof value !== "string" || !ISO_DATETIME_WITH_OFFSET_REGEX.test(value)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return Number.isFinite(Date.parse(value));
|
|
73
|
+
};
|
|
74
|
+
var validateEvent = (event, index) => {
|
|
75
|
+
if (!isRecord(event)) {
|
|
76
|
+
return { success: false, reason: `events[${index}] is not an object` };
|
|
77
|
+
}
|
|
78
|
+
if (!isStringBetween(event.eventName, 1, 100) || !EVENT_NAME_REGEX.test(event.eventName)) {
|
|
79
|
+
return { success: false, reason: `events[${index}].eventName is invalid` };
|
|
80
|
+
}
|
|
81
|
+
if (!isStringBetween(event.sessionId, 1, 128)) {
|
|
82
|
+
return { success: false, reason: `events[${index}].sessionId is invalid` };
|
|
83
|
+
}
|
|
84
|
+
if (!isStringBetween(event.anonId, 1, 128)) {
|
|
85
|
+
return { success: false, reason: `events[${index}].anonId is invalid` };
|
|
86
|
+
}
|
|
87
|
+
if (!isNullableOptionalStringBetween(event.userId, 1, 128)) {
|
|
88
|
+
return { success: false, reason: `events[${index}].userId is invalid` };
|
|
89
|
+
}
|
|
90
|
+
if (!isRecord(event.properties)) {
|
|
91
|
+
return { success: false, reason: `events[${index}].properties is invalid` };
|
|
92
|
+
}
|
|
93
|
+
if (!isOptionalStringMax(event.platform, 64)) {
|
|
94
|
+
return { success: false, reason: `events[${index}].platform is invalid` };
|
|
95
|
+
}
|
|
96
|
+
if (!isOptionalStringMax(event.appVersion, 64)) {
|
|
97
|
+
return { success: false, reason: `events[${index}].appVersion is invalid` };
|
|
98
|
+
}
|
|
99
|
+
if (!isOptionalStringMax(event.appBuild, 64)) {
|
|
100
|
+
return { success: false, reason: `events[${index}].appBuild is invalid` };
|
|
101
|
+
}
|
|
102
|
+
if (!isOptionalStringMax(event.osName, 64)) {
|
|
103
|
+
return { success: false, reason: `events[${index}].osName is invalid` };
|
|
104
|
+
}
|
|
105
|
+
if (!isOptionalStringMax(event.osVersion, 64)) {
|
|
106
|
+
return { success: false, reason: `events[${index}].osVersion is invalid` };
|
|
107
|
+
}
|
|
108
|
+
if (!isOptionalStringMax(event.deviceModel, 128)) {
|
|
109
|
+
return { success: false, reason: `events[${index}].deviceModel is invalid` };
|
|
110
|
+
}
|
|
111
|
+
if (!isOptionalStringMax(event.deviceManufacturer, 128)) {
|
|
112
|
+
return { success: false, reason: `events[${index}].deviceManufacturer is invalid` };
|
|
113
|
+
}
|
|
114
|
+
if (!isOptionalStringMax(event.deviceType, 32)) {
|
|
115
|
+
return { success: false, reason: `events[${index}].deviceType is invalid` };
|
|
116
|
+
}
|
|
117
|
+
if (!isOptionalStringMax(event.locale, 32)) {
|
|
118
|
+
return { success: false, reason: `events[${index}].locale is invalid` };
|
|
119
|
+
}
|
|
120
|
+
if (!isOptionalStringMax(event.country, 8)) {
|
|
121
|
+
return { success: false, reason: `events[${index}].country is invalid` };
|
|
122
|
+
}
|
|
123
|
+
if (!isOptionalStringMax(event.region, 96)) {
|
|
124
|
+
return { success: false, reason: `events[${index}].region is invalid` };
|
|
125
|
+
}
|
|
126
|
+
if (!isOptionalStringMax(event.city, 96)) {
|
|
127
|
+
return { success: false, reason: `events[${index}].city is invalid` };
|
|
128
|
+
}
|
|
129
|
+
if (!isOptionalStringMax(event.timezone, 64)) {
|
|
130
|
+
return { success: false, reason: `events[${index}].timezone is invalid` };
|
|
131
|
+
}
|
|
132
|
+
if (!isOptionalStringMax(event.networkType, 32)) {
|
|
133
|
+
return { success: false, reason: `events[${index}].networkType is invalid` };
|
|
134
|
+
}
|
|
135
|
+
if (!isOptionalStringMax(event.carrier, 64)) {
|
|
136
|
+
return { success: false, reason: `events[${index}].carrier is invalid` };
|
|
137
|
+
}
|
|
138
|
+
if (!isOptionalStringMax(event.installSource, 64)) {
|
|
139
|
+
return { success: false, reason: `events[${index}].installSource is invalid` };
|
|
140
|
+
}
|
|
141
|
+
if (event.eventId !== void 0 && (typeof event.eventId !== "string" || !UUID_REGEX.test(event.eventId))) {
|
|
142
|
+
return { success: false, reason: `events[${index}].eventId is invalid` };
|
|
143
|
+
}
|
|
144
|
+
if (event.ts !== void 0 && !isIsoDatetimeWithOffset(event.ts)) {
|
|
145
|
+
return { success: false, reason: `events[${index}].ts is invalid` };
|
|
146
|
+
}
|
|
147
|
+
if (event.type !== void 0 && (typeof event.type !== "string" || !TYPE_VALUES.has(event.type))) {
|
|
148
|
+
return { success: false, reason: `events[${index}].type is invalid` };
|
|
149
|
+
}
|
|
150
|
+
return { success: true };
|
|
151
|
+
};
|
|
152
|
+
var validateIngestBatch = (batch) => {
|
|
153
|
+
if (batch.sentAt !== void 0 && !isIsoDatetimeWithOffset(batch.sentAt)) {
|
|
154
|
+
return { success: false, reason: "sentAt is invalid" };
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(batch.events)) {
|
|
157
|
+
return { success: false, reason: "events is invalid" };
|
|
158
|
+
}
|
|
159
|
+
if (batch.events.length === 0 || batch.events.length > DEFAULT_INGEST_LIMITS.maxBatchSize) {
|
|
160
|
+
return { success: false, reason: "events length is out of bounds" };
|
|
161
|
+
}
|
|
162
|
+
for (let index = 0; index < batch.events.length; index += 1) {
|
|
163
|
+
const result = validateEvent(batch.events[index], index);
|
|
164
|
+
if (!result.success) {
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { success: true };
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/constants.ts
|
|
172
|
+
var DEVICE_ID_KEY = "pi_device_id";
|
|
173
|
+
var SESSION_ID_KEY = "pi_session_id";
|
|
174
|
+
var LAST_SEEN_KEY = "pi_last_seen";
|
|
175
|
+
var SESSION_EVENT_SEQ_PREFIX = "pi_session_event_seq:";
|
|
176
|
+
var ONBOARDING_STEP_VIEW_STATE_KEY = "pi_onboarding_step_views";
|
|
177
|
+
var DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
178
|
+
var DEFAULT_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 400;
|
|
179
|
+
var DEFAULT_COLLECTOR_ENDPOINT = "https://collector.analyticscli.com";
|
|
180
|
+
|
|
181
|
+
// src/helpers.ts
|
|
182
|
+
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
183
|
+
var isPromiseLike = (value) => {
|
|
184
|
+
return typeof value === "object" && value !== null && "then" in value;
|
|
185
|
+
};
|
|
186
|
+
var normalizeStoredValue = (value) => {
|
|
187
|
+
if (typeof value === "string" || value === null) {
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
};
|
|
192
|
+
var randomId = () => {
|
|
193
|
+
if (globalThis.crypto?.randomUUID) {
|
|
194
|
+
return globalThis.crypto.randomUUID();
|
|
195
|
+
}
|
|
196
|
+
return `${Date.now()}-${Math.random().toString(16).slice(2, 12)}`;
|
|
197
|
+
};
|
|
198
|
+
var readStorageSync = (storage, key) => {
|
|
199
|
+
if (!storage) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const value = storage.getItem(key);
|
|
204
|
+
if (isPromiseLike(value)) {
|
|
205
|
+
void value.catch(() => {
|
|
206
|
+
});
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return normalizeStoredValue(value);
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var readStorageAsync = async (storage, key) => {
|
|
215
|
+
if (!storage) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const value = storage.getItem(key);
|
|
220
|
+
if (isPromiseLike(value)) {
|
|
221
|
+
return normalizeStoredValue(await value.catch(() => null));
|
|
222
|
+
}
|
|
223
|
+
return normalizeStoredValue(value);
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var writeStorageSync = (storage, key, value) => {
|
|
229
|
+
if (!storage) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const result = storage.setItem(key, value);
|
|
234
|
+
if (isPromiseLike(result)) {
|
|
235
|
+
void result.catch(() => {
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var readTrimmedString = (value) => {
|
|
242
|
+
if (typeof value !== "string") {
|
|
243
|
+
return void 0;
|
|
244
|
+
}
|
|
245
|
+
const trimmed = value.trim();
|
|
246
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
247
|
+
};
|
|
248
|
+
var normalizePlatformValue = (value) => {
|
|
249
|
+
const normalized = readTrimmedString(value)?.toLowerCase();
|
|
250
|
+
if (normalized === "web" || normalized === "ios" || normalized === "android" || normalized === "mac" || normalized === "windows") {
|
|
251
|
+
return normalized;
|
|
252
|
+
}
|
|
253
|
+
if (normalized === "macos" || normalized === "osx" || normalized === "darwin") {
|
|
254
|
+
return "mac";
|
|
255
|
+
}
|
|
256
|
+
if (normalized === "win32") {
|
|
257
|
+
return "windows";
|
|
258
|
+
}
|
|
259
|
+
return void 0;
|
|
260
|
+
};
|
|
261
|
+
var readGlobalPlatformOs = () => {
|
|
262
|
+
const withPlatform = globalThis;
|
|
263
|
+
const fromPlatform = normalizePlatformValue(withPlatform.Platform?.OS);
|
|
264
|
+
if (fromPlatform) {
|
|
265
|
+
return fromPlatform;
|
|
266
|
+
}
|
|
267
|
+
const withExpoPlatform = globalThis;
|
|
268
|
+
return normalizePlatformValue(withExpoPlatform.expo?.modules?.ExpoPlatform?.osName);
|
|
269
|
+
};
|
|
270
|
+
var detectDefaultPlatform = () => {
|
|
271
|
+
const detectedNativePlatform = readGlobalPlatformOs();
|
|
272
|
+
if (detectedNativePlatform) {
|
|
273
|
+
return detectedNativePlatform;
|
|
274
|
+
}
|
|
275
|
+
if (typeof navigator === "undefined") {
|
|
276
|
+
return void 0;
|
|
277
|
+
}
|
|
278
|
+
if (navigator.product === "ReactNative") {
|
|
279
|
+
return void 0;
|
|
280
|
+
}
|
|
281
|
+
return "web";
|
|
282
|
+
};
|
|
283
|
+
var detectDefaultAppVersion = () => {
|
|
284
|
+
const withExpoModules = globalThis;
|
|
285
|
+
const expoApp = withExpoModules.expo?.modules?.ExpoApplication;
|
|
286
|
+
const candidates = [
|
|
287
|
+
expoApp?.nativeApplicationVersion,
|
|
288
|
+
expoApp?.applicationVersion,
|
|
289
|
+
expoApp?.version
|
|
290
|
+
];
|
|
291
|
+
for (const candidate of candidates) {
|
|
292
|
+
const value = readTrimmedString(candidate);
|
|
293
|
+
if (value) {
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return void 0;
|
|
298
|
+
};
|
|
299
|
+
var detectRuntimeEnv = () => {
|
|
300
|
+
const globalWithDevFlag = globalThis;
|
|
301
|
+
if (typeof globalWithDevFlag.__DEV__ === "boolean") {
|
|
302
|
+
return globalWithDevFlag.__DEV__ ? "development" : "production";
|
|
303
|
+
}
|
|
304
|
+
const globalWithProcess = globalThis;
|
|
305
|
+
const nodeEnv = globalWithProcess.process?.env?.NODE_ENV;
|
|
306
|
+
if (nodeEnv) {
|
|
307
|
+
return nodeEnv === "production" ? "production" : "development";
|
|
308
|
+
}
|
|
309
|
+
if (typeof window !== "undefined" && typeof window.location?.hostname === "string") {
|
|
310
|
+
const hostname = window.location.hostname.toLowerCase();
|
|
311
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname.endsWith(".local") || hostname.endsWith(".test") || hostname.includes("dev") || hostname.includes("staging") || hostname.includes("preview")) {
|
|
312
|
+
return "development";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return "production";
|
|
316
|
+
};
|
|
317
|
+
var decodeComponentSafe = (value) => {
|
|
318
|
+
try {
|
|
319
|
+
return decodeURIComponent(value);
|
|
320
|
+
} catch {
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
var resolveCookieStorageAdapter = (enabled, cookieDomain, cookieMaxAgeSeconds) => {
|
|
325
|
+
if (!enabled || typeof document === "undefined") {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const normalizedDomain = cookieDomain?.trim();
|
|
329
|
+
const getCookie = (key) => {
|
|
330
|
+
const encodedKey = encodeURIComponent(key);
|
|
331
|
+
const cookies = document.cookie ? document.cookie.split(";") : [];
|
|
332
|
+
for (const rawCookie of cookies) {
|
|
333
|
+
const cookie = rawCookie.trim();
|
|
334
|
+
if (!cookie.startsWith(`${encodedKey}=`)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const rawValue = cookie.slice(encodedKey.length + 1);
|
|
338
|
+
return decodeComponentSafe(rawValue);
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
};
|
|
342
|
+
const setCookie = (key, value) => {
|
|
343
|
+
const attributes = [
|
|
344
|
+
"Path=/",
|
|
345
|
+
"SameSite=Lax",
|
|
346
|
+
`Max-Age=${cookieMaxAgeSeconds}`,
|
|
347
|
+
...normalizedDomain ? [`Domain=${normalizedDomain}`] : [],
|
|
348
|
+
...typeof location !== "undefined" && location.protocol === "https:" ? ["Secure"] : []
|
|
349
|
+
];
|
|
350
|
+
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}; ${attributes.join("; ")}`;
|
|
351
|
+
};
|
|
352
|
+
const removeCookie = (key) => {
|
|
353
|
+
const attributes = [
|
|
354
|
+
"Path=/",
|
|
355
|
+
"SameSite=Lax",
|
|
356
|
+
"Max-Age=0",
|
|
357
|
+
...normalizedDomain ? [`Domain=${normalizedDomain}`] : [],
|
|
358
|
+
...typeof location !== "undefined" && location.protocol === "https:" ? ["Secure"] : []
|
|
359
|
+
];
|
|
360
|
+
document.cookie = `${encodeURIComponent(key)}=; ${attributes.join("; ")}`;
|
|
361
|
+
};
|
|
362
|
+
return {
|
|
363
|
+
getItem: (key) => getCookie(key),
|
|
364
|
+
setItem: (key, value) => setCookie(key, value),
|
|
365
|
+
removeItem: (key) => removeCookie(key)
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
var resolveBrowserStorageAdapter = () => {
|
|
369
|
+
if (typeof window === "undefined") {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
let storage;
|
|
373
|
+
try {
|
|
374
|
+
storage = window.localStorage;
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
if (!storage) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
getItem: (key) => storage.getItem(key),
|
|
383
|
+
setItem: (key, value) => storage.setItem(key, value),
|
|
384
|
+
removeItem: (key) => storage.removeItem(key)
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
var combineStorageAdapters = (primary, secondary) => {
|
|
388
|
+
return {
|
|
389
|
+
getItem: (key) => {
|
|
390
|
+
const primaryValue = primary.getItem(key);
|
|
391
|
+
if (typeof primaryValue === "string") {
|
|
392
|
+
return primaryValue;
|
|
393
|
+
}
|
|
394
|
+
if (primaryValue === null) {
|
|
395
|
+
const secondaryValue = secondary.getItem(key);
|
|
396
|
+
if (typeof secondaryValue === "string") {
|
|
397
|
+
return secondaryValue;
|
|
398
|
+
}
|
|
399
|
+
return secondaryValue === null ? null : null;
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
},
|
|
403
|
+
setItem: (key, value) => {
|
|
404
|
+
primary.setItem(key, value);
|
|
405
|
+
secondary.setItem(key, value);
|
|
406
|
+
},
|
|
407
|
+
removeItem: (key) => {
|
|
408
|
+
primary.removeItem?.(key);
|
|
409
|
+
secondary.removeItem?.(key);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
var sanitizeProperties = (properties) => {
|
|
414
|
+
if (!properties) {
|
|
415
|
+
return {};
|
|
416
|
+
}
|
|
417
|
+
const entries = Object.entries(properties).filter(([key]) => !RESERVED_PII_KEYS.has(key));
|
|
418
|
+
return Object.fromEntries(entries);
|
|
419
|
+
};
|
|
420
|
+
var toStableKey = (value) => {
|
|
421
|
+
if (typeof value !== "string") return void 0;
|
|
422
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_\-:.]/g, "_");
|
|
423
|
+
if (!normalized) return void 0;
|
|
424
|
+
return normalized.slice(0, 80);
|
|
425
|
+
};
|
|
426
|
+
var toNumericBucket = (value) => {
|
|
427
|
+
if (!Number.isFinite(value)) return "nan";
|
|
428
|
+
if (value < 0) return "lt_0";
|
|
429
|
+
if (value <= 10) return "0_10";
|
|
430
|
+
if (value <= 20) return "11_20";
|
|
431
|
+
if (value <= 30) return "21_30";
|
|
432
|
+
if (value <= 40) return "31_40";
|
|
433
|
+
if (value <= 50) return "41_50";
|
|
434
|
+
if (value <= 100) return "51_100";
|
|
435
|
+
return "gt_100";
|
|
436
|
+
};
|
|
437
|
+
var toTextLengthBucket = (value) => {
|
|
438
|
+
const length = value.trim().length;
|
|
439
|
+
if (length === 0) return "empty";
|
|
440
|
+
if (length <= 10) return "1_10";
|
|
441
|
+
if (length <= 30) return "11_30";
|
|
442
|
+
if (length <= 80) return "31_80";
|
|
443
|
+
return "gt_80";
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// src/survey.ts
|
|
447
|
+
var sanitizeSurveyResponseInput = (input) => {
|
|
448
|
+
const surveyKey = toStableKey(input.surveyKey);
|
|
449
|
+
const questionKey = toStableKey(input.questionKey);
|
|
450
|
+
if (!surveyKey || !questionKey) {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
const baseProperties = {
|
|
454
|
+
surveyKey,
|
|
455
|
+
questionKey,
|
|
456
|
+
answerType: input.answerType,
|
|
457
|
+
responseProvided: true,
|
|
458
|
+
...input.appVersion ? { appVersion: input.appVersion } : {},
|
|
459
|
+
...input.isNewUser !== void 0 ? { isNewUser: input.isNewUser } : {},
|
|
460
|
+
...input.onboardingFlowId ? { onboardingFlowId: input.onboardingFlowId } : {},
|
|
461
|
+
...input.onboardingFlowVersion !== void 0 ? { onboardingFlowVersion: input.onboardingFlowVersion } : {},
|
|
462
|
+
...input.stepKey ? { stepKey: input.stepKey } : {},
|
|
463
|
+
...input.stepIndex !== void 0 ? { stepIndex: input.stepIndex } : {},
|
|
464
|
+
...input.stepCount !== void 0 ? { stepCount: input.stepCount } : {},
|
|
465
|
+
...input.experimentVariant ? { experimentVariant: input.experimentVariant } : {},
|
|
466
|
+
...input.paywallId ? { paywallId: input.paywallId } : {},
|
|
467
|
+
...sanitizeProperties(input.properties)
|
|
468
|
+
};
|
|
469
|
+
if (input.answerType === "multiple_choice") {
|
|
470
|
+
const keys = (input.responseKeys ?? []).map((value) => toStableKey(value)).filter((value) => Boolean(value)).slice(0, 20);
|
|
471
|
+
return keys.map((responseKey2) => ({
|
|
472
|
+
...baseProperties,
|
|
473
|
+
responseKey: responseKey2
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
if (input.answerType === "single_choice") {
|
|
477
|
+
const responseKey2 = toStableKey(input.responseKey);
|
|
478
|
+
if (!responseKey2) return [];
|
|
479
|
+
return [
|
|
480
|
+
{
|
|
481
|
+
...baseProperties,
|
|
482
|
+
responseKey: responseKey2
|
|
483
|
+
}
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
if (input.answerType === "boolean") {
|
|
487
|
+
if (typeof input.responseBoolean !== "boolean") return [];
|
|
488
|
+
return [
|
|
489
|
+
{
|
|
490
|
+
...baseProperties,
|
|
491
|
+
responseKey: input.responseBoolean ? "true" : "false"
|
|
492
|
+
}
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
if (input.answerType === "numeric") {
|
|
496
|
+
if (typeof input.responseNumber !== "number") return [];
|
|
497
|
+
return [
|
|
498
|
+
{
|
|
499
|
+
...baseProperties,
|
|
500
|
+
responseKey: toNumericBucket(input.responseNumber)
|
|
501
|
+
}
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
if (input.answerType === "text") {
|
|
505
|
+
if (typeof input.responseText !== "string") return [];
|
|
506
|
+
return [
|
|
507
|
+
{
|
|
508
|
+
...baseProperties,
|
|
509
|
+
responseKey: `text_len:${toTextLengthBucket(input.responseText)}`
|
|
510
|
+
}
|
|
511
|
+
];
|
|
512
|
+
}
|
|
513
|
+
const responseKey = toStableKey(input.responseKey);
|
|
514
|
+
if (!responseKey) return [];
|
|
515
|
+
return [
|
|
516
|
+
{
|
|
517
|
+
...baseProperties,
|
|
518
|
+
responseKey
|
|
519
|
+
}
|
|
520
|
+
];
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// src/analytics-client.ts
|
|
524
|
+
var AnalyticsClient = class {
|
|
525
|
+
apiKey;
|
|
526
|
+
hasIngestConfig;
|
|
527
|
+
endpoint;
|
|
528
|
+
batchSize;
|
|
529
|
+
flushIntervalMs;
|
|
530
|
+
maxRetries;
|
|
531
|
+
debug;
|
|
532
|
+
platform;
|
|
533
|
+
appVersion;
|
|
534
|
+
context;
|
|
535
|
+
storage;
|
|
536
|
+
storageReadsAreAsync;
|
|
537
|
+
sessionTimeoutMs;
|
|
538
|
+
dedupeOnboardingStepViewsPerSession;
|
|
539
|
+
runtimeEnv;
|
|
540
|
+
hasExplicitAnonId;
|
|
541
|
+
hasExplicitSessionId;
|
|
542
|
+
hydrationPromise;
|
|
543
|
+
queue = [];
|
|
544
|
+
flushTimer = null;
|
|
545
|
+
isFlushing = false;
|
|
546
|
+
consentGranted = true;
|
|
547
|
+
userId = null;
|
|
548
|
+
anonId;
|
|
549
|
+
sessionId;
|
|
550
|
+
sessionEventSeq = 0;
|
|
551
|
+
inMemoryLastSeenMs = Date.now();
|
|
552
|
+
hydrationCompleted = false;
|
|
553
|
+
deferredEventsBeforeHydration = [];
|
|
554
|
+
onboardingStepViewStateSessionId = null;
|
|
555
|
+
onboardingStepViewsSeen = /* @__PURE__ */ new Set();
|
|
556
|
+
constructor(options) {
|
|
557
|
+
const normalizedOptions = this.normalizeOptions(options);
|
|
558
|
+
this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
|
|
559
|
+
this.hasIngestConfig = Boolean(this.apiKey);
|
|
560
|
+
if (!this.hasIngestConfig) {
|
|
561
|
+
this.reportMissingApiKey();
|
|
562
|
+
}
|
|
563
|
+
this.endpoint = (this.readRequiredStringOption(normalizedOptions.endpoint) || DEFAULT_COLLECTOR_ENDPOINT).replace(/\/$/, "");
|
|
564
|
+
this.batchSize = Math.min(normalizedOptions.batchSize ?? 20, DEFAULT_INGEST_LIMITS.maxBatchSize);
|
|
565
|
+
this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
|
|
566
|
+
this.maxRetries = normalizedOptions.maxRetries ?? 4;
|
|
567
|
+
this.debug = normalizedOptions.debug ?? false;
|
|
568
|
+
this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
|
|
569
|
+
this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
|
|
570
|
+
this.context = { ...normalizedOptions.context ?? {} };
|
|
571
|
+
this.runtimeEnv = detectRuntimeEnv();
|
|
572
|
+
const useCookieStorage = normalizedOptions.useCookieStorage ?? Boolean(normalizedOptions.cookieDomain);
|
|
573
|
+
const cookieStorage = resolveCookieStorageAdapter(
|
|
574
|
+
useCookieStorage,
|
|
575
|
+
normalizedOptions.cookieDomain,
|
|
576
|
+
normalizedOptions.cookieMaxAgeSeconds ?? DEFAULT_COOKIE_MAX_AGE_SECONDS
|
|
577
|
+
);
|
|
578
|
+
const browserStorage = resolveBrowserStorageAdapter();
|
|
579
|
+
this.storage = normalizedOptions.storage ?? (cookieStorage && browserStorage ? combineStorageAdapters(cookieStorage, browserStorage) : cookieStorage ?? browserStorage);
|
|
580
|
+
this.storageReadsAreAsync = this.detectAsyncStorageReads();
|
|
581
|
+
this.sessionTimeoutMs = normalizedOptions.sessionTimeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
|
|
582
|
+
this.dedupeOnboardingStepViewsPerSession = normalizedOptions.dedupeOnboardingStepViewsPerSession ?? false;
|
|
583
|
+
const providedAnonId = normalizedOptions.anonId?.trim();
|
|
584
|
+
const providedSessionId = normalizedOptions.sessionId?.trim();
|
|
585
|
+
this.hasExplicitAnonId = Boolean(providedAnonId);
|
|
586
|
+
this.hasExplicitSessionId = Boolean(providedSessionId);
|
|
587
|
+
this.anonId = providedAnonId || this.ensureDeviceId();
|
|
588
|
+
this.sessionId = providedSessionId || this.ensureSessionId();
|
|
589
|
+
this.sessionEventSeq = this.readSessionEventSeq(this.sessionId);
|
|
590
|
+
this.consentGranted = this.hasIngestConfig;
|
|
591
|
+
this.hydrationPromise = this.hydrateIdentityFromStorage();
|
|
592
|
+
this.startAutoFlush();
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Resolves once any async storage adapter hydration completes.
|
|
596
|
+
* Useful in React Native when using async persistence (for example AsyncStorage).
|
|
597
|
+
*/
|
|
598
|
+
async ready() {
|
|
599
|
+
await this.hydrationPromise;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Enables or disables event collection.
|
|
603
|
+
* When disabled, queued events are dropped immediately.
|
|
604
|
+
*/
|
|
605
|
+
setConsent(granted) {
|
|
606
|
+
if (granted && !this.hasIngestConfig) {
|
|
607
|
+
this.log("Ignoring consent opt-in because `apiKey` is missing");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
this.consentGranted = granted;
|
|
611
|
+
if (!granted) {
|
|
612
|
+
this.queue = [];
|
|
613
|
+
this.deferredEventsBeforeHydration = [];
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
optIn() {
|
|
617
|
+
this.setConsent(true);
|
|
618
|
+
}
|
|
619
|
+
optOut() {
|
|
620
|
+
this.setConsent(false);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Sets or updates shared event context fields (useful for mobile device/app metadata).
|
|
624
|
+
*/
|
|
625
|
+
setContext(context) {
|
|
626
|
+
this.context = {
|
|
627
|
+
...this.context,
|
|
628
|
+
...context
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Associates following events with a known user id.
|
|
633
|
+
* Anonymous history remains linked by anonId/sessionId.
|
|
634
|
+
*/
|
|
635
|
+
identify(userId, traits) {
|
|
636
|
+
if (!this.consentGranted) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const normalizedUserId = this.readRequiredStringOption(userId);
|
|
640
|
+
if (!normalizedUserId) {
|
|
641
|
+
this.log("Dropping identify call without required `userId`");
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (this.shouldDeferEventsUntilHydrated()) {
|
|
645
|
+
const deferredTraits = this.cloneProperties(traits);
|
|
646
|
+
this.deferEventUntilHydrated(() => {
|
|
647
|
+
this.identify(normalizedUserId, deferredTraits);
|
|
648
|
+
});
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
this.userId = normalizedUserId;
|
|
652
|
+
const sessionId = this.getSessionId();
|
|
653
|
+
this.enqueue({
|
|
654
|
+
eventId: randomId(),
|
|
655
|
+
eventName: "identify",
|
|
656
|
+
ts: nowIso(),
|
|
657
|
+
sessionId,
|
|
658
|
+
anonId: this.anonId,
|
|
659
|
+
userId: normalizedUserId,
|
|
660
|
+
properties: this.withRuntimeMetadata(traits, sessionId),
|
|
661
|
+
platform: this.platform,
|
|
662
|
+
appVersion: this.appVersion,
|
|
663
|
+
...this.withEventContext(),
|
|
664
|
+
type: "identify"
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Convenience helper for login/logout boundaries.
|
|
669
|
+
* - pass a non-empty user id to emit an identify event
|
|
670
|
+
* - pass null/undefined/empty string to clear user linkage
|
|
671
|
+
*/
|
|
672
|
+
setUser(userId, traits) {
|
|
673
|
+
const normalizedUserId = typeof userId === "string" ? this.readRequiredStringOption(userId) : "";
|
|
674
|
+
if (!normalizedUserId) {
|
|
675
|
+
this.clearUser();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
this.identify(normalizedUserId, traits);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Clears the current identified user from in-memory SDK state.
|
|
682
|
+
*/
|
|
683
|
+
clearUser() {
|
|
684
|
+
this.userId = null;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Sends a generic product event.
|
|
688
|
+
*/
|
|
689
|
+
track(eventName, properties) {
|
|
690
|
+
if (!this.consentGranted) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (this.shouldDeferEventsUntilHydrated()) {
|
|
694
|
+
const deferredProperties = this.cloneProperties(properties);
|
|
695
|
+
this.deferEventUntilHydrated(() => {
|
|
696
|
+
this.track(eventName, deferredProperties);
|
|
697
|
+
});
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const sessionId = this.getSessionId();
|
|
701
|
+
if (this.shouldDropOnboardingStepView(eventName, properties, sessionId)) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
this.enqueue({
|
|
705
|
+
eventId: randomId(),
|
|
706
|
+
eventName,
|
|
707
|
+
ts: nowIso(),
|
|
708
|
+
sessionId,
|
|
709
|
+
anonId: this.anonId,
|
|
710
|
+
userId: this.userId,
|
|
711
|
+
properties: this.withRuntimeMetadata(properties, sessionId),
|
|
712
|
+
platform: this.platform,
|
|
713
|
+
appVersion: this.appVersion,
|
|
714
|
+
...this.withEventContext(),
|
|
715
|
+
type: "track"
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Sends a typed onboarding event with conventional onboarding metadata.
|
|
720
|
+
*/
|
|
721
|
+
trackOnboardingEvent(eventName, properties) {
|
|
722
|
+
this.track(eventName, properties);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Creates a scoped onboarding tracker that applies shared flow properties to every onboarding event.
|
|
726
|
+
* This reduces app-side boilerplate while keeping each emitted event fully self-describing.
|
|
727
|
+
*/
|
|
728
|
+
createOnboardingTracker(defaults) {
|
|
729
|
+
const {
|
|
730
|
+
surveyKey: rawDefaultSurveyKey,
|
|
731
|
+
appVersion: rawDefaultAppVersion,
|
|
732
|
+
isNewUser: rawDefaultIsNewUser,
|
|
733
|
+
onboardingFlowId: rawDefaultFlowId,
|
|
734
|
+
onboardingFlowVersion: rawDefaultFlowVersion,
|
|
735
|
+
stepKey: rawDefaultStepKey,
|
|
736
|
+
stepIndex: rawDefaultStepIndex,
|
|
737
|
+
stepCount: rawDefaultStepCount,
|
|
738
|
+
...defaultExtraProperties
|
|
739
|
+
} = defaults;
|
|
740
|
+
const defaultSurveyKey = this.readPropertyAsString(rawDefaultSurveyKey);
|
|
741
|
+
const defaultAppVersion = this.readPropertyAsString(rawDefaultAppVersion);
|
|
742
|
+
const defaultIsNewUser = typeof rawDefaultIsNewUser === "boolean" ? rawDefaultIsNewUser : void 0;
|
|
743
|
+
const defaultFlowId = this.readPropertyAsString(rawDefaultFlowId);
|
|
744
|
+
const defaultFlowVersion = typeof rawDefaultFlowVersion === "string" || typeof rawDefaultFlowVersion === "number" ? rawDefaultFlowVersion : void 0;
|
|
745
|
+
const defaultStepKey = this.readPropertyAsString(rawDefaultStepKey);
|
|
746
|
+
const defaultStepIndex = this.readPropertyAsStepIndex(rawDefaultStepIndex);
|
|
747
|
+
const defaultStepCount = this.readPropertyAsStepIndex(rawDefaultStepCount);
|
|
748
|
+
const mergeEventProperties = (properties) => ({
|
|
749
|
+
...defaultExtraProperties,
|
|
750
|
+
appVersion: defaultAppVersion,
|
|
751
|
+
isNewUser: defaultIsNewUser,
|
|
752
|
+
onboardingFlowId: defaultFlowId,
|
|
753
|
+
onboardingFlowVersion: defaultFlowVersion,
|
|
754
|
+
stepKey: defaultStepKey,
|
|
755
|
+
stepIndex: defaultStepIndex,
|
|
756
|
+
stepCount: defaultStepCount,
|
|
757
|
+
...properties ?? {}
|
|
758
|
+
});
|
|
759
|
+
const track = (eventName, properties) => {
|
|
760
|
+
this.trackOnboardingEvent(eventName, mergeEventProperties(properties));
|
|
761
|
+
};
|
|
762
|
+
const surveyResponse = (input) => {
|
|
763
|
+
this.trackOnboardingSurveyResponse({
|
|
764
|
+
...input,
|
|
765
|
+
surveyKey: input.surveyKey ?? defaultSurveyKey ?? defaultFlowId ?? "onboarding",
|
|
766
|
+
appVersion: input.appVersion ?? defaultAppVersion,
|
|
767
|
+
isNewUser: input.isNewUser ?? defaultIsNewUser,
|
|
768
|
+
onboardingFlowId: input.onboardingFlowId ?? defaultFlowId,
|
|
769
|
+
onboardingFlowVersion: input.onboardingFlowVersion ?? defaultFlowVersion,
|
|
770
|
+
stepKey: input.stepKey ?? defaultStepKey,
|
|
771
|
+
stepIndex: input.stepIndex ?? defaultStepIndex,
|
|
772
|
+
stepCount: input.stepCount ?? defaultStepCount,
|
|
773
|
+
properties: {
|
|
774
|
+
...defaultExtraProperties,
|
|
775
|
+
...input.properties ?? {}
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
};
|
|
779
|
+
const step = (stepKey, stepIndex, properties) => {
|
|
780
|
+
const stepProps = {
|
|
781
|
+
...properties ?? {},
|
|
782
|
+
stepKey,
|
|
783
|
+
stepIndex
|
|
784
|
+
};
|
|
785
|
+
return {
|
|
786
|
+
view: (overrides) => track(ONBOARDING_EVENTS.STEP_VIEW, { ...stepProps, ...overrides ?? {} }),
|
|
787
|
+
complete: (overrides) => track(ONBOARDING_EVENTS.STEP_COMPLETE, { ...stepProps, ...overrides ?? {} }),
|
|
788
|
+
surveyResponse: (input) => surveyResponse({
|
|
789
|
+
...input,
|
|
790
|
+
stepKey,
|
|
791
|
+
stepIndex
|
|
792
|
+
})
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
return {
|
|
796
|
+
track,
|
|
797
|
+
start: (properties) => track(ONBOARDING_EVENTS.START, properties),
|
|
798
|
+
stepView: (properties) => track(ONBOARDING_EVENTS.STEP_VIEW, properties),
|
|
799
|
+
stepComplete: (properties) => track(ONBOARDING_EVENTS.STEP_COMPLETE, properties),
|
|
800
|
+
complete: (properties) => track(ONBOARDING_EVENTS.COMPLETE, properties),
|
|
801
|
+
skip: (properties) => track(ONBOARDING_EVENTS.SKIP, properties),
|
|
802
|
+
surveyResponse,
|
|
803
|
+
step
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Creates a scoped paywall tracker that applies shared paywall defaults to every journey event.
|
|
808
|
+
* Useful when a flow has a stable `source`, `paywallId`, `offering`, or experiment metadata.
|
|
809
|
+
*/
|
|
810
|
+
createPaywallTracker(defaults) {
|
|
811
|
+
const { source: rawDefaultSource, ...defaultProperties } = defaults;
|
|
812
|
+
const defaultSource = this.readRequiredStringOption(rawDefaultSource);
|
|
813
|
+
let currentPaywallEntryId;
|
|
814
|
+
if (!defaultSource) {
|
|
815
|
+
this.log("createPaywallTracker() called without a valid default `source`");
|
|
816
|
+
}
|
|
817
|
+
const mergeProperties = (properties) => {
|
|
818
|
+
const mergedSource = this.readRequiredStringOption(
|
|
819
|
+
this.readPropertyAsString(properties?.source) ?? defaultSource
|
|
820
|
+
);
|
|
821
|
+
return {
|
|
822
|
+
...defaultProperties,
|
|
823
|
+
...properties ?? {},
|
|
824
|
+
source: mergedSource
|
|
825
|
+
};
|
|
826
|
+
};
|
|
827
|
+
const track = (eventName, properties) => {
|
|
828
|
+
const mergedProperties = mergeProperties(properties);
|
|
829
|
+
delete mergedProperties.paywallEntryId;
|
|
830
|
+
if (eventName === PAYWALL_EVENTS.SHOWN) {
|
|
831
|
+
currentPaywallEntryId = randomId();
|
|
832
|
+
mergedProperties.paywallEntryId = currentPaywallEntryId;
|
|
833
|
+
} else {
|
|
834
|
+
if (currentPaywallEntryId) {
|
|
835
|
+
mergedProperties.paywallEntryId = currentPaywallEntryId;
|
|
836
|
+
}
|
|
837
|
+
if (properties?.offering === void 0) {
|
|
838
|
+
delete mergedProperties.offering;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
this.sendPaywallEvent(eventName, mergedProperties, {
|
|
842
|
+
allowPaywallEntryId: true
|
|
843
|
+
});
|
|
844
|
+
};
|
|
845
|
+
return {
|
|
846
|
+
track,
|
|
847
|
+
shown: (properties) => track(PAYWALL_EVENTS.SHOWN, properties),
|
|
848
|
+
skip: (properties) => track(PAYWALL_EVENTS.SKIP, properties),
|
|
849
|
+
purchaseStarted: (properties) => track(PURCHASE_EVENTS.STARTED, properties),
|
|
850
|
+
purchaseSuccess: (properties) => track(PURCHASE_EVENTS.SUCCESS, properties),
|
|
851
|
+
purchaseFailed: (properties) => track(PURCHASE_EVENTS.FAILED, properties),
|
|
852
|
+
purchaseCancel: (properties) => track(PURCHASE_EVENTS.CANCEL, properties)
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Sends a typed paywall/purchase journey event.
|
|
857
|
+
* Direct calls ignore `paywallEntryId`; use `createPaywallTracker(...)` for entry correlation.
|
|
858
|
+
*/
|
|
859
|
+
trackPaywallEvent(eventName, properties) {
|
|
860
|
+
this.sendPaywallEvent(eventName, properties, {
|
|
861
|
+
allowPaywallEntryId: false
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
sendPaywallEvent(eventName, properties, options) {
|
|
865
|
+
if (typeof properties?.source !== "string" || properties.source.trim().length === 0) {
|
|
866
|
+
this.log("Dropping paywall event without required `source` property", { eventName });
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const normalizedProperties = {
|
|
870
|
+
...properties
|
|
871
|
+
};
|
|
872
|
+
if (!options.allowPaywallEntryId && normalizedProperties.paywallEntryId !== void 0) {
|
|
873
|
+
this.log(
|
|
874
|
+
"Ignoring `paywallEntryId` in direct trackPaywallEvent(); use createPaywallTracker()",
|
|
875
|
+
{ eventName }
|
|
876
|
+
);
|
|
877
|
+
delete normalizedProperties.paywallEntryId;
|
|
878
|
+
}
|
|
879
|
+
this.track(eventName, normalizedProperties);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Sends anonymized onboarding survey responses using canonical event naming.
|
|
883
|
+
* Free text and raw numeric values are reduced to coarse buckets.
|
|
884
|
+
*/
|
|
885
|
+
trackOnboardingSurveyResponse(input, eventName = ONBOARDING_SURVEY_EVENTS.RESPONSE) {
|
|
886
|
+
const rows = sanitizeSurveyResponseInput(input);
|
|
887
|
+
for (const properties of rows) {
|
|
888
|
+
this.track(eventName, properties);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Sends a screen-view style event using the `screen:<name>` convention.
|
|
893
|
+
*/
|
|
894
|
+
screen(name, properties) {
|
|
895
|
+
if (!this.consentGranted) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (this.shouldDeferEventsUntilHydrated()) {
|
|
899
|
+
const deferredProperties = this.cloneProperties(properties);
|
|
900
|
+
this.deferEventUntilHydrated(() => {
|
|
901
|
+
this.screen(name, deferredProperties);
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const sessionId = this.getSessionId();
|
|
906
|
+
this.enqueue({
|
|
907
|
+
eventId: randomId(),
|
|
908
|
+
eventName: `screen:${name}`,
|
|
909
|
+
ts: nowIso(),
|
|
910
|
+
sessionId,
|
|
911
|
+
anonId: this.anonId,
|
|
912
|
+
userId: this.userId,
|
|
913
|
+
properties: this.withRuntimeMetadata(properties, sessionId),
|
|
914
|
+
platform: this.platform,
|
|
915
|
+
appVersion: this.appVersion,
|
|
916
|
+
...this.withEventContext(),
|
|
917
|
+
type: "screen"
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Alias of `screen(...)` for web-style naming.
|
|
922
|
+
*/
|
|
923
|
+
page(name, properties) {
|
|
924
|
+
this.screen(name, properties);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Sends a feedback event.
|
|
928
|
+
*/
|
|
929
|
+
feedback(message, rating, properties) {
|
|
930
|
+
if (!this.consentGranted) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (this.shouldDeferEventsUntilHydrated()) {
|
|
934
|
+
const deferredProperties = this.cloneProperties(properties);
|
|
935
|
+
this.deferEventUntilHydrated(() => {
|
|
936
|
+
this.feedback(message, rating, deferredProperties);
|
|
937
|
+
});
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const sessionId = this.getSessionId();
|
|
941
|
+
this.enqueue({
|
|
942
|
+
eventId: randomId(),
|
|
943
|
+
eventName: "feedback_submitted",
|
|
944
|
+
ts: nowIso(),
|
|
945
|
+
sessionId,
|
|
946
|
+
anonId: this.anonId,
|
|
947
|
+
userId: this.userId,
|
|
948
|
+
properties: this.withRuntimeMetadata({ message, rating, ...properties }, sessionId),
|
|
949
|
+
platform: this.platform,
|
|
950
|
+
appVersion: this.appVersion,
|
|
951
|
+
...this.withEventContext(),
|
|
952
|
+
type: "feedback"
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Flushes current event queue to the ingest endpoint.
|
|
957
|
+
*/
|
|
958
|
+
async flush() {
|
|
959
|
+
if (!this.hydrationCompleted && this.deferredEventsBeforeHydration.length > 0) {
|
|
960
|
+
await this.hydrationPromise;
|
|
961
|
+
}
|
|
962
|
+
if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
this.isFlushing = true;
|
|
966
|
+
const batch = this.queue.splice(0, this.batchSize);
|
|
967
|
+
const payload = {
|
|
968
|
+
sentAt: nowIso(),
|
|
969
|
+
events: batch
|
|
970
|
+
};
|
|
971
|
+
const validation = validateIngestBatch(payload);
|
|
972
|
+
if (!validation.success) {
|
|
973
|
+
this.log("Validation failed, dropping batch", validation.reason);
|
|
974
|
+
this.isFlushing = false;
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
try {
|
|
978
|
+
await this.sendWithRetry(payload);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
this.log("Send failed permanently, requeueing batch", error);
|
|
981
|
+
this.queue = [...batch, ...this.queue];
|
|
982
|
+
} finally {
|
|
983
|
+
this.isFlushing = false;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Stops internal timers and unload handlers.
|
|
988
|
+
*/
|
|
989
|
+
shutdown() {
|
|
990
|
+
if (this.flushTimer) {
|
|
991
|
+
clearInterval(this.flushTimer);
|
|
992
|
+
this.flushTimer = null;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
enqueue(event) {
|
|
996
|
+
if (!this.consentGranted) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
this.queue.push(event);
|
|
1000
|
+
if (this.queue.length >= this.batchSize) {
|
|
1001
|
+
this.scheduleFlush();
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
scheduleFlush() {
|
|
1005
|
+
void this.flush().catch((error) => {
|
|
1006
|
+
this.log("Unexpected flush failure", error);
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
async sendWithRetry(payload) {
|
|
1010
|
+
let attempt = 0;
|
|
1011
|
+
let delay = 250;
|
|
1012
|
+
while (attempt <= this.maxRetries) {
|
|
1013
|
+
try {
|
|
1014
|
+
const response = await fetch(`${this.endpoint}/v1/collect`, {
|
|
1015
|
+
method: "POST",
|
|
1016
|
+
headers: {
|
|
1017
|
+
"content-type": "application/json",
|
|
1018
|
+
"x-api-key": this.apiKey
|
|
1019
|
+
},
|
|
1020
|
+
body: JSON.stringify(payload),
|
|
1021
|
+
keepalive: true
|
|
1022
|
+
});
|
|
1023
|
+
if (!response.ok) {
|
|
1024
|
+
throw new Error(`ingest status=${response.status}`);
|
|
1025
|
+
}
|
|
1026
|
+
return;
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
attempt += 1;
|
|
1029
|
+
if (attempt > this.maxRetries) {
|
|
1030
|
+
throw error;
|
|
1031
|
+
}
|
|
1032
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1033
|
+
delay *= 2;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
startAutoFlush() {
|
|
1038
|
+
if (!this.hasIngestConfig) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
this.flushTimer = setInterval(() => {
|
|
1042
|
+
this.scheduleFlush();
|
|
1043
|
+
}, this.flushIntervalMs);
|
|
1044
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
1045
|
+
window.addEventListener("beforeunload", () => {
|
|
1046
|
+
this.scheduleFlush();
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
ensureDeviceId() {
|
|
1051
|
+
if (this.storageReadsAreAsync) {
|
|
1052
|
+
return randomId();
|
|
1053
|
+
}
|
|
1054
|
+
const existing = readStorageSync(this.storage, DEVICE_ID_KEY);
|
|
1055
|
+
if (existing) {
|
|
1056
|
+
return existing;
|
|
1057
|
+
}
|
|
1058
|
+
const value = randomId();
|
|
1059
|
+
writeStorageSync(this.storage, DEVICE_ID_KEY, value);
|
|
1060
|
+
return value;
|
|
1061
|
+
}
|
|
1062
|
+
ensureSessionId() {
|
|
1063
|
+
const now = Date.now();
|
|
1064
|
+
if (this.sessionId && now - this.inMemoryLastSeenMs < this.sessionTimeoutMs) {
|
|
1065
|
+
this.inMemoryLastSeenMs = now;
|
|
1066
|
+
if (!this.storageReadsAreAsync || this.hydrationCompleted) {
|
|
1067
|
+
writeStorageSync(this.storage, SESSION_ID_KEY, this.sessionId);
|
|
1068
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(now));
|
|
1069
|
+
}
|
|
1070
|
+
return this.sessionId;
|
|
1071
|
+
}
|
|
1072
|
+
if (this.storageReadsAreAsync) {
|
|
1073
|
+
this.inMemoryLastSeenMs = now;
|
|
1074
|
+
const next2 = randomId();
|
|
1075
|
+
if (this.hydrationCompleted) {
|
|
1076
|
+
writeStorageSync(this.storage, SESSION_ID_KEY, next2);
|
|
1077
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(now));
|
|
1078
|
+
}
|
|
1079
|
+
return next2;
|
|
1080
|
+
}
|
|
1081
|
+
const existing = readStorageSync(this.storage, SESSION_ID_KEY);
|
|
1082
|
+
const lastSeenRaw = readStorageSync(this.storage, LAST_SEEN_KEY);
|
|
1083
|
+
const lastSeen = lastSeenRaw ? Number(lastSeenRaw) : NaN;
|
|
1084
|
+
if (existing && Number.isFinite(lastSeen) && now - lastSeen < this.sessionTimeoutMs) {
|
|
1085
|
+
this.inMemoryLastSeenMs = now;
|
|
1086
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(now));
|
|
1087
|
+
return existing;
|
|
1088
|
+
}
|
|
1089
|
+
this.inMemoryLastSeenMs = now;
|
|
1090
|
+
const next = randomId();
|
|
1091
|
+
writeStorageSync(this.storage, SESSION_ID_KEY, next);
|
|
1092
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(now));
|
|
1093
|
+
return next;
|
|
1094
|
+
}
|
|
1095
|
+
getSessionId() {
|
|
1096
|
+
const resolvedSessionId = this.ensureSessionId();
|
|
1097
|
+
if (resolvedSessionId !== this.sessionId) {
|
|
1098
|
+
this.sessionId = resolvedSessionId;
|
|
1099
|
+
this.sessionEventSeq = this.readSessionEventSeq(resolvedSessionId);
|
|
1100
|
+
}
|
|
1101
|
+
return this.sessionId;
|
|
1102
|
+
}
|
|
1103
|
+
readSessionEventSeq(sessionId) {
|
|
1104
|
+
const raw = readStorageSync(this.storage, `${SESSION_EVENT_SEQ_PREFIX}${sessionId}`);
|
|
1105
|
+
return this.parseSessionEventSeq(raw);
|
|
1106
|
+
}
|
|
1107
|
+
async readSessionEventSeqAsync(sessionId) {
|
|
1108
|
+
const raw = await readStorageAsync(this.storage, `${SESSION_EVENT_SEQ_PREFIX}${sessionId}`);
|
|
1109
|
+
return this.parseSessionEventSeq(raw);
|
|
1110
|
+
}
|
|
1111
|
+
parseSessionEventSeq(raw) {
|
|
1112
|
+
const parsed = raw ? Number(raw) : Number.NaN;
|
|
1113
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
1114
|
+
return 0;
|
|
1115
|
+
}
|
|
1116
|
+
return Math.floor(parsed);
|
|
1117
|
+
}
|
|
1118
|
+
writeSessionEventSeq(sessionId, value) {
|
|
1119
|
+
writeStorageSync(this.storage, `${SESSION_EVENT_SEQ_PREFIX}${sessionId}`, String(value));
|
|
1120
|
+
}
|
|
1121
|
+
async hydrateIdentityFromStorage() {
|
|
1122
|
+
if (!this.storage) {
|
|
1123
|
+
this.onboardingStepViewStateSessionId = this.sessionId;
|
|
1124
|
+
this.hydrationCompleted = true;
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
try {
|
|
1128
|
+
const [storedAnonId, storedSessionId, storedLastSeen] = await Promise.all([
|
|
1129
|
+
readStorageAsync(this.storage, DEVICE_ID_KEY),
|
|
1130
|
+
readStorageAsync(this.storage, SESSION_ID_KEY),
|
|
1131
|
+
readStorageAsync(this.storage, LAST_SEEN_KEY)
|
|
1132
|
+
]);
|
|
1133
|
+
if (!this.hasExplicitAnonId && storedAnonId) {
|
|
1134
|
+
this.anonId = storedAnonId;
|
|
1135
|
+
}
|
|
1136
|
+
if (!this.hasExplicitSessionId && storedSessionId) {
|
|
1137
|
+
const lastSeenMs = storedLastSeen ? Number(storedLastSeen) : Number.NaN;
|
|
1138
|
+
if (Number.isFinite(lastSeenMs) && Date.now() - lastSeenMs < this.sessionTimeoutMs) {
|
|
1139
|
+
this.sessionId = storedSessionId;
|
|
1140
|
+
this.inMemoryLastSeenMs = Date.now();
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
this.sessionEventSeq = await this.readSessionEventSeqAsync(this.sessionId);
|
|
1144
|
+
await this.hydrateOnboardingStepViewState(this.sessionId);
|
|
1145
|
+
writeStorageSync(this.storage, DEVICE_ID_KEY, this.anonId);
|
|
1146
|
+
writeStorageSync(this.storage, SESSION_ID_KEY, this.sessionId);
|
|
1147
|
+
writeStorageSync(this.storage, LAST_SEEN_KEY, String(this.inMemoryLastSeenMs));
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
this.log("Storage hydration failed; continuing with in-memory identity", error);
|
|
1150
|
+
} finally {
|
|
1151
|
+
this.hydrationCompleted = true;
|
|
1152
|
+
this.drainDeferredEventsAfterHydration();
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
shouldDeferEventsUntilHydrated() {
|
|
1156
|
+
return this.storageReadsAreAsync && !this.hydrationCompleted && (!this.hasExplicitAnonId || !this.hasExplicitSessionId);
|
|
1157
|
+
}
|
|
1158
|
+
deferEventUntilHydrated(action) {
|
|
1159
|
+
const maxDeferredEvents = 1e3;
|
|
1160
|
+
if (this.deferredEventsBeforeHydration.length >= maxDeferredEvents) {
|
|
1161
|
+
this.deferredEventsBeforeHydration.shift();
|
|
1162
|
+
this.log("Dropping oldest deferred pre-hydration event to cap memory usage");
|
|
1163
|
+
}
|
|
1164
|
+
this.deferredEventsBeforeHydration.push(action);
|
|
1165
|
+
}
|
|
1166
|
+
drainDeferredEventsAfterHydration() {
|
|
1167
|
+
if (this.deferredEventsBeforeHydration.length === 0) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const deferred = this.deferredEventsBeforeHydration;
|
|
1171
|
+
this.deferredEventsBeforeHydration = [];
|
|
1172
|
+
for (const action of deferred) {
|
|
1173
|
+
try {
|
|
1174
|
+
action();
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
this.log("Failed to emit deferred pre-hydration event", error);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
cloneProperties(properties) {
|
|
1181
|
+
if (!properties) {
|
|
1182
|
+
return void 0;
|
|
1183
|
+
}
|
|
1184
|
+
return { ...properties };
|
|
1185
|
+
}
|
|
1186
|
+
detectAsyncStorageReads() {
|
|
1187
|
+
if (!this.storage) {
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
try {
|
|
1191
|
+
const value = this.storage.getItem(DEVICE_ID_KEY);
|
|
1192
|
+
if (typeof value === "object" && value !== null && "then" in value) {
|
|
1193
|
+
void value.catch(() => {
|
|
1194
|
+
});
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1197
|
+
return false;
|
|
1198
|
+
} catch {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
withRuntimeMetadata(properties, sessionId) {
|
|
1203
|
+
const sanitized = sanitizeProperties(properties);
|
|
1204
|
+
const nextEventIndex = this.sessionEventSeq + 1;
|
|
1205
|
+
this.sessionEventSeq = nextEventIndex;
|
|
1206
|
+
this.writeSessionEventSeq(sessionId, nextEventIndex);
|
|
1207
|
+
if (typeof sanitized.runtimeEnv !== "string") {
|
|
1208
|
+
sanitized.runtimeEnv = this.runtimeEnv;
|
|
1209
|
+
}
|
|
1210
|
+
if (typeof sanitized.sessionEventIndex !== "number") {
|
|
1211
|
+
sanitized.sessionEventIndex = nextEventIndex;
|
|
1212
|
+
}
|
|
1213
|
+
return sanitized;
|
|
1214
|
+
}
|
|
1215
|
+
shouldDropOnboardingStepView(eventName, properties, sessionId) {
|
|
1216
|
+
if (!this.dedupeOnboardingStepViewsPerSession || eventName !== ONBOARDING_EVENTS.STEP_VIEW) {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
const dedupeKey = this.getOnboardingStepViewDedupeKey(properties);
|
|
1220
|
+
if (!dedupeKey) {
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
this.syncOnboardingStepViewState(sessionId);
|
|
1224
|
+
if (this.onboardingStepViewsSeen.has(dedupeKey)) {
|
|
1225
|
+
this.log("Dropping duplicate onboarding step view for session", { sessionId, dedupeKey });
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1228
|
+
this.onboardingStepViewsSeen.add(dedupeKey);
|
|
1229
|
+
this.persistOnboardingStepViewState(sessionId);
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
getOnboardingStepViewDedupeKey(properties) {
|
|
1233
|
+
if (!properties) {
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
const flowId = toStableKey(this.readPropertyAsString(properties.onboardingFlowId)) ?? "unknown_flow";
|
|
1237
|
+
const flowVersion = toStableKey(this.readPropertyAsString(properties.onboardingFlowVersion)) ?? "unknown_version";
|
|
1238
|
+
const stepKey = toStableKey(this.readPropertyAsString(properties.stepKey));
|
|
1239
|
+
const stepIndex = this.readPropertyAsStepIndex(properties.stepIndex);
|
|
1240
|
+
if (!stepKey && stepIndex === void 0) {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
return `${flowId}|${flowVersion}|${stepKey ?? "unknown_step"}|${stepIndex ?? "unknown_index"}`;
|
|
1244
|
+
}
|
|
1245
|
+
readPropertyAsString(value) {
|
|
1246
|
+
if (typeof value === "string") {
|
|
1247
|
+
return value;
|
|
1248
|
+
}
|
|
1249
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1250
|
+
return String(value);
|
|
1251
|
+
}
|
|
1252
|
+
return void 0;
|
|
1253
|
+
}
|
|
1254
|
+
readPropertyAsStepIndex(value) {
|
|
1255
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1256
|
+
return void 0;
|
|
1257
|
+
}
|
|
1258
|
+
return Math.max(0, Math.floor(value));
|
|
1259
|
+
}
|
|
1260
|
+
syncOnboardingStepViewState(sessionId) {
|
|
1261
|
+
if (this.onboardingStepViewStateSessionId === sessionId) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const persisted = this.parseOnboardingStepViewState(
|
|
1265
|
+
readStorageSync(this.storage, ONBOARDING_STEP_VIEW_STATE_KEY)
|
|
1266
|
+
);
|
|
1267
|
+
this.onboardingStepViewStateSessionId = sessionId;
|
|
1268
|
+
this.onboardingStepViewsSeen = persisted?.sessionId === sessionId ? new Set(persisted.keys) : /* @__PURE__ */ new Set();
|
|
1269
|
+
}
|
|
1270
|
+
async hydrateOnboardingStepViewState(sessionId) {
|
|
1271
|
+
if (!this.dedupeOnboardingStepViewsPerSession) {
|
|
1272
|
+
this.onboardingStepViewStateSessionId = sessionId;
|
|
1273
|
+
this.onboardingStepViewsSeen = /* @__PURE__ */ new Set();
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const persisted = this.parseOnboardingStepViewState(
|
|
1277
|
+
await readStorageAsync(this.storage, ONBOARDING_STEP_VIEW_STATE_KEY)
|
|
1278
|
+
);
|
|
1279
|
+
this.onboardingStepViewStateSessionId = sessionId;
|
|
1280
|
+
this.onboardingStepViewsSeen = persisted?.sessionId === sessionId ? /* @__PURE__ */ new Set([...persisted.keys, ...this.onboardingStepViewsSeen]) : new Set(this.onboardingStepViewsSeen);
|
|
1281
|
+
}
|
|
1282
|
+
persistOnboardingStepViewState(sessionId) {
|
|
1283
|
+
this.onboardingStepViewStateSessionId = sessionId;
|
|
1284
|
+
writeStorageSync(
|
|
1285
|
+
this.storage,
|
|
1286
|
+
ONBOARDING_STEP_VIEW_STATE_KEY,
|
|
1287
|
+
JSON.stringify({
|
|
1288
|
+
sessionId,
|
|
1289
|
+
keys: Array.from(this.onboardingStepViewsSeen)
|
|
1290
|
+
})
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
parseOnboardingStepViewState(raw) {
|
|
1294
|
+
if (!raw) {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
const parsed = JSON.parse(raw);
|
|
1299
|
+
if (typeof parsed.sessionId !== "string" || !Array.isArray(parsed.keys)) {
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
const keys = parsed.keys.filter((value) => typeof value === "string");
|
|
1303
|
+
return {
|
|
1304
|
+
sessionId: parsed.sessionId,
|
|
1305
|
+
keys
|
|
1306
|
+
};
|
|
1307
|
+
} catch {
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
withEventContext() {
|
|
1312
|
+
return {
|
|
1313
|
+
appBuild: this.context.appBuild,
|
|
1314
|
+
osName: this.context.osName,
|
|
1315
|
+
osVersion: this.context.osVersion,
|
|
1316
|
+
country: this.context.country,
|
|
1317
|
+
region: this.context.region,
|
|
1318
|
+
city: this.context.city
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
normalizeOptions(options) {
|
|
1322
|
+
if (typeof options !== "object" || options === null) {
|
|
1323
|
+
return {};
|
|
1324
|
+
}
|
|
1325
|
+
return options;
|
|
1326
|
+
}
|
|
1327
|
+
readRequiredStringOption(value) {
|
|
1328
|
+
if (typeof value !== "string") {
|
|
1329
|
+
return "";
|
|
1330
|
+
}
|
|
1331
|
+
return value.trim();
|
|
1332
|
+
}
|
|
1333
|
+
normalizePlatformOption(value) {
|
|
1334
|
+
const normalized = this.readRequiredStringOption(value).toLowerCase();
|
|
1335
|
+
if (normalized === "web" || normalized === "ios" || normalized === "android" || normalized === "mac" || normalized === "windows") {
|
|
1336
|
+
return normalized;
|
|
1337
|
+
}
|
|
1338
|
+
if (normalized === "macos" || normalized === "osx" || normalized === "darwin") {
|
|
1339
|
+
return "mac";
|
|
1340
|
+
}
|
|
1341
|
+
if (normalized === "win32") {
|
|
1342
|
+
return "windows";
|
|
1343
|
+
}
|
|
1344
|
+
return void 0;
|
|
1345
|
+
}
|
|
1346
|
+
log(message, data) {
|
|
1347
|
+
if (!this.debug) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
console.debug("[analyticscli-sdk]", message, data);
|
|
1351
|
+
}
|
|
1352
|
+
reportMissingApiKey() {
|
|
1353
|
+
console.error(
|
|
1354
|
+
"[analyticscli-sdk] Missing required `apiKey`. Tracking is disabled (safe no-op). Pass your long write key."
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
// src/bootstrap.ts
|
|
1360
|
+
var DEFAULT_API_KEY_ENV_KEYS = [
|
|
1361
|
+
"ANALYTICSCLI_WRITE_KEY",
|
|
1362
|
+
"NEXT_PUBLIC_ANALYTICSCLI_WRITE_KEY",
|
|
1363
|
+
"EXPO_PUBLIC_ANALYTICSCLI_WRITE_KEY",
|
|
1364
|
+
"VITE_ANALYTICSCLI_WRITE_KEY"
|
|
1365
|
+
];
|
|
1366
|
+
var readTrimmedString2 = (value) => {
|
|
1367
|
+
if (typeof value === "string") {
|
|
1368
|
+
return value.trim();
|
|
1369
|
+
}
|
|
1370
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1371
|
+
return String(value).trim();
|
|
1372
|
+
}
|
|
1373
|
+
return "";
|
|
1374
|
+
};
|
|
1375
|
+
var resolveDefaultEnv = () => {
|
|
1376
|
+
const withProcess = globalThis;
|
|
1377
|
+
if (typeof withProcess.process?.env === "object" && withProcess.process.env !== null) {
|
|
1378
|
+
return withProcess.process.env;
|
|
1379
|
+
}
|
|
1380
|
+
return {};
|
|
1381
|
+
};
|
|
1382
|
+
var resolveValueFromEnv = (env, keys) => {
|
|
1383
|
+
for (const key of keys) {
|
|
1384
|
+
const value = readTrimmedString2(env[key]);
|
|
1385
|
+
if (value.length > 0) {
|
|
1386
|
+
return value;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return "";
|
|
1390
|
+
};
|
|
1391
|
+
var toMissingMessage = (details) => {
|
|
1392
|
+
const parts = [];
|
|
1393
|
+
if (details.missingApiKey) {
|
|
1394
|
+
parts.push(`apiKey (searched: ${details.searchedApiKeyEnvKeys.join(", ") || "none"})`);
|
|
1395
|
+
}
|
|
1396
|
+
return `[analyticscli-sdk] Missing required configuration: ${parts.join("; ")}.`;
|
|
1397
|
+
};
|
|
1398
|
+
var initFromEnv = (options = {}) => {
|
|
1399
|
+
const {
|
|
1400
|
+
env,
|
|
1401
|
+
apiKey,
|
|
1402
|
+
apiKeyEnvKeys,
|
|
1403
|
+
missingConfigMode = "noop",
|
|
1404
|
+
onMissingConfig,
|
|
1405
|
+
...clientOptions
|
|
1406
|
+
} = options;
|
|
1407
|
+
const resolvedApiKeyEnvKeys = [...apiKeyEnvKeys ?? DEFAULT_API_KEY_ENV_KEYS];
|
|
1408
|
+
const envSource = env ?? resolveDefaultEnv();
|
|
1409
|
+
const resolvedApiKey = readTrimmedString2(apiKey) || resolveValueFromEnv(envSource, resolvedApiKeyEnvKeys);
|
|
1410
|
+
const missingConfig = {
|
|
1411
|
+
missingApiKey: resolvedApiKey.length === 0,
|
|
1412
|
+
searchedApiKeyEnvKeys: resolvedApiKeyEnvKeys
|
|
1413
|
+
};
|
|
1414
|
+
if (missingConfig.missingApiKey) {
|
|
1415
|
+
onMissingConfig?.(missingConfig);
|
|
1416
|
+
if (missingConfigMode === "throw") {
|
|
1417
|
+
throw new Error(toMissingMessage(missingConfig));
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return new AnalyticsClient({
|
|
1421
|
+
...clientOptions,
|
|
1422
|
+
apiKey: resolvedApiKey
|
|
1423
|
+
});
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
// src/index.ts
|
|
1427
|
+
var normalizeInitInput = (input) => {
|
|
1428
|
+
if (typeof input === "string") {
|
|
1429
|
+
return { apiKey: input };
|
|
1430
|
+
}
|
|
1431
|
+
return input;
|
|
1432
|
+
};
|
|
1433
|
+
var init = (input = {}) => {
|
|
1434
|
+
return new AnalyticsClient(normalizeInitInput(input));
|
|
1435
|
+
};
|
|
1436
|
+
var initAsync = async (input = {}) => {
|
|
1437
|
+
const client = new AnalyticsClient(normalizeInitInput(input));
|
|
1438
|
+
await client.ready();
|
|
1439
|
+
return client;
|
|
1440
|
+
};
|
|
1441
|
+
var BROWSER_API_KEY_ENV_KEYS = [
|
|
1442
|
+
"ANALYTICSCLI_WRITE_KEY",
|
|
1443
|
+
"NEXT_PUBLIC_ANALYTICSCLI_WRITE_KEY",
|
|
1444
|
+
"PUBLIC_ANALYTICSCLI_WRITE_KEY",
|
|
1445
|
+
"VITE_ANALYTICSCLI_WRITE_KEY",
|
|
1446
|
+
"EXPO_PUBLIC_ANALYTICSCLI_WRITE_KEY"
|
|
1447
|
+
];
|
|
1448
|
+
var REACT_NATIVE_API_KEY_ENV_KEYS = [
|
|
1449
|
+
"ANALYTICSCLI_WRITE_KEY",
|
|
1450
|
+
"EXPO_PUBLIC_ANALYTICSCLI_WRITE_KEY"
|
|
1451
|
+
];
|
|
1452
|
+
var initBrowserFromEnv = (options = {}) => {
|
|
1453
|
+
const { apiKeyEnvKeys, ...rest } = options;
|
|
1454
|
+
return initFromEnv({
|
|
1455
|
+
...rest,
|
|
1456
|
+
apiKeyEnvKeys: [...apiKeyEnvKeys ?? BROWSER_API_KEY_ENV_KEYS]
|
|
1457
|
+
});
|
|
1458
|
+
};
|
|
1459
|
+
var initReactNativeFromEnv = (options = {}) => {
|
|
1460
|
+
const { apiKeyEnvKeys, ...rest } = options;
|
|
1461
|
+
return initFromEnv({
|
|
1462
|
+
...rest,
|
|
1463
|
+
apiKeyEnvKeys: [...apiKeyEnvKeys ?? REACT_NATIVE_API_KEY_ENV_KEYS]
|
|
1464
|
+
});
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
export {
|
|
1468
|
+
ONBOARDING_EVENTS,
|
|
1469
|
+
PAYWALL_EVENTS,
|
|
1470
|
+
PURCHASE_EVENTS,
|
|
1471
|
+
ONBOARDING_SURVEY_EVENTS,
|
|
1472
|
+
ONBOARDING_PROGRESS_EVENT_ORDER,
|
|
1473
|
+
PAYWALL_JOURNEY_EVENT_ORDER,
|
|
1474
|
+
ONBOARDING_SCREEN_EVENT_PREFIXES,
|
|
1475
|
+
PAYWALL_ANCHOR_EVENT_CANDIDATES,
|
|
1476
|
+
PAYWALL_SKIP_EVENT_CANDIDATES,
|
|
1477
|
+
PURCHASE_SUCCESS_EVENT_CANDIDATES,
|
|
1478
|
+
AnalyticsClient,
|
|
1479
|
+
DEFAULT_API_KEY_ENV_KEYS,
|
|
1480
|
+
initFromEnv,
|
|
1481
|
+
init,
|
|
1482
|
+
initAsync,
|
|
1483
|
+
BROWSER_API_KEY_ENV_KEYS,
|
|
1484
|
+
REACT_NATIVE_API_KEY_ENV_KEYS,
|
|
1485
|
+
initBrowserFromEnv,
|
|
1486
|
+
initReactNativeFromEnv
|
|
1487
|
+
};
|