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

@misakstvanu/prism-browser

v1.0.1

Published

Browser SDK for Prism: uncaught errors, console lines, logs, breadcrumbs and page views, posted to the Laravel backend the misakstvanu/prism Composer client registers.

Readme

@misakstvanu/prism-browser

The browser SDK for Prism. It captures uncaught errors, unhandled promise rejections, console lines, log lines you write, breadcrumbs and page views (with Web Vitals) in the page, and posts them to the route the misakstvanu/prism Composer client registers in your Laravel application. They land on the same Errors, Logs and Requests screens as your server-side telemetry, filterable by runtime, and a page's error sits beside the backend request it made — same trace, one waterfall.

There is no browser key and no direct line to the workspace: the page reports to your own backend, which knows who is signed in, what the client IP is and what your prism.scrub list says, and forwards the report through the pipeline every other signal already travels. That is the whole shape of the feature, and it is why the setup below has a Composer step in it.

  • ~9 kB gzipped, no dependencies loaded up front (web-vitals is import()ed only when page views are on), ESM with tree-shakeable adapters, plus an IIFE build for a <script> tag.
  • Never throws into the page. Every public call is a no-op before init(), after close() and under SSR; a failure inside the SDK reaches console.debug under debug: true and nothing else.
  • Vue 3 and React adapters that report component errors with the component's name, and name the page after the route.

Requirements

  • misakstvanu/prism 2.1 or later in the Laravel application the page talks to, enabled (PRISM_ENABLED, on by default) with PRISM_BROWSER_ENABLED left on (also the default).
  • A browser with fetch, Promise, URL and crypto — every evergreen browser. The SDK makes no attempt to run on Internet Explorer.
  • For the Vue adapter, Vue ≥ 3.3; for the React adapter, React ≥ 18. Both are optional peers — install neither if you use neither.

Quickstart

Three steps and the next JavaScript error is in the console.

1. Update the Composer client. The endpoint the SDK posts to ships with misakstvanu/prism from 2.1, registered automatically at POST /_prism/browser — there is no route to add and no controller to write:

composer update misakstvanu/prism
php artisan prism:check

prism:check prints a browser line naming the exact address the router will answer and who may post to it. If it reads anything other than POST /_prism/browser (enabled, same-origin), fix that before going further — from the page's side a 404 here is indistinguishable from a wrong URL.

2. Install the package:

npm install @misakstvanu/prism-browser
# or
yarn add @misakstvanu/prism-browser

3. Initialise it once, as early as your bundle runs:

import Prism from '@misakstvanu/prism-browser';

Prism.init({
    release: import.meta.env.VITE_APP_VERSION, // optional, but what makes "since which deploy" answerable
});

That is the whole install. With no other option set, the SDK reports uncaught errors and unhandled rejections with the last 30 breadcrumbs (clicks, navigations, console lines, HTTP calls), captures console.warn / console.error as log lines, and adds a traceparent header to the page's same-origin fetch / XHR calls so a backend request continues the page's trace. Page views and HTTP-failure log lines are opt-in — capture: 'all' turns everything on.

Verify it: open the devtools console, run throw new Error('hello from the browser'), and within five seconds a Browser group appears on the workspace's Errors screen. Its detail shows the page URL, the viewport, the release and the trail of breadcrumbs that led up to it.

Without a bundler

The IIFE build exposes the same API on window.Prism:

<script src="/vendor/prism-browser/prism.iife.js"></script>
<script>
    Prism.init({ release: '2026.08.27' });
</script>

Copy node_modules/@misakstvanu/prism-browser/dist/prism.iife.js into a path your application serves. The IIFE inlines web-vitals (there is no bundler behind a <script> tag to split it out), so it is ~2.5 kB (gzipped) larger than the ESM core.

Frontend on another origin

If your pages are served from a different origin than the Laravel application (app.example.com posting to api.example.com), point the SDK at the backend and list the page's origin on the backend's prism.browser.origins. Nothing else changes: the route answers the preflight, allows credentials for that origin alone, and the session cookie still tells it who is signed in.

Prism.init({ endpoint: 'https://api.example.com/_prism/browser' });

Options

Everything init() accepts. Every key is optional.

