@scoperat/tracker 0.1.0 → 0.2.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 +163 -4
- package/dist/index.js +630 -2
- package/dist/index.js.map +1 -1
- package/dist/widget/index.d.ts +41 -0
- package/dist/widget/index.js +2633 -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 -27
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -178
- 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 -30
- 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,631 @@
|
|
|
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 TICKET_SESSION_KEY = "__scoperat_ticket_session";
|
|
270
|
+
function generateAnonId() {
|
|
271
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
272
|
+
return crypto.randomUUID();
|
|
273
|
+
}
|
|
274
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
275
|
+
const r = Math.random() * 16 | 0;
|
|
276
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
277
|
+
return v.toString(16);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function getOrCreateAnonId() {
|
|
281
|
+
if (typeof window === "undefined" || typeof localStorage === "undefined") {
|
|
282
|
+
return void 0;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
let id = localStorage.getItem(ANON_ID_KEY);
|
|
286
|
+
if (!id) {
|
|
287
|
+
id = generateAnonId();
|
|
288
|
+
localStorage.setItem(ANON_ID_KEY, id);
|
|
289
|
+
}
|
|
290
|
+
return id;
|
|
291
|
+
} catch {
|
|
292
|
+
return generateAnonId();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
var TrackerImpl = class {
|
|
296
|
+
writeKey = null;
|
|
297
|
+
endpoint = "";
|
|
298
|
+
flushAt = DEFAULT_FLUSH_AT;
|
|
299
|
+
flushInterval = DEFAULT_FLUSH_INTERVAL;
|
|
300
|
+
timeout = DEFAULT_TIMEOUT;
|
|
301
|
+
onError;
|
|
302
|
+
buffer = [];
|
|
303
|
+
timer = null;
|
|
304
|
+
visibilityHandler = null;
|
|
305
|
+
userId;
|
|
306
|
+
orgId;
|
|
307
|
+
traits = {};
|
|
308
|
+
userHash;
|
|
309
|
+
anonymousId;
|
|
310
|
+
sessionTimeout = DEFAULT_SESSION_TIMEOUT;
|
|
311
|
+
ticketSessionFetch = null;
|
|
312
|
+
_flagClient = null;
|
|
313
|
+
init(writeKey, options = {}) {
|
|
314
|
+
this.writeKey = writeKey;
|
|
315
|
+
this.endpoint = (options.endpoint ?? globalThis.location?.origin ?? "").replace(/\/+$/, "");
|
|
316
|
+
this.flushAt = options.flushAt ?? DEFAULT_FLUSH_AT;
|
|
317
|
+
this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
|
|
318
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
319
|
+
this.onError = options.onError;
|
|
320
|
+
this.sessionTimeout = options.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
|
|
321
|
+
this.anonymousId = getOrCreateAnonId();
|
|
322
|
+
if (this.timer) clearInterval(this.timer);
|
|
323
|
+
this.timer = setInterval(() => {
|
|
324
|
+
if (this.buffer.length > 0) {
|
|
325
|
+
this.flush().catch(() => {
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}, this.flushInterval);
|
|
329
|
+
if (typeof window !== "undefined") {
|
|
330
|
+
if (this.visibilityHandler) {
|
|
331
|
+
window.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
332
|
+
}
|
|
333
|
+
this.visibilityHandler = () => {
|
|
334
|
+
if (document.visibilityState === "hidden" && this.buffer.length > 0) {
|
|
335
|
+
this.flushSync();
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
window.addEventListener("visibilitychange", this.visibilityHandler);
|
|
339
|
+
}
|
|
340
|
+
if (options.captureErrors) {
|
|
341
|
+
installGlobalHandlers(this, getBreadcrumbs);
|
|
342
|
+
startBreadcrumbCollection();
|
|
343
|
+
}
|
|
344
|
+
this._flagClient?.destroy();
|
|
345
|
+
this._flagClient = new FlagClient({
|
|
346
|
+
endpoint: this.endpoint,
|
|
347
|
+
writeKey,
|
|
348
|
+
timeout: this.timeout,
|
|
349
|
+
environment: options.environment,
|
|
350
|
+
pollInterval: options.flagPollInterval,
|
|
351
|
+
getIdentity: () => ({
|
|
352
|
+
userId: this.userId,
|
|
353
|
+
anonymousId: this.anonymousId,
|
|
354
|
+
orgId: this.orgId
|
|
355
|
+
}),
|
|
356
|
+
onEvaluation: (key, flag) => this.track("$flag_evaluation", {
|
|
357
|
+
flag_key: key,
|
|
358
|
+
flag_value: flag.value,
|
|
359
|
+
flag_type: flag.type,
|
|
360
|
+
flag_source: flag.source
|
|
361
|
+
})
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/** The underlying flag evaluation client. Use for direct SDK integration. */
|
|
365
|
+
get flags() {
|
|
366
|
+
if (!this._flagClient) {
|
|
367
|
+
throw new Error("Tracker not initialized. Call init() first.");
|
|
368
|
+
}
|
|
369
|
+
return this._flagClient;
|
|
370
|
+
}
|
|
371
|
+
identify(userId, traits, options) {
|
|
372
|
+
this.userId = userId;
|
|
373
|
+
if (traits) {
|
|
374
|
+
this.traits = { ...traits };
|
|
375
|
+
if (traits.orgId) this.orgId = traits.orgId;
|
|
376
|
+
}
|
|
377
|
+
this.userHash = options?.userHash;
|
|
378
|
+
for (const evt of this.buffer) {
|
|
379
|
+
if (!evt.user_id) evt.user_id = userId;
|
|
380
|
+
if (!evt.org_id && this.orgId) evt.org_id = this.orgId;
|
|
381
|
+
}
|
|
382
|
+
this._flagClient?.refresh();
|
|
383
|
+
}
|
|
384
|
+
reset() {
|
|
385
|
+
this.userId = void 0;
|
|
386
|
+
this.orgId = void 0;
|
|
387
|
+
this.traits = {};
|
|
388
|
+
this.userHash = void 0;
|
|
389
|
+
this.anonymousId = getOrCreateAnonId();
|
|
390
|
+
this.clearSession();
|
|
391
|
+
this.clearTicketSession();
|
|
392
|
+
}
|
|
393
|
+
clearTicketSession() {
|
|
394
|
+
this.ticketSessionFetch = null;
|
|
395
|
+
if (typeof localStorage === "undefined") return;
|
|
396
|
+
try {
|
|
397
|
+
localStorage.removeItem(TICKET_SESSION_KEY);
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
readStoredTicketSession() {
|
|
402
|
+
if (typeof localStorage === "undefined") return null;
|
|
403
|
+
try {
|
|
404
|
+
const raw = localStorage.getItem(TICKET_SESSION_KEY);
|
|
405
|
+
if (!raw) return null;
|
|
406
|
+
const parsed = JSON.parse(raw);
|
|
407
|
+
if (typeof parsed.anonymousId === "string" && typeof parsed.token === "string" && typeof parsed.expiresAt === "number" && parsed.expiresAt > Date.now() + 6e4) {
|
|
408
|
+
return parsed;
|
|
409
|
+
}
|
|
410
|
+
localStorage.removeItem(TICKET_SESSION_KEY);
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* @internal Used by the widget API helpers. Returns a cached or
|
|
417
|
+
* newly-minted anonymous session token, or `null` if the server
|
|
418
|
+
* has anonymous sessions disabled. Coalesces concurrent callers
|
|
419
|
+
* onto a single in-flight network request so mount-time parallel
|
|
420
|
+
* API calls don't each trigger a `POST /ticket/session`.
|
|
421
|
+
*/
|
|
422
|
+
_ensureTicketSession() {
|
|
423
|
+
const cached = this.readStoredTicketSession();
|
|
424
|
+
if (cached) return Promise.resolve(cached);
|
|
425
|
+
if (this.ticketSessionFetch) return this.ticketSessionFetch;
|
|
426
|
+
if (!this.writeKey) return Promise.resolve(null);
|
|
427
|
+
this.ticketSessionFetch = (async () => {
|
|
428
|
+
try {
|
|
429
|
+
const res = await fetch(`${this.endpoint}/api/ingest/ticket/session`, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
headers: {
|
|
432
|
+
"Content-Type": "application/json",
|
|
433
|
+
Authorization: `Bearer ${this.writeKey}`
|
|
434
|
+
},
|
|
435
|
+
body: "{}",
|
|
436
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
437
|
+
});
|
|
438
|
+
if (!res.ok) return null;
|
|
439
|
+
const json = await res.json();
|
|
440
|
+
if (typeof json?.anonymousId === "string" && typeof json?.token === "string" && typeof json?.expiresAt === "number") {
|
|
441
|
+
try {
|
|
442
|
+
localStorage?.setItem(TICKET_SESSION_KEY, JSON.stringify(json));
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
this.anonymousId = json.anonymousId;
|
|
446
|
+
return json;
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
} finally {
|
|
452
|
+
this.ticketSessionFetch = null;
|
|
453
|
+
}
|
|
454
|
+
})();
|
|
455
|
+
return this.ticketSessionFetch;
|
|
456
|
+
}
|
|
457
|
+
getOrCreateSessionId() {
|
|
458
|
+
if (typeof window === "undefined") return void 0;
|
|
459
|
+
try {
|
|
460
|
+
const now = Date.now();
|
|
461
|
+
const lastActivity = localStorage.getItem(LAST_ACTIVITY_KEY);
|
|
462
|
+
const existingId = sessionStorage.getItem(SESSION_ID_KEY);
|
|
463
|
+
const expired = !lastActivity || now - Number(lastActivity) > this.sessionTimeout;
|
|
464
|
+
if (existingId && !expired) {
|
|
465
|
+
localStorage.setItem(LAST_ACTIVITY_KEY, String(now));
|
|
466
|
+
return existingId;
|
|
467
|
+
}
|
|
468
|
+
const newId = generateAnonId();
|
|
469
|
+
sessionStorage.setItem(SESSION_ID_KEY, newId);
|
|
470
|
+
localStorage.setItem(LAST_ACTIVITY_KEY, String(now));
|
|
471
|
+
return newId;
|
|
472
|
+
} catch {
|
|
473
|
+
return generateAnonId();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
clearSession() {
|
|
477
|
+
if (typeof window === "undefined") return;
|
|
478
|
+
try {
|
|
479
|
+
sessionStorage.removeItem(SESSION_ID_KEY);
|
|
480
|
+
localStorage.removeItem(LAST_ACTIVITY_KEY);
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
getSessionId() {
|
|
485
|
+
return this.getOrCreateSessionId();
|
|
486
|
+
}
|
|
487
|
+
/** @internal Used by the widget to read tracker config. */
|
|
488
|
+
_getEndpoint() {
|
|
489
|
+
return this.endpoint;
|
|
490
|
+
}
|
|
491
|
+
/** @internal Used by the widget to read the write key. */
|
|
492
|
+
_getWriteKey() {
|
|
493
|
+
return this.writeKey;
|
|
494
|
+
}
|
|
495
|
+
/** @internal Used by the widget to read the current user identity. */
|
|
496
|
+
_getIdentity() {
|
|
497
|
+
return {
|
|
498
|
+
userId: this.userId,
|
|
499
|
+
anonymousId: this.anonymousId,
|
|
500
|
+
email: this.traits.email,
|
|
501
|
+
name: this.traits.name,
|
|
502
|
+
userHash: this.userHash
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
captureException(error, context) {
|
|
506
|
+
if (!this.writeKey) return;
|
|
507
|
+
const breadcrumbs = getBreadcrumbs();
|
|
508
|
+
this.track("$exception", {
|
|
509
|
+
type: error.name || "Error",
|
|
510
|
+
message: error.message,
|
|
511
|
+
stack: error.stack,
|
|
512
|
+
handled: true,
|
|
513
|
+
mechanism: context?.mechanism ?? "manual",
|
|
514
|
+
source: "browser",
|
|
515
|
+
...context,
|
|
516
|
+
context: { breadcrumbs }
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
addBreadcrumb(category, message, data) {
|
|
520
|
+
addBreadcrumb({
|
|
521
|
+
type: "custom",
|
|
522
|
+
category,
|
|
523
|
+
message,
|
|
524
|
+
timestamp: Date.now(),
|
|
525
|
+
data
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
track(eventOrName, properties) {
|
|
529
|
+
if (!this.writeKey) return;
|
|
530
|
+
const evt = typeof eventOrName === "string" ? { event: eventOrName, properties } : eventOrName;
|
|
531
|
+
const traitsContext = Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};
|
|
532
|
+
const enriched = {
|
|
533
|
+
...evt,
|
|
534
|
+
occurred_at: evt.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
535
|
+
source: evt.source ?? "browser",
|
|
536
|
+
org_id: evt.org_id ?? this.orgId,
|
|
537
|
+
user_id: evt.user_id ?? this.userId,
|
|
538
|
+
anonymous_id: evt.anonymous_id ?? this.anonymousId,
|
|
539
|
+
session_id: evt.session_id ?? this.getOrCreateSessionId(),
|
|
540
|
+
context: {
|
|
541
|
+
...getBrowserContext(),
|
|
542
|
+
...traitsContext,
|
|
543
|
+
...evt.context
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
this.buffer.push(enriched);
|
|
547
|
+
if (this.buffer.length >= this.flushAt) {
|
|
548
|
+
this.flush().catch(() => {
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async flush() {
|
|
553
|
+
if (!this.writeKey || this.buffer.length === 0) {
|
|
554
|
+
return { success: true, ingested: 0 };
|
|
555
|
+
}
|
|
556
|
+
const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
|
|
557
|
+
try {
|
|
558
|
+
const res = await fetch(`${this.endpoint}/api/ingest/track`, {
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers: {
|
|
561
|
+
"Content-Type": "application/json",
|
|
562
|
+
Authorization: `Bearer ${this.writeKey}`
|
|
563
|
+
},
|
|
564
|
+
body: JSON.stringify({ events: batch }),
|
|
565
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
566
|
+
});
|
|
567
|
+
if (!res.ok) {
|
|
568
|
+
const body = await res.text().catch(() => "");
|
|
569
|
+
throw new Error(`API returned ${res.status}: ${body}`);
|
|
570
|
+
}
|
|
571
|
+
const json = await res.json();
|
|
572
|
+
return { success: true, ingested: json.ingested };
|
|
573
|
+
} catch (error) {
|
|
574
|
+
this.buffer.unshift(...batch);
|
|
575
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
576
|
+
this.onError?.(err, batch);
|
|
577
|
+
return { success: false, ingested: 0 };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
flushSync() {
|
|
581
|
+
if (!this.writeKey) return;
|
|
582
|
+
if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
|
|
583
|
+
if (this.buffer.length === 0) return;
|
|
584
|
+
const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
|
|
585
|
+
const payload = JSON.stringify({ events: batch });
|
|
586
|
+
const url = `${this.endpoint}/api/ingest/track?key=${encodeURIComponent(this.writeKey)}`;
|
|
587
|
+
try {
|
|
588
|
+
const blob = new Blob([payload], { type: "application/json" });
|
|
589
|
+
navigator.sendBeacon(url, blob);
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
getFlag(key, defaultValue) {
|
|
594
|
+
return this._flagClient?.getFlag(key, defaultValue) ?? defaultValue;
|
|
595
|
+
}
|
|
596
|
+
onFlagsReady(callback) {
|
|
597
|
+
this._flagClient?.onFlagsReady(callback);
|
|
598
|
+
}
|
|
599
|
+
onFlagChange(listener) {
|
|
600
|
+
return this._flagClient?.onFlagChange(listener) ?? (() => {
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
async refreshFlags() {
|
|
604
|
+
await this._flagClient?.refresh();
|
|
605
|
+
}
|
|
606
|
+
async shutdown() {
|
|
607
|
+
if (this.timer) {
|
|
608
|
+
clearInterval(this.timer);
|
|
609
|
+
this.timer = null;
|
|
610
|
+
}
|
|
611
|
+
this._flagClient?.destroy();
|
|
612
|
+
this._flagClient = null;
|
|
613
|
+
if (typeof window !== "undefined" && this.visibilityHandler) {
|
|
614
|
+
window.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
615
|
+
this.visibilityHandler = null;
|
|
616
|
+
}
|
|
617
|
+
while (this.buffer.length > 0) {
|
|
618
|
+
await this.flush();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
var scoperat = new TrackerImpl();
|
|
623
|
+
var client_default = scoperat;
|
|
624
|
+
|
|
625
|
+
// src/index.ts
|
|
626
|
+
var src_default = client_default;
|
|
627
|
+
export {
|
|
628
|
+
FlagClient,
|
|
629
|
+
src_default as default
|
|
630
|
+
};
|
|
3
631
|
//# sourceMappingURL=index.js.map
|