@tra-bilisim/report-issue 0.1.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/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/AnnotationEditor-ILMYBTOG.js +379 -0
- package/dist/adapters-axios.d.ts +19 -0
- package/dist/adapters-axios.js +52 -0
- package/dist/chunk-5S66KGBW.js +118 -0
- package/dist/chunk-EXDFVVYA.js +73 -0
- package/dist/chunk-JMQUG5Q7.js +99 -0
- package/dist/chunk-KY2IRP36.js +102 -0
- package/dist/chunk-ZYF6UFBB.js +162 -0
- package/dist/consent-DmS4DxOf.d.ts +135 -0
- package/dist/core.d.ts +16 -0
- package/dist/core.js +4 -0
- package/dist/index.css +594 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +687 -0
- package/dist/screenshot-BQPXCSLD.js +2 -0
- package/package.json +69 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// src/core/videoRecorder.ts
|
|
2
|
+
var DEFAULT_MAX_DURATION_MS = 15e3;
|
|
3
|
+
function pickMimeType() {
|
|
4
|
+
const candidates = [
|
|
5
|
+
"video/mp4;codecs=avc1.42E01E",
|
|
6
|
+
"video/mp4",
|
|
7
|
+
"video/webm;codecs=vp8",
|
|
8
|
+
"video/webm;codecs=vp9",
|
|
9
|
+
"video/webm"
|
|
10
|
+
];
|
|
11
|
+
return candidates.find((type) => MediaRecorder.isTypeSupported(type)) ?? "video/webm";
|
|
12
|
+
}
|
|
13
|
+
function createVideoRecorder(callbacks, options = {}) {
|
|
14
|
+
const maxDurationMs = options.maxDurationMs ?? DEFAULT_MAX_DURATION_MS;
|
|
15
|
+
let recorder = null;
|
|
16
|
+
let stream = null;
|
|
17
|
+
let chunks = [];
|
|
18
|
+
let timeout = null;
|
|
19
|
+
let interval = null;
|
|
20
|
+
const setRecordingState = (next) => {
|
|
21
|
+
callbacks.onStateChange?.(next);
|
|
22
|
+
};
|
|
23
|
+
const clearTimers = () => {
|
|
24
|
+
if (timeout) clearTimeout(timeout);
|
|
25
|
+
if (interval) clearInterval(interval);
|
|
26
|
+
timeout = null;
|
|
27
|
+
interval = null;
|
|
28
|
+
};
|
|
29
|
+
const cleanup = () => {
|
|
30
|
+
clearTimers();
|
|
31
|
+
stream?.getTracks().forEach((track) => track.stop());
|
|
32
|
+
stream = null;
|
|
33
|
+
recorder = null;
|
|
34
|
+
setRecordingState(false);
|
|
35
|
+
callbacks.onTick?.(0);
|
|
36
|
+
};
|
|
37
|
+
const stop = () => {
|
|
38
|
+
if (recorder && recorder.state !== "inactive") {
|
|
39
|
+
try {
|
|
40
|
+
recorder.stop();
|
|
41
|
+
} catch {
|
|
42
|
+
cleanup();
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
cleanup();
|
|
47
|
+
};
|
|
48
|
+
const start = async () => {
|
|
49
|
+
if (!navigator.mediaDevices?.getDisplayMedia) {
|
|
50
|
+
return "unsupported";
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const displayOptions = {
|
|
54
|
+
video: { displaySurface: "browser", width: { max: 1280 }, height: { max: 720 }, frameRate: { max: 15 } },
|
|
55
|
+
audio: false,
|
|
56
|
+
// Offer only "This Tab"; remove the whole-screen option entirely.
|
|
57
|
+
preferCurrentTab: true,
|
|
58
|
+
monitorTypeSurfaces: "exclude"
|
|
59
|
+
};
|
|
60
|
+
const nextStream = await navigator.mediaDevices.getDisplayMedia(displayOptions);
|
|
61
|
+
const surface = nextStream.getVideoTracks()[0]?.getSettings().displaySurface;
|
|
62
|
+
if (surface && surface !== "browser") {
|
|
63
|
+
nextStream.getTracks().forEach((track) => track.stop());
|
|
64
|
+
return "wrong-surface";
|
|
65
|
+
}
|
|
66
|
+
stream = nextStream;
|
|
67
|
+
chunks = [];
|
|
68
|
+
const mimeType = pickMimeType();
|
|
69
|
+
recorder = new MediaRecorder(nextStream, { mimeType, videoBitsPerSecond: 5e5 });
|
|
70
|
+
recorder.ondataavailable = (e) => {
|
|
71
|
+
if (e.data.size > 0) chunks.push(e.data);
|
|
72
|
+
};
|
|
73
|
+
recorder.onstop = () => {
|
|
74
|
+
const blob = new Blob(chunks, { type: mimeType });
|
|
75
|
+
const ext = mimeType.includes("mp4") ? "mp4" : "webm";
|
|
76
|
+
const file = new File([blob], `recording-${Date.now()}.${ext}`, { type: mimeType });
|
|
77
|
+
callbacks.onComplete(file);
|
|
78
|
+
cleanup();
|
|
79
|
+
};
|
|
80
|
+
nextStream.getVideoTracks()[0]?.addEventListener("ended", stop);
|
|
81
|
+
recorder.start();
|
|
82
|
+
setRecordingState(true);
|
|
83
|
+
callbacks.onTick?.(0);
|
|
84
|
+
const startTime = Date.now();
|
|
85
|
+
interval = setInterval(() => {
|
|
86
|
+
callbacks.onTick?.(Math.floor((Date.now() - startTime) / 1e3));
|
|
87
|
+
}, 250);
|
|
88
|
+
timeout = setTimeout(stop, maxDurationMs);
|
|
89
|
+
return "started";
|
|
90
|
+
} catch {
|
|
91
|
+
cleanup();
|
|
92
|
+
return "denied";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const dispose = () => {
|
|
96
|
+
stream?.getTracks().forEach((track) => track.stop());
|
|
97
|
+
clearTimers();
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
start,
|
|
101
|
+
stop,
|
|
102
|
+
dispose,
|
|
103
|
+
maxDurationSeconds: maxDurationMs / 1e3
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/core/consent.ts
|
|
108
|
+
var CONSENT_VERSION = "1.0";
|
|
109
|
+
function createConsent(captureType, version = CONSENT_VERSION) {
|
|
110
|
+
return {
|
|
111
|
+
accepted: true,
|
|
112
|
+
version,
|
|
113
|
+
acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
114
|
+
captureType
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { CONSENT_VERSION, createConsent, createVideoRecorder };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/core/reportLogs.ts
|
|
2
|
+
var MAX_CONSOLE = 200;
|
|
3
|
+
var MAX_NETWORK = 100;
|
|
4
|
+
var consoleLogs = [];
|
|
5
|
+
var networkLogs = [];
|
|
6
|
+
var pendingRequests = /* @__PURE__ */ new Map();
|
|
7
|
+
function safeStringify(arg) {
|
|
8
|
+
if (typeof arg === "string") return arg;
|
|
9
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.stringify(arg);
|
|
12
|
+
} catch {
|
|
13
|
+
return String(arg);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function pushConsoleLog(level, args) {
|
|
17
|
+
consoleLogs.push({ level, message: args.map(safeStringify).join(" "), ts: Date.now() });
|
|
18
|
+
if (consoleLogs.length > MAX_CONSOLE) consoleLogs.shift();
|
|
19
|
+
}
|
|
20
|
+
function beginNetworkRequest(trackingId, method, url) {
|
|
21
|
+
pendingRequests.set(trackingId, { trackingId, method, url, requestTs: Date.now() });
|
|
22
|
+
}
|
|
23
|
+
function completeNetworkRequest(trackingId, status, isError, message, errorMessage) {
|
|
24
|
+
const entry = pendingRequests.get(trackingId);
|
|
25
|
+
if (!entry) return;
|
|
26
|
+
pendingRequests.delete(trackingId);
|
|
27
|
+
networkLogs.push({
|
|
28
|
+
...entry,
|
|
29
|
+
responseTs: Date.now(),
|
|
30
|
+
status,
|
|
31
|
+
isError,
|
|
32
|
+
message: message ?? errorMessage ?? null,
|
|
33
|
+
errorMessage,
|
|
34
|
+
duration: Date.now() - entry.requestTs
|
|
35
|
+
});
|
|
36
|
+
if (networkLogs.length > MAX_NETWORK) networkLogs.shift();
|
|
37
|
+
}
|
|
38
|
+
function failNetworkRequest(trackingId, status, errorMessage) {
|
|
39
|
+
const entry = pendingRequests.get(trackingId);
|
|
40
|
+
if (!entry) return;
|
|
41
|
+
pendingRequests.delete(trackingId);
|
|
42
|
+
networkLogs.push({
|
|
43
|
+
...entry,
|
|
44
|
+
responseTs: Date.now(),
|
|
45
|
+
status,
|
|
46
|
+
isError: true,
|
|
47
|
+
message: errorMessage ?? null,
|
|
48
|
+
errorMessage,
|
|
49
|
+
duration: Date.now() - entry.requestTs
|
|
50
|
+
});
|
|
51
|
+
if (networkLogs.length > MAX_NETWORK) networkLogs.shift();
|
|
52
|
+
}
|
|
53
|
+
function getConsoleLogs() {
|
|
54
|
+
return [...consoleLogs];
|
|
55
|
+
}
|
|
56
|
+
function getNetworkLogs() {
|
|
57
|
+
return [...networkLogs];
|
|
58
|
+
}
|
|
59
|
+
var consolePatched = false;
|
|
60
|
+
function patchConsole() {
|
|
61
|
+
if (consolePatched) return;
|
|
62
|
+
if (typeof console === "undefined") return;
|
|
63
|
+
consolePatched = true;
|
|
64
|
+
["log", "warn", "error", "info"].forEach((level) => {
|
|
65
|
+
const original = console[level].bind(console);
|
|
66
|
+
console[level] = (...args) => {
|
|
67
|
+
original(...args);
|
|
68
|
+
pushConsoleLog(level, args);
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { beginNetworkRequest, completeNetworkRequest, failNetworkRequest, getConsoleLogs, getNetworkLogs, patchConsole, pushConsoleLog };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { patchConsole } from './chunk-EXDFVVYA.js';
|
|
2
|
+
import { createContext, forwardRef, useState, useMemo, useEffect, useContext } from 'react';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var DEFAULT_MAX_FILES = 10;
|
|
6
|
+
var DEFAULT_MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
7
|
+
var DEFAULT_MAX_RECORDING_SECONDS = 15;
|
|
8
|
+
var identityT = (key) => key;
|
|
9
|
+
var defaultEnvironmentResolver = (url) => {
|
|
10
|
+
let host = url.toLowerCase();
|
|
11
|
+
try {
|
|
12
|
+
host = new URL(url, typeof window !== "undefined" ? window.location.origin : void 0).hostname.toLowerCase();
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
if (host === "localhost" || host === "127.0.0.1" || host.includes("local")) return "Locale";
|
|
16
|
+
if (["test", "uat", "stage", "staging", "dev"].some((k) => host.includes(k))) return "Test";
|
|
17
|
+
return "Prod";
|
|
18
|
+
};
|
|
19
|
+
var noopToast = {
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
success: (message) => console.info("[report-issue]", message),
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
error: (message) => console.error("[report-issue]", message)
|
|
24
|
+
};
|
|
25
|
+
function resolveConfig(adapter) {
|
|
26
|
+
return {
|
|
27
|
+
adapter,
|
|
28
|
+
t: adapter.t ?? identityT,
|
|
29
|
+
locale: adapter.locale,
|
|
30
|
+
toast: adapter.toast ?? noopToast,
|
|
31
|
+
environmentResolver: adapter.environmentResolver ?? defaultEnvironmentResolver,
|
|
32
|
+
getCurrentUrl: adapter.getCurrentUrl ?? (() => typeof window !== "undefined" ? window.location.href : ""),
|
|
33
|
+
maxFiles: adapter.maxFiles ?? DEFAULT_MAX_FILES,
|
|
34
|
+
maxFileSizeBytes: adapter.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE,
|
|
35
|
+
maxRecordingSeconds: adapter.maxRecordingSeconds ?? DEFAULT_MAX_RECORDING_SECONDS
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
var ReportIssueContext = createContext(void 0);
|
|
39
|
+
var ReportIssueProvider = ({ config, children }) => {
|
|
40
|
+
const [isCapturing, setIsCapturing] = useState(false);
|
|
41
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
42
|
+
const [isVideoMaskEnabled, setIsVideoMaskEnabled] = useState(false);
|
|
43
|
+
const [isReportOpen, setIsReportOpen] = useState(false);
|
|
44
|
+
const resolved = useMemo(() => resolveConfig(config), [config]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (config.captureConsole !== false) patchConsole();
|
|
47
|
+
}, [config.captureConsole]);
|
|
48
|
+
const captureMode = isCapturing || isRecording;
|
|
49
|
+
const value = useMemo(() => ({
|
|
50
|
+
config: resolved,
|
|
51
|
+
isCapturing,
|
|
52
|
+
setIsCapturing,
|
|
53
|
+
isRecording,
|
|
54
|
+
setIsRecording,
|
|
55
|
+
isVideoMaskEnabled,
|
|
56
|
+
setIsVideoMaskEnabled,
|
|
57
|
+
isReportOpen,
|
|
58
|
+
setIsReportOpen,
|
|
59
|
+
captureMode
|
|
60
|
+
}), [resolved, isCapturing, isRecording, isVideoMaskEnabled, isReportOpen, captureMode]);
|
|
61
|
+
return /* @__PURE__ */ jsx(ReportIssueContext.Provider, { value, children });
|
|
62
|
+
};
|
|
63
|
+
var useReportIssue = () => {
|
|
64
|
+
const ctx = useContext(ReportIssueContext);
|
|
65
|
+
if (!ctx) throw new Error("useReportIssue must be used within ReportIssueProvider");
|
|
66
|
+
return ctx;
|
|
67
|
+
};
|
|
68
|
+
var useReportIssueConfig = () => useReportIssue().config;
|
|
69
|
+
|
|
70
|
+
// src/react/ui/cn.ts
|
|
71
|
+
function cn(...parts) {
|
|
72
|
+
return parts.filter(Boolean).join(" ");
|
|
73
|
+
}
|
|
74
|
+
var Button = forwardRef(
|
|
75
|
+
({ variant = "solid", color = "primary", size = "md", loading = false, disabled, className, children, ...rest }, ref) => /* @__PURE__ */ jsxs(
|
|
76
|
+
"button",
|
|
77
|
+
{
|
|
78
|
+
ref,
|
|
79
|
+
type: "button",
|
|
80
|
+
disabled: disabled || loading,
|
|
81
|
+
className: cn(
|
|
82
|
+
"rpi-btn",
|
|
83
|
+
variant === "outline" && "rpi-btn--outline",
|
|
84
|
+
variant !== "outline" && color === "error" && "rpi-btn--error",
|
|
85
|
+
size === "sm" && "rpi-btn--sm",
|
|
86
|
+
size === "icon" && "rpi-btn--icon",
|
|
87
|
+
className
|
|
88
|
+
),
|
|
89
|
+
...rest,
|
|
90
|
+
children: [
|
|
91
|
+
loading && /* @__PURE__ */ jsx("span", { className: "rpi-btn__spinner", "aria-hidden": true }),
|
|
92
|
+
children
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
Button.displayName = "RpiButton";
|
|
98
|
+
|
|
99
|
+
export { Button, ReportIssueProvider, cn, useReportIssue, useReportIssueConfig };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { applyMask, applyInputMask, removeMask } from './chunk-ZYF6UFBB.js';
|
|
2
|
+
import html2canvas from 'html2canvas-pro';
|
|
3
|
+
|
|
4
|
+
var HIDE_DIALOG_CLASS = "report-hide-dialog";
|
|
5
|
+
var waitForCaptureImages = async () => {
|
|
6
|
+
const images = Array.from(document.images).filter((img) => {
|
|
7
|
+
if (img.closest("[data-report-ignore-capture]")) return false;
|
|
8
|
+
const style = window.getComputedStyle(img);
|
|
9
|
+
return img.getClientRects().length > 0 && style.display !== "none" && style.visibility !== "hidden";
|
|
10
|
+
});
|
|
11
|
+
await Promise.all(images.map((img) => {
|
|
12
|
+
if (img.complete && img.naturalWidth > 0) {
|
|
13
|
+
return img.decode?.().catch(() => void 0) ?? Promise.resolve();
|
|
14
|
+
}
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const done = () => resolve();
|
|
17
|
+
img.addEventListener("load", done, { once: true });
|
|
18
|
+
img.addEventListener("error", done, { once: true });
|
|
19
|
+
setTimeout(done, 1e3);
|
|
20
|
+
});
|
|
21
|
+
}));
|
|
22
|
+
};
|
|
23
|
+
var inlineCaptureSvgImages = async () => {
|
|
24
|
+
const restores = [];
|
|
25
|
+
const svgImages = Array.from(document.images).filter((img) => {
|
|
26
|
+
if (img.closest("[data-report-ignore-capture]")) return false;
|
|
27
|
+
const src = img.currentSrc || img.src;
|
|
28
|
+
if (!src) return false;
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(src, window.location.href);
|
|
31
|
+
return url.origin === window.location.origin && url.pathname.toLowerCase().endsWith(".svg");
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
await Promise.all(svgImages.map(async (img) => {
|
|
37
|
+
const originalSrc = img.getAttribute("src");
|
|
38
|
+
const originalSrcset = img.getAttribute("srcset");
|
|
39
|
+
const src = img.currentSrc || img.src;
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(src);
|
|
42
|
+
if (!response.ok) return;
|
|
43
|
+
const svg = await response.text();
|
|
44
|
+
img.removeAttribute("srcset");
|
|
45
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
46
|
+
restores.push(() => {
|
|
47
|
+
if (originalSrcset === null) img.removeAttribute("srcset");
|
|
48
|
+
else img.setAttribute("srcset", originalSrcset);
|
|
49
|
+
if (originalSrc === null) img.removeAttribute("src");
|
|
50
|
+
else img.setAttribute("src", originalSrc);
|
|
51
|
+
});
|
|
52
|
+
await img.decode?.().catch(() => void 0);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
return () => restores.forEach((restore) => restore());
|
|
57
|
+
};
|
|
58
|
+
async function captureMaskedScreenshot() {
|
|
59
|
+
let restoreCaptureSvgImages = () => {
|
|
60
|
+
};
|
|
61
|
+
document.documentElement.classList.add(HIDE_DIALOG_CLASS);
|
|
62
|
+
applyMask();
|
|
63
|
+
applyInputMask();
|
|
64
|
+
await new Promise((resolve) => {
|
|
65
|
+
setTimeout(resolve, 450);
|
|
66
|
+
});
|
|
67
|
+
applyInputMask();
|
|
68
|
+
restoreCaptureSvgImages = await inlineCaptureSvgImages();
|
|
69
|
+
await waitForCaptureImages();
|
|
70
|
+
const docEl = document.documentElement;
|
|
71
|
+
const viewportWidth = docEl.clientWidth;
|
|
72
|
+
const viewportHeight = window.innerHeight;
|
|
73
|
+
const fullHeight = Math.max(docEl.scrollHeight, document.body.scrollHeight);
|
|
74
|
+
const pageScrolls = fullHeight > viewportHeight + 1;
|
|
75
|
+
const previousScrollX = window.scrollX;
|
|
76
|
+
const previousScrollY = window.scrollY;
|
|
77
|
+
if (pageScrolls) window.scrollTo(0, 0);
|
|
78
|
+
try {
|
|
79
|
+
const canvas = await html2canvas(document.body, {
|
|
80
|
+
useCORS: true,
|
|
81
|
+
scale: window.devicePixelRatio || 1,
|
|
82
|
+
width: viewportWidth,
|
|
83
|
+
height: pageScrolls ? fullHeight : viewportHeight,
|
|
84
|
+
windowWidth: viewportWidth,
|
|
85
|
+
windowHeight: pageScrolls ? fullHeight : viewportHeight,
|
|
86
|
+
ignoreElements: (el) => el.hasAttribute?.("data-report-ignore-capture") || el.getAttribute?.("data-slot") === "dialog-overlay",
|
|
87
|
+
// onclone runs after the DOM is cloned but before rendering. React cannot
|
|
88
|
+
// reset values in the clone, so masking is reliable here.
|
|
89
|
+
onclone: (clonedDoc) => {
|
|
90
|
+
applyInputMask(clonedDoc);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return canvas.toDataURL("image/png");
|
|
94
|
+
} finally {
|
|
95
|
+
if (pageScrolls) window.scrollTo(previousScrollX, previousScrollY);
|
|
96
|
+
restoreCaptureSvgImages();
|
|
97
|
+
removeMask();
|
|
98
|
+
document.documentElement.classList.remove(HIDE_DIALOG_CLASS);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { captureMaskedScreenshot };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/core/reportMask.ts
|
|
2
|
+
var MASK_CHAR = "\u25CF";
|
|
3
|
+
var MASK_LENGTH = 6;
|
|
4
|
+
var MASK_PLACEHOLDER = MASK_CHAR.repeat(MASK_LENGTH);
|
|
5
|
+
var VIDEO_STYLE_ID = "report-mask-video-style";
|
|
6
|
+
var MEDIA_TAGS = /* @__PURE__ */ new Set(["IMG", "PICTURE", "SVG", "CANVAS", "VIDEO"]);
|
|
7
|
+
var MASK_IGNORE_SELECTOR = "[data-report-mask-ignore], [data-sonner-toaster], [data-sonner-toast]";
|
|
8
|
+
var TEXT_MASK_IGNORE_SELECTOR = "[data-report-mask-text-ignore]";
|
|
9
|
+
var FORCE_TEXT_MASK_SELECTOR = "[data-report-mask-control], [data-report-mask-value]";
|
|
10
|
+
var IGNORED_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden", "checkbox", "radio", "file", "button", "submit", "reset", "image", "range", "color", "password"]);
|
|
11
|
+
var maskedTextNodes = /* @__PURE__ */ new Map();
|
|
12
|
+
function isInputElement(el) {
|
|
13
|
+
return el.tagName === "INPUT";
|
|
14
|
+
}
|
|
15
|
+
function isTextareaElement(el) {
|
|
16
|
+
return el.tagName === "TEXTAREA";
|
|
17
|
+
}
|
|
18
|
+
function isFormField(el) {
|
|
19
|
+
return isInputElement(el) || isTextareaElement(el);
|
|
20
|
+
}
|
|
21
|
+
function hasIgnoreAncestor(node) {
|
|
22
|
+
let cur = node.parentNode;
|
|
23
|
+
while (cur && cur !== document.body) {
|
|
24
|
+
if (cur instanceof HTMLElement && cur.matches(MASK_IGNORE_SELECTOR)) return true;
|
|
25
|
+
cur = cur.parentNode;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function hasTextMaskIgnoreAncestor(node) {
|
|
30
|
+
let cur = node.parentNode;
|
|
31
|
+
while (cur && cur !== document.body) {
|
|
32
|
+
if (cur instanceof HTMLElement && cur.matches(TEXT_MASK_IGNORE_SELECTOR)) return true;
|
|
33
|
+
cur = cur.parentNode;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
function hasForceTextMaskAncestor(node) {
|
|
38
|
+
let cur = node.parentNode;
|
|
39
|
+
while (cur && cur !== document.body) {
|
|
40
|
+
if (cur instanceof HTMLElement && cur.matches(FORCE_TEXT_MASK_SELECTOR)) return true;
|
|
41
|
+
cur = cur.parentNode;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function hasCaptureIgnoreAncestor(el) {
|
|
46
|
+
let cur = el.parentElement;
|
|
47
|
+
while (cur && cur !== document.body) {
|
|
48
|
+
if (cur.hasAttribute("data-report-ignore-capture")) return true;
|
|
49
|
+
cur = cur.parentElement;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
function hasMediaAncestor(node) {
|
|
54
|
+
let cur = node.parentNode;
|
|
55
|
+
while (cur && cur !== document.body) {
|
|
56
|
+
if (cur instanceof Element && MEDIA_TAGS.has(cur.tagName)) return true;
|
|
57
|
+
cur = cur.parentNode;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
function applyInputMask(targetDoc = document) {
|
|
62
|
+
const trackForRestore = targetDoc === document;
|
|
63
|
+
targetDoc.querySelectorAll("input, textarea").forEach((el) => {
|
|
64
|
+
if (hasIgnoreAncestor(el)) return;
|
|
65
|
+
if (hasCaptureIgnoreAncestor(el)) return;
|
|
66
|
+
if (isInputElement(el)) {
|
|
67
|
+
if (IGNORED_INPUT_TYPES.has(el.type)) return;
|
|
68
|
+
if (!el.value.trim()) return;
|
|
69
|
+
if (trackForRestore && el.dataset.reportOriginalValue === void 0) el.dataset.reportOriginalValue = el.value;
|
|
70
|
+
if (el.type === "number") {
|
|
71
|
+
if (trackForRestore) el.dataset.reportOriginalType = "number";
|
|
72
|
+
el.type = "text";
|
|
73
|
+
}
|
|
74
|
+
el.value = MASK_PLACEHOLDER;
|
|
75
|
+
} else if (isTextareaElement(el)) {
|
|
76
|
+
if (!el.value.trim()) return;
|
|
77
|
+
if (trackForRestore && el.dataset.reportOriginalValue === void 0) el.dataset.reportOriginalValue = el.value;
|
|
78
|
+
el.value = MASK_PLACEHOLDER;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function applyMask() {
|
|
83
|
+
const walker = document.createTreeWalker(
|
|
84
|
+
document.body,
|
|
85
|
+
NodeFilter.SHOW_TEXT,
|
|
86
|
+
{
|
|
87
|
+
acceptNode(node2) {
|
|
88
|
+
const parent = node2.parentElement;
|
|
89
|
+
if (!parent) return NodeFilter.FILTER_REJECT;
|
|
90
|
+
const tag = parent.tagName;
|
|
91
|
+
if (tag === "SCRIPT" || tag === "STYLE" || tag === "NOSCRIPT") return NodeFilter.FILTER_REJECT;
|
|
92
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
let node = walker.nextNode();
|
|
97
|
+
while (node) {
|
|
98
|
+
const original = node.nodeValue ?? "";
|
|
99
|
+
const shouldMask = original.trim() && !hasIgnoreAncestor(node) && (hasForceTextMaskAncestor(node) || !hasTextMaskIgnoreAncestor(node)) && !hasMediaAncestor(node);
|
|
100
|
+
if (shouldMask) {
|
|
101
|
+
if (maskedTextNodes.has(node)) {
|
|
102
|
+
if (original !== MASK_PLACEHOLDER) {
|
|
103
|
+
maskedTextNodes.set(node, original);
|
|
104
|
+
node.nodeValue = MASK_PLACEHOLDER;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
maskedTextNodes.set(node, original);
|
|
108
|
+
node.nodeValue = MASK_PLACEHOLDER;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
node = walker.nextNode();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function removeMask() {
|
|
115
|
+
document.querySelectorAll("[data-report-original-value]").forEach((el) => {
|
|
116
|
+
const original = el.dataset.reportOriginalValue ?? "";
|
|
117
|
+
if (isInputElement(el) && el.dataset.reportOriginalType) {
|
|
118
|
+
el.type = el.dataset.reportOriginalType;
|
|
119
|
+
delete el.dataset.reportOriginalType;
|
|
120
|
+
}
|
|
121
|
+
if (isFormField(el)) el.value = original;
|
|
122
|
+
delete el.dataset.reportOriginalValue;
|
|
123
|
+
});
|
|
124
|
+
maskedTextNodes.forEach((original, node) => {
|
|
125
|
+
node.nodeValue = original;
|
|
126
|
+
});
|
|
127
|
+
maskedTextNodes.clear();
|
|
128
|
+
}
|
|
129
|
+
var videoMaskObserver = null;
|
|
130
|
+
function toggleVideoMask(enable) {
|
|
131
|
+
if (enable) {
|
|
132
|
+
if (!document.getElementById(VIDEO_STYLE_ID)) {
|
|
133
|
+
const style = document.createElement("style");
|
|
134
|
+
style.id = VIDEO_STYLE_ID;
|
|
135
|
+
style.textContent = `
|
|
136
|
+
.report-video-mask input:not([data-report-mask-ignore]),
|
|
137
|
+
.report-video-mask textarea:not([data-report-mask-ignore]) {
|
|
138
|
+
-webkit-text-security: disc !important;
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
document.head.appendChild(style);
|
|
142
|
+
}
|
|
143
|
+
document.documentElement.classList.add("report-video-mask");
|
|
144
|
+
applyMask();
|
|
145
|
+
if (!videoMaskObserver) {
|
|
146
|
+
videoMaskObserver = new MutationObserver(() => {
|
|
147
|
+
applyMask();
|
|
148
|
+
});
|
|
149
|
+
videoMaskObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
if (videoMaskObserver) {
|
|
153
|
+
videoMaskObserver.disconnect();
|
|
154
|
+
videoMaskObserver = null;
|
|
155
|
+
}
|
|
156
|
+
document.documentElement.classList.remove("report-video-mask");
|
|
157
|
+
document.getElementById(VIDEO_STYLE_ID)?.remove();
|
|
158
|
+
removeMask();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { applyInputMask, applyMask, removeMask, toggleVideoMask };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
interface ConsoleEntry {
|
|
2
|
+
level: 'log' | 'warn' | 'error' | 'info';
|
|
3
|
+
message: string;
|
|
4
|
+
ts: number;
|
|
5
|
+
}
|
|
6
|
+
interface NetworkEntry {
|
|
7
|
+
trackingId: string;
|
|
8
|
+
method?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
requestTs: number;
|
|
11
|
+
responseTs?: number;
|
|
12
|
+
status?: number;
|
|
13
|
+
isError?: boolean;
|
|
14
|
+
duration?: number;
|
|
15
|
+
message?: string | null;
|
|
16
|
+
errorMessage?: string;
|
|
17
|
+
}
|
|
18
|
+
type CaptureType = 'video' | 'screenshot';
|
|
19
|
+
interface RecordingConsent {
|
|
20
|
+
accepted: true;
|
|
21
|
+
version: string;
|
|
22
|
+
acceptedAt: string;
|
|
23
|
+
captureType: CaptureType;
|
|
24
|
+
}
|
|
25
|
+
interface ReportCategoryOption {
|
|
26
|
+
label: string;
|
|
27
|
+
value: number;
|
|
28
|
+
}
|
|
29
|
+
interface PageOption {
|
|
30
|
+
value: string;
|
|
31
|
+
label: string;
|
|
32
|
+
}
|
|
33
|
+
interface ReportMetadata {
|
|
34
|
+
userId?: string | number | null;
|
|
35
|
+
userName?: string | null;
|
|
36
|
+
email?: string | null;
|
|
37
|
+
roles?: string[];
|
|
38
|
+
userAgent?: string;
|
|
39
|
+
screenResolution?: string;
|
|
40
|
+
timestamp?: string;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
/** Structured payload handed to the host `submit` adapter. */
|
|
44
|
+
interface ReportSubmitPayload {
|
|
45
|
+
files: File[];
|
|
46
|
+
page: string;
|
|
47
|
+
environment: string;
|
|
48
|
+
categoryId?: number | null;
|
|
49
|
+
description?: string | null;
|
|
50
|
+
consoleLogs: ConsoleEntry[];
|
|
51
|
+
networkLogs: NetworkEntry[];
|
|
52
|
+
recordingConsent?: RecordingConsent | null;
|
|
53
|
+
screenshotConsent?: RecordingConsent | null;
|
|
54
|
+
metadata: ReportMetadata;
|
|
55
|
+
}
|
|
56
|
+
interface ReportSubmitResult {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
message?: string | null;
|
|
59
|
+
}
|
|
60
|
+
interface ReportIssueToast {
|
|
61
|
+
success: (message: string) => void;
|
|
62
|
+
error: (message: string) => void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The single injection point that removes all host coupling. A consumer supplies
|
|
66
|
+
* how to submit, how to resolve categories/metadata/current page, how to translate,
|
|
67
|
+
* and how to toast. Everything except `submit` has a sensible default.
|
|
68
|
+
*/
|
|
69
|
+
interface ReportIssueAdapter {
|
|
70
|
+
/**
|
|
71
|
+
* Persist a report. Receives both the structured payload and a ready-to-send
|
|
72
|
+
* multipart `FormData` (the canonical wire contract — see README).
|
|
73
|
+
*/
|
|
74
|
+
submit: (payload: ReportSubmitPayload, formData: FormData) => Promise<ReportSubmitResult>;
|
|
75
|
+
/** Category dropdown options. Omit to hide the category field. */
|
|
76
|
+
getCategories?: () => Promise<ReportCategoryOption[]> | ReportCategoryOption[];
|
|
77
|
+
/** Session metadata attached to every report. */
|
|
78
|
+
getMetadata?: () => ReportMetadata | Promise<ReportMetadata>;
|
|
79
|
+
/** Absolute URL of the page the user is on. Defaults to `window.location.href`. */
|
|
80
|
+
getCurrentUrl?: () => string;
|
|
81
|
+
/** Options for the "which page" dropdown. Function form is re-read on open. */
|
|
82
|
+
pageOptions?: PageOption[] | (() => PageOption[]);
|
|
83
|
+
/** Maps a page URL to an environment label (e.g. Locale/Test/Prod). */
|
|
84
|
+
environmentResolver?: (url: string) => string;
|
|
85
|
+
/** i18n. Defaults to identity (returns the key, which is English text). */
|
|
86
|
+
t?: (key: string, options?: Record<string, unknown>) => string;
|
|
87
|
+
locale?: string;
|
|
88
|
+
/** Toast sink. Defaults to `console`-based no-op-ish behaviour. */
|
|
89
|
+
toast?: ReportIssueToast;
|
|
90
|
+
/** Limits. */
|
|
91
|
+
maxFiles?: number;
|
|
92
|
+
maxFileSizeBytes?: number;
|
|
93
|
+
maxRecordingSeconds?: number;
|
|
94
|
+
/** Patch `console.*` to capture logs when the provider mounts. Default true. */
|
|
95
|
+
captureConsole?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
declare function pushConsoleLog(level: ConsoleEntry['level'], args: unknown[]): void;
|
|
99
|
+
declare function beginNetworkRequest(trackingId: string, method?: string, url?: string): void;
|
|
100
|
+
declare function completeNetworkRequest(trackingId: string, status?: number, isError?: boolean, message?: string, errorMessage?: string): void;
|
|
101
|
+
declare function failNetworkRequest(trackingId: string, status?: number, errorMessage?: string): void;
|
|
102
|
+
declare function getConsoleLogs(): ConsoleEntry[];
|
|
103
|
+
declare function getNetworkLogs(): NetworkEntry[];
|
|
104
|
+
/**
|
|
105
|
+
* Wraps `console.log/warn/error/info` so early logs are captured. Idempotent.
|
|
106
|
+
* This is an explicit opt-in call (invoked by the provider) rather than an
|
|
107
|
+
* import-time side effect, so importing the package never mutates globals.
|
|
108
|
+
*/
|
|
109
|
+
declare function patchConsole(): void;
|
|
110
|
+
|
|
111
|
+
type RecorderStartResult = 'started' | 'unsupported' | 'wrong-surface' | 'denied';
|
|
112
|
+
interface VideoRecorderCallbacks {
|
|
113
|
+
onComplete: (file: File) => void;
|
|
114
|
+
onStateChange?: (isRecording: boolean) => void;
|
|
115
|
+
onTick?: (elapsedSeconds: number) => void;
|
|
116
|
+
}
|
|
117
|
+
interface VideoRecorderController {
|
|
118
|
+
start: () => Promise<RecorderStartResult>;
|
|
119
|
+
stop: () => void;
|
|
120
|
+
/** Stop tracks/timers without firing onComplete. Call on unmount. */
|
|
121
|
+
dispose: () => void;
|
|
122
|
+
readonly maxDurationSeconds: number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Framework-agnostic screen recorder restricted to the current browser tab.
|
|
126
|
+
* Enforces "this tab only" and a hard max duration. No React inside.
|
|
127
|
+
*/
|
|
128
|
+
declare function createVideoRecorder(callbacks: VideoRecorderCallbacks, options?: {
|
|
129
|
+
maxDurationMs?: number;
|
|
130
|
+
}): VideoRecorderController;
|
|
131
|
+
|
|
132
|
+
declare const CONSENT_VERSION = "1.0";
|
|
133
|
+
declare function createConsent(captureType: CaptureType, version?: string): RecordingConsent;
|
|
134
|
+
|
|
135
|
+
export { CONSENT_VERSION as C, type NetworkEntry as N, type PageOption as P, type RecorderStartResult as R, type VideoRecorderCallbacks as V, type CaptureType as a, type ConsoleEntry as b, type RecordingConsent as c, type ReportCategoryOption as d, type ReportIssueAdapter as e, type ReportIssueToast as f, type ReportMetadata as g, type ReportSubmitPayload as h, type ReportSubmitResult as i, type VideoRecorderController as j, beginNetworkRequest as k, completeNetworkRequest as l, createConsent as m, createVideoRecorder as n, failNetworkRequest as o, getConsoleLogs as p, getNetworkLogs as q, patchConsole as r, pushConsoleLog as s };
|