@loxel.dev/pharos-browser 0.6.0 → 0.8.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/README.md +129 -2
- package/TECHNICAL-DETAILS.md +24 -6
- package/dist/{index-bc4bw3ba.js → index-temc0wzg.js} +118 -8
- package/dist/{wire-992wvzs1.js → index-xs32mrd9.js} +75 -12
- package/dist/index.d.ts +44 -5
- package/dist/index.js +135 -47
- package/dist/privacy/collect.d.ts +11 -0
- package/dist/replay/index.js +9 -2
- package/dist/replay/privacy-hooks.d.ts +2 -0
- package/dist/wire-90whxzan.js +13 -0
- package/package.json +3 -2
- package/dist/index-c3taa3cg.js +0 -9
package/README.md
CHANGED
|
@@ -127,8 +127,10 @@ targeting rules — send nothing if no rule reads it.
|
|
|
127
127
|
|
|
128
128
|
```typescript
|
|
129
129
|
// Synchronous, no network call — the value was computed server-side and is
|
|
130
|
-
// already here. Your default is returned until the bootstrap lands,
|
|
131
|
-
//
|
|
130
|
+
// already here. Your default is returned until the bootstrap lands, for any
|
|
131
|
+
// key Pharos does not know, and for a value whose type disagrees with your
|
|
132
|
+
// default — a flag sent as the string "false" is not a boolean, and reading
|
|
133
|
+
// it through a boolean gate would otherwise turn the gate ON.
|
|
132
134
|
if (client.flag("new-nav", false)) {
|
|
133
135
|
renderNewNav();
|
|
134
136
|
}
|
|
@@ -154,6 +156,13 @@ await client.identify({
|
|
|
154
156
|
});
|
|
155
157
|
```
|
|
156
158
|
|
|
159
|
+
The new user's flags arrive with it, so `identify()` fires `change` the same
|
|
160
|
+
way a server push does — a component that mounted while the user was
|
|
161
|
+
anonymous re-renders against the identified bucket without the app arranging
|
|
162
|
+
anything. If the re-bootstrap fails it still rejects (catch it), and the
|
|
163
|
+
client schedules a reconnect rather than leaving live updates dead until a
|
|
164
|
+
page reload.
|
|
165
|
+
|
|
157
166
|
### Shut down
|
|
158
167
|
|
|
159
168
|
```typescript
|
|
@@ -318,6 +327,124 @@ you can verify what a recorder would send before turning one on.
|
|
|
318
327
|
|
|
319
328
|
## Version history
|
|
320
329
|
|
|
330
|
+
- `0.8.0` — **`identify()` tells you the flags changed, and a failed one no
|
|
331
|
+
longer kills live updates** (#788).
|
|
332
|
+
|
|
333
|
+
`identify()` swaps the whole flags map and emitted nothing, so a component
|
|
334
|
+
that rendered against the anonymous bootstrap kept that value for the life
|
|
335
|
+
of its mount while the server evaluated the real user's bucket on every
|
|
336
|
+
request. It now emits `change` with the new payload. If its bootstrap
|
|
337
|
+
rejects it still rejects — an app awaiting it on login has to see that —
|
|
338
|
+
but it now also schedules a reconnect, where before the stream was left
|
|
339
|
+
dead with nothing pending and nothing reported. Reconnects back off
|
|
340
|
+
exponentially (1 s doubling to a 30 s ceiling, reset by a delivered `flags`
|
|
341
|
+
push) instead of re-bootstrapping every second for the length of an outage.
|
|
342
|
+
|
|
343
|
+
`flag(key, default)` now **returns your default when the stored value's
|
|
344
|
+
type disagrees with it**. It was a cast (`as T`), so a flag delivered as the
|
|
345
|
+
string `"false"` read as ON through a boolean gate. `null`/`undefined`
|
|
346
|
+
defaults opt out of the check, since `typeof` describes the absence of a
|
|
347
|
+
value for both.
|
|
348
|
+
|
|
349
|
+
**The replay sensitive-field word list is no longer English-only** (#823).
|
|
350
|
+
It now carries the common Spanish, French, German, Portuguese, Japanese and
|
|
351
|
+
Chinese spellings of the same fields, and normalization folds accents
|
|
352
|
+
instead of deleting them (`contraseña` → `contrasena`, `Straße` →
|
|
353
|
+
`strasse`) — without which every accented entry would have missed by a
|
|
354
|
+
character. A localised app previously got essentially nothing from this
|
|
355
|
+
layer. Annotating `autocomplete` is still the recommended integration step:
|
|
356
|
+
it is language-independent and is checked first.
|
|
357
|
+
|
|
358
|
+
**Why MINOR:** `change` now fires on a path where it did not, a
|
|
359
|
+
type-mismatched flag stops being returned, and a localised app's fields are
|
|
360
|
+
masked where they were recorded — so a pinned `collectRecordedStrings`
|
|
361
|
+
snapshot will differ. Every move is in the safe direction, but a consumer
|
|
362
|
+
sees different behaviour either way.
|
|
363
|
+
|
|
364
|
+
- `0.7.0` — **a marker on a shadow host now protects its shadow root** (#821),
|
|
365
|
+
and the verification helper sees what the recorder sees (#822).
|
|
366
|
+
|
|
367
|
+
`hasMarkerAncestor` walked `parentElement`, which is `null` at a shadow
|
|
368
|
+
boundary, so `pharos-mask`/`pharos-exclude` on a web component's host
|
|
369
|
+
protected nothing inside it — while rrweb recorded that content regardless.
|
|
370
|
+
Established empirically before the fix: rrweb captures shadow-root text
|
|
371
|
+
unconditionally (no recorder option gates it), and `maskTextHook` *is*
|
|
372
|
+
invoked for those nodes, so the decision function was the sole failure
|
|
373
|
+
point. The walk now re-roots through `getRootNode()`/`.host`, bounded
|
|
374
|
+
against a malformed tree.
|
|
375
|
+
|
|
376
|
+
`collectRecordedStrings` did not traverse shadow DOM either. That was not a
|
|
377
|
+
compensating margin — **the auditor's blind spot lined up exactly with the
|
|
378
|
+
recorder's leak surface**, so an app could run the helper, get a clean
|
|
379
|
+
report, and be shipping shadow-root content. It now traverses, and
|
|
380
|
+
`pathOf()` renders a boundary crossing as `>>>` rather than truncating the
|
|
381
|
+
path to `''` (which was indistinguishable from a root-level row).
|
|
382
|
+
|
|
383
|
+
**Why MINOR, not PATCH:** two consumer-visible outputs change. Content
|
|
384
|
+
inside a marked shadow host is now masked where it previously shipped in
|
|
385
|
+
the clear, and `collectRecordedStrings` reports strings it previously
|
|
386
|
+
omitted — so a pinned audit snapshot will differ. Both changes are in the
|
|
387
|
+
safe direction, but a consumer sees different output either way.
|
|
388
|
+
|
|
389
|
+
`STRUCTURAL_ATTRS` is unchanged in membership and is now derived from a
|
|
390
|
+
justification map: each entry declares a `kind` — `closed-enumeration`
|
|
391
|
+
(which must name its finite vocabulary) or `cost-tradeoff` (which must
|
|
392
|
+
carry `meetsImpossibilityBar: false`). `class` is recorded honestly as the
|
|
393
|
+
second kind. The gate cannot verify a claim's truth; that stays human
|
|
394
|
+
review.
|
|
395
|
+
|
|
396
|
+
**The first round of that fix only reached the FULL SNAPSHOT.** rrweb
|
|
397
|
+
decides whether to consult `maskTextFn` in `needMaskingText`, which returns
|
|
398
|
+
"nothing to mask" for any node that is not an element and has no parent
|
|
399
|
+
*element* — and a text node whose parent is a `ShadowRoot` is exactly that.
|
|
400
|
+
So on the **incremental** streams (`characterData`, `childList` adds) the
|
|
401
|
+
decision function was never called at all and a **marked** host still
|
|
402
|
+
shipped `shadowRoot.textContent = user.name` in the clear once recording had
|
|
403
|
+
started. An element-wrapped `<p>` inside the same shadow root did not
|
|
404
|
+
reproduce it (rrweb takes its element branch there), which is how it
|
|
405
|
+
shipped. The recorder now post-processes both streams through the same
|
|
406
|
+
policy, and applies it over rrweb's answer in the snapshot too — in the
|
|
407
|
+
masking direction only, never unmasking.
|
|
408
|
+
|
|
409
|
+
**`pharos-exclude` across a shadow boundary shipped attributes.** rrweb's
|
|
410
|
+
`isBlocked` uses `closest()`, which does not cross a boundary, so an add
|
|
411
|
+
into an excluded host's shadow root was not dropped — and the scrub pass
|
|
412
|
+
judges a serialized node through a detached surrogate that by construction
|
|
413
|
+
has no host and no ancestry. Measured: `<p title="SECRET">TEXT</p>` appended
|
|
414
|
+
into an excluded host's shadow root shipped its `title` verbatim while its
|
|
415
|
+
text was masked. Such adds are now dropped, judged against the **live**
|
|
416
|
+
parent the mutation names by id.
|
|
417
|
+
|
|
418
|
+
**Snapshot noise to expect on your first re-run:** shadow roots almost
|
|
419
|
+
always contain a `<style>`, and `collectRecordedStrings` now reaches it and
|
|
420
|
+
reports its CSS text as an ordinary text row. That is not a leak — author
|
|
421
|
+
CSS is not user content — but a pinned audit snapshot will grow by more than
|
|
422
|
+
the app's own strings.
|
|
423
|
+
|
|
424
|
+
**Still not covered:**
|
|
425
|
+
- **Closed shadow roots**, which rrweb cannot reach either (`n.shadowRoot`
|
|
426
|
+
is `null`), so neither the recorder nor the helper sees them.
|
|
427
|
+
- **The full snapshot's own answer for a bare shadow text node is rrweb's,
|
|
428
|
+
not the policy's**, and it flips on the host's shape:
|
|
429
|
+
`needMaskingText` returns `false` for an element with no child nodes, so
|
|
430
|
+
whether a shadow subtree inherits masking depends on whether the host
|
|
431
|
+
happens to have light-DOM children. The policy is now applied over the
|
|
432
|
+
top, but only ever to mask MORE — so an **unmarked** host's bare shadow
|
|
433
|
+
text can be masked in the snapshot while `collectRecordedStrings` reports
|
|
434
|
+
it. The report is the truthful one: the recorder ships that string
|
|
435
|
+
verbatim on the incremental stream.
|
|
436
|
+
- **An excluded host's shadow subtree can still leave structural traces.**
|
|
437
|
+
Content is dropped, but rrweb's mirror holds nodes this SDK removed from
|
|
438
|
+
the `adds` stream, so a later `removes` entry can carry their ids. Ids and
|
|
439
|
+
tree shape, never text or attributes.
|
|
440
|
+
- Light-DOM content projected through a native `<slot>` is **no longer**
|
|
441
|
+
on this list: it was measured and is covered. A slotted node is an
|
|
442
|
+
ordinary light-DOM child of the host — rrweb serializes it under the host
|
|
443
|
+
and the helper reaches it via `el.children` — so the two agree with no
|
|
444
|
+
slot-specific handling, and a marker on the host governs it through
|
|
445
|
+
ordinary light-DOM ancestry. There is a test for it so it does not have to
|
|
446
|
+
be re-derived.
|
|
447
|
+
|
|
321
448
|
- `0.6.0` — **first npm release, as `@loxel.dev/pharos-browser`.** Ships built
|
|
322
449
|
output with an `exports` map, so it resolves as an ordinary package.
|
|
323
450
|
|
package/TECHNICAL-DETAILS.md
CHANGED
|
@@ -185,14 +185,23 @@ documented in full in
|
|
|
185
185
|
- `flag<T>(key, defaultValue): T` — synchronous map lookup, no network call,
|
|
186
186
|
no evaluation. Returns `defaultValue` for any key not present in the
|
|
187
187
|
current flags map (unknown key, or a `server_side`-only flag the bootstrap
|
|
188
|
-
endpoint never sent)
|
|
188
|
+
endpoint never sent), and for a stored value whose `typeof` disagrees with
|
|
189
|
+
`defaultValue`'s — the string `"false"` is not a boolean and must not read
|
|
190
|
+
as one. A `null`/`undefined` default opts out of that check, because
|
|
191
|
+
`typeof` describes the absence of a value for both and checking against it
|
|
192
|
+
would return the default unconditionally.
|
|
189
193
|
- `on(event, cb): () => void` — subscribe to `"change"` (a new flags payload
|
|
190
194
|
was applied — fires exactly once per accepted server push) or `"error"`
|
|
191
195
|
(stream/bootstrap failure). Returns an unsubscribe function.
|
|
192
196
|
- `identify(context): Promise<void>` — re-bootstraps as a new context and
|
|
193
197
|
reopens the stream against the fresh `streamToken`. **This is a full HTTP
|
|
194
198
|
round trip, not a local re-key** — call it on meaningful identity changes
|
|
195
|
-
(login, plan change), not per-render.
|
|
199
|
+
(login, plan change), not per-render. It **fires `"change"`** with the new
|
|
200
|
+
`{version, flags}` once the stream is back up: it replaced the flags map,
|
|
201
|
+
and a consumer that only watched `"change"` would otherwise render the
|
|
202
|
+
previous context's values for the life of its mount. A rejected bootstrap
|
|
203
|
+
**schedules a reconnect** as well as rejecting, so a failure here costs the
|
|
204
|
+
call, not the session's live updates.
|
|
196
205
|
- `captureException(err, opts?): void` — reports one exception; see
|
|
197
206
|
"Error reporting" above. `opts` is `{site?: string, attributes?: Record<string, unknown>}`.
|
|
198
207
|
Never throws; a failed POST is reported via `on("error")`. A no-op after
|
|
@@ -295,10 +304,19 @@ own. The one case it cannot recover from is an **expired stream session**
|
|
|
295
304
|
(the server holds a 10-minute token TTL, refreshed only at connect): the
|
|
296
305
|
browser sees repeated errors and keeps retrying the same dead token forever.
|
|
297
306
|
When `EventSource` settles into its terminal `CLOSED` state, this client
|
|
298
|
-
re-bootstraps
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
307
|
+
re-bootstraps to mint a fresh token and reconnect. The same schedule covers a
|
|
308
|
+
rejected `identify()`, which is otherwise the one way to end up with no
|
|
309
|
+
stream and no `EventSource` left to report an error.
|
|
310
|
+
|
|
311
|
+
Retries **back off exponentially**: 1 s, doubling to a 30 s ceiling. It is a
|
|
312
|
+
delay cap rather than an attempt cap — giving up entirely would leave a tab
|
|
313
|
+
permanently stale after an outage it survived — and the ramp is reset by a
|
|
314
|
+
**delivered `flags` push**, not by a successful bootstrap, so a stream that
|
|
315
|
+
opens and dies before sending anything keeps backing off instead of flapping
|
|
316
|
+
at the floor delay. Against a Pharos that is present-but-down this is the
|
|
317
|
+
difference between one request per second per open tab and two per minute.
|
|
318
|
+
There is still no queue and no jitter; `flag()` keeps serving the last-known
|
|
319
|
+
values throughout, and the next successful reconnect resyncs from scratch.
|
|
302
320
|
|
|
303
321
|
Only payloads with `version >= current` are applied; stale (older-version)
|
|
304
322
|
pushes are ignored. An equal-version push (e.g. the replay a fresh stream
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined")
|
|
5
|
+
return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
1
9
|
// src/privacy/mask.ts
|
|
2
10
|
var MASK_TOKEN = "••••";
|
|
3
11
|
function maskText(_text) {
|
|
@@ -7,12 +15,23 @@ function maskText(_text) {
|
|
|
7
15
|
// src/privacy/markers.ts
|
|
8
16
|
var EXCLUDE_CLASS = "pharos-exclude";
|
|
9
17
|
var MASK_CLASS = "pharos-mask";
|
|
18
|
+
var MAX_HOPS = 1000;
|
|
10
19
|
function hasMarkerAncestor(el, cls) {
|
|
11
20
|
let node = el;
|
|
21
|
+
let hops = 0;
|
|
12
22
|
while (node) {
|
|
13
23
|
if (node.classList?.contains(cls))
|
|
14
24
|
return true;
|
|
15
|
-
|
|
25
|
+
const parentEl = node.parentElement;
|
|
26
|
+
if (parentEl) {
|
|
27
|
+
node = parentEl;
|
|
28
|
+
} else {
|
|
29
|
+
const root = node.getRootNode();
|
|
30
|
+
node = typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot ? root.host : null;
|
|
31
|
+
}
|
|
32
|
+
hops += 1;
|
|
33
|
+
if (hops > MAX_HOPS)
|
|
34
|
+
return true;
|
|
16
35
|
}
|
|
17
36
|
return false;
|
|
18
37
|
}
|
|
@@ -89,11 +108,101 @@ var SENSITIVE_WORDS = [
|
|
|
89
108
|
"cvc",
|
|
90
109
|
"iban",
|
|
91
110
|
"passport",
|
|
92
|
-
"driverslicense"
|
|
111
|
+
"driverslicense",
|
|
112
|
+
"contrasena",
|
|
113
|
+
"contrasenya",
|
|
114
|
+
"usuario",
|
|
115
|
+
"secreto",
|
|
116
|
+
"correo",
|
|
117
|
+
"movil",
|
|
118
|
+
"nombre",
|
|
119
|
+
"apellido",
|
|
120
|
+
"direccion",
|
|
121
|
+
"codigopostal",
|
|
122
|
+
"nacimiento",
|
|
123
|
+
"cumpleanos",
|
|
124
|
+
"dni",
|
|
125
|
+
"seguridadsocial",
|
|
126
|
+
"tarjeta",
|
|
127
|
+
"motdepasse",
|
|
128
|
+
"utilisateur",
|
|
129
|
+
"identifiant",
|
|
130
|
+
"courriel",
|
|
131
|
+
"prenom",
|
|
132
|
+
"nomdefamille",
|
|
133
|
+
"adresse",
|
|
134
|
+
"codepostal",
|
|
135
|
+
"naissance",
|
|
136
|
+
"anniversaire",
|
|
137
|
+
"securitesociale",
|
|
138
|
+
"carteidentite",
|
|
139
|
+
"permisconduire",
|
|
140
|
+
"passwort",
|
|
141
|
+
"kennwort",
|
|
142
|
+
"benutzer",
|
|
143
|
+
"benutzername",
|
|
144
|
+
"vorname",
|
|
145
|
+
"nachname",
|
|
146
|
+
"familienname",
|
|
147
|
+
"anschrift",
|
|
148
|
+
"strasse",
|
|
149
|
+
"postleitzahl",
|
|
150
|
+
"geburt",
|
|
151
|
+
"kreditkarte",
|
|
152
|
+
"kontonummer",
|
|
153
|
+
"steuernummer",
|
|
154
|
+
"sozialversicherung",
|
|
155
|
+
"ausweis",
|
|
156
|
+
"senha",
|
|
157
|
+
"palavrapasse",
|
|
158
|
+
"correio",
|
|
159
|
+
"sobrenome",
|
|
160
|
+
"nomecompleto",
|
|
161
|
+
"endereco",
|
|
162
|
+
"nascimento",
|
|
163
|
+
"cartao",
|
|
164
|
+
"cpf",
|
|
165
|
+
"cnpj",
|
|
166
|
+
"パスワード",
|
|
167
|
+
"メール",
|
|
168
|
+
"電話",
|
|
169
|
+
"氏名",
|
|
170
|
+
"名前",
|
|
171
|
+
"住所",
|
|
172
|
+
"生年月日",
|
|
173
|
+
"誕生日",
|
|
174
|
+
"郵便番号",
|
|
175
|
+
"クレジットカード",
|
|
176
|
+
"マイナンバー",
|
|
177
|
+
"密码",
|
|
178
|
+
"密碼",
|
|
179
|
+
"口令",
|
|
180
|
+
"邮箱",
|
|
181
|
+
"郵箱",
|
|
182
|
+
"邮件",
|
|
183
|
+
"郵件",
|
|
184
|
+
"电话",
|
|
185
|
+
"手机",
|
|
186
|
+
"手機",
|
|
187
|
+
"姓名",
|
|
188
|
+
"地址",
|
|
189
|
+
"生日",
|
|
190
|
+
"出生日期",
|
|
191
|
+
"邮编",
|
|
192
|
+
"身份证",
|
|
193
|
+
"身份證",
|
|
194
|
+
"信用卡",
|
|
195
|
+
"银行卡"
|
|
93
196
|
];
|
|
94
197
|
var NAME_ATTRS = ["name", "id", "aria-label"];
|
|
95
|
-
function
|
|
96
|
-
return value.toLowerCase().replace(/[
|
|
198
|
+
function foldDiacritics(value) {
|
|
199
|
+
return value.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").normalize("NFC").replace(/ß/g, "ss");
|
|
200
|
+
}
|
|
201
|
+
function normalForms(value) {
|
|
202
|
+
const folded = foldDiacritics(value);
|
|
203
|
+
const ascii = folded.replace(/[^a-z0-9]/g, "");
|
|
204
|
+
const script = folded.replace(/[^\p{L}\p{N}]/gu, "");
|
|
205
|
+
return ascii === script ? [ascii] : [ascii, script];
|
|
97
206
|
}
|
|
98
207
|
function isSensitiveField(el) {
|
|
99
208
|
if (el.tagName.toLowerCase() === "input") {
|
|
@@ -112,9 +221,10 @@ function isSensitiveField(el) {
|
|
|
112
221
|
const raw = el.getAttribute(attr);
|
|
113
222
|
if (!raw)
|
|
114
223
|
continue;
|
|
115
|
-
const normalized
|
|
116
|
-
|
|
117
|
-
|
|
224
|
+
for (const normalized of normalForms(raw)) {
|
|
225
|
+
if (SENSITIVE_WORDS.some((word) => normalized.includes(word)))
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
118
228
|
}
|
|
119
229
|
return false;
|
|
120
230
|
}
|
|
@@ -841,4 +951,4 @@ async function clearPersisted(indexedDB = defaultFactory()) {
|
|
|
841
951
|
}
|
|
842
952
|
}
|
|
843
953
|
|
|
844
|
-
export { MASK_TOKEN, maskText, EXCLUDE_CLASS, MASK_CLASS, shouldExclude, shouldMaskByMarker, isSensitiveField, scrubUrl, scrubAttribute, decide, masksValue, MAX_BUFFER_BYTES, RECORDER_DEFAULTS, resolveConfig, ACTIVATION_EVENT_TAG, attachInteractions, EVENT_TYPE_FULL_SNAPSHOT, EVENT_TYPE_META, beginsWithSnapshot, ReplayBuffer, createDropLedger, MAX_WINDOW_BYTES, encodeWindow, randomSessionId, postEnvelope, envelopeMetaFor, encodeForUpload, createUploadSink, LOCAL_TTL_MS, quotaDroppedWindows, resetQuotaDroppedWindows, isQuotaExceeded, persistWindow, drainPersisted, clearPersisted };
|
|
954
|
+
export { __require, MASK_TOKEN, maskText, EXCLUDE_CLASS, MASK_CLASS, shouldExclude, shouldMaskByMarker, isSensitiveField, scrubUrl, scrubAttribute, decide, masksValue, MAX_BUFFER_BYTES, RECORDER_DEFAULTS, resolveConfig, ACTIVATION_EVENT_TAG, attachInteractions, EVENT_TYPE_FULL_SNAPSHOT, EVENT_TYPE_META, beginsWithSnapshot, ReplayBuffer, createDropLedger, MAX_WINDOW_BYTES, encodeWindow, randomSessionId, postEnvelope, envelopeMetaFor, encodeForUpload, createUploadSink, LOCAL_TTL_MS, quotaDroppedWindows, resetQuotaDroppedWindows, isQuotaExceeded, persistWindow, drainPersisted, clearPersisted };
|
|
@@ -26,8 +26,7 @@ import {
|
|
|
26
26
|
resolveConfig,
|
|
27
27
|
scrubAttribute,
|
|
28
28
|
scrubUrl
|
|
29
|
-
} from "./index-
|
|
30
|
-
import"./index-c3taa3cg.js";
|
|
29
|
+
} from "./index-temc0wzg.js";
|
|
31
30
|
|
|
32
31
|
// src/replay/recorder.ts
|
|
33
32
|
import { record } from "rrweb";
|
|
@@ -146,8 +145,34 @@ function isStyleDiff(value) {
|
|
|
146
145
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
147
146
|
}
|
|
148
147
|
var SERIALIZED_ELEMENT = 2;
|
|
148
|
+
var SERIALIZED_TEXT = 3;
|
|
149
149
|
var SOURCE_MUTATION = 0;
|
|
150
150
|
var SOURCE_INPUT = 5;
|
|
151
|
+
function shadowHostOf(node) {
|
|
152
|
+
if (node && typeof ShadowRoot !== "undefined" && node instanceof ShadowRoot)
|
|
153
|
+
return node.host;
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function governingElementOfTextNode(node) {
|
|
157
|
+
if (!node)
|
|
158
|
+
return null;
|
|
159
|
+
return node.parentElement ?? shadowHostOf(node.parentNode);
|
|
160
|
+
}
|
|
161
|
+
function governingElementOfParent(parent) {
|
|
162
|
+
if (!parent)
|
|
163
|
+
return null;
|
|
164
|
+
if (parent.nodeType === 1)
|
|
165
|
+
return parent;
|
|
166
|
+
return shadowHostOf(parent);
|
|
167
|
+
}
|
|
168
|
+
function addLandsInExcludedSubtree(parent) {
|
|
169
|
+
if (!parent)
|
|
170
|
+
return true;
|
|
171
|
+
const el = governingElementOfParent(parent);
|
|
172
|
+
if (el)
|
|
173
|
+
return decide(el) === "exclude";
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
151
176
|
function shouldMaskTextOf(el) {
|
|
152
177
|
if (!el)
|
|
153
178
|
return true;
|
|
@@ -271,14 +296,33 @@ function scrubSerializedNode(node, doc) {
|
|
|
271
296
|
scrubSerializedNode(child, doc);
|
|
272
297
|
}
|
|
273
298
|
}
|
|
299
|
+
function maskUnjudgedShadowText(node, ctx) {
|
|
300
|
+
const children = node.childNodes;
|
|
301
|
+
if (!children)
|
|
302
|
+
return;
|
|
303
|
+
let host;
|
|
304
|
+
for (const child of children) {
|
|
305
|
+
if (child.type === SERIALIZED_TEXT && child.isShadow === true && typeof child.textContent === "string") {
|
|
306
|
+
if (host === undefined) {
|
|
307
|
+
const live = typeof node.id === "number" ? ctx.resolveNode(node.id) : null;
|
|
308
|
+
host = live && live.nodeType === 1 ? live : null;
|
|
309
|
+
}
|
|
310
|
+
if (shouldMaskTextOf(host))
|
|
311
|
+
child.textContent = maskText(child.textContent);
|
|
312
|
+
}
|
|
313
|
+
maskUnjudgedShadowText(child, ctx);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
274
316
|
function scrubEventAttributes(event, ctx) {
|
|
275
317
|
const e = event;
|
|
276
318
|
const data = e.data;
|
|
277
319
|
if (!data)
|
|
278
320
|
return event;
|
|
279
321
|
const node = data.node;
|
|
280
|
-
if (node && typeof node === "object")
|
|
322
|
+
if (node && typeof node === "object") {
|
|
281
323
|
scrubSerializedNode(node, ctx.doc);
|
|
324
|
+
maskUnjudgedShadowText(node, ctx);
|
|
325
|
+
}
|
|
282
326
|
if (data.source === SOURCE_INPUT && typeof data.text === "string") {
|
|
283
327
|
const live = ctx.resolveNode(data.id);
|
|
284
328
|
const el = live && live.nodeType === 1 ? live : null;
|
|
@@ -286,11 +330,34 @@ function scrubEventAttributes(event, ctx) {
|
|
|
286
330
|
}
|
|
287
331
|
if (data.source !== SOURCE_MUTATION)
|
|
288
332
|
return event;
|
|
333
|
+
const texts = data.texts;
|
|
334
|
+
if (Array.isArray(texts)) {
|
|
335
|
+
for (const entry of texts) {
|
|
336
|
+
if (typeof entry?.value !== "string")
|
|
337
|
+
continue;
|
|
338
|
+
if (shouldMaskTextOf(governingElementOfTextNode(ctx.resolveNode(entry.id)))) {
|
|
339
|
+
entry.value = maskText(entry.value);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
289
343
|
const adds = data.adds;
|
|
290
344
|
if (Array.isArray(adds)) {
|
|
291
|
-
for (
|
|
292
|
-
|
|
293
|
-
|
|
345
|
+
for (let i = adds.length - 1;i >= 0; i--) {
|
|
346
|
+
const add = adds[i];
|
|
347
|
+
if (!add?.node)
|
|
348
|
+
continue;
|
|
349
|
+
const parent = typeof add.parentId === "number" ? ctx.resolveNode(add.parentId) : null;
|
|
350
|
+
if (addLandsInExcludedSubtree(parent)) {
|
|
351
|
+
adds.splice(i, 1);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (add.node.type === SERIALIZED_TEXT && typeof add.node.textContent === "string") {
|
|
355
|
+
if (shouldMaskTextOf(governingElementOfParent(parent))) {
|
|
356
|
+
add.node.textContent = maskText(add.node.textContent);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
scrubSerializedNode(add.node, ctx.doc);
|
|
360
|
+
maskUnjudgedShadowText(add.node, ctx);
|
|
294
361
|
}
|
|
295
362
|
}
|
|
296
363
|
const attributes = data.attributes;
|
|
@@ -918,9 +985,5 @@ function startReplayUpload(opts) {
|
|
|
918
985
|
}
|
|
919
986
|
};
|
|
920
987
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
SESSION_STORAGE_KEY,
|
|
924
|
-
resolveSessionId,
|
|
925
|
-
startReplayUpload
|
|
926
|
-
};
|
|
988
|
+
|
|
989
|
+
export { startRecording, SESSION_STORAGE_KEY, resolveSessionId, SESSION_DROPS_KEY, startReplayUpload };
|
package/dist/index.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export declare class PharosBrowserClient {
|
|
|
62
62
|
private listeners;
|
|
63
63
|
private closed;
|
|
64
64
|
private reconnectTimer;
|
|
65
|
+
private reconnectAttempts;
|
|
65
66
|
private errorsEnabled;
|
|
66
67
|
private errorTarget;
|
|
67
68
|
private replayHandle;
|
|
@@ -80,11 +81,22 @@ export declare class PharosBrowserClient {
|
|
|
80
81
|
* request): the browser sees this as an error and keeps retrying the same
|
|
81
82
|
* dead token forever. Heuristic: when the source has settled into CLOSED
|
|
82
83
|
* (its terminal state — EventSource does not reach CLOSED on a retryable
|
|
83
|
-
* drop), re-bootstrap
|
|
84
|
-
*
|
|
85
|
-
*
|
|
84
|
+
* drop), re-bootstrap after a backoff to mint a fresh token, then reconnect
|
|
85
|
+
* the stream. Still no queue and no jitter — the one thing it is not simple
|
|
86
|
+
* about is the ramp, for the reason on `scheduleReconnect`.
|
|
86
87
|
*/
|
|
87
88
|
private maybeReconnect;
|
|
89
|
+
/**
|
|
90
|
+
* Arms the single pending reconnect, backing off exponentially from
|
|
91
|
+
* `RECONNECT_BACKOFF_MS` to `MAX_RECONNECT_BACKOFF_MS`.
|
|
92
|
+
*
|
|
93
|
+
* The ramp is not decoration: against a Pharos that is present-but-down
|
|
94
|
+
* every re-bootstrap fails and re-arms this timer, so a fixed delay is a
|
|
95
|
+
* request per second per open tab for the length of the outage — load
|
|
96
|
+
* arriving exactly when the server can least take it. `reconnectAttempts`
|
|
97
|
+
* is reset by a delivered `flags` push, not here.
|
|
98
|
+
*/
|
|
99
|
+
private scheduleReconnect;
|
|
88
100
|
private onWindowError;
|
|
89
101
|
private onUnhandledRejection;
|
|
90
102
|
private registerErrorListeners;
|
|
@@ -206,14 +218,41 @@ export declare class PharosBrowserClient {
|
|
|
206
218
|
startReplay(options?: Omit<ReplayUploadOptions, "endpoint" | "appKey">): Promise<ReplayUploadHandle | null>;
|
|
207
219
|
/** Stops routing errors into the recorder. Does not stop the recorder itself. */
|
|
208
220
|
detachReplay(): void;
|
|
209
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* Synchronous flag lookup; falls back to defaultValue for an unknown key,
|
|
223
|
+
* and for a value of a different type than the default (issue #788).
|
|
224
|
+
*
|
|
225
|
+
* `as T` was a cast, not a check, so a flag whose value arrived as the
|
|
226
|
+
* STRING `"false"` — truthy — read as ON through a boolean gate, while the
|
|
227
|
+
* dashboard, the payload and the code all looked right. The default carries
|
|
228
|
+
* the type the caller is prepared to handle, so a value that disagrees with
|
|
229
|
+
* it is a misconfiguration the default is the safe answer to.
|
|
230
|
+
*
|
|
231
|
+
* `null`/`undefined` defaults opt out: `typeof` describes the ABSENCE of a
|
|
232
|
+
* value for both, so checking against them would turn `flag(key, null)`
|
|
233
|
+
* into "always null" and stop returning the server's answer at all.
|
|
234
|
+
*/
|
|
210
235
|
flag<T>(key: string, defaultValue: T): T;
|
|
211
236
|
/** Subscribes to "change" (a new flags payload was applied) or "error". Returns an unsubscribe function. */
|
|
212
237
|
on(event: PharosEvent, cb: Listener): () => void;
|
|
213
238
|
private emit;
|
|
214
239
|
/**
|
|
215
240
|
* Re-identifies as a new context: closes the current stream, re-bootstraps
|
|
216
|
-
* (a full round trip — flags plus a fresh streamToken), and
|
|
241
|
+
* (a full round trip — flags plus a fresh streamToken), reconnects, and
|
|
242
|
+
* emits `change`.
|
|
243
|
+
*
|
|
244
|
+
* THE `change` IS NOT OPTIONAL (issue #788). Replacing the flags map is the
|
|
245
|
+
* whole point of this call, and the SSE handler is the only other thing that
|
|
246
|
+
* emits — so without this a component that rendered against the anonymous
|
|
247
|
+
* bootstrap kept that value for the life of its mount, while the server
|
|
248
|
+
* evaluated the real user's bucket on every request. That is not a corner
|
|
249
|
+
* case: it is every authenticated page load that mounts before login.
|
|
250
|
+
*
|
|
251
|
+
* REJECTS, AND STILL SCHEDULES A RECONNECT. An app awaiting this on login
|
|
252
|
+
* has to be able to see it fail, so the error propagates; but it was also
|
|
253
|
+
* the only way to lose the stream permanently — `eventSource` left `null`
|
|
254
|
+
* with nothing pending, since `maybeReconnect` runs only from an
|
|
255
|
+
* `es.onerror` and there is no `es` any more.
|
|
217
256
|
*/
|
|
218
257
|
identify(context: PharosContext): Promise<void>;
|
|
219
258
|
/** Closes the stream, stops any pending reconnect, and unregisters auto-capture. Terminal — construct a new client to resume. */
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MAX_BUFFER_BYTES,
|
|
8
8
|
MAX_WINDOW_BYTES,
|
|
9
9
|
RECORDER_DEFAULTS,
|
|
10
|
+
__require,
|
|
10
11
|
beginsWithSnapshot,
|
|
11
12
|
clearPersisted,
|
|
12
13
|
createDropLedger,
|
|
@@ -27,10 +28,7 @@ import {
|
|
|
27
28
|
scrubUrl,
|
|
28
29
|
shouldExclude,
|
|
29
30
|
shouldMaskByMarker
|
|
30
|
-
} from "./index-
|
|
31
|
-
import {
|
|
32
|
-
__require
|
|
33
|
-
} from "./index-c3taa3cg.js";
|
|
31
|
+
} from "./index-temc0wzg.js";
|
|
34
32
|
|
|
35
33
|
// src/stack.ts
|
|
36
34
|
var MAX_FRAMES = 50;
|
|
@@ -71,17 +69,74 @@ function parseStack(stack) {
|
|
|
71
69
|
return frames;
|
|
72
70
|
}
|
|
73
71
|
// src/privacy/collect.ts
|
|
74
|
-
var
|
|
72
|
+
var STRUCTURAL_ATTR_JUSTIFICATIONS = {
|
|
73
|
+
type: {
|
|
74
|
+
kind: "closed-enumeration",
|
|
75
|
+
claim: "The `type` attribute's value space is a fixed, closed enumeration defined by the HTML Living Standard, scoped per element. No element accepts an application-supplied free-text value there — an out-of-vocabulary `type` is simply not a recognized state.",
|
|
76
|
+
vocabulary: [
|
|
77
|
+
"text",
|
|
78
|
+
"button",
|
|
79
|
+
"checkbox",
|
|
80
|
+
"color",
|
|
81
|
+
"date",
|
|
82
|
+
"datetime-local",
|
|
83
|
+
"email",
|
|
84
|
+
"file",
|
|
85
|
+
"hidden",
|
|
86
|
+
"image",
|
|
87
|
+
"month",
|
|
88
|
+
"number",
|
|
89
|
+
"password",
|
|
90
|
+
"radio",
|
|
91
|
+
"range",
|
|
92
|
+
"reset",
|
|
93
|
+
"search",
|
|
94
|
+
"submit",
|
|
95
|
+
"tel",
|
|
96
|
+
"time",
|
|
97
|
+
"url",
|
|
98
|
+
"week",
|
|
99
|
+
"1",
|
|
100
|
+
"a",
|
|
101
|
+
"A",
|
|
102
|
+
"i",
|
|
103
|
+
"I"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
contenteditable: {
|
|
107
|
+
kind: "closed-enumeration",
|
|
108
|
+
claim: "The `contenteditable` attribute's value space is a closed enumeration defined by the HTML Living Standard. Any other string is not a recognized keyword state, so this attribute cannot carry free text.",
|
|
109
|
+
vocabulary: ["true", "false", "plaintext-only", "inherit", ""]
|
|
110
|
+
},
|
|
111
|
+
class: {
|
|
112
|
+
kind: "cost-tradeoff",
|
|
113
|
+
claim: "class has no closed vocabulary and CAN carry templated content in principle, so it does not clear the content-impossibility bar the enumeration entries clear. It is kept anyway as a stated cost/benefit call: every element has a class attribute, and treating it as content would drown every snapshot in noise.",
|
|
114
|
+
meetsImpossibilityBar: false
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var STRUCTURAL_ATTRS = new Set(Object.keys(STRUCTURAL_ATTR_JUSTIFICATIONS));
|
|
118
|
+
var MAX_PATH_HOPS = 1000;
|
|
75
119
|
function pathOf(el, root) {
|
|
76
120
|
const parts = [];
|
|
77
121
|
let node = el;
|
|
122
|
+
let hops = 0;
|
|
78
123
|
while (node && node !== root) {
|
|
79
124
|
const parent = node.parentElement;
|
|
80
|
-
if (
|
|
125
|
+
if (parent) {
|
|
126
|
+
const index = Array.prototype.indexOf.call(parent.children, node) + 1;
|
|
127
|
+
parts.unshift(`${node.tagName.toLowerCase()}:nth-child(${index})`);
|
|
128
|
+
node = parent;
|
|
129
|
+
} else {
|
|
130
|
+
const shadowRoot = node.getRootNode();
|
|
131
|
+
if (!(typeof ShadowRoot !== "undefined" && shadowRoot instanceof ShadowRoot))
|
|
132
|
+
break;
|
|
133
|
+
const index = Array.prototype.indexOf.call(shadowRoot.children, node) + 1;
|
|
134
|
+
parts.unshift(">>>", `${node.tagName.toLowerCase()}:nth-child(${index})`);
|
|
135
|
+
node = shadowRoot.host;
|
|
136
|
+
}
|
|
137
|
+
hops += 1;
|
|
138
|
+
if (hops > MAX_PATH_HOPS)
|
|
81
139
|
break;
|
|
82
|
-
const index = Array.prototype.indexOf.call(parent.children, node) + 1;
|
|
83
|
-
parts.unshift(`${node.tagName.toLowerCase()}:nth-child(${index})`);
|
|
84
|
-
node = parent;
|
|
85
140
|
}
|
|
86
141
|
return parts.join(" > ");
|
|
87
142
|
}
|
|
@@ -92,8 +147,8 @@ function collectRecordedStrings(root) {
|
|
|
92
147
|
if (decision === "exclude")
|
|
93
148
|
return;
|
|
94
149
|
const path = pathOf(el, root);
|
|
150
|
+
const valueIsMasked = masksValue(el);
|
|
95
151
|
if (decision === "record" || decision === "mask") {
|
|
96
|
-
const valueIsMasked = masksValue(el);
|
|
97
152
|
for (const attr of Array.from(el.attributes)) {
|
|
98
153
|
const name = attr.name.toLowerCase();
|
|
99
154
|
if (STRUCTURAL_ATTRS.has(name))
|
|
@@ -119,6 +174,22 @@ function collectRecordedStrings(root) {
|
|
|
119
174
|
}
|
|
120
175
|
for (const child of Array.from(el.children))
|
|
121
176
|
visit(child);
|
|
177
|
+
if (el.shadowRoot) {
|
|
178
|
+
const shadowPath = path ? `${path} > >>>` : ">>>";
|
|
179
|
+
for (const child of Array.from(el.shadowRoot.childNodes)) {
|
|
180
|
+
if (child.nodeType === 1) {
|
|
181
|
+
visit(child);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (child.nodeType !== 3)
|
|
185
|
+
continue;
|
|
186
|
+
if (decision !== "record" || valueIsMasked)
|
|
187
|
+
continue;
|
|
188
|
+
const text = (child.textContent ?? "").trim();
|
|
189
|
+
if (text)
|
|
190
|
+
out.push({ value: text, path: shadowPath, origin: "text" });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
122
193
|
};
|
|
123
194
|
visit(root);
|
|
124
195
|
return out;
|
|
@@ -203,6 +274,7 @@ function sanitizeAttrs(attrs) {
|
|
|
203
274
|
return out;
|
|
204
275
|
}
|
|
205
276
|
var RECONNECT_BACKOFF_MS = 1000;
|
|
277
|
+
var MAX_RECONNECT_BACKOFF_MS = 30000;
|
|
206
278
|
function buildBootstrapBody(context) {
|
|
207
279
|
const { contextKey, application, release, sessionId, attributes } = context;
|
|
208
280
|
const pharos = { contextKey };
|
|
@@ -230,6 +302,7 @@ class PharosBrowserClient {
|
|
|
230
302
|
};
|
|
231
303
|
closed = false;
|
|
232
304
|
reconnectTimer = null;
|
|
305
|
+
reconnectAttempts = 0;
|
|
233
306
|
errorsEnabled;
|
|
234
307
|
errorTarget;
|
|
235
308
|
replayHandle = null;
|
|
@@ -290,6 +363,7 @@ class PharosBrowserClient {
|
|
|
290
363
|
}
|
|
291
364
|
this.flags = payload.flags ?? {};
|
|
292
365
|
this.version = payload.version;
|
|
366
|
+
this.reconnectAttempts = 0;
|
|
293
367
|
this.emit("change", payload);
|
|
294
368
|
} catch (err) {
|
|
295
369
|
this.emit("error", err);
|
|
@@ -302,16 +376,21 @@ class PharosBrowserClient {
|
|
|
302
376
|
this.eventSource = es;
|
|
303
377
|
}
|
|
304
378
|
maybeReconnect(es) {
|
|
305
|
-
if (this.closed || this.reconnectTimer)
|
|
306
|
-
return;
|
|
307
379
|
if (es.readyState !== 2)
|
|
308
380
|
return;
|
|
381
|
+
this.scheduleReconnect();
|
|
382
|
+
}
|
|
383
|
+
scheduleReconnect() {
|
|
384
|
+
if (this.closed || this.reconnectTimer)
|
|
385
|
+
return;
|
|
386
|
+
const delay = Math.min(RECONNECT_BACKOFF_MS * 2 ** this.reconnectAttempts, MAX_RECONNECT_BACKOFF_MS);
|
|
387
|
+
this.reconnectAttempts += 1;
|
|
309
388
|
this.reconnectTimer = setTimeout(() => {
|
|
310
389
|
this.reconnectTimer = null;
|
|
311
390
|
if (this.closed)
|
|
312
391
|
return;
|
|
313
392
|
this.identify(this.context).catch((err) => this.emit("error", err));
|
|
314
|
-
},
|
|
393
|
+
}, delay);
|
|
315
394
|
}
|
|
316
395
|
onWindowError = (event) => {
|
|
317
396
|
const e = event;
|
|
@@ -400,7 +479,7 @@ class PharosBrowserClient {
|
|
|
400
479
|
async startReplay(options = {}) {
|
|
401
480
|
if (this.closed)
|
|
402
481
|
return null;
|
|
403
|
-
const { startReplayUpload } = await import("./wire-
|
|
482
|
+
const { startReplayUpload } = await import("./wire-90whxzan.js");
|
|
404
483
|
if (this.closed)
|
|
405
484
|
return null;
|
|
406
485
|
const handle = startReplayUpload({
|
|
@@ -423,10 +502,13 @@ class PharosBrowserClient {
|
|
|
423
502
|
this.replayErrorListeners = null;
|
|
424
503
|
}
|
|
425
504
|
flag(key, defaultValue) {
|
|
426
|
-
if (Object.prototype.hasOwnProperty.call(this.flags, key))
|
|
427
|
-
return
|
|
505
|
+
if (!Object.prototype.hasOwnProperty.call(this.flags, key))
|
|
506
|
+
return defaultValue;
|
|
507
|
+
const value = this.flags[key];
|
|
508
|
+
if (defaultValue !== null && defaultValue !== undefined && typeof value !== typeof defaultValue) {
|
|
509
|
+
return defaultValue;
|
|
428
510
|
}
|
|
429
|
-
return
|
|
511
|
+
return value;
|
|
430
512
|
}
|
|
431
513
|
on(event, cb) {
|
|
432
514
|
this.listeners[event].add(cb);
|
|
@@ -447,9 +529,15 @@ class PharosBrowserClient {
|
|
|
447
529
|
this.eventSource.close();
|
|
448
530
|
this.eventSource = null;
|
|
449
531
|
}
|
|
450
|
-
|
|
532
|
+
try {
|
|
533
|
+
await this.bootstrap();
|
|
534
|
+
} catch (err) {
|
|
535
|
+
this.scheduleReconnect();
|
|
536
|
+
throw err;
|
|
537
|
+
}
|
|
451
538
|
if (!this.closed) {
|
|
452
539
|
this.connectStream();
|
|
540
|
+
this.emit("change", { version: this.version, flags: this.flags });
|
|
453
541
|
}
|
|
454
542
|
}
|
|
455
543
|
close() {
|
|
@@ -471,34 +559,34 @@ class PharosBrowserClient {
|
|
|
471
559
|
}
|
|
472
560
|
}
|
|
473
561
|
export {
|
|
474
|
-
|
|
475
|
-
EXCLUDE_CLASS,
|
|
476
|
-
LOCAL_TTL_MS,
|
|
477
|
-
MASK_CLASS,
|
|
478
|
-
MASK_TOKEN,
|
|
479
|
-
MAX_BUFFER_BYTES,
|
|
480
|
-
MAX_WINDOW_BYTES,
|
|
481
|
-
PharosBrowserClient,
|
|
482
|
-
RECORDER_DEFAULTS,
|
|
483
|
-
beginsWithSnapshot,
|
|
484
|
-
clearPersisted,
|
|
485
|
-
collectRecordedStrings,
|
|
486
|
-
createDropLedger,
|
|
487
|
-
createUploadSink,
|
|
488
|
-
decide,
|
|
489
|
-
drainPersisted,
|
|
490
|
-
encodeWindow,
|
|
491
|
-
envelopeMetaFor,
|
|
492
|
-
isSensitiveField,
|
|
493
|
-
maskText,
|
|
494
|
-
masksValue,
|
|
495
|
-
persistWindow,
|
|
496
|
-
postEnvelope,
|
|
497
|
-
quotaDroppedWindows,
|
|
498
|
-
resetQuotaDroppedWindows,
|
|
499
|
-
resolveConfig,
|
|
500
|
-
scrubAttribute,
|
|
501
|
-
scrubUrl,
|
|
562
|
+
shouldMaskByMarker,
|
|
502
563
|
shouldExclude,
|
|
503
|
-
|
|
564
|
+
scrubUrl,
|
|
565
|
+
scrubAttribute,
|
|
566
|
+
resolveConfig,
|
|
567
|
+
resetQuotaDroppedWindows,
|
|
568
|
+
quotaDroppedWindows,
|
|
569
|
+
postEnvelope,
|
|
570
|
+
persistWindow,
|
|
571
|
+
masksValue,
|
|
572
|
+
maskText,
|
|
573
|
+
isSensitiveField,
|
|
574
|
+
envelopeMetaFor,
|
|
575
|
+
encodeWindow,
|
|
576
|
+
drainPersisted,
|
|
577
|
+
decide,
|
|
578
|
+
createUploadSink,
|
|
579
|
+
createDropLedger,
|
|
580
|
+
collectRecordedStrings,
|
|
581
|
+
clearPersisted,
|
|
582
|
+
beginsWithSnapshot,
|
|
583
|
+
RECORDER_DEFAULTS,
|
|
584
|
+
PharosBrowserClient,
|
|
585
|
+
MAX_WINDOW_BYTES,
|
|
586
|
+
MAX_BUFFER_BYTES,
|
|
587
|
+
MASK_TOKEN,
|
|
588
|
+
MASK_CLASS,
|
|
589
|
+
LOCAL_TTL_MS,
|
|
590
|
+
EXCLUDE_CLASS,
|
|
591
|
+
ACTIVATION_EVENT_TAG
|
|
504
592
|
};
|
|
@@ -4,5 +4,16 @@ export interface RecordedString {
|
|
|
4
4
|
origin: 'text' | 'value' | 'attribute';
|
|
5
5
|
attribute?: string;
|
|
6
6
|
}
|
|
7
|
+
type StructuralAttrJustification = {
|
|
8
|
+
readonly kind: 'closed-enumeration';
|
|
9
|
+
readonly claim: string;
|
|
10
|
+
readonly vocabulary: readonly string[];
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'cost-tradeoff';
|
|
13
|
+
readonly claim: string;
|
|
14
|
+
readonly meetsImpossibilityBar: false;
|
|
15
|
+
};
|
|
16
|
+
export declare const STRUCTURAL_ATTR_JUSTIFICATIONS: Readonly<Record<string, StructuralAttrJustification>>;
|
|
7
17
|
export declare const STRUCTURAL_ATTRS: Set<string>;
|
|
8
18
|
export declare function collectRecordedStrings(root: Element): RecordedString[];
|
|
19
|
+
export {};
|
package/dist/replay/index.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
export {
|
|
1
|
+
import {
|
|
3
2
|
SESSION_DROPS_KEY,
|
|
4
3
|
SESSION_STORAGE_KEY,
|
|
5
4
|
resolveSessionId,
|
|
6
5
|
startRecording,
|
|
7
6
|
startReplayUpload
|
|
7
|
+
} from "../index-xs32mrd9.js";
|
|
8
|
+
import"../index-temc0wzg.js";
|
|
9
|
+
export {
|
|
10
|
+
startReplayUpload,
|
|
11
|
+
startRecording,
|
|
12
|
+
resolveSessionId,
|
|
13
|
+
SESSION_STORAGE_KEY,
|
|
14
|
+
SESSION_DROPS_KEY
|
|
8
15
|
};
|
|
@@ -43,6 +43,8 @@ export interface SerializedNode {
|
|
|
43
43
|
tagName?: string;
|
|
44
44
|
attributes?: Record<string, unknown>;
|
|
45
45
|
childNodes?: SerializedNode[];
|
|
46
|
+
/** rrweb-snapshot's text payload — present on `type === SERIALIZED_TEXT`. */
|
|
47
|
+
textContent?: string;
|
|
46
48
|
id?: number;
|
|
47
49
|
[key: string]: unknown;
|
|
48
50
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SESSION_DROPS_KEY,
|
|
3
|
+
SESSION_STORAGE_KEY,
|
|
4
|
+
resolveSessionId,
|
|
5
|
+
startReplayUpload
|
|
6
|
+
} from "./index-xs32mrd9.js";
|
|
7
|
+
import"./index-temc0wzg.js";
|
|
8
|
+
export {
|
|
9
|
+
startReplayUpload,
|
|
10
|
+
resolveSessionId,
|
|
11
|
+
SESSION_STORAGE_KEY,
|
|
12
|
+
SESSION_DROPS_KEY
|
|
13
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loxel.dev/pharos-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Browser client for Pharos — server-evaluated feature flags, error capture, and session replay.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
|
-
"
|
|
48
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
49
|
+
"test": "bun run typecheck && bun test",
|
|
49
50
|
"build": "bun build src/index.ts src/replay/index.ts --outdir dist --root src --splitting --target browser --format esm --external rrweb && bun run build:types",
|
|
50
51
|
"build:types": "tsc -p tsconfig.build.json",
|
|
51
52
|
"prepublishOnly": "bun run build"
|
package/dist/index-c3taa3cg.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined")
|
|
5
|
-
return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
export { __require };
|