@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.
- package/dist/chunk-4QOIFIT5.mjs +50 -0
- package/dist/chunk-4QOIFIT5.mjs.map +1 -0
- package/dist/{chunk-I4GWTJ6Q.mjs → chunk-4QQI5ZYD.mjs} +60 -52
- package/dist/chunk-4QQI5ZYD.mjs.map +1 -0
- package/dist/{chunk-6JDT2KWQ.mjs → chunk-IP4Y5EZE.mjs} +14 -2
- package/dist/chunk-IP4Y5EZE.mjs.map +1 -0
- package/dist/chunk-QJ562NTG.mjs +145 -0
- package/dist/chunk-QJ562NTG.mjs.map +1 -0
- package/dist/embed.min.js +4 -4
- package/dist/embed.min.js.map +1 -1
- package/dist/error-tracking.d.ts +1 -1
- package/dist/error-tracking.mjs +3 -138
- package/dist/error-tracking.mjs.map +1 -1
- package/dist/{index-snrtK3_E.d.ts → index-Bq4QI0xa.d.ts} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +2 -1
- package/dist/loader/react.d.ts +13 -2
- package/dist/loader/react.mjs +7 -3
- package/dist/loader/react.mjs.map +1 -1
- package/dist/loader.d.ts +16 -3
- package/dist/loader.min.js +1 -1
- package/dist/loader.min.js.map +1 -1
- package/dist/loader.mjs +1 -1
- package/dist/react.d.ts +11 -3
- package/dist/react.mjs +13 -2
- package/dist/react.mjs.map +1 -1
- package/dist/replay.d.ts +1 -1
- package/dist/telemetry.d.ts +53 -0
- package/dist/telemetry.mjs +166 -0
- package/dist/telemetry.mjs.map +1 -0
- package/dist/{types-BrsktaE1.d.ts → types-2dZ9bHAn.d.ts} +4 -0
- package/dist/webvitals.d.ts +1 -1
- package/dist/widget.min.js +6 -4
- package/dist/widget.min.js.map +1 -1
- package/package.json +5 -1
- package/dist/chunk-6JDT2KWQ.mjs.map +0 -1
- package/dist/chunk-I4GWTJ6Q.mjs.map +0 -1
|
@@ -170,11 +170,23 @@ async function loadAndAttach(config, deferred) {
|
|
|
170
170
|
bundleUrl: manifest.bundle_url,
|
|
171
171
|
sriHash: manifest.sri_hash
|
|
172
172
|
});
|
|
173
|
-
const real = bundle.createFeedback(
|
|
173
|
+
const real = bundle.createFeedback({
|
|
174
|
+
...config,
|
|
175
|
+
// Forward the project's per-end-user gating flag from the manifest unless
|
|
176
|
+
// the host set it explicitly. The widget then runs the visibility check.
|
|
177
|
+
requiresVisibilityCheck: config.requiresVisibilityCheck ?? manifest.config?.requires_visibility_check ?? false,
|
|
178
|
+
// Error capture is armed INSIDE the bundle (the only auto-updating part
|
|
179
|
+
// of a loader install), so already-deployed hosts pick it up without a
|
|
180
|
+
// redeploy. Precedence: explicit host override > per-project manifest
|
|
181
|
+
// flag > on. The loader provider forwards its `errorTracking` prop here
|
|
182
|
+
// rather than wrapping host-side, so there's exactly one arm point (the
|
|
183
|
+
// real instance) — no deferred/real double-registration.
|
|
184
|
+
errorTracking: config.errorTracking ?? manifest.config?.error_tracking ?? true
|
|
185
|
+
});
|
|
174
186
|
deferred._attach(real);
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
export {
|
|
178
190
|
createFeedback
|
|
179
191
|
};
|
|
180
|
-
//# sourceMappingURL=chunk-
|
|
192
|
+
//# sourceMappingURL=chunk-IP4Y5EZE.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/loader/bundleLoader.ts","../src/loader/manifest.ts","../src/loader/deferredApi.ts","../src/loader/index.ts"],"sourcesContent":["/**\n * Inject the widget bundle as an SRI-protected <script> and wait for its\n * window global to appear.\n *\n * Why a <script> tag and not `import()`:\n * `import()` can fetch cross-origin ESM but doesn't honor Subresource\n * Integrity on the dynamic-import call itself. A <script> tag with\n * `integrity=…` is the only path where the browser actually refuses\n * to execute a bundle whose bytes don't match the manifest's hash.\n *\n * The bundle is built with `tsup --format iife --globalName MhosaicFeedback`,\n * so once it parses it sets `window.MhosaicFeedback = { createFeedback }`\n * (per `src/widget.ts`'s exports). We wait for that to appear.\n *\n * Idempotency: if multiple FeedbackProviders mount on the same page (e.g.\n * during a React fast-refresh cycle), we reuse an already-injected bundle\n * instead of fetching a second copy.\n */\n\nimport type { FeedbackApi, FeedbackConfig } from '../types'\nimport type { ErrorTrackingOptions } from '../modules/error-tracking'\n\n/** What the loader may pass to the bundle's `createFeedback`: the public\n * config plus the bundle-only `errorTracking` knob the loader forwards\n * from the manifest. */\nexport type BundleConfig = FeedbackConfig & {\n errorTracking?: boolean | ErrorTrackingOptions\n}\n\ndeclare global {\n interface Window {\n MhosaicFeedback?: {\n createFeedback(config: BundleConfig): FeedbackApi\n }\n }\n}\n\nconst SCRIPT_DATA_ATTR = 'data-mhosaic-feedback-bundle'\n\nexport interface BundleLoadOptions {\n bundleUrl: string\n sriHash: string\n /** Default 30 s. Long enough for slow networks; short enough to surface\n * CDN outages quickly. */\n timeoutMs?: number\n}\n\nexport interface BundleHandle {\n createFeedback(config: BundleConfig): FeedbackApi\n}\n\nexport async function injectBundle(\n opts: BundleLoadOptions,\n): Promise<BundleHandle> {\n // Reuse a bundle already loaded this page.\n if (window.MhosaicFeedback?.createFeedback) {\n return window.MhosaicFeedback\n }\n\n const existing = document.querySelector<HTMLScriptElement>(\n `script[${SCRIPT_DATA_ATTR}]`,\n )\n if (existing) {\n // Another loader instance started the fetch; wait for the global.\n return waitForGlobal(opts.timeoutMs ?? 30_000)\n }\n\n return new Promise<BundleHandle>((resolve, reject) => {\n const script = document.createElement('script')\n script.src = opts.bundleUrl\n script.integrity = opts.sriHash\n script.crossOrigin = 'anonymous'\n script.async = true\n script.setAttribute(SCRIPT_DATA_ATTR, '1')\n script.onload = () => {\n if (window.MhosaicFeedback?.createFeedback) {\n resolve(window.MhosaicFeedback)\n } else {\n reject(\n new Error(\n 'mhosaic-feedback: bundle loaded but window.MhosaicFeedback.createFeedback is missing — bundle/loader version mismatch?',\n ),\n )\n }\n }\n script.onerror = () => {\n reject(\n new Error(\n `mhosaic-feedback: failed to load bundle from ${opts.bundleUrl} ` +\n '(network, CSP, or SRI mismatch)',\n ),\n )\n }\n document.head.appendChild(script)\n\n const timeoutMs = opts.timeoutMs ?? 30_000\n setTimeout(() => {\n reject(\n new Error(`mhosaic-feedback: bundle load timeout after ${timeoutMs}ms`),\n )\n }, timeoutMs)\n })\n}\n\nfunction waitForGlobal(timeoutMs: number): Promise<BundleHandle> {\n return new Promise((resolve, reject) => {\n const start = Date.now()\n const tick = () => {\n if (window.MhosaicFeedback?.createFeedback) {\n resolve(window.MhosaicFeedback)\n return\n }\n if (Date.now() - start > timeoutMs) {\n reject(\n new Error(\n 'mhosaic-feedback: timed out waiting for bundle global (another loader instance started the fetch but never finished)',\n ),\n )\n return\n }\n setTimeout(tick, 50)\n }\n tick()\n })\n}\n","/**\n * Fetch the widget-manifest from the Mhosaic backend.\n *\n * The loader calls this on every page load. The backend reads `Project.\n * pinned_version` (or current stable) + `Project.widget_enabled` and\n * returns the bundle URL + SRI hash for the version this tenant should\n * receive — or `{enabled: false}` if the widget is killed for the tenant.\n *\n * Network failures are surfaced to the caller. The loader degrades to\n * \"widget not present\" rather than crashing the host page.\n */\n\nexport interface Manifest {\n /** False when the widget is disabled for this project (kill switch) or\n * no stable release exists. Loader bails silently. */\n enabled: boolean\n /** Semver of the bundle being served. Present when enabled=true. */\n version?: string\n /** Fully-qualified URL to the version-pinned bundle (jsDelivr). */\n bundle_url?: string\n /** Subresource integrity hash for the bundle. The loader injects the\n * script tag with `integrity=…` so the browser refuses to run a\n * bundle whose bytes don't match. */\n sri_hash?: string\n /** Static config the manifest endpoint passes through to the widget. */\n config?: {\n endpoint: string\n project_slug: string\n share_reports_with_widget: boolean\n /** Phase 4: project gates the widget per end-user (default false). */\n requires_visibility_check?: boolean\n /** Per-project switch for the bundle's default error capture. When the\n * backend omits it, the bundle falls back to its own default (on). */\n error_tracking?: boolean\n }\n /** Human-readable detail when `enabled` is false. */\n detail?: string\n}\n\nexport async function fetchManifest(\n endpoint: string,\n apiKey: string,\n signal?: AbortSignal,\n): Promise<Manifest> {\n const base = endpoint.replace(/\\/$/, '')\n const url = `${base}/api/feedback/v1/widget-manifest/?pk=${encodeURIComponent(apiKey)}`\n // credentials: 'omit' — we authenticate via the public pk_proj_ query\n // param; sending cookies would just trigger CORS preflights for no\n // reason and is unnecessary since the manifest endpoint doesn't read\n // any session.\n const init: RequestInit = { credentials: 'omit' }\n if (signal) init.signal = signal\n const res = await fetch(url, init)\n if (!res.ok) {\n throw new Error(\n `mhosaic-feedback: manifest fetch failed (HTTP ${res.status})`,\n )\n }\n const body = (await res.json()) as Manifest\n return body\n}\n","/**\n * A FeedbackApi-shaped object that queues method calls until the real\n * widget bundle finishes loading, then drains the queue against the real\n * implementation.\n *\n * Why we need this: hosts that already have code like\n * const fb = createFeedback({...})\n * fb.identify({id, email, name})\n * useEffect(() => fb.open(...), [])\n * shouldn't have to refactor for async. The loader's createFeedback\n * returns sync — a deferred handle that records intent, then replays it\n * once the real bundle arrives. Idiomatic queue-then-flush pattern.\n *\n * If the bundle never loads (network failure, SRI mismatch, kill switch),\n * `_fail()` is called instead. Void methods (identify/setMetadata/open/...)\n * silently drop in that case — the widget just doesn't appear. The\n * `submit` Promise rejects so callers can surface real errors.\n */\n\nimport type {\n FeedbackApi,\n ReportPayload,\n SubmittedReport,\n UserIdentity,\n} from '../types'\n\ntype VoidMethod = 'show' | 'hide' | 'shutdown'\ntype ArgMethod = 'open' | 'identify' | 'setMetadata'\n\ntype Queued =\n | { kind: 'void'; method: VoidMethod }\n | { kind: 'arg'; method: 'open'; arg: Parameters<FeedbackApi['open']>[0] }\n | { kind: 'arg'; method: 'identify'; arg: UserIdentity }\n | { kind: 'arg'; method: 'setMetadata'; arg: Record<string, unknown> }\n | {\n kind: 'submit'\n payload: Partial<ReportPayload> & { description: string }\n resolve: (r: SubmittedReport) => void\n reject: (e: unknown) => void\n }\n\nexport interface DeferredHandle extends FeedbackApi {\n /** Called by the loader once the bundle is loaded and a real API exists. */\n _attach(real: FeedbackApi): void\n /** Called by the loader if the bundle fails to load. */\n _fail(err: Error): void\n}\n\nexport function createDeferredApi(): DeferredHandle {\n let real: FeedbackApi | null = null\n let failure: Error | null = null\n const queue: Queued[] = []\n\n function flush(item: Queued): void {\n if (real) {\n if (item.kind === 'void') {\n ;(real[item.method] as () => void)()\n } else if (item.kind === 'arg') {\n ;(real[item.method] as (a: unknown) => void)(item.arg)\n } else {\n // submit\n Promise.resolve(real.submit(item.payload)).then(\n item.resolve,\n item.reject,\n )\n }\n return\n }\n if (failure) {\n if (item.kind === 'submit') item.reject(failure)\n // Void / arg methods silently dropped — the widget never appeared.\n return\n }\n queue.push(item)\n }\n\n return {\n show() {\n flush({ kind: 'void', method: 'show' })\n },\n hide() {\n flush({ kind: 'void', method: 'hide' })\n },\n shutdown() {\n flush({ kind: 'void', method: 'shutdown' })\n },\n open(arg) {\n flush({ kind: 'arg', method: 'open', arg })\n },\n identify(arg) {\n flush({ kind: 'arg', method: 'identify', arg })\n },\n setMetadata(arg) {\n flush({ kind: 'arg', method: 'setMetadata', arg })\n },\n submit(payload) {\n return new Promise<SubmittedReport>((resolve, reject) => {\n flush({ kind: 'submit', payload, resolve, reject })\n })\n },\n _attach(impl) {\n real = impl\n while (queue.length > 0) flush(queue.shift()!)\n },\n _fail(err) {\n failure = err\n // Drain any pending submits with the failure; drop void/arg.\n for (const item of queue) {\n if (item.kind === 'submit') item.reject(err)\n }\n queue.length = 0\n },\n }\n}\n","/**\n * `@mhosaic/feedback/loader` — public entry for the loader architecture.\n *\n * Same shape as the direct-import path (`@mhosaic/feedback`), but the\n * widget bundle is fetched at runtime from a CDN URL the Mhosaic backend\n * dictates via the manifest endpoint. Letting hosts switch from the\n * direct-import path to the loader path gives them auto-updates without\n * any further code changes — Mhosaic ships a new Release row, every host\n * picks up the new bundle on next page load.\n *\n * Public surface MUST mirror the direct-import path exactly:\n * - `createFeedback(config)` returns a `FeedbackApi`-shaped handle.\n * - Method calls before the bundle finishes loading queue up and replay.\n * - `submit()` returns a Promise that resolves after the bundle is up\n * and the real submit completes.\n *\n * If the manifest endpoint reports the widget is disabled for this\n * project (`enabled: false` — kill switch or no stable release configured),\n * the deferred handle silently no-ops void calls and rejects `submit()`\n * calls with a clear error. The host page never crashes.\n */\n\nimport type { FeedbackApi, FeedbackConfig } from '../types'\nimport type { ErrorTrackingOptions } from '../modules/error-tracking'\nimport { injectBundle } from './bundleLoader'\nimport { fetchManifest } from './manifest'\nimport { createDeferredApi, type DeferredHandle } from './deferredApi'\n\nexport type { FeedbackApi, FeedbackConfig } from '../types'\nexport type { Manifest } from './manifest'\n\n/** Loader config = the public widget config plus the loader-only knobs the\n * host can set to override what the manifest dictates. */\nexport type LoaderConfig = FeedbackConfig & {\n /** Override the bundle's default error capture. Omit (or pass `undefined`)\n * to let the project's manifest flag decide (which itself defaults on). */\n errorTracking?: boolean | ErrorTrackingOptions | undefined\n}\n\nexport function createFeedback(config: LoaderConfig): FeedbackApi {\n const deferred = createDeferredApi()\n loadAndAttach(config, deferred).catch((err: Error) => {\n // Surface to console — host devtools is the canonical channel for\n // widget bootstrap errors. Production telemetry is the bundle's job\n // (it can't run if it never loaded), so console.warn is the floor.\n // eslint-disable-next-line no-console\n console.warn('[mhosaic-feedback] widget did not load:', err.message)\n deferred._fail(err)\n })\n return deferred\n}\n\nasync function loadAndAttach(\n config: LoaderConfig,\n deferred: DeferredHandle,\n): Promise<void> {\n const manifest = await fetchManifest(config.endpoint, config.apiKey)\n if (!manifest.enabled || !manifest.bundle_url || !manifest.sri_hash) {\n // Disabled by Mhosaic (kill switch) or no stable release. Surface a\n // clear failure so submit() callers see an error, but no host crash.\n deferred._fail(\n new Error(\n `mhosaic-feedback: widget disabled for this project${\n manifest.detail ? ` — ${manifest.detail}` : ''\n }`,\n ),\n )\n return\n }\n const bundle = await injectBundle({\n bundleUrl: manifest.bundle_url,\n sriHash: manifest.sri_hash,\n })\n const real: FeedbackApi = bundle.createFeedback({\n ...config,\n // Forward the project's per-end-user gating flag from the manifest unless\n // the host set it explicitly. The widget then runs the visibility check.\n requiresVisibilityCheck:\n config.requiresVisibilityCheck ??\n manifest.config?.requires_visibility_check ??\n false,\n // Error capture is armed INSIDE the bundle (the only auto-updating part\n // of a loader install), so already-deployed hosts pick it up without a\n // redeploy. Precedence: explicit host override > per-project manifest\n // flag > on. The loader provider forwards its `errorTracking` prop here\n // rather than wrapping host-side, so there's exactly one arm point (the\n // real instance) — no deferred/real double-registration.\n errorTracking:\n config.errorTracking ?? manifest.config?.error_tracking ?? true,\n })\n deferred._attach(real)\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/modules/error-tracking.ts
|
|
2
|
+
var DEFAULTS = {
|
|
3
|
+
enabled: true,
|
|
4
|
+
perFingerprintCooldownMs: 5 * 60 * 1e3,
|
|
5
|
+
sampleRate: 1,
|
|
6
|
+
maxDescriptionLen: 200,
|
|
7
|
+
globalRatePerMinute: 30,
|
|
8
|
+
maxDistinctFingerprints: 200
|
|
9
|
+
};
|
|
10
|
+
function normalize(thrown) {
|
|
11
|
+
if (thrown instanceof Error) {
|
|
12
|
+
return {
|
|
13
|
+
name: thrown.name || "Error",
|
|
14
|
+
message: String(thrown.message ?? ""),
|
|
15
|
+
...thrown.stack && { stack: String(thrown.stack) }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (typeof thrown === "string") return { name: "Error", message: thrown };
|
|
19
|
+
if (thrown && typeof thrown === "object") {
|
|
20
|
+
const o = thrown;
|
|
21
|
+
return {
|
|
22
|
+
name: typeof o.name === "string" && o.name || "Error",
|
|
23
|
+
message: typeof o.message === "string" && o.message || (() => {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(thrown);
|
|
26
|
+
} catch {
|
|
27
|
+
return String(thrown);
|
|
28
|
+
}
|
|
29
|
+
})(),
|
|
30
|
+
...typeof o.stack === "string" && { stack: o.stack }
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { name: "Error", message: String(thrown) };
|
|
34
|
+
}
|
|
35
|
+
function clientFingerprint(err, pathname) {
|
|
36
|
+
const firstFrames = (err.stack ?? "").split("\n").slice(0, 4).join("\n");
|
|
37
|
+
const messagePrefix = err.message.slice(0, 30);
|
|
38
|
+
return `${err.name}:${messagePrefix}|${pathname}|${firstFrames}`;
|
|
39
|
+
}
|
|
40
|
+
function truncate(s, max) {
|
|
41
|
+
if (s.length <= max) return s;
|
|
42
|
+
return s.slice(0, Math.max(0, max - 1)) + "\u2026";
|
|
43
|
+
}
|
|
44
|
+
function withErrorTracking(fb, options = {}) {
|
|
45
|
+
const opts = { ...DEFAULTS, ...options };
|
|
46
|
+
if (!opts.enabled) return fb;
|
|
47
|
+
if (typeof window === "undefined") return fb;
|
|
48
|
+
const internal = fb;
|
|
49
|
+
if (internal._errorTrackingArmed) return fb;
|
|
50
|
+
internal._errorTrackingArmed = true;
|
|
51
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
52
|
+
const lastSent = /* @__PURE__ */ new Map();
|
|
53
|
+
const recentSendTimes = [];
|
|
54
|
+
function shouldSend(fp, now) {
|
|
55
|
+
if (opts.globalRatePerMinute > 0) {
|
|
56
|
+
const windowStart = now - 6e4;
|
|
57
|
+
while (recentSendTimes.length > 0 && recentSendTimes[0] < windowStart) {
|
|
58
|
+
recentSendTimes.shift();
|
|
59
|
+
}
|
|
60
|
+
if (recentSendTimes.length >= opts.globalRatePerMinute) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (opts.maxDistinctFingerprints > 0 && !lastSent.has(fp) && lastSent.size >= opts.maxDistinctFingerprints) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (opts.perFingerprintCooldownMs <= 0) {
|
|
68
|
+
recentSendTimes.push(now);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
const prev = lastSent.get(fp);
|
|
72
|
+
if (prev !== void 0 && now - prev < opts.perFingerprintCooldownMs) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const HARD_CAP = 256;
|
|
76
|
+
const TARGET_AFTER_TRIM = 192;
|
|
77
|
+
if (lastSent.size > HARD_CAP) {
|
|
78
|
+
const cutoff = now - opts.perFingerprintCooldownMs * 2;
|
|
79
|
+
for (const [k, v] of lastSent) {
|
|
80
|
+
if (v < cutoff) lastSent.delete(k);
|
|
81
|
+
}
|
|
82
|
+
if (lastSent.size > HARD_CAP) {
|
|
83
|
+
const toDrop = lastSent.size - TARGET_AFTER_TRIM;
|
|
84
|
+
let dropped = 0;
|
|
85
|
+
for (const k of lastSent.keys()) {
|
|
86
|
+
if (dropped >= toDrop) break;
|
|
87
|
+
lastSent.delete(k);
|
|
88
|
+
dropped++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
recentSendTimes.push(now);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
async function report(err) {
|
|
96
|
+
if (opts.sampleRate < 1 && Math.random() >= opts.sampleRate) return;
|
|
97
|
+
const fp = clientFingerprint(err, window.location.pathname);
|
|
98
|
+
if (inFlight.has(fp)) return;
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
if (!shouldSend(fp, now)) return;
|
|
101
|
+
lastSent.set(fp, now);
|
|
102
|
+
const description = truncate(
|
|
103
|
+
err.message ? `${err.name}: ${err.message}` : err.name,
|
|
104
|
+
opts.maxDescriptionLen
|
|
105
|
+
);
|
|
106
|
+
inFlight.add(fp);
|
|
107
|
+
try {
|
|
108
|
+
await internal.submit({
|
|
109
|
+
description,
|
|
110
|
+
feedback_type: "bug",
|
|
111
|
+
severity: "high",
|
|
112
|
+
synthetic: true
|
|
113
|
+
});
|
|
114
|
+
} catch {
|
|
115
|
+
} finally {
|
|
116
|
+
inFlight.delete(fp);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function onError(event) {
|
|
120
|
+
const candidate = event.error ?? {
|
|
121
|
+
name: "Error",
|
|
122
|
+
message: event.message,
|
|
123
|
+
stack: `at ${event.filename}:${event.lineno}:${event.colno}`
|
|
124
|
+
};
|
|
125
|
+
void report(normalize(candidate));
|
|
126
|
+
}
|
|
127
|
+
function onUnhandledRejection(event) {
|
|
128
|
+
void report(normalize(event.reason));
|
|
129
|
+
}
|
|
130
|
+
window.addEventListener("error", onError);
|
|
131
|
+
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
132
|
+
const originalShutdown = internal.shutdown.bind(internal);
|
|
133
|
+
internal.shutdown = () => {
|
|
134
|
+
window.removeEventListener("error", onError);
|
|
135
|
+
window.removeEventListener("unhandledrejection", onUnhandledRejection);
|
|
136
|
+
lastSent.clear();
|
|
137
|
+
originalShutdown();
|
|
138
|
+
};
|
|
139
|
+
return fb;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
withErrorTracking
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=chunk-QJ562NTG.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/modules/error-tracking.ts"],"sourcesContent":["/**\n * Always-on debugger module. Hooks `window.error` and `unhandledrejection`,\n * then ships each fresh runtime error as a synthetic FeedbackReport via\n * the existing widget submit pipeline.\n *\n * Same opt-in pattern as `withReplay` / `withWebVitals` — host apps that\n * want curated-feedback-only simply don't import it.\n *\n * Design choices (see PR description):\n * - Per-fingerprint cooldown (default 5 min) absorbs render loops without\n * flooding the operator dashboard or burning the per-key write throttle.\n * - Server-side fingerprinting groups across sessions; the client-side\n * fingerprint exists only to dedup within a single page lifetime, so\n * it doesn't need to match the server's hash.\n * - Recursion guard: an error thrown *inside* the submit pipeline must\n * not re-enter the handler.\n * - Description is the error message truncated at 200 chars — the full\n * stack lives in technical_context.errors[].\n * - No screenshot is taken (buildAndSubmit honors `synthetic: true`):\n * html2canvas is slow + the DOM is unreliable mid-error.\n */\n\nimport type { FeedbackApi } from '../types'\n\nexport interface ErrorTrackingOptions {\n /** Opt-out kill switch without removing the import. Default `true`. */\n enabled?: boolean\n /**\n * Drop repeats of the same fingerprint within this window.\n * Default 300_000 ms (5 minutes). Set to 0 to disable dedup\n * (every fired error becomes a report — useful only for tests).\n */\n perFingerprintCooldownMs?: number\n /**\n * Probabilistic head sampling: 1.0 captures everything, 0.5 drops half.\n * Default 1.0. Sampled-out errors don't count toward the cooldown — if\n * you sample at 0.1, you'll still see ~10% of the volume per fingerprint.\n */\n sampleRate?: number\n /** Max characters in the auto-generated description. Default 200. */\n maxDescriptionLen?: number\n /**\n * Global ceiling on synthetic submissions per minute, regardless of\n * fingerprint. Defends against a hostile / buggy page that throws\n * errors with attacker-varying `message` strings (counter, nonce) —\n * each unique message creates a fresh fingerprint, bypassing the\n * per-fingerprint cooldown, and would otherwise burn the project's\n * backend write quota.\n * Default 30. Set to 0 to disable the global cap (not recommended).\n */\n globalRatePerMinute?: number\n /**\n * Max number of distinct fingerprints emitted per page lifetime. Once\n * exceeded, NEW fingerprints are dropped (the cooldown still applies\n * to already-seen ones, so legitimate repeating errors keep cycling).\n * Defense against a page that synthesizes an unbounded number of\n * unique error shapes.\n * Default 200. Set to 0 to disable.\n */\n maxDistinctFingerprints?: number\n}\n\ninterface InternalApi extends FeedbackApi {\n _registerTransformer?: (fn: unknown) => void\n /** Set once withErrorTracking has armed this instance (idempotency guard). */\n _errorTrackingArmed?: boolean\n}\n\ninterface NormalizedError {\n name: string\n message: string\n stack?: string\n}\n\nconst DEFAULTS = {\n enabled: true,\n perFingerprintCooldownMs: 5 * 60 * 1000,\n sampleRate: 1,\n maxDescriptionLen: 200,\n globalRatePerMinute: 30,\n maxDistinctFingerprints: 200,\n} as const satisfies Required<ErrorTrackingOptions>\n\n/**\n * Pull a normalized {name, message, stack} from whatever the runtime\n * handed us. Browsers throw genuine Error subclasses for uncaught\n * exceptions but `unhandledrejection.reason` is sometimes a string,\n * a plain object, or undefined.\n */\nfunction normalize(thrown: unknown): NormalizedError {\n if (thrown instanceof Error) {\n return {\n name: thrown.name || 'Error',\n message: String(thrown.message ?? ''),\n ...(thrown.stack && { stack: String(thrown.stack) }),\n }\n }\n if (typeof thrown === 'string') return { name: 'Error', message: thrown }\n if (thrown && typeof thrown === 'object') {\n const o = thrown as Record<string, unknown>\n return {\n name: (typeof o.name === 'string' && o.name) || 'Error',\n message:\n (typeof o.message === 'string' && o.message) ||\n (() => {\n try { return JSON.stringify(thrown) } catch { return String(thrown) }\n })(),\n ...(typeof o.stack === 'string' && { stack: o.stack }),\n }\n }\n return { name: 'Error', message: String(thrown) }\n}\n\n/**\n * Stable per-session signature. Server still computes its own fingerprint\n * across sessions — this only needs to dedup within one page lifetime.\n * Stack frames are the strongest discriminator; pathname keeps two distinct\n * routes from collapsing onto the same key.\n *\n * SECURITY: we include only a 30-char message prefix, not the full message.\n * A hostile or buggy page that throws errors with counter-suffixed messages\n * (e.g. `Error 1`, `Error 2`, …) would otherwise produce a fresh fingerprint\n * per throw and bypass the per-fingerprint cooldown. The stack frames are\n * the same across those counter variants, so collapsing the message into a\n * prefix preserves real-error distinction while killing the counter bypass.\n */\nfunction clientFingerprint(err: NormalizedError, pathname: string): string {\n const firstFrames = (err.stack ?? '').split('\\n').slice(0, 4).join('\\n')\n const messagePrefix = err.message.slice(0, 30)\n return `${err.name}:${messagePrefix}|${pathname}|${firstFrames}`\n}\n\nfunction truncate(s: string, max: number): string {\n if (s.length <= max) return s\n return s.slice(0, Math.max(0, max - 1)) + '…'\n}\n\nexport function withErrorTracking(\n fb: FeedbackApi,\n options: ErrorTrackingOptions = {},\n): FeedbackApi {\n const opts = { ...DEFAULTS, ...options }\n if (!opts.enabled) return fb\n if (typeof window === 'undefined') return fb\n\n const internal = fb as InternalApi\n\n // Idempotency guard. The same instance can now be armed from two places:\n // the host-side <FeedbackProvider> (opt-out default) AND the runtime\n // widget bundle (which self-arms on the loader path). A client who both\n // upgrades the provider and receives the self-arming bundle would\n // otherwise double-register the window listeners and file every error\n // twice. First arm wins; later calls are no-ops.\n if (internal._errorTrackingArmed) return fb\n internal._errorTrackingArmed = true\n\n // Recursion guard, scoped per-fingerprint: while we're posting a synthetic\n // report for fingerprint X, a re-entry for the SAME X (e.g. an error\n // raised by the submit pipeline itself) is dropped. Distinct errors that\n // fire concurrently still both go through.\n const inFlight = new Set<string>()\n\n // fingerprint -> last-sent epoch ms. Periodically pruned in shouldSend().\n const lastSent = new Map<string, number>()\n // Sliding-window of recent send timestamps for the global rate cap.\n // Trimmed in shouldSend(); never grows beyond globalRatePerMinute entries.\n const recentSendTimes: number[] = []\n\n function shouldSend(fp: string, now: number): boolean {\n // (1) Global rate ceiling — runs first because it doesn't depend on\n // fingerprint state. Defends against the \"counter-suffixed error\n // messages\" attack that would otherwise produce one unique\n // fingerprint per throw.\n if (opts.globalRatePerMinute > 0) {\n const windowStart = now - 60_000\n // Drop expired entries from the front of the sliding window.\n while (recentSendTimes.length > 0 && recentSendTimes[0]! < windowStart) {\n recentSendTimes.shift()\n }\n if (recentSendTimes.length >= opts.globalRatePerMinute) {\n return false\n }\n }\n // (2) Per-page cap on the number of distinct fingerprints. A hostile\n // page that synthesizes an unbounded number of unique error\n // shapes runs out of new slots after this many — already-seen\n // fingerprints keep cycling under the per-fp cooldown.\n if (\n opts.maxDistinctFingerprints > 0 &&\n !lastSent.has(fp) &&\n lastSent.size >= opts.maxDistinctFingerprints\n ) {\n return false\n }\n if (opts.perFingerprintCooldownMs <= 0) {\n recentSendTimes.push(now)\n return true\n }\n const prev = lastSent.get(fp)\n if (prev !== undefined && now - prev < opts.perFingerprintCooldownMs) {\n return false\n }\n // Cheap stale-entry sweep so the Map doesn't grow unboundedly on a\n // long-lived session with many distinct errors. First pass: drop\n // entries past `cooldown * 2`. If we're still above a hard cap of\n // 256 — i.e. an SPA that's been live for hours and keeps emitting\n // *fresh* fingerprints faster than they go stale — drop the oldest\n // entries until we're back to a comfortable ceiling. Map iteration\n // order is insertion order so the first N keys are the oldest.\n const HARD_CAP = 256\n const TARGET_AFTER_TRIM = 192\n if (lastSent.size > HARD_CAP) {\n const cutoff = now - opts.perFingerprintCooldownMs * 2\n for (const [k, v] of lastSent) {\n if (v < cutoff) lastSent.delete(k)\n }\n if (lastSent.size > HARD_CAP) {\n const toDrop = lastSent.size - TARGET_AFTER_TRIM\n let dropped = 0\n for (const k of lastSent.keys()) {\n if (dropped >= toDrop) break\n lastSent.delete(k)\n dropped++\n }\n }\n }\n recentSendTimes.push(now)\n return true\n }\n\n async function report(err: NormalizedError) {\n if (opts.sampleRate < 1 && Math.random() >= opts.sampleRate) return\n\n const fp = clientFingerprint(err, window.location.pathname)\n if (inFlight.has(fp)) return\n const now = Date.now()\n if (!shouldSend(fp, now)) return\n lastSent.set(fp, now)\n\n const description = truncate(\n err.message ? `${err.name}: ${err.message}` : err.name,\n opts.maxDescriptionLen,\n )\n\n inFlight.add(fp)\n try {\n await internal.submit({\n description,\n feedback_type: 'bug',\n severity: 'high',\n synthetic: true,\n } as Parameters<FeedbackApi['submit']>[0] & { synthetic: boolean })\n } catch {\n // Swallow submit errors — propagating them would just trigger another\n // unhandledrejection and feed the loop. The host's `onError` callback\n // (configured on createFeedback) still fires for visibility.\n } finally {\n inFlight.delete(fp)\n }\n }\n\n function onError(event: ErrorEvent) {\n const candidate: unknown = event.error ?? {\n name: 'Error',\n message: event.message,\n stack: `at ${event.filename}:${event.lineno}:${event.colno}`,\n }\n void report(normalize(candidate))\n }\n\n function onUnhandledRejection(event: PromiseRejectionEvent) {\n void report(normalize(event.reason))\n }\n\n window.addEventListener('error', onError)\n window.addEventListener('unhandledrejection', onUnhandledRejection)\n\n // Wrap shutdown so listeners come down with the widget. Without this, a\n // host that re-mounts the widget under React StrictMode would double the\n // listeners and emit each error twice.\n const originalShutdown = internal.shutdown.bind(internal)\n internal.shutdown = () => {\n window.removeEventListener('error', onError)\n window.removeEventListener('unhandledrejection', onUnhandledRejection)\n lastSent.clear()\n originalShutdown()\n }\n\n return fb\n}\n"],"mappings":";AA0EA,IAAM,WAAW;AAAA,EACf,SAAS;AAAA,EACT,0BAA0B,IAAI,KAAK;AAAA,EACnC,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,yBAAyB;AAC3B;AAQA,SAAS,UAAU,QAAkC;AACnD,MAAI,kBAAkB,OAAO;AAC3B,WAAO;AAAA,MACL,MAAM,OAAO,QAAQ;AAAA,MACrB,SAAS,OAAO,OAAO,WAAW,EAAE;AAAA,MACpC,GAAI,OAAO,SAAS,EAAE,OAAO,OAAO,OAAO,KAAK,EAAE;AAAA,IACpD;AAAA,EACF;AACA,MAAI,OAAO,WAAW,SAAU,QAAO,EAAE,MAAM,SAAS,SAAS,OAAO;AACxE,MAAI,UAAU,OAAO,WAAW,UAAU;AACxC,UAAM,IAAI;AACV,WAAO;AAAA,MACL,MAAO,OAAO,EAAE,SAAS,YAAY,EAAE,QAAS;AAAA,MAChD,SACG,OAAO,EAAE,YAAY,YAAY,EAAE,YACnC,MAAM;AACL,YAAI;AAAE,iBAAO,KAAK,UAAU,MAAM;AAAA,QAAE,QAAQ;AAAE,iBAAO,OAAO,MAAM;AAAA,QAAE;AAAA,MACtE,GAAG;AAAA,MACL,GAAI,OAAO,EAAE,UAAU,YAAY,EAAE,OAAO,EAAE,MAAM;AAAA,IACtD;AAAA,EACF;AACA,SAAO,EAAE,MAAM,SAAS,SAAS,OAAO,MAAM,EAAE;AAClD;AAeA,SAAS,kBAAkB,KAAsB,UAA0B;AACzE,QAAM,eAAe,IAAI,SAAS,IAAI,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI;AACvE,QAAM,gBAAgB,IAAI,QAAQ,MAAM,GAAG,EAAE;AAC7C,SAAO,GAAG,IAAI,IAAI,IAAI,aAAa,IAAI,QAAQ,IAAI,WAAW;AAChE;AAEA,SAAS,SAAS,GAAW,KAAqB;AAChD,MAAI,EAAE,UAAU,IAAK,QAAO;AAC5B,SAAO,EAAE,MAAM,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC,CAAC,IAAI;AAC5C;AAEO,SAAS,kBACd,IACA,UAAgC,CAAC,GACpB;AACb,QAAM,OAAO,EAAE,GAAG,UAAU,GAAG,QAAQ;AACvC,MAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,MAAI,OAAO,WAAW,YAAa,QAAO;AAE1C,QAAM,WAAW;AAQjB,MAAI,SAAS,oBAAqB,QAAO;AACzC,WAAS,sBAAsB;AAM/B,QAAM,WAAW,oBAAI,IAAY;AAGjC,QAAM,WAAW,oBAAI,IAAoB;AAGzC,QAAM,kBAA4B,CAAC;AAEnC,WAAS,WAAW,IAAY,KAAsB;AAKpD,QAAI,KAAK,sBAAsB,GAAG;AAChC,YAAM,cAAc,MAAM;AAE1B,aAAO,gBAAgB,SAAS,KAAK,gBAAgB,CAAC,IAAK,aAAa;AACtE,wBAAgB,MAAM;AAAA,MACxB;AACA,UAAI,gBAAgB,UAAU,KAAK,qBAAqB;AACtD,eAAO;AAAA,MACT;AAAA,IACF;AAKA,QACE,KAAK,0BAA0B,KAC/B,CAAC,SAAS,IAAI,EAAE,KAChB,SAAS,QAAQ,KAAK,yBACtB;AACA,aAAO;AAAA,IACT;AACA,QAAI,KAAK,4BAA4B,GAAG;AACtC,sBAAgB,KAAK,GAAG;AACxB,aAAO;AAAA,IACT;AACA,UAAM,OAAO,SAAS,IAAI,EAAE;AAC5B,QAAI,SAAS,UAAa,MAAM,OAAO,KAAK,0BAA0B;AACpE,aAAO;AAAA,IACT;AAQA,UAAM,WAAW;AACjB,UAAM,oBAAoB;AAC1B,QAAI,SAAS,OAAO,UAAU;AAC5B,YAAM,SAAS,MAAM,KAAK,2BAA2B;AACrD,iBAAW,CAAC,GAAG,CAAC,KAAK,UAAU;AAC7B,YAAI,IAAI,OAAQ,UAAS,OAAO,CAAC;AAAA,MACnC;AACA,UAAI,SAAS,OAAO,UAAU;AAC5B,cAAM,SAAS,SAAS,OAAO;AAC/B,YAAI,UAAU;AACd,mBAAW,KAAK,SAAS,KAAK,GAAG;AAC/B,cAAI,WAAW,OAAQ;AACvB,mBAAS,OAAO,CAAC;AACjB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,oBAAgB,KAAK,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,iBAAe,OAAO,KAAsB;AAC1C,QAAI,KAAK,aAAa,KAAK,KAAK,OAAO,KAAK,KAAK,WAAY;AAE7D,UAAM,KAAK,kBAAkB,KAAK,OAAO,SAAS,QAAQ;AAC1D,QAAI,SAAS,IAAI,EAAE,EAAG;AACtB,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,CAAC,WAAW,IAAI,GAAG,EAAG;AAC1B,aAAS,IAAI,IAAI,GAAG;AAEpB,UAAM,cAAc;AAAA,MAClB,IAAI,UAAU,GAAG,IAAI,IAAI,KAAK,IAAI,OAAO,KAAK,IAAI;AAAA,MAClD,KAAK;AAAA,IACP;AAEA,aAAS,IAAI,EAAE;AACf,QAAI;AACF,YAAM,SAAS,OAAO;AAAA,QACpB;AAAA,QACA,eAAe;AAAA,QACf,UAAU;AAAA,QACV,WAAW;AAAA,MACb,CAAkE;AAAA,IACpE,QAAQ;AAAA,IAIR,UAAE;AACA,eAAS,OAAO,EAAE;AAAA,IACpB;AAAA,EACF;AAEA,WAAS,QAAQ,OAAmB;AAClC,UAAM,YAAqB,MAAM,SAAS;AAAA,MACxC,MAAM;AAAA,MACN,SAAS,MAAM;AAAA,MACf,OAAO,MAAM,MAAM,QAAQ,IAAI,MAAM,MAAM,IAAI,MAAM,KAAK;AAAA,IAC5D;AACA,SAAK,OAAO,UAAU,SAAS,CAAC;AAAA,EAClC;AAEA,WAAS,qBAAqB,OAA8B;AAC1D,SAAK,OAAO,UAAU,MAAM,MAAM,CAAC;AAAA,EACrC;AAEA,SAAO,iBAAiB,SAAS,OAAO;AACxC,SAAO,iBAAiB,sBAAsB,oBAAoB;AAKlE,QAAM,mBAAmB,SAAS,SAAS,KAAK,QAAQ;AACxD,WAAS,WAAW,MAAM;AACxB,WAAO,oBAAoB,SAAS,OAAO;AAC3C,WAAO,oBAAoB,sBAAsB,oBAAoB;AACrE,aAAS,MAAM;AACf,qBAAiB;AAAA,EACnB;AAEA,SAAO;AACT;","names":[]}
|