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

@logg/signals

v0.3.0

Published

Universal event tracking SDK for Logg Signals, with an embeddable on-device recommender

Readme

@logg/signals

Universal event tracking SDK for Logg Signals. Track events from web, React Native, and Node.js applications.

Also ships an embeddable on-device recommender as a separate entry point — see On-Device Recommender.

Version 0.3.0

Features

Universal - Works in browsers, React Native, and Node.js
Type-safe - Full TypeScript support
Automatic batching - Efficient event batching with configurable thresholds
Persistent storage - Uses localStorage, AsyncStorage, or memory as fallback
Retry logic - Exponential backoff for failed requests
Auto metadata - Automatically collects browser/device information
Small bundle - <5KB gzipped (tracking entry; recommender is a separate opt-in entry)

Installation

npm install @logg/signals

For React Native, also install AsyncStorage:

npm install @react-native-async-storage/async-storage

Quick Start

Web (Browser)

import { Signals } from '@logg/signals';

const signals = new Signals({
  apiKey: 'your-api-key',
  endpoint: 'https://signals.yourdomain.com/events',
});

// Track events
signals.event({
  type: 'page_view',
  page: '/dashboard',
  userId: '12345',
});

signals.event({
  type: 'button_click',
  element: 'signup_cta',
  userId: '12345',
});

React Native

import { Signals } from '@logg/signals';
import AsyncStorage from '@react-native-async-storage/async-storage';

const signals = new Signals({
  apiKey: 'your-api-key',
  endpoint: 'https://signals.yourdomain.com/events',
  // AsyncStorage is auto-detected, but you can pass it explicitly
});

// Track events
signals.event({
  type: 'screen_view',
  screen: 'HomeScreen',
  userId: user.id,
});

signals.event({
  type: 'purchase',
  productId: 'premium-plan',
  amount: 29.99,
  userId: user.id,
});

Node.js

import { Signals } from '@logg/signals';

const signals = new Signals({
  apiKey: 'your-api-key',
  endpoint: 'https://signals.yourdomain.com/events',
});

// Track server-side events
await signals.event({
  type: 'api_call',
  endpoint: '/api/users',
  method: 'POST',
  userId: req.user.id,
});

// Make sure to flush before process exit
process.on('beforeExit', async () => {
  await signals.flush();
});

Configuration

const signals = new Signals({
  // Required
  apiKey: 'your-api-key',
  endpoint: 'https://signals.yourdomain.com/events',

  // Optional
  batchSize: 10,           // Send after 10 events (default: 10)
  batchInterval: 5000,     // Or every 5 seconds (default: 5000)
  maxRetries: 3,           // Retry failed requests 3 times (default: 3)
  retryDelay: 1000,        // Initial retry delay in ms (default: 1000)
  debug: false,            // Enable debug logging (default: false)
  sessionId: 'custom-id',  // Custom session ID (auto-generated by default)
  storage: customAdapter,  // Custom storage adapter (auto-detected by default)
});

API Reference

signals.event(eventData)

Track an event. Events are automatically batched and sent based on batchSize and batchInterval config.

await signals.event({
  type: 'event_type',      // Required: event type
  userId: 'user-123',      // Optional: user ID
  // ... any other properties
});

Auto-added fields:

  • event_id - Unique event identifier (UUID v4)
  • timestamp - ISO 8601 timestamp
  • session_id - Session identifier
  • client - Client metadata (type, version, user_agent, screen, locale, timezone)

signals.flush()

Manually flush all pending events immediately.

await signals.flush();

signals.getSessionId()

Get the current session ID.

const sessionId = signals.getSessionId();

signals.getQueueSize()

Get the number of events in the queue.

const queueSize = signals.getQueueSize();

signals.destroy()

Destroy the client. Drains the entire queue (flushes batches until empty or sends start stalling) before shutting down. Always await this on process exit / app teardown — anything left in the in-memory queue after the Node process exits is lost.

await signals.destroy();

signals.flushAll()

Drain the queue without destroying the client. Useful for backfill scripts that want a checkpoint before moving on. Returns the number of events that could not be sent (zero on full success).

