@tra-bilisim/report-issue 0.1.0 → 0.1.2
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/README.md +222 -5
- package/dist/{AnnotationEditor-ILMYBTOG.js → AnnotationEditor-XYSZQLAH.js} +2 -1
- package/dist/{chunk-JMQUG5Q7.js → chunk-6IHDBCB5.js} +6 -1
- package/dist/{chunk-ZYF6UFBB.js → chunk-7ONPNSFH.js} +30 -7
- package/dist/{chunk-KY2IRP36.js → chunk-DCNBXNWY.js} +11 -9
- package/dist/{consent-DmS4DxOf.d.ts → consent-BSWTFAIV.d.ts} +33 -1
- package/dist/core.d.ts +12 -7
- package/dist/core.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +23 -12
- package/dist/screenshot-ZBPAR3UN.js +2 -0
- package/package.json +1 -1
- package/dist/screenshot-BQPXCSLD.js +0 -2
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ const adapter: ReportIssueAdapter = {
|
|
|
40
40
|
getMetadata: () => ({ userId: currentUser.id, roles: currentUser.roles }),
|
|
41
41
|
getCurrentUrl: () => window.location.href,
|
|
42
42
|
pageOptions: [{ value: 'https://app/x', label: '/x' }],
|
|
43
|
+
pageInputMode: 'select', // 'select' (default, uses pageOptions) | 'input' (free-text URL)
|
|
44
|
+
maskCapture: true, // default true (KVKK-safe); set false for unmasked screenshots/recordings
|
|
43
45
|
environmentResolver: url => (url.includes('localhost') ? 'Locale' : 'Prod'),
|
|
44
46
|
t: key => i18n.translate(key), // English keys are the default fallback text
|
|
45
47
|
locale: 'tr',
|
|
@@ -56,17 +58,183 @@ function App() {
|
|
|
56
58
|
}
|
|
57
59
|
```
|
|
58
60
|
|
|
61
|
+
## Localization (i18n)
|
|
62
|
+
|
|
63
|
+
The widget never bundles its own i18n library — it calls `t(key)` from your adapter
|
|
64
|
+
for every user-facing string. **The key itself is the English fallback text**, so if
|
|
65
|
+
you omit `t`, the UI renders in English as-is.
|
|
66
|
+
|
|
67
|
+
Wire it to whatever i18n system you already use:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
const adapter: ReportIssueAdapter = {
|
|
71
|
+
submit: ...,
|
|
72
|
+
t: key => i18n.translate(key), // e.g. react-i18next: (key) => i18next.t(key)
|
|
73
|
+
locale: 'tr',
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Below is the **complete list of keys** used by the widget, with Turkish
|
|
78
|
+
translations. Drop this straight into your translation resource file (adjust the
|
|
79
|
+
shape to whatever your `i18n.translate`/`t` implementation expects — this is a plain
|
|
80
|
+
key → value map):
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
{
|
|
84
|
+
// Annotation editor toolbar
|
|
85
|
+
'Select': 'Seç',
|
|
86
|
+
'Draw': 'Çizim',
|
|
87
|
+
'Line': 'Çizgi',
|
|
88
|
+
'Arrow': 'Ok',
|
|
89
|
+
'Rectangle': 'Dikdörtgen',
|
|
90
|
+
'Circle': 'Daire',
|
|
91
|
+
'Text': 'Metin',
|
|
92
|
+
'Highlight': 'Vurgula',
|
|
93
|
+
'Thickness': 'Kalınlık',
|
|
94
|
+
'Undo': 'Geri al',
|
|
95
|
+
'Redo': 'Yinele',
|
|
96
|
+
'Cancel': 'İptal',
|
|
97
|
+
'Confirm': 'Onayla',
|
|
98
|
+
|
|
99
|
+
// Floating button / dialog title
|
|
100
|
+
'Report an Issue': 'Sorun Bildir',
|
|
101
|
+
'Describe the problem and attach screenshots or a screen recording.': 'Sorunu açıklayın ve ekran görüntüsü veya ekran kaydı ekleyin.',
|
|
102
|
+
|
|
103
|
+
// Attachments
|
|
104
|
+
'Attachments': 'Ekler',
|
|
105
|
+
'Take a Screenshot': 'Ekran Görüntüsü Al',
|
|
106
|
+
'Record Video': 'Video Kaydet',
|
|
107
|
+
'{name} exceeds the size limit.': '{name} boyut sınırını aşıyor.',
|
|
108
|
+
'You can attach at most {count} files.': 'En fazla {count} dosya ekleyebilirsiniz.',
|
|
109
|
+
'Failed to capture screenshot.': 'Ekran görüntüsü alınamadı.',
|
|
110
|
+
|
|
111
|
+
// Screen recording
|
|
112
|
+
'Screen recording is only supported on Chrome and Edge browsers.': 'Ekran kaydı yalnızca Chrome ve Edge tarayıcılarında desteklenir.',
|
|
113
|
+
'Only this application tab can be recorded. Please share this tab.': 'Yalnızca bu uygulama sekmesi kaydedilebilir. Lütfen bu sekmeyi paylaşın.',
|
|
114
|
+
'Screen recording permission was denied.': 'Ekran kaydı izni reddedildi.',
|
|
115
|
+
'Stop': 'Durdur',
|
|
116
|
+
|
|
117
|
+
// Recording / screenshot consent dialog (KVKK)
|
|
118
|
+
'Screenshot Consent': 'Ekran Görüntüsü Onayı',
|
|
119
|
+
'Screen Recording Consent': 'Ekran Kaydı Onayı',
|
|
120
|
+
'Before the screenshot is taken, please review which data is collected and give your consent.': 'Ekran görüntüsü alınmadan önce, hangi verilerin toplandığını inceleyip onayınızı verin.',
|
|
121
|
+
'Before the recording starts, please review which data is collected and give your consent.': 'Kayıt başlamadan önce, hangi verilerin toplandığını inceleyip onayınızı verin.',
|
|
122
|
+
'To diagnose the issue you reported, a screenshot of this application screen will be captured. Sensitive on-screen data is masked before capture.': 'Bildirdiğiniz sorunu teşhis edebilmek için bu uygulama ekranının bir görüntüsü alınacaktır. Hassas veriler görüntü alınmadan önce maskelenir.',
|
|
123
|
+
'To diagnose the issue you reported, a short screen recording of only this tab will be captured. All on-screen data is masked during recording; you may temporarily reveal it.': 'Bildirdiğiniz sorunu teşhis edebilmek için yalnızca bu sekmenin kısa bir ekran kaydı alınacaktır. Kayıt sırasında tüm ekran verileri maskelenir; isterseniz geçici olarak görünür hale getirebilirsiniz.',
|
|
124
|
+
'Data that will be collected:': 'Toplanacak veriler:',
|
|
125
|
+
'A masked screenshot of this application screen': 'Bu uygulama ekranının maskelenmiş bir görüntüsü',
|
|
126
|
+
'A masked screen recording limited to this application tab': 'Yalnızca bu sekmeyle sınırlı, maskelenmiş bir ekran kaydı',
|
|
127
|
+
'Console and network logs of this session': 'Bu oturuma ait konsol ve ağ kayıtları',
|
|
128
|
+
'Session metadata (user, roles, browser, page)': 'Oturum meta verileri (kullanıcı, roller, tarayıcı, sayfa)',
|
|
129
|
+
'This data is used solely to diagnose and resolve the reported issue and is shared only with the relevant technical team. You may withdraw your consent by not submitting the report.': 'Bu veriler yalnızca bildirdiğiniz sorunu teşhis edip çözmek amacıyla kullanılır ve yalnızca ilgili teknik ekiple paylaşılır. Raporu göndermeyerek onayınızı geri çekebilirsiniz.',
|
|
130
|
+
'I have read the above and give my explicit consent to the processing and sharing of this data.': 'Yukarıdakileri okudum ve bu verilerin işlenmesine ve paylaşılmasına açıkça onay veriyorum.',
|
|
131
|
+
'Take Screenshot': 'Ekran Görüntüsü Al',
|
|
132
|
+
'Start Recording': 'Kaydı Başlat',
|
|
133
|
+
|
|
134
|
+
// Page field
|
|
135
|
+
'The page where the issue occurred': 'Sorunun oluştuğu sayfa',
|
|
136
|
+
'Current page': 'Geçerli sayfa',
|
|
137
|
+
'Enter the page where the issue occurred': 'Sorunun oluştuğu sayfayı girin',
|
|
138
|
+
'Please specify the page.': 'Lütfen sayfayı belirtin.',
|
|
139
|
+
|
|
140
|
+
// Category / description
|
|
141
|
+
'Category': 'Kategori',
|
|
142
|
+
'optional': 'opsiyonel',
|
|
143
|
+
'Description': 'Açıklama',
|
|
144
|
+
'Describe what happened...': 'Ne olduğunu açıklayın...',
|
|
145
|
+
|
|
146
|
+
// Select control
|
|
147
|
+
'Search': 'Ara',
|
|
148
|
+
'No results': 'Sonuç bulunamadı',
|
|
149
|
+
|
|
150
|
+
// Submit
|
|
151
|
+
'Submit': 'Gönder',
|
|
152
|
+
'An error occurred!': 'Bir hata oluştu!',
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`{name}` and `{count}` are literal placeholders — the widget does simple
|
|
157
|
+
`.replace('{name}', ...)` / `.replace('{count}', ...)` substitution itself, so keep
|
|
158
|
+
those tokens verbatim in your translated value.
|
|
159
|
+
|
|
59
160
|
## Network log capture (optional)
|
|
60
161
|
|
|
61
|
-
|
|
162
|
+
`attachAxiosNetworkLogging(instance, options?)` installs request/response
|
|
163
|
+
interceptors on your axios instance that feed the shared report-issue log store.
|
|
164
|
+
The captured requests are attached to a report as `networkLogs` automatically.
|
|
165
|
+
|
|
166
|
+
Call it **once**, at module scope where you create your axios instance — not inside
|
|
167
|
+
a React component (that would re-attach on every render):
|
|
62
168
|
|
|
63
169
|
```ts
|
|
170
|
+
// src/api/http.ts
|
|
171
|
+
import axios from 'axios';
|
|
64
172
|
import { attachAxiosNetworkLogging } from '@tra-bilisim/report-issue/adapters/axios';
|
|
65
|
-
|
|
173
|
+
|
|
174
|
+
export const http = axios.create({ baseURL: '/api' });
|
|
175
|
+
|
|
176
|
+
// One call — done. Every request through `http` is now logged.
|
|
177
|
+
attachAxiosNetworkLogging(http);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`attachAxiosNetworkLogging` returns a **detach** function that removes the
|
|
181
|
+
interceptors (useful in tests or HMR):
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const detach = attachAxiosNetworkLogging(http);
|
|
185
|
+
// ...later
|
|
186
|
+
detach();
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Options
|
|
190
|
+
|
|
191
|
+
Both options are optional — the defaults cover common API shapes.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
attachAxiosNetworkLogging(http, {
|
|
195
|
+
// Pull a human-readable message out of the response body for the log entry.
|
|
196
|
+
// Default looks for { message | Message | errorMessage | ErrorMessage }.
|
|
197
|
+
getMessage: data => (data as any)?.error?.detail,
|
|
198
|
+
|
|
199
|
+
// Decide whether a response counts as an error in the log.
|
|
200
|
+
// Default: any status < 200 or >= 300.
|
|
201
|
+
isErrorResponse: (status, data) =>
|
|
202
|
+
status >= 400 || (data as any)?.success === false,
|
|
203
|
+
});
|
|
66
204
|
```
|
|
67
205
|
|
|
68
|
-
|
|
69
|
-
|
|
206
|
+
### Request correlation header
|
|
207
|
+
|
|
208
|
+
Each logged request is tagged with an `X-Request-Tracking-Id` header. If your
|
|
209
|
+
backend already sets/echoes this header, the adapter **reuses** it; otherwise it
|
|
210
|
+
generates one (`rpi_<time>_<rand>`). This lets you cross-reference a captured
|
|
211
|
+
request with your server logs.
|
|
212
|
+
|
|
213
|
+
### Custom transport (no axios)
|
|
214
|
+
|
|
215
|
+
Using `fetch` or another client? Skip the adapter and call the core primitives
|
|
216
|
+
directly from `@tra-bilisim/report-issue/core`:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import {
|
|
220
|
+
beginNetworkRequest,
|
|
221
|
+
completeNetworkRequest,
|
|
222
|
+
failNetworkRequest,
|
|
223
|
+
} from '@tra-bilisim/report-issue/core';
|
|
224
|
+
|
|
225
|
+
async function loggedFetch(url: string, init?: RequestInit) {
|
|
226
|
+
const id = crypto.randomUUID();
|
|
227
|
+
beginNetworkRequest(id, init?.method ?? 'GET', url);
|
|
228
|
+
try {
|
|
229
|
+
const res = await fetch(url, init);
|
|
230
|
+
completeNetworkRequest(id, res.status, !res.ok);
|
|
231
|
+
return res;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
failNetworkRequest(id, undefined, (err as Error).message);
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
70
238
|
|
|
71
239
|
## Wire contract (`submit`)
|
|
72
240
|
|
|
@@ -88,13 +256,62 @@ Or wire your own transport by calling the primitives from `@tra-bilisim/report-i
|
|
|
88
256
|
|
|
89
257
|
`submit` must resolve to `{ ok: boolean; message?: string | null }`.
|
|
90
258
|
|
|
91
|
-
## Masking
|
|
259
|
+
## Masking (KVKK privacy)
|
|
260
|
+
|
|
261
|
+
By default, **everything is masked**: page text is replaced with `●●●●●●` and
|
|
262
|
+
input/textarea values are hidden before a screenshot is taken or a recording
|
|
263
|
+
starts. This is the safe default — turn it off only if your app has no
|
|
264
|
+
sensitive on-screen data.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const adapter: ReportIssueAdapter = {
|
|
268
|
+
submit: ...,
|
|
269
|
+
maskCapture: false, // disable masking entirely — screenshots/recordings capture the real UI
|
|
270
|
+
};
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`maskCapture` is a single on/off switch for **both** capture types (screenshot and
|
|
274
|
+
screen recording); there's no separate flag per type. When `true` (default), the
|
|
275
|
+
per-element data attributes below give you fine-grained control over what stays
|
|
276
|
+
masked/visible within that overall mask:
|
|
277
|
+
|
|
278
|
+
### Masking controls (data attributes)
|
|
92
279
|
|
|
93
280
|
- `data-report-mask-ignore` — never mask this subtree (also excluded from the video mask)
|
|
94
281
|
- `data-report-mask-text-ignore` — keep text visible (still masks inputs)
|
|
95
282
|
- `data-report-ignore-capture` — exclude from screenshots entirely
|
|
96
283
|
- `data-report-mask-control` / `data-report-mask-value` — force text masking
|
|
97
284
|
|
|
285
|
+
### Masking controls for third-party markup (CSS selectors)
|
|
286
|
+
|
|
287
|
+
The attributes above require you to add them to your own JSX. If the element you
|
|
288
|
+
need to exclude is rendered by a component you don't control — a `<legend>` from a
|
|
289
|
+
form library, a chart tooltip, a portal from another package — you can't attach an
|
|
290
|
+
attribute to it. `maskSelectors` gives you the same four controls as **CSS
|
|
291
|
+
selectors** instead, so you can target it from outside without touching its markup:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
const adapter: ReportIssueAdapter = {
|
|
295
|
+
submit: ...,
|
|
296
|
+
maskSelectors: {
|
|
297
|
+
ignore: ['legend', '.third-party-lib__tooltip'], // == data-report-mask-ignore
|
|
298
|
+
textIgnore: ['.some-lib-readonly-label'], // == data-report-mask-text-ignore
|
|
299
|
+
forceTextMask: [], // == data-report-mask-control
|
|
300
|
+
ignoreCapture: [], // == data-report-ignore-capture
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
All four arrays are optional and additive — they extend the built-in attributes,
|
|
306
|
+
they don't replace them. Selectors are matched the same way the attributes are:
|
|
307
|
+
against the element itself when masking text (a `<legend>Personal info</legend>`
|
|
308
|
+
selector directly excludes its own text) and against **ancestors** when masking
|
|
309
|
+
inputs (`ignore: ['.card']` protects inputs nested inside `.card`, not a `.card`
|
|
310
|
+
element that is itself an input). Prefer a selector specific enough to avoid
|
|
311
|
+
unintentionally un-masking real user data elsewhere on the page (e.g. `'legend'` is
|
|
312
|
+
fine if you have no other sensitive `<legend>` elements; otherwise scope it further,
|
|
313
|
+
e.g. `'.my-form legend'`).
|
|
314
|
+
|
|
98
315
|
## Entry points
|
|
99
316
|
|
|
100
317
|
- `@tra-bilisim/report-issue` — React provider, button, dialog, hooks, types
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useReportIssueConfig, Button } from './chunk-
|
|
1
|
+
import { useReportIssueConfig, Button } from './chunk-6IHDBCB5.js';
|
|
2
2
|
import './chunk-EXDFVVYA.js';
|
|
3
|
+
import './chunk-7ONPNSFH.js';
|
|
3
4
|
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
4
5
|
import { Canvas, PencilBrush, FabricImage, Rect, Ellipse, Line, IText, Triangle, Group } from 'fabric';
|
|
5
6
|
import { MousePointer2, Pencil, Minus, MoveRight, Square, Circle, Type, Highlighter, Undo2, Redo2 } from 'lucide-react';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { patchConsole } from './chunk-EXDFVVYA.js';
|
|
2
|
+
import { configureMaskSelectors } from './chunk-7ONPNSFH.js';
|
|
2
3
|
import { createContext, forwardRef, useState, useMemo, useEffect, useContext } from 'react';
|
|
3
4
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
5
|
|
|
@@ -32,7 +33,8 @@ function resolveConfig(adapter) {
|
|
|
32
33
|
getCurrentUrl: adapter.getCurrentUrl ?? (() => typeof window !== "undefined" ? window.location.href : ""),
|
|
33
34
|
maxFiles: adapter.maxFiles ?? DEFAULT_MAX_FILES,
|
|
34
35
|
maxFileSizeBytes: adapter.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE,
|
|
35
|
-
maxRecordingSeconds: adapter.maxRecordingSeconds ?? DEFAULT_MAX_RECORDING_SECONDS
|
|
36
|
+
maxRecordingSeconds: adapter.maxRecordingSeconds ?? DEFAULT_MAX_RECORDING_SECONDS,
|
|
37
|
+
maskCapture: adapter.maskCapture ?? true
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
var ReportIssueContext = createContext(void 0);
|
|
@@ -45,6 +47,9 @@ var ReportIssueProvider = ({ config, children }) => {
|
|
|
45
47
|
useEffect(() => {
|
|
46
48
|
if (config.captureConsole !== false) patchConsole();
|
|
47
49
|
}, [config.captureConsole]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
configureMaskSelectors(config.maskSelectors);
|
|
52
|
+
}, [config.maskSelectors]);
|
|
48
53
|
const captureMode = isCapturing || isRecording;
|
|
49
54
|
const value = useMemo(() => ({
|
|
50
55
|
config: resolved,
|
|
@@ -7,8 +7,26 @@ var MEDIA_TAGS = /* @__PURE__ */ new Set(["IMG", "PICTURE", "SVG", "CANVAS", "VI
|
|
|
7
7
|
var MASK_IGNORE_SELECTOR = "[data-report-mask-ignore], [data-sonner-toaster], [data-sonner-toast]";
|
|
8
8
|
var TEXT_MASK_IGNORE_SELECTOR = "[data-report-mask-text-ignore]";
|
|
9
9
|
var FORCE_TEXT_MASK_SELECTOR = "[data-report-mask-control], [data-report-mask-value]";
|
|
10
|
+
var CAPTURE_IGNORE_SELECTOR = "[data-report-ignore-capture]";
|
|
10
11
|
var IGNORED_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden", "checkbox", "radio", "file", "button", "submit", "reset", "image", "range", "color", "password"]);
|
|
11
12
|
var maskedTextNodes = /* @__PURE__ */ new Map();
|
|
13
|
+
var extraSelectors = {
|
|
14
|
+
ignore: [],
|
|
15
|
+
textIgnore: [],
|
|
16
|
+
forceTextMask: [],
|
|
17
|
+
ignoreCapture: []
|
|
18
|
+
};
|
|
19
|
+
function configureMaskSelectors(overrides = {}) {
|
|
20
|
+
extraSelectors.ignore = overrides.ignore ?? [];
|
|
21
|
+
extraSelectors.textIgnore = overrides.textIgnore ?? [];
|
|
22
|
+
extraSelectors.forceTextMask = overrides.forceTextMask ?? [];
|
|
23
|
+
extraSelectors.ignoreCapture = overrides.ignoreCapture ?? [];
|
|
24
|
+
}
|
|
25
|
+
var combineSelector = (base, extra) => extra.length ? `${base}, ${extra.join(", ")}` : base;
|
|
26
|
+
var ignoreSelector = () => combineSelector(MASK_IGNORE_SELECTOR, extraSelectors.ignore);
|
|
27
|
+
var textIgnoreSelector = () => combineSelector(TEXT_MASK_IGNORE_SELECTOR, extraSelectors.textIgnore);
|
|
28
|
+
var forceTextMaskSelector = () => combineSelector(FORCE_TEXT_MASK_SELECTOR, extraSelectors.forceTextMask);
|
|
29
|
+
var captureIgnoreSelector = () => combineSelector(CAPTURE_IGNORE_SELECTOR, extraSelectors.ignoreCapture);
|
|
12
30
|
function isInputElement(el) {
|
|
13
31
|
return el.tagName === "INPUT";
|
|
14
32
|
}
|
|
@@ -20,32 +38,36 @@ function isFormField(el) {
|
|
|
20
38
|
}
|
|
21
39
|
function hasIgnoreAncestor(node) {
|
|
22
40
|
let cur = node.parentNode;
|
|
41
|
+
const selector = ignoreSelector();
|
|
23
42
|
while (cur && cur !== document.body) {
|
|
24
|
-
if (cur instanceof HTMLElement && cur.matches(
|
|
43
|
+
if (cur instanceof HTMLElement && cur.matches(selector)) return true;
|
|
25
44
|
cur = cur.parentNode;
|
|
26
45
|
}
|
|
27
46
|
return false;
|
|
28
47
|
}
|
|
29
48
|
function hasTextMaskIgnoreAncestor(node) {
|
|
30
49
|
let cur = node.parentNode;
|
|
50
|
+
const selector = textIgnoreSelector();
|
|
31
51
|
while (cur && cur !== document.body) {
|
|
32
|
-
if (cur instanceof HTMLElement && cur.matches(
|
|
52
|
+
if (cur instanceof HTMLElement && cur.matches(selector)) return true;
|
|
33
53
|
cur = cur.parentNode;
|
|
34
54
|
}
|
|
35
55
|
return false;
|
|
36
56
|
}
|
|
37
57
|
function hasForceTextMaskAncestor(node) {
|
|
38
58
|
let cur = node.parentNode;
|
|
59
|
+
const selector = forceTextMaskSelector();
|
|
39
60
|
while (cur && cur !== document.body) {
|
|
40
|
-
if (cur instanceof HTMLElement && cur.matches(
|
|
61
|
+
if (cur instanceof HTMLElement && cur.matches(selector)) return true;
|
|
41
62
|
cur = cur.parentNode;
|
|
42
63
|
}
|
|
43
64
|
return false;
|
|
44
65
|
}
|
|
45
66
|
function hasCaptureIgnoreAncestor(el) {
|
|
46
67
|
let cur = el.parentElement;
|
|
68
|
+
const selector = captureIgnoreSelector();
|
|
47
69
|
while (cur && cur !== document.body) {
|
|
48
|
-
if (cur.
|
|
70
|
+
if (cur.matches(selector)) return true;
|
|
49
71
|
cur = cur.parentElement;
|
|
50
72
|
}
|
|
51
73
|
return false;
|
|
@@ -132,9 +154,10 @@ function toggleVideoMask(enable) {
|
|
|
132
154
|
if (!document.getElementById(VIDEO_STYLE_ID)) {
|
|
133
155
|
const style = document.createElement("style");
|
|
134
156
|
style.id = VIDEO_STYLE_ID;
|
|
157
|
+
const notIgnored = ignoreSelector();
|
|
135
158
|
style.textContent = `
|
|
136
|
-
.report-video-mask input:not(
|
|
137
|
-
.report-video-mask textarea:not(
|
|
159
|
+
.report-video-mask input:not(${notIgnored}),
|
|
160
|
+
.report-video-mask textarea:not(${notIgnored}) {
|
|
138
161
|
-webkit-text-security: disc !important;
|
|
139
162
|
}
|
|
140
163
|
`;
|
|
@@ -159,4 +182,4 @@ function toggleVideoMask(enable) {
|
|
|
159
182
|
}
|
|
160
183
|
}
|
|
161
184
|
|
|
162
|
-
export { applyInputMask, applyMask, removeMask, toggleVideoMask };
|
|
185
|
+
export { applyInputMask, applyMask, captureIgnoreSelector, configureMaskSelectors, removeMask, toggleVideoMask };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { applyMask, applyInputMask, removeMask } from './chunk-
|
|
1
|
+
import { applyMask, applyInputMask, captureIgnoreSelector, removeMask } from './chunk-7ONPNSFH.js';
|
|
2
2
|
import html2canvas from 'html2canvas-pro';
|
|
3
3
|
|
|
4
4
|
var HIDE_DIALOG_CLASS = "report-hide-dialog";
|
|
5
5
|
var waitForCaptureImages = async () => {
|
|
6
6
|
const images = Array.from(document.images).filter((img) => {
|
|
7
|
-
if (img.closest(
|
|
7
|
+
if (img.closest(captureIgnoreSelector())) return false;
|
|
8
8
|
const style = window.getComputedStyle(img);
|
|
9
9
|
return img.getClientRects().length > 0 && style.display !== "none" && style.visibility !== "hidden";
|
|
10
10
|
});
|
|
@@ -23,7 +23,7 @@ var waitForCaptureImages = async () => {
|
|
|
23
23
|
var inlineCaptureSvgImages = async () => {
|
|
24
24
|
const restores = [];
|
|
25
25
|
const svgImages = Array.from(document.images).filter((img) => {
|
|
26
|
-
if (img.closest(
|
|
26
|
+
if (img.closest(captureIgnoreSelector())) return false;
|
|
27
27
|
const src = img.currentSrc || img.src;
|
|
28
28
|
if (!src) return false;
|
|
29
29
|
try {
|
|
@@ -55,16 +55,18 @@ var inlineCaptureSvgImages = async () => {
|
|
|
55
55
|
}));
|
|
56
56
|
return () => restores.forEach((restore) => restore());
|
|
57
57
|
};
|
|
58
|
-
async function captureMaskedScreenshot() {
|
|
58
|
+
async function captureMaskedScreenshot(mask = true) {
|
|
59
59
|
let restoreCaptureSvgImages = () => {
|
|
60
60
|
};
|
|
61
61
|
document.documentElement.classList.add(HIDE_DIALOG_CLASS);
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if (mask) {
|
|
63
|
+
applyMask();
|
|
64
|
+
applyInputMask();
|
|
65
|
+
}
|
|
64
66
|
await new Promise((resolve) => {
|
|
65
67
|
setTimeout(resolve, 450);
|
|
66
68
|
});
|
|
67
|
-
applyInputMask();
|
|
69
|
+
if (mask) applyInputMask();
|
|
68
70
|
restoreCaptureSvgImages = await inlineCaptureSvgImages();
|
|
69
71
|
await waitForCaptureImages();
|
|
70
72
|
const docEl = document.documentElement;
|
|
@@ -83,11 +85,11 @@ async function captureMaskedScreenshot() {
|
|
|
83
85
|
height: pageScrolls ? fullHeight : viewportHeight,
|
|
84
86
|
windowWidth: viewportWidth,
|
|
85
87
|
windowHeight: pageScrolls ? fullHeight : viewportHeight,
|
|
86
|
-
ignoreElements: (el) => el.
|
|
88
|
+
ignoreElements: (el) => el.matches?.(captureIgnoreSelector()) || el.getAttribute?.("data-slot") === "dialog-overlay",
|
|
87
89
|
// onclone runs after the DOM is cloned but before rendering. React cannot
|
|
88
90
|
// reset values in the clone, so masking is reliable here.
|
|
89
91
|
onclone: (clonedDoc) => {
|
|
90
|
-
applyInputMask(clonedDoc);
|
|
92
|
+
if (mask) applyInputMask(clonedDoc);
|
|
91
93
|
}
|
|
92
94
|
});
|
|
93
95
|
return canvas.toDataURL("image/png");
|
|
@@ -30,6 +30,24 @@ interface PageOption {
|
|
|
30
30
|
value: string;
|
|
31
31
|
label: string;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* CSS-selector-based equivalents of the `data-report-*` masking attributes, for
|
|
35
|
+
* elements you cannot add attributes to (e.g. markup rendered by a third-party
|
|
36
|
+
* component — a `<legend>` from a form library, a chart tooltip, etc.). Each
|
|
37
|
+
* array is additive: it extends, rather than replaces, the corresponding
|
|
38
|
+
* attribute. Selectors are matched against ancestors the same way the
|
|
39
|
+
* attributes are — put them on a wrapper that contains the target content.
|
|
40
|
+
*/
|
|
41
|
+
interface MaskSelectorOverrides {
|
|
42
|
+
/** Extra CSS selectors whose subtree is never masked. Same effect as `data-report-mask-ignore`. */
|
|
43
|
+
ignore?: string[];
|
|
44
|
+
/** Extra CSS selectors whose text stays visible (inputs still masked). Same effect as `data-report-mask-text-ignore`. */
|
|
45
|
+
textIgnore?: string[];
|
|
46
|
+
/** Extra CSS selectors whose text is force-masked. Same effect as `data-report-mask-control`. */
|
|
47
|
+
forceTextMask?: string[];
|
|
48
|
+
/** Extra CSS selectors excluded entirely from screenshots. Same effect as `data-report-ignore-capture`. */
|
|
49
|
+
ignoreCapture?: string[];
|
|
50
|
+
}
|
|
33
51
|
interface ReportMetadata {
|
|
34
52
|
userId?: string | number | null;
|
|
35
53
|
userName?: string | null;
|
|
@@ -80,6 +98,12 @@ interface ReportIssueAdapter {
|
|
|
80
98
|
getCurrentUrl?: () => string;
|
|
81
99
|
/** Options for the "which page" dropdown. Function form is re-read on open. */
|
|
82
100
|
pageOptions?: PageOption[] | (() => PageOption[]);
|
|
101
|
+
/**
|
|
102
|
+
* How the "which page" field is rendered when "current page" is unchecked:
|
|
103
|
+
* - `'select'` (default): searchable dropdown fed by `pageOptions`.
|
|
104
|
+
* - `'input'`: free-text field for an arbitrary URL/path (`pageOptions` ignored).
|
|
105
|
+
*/
|
|
106
|
+
pageInputMode?: 'select' | 'input';
|
|
83
107
|
/** Maps a page URL to an environment label (e.g. Locale/Test/Prod). */
|
|
84
108
|
environmentResolver?: (url: string) => string;
|
|
85
109
|
/** i18n. Defaults to identity (returns the key, which is English text). */
|
|
@@ -93,6 +117,14 @@ interface ReportIssueAdapter {
|
|
|
93
117
|
maxRecordingSeconds?: number;
|
|
94
118
|
/** Patch `console.*` to capture logs when the provider mounts. Default true. */
|
|
95
119
|
captureConsole?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Mask sensitive on-screen text/input values (KVKK-style privacy mask) during
|
|
122
|
+
* screenshot capture and screen recording. Default `true`. Set to `false` if
|
|
123
|
+
* your app has no sensitive on-screen data and you want unmasked captures.
|
|
124
|
+
*/
|
|
125
|
+
maskCapture?: boolean;
|
|
126
|
+
/** CSS-selector overrides for elements you can't attach `data-report-*` attributes to. */
|
|
127
|
+
maskSelectors?: MaskSelectorOverrides;
|
|
96
128
|
}
|
|
97
129
|
|
|
98
130
|
declare function pushConsoleLog(level: ConsoleEntry['level'], args: unknown[]): void;
|
|
@@ -132,4 +164,4 @@ declare function createVideoRecorder(callbacks: VideoRecorderCallbacks, options?
|
|
|
132
164
|
declare const CONSENT_VERSION = "1.0";
|
|
133
165
|
declare function createConsent(captureType: CaptureType, version?: string): RecordingConsent;
|
|
134
166
|
|
|
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 };
|
|
167
|
+
export { CONSENT_VERSION as C, type MaskSelectorOverrides as M, 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 };
|
package/dist/core.d.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import { M as MaskSelectorOverrides } from './consent-BSWTFAIV.js';
|
|
2
|
+
export { C as CONSENT_VERSION, a as CaptureType, b as ConsoleEntry, N as NetworkEntry, P as PageOption, R as RecorderStartResult, c as RecordingConsent, d as ReportCategoryOption, e as ReportIssueAdapter, f as ReportIssueToast, g as ReportMetadata, h as ReportSubmitPayload, i as ReportSubmitResult, V as VideoRecorderCallbacks, j as VideoRecorderController, k as beginNetworkRequest, l as completeNetworkRequest, m as createConsent, n as createVideoRecorder, o as failNetworkRequest, p as getConsoleLogs, q as getNetworkLogs, r as patchConsole, s as pushConsoleLog } from './consent-BSWTFAIV.js';
|
|
2
3
|
|
|
4
|
+
declare function configureMaskSelectors(overrides?: MaskSelectorOverrides): void;
|
|
5
|
+
/** CSS selector matching everything excluded from screenshots — built-in attribute plus any `maskSelectors.ignoreCapture` overrides. */
|
|
6
|
+
declare const captureIgnoreSelector: () => string;
|
|
3
7
|
declare function applyInputMask(targetDoc?: Document): void;
|
|
4
8
|
declare function applyMask(): void;
|
|
5
9
|
declare function removeMask(): void;
|
|
6
10
|
declare function toggleVideoMask(enable: boolean): void;
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
|
-
* Captures a
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* Captures a PNG screenshot of the whole document body and returns a data URL.
|
|
14
|
+
* Hides any element marked with `data-report-ignore-capture` (the dialog itself)
|
|
15
|
+
* during the capture, then restores everything before resolving. Framework-agnostic.
|
|
16
|
+
*
|
|
17
|
+
* @param mask Apply KVKK text/input masking before capturing. Default `true`.
|
|
13
18
|
*/
|
|
14
|
-
declare function captureMaskedScreenshot(): Promise<string>;
|
|
19
|
+
declare function captureMaskedScreenshot(mask?: boolean): Promise<string>;
|
|
15
20
|
|
|
16
|
-
export { applyInputMask, applyMask, captureMaskedScreenshot, removeMask, toggleVideoMask };
|
|
21
|
+
export { MaskSelectorOverrides, applyInputMask, applyMask, captureIgnoreSelector, captureMaskedScreenshot, configureMaskSelectors, removeMask, toggleVideoMask };
|
package/dist/core.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { CONSENT_VERSION, createConsent, createVideoRecorder } from './chunk-5S66KGBW.js';
|
|
2
2
|
export { beginNetworkRequest, completeNetworkRequest, failNetworkRequest, getConsoleLogs, getNetworkLogs, patchConsole, pushConsoleLog } from './chunk-EXDFVVYA.js';
|
|
3
|
-
export { captureMaskedScreenshot } from './chunk-
|
|
4
|
-
export { applyInputMask, applyMask, removeMask, toggleVideoMask } from './chunk-
|
|
3
|
+
export { captureMaskedScreenshot } from './chunk-DCNBXNWY.js';
|
|
4
|
+
export { applyInputMask, applyMask, captureIgnoreSelector, configureMaskSelectors, removeMask, toggleVideoMask } from './chunk-7ONPNSFH.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { e as ReportIssueAdapter, f as ReportIssueToast, a as CaptureType, c as RecordingConsent, b as ConsoleEntry, N as NetworkEntry, R as RecorderStartResult } from './consent-
|
|
4
|
-
export { C as CONSENT_VERSION, P as PageOption, d as ReportCategoryOption, g as ReportMetadata, h as ReportSubmitPayload, i as ReportSubmitResult, p as getConsoleLogs, q as getNetworkLogs, r as patchConsole } from './consent-
|
|
3
|
+
import { e as ReportIssueAdapter, f as ReportIssueToast, a as CaptureType, c as RecordingConsent, b as ConsoleEntry, N as NetworkEntry, R as RecorderStartResult } from './consent-BSWTFAIV.js';
|
|
4
|
+
export { C as CONSENT_VERSION, P as PageOption, d as ReportCategoryOption, g as ReportMetadata, h as ReportSubmitPayload, i as ReportSubmitResult, p as getConsoleLogs, q as getNetworkLogs, r as patchConsole } from './consent-BSWTFAIV.js';
|
|
5
5
|
|
|
6
6
|
/** Adapter with every optional field filled in — what components actually read. */
|
|
7
7
|
interface ResolvedConfig {
|
|
@@ -14,6 +14,7 @@ interface ResolvedConfig {
|
|
|
14
14
|
maxFiles: number;
|
|
15
15
|
maxFileSizeBytes: number;
|
|
16
16
|
maxRecordingSeconds: number;
|
|
17
|
+
maskCapture: boolean;
|
|
17
18
|
}
|
|
18
19
|
interface ReportIssueContextValue {
|
|
19
20
|
config: ResolvedConfig;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createVideoRecorder, CONSENT_VERSION } from './chunk-5S66KGBW.js';
|
|
2
2
|
export { CONSENT_VERSION } from './chunk-5S66KGBW.js';
|
|
3
|
-
import { cn, useReportIssueConfig, Button, useReportIssue } from './chunk-
|
|
4
|
-
export { ReportIssueProvider, useReportIssue, useReportIssueConfig } from './chunk-
|
|
3
|
+
import { cn, useReportIssueConfig, Button, useReportIssue } from './chunk-6IHDBCB5.js';
|
|
4
|
+
export { ReportIssueProvider, useReportIssue, useReportIssueConfig } from './chunk-6IHDBCB5.js';
|
|
5
5
|
import { getConsoleLogs, getNetworkLogs } from './chunk-EXDFVVYA.js';
|
|
6
6
|
export { getConsoleLogs, getNetworkLogs, patchConsole } from './chunk-EXDFVVYA.js';
|
|
7
|
-
import { toggleVideoMask } from './chunk-
|
|
7
|
+
import { toggleVideoMask } from './chunk-7ONPNSFH.js';
|
|
8
8
|
import { createPortal } from 'react-dom';
|
|
9
9
|
import { X, ShieldCheck, Camera, Video, Film, FileText, Circle, Square, MessageSquareWarning, Check, ChevronDown } from 'lucide-react';
|
|
10
10
|
import { forwardRef, lazy, useCallback, useState, useRef, useEffect, useMemo, Suspense } from 'react';
|
|
@@ -257,7 +257,7 @@ var RecordingConsentDialog = ({ open, captureType = "video", onOpenChange, onAcc
|
|
|
257
257
|
] }) });
|
|
258
258
|
};
|
|
259
259
|
var RecordingConsentDialog_default = RecordingConsentDialog;
|
|
260
|
-
var AnnotationEditor = lazy(() => import('./AnnotationEditor-
|
|
260
|
+
var AnnotationEditor = lazy(() => import('./AnnotationEditor-XYSZQLAH.js'));
|
|
261
261
|
var getAppOrigin = () => typeof window !== "undefined" ? window.location.origin : "";
|
|
262
262
|
var toAbsoluteUrl = (value) => {
|
|
263
263
|
if (!value) return "";
|
|
@@ -284,7 +284,8 @@ var ReportIssueDialog = ({ open, onOpenChange }) => {
|
|
|
284
284
|
getCurrentUrl,
|
|
285
285
|
maxFiles,
|
|
286
286
|
maxFileSizeBytes,
|
|
287
|
-
maxRecordingSeconds
|
|
287
|
+
maxRecordingSeconds,
|
|
288
|
+
maskCapture
|
|
288
289
|
} = config;
|
|
289
290
|
const { collectConsoleLogs, collectNetworkLogs } = useReportIssueCapture();
|
|
290
291
|
const [view, setView] = useState("form");
|
|
@@ -354,8 +355,8 @@ var ReportIssueDialog = ({ open, onOpenChange }) => {
|
|
|
354
355
|
const captureScreenshot = useCallback(async () => {
|
|
355
356
|
setIsCapturing(true);
|
|
356
357
|
try {
|
|
357
|
-
const { captureMaskedScreenshot } = await import('./screenshot-
|
|
358
|
-
const dataUrl = await captureMaskedScreenshot();
|
|
358
|
+
const { captureMaskedScreenshot } = await import('./screenshot-ZBPAR3UN.js');
|
|
359
|
+
const dataUrl = await captureMaskedScreenshot(maskCapture);
|
|
359
360
|
setEditorImage(dataUrl);
|
|
360
361
|
await new Promise((resolve) => {
|
|
361
362
|
requestAnimationFrame(() => {
|
|
@@ -368,7 +369,7 @@ var ReportIssueDialog = ({ open, onOpenChange }) => {
|
|
|
368
369
|
} finally {
|
|
369
370
|
setIsCapturing(false);
|
|
370
371
|
}
|
|
371
|
-
}, [setIsCapturing, t, toast]);
|
|
372
|
+
}, [setIsCapturing, t, toast, maskCapture]);
|
|
372
373
|
const handleScreenshotConsentAccept = useCallback((consent) => {
|
|
373
374
|
screenshotConsentRef.current = consent;
|
|
374
375
|
setScreenshotConsentOpen(false);
|
|
@@ -399,8 +400,10 @@ var ReportIssueDialog = ({ open, onOpenChange }) => {
|
|
|
399
400
|
maxDurationMs: maxRecordingSeconds * 1e3
|
|
400
401
|
});
|
|
401
402
|
const handleStartRecording = useCallback(async () => {
|
|
402
|
-
|
|
403
|
-
|
|
403
|
+
if (maskCapture) {
|
|
404
|
+
setIsVideoMaskEnabled(true);
|
|
405
|
+
toggleVideoMask(true);
|
|
406
|
+
}
|
|
404
407
|
onOpenChange(false);
|
|
405
408
|
const result = await start();
|
|
406
409
|
if (result !== "started") {
|
|
@@ -409,7 +412,7 @@ var ReportIssueDialog = ({ open, onOpenChange }) => {
|
|
|
409
412
|
const message = result === "unsupported" ? t("Screen recording is only supported on Chrome and Edge browsers.") : result === "wrong-surface" ? t("Only this application tab can be recorded. Please share this tab.") : t("Screen recording permission was denied.");
|
|
410
413
|
toast.error(message);
|
|
411
414
|
}
|
|
412
|
-
}, [start, setIsVideoMaskEnabled, handleRecordingChange, onOpenChange, t, toast]);
|
|
415
|
+
}, [start, setIsVideoMaskEnabled, handleRecordingChange, onOpenChange, t, toast, maskCapture]);
|
|
413
416
|
const handleConsentAccept = useCallback((consent) => {
|
|
414
417
|
consentRef.current = consent;
|
|
415
418
|
setConsentOpen(false);
|
|
@@ -567,7 +570,15 @@ var ReportIssueDialog = ({ open, onOpenChange }) => {
|
|
|
567
570
|
label: `${t("Current page")}: ${currentUrl}`
|
|
568
571
|
}
|
|
569
572
|
),
|
|
570
|
-
|
|
573
|
+
adapter.pageInputMode === "input" ? /* @__PURE__ */ jsx(
|
|
574
|
+
Input,
|
|
575
|
+
{
|
|
576
|
+
placeholder: t("Enter the page where the issue occurred"),
|
|
577
|
+
value: customPage,
|
|
578
|
+
onChange: (e) => setCustomPage(e.target.value),
|
|
579
|
+
disabled: useCurrentPage
|
|
580
|
+
}
|
|
581
|
+
) : pageOptions.length > 0 && /* @__PURE__ */ jsx(
|
|
571
582
|
Select,
|
|
572
583
|
{
|
|
573
584
|
placeholder: t("Enter the page where the issue occurred"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tra-bilisim/report-issue",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Headless-core + React UI \"Report an Issue\" widget: masked screenshot/annotation, tab-only screen recording, console/network log capture and KVKK consent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "TRA Bilişim",
|