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

nohmo

v0.3.50

Published

Official analytics SDK for Nohmo — device tracking, session journeys, and event batching for React, Next.js, and React Native

Readme

nohmo

Official analytics SDK for Nohmo — device tracking, session journeys, UTM attribution, and real-time event streaming for React, Next.js, and plain HTML / Django templates.

Install

# Web (React / Next.js)
npm install nohmo

# React Native (iOS & Android)
npm install nohmo

# Optional — recommended for persisting device identity across app restarts
npm install @react-native-async-storage/async-storage

Quick start

Next.js (App Router)

// app/layout.tsx
import { NohmoNextProvider } from 'nohmo'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <NohmoNextProvider
          projectId={process.env.NEXT_PUBLIC_NOHMO_PROJECT_ID!}
          apiKey={process.env.NEXT_PUBLIC_NOHMO_API_KEY!}
        >
          {children}
        </NohmoNextProvider>
      </body>
    </html>
  )
}

Page views, time spent, scroll depth, and clicks are tracked automatically from this point.

NEXT_PUBLIC_NOHMO_PROJECT_ID=proj_xxxx
NEXT_PUBLIC_NOHMO_API_KEY=pk_xxxx

Next.js (Pages Router)

// pages/_app.tsx
import { NohmoProvider } from 'nohmo'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <NohmoProvider
      projectId={process.env.NEXT_PUBLIC_NOHMO_PROJECT_ID!}
      apiKey={process.env.NEXT_PUBLIC_NOHMO_API_KEY!}
    >
      <Component {...pageProps} />
    </NohmoProvider>
  )
}

Plain React (Vite, CRA)

import { NohmoProvider } from 'nohmo'

function App() {
  return (
    <NohmoProvider projectId="proj_xxxx" apiKey="pk_xxxx">
      <YourApp />
    </NohmoProvider>
  )
}

No automatic route-change tracking — use usePageView('/path') in each page component or call send('PAGE_VIEW', …) manually on route changes.

Plain HTML / Django templates (no build step)

Add one script tag. No npm, no bundler, no build step required.

<!-- In your <head> or before </body> -->
<script
  src="https://cdn.jsdelivr.net/npm/nohmo@latest/dist/n.min.js"
  data-project="proj_xxxx"
  data-api-key="pk_xxxx"
  defer
></script>

That's it. Page views, clicks, scroll depth, time spent, and rage-clicks are tracked automatically the moment the script loads.

Track custom events from any inline script:

<button onclick="window.nohmo.send('signup_clicked', { plan: 'pro' })">
  Sign up
</button>

Identify users (e.g. in a Django template after login):

{% if user.is_authenticated %}
<script>
  window.nohmo.identify('{{ user.pk }}', '{{ user.email }}')
</script>
{% endif %}

window.nohmo is available as soon as the script finishes loading (defer guarantees it runs after the DOM is ready). For inline scripts that run before the page finishes loading, use window.addEventListener('load', () => { window.nohmo.send(...) }).


Track custom events

import { useNohmo } from 'nohmo'

export default function BuyButton({ item }: { item: { id: string; price: number } }) {
  const { send } = useNohmo()

  return (
    <button onClick={() => send('purchase_started', { itemId: item.id, price: item.price })}>
      Buy now
    </button>
  )
}

Events are queued in memory and flushed as a batch every flushInterval ms via navigator.sendBeacon (falling back to fetch). They survive page unload and never block the main thread.

Identify users after login

import { useNohmo } from 'nohmo'

export default function LoginForm() {
  const { linkUser } = useNohmo()

  const handleLogin = async () => {
    const user = await loginAPI()
    await linkUser(user.id, user.email, { plan: user.plan })
  }

  return <button onClick={handleLogin}>Login</button>
}

Every event fired before linkUser() — including across previous sessions — is retroactively attached to the user on the backend. Nothing is lost.

Once a user is linked, their identity persists. If they visit from a different device and call linkUser() again with the same ID, their profile and metadata are automatically merged.

Manual page view hook

import { usePageView } from 'nohmo'

export default function MyPage() {
  usePageView('/my-page') // fires PAGE_VIEW once on mount
  return <div>…</div>
}

React Native (iOS & Android)

Nohmo includes a first-party React Native SDK under nohmo/react-native. One package, two platforms.

Setup

npm install nohmo

# Recommended — persists device identity across app restarts
npm install @react-native-async-storage/async-storage

