@mhosaic/feedback 0.10.0 → 0.12.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-DKE6ELJ4.mjs +30 -0
- package/dist/chunk-DKE6ELJ4.mjs.map +1 -0
- package/dist/{chunk-XSAUJEJN.mjs → chunk-XSYC5TDL.mjs} +2039 -809
- package/dist/chunk-XSYC5TDL.mjs.map +1 -0
- package/dist/embed.min.js +535 -23
- package/dist/embed.min.js.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/react.mjs +2 -1
- package/dist/react.mjs.map +1 -1
- package/dist/replay.mjs +7 -2
- package/dist/replay.mjs.map +1 -1
- package/dist/webvitals.mjs +18 -1
- package/dist/webvitals.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-XSAUJEJN.mjs.map +0 -1
package/dist/index.mjs
CHANGED
package/dist/react.mjs
CHANGED
package/dist/react.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react/FeedbackProvider.tsx"],"sourcesContent":["/** @jsxImportSource react */\nimport { createContext, useContext, useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\n\nimport { createFeedback, type InternalConfig } from '../core'\nimport type { FeedbackApi } from '../types'\n\nconst FeedbackContext = createContext<FeedbackApi | null>(null)\n\ninterface FeedbackProviderProps extends InternalConfig {\n children?: ReactNode\n}\n\nexport function FeedbackProvider({ children, ...config }: FeedbackProviderProps) {\n const apiRef = useRef<FeedbackApi | null>(null)\n const [api, setApi] = useState<FeedbackApi | null>(null)\n\n useEffect(() => {\n const instance = createFeedback(config)\n apiRef.current = instance\n setApi(instance)\n return () => {\n instance.shutdown()\n apiRef.current = null\n setApi(null)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config.apiKey])\n\n // Only render children once the api instance is ready so useFeedback()\n // never sees a null context value.\n if (!api) return null\n\n return <FeedbackContext.Provider value={api}>{children}</FeedbackContext.Provider>\n}\n\nexport { FeedbackContext }\n\nexport function useFeedback(): FeedbackApi {\n const api = useContext(FeedbackContext)\n if (!api) throw new Error('useFeedback must be used inside <FeedbackProvider>')\n return api\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/react/FeedbackProvider.tsx"],"sourcesContent":["/** @jsxImportSource react */\nimport { createContext, useContext, useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\n\nimport { createFeedback, type InternalConfig } from '../core'\nimport type { FeedbackApi } from '../types'\n\nconst FeedbackContext = createContext<FeedbackApi | null>(null)\n\ninterface FeedbackProviderProps extends InternalConfig {\n children?: ReactNode\n}\n\nexport function FeedbackProvider({ children, ...config }: FeedbackProviderProps) {\n const apiRef = useRef<FeedbackApi | null>(null)\n const [api, setApi] = useState<FeedbackApi | null>(null)\n\n useEffect(() => {\n const instance = createFeedback(config)\n apiRef.current = instance\n setApi(instance)\n return () => {\n instance.shutdown()\n apiRef.current = null\n setApi(null)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config.apiKey])\n\n // Only render children once the api instance is ready so useFeedback()\n // never sees a null context value.\n if (!api) return null\n\n return <FeedbackContext.Provider value={api}>{children}</FeedbackContext.Provider>\n}\n\nexport { FeedbackContext }\n\nexport function useFeedback(): FeedbackApi {\n const api = useContext(FeedbackContext)\n if (!api) throw new Error('useFeedback must be used inside <FeedbackProvider>')\n return api\n}\n"],"mappings":";;;;;;AACA,SAAS,eAAe,YAAY,WAAW,QAAQ,gBAAgB;AAgC9D;AA1BT,IAAM,kBAAkB,cAAkC,IAAI;AAMvD,SAAS,iBAAiB,EAAE,UAAU,GAAG,OAAO,GAA0B;AAC/E,QAAM,SAAS,OAA2B,IAAI;AAC9C,QAAM,CAAC,KAAK,MAAM,IAAI,SAA6B,IAAI;AAEvD,YAAU,MAAM;AACd,UAAM,WAAW,eAAe,MAAM;AACtC,WAAO,UAAU;AACjB,WAAO,QAAQ;AACf,WAAO,MAAM;AACX,eAAS,SAAS;AAClB,aAAO,UAAU;AACjB,aAAO,IAAI;AAAA,IACb;AAAA,EAEF,GAAG,CAAC,OAAO,MAAM,CAAC;AAIlB,MAAI,CAAC,IAAK,QAAO;AAEjB,SAAO,oBAAC,gBAAgB,UAAhB,EAAyB,OAAO,KAAM,UAAS;AACzD;AAIO,SAAS,cAA2B;AACzC,QAAM,MAAM,WAAW,eAAe;AACtC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,oDAAoD;AAC9E,SAAO;AACT;","names":[]}
|
package/dist/replay.mjs
CHANGED
|
@@ -12,15 +12,20 @@ function withReplay(fb, options = {}) {
|
|
|
12
12
|
void (async () => {
|
|
13
13
|
try {
|
|
14
14
|
const { record } = await import("rrweb");
|
|
15
|
+
const maskAll = options.maskAllText !== false;
|
|
15
16
|
stopFn = record({
|
|
16
17
|
emit(event) {
|
|
17
18
|
events.push(event);
|
|
18
19
|
evictOld();
|
|
19
20
|
},
|
|
20
21
|
maskAllInputs: options.maskAllInputs ?? true,
|
|
21
|
-
...
|
|
22
|
+
...maskAll && { maskTextClass: /.*/ },
|
|
22
23
|
...options.maskTextSelector !== void 0 && { maskTextSelector: options.maskTextSelector },
|
|
23
|
-
|
|
24
|
+
// Block selector: elements matching this are wholly excluded from
|
|
25
|
+
// the replay (not even shape captured). The .mfb-mask class +
|
|
26
|
+
// data-mfb-mask attr line up with the screenshot mask defaults,
|
|
27
|
+
// so consumers tagging one tag both surfaces.
|
|
28
|
+
blockSelector: options.blockSelector ?? "[data-mfb-block],[data-mfb-mask],.mfb-mask"
|
|
24
29
|
}) ?? null;
|
|
25
30
|
} catch {
|
|
26
31
|
}
|
package/dist/replay.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/modules/replay.ts"],"sourcesContent":["import type { FeedbackApi, FeedbackApiInternal, ReplayBlob } from '../types'\n\nexport interface ReplayOptions {\n durationMs?: number\n maskAllText?: boolean\n maskAllInputs?: boolean\n maskTextSelector?: string\n blockSelector?: string\n}\n\ntype RrwebEvent = { timestamp: number } & Record<string, unknown>\n\nexport function withReplay(fb: FeedbackApi, options: ReplayOptions = {}): FeedbackApi {\n const internal = fb as FeedbackApiInternal\n if (typeof internal._registerTransformer !== 'function') return fb\n\n const duration = options.durationMs ?? 60_000\n const events: RrwebEvent[] = []\n let stopFn: (() => void) | null = null\n\n function evictOld() {\n const cutoff = Date.now() - duration\n while (events.length > 0 && (events[0]!.timestamp ?? 0) < cutoff) events.shift()\n }\n\n void (async () => {\n try {\n const { record } = await import('rrweb')\n stopFn = record({\n emit(event: RrwebEvent) {\n events.push(event)\n evictOld()\n },\n maskAllInputs: options.maskAllInputs ?? true,\n ...(
|
|
1
|
+
{"version":3,"sources":["../src/modules/replay.ts"],"sourcesContent":["import type { FeedbackApi, FeedbackApiInternal, ReplayBlob } from '../types'\n\nexport interface ReplayOptions {\n durationMs?: number\n maskAllText?: boolean\n maskAllInputs?: boolean\n maskTextSelector?: string\n blockSelector?: string\n}\n\ntype RrwebEvent = { timestamp: number } & Record<string, unknown>\n\nexport function withReplay(fb: FeedbackApi, options: ReplayOptions = {}): FeedbackApi {\n const internal = fb as FeedbackApiInternal\n if (typeof internal._registerTransformer !== 'function') return fb\n\n const duration = options.durationMs ?? 60_000\n const events: RrwebEvent[] = []\n let stopFn: (() => void) | null = null\n\n function evictOld() {\n const cutoff = Date.now() - duration\n while (events.length > 0 && (events[0]!.timestamp ?? 0) < cutoff) events.shift()\n }\n\n void (async () => {\n try {\n const { record } = await import('rrweb')\n // Privacy posture (v0.11): mask aggressively by default. Hosts can\n // opt OUT explicitly by passing maskAllInputs:false / maskAllText:false,\n // but the default should never ship sensitive text or input values.\n // rrweb does not expose a maskAllText:boolean directly; mask-everything\n // is achieved by passing a maskTextClass regex that matches all classes.\n const maskAll = options.maskAllText !== false\n stopFn = record({\n emit(event: RrwebEvent) {\n events.push(event)\n evictOld()\n },\n maskAllInputs: options.maskAllInputs ?? true,\n ...(maskAll && { maskTextClass: /.*/ }),\n ...(options.maskTextSelector !== undefined && { maskTextSelector: options.maskTextSelector }),\n // Block selector: elements matching this are wholly excluded from\n // the replay (not even shape captured). The .mfb-mask class +\n // data-mfb-mask attr line up with the screenshot mask defaults,\n // so consumers tagging one tag both surfaces.\n blockSelector: options.blockSelector ?? '[data-mfb-block],[data-mfb-mask],.mfb-mask',\n }) ?? null\n } catch { /* rrweb unavailable */ }\n })()\n\n internal._registerTransformer((payload) => {\n if (events.length === 0) return payload\n evictOld()\n const blob: ReplayBlob = {\n events: events.slice(),\n durationMs: Math.min(duration, Date.now() - (events[0]?.timestamp ?? Date.now())),\n }\n return {\n ...payload,\n technical_context: { ...payload.technical_context, replay: blob },\n }\n })\n\n const originalShutdown = fb.shutdown.bind(fb)\n fb.shutdown = () => {\n try { stopFn?.() } catch { /* noop */ }\n originalShutdown()\n }\n\n return fb\n}\n"],"mappings":";AAYO,SAAS,WAAW,IAAiB,UAAyB,CAAC,GAAgB;AACpF,QAAM,WAAW;AACjB,MAAI,OAAO,SAAS,yBAAyB,WAAY,QAAO;AAEhE,QAAM,WAAW,QAAQ,cAAc;AACvC,QAAM,SAAuB,CAAC;AAC9B,MAAI,SAA8B;AAElC,WAAS,WAAW;AAClB,UAAM,SAAS,KAAK,IAAI,IAAI;AAC5B,WAAO,OAAO,SAAS,MAAM,OAAO,CAAC,EAAG,aAAa,KAAK,OAAQ,QAAO,MAAM;AAAA,EACjF;AAEA,QAAM,YAAY;AAChB,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,MAAM,OAAO,OAAO;AAMvC,YAAM,UAAU,QAAQ,gBAAgB;AACxC,eAAS,OAAO;AAAA,QACd,KAAK,OAAmB;AACtB,iBAAO,KAAK,KAAK;AACjB,mBAAS;AAAA,QACX;AAAA,QACA,eAAe,QAAQ,iBAAiB;AAAA,QACxC,GAAI,WAAW,EAAE,eAAe,KAAK;AAAA,QACrC,GAAI,QAAQ,qBAAqB,UAAa,EAAE,kBAAkB,QAAQ,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,QAK3F,eAAe,QAAQ,iBAAiB;AAAA,MAC1C,CAAC,KAAK;AAAA,IACR,QAAQ;AAAA,IAA0B;AAAA,EACpC,GAAG;AAEH,WAAS,qBAAqB,CAAC,YAAY;AACzC,QAAI,OAAO,WAAW,EAAG,QAAO;AAChC,aAAS;AACT,UAAM,OAAmB;AAAA,MACvB,QAAQ,OAAO,MAAM;AAAA,MACrB,YAAY,KAAK,IAAI,UAAU,KAAK,IAAI,KAAK,OAAO,CAAC,GAAG,aAAa,KAAK,IAAI,EAAE;AAAA,IAClF;AACA,WAAO;AAAA,MACL,GAAG;AAAA,MACH,mBAAmB,EAAE,GAAG,QAAQ,mBAAmB,QAAQ,KAAK;AAAA,IAClE;AAAA,EACF,CAAC;AAED,QAAM,mBAAmB,GAAG,SAAS,KAAK,EAAE;AAC5C,KAAG,WAAW,MAAM;AAClB,QAAI;AAAE,eAAS;AAAA,IAAE,QAAQ;AAAA,IAAa;AACtC,qBAAiB;AAAA,EACnB;AAEA,SAAO;AACT;","names":[]}
|
package/dist/webvitals.mjs
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
scrubCredentials
|
|
3
|
+
} from "./chunk-DKE6ELJ4.mjs";
|
|
4
|
+
|
|
1
5
|
// src/modules/webvitals.ts
|
|
2
6
|
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals/attribution";
|
|
7
|
+
function _sanitizeAttribution(value, depth = 0) {
|
|
8
|
+
if (depth > 4) return void 0;
|
|
9
|
+
if (value == null) return value;
|
|
10
|
+
if (typeof value === "string") return scrubCredentials(value).slice(0, 200);
|
|
11
|
+
if (typeof value !== "object") return value;
|
|
12
|
+
if (Array.isArray(value)) return value.map((item) => _sanitizeAttribution(item, depth + 1));
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const [k, v] of Object.entries(value)) {
|
|
15
|
+
out[k] = _sanitizeAttribution(v, depth + 1);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
3
19
|
function withWebVitals(fb, options = {}) {
|
|
4
20
|
const internal = fb;
|
|
5
21
|
if (typeof internal._registerTransformer !== "function") return fb;
|
|
6
22
|
const entries = /* @__PURE__ */ new Map();
|
|
7
23
|
const record = (metric) => {
|
|
24
|
+
const attribution = metric.attribution !== void 0 ? _sanitizeAttribution(metric.attribution) : void 0;
|
|
8
25
|
entries.set(metric.name, {
|
|
9
26
|
name: metric.name,
|
|
10
27
|
value: metric.value,
|
|
11
28
|
rating: metric.rating,
|
|
12
|
-
...
|
|
29
|
+
...attribution !== void 0 && { attribution }
|
|
13
30
|
});
|
|
14
31
|
};
|
|
15
32
|
const opts = { reportAllChanges: options.reportAllChanges ?? false };
|
package/dist/webvitals.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/modules/webvitals.ts"],"sourcesContent":["import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals/attribution'\n\nimport type { FeedbackApi, FeedbackApiInternal, WebVitalEntry } from '../types'\n\nexport interface WebVitalsOptions {\n reportAllChanges?: boolean\n}\n\nexport function withWebVitals(fb: FeedbackApi, options: WebVitalsOptions = {}): FeedbackApi {\n const internal = fb as FeedbackApiInternal\n if (typeof internal._registerTransformer !== 'function') return fb\n\n const entries = new Map<string, WebVitalEntry>()\n const record = (metric: { name: string; value: number; rating: string; attribution?: unknown }) => {\n entries.set(metric.name, {\n name: metric.name as WebVitalEntry['name'],\n value: metric.value,\n rating: metric.rating as WebVitalEntry['rating'],\n ...(
|
|
1
|
+
{"version":3,"sources":["../src/modules/webvitals.ts"],"sourcesContent":["import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals/attribution'\n\nimport type { FeedbackApi, FeedbackApiInternal, WebVitalEntry } from '../types'\nimport { scrubCredentials } from '../capture/scrub'\n\nexport interface WebVitalsOptions {\n reportAllChanges?: boolean\n}\n\n// web-vitals/attribution returns DOM-pointer fields like `lcpElement`,\n// `largestShiftTarget`, `eventTarget` — selectors derived from real DOM\n// content. Hosts commonly bake PII into element ids (\"#user-bob-row\",\n// \"[data-email='bob@x.com']\") so we scrub the same patterns the\n// console/error capture paths scrub, before the payload leaves the page.\n//\n// Audit R6/F2: attribution objects nest. `lcpResourceEntry.name`,\n// `lcpEntry.name`, `processedEventEntries[].name`, `largestShiftSources`\n// all carry URLs (often with OAuth tokens / email querystrings) one or\n// more levels down. Recurse so nested string leaves get the same scrub\n// as top-level fields.\nfunction _sanitizeAttribution(value: unknown, depth = 0): unknown {\n if (depth > 4) return undefined // bound recursion against pathological inputs\n if (value == null) return value\n if (typeof value === 'string') return scrubCredentials(value).slice(0, 200)\n if (typeof value !== 'object') return value\n if (Array.isArray(value)) return value.map((item) => _sanitizeAttribution(item, depth + 1))\n // Strip PerformanceEntry instances down to their useful fields so we don't\n // serialize a huge object graph (and don't leak `responseHeaders` etc.).\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = _sanitizeAttribution(v, depth + 1)\n }\n return out\n}\n\nexport function withWebVitals(fb: FeedbackApi, options: WebVitalsOptions = {}): FeedbackApi {\n const internal = fb as FeedbackApiInternal\n if (typeof internal._registerTransformer !== 'function') return fb\n\n const entries = new Map<string, WebVitalEntry>()\n const record = (metric: { name: string; value: number; rating: string; attribution?: unknown }) => {\n const attribution = metric.attribution !== undefined\n ? _sanitizeAttribution(metric.attribution)\n : undefined\n entries.set(metric.name, {\n name: metric.name as WebVitalEntry['name'],\n value: metric.value,\n rating: metric.rating as WebVitalEntry['rating'],\n ...(attribution !== undefined && { attribution }),\n })\n }\n\n const opts = { reportAllChanges: options.reportAllChanges ?? false }\n try {\n onCLS(record, opts)\n onINP(record, opts)\n onLCP(record, opts)\n onFCP(record, opts)\n onTTFB(record, opts)\n } catch { /* unsupported env */ }\n\n internal._registerTransformer((payload) => ({\n ...payload,\n technical_context: {\n ...payload.technical_context,\n webVitals: Array.from(entries.values()),\n },\n }))\n\n return fb\n}\n"],"mappings":";;;;;AAAA,SAAS,OAAO,OAAO,OAAO,OAAO,cAAc;AAoBnD,SAAS,qBAAqB,OAAgB,QAAQ,GAAY;AAChE,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI,OAAO,UAAU,SAAU,QAAO,iBAAiB,KAAK,EAAE,MAAM,GAAG,GAAG;AAC1E,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,CAAC,SAAS,qBAAqB,MAAM,QAAQ,CAAC,CAAC;AAG1F,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACrE,QAAI,CAAC,IAAI,qBAAqB,GAAG,QAAQ,CAAC;AAAA,EAC5C;AACA,SAAO;AACT;AAEO,SAAS,cAAc,IAAiB,UAA4B,CAAC,GAAgB;AAC1F,QAAM,WAAW;AACjB,MAAI,OAAO,SAAS,yBAAyB,WAAY,QAAO;AAEhE,QAAM,UAAU,oBAAI,IAA2B;AAC/C,QAAM,SAAS,CAAC,WAAmF;AACjG,UAAM,cAAc,OAAO,gBAAgB,SACvC,qBAAqB,OAAO,WAAW,IACvC;AACJ,YAAQ,IAAI,OAAO,MAAM;AAAA,MACvB,MAAM,OAAO;AAAA,MACb,OAAO,OAAO;AAAA,MACd,QAAQ,OAAO;AAAA,MACf,GAAI,gBAAgB,UAAa,EAAE,YAAY;AAAA,IACjD,CAAC;AAAA,EACH;AAEA,QAAM,OAAO,EAAE,kBAAkB,QAAQ,oBAAoB,MAAM;AACnE,MAAI;AACF,UAAM,QAAQ,IAAI;AAClB,UAAM,QAAQ,IAAI;AAClB,UAAM,QAAQ,IAAI;AAClB,UAAM,QAAQ,IAAI;AAClB,WAAO,QAAQ,IAAI;AAAA,EACrB,QAAQ;AAAA,EAAwB;AAEhC,WAAS,qBAAqB,CAAC,aAAa;AAAA,IAC1C,GAAG;AAAA,IACH,mBAAmB;AAAA,MACjB,GAAG,QAAQ;AAAA,MACX,WAAW,MAAM,KAAK,QAAQ,OAAO,CAAC;AAAA,IACxC;AAAA,EACF,EAAE;AAEF,SAAO;AACT;","names":[]}
|