@mhosaic/feedback 0.36.0 → 0.37.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.
@@ -202,7 +202,8 @@ async function loadAndAttach(config, deferred) {
202
202
  // an explicit host value still wins.
203
203
  ...config.openToCurrentPageFeedback === void 0 && typeof manifest.config?.behavior?.openToCurrentPageFeedback === "boolean" ? {
204
204
  openToCurrentPageFeedback: manifest.config.behavior.openToCurrentPageFeedback
205
- } : {}
205
+ } : {},
206
+ ...config.metaFeedbackEnabled === void 0 && typeof manifest.config?.behavior?.metaFeedbackEnabled === "boolean" ? { metaFeedbackEnabled: manifest.config.behavior.metaFeedbackEnabled } : {}
206
207
  });
207
208
  deferred._attach(real);
208
209
  }
@@ -210,4 +211,4 @@ async function loadAndAttach(config, deferred) {
210
211
  export {
211
212
  createFeedback
212
213
  };
213
- //# sourceMappingURL=chunk-557ACRBU.mjs.map
214
+ //# sourceMappingURL=chunk-BQDUJOWL.mjs.map
@@ -1 +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 behavior?: Record<string, 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 /** Buffered transformer registry — forwarded to the real instance on\n * attach, so wrapper modules (withReplay) work over loader installs. */\n _registerTransformer(fn: unknown): void\n}\n\nexport function createDeferredApi(): DeferredHandle {\n let real: FeedbackApi | null = null\n let failure: Error | null = null\n const queue: Queued[] = []\n // Transformers (withReplay & friends) registered before the bundle\n // attaches. Forwarded to the real instance's registry on _attach so\n // host-side wrappers work over a loader install too (roadmap #7).\n const pendingTransformers: unknown[] = []\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 _registerTransformer(fn: unknown) {\n const target = real as { _registerTransformer?: (f: unknown) => void } | null\n if (target && typeof target._registerTransformer === 'function') {\n target._registerTransformer(fn)\n return\n }\n if (failure) return // widget never appeared — nothing to transform\n pendingTransformers.push(fn)\n },\n _attach(impl) {\n real = impl\n const target = impl as { _registerTransformer?: (f: unknown) => void }\n if (typeof target._registerTransformer === 'function') {\n for (const fn of pendingTransformers) target._registerTransformer(fn)\n }\n pendingTransformers.length = 0\n while (queue.length > 0) flush(queue.shift()!)\n },\n _fail(err) {\n failure = err\n pendingTransformers.length = 0\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 // Roadmap #17: operator-flipped behavior toggles ride the manifest;\n // an explicit host value still wins.\n ...(config.openToCurrentPageFeedback === undefined &&\n typeof manifest.config?.behavior?.openToCurrentPageFeedback === 'boolean'\n ? {\n openToCurrentPageFeedback:\n manifest.config.behavior.openToCurrentPageFeedback,\n }\n : {}),\n })\n deferred._attach(real)\n}\n"],"mappings":"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","names":[]}
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 behavior?: Record<string, 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 /** Buffered transformer registry — forwarded to the real instance on\n * attach, so wrapper modules (withReplay) work over loader installs. */\n _registerTransformer(fn: unknown): void\n}\n\nexport function createDeferredApi(): DeferredHandle {\n let real: FeedbackApi | null = null\n let failure: Error | null = null\n const queue: Queued[] = []\n // Transformers (withReplay & friends) registered before the bundle\n // attaches. Forwarded to the real instance's registry on _attach so\n // host-side wrappers work over a loader install too (roadmap #7).\n const pendingTransformers: unknown[] = []\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 _registerTransformer(fn: unknown) {\n const target = real as { _registerTransformer?: (f: unknown) => void } | null\n if (target && typeof target._registerTransformer === 'function') {\n target._registerTransformer(fn)\n return\n }\n if (failure) return // widget never appeared — nothing to transform\n pendingTransformers.push(fn)\n },\n _attach(impl) {\n real = impl\n const target = impl as { _registerTransformer?: (f: unknown) => void }\n if (typeof target._registerTransformer === 'function') {\n for (const fn of pendingTransformers) target._registerTransformer(fn)\n }\n pendingTransformers.length = 0\n while (queue.length > 0) flush(queue.shift()!)\n },\n _fail(err) {\n failure = err\n pendingTransformers.length = 0\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(config: LoaderConfig, deferred: DeferredHandle): 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 ?? manifest.config?.requires_visibility_check ?? 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: config.errorTracking ?? manifest.config?.error_tracking ?? true,\n // Roadmap #17: operator-flipped behavior toggles ride the manifest;\n // an explicit host value still wins.\n ...(config.openToCurrentPageFeedback === undefined &&\n typeof manifest.config?.behavior?.openToCurrentPageFeedback === 'boolean'\n ? {\n openToCurrentPageFeedback: manifest.config.behavior.openToCurrentPageFeedback,\n }\n : {}),\n ...(config.metaFeedbackEnabled === undefined &&\n typeof manifest.config?.behavior?.metaFeedbackEnabled === 'boolean'\n ? { metaFeedbackEnabled: manifest.config.behavior.metaFeedbackEnabled }\n : {}),\n })\n deferred._attach(real)\n}\n"],"mappings":"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","names":[]}
@@ -20,9 +20,7 @@ function assertSafeEndpoint(endpoint) {
20
20
  try {
21
21
  parsed = new URL(endpoint);
22
22
  } catch {
23
- throw new Error(
24
- `[mhosaic-feedback] \`endpoint\` is not a valid URL: ${endpoint}`
25
- );
23
+ throw new Error(`[mhosaic-feedback] \`endpoint\` is not a valid URL: ${endpoint}`);
26
24
  }
27
25
  if (parsed.protocol === "https:") return;
28
26
  if (parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]")) {
@@ -91,6 +89,7 @@ function createApiClient(options) {
91
89
  form.append("screenshot", blob, i === 0 ? "screenshot.png" : `screenshot-${i + 1}.png`);
92
90
  });
93
91
  if (payload.synthetic) form.append("synthetic", "true");
92
+ if (payload.meta) form.append("meta", "true");
94
93
  if (payload.user?.id) {
95
94
  form.append("user", JSON.stringify(payload.user));
96
95
  }
@@ -172,29 +171,26 @@ function createApiClient(options) {
172
171
  return "healthy";
173
172
  }
174
173
  async function listChangelog(externalId) {
175
- const response = await fetcher(
176
- `${endpoint}/api/feedback/v1/reports/widget/changelog/`,
177
- { method: "GET", headers: widgetHeaders(externalId) }
178
- );
174
+ const response = await fetcher(`${endpoint}/api/feedback/v1/reports/widget/changelog/`, {
175
+ method: "GET",
176
+ headers: widgetHeaders(externalId)
177
+ });
179
178
  if (response.status === 404) return [];
180
179
  return readJsonArray(response, "listChangelog");
181
180
  }
182
181
  async function getReport(reportId, externalId, opts) {
183
- const response = await fetcher(
184
- `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
185
- {
186
- method: "GET",
187
- headers: widgetHeaders(externalId),
188
- ...opts?.signal ? { signal: opts.signal } : {}
189
- }
190
- );
182
+ const response = await fetcher(`${endpoint}/api/feedback/v1/reports/widget/${reportId}/`, {
183
+ method: "GET",
184
+ headers: widgetHeaders(externalId),
185
+ ...opts?.signal ? { signal: opts.signal } : {}
186
+ });
191
187
  return readJsonObject(response, "getReport");
192
188
  }
193
189
  async function getReportBySeq(seq, externalId) {
194
- const response = await fetcher(
195
- `${endpoint}/api/feedback/v1/reports/widget/by-seq/${seq}/`,
196
- { method: "GET", headers: widgetHeaders(externalId) }
197
- );
190
+ const response = await fetcher(`${endpoint}/api/feedback/v1/reports/widget/by-seq/${seq}/`, {
191
+ method: "GET",
192
+ headers: widgetHeaders(externalId)
193
+ });
198
194
  return readJsonObject(response, "getReportBySeq");
199
195
  }
200
196
  async function addComment(reportId, externalId, body, clientNonce, attachments) {
@@ -203,9 +199,7 @@ function createApiClient(options) {
203
199
  const form = new FormData();
204
200
  form.append("body", body);
205
201
  if (clientNonce !== void 0) form.append("client_nonce", clientNonce);
206
- attachments.forEach(
207
- (blob, i) => form.append("attachment", blob, `attachment-${i + 1}.png`)
208
- );
202
+ attachments.forEach((blob, i) => form.append("attachment", blob, `attachment-${i + 1}.png`));
209
203
  init = { method: "POST", headers: widgetHeaders(externalId), body: form };
210
204
  } else {
211
205
  init = {
@@ -231,17 +225,14 @@ function createApiClient(options) {
231
225
  return response.json();
232
226
  }
233
227
  async function editDescription(reportId, externalId, description) {
234
- const response = await fetcher(
235
- `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
236
- {
237
- method: "PATCH",
238
- headers: {
239
- ...widgetHeaders(externalId),
240
- "Content-Type": "application/json"
241
- },
242
- body: JSON.stringify({ description })
243
- }
244
- );
228
+ const response = await fetcher(`${endpoint}/api/feedback/v1/reports/widget/${reportId}/`, {
229
+ method: "PATCH",
230
+ headers: {
231
+ ...widgetHeaders(externalId),
232
+ "Content-Type": "application/json"
233
+ },
234
+ body: JSON.stringify({ description })
235
+ });
245
236
  if (!response.ok) {
246
237
  const text = await response.text().catch(() => "");
247
238
  throw new Error(`editDescription failed: ${response.status} ${text}`);
@@ -249,17 +240,14 @@ function createApiClient(options) {
249
240
  return response.json();
250
241
  }
251
242
  async function closeAsResolved(reportId, externalId) {
252
- const response = await fetcher(
253
- `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
254
- {
255
- method: "PATCH",
256
- headers: {
257
- ...widgetHeaders(externalId),
258
- "Content-Type": "application/json"
259
- },
260
- body: JSON.stringify({ status: "closed" })
261
- }
262
- );
243
+ const response = await fetcher(`${endpoint}/api/feedback/v1/reports/widget/${reportId}/`, {
244
+ method: "PATCH",
245
+ headers: {
246
+ ...widgetHeaders(externalId),
247
+ "Content-Type": "application/json"
248
+ },
249
+ body: JSON.stringify({ status: "closed" })
250
+ });
263
251
  if (!response.ok) {
264
252
  const text = await response.text().catch(() => "");
265
253
  throw new Error(`closeAsResolved failed: ${response.status} ${text}`);
@@ -267,17 +255,14 @@ function createApiClient(options) {
267
255
  return response.json();
268
256
  }
269
257
  async function reopenUnresolved(reportId, externalId) {
270
- const response = await fetcher(
271
- `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
272
- {
273
- method: "PATCH",
274
- headers: {
275
- ...widgetHeaders(externalId),
276
- "Content-Type": "application/json"
277
- },
278
- body: JSON.stringify({ status: "in_progress" })
279
- }
280
- );
258
+ const response = await fetcher(`${endpoint}/api/feedback/v1/reports/widget/${reportId}/`, {
259
+ method: "PATCH",
260
+ headers: {
261
+ ...widgetHeaders(externalId),
262
+ "Content-Type": "application/json"
263
+ },
264
+ body: JSON.stringify({ status: "in_progress" })
265
+ });
281
266
  if (!response.ok) {
282
267
  const text = await response.text().catch(() => "");
283
268
  throw new Error(`reopenUnresolved failed: ${response.status} ${text}`);
@@ -331,20 +316,17 @@ function createApiClient(options) {
331
316
  return readJsonObject(response, "fetchBoardKpis");
332
317
  }
333
318
  async function checkVisibility(externalId, email) {
334
- const response = await fetcher(
335
- `${endpoint}/api/feedback/v1/widget/visibility/`,
336
- {
337
- method: "POST",
338
- headers: {
339
- ...widgetHeaders(externalId),
340
- "Content-Type": "application/json"
341
- },
342
- body: JSON.stringify({
343
- external_id: externalId,
344
- ...email ? { email } : {}
345
- })
346
- }
347
- );
319
+ const response = await fetcher(`${endpoint}/api/feedback/v1/widget/visibility/`, {
320
+ method: "POST",
321
+ headers: {
322
+ ...widgetHeaders(externalId),
323
+ "Content-Type": "application/json"
324
+ },
325
+ body: JSON.stringify({
326
+ external_id: externalId,
327
+ ...email ? { email } : {}
328
+ })
329
+ });
348
330
  if (!response.ok) {
349
331
  throw new Error(`checkVisibility failed: ${response.status}`);
350
332
  }
@@ -677,6 +659,9 @@ var DEFAULT_STRINGS = {
677
659
  "form.cancel": "Cancel",
678
660
  "form.close": "Close",
679
661
  "form.submitting": "Sending\u2026",
662
+ "form.meta.action": "Report a problem with this feedback tool",
663
+ "form.meta.badge": "\u2192 goes to the feedback-tool team",
664
+ "form.meta.cancel": "Back to normal feedback",
680
665
  "form.success": "Thanks \u2014 your feedback was sent.",
681
666
  "form.success.seq": "Thanks \u2014 report #{seq} was sent. \u2713",
682
667
  "form.discard.title": "Unsaved changes",
@@ -881,6 +866,9 @@ var FRENCH_STRINGS = {
881
866
  "form.cancel": "Annuler",
882
867
  "form.close": "Fermer",
883
868
  "form.submitting": "Envoi\u2026",
869
+ "form.meta.action": "Signaler un probl\xE8me avec l'outil de feedback",
870
+ "form.meta.badge": "\u2192 envoy\xE9 \xE0 l'\xE9quipe de l'outil de feedback",
871
+ "form.meta.cancel": "Revenir au commentaire normal",
884
872
  "form.success": "Merci \u2014 votre commentaire a \xE9t\xE9 envoy\xE9.",
885
873
  "form.success.seq": "Merci \u2014 rapport n\xB0{seq} envoy\xE9. \u2713",
886
874
  "form.discard.title": "Modifications non enregistr\xE9es",
@@ -1318,6 +1306,26 @@ var ALLOWED_IMAGE_TYPES = [
1318
1306
  ];
1319
1307
  var MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
1320
1308
  var MAX_SCREENSHOTS = 5;
1309
+ var ALLOWED_ATTACHMENT_TYPES = [
1310
+ ...ALLOWED_IMAGE_TYPES,
1311
+ "application/pdf",
1312
+ "text/plain",
1313
+ "text/csv",
1314
+ "application/json",
1315
+ ""
1316
+ ];
1317
+ var ATTACHMENT_ACCEPT = "image/png,image/jpeg,image/webp,application/pdf,text/plain,.log,.txt,.csv,.json";
1318
+ function validateAttachmentFile(file) {
1319
+ if (!ALLOWED_ATTACHMENT_TYPES.includes(
1320
+ file.type
1321
+ )) {
1322
+ return { kind: "type" };
1323
+ }
1324
+ if (file.size > MAX_SCREENSHOT_BYTES) {
1325
+ return { kind: "size", maxMb: MAX_SCREENSHOT_BYTES / (1024 * 1024) };
1326
+ }
1327
+ return null;
1328
+ }
1321
1329
  function validateScreenshotFile(file) {
1322
1330
  if (!ALLOWED_IMAGE_TYPES.includes(
1323
1331
  file.type
@@ -1404,7 +1412,7 @@ function ReportDetailView({
1404
1412
  const remaining = MAX_SCREENSHOTS - composeShots.length;
1405
1413
  const batch = files.slice(0, Math.max(0, remaining));
1406
1414
  for (const file of batch) {
1407
- const err = validateScreenshotFile(file);
1415
+ const err = validateAttachmentFile(file);
1408
1416
  if (err) {
1409
1417
  setAttachError(
1410
1418
  err.kind === "type" ? strings["form.screenshot.error_type"] : strings["form.screenshot.error_size"].replace("{max}", String(err.maxMb))
@@ -1776,7 +1784,7 @@ function ReportDetailView({
1776
1784
  {
1777
1785
  ref: fileInputRef,
1778
1786
  type: "file",
1779
- accept: "image/png,image/jpeg,image/webp",
1787
+ accept: ATTACHMENT_ACCEPT,
1780
1788
  multiple: true,
1781
1789
  class: "compose-attach-input",
1782
1790
  onChange: handleAttachChange,
@@ -3386,8 +3394,10 @@ function Form({
3386
3394
  status,
3387
3395
  errorMessage,
3388
3396
  submittedSeq,
3389
- onDirtyChange
3397
+ onDirtyChange,
3398
+ metaFeedbackEnabled
3390
3399
  }) {
3400
+ const [metaMode, setMetaMode] = useState6(false);
3391
3401
  const [description, setDescription] = useState6("");
3392
3402
  const [feedbackType, setFeedbackType] = useState6("bug");
3393
3403
  const [severity, setSeverity] = useState6("medium");
@@ -3507,10 +3517,14 @@ function Form({
3507
3517
  values.screenshots = shots.map((s) => s.blob);
3508
3518
  values.capture_method = "manual";
3509
3519
  }
3520
+ if (metaMode) {
3521
+ values.meta = true;
3522
+ }
3510
3523
  onSubmit(values);
3511
3524
  };
3512
3525
  return /* @__PURE__ */ jsxs9("form", { onSubmit: handleSubmit, onPaste: handlePaste, children: [
3513
3526
  /* @__PURE__ */ jsx9("h2", { children: strings["form.title"] }),
3527
+ metaMode && /* @__PURE__ */ jsx9("div", { class: "meta-badge", children: strings["form.meta.badge"] }),
3514
3528
  /* @__PURE__ */ jsxs9("div", { class: "field", children: [
3515
3529
  /* @__PURE__ */ jsx9("label", { for: "mfb-desc", children: strings["form.description.label"] }),
3516
3530
  /* @__PURE__ */ jsx9(
@@ -3677,6 +3691,16 @@ function Form({
3677
3691
  /* @__PURE__ */ jsx9("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
3678
3692
  /* @__PURE__ */ jsx9("button", { type: "submit", class: "btn btn--primary", disabled: submitting, children: submitLabel })
3679
3693
  ] }),
3694
+ metaFeedbackEnabled && /* @__PURE__ */ jsx9(
3695
+ "button",
3696
+ {
3697
+ type: "button",
3698
+ class: "meta-toggle",
3699
+ onClick: () => setMetaMode(!metaMode),
3700
+ disabled: submitting,
3701
+ children: metaMode ? strings["form.meta.cancel"] : strings["form.meta.action"]
3702
+ }
3703
+ ),
3680
3704
  annotatingIndex !== null && shots[annotatingIndex] && /* @__PURE__ */ jsx9(
3681
3705
  Annotator,
3682
3706
  {
@@ -4540,6 +4564,12 @@ var WIDGET_STYLES = `
4540
4564
  /* REQ-A7: quiet transparency line under the form. */
4541
4565
  .capture-notice { color: var(--mfb-muted, #6b7280); font-size: 11px; line-height: 1.4; margin-top: 6px; }
4542
4566
 
4567
+ /* Meta-feedback: footer escape hatch + active-mode badge. Deliberately as
4568
+ quiet as the capture notice \u2014 never competes with the host's own flow. */
4569
+ .meta-toggle { background: none; border: none; padding: 0; margin-top: 10px; color: var(--mfb-muted, #6b7280); font-size: 11px; text-decoration: underline; cursor: pointer; align-self: flex-start; }
4570
+ .meta-toggle:hover { color: var(--mfb-text, #111827); }
4571
+ .meta-badge { display: inline-block; margin: 2px 0 6px; padding: 2px 8px; border-radius: 999px; background: var(--mfb-accent-soft, #eef2ff); color: var(--mfb-accent, #4f46e5); font-size: 11px; font-weight: 600; }
4572
+
4543
4573
  /* #85: inline "#NN" report reference rendered as a link (a button for a11y). */
4544
4574
  .seq-link {
4545
4575
  display: inline;
@@ -6323,8 +6353,7 @@ function mountWidget(options) {
6323
6353
  });
6324
6354
  }, 1200);
6325
6355
  } catch (err) {
6326
- if (typeof console !== "undefined")
6327
- console.warn("[mhosaic] submit:", err);
6356
+ if (typeof console !== "undefined") console.warn("[mhosaic] submit:", err);
6328
6357
  rerender({
6329
6358
  ...currentState,
6330
6359
  status: "error",
@@ -6358,9 +6387,7 @@ function mountWidget(options) {
6358
6387
  enabled: pageActivityEnabled
6359
6388
  });
6360
6389
  const fabLabel = pageSignal.open === 1 ? options.strings["page.badge.aria.one"] : pageSignal.open > 0 ? options.strings["page.badge.aria"].replace("{n}", String(pageSignal.open)) : options.strings["fab.label"];
6361
- const [visibilityAllowed, setVisibilityAllowed] = useState10(
6362
- !options.requiresVisibilityCheck
6363
- );
6390
+ const [visibilityAllowed, setVisibilityAllowed] = useState10(!options.requiresVisibilityCheck);
6364
6391
  useEffect10(() => {
6365
6392
  if (!options.requiresVisibilityCheck) {
6366
6393
  setVisibilityAllowed(true);
@@ -6564,6 +6591,9 @@ function mountWidget(options) {
6564
6591
  },
6565
6592
  onDirtyChange: (d) => {
6566
6593
  formDirty = d;
6594
+ },
6595
+ ...options.metaFeedbackEnabled !== void 0 && {
6596
+ metaFeedbackEnabled: options.metaFeedbackEnabled
6567
6597
  }
6568
6598
  }
6569
6599
  )
@@ -6607,8 +6637,12 @@ function mountWidget(options) {
6607
6637
  api: options.api,
6608
6638
  externalId,
6609
6639
  strings: options.strings,
6610
- ...options.getCurrentPage !== void 0 && { getCurrentPage: options.getCurrentPage },
6611
- ...state.boardSelectId !== void 0 && { initialSelectedId: state.boardSelectId }
6640
+ ...options.getCurrentPage !== void 0 && {
6641
+ getCurrentPage: options.getCurrentPage
6642
+ },
6643
+ ...state.boardSelectId !== void 0 && {
6644
+ initialSelectedId: state.boardSelectId
6645
+ }
6612
6646
  }
6613
6647
  )
6614
6648
  ]
@@ -6623,9 +6657,7 @@ function mountWidget(options) {
6623
6657
  if (!api || !externalId || !ref) return;
6624
6658
  const seq = /^\d+$/.test(ref) ? parseInt(ref, 10) : null;
6625
6659
  const resolve = seq !== null ? api.getReportBySeq(seq, externalId) : api.getReport(ref, externalId);
6626
- void resolve.then(
6627
- (r) => rerender({ ...currentState, open: true, tab: "mine", selectedReportId: r.id })
6628
- ).catch(() => {
6660
+ void resolve.then((r) => rerender({ ...currentState, open: true, tab: "mine", selectedReportId: r.id })).catch(() => {
6629
6661
  });
6630
6662
  }
6631
6663
  if (options.deepLinkParam !== false) {
@@ -6664,10 +6696,7 @@ function defaultQaPosition() {
6664
6696
  function createFeedback(config) {
6665
6697
  const env = config.env ?? "prod";
6666
6698
  const locale = config.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0);
6667
- const strings = resolveStrings(
6668
- config.translations ?? {},
6669
- locale !== void 0 ? { locale } : {}
6670
- );
6699
+ const strings = resolveStrings(config.translations ?? {}, locale !== void 0 ? { locale } : {});
6671
6700
  const capture = installCapture({
6672
6701
  ...config.sanitizeUrl !== void 0 && { sanitizeUrl: config.sanitizeUrl }
6673
6702
  });
@@ -6722,14 +6751,15 @@ function createFeedback(config) {
6722
6751
  capture_method: captureMethod,
6723
6752
  technical_context
6724
6753
  };
6725
- if ("0.36.0") {
6726
- payload.widget_version = "0.36.0";
6754
+ if ("0.37.0") {
6755
+ payload.widget_version = "0.37.0";
6727
6756
  }
6728
6757
  if (manualScreenshots?.length) {
6729
6758
  payload.screenshots = manualScreenshots;
6730
6759
  payload.screenshot = manualScreenshots[0];
6731
6760
  }
6732
6761
  if (values.synthetic) payload.synthetic = true;
6762
+ if (values.meta) payload.meta = true;
6733
6763
  const pagePath = currentPagePath(config);
6734
6764
  if (pagePath) payload.page_path = pagePath;
6735
6765
  if (user?.id !== void 0 && user.id !== null && user.id !== "") {
@@ -6779,12 +6809,15 @@ function createFeedback(config) {
6779
6809
  ...config.openToCurrentPageFeedback !== void 0 && {
6780
6810
  openToCurrentPageFeedback: config.openToCurrentPageFeedback
6781
6811
  },
6812
+ ...config.metaFeedbackEnabled !== void 0 && {
6813
+ metaFeedbackEnabled: config.metaFeedbackEnabled
6814
+ },
6782
6815
  ...config.showPageActivity !== void 0 && {
6783
6816
  showPageActivity: config.showPageActivity
6784
6817
  }
6785
6818
  };
6786
6819
  const handle = mountWidget(mountOptions);
6787
- if (config.openToCurrentPageFeedback === void 0 && config.showFAB !== false) {
6820
+ if ((config.openToCurrentPageFeedback === void 0 || config.metaFeedbackEnabled === void 0) && config.showFAB !== false) {
6788
6821
  const behaviorFetch = config.fetchImpl ?? globalThis.fetch;
6789
6822
  void (async () => {
6790
6823
  try {
@@ -6797,6 +6830,10 @@ function createFeedback(config) {
6797
6830
  if (typeof v === "boolean" && mountOptions.openToCurrentPageFeedback === void 0) {
6798
6831
  mountOptions.openToCurrentPageFeedback = v;
6799
6832
  }
6833
+ const meta = m?.config?.behavior?.metaFeedbackEnabled;
6834
+ if (typeof meta === "boolean" && mountOptions.metaFeedbackEnabled === void 0) {
6835
+ mountOptions.metaFeedbackEnabled = meta;
6836
+ }
6800
6837
  } catch {
6801
6838
  }
6802
6839
  })();
@@ -6867,4 +6904,4 @@ function createFeedback(config) {
6867
6904
  export {
6868
6905
  createFeedback
6869
6906
  };
6870
- //# sourceMappingURL=chunk-T36VC7TZ.mjs.map
6907
+ //# sourceMappingURL=chunk-C3ILJCVI.mjs.map