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

@digifinn/bug-report-widget

v0.3.7

Published

Framework-agnostic, zero-dependency bug report drawer (vanilla TS, single file).

Readme

bug-report-widget

Last updated: 2026-08-17

A framework-agnostic, zero-dependency bug report drawer. Vanilla TypeScript, one injected stylesheet, no runtime dependencies, 14.7 KB gzipped.

Docks to the edge of the viewport at full height, collects a structured report — severity, what happened (25+ characters unless a file is attached), optional email, optional evidence — and hands it to whatever transport you configure.

Sibling package to @digifinn/accessibility-widget; the two share a design system and can sit on the same page without collision.

npm i @digifinn/bug-report-widget

Quickstart

No build (script tag)

<script
  src="https://cdn.jsdelivr.net/npm/@digifinn/[email protected]/dist/bug-report-widget.iife.js"
  integrity="sha384-…"
  crossorigin="anonymous"
  data-bug-report-auto
  data-endpoint="/api/bug-reports"></script>

That's the whole integration. The widget mounts itself, renders a floating launcher, and POSTs submitted reports to your endpoint.

Loading from a third-party CDN in production: pin an exact version (as above) and add a Subresource Integrity hash, so a compromised CDN can't swap the bundle for something that reads your users' pages. Serving the file from your own origin avoids the question entirely.

Bundler (ESM)

import { BugReportWidget } from "@digifinn/bug-report-widget";

const widget = new BugReportWidget({
  appVersion: "1.4.0",
  onSubmit: async (report) => {
    const { id } = await api.fileBug(report);
    return { ticketId: id };
  },
}).mount();

Submitting a report

Two transports. onSubmit wins when both are set.

onSubmit — you own the request

new BugReportWidget({
  onSubmit: async (report) => {
    // report.attachments[].blob are real Blobs — send them however you like.
    const body = new FormData();
    body.append("payload", JSON.stringify(report));
    for (const a of report.attachments) body.append("files", a.blob, a.name);

    const res = await fetch("/api/bugs", { method: "POST", body });
    if (!res.ok) throw new Error("Could not file the report");
    return { ticketId: (await res.json()).id };
  },
});

Resolve with { ticketId } to control the reference shown on the success screen. Throw to drive the error state — the draft is preserved, so nothing the user wrote is lost, and a Try again button re-runs the submit.

Digifinn public bugs (portal + endpoint)

When portal is set, endpoint POSTs the Digifinn public contract (flat multipart, credentials: "include", no Bearer). Success must return data.reference_code — the widget never invents a DF-BUG-… id on this path.

new BugReportWidget({
  endpoint: "https://dev-api.digifinn.com/retail/presignup/api/v1/presignup/public/bugs",
  portal: "RETAIL",
  appVersion: "1.4.0",
});

turnstileSiteKey is only for presignup hosts (action bug-report). Postsignup portals omit it. An idempotency_key is minted on each open and reused on retry; close or a host navigation discards it (and navigation also clears the draft).

endpoint — generic transport (no portal)

new BugReportWidget({ endpoint: "/api/bug-reports" });

POSTs multipart/form-data with:

| Part | Content | |---|---| | report | JSON: severity, description, email, diagnostics, submittedAt, and attachment metadata | | attachments | One part per file (repeated) |

Respond with { "ticketId": "..." } or { "id": "..." }. Anything else and the widget generates a client-side reference so the user always has something to quote.

Configure one of the two. With neither, submitting surfaces the misconfiguration in the widget's error state rather than silently pretending the report was filed.

Evidence

Take screenshot uses the browser's native getDisplayMedia — no DOM-rasteriser dependency, and it captures canvas, iframes, cross-origin images and native UI exactly as the user sees them. The user picks the surface in a browser permission prompt; the widget removes its own drawer and launcher from the render tree for the frame grab, so they never appear in their own evidence. (display: none, not visibility: hidden — the latter leaves composited descendants painted in the captured frame. Both elements are position: fixed, so removing them reflows nothing.) Everything is restored afterwards, including focus and scroll position. The capture preview is a 1×1 in-document <video> — Chrome does not decode Entire Screen / other-monitor frames into a detached element, which left the share bar up and no PNG. If no frame arrives, the share is stopped instead of hanging. Chrome tab or Window in the picker is still the most reliable surface.

