@scoperat/tracker 0.2.0 → 0.3.1
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 +51 -1
- package/dist/index.js +178 -18
- package/dist/index.js.map +1 -1
- package/dist/widget/index.js +178 -18
- package/dist/widget/index.js.map +1 -1
- package/dist/widget.global.js +49 -49
- package/dist/widget.global.js.map +1 -1
- package/package.json +1 -1
package/dist/widget/index.js
CHANGED
|
@@ -1204,6 +1204,9 @@ var MAX_BATCH_SIZE = 100;
|
|
|
1204
1204
|
var ANON_ID_KEY = "__scoperat_anon_id";
|
|
1205
1205
|
var SESSION_ID_KEY = "__scoperat_session_id";
|
|
1206
1206
|
var LAST_ACTIVITY_KEY = "__scoperat_last_activity";
|
|
1207
|
+
var USER_ID_KEY = "__scoperat_user_id";
|
|
1208
|
+
var SESSION_START_EVENT = "$session_start";
|
|
1209
|
+
var SESSION_END_EVENT = "$session_end";
|
|
1207
1210
|
var TICKET_SESSION_KEY = "__scoperat_ticket_session";
|
|
1208
1211
|
function generateAnonId() {
|
|
1209
1212
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
@@ -1245,6 +1248,7 @@ var TrackerImpl = class {
|
|
|
1245
1248
|
traits = {};
|
|
1246
1249
|
userHash;
|
|
1247
1250
|
anonymousId;
|
|
1251
|
+
sessionId;
|
|
1248
1252
|
sessionTimeout = DEFAULT_SESSION_TIMEOUT;
|
|
1249
1253
|
ticketSessionFetch = null;
|
|
1250
1254
|
_flagClient = null;
|
|
@@ -1307,6 +1311,7 @@ var TrackerImpl = class {
|
|
|
1307
1311
|
return this._flagClient;
|
|
1308
1312
|
}
|
|
1309
1313
|
identify(userId, traits, options) {
|
|
1314
|
+
const previousUserId = this.userId ?? this.readStoredUserId();
|
|
1310
1315
|
this.userId = userId;
|
|
1311
1316
|
if (traits) {
|
|
1312
1317
|
this.traits = { ...traits };
|
|
@@ -1317,17 +1322,83 @@ var TrackerImpl = class {
|
|
|
1317
1322
|
if (!evt.user_id) evt.user_id = userId;
|
|
1318
1323
|
if (!evt.org_id && this.orgId) evt.org_id = this.orgId;
|
|
1319
1324
|
}
|
|
1325
|
+
if (userId !== previousUserId) {
|
|
1326
|
+
this.writeStoredUserId(userId);
|
|
1327
|
+
this.startSession("login");
|
|
1328
|
+
}
|
|
1320
1329
|
this._flagClient?.refresh();
|
|
1321
1330
|
}
|
|
1322
1331
|
reset() {
|
|
1332
|
+
this.endSession("logout", true);
|
|
1323
1333
|
this.userId = void 0;
|
|
1324
1334
|
this.orgId = void 0;
|
|
1325
1335
|
this.traits = {};
|
|
1326
1336
|
this.userHash = void 0;
|
|
1327
1337
|
this.anonymousId = getOrCreateAnonId();
|
|
1328
|
-
this.
|
|
1338
|
+
this.clearStoredUserId();
|
|
1329
1339
|
this.clearTicketSession();
|
|
1330
1340
|
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Begin a new analytics session, ending the current one first if it
|
|
1343
|
+
* exists. Emits `$session_end` (for the old session) and
|
|
1344
|
+
* `$session_start` (for the new). Called automatically on login via
|
|
1345
|
+
* {@link identify}; also exposed for explicit control.
|
|
1346
|
+
*/
|
|
1347
|
+
startSession(reason = "login") {
|
|
1348
|
+
if (typeof window === "undefined") return this.sessionId;
|
|
1349
|
+
const now = Date.now();
|
|
1350
|
+
const existingId = this.readStoredSession();
|
|
1351
|
+
if (existingId) {
|
|
1352
|
+
this.emitSessionEvent(SESSION_END_EVENT, existingId, reason);
|
|
1353
|
+
}
|
|
1354
|
+
const newId = generateAnonId();
|
|
1355
|
+
this.persistSession(newId, now);
|
|
1356
|
+
this.emitSessionEvent(SESSION_START_EVENT, newId, reason);
|
|
1357
|
+
return newId;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* End the current analytics session (if any), emitting `$session_end`,
|
|
1361
|
+
* and clear the stored session. The next tracked event will lazily
|
|
1362
|
+
* start a fresh session. Called automatically on logout via
|
|
1363
|
+
* {@link reset}; also exposed for explicit control.
|
|
1364
|
+
*
|
|
1365
|
+
* @param immediate - flush buffered events synchronously (via
|
|
1366
|
+
* `sendBeacon`) so the end event isn't lost if the page unloads.
|
|
1367
|
+
*/
|
|
1368
|
+
endSession(reason = "logout", immediate = false) {
|
|
1369
|
+
if (typeof window === "undefined") return;
|
|
1370
|
+
const existingId = this.readStoredSession();
|
|
1371
|
+
if (existingId) {
|
|
1372
|
+
this.emitSessionEvent(SESSION_END_EVENT, existingId, reason);
|
|
1373
|
+
}
|
|
1374
|
+
this.sessionId = void 0;
|
|
1375
|
+
this.clearSession();
|
|
1376
|
+
if (immediate) {
|
|
1377
|
+
this.flushSync();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
readStoredUserId() {
|
|
1381
|
+
if (typeof localStorage === "undefined") return void 0;
|
|
1382
|
+
try {
|
|
1383
|
+
return localStorage.getItem(USER_ID_KEY) ?? void 0;
|
|
1384
|
+
} catch {
|
|
1385
|
+
return void 0;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
writeStoredUserId(userId) {
|
|
1389
|
+
if (typeof localStorage === "undefined") return;
|
|
1390
|
+
try {
|
|
1391
|
+
localStorage.setItem(USER_ID_KEY, userId);
|
|
1392
|
+
} catch {
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
clearStoredUserId() {
|
|
1396
|
+
if (typeof localStorage === "undefined") return;
|
|
1397
|
+
try {
|
|
1398
|
+
localStorage.removeItem(USER_ID_KEY);
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1331
1402
|
clearTicketSession() {
|
|
1332
1403
|
this.ticketSessionFetch = null;
|
|
1333
1404
|
if (typeof localStorage === "undefined") return;
|
|
@@ -1392,25 +1463,96 @@ var TrackerImpl = class {
|
|
|
1392
1463
|
})();
|
|
1393
1464
|
return this.ticketSessionFetch;
|
|
1394
1465
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1466
|
+
/**
|
|
1467
|
+
* Return the id of the current session, rotating it when the previous
|
|
1468
|
+
* session has expired through inactivity. On rotation it emits a
|
|
1469
|
+
* `$session_end` for the timed-out session (backdated to the last
|
|
1470
|
+
* recorded activity) and a `$session_start` for the new one. Called on
|
|
1471
|
+
* every tracked event so sessions advance with real user activity.
|
|
1472
|
+
*/
|
|
1473
|
+
ensureSession(now) {
|
|
1474
|
+
if (typeof window === "undefined") return this.sessionId;
|
|
1397
1475
|
try {
|
|
1398
|
-
const
|
|
1399
|
-
const
|
|
1400
|
-
const
|
|
1401
|
-
const expired = !lastActivity || now - Number(lastActivity) > this.sessionTimeout;
|
|
1476
|
+
const lastActivity = this.readLastActivity();
|
|
1477
|
+
const existingId = this.readStoredSession();
|
|
1478
|
+
const expired = lastActivity === void 0 || now - lastActivity > this.sessionTimeout;
|
|
1402
1479
|
if (existingId && !expired) {
|
|
1403
|
-
|
|
1480
|
+
this.sessionId = existingId;
|
|
1481
|
+
try {
|
|
1482
|
+
localStorage.setItem(LAST_ACTIVITY_KEY, String(now));
|
|
1483
|
+
} catch {
|
|
1484
|
+
}
|
|
1404
1485
|
return existingId;
|
|
1405
1486
|
}
|
|
1487
|
+
if (existingId) {
|
|
1488
|
+
this.emitSessionEvent(
|
|
1489
|
+
SESSION_END_EVENT,
|
|
1490
|
+
existingId,
|
|
1491
|
+
"timeout",
|
|
1492
|
+
lastActivity !== void 0 ? new Date(lastActivity).toISOString() : void 0
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1406
1495
|
const newId = generateAnonId();
|
|
1407
|
-
|
|
1408
|
-
|
|
1496
|
+
this.persistSession(newId, now);
|
|
1497
|
+
this.emitSessionEvent(
|
|
1498
|
+
SESSION_START_EVENT,
|
|
1499
|
+
newId,
|
|
1500
|
+
existingId ? "timeout" : "initial"
|
|
1501
|
+
);
|
|
1409
1502
|
return newId;
|
|
1410
1503
|
} catch {
|
|
1411
|
-
return generateAnonId();
|
|
1504
|
+
return this.sessionId ?? generateAnonId();
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
readStoredSession() {
|
|
1508
|
+
if (this.sessionId) return this.sessionId;
|
|
1509
|
+
if (typeof sessionStorage === "undefined") return void 0;
|
|
1510
|
+
try {
|
|
1511
|
+
return sessionStorage.getItem(SESSION_ID_KEY) ?? void 0;
|
|
1512
|
+
} catch {
|
|
1513
|
+
return void 0;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
readLastActivity() {
|
|
1517
|
+
if (typeof localStorage === "undefined") return void 0;
|
|
1518
|
+
try {
|
|
1519
|
+
const raw = localStorage.getItem(LAST_ACTIVITY_KEY);
|
|
1520
|
+
return raw ? Number(raw) : void 0;
|
|
1521
|
+
} catch {
|
|
1522
|
+
return void 0;
|
|
1412
1523
|
}
|
|
1413
1524
|
}
|
|
1525
|
+
persistSession(id, now) {
|
|
1526
|
+
this.sessionId = id;
|
|
1527
|
+
if (typeof window === "undefined") return;
|
|
1528
|
+
try {
|
|
1529
|
+
sessionStorage.setItem(SESSION_ID_KEY, id);
|
|
1530
|
+
localStorage.setItem(LAST_ACTIVITY_KEY, String(now));
|
|
1531
|
+
} catch {
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Enqueue a session lifecycle event already stamped with its session id
|
|
1536
|
+
* (so it never re-enters {@link ensureSession} and risks recursion).
|
|
1537
|
+
*/
|
|
1538
|
+
emitSessionEvent(event, sessionId, reason, occurredAt) {
|
|
1539
|
+
if (!this.writeKey) return;
|
|
1540
|
+
const traitsContext = Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};
|
|
1541
|
+
this.buffer.push({
|
|
1542
|
+
event,
|
|
1543
|
+
properties: { reason },
|
|
1544
|
+
occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1545
|
+
source: "browser",
|
|
1546
|
+
org_id: this.orgId,
|
|
1547
|
+
user_id: this.userId,
|
|
1548
|
+
anonymous_id: this.anonymousId,
|
|
1549
|
+
session_id: sessionId,
|
|
1550
|
+
context: {
|
|
1551
|
+
...getBrowserContext(),
|
|
1552
|
+
...traitsContext
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1414
1556
|
clearSession() {
|
|
1415
1557
|
if (typeof window === "undefined") return;
|
|
1416
1558
|
try {
|
|
@@ -1420,7 +1562,7 @@ var TrackerImpl = class {
|
|
|
1420
1562
|
}
|
|
1421
1563
|
}
|
|
1422
1564
|
getSessionId() {
|
|
1423
|
-
return this.
|
|
1565
|
+
return this.ensureSession(Date.now());
|
|
1424
1566
|
}
|
|
1425
1567
|
/** @internal Used by the widget to read tracker config. */
|
|
1426
1568
|
_getEndpoint() {
|
|
@@ -1474,7 +1616,7 @@ var TrackerImpl = class {
|
|
|
1474
1616
|
org_id: evt.org_id ?? this.orgId,
|
|
1475
1617
|
user_id: evt.user_id ?? this.userId,
|
|
1476
1618
|
anonymous_id: evt.anonymous_id ?? this.anonymousId,
|
|
1477
|
-
session_id: evt.session_id ?? this.
|
|
1619
|
+
session_id: evt.session_id ?? this.ensureSession(Date.now()),
|
|
1478
1620
|
context: {
|
|
1479
1621
|
...getBrowserContext(),
|
|
1480
1622
|
...traitsContext,
|
|
@@ -1515,16 +1657,34 @@ var TrackerImpl = class {
|
|
|
1515
1657
|
return { success: false, ingested: 0 };
|
|
1516
1658
|
}
|
|
1517
1659
|
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Fire-and-forget flush that survives page unload.
|
|
1662
|
+
*
|
|
1663
|
+
* Uses `fetch` with `keepalive` rather than `navigator.sendBeacon`:
|
|
1664
|
+
* beacons are always dispatched with credentials, and a JSON beacon is
|
|
1665
|
+
* preflighted — a credentialed preflighted request fails the CORS check
|
|
1666
|
+
* against the wildcard (`Access-Control-Allow-Origin: *`) ingest policy,
|
|
1667
|
+
* so the browser silently drops the beacon before it is ever sent. A
|
|
1668
|
+
* keepalive fetch is non-credentialed by default and carries the normal
|
|
1669
|
+
* Bearer header, giving it the exact CORS profile of the regular
|
|
1670
|
+
* `flush()` path while still outliving the page.
|
|
1671
|
+
*/
|
|
1518
1672
|
flushSync() {
|
|
1519
1673
|
if (!this.writeKey) return;
|
|
1520
|
-
if (typeof
|
|
1674
|
+
if (typeof fetch === "undefined") return;
|
|
1521
1675
|
if (this.buffer.length === 0) return;
|
|
1522
1676
|
const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
|
|
1523
|
-
const payload = JSON.stringify({ events: batch });
|
|
1524
|
-
const url = `${this.endpoint}/api/ingest/track?key=${encodeURIComponent(this.writeKey)}`;
|
|
1525
1677
|
try {
|
|
1526
|
-
|
|
1527
|
-
|
|
1678
|
+
void fetch(`${this.endpoint}/api/ingest/track`, {
|
|
1679
|
+
method: "POST",
|
|
1680
|
+
keepalive: true,
|
|
1681
|
+
headers: {
|
|
1682
|
+
"Content-Type": "application/json",
|
|
1683
|
+
Authorization: `Bearer ${this.writeKey}`
|
|
1684
|
+
},
|
|
1685
|
+
body: JSON.stringify({ events: batch })
|
|
1686
|
+
}).catch(() => {
|
|
1687
|
+
});
|
|
1528
1688
|
} catch {
|
|
1529
1689
|
}
|
|
1530
1690
|
}
|