The SDK works out of the box with no additional dependencies. Without @react-native-async-storage/async-storage a new device ID is generated on every cold start.

// App.tsx
import { NohmoProvider } from 'nohmo/react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'

export default function App() {
  return (
    <NohmoProvider
      projectId="proj_xxxx"
      apiKey="pk_xxxx"
      options={{ appVersion: '1.0.0', debug: __DEV__, storage: AsyncStorage }}
    >
      <YourApp />
    </NohmoProvider>
  )
}

If you skip storage, everything works — events are tracked, screens are recorded, users can be identified — you just won't get returning-device recognition after an app kill.

What gets tracked automatically

| Event | Trigger | |-------|---------| | APP_INSTALL | First time the app ever opens | | APP_OPEN | Every time the app becomes active | | APP_BACKGROUND | When the app goes to background, with session duration | | TIME_SPENT | When leaving a screen or backgrounding the app, with seconds on the screen | | JS_ERROR | A non-fatal JS error caught by the global handler, with message + stack | | APP_CRASH | A fatal JS crash — persisted and reported on the next app launch, attributed to the session it happened in | | INSTALL_ATTRIBUTED | Attribution resolved on first open — Play Store referrer on Android, system pasteboard on iOS (built-in, no extra packages) |

Track screens automatically

The Babel plugin handles this too — no prop changes needed. It detects <NavigationContainer> in your JSX and injects onStateChange and onReady at compile time:

// You write this (unchanged):
<NavigationContainer ref={navigationRef} theme={navigationTheme}>
  <RootNavigator />
</NavigationContainer>

// Plugin compiles it to:
<NavigationContainer
  ref={navigationRef}
  theme={navigationTheme}
  onStateChange={__nohmoNavStateChange}
  onReady={__nohmoMakeReady(navigationRef)}
>
  <RootNavigator />
</NavigationContainer>

Everything is driven by the single plugins: ['nohmo/babel-plugin'] line in babel.config.js. No manual prop wiring.

Manual tracking — if you prefer per-screen control without the plugin:

import { useScreenView } from 'nohmo/react-native'

export default function HomeScreen() {
  useScreenView('Home')   // fires SCREEN_VIEW on mount
  return <View>…</View>
}

Custom events

const { send } = useNohmo()

send('button_tapped', { buttonId: 'cta_signup' })
send('checkout_started', { cartValue: 49.99 })

Identify users

const { linkUser } = useNohmo()

// After login
await linkUser(user.id, user.email, { plan: user.plan })

Track conversions

const { trackConversion } = useNohmo()

trackConversion('user_created')
trackConversion('purchase', { amount: 29.99, currency: 'USD' })

Uninstall detection

Nohmo detects app uninstalls using the same silent-push technique used by AppsFlyer and Adjust.

1. Upload your Firebase Service Account JSON in Settings → App in your Nohmo dashboard.

2. Install Firebase Messaging:

npm install @react-native-firebase/app @react-native-firebase/messaging

3. Register the push token — one component, zero ongoing maintenance:

import { useNohmo } from 'nohmo/react-native'
import messaging from '@react-native-firebase/messaging'

function PushTokenRegistrar() {
  const { registerPushToken } = useNohmo()

  useEffect(() => {
    messaging().getToken().then(registerPushToken)
    return messaging().onTokenRefresh(registerPushToken) // handles token rotation
  }, [])

  return null
}

How it works:

  • Every night at 03:00 UTC, Nohmo sends a silent data-only FCM message to every device that hasn't opened the app in 24h
  • If FCM returns NotRegistered → app was uninstalled → device is marked automatically
  • No code needed after the one-time setup
  • Results in App Analytics → Uninstalls with daily chart, uninstall rate, and D1/D7/D30 retention

Accuracy: ~85–90% — users with push notifications disabled cannot be detected (same limitation as every major analytics SDK).

React Native options

| Option | Type | Default | Description | |--------|------|---------|-------------| | appVersion | string | '' | App version string sent with every event | | flushInterval | number | 5000 | Milliseconds between batch event flushes | | debug | boolean | false | Log all SDK activity to the console | | autoAppLifecycle | boolean | true | Auto-track APP_OPEN and APP_BACKGROUND on foreground/background transitions | | autoErrors | boolean | true | Capture JS errors (JS_ERROR) and crashes (APP_CRASH) — including native Android/iOS crashes | | storage | NohmoStorage | in-memory | Provide an AsyncStorage-compatible object to persist device identity across app restarts. Pass AsyncStorage from @react-native-async-storage/async-storage. Without this, a new device ID is generated on every cold start. |