| Option | Type | Default | Meaning | | --- | --- | --- | --- | | endpoint | string | /_prism/browser | Where reports are posted. A relative path is anchored on the page's origin at init(); use an absolute URL for a backend on another origin. Must match the backend's prism.browser.path. | | release | string | — | The deployed version — a git SHA, a build number, a date. It rides every event and every report's session, and is what the Errors screen groups a browser error's "since" by. | | capture | object \| 'all' | see below | Which signals the SDK captures. 'all' turns every switch on. | | capture.errors | boolean | true | Uncaught errors, unhandled rejections and captureException(). | | capture.console | ConsoleMethod[] \| boolean | ['warn', 'error'] | Console methods captured as log lines. true is every one of debug, log, info, warn, error; false is none. | | capture.http | boolean | false | A log line for every fetch / XHR that failed (network failure, 4xx, 5xx). Breadcrumbs for HTTP calls do not depend on this. | | capture.breadcrumbs | boolean | true | The SDK's own click, navigation, console and HTTP crumbs. addBreadcrumb() works regardless. | | capture.pageViews | boolean | false | Page views — the document load and every client-side navigation — with Web Vitals on the load. Off by default because it is the one signal that grows with traffic rather than with faults. | | tracing.propagate | 'same-origin' \| boolean \| Array<string \| RegExp> | 'same-origin' | Which outgoing calls carry a traceparent header. 'same-origin' is the page's own origin; true is every call, false none; a list matches string prefixes against the full URL or, for a same-origin call, its path ('/api'), and RegExps against the full URL. A header on a cross-origin call triggers a CORS preflight, which is why the default stops at the origin. | | sampleRate | number | 1 | The share of page views and log lines kept, 0..1, rolled per event. Errors are never sampled. A value that is not a number is read as 1, never 0. | | maxBreadcrumbs | number | 30 | How many breadcrumbs ride an error. 0 disables the trail. | | scrub | string[] | see below | Context keys to redact, merged with the defaults, matched case-insensitively at any depth. | | beforeSend | (event) => event \| null \| false | — | Inspect or rewrite every event before it is queued. Return null or false to drop it. A beforeSend that throws drops the event. | | user | { id: string \| number } | — | The signed-in user, as a hint. See setUser(). | | debug | boolean | false | Report the SDK's own failures through console.debug. Never on in production. |

The default capture is { errors: true, console: ['warn', 'error'], http: false, breadcrumbs: true, pageViews: false }. A partial object is merged over it, so capture: { pageViews: true } turns views on and leaves the rest as they are.

The default scrub list is the one the Composer client ships as prism.scrub's default, so a page and its backend agree about what a secret looks like: password, passwd, secret, token, api_key, apikey, authorization, cookie, credit_card, card_number, cvv, ssn. The backend applies its own list again to everything that arrives, so a key you only add server-side is still redacted before it reaches the workspace.

init() is idempotent: a second call updates the options and installs no second listener, so a framework that boots twice in development costs nothing. The set of console methods wrapped is fixed at the first call; a later init() that narrows the list stops the capture without re-wrapping.

API

The default export and the named exports are the same nine functions. All of them are safe to call anywhere — before init(), after close(), on the server — and none of them throws.

import Prism, { init, log, captureException, addBreadcrumb, setContext, setUser, traceId, flush, close } from '@misakstvanu/prism-browser';

| Call | What it does | | --- | --- | | init(options?) | Start the SDK. Outside a browser (no window) it returns at once and installs nothing. | | log(message, context?, level = 'info') | Queue a log line for the workspace's Logs screen, on channel browser. level is debug | info | warning | error (warn is accepted as warning). | | captureException(error, context?) | Report an error the page caught itself. It is marked handled and meets the same one-second dedupe as an uncaught one, so reporting an error you then also console.error costs one event, not two. Anything can be thrown: a non-Error is reported as class Error with its string form (an object JSON-encoded, up to 1 KB) as the message. | | addBreadcrumb({ category?, message?, data?, timestamp? }) | Add a step to the trail the next error carries. category defaults to custom, the timestamp to now; data is scrubbed on the way in. Works whatever capture.breadcrumbs says — that switch governs the SDK's own listeners. | | setContext(key, value) | Attach a value to every later event's context (undefined removes the key). Scrubbed by key like everything else. | | setUser({ id } \| null) | Name the signed-in user. It travels as a hint on the report; the backend reads the identity off its own session and believes the hint only when it resolved nobody and prism.browser.trust_client_user is on. | | traceId() | The page's current trace id — 32 lowercase hex, re-minted on every client-side navigation to a new path — or '' before init(). Put it in a support ticket, or on a request you make outside fetch / XHR. | | flush() | Send everything queued now, without waiting for the batch timer. Resolves once every pending batch has settled — sent, refused, or dropped after its one retry. | | close() | Flush, remove every listener and wrapper, and forget the client. init() starts over afterwards. |

