@reliantlabs/forge-web-runtime 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -137,7 +137,19 @@ on the server, and calls made while the SDK chunk is still loading are held
137
137
  first `setTelemetryUser(id)` per user also emits one `user_identified` event
138
138
  with `visitor_id` and the stored utm values. `withAttribution(url)` appends
139
139
  `rid` and the utm values to an outbound absolute URL, and is the identity
140
- before init or after `disableTelemetry()`.
140
+ before init or after `disableTelemetry()`. A visit carrying new values
141
+ replaces the stored ones (last touch); `attributionTouch: "first"` keeps the
142
+ first for good, for a marketing site attributing acquisition.
143
+
144
+ A site whose visitor id needs a consent its error telemetry does not (error
145
+ telemetry for everyone, the visitor id only with analytics consent) starts
146
+ with `attribution: false`: no visitor id is created, read or stamped and
147
+ nothing is captured until it calls `enableAttribution({ search })` (from
148
+ `/telemetry`) on grant. `search` is the landing URL's query string kept from
149
+ page load, for when a client-side navigation has dropped it since. To erase
150
+ on withdrawal, remove `ATTRIBUTION_STORAGE_KEY`, `IDENTIFIED_USER_STORAGE_KEY`
151
+ and the visitor id key; `getAttribution()` is the captured record, for a site
152
+ that stamps campaign values on its own events.
141
153
 
142
154
  Privacy is forced, not configurable: when replay records, all text and inputs
143
155
  are masked, canvas is never recorded, `img, picture, video, audio, iframe,
@@ -17,16 +17,20 @@ export type Attribution = {
17
17
  export declare function detectAppClient(explicit?: string): AppClient;
18
18
  /** Pull `rid` and `utm_*` out of a query string. Pure; exported for tests. */
19
19
  export declare function parseAttribution(search: string): Attribution;
20
+ /** Which captured record wins when a visit carries new attribution. */
21
+ export type AttributionTouch = "first" | "last";
20
22
  /**
21
23
  * 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.
24
+ * and note which storage key holds this browser's own visitor id. A visit that
25
+ * carries any replaces what was stored (`touch: "last"`, the default) or is
26
+ * ignored once a record exists (`"first"`); one that carries none keeps it.
27
+ * Called by initBrowserTelemetry and enableAttribution, i.e. only once
28
+ * telemetry may run. Returns the visitor id. Never throws.
26
29
  */
27
30
  export declare function captureAttribution(options?: {
28
31
  search?: string;
29
32
  visitorIdStorageKey?: string;
33
+ touch?: AttributionTouch;
30
34
  }): string;
31
35
  /** The captured attribution, or `{}` before captureAttribution. */
32
36
  export declare function getAttribution(): Attribution;
@@ -17,8 +17,21 @@
17
17
  //
18
18
  // Nothing here runs until initBrowserTelemetry does, and that is the point an
19
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.
20
+ // stores nothing and withAttribution returns the URL untouched.
21
+ //
22
+ // A site whose consent has two tiers — error telemetry for everyone, a visitor
23
+ // id only with analytics consent — starts telemetry with `attribution: false`
24
+ // and calls enableAttribution() (telemetry.ts) once analytics is granted. Until
25
+ // then no visitor id is created, read or stamped, and nothing is captured.
26
+ //
27
+ // ── First or last touch ──────────────────────────────────────────────────
28
+ //
29
+ // By default a visit that carries `rid`/`utm_*` replaces what was stored: last
30
+ // touch wins, which is right for the app (each hop from the marketing site
31
+ // carries that site's own view). A marketing site that attributes acquisition
32
+ // to the FIRST campaign that brought a visitor passes `attributionTouch:
33
+ // "first"`: once a record is stored, later visits never replace it.
34
+ // Dependency-free apart from track.ts, like it.
22
35
  import { isTelemetryDisabled } from "./track.js";
23
36
  /** localStorage key of the captured `{rid, utm_*}` record. */
24
37
  export const ATTRIBUTION_STORAGE_KEY = "forge.attribution";
@@ -149,10 +162,11 @@ function readStoredAttribution() {
149
162
  }
150
163
  /**
151
164
  * 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.
165
+ * and note which storage key holds this browser's own visitor id. A visit that
166
+ * carries any replaces what was stored (`touch: "last"`, the default) or is
167
+ * ignored once a record exists (`"first"`); one that carries none keeps it.
168
+ * Called by initBrowserTelemetry and enableAttribution, i.e. only once
169
+ * telemetry may run. Returns the visitor id. Never throws.
156
170
  */
