@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 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
- Feed your axios instance into the shared log store with one call:
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
- attachAxiosNetworkLogging(myAxiosInstance);
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
- Or wire your own transport by calling the primitives from `@tra-bilisim/report-issue/core`:
69
- `beginNetworkRequest`, `completeNetworkRequest`, `failNetworkRequest`.
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 controls (data attributes)
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-JMQUG5Q7.js';
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(MASK_IGNORE_SELECTOR)) return true;
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(TEXT_MASK_IGNORE_SELECTOR)) return true;
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(FORCE_TEXT_MASK_SELECTOR)) return true;
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.hasAttribute("data-report-ignore-capture")) return true;
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([data-report-mask-ignore]),
137
- .report-video-mask textarea:not([data-report-mask-ignore]) {
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-ZYF6UFBB.js';
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("[data-report-ignore-capture]")) return false;
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("[data-report-ignore-capture]")) return false;
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
- applyMask();
63
- applyInputMask();
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.hasAttribute?.("data-report-ignore-capture") || el.getAttribute?.("data-slot") === "dialog-overlay",
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
- 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-DmS4DxOf.js';
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 masked PNG screenshot of the whole document body and returns a
10
- * data URL. Applies KVKK text/input masking and hides any element marked with
11
- * `data-report-ignore-capture` (the dialog itself) during the capture, then
12
- * restores everything before resolving. Framework-agnostic.
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-KY2IRP36.js';
4
- export { applyInputMask, applyMask, removeMask, toggleVideoMask } from './chunk-ZYF6UFBB.js';
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-DmS4DxOf.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-DmS4DxOf.js';
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-JMQUG5Q7.js';
4
- export { ReportIssueProvider, useReportIssue, useReportIssueConfig } from './chunk-JMQUG5Q7.js';
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-ZYF6UFBB.js';
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-ILMYBTOG.js'));
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-BQPXCSLD.js');
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
- setIsVideoMaskEnabled(true);
403
- toggleVideoMask(true);
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
- pageOptions.length > 0 && /* @__PURE__ */ jsx(
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"),
@@ -0,0 +1,2 @@
1
+ export { captureMaskedScreenshot } from './chunk-DCNBXNWY.js';
2
+ import './chunk-7ONPNSFH.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tra-bilisim/report-issue",
3
- "version": "0.1.0",
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",
@@ -1,2 +0,0 @@
1
- export { captureMaskedScreenshot } from './chunk-KY2IRP36.js';
2
- import './chunk-ZYF6UFBB.js';