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

@crosspane/agent

v0.8.2

Published

Tiny in-page debugging agent — capture console/errors/network in webviews and locked-down environments, export or live-stream to the crosspane dashboard

Readme

@crosspane/agent

Tiny in-page debugging agent for the places devtools can't reach — in-app webviews, in-app browsers, kiosks, and security-hardened builds where remote inspectors are blocked outright.

Captures console output, uncaught errors, unhandled rejections, fetch/XHR and navigations. Stream it live to the crosspane dashboard, or export a capture file when the network isn't an option.

Zero dependencies. ~5 KB gzipped.

Install

npm install @crosspane/agent

Use

Call it once, as early as your app can run code. Anything before the call is not hooked — requests are partly recovered from resource timing afterwards, but console logs from before the call are gone for good.

| Your setup | File | Where in it | |---|---|---| | Next.js (App Router) | a new app/crosspane.tsx with 'use client', imported from app/layout.tsx | top level of that module — see below | | Next.js (Pages Router) | pages/_app.tsx | top of the file, outside the component | | Vite (React/Vue/Svelte/Solid) | src/main.ts / src/main.tsx | first lines, before createApp / createRoot | | Create React App | src/index.tsx | first lines, before createRoot | | SvelteKit | src/routes/+layout.svelte | inside <script>, guarded by browser | | Astro | your base layout's <script> | before other scripts | | No bundler | a <script> in <head> | before your other scripts — see below |

// src/main.tsx — Vite, CRA. First lines of the file.
import { initCrosspane } from '@crosspane/agent'

initCrosspane({
  label: 'checkout webview',
  serverUrl: import.meta.env.VITE_CROSSPANE_URL,   // omit entirely on localhost
})

// ...your own imports and createRoot() below

Next.js App Router needs one extra step. app/layout.tsx is a server component: calling initCrosspane() there runs it on the server, where there is no page to hook — it does not crash, it silently does nothing, which is harder to notice than a crash.

// app/crosspane.tsx
'use client'
import { initCrosspane } from '@crosspane/agent'

// Top level, not inside the component and not in useEffect — a module runs as soon as the
// client bundle loads, while useEffect waits for React to mount and misses your early logs.
initCrosspane({
  label: 'checkout webview',
  serverUrl: process.env.NEXT_PUBLIC_CROSSPANE_URL,
})

export function Crosspane() {
  return null
}

Then render <Crosspane /> once anywhere inside <body> in app/layout.tsx. Calling initCrosspane() twice is safe — the second call returns the same agent rather than hooking anything again.

const agent = initCrosspane({ label: 'checkout webview' })

// Offline mode: wire this to a debug gesture or hidden QA menu
agent.exportFile() // downloads <label>.crosspane.json
await agent.copyCapture() // or put it on the clipboard — see the crosspane README,
                          // "Getting captures off a locked device"

Then run the hub and open the dashboard:

npx crosspane --host 0.0.0.0

Drop the exported .crosspane.json into the dashboard to replay it — same UI, no server connection needed.

Without a bundler

If you can't run a bundler — injecting into a page through a proxy, a kiosk build, a plain static page — use the prebuilt single-file bundles (~5 KB gzipped):

<!-- ES module -->
<script type="module">
  import { initCrosspane } from 'https://unpkg.com/@crosspane/agent/dist/crosspane-agent.esm.js'
  initCrosspane({ label: 'kiosk display' })
</script>

<!-- or a classic script tag: exposes window.crosspane -->
<script src="https://unpkg.com/@crosspane/agent/dist/crosspane-agent.global.js"></script>
<script>
  crosspane.initCrosspane({ label: 'kiosk display' })
</script>

Under a strict CSP, self-host the file instead of loading it from a CDN.

Shipping safely

This is a debug-build feature. The cleanest approach is to let your bundler drop it from production entirely:

if (process.env.NODE_ENV !== 'production') {
  const { initCrosspane } = await import('@crosspane/agent')
  initCrosspane({ label: 'checkout webview' })
}

Or gate it at runtime — with enabled: false the agent installs no hooks at all, leaving console, fetch and XMLHttpRequest untouched:

initCrosspane({ enabled: () => user.isInternal })

Response bodies are not captured unless you pass captureBodies: true.

API

initCrosspane(options?: {
  label?: string             // shown in the dashboard (default: document.title)
  enabled?: boolean | (() => boolean)
  serverUrl?: string         // usually omit — resolved automatically (see below)
  bufferSize?: number        // ring buffer size (default: 2000 events)
  captureBodies?: boolean    // capture response bodies (default: false)
  bodyPreviewLimit?: number  // default: 2048 chars
  maxTextLength?: number     // per console/error entry (default: 10000 chars)
}): CrosspaneAgent

| Method | Description | |---|---| | agent.capture() | Returns a SessionCapture object (the ring buffer contents) | | agent.exportFile() | Downloads it as .crosspane.json. Silently does nothing in webviews whose host app doesn't implement downloads | | agent.copyCapture() | Puts the capture JSON on the clipboard; resolves to false if it couldn't. Works on non-secure origins (http://<lan-ip>), where navigator.clipboard is undefined. Needs a user gesture | | agent.dispose() | Restores console/fetch/XHR, closes the live connection | | agent.session | Session metadata (id, label, userAgent, platform) | | agent.enabled | false when gated off | | agent.live | true if a hub address was resolved — not that the hub answered. false means offline capture only. To confirm sessions arrive, watch the hub's terminal for ● session · <label> |

Where the hub address comes from

One address, and nothing varies between localhost, a phone, a deployed page and production:

initCrosspane({
  label: 'checkout webview',
  serverUrl: process.env.NEXT_PUBLIC_CROSSPANE_URL,  // Vite: import.meta.env.VITE_CROSSPANE_URL
})
NEXT_PUBLIC_CROSSPANE_URL=https://crosspane.example.com

Just the address, like any other base URL. The same value is fine in every environment including production: sending sessions needs no credential, while reading them needs a token that stays on the developer's machine. Run crosspane --tunnel --write-env and even this is filled in for you during local and on-device development.

On localhost you can omit it entirely — the agent looks for a hub on http://localhost:7788 by itself. Unset and not on localhost means offline capture only; the agent never guesses.

Who actually streams

Every install with that address streams, which is what you want in a dev or QA build. If the same build reaches people you don't want sending you logs, gate on something the app already knows — with enabled: false the agent installs no hooks at all:

initCrosspane({ serverUrl: HUB, enabled: () => user.isQA })

That is the right gate for a webview the app opens itself: there is no address bar in one, so nothing URL-based can work there.

For a page with no user model (a static site, a kiosk), isDebugActivated gates on a link instead — open it once with ?__crosspane=on, clear with ?__crosspane=off:

import { initCrosspane, isDebugActivated } from '@crosspane/agent'
initCrosspane({ serverUrl: HUB, enabled: isDebugActivated })

agent.live tells you which state you ended up in. agent.copyCapture() works regardless — no address, certificate or origin involved.

Notes

  • Calling initCrosspane twice returns the same agent — hooks are never installed twice, so hot reloads and duplicated bundles are safe.
  • Call initCrosspane as early as possible — anything logged before it runs isn't captured.
  • The ring buffer keeps the last N events, so a crash still leaves you the moments before it. Events are buffered whether or not the live connection is up.
  • Under a strict CSP you need connect-src to allow the hub for live mode. Bundling the agent (rather than loading it from a CDN) avoids script-src problems entirely.
  • Breakpoints are not possible from inside the page — JavaScript can't pause itself. When a remote inspector is available, use it; this agent is for when it isn't.

License

MIT