@reliantlabs/forge-web-runtime 0.8.0 → 0.9.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 CHANGED
@@ -130,6 +130,15 @@ on the server, and calls made while the SDK chunk is still loading are held
130
130
  switch once running: an app that must send nothing without consent should not
131
131
  call `initBrowserTelemetry` before it has it.
132
132
 
133
+ - Attribution (`appClient`, `visitorIdStorageKey`, `withAttribution(url)`): init
134
+ stamps `app.client` (`electron` inside Electron, else `web`; pass `"landing"`
135
+ from a marketing site) and `visitor.id` on every event, and reads `rid` and
136
+ `utm_source|medium|campaign|term|content` from the URL into localStorage. The
137
+ first `setTelemetryUser(id)` per user also emits one `user_identified` event
138
+ with `visitor_id` and the stored utm values. `withAttribution(url)` appends
139
+ `rid` and the utm values to an outbound absolute URL, and is the identity
140
+ before init or after `disableTelemetry()`.
141
+
133
142
  Privacy is forced, not configurable: when replay records, all text and inputs
134
143
  are masked, canvas is never recorded, `img, picture, video, audio, iframe,
135
144
  object, embed` are blocked, images are not inlined and fonts are not collected.
@@ -0,0 +1,54 @@
1
+ /** localStorage key of the captured `{rid, utm_*}` record. */
2
+ export declare const ATTRIBUTION_STORAGE_KEY = "forge.attribution";
3
+ /** Default localStorage key of this browser's own anonymous visitor id. */
4
+ export declare const VISITOR_ID_STORAGE_KEY = "forge.visitor_id";
5
+ /** localStorage key of the user id `user_identified` last fired for. */
6
+ export declare const IDENTIFIED_USER_STORAGE_KEY = "forge.identified_user";
7
+ /** The query parameters captured from the landing URL, besides `rid`. */
8
+ export declare const UTM_PARAMS: readonly ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
9
+ /** Longest captured attribution value. */
10
+ export declare const MAX_ATTRIBUTION_VALUE_CHARS = 128;
11
+ /** Where an event came from. `landing` is for a marketing site; pass any string for another kind. */
12
+ export type AppClient = "web" | "electron" | "landing" | (string & {});
13
+ export type Attribution = {
14
+ rid?: string;
15
+ } & Partial<Record<(typeof UTM_PARAMS)[number], string>>;
16
+ /** The `app.client` for this page: an explicit choice, else `electron` inside Electron, else `web`. */
17
+ export declare function detectAppClient(explicit?: string): AppClient;
18
+ /** Pull `rid` and `utm_*` out of a query string. Pure; exported for tests. */
19
+ export declare function parseAttribution(search: string): Attribution;
20
+ /**
21
+ * Start attribution for this page load: record `rid` and `utm_*` from the URL
22
+ * (a visit that carries any replaces what was stored — last touch wins; one
23
+ * that carries none keeps it) and note which storage key holds this browser's
24
+ * own visitor id. Called by initBrowserTelemetry, i.e. only once telemetry may
25
+ * run. Returns the visitor id. Never throws.
26
+ */
27
+ export declare function captureAttribution(options?: {
28
+ search?: string;
29
+ visitorIdStorageKey?: string;
30
+ }): string;
31
+ /** The captured attribution, or `{}` before captureAttribution. */
32
+ export declare function getAttribution(): Attribution;
33
+ /**
34
+ * This browser's visitor id: the `rid` it arrived with, else its own anonymous
35
+ * id (created on first use and kept in localStorage). Empty before
36
+ * captureAttribution.
37
+ */
38
+ export declare function visitorId(): string;
39
+ /**
40
+ * Append this browser's `rid` and captured `utm_*` to an outbound URL — the
41
+ * link from a marketing site to the app, or to a download. Parameters the URL
42
+ * already carries are kept. Returns the URL unchanged when it is not an
43
+ * absolute http(s) URL, before telemetry has started, and after
44
+ * disableTelemetry() — so it is safe to wrap every outbound link.
45
+ */
46
+ export declare function withAttribution(url: string): string;
47
+ /**
48
+ * The props of the one-time `user_identified` event for `userId`, or null when
49
+ * it already fired for that user in this browser (or attribution is not
50
+ * running). Marks it fired. Empty ids never identify.
51
+ */
52
+ export declare function takeIdentifyProps(userId: string): Record<string, string> | null;
53
+ /** Test seam. */
54
+ export declare function resetAttributionState(): void;
@@ -0,0 +1,289 @@
1
+ // Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.
2
+ //
3
+ // Visitor identity and campaign attribution: the part of browser telemetry
4
+ // that ties an anonymous visit on a marketing site to the user who later signs
5
+ // in to the app.
6
+ //
7
+ // ── The seam ─────────────────────────────────────────────────────────────
8
+ //
9
+ // A marketing site links to the app with `?rid=<visitor id>&utm_*=…`
10
+ // (withAttribution). The app, on startup, reads those parameters into storage
11
+ // (captureAttribution) and adopts `rid` as its visitor id. When a user signs in
12
+ // (setTelemetryUser) the runtime emits one `user_identified` event carrying
13
+ // `visitor_id` and the stored utm_* values, so a funnel can join "visited the
14
+ // site" to "is user X". Every event also carries `visitor.id` and `app.client`.
15
+ //
16
+ // ── Consent ──────────────────────────────────────────────────────────────
17
+ //
18
+ // Nothing here runs until initBrowserTelemetry does, and that is the point an
19
+ // app gates on consent. Before it (or after disableTelemetry) captureAttribution
20
+ // stores nothing and withAttribution returns the URL untouched. Dependency-free
21
+ // apart from track.ts, like it.
22
+ import { isTelemetryDisabled } from "./track.js";
23
+ /** localStorage key of the captured `{rid, utm_*}` record. */
24
+ export const ATTRIBUTION_STORAGE_KEY = "forge.attribution";
25
+ /** Default localStorage key of this browser's own anonymous visitor id. */
26
+ export const VISITOR_ID_STORAGE_KEY = "forge.visitor_id";
27
+ /** localStorage key of the user id `user_identified` last fired for. */
28
+ export const IDENTIFIED_USER_STORAGE_KEY = "forge.identified_user";
29
+ /** The query parameters captured from the landing URL, besides `rid`. */
30
+ export const UTM_PARAMS = [
31
+ "utm_source",
32
+ "utm_medium",
33
+ "utm_campaign",
34
+ "utm_term",
35
+ "utm_content",
36
+ ];
37
+ /** Longest captured attribution value. */
38
+ export const MAX_ATTRIBUTION_VALUE_CHARS = 128;
39
+ /** A visitor id: opaque, URL-safe. Anything else in `rid` is ignored. */
40
+ const RID_RE = /^[A-Za-z0-9_-]{8,64}$/;
41
+ let active = false;
42
+ let visitorKey = VISITOR_ID_STORAGE_KEY;
43
+ let memory = {};
44
+ let memoryVisitorId;
45
+ let memoryIdentified;
46
+ function storage() {
47
+ try {
48
+ return typeof window !== "undefined" ? window.localStorage : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function read(key) {
55
+ try {
56
+ return storage()?.getItem(key) ?? null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ function write(key, value) {
63
+ try {
64
+ storage()?.setItem(key, value);
65
+ }
66
+ catch {
67
+ // Storage blocked or full: the in-memory copy still serves this page load.
68
+ }
69
+ }
70
+ /** The `app.client` for this page: an explicit choice, else `electron` inside Electron, else `web`. */
71
+ export function detectAppClient(explicit) {
72
+ if (typeof explicit === "string" && explicit.trim() !== "") {
73
+ return explicit.trim().slice(0, 32);
74
+ }
75
+ if (typeof window === "undefined") {
76
+ return "web";
77
+ }
78
+ try {
79
+ const proc = window.process;
80
+ if (proc?.versions?.electron) {
81
+ return "electron";
82
+ }
83
+ if (typeof navigator !== "undefined" &&
84
+ /\bElectron\//.test(navigator.userAgent)) {
85
+ return "electron";
86
+ }
87
+ }
88
+ catch {
89
+ // Fall through to web.
90
+ }
91
+ return "web";
92
+ }
93
+ function cleanValue(value) {
94
+ if (typeof value !== "string") {
95
+ return undefined;
96
+ }
97
+ // eslint-disable-next-line no-control-regex
98
+ const v = value.replace(/[\u0000-\u001f\u007f]/g, "").trim();
99
+ return v === "" ? undefined : v.slice(0, MAX_ATTRIBUTION_VALUE_CHARS);
100
+ }
101
+ /** Pull `rid` and `utm_*` out of a query string. Pure; exported for tests. */
102
+ export function parseAttribution(search) {
103
+ const out = {};
104
+ let params;
105
+ try {
106
+ params = new URLSearchParams(search);
107
+ }
108
+ catch {
109
+ return out;
110
+ }
111
+ const rid = params.get("rid")?.trim();
112
+ if (rid && RID_RE.test(rid)) {
113
+ out.rid = rid;
114
+ }
115
+ for (const key of UTM_PARAMS) {
116
+ const v = cleanValue(params.get(key));
117
+ if (v) {
118
+ out[key] = v;
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ function readStoredAttribution() {
124
+ const raw = read(ATTRIBUTION_STORAGE_KEY);
125
+ if (!raw) {
126
+ return memory;
127
+ }
128
+ try {
129
+ const parsed = JSON.parse(raw);
130
+ if (typeof parsed !== "object" || parsed === null) {
131
+ return memory;
132
+ }
133
+ const out = {};
134
+ const rid = parsed.rid;
135
+ if (typeof rid === "string" && RID_RE.test(rid)) {
136
+ out.rid = rid;
137
+ }
138
+ for (const key of UTM_PARAMS) {
139
+ const v = cleanValue(parsed[key]);
140
+ if (v) {
141
+ out[key] = v;
142
+ }
143
+ }
144
+ return out;
145
+ }
146
+ catch {
147
+ return memory;
148
+ }
149
+ }
150
+ /**
151
+ * Start attribution for this page load: record `rid` and `utm_*` from the URL
152
+ * (a visit that carries any replaces what was stored — last touch wins; one
153
+ * that carries none keeps it) and note which storage key holds this browser's
154
+ * own visitor id. Called by initBrowserTelemetry, i.e. only once telemetry may
155
+ * run. Returns the visitor id. Never throws.
156
+ */
157
+ export function captureAttribution(options) {
158
+ if (isTelemetryDisabled()) {
159
+ return "";
160
+ }
161
+ active = true;
162
+ if (options?.visitorIdStorageKey) {
163
+ visitorKey = options.visitorIdStorageKey;
164
+ }
165
+ const search = options?.search ??
166
+ (typeof window !== "undefined" ? (window.location?.search ?? "") : "");
167
+ const incoming = parseAttribution(search);
168
+ if (Object.keys(incoming).length > 0) {
169
+ memory = incoming;
170
+ write(ATTRIBUTION_STORAGE_KEY, JSON.stringify(incoming));
171
+ }
172
+ else {
173
+ memory = readStoredAttribution();
174
+ }
175
+ return visitorId();
176
+ }
177
+ /** The captured attribution, or `{}` before captureAttribution. */
178
+ export function getAttribution() {
179
+ return active ? readStoredAttribution() : {};
180
+ }
181
+ function newId() {
182
+ const c = globalThis.crypto;
183
+ if (c && typeof c.randomUUID === "function") {
184
+ return c.randomUUID();
185
+ }
186
+ let s = "";
187
+ for (let i = 0; i < 4; i++) {
188
+ s += Math.floor(Math.random() * 0x100000000)
189
+ .toString(16)
190
+ .padStart(8, "0");
191
+ }
192
+ return s;
193
+ }
194
+ /**
195
+ * This browser's visitor id: the `rid` it arrived with, else its own anonymous
196
+ * id (created on first use and kept in localStorage). Empty before
197
+ * captureAttribution.
198
+ */
199
+ export function visitorId() {
200
+ if (!active) {
201
+ return "";
202
+ }
203
+ const rid = readStoredAttribution().rid;
204
+ if (rid) {
205
+ return rid;
206
+ }
207
+ const held = read(visitorKey);
208
+ if (held && RID_RE.test(held)) {
209
+ return held;
210
+ }
211
+ if (!memoryVisitorId) {
212
+ memoryVisitorId = newId();
213
+ }
214
+ write(visitorKey, memoryVisitorId);
215
+ return memoryVisitorId;
216
+ }
217
+ /**
218
+ * Append this browser's `rid` and captured `utm_*` to an outbound URL — the
219
+ * link from a marketing site to the app, or to a download. Parameters the URL
220
+ * already carries are kept. Returns the URL unchanged when it is not an
221
+ * absolute http(s) URL, before telemetry has started, and after
222
+ * disableTelemetry() — so it is safe to wrap every outbound link.
223
+ */
224
+ export function withAttribution(url) {
225
+ if (!active || isTelemetryDisabled() || typeof url !== "string") {
226
+ return url;
227
+ }
228
+ let parsed;
229
+ try {
230
+ parsed = new URL(url);
231
+ }
232
+ catch {
233
+ return url;
234
+ }
235
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
236
+ return url;
237
+ }
238
+ const stored = readStoredAttribution();
239
+ const add = {};
240
+ const rid = visitorId();
241
+ if (rid) {
242
+ add.rid = rid;
243
+ }
244
+ for (const key of UTM_PARAMS) {
245
+ const v = stored[key];
246
+ if (v) {
247
+ add[key] = v;
248
+ }
249
+ }
250
+ for (const [key, value] of Object.entries(add)) {
251
+ if (!parsed.searchParams.has(key)) {
252
+ parsed.searchParams.set(key, value);
253
+ }
254
+ }
255
+ return parsed.toString();
256
+ }
257
+ /**
258
+ * The props of the one-time `user_identified` event for `userId`, or null when
259
+ * it already fired for that user in this browser (or attribution is not
260
+ * running). Marks it fired. Empty ids never identify.
261
+ */
262
+ export function takeIdentifyProps(userId) {
263
+ if (!active || isTelemetryDisabled() || userId === "") {
264
+ return null;
265
+ }
266
+ if ((read(IDENTIFIED_USER_STORAGE_KEY) ?? memoryIdentified) === userId) {
267
+ return null;
268
+ }
269
+ memoryIdentified = userId;
270
+ write(IDENTIFIED_USER_STORAGE_KEY, userId);
271
+ const props = { visitor_id: visitorId() };
272
+ const stored = readStoredAttribution();
273
+ for (const key of UTM_PARAMS) {
274
+ const v = stored[key];
275
+ if (v) {
276
+ props[key] = v;
277
+ }
278
+ }
279
+ return props;
280
+ }
281
+ /** Test seam. */
282
+ export function resetAttributionState() {
283
+ active = false;
284
+ visitorKey = VISITOR_ID_STORAGE_KEY;
285
+ memory = {};
286
+ memoryVisitorId = undefined;
287
+ memoryIdentified = undefined;
288
+ }
289
+ //# sourceMappingURL=attribution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attribution.js","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Visitor identity and campaign attribution: the part of browser telemetry\n// that ties an anonymous visit on a marketing site to the user who later signs\n// in to the app.\n//\n// ── The seam ─────────────────────────────────────────────────────────────\n//\n// A marketing site links to the app with `?rid=<visitor id>&utm_*=…`\n// (withAttribution). The app, on startup, reads those parameters into storage\n// (captureAttribution) and adopts `rid` as its visitor id. When a user signs in\n// (setTelemetryUser) the runtime emits one `user_identified` event carrying\n// `visitor_id` and the stored utm_* values, so a funnel can join \"visited the\n// site\" to \"is user X\". Every event also carries `visitor.id` and `app.client`.\n//\n// ── Consent ──────────────────────────────────────────────────────────────\n//\n// Nothing here runs until initBrowserTelemetry does, and that is the point an\n// app gates on consent. Before it (or after disableTelemetry) captureAttribution\n// stores nothing and withAttribution returns the URL untouched. Dependency-free\n// apart from track.ts, like it.\nimport { isTelemetryDisabled } from \"./track.js\";\n\n/** localStorage key of the captured `{rid, utm_*}` record. */\nexport const ATTRIBUTION_STORAGE_KEY = \"forge.attribution\";\n/** Default localStorage key of this browser's own anonymous visitor id. */\nexport const VISITOR_ID_STORAGE_KEY = \"forge.visitor_id\";\n/** localStorage key of the user id `user_identified` last fired for. */\nexport const IDENTIFIED_USER_STORAGE_KEY = \"forge.identified_user\";\n\n/** The query parameters captured from the landing URL, besides `rid`. */\nexport const UTM_PARAMS = [\n \"utm_source\",\n \"utm_medium\",\n \"utm_campaign\",\n \"utm_term\",\n \"utm_content\",\n] as const;\n\n/** Longest captured attribution value. */\nexport const MAX_ATTRIBUTION_VALUE_CHARS = 128;\n\n/** A visitor id: opaque, URL-safe. Anything else in `rid` is ignored. */\nconst RID_RE = /^[A-Za-z0-9_-]{8,64}$/;\n\n/** Where an event came from. `landing` is for a marketing site; pass any string for another kind. */\nexport type AppClient = \"web\" | \"electron\" | \"landing\" | (string & {});\n\nexport type Attribution = { rid?: string } & Partial<\n Record<(typeof UTM_PARAMS)[number], string>\n>;\n\ninterface Store {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n}\n\nlet active = false;\nlet visitorKey = VISITOR_ID_STORAGE_KEY;\nlet memory: Attribution = {};\nlet memoryVisitorId: string | undefined;\nlet memoryIdentified: string | undefined;\n\nfunction storage(): Store | null {\n try {\n return typeof window !== \"undefined\" ? window.localStorage : null;\n } catch {\n return null;\n }\n}\n\nfunction read(key: string): string | null {\n try {\n return storage()?.getItem(key) ?? null;\n } catch {\n return null;\n }\n}\n\nfunction write(key: string, value: string): void {\n try {\n storage()?.setItem(key, value);\n } catch {\n // Storage blocked or full: the in-memory copy still serves this page load.\n }\n}\n\n/** The `app.client` for this page: an explicit choice, else `electron` inside Electron, else `web`. */\nexport function detectAppClient(explicit?: string): AppClient {\n if (typeof explicit === \"string\" && explicit.trim() !== \"\") {\n return explicit.trim().slice(0, 32);\n }\n if (typeof window === \"undefined\") {\n return \"web\";\n }\n try {\n const proc = (\n window as unknown as { process?: { versions?: { electron?: unknown } } }\n ).process;\n if (proc?.versions?.electron) {\n return \"electron\";\n }\n if (\n typeof navigator !== \"undefined\" &&\n /\\bElectron\\//.test(navigator.userAgent)\n ) {\n return \"electron\";\n }\n } catch {\n // Fall through to web.\n }\n return \"web\";\n}\n\nfunction cleanValue(value: string | null | undefined): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n // eslint-disable-next-line no-control-regex\n const v = value.replace(/[\\u0000-\\u001f\\u007f]/g, \"\").trim();\n return v === \"\" ? undefined : v.slice(0, MAX_ATTRIBUTION_VALUE_CHARS);\n}\n\n/** Pull `rid` and `utm_*` out of a query string. Pure; exported for tests. */\nexport function parseAttribution(search: string): Attribution {\n const out: Attribution = {};\n let params: URLSearchParams;\n try {\n params = new URLSearchParams(search);\n } catch {\n return out;\n }\n const rid = params.get(\"rid\")?.trim();\n if (rid && RID_RE.test(rid)) {\n out.rid = rid;\n }\n for (const key of UTM_PARAMS) {\n const v = cleanValue(params.get(key));\n if (v) {\n out[key] = v;\n }\n }\n return out;\n}\n\nfunction readStoredAttribution(): Attribution {\n const raw = read(ATTRIBUTION_STORAGE_KEY);\n if (!raw) {\n return memory;\n }\n try {\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== \"object\" || parsed === null) {\n return memory;\n }\n const out: Attribution = {};\n const rid = (parsed as Record<string, unknown>).rid;\n if (typeof rid === \"string\" && RID_RE.test(rid)) {\n out.rid = rid;\n }\n for (const key of UTM_PARAMS) {\n const v = cleanValue((parsed as Record<string, string>)[key]);\n if (v) {\n out[key] = v;\n }\n }\n return out;\n } catch {\n return memory;\n }\n}\n\n/**\n * Start attribution for this page load: record `rid` and `utm_*` from the URL\n * (a visit that carries any replaces what was stored — last touch wins; one\n * that carries none keeps it) and note which storage key holds this browser's\n * own visitor id. Called by initBrowserTelemetry, i.e. only once telemetry may\n * run. Returns the visitor id. Never throws.\n */\nexport function captureAttribution(options?: {\n search?: string;\n visitorIdStorageKey?: string;\n}): string {\n if (isTelemetryDisabled()) {\n return \"\";\n }\n active = true;\n if (options?.visitorIdStorageKey) {\n visitorKey = options.visitorIdStorageKey;\n }\n const search =\n options?.search ??\n (typeof window !== \"undefined\" ? (window.location?.search ?? \"\") : \"\");\n const incoming = parseAttribution(search);\n if (Object.keys(incoming).length > 0) {\n memory = incoming;\n write(ATTRIBUTION_STORAGE_KEY, JSON.stringify(incoming));\n } else {\n memory = readStoredAttribution();\n }\n return visitorId();\n}\n\n/** The captured attribution, or `{}` before captureAttribution. */\nexport function getAttribution(): Attribution {\n return active ? readStoredAttribution() : {};\n}\n\nfunction newId(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c && typeof c.randomUUID === \"function\") {\n return c.randomUUID();\n }\n let s = \"\";\n for (let i = 0; i < 4; i++) {\n s += Math.floor(Math.random() * 0x100000000)\n .toString(16)\n .padStart(8, \"0\");\n }\n return s;\n}\n\n/**\n * This browser's visitor id: the `rid` it arrived with, else its own anonymous\n * id (created on first use and kept in localStorage). Empty before\n * captureAttribution.\n */\nexport function visitorId(): string {\n if (!active) {\n return \"\";\n }\n const rid = readStoredAttribution().rid;\n if (rid) {\n return rid;\n }\n const held = read(visitorKey);\n if (held && RID_RE.test(held)) {\n return held;\n }\n if (!memoryVisitorId) {\n memoryVisitorId = newId();\n }\n write(visitorKey, memoryVisitorId);\n return memoryVisitorId;\n}\n\n/**\n * Append this browser's `rid` and captured `utm_*` to an outbound URL — the\n * link from a marketing site to the app, or to a download. Parameters the URL\n * already carries are kept. Returns the URL unchanged when it is not an\n * absolute http(s) URL, before telemetry has started, and after\n * disableTelemetry() — so it is safe to wrap every outbound link.\n */\nexport function withAttribution(url: string): string {\n if (!active || isTelemetryDisabled() || typeof url !== \"string\") {\n return url;\n }\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return url;\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return url;\n }\n const stored = readStoredAttribution();\n const add: Record<string, string> = {};\n const rid = visitorId();\n if (rid) {\n add.rid = rid;\n }\n for (const key of UTM_PARAMS) {\n const v = stored[key];\n if (v) {\n add[key] = v;\n }\n }\n for (const [key, value] of Object.entries(add)) {\n if (!parsed.searchParams.has(key)) {\n parsed.searchParams.set(key, value);\n }\n }\n return parsed.toString();\n}\n\n/**\n * The props of the one-time `user_identified` event for `userId`, or null when\n * it already fired for that user in this browser (or attribution is not\n * running). Marks it fired. Empty ids never identify.\n */\nexport function takeIdentifyProps(\n userId: string,\n): Record<string, string> | null {\n if (!active || isTelemetryDisabled() || userId === \"\") {\n return null;\n }\n if ((read(IDENTIFIED_USER_STORAGE_KEY) ?? memoryIdentified) === userId) {\n return null;\n }\n memoryIdentified = userId;\n write(IDENTIFIED_USER_STORAGE_KEY, userId);\n const props: Record<string, string> = { visitor_id: visitorId() };\n const stored = readStoredAttribution();\n for (const key of UTM_PARAMS) {\n const v = stored[key];\n if (v) {\n props[key] = v;\n }\n }\n return props;\n}\n\n/** Test seam. */\nexport function resetAttributionState(): void {\n active = false;\n visitorKey = VISITOR_ID_STORAGE_KEY;\n memory = {};\n memoryVisitorId = undefined;\n memoryIdentified = undefined;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -52,6 +52,13 @@ export { SERVER_TRACE_ID_ATTRIBUTE, serverTraceIdOf } from "./trace-id.js";
52
52
  * "/telemetry" subpath, never the barrel.
53
53
  */
54
54
  export { disableTelemetry, setTelemetryUser, track, type TrackProps, type TrackValue, } from "./track.js";
55
+ /**
56
+ * Visitor identity and campaign attribution: `withAttribution(url)` appends
57
+ * this browser's `rid` and captured `utm_*` to an outbound link (a marketing
58
+ * site pointing at the app); the runtime reads them on arrival and emits one
59
+ * `user_identified` event at first sign-in. Consent-gated like track().
60
+ */
61
+ export { withAttribution, type AppClient, type Attribution, } from "./attribution.js";
55
62
  /**
56
63
  * Dev-only browser log forwarding: mirrors console output and uncaught errors
57
64
  * to the dev server, which prints them into
package/dist/index.js CHANGED
@@ -70,6 +70,13 @@ export { SERVER_TRACE_ID_ATTRIBUTE, serverTraceIdOf } from "./trace-id.js";
70
70
  * "/telemetry" subpath, never the barrel.
71
71
  */
72
72
  export { disableTelemetry, setTelemetryUser, track, } from "./track.js";
73
+ /**
74
+ * Visitor identity and campaign attribution: `withAttribution(url)` appends
75
+ * this browser's `rid` and captured `utm_*` to an outbound link (a marketing
76
+ * site pointing at the app); the runtime reads them on arrival and emits one
77
+ * `user_identified` event at first sign-in. Consent-gated like track().
78
+ */
79
+ export { withAttribution, } from "./attribution.js";
73
80
  /**
74
81
  * Dev-only browser log forwarding: mirrors console output and uncaught errors
75
82
  * to the dev server, which prints them into
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,yEAAyE;AACzE,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,4EAA4E;AAC5E,8EAA8E;AAC9E,gEAAgE;AAChE,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,4CAA4C;AAE5C,yEAAyE;AACzE,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,sEAAsE;AAEtE,mCAAmC;AACnC,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GAEjB,MAAM,mBAAmB,CAAC;AAE3B;;;GAGG;AACH,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,yBAAyB,EACzB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,OAAO,EACL,cAAc,EACd,YAAY,EACZ,iBAAiB,GAElB,MAAM,eAAe,CAAC;AAEvB;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAKhB,MAAM,YAAY,CAAC;AAEpB,qCAAqC;AACrC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,cAAc,EACd,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAEpB,qFAAqF;AACrF,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,GAIX,MAAM,cAAc,CAAC;AAEtB,gFAAgF;AAChF,OAAO,EAAE,YAAY,EAA0B,MAAM,gBAAgB,CAAC;AAEtE,+EAA+E;AAC/E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,yEAAyE;AACzE,OAAO,EACL,gBAAgB,GAMjB,MAAM,iBAAiB,CAAC;AAEzB,gEAAgE;AAChE,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AAEpE,uFAAuF;AACvF,OAAO,EACL,QAAQ,GAIT,MAAM,eAAe,CAAC;AAEvB;;;;GAIG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,GAErB,MAAM,qBAAqB,CAAC;AAE7B,qFAAqF;AACrF,OAAO,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAE3E;;;;;;GAMG;AACH,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,GAGN,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,GAGpB,MAAM,aAAa,CAAC","sourcesContent":["// @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Public barrel. Import batteries from \"@reliantlabs/forge-web-runtime\".\n//\n// Everything here is MECHANISM: the transport interceptor stack, the session,\n// the error boundary, the toast queue, the <Resource> tristate ladder, W3C\n// trace propagation, client RUM. Nothing here reaches into an application —\n// app-shaped values (auth state, event subscriptions, toast markup) arrive as\n// props, so an app can restyle or replace its own files freely.\n//\n// Presentation that a project is meant to own — the component library under\n// src/components/ui, globals.css, nav, layout — deliberately stays in the\n// scaffold and is NOT part of this package.\n\n// The per-group docs below are JSDoc, not `//`, on purpose: TypeScript's\n// declaration emit preserves JSDoc and DROPS line comments, and dist/ is now\n// the whole of what this package publishes. `forge project libraries` reads\n// this inventory out of the published barrel, and a consumer's editor shows\n// it on hover — both would get symbols with no explanation otherwise.\n\n/** Transport interceptor stack. */\nexport {\n authInterceptor,\n buildRuntimeInterceptors,\n errorNormalizeInterceptor,\n headerInterceptor,\n retryInterceptor,\n traceInterceptor,\n type RuntimeInterceptorConfig,\n} from \"./interceptors.js\";\n\n/**\n * Typed client errors, and the backend-framing strippers every surface that\n * SHOWS an error goes through.\n */\nexport {\n codeName,\n ConnectClientError,\n FORGE_ERROR_REASON_HEADER,\n isRetryableCode,\n normalizeError,\n stripServerFraming,\n userMessage,\n} from \"./errors.js\";\n\n/**\n * Mount-prefix helpers, for the URLs Next.js's router never sees — absolute\n * URLs handed to external systems that round-trip back into the app.\n *\n * Barrel-exported rather than behind a subpath: three string functions with\n * no imports, so there is no dependency footprint for a subpath to fence off.\n */\nexport {\n createBasePath,\n joinBasePath,\n normalizeBasePath,\n type BasePathHelpers,\n} from \"./basepath.js\";\n\n/**\n * Money and basis points as integers: format minor units through Intl, and\n * parse form input back to minor units without rounding.\n *\n * The web counterpart of forge/pkg/money — an int64 count of the currency's\n * minor unit plus an ISO 4217 code, exact past 2^53. Barrel-exported for the\n * same reason as the base-path helpers: it imports nothing.\n */\nexport {\n basisPointsToInput,\n currencyMinorDigits,\n formatBasisPoints,\n formatMinorUnits,\n minorUnitsToInput,\n parseBasisPoints,\n parseMinorUnits,\n type BasisPointsOptions,\n type MoneyOptions,\n type ParseBasisPointsOptions,\n type ParseMoneyOptions,\n} from \"./money.js\";\n\n/** W3C trace-context propagation. */\nexport {\n freshTraceparent,\n injectTraceContext,\n setTraceSampled,\n traceIdFrom,\n TRACEPARENT_RE,\n TRACERESPONSE_HEADER,\n} from \"./trace.js\";\n\n/** Session: the signed-in user, derived from the app's auth state and JWT claims. */\nexport {\n decodeJwt,\n SessionProvider,\n useSession,\n type JwtClaims,\n type Session,\n type SessionAuth,\n} from \"./session.js\";\n\n/** The app-shell provider set: session + error boundary + toast host in one. */\nexport { RuntimeShell, type RuntimeShellProps } from \"./providers.js\";\n\n/** Error boundary with a designed crash fallback, for a route or a subtree. */\nexport {\n DefaultErrorFallback,\n RuntimeErrorBoundary,\n} from \"./error-boundary.js\";\n\n/** The toast queue. The app supplies the event source and the markup. */\nexport {\n RuntimeToastHost,\n type RuntimeToast,\n type RuntimeToastHostProps,\n type RuntimeToastPayload,\n type RuntimeToastSink,\n type RuntimeToastVariant,\n} from \"./toast-host.js\";\n\n/** Route guarding: render a page only to a signed-in caller. */\nexport { RouteGuard, type RouteGuardProps } from \"./route-guard.js\";\n\n/** Generic data-table container: the tristate ladder, filter and cursor pagination. */\nexport {\n Resource,\n type ResourceColumn,\n type ResourceProps,\n type ResourceStatus,\n} from \"./resource.js\";\n\n/**\n * Record a caught error with the browser telemetry SDK, when one is running.\n * A no-op otherwise, so it is safe in any catch block. The SDK itself arrives\n * through the \"/telemetry\" subpath, never the barrel.\n */\nexport {\n reportException,\n setExceptionReporter,\n type ExceptionAttributes,\n} from \"./error-reporter.js\";\n\n/** The server trace id a failed RPC's error carries (ConnectClientError.traceId). */\nexport { SERVER_TRACE_ID_ATTRIBUTE, serverTraceIdOf } from \"./trace-id.js\";\n\n/**\n * Product events, the signed-in user and the consent kill-switch for browser\n * telemetry. Like reportException these are safe to call anywhere — no-ops on\n * the server and when telemetry is off — and calls made while the SDK is still\n * loading are held and replayed. The SDK itself arrives through the\n * \"/telemetry\" subpath, never the barrel.\n */\nexport {\n disableTelemetry,\n setTelemetryUser,\n track,\n type TrackProps,\n type TrackValue,\n} from \"./track.js\";\n\n/**\n * Dev-only browser log forwarding: mirrors console output and uncaught errors\n * to the dev server, which prints them into\n * `.forge/logs/<env>/frontend_<name>.log`. No-ops unless `dev: true`, and the\n * receiving endpoint exists only in a dev server — so a production bundle\n * neither posts nor has anywhere to post to.\n */\nexport {\n DEV_LOG_ENDPOINT,\n devLoggingInstalled,\n installDevLogging,\n uninstallDevLogging,\n type DevLoggingOptions,\n type DevLogPayload,\n} from \"./devlog.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,yEAAyE;AACzE,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,4EAA4E;AAC5E,8EAA8E;AAC9E,gEAAgE;AAChE,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,4CAA4C;AAE5C,yEAAyE;AACzE,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,sEAAsE;AAEtE,mCAAmC;AACnC,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GAEjB,MAAM,mBAAmB,CAAC;AAE3B;;;GAGG;AACH,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,yBAAyB,EACzB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,OAAO,EACL,cAAc,EACd,YAAY,EACZ,iBAAiB,GAElB,MAAM,eAAe,CAAC;AAEvB;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAKhB,MAAM,YAAY,CAAC;AAEpB,qCAAqC;AACrC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,cAAc,EACd,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAEpB,qFAAqF;AACrF,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,GAIX,MAAM,cAAc,CAAC;AAEtB,gFAAgF;AAChF,OAAO,EAAE,YAAY,EAA0B,MAAM,gBAAgB,CAAC;AAEtE,+EAA+E;AAC/E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,yEAAyE;AACzE,OAAO,EACL,gBAAgB,GAMjB,MAAM,iBAAiB,CAAC;AAEzB,gEAAgE;AAChE,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AAEpE,uFAAuF;AACvF,OAAO,EACL,QAAQ,GAIT,MAAM,eAAe,CAAC;AAEvB;;;;GAIG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,GAErB,MAAM,qBAAqB,CAAC;AAE7B,qFAAqF;AACrF,OAAO,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAE3E;;;;;;GAMG;AACH,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,GAGN,MAAM,YAAY,CAAC;AAEpB;;;;;GAKG;AACH,OAAO,EACL,eAAe,GAGhB,MAAM,kBAAkB,CAAC;AAE1B;;;;;;GAMG;AACH,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,GAGpB,MAAM,aAAa,CAAC","sourcesContent":["// @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Public barrel. Import batteries from \"@reliantlabs/forge-web-runtime\".\n//\n// Everything here is MECHANISM: the transport interceptor stack, the session,\n// the error boundary, the toast queue, the <Resource> tristate ladder, W3C\n// trace propagation, client RUM. Nothing here reaches into an application —\n// app-shaped values (auth state, event subscriptions, toast markup) arrive as\n// props, so an app can restyle or replace its own files freely.\n//\n// Presentation that a project is meant to own — the component library under\n// src/components/ui, globals.css, nav, layout — deliberately stays in the\n// scaffold and is NOT part of this package.\n\n// The per-group docs below are JSDoc, not `//`, on purpose: TypeScript's\n// declaration emit preserves JSDoc and DROPS line comments, and dist/ is now\n// the whole of what this package publishes. `forge project libraries` reads\n// this inventory out of the published barrel, and a consumer's editor shows\n// it on hover — both would get symbols with no explanation otherwise.\n\n/** Transport interceptor stack. */\nexport {\n authInterceptor,\n buildRuntimeInterceptors,\n errorNormalizeInterceptor,\n headerInterceptor,\n retryInterceptor,\n traceInterceptor,\n type RuntimeInterceptorConfig,\n} from \"./interceptors.js\";\n\n/**\n * Typed client errors, and the backend-framing strippers every surface that\n * SHOWS an error goes through.\n */\nexport {\n codeName,\n ConnectClientError,\n FORGE_ERROR_REASON_HEADER,\n isRetryableCode,\n normalizeError,\n stripServerFraming,\n userMessage,\n} from \"./errors.js\";\n\n/**\n * Mount-prefix helpers, for the URLs Next.js's router never sees — absolute\n * URLs handed to external systems that round-trip back into the app.\n *\n * Barrel-exported rather than behind a subpath: three string functions with\n * no imports, so there is no dependency footprint for a subpath to fence off.\n */\nexport {\n createBasePath,\n joinBasePath,\n normalizeBasePath,\n type BasePathHelpers,\n} from \"./basepath.js\";\n\n/**\n * Money and basis points as integers: format minor units through Intl, and\n * parse form input back to minor units without rounding.\n *\n * The web counterpart of forge/pkg/money — an int64 count of the currency's\n * minor unit plus an ISO 4217 code, exact past 2^53. Barrel-exported for the\n * same reason as the base-path helpers: it imports nothing.\n */\nexport {\n basisPointsToInput,\n currencyMinorDigits,\n formatBasisPoints,\n formatMinorUnits,\n minorUnitsToInput,\n parseBasisPoints,\n parseMinorUnits,\n type BasisPointsOptions,\n type MoneyOptions,\n type ParseBasisPointsOptions,\n type ParseMoneyOptions,\n} from \"./money.js\";\n\n/** W3C trace-context propagation. */\nexport {\n freshTraceparent,\n injectTraceContext,\n setTraceSampled,\n traceIdFrom,\n TRACEPARENT_RE,\n TRACERESPONSE_HEADER,\n} from \"./trace.js\";\n\n/** Session: the signed-in user, derived from the app's auth state and JWT claims. */\nexport {\n decodeJwt,\n SessionProvider,\n useSession,\n type JwtClaims,\n type Session,\n type SessionAuth,\n} from \"./session.js\";\n\n/** The app-shell provider set: session + error boundary + toast host in one. */\nexport { RuntimeShell, type RuntimeShellProps } from \"./providers.js\";\n\n/** Error boundary with a designed crash fallback, for a route or a subtree. */\nexport {\n DefaultErrorFallback,\n RuntimeErrorBoundary,\n} from \"./error-boundary.js\";\n\n/** The toast queue. The app supplies the event source and the markup. */\nexport {\n RuntimeToastHost,\n type RuntimeToast,\n type RuntimeToastHostProps,\n type RuntimeToastPayload,\n type RuntimeToastSink,\n type RuntimeToastVariant,\n} from \"./toast-host.js\";\n\n/** Route guarding: render a page only to a signed-in caller. */\nexport { RouteGuard, type RouteGuardProps } from \"./route-guard.js\";\n\n/** Generic data-table container: the tristate ladder, filter and cursor pagination. */\nexport {\n Resource,\n type ResourceColumn,\n type ResourceProps,\n type ResourceStatus,\n} from \"./resource.js\";\n\n/**\n * Record a caught error with the browser telemetry SDK, when one is running.\n * A no-op otherwise, so it is safe in any catch block. The SDK itself arrives\n * through the \"/telemetry\" subpath, never the barrel.\n */\nexport {\n reportException,\n setExceptionReporter,\n type ExceptionAttributes,\n} from \"./error-reporter.js\";\n\n/** The server trace id a failed RPC's error carries (ConnectClientError.traceId). */\nexport { SERVER_TRACE_ID_ATTRIBUTE, serverTraceIdOf } from \"./trace-id.js\";\n\n/**\n * Product events, the signed-in user and the consent kill-switch for browser\n * telemetry. Like reportException these are safe to call anywhere — no-ops on\n * the server and when telemetry is off — and calls made while the SDK is still\n * loading are held and replayed. The SDK itself arrives through the\n * \"/telemetry\" subpath, never the barrel.\n */\nexport {\n disableTelemetry,\n setTelemetryUser,\n track,\n type TrackProps,\n type TrackValue,\n} from \"./track.js\";\n\n/**\n * Visitor identity and campaign attribution: `withAttribution(url)` appends\n * this browser's `rid` and captured `utm_*` to an outbound link (a marketing\n * site pointing at the app); the runtime reads them on arrival and emits one\n * `user_identified` event at first sign-in. Consent-gated like track().\n */\nexport {\n withAttribution,\n type AppClient,\n type Attribution,\n} from \"./attribution.js\";\n\n/**\n * Dev-only browser log forwarding: mirrors console output and uncaught errors\n * to the dev server, which prints them into\n * `.forge/logs/<env>/frontend_<name>.log`. No-ops unless `dev: true`, and the\n * receiving endpoint exists only in a dev server — so a production bundle\n * neither posts nor has anywhere to post to.\n */\nexport {\n DEV_LOG_ENDPOINT,\n devLoggingInstalled,\n installDevLogging,\n uninstallDevLogging,\n type DevLoggingOptions,\n type DevLogPayload,\n} from \"./devlog.js\";\n"]}
@@ -66,6 +66,19 @@ export interface BrowserTelemetryConfig {
66
66
  replaySampleRate?: number;
67
67
  /** Verbose SDK diagnostics to the console. Default false. */
68
68
  debug?: boolean;
69
+ /**
70
+ * The `app.client` stamped on every event: `web`, `electron` or `landing`
71
+ * (any short string is accepted). Unset, it is `electron` inside Electron
72
+ * and `web` elsewhere.
73
+ */
74
+ appClient?: string;
75
+ /**
76
+ * localStorage key holding this browser's anonymous visitor id (stamped as
77
+ * `visitor.id`). Set it to adopt an id a site already keeps, e.g. a landing
78
+ * page's `reliant.visitor_id`. Default `forge.visitor_id`. A `rid` in the
79
+ * landing URL wins over it.
80
+ */
81
+ visitorIdStorageKey?: string;
69
82
  }
70
83
  /**
71
84
  * initBrowserTelemetry starts the HyperDX browser SDK once per page load and
package/dist/telemetry.js CHANGED
@@ -70,6 +70,7 @@
70
70
  // Fast Refresh. It is a no-op without a DOM (SSR, prerender) and without an
71
71
  // endpoint, and it never throws.
72
72
  import { trace } from "@opentelemetry/api";
73
+ import { captureAttribution, detectAppClient, takeIdentifyProps, } from "./attribution.js";
73
74
  import { setExceptionReporter } from "./error-reporter.js";
74
75
  import { registerTelemetryStartup } from "./telemetry-gate.js";
75
76
  import { isTelemetryDisabled, setTelemetrySink, } from "./track.js";
@@ -116,6 +117,14 @@ async function start(config, ingest) {
116
117
  const replay = decideReplay(config, sessionStorageOrNull(), Math.random);
117
118
  hdx.init(buildSdkOptions({ ...config, replay }, ingest, window.location.origin));
118
119
  tapSdkInputs();
120
+ // Every span carries where it came from and which visitor made it; the
121
+ // product-event view reads these as app.client and visitor.id.
122
+ hdx.setGlobalAttributes?.({
123
+ "app.client": detectAppClient(config.appClient),
124
+ "visitor.id": captureAttribution({
125
+ visitorIdStorageKey: config.visitorIdStorageKey,
126
+ }),
127
+ });
119
128
  setExceptionReporter((error, attributes) => {
120
129
  if (alreadyRecorded(error)) {
121
130
  return;
@@ -124,7 +133,15 @@ async function start(config, ingest) {
124
133
  });
125
134
  setTelemetrySink({
126
135
  track: (name, attributes) => hdx.addAction?.(name, attributes),
127
- setUser: (id) => hdx.setGlobalAttributes?.({ userId: id }),
136
+ setUser: (id) => {
137
+ hdx.setGlobalAttributes?.({ userId: id });
138
+ // The first sign-in of a user in this browser links the visitor to
139
+ // them: one event, never repeated for the same user.
140
+ const identify = takeIdentifyProps(id);
141
+ if (identify) {
142
+ hdx.addAction?.("user_identified", identify);
143
+ }
144
+ },
128
145
  disable: () => {
129
146
  // Back to "no SDK": reportException returns false, so a caller falls
130
147
  // back to console.error and the error does not vanish.
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Browser telemetry: the HyperDX browser SDK, initialised the forge way.\n//\n// Captures errors (uncaught, unhandled rejections, boundary-caught), console\n// output, fetch/XHR spans, document load, long tasks and web vitals, and\n// ships them to ClickStack through a SAME-ORIGIN `/_otel` route.\n//\n// ── Why HyperDX REPLACES the old OTel web wiring, not joins it ───────────\n//\n// @hyperdx/browser wraps @hyperdx/otel-web, which builds its own\n// WebTracerProvider and calls provider.register() — the global tracer\n// provider, context manager and propagator. A second registration (the old\n// `./otel` subpath did exactly that) is rejected by @opentelemetry/api with a\n// \"duplicate registration\" diagnostic and silently loses: whichever ran\n// second would simply never see a span. Two tracer providers cannot coexist\n// in one page, so there is one, and it is HyperDX's. `./otel` and its eight\n// @opentelemetry/* SDK peer dependencies are gone.\n//\n// traceparent on RPCs is unaffected. traceInterceptor (trace.ts) injects the\n// active span's context into every Connect call, or synthesises a valid\n// traceparent when none is active — with no SDK at all. With the SDK running,\n// the fetch span HyperDX opens for the call carries the same trace id, and\n// `apiOrigins` below makes that hold for a cross-origin API too (which is\n// every dev loop: the API binds its own port).\n//\n// ── The browser holds no secret ──────────────────────────────────────────\n//\n// `url` is `${location.origin}/_otel`. The browser talks only to its own\n// origin; a dev-server proxy (next.config.ts rewrites / Vite server.proxy) or\n// the deployment's ingress forwards `/_otel/v1/{traces,logs}` to the OTLP/HTTP\n// port of a collector, and that hop — never the bundle — owns credentials.\n//\n// The SDK nevertheless wants an `apiKey` and sends it as an `authorization`\n// header. We pass PLACEHOLDER_API_KEY, a constant that grants nothing and is\n// public by construction. A collector that enforces ingestion auth (ClickStack\n// `collectorAuthenticationEnforced`) rejects it with 401, so the proxy in\n// front of such a collector must strip the header or replace it with the real\n// key. The local ClickStack collector does not enforce, and ignores it.\n//\n// ── Replay: shipped, off, masked, media blocked ─────────────────────────\n//\n// The SDK bundles an rrweb recorder. It is DISABLED unless `replay: true`.\n// Privacy is forced, not offered: every input and all text is masked, canvas is\n// never recorded, and images, video, audio, iframes and embeds are BLOCKED\n// (replaced by a same-size placeholder; their pixels never leave the browser).\n// Images are not inlined and web fonts are not collected. There is deliberately\n// no option to turn any of that off while replay is on; an app that needs\n// unmasked replay passes its own SDK options.\n//\n// `replaySampleRate` (0..1, default 1) records only that share of SESSIONS. The\n// SDK has no session sampler, so the decision is made here, once, and held in\n// sessionStorage for the tab's session.\n//\n// ── Network URLs carry no query string ───────────────────────────────────\n//\n// fetch/XHR spans record the request URL, and a query string is where tokens,\n// OAuth codes and search terms live. The runtime rewrites `http.url` and\n// `url.full` on every fetch/XHR span to the URL without query or fragment.\n// The SDK's own `location.href` attribute (the page URL, stamped on every span)\n// has no client-side hook; the ingest side scrubs it.\n//\n// ── Loading ──────────────────────────────────────────────────────────────\n//\n// The SDK is ~600 KB minified (rrweb and the OTel web stack are bundled in\n// it), so it is imported lazily: bundlers split it into its own chunk that\n// loads after the app is interactive and costs the first paint nothing.\n// initBrowserTelemetry is therefore async and idempotent — every caller gets\n// the same promise, which is what survives React StrictMode's double mount and\n// Fast Refresh. It is a no-op without a DOM (SSR, prerender) and without an\n// endpoint, and it never throws.\nimport { trace } from \"@opentelemetry/api\";\n\nimport { setExceptionReporter } from \"./error-reporter.js\";\nimport { registerTelemetryStartup } from \"./telemetry-gate.js\";\nimport {\n isTelemetryDisabled,\n setTelemetrySink,\n type TrackValue,\n} from \"./track.js\";\n\n/** Where the browser ships telemetry: this origin's `/_otel`. */\nexport const DEFAULT_INGEST_PATH = \"/_otel\";\n\n/**\n * The apiKey handed to the SDK. Public by construction: it identifies\n * nothing and authorises nothing. See the header — the proxy or collector\n * owns the real credential.\n */\nexport const PLACEHOLDER_API_KEY = \"forge-browser-public\";\n\nexport interface BrowserTelemetryConfig {\n /** `service.name` on everything this page emits. */\n serviceName: string;\n /** `service.version`. Omit for none. */\n serviceVersion?: string;\n /**\n * The forge environment name. Emitted as BOTH `deployment.environment.name`\n * (current semconv) and `deployment.environment` (the legacy key HyperDX's\n * default sources still key on).\n */\n environment?: string;\n /** `service.namespace`, conventionally the project name. */\n serviceNamespace?: string;\n /**\n * The forge frontend's name, emitted as the `forge.frontend` resource\n * attribute. Unlike `serviceName` (which an app may override to anything) it\n * is the stable key that joins this page's telemetry to the frontend and its\n * source maps. Omit for none.\n */\n frontend?: string;\n /**\n * Ingest base: a path on this origin (`/_otel`) or, as an escape hatch, an\n * absolute URL. Unset, empty, or `off` disables telemetry — the app runs\n * untraced rather than throwing, which is what makes it safe to call\n * unconditionally from the app shell.\n */\n endpoint?: string;\n /**\n * Origins (or URLs — only the origin is used) of the app's own backend.\n * Requests to them always get a `traceparent` from the SDK's fetch/XHR\n * spans. This page's own origin is always included.\n */\n apiOrigins?: Array<string | undefined>;\n /**\n * Mirror console.log/info/warn/debug into telemetry spans. Default true;\n * pass false when console output can hold user content. It does NOT stop\n * `console.error(err)` from being recorded as an error: the SDK's error\n * instrumentation is separate and always on.\n */\n consoleCapture?: boolean;\n /**\n * Capture full request/response headers AND BODIES on fetch/XHR spans.\n * Default false, and it should stay false: bodies are user data.\n */\n advancedNetworkCapture?: boolean;\n /**\n * Session replay (rrweb). Default false. When true, all text and inputs are\n * masked, media is blocked and canvas is not recorded (see the header).\n */\n replay?: boolean;\n /**\n * The share of sessions to record when `replay` is true, 0..1. Default 1\n * (every session). Decided once per session and held in sessionStorage. A\n * value that is not a number records nothing. An app with its own policy\n * (every session of a new user, a fraction of the rest) computes the rate\n * it wants for this session and passes it here.\n */\n replaySampleRate?: number;\n /** Verbose SDK diagnostics to the console. Default false. */\n debug?: boolean;\n}\n\n/** The slice of the SDK this module calls. Typed here, not imported, so the module loads without it. */\ninterface HyperDXClient {\n init(config: Record<string, unknown>): void;\n recordException(error: unknown, attributes?: Record<string, unknown>): void;\n addAction?(name: string, attributes?: Record<string, TrackValue>): void;\n setGlobalAttributes?(attributes: Record<string, string>): void;\n stopSessionRecorder?(): void;\n}\n\nlet started: Promise<boolean> | null = null;\n\n/**\n * initBrowserTelemetry starts the HyperDX browser SDK once per page load and\n * resolves to whether it is running. See the module header for the contract.\n */\nexport function initBrowserTelemetry(\n config: BrowserTelemetryConfig,\n): Promise<boolean> {\n if (typeof window === \"undefined\") {\n return Promise.resolve(false);\n }\n if (started) {\n return started;\n }\n if (isTelemetryDisabled()) {\n return Promise.resolve(false);\n }\n const ingest = resolveIngestUrl(config.endpoint, window.location.origin);\n if (!ingest) {\n // Nothing will ever drain track() calls held for the SDK; let them go.\n setTelemetrySink(null);\n return Promise.resolve(false);\n }\n started = start(config, ingest);\n registerTelemetryStartup(started);\n return started;\n}\n\nasync function start(\n config: BrowserTelemetryConfig,\n ingest: string,\n): Promise<boolean> {\n try {\n const hdx = resolveClient(await import(\"@hyperdx/browser\"));\n if (!hdx || isTelemetryDisabled()) {\n setTelemetrySink(null);\n return false;\n }\n const replay = decideReplay(config, sessionStorageOrNull(), Math.random);\n hdx.init(\n buildSdkOptions({ ...config, replay }, ingest, window.location.origin),\n );\n tapSdkInputs();\n setExceptionReporter((error, attributes) => {\n if (alreadyRecorded(error)) {\n return;\n }\n hdx.recordException(error, attributes);\n });\n setTelemetrySink({\n track: (name, attributes) => hdx.addAction?.(name, attributes),\n setUser: (id) => hdx.setGlobalAttributes?.({ userId: id }),\n disable: () => {\n // Back to \"no SDK\": reportException returns false, so a caller falls\n // back to console.error and the error does not vanish.\n setExceptionReporter(null);\n hdx.stopSessionRecorder?.();\n },\n });\n return true;\n } catch (err) {\n // Blocked chunk, ad-blocker, SDK throw: telemetry must never break the app.\n console.debug(\"[telemetry] disabled:\", err);\n setTelemetrySink(null);\n return false;\n }\n}\n\n// ── One failure, one record ──────────────────────────────────────────────\n//\n// The SDK records an Error that reaches window \"error\", window\n// \"unhandledrejection\", or console.error. React (18 and 19) console.errors an\n// error a boundary catches BEFORE it calls componentDidCatch, and Next\n// dispatches an uncaught render error to window \"error\" — so by the time\n// reportException runs, the SDK has usually recorded that very error already.\n// Recording it again would count one failure twice, and the issue layer\n// (which groups by fingerprint) would report two.\n//\n// So the taps below remember which Error OBJECTS the SDK was shown, and the\n// installed recorder skips those. Whatever the SDK did not see — a custom\n// onCaughtError that silences React's logging, an error rethrown as a new\n// object — is still recorded. The console tap is installed after the SDK's own\n// wrapper, so it sits outermost and only observes; every call is forwarded\n// untouched.\n//\n// The cost is the React component stack: the SDK's record of a boundary-caught\n// error comes from console.error(error), which carries the error and its\n// stack but not React's componentStack, and attributes passed to a skipped\n// reportException are dropped with it.\nconst seenBySdk = new WeakSet<object>();\nlet tapped = false;\n\nfunction noteSeen(value: unknown): void {\n if (typeof value === \"object\" && value !== null) {\n seenBySdk.add(value);\n }\n}\n\nfunction tapSdkInputs(): void {\n if (tapped) {\n return;\n }\n tapped = true;\n const wrapped = console.error;\n console.error = function (this: unknown, ...args: unknown[]) {\n for (const arg of args) {\n if (arg instanceof Error) {\n noteSeen(arg);\n }\n }\n return wrapped.apply(this, args);\n };\n window.addEventListener(\"error\", (e) => noteSeen(e.error), true);\n window.addEventListener(\n \"unhandledrejection\",\n (e) => noteSeen(e.reason),\n true,\n );\n}\n\nfunction alreadyRecorded(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && seenBySdk.has(error);\n}\n\n/**\n * The options passed to HyperDX.init. Exported for tests and for apps that\n * want to see exactly what is configured — it is pure.\n */\nexport function buildSdkOptions(\n config: BrowserTelemetryConfig,\n ingestUrl: string,\n pageOrigin: string,\n): Record<string, unknown> {\n // `config.replay` is the FINAL decision here. initBrowserTelemetry resolves\n // replaySampleRate into it (decideReplay) before calling this.\n const replay = config.replay === true;\n const resourceAttributes: Record<string, string> = {};\n if (config.serviceVersion) {\n resourceAttributes[\"service.version\"] = config.serviceVersion;\n }\n if (config.serviceNamespace) {\n resourceAttributes[\"service.namespace\"] = config.serviceNamespace;\n }\n if (config.frontend) {\n resourceAttributes[\"forge.frontend\"] = config.frontend;\n }\n if (config.environment) {\n resourceAttributes[\"deployment.environment.name\"] = config.environment;\n resourceAttributes[\"deployment.environment\"] = config.environment;\n }\n\n const targets = propagationTargets(pageOrigin, config.apiOrigins);\n const advancedNetworkCapture = config.advancedNetworkCapture ?? false;\n const options: Record<string, unknown> = {\n apiKey: PLACEHOLDER_API_KEY,\n service: config.serviceName,\n url: ingestUrl,\n tracePropagationTargets: targets,\n // The SDK would otherwise trace its own exports, and it must never\n // observe the route it ships through.\n ignoreUrls: [new RegExp(`^${escapeRegExp(ingestUrl)}`)],\n consoleCapture: config.consoleCapture ?? true,\n advancedNetworkCapture,\n // The SDK spreads this over ITS fetch/xhr config as whole keys, so\n // everything it would have set (propagation targets, the capture switch)\n // must be restated here or it is lost.\n instrumentations: {\n fetch: {\n propagateTraceHeaderCorsUrls: targets,\n advancedNetworkCapture: () => advancedNetworkCapture,\n applyCustomAttributesOnSpan: stripNetworkQuery,\n },\n xhr: {\n propagateTraceHeaderCorsUrls: targets,\n advancedNetworkCapture: () => advancedNetworkCapture,\n applyCustomAttributesOnSpan: stripNetworkQuery,\n },\n },\n // Polls for window.Intercom on a timer; nothing here uses Intercom.\n disableIntercom: true,\n disableReplay: !replay,\n otelResourceAttributes: resourceAttributes,\n debug: config.debug ?? false,\n // Privacy is forced and unconditional: no path through this function\n // yields an unmasked or media-recording replay. The SDK's own default\n // masks inputs but NOT text, and text is where chat content and records\n // live.\n maskAllInputs: true,\n maskAllText: true,\n recordCanvas: false,\n blockSelector: REPLAY_BLOCK_SELECTOR,\n replay: { inlineImages: false, collectFonts: false },\n };\n return options;\n}\n\n/** Elements a replay never records: their pixels stay in the browser. */\nexport const REPLAY_BLOCK_SELECTOR =\n \"img, picture, video, audio, iframe, object, embed\";\n\n/** Where a replay sampling decision is held for the tab's session. */\nexport const REPLAY_SAMPLE_KEY = \"forge.replay.sampled\";\n\n/** The slice of Storage the sampling decision needs. */\ninterface DecisionStore {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n}\n\n/**\n * Whether this session records a replay: config.replay, then the sample rate,\n * drawn once and remembered in `store` so a reload (or a route change that\n * re-runs init) never re-rolls into a session that only half-recorded. With no\n * usable store the draw is made each load. Pure given its inputs; exported for\n * tests.\n */\nexport function decideReplay(\n config: Pick<BrowserTelemetryConfig, \"replay\" | \"replaySampleRate\">,\n store: DecisionStore | null,\n random: () => number,\n): boolean {\n if (config.replay !== true) {\n return false;\n }\n const rate = clampRate(config.replaySampleRate);\n if (rate >= 1) {\n return true;\n }\n if (rate <= 0) {\n return false;\n }\n try {\n const held = store?.getItem(REPLAY_SAMPLE_KEY);\n if (held === \"1\" || held === \"0\") {\n return held === \"1\";\n }\n } catch {\n // Storage blocked: fall through to a fresh draw.\n }\n const sampled = random() < rate;\n try {\n store?.setItem(REPLAY_SAMPLE_KEY, sampled ? \"1\" : \"0\");\n } catch {\n // Not persisted: the next load draws again.\n }\n return sampled;\n}\n\n// Unset is \"every session\"; anything that is not a number is a bug, and a bug\n// must not start recording people.\nfunction clampRate(rate: number | undefined): number {\n if (rate === undefined) {\n return 1;\n }\n if (typeof rate !== \"number\" || Number.isNaN(rate)) {\n return 0;\n }\n return Math.min(1, Math.max(0, rate));\n}\n\nfunction sessionStorageOrNull(): DecisionStore | null {\n try {\n return window.sessionStorage;\n } catch {\n return null;\n }\n}\n\n/** The span attributes that hold a request URL. */\nconst URL_ATTRIBUTES = [\"http.url\", \"url.full\"] as const;\n\n/** `url` without its query string and fragment. */\nexport function stripUrlQuery(url: string): string {\n const cut = url.search(/[?#]/);\n return cut === -1 ? url : url.slice(0, cut);\n}\n\ninterface UrlSpan {\n attributes?: Record<string, unknown>;\n setAttribute(key: string, value: string): unknown;\n}\n\n// applyCustomAttributesOnSpan for both instrumentations: it runs before the\n// span ends, so the rewritten value is the one exported.\nfunction stripNetworkQuery(span: UrlSpan): void {\n for (const key of URL_ATTRIBUTES) {\n const value = span.attributes?.[key];\n if (typeof value === \"string\") {\n const stripped = stripUrlQuery(value);\n if (stripped !== value) {\n span.setAttribute(key, stripped);\n }\n }\n }\n}\n\n/**\n * resolveIngestUrl turns the configured endpoint into the absolute URL the SDK\n * is given, or null when telemetry is off. A path resolves against the page\n * origin, so the default is same-origin by construction.\n */\nexport function resolveIngestUrl(\n endpoint: string | undefined,\n pageOrigin: string,\n): string | null {\n const value = (endpoint ?? \"\").trim();\n if (value === \"\" || value.toLowerCase() === \"off\") {\n return null;\n }\n try {\n return new URL(value, pageOrigin).toString().replace(/\\/+$/, \"\");\n } catch {\n return null;\n }\n}\n\nfunction propagationTargets(\n pageOrigin: string,\n apiOrigins: Array<string | undefined> = [],\n): RegExp[] {\n const origins = new Set<string>([pageOrigin]);\n for (const raw of apiOrigins) {\n if (!raw) {\n continue;\n }\n try {\n origins.add(new URL(raw, pageOrigin).origin);\n } catch {\n // An unparsable origin is not a target.\n }\n }\n return [...origins].map((o) => new RegExp(`^${escapeRegExp(o)}(?:[/?#]|$)`));\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n// @hyperdx/browser is a UMD/CJS bundle whose module.exports IS the client, and\n// its methods use `this`. A bundler hands back a module NAMESPACE that exposes\n// both the client's members at the top level (as read-only getters) and the\n// client itself as `default`. Calling init on the namespace binds `this` to that\n// frozen object and the SDK throws on its first property write, which disabled\n// telemetry silently. So descend to the deepest `default` that is still a\n// client, the real instance, and never settle for the namespace in front of it.\nfunction resolveClient(mod: unknown): HyperDXClient | null {\n let cur: unknown = mod;\n for (let depth = 0; depth < 4; depth++) {\n const next = peek(cur, \"default\");\n if (!isClient(next)) {\n break;\n }\n cur = next;\n }\n return isClient(cur) ? cur : null;\n}\n\nfunction isClient(v: unknown): v is HyperDXClient {\n return (\n typeof peek(v, \"init\") === \"function\" &&\n typeof peek(v, \"recordException\") === \"function\"\n );\n}\n\n// A module namespace object may throw on an absent key (strict ESM\n// interop shims and test doubles do), and \"absent\" is an answer here.\nfunction peek(obj: unknown, key: string): unknown {\n try {\n return (obj as Record<string, unknown>)[key];\n } catch {\n return undefined;\n }\n}\n\n/** The tracer to open manual spans on. Resolves HyperDX's provider once it is running. */\nexport function getTracer(name = \"default\") {\n return trace.getTracer(name);\n}\n\nexport { context, propagation, trace } from \"@opentelemetry/api\";\n"]}
1
+ {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,yEAAyE;AACzE,EAAE;AACF,6EAA6E;AAC7E,yEAAyE;AACzE,iEAAiE;AACjE,EAAE;AACF,4EAA4E;AAC5E,EAAE;AACF,iEAAiE;AACjE,sEAAsE;AACtE,2EAA2E;AAC3E,8EAA8E;AAC9E,wEAAwE;AACxE,4EAA4E;AAC5E,4EAA4E;AAC5E,mDAAmD;AACnD,EAAE;AACF,6EAA6E;AAC7E,wEAAwE;AACxE,8EAA8E;AAC9E,2EAA2E;AAC3E,0EAA0E;AAC1E,+CAA+C;AAC/C,EAAE;AACF,4EAA4E;AAC5E,EAAE;AACF,yEAAyE;AACzE,8EAA8E;AAC9E,+EAA+E;AAC/E,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,+EAA+E;AAC/E,0EAA0E;AAC1E,8EAA8E;AAC9E,wEAAwE;AACxE,EAAE;AACF,2EAA2E;AAC3E,EAAE;AACF,2EAA2E;AAC3E,gFAAgF;AAChF,2EAA2E;AAC3E,+EAA+E;AAC/E,gFAAgF;AAChF,0EAA0E;AAC1E,8CAA8C;AAC9C,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,EAAE;AACF,8EAA8E;AAC9E,yEAAyE;AACzE,2EAA2E;AAC3E,gFAAgF;AAChF,sDAAsD;AACtD,EAAE;AACF,4EAA4E;AAC5E,EAAE;AACF,2EAA2E;AAC3E,2EAA2E;AAC3E,wEAAwE;AACxE,6EAA6E;AAC7E,+EAA+E;AAC/E,4EAA4E;AAC5E,iCAAiC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EACL,mBAAmB,EACnB,gBAAgB,GAEjB,MAAM,YAAY,CAAC;AAEpB,iEAAiE;AACjE,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAQ,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,sBAAsB,CAAC;AAsF1D,IAAI,OAAO,GAA4B,IAAI,CAAC;AAE5C;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA8B;IAE9B,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IACD,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,mBAAmB,EAAE,EAAE,CAAC;QAC1B,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IACD,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,uEAAuE;QACvE,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,GAAG,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,wBAAwB,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,KAAK,UAAU,KAAK,CAClB,MAA8B,EAC9B,MAAc;IAEd,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC;QAC5D,IAAI,CAAC,GAAG,IAAI,mBAAmB,EAAE,EAAE,CAAC;YAClC,gBAAgB,CAAC,IAAI,CAAC,CAAC;YACvB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,oBAAoB,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACzE,GAAG,CAAC,IAAI,CACN,eAAe,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CACvE,CAAC;QACF,YAAY,EAAE,CAAC;QACf,uEAAuE;QACvE,+DAA+D;QAC/D,GAAG,CAAC,mBAAmB,EAAE,CAAC;YACxB,YAAY,EAAE,eAAe,CAAC,MAAM,CAAC,SAAS,CAAC;YAC/C,YAAY,EAAE,kBAAkB,CAAC;gBAC/B,mBAAmB,EAAE,MAAM,CAAC,mBAAmB;aAChD,CAAC;SACH,CAAC,CAAC;QACH,oBAAoB,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;YACzC,IAAI,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC3B,OAAO;YACT,CAAC;YACD,GAAG,CAAC,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QACH,gBAAgB,CAAC;YACf,KAAK,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC;YAC9D,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE;gBACd,GAAG,CAAC,mBAAmB,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;gBAC1C,mEAAmE;gBACnE,qDAAqD;gBACrD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,EAAE,CAAC,CAAC;gBACvC,IAAI,QAAQ,EAAE,CAAC;oBACb,GAAG,CAAC,SAAS,EAAE,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;gBAC/C,CAAC;YACH,CAAC;YACD,OAAO,EAAE,GAAG,EAAE;gBACZ,qEAAqE;gBACrE,uDAAuD;gBACvD,oBAAoB,CAAC,IAAI,CAAC,CAAC;gBAC3B,GAAG,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAC9B,CAAC;SACF,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,4EAA4E;QAC5E,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;QAC5C,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,EAAE;AACF,+DAA+D;AAC/D,8EAA8E;AAC9E,uEAAuE;AACvE,yEAAyE;AACzE,8EAA8E;AAC9E,wEAAwE;AACxE,kDAAkD;AAClD,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,0EAA0E;AAC1E,+EAA+E;AAC/E,2EAA2E;AAC3E,aAAa;AACb,EAAE;AACF,+EAA+E;AAC/E,yEAAyE;AACzE,2EAA2E;AAC3E,uCAAuC;AACvC,MAAM,SAAS,GAAG,IAAI,OAAO,EAAU,CAAC;AACxC,IAAI,MAAM,GAAG,KAAK,CAAC;AAEnB,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED,SAAS,YAAY;IACnB,IAAI,MAAM,EAAE,CAAC;QACX,OAAO;IACT,CAAC;IACD,MAAM,GAAG,IAAI,CAAC;IACd,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC;IAC9B,OAAO,CAAC,KAAK,GAAG,UAAyB,GAAG,IAAe;QACzD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;gBACzB,QAAQ,CAAC,GAAG,CAAC,CAAC;YAChB,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC,CAAC;IACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;IACjE,MAAM,CAAC,gBAAgB,CACrB,oBAAoB,EACpB,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,EACzB,IAAI,CACL,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC7E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,MAA8B,EAC9B,SAAiB,EACjB,UAAkB;IAElB,4EAA4E;IAC5E,+DAA+D;IAC/D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC;IACtC,MAAM,kBAAkB,GAA2B,EAAE,CAAC;IACtD,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC1B,kBAAkB,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC;IAChE,CAAC;IACD,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAC5B,kBAAkB,CAAC,mBAAmB,CAAC,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACpE,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,kBAAkB,CAAC,gBAAgB,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC;IACzD,CAAC;IACD,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,kBAAkB,CAAC,6BAA6B,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC;QACvE,kBAAkB,CAAC,wBAAwB,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC;IACpE,CAAC;IAED,MAAM,OAAO,GAAG,kBAAkB,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAClE,MAAM,sBAAsB,GAAG,MAAM,CAAC,sBAAsB,IAAI,KAAK,CAAC;IACtE,MAAM,OAAO,GAA4B;QACvC,MAAM,EAAE,mBAAmB;QAC3B,OAAO,EAAE,MAAM,CAAC,WAAW;QAC3B,GAAG,EAAE,SAAS;QACd,uBAAuB,EAAE,OAAO;QAChC,mEAAmE;QACnE,sCAAsC;QACtC,UAAU,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,YAAY,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QACvD,cAAc,EAAE,MAAM,CAAC,cAAc,IAAI,IAAI;QAC7C,sBAAsB;QACtB,mEAAmE;QACnE,yEAAyE;QACzE,uCAAuC;QACvC,gBAAgB,EAAE;YAChB,KAAK,EAAE;gBACL,4BAA4B,EAAE,OAAO;gBACrC,sBAAsB,EAAE,GAAG,EAAE,CAAC,sBAAsB;gBACpD,2BAA2B,EAAE,iBAAiB;aAC/C;YACD,GAAG,EAAE;gBACH,4BAA4B,EAAE,OAAO;gBACrC,sBAAsB,EAAE,GAAG,EAAE,CAAC,sBAAsB;gBACpD,2BAA2B,EAAE,iBAAiB;aAC/C;SACF;QACD,oEAAoE;QACpE,eAAe,EAAE,IAAI;QACrB,aAAa,EAAE,CAAC,MAAM;QACtB,sBAAsB,EAAE,kBAAkB;QAC1C,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,KAAK;QAC5B,qEAAqE;QACrE,sEAAsE;QACtE,wEAAwE;QACxE,QAAQ;QACR,aAAa,EAAE,IAAI;QACnB,WAAW,EAAE,IAAI;QACjB,YAAY,EAAE,KAAK;QACnB,aAAa,EAAE,qBAAqB;QACpC,MAAM,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE;KACrD,CAAC;IACF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,MAAM,qBAAqB,GAChC,mDAAmD,CAAC;AAEtD,sEAAsE;AACtE,MAAM,CAAC,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AAQxD;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAmE,EACnE,KAA2B,EAC3B,MAAoB;IAEpB,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;IAChD,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;QACd,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,KAAK,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAC/C,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjC,OAAO,IAAI,KAAK,GAAG,CAAC;QACtB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,iDAAiD;IACnD,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IAChC,IAAI,CAAC;QACH,KAAK,EAAE,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,4CAA4C;IAC9C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,8EAA8E;AAC9E,mCAAmC;AACnC,SAAS,SAAS,CAAC,IAAwB;IACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AACxC,CAAC;AAED,SAAS,oBAAoB;IAC3B,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,cAAc,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,mDAAmD;AACnD,MAAM,cAAc,GAAG,CAAC,UAAU,EAAE,UAAU,CAAU,CAAC;AAEzD,mDAAmD;AACnD,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC9C,CAAC;AAOD,4EAA4E;AAC5E,yDAAyD;AACzD,SAAS,iBAAiB,CAAC,IAAa;IACtC,KAAK,MAAM,GAAG,IAAI,cAAc,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;YACtC,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;gBACvB,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAA4B,EAC5B,UAAkB;IAElB,MAAM,KAAK,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACtC,IAAI,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,KAAK,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACnE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CACzB,UAAkB,EAClB,aAAwC,EAAE;IAE1C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,CAAC,UAAU,CAAC,CAAC,CAAC;IAC9C,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,SAAS;QACX,CAAC;QACD,IAAI,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;QAC/C,CAAC;QAAC,MAAM,CAAC;YACP,wCAAwC;QAC1C,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,iFAAiF;AACjF,+EAA+E;AAC/E,0EAA0E;AAC1E,gFAAgF;AAChF,SAAS,aAAa,CAAC,GAAY;IACjC,IAAI,GAAG,GAAY,GAAG,CAAC;IACvB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAClC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACpB,MAAM;QACR,CAAC;QACD,GAAG,GAAG,IAAI,CAAC;IACb,CAAC;IACD,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACpC,CAAC;AAED,SAAS,QAAQ,CAAC,CAAU;IAC1B,OAAO,CACL,OAAO,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,UAAU;QACrC,OAAO,IAAI,CAAC,CAAC,EAAE,iBAAiB,CAAC,KAAK,UAAU,CACjD,CAAC;AACJ,CAAC;AAED,mEAAmE;AACnE,sEAAsE;AACtE,SAAS,IAAI,CAAC,GAAY,EAAE,GAAW;IACrC,IAAI,CAAC;QACH,OAAQ,GAA+B,CAAC,GAAG,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,SAAS,CAAC,IAAI,GAAG,SAAS;IACxC,OAAO,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC;AAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Browser telemetry: the HyperDX browser SDK, initialised the forge way.\n//\n// Captures errors (uncaught, unhandled rejections, boundary-caught), console\n// output, fetch/XHR spans, document load, long tasks and web vitals, and\n// ships them to ClickStack through a SAME-ORIGIN `/_otel` route.\n//\n// ── Why HyperDX REPLACES the old OTel web wiring, not joins it ───────────\n//\n// @hyperdx/browser wraps @hyperdx/otel-web, which builds its own\n// WebTracerProvider and calls provider.register() — the global tracer\n// provider, context manager and propagator. A second registration (the old\n// `./otel` subpath did exactly that) is rejected by @opentelemetry/api with a\n// \"duplicate registration\" diagnostic and silently loses: whichever ran\n// second would simply never see a span. Two tracer providers cannot coexist\n// in one page, so there is one, and it is HyperDX's. `./otel` and its eight\n// @opentelemetry/* SDK peer dependencies are gone.\n//\n// traceparent on RPCs is unaffected. traceInterceptor (trace.ts) injects the\n// active span's context into every Connect call, or synthesises a valid\n// traceparent when none is active — with no SDK at all. With the SDK running,\n// the fetch span HyperDX opens for the call carries the same trace id, and\n// `apiOrigins` below makes that hold for a cross-origin API too (which is\n// every dev loop: the API binds its own port).\n//\n// ── The browser holds no secret ──────────────────────────────────────────\n//\n// `url` is `${location.origin}/_otel`. The browser talks only to its own\n// origin; a dev-server proxy (next.config.ts rewrites / Vite server.proxy) or\n// the deployment's ingress forwards `/_otel/v1/{traces,logs}` to the OTLP/HTTP\n// port of a collector, and that hop — never the bundle — owns credentials.\n//\n// The SDK nevertheless wants an `apiKey` and sends it as an `authorization`\n// header. We pass PLACEHOLDER_API_KEY, a constant that grants nothing and is\n// public by construction. A collector that enforces ingestion auth (ClickStack\n// `collectorAuthenticationEnforced`) rejects it with 401, so the proxy in\n// front of such a collector must strip the header or replace it with the real\n// key. The local ClickStack collector does not enforce, and ignores it.\n//\n// ── Replay: shipped, off, masked, media blocked ─────────────────────────\n//\n// The SDK bundles an rrweb recorder. It is DISABLED unless `replay: true`.\n// Privacy is forced, not offered: every input and all text is masked, canvas is\n// never recorded, and images, video, audio, iframes and embeds are BLOCKED\n// (replaced by a same-size placeholder; their pixels never leave the browser).\n// Images are not inlined and web fonts are not collected. There is deliberately\n// no option to turn any of that off while replay is on; an app that needs\n// unmasked replay passes its own SDK options.\n//\n// `replaySampleRate` (0..1, default 1) records only that share of SESSIONS. The\n// SDK has no session sampler, so the decision is made here, once, and held in\n// sessionStorage for the tab's session.\n//\n// ── Network URLs carry no query string ───────────────────────────────────\n//\n// fetch/XHR spans record the request URL, and a query string is where tokens,\n// OAuth codes and search terms live. The runtime rewrites `http.url` and\n// `url.full` on every fetch/XHR span to the URL without query or fragment.\n// The SDK's own `location.href` attribute (the page URL, stamped on every span)\n// has no client-side hook; the ingest side scrubs it.\n//\n// ── Loading ──────────────────────────────────────────────────────────────\n//\n// The SDK is ~600 KB minified (rrweb and the OTel web stack are bundled in\n// it), so it is imported lazily: bundlers split it into its own chunk that\n// loads after the app is interactive and costs the first paint nothing.\n// initBrowserTelemetry is therefore async and idempotent — every caller gets\n// the same promise, which is what survives React StrictMode's double mount and\n// Fast Refresh. It is a no-op without a DOM (SSR, prerender) and without an\n// endpoint, and it never throws.\nimport { trace } from \"@opentelemetry/api\";\n\nimport {\n captureAttribution,\n detectAppClient,\n takeIdentifyProps,\n} from \"./attribution.js\";\nimport { setExceptionReporter } from \"./error-reporter.js\";\nimport { registerTelemetryStartup } from \"./telemetry-gate.js\";\nimport {\n isTelemetryDisabled,\n setTelemetrySink,\n type TrackValue,\n} from \"./track.js\";\n\n/** Where the browser ships telemetry: this origin's `/_otel`. */\nexport const DEFAULT_INGEST_PATH = \"/_otel\";\n\n/**\n * The apiKey handed to the SDK. Public by construction: it identifies\n * nothing and authorises nothing. See the header — the proxy or collector\n * owns the real credential.\n */\nexport const PLACEHOLDER_API_KEY = \"forge-browser-public\";\n\nexport interface BrowserTelemetryConfig {\n /** `service.name` on everything this page emits. */\n serviceName: string;\n /** `service.version`. Omit for none. */\n serviceVersion?: string;\n /**\n * The forge environment name. Emitted as BOTH `deployment.environment.name`\n * (current semconv) and `deployment.environment` (the legacy key HyperDX's\n * default sources still key on).\n */\n environment?: string;\n /** `service.namespace`, conventionally the project name. */\n serviceNamespace?: string;\n /**\n * The forge frontend's name, emitted as the `forge.frontend` resource\n * attribute. Unlike `serviceName` (which an app may override to anything) it\n * is the stable key that joins this page's telemetry to the frontend and its\n * source maps. Omit for none.\n */\n frontend?: string;\n /**\n * Ingest base: a path on this origin (`/_otel`) or, as an escape hatch, an\n * absolute URL. Unset, empty, or `off` disables telemetry — the app runs\n * untraced rather than throwing, which is what makes it safe to call\n * unconditionally from the app shell.\n */\n endpoint?: string;\n /**\n * Origins (or URLs — only the origin is used) of the app's own backend.\n * Requests to them always get a `traceparent` from the SDK's fetch/XHR\n * spans. This page's own origin is always included.\n */\n apiOrigins?: Array<string | undefined>;\n /**\n * Mirror console.log/info/warn/debug into telemetry spans. Default true;\n * pass false when console output can hold user content. It does NOT stop\n * `console.error(err)` from being recorded as an error: the SDK's error\n * instrumentation is separate and always on.\n */\n consoleCapture?: boolean;\n /**\n * Capture full request/response headers AND BODIES on fetch/XHR spans.\n * Default false, and it should stay false: bodies are user data.\n */\n advancedNetworkCapture?: boolean;\n /**\n * Session replay (rrweb). Default false. When true, all text and inputs are\n * masked, media is blocked and canvas is not recorded (see the header).\n */\n replay?: boolean;\n /**\n * The share of sessions to record when `replay` is true, 0..1. Default 1\n * (every session). Decided once per session and held in sessionStorage. A\n * value that is not a number records nothing. An app with its own policy\n * (every session of a new user, a fraction of the rest) computes the rate\n * it wants for this session and passes it here.\n */\n replaySampleRate?: number;\n /** Verbose SDK diagnostics to the console. Default false. */\n debug?: boolean;\n /**\n * The `app.client` stamped on every event: `web`, `electron` or `landing`\n * (any short string is accepted). Unset, it is `electron` inside Electron\n * and `web` elsewhere.\n */\n appClient?: string;\n /**\n * localStorage key holding this browser's anonymous visitor id (stamped as\n * `visitor.id`). Set it to adopt an id a site already keeps, e.g. a landing\n * page's `reliant.visitor_id`. Default `forge.visitor_id`. A `rid` in the\n * landing URL wins over it.\n */\n visitorIdStorageKey?: string;\n}\n\n/** The slice of the SDK this module calls. Typed here, not imported, so the module loads without it. */\ninterface HyperDXClient {\n init(config: Record<string, unknown>): void;\n recordException(error: unknown, attributes?: Record<string, unknown>): void;\n addAction?(name: string, attributes?: Record<string, TrackValue>): void;\n setGlobalAttributes?(attributes: Record<string, string>): void;\n stopSessionRecorder?(): void;\n}\n\nlet started: Promise<boolean> | null = null;\n\n/**\n * initBrowserTelemetry starts the HyperDX browser SDK once per page load and\n * resolves to whether it is running. See the module header for the contract.\n */\nexport function initBrowserTelemetry(\n config: BrowserTelemetryConfig,\n): Promise<boolean> {\n if (typeof window === \"undefined\") {\n return Promise.resolve(false);\n }\n if (started) {\n return started;\n }\n if (isTelemetryDisabled()) {\n return Promise.resolve(false);\n }\n const ingest = resolveIngestUrl(config.endpoint, window.location.origin);\n if (!ingest) {\n // Nothing will ever drain track() calls held for the SDK; let them go.\n setTelemetrySink(null);\n return Promise.resolve(false);\n }\n started = start(config, ingest);\n registerTelemetryStartup(started);\n return started;\n}\n\nasync function start(\n config: BrowserTelemetryConfig,\n ingest: string,\n): Promise<boolean> {\n try {\n const hdx = resolveClient(await import(\"@hyperdx/browser\"));\n if (!hdx || isTelemetryDisabled()) {\n setTelemetrySink(null);\n return false;\n }\n const replay = decideReplay(config, sessionStorageOrNull(), Math.random);\n hdx.init(\n buildSdkOptions({ ...config, replay }, ingest, window.location.origin),\n );\n tapSdkInputs();\n // Every span carries where it came from and which visitor made it; the\n // product-event view reads these as app.client and visitor.id.\n hdx.setGlobalAttributes?.({\n \"app.client\": detectAppClient(config.appClient),\n \"visitor.id\": captureAttribution({\n visitorIdStorageKey: config.visitorIdStorageKey,\n }),\n });\n setExceptionReporter((error, attributes) => {\n if (alreadyRecorded(error)) {\n return;\n }\n hdx.recordException(error, attributes);\n });\n setTelemetrySink({\n track: (name, attributes) => hdx.addAction?.(name, attributes),\n setUser: (id) => {\n hdx.setGlobalAttributes?.({ userId: id });\n // The first sign-in of a user in this browser links the visitor to\n // them: one event, never repeated for the same user.\n const identify = takeIdentifyProps(id);\n if (identify) {\n hdx.addAction?.(\"user_identified\", identify);\n }\n },\n disable: () => {\n // Back to \"no SDK\": reportException returns false, so a caller falls\n // back to console.error and the error does not vanish.\n setExceptionReporter(null);\n hdx.stopSessionRecorder?.();\n },\n });\n return true;\n } catch (err) {\n // Blocked chunk, ad-blocker, SDK throw: telemetry must never break the app.\n console.debug(\"[telemetry] disabled:\", err);\n setTelemetrySink(null);\n return false;\n }\n}\n\n// ── One failure, one record ──────────────────────────────────────────────\n//\n// The SDK records an Error that reaches window \"error\", window\n// \"unhandledrejection\", or console.error. React (18 and 19) console.errors an\n// error a boundary catches BEFORE it calls componentDidCatch, and Next\n// dispatches an uncaught render error to window \"error\" — so by the time\n// reportException runs, the SDK has usually recorded that very error already.\n// Recording it again would count one failure twice, and the issue layer\n// (which groups by fingerprint) would report two.\n//\n// So the taps below remember which Error OBJECTS the SDK was shown, and the\n// installed recorder skips those. Whatever the SDK did not see — a custom\n// onCaughtError that silences React's logging, an error rethrown as a new\n// object — is still recorded. The console tap is installed after the SDK's own\n// wrapper, so it sits outermost and only observes; every call is forwarded\n// untouched.\n//\n// The cost is the React component stack: the SDK's record of a boundary-caught\n// error comes from console.error(error), which carries the error and its\n// stack but not React's componentStack, and attributes passed to a skipped\n// reportException are dropped with it.\nconst seenBySdk = new WeakSet<object>();\nlet tapped = false;\n\nfunction noteSeen(value: unknown): void {\n if (typeof value === \"object\" && value !== null) {\n seenBySdk.add(value);\n }\n}\n\nfunction tapSdkInputs(): void {\n if (tapped) {\n return;\n }\n tapped = true;\n const wrapped = console.error;\n console.error = function (this: unknown, ...args: unknown[]) {\n for (const arg of args) {\n if (arg instanceof Error) {\n noteSeen(arg);\n }\n }\n return wrapped.apply(this, args);\n };\n window.addEventListener(\"error\", (e) => noteSeen(e.error), true);\n window.addEventListener(\n \"unhandledrejection\",\n (e) => noteSeen(e.reason),\n true,\n );\n}\n\nfunction alreadyRecorded(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && seenBySdk.has(error);\n}\n\n/**\n * The options passed to HyperDX.init. Exported for tests and for apps that\n * want to see exactly what is configured — it is pure.\n */\nexport function buildSdkOptions(\n config: BrowserTelemetryConfig,\n ingestUrl: string,\n pageOrigin: string,\n): Record<string, unknown> {\n // `config.replay` is the FINAL decision here. initBrowserTelemetry resolves\n // replaySampleRate into it (decideReplay) before calling this.\n const replay = config.replay === true;\n const resourceAttributes: Record<string, string> = {};\n if (config.serviceVersion) {\n resourceAttributes[\"service.version\"] = config.serviceVersion;\n }\n if (config.serviceNamespace) {\n resourceAttributes[\"service.namespace\"] = config.serviceNamespace;\n }\n if (config.frontend) {\n resourceAttributes[\"forge.frontend\"] = config.frontend;\n }\n if (config.environment) {\n resourceAttributes[\"deployment.environment.name\"] = config.environment;\n resourceAttributes[\"deployment.environment\"] = config.environment;\n }\n\n const targets = propagationTargets(pageOrigin, config.apiOrigins);\n const advancedNetworkCapture = config.advancedNetworkCapture ?? false;\n const options: Record<string, unknown> = {\n apiKey: PLACEHOLDER_API_KEY,\n service: config.serviceName,\n url: ingestUrl,\n tracePropagationTargets: targets,\n // The SDK would otherwise trace its own exports, and it must never\n // observe the route it ships through.\n ignoreUrls: [new RegExp(`^${escapeRegExp(ingestUrl)}`)],\n consoleCapture: config.consoleCapture ?? true,\n advancedNetworkCapture,\n // The SDK spreads this over ITS fetch/xhr config as whole keys, so\n // everything it would have set (propagation targets, the capture switch)\n // must be restated here or it is lost.\n instrumentations: {\n fetch: {\n propagateTraceHeaderCorsUrls: targets,\n advancedNetworkCapture: () => advancedNetworkCapture,\n applyCustomAttributesOnSpan: stripNetworkQuery,\n },\n xhr: {\n propagateTraceHeaderCorsUrls: targets,\n advancedNetworkCapture: () => advancedNetworkCapture,\n applyCustomAttributesOnSpan: stripNetworkQuery,\n },\n },\n // Polls for window.Intercom on a timer; nothing here uses Intercom.\n disableIntercom: true,\n disableReplay: !replay,\n otelResourceAttributes: resourceAttributes,\n debug: config.debug ?? false,\n // Privacy is forced and unconditional: no path through this function\n // yields an unmasked or media-recording replay. The SDK's own default\n // masks inputs but NOT text, and text is where chat content and records\n // live.\n maskAllInputs: true,\n maskAllText: true,\n recordCanvas: false,\n blockSelector: REPLAY_BLOCK_SELECTOR,\n replay: { inlineImages: false, collectFonts: false },\n };\n return options;\n}\n\n/** Elements a replay never records: their pixels stay in the browser. */\nexport const REPLAY_BLOCK_SELECTOR =\n \"img, picture, video, audio, iframe, object, embed\";\n\n/** Where a replay sampling decision is held for the tab's session. */\nexport const REPLAY_SAMPLE_KEY = \"forge.replay.sampled\";\n\n/** The slice of Storage the sampling decision needs. */\ninterface DecisionStore {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n}\n\n/**\n * Whether this session records a replay: config.replay, then the sample rate,\n * drawn once and remembered in `store` so a reload (or a route change that\n * re-runs init) never re-rolls into a session that only half-recorded. With no\n * usable store the draw is made each load. Pure given its inputs; exported for\n * tests.\n */\nexport function decideReplay(\n config: Pick<BrowserTelemetryConfig, \"replay\" | \"replaySampleRate\">,\n store: DecisionStore | null,\n random: () => number,\n): boolean {\n if (config.replay !== true) {\n return false;\n }\n const rate = clampRate(config.replaySampleRate);\n if (rate >= 1) {\n return true;\n }\n if (rate <= 0) {\n return false;\n }\n try {\n const held = store?.getItem(REPLAY_SAMPLE_KEY);\n if (held === \"1\" || held === \"0\") {\n return held === \"1\";\n }\n } catch {\n // Storage blocked: fall through to a fresh draw.\n }\n const sampled = random() < rate;\n try {\n store?.setItem(REPLAY_SAMPLE_KEY, sampled ? \"1\" : \"0\");\n } catch {\n // Not persisted: the next load draws again.\n }\n return sampled;\n}\n\n// Unset is \"every session\"; anything that is not a number is a bug, and a bug\n// must not start recording people.\nfunction clampRate(rate: number | undefined): number {\n if (rate === undefined) {\n return 1;\n }\n if (typeof rate !== \"number\" || Number.isNaN(rate)) {\n return 0;\n }\n return Math.min(1, Math.max(0, rate));\n}\n\nfunction sessionStorageOrNull(): DecisionStore | null {\n try {\n return window.sessionStorage;\n } catch {\n return null;\n }\n}\n\n/** The span attributes that hold a request URL. */\nconst URL_ATTRIBUTES = [\"http.url\", \"url.full\"] as const;\n\n/** `url` without its query string and fragment. */\nexport function stripUrlQuery(url: string): string {\n const cut = url.search(/[?#]/);\n return cut === -1 ? url : url.slice(0, cut);\n}\n\ninterface UrlSpan {\n attributes?: Record<string, unknown>;\n setAttribute(key: string, value: string): unknown;\n}\n\n// applyCustomAttributesOnSpan for both instrumentations: it runs before the\n// span ends, so the rewritten value is the one exported.\nfunction stripNetworkQuery(span: UrlSpan): void {\n for (const key of URL_ATTRIBUTES) {\n const value = span.attributes?.[key];\n if (typeof value === \"string\") {\n const stripped = stripUrlQuery(value);\n if (stripped !== value) {\n span.setAttribute(key, stripped);\n }\n }\n }\n}\n\n/**\n * resolveIngestUrl turns the configured endpoint into the absolute URL the SDK\n * is given, or null when telemetry is off. A path resolves against the page\n * origin, so the default is same-origin by construction.\n */\nexport function resolveIngestUrl(\n endpoint: string | undefined,\n pageOrigin: string,\n): string | null {\n const value = (endpoint ?? \"\").trim();\n if (value === \"\" || value.toLowerCase() === \"off\") {\n return null;\n }\n try {\n return new URL(value, pageOrigin).toString().replace(/\\/+$/, \"\");\n } catch {\n return null;\n }\n}\n\nfunction propagationTargets(\n pageOrigin: string,\n apiOrigins: Array<string | undefined> = [],\n): RegExp[] {\n const origins = new Set<string>([pageOrigin]);\n for (const raw of apiOrigins) {\n if (!raw) {\n continue;\n }\n try {\n origins.add(new URL(raw, pageOrigin).origin);\n } catch {\n // An unparsable origin is not a target.\n }\n }\n return [...origins].map((o) => new RegExp(`^${escapeRegExp(o)}(?:[/?#]|$)`));\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n// @hyperdx/browser is a UMD/CJS bundle whose module.exports IS the client, and\n// its methods use `this`. A bundler hands back a module NAMESPACE that exposes\n// both the client's members at the top level (as read-only getters) and the\n// client itself as `default`. Calling init on the namespace binds `this` to that\n// frozen object and the SDK throws on its first property write, which disabled\n// telemetry silently. So descend to the deepest `default` that is still a\n// client, the real instance, and never settle for the namespace in front of it.\nfunction resolveClient(mod: unknown): HyperDXClient | null {\n let cur: unknown = mod;\n for (let depth = 0; depth < 4; depth++) {\n const next = peek(cur, \"default\");\n if (!isClient(next)) {\n break;\n }\n cur = next;\n }\n return isClient(cur) ? cur : null;\n}\n\nfunction isClient(v: unknown): v is HyperDXClient {\n return (\n typeof peek(v, \"init\") === \"function\" &&\n typeof peek(v, \"recordException\") === \"function\"\n );\n}\n\n// A module namespace object may throw on an absent key (strict ESM\n// interop shims and test doubles do), and \"absent\" is an answer here.\nfunction peek(obj: unknown, key: string): unknown {\n try {\n return (obj as Record<string, unknown>)[key];\n } catch {\n return undefined;\n }\n}\n\n/** The tracer to open manual spans on. Resolves HyperDX's provider once it is running. */\nexport function getTracer(name = \"default\") {\n return trace.getTracer(name);\n}\n\nexport { context, propagation, trace } from \"@opentelemetry/api\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reliantlabs/forge-web-runtime",
3
- "version": "0.8.0",
3
+ "version": "0.9.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
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.",