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

@compounding/logger

v1.8.0

Published

Event logging SDK for React Native and web: session/client id lifecycle, a persistent retry queue that survives restarts, and bot filtering on by default.

Readme

@compounding/logger

Event logging SDK for React Native and web apps. One implementation covers both: storage and platform metadata are injected, so no platform-specific code lives in this package.

  • Session / client id lifecycle — 30-minute inactivity timeout, evaluated on every call rather than by a background timer.
  • Persistent retry queue — events are stored before they are sent and only dropped once the server acknowledges them, so they survive offline periods and app restarts.
  • Bot filtering — crawler and automation traffic is discarded before it is queued. On by default.
npm install @compounding/logger

What you need

A project id (a number), issued by whoever administers your analytics dashboard. There is no API key or token — ask for the id and you are done.

Use the id you were given. The ingestion endpoint is unauthenticated, so a wrong project id does not fail — it silently writes your events into someone else's project. Nothing surfaces until someone opens the dashboard.

The default endpoint is https://api2.bake-now.com. Override it with the endpoint option if you were pointed at a different environment.

Quick start — React Native

AsyncStorage can back both the persistent and the session storage.

import AsyncStorage from '@react-native-async-storage/async-storage';
import { AppState, Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { createCompLogger, StorageAdapter } from '@compounding/logger';

const asyncAdapter: StorageAdapter = {
  get: key => AsyncStorage.getItem(key).then(v => v ?? undefined),
  set: (key, value) => AsyncStorage.setItem(key, value),
  remove: key => AsyncStorage.removeItem(key),
};

export const CompLogger = createCompLogger({
  projectId: YOUR_PROJECT_ID,
  platform: Platform.OS,
  persistentStorage: asyncAdapter,
  sessionStorage: asyncAdapter,
  collectMetadata: async () => ({
    deviceInfo: await DeviceInfo.getDeviceName(),
    appVersion: await DeviceInfo.getVersion(),
  }),
  // Flush whatever is queued as soon as the app comes back to the foreground.
  onResume: flush => {
    const sub = AppState.addEventListener('change', s => {
      if (s === 'active') flush();
    });
    return () => sub.remove();
  },
});

React Native has no navigator.userAgent, so bot filtering never matches there and is a no-op. Nothing to turn off.

Quick start — Web

Use localStorage for persistent storage and sessionStorage for session storage. Getting these two the wrong way round is the most common setup mistake — see Pitfalls.

import { createCompLogger, StorageAdapter } from '@compounding/logger';

const localAdapter: StorageAdapter = {
  get: async key => localStorage.getItem(key) ?? undefined,
  set: async (key, value) => localStorage.setItem(key, value),
  remove: async key => localStorage.removeItem(key),
};
const sessionAdapter: StorageAdapter = {
  get: async key => sessionStorage.getItem(key) ?? undefined,
  set: async (key, value) => sessionStorage.setItem(key, value),
  remove: async key => sessionStorage.removeItem(key),
};

export const CompLogger = createCompLogger({
  projectId: YOUR_PROJECT_ID,
  platform: 'web',
  persistentStorage: localAdapter,
  sessionStorage: sessionAdapter,
  collectMetadata: async () => ({ userAgent: navigator.userAgent }),
  // Called once, only the first time this install needs a new client id.
  collectClientIdSeed: async () =>
    `${navigator.userAgent}_${screen.width}x${screen.height}`,
  onResume: flush => {
    const onOnline = () => flush();
    const onVisible = () => {
      if (document.visibilityState === 'visible') flush();
    };
    window.addEventListener('online', onOnline);
    document.addEventListener('visibilitychange', onVisible);
    return () => {
      window.removeEventListener('online', onOnline);
      document.removeEventListener('visibilitychange', onVisible);
    };
  },
  // Without this, an event logged immediately before the page goes away dies
  // inside the 100ms debounce, before it ever reaches the persistent queue.
  onSuspend: flush => {
    const onHide = () => flush();
    const onHidden = () => {
      if (document.visibilityState === 'hidden') flush();
    };
    window.addEventListener('pagehide', onHide);
    document.addEventListener('visibilitychange', onHidden);
    return () => {
      window.removeEventListener('pagehide', onHide);
      document.removeEventListener('visibilitychange', onHidden);
    };
  },
  // Last chance for a visitor who leaves and never comes back (app store,
  // external link). fetch+keepalive rather than sendBeacon: the endpoint is
  // cross-origin JSON, so the request needs a normal CORS preflight.
  beaconSend: (url, payload) => {
    void fetch(url, {
      method: 'POST',
      keepalive: true,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    }).catch(() => {});
    return true;
  },
});

Call createCompLogger() once in a module you can import from anywhere, and reuse that instance.

API

CompLogger.log(name: string, params?: Record<string, unknown>, options?: LogOptions): Promise<void>;
CompLogger.logPageView(page: string, params?: Record<string, unknown>): Promise<void>;
CompLogger.logClick(name: string, params?: Record<string, unknown>): Promise<void>;
CompLogger.setLoginUser(userId: string): void;
CompLogger.unsetLoginUser(): void;
CompLogger.setUserProperties(properties: Record<string, unknown>): Promise<void>;

Call setLoginUser right after sign-in to attribute subsequent events to that user, and unsetLoginUser on sign-out. setUserProperties is only meaningful once a login user is set.

Also exported: isBotClient(), isBotUserAgent(ua), getSessionId(), getClientId().

log() is fire-and-forget by default

log() resolves once its 100 ms debounce timer is scheduled — awaiting it does not mean the event was stored. If the calling context may not outlive that window (a headless background task handling a push open, a flush on unload), set immediate so you can hold the context open until the event is recoverable:

await CompLogger.log('push_opened', { id }, { immediate: true });

immediate skips debouncing, so repeated identical events are no longer collapsed.

Attributing a single event to a specific user

The global login user is read at send time. If setLoginUser / unsetLoginUser runs between your log() call and the actual send, that event's attribution changes with it. Usually harmless — but for events where the user at call time is the fact you want recorded (a push open that arrives while the app is still resolving its login state), pass userId to pin it:

// Applies to this event only; the global login user is left untouched.
await CompLogger.log('push_opened', { id }, { userId, immediate: true });

An empty string means "this event is anonymous" and clears the attribution even when a global user is set. Omit the option entirely to keep the default global behaviour.

Configuration

| Option | | Description | | --- | --- | --- | | projectId | required | The project number you were issued. | | platform | required | Metadata tag: 'ios' / 'android' / 'web'. | | persistentStorage | required | Holds the client id and the retry queue. Must survive restarts. | | sessionStorage | required | Holds the session id and its last-active-at marker. | | endpoint | optional | Default https://api2.bake-now.com. | | collectMetadata | optional | Device/browser metadata attached to every log. | | collectClientIdSeed | optional | Fingerprint entropy, used once when a client id is first created. Defaults to platform alone. | | onResume | optional | Subscribe to "app/tab became active"; call flush, return an unsubscribe function. | | onSuspend | optional | Subscribe to "app/tab is going away" (web pagehide, RN AppState background). Drains pending debounced events into the queue. Web hosts should always set this. | | beaconSend | optional | Send during unload, returning whether the browser accepted it. Only reachable path for a visitor who never returns. Web only — React Native has no equivalent. | | sessionTimeoutMs | optional | Inactivity window before a new session id. Default 30 minutes. | | skipBots | optional | Default true. Set false only if crawler traffic is what you are measuring. | | injectedSessionId | optional | Use an externally supplied session id verbatim. See below. | | injectedClientId | optional | Same, for the client id. |

Sharing ids with a wrapping native app

When a native app embeds a webview, the web side generates its own session and the same user's activity splits across two sessions. Pass the native ids as injectedSessionId / injectedClientId to line them up. How the values reach the config — postMessage, an injected global, a URL parameter — is entirely up to the host app.

const injected = typeof window !== 'undefined' ? window.__NATIVE_APP_IDS__ : undefined;

export const CompLogger = createCompLogger({
  projectId: YOUR_PROJECT_ID,
  platform: 'web',
  persistentStorage,
  sessionStorage,
  injectedSessionId: injected?.sessionId,
  injectedClientId: injected?.clientId,
});

An injected id is used verbatim and bypasses the storage-backed lifecycle entirely — nothing is read from or written to storage for it. This is one-directional (native → web): web activity does not extend the native app's session timeout.

How it behaves

Sessions

A new session_id is issued when more than sessionTimeoutMs (default 30 minutes, matching the Amplitude/GA convention) has passed since the last log call. The check runs on every call rather than relying on a background timer, so an app that was closed for days is correctly treated as a new session.

Bot filtering

Events from bots, crawlers and automation tools are dropped before they are queued. Detection looks at navigator.userAgent, the navigator.webdriver flag (so UA-spoofing automation is still caught), and the userAgent returned by collectMetadata().

This lives in the package because crawler noise is not one app's problem. When each app wrapped its own filter the rules diverged, and new apps started with no filter at all — inflating page-view denominators while click events stayed real, so every conversion rate read far below its true value.

False positives are treated as worse than false negatives: a real user's events disappearing is invisible, while some extra bot noise is not. The UA pattern deliberately avoids tokens that can appear in real browsers, and in-app webviews (KakaoTalk, Naver, Instagram) are pinned as non-matching in the test suite against user agents taken from production logs.

To skip work of your own for a crawler whose events would be discarded anyway:

import { isBotClient } from '@compounding/logger';

useEffect(() => {
  if (isBotClient()) return; // no observers, no listeners, no beacons
  // ...attach analytics listeners
}, []);

Retry queue

Requests are persisted before they are attempted and removed only once the server acknowledges them (or the response makes retrying pointless). Anything left over resumes on the next launch or visit. Writes always merge against the current storage contents, so multiple browser tabs sharing an origin never clobber each other's in-flight entries.

| | | | --- | --- | | Max retries | 8 | | Queue cap | 500 events (oldest dropped, with a console warning) | | Backoff | 1 s, doubling, capped at 30 s |

| Response | Handling | | --- | --- | | Network failure | Retried — the request may never have reached the server. | | 429 / 5xx | Retried as transient. | | 400 | Dropped, with the reason logged. The payload is wrong; retrying changes nothing. | | 403 | Dropped, and all sending pauses for 5 minutes. Resending would only re-trigger the block. |

Pitfalls

  • Leaving a placeholder project id in place. Ingestion is unauthenticated, so a wrong id writes silently into another project. Verify it before your first commit.
  • Treating await log() as a durability guarantee. It is not; use { immediate: true } where the context may disappear.
  • Logging right before navigating away, on web, without onSuspend. An <a href> click, a location.href assignment, or a tab close destroys the 100ms debounce timer before the event reaches the queue, so it is not merely delayed — it never existed. Wire onSuspend (and beaconSend if visitors may not return), or pass { immediate: true } and await it before navigating.
  • Swapping the two storages on web. localStorage in the sessionStorage slot means sessions never end; sessionStorage in the persistentStorage slot loses the client id and the pending queue every time the tab closes. On React Native both being AsyncStorage is correct.
  • Long offline periods. Past 500 queued events the oldest are dropped. Check for [CompLogger] console warnings if that is plausible for your app.

License

UNLICENSED — proprietary. Published to npm for distribution convenience only; this is not a grant of permission to use, copy, or redistribute. Contact the package owner for usage terms.