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

@hanzo/event

v0.3.11

Published

Hanzo Event — the ONE telemetry client. Emits pageview/event/identify/group to the Hanzo Cloud event stream (POST /v1/event), AND reports errors to Sentry as real Sentry envelopes — the error plane needs a DSN, without one nothing reaches Sentry. First-to

Readme

@hanzo/event

The ONE telemetry client for Hanzo surfaces. Emits every kind of event — pageview / event / identify / group and errors — to Hanzo Cloud, never to a third-party.

ONE API surface over TWO planes. They are separate pipes, and neither can starve the other:

1. event stream  POST {host}/v1/event    body: { batch: [Event, …] } -> { accepted, dropped }
2. error plane   POST {dsn}/v1/sentry/{projectId}/envelope/?sentry_key=…   (a real Sentry envelope)

Errors need a DSN. Without one, nothing reaches Sentry.

There is no server-side fan-out from /v1/event into Sentry. Versions ≤ 0.3.1 of this README claimed there was — that "Cloud fans the one stream out into three lenses". It does not. /v1/event writes a type:'error' row to the cloud event warehouse (readable via GET /v1/errors) and stops there. Because every Hanzo property believed that claim, nobody set a DSN, and the entire fleet reported zero errors to Sentry until 0.3.2 added the envelope.

Set dsn (or NEXT_PUBLIC_HANZO_EVENT_DSN). Mint one per property with POST /v1/sentry/projects. The key is publishable and write-only — safe in a browser bundle, same trust class as a pk_ ingest key. No DSN means the error plane is inert: nothing is sent, nothing throws, analytics is unaffected. Assert client.errorPlaneEnabled if you want to know which you have.

No bundler? Use the hz.js tag — same client, same wire

<script async src="https://unpkg.com/@hanzo/event/hz.js"
        data-product="hanzo.ai"
        data-capture="1"></script>

hz.js (in this package) is the script-tag distribution of this client, for surfaces with no build step. It posts the SAME { batch: [WireEvent, …] } to the SAME POST {host}/v1/event, and adds DOM autocapture$click (with an element locator), $outbound, $scroll, $form, $vitals — which a bundled app does not need and a plain page cannot get. Manual API: window.hanzo.track() / identify() / page().

The tag used to live in hanzoai/analytics and post a bare JSON array of {site, ts, type, path, …} to analytics.hanzo.ai/v1/event — a second protocol behind an identical path spelling, served by a second collector with its own database. POST api.hanzo.ai/v1/event {"batch":[]} answered 200 while POST analytics.hanzo.ai/v1/event [] answered 204, and a client pointed at the wrong host failed silently. Both the second door and the second collector are deleted. One wire, one door, one client home — this package.

  • Batched with a size + interval flush, and beacon-on-unload (sendBeacon for cookie/publishable-key apps, fetch(keepalive) for token apps).
  • Auto error capture (subsumes @sentry): window.onerror, unhandledrejection, and a React ErrorBoundary are reported on both planes — a Sentry envelope to the DSN host, and a correlated type:'error' event on the stream. Opt out with captureErrors: false.
  • Scrubbed at the source: secrets are always redacted and PII is masked client-side before an error leaves the device.
  • First-touch attribution: UTM + referrer + refCode are parsed once and persisted, then attached to every event.
  • Cohorts: signupWeek, channel, refCode ride each event.
  • Tenant-safe: the client NEVER sends an org/tenant — Cloud stamps it from the validated session or the signed publishable key.
  • Fail-soft: telemetry loss is swallowed; the client never throws into the app.
  • SSR-safe: importing on the server is a no-op; it only acts in the browser.

Core (framework-agnostic)

import { createAnalytics, EVENTS } from '@hanzo/event'

// Cookie/session apps (console, admin): same-origin, no token.
const analytics = createAnalytics({ product: 'console' })

// Token apps (app, site): give the cloud host + a bearer getter.
const analytics = createAnalytics({
  product: 'app',
  host: 'https://api.hanzo.ai',
  getToken: () => localStorage.getItem('hanzo_access_token') ?? undefined,
})

