@reliantlabs/forge-web-runtime 0.4.0 → 0.5.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/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +46 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors.d.ts +4 -0
- package/dist/interceptors.js +6 -0
- package/dist/interceptors.js.map +1 -1
- package/dist/telemetry-gate.d.ts +12 -0
- package/dist/telemetry-gate.js +51 -0
- package/dist/telemetry-gate.js.map +1 -0
- package/dist/telemetry.d.ts +44 -2
- package/dist/telemetry.js +154 -16
- package/dist/telemetry.js.map +1 -1
- package/dist/track.d.ts +72 -0
- package/dist/track.js +243 -0
- package/dist/track.js.map +1 -0
- package/package.json +10 -3
package/dist/track.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.
|
|
2
|
+
//
|
|
3
|
+
// Product events, the signed-in user and the consent kill-switch: the part of
|
|
4
|
+
// browser telemetry an app calls from ordinary components.
|
|
5
|
+
//
|
|
6
|
+
// Dependency-free on purpose, for the same reason as error-reporter.ts. This
|
|
7
|
+
// module is exported from the barrel, which every frontend imports —
|
|
8
|
+
// including ones that never install the HyperDX SDK — so it cannot import the
|
|
9
|
+
// SDK. The `/telemetry` subpath, which does, installs the real sink here once
|
|
10
|
+
// the SDK has initialised. Until then (the SDK chunk is still loading) calls
|
|
11
|
+
// are held, bounded, and replayed in order; with telemetry off they are
|
|
12
|
+
// dropped. That is what makes `track()` safe to call unconditionally from a
|
|
13
|
+
// click handler or an effect.
|
|
14
|
+
//
|
|
15
|
+
// ── What `track` sends ───────────────────────────────────────────────────
|
|
16
|
+
//
|
|
17
|
+
// `track(name, props)` becomes one span from the SDK's `custom-action` tracer
|
|
18
|
+
// (HyperDX.addAction), so a product event inherits the session id, page,
|
|
19
|
+
// trace context and release the SDK stamps on every span — which is what lets
|
|
20
|
+
// a funnel step link to the replay and the errors of the same session.
|
|
21
|
+
//
|
|
22
|
+
// Props are user data in flight, so they are bounded and scrubbed HERE, before
|
|
23
|
+
// the SDK sees them, and not left to a server-side filter:
|
|
24
|
+
// - at most MAX_PROP_KEYS keys; a key must match PROP_KEY_RE or it is dropped
|
|
25
|
+
// - values are string | number | boolean; anything else is dropped
|
|
26
|
+
// - string values have credential shapes redacted, then are cut to
|
|
27
|
+
// MAX_PROP_VALUE_CHARS
|
|
28
|
+
//
|
|
29
|
+
// ── Consent ──────────────────────────────────────────────────────────────
|
|
30
|
+
//
|
|
31
|
+
// `disableTelemetry()` is the runtime half of "the user turned it off": it
|
|
32
|
+
// stops the replay recorder and makes track, setTelemetryUser and
|
|
33
|
+
// reportException no-ops for the rest of the page load. It cannot recall what
|
|
34
|
+
// the SDK already batched, and the SDK's own auto-instrumentation (page
|
|
35
|
+
// loads, fetch/XHR, uncaught errors) has no off switch once running — an app
|
|
36
|
+
// that must send nothing without consent must not call initBrowserTelemetry
|
|
37
|
+
// until it has it. There is no re-enable; init again on the next page load.
|
|
38
|
+
/** Most events held while the SDK is still loading. Later calls are dropped. */
|
|
39
|
+
export const MAX_QUEUED_EVENTS = 100;
|
|
40
|
+
/** Most props kept on one event; the rest are dropped in insertion order. */
|
|
41
|
+
export const MAX_PROP_KEYS = 20;
|
|
42
|
+
/** Longest string prop value, after redaction. */
|
|
43
|
+
export const MAX_PROP_VALUE_CHARS = 256;
|
|
44
|
+
/** Longest event name. */
|
|
45
|
+
export const MAX_EVENT_NAME_CHARS = 128;
|
|
46
|
+
/** Longest user id. */
|
|
47
|
+
export const MAX_USER_ID_CHARS = 128;
|
|
48
|
+
/** A prop key is a dotted lowercase identifier. */
|
|
49
|
+
export const PROP_KEY_RE = /^[a-z0-9_.]{1,64}$/;
|
|
50
|
+
/** What a redacted credential becomes. */
|
|
51
|
+
export const REDACTED = "[redacted]";
|
|
52
|
+
let sink = null;
|
|
53
|
+
let disabled = false;
|
|
54
|
+
let pending = [];
|
|
55
|
+
// Credential shapes, the same set the reliant web app scrubs from error text
|
|
56
|
+
// (and the backend scrubs server-side). Kept here so a product event cannot
|
|
57
|
+
// carry what an error message may not.
|
|
58
|
+
const SECRET_PATTERNS = [
|
|
59
|
+
// URL userinfo: postgres://user:password@host
|
|
60
|
+
[/([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/g, `$1${REDACTED}@`],
|
|
61
|
+
// key=value / "key": "value" for credential-named keys.
|
|
62
|
+
[
|
|
63
|
+
/\b(authorization|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|secret|password|passwd|token)(["']?\s*[:=]\s*["']?)(?:(?:bearer|basic|token)\s+)?[^\s"'&,;]+/gi,
|
|
64
|
+
`$1$2${REDACTED}`,
|
|
65
|
+
],
|
|
66
|
+
// A bare authorization scheme outside a key=value pair.
|
|
67
|
+
[/\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]+/gi, `$1 ${REDACTED}`],
|
|
68
|
+
// JWTs.
|
|
69
|
+
[/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]*/g, REDACTED],
|
|
70
|
+
// Provider and platform key prefixes.
|
|
71
|
+
[
|
|
72
|
+
/\b(?:sk-ant-[A-Za-z0-9_-]{8,}|sk-[A-Za-z0-9_-]{16,}|sk_(?:live|test)_[A-Za-z0-9]{8,}|(?:rlat|dpat|gho|ghp|ghu|ghs|ghr|github_pat|glpat|xox[abprs])[_-][A-Za-z0-9_-]{10,}|AIza[0-9A-Za-z_-]{20,}|ya29\.[0-9A-Za-z_-]+|AKIA[0-9A-Z]{16})/g,
|
|
73
|
+
REDACTED,
|
|
74
|
+
],
|
|
75
|
+
// Long opaque runs (digests, base64 blobs). '-', '/' and '.' are excluded so
|
|
76
|
+
// UUIDs, paths and dotted names survive.
|
|
77
|
+
[/[A-Za-z0-9+=_]{40,}/g, REDACTED],
|
|
78
|
+
];
|
|
79
|
+
/** Redact credential shapes from a string. Exported for tests. */
|
|
80
|
+
export function redactSecrets(value) {
|
|
81
|
+
let out = value;
|
|
82
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
83
|
+
out = out.replace(pattern, replacement);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Reduce caller props to what an event may carry: bounded, typed, redacted.
|
|
89
|
+
* Pure; exported for tests.
|
|
90
|
+
*/
|
|
91
|
+
export function sanitizeProps(props) {
|
|
92
|
+
const out = {};
|
|
93
|
+
if (typeof props !== "object" || props === null) {
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
let kept = 0;
|
|
97
|
+
for (const key of Object.keys(props)) {
|
|
98
|
+
if (kept >= MAX_PROP_KEYS) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if (!PROP_KEY_RE.test(key)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const value = props[key];
|
|
105
|
+
if (typeof value === "string") {
|
|
106
|
+
// Cut first so redaction never scans an unbounded string, with headroom
|
|
107
|
+
// so a secret straddling the final cut is still seen whole.
|
|
108
|
+
out[key] = redactSecrets(value.slice(0, MAX_PROP_VALUE_CHARS * 4)).slice(0, MAX_PROP_VALUE_CHARS);
|
|
109
|
+
}
|
|
110
|
+
else if (typeof value === "number" && Number.isFinite(value)) {
|
|
111
|
+
out[key] = value;
|
|
112
|
+
}
|
|
113
|
+
else if (typeof value === "boolean") {
|
|
114
|
+
out[key] = value;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
kept++;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function enqueue(op) {
|
|
124
|
+
if (pending.length < MAX_QUEUED_EVENTS) {
|
|
125
|
+
pending.push(op);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Record a product event — a funnel step, a feature used — as a span on the
|
|
130
|
+
* current session. Never throws, and is a no-op on the server, after
|
|
131
|
+
* disableTelemetry(), and (once the SDK has started) whenever telemetry is
|
|
132
|
+
* off. Calls made while the SDK is still loading are held, up to
|
|
133
|
+
* MAX_QUEUED_EVENTS, and replayed in order.
|
|
134
|
+
*
|
|
135
|
+
* `name` is a stable, low-cardinality event name ("onboarding_completed"),
|
|
136
|
+
* not a sentence. `props` is bounded and redacted as described in the module
|
|
137
|
+
* header; do not put user content in it.
|
|
138
|
+
*/
|
|
139
|
+
export function track(name, props) {
|
|
140
|
+
if (typeof window === "undefined" || disabled) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (typeof name !== "string") {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const trimmed = name.trim().slice(0, MAX_EVENT_NAME_CHARS);
|
|
147
|
+
if (trimmed === "") {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const attributes = sanitizeProps(props);
|
|
151
|
+
try {
|
|
152
|
+
if (sink) {
|
|
153
|
+
sink.track(trimmed, attributes);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
enqueue({ kind: "track", name: trimmed, attributes });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Telemetry must never break the app.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Attach the signed-in user's id to every span and replay from here on, so an
|
|
165
|
+
* error, a funnel step and a replay can be joined to a user. Pass an opaque
|
|
166
|
+
* id — never an email, a name or anything else a person would recognise. Pass
|
|
167
|
+
* null (sign-out) to clear it.
|
|
168
|
+
*
|
|
169
|
+
* The id is carried as the `userId` global attribute. The SDK has no way to
|
|
170
|
+
* delete an attribute, so clearing writes an empty `userId`. Held until the
|
|
171
|
+
* SDK has started; the last call wins.
|
|
172
|
+
*/
|
|
173
|
+
export function setTelemetryUser(id) {
|
|
174
|
+
if (typeof window === "undefined" || disabled) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const value = typeof id === "string" ? id.trim().slice(0, MAX_USER_ID_CHARS) : "";
|
|
178
|
+
try {
|
|
179
|
+
if (sink) {
|
|
180
|
+
sink.setUser(value);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
pending = pending.filter((op) => op.kind !== "user");
|
|
184
|
+
enqueue({ kind: "user", id: value });
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Telemetry must never break the app.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Turn browser telemetry off for the rest of this page load: stop the replay
|
|
192
|
+
* recorder, drop anything held, and make track(), setTelemetryUser() and
|
|
193
|
+
* reportException() no-ops. For when the user withdraws consent mid-session.
|
|
194
|
+
* See the module header for what it cannot undo.
|
|
195
|
+
*/
|
|
196
|
+
export function disableTelemetry() {
|
|
197
|
+
disabled = true;
|
|
198
|
+
pending = [];
|
|
199
|
+
try {
|
|
200
|
+
sink?.disable();
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Telemetry must never break the app.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** Whether disableTelemetry() has been called. Read by the installed recorder. */
|
|
207
|
+
export function isTelemetryDisabled() {
|
|
208
|
+
return disabled;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Install (or clear, with null) the sink track() forwards to, replaying
|
|
212
|
+
* whatever was held. Called by initBrowserTelemetry; an app has no reason to
|
|
213
|
+
* call it directly. Clearing also drops what was held, so a start that failed
|
|
214
|
+
* does not keep a queue nobody will drain.
|
|
215
|
+
*/
|
|
216
|
+
export function setTelemetrySink(next) {
|
|
217
|
+
sink = next;
|
|
218
|
+
const held = pending;
|
|
219
|
+
pending = [];
|
|
220
|
+
if (!next || disabled) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const op of held) {
|
|
224
|
+
try {
|
|
225
|
+
if (op.kind === "track") {
|
|
226
|
+
next.track(op.name, op.attributes);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
next.setUser(op.id);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Telemetry must never break the app.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Test seam. */
|
|
238
|
+
export function resetTrackState() {
|
|
239
|
+
sink = null;
|
|
240
|
+
disabled = false;
|
|
241
|
+
pending = [];
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=track.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track.js","sourceRoot":"","sources":["../src/track.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Product events, the signed-in user and the consent kill-switch: the part of\n// browser telemetry an app calls from ordinary components.\n//\n// Dependency-free on purpose, for the same reason as error-reporter.ts. This\n// module is exported from the barrel, which every frontend imports —\n// including ones that never install the HyperDX SDK — so it cannot import the\n// SDK. The `/telemetry` subpath, which does, installs the real sink here once\n// the SDK has initialised. Until then (the SDK chunk is still loading) calls\n// are held, bounded, and replayed in order; with telemetry off they are\n// dropped. That is what makes `track()` safe to call unconditionally from a\n// click handler or an effect.\n//\n// ── What `track` sends ───────────────────────────────────────────────────\n//\n// `track(name, props)` becomes one span from the SDK's `custom-action` tracer\n// (HyperDX.addAction), so a product event inherits the session id, page,\n// trace context and release the SDK stamps on every span — which is what lets\n// a funnel step link to the replay and the errors of the same session.\n//\n// Props are user data in flight, so they are bounded and scrubbed HERE, before\n// the SDK sees them, and not left to a server-side filter:\n// - at most MAX_PROP_KEYS keys; a key must match PROP_KEY_RE or it is dropped\n// - values are string | number | boolean; anything else is dropped\n// - string values have credential shapes redacted, then are cut to\n// MAX_PROP_VALUE_CHARS\n//\n// ── Consent ──────────────────────────────────────────────────────────────\n//\n// `disableTelemetry()` is the runtime half of \"the user turned it off\": it\n// stops the replay recorder and makes track, setTelemetryUser and\n// reportException no-ops for the rest of the page load. It cannot recall what\n// the SDK already batched, and the SDK's own auto-instrumentation (page\n// loads, fetch/XHR, uncaught errors) has no off switch once running — an app\n// that must send nothing without consent must not call initBrowserTelemetry\n// until it has it. There is no re-enable; init again on the next page load.\n\n/** Most events held while the SDK is still loading. Later calls are dropped. */\nexport const MAX_QUEUED_EVENTS = 100;\n/** Most props kept on one event; the rest are dropped in insertion order. */\nexport const MAX_PROP_KEYS = 20;\n/** Longest string prop value, after redaction. */\nexport const MAX_PROP_VALUE_CHARS = 256;\n/** Longest event name. */\nexport const MAX_EVENT_NAME_CHARS = 128;\n/** Longest user id. */\nexport const MAX_USER_ID_CHARS = 128;\n/** A prop key is a dotted lowercase identifier. */\nexport const PROP_KEY_RE = /^[a-z0-9_.]{1,64}$/;\n/** What a redacted credential becomes. */\nexport const REDACTED = \"[redacted]\";\n\n/** A prop value an event may carry. Anything else is dropped. */\nexport type TrackValue = string | number | boolean;\n/** Props accepted by track(); non-conforming entries are dropped, not thrown on. */\nexport type TrackProps = Record<string, unknown>;\n\n/** What the `/telemetry` subpath installs once the SDK is running. */\nexport interface TelemetrySink {\n track(name: string, attributes: Record<string, TrackValue>): void;\n setUser(id: string): void;\n disable(): void;\n}\n\ntype Pending =\n | { kind: \"track\"; name: string; attributes: Record<string, TrackValue> }\n | { kind: \"user\"; id: string };\n\nlet sink: TelemetrySink | null = null;\nlet disabled = false;\nlet pending: Pending[] = [];\n\n// Credential shapes, the same set the reliant web app scrubs from error text\n// (and the backend scrubs server-side). Kept here so a product event cannot\n// carry what an error message may not.\nconst SECRET_PATTERNS: ReadonlyArray<readonly [RegExp, string]> = [\n // URL userinfo: postgres://user:password@host\n [/([A-Za-z][A-Za-z0-9+.-]*:\\/\\/)[^\\s/@:]+:[^\\s/@]+@/g, `$1${REDACTED}@`],\n // key=value / \"key\": \"value\" for credential-named keys.\n [\n /\\b(authorization|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|secret|password|passwd|token)([\"']?\\s*[:=]\\s*[\"']?)(?:(?:bearer|basic|token)\\s+)?[^\\s\"'&,;]+/gi,\n `$1$2${REDACTED}`,\n ],\n // A bare authorization scheme outside a key=value pair.\n [/\\b(bearer|basic)\\s+[A-Za-z0-9._~+/=-]+/gi, `$1 ${REDACTED}`],\n // JWTs.\n [/\\beyJ[A-Za-z0-9_-]{5,}\\.[A-Za-z0-9_-]{5,}\\.[A-Za-z0-9_-]*/g, REDACTED],\n // Provider and platform key prefixes.\n [\n /\\b(?:sk-ant-[A-Za-z0-9_-]{8,}|sk-[A-Za-z0-9_-]{16,}|sk_(?:live|test)_[A-Za-z0-9]{8,}|(?:rlat|dpat|gho|ghp|ghu|ghs|ghr|github_pat|glpat|xox[abprs])[_-][A-Za-z0-9_-]{10,}|AIza[0-9A-Za-z_-]{20,}|ya29\\.[0-9A-Za-z_-]+|AKIA[0-9A-Z]{16})/g,\n REDACTED,\n ],\n // Long opaque runs (digests, base64 blobs). '-', '/' and '.' are excluded so\n // UUIDs, paths and dotted names survive.\n [/[A-Za-z0-9+=_]{40,}/g, REDACTED],\n];\n\n/** Redact credential shapes from a string. Exported for tests. */\nexport function redactSecrets(value: string): string {\n let out = value;\n for (const [pattern, replacement] of SECRET_PATTERNS) {\n out = out.replace(pattern, replacement);\n }\n return out;\n}\n\n/**\n * Reduce caller props to what an event may carry: bounded, typed, redacted.\n * Pure; exported for tests.\n */\nexport function sanitizeProps(\n props: TrackProps | undefined,\n): Record<string, TrackValue> {\n const out: Record<string, TrackValue> = {};\n if (typeof props !== \"object\" || props === null) {\n return out;\n }\n let kept = 0;\n for (const key of Object.keys(props)) {\n if (kept >= MAX_PROP_KEYS) {\n break;\n }\n if (!PROP_KEY_RE.test(key)) {\n continue;\n }\n const value = props[key];\n if (typeof value === \"string\") {\n // Cut first so redaction never scans an unbounded string, with headroom\n // so a secret straddling the final cut is still seen whole.\n out[key] = redactSecrets(value.slice(0, MAX_PROP_VALUE_CHARS * 4)).slice(\n 0,\n MAX_PROP_VALUE_CHARS,\n );\n } else if (typeof value === \"number\" && Number.isFinite(value)) {\n out[key] = value;\n } else if (typeof value === \"boolean\") {\n out[key] = value;\n } else {\n continue;\n }\n kept++;\n }\n return out;\n}\n\nfunction enqueue(op: Pending): void {\n if (pending.length < MAX_QUEUED_EVENTS) {\n pending.push(op);\n }\n}\n\n/**\n * Record a product event — a funnel step, a feature used — as a span on the\n * current session. Never throws, and is a no-op on the server, after\n * disableTelemetry(), and (once the SDK has started) whenever telemetry is\n * off. Calls made while the SDK is still loading are held, up to\n * MAX_QUEUED_EVENTS, and replayed in order.\n *\n * `name` is a stable, low-cardinality event name (\"onboarding_completed\"),\n * not a sentence. `props` is bounded and redacted as described in the module\n * header; do not put user content in it.\n */\nexport function track(name: string, props?: TrackProps): void {\n if (typeof window === \"undefined\" || disabled) {\n return;\n }\n if (typeof name !== \"string\") {\n return;\n }\n const trimmed = name.trim().slice(0, MAX_EVENT_NAME_CHARS);\n if (trimmed === \"\") {\n return;\n }\n const attributes = sanitizeProps(props);\n try {\n if (sink) {\n sink.track(trimmed, attributes);\n } else {\n enqueue({ kind: \"track\", name: trimmed, attributes });\n }\n } catch {\n // Telemetry must never break the app.\n }\n}\n\n/**\n * Attach the signed-in user's id to every span and replay from here on, so an\n * error, a funnel step and a replay can be joined to a user. Pass an opaque\n * id — never an email, a name or anything else a person would recognise. Pass\n * null (sign-out) to clear it.\n *\n * The id is carried as the `userId` global attribute. The SDK has no way to\n * delete an attribute, so clearing writes an empty `userId`. Held until the\n * SDK has started; the last call wins.\n */\nexport function setTelemetryUser(id: string | null | undefined): void {\n if (typeof window === \"undefined\" || disabled) {\n return;\n }\n const value =\n typeof id === \"string\" ? id.trim().slice(0, MAX_USER_ID_CHARS) : \"\";\n try {\n if (sink) {\n sink.setUser(value);\n return;\n }\n pending = pending.filter((op) => op.kind !== \"user\");\n enqueue({ kind: \"user\", id: value });\n } catch {\n // Telemetry must never break the app.\n }\n}\n\n/**\n * Turn browser telemetry off for the rest of this page load: stop the replay\n * recorder, drop anything held, and make track(), setTelemetryUser() and\n * reportException() no-ops. For when the user withdraws consent mid-session.\n * See the module header for what it cannot undo.\n */\nexport function disableTelemetry(): void {\n disabled = true;\n pending = [];\n try {\n sink?.disable();\n } catch {\n // Telemetry must never break the app.\n }\n}\n\n/** Whether disableTelemetry() has been called. Read by the installed recorder. */\nexport function isTelemetryDisabled(): boolean {\n return disabled;\n}\n\n/**\n * Install (or clear, with null) the sink track() forwards to, replaying\n * whatever was held. Called by initBrowserTelemetry; an app has no reason to\n * call it directly. Clearing also drops what was held, so a start that failed\n * does not keep a queue nobody will drain.\n */\nexport function setTelemetrySink(next: TelemetrySink | null): void {\n sink = next;\n const held = pending;\n pending = [];\n if (!next || disabled) {\n return;\n }\n for (const op of held) {\n try {\n if (op.kind === \"track\") {\n next.track(op.name, op.attributes);\n } else {\n next.setUser(op.id);\n }\n } catch {\n // Telemetry must never break the app.\n }\n }\n}\n\n/** Test seam. */\nexport function resetTrackState(): void {\n sink = null;\n disabled = false;\n pending = [];\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reliantlabs/forge-web-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The forge frontend runtime: Connect interceptor stack, session, error boundary, toast queue, <Resource> container, W3C trace propagation and the HyperDX browser SDK wiring (errors, console, network, traces; replay off by default). The web twin of forge/pkg.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
6
|
"//repository note": "REQUIRED for `npm publish --provenance`, not decoration. npm cross-checks this against the repository in the signed provenance statement and rejects the publish with a 422 if they disagree — an absent field reads as \"\" and fails that comparison. `directory` names the subdirectory this package lives in, so the provenance points at the real source. Omitting this is what made the first 0.3.1 publish fail AFTER the tarball had been signed and written to the sigstore transparency log.",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -17,12 +17,19 @@
|
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
|
+
"// overrides note": "The recorder still pins vulnerable fflate 0.7.4 (GHSA-px8p-9vwx-vf98); use the patched 0.7 line only in that subtree. npm applies overrides only at the install root, not from published dependencies, so the Next.js and Vite scaffold manifests carry the same override. Remove when HyperDX updates the recorder's pin.",
|
|
21
|
+
"overrides": {
|
|
22
|
+
"@hyperdx/otel-web-session-recorder": {
|
|
23
|
+
"fflate": "^0.7.5"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
20
26
|
"type": "module",
|
|
21
27
|
"sideEffects": false,
|
|
22
28
|
"files": [
|
|
23
29
|
"dist",
|
|
24
30
|
"interceptors",
|
|
25
|
-
"README.md"
|
|
31
|
+
"README.md",
|
|
32
|
+
"NOTICE"
|
|
26
33
|
],
|
|
27
34
|
"main": "./dist/index.js",
|
|
28
35
|
"types": "./dist/index.d.ts",
|