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

@locksyk/webview-debug-panel

v0.2.0

Published

Devtools stand-in for embedded webviews (Teams tabs, Electron, kiosks): a floating panel recording fetch exchanges with Symfony profiler links, plus captured console output and uncaught errors. Vue 3.

Downloads

306

Readme

@locksyk/webview-debug-panel

Devtools for places that have none: embedded webviews (a Microsoft Teams tab, Electron, a kiosk). A floating bug button opens a drawer with two tabs - every recorded fetch exchange (method, path, status, timing, clipped request/response bodies, and a link to the request's Symfony profiler entry when the backend sends X-Debug-Token-Link), and captured console output plus uncaught errors.

// main.ts - before your router mounts (it may strip the query string)
import '@locksyk/webview-debug-panel/style.css'
import { detectDebugFlag } from '@locksyk/webview-debug-panel'
detectDebugFlag()

The stylesheet is a separate import because the package ships built: without it the panel renders, and renders unstyled.

<!-- App.vue -->
<script setup lang="ts">
import { DebugPanel } from '@locksyk/webview-debug-panel'
</script>
<template>
  <DebugPanel /> <!-- renders nothing unless debug mode is on -->
</template>
// your fetch wrapper
import { autoEnableFromResponse, isRecording, recordApiExchange } from '@locksyk/webview-debug-panel'

autoEnableFromResponse(response) // dev backend (X-Debug-Token) switches the panel on
if (isRecording()) {
  void recordApiExchange(method, path, startedAt, response.clone(), requestBody)
}

Enable explicitly by opening any page with ?debug=1 (persisted for the session; ?debug=0 turns it off), or automatically when the backend advertises the Symfony profiler.

Two switches: recording and debug mode

Capture and the devtools UI are separate, because a user reporting a bug needs the first without the second.

  • Recording (isRecording(), debugState.recording) is capture. startRecording() turns it on for everyone - call it at boot if users are to report bugs - and stopRecording() turns it off again.
  • Debug mode (isDebugEnabled(), debugState.enabled) is the devtools UI: ?debug=1, or a dev backend advertising the profiler. It implies recording, so a developer needs neither call.

Recording an app asked for is pinned: the panel's Off switch leaves debug mode without stopping capture, so a bug report started earlier still has its contents. Debug mode's own recording does stop there.

Nothing is captured while recording is off - recordApiExchange returns immediately, so the isRecording() guard above is an optimisation (it skips response.clone()) rather than a correctness requirement.

Styling uses CSS custom properties with neutral fallbacks - set --white, --mist, --hairline, --tide to blend with your design system.

Customising the panel

Nothing below is required: <DebugPanel /> on its own is the two-tab drawer described above.

The button. icon is the character on the fab, and it is reactive - change it whenever you like. When a character is not enough, the #fab slot replaces its content entirely and receives { errorCount, open }. The error badge stays on top either way.

<DebugPanel :icon="hasErrors ? '\u{1F525}' : '\u{1F41E}'" />

<DebugPanel>
  <template #fab="{ errorCount }">
    <BugIcon :alert="errorCount > 0" />
  </template>
</DebugPanel>

The tabs. tabs is the whole tab set, in order. The ids api, console and bug render the built-in views; any other id renders the matching #tab-<id> slot, which receives { tab, close }. Listing an id is what puts it in the drawer, so you can reorder the built-ins, drop them, or override one with a slot of the same name. A tab may be a bare string or { id, label, count } - count is the number in brackets after the label, and the built-ins count their own entries unless you say otherwise. The #actions slot adds buttons to the header, next to Clear and Off.

<DebugPanel :tabs="[{ id: 'timings', label: 'Timings' }, 'bug', 'api', 'console']">
  <template #tab-timings="{ close }">
    <MyTimings @done="close" />
  </template>
  <template #actions>
    <button class="quiet" @click="copySnapshot">Copy</button>
  </template>
</DebugPanel>

Somebody else's shell. ApiLogView and ConsoleLogView are exported on their own, so the logs can sit inside your own dialog with no DebugPanel in sight. Neither scrolls by itself - the container decides how tall it is. openDebugPanel(tab?) and closeDebugPanel() drive the built-in drawer from anywhere (a menu item, a keyboard shortcut), and debugState.open / debugState.tab are readable if you want to mirror its state.

<MyModal title="What went wrong">
  <BugReportForm />
  <ApiLogView />
</MyModal>

Modals

The panel teleports to <body>, so a modal that inerts the page by walking document.body.children meets one element rather than two. Whether it skips that element is up to your modal, and every implementation names the exception differently - so this package supplies no name of its own. hostAttrs puts whatever yours looks for on the host:

<!-- whatever attribute your modal leaves un-inerted -->
<DebugPanel :host-attrs="{ 'data-modal-persistent': '' }" />

That matters because the moment a bug report is most wanted is while a dialog is showing the error, not after dismissing it. The host stacks at var(--z-debug-panel, 200); set that property, or pass :z-index="...", when 200 is not above your overlays.

Nothing is imported in either direction: an attribute you name and a z-index you choose are the whole agreement, which is what lets this package cooperate with a modal it has never heard of.

teleport controls where the panel mounts: true (the default) for <body>, a selector string for somewhere else, false to leave it where it is written - handy in tests, and for an app that positions the panel inside its own layout.

Report a bug

bug is the third built-in tab: a box for what went wrong, screenshots (a file picker, which is a phone's camera roll; a clipboard paste, which is where a macOS screenshot lands; or a drop), and the recording. Where a report goes is the one thing this package cannot know, so submitReport is the whole integration - an endpoint, an issue tracker, a chat webhook, your choice. Return a string to show the person a reference; throw to show them why it failed.

<DebugPanel always :tabs="['bug', 'api', 'console']" :submit-report="fileReport" />
async function fileReport({ note, screenshots, snapshot, text }: BugReport) {
  const form = new FormData()
  form.set('note', note)
  form.set('snapshot', JSON.stringify(snapshot))
  form.set('text', text)
  screenshots.forEach((shot) => form.append('screenshots[]', shot))

  const { reference } = await postSomehow('/api/bug_reports', form)
  return reference // shown to the reporter
}

A tab listed without a handler says so rather than pretending: a Send button that discards is worse than no tab.

The draft outlives the panel being closed - somebody writing a report closes it to re-read the error, or to go and take the screenshot - and the sentence outlives a reload too. The screenshots cannot: a File is not serialisable.

BugReportView is exported on its own if you would rather build the tab yourself, and the pieces underneath are public as well: snapshotDebug(context?, { redact }) returns everything recorded as plain JSON, and formatDebugSnapshot() renders it as text for a ticket or a chat message.

What a report must not carry

A recording of an app's API traffic contains what that traffic is authenticated with: a session token in the response that minted it, a single-use enrolment token in the request that spends it, a WebAuthn challenge, whatever this particular app calls its own.

So snapshotDebug() redacts - on the way out, not on the way in. Showing a token in the drawer is no worse than a browser's network tab, which is what this package stands in for, and an API tab that lies about what was sent is a poor devtool. Sending it somewhere is the different act, and that is where the scrubbing happens.

Values are replaced when their key looks secret (token matches access_token, X-Auth-Token, enrolmentToken - case and separators ignored), in JSON at any depth, in query strings, in form bodies, and for bare JWTs anywhere. A body too long to have survived as parseable JSON falls back to a text pass, because a clipped body is exactly when a token would otherwise walk out.

None of it is a security boundary. It is pattern-matching over somebody else's JSON, and a secret under a name nobody thought of goes straight through - which is why the app gets a say:

configureRedaction({
  // Your own names, added to the built-in list.
  keys: ['employeeNumber'],
  // The last word on an exchange. Return it changed, or null to keep it
  // out of reports entirely.
  exchange: (e) => (e.path.startsWith('/api/keys') ? null : e),
})

The hook is handed what is already redacted, never the original, so an app's rule can only ever tighten this.

Two escape hatches, in opposite directions:

  • redactReport: false on the panel (or snapshotDebug(ctx, { redact: false })) sends the recording as it happened - reasonable when the report goes somewhere already trusted with the traffic, such as the server that served it.
  • configureRedaction({ atRecord: true }) scrubs as exchanges are recorded, so the buffer never holds the secret at all. The drawer is then redacted too, and the original is gone for good - including for redact: false, which has nothing left to hand back.

Users reporting bugs are not in debug mode, so the fab would normally be hidden from them and there would be nothing recorded to attach. startRecording() at boot fixes the second half, and always shows the button regardless of debug mode; pair it with a tab set that only grows when debug mode is on.

// main.ts
detectDebugFlag()
startRecording() // everyone, so a report has something to attach
<DebugPanel
  always
  icon="!"
  :tabs="isDebugEnabled() ? ['bug', 'api', 'console'] : ['bug']"
  :submit-report="fileReport"
/>

The Off switch (which turns debug mode off for the session) only appears when debug mode is on, so an always panel shows it to developers and hides it from everyone else.

Building

npm run build produces dist/: an ES module, the stylesheet, type declarations and sourcemaps. That directory is what the package exports, so a checkout consumed through a file: link has to be built before the app that links it - npm run watch while working on both. The sources ship alongside it, so a stack trace through the panel lands in readable code, which is the least a debugging tool can do.

vue is a peer dependency and stays external.

License

GPL-2.0-only.