Where getDisplayMedia is unavailable (iOS Safari, some embedded webviews) the button is omitted and Upload file spans the row. Dismissing the picker is a no-op, not an error.

Upload file accepts png / jpeg / webp / pdf / txt / md / csv / json / mp4 / webm / mov by default, capped at 5 files / 5 MB each (25 MB for video) / 50 MB total. Each row shows a file-type tile (PNG, PDF, MP4, …) like the admin bug detail. Click the row to preview the file in a full-size window (about 92% of the viewport — the same scale as admin). With more than one file, a left list jumps to that file. Videos are last in the preview list only. Escape closes the preview first, then the drawer. While the report is sending, the drawer shows a percentage loader. All of these are configurable.

Diagnostics

Every report carries the environment facts triage needs:

{
  url, referrer, userAgent, language, timezone,
  viewport: { width, height },
  screen:   { width, height },
  devicePixelRatio,
  appVersion,        // from config
  capturedAt,        // ISO
}

Nothing else — no cookies, no storage contents, no identifiers beyond the reporter email (typed in the field, or supplied by the host when hideEmail is on).

This collection is silent. There is no in-widget notice and no user opt-out — the reporter is not told it is happening. That is a deliberate product decision, and it makes disclosure your responsibility: name these fields in your privacy policy before you ship. Set collectDiagnostics: false to turn collection off entirely, or add your own notice near whatever opens the drawer.

Config reference

| Option | Type | Default | Notes | |---|---|---|---| | onSubmit | (report) => Promise<{ticketId?} \| void> | — | Host transport. Wins over endpoint. | | endpoint | string | — | Built-in POST target. With portal, uses the Digifinn contract. | | portal | BugPortal | — | Digifinn fallback: PRESIGNUP / RETAIL / KYC / ADMIN / SELLER / BROKER / PARTNER. | | turnstileSiteKey | string | — | Presignup only. Action bug-report. Dummy / placeholder keys are ignored. | | email | string | — | Prefill the optional email field, or the submitted address when hideEmail is on. | | hideEmail | boolean | false | Hide the email field. Submit uses host email / setEmail(). | | userId | string | — | Host account id. Logged-in Digifinn portals set this from /me. | | requestTimeoutMs | number | 120000 | Abort the built-in upload after this many ms. | | headers | Record<string,string> | {} | Extra headers for the generic endpoint only. Never set Content-Type. | | launcher | "icon" \| "pill" \| "tab" | "icon" | See below. | | launcherPosition | "bottom-right" \| "bottom-left" \| "top-right" \| "top-left" | "bottom-right" | Ignored by tab, which docks to drawerSide. | | showLauncher | boolean | true | Turn off to use only your own triggers. | | drawerSide | "left" \| "right" | "right" | | | zIndex | number | 2147483000 | | | severities | Severity[] | Minor, Annoying, Blocking | Replaces the defaults wholesale. The last one renders in the amber warn tone. | | minDescription | number | 25 | Visible characters required when no file is attached. A screenshot or upload waives this. Format / zero-width characters do not count. | | maxDescription | number | 8000 | Trimmed characters above which submit stays locked. | | collectDiagnostics | boolean | true | Host-level switch. There is no user-facing toggle — see Diagnostics. | | appVersion | string \| null | null | Surfaced in diagnostics. | | enableScreenshot | boolean | true | Also requires browser support. | | maxFiles / maxFileSize / maxVideoFileSize / maxTotalSize | number | 5 / 5 MB / 25 MB / 50 MB | | | acceptTypes | string | png/jpeg/webp/pdf/txt/md/csv/json/mp4/webm/mov | accept-style list. | | storageKey | string | "digifinn-bug-report" | localStorage key for the draft. | | labels | Partial<Record<StringKey,string>> | {} | Every visible string; see below. | | icons | Partial<Record<"bug"\|"screenshot"\|"upload", string>> | {} | Raw SVG markup. | | onOpen / onClose | () => void | — | | | onSubmitSuccess / onSubmitError | (payload) => void | — | Same payloads as the events. |

Launcher variants

| Variant | Behaviour | |---|---| | icon | A 60px circle that grows into a labelled pill on hover/focus. | | pill | Always labelled. | | tab | Vertical, docked to the drawer's edge. |

