npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tra-bilisim/report-issue

v0.2.18

Published

Headless-core + React UI "Report an Issue" widget: masked screenshot/annotation, tab-only screen recording, console/network log capture and KVKK consent.

Readme

@tra-bilisim/report-issue

Self-contained "Report an Issue" widget for React apps. Zero Tailwind, zero design-system coupling — bring your own submit, everything else has a default.

  • 📸 Masked screenshot with an annotation editor (arrows, boxes, text, highlighter, crop)
  • 🎥 Browser-tab screen recording (current-tab-focused Chrome/Edge picker, hard max duration)
  • 🕵️ Console + network log capture (ring buffer)
  • 🔒 KVKK explicit-consent gate before any capture, with versioned consent records
  • 🎨 Standalone Radix + scoped CSS (.rpi-*, --rpi-*), dark mode via a .dark class

Install

Current package version: 0.2.18.

npm install @tra-bilisim/report-issue

Peer deps: react and react-dom (>=18). axios is only needed if you use the axios network-logging adapter.

Console and global error capture

With captureConsole enabled (the default), the provider records console.log, console.info, console.warn, console.error, uncaught page errors (window.onerror), and unhandled promise rejections. The report stores the last 200 entries after the provider mounts; access tokens and Bearer values are redacted before storage.

Browser DevTools diagnostics (such as forced-reflow warnings) and browser extension messages run outside the page context, so they cannot be collected by the widget.

Usage

import {
  ReportIssueProvider,
  FloatingReportButton,
  type ReportIssueAdapter,
} from '@tra-bilisim/report-issue';
import '@tra-bilisim/report-issue/styles.css';

const adapter: ReportIssueAdapter = {
  // The ONLY required field. Persist the report however you like.
  submit: async (payload, formData) => {
    const res = await fetch('/api/report-issue', { method: 'POST', body: formData });
    return { ok: res.ok };
  },

  // Everything below is optional.
  getCategories: async () => [{ label: 'UI', value: 1 }, { label: 'Data', value: 2 }],
  getMetadata: () => ({ userId: currentUser.id, roles: currentUser.roles }),
  getCurrentUrl: () => window.location.href,
  pageOptions: [{ value: 'https://app/x', label: '/x' }],
  pageInputMode: 'select',          // 'select' (default, uses pageOptions) | 'input' (free-text URL)
  maskCapture: true,                 // default true (KVKK-safe); set false for unmasked screenshots/recordings
  screenshotCapture: { mode: 'bounded', maxViewportHeights: 2 }, // default: cap long pages at 2x viewport height
  environmentResolver: url => (url.includes('localhost') ? 'Locale' : 'Prod'),
  t: key => i18n.translate(key),   // English keys are the default fallback text
  locale: 'tr',
  toast: { success: msg => toast.success(msg), error: msg => toast.error(msg) },
};

function App() {
  return (
    <ReportIssueProvider config={adapter}>
      <YourApp />
      <FloatingReportButton />
    </ReportIssueProvider>
  );
}

Optional automatic capture

ReportIssueProvider keeps capture disabled by default. Set autoStartCapture when opening the report dialog should immediately begin one of the consent-first workflows:

<ReportIssueProvider config={adapter} autoStartCapture="screenshot">
  <YourApp />
  <FloatingReportButton />
</ReportIssueProvider>

Allowed values are 'off' (the default), 'screenshot', and 'video'. Both automatic modes show the relevant KVKK consent text before starting. Screenshot mode then opens the annotation editor; video mode opens the browser capture picker and temporarily hides the report dialog while recording.

Session consent for quick capture

By default, captureConsentMode="perCapture" displays the consent text before every screenshot and recording. To keep the consent-first flow while allowing quick repeat capture during the same browser tab session, use "session":

import { clearSessionCaptureConsent } from '@tra-bilisim/report-issue';

<ReportIssueProvider
  config={adapter}
  autoStartCapture="screenshot"
  captureConsentMode="session"
  captureConsentSessionKey={String(currentUser.id)}
>
  <YourApp />
  <FloatingReportButton />
</ReportIssueProvider>