Framework adapters

Both adapters reach the core through the package's own entry, so a page that imports both gets one SDK, one queue and one trace id. Each does two things the core cannot see from window: report a component error, which a framework catches before it ever reaches window.onerror, with the component's name; and name the page after the route, so route on every event is orders.show rather than /orders/42, and a client-side navigation counts as one page view rather than two.

Vue 3

import { createApp } from 'vue';
import Prism from '@misakstvanu/prism-browser';
import { PrismVue } from '@misakstvanu/prism-browser/vue';

Prism.init({ release: __APP_VERSION__ });

createApp(App)
    .use(router)
    .use(PrismVue, { router }) // router is optional
    .mount('#app');

The plugin installs an app.config.errorHandler that chains to whatever handler the app had already set (capture runs first, so a chained handler that throws costs nothing) and reports each error with context.component (the component's name, its <script setup> file name, or Anonymous) and context.lifecycleHook (Vue's own info string). When no handler existed, the error is also written to console.error, because Vue stops logging the moment a handler is set.

Given a vue-router instance, every event's route becomes to.name ?? to.path, and each completed navigation leaves a navigation breadcrumb and reports the page view. A navigation with a NavigationFailure (aborted, cancelled, duplicate) reports nothing — the page did not move. The router is typed structurally, so the adapter's types never require vue-router to be installed.

React

import { useLocation } from 'react-router-dom';
import Prism from '@misakstvanu/prism-browser';
import { PrismErrorBoundary, useReportNavigation } from '@misakstvanu/prism-browser/react';

Prism.init({ release: process.env.REACT_APP_VERSION });

function App() {
    const { pathname } = useLocation(); // react-router; Next's usePathname() works the same way
    useReportNavigation(pathname);

    return (
        <PrismErrorBoundary fallback={<p>Something went wrong.</p>}>
            <Routes />
        </PrismErrorBoundary>
    );
}
  • <PrismErrorBoundary fallback onError> reports from componentDidCatch with context.component (the first entry of React's own component stack) and context.componentStack (React's text, cut at 2 KB), then renders fallback — a node, or a function of the error — and calls onError(error, info). A thrown non-Error reaches both as an Error. There is no boundary-less capture of render errors: React unwinds to the nearest boundary and, when there is none, unmounts the whole tree, so put one near the root.
  • withPrismErrorBoundary(Component, boundaryProps?) is the same thing as a HOC.
  • useReportNavigation(pathname, routeName?) takes the pathname from whatever router the app uses — the adapter imports none — and on every change sets route (to routeName, or the pathname), leaves a navigation breadcrumb and reports the page view. The first run names the load view and leaves no crumb. It is Strict-Mode safe: every step is idempotent for a same-pathname re-run.

react is a peer dependency and is never bundled (two Reacts on one page is the "invalid hook call" error); the adapter is plain createElement, so it needs no JSX runtime configuration.

What is sent

Every post is one JSON report:

{
    "v": 1,
    "sdk": { "name": "@misakstvanu/prism-browser", "version": "1.0.0" },
    "sent_at": "2026-08-27T10:15:30.123Z",
    "session": { "id": "…uuid…", "release": "2026.08.27" },
    "user": { "id": 42 },
    "events": [
        { "type": "exception", "timestamp": "…", "trace_id": "…32 hex…", "payload": { … } }
    ]
}

session.release and user are present only when set. session.id is a UUID kept in sessionStorage under prism:session for the life of the tab (in memory when storage is unavailable), which is what makes "sessions" answerable beside "views" for a visitor who never signs in. Each event's trace_id is the page trace current when it was captured.

The payload keys are the workspace's column names, verbatim. A key with no column is dropped silently on the far side, so the lists below are exact.

exception — an uncaught error, an unhandled rejection, captureException(), a console.error(err) whose first argument is an Error, or a framework component error:

runtime      "browser"
class        the error's name — or its subclass's, when that is 3+ characters
message      the error's message (a non-Error's string form, up to 1 KB)
file, line   the top frame's — the frame the workspace fingerprints on
frames       [{ file, line, column, function, vendor }], top frame first, up to 50
route        the route name an adapter set, else location.pathname
handled      1 for captureException() and console.error, 0 for the two window listeners
breadcrumbs  [{ timestamp, category, message, data? }], oldest first, up to maxBreadcrumbs
context      see below

