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

@abdoseadaa/raqib

v0.3.0

Published

Lightweight SDK for sending events to your tracker service

Downloads

60

Readme

@abdoseadaa/raqib

Lightweight event-tracking SDK. Works in Node.js microservices and the browser. Two lines of setup, one line per event.

npm install @abdoseadaa/raqib

Quick start

import { init } from "@abdoseadaa/raqib"

const tracker = init({
  serviceUrl: process.env.TRACKER_URL,
  apiKey:     process.env.TRACKER_KEY,
})

tracker.on("payment.initiated", { userId: "u_123", amount: 99 })

That's it. The event is buffered and sent automatically in the background — your code never waits on the tracker.


Table of contents


Installation

npm install @abdoseadaa/raqib

Requirements: Node.js 18+ (uses native fetch and crypto.subtle)


init()

Creates and returns a Tracker instance bound to your config. Throws synchronously if any required field is missing — misconfiguration is caught at startup, not silently at runtime.

import { init } from "@abdoseadaa/raqib"

const tracker = init({
  serviceUrl: process.env.TRACKER_URL,   // required
  apiKey:     process.env.TRACKER_KEY,   // required
})

You can create multiple independent instances in the same process — each carries its own config, queue, context, and hooks. This is useful when sending to different tracker service deployments with different base URLs:

const stagingTracker    = init({ serviceUrl: "https://tracker.staging.internal", apiKey: "et_live_stg_..." })
const productionTracker = init({ serviceUrl: "https://tracker.prod.internal",    apiKey: "et_live_prd_..." })

The apiKey controls authentication only — it does not route events to a different destination. If you are sending to the same service URL, one instance is all you need.

Higher-order event senders

The more useful pattern is creating pre-bound senders from a single instance. Instead of repeating the event name everywhere, wrap it in a function that only takes the payload:

const tracker = init({
  serviceUrl: process.env.TRACKER_URL,
  apiKey:     process.env.TRACKER_KEY,
})

// Pre-bind event names — callers only pass the payload
const onPaymentInitiated  = (payload: Record<string, unknown>) => tracker.on("payment.initiated",  payload)
const onPaymentSucceeded  = (payload: Record<string, unknown>) => tracker.on("payment.succeeded",  payload)
const onPaymentFailed     = (payload: Record<string, unknown>) => tracker.on("payment.failed",     payload)
const onUserSignedIn      = (payload: Record<string, unknown>) => tracker.on("user.signed_in",     payload)

// Usage — clean, no event name string to remember or mistype
onPaymentInitiated({ userId: "u_123", amount: 99 })
onPaymentSucceeded({ userId: "u_123", transactionId: "txn_456" })
onUserSignedIn({ userId: "u_123", method: "email" })

Or use a factory to generate them:

function bindEvent(eventName: string) {
  return (payload?: Record<string, unknown>) => tracker.on(eventName, payload ?? {})
}

const events = {
  payment: {
    initiated: bindEvent("payment.initiated"),
    succeeded: bindEvent("payment.succeeded"),
    failed:    bindEvent("payment.failed"),
  },
  user: {
    signedIn:  bindEvent("user.signed_in"),
    signedOut: bindEvent("user.signed_out"),
  },
}

// Usage — fully typed, autocomplete-friendly
events.payment.initiated({ userId: "u_123", amount: 99 })
events.user.signedIn({ userId: "u_123", method: "oauth" })

tracker.on()

Fire-and-forget event tracking. Always returns synchronously. Never throws.

tracker.on("event.name")
tracker.on("event.name", { key: "value" })

Examples:

// Simple event
tracker.on("page.viewed", { page: "/dashboard" })

// With nested properties
tracker.on("order.placed", {
  orderId: "ord_001",
  total:   149,
  items:   [{ sku: "SKU-01", qty: 2 }],
})

// No properties needed
tracker.on("app.started")

tracker.setContext()

Sets persistent fields that are automatically merged into every subsequent event. Calling it again extends (never replaces) the existing context.

tracker.setContext({ userId: "u_123", plan: "pro" })

tracker.on("page.viewed",    { page: "/dashboard" })
// → properties: { userId: "u_123", plan: "pro", page: "/dashboard" }

tracker.on("button.clicked", { button: "upgrade" })
// → properties: { userId: "u_123", plan: "pro", button: "upgrade" }

// Extend context later — merges with existing
tracker.setContext({ sessionId: "sess_abc" })
// Now every event also carries sessionId

Priority: setContext fields are overridden by hook() and by explicit on() properties.