Autocapture (press events)

Add one line to your Babel config and every onPress / onLongPress in your app is tracked automatically — no code changes per screen.

// babel.config.js
module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: ['nohmo/babel-plugin'],  // ← add this
}

That's it. The plugin rewrites this at build time:

// What you write
<Pressable onPress={handleBuy}>
  <Text>Buy now</Text>
</Pressable>
// What gets compiled (you never see this)
<Pressable onPress={__nohmoWrap(handleBuy, { c: 'Pressable', t: 'Buy now', f: 'CheckoutScreen', l: 42 })}>
  <Text>Buy now</Text>
</Pressable>

What gets captured automatically:

| Event | Trigger | |-------|---------| | PRESS | Any onPress tap | | LONG_PRESS | Any onLongPress |

Each event includes component (e.g. Pressable), text (button label if it's a static string), file, and line.

What it doesn't capture: dynamic text from variables/state, onPressIn/onPressOut (intentionally excluded — too noisy), or press handlers inside node_modules.

Install attribution (Android + iOS)

Nohmo uses the same deterministic attribution mechanism as AppsFlyer and Adjust. On Android, a click UUID is embedded in the Play Store referrer param. On iOS, the click-link interstitial writes the UUID to the system pasteboard, which the SDK reads on first open. No GAID or fingerprinting required on either platform.

How it works end-to-end:

  1. Build a tracking link in Settings → App → Attribution Link Builder in your Nohmo dashboard. Fill in your UTM fields and copy the generated link:

    https://www.nohmo.in/api/click/<project-code>/?utm_source=facebook&utm_medium=cpc&utm_campaign=summer

    Click Save & shorten to store the link and get a tidy short URL (https://www.nohmo.in/api/l/<code>) you can reuse from the Saved links list.

  2. Use the link in your ad. When a user clicks it, Nohmo records the click and routes them to the correct store:

    • Android: redirects to your Play Store URL with the click UUID in the referrer param — Google Play delivers this to the app on first open.
    • iOS: serves a brief interstitial page that writes the click UUID to the system pasteboard, then redirects to your App Store URL — the SDK reads and clears it on first open.
  3. No extra setup needed. Attribution is built into the Nohmo SDK — the SDK reads the Play Store referrer (Android) or system pasteboard (iOS) automatically on first open and sends it to the backend for matching. Zero code needed in your app.

  4. Results appear in App Analytics → Install Attribution with a breakdown by source, campaign, and match type.

Attribution priority:

| Priority | Method | Accuracy | |----------|--------|----------| | 1 (Android) | nohmo_click UUID in Play Store referrer | 100% deterministic | | 1 (iOS) | nohmo_click UUID in system pasteboard | 100% deterministic | | 2 | GAID / IDFA match | Deterministic | | 3 | UTMs in referrer (no click ID) | High | | 4 | IP + platform within 24h | Probabilistic | | 5 | No match | Organic |

iOS note: The App Store has no referrer param, so iOS uses the system pasteboard — deterministic when the user taps through the click interstitial — with GAID/IDFA and probabilistic IP matching as fallbacks.

Attribution via deep links

Pass UTM params in your deep link URL and the SDK captures them automatically:

yourapp://open?utm_source=meta&utm_medium=cpc&utm_campaign=summer

Attribution appears in Traffic → Conversions and is linked to every event in that session.

Invite a friend (referral attribution)

Want installs from in-app sharing — "invite a friend" — attributed back to the user who shared? Share a Nohmo link instead of the raw store URL. buildInviteLink() returns a short link that carries the current user's id, so you can see exactly who referred whom.

import { Share } from 'react-native'
import { useNohmo } from 'nohmo/react-native'

function InviteButton() {
  const { buildInviteLink } = useNohmo()

  const invite = async () => {
    const link = await buildInviteLink({ channel: 'whatsapp' })
    // → https://www.nohmo.in/api/l/aB3xK9q
    await Share.share({ message: `Join me on the app! ${link}` })
  }

  return <Button title="Invite a friend" onPress={invite} />
}
  • Call linkUser() first — the sharer's id is captured as utm_content. Without it the link is a generic referral link with no referrer.
  • Returns a short URL (/api/l/<code>). The same user + options always resolves to the same code, and it's cached, so repeated shares never create duplicate links. Offline, it falls back to the full click URL.
  • Options: channelutm_medium (e.g. 'whatsapp'), campaignutm_campaign, sourceutm_source (defaults to 'referral').

When the invitee installs through the link, their device's attribution shows the sharer's id — deterministic on Android (Play Install Referrer), best-effort on iOS (pasteboard when they tap through the click interstitial, probabilistic otherwise). Requires your iOS App Store URL to be set in Settings → App. Results appear in App Analytics → Install Attribution and on each device's Came from card.


Track conversions

Conversions let you measure what matters — signups, deposits, purchases — and see exactly which traffic source (Google Ads, Meta Ads, organic, etc.) drove each one.

1. Define goals in the dashboard

Go to Settings → Conversions and create a goal. Each goal has a human-readable name and a slug you reference in code:

| Name | Slug | |------|------| | User Created | user_created | | Money Deposit | money_deposit | | Subscription Started | subscription_started |

2. Call trackConversion() in your code

import { useNohmo } from 'nohmo'

export default function SignupSuccess() {
  const { trackConversion } = useNohmo()

  useEffect(() => {
    trackConversion('user_created')
  }, [])
}

Pass optional properties for richer data:

trackConversion('money_deposit', { amount: 500, currency: 'USD' })

Plain HTML / Django templates:

<script>
  window.nohmo.conversion('money_deposit', { amount: 500 })
</script>

3. See results in Traffic → Conversions

The Traffic page has a Conversions tab showing total conversions broken down by UTM source, medium, campaign, and custom attribution parameters. Filter by a specific goal to drill into which channels drive that conversion type.

Attribution is automatic — if the user arrived via ?utm_source=google&utm_medium=cpc, that conversion is attributed to Google CPC with no extra code.


What gets tracked automatically

| Event | Trigger | Data | |-------|---------|------| | PAGE_VIEW | Every route change (Next.js) or usePageView() | page, referrer | | TIME_SPENT | When navigating away from a page | seconds | | SCROLL_DEPTH | At 25 / 50 / 75 / 100% scroll milestones | depth | | CLICK | Click on any interactive element | tag, text, href | | RAGE_CLICK | Three or more rapid clicks in the same spot | tag, text | | FORM_SUBMIT | Submission of any <form> | tag, text | | INPUT_CHANGE | Change on any <input>, <select>, or <textarea> | tag, text | | JS_ERROR | Uncaught exception or unhandled promise rejection | message, stack, filename, lineno | | HTTP_ERROR | A fetch/XHR request returning 4xx/5xx, or a resource (img/script/css) that fails to load | status, method, url, kind | | USER_LINKED | When linkUser() is called | email |

Disable any category via the options prop.

Privacy: FORM_SUBMIT and INPUT_CHANGE never capture field values — only that the interaction happened. Inputs marked data-sensitive, password fields, and credit-card fields (autocomplete="cc-*") are skipped entirely, as is any element carrying the data-nohmo-ignore attribute.

Error & crash tracking

Error tracking is on by default. The SDK captures:

  • Web — uncaught JS exceptions and unhandled promise rejections (JS_ERROR), plus failed fetch/XHR requests (4xx/5xx) and resource 404s (HTTP_ERROR).
  • React Native — non-fatal JS errors (JS_ERROR) and fatal crashes (APP_CRASH), including native crashes: Android Java/Kotlin uncaught exceptions, and iOS Objective-C exceptions and Swift/signal crashes (force-unwraps, fatalError, segfaults). Crashes are persisted natively and reported on the next app launch, attributed back to the session — and journey — they happened in.

Every error is just an event, so it carries the same session and device context as everything else — meaning the dashboard's Errors page can show you not just what broke but the journey leading up to the crash: the exact sequence of pages/screens, clicks, and taps right before it. Group errors are deduplicated by signature, with affected users, devices, and a sample stack trace.

Real-time alerts: add an Event Match webhook (Settings → Webhooks) on JS_ERROR or APP_CRASH to get notified the moment errors happen.

Scope: native capture covers uncaught JVM exceptions (Android) and Obj-C exceptions + signal crashes (iOS) — not Android NDK/C++ crashes or ANRs. Stack traces are raw / unsymbolicated for now (dSYM & ProGuard symbolication are planned).

Turn it off:

// React / Next.js / React Native
<NohmoProvider options={{ autoErrors: false }} … />
<!-- Script tag -->
<script src="…/n.min.js" data-project="…" data-api-key="…" data-errors="false" defer></script>

Privacy: error messages are truncated, query strings are stripped from captured URLs, and the SDK never reports failures of its own tracking endpoint.

UTM attribution

UTM parameters are captured automatically on the first page load of each session and sent with every subsequent event. No extra code needed.

https://yourapp.com?utm_source=google&utm_medium=cpc&utm_campaign=spring-sale

Supported parameters: utm_source, utm_medium, utm_campaign, utm_term, utm_content.

Parameters are stored in sessionStorage so they persist across SPA navigations even when the user lands on a clean URL. Attribution is first-touch per session. Results appear in the Traffic dashboard.

Custom attribution parameters

Not everyone uses full UTM strings. Nohmo lets you define short custom parameter names (e.g. ?ref=, ?from=, ?via=) that are treated as attribution when no standard utm_* params are present.

Configure in the dashboard — go to Settings → General → Attribution parameters and add the parameter names you want to track. Changes take effect on the next page load; no code change or SDK rebuild needed.

# Examples of URLs that will be attributed automatically
https://yourapp.com?ref=meta_ads          → source: meta_ads, medium: ref
https://yourapp.com?from=newsletter       → source: newsletter, medium: from
https://yourapp.com?via=partner_site      → source: partner_site, medium: via

The SDK fetches your configured list from the backend when it initialises, so the same configuration works across every framework (Next.js, React, plain HTML, Django templates) without any local config.

?ref= is always supported as a built-in default, even before you add anything in the dashboard.


Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | flushInterval | number | 3000 | Milliseconds between batch event flushes | | debug | boolean | false | Log all events and state to the browser console | | autoPageView | boolean | true | Send PAGE_VIEW on every route change (Next.js only) | | autoScrollDepth | boolean | true | Track scroll depth at 25 / 50 / 75 / 100% | | autoTimeSpent | boolean | true | Send TIME_SPENT when leaving a page | | autoCapture | boolean | true | Capture clicks, rage-clicks, form submits, and input changes automatically (field values are never captured) | | autoErrors | boolean | true | Capture uncaught JS errors, unhandled rejections, failed network requests, and resource 404s as JS_ERROR / HTTP_ERROR |

<NohmoNextProvider
  projectId="..."
  apiKey="..."
  options={{
    flushInterval: 5000,
    debug: true,
    autoScrollDepth: false,
  }}
>
  {children}
</NohmoNextProvider>

What the dashboard shows

| Dashboard page | What you get | |----------------|-------------| | Overview | Event volume chart, unique devices, sessions, avg time spent, top pages | | Devices | Every device with browser, OS, screen size, timezone, country, city, last seen, pages visited | | Device journey | Full chronological event history per device, grouped by session | | Live feed | Real-time event stream via WebSocket — see who is on your site right now | | Events | GA4-style top actions ranked by count / users / per-user, an activity breakdown by event type, and a live recent-activity feed | | Journeys | Page flows (which path users take from page to page) plus entry & exit pages with bounce and exit rates | | Traffic → Attribution | Session breakdown by UTM source, medium, campaign, and custom attribution params | | Traffic → Conversions | Conversion counts by goal, source, medium, campaign — shows which ads drove results | | App analytics | Installs, DAU/MAU, D1/D7/D30 retention, uninstalls & uninstall rate, reinstalls, crashes, platform split, app versions, top screens, and install attribution | | Settings → Webhooks | Friction triggers — fire an HMAC-signed HTTP webhook in real time on rage clicks, a friction-score threshold, or a matched event |

How it works

  1. On first load, a 128-bit random device ID is generated via the Web Crypto API and stored in localStorage. Subsequent visits on the same browser reuse it.
  2. The SDK registers the device with the Nohmo backend, recording browser, OS, screen resolution, timezone, and language. The backend resolves GeoIP location from the request IP.
  3. UTM parameters are read from the URL and stored in sessionStorage for the duration of the session.
  4. Events are queued locally and flushed in batches via navigator.sendBeacon. Each event carries the device ID, session ID, page, timestamp, and any UTM context.
  5. When linkUser() is called, the device is associated with a real user identity server-side. All prior anonymous events are attributed to that user retroactively.

Pricing

One plan: $49/month per project — unlimited events, no per-event overages. Every project starts with a free 4-day trial (no card required).

🎉 Free during early access. While Nohmo is in early access, the full platform is free. Email [email protected] and we'll unlock your project at no cost.

See full pricing →

License

MIT