frames come from one parser that reads Chrome's, Firefox's and Safari's error.stack into the same shape — vendor is a frame from another origin or a /node_modules/ path — so a fault groups the same whichever browser threw it. A cross-origin script's Script error. carries no stack; the browser's own file / line become the one frame so it still groups by location.

logPrism.log(), a captured console call, or a failed HTTP call:

runtime      "browser"
level        debug | info | warning | error
message      the line (console arguments formatted with %s %d %o … substitution, cut at 4 KB)
channel      browser (Prism.log) | console | http
context      see below — a console line adds `console` (the remaining arguments), an http line adds { method, url, status, duration_ms }

page_view — the document load, then every client-side navigation to a new path or query (a hash-only change is the same page). One row per view, sent when the view ends:

url          location.href
path         location.pathname
route        the route name an adapter set, else ""
referrer     document.referrer (the previous page's href for a navigation)
kind         load | navigation
duration_ms  loadEventEnd − startTime for a load; time on page for a navigation
status       the document's response status where the browser reports one (Chromium), else 0
user_agent   navigator.userAgent
viewport_w, viewport_h
release      the configured release, else ""
session_id
ttfb_ms, fcp_ms, lcp_ms, cls, inp_ms   Web Vitals, on kind = load only — cls is a unitless score, the rest milliseconds

The vitals come from Google's web-vitals, loaded lazily the moment capture.pageViews is on and never otherwise, with the values final at the time the row goes out. A metric that never landed is left off so the column keeps its default; a navigation row carries none, because only a real document load has an LCP.

context on an exception or log is, from the bottom up: the page facts — url, referrer, user_agent, viewport (1440x900), session_id, release, language, online — then everything setContext() set, then the per-call context, with the scrub list applied over the whole thing.

What the backend adds and overrides

The report is an unauthenticated write from an origin nobody controls, so four things are the backend's answer rather than the page's. user_id is read off the backend's own session (prism.browser.guard); the user hint in the report is used only when that resolved nobody and the backend is configured to trust it. context.ip is the client address the server saw — a page cannot know its own public address. context.user_agent (and a page view's user_agent column) is the request header, written over the SDK's copy. And every timestamp is corrected against sent_at: past a five-second tolerance the whole report shifts by the offset between the page's clock and the server's, so a device whose clock is a year out still lands in today's partition. The application, environment and replica the events belong to are the backend's too — they ride the batch envelope, so the page never names them.

The backend also runs its prism.scrub list over everything, rewrites query strings in every URL pair by pair (?token=abc&step=2?token=%5BREDACTED%5D&step=2), and redacts name = value pairs in free text. See the Composer client's README, Browser telemetry.

Sampling and quota

Browser events count against the workspace's monthly event quota like every other signal — a page view per visit and a log line per console.warn add up on a busy site in a way errors do not, which is why capture.pageViews and capture.http are off by default and capture.console starts at warn. sampleRate is the first knob: it keeps that share of page views and log lines, rolled per event, and never touches an exception. The workspace's own per-application rules (Console → Settings → Sampling) are the second, and the two multiply. Exceptions are pinned to 100% on both sides, and a workspace that is over quota still keeps them.

Breadcrumbs

The trail attached to every exception, newest last, capped at maxBreadcrumbs. Each crumb is { timestamp, category, message, data? }:

| category | message | data | | --- | --- | --- | | click | button#save, a.nav-link, div — tag plus id or first class | { text }, the element's text up to 64 characters — never for input, textarea, select or option, whose text is their value | | navigation | /orders → /orders/42 (path, query and hash for a same-origin address) | { from, to } as full hrefs; an adapter adds route | | console | the formatted line, cut at 200 characters | { level } | | http | GET /api/orders → 503 (path for a same-origin call, origin + path otherwise, never the query) | { method, url, status, duration_ms } | | custom (or anything you name) | yours | yours, scrubbed on the way in |

A console crumb is written after the log line or exception it describes, so a console.error(err) does not carry itself as its own last step. An HTTP call the page aborted leaves no crumb (it neither succeeded nor failed), and neither does the SDK's own post.

Trace propagation

