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

trashlytics

v0.1.1

Published

A lightweight event tracking library

Readme

trashlytics

A lightweight, generic event tracking library with built-in batching, retry logic, and middleware support. Uses Effect internally for robust async handling, but exposes a simple vanilla JavaScript API.

Features

  • Simple API - Just functions and Promises, no framework knowledge required
  • Generic Events - Track any payload type with full TypeScript support
  • Multiple Transports - Fan-out events to multiple destinations concurrently
  • Batching - Configurable batch size and flush interval
  • Retry Logic - Exponential backoff with jitter for failed sends
  • Middleware - Composable event transformations (filter, enrich, transform)
  • Queue Strategies - Bounded, dropping, or sliding window queues
  • Universal - Works in browser and Node.js environments

Installation

npm install trashlytics effect
# or
pnpm add trashlytics effect
# or
yarn add trashlytics effect

Quick Start

import { createTracker, TransportError } from "trashlytics"

// 1. Define your event types
type MyEvents = {
  page_view: { page: string; referrer?: string }
  button_click: { buttonId: string }
}

// 2. Create a typed tracker with your transport
const tracker = createTracker<MyEvents>({
  transports: [{
    name: "http",
    send: async (events) => {
      const response = await fetch("/api/analytics", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(events),
      })
      if (!response.ok) {
        throw new TransportError({
          transport: "http",
          reason: `HTTP ${response.status}`,
          retryable: response.status >= 500,
        })
      }
    },
  }],
  batchSize: 10,
  flushIntervalMs: 5000,
})

// 3. Track events with full type safety (fire-and-forget)
tracker.track("page_view", { page: "/home" })
tracker.track("button_click", { buttonId: "signup" })

// 4. Graceful shutdown when done
await tracker.shutdown()

Configuration

import { createTracker, consoleLogger, noopLogger } from "trashlytics"

const tracker = createTracker({
  // Required: array of transports
  transports: [httpTransport, consoleTransport],

  // Batching
  batchSize: 10,              // Events per batch (default: 10)
  flushIntervalMs: 5000,      // Max time before flush in ms (default: 5000)

  // Queue
  queueCapacity: 1000,        // Max queued events (default: 1000)
  queueStrategy: "dropping",  // "bounded" | "dropping" | "sliding"

  // Retry
  retryAttempts: 3,           // Max retry attempts (default: 3)
  retryDelayMs: 1000,         // Base delay for backoff in ms (default: 1000)

  // Shutdown
  shutdownTimeoutMs: 30000,   // Max shutdown wait in ms (default: 30000)

  // ID Generation
  generateId: () => crypto.randomUUID(),  // Custom ID generator

  // Global Metadata (added to all events)
  metadata: {
    appVersion: "1.0.0",
    environment: "production",
  },

  // Logging (default: consoleLogger)
  logger: consoleLogger,      // Use noopLogger to silence output

  // Error Callback (called after all retries exhausted)
  onError: (error, events) => {
    console.error(`Failed to send ${events.length} events:`, error.message)
  },
})

Middleware

Middleware allows you to transform, filter, or enrich events before they're sent. Middleware functions receive an event and return a transformed event (or null to filter it out).

Built-in Middleware

import { createTracker, compose, filter, addMetadata, mapName, tap } from "trashlytics"

// Compose multiple middlewares (executed left to right)
const middleware = compose(
  // Filter out internal events
  filter((event) => !event.name.startsWith("_")),

  // Add static metadata
  addMetadata({
    appVersion: "1.0.0",
    platform: "web",
  }),

  // Prefix event names
  mapName((name) => `app.${name}`),

  // Side effects (logging, etc.)
  tap((event) => console.log("Tracking:", event.name)),
)

// Use with tracker
const tracker = createTracker({ transports }, middleware)

Available Middleware Functions

| Function | Description | |----------|-------------| | filter(predicate) | Filter events based on a predicate | | addMetadata(obj) | Add static metadata to all events | | addMetadataFrom(fn) | Add dynamic metadata based on event | | mapName(fn) | Transform event name | | mapPayload(fn) | Transform event payload | | map(fn) | Transform entire event | | tap(fn) | Side effects without modifying event | | compose(...middlewares) | Compose multiple middlewares | | identity | Pass-through middleware |

