@edraj/sauron-browser 1.4.0 → 1.4.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/CHANGELOG.md +34 -0
- package/README.md +46 -3
- package/dist/index.cjs +131 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -8
- package/dist/index.d.ts +62 -8
- package/dist/index.js +131 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -374,6 +374,24 @@ declare class Scope {
|
|
|
374
374
|
readonly extra: Record<string, unknown>;
|
|
375
375
|
constructor(maxBreadcrumbs?: number);
|
|
376
376
|
setMaxBreadcrumbs(max: number): void;
|
|
377
|
+
/**
|
|
378
|
+
* Replace the scope user.
|
|
379
|
+
*
|
|
380
|
+
* `id` is coerced with `String()` for the same reason `SauronClient.
|
|
381
|
+
* prepareIdentify` coerces its own — a plain-JS caller can (and does) pass
|
|
382
|
+
* `setUser({ id: user.id })` where `user.id` is a number, and TypeScript
|
|
383
|
+
* cannot stop them. This path is the one that BYPASSES `identify()`'s
|
|
384
|
+
* coercion entirely, and the consequence is not cosmetic: the scope user
|
|
385
|
+
* lands in the envelope context, where the server's `distinct_id` is a
|
|
386
|
+
* non-`Option` Rust `String`. A JSON number there fails deserialization of
|
|
387
|
+
* the ENVELOPE, not of the one field — so the whole batch 400s, and a 400
|
|
388
|
+
* is non-retryable, so every event in it is dropped for good.
|
|
389
|
+
*
|
|
390
|
+
* Rebuilding the whole object (rather than merging into the existing one)
|
|
391
|
+
* is deliberate and is the behaviour the Flutter SDK was fixed to match:
|
|
392
|
+
* `email` and `traits` come from the input alone, so setting a new user
|
|
393
|
+
* never inherits the previous person's contact details.
|
|
394
|
+
*/
|
|
377
395
|
setUser(user: UserInput): void;
|
|
378
396
|
/** The user context for an envelope. Never null — defaults to an empty user. */
|
|
379
397
|
getUser(): UserContext;
|
|
@@ -438,14 +456,40 @@ declare class SauronClient {
|
|
|
438
456
|
/** The anonymous id, or null when it was never actually used as an identity. */
|
|
439
457
|
getAnonymousId(): string | null;
|
|
440
458
|
/**
|
|
441
|
-
* Forget the current person: clear the scope user
|
|
442
|
-
* id.
|
|
459
|
+
* Forget the current person: clear the scope user, mint a fresh anonymous
|
|
460
|
+
* id, forget the last identified user, and rotate the session id.
|
|
443
461
|
*
|
|
444
462
|
* MUST BE CALLED ON LOGOUT. Without it, the next anonymous visitor on this
|
|
445
463
|
* browser reuses the persisted anon id, and a later identify() aliases their
|
|
446
|
-
* activity to the previous account server-side, permanently.
|
|
464
|
+
* activity to the previous account server-side, permanently. Rotating the
|
|
465
|
+
* session id matters too: the server's `bump_session` is last-write-wins on
|
|
466
|
+
* `distinct_id`, so without rotation one `sessions` row could otherwise
|
|
467
|
+
* serially represent two different people and record only whichever wrote
|
|
468
|
+
* last.
|
|
447
469
|
*/
|
|
448
470
|
reset(): void;
|
|
471
|
+
/**
|
|
472
|
+
* Prepare for an `identify()`; returns the `anonymous_id` to send.
|
|
473
|
+
*
|
|
474
|
+
* When a DIFFERENT user identifies than last time, the current anon id
|
|
475
|
+
* belongs to the previous person and is already burned server-side, so it is
|
|
476
|
+
* replaced before anything else happens and `null` is sent instead of a
|
|
477
|
+
* cross-user alias. This cannot repair events already sent under the burned
|
|
478
|
+
* alias — nothing can — but it bounds a forgotten `reset()` to one guest
|
|
479
|
+
* window instead of every future one.
|
|
480
|
+
*
|
|
481
|
+
* `id` is coerced with `String()` before comparing/persisting: a plain-JS
|
|
482
|
+
* caller can pass a number (`Sauron.identify(user.id)`), and `Storage`
|
|
483
|
+
* itself applies `ToString` on write — so comparing an un-coerced `id`
|
|
484
|
+
* against a value that already round-tripped through storage would treat
|
|
485
|
+
* the SAME numeric user as a switch on every single call. The comparison
|
|
486
|
+
* against `last` is an explicit `!== null` (not a truthiness check) so an
|
|
487
|
+
* app that (unusually) identifies with `''` still has a later, different id
|
|
488
|
+
* correctly detected as a real switch — a falsy string is not "no identity
|
|
489
|
+
* yet". `last`/the persisted value are digests, not the raw id — see
|
|
490
|
+
* `hashIdentity`.
|
|
491
|
+
*/
|
|
492
|
+
prepareIdentify(id: string): string | null;
|
|
449
493
|
/** Stamp a fresh envelope (new `sent_at`, current context) around `items`. */
|
|
450
494
|
makeEnvelope(items: EnvelopeItem[]): Envelope;
|
|
451
495
|
/** Add a breadcrumb, running it through `beforeBreadcrumb` first. */
|
|
@@ -517,7 +561,7 @@ declare function parseError(err: unknown): Frame[];
|
|
|
517
561
|
/** Small dependency-free helpers shared across the SDK. */
|
|
518
562
|
/** SDK identity, embedded in every envelope header. */
|
|
519
563
|
declare const SDK_NAME = "sauron.javascript";
|
|
520
|
-
declare const SDK_VERSION = "1.4.
|
|
564
|
+
declare const SDK_VERSION = "1.4.1";
|
|
521
565
|
|
|
522
566
|
/**
|
|
523
567
|
* `@edraj/sauron-browser` — public API surface.
|
|
@@ -540,7 +584,16 @@ declare function captureException(err: unknown, hint?: Hint): void;
|
|
|
540
584
|
declare function captureMessage(message: string, level?: Level, hint?: Hint): void;
|
|
541
585
|
/** Record a product-analytics event, optionally with per-call tags/contexts/extra. */
|
|
542
586
|
declare function track(name: string, properties?: Record<string, unknown>, options?: TrackOptions): void;
|
|
543
|
-
/**
|
|
587
|
+
/**
|
|
588
|
+
* Associate the session with a known user.
|
|
589
|
+
*
|
|
590
|
+
* The `anonymous_id` sent with the identify item is the current anon id — but
|
|
591
|
+
* only when it was actually used as a `distinct_id` this session, and never
|
|
592
|
+
* when it belongs to a different person than the last one who identified on
|
|
593
|
+
* this device. In that case a fresh anon id is minted first and `null` is
|
|
594
|
+
* sent instead, since the old one is already permanently bound to the
|
|
595
|
+
* previous person server-side (see `reset()`).
|
|
596
|
+
*/
|
|
544
597
|
declare function identify(id: string, traits?: Record<string, unknown>): void;
|
|
545
598
|
/** Record a performance transaction (navigation, http, screen load, ...). */
|
|
546
599
|
declare function trackTransaction(input: TransactionInput): void;
|
|
@@ -570,14 +623,15 @@ declare function addBreadcrumb(breadcrumb: BreadcrumbInput, hint?: Hint): void;
|
|
|
570
623
|
/**
|
|
571
624
|
* Set (or clear, with `null`) the current user.
|
|
572
625
|
*
|
|
573
|
-
* `setUser(null)` is a logout, so it also
|
|
626
|
+
* `setUser(null)` is a logout, so it also calls `reset()` for you — otherwise
|
|
574
627
|
* the next anonymous visitor on this browser inherits the previous person's
|
|
575
628
|
* durable id and a later identify() aliases them together server-side.
|
|
576
629
|
*/
|
|
577
630
|
declare function setUser(user: UserInput): void;
|
|
578
631
|
/**
|
|
579
|
-
* Forget the current person: clears the scope user
|
|
580
|
-
* id
|
|
632
|
+
* Forget the current person: clears the scope user, mints a fresh anonymous
|
|
633
|
+
* id, forgets the last identified user, and rotates the session id so a
|
|
634
|
+
* single session can never span two different people. Call this on logout.
|
|
581
635
|
*/
|
|
582
636
|
declare function reset(): void;
|
|
583
637
|
/** Set a single scope tag (lifted onto later errors/events). */
|
package/dist/index.d.ts
CHANGED
|
@@ -374,6 +374,24 @@ declare class Scope {
|
|
|
374
374
|
readonly extra: Record<string, unknown>;
|
|
375
375
|
constructor(maxBreadcrumbs?: number);
|
|
376
376
|
setMaxBreadcrumbs(max: number): void;
|
|
377
|
+
/**
|
|
378
|
+
* Replace the scope user.
|
|
379
|
+
*
|
|
380
|
+
* `id` is coerced with `String()` for the same reason `SauronClient.
|
|
381
|
+
* prepareIdentify` coerces its own — a plain-JS caller can (and does) pass
|
|
382
|
+
* `setUser({ id: user.id })` where `user.id` is a number, and TypeScript
|
|
383
|
+
* cannot stop them. This path is the one that BYPASSES `identify()`'s
|
|
384
|
+
* coercion entirely, and the consequence is not cosmetic: the scope user
|
|
385
|
+
* lands in the envelope context, where the server's `distinct_id` is a
|
|
386
|
+
* non-`Option` Rust `String`. A JSON number there fails deserialization of
|
|
387
|
+
* the ENVELOPE, not of the one field — so the whole batch 400s, and a 400
|
|
388
|
+
* is non-retryable, so every event in it is dropped for good.
|
|
389
|
+
*
|
|
390
|
+
* Rebuilding the whole object (rather than merging into the existing one)
|
|
391
|
+
* is deliberate and is the behaviour the Flutter SDK was fixed to match:
|
|
392
|
+
* `email` and `traits` come from the input alone, so setting a new user
|
|
393
|
+
* never inherits the previous person's contact details.
|
|
394
|
+
*/
|
|
377
395
|
setUser(user: UserInput): void;
|
|
378
396
|
/** The user context for an envelope. Never null — defaults to an empty user. */
|
|
379
397
|
getUser(): UserContext;
|
|
@@ -438,14 +456,40 @@ declare class SauronClient {
|
|
|
438
456
|
/** The anonymous id, or null when it was never actually used as an identity. */
|
|
439
457
|
getAnonymousId(): string | null;
|
|
440
458
|
/**
|
|
441
|
-
* Forget the current person: clear the scope user
|
|
442
|
-
* id.
|
|
459
|
+
* Forget the current person: clear the scope user, mint a fresh anonymous
|
|
460
|
+
* id, forget the last identified user, and rotate the session id.
|
|
443
461
|
*
|
|
444
462
|
* MUST BE CALLED ON LOGOUT. Without it, the next anonymous visitor on this
|
|
445
463
|
* browser reuses the persisted anon id, and a later identify() aliases their
|
|
446
|
-
* activity to the previous account server-side, permanently.
|
|
464
|
+
* activity to the previous account server-side, permanently. Rotating the
|
|
465
|
+
* session id matters too: the server's `bump_session` is last-write-wins on
|
|
466
|
+
* `distinct_id`, so without rotation one `sessions` row could otherwise
|
|
467
|
+
* serially represent two different people and record only whichever wrote
|
|
468
|
+
* last.
|
|
447
469
|
*/
|
|
448
470
|
reset(): void;
|
|
471
|
+
/**
|
|
472
|
+
* Prepare for an `identify()`; returns the `anonymous_id` to send.
|
|
473
|
+
*
|
|
474
|
+
* When a DIFFERENT user identifies than last time, the current anon id
|
|
475
|
+
* belongs to the previous person and is already burned server-side, so it is
|
|
476
|
+
* replaced before anything else happens and `null` is sent instead of a
|
|
477
|
+
* cross-user alias. This cannot repair events already sent under the burned
|
|
478
|
+
* alias — nothing can — but it bounds a forgotten `reset()` to one guest
|
|
479
|
+
* window instead of every future one.
|
|
480
|
+
*
|
|
481
|
+
* `id` is coerced with `String()` before comparing/persisting: a plain-JS
|
|
482
|
+
* caller can pass a number (`Sauron.identify(user.id)`), and `Storage`
|
|
483
|
+
* itself applies `ToString` on write — so comparing an un-coerced `id`
|
|
484
|
+
* against a value that already round-tripped through storage would treat
|
|
485
|
+
* the SAME numeric user as a switch on every single call. The comparison
|
|
486
|
+
* against `last` is an explicit `!== null` (not a truthiness check) so an
|
|
487
|
+
* app that (unusually) identifies with `''` still has a later, different id
|
|
488
|
+
* correctly detected as a real switch — a falsy string is not "no identity
|
|
489
|
+
* yet". `last`/the persisted value are digests, not the raw id — see
|
|
490
|
+
* `hashIdentity`.
|
|
491
|
+
*/
|
|
492
|
+
prepareIdentify(id: string): string | null;
|
|
449
493
|
/** Stamp a fresh envelope (new `sent_at`, current context) around `items`. */
|
|
450
494
|
makeEnvelope(items: EnvelopeItem[]): Envelope;
|
|
451
495
|
/** Add a breadcrumb, running it through `beforeBreadcrumb` first. */
|
|
@@ -517,7 +561,7 @@ declare function parseError(err: unknown): Frame[];
|
|
|
517
561
|
/** Small dependency-free helpers shared across the SDK. */
|
|
518
562
|
/** SDK identity, embedded in every envelope header. */
|
|
519
563
|
declare const SDK_NAME = "sauron.javascript";
|
|
520
|
-
declare const SDK_VERSION = "1.4.
|
|
564
|
+
declare const SDK_VERSION = "1.4.1";
|
|
521
565
|
|
|
522
566
|
/**
|
|
523
567
|
* `@edraj/sauron-browser` — public API surface.
|
|
@@ -540,7 +584,16 @@ declare function captureException(err: unknown, hint?: Hint): void;
|
|
|
540
584
|
declare function captureMessage(message: string, level?: Level, hint?: Hint): void;
|
|
541
585
|
/** Record a product-analytics event, optionally with per-call tags/contexts/extra. */
|
|
542
586
|
declare function track(name: string, properties?: Record<string, unknown>, options?: TrackOptions): void;
|
|
543
|
-
/**
|
|
587
|
+
/**
|
|
588
|
+
* Associate the session with a known user.
|
|
589
|
+
*
|
|
590
|
+
* The `anonymous_id` sent with the identify item is the current anon id — but
|
|
591
|
+
* only when it was actually used as a `distinct_id` this session, and never
|
|
592
|
+
* when it belongs to a different person than the last one who identified on
|
|
593
|
+
* this device. In that case a fresh anon id is minted first and `null` is
|
|
594
|
+
* sent instead, since the old one is already permanently bound to the
|
|
595
|
+
* previous person server-side (see `reset()`).
|
|
596
|
+
*/
|
|
544
597
|
declare function identify(id: string, traits?: Record<string, unknown>): void;
|
|
545
598
|
/** Record a performance transaction (navigation, http, screen load, ...). */
|
|
546
599
|
declare function trackTransaction(input: TransactionInput): void;
|
|
@@ -570,14 +623,15 @@ declare function addBreadcrumb(breadcrumb: BreadcrumbInput, hint?: Hint): void;
|
|
|
570
623
|
/**
|
|
571
624
|
* Set (or clear, with `null`) the current user.
|
|
572
625
|
*
|
|
573
|
-
* `setUser(null)` is a logout, so it also
|
|
626
|
+
* `setUser(null)` is a logout, so it also calls `reset()` for you — otherwise
|
|
574
627
|
* the next anonymous visitor on this browser inherits the previous person's
|
|
575
628
|
* durable id and a later identify() aliases them together server-side.
|
|
576
629
|
*/
|
|
577
630
|
declare function setUser(user: UserInput): void;
|
|
578
631
|
/**
|
|
579
|
-
* Forget the current person: clears the scope user
|
|
580
|
-
* id
|
|
632
|
+
* Forget the current person: clears the scope user, mints a fresh anonymous
|
|
633
|
+
* id, forgets the last identified user, and rotates the session id so a
|
|
634
|
+
* single session can never span two different people. Call this on logout.
|
|
581
635
|
*/
|
|
582
636
|
declare function reset(): void;
|
|
583
637
|
/** Set a single scope tag (lifted onto later errors/events). */
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4
4
|
|
|
5
5
|
// src/utils.ts
|
|
6
6
|
var SDK_NAME = "sauron.javascript";
|
|
7
|
-
var SDK_VERSION = "1.4.
|
|
7
|
+
var SDK_VERSION = "1.4.1";
|
|
8
8
|
function getGlobal() {
|
|
9
9
|
return globalThis;
|
|
10
10
|
}
|
|
@@ -88,6 +88,29 @@ function makeLogger(debug) {
|
|
|
88
88
|
var DEVICE_ID_KEY = "sauron.device_id";
|
|
89
89
|
var SESSION_ID_KEY = "sauron.session_id";
|
|
90
90
|
var ANON_ID_KEY = "sauron.anon_id";
|
|
91
|
+
var LAST_IDENTIFIED_KEY = "sauron.last_identified";
|
|
92
|
+
var LAST_IDENTIFIED_FORMAT = "v1";
|
|
93
|
+
function encodeLastIdentified(digest) {
|
|
94
|
+
return `${LAST_IDENTIFIED_FORMAT}:${digest}`;
|
|
95
|
+
}
|
|
96
|
+
function decodeLastIdentified(raw) {
|
|
97
|
+
if (raw === null) return null;
|
|
98
|
+
const sep = raw.indexOf(":");
|
|
99
|
+
if (sep < 0 || raw.slice(0, sep) !== LAST_IDENTIFIED_FORMAT) return null;
|
|
100
|
+
const digest = raw.slice(sep + 1);
|
|
101
|
+
return digest === "" ? null : digest;
|
|
102
|
+
}
|
|
103
|
+
function fnv1a32(s) {
|
|
104
|
+
let h = 2166136261;
|
|
105
|
+
for (let i = 0; i < s.length; i++) {
|
|
106
|
+
h ^= s.charCodeAt(i);
|
|
107
|
+
h = Math.imul(h, 16777619);
|
|
108
|
+
}
|
|
109
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
110
|
+
}
|
|
111
|
+
function hashIdentity(id) {
|
|
112
|
+
return fnv1a32(id) + fnv1a32("" + id);
|
|
113
|
+
}
|
|
91
114
|
function webStorage(name) {
|
|
92
115
|
try {
|
|
93
116
|
const s = globalThis[name];
|
|
@@ -121,6 +144,7 @@ function persistentId(cached, storage, key) {
|
|
|
121
144
|
var deviceId = null;
|
|
122
145
|
var sessionId = null;
|
|
123
146
|
var anonymousId = null;
|
|
147
|
+
var lastIdentified = null;
|
|
124
148
|
function getDeviceId() {
|
|
125
149
|
deviceId = persistentId(deviceId, webStorage("localStorage"), DEVICE_ID_KEY);
|
|
126
150
|
return deviceId;
|
|
@@ -129,6 +153,17 @@ function getSessionId() {
|
|
|
129
153
|
sessionId = persistentId(sessionId, webStorage("sessionStorage"), SESSION_ID_KEY);
|
|
130
154
|
return sessionId;
|
|
131
155
|
}
|
|
156
|
+
function rotateSessionId() {
|
|
157
|
+
sessionId = null;
|
|
158
|
+
const storage = webStorage("sessionStorage");
|
|
159
|
+
if (storage) {
|
|
160
|
+
try {
|
|
161
|
+
storage.removeItem(SESSION_ID_KEY);
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return getSessionId();
|
|
166
|
+
}
|
|
132
167
|
function getAnonymousId() {
|
|
133
168
|
if (anonymousId) return anonymousId;
|
|
134
169
|
const storage = webStorage("localStorage");
|
|
@@ -163,6 +198,37 @@ function resetAnonymousId() {
|
|
|
163
198
|
}
|
|
164
199
|
return getAnonymousId();
|
|
165
200
|
}
|
|
201
|
+
function getLastIdentified() {
|
|
202
|
+
const storage = webStorage("localStorage");
|
|
203
|
+
if (!storage) return decodeLastIdentified(lastIdentified);
|
|
204
|
+
try {
|
|
205
|
+
const stored = storage.getItem(LAST_IDENTIFIED_KEY);
|
|
206
|
+
return decodeLastIdentified(stored ?? lastIdentified);
|
|
207
|
+
} catch {
|
|
208
|
+
return decodeLastIdentified(lastIdentified);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function setLastIdentified(id) {
|
|
212
|
+
const encoded = encodeLastIdentified(id);
|
|
213
|
+
lastIdentified = encoded;
|
|
214
|
+
const storage = webStorage("localStorage");
|
|
215
|
+
if (storage) {
|
|
216
|
+
try {
|
|
217
|
+
storage.setItem(LAST_IDENTIFIED_KEY, encoded);
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function clearLastIdentified() {
|
|
223
|
+
lastIdentified = null;
|
|
224
|
+
const storage = webStorage("localStorage");
|
|
225
|
+
if (storage) {
|
|
226
|
+
try {
|
|
227
|
+
storage.removeItem(LAST_IDENTIFIED_KEY);
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
166
232
|
|
|
167
233
|
// src/context.ts
|
|
168
234
|
function getNavigator() {
|
|
@@ -809,13 +875,31 @@ var Scope = class {
|
|
|
809
875
|
this.maxBreadcrumbs = Math.max(0, max);
|
|
810
876
|
this.trim();
|
|
811
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Replace the scope user.
|
|
880
|
+
*
|
|
881
|
+
* `id` is coerced with `String()` for the same reason `SauronClient.
|
|
882
|
+
* prepareIdentify` coerces its own — a plain-JS caller can (and does) pass
|
|
883
|
+
* `setUser({ id: user.id })` where `user.id` is a number, and TypeScript
|
|
884
|
+
* cannot stop them. This path is the one that BYPASSES `identify()`'s
|
|
885
|
+
* coercion entirely, and the consequence is not cosmetic: the scope user
|
|
886
|
+
* lands in the envelope context, where the server's `distinct_id` is a
|
|
887
|
+
* non-`Option` Rust `String`. A JSON number there fails deserialization of
|
|
888
|
+
* the ENVELOPE, not of the one field — so the whole batch 400s, and a 400
|
|
889
|
+
* is non-retryable, so every event in it is dropped for good.
|
|
890
|
+
*
|
|
891
|
+
* Rebuilding the whole object (rather than merging into the existing one)
|
|
892
|
+
* is deliberate and is the behaviour the Flutter SDK was fixed to match:
|
|
893
|
+
* `email` and `traits` come from the input alone, so setting a new user
|
|
894
|
+
* never inherits the previous person's contact details.
|
|
895
|
+
*/
|
|
812
896
|
setUser(user) {
|
|
813
897
|
if (user === null) {
|
|
814
898
|
this.user = null;
|
|
815
899
|
return;
|
|
816
900
|
}
|
|
817
901
|
this.user = {
|
|
818
|
-
id: user.id
|
|
902
|
+
id: user.id === null || user.id === void 0 ? null : String(user.id),
|
|
819
903
|
email: user.email ?? null,
|
|
820
904
|
traits: user.traits ?? {}
|
|
821
905
|
};
|
|
@@ -916,11 +1000,12 @@ function setScreen(name) {
|
|
|
916
1000
|
function identify(id, traits = {}) {
|
|
917
1001
|
const client = getClient();
|
|
918
1002
|
if (!client) return;
|
|
919
|
-
const
|
|
920
|
-
client.
|
|
1003
|
+
const distinctId = String(id);
|
|
1004
|
+
const anonymousId2 = client.prepareIdentify(distinctId);
|
|
1005
|
+
client.getScope().setUser({ id: distinctId, traits });
|
|
921
1006
|
const item = {
|
|
922
1007
|
type: "identify",
|
|
923
|
-
distinct_id:
|
|
1008
|
+
distinct_id: distinctId,
|
|
924
1009
|
anonymous_id: anonymousId2,
|
|
925
1010
|
traits: traits ?? {}
|
|
926
1011
|
};
|
|
@@ -1850,18 +1935,56 @@ var SauronClient = class {
|
|
|
1850
1935
|
return this.anonUsed ? getAnonymousId() : null;
|
|
1851
1936
|
}
|
|
1852
1937
|
/**
|
|
1853
|
-
* Forget the current person: clear the scope user
|
|
1854
|
-
* id.
|
|
1938
|
+
* Forget the current person: clear the scope user, mint a fresh anonymous
|
|
1939
|
+
* id, forget the last identified user, and rotate the session id.
|
|
1855
1940
|
*
|
|
1856
1941
|
* MUST BE CALLED ON LOGOUT. Without it, the next anonymous visitor on this
|
|
1857
1942
|
* browser reuses the persisted anon id, and a later identify() aliases their
|
|
1858
|
-
* activity to the previous account server-side, permanently.
|
|
1943
|
+
* activity to the previous account server-side, permanently. Rotating the
|
|
1944
|
+
* session id matters too: the server's `bump_session` is last-write-wins on
|
|
1945
|
+
* `distinct_id`, so without rotation one `sessions` row could otherwise
|
|
1946
|
+
* serially represent two different people and record only whichever wrote
|
|
1947
|
+
* last.
|
|
1859
1948
|
*/
|
|
1860
1949
|
reset() {
|
|
1861
1950
|
this.scope.setUser(null);
|
|
1862
1951
|
resetAnonymousId();
|
|
1952
|
+
clearLastIdentified();
|
|
1953
|
+
rotateSessionId();
|
|
1863
1954
|
this.anonUsed = false;
|
|
1864
1955
|
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Prepare for an `identify()`; returns the `anonymous_id` to send.
|
|
1958
|
+
*
|
|
1959
|
+
* When a DIFFERENT user identifies than last time, the current anon id
|
|
1960
|
+
* belongs to the previous person and is already burned server-side, so it is
|
|
1961
|
+
* replaced before anything else happens and `null` is sent instead of a
|
|
1962
|
+
* cross-user alias. This cannot repair events already sent under the burned
|
|
1963
|
+
* alias — nothing can — but it bounds a forgotten `reset()` to one guest
|
|
1964
|
+
* window instead of every future one.
|
|
1965
|
+
*
|
|
1966
|
+
* `id` is coerced with `String()` before comparing/persisting: a plain-JS
|
|
1967
|
+
* caller can pass a number (`Sauron.identify(user.id)`), and `Storage`
|
|
1968
|
+
* itself applies `ToString` on write — so comparing an un-coerced `id`
|
|
1969
|
+
* against a value that already round-tripped through storage would treat
|
|
1970
|
+
* the SAME numeric user as a switch on every single call. The comparison
|
|
1971
|
+
* against `last` is an explicit `!== null` (not a truthiness check) so an
|
|
1972
|
+
* app that (unusually) identifies with `''` still has a later, different id
|
|
1973
|
+
* correctly detected as a real switch — a falsy string is not "no identity
|
|
1974
|
+
* yet". `last`/the persisted value are digests, not the raw id — see
|
|
1975
|
+
* `hashIdentity`.
|
|
1976
|
+
*/
|
|
1977
|
+
prepareIdentify(id) {
|
|
1978
|
+
const digest = hashIdentity(String(id));
|
|
1979
|
+
const last = getLastIdentified();
|
|
1980
|
+
if (last !== null && last !== digest) {
|
|
1981
|
+
resetAnonymousId();
|
|
1982
|
+
rotateSessionId();
|
|
1983
|
+
this.anonUsed = false;
|
|
1984
|
+
}
|
|
1985
|
+
setLastIdentified(digest);
|
|
1986
|
+
return this.getAnonymousId();
|
|
1987
|
+
}
|
|
1865
1988
|
/** Stamp a fresh envelope (new `sent_at`, current context) around `items`. */
|
|
1866
1989
|
makeEnvelope(items) {
|
|
1867
1990
|
const header = {
|