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

@gaozh1024/rn-observatory

v0.4.0

Published

Observability and product analytics SDK for Expo / React Native.

Readme

@gaozh1024/rn-observatory

Observability and product analytics SDK for Expo / React Native apps.

It captures global JavaScript errors, unhandled promise rejections, API failures, manual business exceptions, breadcrumbs, app/session observability events, queued uploads, and inferred previous-session crashes. Native process crashes are intentionally adapter-based so the core package stays vendor-neutral.

Install

pnpm add @gaozh1024/rn-observatory

Peer dependencies are provided by your app:

pnpm add react react-native

Quick start with rn-kit

import { AppProvider } from '@gaozh1024/rn-kit';
import { AppObservatoryProvider } from '@gaozh1024/rn-observatory';

export default function App() {
  return (
    <AppObservatoryProvider
      enabled={!__DEV__}
      appId="mobile-app"
      appVersion="1.2.3"
      buildNumber="45"
      endpoint="https://api.example.com/api/app-observatory/events"
      ingestToken="your-ingest-token"
    >
      {health => (
        <AppProvider enableErrorBoundary enableLogger={__DEV__} healthReporter={health}>
          <RootNavigator />
        </AppProvider>
      )}
    </AppObservatoryProvider>
  );
}

When connected to rn-kit, React render errors from AppErrorBoundary are reported through captureException, and logger writes become health breadcrumbs.

Anonymous identity and consent

For behavior analytics, use a stable anonymous install ID instead of phone numbers, emails, or real names. rn-observatory can generate and persist an install ID through the configured storage adapter:

<AppObservatoryProvider
  enabled={!__DEV__}
  appId="mobile-app"
  endpoint="https://api.example.com/api/app-observatory/events"
  ingestToken="your-ingest-token"
  storage={createAsyncStorageObservatoryStorage(AsyncStorage)}
  identity={{ autoInstallId: true }}
  consent={{
    crash: privacyConsent.diagnostics,
    analytics: privacyConsent.analytics,
    device: privacyConsent.analytics,
  }}
>
  {health => (
    <AppProvider enableErrorBoundary healthReporter={health}>
      {children}
    </AppProvider>
  )}
</AppObservatoryProvider>

When identity.autoInstallId is enabled, events are tagged with installId. If no userId is provided, the install ID is also used as the anonymous user.id. After login, prefer a hashed business user ID:

health.setUser({ id: `user_${hashUserId(user.id)}` });

After logout, switch back to the anonymous install ID if you still want anonymous diagnostics.

consent separates diagnostics from analytics:

  • crash: JavaScript errors, React errors, unhandled rejections, previous-session crash inference, native crash adapter.
  • analytics: trackEvent, trackScreen, and app lifecycle analytics events.
  • device: optional extended device information such as model and brand.

Behavior analytics

Use trackScreen for page visits and trackEvent for user actions. Both are no-ops unless consent.analytics is true.

const health = useAppObservatory();

await health.trackScreen('Home');

await health.trackEvent('button.click', {
  screen: 'Home',
  target: 'submit-order',
});

await health.trackEvent('order.success', {
  screen: 'Checkout',
  paymentMethod: 'wechat',
});

Analytics events are uploaded through the same queue and transport as error events, with type: "analytics_event" or type: "screen_view" and an analytics payload.

Extended device information

The core SDK does not depend on expo-device or react-native-device-info. Provide device model/brand yourself when the user has granted analytics/device consent:

import * as Device from 'expo-device';

<AppObservatoryProvider
  consent={{ analytics: privacyConsent.analytics, device: privacyConsent.analytics }}
  deviceInfoProvider={() => ({
    model: Device.modelName ?? undefined,
    brand: Device.brand ?? undefined,
  })}
/>;

Manual capture

import { useAppObservatory } from '@gaozh1024/rn-observatory';

function SubmitButton() {
  const health = useAppObservatory();

  async function submit() {
    health.addBreadcrumb({ category: 'ui', message: '点击提交订单' });

    try {
      await submitOrder();
    } catch (error) {
      await health.captureException(error, {
        source: 'order.submit',
        tags: { scene: 'checkout' },
      });
    }
  }
}

Upload protocol

The built-in fetch transport sends batches to:

POST /api/app-observatory/events
Content-Type: application/json

Payload:

{
  "events": [
    {
      "id": "evt_xxx",
      "type": "js_error",
      "level": "error",
      "timestamp": 1710000000000,
      "app": { "id": "mobile-app", "version": "1.2.3", "buildNumber": "45" },
      "device": { "platform": "ios", "osVersion": "17.0" },
      "session": { "id": "sess_xxx", "startedAt": 1710000000000 },
      "error": { "name": "TypeError", "message": "boom", "stack": "...", "fingerprint": "fp_xxx" },
      "breadcrumbs": []
    }
  ]
}

A 2xx response is treated as success. Failed uploads remain queued and are retried on later flush() calls. If ingestToken is provided, the built-in fetch transport sends authorization: Bearer <ingestToken> automatically; explicit headers.authorization takes precedence.

