@mhosaic/feedback 0.16.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ import {
2
+ sanitizeUrl
3
+ } from "./chunk-4QOIFIT5.mjs";
4
+ import {
5
+ scrubCredentials
6
+ } from "./chunk-FGA63IEZ.mjs";
7
+
8
+ // src/modules/telemetry.ts
9
+ var DEFAULTS = {
10
+ enabled: true,
11
+ flushIntervalMs: 1e4,
12
+ maxBatchSize: 50
13
+ };
14
+ function withTelemetryStream(fb, config, options = {}) {
15
+ const opts = { ...DEFAULTS, ...options };
16
+ if (!opts.enabled) return fb;
17
+ if (typeof window === "undefined") return fb;
18
+ const endpoint = config.endpoint.replace(/\/+$/, "");
19
+ const apiKey = config.apiKey;
20
+ const sanitize = opts.sanitizeUrl ?? sanitizeUrl;
21
+ const queue = [];
22
+ const HARD_CAP = opts.maxBatchSize * 4;
23
+ function enqueue(event) {
24
+ queue.push(event);
25
+ while (queue.length > HARD_CAP) queue.shift();
26
+ }
27
+ function onWindowError(ev) {
28
+ const thrown = ev.error;
29
+ let message;
30
+ let stack;
31
+ if (thrown instanceof Error) {
32
+ message = scrubCredentials(thrown.message || ev.message || "Unknown error");
33
+ stack = thrown.stack ? scrubCredentials(thrown.stack) : void 0;
34
+ } else {
35
+ message = scrubCredentials(ev.message || String(thrown) || "Unknown error");
36
+ }
37
+ enqueue({ kind: "error", ts: Date.now(), message, ...stack !== void 0 && { stack } });
38
+ }
39
+ function onUnhandledRejection(ev) {
40
+ const reason = ev.reason;
41
+ let message;
42
+ let stack;
43
+ if (reason instanceof Error) {
44
+ message = scrubCredentials(reason.message || "Unhandled rejection");
45
+ stack = reason.stack ? scrubCredentials(reason.stack) : void 0;
46
+ } else if (typeof reason === "string") {
47
+ message = scrubCredentials(reason);
48
+ } else {
49
+ message = scrubCredentials(
50
+ (() => {
51
+ try {
52
+ return JSON.stringify(reason);
53
+ } catch {
54
+ return String(reason);
55
+ }
56
+ })()
57
+ );
58
+ }
59
+ enqueue({ kind: "error", ts: Date.now(), message, ...stack !== void 0 && { stack } });
60
+ }
61
+ const originalFetch = typeof window.fetch === "function" ? window.fetch.bind(window) : null;
62
+ let fetchPatched = false;
63
+ if (originalFetch) {
64
+ window.fetch = async function telemetryFetch(input, init) {
65
+ const start = performance.now();
66
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
67
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
68
+ const isTelemetryUrl = url.includes("/api/feedback/v1/telemetry/");
69
+ try {
70
+ const response = await originalFetch(input, init);
71
+ if (!isTelemetryUrl && response.status >= 400) {
72
+ enqueue({
73
+ kind: "network",
74
+ ts: Date.now(),
75
+ method,
76
+ url: sanitize(url),
77
+ status: response.status,
78
+ durationMs: Math.round(performance.now() - start)
79
+ });
80
+ }
81
+ return response;
82
+ } catch (err) {
83
+ if (!isTelemetryUrl) {
84
+ enqueue({
85
+ kind: "network",
86
+ ts: Date.now(),
87
+ method,
88
+ url: sanitize(url),
89
+ status: 0,
90
+ durationMs: Math.round(performance.now() - start)
91
+ });
92
+ }
93
+ throw err;
94
+ }
95
+ };
96
+ fetchPatched = true;
97
+ }
98
+ void (async () => {
99
+ try {
100
+ const { onCLS, onFCP, onINP, onLCP, onTTFB } = await import("web-vitals/attribution");
101
+ const record = (metric) => {
102
+ enqueue({
103
+ kind: "web-vital",
104
+ ts: Date.now(),
105
+ name: metric.name,
106
+ value: metric.value,
107
+ rating: metric.rating
108
+ });
109
+ };
110
+ onCLS(record);
111
+ onINP(record);
112
+ onLCP(record);
113
+ onFCP(record);
114
+ onTTFB(record);
115
+ } catch {
116
+ }
117
+ })();
118
+ const TELEMETRY_URL = `${endpoint}/api/feedback/v1/telemetry/`;
119
+ const authHeader = `Bearer ${apiKey}`;
120
+ function flush() {
121
+ if (queue.length === 0) return;
122
+ const batch = queue.splice(0, opts.maxBatchSize);
123
+ const body = JSON.stringify({ events: batch });
124
+ const flushFetch = originalFetch ?? window.fetch.bind(window);
125
+ void flushFetch(TELEMETRY_URL, {
126
+ method: "POST",
127
+ headers: {
128
+ "Content-Type": "application/json",
129
+ Authorization: authHeader
130
+ },
131
+ body,
132
+ keepalive: true
133
+ }).catch(() => {
134
+ });
135
+ }
136
+ const intervalId = setInterval(() => {
137
+ flush();
138
+ }, opts.flushIntervalMs);
139
+ function onVisibilityChange() {
140
+ if (document.visibilityState === "hidden") flush();
141
+ }
142
+ function onPageHide() {
143
+ flush();
144
+ }
145
+ window.addEventListener("error", onWindowError);
146
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
147
+ document.addEventListener("visibilitychange", onVisibilityChange);
148
+ window.addEventListener("pagehide", onPageHide);
149
+ const originalShutdown = fb.shutdown.bind(fb);
150
+ fb.shutdown = () => {
151
+ clearInterval(intervalId);
152
+ window.removeEventListener("error", onWindowError);
153
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
154
+ document.removeEventListener("visibilitychange", onVisibilityChange);
155
+ window.removeEventListener("pagehide", onPageHide);
156
+ if (fetchPatched && originalFetch) {
157
+ window.fetch = originalFetch;
158
+ }
159
+ originalShutdown();
160
+ };
161
+ return fb;
162
+ }
163
+ export {
164
+ withTelemetryStream
165
+ };
166
+ //# sourceMappingURL=telemetry.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/modules/telemetry.ts"],"sourcesContent":["/**\n * withTelemetryStream — opt-in module that streams already-captured browser\n * events (JS errors, failed network requests, web-vitals) to the backend\n * telemetry ingestion endpoint in batches.\n *\n * Same opt-in wrapper pattern as withErrorTracking / withWebVitals / withReplay.\n * Host apps that want curated-feedback-only simply don't import this.\n *\n * Design notes:\n * - Installs its own lightweight capture listeners (same events as the ring\n * buffers in capture/) rather than reading snapshots from them. The ring\n * buffer API is internal to core.ts with no public subscription hook, so\n * the correct pattern — established by withErrorTracking — is to install\n * independent listeners that observe the same global surfaces.\n * - Network capture: only failed requests (status 0 or >=400) are emitted to\n * keep volume low. Successful requests are high-cardinality noise in a\n * telemetry stream.\n * - Scrubbing: error messages/stacks are scrubbed via the shared\n * scrubCredentials helper. URLs are sanitized via sanitizeUrl (same helper\n * the ring-buffer patch uses). Data entering this module's buffers is already\n * scrubbed at the source by the global patches; we add a second pass for the\n * events we capture independently.\n * - Flush: periodic (default 10 s) + visibilitychange→hidden/pagehide via\n * keepalive fetch. We do NOT use navigator.sendBeacon — it can't carry the\n * Authorization header, and the backend authenticates the pk_proj_ key only\n * from `Authorization: Bearer`. The key never travels in a query string.\n * - Per-flush cap (default 50 events) prevents a single bad page from\n * flooding the backend on flush.\n * - No rrweb / session replay. Events only.\n */\n\nimport type { FeedbackApi } from '../types'\nimport { scrubCredentials } from '../capture/scrub'\nimport { sanitizeUrl as defaultSanitizeUrl } from '../capture/urlSanitizer'\n\n// ---- Public option types ---------------------------------------------------\n\nexport interface TelemetryStreamOptions {\n /** Kill switch without removing the import. Default `true`. */\n enabled?: boolean\n /** Flush interval in ms. Default 10 000 (10 s). */\n flushIntervalMs?: number\n /** Max events per flush. Older events are dropped first. Default 50. */\n maxBatchSize?: number\n /**\n * Custom URL sanitizer for network event URLs. Defaults to the same\n * sanitizeUrl used by the widget's network ring-buffer patch.\n */\n sanitizeUrl?: (url: string) => string\n}\n\n// ---- Internal event shapes (match the backend contract) -------------------\n\ninterface ErrorEvent_ {\n kind: 'error'\n ts: number\n message: string\n stack?: string\n}\n\ninterface NetworkEvent {\n kind: 'network'\n ts: number\n method: string\n url: string\n status: number\n durationMs: number\n}\n\ninterface WebVitalEvent {\n kind: 'web-vital'\n ts: number\n name: string\n value: number\n rating: string\n}\n\ntype TelemetryEvent = ErrorEvent_ | NetworkEvent | WebVitalEvent\n\n// ---- InternalApi compat (read endpoint + apiKey from the fb instance) -----\n\n/**\n * The public FeedbackApi doesn't expose endpoint/apiKey. We reach into the\n * FeedbackApiInternal config via the same cast pattern error-tracking.ts uses\n * for _registerTransformer — typed minimally so we're not coupled to\n * unexported internals more than necessary.\n */\ninterface InternalApi extends FeedbackApi {\n _telemetryConfig?: { endpoint: string; apiKey: string }\n shutdown(): void\n}\n\n// We need endpoint + apiKey from the fb closure. Since FeedbackApi doesn't\n// expose them directly, withTelemetryStream accepts them explicitly. This\n// mirrors how host apps have them in scope at the call site (they just passed\n// them to createFeedback a line above).\nexport interface TelemetryStreamConfig {\n endpoint: string\n apiKey: string\n}\n\n// ---- Constants -------------------------------------------------------------\n\nconst DEFAULTS = {\n enabled: true,\n flushIntervalMs: 10_000,\n maxBatchSize: 50,\n} as const satisfies Required<Omit<TelemetryStreamOptions, 'sanitizeUrl'>>\n\n// ---- Main export ----------------------------------------------------------\n\nexport function withTelemetryStream(\n fb: FeedbackApi,\n config: TelemetryStreamConfig,\n options: TelemetryStreamOptions = {},\n): FeedbackApi {\n const opts = { ...DEFAULTS, ...options }\n if (!opts.enabled) return fb\n if (typeof window === 'undefined') return fb\n\n const endpoint = config.endpoint.replace(/\\/+$/, '')\n const apiKey = config.apiKey\n const sanitize = opts.sanitizeUrl ?? defaultSanitizeUrl\n\n // ---- Event queue ---------------------------------------------------------\n // Simple FIFO array. When we accumulate more than maxBatchSize * 2 between\n // flushes (e.g. a tight error loop), we evict from the front to keep memory\n // bounded. The per-flush cap is enforced at flush time.\n const queue: TelemetryEvent[] = []\n const HARD_CAP = opts.maxBatchSize * 4\n\n function enqueue(event: TelemetryEvent): void {\n queue.push(event)\n // Evict from the front — oldest events are least actionable.\n while (queue.length > HARD_CAP) queue.shift()\n }\n\n // ---- Error capture -------------------------------------------------------\n function onWindowError(ev: globalThis.ErrorEvent): void {\n const thrown = ev.error\n let message: string\n let stack: string | undefined\n if (thrown instanceof Error) {\n message = scrubCredentials(thrown.message || ev.message || 'Unknown error')\n stack = thrown.stack ? scrubCredentials(thrown.stack) : undefined\n } else {\n message = scrubCredentials(ev.message || String(thrown) || 'Unknown error')\n }\n enqueue({ kind: 'error', ts: Date.now(), message, ...(stack !== undefined && { stack }) })\n }\n\n function onUnhandledRejection(ev: PromiseRejectionEvent): void {\n const reason = ev.reason\n let message: string\n let stack: string | undefined\n if (reason instanceof Error) {\n message = scrubCredentials(reason.message || 'Unhandled rejection')\n stack = reason.stack ? scrubCredentials(reason.stack) : undefined\n } else if (typeof reason === 'string') {\n message = scrubCredentials(reason)\n } else {\n message = scrubCredentials(\n (() => { try { return JSON.stringify(reason) } catch { return String(reason) } })(),\n )\n }\n enqueue({ kind: 'error', ts: Date.now(), message, ...(stack !== undefined && { stack }) })\n }\n\n // ---- Network capture (failed requests only) ------------------------------\n const originalFetch = typeof window.fetch === 'function' ? window.fetch.bind(window) : null\n\n let fetchPatched = false\n if (originalFetch) {\n window.fetch = async function telemetryFetch(\n input: RequestInfo | URL,\n init?: RequestInit,\n ) {\n const start = performance.now()\n const url =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (input as Request).url\n const method = (\n init?.method ?? (input instanceof Request ? input.method : 'GET')\n ).toUpperCase()\n // Belt-and-suspenders: never capture telemetry endpoint requests to\n // prevent a self-amplifying loop if the telemetry POST itself fails.\n const isTelemetryUrl = url.includes('/api/feedback/v1/telemetry/')\n try {\n const response = await originalFetch(input, init)\n if (!isTelemetryUrl && response.status >= 400) {\n enqueue({\n kind: 'network',\n ts: Date.now(),\n method,\n url: sanitize(url),\n status: response.status,\n durationMs: Math.round(performance.now() - start),\n })\n }\n return response\n } catch (err) {\n if (!isTelemetryUrl) {\n enqueue({\n kind: 'network',\n ts: Date.now(),\n method,\n url: sanitize(url),\n status: 0,\n durationMs: Math.round(performance.now() - start),\n })\n }\n throw err\n }\n } as typeof fetch\n fetchPatched = true\n }\n\n // ---- Web vitals capture --------------------------------------------------\n // Dynamically import web-vitals so the module stays tree-shakeable and\n // the import doesn't fail in environments where the package isn't present.\n void (async () => {\n try {\n const { onCLS, onFCP, onINP, onLCP, onTTFB } = await import('web-vitals/attribution')\n const record = (metric: { name: string; value: number; rating: string }) => {\n enqueue({\n kind: 'web-vital',\n ts: Date.now(),\n name: metric.name,\n value: metric.value,\n rating: metric.rating,\n })\n }\n onCLS(record)\n onINP(record)\n onLCP(record)\n onFCP(record)\n onTTFB(record)\n } catch { /* web-vitals unavailable or not installed */ }\n })()\n\n // ---- Flush logic ---------------------------------------------------------\n const TELEMETRY_URL = `${endpoint}/api/feedback/v1/telemetry/`\n const authHeader = `Bearer ${apiKey}`\n\n /**\n * Drain up to maxBatchSize events from the front of the queue and POST\n * them to the backend.\n *\n * Always uses keepalive fetch — including on visibilitychange→hidden /\n * pagehide. We deliberately do NOT use navigator.sendBeacon: it can't\n * carry the Authorization header, and the backend's BearerApiKeyAuthentication\n * reads the pk_proj_ key only from `Authorization: Bearer`. The key must\n * never travel in a query string. keepalive fetch supports headers and is\n * delivered during unload (subject to the browser's 64 KB body cap, which\n * is comfortably above our capped batch size). Telemetry is best-effort:\n * losing an occasional unload batch is acceptable; a silent 401 from a\n * mis-authed beacon is not.\n */\n function flush(): void {\n if (queue.length === 0) return\n\n const batch = queue.splice(0, opts.maxBatchSize)\n const body = JSON.stringify({ events: batch })\n\n // Use originalFetch (the native, pre-patch fetch) so the telemetry POST\n // is never captured by our own network interceptor — preventing a\n // self-amplifying loop where a failing telemetry POST re-enqueues itself.\n const flushFetch = originalFetch ?? window.fetch.bind(window)\n void flushFetch(TELEMETRY_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: authHeader,\n },\n body,\n keepalive: true,\n }).catch(() => { /* best-effort; swallow silently */ })\n }\n\n // Periodic flush\n const intervalId = setInterval(() => { flush() }, opts.flushIntervalMs)\n\n // Flush on page hide — keepalive fetch is delivered during unload.\n function onVisibilityChange(): void {\n if (document.visibilityState === 'hidden') flush()\n }\n function onPageHide(): void {\n flush()\n }\n\n window.addEventListener('error', onWindowError)\n window.addEventListener('unhandledrejection', onUnhandledRejection)\n document.addEventListener('visibilitychange', onVisibilityChange)\n window.addEventListener('pagehide', onPageHide)\n\n // ---- Cleanup -------------------------------------------------------------\n const originalShutdown = (fb as InternalApi).shutdown.bind(fb)\n ;(fb as InternalApi).shutdown = () => {\n clearInterval(intervalId)\n window.removeEventListener('error', onWindowError)\n window.removeEventListener('unhandledrejection', onUnhandledRejection)\n document.removeEventListener('visibilitychange', onVisibilityChange)\n window.removeEventListener('pagehide', onPageHide)\n // Restore original fetch if we patched it (only ours; check identity).\n if (fetchPatched && originalFetch) {\n window.fetch = originalFetch\n }\n originalShutdown()\n }\n\n return fb\n}\n"],"mappings":"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","names":[]}
@@ -39,6 +39,10 @@ interface FeedbackConfig {
39
39
  maskAllInputs?: boolean;
40
40
  sanitizeUrl?: (url: string) => string;
41
41
  showFAB?: boolean;
42
+ /** Phase 4: gate the FAB on a per-end-user server visibility check. Normally
43
+ * set automatically by the loader from the manifest's
44
+ * `requires_visibility_check`; rarely set by hosts directly. */
45
+ requiresVisibilityCheck?: boolean;
42
46
  attachTo?: string | Element;
43
47
  locale?: string;
44
48
  translations?: Record<string, string>;
@@ -1,4 +1,4 @@
1
- import { F as FeedbackApi } from './types-BrsktaE1.js';
1
+ import { F as FeedbackApi } from './types-2dZ9bHAn.js';
2
2
 
3
3
  interface WebVitalsOptions {
4
4
  reportAllChanges?: boolean;