analytics.pageview()
analytics.identify('user-42')
analytics.capture(EVENTS.SIGNUP_COMPLETED, { plan: 'pro' })
analytics.capture(EVENTS.ORDER_COMPLETED, { kind: 'plan' }, { productId: 'plan_pro', revenue: 49, quantity: 1, currency: 'usd' })

React

'use client'
import { AnalyticsProvider, useAnalytics, usePageview } from '@hanzo/event/react'
import { usePathname } from 'next/navigation'

export function Providers({ children }) {
  return <AnalyticsProvider config={{ product: 'console' }}>{children}</AnalyticsProvider>
}

function RouteTracker() {
  usePageview(usePathname()) // one pageview per navigation
  return null
}

function UpgradeButton() {
  const a = useAnalytics()
  return <button onClick={() => a.capture(EVENTS.PLAN_CLICKED, { plan: 'pro' })}>Upgrade</button>
}

Errors (the @sentry replacement)

Unhandled errors and promise rejections are captured automatically. React render errors never reach window.onerror, so wrap your tree in the ErrorBoundary to catch those too. Report caught errors yourself with captureError.

Pass a dsn or none of this reaches Sentry.

import { ErrorBoundary } from '@hanzo/event/react'

<AnalyticsProvider config={{ product: 'console', dsn: process.env.NEXT_PUBLIC_HANZO_EVENT_DSN }}>
  <ErrorBoundary fallback={(err, reset) => <Crash error={err} onReset={reset} />}>
    <App />
  </ErrorBoundary>
</AnalyticsProvider>
try { risky() } catch (err) { analytics.captureError(err, { properties: { where: 'checkout' } }) }

Each report goes to both planes:

  • Sentry — a real Sentry envelope to the DSN's ingest route. This is the only thing that creates an issue in the error dashboard, with grouping and stack frames. Sent one envelope per error, immediately; batching a crash report is how you lose it.
  • the event stream — a type:'error' event; Cloud folds the exception into properties.$exception and stamps event_type='error', so the error stays correlated with the session's pageviews (GET /v1/errors). This is product signal, not error tracking, and it never reaches Sentry on its own.

The message and any properties are scrubbed of secrets and PII before sending, and the message is capped at 8KB. captureError never throws back into your app, and a failure on one plane cannot suppress the other.

Publishable key (public pages, no bearer)

Marketing/public pages have no session. Mint a write-only publishable key (POST /v1/ingest/keys) and pass it as ingestKey; it rides Authorization on fetch and ?ingest_key on an unload beacon, so the event stream accepts anonymous traffic. It is safe to ship in a bundle (write-only, cannot read).

The ingestKey authenticates the event stream ONLY. The error plane authenticates independently with the DSN key on ?sentry_key=, and the two credentials are never sent to each other's host. A public page that wants errors in Sentry needs the dsn as well:

createAnalytics({
  product: 'site',
  host: 'https://api.hanzo.ai',
  ingestKey: 'pk_live_…',                          // event stream
  dsn: process.env.NEXT_PUBLIC_HANZO_EVENT_DSN,    // error plane
})

Taxonomy, funnels & goals

TAXONOMY.md is the canonical spec — naming convention, property rules, identify/group semantics, the funnels for hanzo.ai / hanzo.app / hanzo.chat, and the exact emit site (file:line) of every event on each surface. Read it before adding an event.

FUNNELS (see funnels.ts) is the one funnel registry: each journey is an ordered list of steps naming EVENTS values, scoped by product. A funnel that spans origins while logged out is marked join: 'aggregate' — two origins mean two anonymousIds, so a per-person rate across them would be a lie.

import { FUNNELS, GOALS } from '@hanzo/event'

FUNNELS.appShip.steps.map((s) => s.event)
// ['$pageview','build_started','generation_completed','deploy_started','deploy_succeeded']
GOALS.signup.funnel // derived from FUNNELS.signup — never restated

Goals & cohorts

GOALS and COHORTS (see goals.ts) are the shared, machine-readable insights spec: Signup (funnel view→submit→verify→first-action), Sale (a order_completed with kind=plan), and Upgrade Intent (plan_clicked, funnel from pricing_viewed). Cohort fields map to the signup_week, channel, and ref_code columns of hanzo.events.