The first screenshot and the first video recording each show their own consent text. After acceptance, only that capture type can start directly for the same session, account scope, and consent-text version. The package stores only the accepted flag, type, version, and timestamp in sessionStorage; it sends the same consent record with every submitted report that contains a capture.

Pass a non-sensitive, stable account identifier as captureConsentSessionKey when the application allows account switching in one browser tab. On logout or when the user withdraws their preference, clear the scope explicitly:

clearSessionCaptureConsent(String(currentUser.id));

Browser screen-recording APIs require a user gesture. A click on the floating Report an Issue button or on Record Video preserves that gesture and can start a session-approved recording directly. If a host opens the dialog programmatically, the user must click Record Video once.

Screen-recording picker behavior

The recorder is configured for the Chrome/Edge flow used by this package: preferCurrentTab focuses the picker on the application tab and monitor surfaces are excluded. In the supported browser versions targeted by the package, the picker presents the tab in which the project is open rather than highlighting it among unrelated tabs. As a defense-in-depth check, the package rejects a returned screen or window surface and starts recording only for a browser-tab surface.

The capture picker is browser-owned UI. Hosts should test this behavior on the exact Chrome/Edge versions they support after browser upgrades.

Dark mode

The widget is light by default and switches to dark when a .dark class is present on <html>, <body>, or any ancestor of the widget (the dialog/floating button render into a portal on document.body, so the class needs to be on document.body itself or higher — not on some inner <div> further down).

document.documentElement.classList.add('dark');    // dark
document.documentElement.classList.remove('dark'); // light

