@scoperat/tracker 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +201 -4
- package/dist/index.js +777 -2
- package/dist/index.js.map +1 -1
- package/dist/widget/index.d.ts +41 -0
- package/dist/widget/index.js +2780 -0
- package/dist/widget/index.js.map +1 -0
- package/dist/widget.global.js +970 -0
- package/dist/widget.global.js.map +1 -0
- package/package.json +15 -3
- package/dist/client.d.ts +0 -30
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -217
- package/dist/client.js.map +0 -1
- package/dist/context.d.ts +0 -5
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -24
- package/dist/context.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -33
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,778 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// src/breadcrumbs.ts
|
|
2
|
+
var MAX_BREADCRUMBS = 20;
|
|
3
|
+
var MAX_TEXT_LENGTH = 120;
|
|
4
|
+
var buffer = [];
|
|
5
|
+
var installed = false;
|
|
6
|
+
function push(crumb) {
|
|
7
|
+
buffer.push(crumb);
|
|
8
|
+
if (buffer.length > MAX_BREADCRUMBS) {
|
|
9
|
+
buffer = buffer.slice(-MAX_BREADCRUMBS);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function truncate(str, max = MAX_TEXT_LENGTH) {
|
|
13
|
+
return str.length > max ? str.slice(0, max) + "\u2026" : str;
|
|
14
|
+
}
|
|
15
|
+
function getBreadcrumbs() {
|
|
16
|
+
return [...buffer];
|
|
17
|
+
}
|
|
18
|
+
function addBreadcrumb(crumb) {
|
|
19
|
+
push(crumb);
|
|
20
|
+
}
|
|
21
|
+
function startBreadcrumbCollection() {
|
|
22
|
+
if (installed || typeof window === "undefined") return;
|
|
23
|
+
installed = true;
|
|
24
|
+
document.addEventListener(
|
|
25
|
+
"click",
|
|
26
|
+
(e) => {
|
|
27
|
+
const target = e.target;
|
|
28
|
+
if (!target) return;
|
|
29
|
+
const text = target.textContent?.trim() || target.getAttribute("aria-label") || "";
|
|
30
|
+
push({
|
|
31
|
+
type: "user",
|
|
32
|
+
category: "click",
|
|
33
|
+
message: truncate(
|
|
34
|
+
`${target.tagName.toLowerCase()}${text ? ` "${text}"` : ""}`
|
|
35
|
+
),
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
data: {
|
|
38
|
+
tagName: target.tagName.toLowerCase(),
|
|
39
|
+
className: target.className ? truncate(target.className, 80) : void 0
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
{ capture: true }
|
|
44
|
+
);
|
|
45
|
+
const origPushState = history.pushState;
|
|
46
|
+
history.pushState = function(...args) {
|
|
47
|
+
const from = location.pathname;
|
|
48
|
+
origPushState.apply(this, args);
|
|
49
|
+
push({
|
|
50
|
+
type: "navigation",
|
|
51
|
+
category: "navigation",
|
|
52
|
+
message: `${from} \u2192 ${location.pathname}`,
|
|
53
|
+
timestamp: Date.now()
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
window.addEventListener("popstate", () => {
|
|
57
|
+
push({
|
|
58
|
+
type: "navigation",
|
|
59
|
+
category: "navigation",
|
|
60
|
+
message: `popstate \u2192 ${location.pathname}`,
|
|
61
|
+
timestamp: Date.now()
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
const wrapConsole = (level) => {
|
|
65
|
+
const orig = console[level];
|
|
66
|
+
console[level] = (...args) => {
|
|
67
|
+
push({
|
|
68
|
+
type: "console",
|
|
69
|
+
category: `console.${level}`,
|
|
70
|
+
message: truncate(
|
|
71
|
+
args.map((a) => typeof a === "string" ? a : String(a)).join(" ")
|
|
72
|
+
),
|
|
73
|
+
timestamp: Date.now()
|
|
74
|
+
});
|
|
75
|
+
orig.apply(console, args);
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
wrapConsole("error");
|
|
79
|
+
wrapConsole("warn");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/context.ts
|
|
83
|
+
function getBrowserContext() {
|
|
84
|
+
if (typeof window === "undefined") {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
page: {
|
|
89
|
+
url: window.location.href,
|
|
90
|
+
path: window.location.pathname,
|
|
91
|
+
title: document.title,
|
|
92
|
+
referrer: document.referrer || void 0
|
|
93
|
+
},
|
|
94
|
+
screen: {
|
|
95
|
+
width: window.screen.width,
|
|
96
|
+
height: window.screen.height
|
|
97
|
+
},
|
|
98
|
+
locale: navigator.language,
|
|
99
|
+
userAgent: navigator.userAgent,
|
|
100
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/errors.ts
|
|
105
|
+
var DEDUP_COOLDOWN_MS = 6e4;
|
|
106
|
+
var dedupMap = /* @__PURE__ */ new Map();
|
|
107
|
+
function simpleFingerprint(type, stack) {
|
|
108
|
+
const firstFrame = stack?.split("\n")[1]?.trim() ?? "";
|
|
109
|
+
return `${type}:${firstFrame}`;
|
|
110
|
+
}
|
|
111
|
+
function isDuplicate(fingerprint) {
|
|
112
|
+
const lastSent = dedupMap.get(fingerprint);
|
|
113
|
+
if (lastSent && Date.now() - lastSent < DEDUP_COOLDOWN_MS) return true;
|
|
114
|
+
dedupMap.set(fingerprint, Date.now());
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function sendException(tracker, props, getBreadcrumbs2) {
|
|
118
|
+
const fp = simpleFingerprint(props.type, props.stack);
|
|
119
|
+
if (isDuplicate(fp)) return;
|
|
120
|
+
tracker.track("$exception", {
|
|
121
|
+
...props,
|
|
122
|
+
context: { breadcrumbs: getBreadcrumbs2() }
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function installGlobalHandlers(tracker, getBreadcrumbs2) {
|
|
126
|
+
if (typeof window === "undefined") return;
|
|
127
|
+
window.onerror = (_message, filename, lineno, colno, error) => {
|
|
128
|
+
const err = error ?? new Error(String(_message));
|
|
129
|
+
sendException(
|
|
130
|
+
tracker,
|
|
131
|
+
{
|
|
132
|
+
type: err.name || "Error",
|
|
133
|
+
message: err.message || String(_message),
|
|
134
|
+
stack: err.stack,
|
|
135
|
+
handled: false,
|
|
136
|
+
mechanism: "onerror",
|
|
137
|
+
source: "browser",
|
|
138
|
+
filename,
|
|
139
|
+
lineno,
|
|
140
|
+
colno
|
|
141
|
+
},
|
|
142
|
+
getBreadcrumbs2
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
window.onunhandledrejection = (event) => {
|
|
146
|
+
const reason = event.reason;
|
|
147
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
148
|
+
sendException(
|
|
149
|
+
tracker,
|
|
150
|
+
{
|
|
151
|
+
type: err.name || "UnhandledRejection",
|
|
152
|
+
message: err.message,
|
|
153
|
+
stack: err.stack,
|
|
154
|
+
handled: false,
|
|
155
|
+
mechanism: "onunhandledrejection",
|
|
156
|
+
source: "browser"
|
|
157
|
+
},
|
|
158
|
+
getBreadcrumbs2
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/flag-client.ts
|
|
164
|
+
var DEFAULT_FLAG_POLL_INTERVAL = 6e4;
|
|
165
|
+
var FlagClient = class {
|
|
166
|
+
endpoint;
|
|
167
|
+
writeKey;
|
|
168
|
+
timeout;
|
|
169
|
+
environment;
|
|
170
|
+
getIdentity;
|
|
171
|
+
onEvaluation;
|
|
172
|
+
cache = /* @__PURE__ */ new Map();
|
|
173
|
+
ready = false;
|
|
174
|
+
readyCallbacks = [];
|
|
175
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
176
|
+
evalTracked = /* @__PURE__ */ new Set();
|
|
177
|
+
timer = null;
|
|
178
|
+
constructor(config) {
|
|
179
|
+
this.endpoint = config.endpoint;
|
|
180
|
+
this.writeKey = config.writeKey;
|
|
181
|
+
this.timeout = config.timeout ?? 1e4;
|
|
182
|
+
this.environment = config.environment;
|
|
183
|
+
this.getIdentity = config.getIdentity;
|
|
184
|
+
this.onEvaluation = config.onEvaluation;
|
|
185
|
+
const interval = config.pollInterval ?? DEFAULT_FLAG_POLL_INTERVAL;
|
|
186
|
+
this.fetch();
|
|
187
|
+
this.timer = setInterval(() => this.fetch(), interval);
|
|
188
|
+
}
|
|
189
|
+
getFlag(key, defaultValue) {
|
|
190
|
+
const flag = this.cache.get(key);
|
|
191
|
+
if (!flag) return defaultValue;
|
|
192
|
+
if (!this.evalTracked.has(key)) {
|
|
193
|
+
this.evalTracked.add(key);
|
|
194
|
+
this.onEvaluation?.(key, flag);
|
|
195
|
+
}
|
|
196
|
+
return flag.value;
|
|
197
|
+
}
|
|
198
|
+
onFlagsReady(callback) {
|
|
199
|
+
if (this.ready) {
|
|
200
|
+
callback();
|
|
201
|
+
} else {
|
|
202
|
+
this.readyCallbacks.push(callback);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
onFlagChange(listener) {
|
|
206
|
+
this.changeListeners.add(listener);
|
|
207
|
+
return () => this.changeListeners.delete(listener);
|
|
208
|
+
}
|
|
209
|
+
async refresh() {
|
|
210
|
+
await this.fetch();
|
|
211
|
+
}
|
|
212
|
+
destroy() {
|
|
213
|
+
if (this.timer) {
|
|
214
|
+
clearInterval(this.timer);
|
|
215
|
+
this.timer = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async fetch() {
|
|
219
|
+
if (!this.writeKey || !this.endpoint) return;
|
|
220
|
+
const params = new URLSearchParams();
|
|
221
|
+
if (this.environment) params.set("environment", this.environment);
|
|
222
|
+
const identity = this.getIdentity();
|
|
223
|
+
const identifier = identity.userId ?? identity.anonymousId;
|
|
224
|
+
if (identifier) params.set("identifier", identifier);
|
|
225
|
+
if (identity.orgId) params.set("orgId", identity.orgId);
|
|
226
|
+
const qs = params.toString();
|
|
227
|
+
const url = `${this.endpoint}/api/flags${qs ? `?${qs}` : ""}`;
|
|
228
|
+
try {
|
|
229
|
+
const res = await globalThis.fetch(url, {
|
|
230
|
+
headers: { Authorization: `Bearer ${this.writeKey}` },
|
|
231
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
232
|
+
});
|
|
233
|
+
if (!res.ok) return;
|
|
234
|
+
const json = await res.json();
|
|
235
|
+
this.cache.clear();
|
|
236
|
+
for (const [key, flag] of Object.entries(json.flags)) {
|
|
237
|
+
this.cache.set(key, flag);
|
|
238
|
+
}
|
|
239
|
+
if (!this.ready) {
|
|
240
|
+
this.ready = true;
|
|
241
|
+
for (const cb of this.readyCallbacks) {
|
|
242
|
+
try {
|
|
243
|
+
cb();
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.readyCallbacks = [];
|
|
248
|
+
}
|
|
249
|
+
for (const listener of this.changeListeners) {
|
|
250
|
+
try {
|
|
251
|
+
listener();
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// src/client.ts
|
|
261
|
+
var DEFAULT_FLUSH_AT = 10;
|
|
262
|
+
var DEFAULT_FLUSH_INTERVAL = 3e3;
|
|
263
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
264
|
+
var DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1e3;
|
|
265
|
+
var MAX_BATCH_SIZE = 100;
|
|
266
|
+
var ANON_ID_KEY = "__scoperat_anon_id";
|
|
267
|
+
var SESSION_ID_KEY = "__scoperat_session_id";
|
|
268
|
+
var LAST_ACTIVITY_KEY = "__scoperat_last_activity";
|
|
269
|
+
var USER_ID_KEY = "__scoperat_user_id";
|
|
270
|
+
var SESSION_START_EVENT = "$session_start";
|
|
271
|
+
var SESSION_END_EVENT = "$session_end";
|
|
272
|
+
var TICKET_SESSION_KEY = "__scoperat_ticket_session";
|
|
273
|
+
function generateAnonId() {
|
|
274
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
275
|
+
return crypto.randomUUID();
|
|
276
|
+
}
|
|
277
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
278
|
+
const r = Math.random() * 16 | 0;
|
|
279
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
280
|
+
return v.toString(16);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function getOrCreateAnonId() {
|
|
284
|
+
if (typeof window === "undefined" || typeof localStorage === "undefined") {
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
let id = localStorage.getItem(ANON_ID_KEY);
|
|
289
|
+
if (!id) {
|
|
290
|
+
id = generateAnonId();
|
|
291
|
+
localStorage.setItem(ANON_ID_KEY, id);
|
|
292
|
+
}
|
|
293
|
+
return id;
|
|
294
|
+
} catch {
|
|
295
|
+
return generateAnonId();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
var TrackerImpl = class {
|
|
299
|
+
writeKey = null;
|
|
300
|
+
endpoint = "";
|
|
301
|
+
flushAt = DEFAULT_FLUSH_AT;
|
|
302
|
+
flushInterval = DEFAULT_FLUSH_INTERVAL;
|
|
303
|
+
timeout = DEFAULT_TIMEOUT;
|
|
304
|
+
onError;
|
|
305
|
+
buffer = [];
|
|
306
|
+
timer = null;
|
|
307
|
+
visibilityHandler = null;
|
|
308
|
+
userId;
|
|
309
|
+
orgId;
|
|
310
|
+
traits = {};
|
|
311
|
+
userHash;
|
|
312
|
+
anonymousId;
|
|
313
|
+
sessionId;
|
|
314
|
+
sessionTimeout = DEFAULT_SESSION_TIMEOUT;
|
|
315
|
+
ticketSessionFetch = null;
|
|
316
|
+
_flagClient = null;
|
|
317
|
+
init(writeKey, options = {}) {
|
|
318
|
+
this.writeKey = writeKey;
|
|
319
|
+
this.endpoint = (options.endpoint ?? globalThis.location?.origin ?? "").replace(/\/+$/, "");
|
|
320
|
+
this.flushAt = options.flushAt ?? DEFAULT_FLUSH_AT;
|
|
321
|
+
this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
|
|
322
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
323
|
+
this.onError = options.onError;
|
|
324
|
+
this.sessionTimeout = options.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
|
|
325
|
+
this.anonymousId = getOrCreateAnonId();
|
|
326
|
+
if (this.timer) clearInterval(this.timer);
|
|
327
|
+
this.timer = setInterval(() => {
|
|
328
|
+
if (this.buffer.length > 0) {
|
|
329
|
+
this.flush().catch(() => {
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}, this.flushInterval);
|
|
333
|
+
if (typeof window !== "undefined") {
|
|
334
|
+
if (this.visibilityHandler) {
|
|
335
|
+
window.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
336
|
+
}
|
|
337
|
+
this.visibilityHandler = () => {
|
|
338
|
+
if (document.visibilityState === "hidden" && this.buffer.length > 0) {
|
|
339
|
+
this.flushSync();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
window.addEventListener("visibilitychange", this.visibilityHandler);
|
|
343
|
+
}
|
|
344
|
+
if (options.captureErrors) {
|
|
345
|
+
installGlobalHandlers(this, getBreadcrumbs);
|
|
346
|
+
startBreadcrumbCollection();
|
|
347
|
+
}
|
|
348
|
+
this._flagClient?.destroy();
|
|
349
|
+
this._flagClient = new FlagClient({
|
|
350
|
+
endpoint: this.endpoint,
|
|
351
|
+
writeKey,
|
|
352
|
+
timeout: this.timeout,
|
|
353
|
+
environment: options.environment,
|
|
354
|
+
pollInterval: options.flagPollInterval,
|
|
355
|
+
getIdentity: () => ({
|
|
356
|
+
userId: this.userId,
|
|
357
|
+
anonymousId: this.anonymousId,
|
|
358
|
+
orgId: this.orgId
|
|
359
|
+
}),
|
|
360
|
+
onEvaluation: (key, flag) => this.track("$flag_evaluation", {
|
|
361
|
+
flag_key: key,
|
|
362
|
+
flag_value: flag.value,
|
|
363
|
+
flag_type: flag.type,
|
|
364
|
+
flag_source: flag.source
|
|
365
|
+
})
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
/** The underlying flag evaluation client. Use for direct SDK integration. */
|
|
369
|
+
get flags() {
|
|
370
|
+
if (!this._flagClient) {
|
|
371
|
+
throw new Error("Tracker not initialized. Call init() first.");
|
|
372
|
+
}
|
|
373
|
+
return this._flagClient;
|
|
374
|
+
}
|
|
375
|
+
identify(userId, traits, options) {
|
|
376
|
+
const previousUserId = this.userId ?? this.readStoredUserId();
|
|
377
|
+
this.userId = userId;
|
|
378
|
+
if (traits) {
|
|
379
|
+
this.traits = { ...traits };
|
|
380
|
+
if (traits.orgId) this.orgId = traits.orgId;
|
|
381
|
+
}
|
|
382
|
+
this.userHash = options?.userHash;
|
|
383
|
+
for (const evt of this.buffer) {
|
|
384
|
+
if (!evt.user_id) evt.user_id = userId;
|
|
385
|
+
if (!evt.org_id && this.orgId) evt.org_id = this.orgId;
|
|
386
|
+
}
|
|
387
|
+
if (userId !== previousUserId) {
|
|
388
|
+
this.writeStoredUserId(userId);
|
|
389
|
+
this.startSession("login");
|
|
390
|
+
}
|
|
391
|
+
this._flagClient?.refresh();
|
|
392
|
+
}
|
|
393
|
+
reset() {
|
|
394
|
+
this.endSession("logout", true);
|
|
395
|
+
this.userId = void 0;
|
|
396
|
+
this.orgId = void 0;
|
|
397
|
+
this.traits = {};
|
|
398
|
+
this.userHash = void 0;
|
|
399
|
+
this.anonymousId = getOrCreateAnonId();
|
|
400
|
+
this.clearStoredUserId();
|
|
401
|
+
this.clearTicketSession();
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Begin a new analytics session, ending the current one first if it
|
|
405
|
+
* exists. Emits `$session_end` (for the old session) and
|
|
406
|
+
* `$session_start` (for the new). Called automatically on login via
|
|
407
|
+
* {@link identify}; also exposed for explicit control.
|
|
408
|
+
*/
|
|
409
|
+
startSession(reason = "login") {
|
|
410
|
+
if (typeof window === "undefined") return this.sessionId;
|
|
411
|
+
const now = Date.now();
|
|
412
|
+
const existingId = this.readStoredSession();
|
|
413
|
+
if (existingId) {
|
|
414
|
+
this.emitSessionEvent(SESSION_END_EVENT, existingId, reason);
|
|
415
|
+
}
|
|
416
|
+
const newId = generateAnonId();
|
|
417
|
+
this.persistSession(newId, now);
|
|
418
|
+
this.emitSessionEvent(SESSION_START_EVENT, newId, reason);
|
|
419
|
+
return newId;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* End the current analytics session (if any), emitting `$session_end`,
|
|
423
|
+
* and clear the stored session. The next tracked event will lazily
|
|
424
|
+
* start a fresh session. Called automatically on logout via
|
|
425
|
+
* {@link reset}; also exposed for explicit control.
|
|
426
|
+
*
|
|
427
|
+
* @param immediate - flush buffered events synchronously (via
|
|
428
|
+
* `sendBeacon`) so the end event isn't lost if the page unloads.
|
|
429
|
+
*/
|
|
430
|
+
endSession(reason = "logout", immediate = false) {
|
|
431
|
+
if (typeof window === "undefined") return;
|
|
432
|
+
const existingId = this.readStoredSession();
|
|
433
|
+
if (existingId) {
|
|
434
|
+
this.emitSessionEvent(SESSION_END_EVENT, existingId, reason);
|
|
435
|
+
}
|
|
436
|
+
this.sessionId = void 0;
|
|
437
|
+
this.clearSession();
|
|
438
|
+
if (immediate) {
|
|
439
|
+
if (typeof navigator === "undefined" || !navigator.sendBeacon) {
|
|
440
|
+
this.flush().catch(() => {
|
|
441
|
+
});
|
|
442
|
+
} else {
|
|
443
|
+
this.flushSync();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
readStoredUserId() {
|
|
448
|
+
if (typeof localStorage === "undefined") return void 0;
|
|
449
|
+
try {
|
|
450
|
+
return localStorage.getItem(USER_ID_KEY) ?? void 0;
|
|
451
|
+
} catch {
|
|
452
|
+
return void 0;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
writeStoredUserId(userId) {
|
|
456
|
+
if (typeof localStorage === "undefined") return;
|
|
457
|
+
try {
|
|
458
|
+
localStorage.setItem(USER_ID_KEY, userId);
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
clearStoredUserId() {
|
|
463
|
+
if (typeof localStorage === "undefined") return;
|
|
464
|
+
try {
|
|
465
|
+
localStorage.removeItem(USER_ID_KEY);
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
clearTicketSession() {
|
|
470
|
+
this.ticketSessionFetch = null;
|
|
471
|
+
if (typeof localStorage === "undefined") return;
|
|
472
|
+
try {
|
|
473
|
+
localStorage.removeItem(TICKET_SESSION_KEY);
|
|
474
|
+
} catch {
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
readStoredTicketSession() {
|
|
478
|
+
if (typeof localStorage === "undefined") return null;
|
|
479
|
+
try {
|
|
480
|
+
const raw = localStorage.getItem(TICKET_SESSION_KEY);
|
|
481
|
+
if (!raw) return null;
|
|
482
|
+
const parsed = JSON.parse(raw);
|
|
483
|
+
if (typeof parsed.anonymousId === "string" && typeof parsed.token === "string" && typeof parsed.expiresAt === "number" && parsed.expiresAt > Date.now() + 6e4) {
|
|
484
|
+
return parsed;
|
|
485
|
+
}
|
|
486
|
+
localStorage.removeItem(TICKET_SESSION_KEY);
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* @internal Used by the widget API helpers. Returns a cached or
|
|
493
|
+
* newly-minted anonymous session token, or `null` if the server
|
|
494
|
+
* has anonymous sessions disabled. Coalesces concurrent callers
|
|
495
|
+
* onto a single in-flight network request so mount-time parallel
|
|
496
|
+
* API calls don't each trigger a `POST /ticket/session`.
|
|
497
|
+
*/
|
|
498
|
+
_ensureTicketSession() {
|
|
499
|
+
const cached = this.readStoredTicketSession();
|
|
500
|
+
if (cached) return Promise.resolve(cached);
|
|
501
|
+
if (this.ticketSessionFetch) return this.ticketSessionFetch;
|
|
502
|
+
if (!this.writeKey) return Promise.resolve(null);
|
|
503
|
+
this.ticketSessionFetch = (async () => {
|
|
504
|
+
try {
|
|
505
|
+
const res = await fetch(`${this.endpoint}/api/ingest/ticket/session`, {
|
|
506
|
+
method: "POST",
|
|
507
|
+
headers: {
|
|
508
|
+
"Content-Type": "application/json",
|
|
509
|
+
Authorization: `Bearer ${this.writeKey}`
|
|
510
|
+
},
|
|
511
|
+
body: "{}",
|
|
512
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
513
|
+
});
|
|
514
|
+
if (!res.ok) return null;
|
|
515
|
+
const json = await res.json();
|
|
516
|
+
if (typeof json?.anonymousId === "string" && typeof json?.token === "string" && typeof json?.expiresAt === "number") {
|
|
517
|
+
try {
|
|
518
|
+
localStorage?.setItem(TICKET_SESSION_KEY, JSON.stringify(json));
|
|
519
|
+
} catch {
|
|
520
|
+
}
|
|
521
|
+
this.anonymousId = json.anonymousId;
|
|
522
|
+
return json;
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
} catch {
|
|
526
|
+
return null;
|
|
527
|
+
} finally {
|
|
528
|
+
this.ticketSessionFetch = null;
|
|
529
|
+
}
|
|
530
|
+
})();
|
|
531
|
+
return this.ticketSessionFetch;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Return the id of the current session, rotating it when the previous
|
|
535
|
+
* session has expired through inactivity. On rotation it emits a
|
|
536
|
+
* `$session_end` for the timed-out session (backdated to the last
|
|
537
|
+
* recorded activity) and a `$session_start` for the new one. Called on
|
|
538
|
+
* every tracked event so sessions advance with real user activity.
|
|
539
|
+
*/
|
|
540
|
+
ensureSession(now) {
|
|
541
|
+
if (typeof window === "undefined") return this.sessionId;
|
|
542
|
+
try {
|
|
543
|
+
const lastActivity = this.readLastActivity();
|
|
544
|
+
const existingId = this.readStoredSession();
|
|
545
|
+
const expired = lastActivity === void 0 || now - lastActivity > this.sessionTimeout;
|
|
546
|
+
if (existingId && !expired) {
|
|
547
|
+
this.sessionId = existingId;
|
|
548
|
+
try {
|
|
549
|
+
localStorage.setItem(LAST_ACTIVITY_KEY, String(now));
|
|
550
|
+
} catch {
|
|
551
|
+
}
|
|
552
|
+
return existingId;
|
|
553
|
+
}
|
|
554
|
+
if (existingId) {
|
|
555
|
+
this.emitSessionEvent(
|
|
556
|
+
SESSION_END_EVENT,
|
|
557
|
+
existingId,
|
|
558
|
+
"timeout",
|
|
559
|
+
lastActivity !== void 0 ? new Date(lastActivity).toISOString() : void 0
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
const newId = generateAnonId();
|
|
563
|
+
this.persistSession(newId, now);
|
|
564
|
+
this.emitSessionEvent(
|
|
565
|
+
SESSION_START_EVENT,
|
|
566
|
+
newId,
|
|
567
|
+
existingId ? "timeout" : "initial"
|
|
568
|
+
);
|
|
569
|
+
return newId;
|
|
570
|
+
} catch {
|
|
571
|
+
return this.sessionId ?? generateAnonId();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
readStoredSession() {
|
|
575
|
+
if (this.sessionId) return this.sessionId;
|
|
576
|
+
if (typeof sessionStorage === "undefined") return void 0;
|
|
577
|
+
try {
|
|
578
|
+
return sessionStorage.getItem(SESSION_ID_KEY) ?? void 0;
|
|
579
|
+
} catch {
|
|
580
|
+
return void 0;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
readLastActivity() {
|
|
584
|
+
if (typeof localStorage === "undefined") return void 0;
|
|
585
|
+
try {
|
|
586
|
+
const raw = localStorage.getItem(LAST_ACTIVITY_KEY);
|
|
587
|
+
return raw ? Number(raw) : void 0;
|
|
588
|
+
} catch {
|
|
589
|
+
return void 0;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
persistSession(id, now) {
|
|
593
|
+
this.sessionId = id;
|
|
594
|
+
if (typeof window === "undefined") return;
|
|
595
|
+
try {
|
|
596
|
+
sessionStorage.setItem(SESSION_ID_KEY, id);
|
|
597
|
+
localStorage.setItem(LAST_ACTIVITY_KEY, String(now));
|
|
598
|
+
} catch {
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Enqueue a session lifecycle event already stamped with its session id
|
|
603
|
+
* (so it never re-enters {@link ensureSession} and risks recursion).
|
|
604
|
+
*/
|
|
605
|
+
emitSessionEvent(event, sessionId, reason, occurredAt) {
|
|
606
|
+
if (!this.writeKey) return;
|
|
607
|
+
const traitsContext = Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};
|
|
608
|
+
this.buffer.push({
|
|
609
|
+
event,
|
|
610
|
+
properties: { reason },
|
|
611
|
+
occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
612
|
+
source: "browser",
|
|
613
|
+
org_id: this.orgId,
|
|
614
|
+
user_id: this.userId,
|
|
615
|
+
anonymous_id: this.anonymousId,
|
|
616
|
+
session_id: sessionId,
|
|
617
|
+
context: {
|
|
618
|
+
...getBrowserContext(),
|
|
619
|
+
...traitsContext
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
clearSession() {
|
|
624
|
+
if (typeof window === "undefined") return;
|
|
625
|
+
try {
|
|
626
|
+
sessionStorage.removeItem(SESSION_ID_KEY);
|
|
627
|
+
localStorage.removeItem(LAST_ACTIVITY_KEY);
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
getSessionId() {
|
|
632
|
+
return this.ensureSession(Date.now());
|
|
633
|
+
}
|
|
634
|
+
/** @internal Used by the widget to read tracker config. */
|
|
635
|
+
_getEndpoint() {
|
|
636
|
+
return this.endpoint;
|
|
637
|
+
}
|
|
638
|
+
/** @internal Used by the widget to read the write key. */
|
|
639
|
+
_getWriteKey() {
|
|
640
|
+
return this.writeKey;
|
|
641
|
+
}
|
|
642
|
+
/** @internal Used by the widget to read the current user identity. */
|
|
643
|
+
_getIdentity() {
|
|
644
|
+
return {
|
|
645
|
+
userId: this.userId,
|
|
646
|
+
anonymousId: this.anonymousId,
|
|
647
|
+
email: this.traits.email,
|
|
648
|
+
name: this.traits.name,
|
|
649
|
+
userHash: this.userHash
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
captureException(error, context) {
|
|
653
|
+
if (!this.writeKey) return;
|
|
654
|
+
const breadcrumbs = getBreadcrumbs();
|
|
655
|
+
this.track("$exception", {
|
|
656
|
+
type: error.name || "Error",
|
|
657
|
+
message: error.message,
|
|
658
|
+
stack: error.stack,
|
|
659
|
+
handled: true,
|
|
660
|
+
mechanism: context?.mechanism ?? "manual",
|
|
661
|
+
source: "browser",
|
|
662
|
+
...context,
|
|
663
|
+
context: { breadcrumbs }
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
addBreadcrumb(category, message, data) {
|
|
667
|
+
addBreadcrumb({
|
|
668
|
+
type: "custom",
|
|
669
|
+
category,
|
|
670
|
+
message,
|
|
671
|
+
timestamp: Date.now(),
|
|
672
|
+
data
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
track(eventOrName, properties) {
|
|
676
|
+
if (!this.writeKey) return;
|
|
677
|
+
const evt = typeof eventOrName === "string" ? { event: eventOrName, properties } : eventOrName;
|
|
678
|
+
const traitsContext = Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};
|
|
679
|
+
const enriched = {
|
|
680
|
+
...evt,
|
|
681
|
+
occurred_at: evt.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
682
|
+
source: evt.source ?? "browser",
|
|
683
|
+
org_id: evt.org_id ?? this.orgId,
|
|
684
|
+
user_id: evt.user_id ?? this.userId,
|
|
685
|
+
anonymous_id: evt.anonymous_id ?? this.anonymousId,
|
|
686
|
+
session_id: evt.session_id ?? this.ensureSession(Date.now()),
|
|
687
|
+
context: {
|
|
688
|
+
...getBrowserContext(),
|
|
689
|
+
...traitsContext,
|
|
690
|
+
...evt.context
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
this.buffer.push(enriched);
|
|
694
|
+
if (this.buffer.length >= this.flushAt) {
|
|
695
|
+
this.flush().catch(() => {
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async flush() {
|
|
700
|
+
if (!this.writeKey || this.buffer.length === 0) {
|
|
701
|
+
return { success: true, ingested: 0 };
|
|
702
|
+
}
|
|
703
|
+
const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
|
|
704
|
+
try {
|
|
705
|
+
const res = await fetch(`${this.endpoint}/api/ingest/track`, {
|
|
706
|
+
method: "POST",
|
|
707
|
+
headers: {
|
|
708
|
+
"Content-Type": "application/json",
|
|
709
|
+
Authorization: `Bearer ${this.writeKey}`
|
|
710
|
+
},
|
|
711
|
+
body: JSON.stringify({ events: batch }),
|
|
712
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
713
|
+
});
|
|
714
|
+
if (!res.ok) {
|
|
715
|
+
const body = await res.text().catch(() => "");
|
|
716
|
+
throw new Error(`API returned ${res.status}: ${body}`);
|
|
717
|
+
}
|
|
718
|
+
const json = await res.json();
|
|
719
|
+
return { success: true, ingested: json.ingested };
|
|
720
|
+
} catch (error) {
|
|
721
|
+
this.buffer.unshift(...batch);
|
|
722
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
723
|
+
this.onError?.(err, batch);
|
|
724
|
+
return { success: false, ingested: 0 };
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
flushSync() {
|
|
728
|
+
if (!this.writeKey) return;
|
|
729
|
+
if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
|
|
730
|
+
if (this.buffer.length === 0) return;
|
|
731
|
+
const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
|
|
732
|
+
const payload = JSON.stringify({ events: batch });
|
|
733
|
+
const url = `${this.endpoint}/api/ingest/track?key=${encodeURIComponent(this.writeKey)}`;
|
|
734
|
+
try {
|
|
735
|
+
const blob = new Blob([payload], { type: "application/json" });
|
|
736
|
+
navigator.sendBeacon(url, blob);
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
getFlag(key, defaultValue) {
|
|
741
|
+
return this._flagClient?.getFlag(key, defaultValue) ?? defaultValue;
|
|
742
|
+
}
|
|
743
|
+
onFlagsReady(callback) {
|
|
744
|
+
this._flagClient?.onFlagsReady(callback);
|
|
745
|
+
}
|
|
746
|
+
onFlagChange(listener) {
|
|
747
|
+
return this._flagClient?.onFlagChange(listener) ?? (() => {
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async refreshFlags() {
|
|
751
|
+
await this._flagClient?.refresh();
|
|
752
|
+
}
|
|
753
|
+
async shutdown() {
|
|
754
|
+
if (this.timer) {
|
|
755
|
+
clearInterval(this.timer);
|
|
756
|
+
this.timer = null;
|
|
757
|
+
}
|
|
758
|
+
this._flagClient?.destroy();
|
|
759
|
+
this._flagClient = null;
|
|
760
|
+
if (typeof window !== "undefined" && this.visibilityHandler) {
|
|
761
|
+
window.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
762
|
+
this.visibilityHandler = null;
|
|
763
|
+
}
|
|
764
|
+
while (this.buffer.length > 0) {
|
|
765
|
+
await this.flush();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
var scoperat = new TrackerImpl();
|
|
770
|
+
var client_default = scoperat;
|
|
771
|
+
|
|
772
|
+
// src/index.ts
|
|
773
|
+
var src_default = client_default;
|
|
774
|
+
export {
|
|
775
|
+
FlagClient,
|
|
776
|
+
src_default as default
|
|
777
|
+
};
|
|
3
778
|
//# sourceMappingURL=index.js.map
|