const stranded = await signals.flushAll();
if (stranded > 0) {
  console.warn(`${stranded} events failed to deliver`);
}

signals.on('error', listener) / signals.off('error', listener)

Subscribe to delivery errors. signals.event() and signals.flush() never throw on backend failures, so this is how you observe them. The listener receives a SignalsErrorEvent:

const off = signals.on('error', (e) => {
  // e.type: 'send_failed' | 'send_retry' | 'destroy_pending'
  // e.message:      human-readable description
  // e.error:        the underlying Error
  // e.batchId:      uuid of the failing/retrying batch (if applicable)
  // e.eventCount:   number of events in the affected batch
  // e.pendingCount: number of events still queued after the failure
  // e.attempt:      1-indexed retry attempt (for 'send_retry' only)
  console.error('[signals]', e.type, e.message, e.pendingCount, 'queued');
});

// later
off();

If no error listener is attached, the SDK falls back to console.warn so failures are at least visible during development.

Event Batching

Events are automatically batched to reduce network requests:

  1. Batch by size: Sends when batchSize events are queued (default: 10)
  2. Batch by time: Sends every batchInterval milliseconds (default: 5000)
  3. Manual flush: Call signals.flush() to send immediately

Batch format sent to backend:

{
  "api_key": "your-api-key",
  "batch_id": "batch-uuid",
  "timestamp": "2025-12-02T10:30:00.000Z",
  "metadata": {
    "type": "web",
    "version": "0.1.0",
    "user_agent": "Mozilla/5.0...",
    "screen": { "width": 1920, "height": 1080 },
    "locale": "en-US",
    "timezone": "America/New_York"
  },
  "events": [
    {
      "event_id": "uuid-1",
      "timestamp": "2025-12-02T10:30:00.000Z",
      "session_id": "session-uuid",
      "type": "page_view",
      "userId": "12345",
      "page": "/dashboard"
    }
  ]
}

Storage Adapters

The SDK automatically detects the best storage adapter:

  1. Web: LocalStorageAdapter (uses localStorage)
  2. React Native: AsyncStorageAdapter (uses @react-native-async-storage/async-storage)
  3. Node.js: MemoryStorageAdapter (in-memory, no persistence)

Custom Storage Adapter

You can provide a custom storage adapter:

import { Signals, StorageAdapter } from '@logg/signals';

class CustomStorageAdapter implements StorageAdapter {
  async getItem(key: string): Promise<string | null> {
    // Your implementation
  }

  async setItem(key: string, value: string): Promise<void> {
    // Your implementation
  }

  async removeItem(key: string): Promise<void> {
    // Your implementation
  }
}

const signals = new Signals({
  apiKey: 'your-api-key',
  endpoint: 'https://signals.yourdomain.com/events',
  storage: new CustomStorageAdapter(),
});

Error Handling

The SDK includes automatic retry logic with exponential backoff:

  • Failed requests are retried up to maxRetries times (default: 3)
  • Retry delay doubles after each attempt (exponential backoff)
  • Events are persisted in storage and retried on next batch
const signals = new Signals({
  apiKey: 'your-api-key',
  endpoint: 'https://signals.yourdomain.com/events',
  maxRetries: 5,      // Retry up to 5 times
  retryDelay: 2000,   // Start with 2 second delay
  debug: true,        // Log retry attempts
});

React Integration

Track Page Views

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();

  useEffect(() => {
    signals.event({
      type: 'page_view',
      page: location.pathname,
      title: document.title,
    });
  }, [location]);

  return <div>...</div>;
}

Track User Actions

function SignupButton() {
  const handleClick = () => {
    signals.event({
      type: 'button_click',
      element: 'signup_cta',
      page: '/landing',
    });

    // Navigate to signup...
  };

  return <button onClick={handleClick}>Sign Up</button>;
}

React Native Integration