Custom Middleware

import type { Middleware } from "trashlytics"

// Middleware is just a function: Event -> Event | null
const redactPasswords: Middleware = (event) => ({
  ...event,
  payload: {
    ...event.payload,
    password: event.payload.password ? "[REDACTED]" : undefined,
  },
})

Multiple Transports

Send events to multiple destinations simultaneously:

import { createTracker, TransportError } from "trashlytics"

const httpTransport = {
  name: "http",
  send: async (events) => {
    await fetch("/api/analytics", {
      method: "POST",
      body: JSON.stringify(events),
    })
  },
}

const consoleTransport = {
  name: "console",
  send: async (events) => {
    console.log("[Analytics]", events)
  },
}

// All transports receive events concurrently
const tracker = createTracker({
  transports: [httpTransport, consoleTransport],
})

Custom Transport

Implement the Transport interface:

import type { Transport } from "trashlytics"
import { TransportError } from "trashlytics"

const myTransport: Transport = {
  name: "my-analytics",
  send: async (events) => {
    try {
      await myAnalyticsSDK.track(events)
    } catch (error) {
      throw new TransportError({
        transport: "my-analytics",
        reason: String(error),
        retryable: true, // Set false for non-retryable errors
      })
    }
  },
}

Queue Strategies

Control behavior when the event queue is full:

| Strategy | Behavior | |----------|----------| | "bounded" | Back-pressure - blocks until space is available | | "dropping" | Drops new events when queue is full (default) | | "sliding" | Drops oldest events when queue is full |

const tracker = createTracker({
  transports,
  queueCapacity: 500,
  queueStrategy: "sliding", // Keep most recent events
})

API Reference

Tracker

interface Tracker {
  // Track an event (fire-and-forget)
  track<T>(name: string, payload: T): void

  // Track and wait for queue
  trackAsync<T>(name: string, payload: T): Promise<void>

  // Track with additional metadata
  trackWith<T>(name: string, payload: T, metadata: Record<string, unknown>): void

  // Flush all queued events immediately
  flush(): Promise<void>

  // Graceful shutdown (flush + cleanup)
  shutdown(): Promise<void>
}

Event

interface Event<T = unknown> {
  id: string
  name: string
  timestamp: number
  payload: T
  metadata: Record<string, unknown>
}

TransportError

class TransportError extends Error {
  transport: string   // Transport name
  retryable: boolean  // Whether to retry
}

Logger

interface Logger {
  debug: (message: string, ...args: unknown[]) => void
  info: (message: string, ...args: unknown[]) => void
  warn: (message: string, ...args: unknown[]) => void
  error: (message: string, ...args: unknown[]) => void
}

Custom Logging

Control library logging output:

import { createTracker, consoleLogger, noopLogger, createMinLevelLogger } from "trashlytics"

// Default: logs to console
const tracker = createTracker({
  transports,
  logger: consoleLogger,
})

// Silence all logging
const silentTracker = createTracker({
  transports,
  logger: noopLogger,
})

// Only log warnings and errors
const warnTracker = createTracker({
  transports,
  logger: createMinLevelLogger("warn"),
})

// Custom logger integration
const customTracker = createTracker({
  transports,
  logger: {
    debug: (msg, ...args) => myLogger.debug("[analytics]", msg, ...args),
    info: (msg, ...args) => myLogger.info("[analytics]", msg, ...args),
    warn: (msg, ...args) => myLogger.warn("[analytics]", msg, ...args),
    error: (msg, ...args) => myLogger.error("[analytics]", msg, ...args),
  },
})

Browser Tips

Beacon API Transport

For reliable delivery on page unload:

const beaconTransport: Transport = {
  name: "beacon",
  send: async (events) => {
    const success = navigator.sendBeacon("/api/analytics", JSON.stringify(events))
    if (!success) {
      // Fallback to fetch
      await fetch("/api/analytics", {
        method: "POST",
        body: JSON.stringify(events),
        keepalive: true,
      })
    }
  },
}

Page Lifecycle Events

Flush events when the page is hidden or unloaded:

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    tracker.flush()
  }
})

window.addEventListener("pagehide", () => {
  tracker.flush()
})

License

MIT