Or bring your own trigger — showLauncher: false plus either widget.attachLauncher(el) or a data-bug-report-launcher attribute on any element. Every registered trigger keeps aria-expanded / aria-haspopup / aria-controls in sync.

API & events

widget.mount() / unmount()
widget.open() / close() / toggle()
widget.submit()                      // programmatic; resolves when the transport settles
widget.getDraft() / getAttachments()
widget.attachLauncher(elOrSelector)  // returns a detach function
widget.reset()                       // discard the draft and any evidence
widget.setEmail(email)               // host reporter; no remount (use with hideEmail)
widget.setUserId(userId)             // host account id from session /me; no remount
widget.updateConfig({ ... })
widget.on(event, handler)            // returns an unsubscribe function

Events: mount, unmount, open, close, draftChange, submitStart, submitSuccess, submitError, reset, configChange.

Theming

Every colour, radius and duration is a CSS custom property on :root. Override what you need:

:root {
  --br-accent: #0e8a63;
  --br-accent-strong: #0c7a57;   /* accent where it meets white text */
  --br-drawer-width: 420px;
  --br-font: "Figtree", system-ui, sans-serif;
}

--br-accent-strong exists because the brand green reaches only 4.34:1 against white — just under WCAG AA for body-size text. Filled buttons with white labels and accent-coloured text on white use the 2%-darker step (5.33:1); tints, borders and icon fills keep the exact brand green. If you re-theme, keep that distinction or re-check contrast.

Accessibility

role="dialog" + aria-modal, inert while closed, focus trapped on Tab, Esc to dismiss (handled at the document, so it works even when focus has left the drawer; an open attachment preview takes Escape first), focus returned to the invoking trigger, and focus moved to Try again when a submit fails. Chips are real radiogroups with roving tabindex and arrow-key navigation; attachment and validation changes are announced through a polite live region. Motion respects prefers-reduced-motion.

The Playwright suite runs @axe-core/playwright against the launcher, the open drawer, a filled form with attachments, and the success screen, gating on zero WCAG 2.1 AA violations.

Host isolation

The widget injects one stylesheet into document.head. It deliberately does not import Tailwind: Preflight is emitted as bare element selectors (*, button, h1, a, ul, img) which would silently restyle the host page the moment the sheet lands. Instead the reset is hand-written and scoped to .br-* classes only — nothing in the sheet can match host content, and :root carries custom properties and nothing else. Both invariants are asserted in the unit tests, and an e2e spec snapshots a host page's computed styles before and after mount to prove nothing moved.

Browser support

Last 2 versions of Chrome, Edge, Firefox and Safari. Screenshot capture additionally requires getDisplayMedia; everything else degrades cleanly without it.

Examples

  • examples/plain-html/iife.html — script tag, with a live launcher-variant switcher
  • examples/plain-html/auto-init.html — zero host JavaScript
  • examples/plain-html/esm.html — ES module import with event listeners
  • examples/react/App.tsxuseEffect mount/unmount
  • examples/nextjs/BugReportClient.tsx — App Router client component

Run pnpm build first — the examples load dist/ by relative path.

Development

pnpm install
pnpm build        # inline CSS, then four bundles
pnpm typecheck    # src + tests (tsup does not typecheck)
pnpm test         # vitest, 237 unit tests
pnpm e2e          # playwright, 45 specs incl. the axe AA gate
pnpm e2e:capture  # screen-capture specs — needs a HEADED browser
pnpm size         # gzip budget gate (30 KB)
pnpm typecheck:examples

The design source of truth is docs/design-reference.html.

Publish (npm)

The package is public: @digifinn/bug-report-widget. Digifinn portals install it from the registry — not a vendor/*.tgz.

npm login
npm version patch
npm publish --access public
git push && git push --tags

prepublishOnly runs tests and the production build. After publish, portal lockfiles still pin the old version until someone runs npm update @digifinn/bug-report-widget (or npm install @digifinn/bug-report-widget@latest).

Digifinn portals

Product, QA, and backend contract (submit URL, Turnstile, admin inbox, error catalog): BUG-REPORTING.md.

Intended install after this version is on npm:

"@digifinn/bug-report-widget": "^0.3.0"

Same pattern as @digifinn/accessibility-widget. After publish, run npm update @digifinn/bug-report-widget so the lockfile moves.

License

MIT