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

@prostoteam/prostometrics-web

v0.1.3

Published

Lightweight browser client for emitting Prostometrics application telemetry.

Readme

prostometrics-web

Browser client for Prostometrics application telemetry.

This is for your app's own metrics — how long your API calls take, which steps of a flow people complete, how many distinct users a feature has. It is not a web analytics tool: there are no automatic pageviews, referrers, campaign parameters, or user-agent parsing. It records what you ask it to and nothing else.

Under 5 KB gzipped, no dependencies.

Install

npm install @prostoteam/prostometrics-web

Or load it from a CDN, with no build step:

<script src="https://cdn.jsdelivr.net/npm/@prostoteam/[email protected]/dist/prostometrics.min.js"></script>
<script>
  prostometrics.init({ publicKey: "42_pk_yourkeyhere" });
  prostometrics.count("app.web.page.opens");
</script>

Pin the version. The unpinned forms of that URL resolve to whatever was published most recently, so a release you have not looked at reaches your users the moment it ships. A pinned URL is also served immutably, which makes it faster.

To pin the exact bytes as well, add an integrity hash:

curl -sL https://cdn.jsdelivr.net/npm/@prostoteam/[email protected]/dist/prostometrics.min.js \
  | openssl dgst -sha384 -binary | openssl base64 -A
<script src="https://cdn.jsdelivr.net/npm/@prostoteam/[email protected]/dist/prostometrics.min.js"
        integrity="sha384-PASTE_THE_HASH" crossorigin="anonymous"></script>

Use

import { init, count, value, unique, match } from "@prostoteam/prostometrics-web";

init({ publicKey: "42_pk_yourkeyhere" });

count("app.web.checkout.step", 1, { step: "payment" });
value("app.web.api.duration_ms", 118, { route: match(location.pathname, ["/orders/:id", "/cart"]) });
unique("app.web.users", currentUser.id);

Create a client key in your project settings.

The key is meant to be public

publicKey is designed to be readable by anyone: commit it, bundle it, serve it in page source. It is write-only, it is confined to a single workload, and you can revoke it from project settings at any time.

Never put a server key here. Those belong on a server, this endpoint refuses them, and the client says so the first time it is refused rather than waiting out the startup grace — a server key cannot start working, and one that reached page source has to be revoked, not retried.

What to record

| | Use for | | --- | --- | | count(metric, delta, labels) | rates and totals — clicks, errors, steps completed | | value(metric, sample, labels) | distributions — durations, sizes, scores | | sparse(metric, sample, labels) | like value, for readings that arrive irregularly | | unique(metric, id, labels) | distinct counts — daily active users |

Count things with count, measure them with value. If you want both "how many API calls" and "how slow were they", emit both. Counters are exact at any call volume; value samples are thinned when a series produces more of them than one batch should carry, so a count derived from them under-reports.

unique never sends the id — it is hashed in the browser and merged into a sketch on the server, so an opaque account id is safe to pass. Hash anything that is itself personal data before handing it over.

Labels and why match exists

Every distinct combination of label values becomes a stored series. That is fine for { step: "payment" } and expensive for { route: "/orders/8412" }, which creates one series per order.

match maps a dynamic value onto one of the patterns you list at the call site and stores the pattern:

value("app.web.api.duration_ms", ms, {
  route: match(location.pathname, ["/orders/:id", "/cart", "/checkout/:step"]),
});

Because the stored value can only be a literal from your own source, your series count is bounded by the size of your code rather than by your traffic. :name matches one path segment, * matches the rest, anything unmatched becomes other.

Two backstops run regardless: a label that reaches 50 distinct values on one metric collapses to other and warns, and debug: true warns when a value looks like an identifier.

Behavior worth knowing

Flushes every 10 seconds, and again when the page is hidden or closed. A shorter interval shows you nothing sooner — stored buckets are ten seconds wide — and costs more requests.

Counters merge inside the window. Ten thousand count() calls in a render loop produce one event carrying ten thousand.

Nothing is retried or persisted. If a send fails, that window is gone rather than replayed later out of order.

Events are timestamped on arrival, not by the browser, because end-user device clocks cannot be relied on.

The service can stop or pause the client, and both are logged with console.error. A revoked key stops it for good, as does a client release the service has retired — a page already loaded cannot be updated any other way. A project that is out of balance or over its rate limit pauses it until the service says it is ready.

For the first 30 seconds after the client starts, a refused key is warned about rather than acted on, because a key created moments earlier takes a few seconds to become usable. After that window a refusal stops the client as described above, so deleting a key still silences the pages already running. A key that is not a client key at all skips the grace entirely and is named as the mistake.

Nothing is sent that you did not record. The client emits no metrics of its own, so your metric list stays yours. When a ceiling thins something it warns, and stats().dropped counts it.

Options

init({
  publicKey: "42_pk_yourkeyhere",

  // Defaults to the hosted endpoint. A host alone gets the ingest path
  // appended; a full path is used as given.
  endpoint: "https://yourapp.com/_pm",

  flushIntervalMs: 10000,

  // Warns about label values that look like identifiers. Development only.
  debug: import.meta.env.DEV,

  onWarning: (message) => console.warn(message),

  // Ingest stopped or paused by the service. Defaults to console.error.
  onError: (message) => console.error(message),
});

flush() sends immediately. close() stops the timers and sends what is left. stats() returns { dropped, stopped } — how many events client-side ceilings have discarded, and whether the service has stopped this client.

Ad blockers

Some browsers block requests to third-party metrics hosts, so a share of your audience will not report. If that matters, proxy the endpoint through your own origin — for most sites a few lines of nginx rather than a service:

location /_pm {
    proxy_pass https://prostometrics.ru/api/i/web;
}

Then set endpoint: "https://yourapp.com/_pm".

Protocol

This client implements the Prostometrics public client protocol, shared with the mobile clients. Ask us if you need the specification.