import { Signals } from '@logg/signals';
import { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';

const signals = new Signals({
  apiKey: 'your-api-key',
  endpoint: 'https://signals.yourdomain.com/events',
});

function HomeScreen() {
  const navigation = useNavigation();

  useEffect(() => {
    // Track screen view
    signals.event({
      type: 'screen_view',
      screen: 'HomeScreen',
    });
  }, []);

  return (
    <Button
      title="Buy Premium"
      onPress={() => {
        signals.event({
          type: 'button_press',
          button: 'buy_premium',
          screen: 'HomeScreen',
        });
        navigation.navigate('Checkout');
      }}
    />
  );
}

Best Practices

1. Initialize Once

Create a single instance and reuse it:

// lib/signals.ts
import { Signals } from '@logg/signals';

export const signals = new Signals({
  apiKey: process.env.SIGNALS_API_KEY!,
  endpoint: process.env.SIGNALS_ENDPOINT!,
});

// app.tsx
import { signals } from './lib/signals';

signals.event({ type: 'app_opened' });

2. Flush on Exit

Always flush events before the app closes:

// React Native
useEffect(() => {
  return () => {
    signals.flush();
  };
}, []);

// Node.js
process.on('beforeExit', async () => {
  await signals.flush();
});

3. Type-safe Events

Define your event types for better DX:

type AppEvent =
  | { type: 'page_view'; page: string; title: string }
  | { type: 'button_click'; element: string }
  | { type: 'purchase'; productId: string; amount: number };

const signals = new Signals({...});

function trackEvent(event: AppEvent) {
  signals.event(event);
}

// Now fully type-safe!
trackEvent({ type: 'page_view', page: '/home', title: 'Home' });

4. User Identification

Include user ID in all events:

function trackUserEvent(event: Omit<EventData, 'userId'>) {
  const userId = getCurrentUserId();
  signals.event({ ...event, userId });
}

On-Device Recommender (@logg/signals/reco)

A pure-TypeScript, zero-dependency recommendation engine that runs entirely on the client — ship it inside a React Native bundle, a web app, or a Node process. The backend hands the app a flat catalog (a few thousand items × a few feature columns); the device ranks it live as the user scrolls, dwells, taps, and favourites.

It lives at its own entry point so tracking-only consumers don't pay for it:

import { Recommender, DualBucketRecommender, SIGNALS } from '@logg/signals/reco';
import type { BaseItem, Schema } from '@logg/signals/reco';

Two engines ship behind the same surface — pick at construction time:

| Engine | Class | Best for | |---|---|---| | v1 | Recommender | Probabilistic exploration, smoother defaults | | v2 | DualBucketRecommender | Explicit liked/disliked separation, tighter exploit |

The library is domain-agnostic and generic over your item shape. You bring an item type extending BaseItem (only id is required) and a Schema<T> that extracts categorical feature values from each item — the engine has no built-in vocabulary of brands, prices, or categories.

import { Recommender, SIGNALS, logDecadeBucket, type BaseItem, type Schema } from '@logg/signals/reco';

interface Listing extends BaseItem {
  brand: string | null;
  category: string | null;
  price_cents: number | null;
  popularity: number;
}

const schema = {
  brand: { extract: (i: Listing) => i.brand, capacity: 4, weight: 0.4 },
  category: { extract: (i: Listing) => i.category, capacity: 2, weight: 0.2 },
  price_band: {
    extract: (i: Listing) => (i.price_cents != null ? logDecadeBucket(i.price_cents / 100) : null),
    weight: 0.4,
  },
} satisfies Schema<Listing>;

const reco = new Recommender(catalog, {
  schema,
  popularity: (i) => i.popularity, // optional cold-start prior
});

reco.prime(usersCollection);          // pre-warm from a known collection
reco.setOwned(['id-1', 'id-2']);      // excluded from results

reco.engage(item, SIGNALS.view);      // saw and scrolled past
reco.engage(item, SIGNALS.collect);   // added to collection
reco.engage(item, -3);                // strong negative — caller picks any magnitude

const { items, scores, diagnostics } = reco.recommend(20);

Methods shared by both engines: prime(), engage(), recommend(), setOwned(), clearSeen(), reset(), seenCount(). The dual engine adds setSampleSize(k) to tune explore vs. exploit.

Advanced primitives (slot tables, interest state, bucket admission, the weighted sampler, and seededRng for deterministic tests) are exported from the same entry for callers building custom pipelines or CLIs on top.

License

MIT