@edraj/sauron-browser 1.3.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 +87 -0
- package/README.md +77 -14
- package/dist/index.cjs +231 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -11
- package/dist/index.d.ts +108 -11
- package/dist/index.js +231 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
7
|
+
var SDK_VERSION = "1.4.1";
|
|
8
8
|
function getGlobal() {
|
|
9
9
|
return globalThis;
|
|
10
10
|
}
|
|
@@ -87,6 +87,30 @@ function makeLogger(debug) {
|
|
|
87
87
|
// src/identity.ts
|
|
88
88
|
var DEVICE_ID_KEY = "sauron.device_id";
|
|
89
89
|
var SESSION_ID_KEY = "sauron.session_id";
|
|
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
|
+
}
|
|
90
114
|
function webStorage(name) {
|
|
91
115
|
try {
|
|
92
116
|
const s = globalThis[name];
|
|
@@ -119,6 +143,8 @@ function persistentId(cached, storage, key) {
|
|
|
119
143
|
}
|
|
120
144
|
var deviceId = null;
|
|
121
145
|
var sessionId = null;
|
|
146
|
+
var anonymousId = null;
|
|
147
|
+
var lastIdentified = null;
|
|
122
148
|
function getDeviceId() {
|
|
123
149
|
deviceId = persistentId(deviceId, webStorage("localStorage"), DEVICE_ID_KEY);
|
|
124
150
|
return deviceId;
|
|
@@ -127,6 +153,82 @@ function getSessionId() {
|
|
|
127
153
|
sessionId = persistentId(sessionId, webStorage("sessionStorage"), SESSION_ID_KEY);
|
|
128
154
|
return sessionId;
|
|
129
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
|
+
}
|
|
167
|
+
function getAnonymousId() {
|
|
168
|
+
if (anonymousId) return anonymousId;
|
|
169
|
+
const storage = webStorage("localStorage");
|
|
170
|
+
if (storage) {
|
|
171
|
+
try {
|
|
172
|
+
const existing = storage.getItem(ANON_ID_KEY);
|
|
173
|
+
if (existing) {
|
|
174
|
+
anonymousId = existing;
|
|
175
|
+
return anonymousId;
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const fresh = `anon_${uuidv4()}`;
|
|
181
|
+
if (storage) {
|
|
182
|
+
try {
|
|
183
|
+
storage.setItem(ANON_ID_KEY, fresh);
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
anonymousId = fresh;
|
|
188
|
+
return anonymousId;
|
|
189
|
+
}
|
|
190
|
+
function resetAnonymousId() {
|
|
191
|
+
anonymousId = null;
|
|
192
|
+
const storage = webStorage("localStorage");
|
|
193
|
+
if (storage) {
|
|
194
|
+
try {
|
|
195
|
+
storage.removeItem(ANON_ID_KEY);
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return getAnonymousId();
|
|
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
|
+
}
|
|
130
232
|
|
|
131
233
|
// src/context.ts
|
|
132
234
|
function getNavigator() {
|
|
@@ -624,12 +726,7 @@ function captureMessage(message, level = "info", hint) {
|
|
|
624
726
|
type: "error",
|
|
625
727
|
timestamp: nowIso(),
|
|
626
728
|
level,
|
|
627
|
-
|
|
628
|
-
type: null,
|
|
629
|
-
value: message,
|
|
630
|
-
mechanism: { type: "message", handled: true },
|
|
631
|
-
stacktrace: []
|
|
632
|
-
},
|
|
729
|
+
message,
|
|
633
730
|
breadcrumbs,
|
|
634
731
|
fingerprint: hint?.fingerprint ?? null,
|
|
635
732
|
session_id: getSessionId(),
|
|
@@ -778,13 +875,31 @@ var Scope = class {
|
|
|
778
875
|
this.maxBreadcrumbs = Math.max(0, max);
|
|
779
876
|
this.trim();
|
|
780
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
|
+
*/
|
|
781
896
|
setUser(user) {
|
|
782
897
|
if (user === null) {
|
|
783
898
|
this.user = null;
|
|
784
899
|
return;
|
|
785
900
|
}
|
|
786
901
|
this.user = {
|
|
787
|
-
id: user.id
|
|
902
|
+
id: user.id === null || user.id === void 0 ? null : String(user.id),
|
|
788
903
|
email: user.email ?? null,
|
|
789
904
|
traits: user.traits ?? {}
|
|
790
905
|
};
|
|
@@ -885,12 +1000,13 @@ function setScreen(name) {
|
|
|
885
1000
|
function identify(id, traits = {}) {
|
|
886
1001
|
const client = getClient();
|
|
887
1002
|
if (!client) return;
|
|
888
|
-
const
|
|
889
|
-
client.
|
|
1003
|
+
const distinctId = String(id);
|
|
1004
|
+
const anonymousId2 = client.prepareIdentify(distinctId);
|
|
1005
|
+
client.getScope().setUser({ id: distinctId, traits });
|
|
890
1006
|
const item = {
|
|
891
1007
|
type: "identify",
|
|
892
|
-
distinct_id:
|
|
893
|
-
anonymous_id:
|
|
1008
|
+
distinct_id: distinctId,
|
|
1009
|
+
anonymous_id: anonymousId2,
|
|
894
1010
|
traits: traits ?? {}
|
|
895
1011
|
};
|
|
896
1012
|
client.captureItem(item);
|
|
@@ -1395,6 +1511,21 @@ var OfflineQueue = class {
|
|
|
1395
1511
|
if (entries.length) this.write([]);
|
|
1396
1512
|
return entries;
|
|
1397
1513
|
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Put drained payloads BACK at the head, keeping their relative order.
|
|
1516
|
+
*
|
|
1517
|
+
* The counterpart to {@link drain}: a drain empties the store immediately, so
|
|
1518
|
+
* whatever the caller could not deliver only exists in its local array and is
|
|
1519
|
+
* lost the moment the caller returns. Re-parking at the head (rather than via
|
|
1520
|
+
* {@link enqueue}) keeps the queue oldest-first, which is what the byte-cap
|
|
1521
|
+
* eviction policy assumes — the oldest entry must stay the first one evicted.
|
|
1522
|
+
*/
|
|
1523
|
+
requeueFront(payloads) {
|
|
1524
|
+
if (!this.storage || payloads.length === 0) return;
|
|
1525
|
+
const entries = [...payloads, ...this.read()];
|
|
1526
|
+
this.evict(entries);
|
|
1527
|
+
this.write(entries);
|
|
1528
|
+
}
|
|
1398
1529
|
/** Non-destructive read of the current entries, oldest first. */
|
|
1399
1530
|
peek() {
|
|
1400
1531
|
return this.read();
|
|
@@ -1580,11 +1711,21 @@ var Transport = class {
|
|
|
1580
1711
|
}
|
|
1581
1712
|
}
|
|
1582
1713
|
}
|
|
1583
|
-
/**
|
|
1714
|
+
/**
|
|
1715
|
+
* Re-attempt any envelopes that were parked while offline.
|
|
1716
|
+
*
|
|
1717
|
+
* `drain()` empties `localStorage` in one shot, so from here on the ONLY copy
|
|
1718
|
+
* of the backlog is the local `payloads` array. Every early return therefore
|
|
1719
|
+
* has to re-park the whole untried remainder, not just the payload that
|
|
1720
|
+
* failed: this used to re-park the failing one and return, which silently
|
|
1721
|
+
* deleted every payload behind it — the exact reconnect-then-one-500 case the
|
|
1722
|
+
* queue exists for.
|
|
1723
|
+
*/
|
|
1584
1724
|
async drainOfflineQueue() {
|
|
1585
1725
|
if (this.disabled || !this.offline.available) return;
|
|
1586
1726
|
const payloads = this.offline.drain();
|
|
1587
|
-
for (
|
|
1727
|
+
for (let i = 0; i < payloads.length; i++) {
|
|
1728
|
+
const json = payloads[i];
|
|
1588
1729
|
let outcome;
|
|
1589
1730
|
try {
|
|
1590
1731
|
outcome = await this.post(json);
|
|
@@ -1592,13 +1733,14 @@ var Transport = class {
|
|
|
1592
1733
|
outcome = { action: "retry_backoff" };
|
|
1593
1734
|
}
|
|
1594
1735
|
if (outcome.action === "disable") {
|
|
1736
|
+
this.logger.warn("server rejected credentials while draining; disabling client");
|
|
1595
1737
|
this.disable();
|
|
1596
1738
|
this.onDisable();
|
|
1597
|
-
this.offline.
|
|
1739
|
+
this.offline.requeueFront(payloads.slice(i));
|
|
1598
1740
|
return;
|
|
1599
1741
|
}
|
|
1600
1742
|
if (outcome.action === "retry_after" || outcome.action === "retry_backoff") {
|
|
1601
|
-
this.offline.
|
|
1743
|
+
this.offline.requeueFront(payloads.slice(i));
|
|
1602
1744
|
return;
|
|
1603
1745
|
}
|
|
1604
1746
|
}
|
|
@@ -1708,8 +1850,17 @@ var SauronClient = class {
|
|
|
1708
1850
|
__publicField(this, "nativeFetch");
|
|
1709
1851
|
__publicField(this, "enabled", true);
|
|
1710
1852
|
__publicField(this, "installed", false);
|
|
1711
|
-
__publicField(this, "anonymousId", null);
|
|
1712
1853
|
__publicField(this, "beaconCleanup", null);
|
|
1854
|
+
/**
|
|
1855
|
+
* Whether the anonymous id has actually been USED as a `distinct_id` in this
|
|
1856
|
+
* browser session.
|
|
1857
|
+
*
|
|
1858
|
+
* A persisted id that has never been observed anonymously must not create a
|
|
1859
|
+
* permanent `identities` alias row on the server: aliasing is a durable
|
|
1860
|
+
* server-side binding of this browser profile to a named user, and an
|
|
1861
|
+
* identify() on a first-ever page load has no anonymous history to link.
|
|
1862
|
+
*/
|
|
1863
|
+
__publicField(this, "anonUsed", false);
|
|
1713
1864
|
this.options = options;
|
|
1714
1865
|
this.dsn = parseDsn(options.dsn);
|
|
1715
1866
|
this.logger = makeLogger(options.debug);
|
|
@@ -1776,15 +1927,63 @@ var SauronClient = class {
|
|
|
1776
1927
|
getDistinctId() {
|
|
1777
1928
|
const user = this.scope.getUser();
|
|
1778
1929
|
if (user.id) return user.id;
|
|
1779
|
-
|
|
1930
|
+
this.anonUsed = true;
|
|
1931
|
+
return getAnonymousId();
|
|
1780
1932
|
}
|
|
1781
|
-
/** The anonymous id, or null
|
|
1933
|
+
/** The anonymous id, or null when it was never actually used as an identity. */
|
|
1782
1934
|
getAnonymousId() {
|
|
1783
|
-
return this.
|
|
1935
|
+
return this.anonUsed ? getAnonymousId() : null;
|
|
1784
1936
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1937
|
+
/**
|
|
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.
|
|
1940
|
+
*
|
|
1941
|
+
* MUST BE CALLED ON LOGOUT. Without it, the next anonymous visitor on this
|
|
1942
|
+
* browser reuses the persisted anon id, and a later identify() aliases their
|
|
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.
|
|
1948
|
+
*/
|
|
1949
|
+
reset() {
|
|
1950
|
+
this.scope.setUser(null);
|
|
1951
|
+
resetAnonymousId();
|
|
1952
|
+
clearLastIdentified();
|
|
1953
|
+
rotateSessionId();
|
|
1954
|
+
this.anonUsed = false;
|
|
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();
|
|
1788
1987
|
}
|
|
1789
1988
|
/** Stamp a fresh envelope (new `sent_at`, current context) around `items`. */
|
|
1790
1989
|
makeEnvelope(items) {
|
|
@@ -2043,8 +2242,15 @@ function addBreadcrumb2(breadcrumb, hint) {
|
|
|
2043
2242
|
addBreadcrumb(breadcrumb, hint);
|
|
2044
2243
|
}
|
|
2045
2244
|
function setUser(user) {
|
|
2245
|
+
if (user === null) {
|
|
2246
|
+
getClient()?.reset();
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2046
2249
|
getClient()?.getScope().setUser(user);
|
|
2047
2250
|
}
|
|
2251
|
+
function reset() {
|
|
2252
|
+
getClient()?.reset();
|
|
2253
|
+
}
|
|
2048
2254
|
function setTag(key, value) {
|
|
2049
2255
|
getClient()?.getScope().setTag(key, value);
|
|
2050
2256
|
}
|
|
@@ -2074,6 +2280,7 @@ var Sauron = {
|
|
|
2074
2280
|
identify: identify2,
|
|
2075
2281
|
addBreadcrumb: addBreadcrumb2,
|
|
2076
2282
|
setUser,
|
|
2283
|
+
reset,
|
|
2077
2284
|
setTag,
|
|
2078
2285
|
setTags,
|
|
2079
2286
|
setContext,
|
|
@@ -2090,6 +2297,6 @@ var Sauron = {
|
|
|
2090
2297
|
};
|
|
2091
2298
|
var index_default = Sauron;
|
|
2092
2299
|
|
|
2093
|
-
export { DsnError, SDK_NAME, SDK_VERSION, Sauron, SauronClient, addBreadcrumb2 as addBreadcrumb, buildEnvelope, cancelWorkflow2 as cancelWorkflow, captureException2 as captureException, captureMessage2 as captureMessage, close, index_default as default, endWorkflow2 as endWorkflow, flush, getClient, getScreen2 as getScreen, getWorkflow2 as getWorkflow, identify2 as identify, init2 as init, isInAppFrame, parseDsn, parseError, parseStackString, setContext, setExtra, setScreen2 as setScreen, setTag, setTags, setUser, startWorkflow2 as startWorkflow, track2 as track, trackTransaction2 as trackTransaction };
|
|
2300
|
+
export { DsnError, SDK_NAME, SDK_VERSION, Sauron, SauronClient, addBreadcrumb2 as addBreadcrumb, buildEnvelope, cancelWorkflow2 as cancelWorkflow, captureException2 as captureException, captureMessage2 as captureMessage, close, index_default as default, endWorkflow2 as endWorkflow, flush, getClient, getScreen2 as getScreen, getWorkflow2 as getWorkflow, identify2 as identify, init2 as init, isInAppFrame, parseDsn, parseError, parseStackString, reset, setContext, setExtra, setScreen2 as setScreen, setTag, setTags, setUser, startWorkflow2 as startWorkflow, track2 as track, trackTransaction2 as trackTransaction };
|
|
2094
2301
|
//# sourceMappingURL=index.js.map
|
|
2095
2302
|
//# sourceMappingURL=index.js.map
|