tracker.hook()

Registers a dynamic callback that is called fresh on every event. Returns an unregister function.

Unlike setContext() which stores static values, a hook runs a function each time — so it can read live request data, session state, cookies, or anything else available in the current scope.

const unhook = tracker.hook(() => ({
  userId:    getCurrentUserId(),
  sessionId: getCurrentSessionId(),
}))

tracker.on("page.viewed", { page: "/profile" })
// → properties: { userId: "u_123", sessionId: "sess_abc", page: "/profile" }

unhook()   // remove the hook when done

Use in Express middleware

The most powerful use case — inject request-scoped data without touching every tracker.on() call:

import express from "express"

const app = express()

app.use((req, res, next) => {
  const unhook = tracker.hook(() => ({
    userId:    req.user?.id,
    requestId: req.headers["x-request-id"],
    ip:        req.ip,
    userAgent: req.headers["user-agent"],
  }))

  res.on("finish", unhook)   // automatically clean up when response ends
  next()
})

// Now every tracker.on() in any route automatically carries userId and requestId
app.post("/api/checkout", (req, res) => {
  tracker.on("checkout.started", { cartId: req.body.cartId })
  // → properties: { userId: "u_123", requestId: "req_abc", ip: "...", cartId: "cart_1" }
})

Multiple hooks

Each hook() call registers an independent callback. All hooks run on every event and their results merge:

const unhook1 = tracker.hook(() => ({ userId: req.user.id }))
const unhook2 = tracker.hook(() => ({ region: getRegion() }))
const unhook3 = tracker.hook(() => ({ featureFlags: getFlags() }))

// All three inject into every event
// Clean up each independently
res.on("finish", () => { unhook1(); unhook2(); unhook3() })

Priority: hook data overrides setContext but is overridden by explicit on() properties.


tracker.trackError()

Shorthand for exception tracking. Normalises an Error object into { name, message, stack } and fires it under the reserved event name sdk.error.

try {
  await chargeCard({ userId: "u_123", amount: 99 })
} catch (err) {
  tracker.trackError(err as Error, { route: "/api/checkout", userId: "u_123" })
}

The resulting event:

{
  "event_name": "sdk.error",
  "properties": {
    "route": "/api/checkout",
    "userId": "u_123",
    "error": {
      "name":    "CardDeclinedError",
      "message": "Card was declined by the issuer",
      "stack":   "CardDeclinedError: ...\n    at ..."
    }
  }
}

Any active setContext or hook data is also merged in automatically.


env callback

A function defined once in init() that runs on every event and injects its result into properties.env. Use it for metadata you always want attached: timestamps, runtime info, browser data, session identifiers.

// Node.js / backend
const tracker = init({
  serviceUrl: process.env.TRACKER_URL,
  apiKey:     process.env.TRACKER_KEY,
  env: () => ({
    timestamp:   new Date().toISOString(),
    serviceName: "payment-service",
    nodeVersion: process.version,
    env:         process.env.NODE_ENV,
  }),
})
// Browser / frontend
const tracker = init({
  serviceUrl: process.env.TRACKER_URL,
  apiKey:     process.env.TRACKER_KEY,
  env: () => ({
    timestamp:  new Date().toISOString(),
    userAgent:  navigator.userAgent,
    language:   navigator.language,
    screenSize: `${window.innerWidth}x${window.innerHeight}`,
    userId:     localStorage.getItem("userId"),
    sessionId:  sessionStorage.getItem("sessionId"),
  }),
})

The env object is deep-merged from all sources. Priority (lowest → highest):

config.env() callback
  ↓ merged with
setContext({ env: { ... } })
  ↓ merged with
hook() returning { env: { ... } }
  ↓ merged with
tracker.on("x", { env: { ... } })   ← explicit call always wins

Encryption

When encryptionKey is set, the entire batch payload is AES-256-GCM encrypted before sending. The network tab shows only an opaque base64 blob — event names, property keys, and values are all hidden.

const tracker = init({
  serviceUrl:    process.env.TRACKER_URL,
  apiKey:        process.env.TRACKER_KEY,
  encryptionKey: process.env.TRACKER_ENCRYPTION_KEY,
})

What the network sees:

{ "encrypted": true, "d": "A1b2C3d4...base64gibberish...==" }

Key rules:

  • Any string length — the key is SHA-256 hashed internally to produce a 32-byte AES-256 key
  • Both sides must use the exact same string
  • Store it as an environment variable — never hardcode it
  • Generate a strong key: openssl rand -base64 32