Production setup checklist

For production apps, prefer the full setup below:

import AsyncStorage from '@react-native-async-storage/async-storage';
import { AppProvider } from '@gaozh1024/rn-kit';
import {
  AppObservatoryProvider,
  createAsyncStorageObservatoryStorage,
} from '@gaozh1024/rn-observatory';

export default function App() {
  return (
    <AppObservatoryProvider
      enabled={!__DEV__}
      appId="mobile-app"
      appVersion="1.2.3"
      buildNumber="45"
      environment="production"
      endpoint="https://api.example.com/api/app-observatory/events"
      ingestToken="your-ingest-token"
      transportTimeoutMs={10_000}
      storage={createAsyncStorageObservatoryStorage(AsyncStorage)}
    >
      {health => (
        <AppProvider enableErrorBoundary healthReporter={health}>
          <RootNavigator />
        </AppProvider>
      )}
    </AppObservatoryProvider>
  );
}

Production notes:

  • MemoryObservatoryStorage is only for tests and demos; inject persistent storage so queued events and previous-session crash inference survive process restarts.
  • When using @gaozh1024/rn-kit, explicitly set enableErrorBoundary in production if you want React render errors reported.
  • Unhandled rejection capture is best-effort across React Native runtimes; it supports DOM unhandledrejection, global event targets, and globalThis.onunhandledrejection fallback when available.
  • Native process crashes require nativeCrashAdapter; the core package stays vendor-neutral and does not include a native crash SDK.
  • Source map symbolication is not included in 0.2.x; production stack traces are uploaded raw.
  • Do not upload raw authorization headers, cookies, request/response bodies, phone numbers, or tokens. Keep or customize the sanitizer.

Persistent storage

The default MemoryObservatoryStorage is useful for tests and simple demos. Production apps should inject persistent storage so queued events and previous-session crash inference survive process restarts.

import AsyncStorage from '@react-native-async-storage/async-storage';
import { createAsyncStorageObservatoryStorage } from '@gaozh1024/rn-observatory';

<AppObservatoryProvider storage={createAsyncStorageObservatoryStorage(AsyncStorage)} />;

Transport timeout

The built-in fetch transport aborts uploads after 10_000ms by default so monitoring cannot hang indefinitely behind a stuck network request. Override it with transportTimeoutMs, or set 0 to disable the abort timeout.

<AppObservatoryProvider
  endpoint="https://api.example.com/api/app-observatory/events"
  transportTimeoutMs={5_000}
/>

API error capture

0.2.0 adds dependency-free helpers for API monitoring. They capture network errors and 5xx responses by default. 4xx capture is opt-in to avoid noisy user-input errors. URLs are sanitized by default by removing query strings and hashes; request/response bodies and headers are not uploaded.

fetch

import { createMonitoredFetch, useAppObservatory } from '@gaozh1024/rn-observatory';

function useApiFetch() {
  const health = useAppObservatory();
  return createMonitoredFetch(fetch, health, {
    tags: { client: 'fetch' },
    capture4xx: false,
  });
}

axios

import axios from 'axios';
import {
  installAxiosObservatoryInterceptor,
  type AppObservatoryReporter,
} from '@gaozh1024/rn-observatory';

function installApiMonitoring(health: AppObservatoryReporter) {
  return installAxiosObservatoryInterceptor(axios, health, {
    tags: { client: 'axios' },
    capture4xx: false,
  });
}

Call the disposer returned by installAxiosObservatoryInterceptor when the axios instance or app shell is torn down.

Sanitization

The default sanitizer recursively redacts keys containing:

  • password
  • token
  • accessToken
  • refreshToken
  • authorization
  • cookie
  • phone
  • idCard
  • email

You can provide a custom sanitizer:

<AppObservatoryProvider
  sanitize={event => ({
    ...event,
    extra: undefined,
  })}
/>

Native crash adapter

Pure JavaScript cannot reliably capture native process crashes after the process dies. Use nativeCrashAdapter to bridge Sentry, Firebase Crashlytics, or a self-hosted native crash module.

<AppObservatoryProvider
  nativeCrashAdapter={{
    install: () => nativeCrashSdk.install(),
    getPendingCrashReports: () => nativeCrashSdk.getPendingReports(),
    clearPendingCrashReports: ids => nativeCrashSdk.clearReports(ids),
  }}
/>

Public APIs

  • AppObservatoryProvider
  • useAppObservatory
  • createAppObservatoryClient
  • createAppObservatoryQueue
  • createFetchObservatoryTransport
  • createAsyncStorageObservatoryStorage
  • createMonitoredFetch
  • installAxiosObservatoryInterceptor
  • defaultAppObservatorySanitizer
  • MemoryObservatoryStorage
  • installGlobalErrorHandlers

Notes

  • previous_session_crash is an abnormal-exit inference, not a guaranteed native crash report.
  • Do not upload raw authorization headers, request bodies, phone numbers, or tokens without a sanitizer.
  • Do not block user operations on monitoring upload success.