There is no automatic prefers-color-scheme detection — the widget never guesses from the OS/browser setting. This is intentional: most hosts already run a .dark-class-based theme system (Tailwind's darkMode: 'class', shadcn/ui, etc.) where "no .dark class" deliberately means light, independent of the visitor's OS setting. Auto-following the OS would silently fight that and show dark even when the host's own UI is light. If your toggle logic only ever adds .dark conditionally and calls classList.remove('dark') for light — as most setups do — you don't need to do anything else; just make sure .dark is never left on <html>/<body> when the app itself is in light mode.

If you need to force light inside a .dark-classed ancestor you don't control (rare — e.g. a 3rd-party wrapper always adds .dark), add an explicit .light class; it wins over .dark.

Localization (i18n)

The widget never bundles its own i18n library — it calls t(key) from your adapter for every user-facing string. The key itself is the English fallback text, so if you omit t, the UI renders in English as-is.

Wire it to whatever i18n system you already use:

const adapter: ReportIssueAdapter = {
  submit: ...,
  t: key => i18n.translate(key),   // e.g. react-i18next: (key) => i18next.t(key)
  locale: 'tr',
};

Below is the complete list of keys used by the widget, with Turkish translations. Drop this straight into your translation resource file (adjust the shape to whatever your i18n.translate/t implementation expects — this is a plain key → value map):

{
  // Annotation editor toolbar
  'Select': 'Seç',
  'Draw': 'Çizim',
  'Line': 'Çizgi',
  'Arrow': 'Ok',
  'Rectangle': 'Dikdörtgen',
  'Circle': 'Daire',
  'Text': 'Metin',
  'Highlight': 'Vurgula',
  'Crop': 'Kırp',
  'Thickness': 'Kalınlık',
  'Undo': 'Geri al',
  'Redo': 'Yinele',
  'Cancel': 'İptal',
  'Confirm': 'Onayla',

  // Floating button / dialog title
  'Report an Issue': 'Sorun Bildir',
  'Describe the problem and attach screenshots or a screen recording.': 'Sorunu açıklayın ve ekran görüntüsü veya ekran kaydı ekleyin.',

  // Attachments
  'Attachments': 'Ekler',
  'Take a Screenshot': 'Ekran Görüntüsü Al',
  'Record Video': 'Video Kaydet',
  '{name} exceeds the size limit.': '{name} boyut sınırını aşıyor.',
  'You can attach at most {count} files.': 'En fazla {count} dosya ekleyebilirsiniz.',
  'Failed to capture screenshot.': 'Ekran görüntüsü alınamadı.',

  // Screen recording
  'Screen recording is only supported on Chrome and Edge browsers.': 'Ekran kaydı yalnızca Chrome ve Edge tarayıcılarında desteklenir.',
  'Only this application tab can be recorded. Please share this tab.': 'Yalnızca bu uygulama sekmesi kaydedilebilir. Lütfen bu sekmeyi paylaşın.',
  'Screen recording permission was denied.': 'Ekran kaydı izni reddedildi.',
  'Stop': 'Durdur',

  // Recording / screenshot consent dialog (KVKK)
  'Screenshot Consent': 'Ekran Görüntüsü Onayı',
  'Screen Recording Consent': 'Ekran Kaydı Onayı',
  '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.',
  '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.',
  'To diagnose the issue you reported, a screenshot of this application screen will be captured. On-screen text and supported form values are masked before capture; visual media and embedded content are not automatically redacted.': 'Bildirdiğiniz sorunu teşhis edebilmek için bu uygulama ekranının bir görüntüsü alınacaktır. Ekrandaki metinler ve desteklenen form değerleri görüntü alınmadan önce maskelenir; görsel medya ve gömülü içerikler otomatik olarak sansürlenmez.',
  'To diagnose the issue you reported, a short screen recording of only this tab will be captured. On-screen text and supported form values are masked during recording; visual media and embedded content are not automatically redacted. You may temporarily reveal masked text and form values.': 'Bildirdiğiniz sorunu teşhis edebilmek için yalnızca bu sekmenin kısa bir ekran kaydı alınacaktır. Ekrandaki metinler ve desteklenen form değerleri kayıt sırasında maskelenir; görsel medya ve gömülü içerikler otomatik olarak sansürlenmez. Maskelenmiş metinleri ve form değerlerini geçici olarak görünür hale getirebilirsiniz.',
  'Data that will be collected:': 'Toplanacak veriler:',
  'A screenshot with supported text and form values masked': 'Desteklenen metin ve form değerleri maskelenmiş bir ekran görüntüsü',
  'A screen recording with supported text and form values masked, limited to this application tab': 'Yalnızca bu uygulama sekmesiyle sınırlı, desteklenen metin ve form değerleri maskelenmiş bir ekran kaydı',
  'Console and network logs of this session': 'Bu oturuma ait konsol ve ağ kayıtları',
  'Session metadata (user, roles, browser, page)': 'Oturum meta verileri (kullanıcı, roller, tarayıcı, sayfa)',
  '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.',
  '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.',
  'This field is required, please give your consent to continue.': 'Devam etmek için bu alanı onaylamanız gerekmektedir.',
  'Take Screenshot': 'Ekran Görüntüsü Al',
  'Start Recording': 'Kaydı Başlat',

  // Page field
  'The page where the issue occurred': 'Sorunun oluştuğu sayfa',
  'Current page': 'Geçerli sayfa',
  'Enter the page where the issue occurred': 'Sorunun oluştuğu sayfayı girin',
  'Please specify the page.': 'Lütfen sayfayı belirtin.',

  // Category / description
  'Category': 'Kategori',
  'optional': 'opsiyonel',
  'Description': 'Açıklama',
  'Describe what happened...': 'Ne olduğunu açıklayın...',

  // Select control
  'Search': 'Ara',
  'No results': 'Sonuç bulunamadı',

  // Submit
  'Submit': 'Gönder',
  'An error occurred!': 'Bir hata oluştu!',
  'Please try again in {minutes} minutes.': 'Lütfen {minutes} dakika sonra tekrar deneyin.',
  'Please try again in {seconds} seconds.': 'Lütfen {seconds} saniye sonra tekrar deneyin.',
}

{name} and {count} are literal placeholders — the widget does simple .replace('{name}', ...) / .replace('{count}', ...) substitution itself, so keep those tokens verbatim in your translated value.

Network log capture (optional)

Native Fetch adapter

attachFetchNetworkLogging(options?) wraps the global native fetch once and feeds the shared report-issue log store. It has no axios dependency and covers both centralized fetch wrappers and direct fetch calls in the host:

import { attachFetchNetworkLogging } from '@tra-bilisim/report-issue/adapters/fetch';

// Bootstrap/module scope only. Do not attach inside a React component.
const detach = attachFetchNetworkLogging({
  // The default already removes the query string and fragment. This host rule
  // additionally masks numeric/GUID-like path segments when necessary.
  sanitizeUrl: url => url
    .replace(/[?#].*$/, '')
    .replace(/\/[0-9a-f-]{16,}(?=\/|$)/gi, '/[REDACTED]'),

  // Optional: omit endpoints that should not enter a diagnostic report.
  shouldLog: ({ url }) => !url.includes('/health'),
});

// Useful in tests/HMR. Repeated attaches are reference-counted.
detach();

By default the adapter:

  • records all fetch requests but strips query strings and fragments from logged URLs;
  • injects X-Request-Tracking-Id only into relative or same-origin requests;
  • does not inject a custom header into third-party calls, avoiding accidental CORS preflights;
  • never records request bodies, response bodies, cookies, authorization headers, or arbitrary headers;
  • does not read/clone response bodies unless the host explicitly supplies getMessage;
  • preserves the original Response and rethrows the original fetch rejection.

For dependency injection or tests, createInstrumentedFetch(fetchImplementation, options?) returns the same fetch-compatible wrapper without changing globalThis.fetch.

import { createInstrumentedFetch } from '@tra-bilisim/report-issue/adapters/fetch';

export const loggedFetch = createInstrumentedFetch(fetch, {
  shouldInjectTrackingHeader: ({ url }) => url.startsWith('/api/'),
  isErrorResponse: response => !response.ok,
});

XMLHttpRequest traffic is separate from native fetch and is therefore not intercepted. Hosts with centralized XHR upload helpers should instrument those helpers with the core primitives shown below.

Axios adapter

attachAxiosNetworkLogging(instance, options?) installs request/response interceptors on your axios instance that feed the shared report-issue log store. The captured requests are attached to a report as networkLogs automatically.

Call it once, at module scope where you create your axios instance — not inside a React component (that would re-attach on every render):

// src/api/http.ts
import axios from 'axios';
import { attachAxiosNetworkLogging } from '@tra-bilisim/report-issue/adapters/axios';

export const http = axios.create({ baseURL: '/api' });

// One call — done. Every request through `http` is now logged.
attachAxiosNetworkLogging(http);

attachAxiosNetworkLogging returns a detach function that removes the interceptors (useful in tests or HMR):

const detach = attachAxiosNetworkLogging(http);
// ...later
detach();

Options

Both options are optional — the defaults cover common API shapes.

attachAxiosNetworkLogging(http, {
  // Pull a human-readable message out of the response body for the log entry.
  // Default looks for { message | Message | errorMessage | ErrorMessage }.
  getMessage: data => (data as any)?.error?.detail,

  // Decide whether a response counts as an error in the log.
  // Default: any status < 200 or >= 300.
  isErrorResponse: (status, data) =>
    status >= 400 || (data as any)?.success === false,
});

Request correlation header

Each logged request is tagged with an X-Request-Tracking-Id header. If your backend already sets/echoes this header, the adapter reuses it; otherwise it generates one (rpi_<time>_<rand>). This lets you cross-reference a captured request with your server logs.

Socket log capture (optional)

The package intentionally does not depend on a real-time transport. Hosts can record safe socket-event summaries through the headless core; do not include the raw message payload:

import { pushSocketLog } from '@tra-bilisim/report-issue/core';

connection.on('OrderUpdated', message => {
  pushSocketLog({
    event: 'message-received',
    hub: '/OrderHub',
    method: 'OrderUpdated',
    trackingId: message.trackingId,
  });
  // update local state
});

High-frequency market frames should be filtered by the host before calling the core. For example, Kointra omits TICKER and BOOK so the 100-entry socket ring buffer remains useful for lifecycle, permission, settings, and error events:

if (messageType !== 'TICKER' && messageType !== 'BOOK') {
  pushSocketLog({
    event: 'message-received',
    hub: '/t/adminws', // never include a JWT-bearing socket URL
    method: messageType,
  });
}

For an exact HTTP-to-socket link, copy the originating HTTP request's X-Request-Tracking-Id into the hub message as trackingId (or an equivalent correlationId) on the server. The value is optional; without it, reports still show a chronological network/socket timeline.

Custom transport primitives

Using XHR or another custom client? Call the core primitives directly from @tra-bilisim/report-issue/core:

import {
  beginNetworkRequest,
  completeNetworkRequest,
  failNetworkRequest,
} from '@tra-bilisim/report-issue/core';

async function loggedFetch(url: string, init?: RequestInit) {
  const id = crypto.randomUUID();
  beginNetworkRequest(id, init?.method ?? 'GET', url);
  try {
    const res = await fetch(url, init);
    completeNetworkRequest(id, res.status, !res.ok);
    return res;
  } catch (err) {
    failNetworkRequest(id, undefined, (err as Error).message);
    throw err;
  }
}

Wire contract (submit)

submit(payload, formData) receives both a structured payload and a ready-to-POST FormData. The FormData fields are the canonical contract your backend consumes:

| Field | Type | Notes | |---|---|---| | files | File[] (repeated) | screenshots / recordings / uploads | | page | string | absolute URL | | environment | string | from environmentResolver | | categoryId | string | omitted when no category | | description | string | omitted when empty | | consoleLogs | JSON string | ConsoleEntry[] | | networkLogs | JSON string | NetworkEntry[] | | socketLogs | JSON string | host-recorded SocketEntry[]; raw message payloads must not be included | | visitedPages | JSON string | optional VisitedPage[]; omitted when the adapter does not provide entries | | attachmentFormat | string | optional 'json' or 'html'; omitted when the default 'both' is used | | recordingConsent | JSON string | only when a recording is attached | | screenshotConsent | JSON string | only when a screenshot is attached | | metadata | JSON string | ReportMetadata (user, roles, UA, resolution, ts) |

submit must resolve to { ok: boolean; message?: string | null; retryAfterSeconds?: number | null }.

Diagnostic attachment format

Generated console, network, socket, visited-page, and activity-timeline attachments default to both JSON and HTML. Set attachmentFormat to limit them:

const adapter: ReportIssueAdapter = {
  submit: async (payload, formData) => {
    const res = await fetch('/api/report-issue', { method: 'POST', body: formData });
    return { ok: res.ok };
  },
  attachmentFormat: 'json', // 'html' | 'both'
};

retryAfterSeconds is optional and only relevant for rate-limit rejections (HTTP 429): when present, the widget appends the remaining wait time to the error toast ("Too many report issue requests. Please try again later. Please try again in 10 minutes."). If the adapter rejects instead of resolving on a 429, the widget also picks the value up from the thrown error's response body or its Retry-After header.

This matches the Tra.Bilisim.ReportIssue.AspNetCore backend package's POST /api/report-issue endpoint (legacy alias: POST /api/ReportIssue/Create) — see ../backend/src/Tra.Bilisim.ReportIssue.AspNetCore/REPORT_ISSUE_INTEGRATION.md for the server side.

Visited pages

Pass the pages visited during the current browser-tab session through the optional visitedPages adapter field. Each entry contains the full URL and ISO-8601 visit time. When the array is empty or omitted, the widget does not send the field and the backend does not create visited-page attachments.

const adapter: ReportIssueAdapter = {
  submit: async (payload, formData) => {
    const res = await fetch('/api/report-issue', { method: 'POST', body: formData });
    return { ok: res.ok };
  },
  visitedPages: [
    { href: 'https://app.example.com/customers/42', visitedAt: '2026-08-26T09:00:00.000Z' },
  ],
};

Screenshot height

Long, scrollable documents are capped by default so the annotation editor does not receive an unreadably tall image or allocate a huge canvas. The default is:

const adapter: ReportIssueAdapter = {
  submit: ...,
  screenshotCapture: {
    mode: 'bounded',
    maxViewportHeights: 2,
  },
};

Modes:

  • bounded - capture from the user's current scroll position, capped by maxViewportHeights and optionally maxHeightPx.
  • viewport - capture only the visible viewport.
  • fullPage - preserve the old behavior and capture the whole document when the page itself scrolls.

For an absolute hard cap, add maxHeightPx, for example { mode: 'bounded', maxViewportHeights: 2, maxHeightPx: 2400 }.

Masking (KVKK privacy)

By default, supported page text is replaced with ●●●●●● and supported input/textarea values are hidden before a screenshot is taken or a recording starts. Masking is a data-minimization aid, not a guarantee that every sensitive pixel or value on the page is removed.

const adapter: ReportIssueAdapter = {
  submit: ...,
  maskCapture: false, // disable masking entirely — screenshots/recordings capture the real UI
};

maskCapture is a single on/off switch for both capture types (screenshot and screen recording); there's no separate flag per type. When true (default), the per-element data attributes below give you fine-grained control over what stays masked/visible within that overall mask:

To keep the normal application UI masked too (for example, to test masking without starting a screenshot or recording), set alwaysMask to true. The mask is applied when ReportIssueProvider mounts and remains active until it unmounts.

const adapter: ReportIssueAdapter = {
  submit: ...,
  alwaysMask: true,
};

alwaysMask defaults to false. It uses the same rules and exclusions as capture masking, and it is restored automatically after a screenshot or recording ends.

What is and is not masked

The built-in mask covers:

  • ordinary DOM text nodes, including text added while a recording is in progress;
  • non-empty input and textarea values, except for the control types listed below;
  • number inputs, which are temporarily rendered as text controls so their values can be obscured.

The built-in mask does not inspect or redact:

  • pixels or content inside img, picture, inline/external svg, canvas, or video elements;
  • CSS background images, generated ::before/::after content, or text drawn by charting and canvas libraries;
  • content inside iframes or shadow roots;
  • the visible state/value of hidden, checkbox, radio, file, button, submit, reset, image, range, color, and password input controls;
  • content deliberately opted out with data-report-mask-ignore, data-report-mask-text-ignore, or matching maskSelectors entries.

Review pages that contain profile photos, uploaded document previews, charts, signatures, QR codes, account data rendered to canvas/SVG, or third-party iframe content. Hide or replace those elements before capture, exclude them from screenshots with data-report-ignore-capture, or disable the capture feature for that page. Test screenshots and recordings against representative sensitive data; the package cannot determine whether arbitrary visual content is personal data.

Masking controls (data attributes)

  • data-report-mask-ignore — keep text in this subtree visible. For screen recording, place it directly on nested input/textarea controls whose values must also remain visible.
  • data-report-mask-text-ignore — keep text visible (still masks inputs)
  • data-report-ignore-capture — exclude from screenshots entirely
  • data-report-mask-control / data-report-mask-value — force text masking

Masking controls for third-party markup (CSS selectors)

The attributes above require you to add them to your own JSX. If the element you need to exclude is rendered by a component you don't control — a <legend> from a form library, a chart tooltip, a portal from another package — you can't attach an attribute to it. maskSelectors gives you the same four controls as CSS selectors instead, so you can target it from outside without touching its markup:

const adapter: ReportIssueAdapter = {
  submit: ...,
  maskSelectors: {
    ignore: ['legend', '.third-party-lib__tooltip'],   // == data-report-mask-ignore
    textIgnore: ['.some-lib-readonly-label'],           // == data-report-mask-text-ignore
    forceTextMask: [],                                  // == data-report-mask-control
    ignoreCapture: [],                                  // == data-report-ignore-capture
  },
};

All four arrays are optional and additive — they extend the built-in attributes, they don't replace them. Selectors are matched against the element itself when masking text and against ancestors when masking screenshot input values. During a screen recording, an ignore selector must also match an input/textarea directly to keep that control's value visible. Prefer a selector specific enough to avoid unintentionally revealing real user data elsewhere on the page.

Entry points

  • @tra-bilisim/report-issue — React provider, button, dialog, hooks, types
  • @tra-bilisim/report-issue/core — headless, framework-agnostic engine
  • @tra-bilisim/report-issue/adapters/axios — axios network-logging adapter
  • @tra-bilisim/report-issue/adapters/fetch — native fetch network-logging adapter
  • @tra-bilisim/report-issue/styles.css — required stylesheet

Build

npm install
npm run build      # tsup → dist/ (esm + d.ts + index.css)
npm run dev         # tsup --watch
npm run typecheck   # tsc --noEmit

License

MIT — see LICENSE.