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

@upstash/agent-analytics

v0.1.3

Published

AI citation analytics SDK for Upstash Redis.

Downloads

394

Readme

@upstash/agent-analytics

AI citation analytics for Upstash Redis, built directly on the Redis Search extension — no @upstash/core-analytics dependency.

Installation

npm install @upstash/agent-analytics @upstash/redis

@upstash/redis is a peer dependency — install it alongside the SDK. Then create the analytics client:

import { AgentAnalytics } from "@upstash/agent-analytics";

// Reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN from the environment:
const analytics = AgentAnalytics.fromEnv();

Or pass your own @upstash/redis client — useful when you configure it explicitly or reuse an existing instance:

import { AgentAnalytics } from "@upstash/agent-analytics";
import { Redis } from "@upstash/redis";

const analytics = new AgentAnalytics({
  redis: new Redis({ url: "...", token: "..." }),
});

How it works

Every citation is bucketed by the hour and by its dimensions. A single Redis hash holds the counter for one combination of dimensions in one hour:

key:   <prefix>:event:<data-hash>:<hour>
value: { count, hour, provider, path }
  • data-hash is derived from the event's dimensions. It is order-independent: track({ provider, path }) and track({ path, provider }) map to the same key.
  • hour is an integer hour bucket. It is never exposed in the public API — every method takes and returns Date.
  • Ingestion runs a small Lua script: it HINCRBYs the count field, and only the first time the counter is created does it write the immutable metadata and set the expiry (28 days by default, configurable).
  • A Redis Search index over these hashes (with count and hour as numeric fields) powers the aggregations.

Tracking

track is overloaded — pass a Request (dimensions are inferred) or an explicit event. It returns a promise resolving to the counter's new value.

import { AgentAnalytics } from "@upstash/agent-analytics";
const analytics = AgentAnalytics.fromEnv();

// From a Fetch/NextRequest — provider and path are inferred:
await analytics.track(request);

// Or pass explicit dimensions (time defaults to now):
await analytics.track({ provider: "chatgpt", path: "/pricing" });

Don't block the response — use after

In a request handler you rarely want to await the write. On Next.js, schedule it with after so it runs as a background side-effect once the response has been sent:

import { after } from "next/server";
import { AgentAnalytics } from "@upstash/agent-analytics";

const analytics = AgentAnalytics.fromEnv();

export async function GET(req: Request) {
  // Returns immediately; the write happens after the response is sent.
  after(() => analytics.track(req));
  return Response.json({ ok: true });
}

after works the same way in middleware, Route Handlers, and Server Actions — the function stays alive for the background write without delaying the response.

Earlier Next.js versions: if after isn't available, use waitUntil instead — event.waitUntil(analytics.track(req)) in middleware (via NextFetchEvent), or waitUntil from @vercel/functions elsewhere. See Using after in Next.js.

Analytics

The read side lives under .query. Both queries take a { since, until? } window of Dates (until defaults to now). They are designed for windows from 24 hours up to 7 days.

const since = new Date(Date.now() - 24 * 60 * 60 * 1000);

// Sum of citations grouped by one dimension. The field is type-safe.
await analytics.query.aggregateBy({ field: "provider", since });
// -> { chatgpt: 12, claude: 7, perplexity: 3 }

await analytics.query.aggregateBy({ field: "path", since });
// -> { "/pricing": 9, "/blog": 13 }

// Hourly time series, grouped by provider (default). One gap-filled bucket per
// hour in the window, sorted ascending — ready to chart.
await analytics.query.timeseries({ since });
await analytics.query.timeseries({ since, groupBy: "path" });
// -> [{ time: Date, values: { chatgpt: 2, claude: 0 } }, ...]

Setup & the search index

The queries above issue a single search request against a cheap local index reference; they assume the index already exists and throw IndexNotFoundError if it doesn't. Create it once, at setup (it's idempotent):

await analytics.query.getIndex(); // creates the search index if missing

Indexing is then asynchronous, and the queries read whatever has been indexed so far — they do not wait. When you need a read to reflect events you just recorded, call analytics.query.waitIndexing() first. dropIndex() removes the index (the event hashes are left untouched).

Configuration

new AgentAnalytics({
  redis,
  prefix: "@upstash/agent-analytics", // key namespace (default)
  retention: "28d",                   // hour-bucket TTL (default); also accepts seconds
  indexName: "...",                   // search index name (defaults to one derived from prefix)
});