@pl4yzonellc/empire-analytics 0.0.2
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +744 -0
- package/dist/chunk-J5GXHJNU.js +1105 -0
- package/dist/chunk-J5GXHJNU.js.map +1 -0
- package/dist/index.d.ts +200 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/index.d.ts +91 -0
- package/dist/testing/index.js +108 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-Bzaq1v4t.d.ts +507 -0
- package/package.json +97 -0
|
@@ -0,0 +1,1105 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var AnalyticsError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
cause;
|
|
5
|
+
constructor(message, options = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "AnalyticsError";
|
|
8
|
+
this.code = options.code ?? "unknown";
|
|
9
|
+
if (options.cause !== void 0) {
|
|
10
|
+
this.cause = options.cause;
|
|
11
|
+
}
|
|
12
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var AnalyticsConfigError = class extends AnalyticsError {
|
|
16
|
+
constructor(message, options = {}) {
|
|
17
|
+
super(message, { ...options, code: "config_invalid" });
|
|
18
|
+
this.name = "AnalyticsConfigError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function toAnalyticsError(value, code) {
|
|
22
|
+
if (value instanceof AnalyticsError) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
const message = value instanceof Error && value.message ? value.message : `Analytics operation failed (${code})`;
|
|
26
|
+
return new AnalyticsError(message, { code, cause: value });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/core/environment.ts
|
|
30
|
+
var ANALYTICS_ENVIRONMENTS = [
|
|
31
|
+
"development",
|
|
32
|
+
"test",
|
|
33
|
+
"staging",
|
|
34
|
+
"production"
|
|
35
|
+
];
|
|
36
|
+
function isAnalyticsEnvironment(value) {
|
|
37
|
+
return typeof value === "string" && ANALYTICS_ENVIRONMENTS.includes(value);
|
|
38
|
+
}
|
|
39
|
+
function defaultEnabledForEnvironment(environment) {
|
|
40
|
+
return environment === "production";
|
|
41
|
+
}
|
|
42
|
+
function defaultDebugForEnvironment(environment) {
|
|
43
|
+
return environment === "development";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/privacy/errors.ts
|
|
47
|
+
var AnalyticsPrivacyError = class extends AnalyticsError {
|
|
48
|
+
key;
|
|
49
|
+
category;
|
|
50
|
+
constructor(params) {
|
|
51
|
+
super(`Analytics blocked a ${params.category} property: "${params.key}"`, {
|
|
52
|
+
code: "privacy_violation"
|
|
53
|
+
});
|
|
54
|
+
this.name = "AnalyticsPrivacyError";
|
|
55
|
+
this.key = params.key;
|
|
56
|
+
this.category = params.category;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/privacy/rules.ts
|
|
61
|
+
var SENSITIVE_SUBSTRINGS = [
|
|
62
|
+
"password",
|
|
63
|
+
"passphrase",
|
|
64
|
+
"passcode",
|
|
65
|
+
"pwd",
|
|
66
|
+
"secret",
|
|
67
|
+
"token",
|
|
68
|
+
"apikey",
|
|
69
|
+
"accesskey",
|
|
70
|
+
"privatekey",
|
|
71
|
+
"secretkey",
|
|
72
|
+
"clientsecret",
|
|
73
|
+
"authorization",
|
|
74
|
+
"bearer",
|
|
75
|
+
"credential",
|
|
76
|
+
"jwt",
|
|
77
|
+
"creditcard",
|
|
78
|
+
"cardnumber",
|
|
79
|
+
"cardno",
|
|
80
|
+
"cvv",
|
|
81
|
+
"cvc",
|
|
82
|
+
"cardsecuritycode",
|
|
83
|
+
"ssn",
|
|
84
|
+
"socialsecurity",
|
|
85
|
+
"sortcode",
|
|
86
|
+
"routingnumber",
|
|
87
|
+
"accountnumber",
|
|
88
|
+
"iban",
|
|
89
|
+
"bankaccount"
|
|
90
|
+
];
|
|
91
|
+
var SENSITIVE_EXACT = /* @__PURE__ */ new Set([
|
|
92
|
+
"pin",
|
|
93
|
+
"otp",
|
|
94
|
+
"mfa",
|
|
95
|
+
"totp",
|
|
96
|
+
"auth",
|
|
97
|
+
"cookie",
|
|
98
|
+
"session",
|
|
99
|
+
"sessionid",
|
|
100
|
+
"salt",
|
|
101
|
+
"signature",
|
|
102
|
+
"sig",
|
|
103
|
+
"taxid",
|
|
104
|
+
"ein",
|
|
105
|
+
"nationalid",
|
|
106
|
+
"passportnumber",
|
|
107
|
+
"driverslicense",
|
|
108
|
+
"licensenumber",
|
|
109
|
+
"ccv",
|
|
110
|
+
"securitycode"
|
|
111
|
+
]);
|
|
112
|
+
var PII_SUBSTRINGS = [
|
|
113
|
+
"email",
|
|
114
|
+
"phone",
|
|
115
|
+
"firstname",
|
|
116
|
+
"lastname",
|
|
117
|
+
"fullname",
|
|
118
|
+
"givenname",
|
|
119
|
+
"familyname",
|
|
120
|
+
"streetaddress",
|
|
121
|
+
"homeaddress",
|
|
122
|
+
"mailingaddress",
|
|
123
|
+
"billingaddress",
|
|
124
|
+
"shippingaddress"
|
|
125
|
+
];
|
|
126
|
+
var PII_EXACT = /* @__PURE__ */ new Set([
|
|
127
|
+
"phonenumber",
|
|
128
|
+
"mobilenumber",
|
|
129
|
+
"mobilephone",
|
|
130
|
+
"telephone",
|
|
131
|
+
"tel",
|
|
132
|
+
"address",
|
|
133
|
+
"dob",
|
|
134
|
+
"dateofbirth",
|
|
135
|
+
"birthdate",
|
|
136
|
+
"ipaddress"
|
|
137
|
+
]);
|
|
138
|
+
var DEFAULT_SENSITIVE_KEYS = [
|
|
139
|
+
...SENSITIVE_SUBSTRINGS,
|
|
140
|
+
...SENSITIVE_EXACT
|
|
141
|
+
];
|
|
142
|
+
var DEFAULT_PII_KEYS = [...PII_SUBSTRINGS, ...PII_EXACT];
|
|
143
|
+
function normalizeKey(key) {
|
|
144
|
+
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
145
|
+
}
|
|
146
|
+
function classifyKey(rawKey, options = {}) {
|
|
147
|
+
const normalized = normalizeKey(rawKey);
|
|
148
|
+
if (normalized === "") {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const allow = new Set((options.allowKeys ?? []).map(normalizeKey));
|
|
152
|
+
if (allow.has(normalized)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const extraBlocked = (options.extraBlockedKeys ?? []).map(normalizeKey);
|
|
156
|
+
if (extraBlocked.some((term) => term !== "" && normalized.includes(term))) {
|
|
157
|
+
return "sensitive";
|
|
158
|
+
}
|
|
159
|
+
if (SENSITIVE_EXACT.has(normalized)) {
|
|
160
|
+
return "sensitive";
|
|
161
|
+
}
|
|
162
|
+
if (SENSITIVE_SUBSTRINGS.some((term) => normalized.includes(term))) {
|
|
163
|
+
return "sensitive";
|
|
164
|
+
}
|
|
165
|
+
if (PII_EXACT.has(normalized)) {
|
|
166
|
+
return "pii";
|
|
167
|
+
}
|
|
168
|
+
if (PII_SUBSTRINGS.some((term) => normalized.includes(term))) {
|
|
169
|
+
return "pii";
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/utils/logger.ts
|
|
175
|
+
var NOOP = () => void 0;
|
|
176
|
+
var SILENT_LOGGER = {
|
|
177
|
+
debug: NOOP,
|
|
178
|
+
info: NOOP,
|
|
179
|
+
warn: NOOP,
|
|
180
|
+
error: NOOP,
|
|
181
|
+
child: () => SILENT_LOGGER
|
|
182
|
+
};
|
|
183
|
+
function createLogger(options = {}) {
|
|
184
|
+
const { debug = false, prefix = "[Analytics]", sink } = options;
|
|
185
|
+
if (!debug) {
|
|
186
|
+
return SILENT_LOGGER;
|
|
187
|
+
}
|
|
188
|
+
const target = sink ?? (typeof console !== "undefined" ? console : { debug: NOOP, info: NOOP, warn: NOOP, error: NOOP });
|
|
189
|
+
const write = (level) => (message, data) => {
|
|
190
|
+
const line = `${prefix} ${message}`;
|
|
191
|
+
if (data === void 0) {
|
|
192
|
+
target[level](line);
|
|
193
|
+
} else {
|
|
194
|
+
target[level](line, data);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const logger = {
|
|
198
|
+
debug: write("debug"),
|
|
199
|
+
info: write("info"),
|
|
200
|
+
warn: write("warn"),
|
|
201
|
+
error: write("error"),
|
|
202
|
+
child: (scope) => createLogger({ debug, prefix: `${prefix} ${scope}`, sink })
|
|
203
|
+
};
|
|
204
|
+
return logger;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/privacy/sanitize.ts
|
|
208
|
+
var DEFAULT_POLICY = {
|
|
209
|
+
onSensitive: "strip",
|
|
210
|
+
onPii: "warn",
|
|
211
|
+
blockKeys: [],
|
|
212
|
+
allowKeys: [],
|
|
213
|
+
maxDepth: 4,
|
|
214
|
+
maxProperties: 64,
|
|
215
|
+
maxStringLength: 1024,
|
|
216
|
+
maxArrayLength: 64
|
|
217
|
+
};
|
|
218
|
+
var DROP = { keep: false };
|
|
219
|
+
function sanitizeProperties(input, policy = DEFAULT_POLICY, logger = SILENT_LOGGER) {
|
|
220
|
+
const blocked = [];
|
|
221
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
222
|
+
let kept = 0;
|
|
223
|
+
const handleViolation = (path, category) => {
|
|
224
|
+
blocked.push(path);
|
|
225
|
+
const mode = category === "sensitive" ? policy.onSensitive : policy.onPii;
|
|
226
|
+
if (mode === "throw") {
|
|
227
|
+
throw new AnalyticsPrivacyError({ key: path, category });
|
|
228
|
+
}
|
|
229
|
+
if (mode === "warn") {
|
|
230
|
+
logger.warn(`Blocked ${category} property "${path}" (value omitted)`);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const walkValue = (value2, depth, path) => {
|
|
234
|
+
if (value2 === null || value2 === void 0) return DROP;
|
|
235
|
+
switch (typeof value2) {
|
|
236
|
+
case "function":
|
|
237
|
+
case "symbol":
|
|
238
|
+
logger.debug(`Dropped ${typeof value2} value at "${path}"`);
|
|
239
|
+
return DROP;
|
|
240
|
+
case "bigint":
|
|
241
|
+
return { keep: true, value: value2.toString() };
|
|
242
|
+
case "boolean":
|
|
243
|
+
return { keep: true, value: value2 };
|
|
244
|
+
case "number":
|
|
245
|
+
return Number.isFinite(value2) ? { keep: true, value: value2 } : DROP;
|
|
246
|
+
case "string":
|
|
247
|
+
return {
|
|
248
|
+
keep: true,
|
|
249
|
+
value: value2.length > policy.maxStringLength ? value2.slice(0, policy.maxStringLength) : value2
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (value2 instanceof Date) {
|
|
253
|
+
return Number.isFinite(value2.getTime()) ? { keep: true, value: value2.toISOString() } : DROP;
|
|
254
|
+
}
|
|
255
|
+
if (typeof value2 !== "object" || value2 === null) return DROP;
|
|
256
|
+
if (seen.has(value2)) {
|
|
257
|
+
logger.debug(`Dropped circular reference at "${path}"`);
|
|
258
|
+
return DROP;
|
|
259
|
+
}
|
|
260
|
+
if (depth >= policy.maxDepth) {
|
|
261
|
+
logger.debug(`Dropped value at "${path}" (max depth ${policy.maxDepth})`);
|
|
262
|
+
return DROP;
|
|
263
|
+
}
|
|
264
|
+
seen.add(value2);
|
|
265
|
+
let outcome;
|
|
266
|
+
if (Array.isArray(value2)) {
|
|
267
|
+
const out = [];
|
|
268
|
+
for (const item of value2.slice(0, policy.maxArrayLength)) {
|
|
269
|
+
const child = walkValue(item, depth + 1, `${path}[]`);
|
|
270
|
+
if (child.keep) out.push(child.value);
|
|
271
|
+
}
|
|
272
|
+
outcome = { keep: true, value: out };
|
|
273
|
+
} else {
|
|
274
|
+
outcome = {
|
|
275
|
+
keep: true,
|
|
276
|
+
value: walkObject(value2, depth, path)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
seen.delete(value2);
|
|
280
|
+
return outcome;
|
|
281
|
+
};
|
|
282
|
+
const walkObject = (obj, depth, parentPath) => {
|
|
283
|
+
const out = {};
|
|
284
|
+
for (const [key, raw] of Object.entries(obj)) {
|
|
285
|
+
const path = parentPath ? `${parentPath}.${key}` : key;
|
|
286
|
+
const classification = classifyKey(key, {
|
|
287
|
+
extraBlockedKeys: policy.blockKeys,
|
|
288
|
+
allowKeys: policy.allowKeys
|
|
289
|
+
});
|
|
290
|
+
if (classification === "sensitive") {
|
|
291
|
+
handleViolation(path, "sensitive");
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (classification === "pii") {
|
|
295
|
+
if (policy.onPii === "allow") ; else {
|
|
296
|
+
handleViolation(path, "pii");
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (kept >= policy.maxProperties) {
|
|
301
|
+
logger.debug(`Reached max property count (${policy.maxProperties}); dropping "${path}"`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const child = walkValue(raw, depth + 1, path);
|
|
305
|
+
if (child.keep) {
|
|
306
|
+
out[key] = child.value;
|
|
307
|
+
kept += 1;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
};
|
|
312
|
+
const root = input ?? {};
|
|
313
|
+
seen.add(root);
|
|
314
|
+
const value = walkObject(root, 0, "");
|
|
315
|
+
return { value, blocked };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/privacy/path.ts
|
|
319
|
+
function stripSensitiveQueryParams(pathOrUrl) {
|
|
320
|
+
const queryIndex = pathOrUrl.indexOf("?");
|
|
321
|
+
if (queryIndex === -1) {
|
|
322
|
+
return pathOrUrl;
|
|
323
|
+
}
|
|
324
|
+
const base = pathOrUrl.slice(0, queryIndex);
|
|
325
|
+
const afterQuery = pathOrUrl.slice(queryIndex + 1);
|
|
326
|
+
const hashIndex = afterQuery.indexOf("#");
|
|
327
|
+
const rawQuery = hashIndex === -1 ? afterQuery : afterQuery.slice(0, hashIndex);
|
|
328
|
+
const hash = hashIndex === -1 ? "" : afterQuery.slice(hashIndex);
|
|
329
|
+
if (rawQuery === "") {
|
|
330
|
+
return pathOrUrl;
|
|
331
|
+
}
|
|
332
|
+
const kept = [];
|
|
333
|
+
for (const pair of rawQuery.split("&")) {
|
|
334
|
+
if (pair === "") continue;
|
|
335
|
+
const eq = pair.indexOf("=");
|
|
336
|
+
const key = decodeURIComponentSafe(eq === -1 ? pair : pair.slice(0, eq));
|
|
337
|
+
if (classifyKey(key) === "sensitive") {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
kept.push(pair);
|
|
341
|
+
}
|
|
342
|
+
const query = kept.length > 0 ? `?${kept.join("&")}` : "";
|
|
343
|
+
return `${base}${query}${hash}`;
|
|
344
|
+
}
|
|
345
|
+
function decodeURIComponentSafe(value) {
|
|
346
|
+
try {
|
|
347
|
+
return decodeURIComponent(value.replace(/\+/g, " "));
|
|
348
|
+
} catch {
|
|
349
|
+
return value;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/utils/stable-stringify.ts
|
|
354
|
+
function stableStringify(value) {
|
|
355
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
356
|
+
const walk = (input) => {
|
|
357
|
+
if (input === null) return "null";
|
|
358
|
+
if (input === void 0) return "undefined";
|
|
359
|
+
if (typeof input === "function") return '"[fn]"';
|
|
360
|
+
if (typeof input === "symbol") return '"[symbol]"';
|
|
361
|
+
if (typeof input === "string") return JSON.stringify(input);
|
|
362
|
+
if (typeof input === "boolean") return input ? "true" : "false";
|
|
363
|
+
if (typeof input === "bigint") return `"${input.toString()}n"`;
|
|
364
|
+
if (typeof input === "number") return Number.isFinite(input) ? `${input}` : '"[number]"';
|
|
365
|
+
if (typeof input === "object") {
|
|
366
|
+
if (seen.has(input)) return '"[circular]"';
|
|
367
|
+
seen.add(input);
|
|
368
|
+
let result;
|
|
369
|
+
if (Array.isArray(input)) {
|
|
370
|
+
result = `[${input.map((item) => walk(item)).join(",")}]`;
|
|
371
|
+
} else {
|
|
372
|
+
const record = input;
|
|
373
|
+
const keys = Object.keys(record).sort();
|
|
374
|
+
result = `{${keys.map((key) => `${JSON.stringify(key)}:${walk(record[key])}`).join(",")}}`;
|
|
375
|
+
}
|
|
376
|
+
seen.delete(input);
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
return '"[unknown]"';
|
|
380
|
+
};
|
|
381
|
+
return walk(value);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/core/config.ts
|
|
385
|
+
function resolvePrivacy(privacy, environment, debug) {
|
|
386
|
+
const isProduction = environment === "production";
|
|
387
|
+
const defaultSensitive = debug ? "throw" : isProduction ? "strip" : "warn";
|
|
388
|
+
let onSensitive = privacy?.onSensitive ?? defaultSensitive;
|
|
389
|
+
let onPii = privacy?.onPii ?? "warn";
|
|
390
|
+
if (isProduction && onSensitive === "throw") onSensitive = "strip";
|
|
391
|
+
if (isProduction && onPii === "throw") onPii = "warn";
|
|
392
|
+
const clampPositive = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
393
|
+
return {
|
|
394
|
+
onSensitive,
|
|
395
|
+
onPii,
|
|
396
|
+
blockKeys: privacy?.blockKeys ?? [],
|
|
397
|
+
allowKeys: privacy?.allowKeys ?? [],
|
|
398
|
+
maxDepth: clampPositive(privacy?.maxDepth, 4),
|
|
399
|
+
maxProperties: clampPositive(privacy?.maxProperties, 64),
|
|
400
|
+
maxStringLength: clampPositive(privacy?.maxStringLength, 1024),
|
|
401
|
+
maxArrayLength: clampPositive(privacy?.maxArrayLength, 64)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function resolvePlausible(config) {
|
|
405
|
+
const p = config.plausible;
|
|
406
|
+
if (!p || typeof p.domain !== "string" || p.domain.trim() === "") {
|
|
407
|
+
throw new AnalyticsConfigError(
|
|
408
|
+
"Plausible provider requires a non-empty `plausible.domain` (e.g. 'example.com')."
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const resolved = {
|
|
412
|
+
domain: p.domain.trim(),
|
|
413
|
+
mode: p.mode === "package" ? "package" : "cdn",
|
|
414
|
+
injectScript: p.injectScript ?? true,
|
|
415
|
+
autoPageViews: p.autoPageViews ?? true,
|
|
416
|
+
hashRouting: p.hashRouting ?? false,
|
|
417
|
+
outboundLinks: p.outboundLinks ?? false,
|
|
418
|
+
fileDownloads: p.fileDownloads ?? false,
|
|
419
|
+
taggedEvents: p.taggedEvents ?? false,
|
|
420
|
+
revenue: p.revenue ?? true,
|
|
421
|
+
trackLocalhost: p.trackLocalhost ?? false
|
|
422
|
+
};
|
|
423
|
+
if (p.scriptUrl) resolved.scriptUrl = p.scriptUrl;
|
|
424
|
+
if (p.endpoint) resolved.endpoint = p.endpoint;
|
|
425
|
+
return { resolved };
|
|
426
|
+
}
|
|
427
|
+
function resolveConsole(config) {
|
|
428
|
+
const c = config.console;
|
|
429
|
+
const fallbackSink = typeof console !== "undefined" ? console : {
|
|
430
|
+
log: () => void 0,
|
|
431
|
+
info: () => void 0,
|
|
432
|
+
group: () => void 0,
|
|
433
|
+
groupEnd: () => void 0
|
|
434
|
+
};
|
|
435
|
+
return {
|
|
436
|
+
sink: c?.sink ?? fallbackSink,
|
|
437
|
+
prefix: c?.prefix ?? "[Analytics:console]"
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function resolveConfig(config) {
|
|
441
|
+
if (config == null || typeof config !== "object") {
|
|
442
|
+
throw new AnalyticsConfigError("Analytics config must be an object.");
|
|
443
|
+
}
|
|
444
|
+
if (!isAnalyticsEnvironment(config.environment)) {
|
|
445
|
+
throw new AnalyticsConfigError(
|
|
446
|
+
`Unknown environment "${String(
|
|
447
|
+
config.environment
|
|
448
|
+
)}". Expected: development | test | staging | production.`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
const environment = config.environment;
|
|
452
|
+
const providerName = config.provider;
|
|
453
|
+
if (providerName !== "plausible" && providerName !== "console" && providerName !== "custom") {
|
|
454
|
+
throw new AnalyticsConfigError(
|
|
455
|
+
`Unknown provider "${providerName}". Expected: plausible | console | custom.`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
const enabledExplicit = typeof config.enabled === "boolean";
|
|
459
|
+
const enabled = enabledExplicit ? config.enabled : defaultEnabledForEnvironment(environment);
|
|
460
|
+
const disabledReason = enabled ? void 0 : enabledExplicit ? "analytics disabled by config (enabled: false)" : `analytics disabled by default for the "${environment}" environment`;
|
|
461
|
+
const debug = typeof config.debug === "boolean" ? config.debug : defaultDebugForEnvironment(environment);
|
|
462
|
+
const resolved = {
|
|
463
|
+
provider: config.provider,
|
|
464
|
+
environment,
|
|
465
|
+
enabled,
|
|
466
|
+
debug,
|
|
467
|
+
includeTenantKey: config.includeTenantKey ?? false,
|
|
468
|
+
tenantPropertyName: config.tenantPropertyName ?? "tenant",
|
|
469
|
+
defaultProperties: { ...config.defaultProperties },
|
|
470
|
+
privacy: resolvePrivacy(config.privacy, environment, debug)
|
|
471
|
+
};
|
|
472
|
+
if (disabledReason) resolved.disabledReason = disabledReason;
|
|
473
|
+
if (typeof config.tenantKey === "string" && config.tenantKey !== "") {
|
|
474
|
+
resolved.tenantKey = config.tenantKey;
|
|
475
|
+
}
|
|
476
|
+
if (config.onError) resolved.onError = config.onError;
|
|
477
|
+
if (config.onEvent) resolved.onEvent = config.onEvent;
|
|
478
|
+
if (config.provider === "plausible") {
|
|
479
|
+
resolved.plausible = resolvePlausible(config).resolved;
|
|
480
|
+
} else if (config.provider === "console") {
|
|
481
|
+
resolved.console = resolveConsole(config);
|
|
482
|
+
} else {
|
|
483
|
+
if (typeof config.adapter !== "function") {
|
|
484
|
+
throw new AnalyticsConfigError(
|
|
485
|
+
"Custom provider requires an `adapter` factory: { provider: 'custom', adapter: (ctx) => AnalyticsAdapter }."
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
resolved.adapter = config.adapter;
|
|
489
|
+
}
|
|
490
|
+
return resolved;
|
|
491
|
+
}
|
|
492
|
+
function configSignature(config) {
|
|
493
|
+
return stableStringify(config);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/utils/env.ts
|
|
497
|
+
function getWindow() {
|
|
498
|
+
return typeof window !== "undefined" ? window : void 0;
|
|
499
|
+
}
|
|
500
|
+
function getDocument() {
|
|
501
|
+
return typeof document !== "undefined" ? document : void 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/providers/plausible/script.ts
|
|
505
|
+
var SCRIPT_MARKER = "data-empire-analytics";
|
|
506
|
+
var SCRIPT_MARKER_VALUE = "plausible";
|
|
507
|
+
var DEFAULT_HOST = "https://plausible.io";
|
|
508
|
+
var EXTENSION_ORDER = [
|
|
509
|
+
"hash",
|
|
510
|
+
"outbound-links",
|
|
511
|
+
"file-downloads",
|
|
512
|
+
"tagged-events",
|
|
513
|
+
"pageview-props",
|
|
514
|
+
"revenue",
|
|
515
|
+
"compat",
|
|
516
|
+
"manual",
|
|
517
|
+
"local"
|
|
518
|
+
];
|
|
519
|
+
var loadPromises = /* @__PURE__ */ new Map();
|
|
520
|
+
var managedScripts = /* @__PURE__ */ new Set();
|
|
521
|
+
var createdQueueStub = false;
|
|
522
|
+
function buildScriptUrl(options) {
|
|
523
|
+
if (options.scriptUrl) {
|
|
524
|
+
return options.scriptUrl;
|
|
525
|
+
}
|
|
526
|
+
const requested = new Set(options.extensions ?? []);
|
|
527
|
+
const ordered = EXTENSION_ORDER.filter((ext) => requested.has(ext));
|
|
528
|
+
const suffix = ordered.length > 0 ? `.${ordered.join(".")}` : "";
|
|
529
|
+
return `${DEFAULT_HOST}/js/script${suffix}.js`;
|
|
530
|
+
}
|
|
531
|
+
function ensureQueueStub() {
|
|
532
|
+
const win = getWindow();
|
|
533
|
+
if (!win) return;
|
|
534
|
+
if (typeof win.plausible === "function") return;
|
|
535
|
+
const stub = function plausibleStub(...args) {
|
|
536
|
+
(stub.q = stub.q ?? []).push(args);
|
|
537
|
+
};
|
|
538
|
+
stub.q = [];
|
|
539
|
+
win.plausible = stub;
|
|
540
|
+
createdQueueStub = true;
|
|
541
|
+
}
|
|
542
|
+
function findExistingScript(doc, src) {
|
|
543
|
+
const ours = doc.querySelector(
|
|
544
|
+
`script[${SCRIPT_MARKER}="${SCRIPT_MARKER_VALUE}"]`
|
|
545
|
+
);
|
|
546
|
+
if (ours) return ours;
|
|
547
|
+
const scripts = Array.from(doc.querySelectorAll("script[src]"));
|
|
548
|
+
return scripts.find(
|
|
549
|
+
(el) => el.src === src || el.src.includes("/js/script") && el.hasAttribute("data-domain")
|
|
550
|
+
) ?? null;
|
|
551
|
+
}
|
|
552
|
+
function loadPlausibleScript(options, logger = SILENT_LOGGER) {
|
|
553
|
+
const win = getWindow();
|
|
554
|
+
const doc = getDocument();
|
|
555
|
+
if (!win || !doc) {
|
|
556
|
+
logger.debug("No browser environment; skipping Plausible script load");
|
|
557
|
+
return Promise.resolve();
|
|
558
|
+
}
|
|
559
|
+
const src = buildScriptUrl(options);
|
|
560
|
+
ensureQueueStub();
|
|
561
|
+
const cached = loadPromises.get(src);
|
|
562
|
+
if (cached) return cached;
|
|
563
|
+
const existing = findExistingScript(doc, src);
|
|
564
|
+
if (existing) {
|
|
565
|
+
logger.debug("Plausible script already present; not injecting a duplicate");
|
|
566
|
+
const resolved = Promise.resolve();
|
|
567
|
+
loadPromises.set(src, resolved);
|
|
568
|
+
return resolved;
|
|
569
|
+
}
|
|
570
|
+
const promise = new Promise((resolve, reject) => {
|
|
571
|
+
const script = doc.createElement("script");
|
|
572
|
+
script.src = src;
|
|
573
|
+
script.defer = true;
|
|
574
|
+
script.setAttribute(SCRIPT_MARKER, SCRIPT_MARKER_VALUE);
|
|
575
|
+
script.setAttribute("data-domain", options.domain);
|
|
576
|
+
if (options.endpoint) {
|
|
577
|
+
script.setAttribute("data-api", options.endpoint);
|
|
578
|
+
}
|
|
579
|
+
script.addEventListener("load", () => {
|
|
580
|
+
logger.debug("Plausible script loaded", { src });
|
|
581
|
+
resolve();
|
|
582
|
+
});
|
|
583
|
+
script.addEventListener("error", () => {
|
|
584
|
+
managedScripts.delete(script);
|
|
585
|
+
script.remove();
|
|
586
|
+
loadPromises.delete(src);
|
|
587
|
+
logger.debug("Plausible script failed to load", { src });
|
|
588
|
+
reject(
|
|
589
|
+
new AnalyticsError("Analytics tracking script failed to load", {
|
|
590
|
+
code: "script_load_failed"
|
|
591
|
+
})
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
const mount = doc.head ?? doc.body ?? doc.documentElement;
|
|
595
|
+
mount.appendChild(script);
|
|
596
|
+
managedScripts.add(script);
|
|
597
|
+
});
|
|
598
|
+
loadPromises.set(src, promise);
|
|
599
|
+
return promise;
|
|
600
|
+
}
|
|
601
|
+
function removePlausibleScript() {
|
|
602
|
+
for (const script of managedScripts) {
|
|
603
|
+
script.remove();
|
|
604
|
+
}
|
|
605
|
+
managedScripts.clear();
|
|
606
|
+
loadPromises.clear();
|
|
607
|
+
if (createdQueueStub) {
|
|
608
|
+
const win = getWindow();
|
|
609
|
+
if (win) {
|
|
610
|
+
try {
|
|
611
|
+
delete win.plausible;
|
|
612
|
+
} catch {
|
|
613
|
+
win.plausible = void 0;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
createdQueueStub = false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/providers/plausible/revenue.ts
|
|
621
|
+
function toPlausiblePayload(properties) {
|
|
622
|
+
if (!properties) return void 0;
|
|
623
|
+
const { revenue, currency, ...rest } = properties;
|
|
624
|
+
const amount = typeof revenue === "number" ? revenue : isRevenueObject(revenue) && typeof revenue.amount === "number" ? revenue.amount : void 0;
|
|
625
|
+
const currencyCode = typeof currency === "string" ? currency : isRevenueObject(revenue) && typeof revenue.currency === "string" ? revenue.currency : void 0;
|
|
626
|
+
const props = {};
|
|
627
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
628
|
+
const coerced = coerce(value);
|
|
629
|
+
if (coerced !== void 0) {
|
|
630
|
+
props[key] = coerced;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const options = {};
|
|
634
|
+
if (amount !== void 0 && Number.isFinite(amount) && currencyCode) {
|
|
635
|
+
options.revenue = { amount, currency: currencyCode };
|
|
636
|
+
} else {
|
|
637
|
+
if (amount !== void 0 && Number.isFinite(amount)) props.revenue = amount;
|
|
638
|
+
if (currencyCode) props.currency = currencyCode;
|
|
639
|
+
}
|
|
640
|
+
if (Object.keys(props).length > 0) {
|
|
641
|
+
options.props = props;
|
|
642
|
+
}
|
|
643
|
+
return Object.keys(options).length > 0 ? options : void 0;
|
|
644
|
+
}
|
|
645
|
+
function isRevenueObject(value) {
|
|
646
|
+
return typeof value === "object" && value !== null;
|
|
647
|
+
}
|
|
648
|
+
function coerce(value) {
|
|
649
|
+
if (value === null || value === void 0) return void 0;
|
|
650
|
+
const type = typeof value;
|
|
651
|
+
if (type === "string" || type === "number" || type === "boolean") {
|
|
652
|
+
return value;
|
|
653
|
+
}
|
|
654
|
+
if (type === "object") {
|
|
655
|
+
try {
|
|
656
|
+
return JSON.stringify(value);
|
|
657
|
+
} catch {
|
|
658
|
+
return void 0;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return void 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/providers/plausible/package-runtime.ts
|
|
665
|
+
var SPECIFIER = "@plausible-analytics/tracker";
|
|
666
|
+
var cache;
|
|
667
|
+
function loadPlausibleTrackerModule(logger) {
|
|
668
|
+
cache ??= importTrackerModule(logger);
|
|
669
|
+
return cache;
|
|
670
|
+
}
|
|
671
|
+
async function importTrackerModule(logger) {
|
|
672
|
+
try {
|
|
673
|
+
const mod = await import(SPECIFIER);
|
|
674
|
+
if (typeof mod.init !== "function" || typeof mod.track !== "function") {
|
|
675
|
+
throw new Error(`"${SPECIFIER}" did not export init()/track()`);
|
|
676
|
+
}
|
|
677
|
+
return { init: mod.init, track: mod.track };
|
|
678
|
+
} catch (cause) {
|
|
679
|
+
cache = void 0;
|
|
680
|
+
logger.debug(`Could not load ${SPECIFIER}`, {
|
|
681
|
+
error: cause instanceof Error ? cause.message : String(cause)
|
|
682
|
+
});
|
|
683
|
+
throw new AnalyticsError(
|
|
684
|
+
`Plausible "package" mode needs the optional peer dependency "${SPECIFIER}". Run \`pnpm add ${SPECIFIER}\` or switch to \`plausible.mode: 'cdn'\`.`,
|
|
685
|
+
{ code: "provider_unavailable", cause }
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/providers/plausible/PlausibleAdapter.ts
|
|
691
|
+
function resolveExtensions(config) {
|
|
692
|
+
const extensions = [];
|
|
693
|
+
if (!config.autoPageViews) extensions.push("manual");
|
|
694
|
+
if (config.hashRouting) extensions.push("hash");
|
|
695
|
+
if (config.outboundLinks) extensions.push("outbound-links");
|
|
696
|
+
if (config.fileDownloads) extensions.push("file-downloads");
|
|
697
|
+
if (config.taggedEvents) extensions.push("tagged-events");
|
|
698
|
+
if (config.revenue) extensions.push("revenue");
|
|
699
|
+
if (config.trackLocalhost) extensions.push("local");
|
|
700
|
+
return extensions;
|
|
701
|
+
}
|
|
702
|
+
function toAbsoluteUrl(path, win) {
|
|
703
|
+
try {
|
|
704
|
+
return new URL(path, win.location.href).toString();
|
|
705
|
+
} catch {
|
|
706
|
+
return path;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
var PlausibleAdapter = class {
|
|
710
|
+
name = "plausible";
|
|
711
|
+
#config;
|
|
712
|
+
#logger;
|
|
713
|
+
#packageTrack = null;
|
|
714
|
+
constructor(config, context) {
|
|
715
|
+
this.#config = config;
|
|
716
|
+
this.#logger = context.logger.child("[plausible]");
|
|
717
|
+
}
|
|
718
|
+
async initialize() {
|
|
719
|
+
const win = getWindow();
|
|
720
|
+
if (!win) {
|
|
721
|
+
this.#logger.debug("No window; Plausible runs browser-side only (SSR no-op)");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (this.#config.mode === "package") {
|
|
725
|
+
await this.#initPackage();
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (this.#config.injectScript) {
|
|
729
|
+
await loadPlausibleScript(
|
|
730
|
+
{
|
|
731
|
+
domain: this.#config.domain,
|
|
732
|
+
scriptUrl: this.#config.scriptUrl,
|
|
733
|
+
endpoint: this.#config.endpoint,
|
|
734
|
+
extensions: resolveExtensions(this.#config)
|
|
735
|
+
},
|
|
736
|
+
this.#logger
|
|
737
|
+
);
|
|
738
|
+
} else {
|
|
739
|
+
this.#logger.debug("injectScript=false; expecting a host-provided Plausible snippet");
|
|
740
|
+
ensureQueueStub();
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
track(eventName, properties) {
|
|
744
|
+
const payload = toPlausiblePayload(properties);
|
|
745
|
+
if (this.#config.mode === "package") {
|
|
746
|
+
this.#packageCall(eventName, payload);
|
|
747
|
+
} else {
|
|
748
|
+
this.#cdnCall(eventName, payload);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
pageView(path) {
|
|
752
|
+
const win = getWindow();
|
|
753
|
+
const url = path && win ? toAbsoluteUrl(path, win) : void 0;
|
|
754
|
+
if (this.#config.mode === "package") {
|
|
755
|
+
this.#packageCall("pageview", url ? { u: url } : void 0);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
this.#cdnCall("pageview", url ? { u: url } : void 0);
|
|
759
|
+
}
|
|
760
|
+
destroy() {
|
|
761
|
+
if (this.#config.mode === "package") {
|
|
762
|
+
this.#packageTrack = null;
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (this.#config.injectScript) {
|
|
766
|
+
removePlausibleScript();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async #initPackage() {
|
|
770
|
+
const mod = await loadPlausibleTrackerModule(this.#logger);
|
|
771
|
+
const initConfig = {
|
|
772
|
+
domain: this.#config.domain,
|
|
773
|
+
autoCapturePageviews: this.#config.autoPageViews,
|
|
774
|
+
hashBasedRouting: this.#config.hashRouting,
|
|
775
|
+
outboundLinks: this.#config.outboundLinks,
|
|
776
|
+
fileDownloads: this.#config.fileDownloads,
|
|
777
|
+
captureOnLocalhost: this.#config.trackLocalhost,
|
|
778
|
+
bindToWindow: false,
|
|
779
|
+
logging: false
|
|
780
|
+
};
|
|
781
|
+
if (this.#config.endpoint) initConfig.endpoint = this.#config.endpoint;
|
|
782
|
+
mod.init(initConfig);
|
|
783
|
+
this.#packageTrack = mod.track;
|
|
784
|
+
this.#logger.debug("Plausible tracker package ready", { domain: this.#config.domain });
|
|
785
|
+
}
|
|
786
|
+
#packageCall(eventName, cdnPayload) {
|
|
787
|
+
const track = this.#packageTrack;
|
|
788
|
+
if (!track) {
|
|
789
|
+
this.#logger.debug("Plausible tracker not ready; event dropped", { event: eventName });
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const options = {};
|
|
793
|
+
if (cdnPayload?.props) options.props = cdnPayload.props;
|
|
794
|
+
if (cdnPayload?.revenue) options.revenue = cdnPayload.revenue;
|
|
795
|
+
if (cdnPayload?.u) options.url = cdnPayload.u;
|
|
796
|
+
try {
|
|
797
|
+
if (Object.keys(options).length > 0) {
|
|
798
|
+
track(eventName, options);
|
|
799
|
+
} else {
|
|
800
|
+
track(eventName);
|
|
801
|
+
}
|
|
802
|
+
} catch (error) {
|
|
803
|
+
this.#logger.debug("Plausible tracker threw; event dropped", {
|
|
804
|
+
event: eventName,
|
|
805
|
+
error: error instanceof Error ? error.message : String(error)
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
#cdnCall(eventName, options) {
|
|
810
|
+
const win = getWindow();
|
|
811
|
+
const plausible = win?.plausible;
|
|
812
|
+
if (typeof plausible !== "function") {
|
|
813
|
+
this.#logger.debug("window.plausible unavailable; event dropped", { event: eventName });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
if (options) {
|
|
818
|
+
plausible(eventName, options);
|
|
819
|
+
} else {
|
|
820
|
+
plausible(eventName);
|
|
821
|
+
}
|
|
822
|
+
} catch (error) {
|
|
823
|
+
this.#logger.debug("window.plausible threw; event dropped", {
|
|
824
|
+
event: eventName,
|
|
825
|
+
error: error instanceof Error ? error.message : String(error)
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
// src/providers/console/ConsoleAdapter.ts
|
|
832
|
+
var ConsoleAdapter = class {
|
|
833
|
+
name = "console";
|
|
834
|
+
#config;
|
|
835
|
+
constructor(config, _context) {
|
|
836
|
+
this.#config = config;
|
|
837
|
+
}
|
|
838
|
+
initialize() {
|
|
839
|
+
this.#config.sink.info(`${this.#config.prefix} ready`);
|
|
840
|
+
}
|
|
841
|
+
track(eventName, properties) {
|
|
842
|
+
this.#config.sink.log(`${this.#config.prefix} track`, {
|
|
843
|
+
event: eventName,
|
|
844
|
+
properties: properties ?? {}
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
pageView(path) {
|
|
848
|
+
this.#config.sink.log(`${this.#config.prefix} pageView`, { path: path ?? "(current)" });
|
|
849
|
+
}
|
|
850
|
+
destroy() {
|
|
851
|
+
this.#config.sink.info(`${this.#config.prefix} destroyed`);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// src/providers/factory.ts
|
|
856
|
+
function createAdapter(config, context) {
|
|
857
|
+
switch (config.provider) {
|
|
858
|
+
case "plausible": {
|
|
859
|
+
if (!config.plausible) {
|
|
860
|
+
throw new AnalyticsConfigError("Resolved config is missing the Plausible block.");
|
|
861
|
+
}
|
|
862
|
+
return new PlausibleAdapter(config.plausible, context);
|
|
863
|
+
}
|
|
864
|
+
case "console": {
|
|
865
|
+
if (!config.console) {
|
|
866
|
+
throw new AnalyticsConfigError("Resolved config is missing the console block.");
|
|
867
|
+
}
|
|
868
|
+
return new ConsoleAdapter(config.console, context);
|
|
869
|
+
}
|
|
870
|
+
case "custom": {
|
|
871
|
+
if (!config.adapter) {
|
|
872
|
+
throw new AnalyticsConfigError("Resolved config is missing the custom adapter factory.");
|
|
873
|
+
}
|
|
874
|
+
return config.adapter(context);
|
|
875
|
+
}
|
|
876
|
+
default: {
|
|
877
|
+
const exhaustive = config.provider;
|
|
878
|
+
throw new AnalyticsConfigError(`Unsupported provider: ${String(exhaustive)}`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/core/AnalyticsClient.ts
|
|
884
|
+
var AnalyticsClientImpl = class {
|
|
885
|
+
#config = null;
|
|
886
|
+
#adapter = null;
|
|
887
|
+
#logger = createLogger();
|
|
888
|
+
#initialized = false;
|
|
889
|
+
#enabled = false;
|
|
890
|
+
#signature = null;
|
|
891
|
+
#initPromise = null;
|
|
892
|
+
/** Bumped on every initialize()/destroy(); guards stale async work. */
|
|
893
|
+
#epoch = 0;
|
|
894
|
+
async initialize(config) {
|
|
895
|
+
const resolved = resolveConfig(config);
|
|
896
|
+
const signature = configSignature(config);
|
|
897
|
+
if (this.#initialized && this.#signature === signature) {
|
|
898
|
+
await (this.#initPromise ?? Promise.resolve());
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (this.#initialized) {
|
|
902
|
+
this.#teardownAdapter();
|
|
903
|
+
}
|
|
904
|
+
this.#epoch += 1;
|
|
905
|
+
this.#config = resolved;
|
|
906
|
+
this.#signature = signature;
|
|
907
|
+
this.#logger = createLogger({ debug: resolved.debug });
|
|
908
|
+
this.#enabled = resolved.enabled;
|
|
909
|
+
this.#initialized = true;
|
|
910
|
+
if (!resolved.enabled) {
|
|
911
|
+
this.#logger.debug("Analytics disabled", {
|
|
912
|
+
environment: resolved.environment,
|
|
913
|
+
provider: resolved.provider,
|
|
914
|
+
reason: resolved.disabledReason
|
|
915
|
+
});
|
|
916
|
+
this.#initPromise = Promise.resolve();
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
this.#initPromise = this.#startAdapter(resolved);
|
|
920
|
+
await this.#initPromise;
|
|
921
|
+
}
|
|
922
|
+
track(event, ...args) {
|
|
923
|
+
const properties = args[0];
|
|
924
|
+
try {
|
|
925
|
+
if (!this.#initialized || !this.#config) {
|
|
926
|
+
this.#logger.warn(`track("${event}") called before initialize()`);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const config = this.#config;
|
|
930
|
+
const baseInfo = {
|
|
931
|
+
type: "event",
|
|
932
|
+
name: event,
|
|
933
|
+
provider: config.provider,
|
|
934
|
+
environment: config.environment,
|
|
935
|
+
enabled: this.#enabled
|
|
936
|
+
};
|
|
937
|
+
if (!this.#enabled) {
|
|
938
|
+
const reason = config.disabledReason ?? "analytics disabled";
|
|
939
|
+
this.#logger.debug("Event skipped", { ...baseInfo, reason });
|
|
940
|
+
this.#emitEventInfo({ ...baseInfo, delivered: false, reason });
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const merged = {
|
|
944
|
+
...config.defaultProperties,
|
|
945
|
+
...properties
|
|
946
|
+
};
|
|
947
|
+
if (config.includeTenantKey && config.tenantKey !== void 0) {
|
|
948
|
+
merged[config.tenantPropertyName] = config.tenantKey;
|
|
949
|
+
}
|
|
950
|
+
const { value: safeProperties, blocked } = sanitizeProperties(
|
|
951
|
+
merged,
|
|
952
|
+
config.privacy,
|
|
953
|
+
this.#logger.child("[privacy]")
|
|
954
|
+
);
|
|
955
|
+
this.#logger.debug("Event", {
|
|
956
|
+
...baseInfo,
|
|
957
|
+
properties: safeProperties,
|
|
958
|
+
...blocked.length > 0 ? { blockedKeys: blocked } : {}
|
|
959
|
+
});
|
|
960
|
+
this.#adapter?.track(event, safeProperties);
|
|
961
|
+
this.#emitEventInfo({
|
|
962
|
+
...baseInfo,
|
|
963
|
+
delivered: this.#adapter != null,
|
|
964
|
+
properties: safeProperties,
|
|
965
|
+
...blocked.length > 0 ? { blockedKeys: blocked } : {},
|
|
966
|
+
...this.#adapter == null ? { reason: "provider not ready" } : {}
|
|
967
|
+
});
|
|
968
|
+
} catch (error) {
|
|
969
|
+
if (error instanceof AnalyticsPrivacyError) {
|
|
970
|
+
this.#handleError(error);
|
|
971
|
+
throw error;
|
|
972
|
+
}
|
|
973
|
+
this.#handleError(toAnalyticsError(error, "track_failed"));
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
pageView(path) {
|
|
977
|
+
try {
|
|
978
|
+
if (!this.#initialized || !this.#config) {
|
|
979
|
+
this.#logger.warn("pageView() called before initialize()");
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const config = this.#config;
|
|
983
|
+
const safePath = typeof path === "string" ? stripSensitiveQueryParams(path) : void 0;
|
|
984
|
+
const baseInfo = {
|
|
985
|
+
type: "pageview",
|
|
986
|
+
name: safePath ?? "(current)",
|
|
987
|
+
provider: config.provider,
|
|
988
|
+
environment: config.environment,
|
|
989
|
+
enabled: this.#enabled
|
|
990
|
+
};
|
|
991
|
+
if (!this.#enabled) {
|
|
992
|
+
const reason = config.disabledReason ?? "analytics disabled";
|
|
993
|
+
this.#logger.debug("Page view skipped", { ...baseInfo, reason });
|
|
994
|
+
this.#emitEventInfo({ ...baseInfo, delivered: false, reason });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
this.#logger.debug("Page view", baseInfo);
|
|
998
|
+
this.#adapter?.pageView(safePath);
|
|
999
|
+
this.#emitEventInfo({ ...baseInfo, delivered: this.#adapter != null });
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
this.#handleError(toAnalyticsError(error, "pageview_failed"));
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
isEnabled() {
|
|
1005
|
+
return this.#enabled;
|
|
1006
|
+
}
|
|
1007
|
+
isInitialized() {
|
|
1008
|
+
return this.#initialized;
|
|
1009
|
+
}
|
|
1010
|
+
setEnabled(enabled) {
|
|
1011
|
+
if (this.#enabled === enabled) return;
|
|
1012
|
+
this.#enabled = enabled;
|
|
1013
|
+
if (!this.#initialized || !this.#config) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
this.#logger.debug(`Analytics ${enabled ? "enabled" : "disabled"} at runtime`);
|
|
1017
|
+
if (enabled && !this.#adapter) {
|
|
1018
|
+
this.#initPromise = this.#startAdapter(this.#config);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
getState() {
|
|
1022
|
+
return {
|
|
1023
|
+
initialized: this.#initialized,
|
|
1024
|
+
enabled: this.#enabled,
|
|
1025
|
+
environment: this.#config?.environment ?? null,
|
|
1026
|
+
provider: this.#config?.provider ?? null,
|
|
1027
|
+
debug: this.#config?.debug ?? false
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
destroy() {
|
|
1031
|
+
this.#epoch += 1;
|
|
1032
|
+
this.#teardownAdapter();
|
|
1033
|
+
this.#logger.debug("Analytics destroyed");
|
|
1034
|
+
this.#config = null;
|
|
1035
|
+
this.#signature = null;
|
|
1036
|
+
this.#initialized = false;
|
|
1037
|
+
this.#enabled = false;
|
|
1038
|
+
this.#initPromise = null;
|
|
1039
|
+
this.#logger = createLogger();
|
|
1040
|
+
}
|
|
1041
|
+
#startAdapter(resolved) {
|
|
1042
|
+
let adapter;
|
|
1043
|
+
try {
|
|
1044
|
+
adapter = createAdapter(resolved, { logger: this.#logger });
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
this.#handleError(toAnalyticsError(error, "initialization_failed"));
|
|
1047
|
+
return Promise.resolve();
|
|
1048
|
+
}
|
|
1049
|
+
this.#adapter = adapter;
|
|
1050
|
+
const epoch = this.#epoch;
|
|
1051
|
+
return (async () => {
|
|
1052
|
+
try {
|
|
1053
|
+
await adapter.initialize();
|
|
1054
|
+
if (this.#epoch !== epoch) return;
|
|
1055
|
+
this.#logger.debug("Provider ready", {
|
|
1056
|
+
provider: adapter.name,
|
|
1057
|
+
environment: resolved.environment
|
|
1058
|
+
});
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
if (this.#epoch !== epoch) return;
|
|
1061
|
+
this.#handleError(toAnalyticsError(error, "initialization_failed"));
|
|
1062
|
+
}
|
|
1063
|
+
})();
|
|
1064
|
+
}
|
|
1065
|
+
#teardownAdapter() {
|
|
1066
|
+
const adapter = this.#adapter;
|
|
1067
|
+
this.#adapter = null;
|
|
1068
|
+
if (!adapter) return;
|
|
1069
|
+
try {
|
|
1070
|
+
adapter.destroy();
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
this.#handleError(toAnalyticsError(error, "destroy_failed"));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
#emitEventInfo(info) {
|
|
1076
|
+
const handler = this.#config?.onEvent;
|
|
1077
|
+
if (!handler) return;
|
|
1078
|
+
try {
|
|
1079
|
+
handler(info);
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
this.#logger.debug("config.onEvent handler threw (ignored)", {
|
|
1082
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
#handleError(error) {
|
|
1087
|
+
this.#logger.error(error.message, { code: error.code });
|
|
1088
|
+
const handler = this.#config?.onError;
|
|
1089
|
+
if (!handler) return;
|
|
1090
|
+
try {
|
|
1091
|
+
handler(error);
|
|
1092
|
+
} catch (handlerError) {
|
|
1093
|
+
this.#logger.debug("config.onError handler threw (ignored)", {
|
|
1094
|
+
error: handlerError instanceof Error ? handlerError.message : String(handlerError)
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
function createAnalyticsClient() {
|
|
1100
|
+
return new AnalyticsClientImpl();
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
export { ANALYTICS_ENVIRONMENTS, AnalyticsConfigError, AnalyticsError, AnalyticsPrivacyError, DEFAULT_PII_KEYS, DEFAULT_SENSITIVE_KEYS, classifyKey, configSignature, createAnalyticsClient, isAnalyticsEnvironment, normalizeKey, sanitizeProperties, stripSensitiveQueryParams, toAnalyticsError };
|
|
1104
|
+
//# sourceMappingURL=chunk-J5GXHJNU.js.map
|
|
1105
|
+
//# sourceMappingURL=chunk-J5GXHJNU.js.map
|