157
171
  export function captureAttribution(options) {
158
172
  if (isTelemetryDisabled()) {
@@ -165,7 +179,9 @@ export function captureAttribution(options) {
165
179
  const search = options?.search ??
166
180
  (typeof window !== "undefined" ? (window.location?.search ?? "") : "");
167
181
  const incoming = parseAttribution(search);
168
- if (Object.keys(incoming).length > 0) {
182
+ const keep = options?.touch === "first" &&
183
+ Object.keys(readStoredAttribution()).length > 0;
184
+ if (Object.keys(incoming).length > 0 && !keep) {
169
185
  memory = incoming;
170
186
  write(ATTRIBUTION_STORAGE_KEY, JSON.stringify(incoming));
171
187
  }
@@ -1 +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"]}
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.\n//\n// A site whose consent has two tiers — error telemetry for everyone, a visitor\n// id only with analytics consent — starts telemetry with `attribution: false`\n// and calls enableAttribution() (telemetry.ts) once analytics is granted. Until\n// then no visitor id is created, read or stamped, and nothing is captured.\n//\n// ── First or last touch ──────────────────────────────────────────────────\n//\n// By default a visit that carries `rid`/`utm_*` replaces what was stored: last\n// touch wins, which is right for the app (each hop from the marketing site\n// carries that site's own view). A marketing site that attributes acquisition\n// to the FIRST campaign that brought a visitor passes `attributionTouch:\n// \"first\"`: once a record is stored, later visits never replace it.\n// Dependency-free 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/** Which captured record wins when a visit carries new attribution. */\nexport type AttributionTouch = \"first\" | \"last\";\n\n/**\n * Start attribution for this page load: record `rid` and `utm_*` from the URL\n * and note which storage key holds this browser's own visitor id. A visit that\n * carries any replaces what was stored (`touch: \"last\"`, the default) or is\n * ignored once a record exists (`\"first\"`); one that carries none keeps it.\n * Called by initBrowserTelemetry and enableAttribution, i.e. only once\n * telemetry may run. Returns the visitor id. Never throws.\n */\nexport function captureAttribution(options?: {\n search?: string;\n visitorIdStorageKey?: string;\n touch?: AttributionTouch;\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 const keep =\n options?.touch === \"first\" &&\n Object.keys(readStoredAttribution()).length > 0;\n if (Object.keys(incoming).length > 0 && !keep) {\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
@@ -57,8 +57,10 @@ export { disableTelemetry, setTelemetryUser, track, type TrackProps, type TrackV
57
57
  * this browser's `rid` and captured `utm_*` to an outbound link (a marketing
58
58
  * site pointing at the app); the runtime reads them on arrival and emits one
59
59
  * `user_identified` event at first sign-in. Consent-gated like track().
60
+ * `getAttribution()` is the captured record (to stamp on a site's own events);
61
+ * the storage keys are exported so a site that withdraws consent can erase them.
60
62
  */
61
- export { withAttribution, type AppClient, type Attribution, } from "./attribution.js";
63
+ export { ATTRIBUTION_STORAGE_KEY, IDENTIFIED_USER_STORAGE_KEY, UTM_PARAMS, VISITOR_ID_STORAGE_KEY, getAttribution, withAttribution, type AppClient, type Attribution, type AttributionTouch, } from "./attribution.js";
62
64
  /**
63
65
  * Dev-only browser log forwarding: mirrors console output and uncaught errors
64
66
  * to the dev server, which prints them into
package/dist/index.js CHANGED
@@ -75,8 +75,10 @@ export { disableTelemetry, setTelemetryUser, track, } from "./track.js";
75
75
  * this browser's `rid` and captured `utm_*` to an outbound link (a marketing
76
76
  * site pointing at the app); the runtime reads them on arrival and emits one
77
77
  * `user_identified` event at first sign-in. Consent-gated like track().
78
+ * `getAttribution()` is the captured record (to stamp on a site's own events);
79
+ * the storage keys are exported so a site that withdraws consent can erase them.
78
80
  */
79
- export { withAttribution, } from "./attribution.js";
81
+ export { ATTRIBUTION_STORAGE_KEY, IDENTIFIED_USER_STORAGE_KEY, UTM_PARAMS, VISITOR_ID_STORAGE_KEY, getAttribution, withAttribution, } from "./attribution.js";
80
82
  /**
81
83
  * Dev-only browser log forwarding: mirrors console output and uncaught errors
82
84
  * 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;;;;;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"]}
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;;;;;;;GAOG;AACH,OAAO,EACL,uBAAuB,EACvB,2BAA2B,EAC3B,UAAU,EACV,sBAAsB,EACtB,cAAc,EACd,eAAe,GAIhB,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 * `getAttribution()` is the captured record (to stamp on a site's own events);\n * the storage keys are exported so a site that withdraws consent can erase them.\n */\nexport {\n ATTRIBUTION_STORAGE_KEY,\n IDENTIFIED_USER_STORAGE_KEY,\n UTM_PARAMS,\n VISITOR_ID_STORAGE_KEY,\n getAttribution,\n withAttribution,\n type AppClient,\n type Attribution,\n type AttributionTouch,\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"]}
@@ -1,3 +1,4 @@
1
+ import { type AttributionTouch } from "./attribution.js";
1
2
  /** Where the browser ships telemetry: this origin's `/_otel`. */
2
3
  export declare const DEFAULT_INGEST_PATH = "/_otel";
3
4
  /**
@@ -79,7 +80,34 @@ export interface BrowserTelemetryConfig {
79
80
  * landing URL wins over it.
80
81
  */
81
82
  visitorIdStorageKey?: string;
83
+ /**
84
+ * Start visitor attribution with the SDK. Default true. Pass false when the
85
+ * visitor id needs a consent the page does not have yet (error telemetry for
86
+ * everyone, a visitor id only with analytics consent): no visitor id is
87
+ * created, read or stamped, nothing is captured from the URL, withAttribution
88
+ * returns URLs untouched and `user_identified` never fires, until the app
89
+ * calls enableAttribution().
90
+ */
91
+ attribution?: boolean;
92
+ /**
93
+ * `last` (default): a visit carrying `rid`/`utm_*` replaces the stored
94
+ * record. `first`: the first stored record is kept for good — a marketing
95
+ * site attributing acquisition to the campaign that first brought a visitor.
96
+ */
97
+ attributionTouch?: AttributionTouch;
82
98
  }
99
+ /**
100
+ * Turn on visitor attribution for a page that started telemetry with
101
+ * `attribution: false`, once the consent it needs is granted: from here on
102
+ * every event carries `visitor.id`, withAttribution stamps links and the first
103
+ * sign-in emits `user_identified`. Pass `search` (the landing URL's query
104
+ * string, kept from page load) when a client-side navigation may have dropped
105
+ * it since. Safe to call before init resolves, more than once, and on the
106
+ * server; a no-op after disableTelemetry().
107
+ */
108
+ export declare function enableAttribution(options?: {
109
+ search?: string;
110
+ }): void;
83
111
  /**
84
112
  * initBrowserTelemetry starts the HyperDX browser SDK once per page load and
85
113
  * resolves to whether it is running. See the module header for the contract.
package/dist/telemetry.js CHANGED
@@ -83,6 +83,50 @@ export const DEFAULT_INGEST_PATH = "/_otel";
83
83
  */
84
84
  export const PLACEHOLDER_API_KEY = "forge-browser-public";
85
85
  let started = null;
86
+ // Attribution state shared by start() and enableAttribution(): whichever runs
87
+ // second does the stamping, so the call order does not matter.
88
+ let attributionWanted = false;
89
+ let attributionOptions = {};
90
+ let runningConfig = null;
91
+ let runningClient = null;
92
+ function captureVisitor(config) {
93
+ return captureAttribution({
94
+ search: attributionOptions.search,
95
+ visitorIdStorageKey: config.visitorIdStorageKey,
96
+ touch: config.attributionTouch,
97
+ });
98
+ }
99
+ /**
100
+ * Turn on visitor attribution for a page that started telemetry with
101
+ * `attribution: false`, once the consent it needs is granted: from here on
102
+ * every event carries `visitor.id`, withAttribution stamps links and the first
103
+ * sign-in emits `user_identified`. Pass `search` (the landing URL's query
104
+ * string, kept from page load) when a client-side navigation may have dropped
105
+ * it since. Safe to call before init resolves, more than once, and on the
106
+ * server; a no-op after disableTelemetry().
107
+ */
108
+ export function enableAttribution(options) {
109
+ if (typeof window === "undefined" || isTelemetryDisabled()) {
110
+ return;
111
+ }
112
+ if (options?.search !== undefined) {
113
+ attributionOptions = { search: options.search };
114
+ }
115
+ if (attributionWanted) {
116
+ return;
117
+ }
118
+ attributionWanted = true;
119
+ if (runningClient && runningConfig) {
120
+ try {
121
+ runningClient.setGlobalAttributes?.({
122
+ "visitor.id": captureVisitor(runningConfig),
123
+ });
124
+ }
125
+ catch {
126
+ // Telemetry must never break the app.
127
+ }
128
+ }
129
+ }
86
130
  /**
87
131
  * initBrowserTelemetry starts the HyperDX browser SDK once per page load and
88
132
  * resolves to whether it is running. See the module header for the contract.
@@ -119,12 +163,20 @@ async function start(config, ingest) {
119
163
  tapSdkInputs();
120
164
  // Every span carries where it came from and which visitor made it; the
121
165
  // product-event view reads these as app.client and visitor.id.
122
- hdx.setGlobalAttributes?.({
166
+ // With attribution deferred (attribution: false), visitor.id joins them
167
+ // when enableAttribution() runs.
168
+ if (config.attribution !== false) {
169
+ attributionWanted = true;
170
+ }
171
+ const globals = {
123
172
  "app.client": detectAppClient(config.appClient),
124
- "visitor.id": captureAttribution({
125
- visitorIdStorageKey: config.visitorIdStorageKey,
126
- }),
127
- });
173
+ };
174
+ if (attributionWanted) {
175
+ globals["visitor.id"] = captureVisitor(config);
176
+ }
177
+ hdx.setGlobalAttributes?.(globals);
178
+ runningClient = hdx;
179
+ runningConfig = config;
128
180
  setExceptionReporter((error, attributes) => {
129
181
  if (alreadyRecorded(error)) {
130
182
  return;
@@ -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 {\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"]}
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 {\n captureAttribution,\n type AttributionTouch,\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 * Start visitor attribution with the SDK. Default true. Pass false when the\n * visitor id needs a consent the page does not have yet (error telemetry for\n * everyone, a visitor id only with analytics consent): no visitor id is\n * created, read or stamped, nothing is captured from the URL, withAttribution\n * returns URLs untouched and `user_identified` never fires, until the app\n * calls enableAttribution().\n */\n attribution?: boolean;\n /**\n * `last` (default): a visit carrying `rid`/`utm_*` replaces the stored\n * record. `first`: the first stored record is kept for good — a marketing\n * site attributing acquisition to the campaign that first brought a visitor.\n */\n attributionTouch?: AttributionTouch;\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// Attribution state shared by start() and enableAttribution(): whichever runs\n// second does the stamping, so the call order does not matter.\nlet attributionWanted = false;\nlet attributionOptions: { search?: string } = {};\nlet runningConfig: BrowserTelemetryConfig | null = null;\nlet runningClient: HyperDXClient | null = null;\n\nfunction captureVisitor(config: BrowserTelemetryConfig): string {\n return captureAttribution({\n search: attributionOptions.search,\n visitorIdStorageKey: config.visitorIdStorageKey,\n touch: config.attributionTouch,\n });\n}\n\n/**\n * Turn on visitor attribution for a page that started telemetry with\n * `attribution: false`, once the consent it needs is granted: from here on\n * every event carries `visitor.id`, withAttribution stamps links and the first\n * sign-in emits `user_identified`. Pass `search` (the landing URL's query\n * string, kept from page load) when a client-side navigation may have dropped\n * it since. Safe to call before init resolves, more than once, and on the\n * server; a no-op after disableTelemetry().\n */\nexport function enableAttribution(options?: { search?: string }): void {\n if (typeof window === \"undefined\" || isTelemetryDisabled()) {\n return;\n }\n if (options?.search !== undefined) {\n attributionOptions = { search: options.search };\n }\n if (attributionWanted) {\n return;\n }\n attributionWanted = true;\n if (runningClient && runningConfig) {\n try {\n runningClient.setGlobalAttributes?.({\n \"visitor.id\": captureVisitor(runningConfig),\n });\n } catch {\n // Telemetry must never break the app.\n }\n }\n}\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 // With attribution deferred (attribution: false), visitor.id joins them\n // when enableAttribution() runs.\n if (config.attribution !== false) {\n attributionWanted = true;\n }\n const globals: Record<string, string> = {\n \"app.client\": detectAppClient(config.appClient),\n };\n if (attributionWanted) {\n globals[\"visitor.id\"] = captureVisitor(config);\n }\n hdx.setGlobalAttributes?.(globals);\n runningClient = hdx;\n runningConfig = config;\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.9.0",
3
+ "version": "0.9.1",
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.",