init() mints a 32-hex trace id for the page and re-mints it on every client-side navigation to a new path or query. Every event carries the id current when it was captured, and every same-origin fetch / XHR call (see tracing.propagate) gets a W3C traceparent header — 00-{traceId}-{spanId}-01, a fresh span id per call — unless the caller already set one. The Laravel backend continues that trace rather than starting its own (the Composer client leaves the tracecontext propagator at its default and applies no allow-list to a browser's id), which is what puts a JavaScript error beside the API call it made, with that call's queries and log lines, on one Traces screen, and what lets the error detail link to the backend request of the page it broke on.

The wrappers never mutate the caller's Headers, array or object (a copy is extended), hand back the original's own promise, and never read a body. A URL the browser cannot resolve is passed through untouched so the browser reports its own error.

Delivery

  • Events queue in memory (at most 200; the oldest are dropped past that) and go out 5 seconds after the first one, or at once at 20. A batch is at most 50 events / 200 KB; an oversize event goes alone rather than being dropped.
  • The transport is a keepalive fetch with credentials: 'same-origin' — the session cookie is how the backend knows who is signed in — and no header but Content-Type. A network failure is retried once, 2 seconds later; a 4xx / 5xx is dropped without retry (the backend answers 413 for an oversize post, 429 when throttled, and never a body). While navigator.onLine === false nothing is sent; online flushes.
  • Leaving the page uses navigator.sendBeacon — on pagehide and on visibilitychange → hidden, which is when the last report of a session (and every page view's row) goes out, and the only transport a browser lets finish once the page is gone. A beacon sends the body as a string, so it arrives as text/plain and cannot set a header; the backend's route decodes the body by hand rather than by content type and runs without CSRF verification for exactly this reason. Where sendBeacon is missing or refuses, the keepalive fetch is the fallback.
  • Errors are deduplicated within one second — same class, message and top frame, whichever door they came through — so a framework handler that reports and then console.errors costs one event, and an error thrown in a loop reports once a second rather than once per iteration.

Content Security Policy

connect-src 'self' is all the SDK needs on a page whose backend is its own origin; for a frontend on another origin, add that backend's origin to connect-src. Nothing else is touched: the SDK injects no inline script, no <script> element, no eval and no worker, so script-src stays whatever your bundle already requires. sendBeacon is governed by connect-src like fetch.

What is not captured

  • Errors thrown before init() runs. There is no pre-init loader snippet; call init() as early as your bundle executes. A framework's own boot errors before that point are the framework's to log.
  • Resource load errors. A failed <img>, <script> or stylesheet fires error on the element and does not bubble to window; the SDK's listener is deliberately non-capturing so those do not become error groups. CSP violation reports are not collected either.
  • Request and response bodies. The HTTP wrappers observe how a call ended — method, URL, status, duration — and never read a body in either direction (bodyUsed stays false).
  • Input values. A click breadcrumb records an element's text for a button or a link, never for an input, textarea, select or option, whose text is what was typed or chosen; nothing reads a form field.
  • Anything the scrub list names, at any depth in a context, a breadcrumb's data or a URL's query string — replaced by [REDACTED] in the page, before it is queued.
  • Session replay, per-visitor timelines, browser spans on the trace waterfall. The trail on an error is the breadcrumbs; a page's fetch timings are breadcrumbs and HTTP log lines, not spans rows — the page trace links to the backend's waterfall, it does not extend it.

Source maps and minified frames

Source maps are not consumed. Frames render in the console exactly as the browser reported them — the bundled file, the bundled line and column, and the minified function label — and a frame whose label is two characters or shorter is marked minified, once, above the trace. What is engineered instead is that grouping stays stable across deploys without them: the backend fingerprints a browser error on its class, the top application frame's file with the content hash stripped (app-Bx3kq9.js and app-Q7mLp2.js are one file), its function label only when it is three characters or longer (a minified t changes every build), and the message with URLs, ids and numbers replaced by placeholders — so the same fault is one group before and after a release, and …/orders/123 and …/orders/456 are one group too. Ship release and the group's detail says which deploy it started in.

Development

The package is a standalone Vite / TypeScript project inside the Prism repository, with its own yarn.lock and its own node_modules. It is deliberately not a workspace of the repository root: the Prism console dogfoods the published package from the npm registry, the way a customer's install resolves it, so a change here reaches the console only through npm publish and a version bump in the root package.json. The scripts run from this directory:

yarn install      # in this directory — the root install does not cover the package
yarn build        # ESM (dist/index.js, vue.js, react.js), the IIFE, then the .d.ts
yarn test         # vitest, happy-dom
yarn lint:check
yarn types:check
yarn check:size   # the budget: index.js ≤ 10 kB gzipped, each adapter ≤ 2 kB