Backend decryption — use the included middleware (see Backend middleware) or implement manually:

async function decryptPayload(d: string, encryptionKey: string) {
  const keyBytes = await crypto.subtle.digest(
    "SHA-256",
    new TextEncoder().encode(encryptionKey)
  )
  const key = await crypto.subtle.importKey(
    "raw", keyBytes, { name: "AES-GCM" }, false, ["decrypt"]
  )
  const combined   = Buffer.from(d, "base64")
  const iv         = combined.subarray(0, 12)    // first 12 bytes = IV
  const ciphertext = combined.subarray(12)       // rest = ciphertext
  const plaintext  = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext)
  return JSON.parse(new TextDecoder().decode(plaintext))
  // → { events: [{ event_name: "...", properties: { ... } }] }
}

Graceful shutdown

The SDK runs a background flush timer. On process termination, stop the timer and drain the buffer so no events are lost:

process.on("SIGTERM", async () => {
  tracker.destroy()      // stop background timer
  await tracker.flush()  // send remaining buffered events
  process.exit(0)
})

You can also call flush() at any time to force an immediate send:

// After a critical event, flush immediately without waiting for the timer
tracker.on("payment.succeeded", { transactionId: "txn_456" })
await tracker.flush()

Backend middleware

Import the Express middleware to transparently decrypt incoming payloads from SDK clients that have encryptionKey set.

import express from "express"
import { decryptMiddleware } from "@abdoseadaa/raqib/middleware"

const app = express()

app.use(express.json())
app.use(decryptMiddleware(process.env.TRACKER_ENCRYPTION_KEY))

app.post("/v1/events/webhook/bulk", (req, res) => {
  // req.body is always plain JSON here — decrypted transparently if needed
  const { events } = req.body
  // events → [{ event_name: "...", properties: { ... } }, ...]
})

Unencrypted requests pass through untouched. The middleware handles both encrypted and plain payloads on the same route.

Error responses:

| Scenario | Status | Code | |---|---|---| | encrypted: true but no d field | 400 | INVALID_ENCRYPTED_PAYLOAD | | Wrong key or corrupted data | 400 | DECRYPT_FAILED | | Not encrypted | Pass-through | — |


How batching works

tracker.on() never blocks. Events go into an in-memory queue and are sent in batches automatically.

tracker.on("x", {...})
       ↓
  in-memory buffer
       ↓
  flush when EITHER:
    → flushIntervalMs elapsed (default: 2000ms)   time trigger
    → batchSize events buffered (default: 20)      count trigger
       ↓
  POST /v1/events/webhook/bulk
  { events: [...up to 20...] }
       ↓
  retry on failure (up to 3×, exponential backoff + jitter)

Time trigger — prevents events from going stale in low-traffic periods. Count trigger — prevents oversized payloads during traffic spikes.

Tune both in init():

const tracker = init({
  serviceUrl:      process.env.TRACKER_URL,
  apiKey:          process.env.TRACKER_KEY,
  flushIntervalMs: 5000,   // flush every 5s (default: 2000)
  batchSize:       50,     // or when 50 events buffer up (default: 20)
})

Wire format

Every flush is a single POST request:

POST /v1/events/webhook/bulk
Authorization: Bearer <apiKey>
Content-Type: application/json

Plain (no encryption):

{
  "events": [
    {
      "event_name": "payment.initiated",
      "properties": {
        "userId": "u_123",
        "amount": 99,
        "env": {
          "timestamp":   "2026-04-15T10:23:45.123Z",
          "serviceName": "payment-service"
        }
      }
    },
    {
      "event_name": "payment.succeeded",
      "properties": {
        "userId":        "u_123",
        "transactionId": "txn_456",
        "env": {
          "timestamp":   "2026-04-15T10:23:45.890Z",
          "serviceName": "payment-service"
        }
      }
    }
  ]
}

Encrypted (encryptionKey set):

{
  "encrypted": true,
  "d": "A1b2C3d4E5f6...base64gibberish...=="
}

Config reference

| Option | Type | Required | Default | Description | |---|---|---|---|---| | serviceUrl | string | Yes | — | Base URL of your tracker service | | apiKey | string | Yes | — | Org API key (et_live_…) sent as Authorization: Bearer | | env | () => object | No | — | Called on every event — result injected into properties.env | | encryptionKey | string | No | — | AES-256-GCM encrypt the batch before sending | | flushIntervalMs | number | No | 2000 | Max ms an event waits before being sent | | batchSize | number | No | 20 | Flush immediately when buffer reaches this size |