@loxel.dev/pharos-browser 0.6.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 +360 -0
- package/TECHNICAL-DETAILS.md +480 -0
- package/dist/index-bc4bw3ba.js +844 -0
- package/dist/index-c3taa3cg.js +9 -0
- package/dist/index.d.ts +235 -0
- package/dist/index.js +504 -0
- package/dist/privacy/attributes.d.ts +12 -0
- package/dist/privacy/collect.d.ts +8 -0
- package/dist/privacy/markers.d.ts +7 -0
- package/dist/privacy/mask.d.ts +2 -0
- package/dist/privacy/policy.d.ts +27 -0
- package/dist/privacy/sensitivity.d.ts +1 -0
- package/dist/replay/budget.d.ts +49 -0
- package/dist/replay/buffer.d.ts +155 -0
- package/dist/replay/config.d.ts +190 -0
- package/dist/replay/drops.d.ts +46 -0
- package/dist/replay/index.d.ts +2 -0
- package/dist/replay/index.js +8 -0
- package/dist/replay/interactions.d.ts +48 -0
- package/dist/replay/persist.d.ts +75 -0
- package/dist/replay/privacy-hooks.d.ts +101 -0
- package/dist/replay/recorder.d.ts +20 -0
- package/dist/replay/triggers.d.ts +48 -0
- package/dist/replay/upload.d.ts +194 -0
- package/dist/replay/wire.d.ts +132 -0
- package/dist/stack.d.ts +8 -0
- package/dist/wire-992wvzs1.js +926 -0
- package/package.json +62 -0
|
@@ -0,0 +1,9 @@
|
|
|
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 };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { RecorderHandle } from "./replay/config";
|
|
2
|
+
import type { ReplayUploadHandle, ReplayUploadOptions } from "./replay/wire";
|
|
3
|
+
/** Context identifying who is asking, plus free-form targeting attributes. */
|
|
4
|
+
export interface PharosContext {
|
|
5
|
+
contextKey: string;
|
|
6
|
+
application?: string;
|
|
7
|
+
release?: string;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
attributes?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Minimal listener-registration surface auto-capture needs — matched by
|
|
13
|
+
* `window`/`globalThis` in a real browser, and by a plain fake in tests
|
|
14
|
+
* (bun test has no DOM, so `window` does not exist there).
|
|
15
|
+
*/
|
|
16
|
+
export interface ErrorListenerTarget {
|
|
17
|
+
addEventListener(type: string, listener: (event: unknown) => void): void;
|
|
18
|
+
removeEventListener(type: string, listener: (event: unknown) => void): void;
|
|
19
|
+
}
|
|
20
|
+
export interface PharosConfig {
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
clientKey: string;
|
|
23
|
+
context: PharosContext;
|
|
24
|
+
/**
|
|
25
|
+
* Injectable EventSource constructor, defaulting to the global
|
|
26
|
+
* `window.EventSource`. Exists so tests (and non-browser hosts) can
|
|
27
|
+
* supply a fake without a DOM.
|
|
28
|
+
*/
|
|
29
|
+
eventSourceFactory?: (url: string) => EventSource;
|
|
30
|
+
/**
|
|
31
|
+
* Auto-capture uncaught errors (`window.onerror`) and unhandled promise
|
|
32
|
+
* rejections (`window.onunhandledrejection`) via `captureException`.
|
|
33
|
+
* Defaults to `true`; set `false` to only report via explicit
|
|
34
|
+
* `captureException` calls.
|
|
35
|
+
*/
|
|
36
|
+
errors?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Injectable event-listener target for auto-capture, defaulting to the
|
|
39
|
+
* global `window`/`globalThis`. Exists so tests (and non-browser hosts)
|
|
40
|
+
* can supply a fake without a DOM — mirrors `eventSourceFactory`.
|
|
41
|
+
*/
|
|
42
|
+
errorTarget?: ErrorListenerTarget;
|
|
43
|
+
}
|
|
44
|
+
/** Options accepted by {@link PharosBrowserClient.captureException}. */
|
|
45
|
+
export interface CaptureExceptionOptions {
|
|
46
|
+
/** Free-form label for where the error was caught (e.g. a route or component name). */
|
|
47
|
+
site?: string;
|
|
48
|
+
/** Extra scalar (or array-of-scalar) attributes to attach to this occurrence only. */
|
|
49
|
+
attributes?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
type PharosEvent = "change" | "error";
|
|
52
|
+
type Listener = (payload: unknown) => void;
|
|
53
|
+
export declare class PharosBrowserClient {
|
|
54
|
+
private baseUrl;
|
|
55
|
+
private clientKey;
|
|
56
|
+
private context;
|
|
57
|
+
private eventSourceFactory;
|
|
58
|
+
private flags;
|
|
59
|
+
private version;
|
|
60
|
+
private streamToken;
|
|
61
|
+
private eventSource;
|
|
62
|
+
private listeners;
|
|
63
|
+
private closed;
|
|
64
|
+
private reconnectTimer;
|
|
65
|
+
private errorsEnabled;
|
|
66
|
+
private errorTarget;
|
|
67
|
+
private replayHandle;
|
|
68
|
+
private replayTriggerKind;
|
|
69
|
+
private replayErrorListeners;
|
|
70
|
+
private onReplayError;
|
|
71
|
+
private onReplayRejection;
|
|
72
|
+
private constructor();
|
|
73
|
+
/** Bootstraps against the server, opens the live-update stream, and (by default) wires auto-capture. */
|
|
74
|
+
static init(config: PharosConfig): Promise<PharosBrowserClient>;
|
|
75
|
+
private bootstrap;
|
|
76
|
+
private connectStream;
|
|
77
|
+
/**
|
|
78
|
+
* EventSource auto-reconnects on transient drops. The one case it cannot
|
|
79
|
+
* recover from alone is an expired stream session (401 on the SSE
|
|
80
|
+
* request): the browser sees this as an error and keeps retrying the same
|
|
81
|
+
* dead token forever. Heuristic: when the source has settled into CLOSED
|
|
82
|
+
* (its terminal state — EventSource does not reach CLOSED on a retryable
|
|
83
|
+
* drop), re-bootstrap once after a short backoff to mint a fresh token,
|
|
84
|
+
* then reconnect the stream. This is intentionally simple — no queue, no
|
|
85
|
+
* exponential ramp — matching the brief's "don't over-engineer" guidance.
|
|
86
|
+
*/
|
|
87
|
+
private maybeReconnect;
|
|
88
|
+
private onWindowError;
|
|
89
|
+
private onUnhandledRejection;
|
|
90
|
+
private registerErrorListeners;
|
|
91
|
+
private unregisterErrorListeners;
|
|
92
|
+
/**
|
|
93
|
+
* Reports one exception as a single-entry `/api/v1/errors` batch: message
|
|
94
|
+
* from `err.message ?? String(err)` (empty message becomes
|
|
95
|
+
* `"(no message)"`), `kind` from `err.name`, and `stack` parsed via
|
|
96
|
+
* {@link parseStack} when `err.stack` is a string. Every field is
|
|
97
|
+
* sanitized/truncated to the server's caps before sending — the server
|
|
98
|
+
* rejects the whole batch on any single field violating them, and this is
|
|
99
|
+
* most often called from uncaught-exception paths where the input isn't
|
|
100
|
+
* under the caller's control.
|
|
101
|
+
*
|
|
102
|
+
* Fire-and-forget: never throws, never returns a promise for the caller to
|
|
103
|
+
* await. A failed POST (network error, non-2xx) is reported via
|
|
104
|
+
* `on("error")`, matching the stream's existing error-reporting shape.
|
|
105
|
+
*
|
|
106
|
+
* A no-op after `close()`, matching `identify()`'s documented behavior —
|
|
107
|
+
* this client is terminal past `close()`.
|
|
108
|
+
*/
|
|
109
|
+
captureException(err: unknown, opts?: CaptureExceptionOptions): void;
|
|
110
|
+
/**
|
|
111
|
+
* Routes this client's error paths into a running session recorder, so an
|
|
112
|
+
* error flushes a replay window around itself.
|
|
113
|
+
*
|
|
114
|
+
* The recorder is started SEPARATELY, by the application, via
|
|
115
|
+
* `startRecording()` — attaching a client never starts recording. That
|
|
116
|
+
* separation is the consent boundary: adding the SDK must not begin
|
|
117
|
+
* recording a user's screen.
|
|
118
|
+
*
|
|
119
|
+
* ONE RECORDER AT A TIME, ENFORCED BY THROWING. rrweb's `record` is a
|
|
120
|
+
* process singleton (`wrappedEmit`, `takeFullSnapshot`, `mirror` and the
|
|
121
|
+
* `recording` flag are all module-level in rrweb 2.1.1), so a second
|
|
122
|
+
* `startRecording()` silently takes the session over: the first recorder's
|
|
123
|
+
* observers begin emitting into the SECOND recorder's buffer while the
|
|
124
|
+
* first handle still reports healthy stats. This is the one place a client
|
|
125
|
+
* can observe two recorders at once, and it refuses rather than swapping —
|
|
126
|
+
* swapping would leave the caller holding a handle it believes is wired,
|
|
127
|
+
* and stopping the first for them would set rrweb's module-level
|
|
128
|
+
* `recording = false` and half-kill the survivor. Re-attaching the SAME
|
|
129
|
+
* handle is idempotent; deliberately swapping means `detachReplay()` first.
|
|
130
|
+
*
|
|
131
|
+
* Unhandled promise rejections are guaranteed triggers. With auto-capture
|
|
132
|
+
* on (the default) they already route through `captureException`, and no
|
|
133
|
+
* extra listener is added — a second listener would double-count one
|
|
134
|
+
* rejection. With `errors: false` the window listeners were never
|
|
135
|
+
* registered, so this registers REPLAY-ONLY listeners that trigger the
|
|
136
|
+
* recorder without reporting anything to the errors API, leaving the app's
|
|
137
|
+
* opt-out intact.
|
|
138
|
+
*
|
|
139
|
+
* TWO MISUSES, TWO DIFFERENT ANSWERS, ON PURPOSE. Attaching after `close()`
|
|
140
|
+
* is a SILENT no-op, because a closed client is terminal — the same answer
|
|
141
|
+
* `identify()` and `captureException()` give, and nothing is left running
|
|
142
|
+
* for the caller to be wrong about. Attaching a SECOND, different handle
|
|
143
|
+
* THROWS, because that is a live contract violation with a resource
|
|
144
|
+
* collision behind it: two recorders exist, one of them is already being
|
|
145
|
+
* taken over, and silence would leave the caller holding a handle it
|
|
146
|
+
* believes is wired.
|
|
147
|
+
*
|
|
148
|
+
* @throws if a DIFFERENT recorder is already attached.
|
|
149
|
+
*/
|
|
150
|
+
attachReplay(handle: RecorderHandle): void;
|
|
151
|
+
/**
|
|
152
|
+
* TWO INDEPENDENTLY-MINTED VALUES UNDER ONE NAME (final review, Important 4).
|
|
153
|
+
*
|
|
154
|
+
* `PharosContext.sessionId` is what rides the errors envelope
|
|
155
|
+
* (`captureException` above) and what `error_store.lox` stores.
|
|
156
|
+
* `startReplayUpload` mints its OWN unless the host passes one. When they
|
|
157
|
+
* disagree, an error row and the replay window captured around that same
|
|
158
|
+
* error CANNOT BE JOINED — which is precisely the correlation slice B3's
|
|
159
|
+
* console exists to perform, and nothing anywhere would have said so.
|
|
160
|
+
*
|
|
161
|
+
* A WARNING, NOT A THROW, and not a silent repair. `attachReplay` throws for
|
|
162
|
+
* a second recorder because that is a live resource collision; this is a
|
|
163
|
+
* degraded-correlation wiring mistake on an already-working setup, and the
|
|
164
|
+
* app author is the only one who can decide which id is authoritative.
|
|
165
|
+
* Repairing it here would mean re-keying windows the recorder has already
|
|
166
|
+
* uploaded under the other id, which this client cannot do. `startReplay()`
|
|
167
|
+
* below is the version that cannot get it wrong.
|
|
168
|
+
*/
|
|
169
|
+
private warnIfSessionIdsDisagree;
|
|
170
|
+
/**
|
|
171
|
+
* Starts a replay recorder wired to THIS client — same base URL, same client
|
|
172
|
+
* key, and THE SAME `sessionId` the errors envelope carries.
|
|
173
|
+
*
|
|
174
|
+
* The reason this exists rather than leaving every application to call
|
|
175
|
+
* `startReplayUpload` itself: the two session ids above are two independent
|
|
176
|
+
* values under one name, and an application that never noticed gets a
|
|
177
|
+
* replay pillar whose windows cannot be joined to the errors that produced
|
|
178
|
+
* them. Here there is one id by construction.
|
|
179
|
+
*
|
|
180
|
+
* Also attaches the handle (`attachReplay`), so an uncaught error triggers a
|
|
181
|
+
* window — the whole point of the pairing. A client that already has a
|
|
182
|
+
* different recorder attached throws, exactly as `attachReplay` does —
|
|
183
|
+
* except that by the time `attachReplay` can throw, THIS function has
|
|
184
|
+
* already started a live recorder `attachReplay` never gets to own:
|
|
185
|
+
* `handle`'s `visibilitychange`/`pagehide` listeners are attached, its
|
|
186
|
+
* start-up drain has already run, and no caller holds the reference needed
|
|
187
|
+
* to `stop()` any of that. So the throw here also stops the orphan first,
|
|
188
|
+
* which is not part of what `attachReplay` itself does or needs to do —
|
|
189
|
+
* `attachReplay` never allocates.
|
|
190
|
+
*
|
|
191
|
+
* `endpoint` and `appKey` are supplied and may not be overridden; every
|
|
192
|
+
* other `startReplayUpload` option passes through unchanged. Resolves to
|
|
193
|
+
* `null` after `close()`, matching `attachReplay`'s silent no-op on a
|
|
194
|
+
* terminal client — a handle recording into a closed client would be a
|
|
195
|
+
* recorder nothing triggers.
|
|
196
|
+
*
|
|
197
|
+
* ASYNC BECAUSE IT LOADS THE RECORDER ON DEMAND (0.6.0). rrweb is ~63 kB
|
|
198
|
+
* gzipped, and while this was synchronous it was reachable from the root
|
|
199
|
+
* entry and therefore bundled by every consumer — including the ones that
|
|
200
|
+
* never record. It is now a dynamic `import()`, so the recorder is a
|
|
201
|
+
* separate chunk fetched only when an application actually starts replay.
|
|
202
|
+
* The chunk still ships with the deployment, which is the point: enabling
|
|
203
|
+
* replay stays a pure configuration change (loxel#979), it just stops
|
|
204
|
+
* costing anything up front. Rejects only if the chunk itself cannot load.
|
|
205
|
+
*/
|
|
206
|
+
startReplay(options?: Omit<ReplayUploadOptions, "endpoint" | "appKey">): Promise<ReplayUploadHandle | null>;
|
|
207
|
+
/** Stops routing errors into the recorder. Does not stop the recorder itself. */
|
|
208
|
+
detachReplay(): void;
|
|
209
|
+
/** Synchronous flag lookup; falls back to defaultValue for unknown keys. */
|
|
210
|
+
flag<T>(key: string, defaultValue: T): T;
|
|
211
|
+
/** Subscribes to "change" (a new flags payload was applied) or "error". Returns an unsubscribe function. */
|
|
212
|
+
on(event: PharosEvent, cb: Listener): () => void;
|
|
213
|
+
private emit;
|
|
214
|
+
/**
|
|
215
|
+
* Re-identifies as a new context: closes the current stream, re-bootstraps
|
|
216
|
+
* (a full round trip — flags plus a fresh streamToken), and reconnects.
|
|
217
|
+
*/
|
|
218
|
+
identify(context: PharosContext): Promise<void>;
|
|
219
|
+
/** Closes the stream, stops any pending reconnect, and unregisters auto-capture. Terminal — construct a new client to resume. */
|
|
220
|
+
close(): void;
|
|
221
|
+
}
|
|
222
|
+
export { MASK_TOKEN, maskText } from './privacy/mask';
|
|
223
|
+
export { EXCLUDE_CLASS, MASK_CLASS, shouldExclude, shouldMaskByMarker } from './privacy/markers';
|
|
224
|
+
export { isSensitiveField } from './privacy/sensitivity';
|
|
225
|
+
export { scrubAttribute, scrubUrl } from './privacy/attributes';
|
|
226
|
+
export { decide, masksValue, type Decision } from './privacy/policy';
|
|
227
|
+
export { collectRecordedStrings, type RecordedString } from './privacy/collect';
|
|
228
|
+
export { MAX_BUFFER_BYTES, RECORDER_DEFAULTS, resolveConfig, type ActivationMode, type DegradationReason, type DegradationRecord, type RecorderHandle, type RecorderOptions, type RecorderStats, type ReplayWindow, type ResolvedRecorderConfig, type Sink, type TriggerInfo, type TriggerKind, type TriggerSource, } from './replay/config';
|
|
229
|
+
export { ACTIVATION_EVENT_TAG } from './replay/interactions';
|
|
230
|
+
export { beginsWithSnapshot } from './replay/buffer';
|
|
231
|
+
export type { ReplayUploadHandle, ReplayUploadOptions } from './replay/wire';
|
|
232
|
+
export { MAX_WINDOW_BYTES, createUploadSink, encodeWindow, envelopeMetaFor, postEnvelope, type PostTarget, type UploadEnvelopeMeta, type UploadOptions, } from './replay/upload';
|
|
233
|
+
export { LOCAL_TTL_MS, clearPersisted, drainPersisted, persistWindow, quotaDroppedWindows, resetQuotaDroppedWindows, } from './replay/persist';
|
|
234
|
+
export { createDropLedger, type DropLedger, type DropReason } from './replay/drops';
|
|
235
|
+
export type { WindowDropReason } from './replay/config';
|