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

foyer-ax

v0.4.0

Published

Agent-experience analytics for Vercel logs — sessionize, classify human vs. AI agent, detect agent friction, and measure agent-vs-human task-success funnels.

Readme

foyer

npm CI license

Agent-experience analytics for web logs - PostHog for the agent medium.

Your analytics stack was built to watch humans click around a page. It has no idea what to do with an AI agent that never renders anything, never fires a pageview event, and either completes its task in three requests or gets stuck forever. foyer ingests a Vercel log export, sessionizes the raw requests, classifies each session human vs. AI-agent (with a confidence score and the signals that fired), detects agent-shaped friction (retry storms, 4xx clusters, auth walls, abandonment), and computes agent-vs-human task-success funnels for the flows you care about - checkout, signup, search, whatever a "task" means for your product. The lens is agent task success, not human engagement.

Install

npx foyer-ax analyze <vercel-log-export.json>

or install it:

npm install -g foyer-ax

CLI usage

foyer analyze <logfile> [--flows <path>] [--html <path>] [--source <name>]
foyer update-bots
foyer --help
foyer --version
  • analyze <logfile> - parse a Vercel log export, sessionize, classify, detect friction, and (if --flows is given) compute a task-success funnel for each configured flow.
  • --flows <path> - path to a flow-config JSON file (see Flow config below).
  • --html <path> - write the editorial HTML report (see below) to <path> instead of printing the plain-text report to stdout.
  • --source <name> - label shown in the HTML report's header (defaults to the log filename).
  • update-bots - refresh the vendored AI-bot list from the community ai-robots-txt project.
foyer analyze examples/sample-vercel.json --flows examples/foyer.config.json

HTML report

--html renders the same AnalysisReport as a self-contained, printable HTML document instead of the plain-text table: composition bar, agent vendors, per-flow human-vs-agent funnel (as SVG), and friction, all derived from the report data:

foyer analyze examples/sample-vercel.json \
  --flows examples/foyer.config.json \
  --html report.html \
  --source shop.example.com

See examples/sample-report.html for the rendered output. As a library, the same renderer is renderHtmlReport(report, meta) (see Library usage).

Library usage

import { analyze, formatReport, parseVercelLog, renderHtmlReport } from "foyer-ax";
import { readFileSync } from "node:fs";

const records = parseVercelLog(readFileSync("vercel-log-export.json", "utf8"));
const flows = [
  { name: "checkout", steps: [{ path: "/cart" }, { path: "/checkout" }, { method: "POST", path: "/api/order" }] },
];

const report = analyze(records, flows);
console.log(formatReport(report));
// or use `report` directly - byLabel, vendors, frictionByKind, funnels, ...

// or render the editorial HTML report instead of the plain-text one:
const html = renderHtmlReport(report, { source: "shop.example.com", adapter: "vercel", version: "0.1.0" });

Flow config

A flow is a named sequence of steps an agent or human is expected to move through in order (gaps are allowed - other requests in between don't break the match, but the steps must appear in order). Define flows as a JSON array, either in foyer.config.json (auto-detected) or any file passed via --flows:

[
  {
    "name": "checkout",
    "steps": [
      { "path": "/cart" },
      { "path": "/checkout" },
      { "method": "POST", "path": "/api/order" }
    ]
  }
]
  • path - exact match (query string ignored), or a trailing wildcard like "/product/*".
  • method - optional; omit to match any method.

See examples/foyer.config.json for a checkout/signup/search starter set.

Example output

Running foyer analyze examples/sample-vercel.json --flows examples/foyer.config.json against the bundled example fixture (a mix of human and agent sessions moving through a checkout flow):

═══ foyer report ═══
Requests: 72   Sessions: 13
Agent sessions: 53.8%

By label:
  human            6
  declared-agent   6
  likely-agent     1

Agent vendors:
  OpenAI           1
  Anthropic        1
  Perplexity       1
  Amazon           1
  Common Crawl Foundation 1
  ByteDance        1
  Other/Unknown    1

Sessions with friction: 2
  abandonment      2
  retry-storm      1
  4xx-cluster      1
  auth-wall        1

Flows (agent vs human task success):
  checkout: agents 14% vs humans 83% - agents fall behind at step 3 (POST /api/order)
  signup: agents 0% vs humans 0%
  search: agents 0% vs humans 0%

The checkout line is the headline: agents reach the cart and checkout pages about as often as humans do, but almost none of them complete the order - they fall behind exactly at the POST /api/order step, which is where you'd look first if you wanted agents to be able to complete a purchase on your site.

Architecture

Vercel log export   →   RequestRecord[]   →   engine                                    →   presentation
(src/analyze/vercel.ts)                       sessionize → classify → friction → funnel      report / CLI
  • Ingestion (src/analyze/vercel.ts) turns a Vercel log export into the shared RequestRecord[] shape. Format-specific quirks (field names, timestamp encoding, the host embedded in the request path) are resolved here and nowhere else.
  • The engine (sessionize → classify → detectFriction → computeFunnel, wired together by analyze()) only ever sees RequestRecord[]. It doesn't know or care that the data came from Vercel - see "Adding another log source" below.
  • Presentation (formatReport for plain text, renderHtmlReport for the editorial HTML report, the CLI) turns the engine's output into something a person reads.

Files

The source tree is organized by runtime environment first, subsystem second: everything under src/ outside of src/node/ is pure/isomorphic (no node:* imports, no browser globals) and safe to run on the Vercel Edge runtime; src/node/ (plus src/cli.ts) is where file/network I/O and node:http adapters live.

| File | Layer | Responsibility | | --- | --- | --- | | src/types.ts | shared | RequestRecord, Session, Classification, FrictionReport, Flow, FunnelResult, etc. | | src/analyze/vercel.ts | ingestion | Vercel's log export (no client IP; host embedded in the request path) | | src/analyze/sessionize.ts | engine | groups records into sessions by ip\|userAgent, 30-min inactivity gap (degrades to UA-only when IP is absent, e.g. Vercel) | | src/capture/classifier.ts | engine | weighted-signal human/agent classifiers: classify (Vercel-log grade) and classifySignals (fuses the SDK's identity/header/client layers) | | src/bots.ts | engine | known-bot lookup (isBotUserAgent, vendorForUserAgent), backed by data/bots.data.json | | src/analyze/friction.ts | engine | the four friction detectors | | src/analyze/flows.ts | engine | matchFlow (in-order subsequence match) and computeFunnel (agent-vs-human completion + parity-gap step) | | src/config.ts | engine | parses + validates a flow-config JSON document (parseFlowConfig) | | src/analyze/pipeline.ts | engine + presentation | analyze() composes the engine stages into a report; formatReport() renders the plain-text report | | src/analyze/html.ts | presentation | renderHtmlReport() renders the same report as a self-contained editorial HTML document (composition bar, vendors, funnel SVGs, friction) | | src/capture/capture.ts, src/capture/beacon.ts, src/events.ts, src/sink.ts | SDK | pure request/beacon capture pipeline (server- and client-side evidence gathering) for the classifySignals path | | src/index.ts | public API | the pure/edge-safe "." entry point - see below | | src/node.ts | public API | the Node-only "./node" entry point - see below | | src/cli.ts | presentation | the foyer CLI (analyze - --flows, --html, --source - and update-bots) | | src/node/adapters.ts | node | connect/express/node:http capture middleware (foyerMiddleware, ensureSessionId) | | src/node/sink.ts | node | createJsonlSink - appends captured events to a JSONL file | | src/node/config.ts | node | loadFlowConfigFile - reads + parses a flow-config file from disk | | src/botsUpdater.ts | data | refreshes data/bots.data.json from the community ai-robots-txt list; shared by update-bots (dev script) and foyer update-bots (CLI) | | data/bots.data.json | data | vendored bot-list snapshot (see below) |

Public API

foyer-ax ships two entry points, split by runtime:

  • "foyer-ax" (src/index.ts) - the pure/edge-safe core: analyze, formatReport, renderHtmlReport, parseVercelLog, sessionize, classify, detectFriction, matchFlow, computeFunnel, isBotUserAgent, vendorForUserAgent, parseFlowConfig, and their associated types (RequestRecord, Session, Classification, Flow, FunnelResult, AnalysisReport, ReportMeta, ...).
  • "foyer-ax/node" (src/node.ts) - Node-only functionality that touches the filesystem or node:http: loadFlowConfigFile, createJsonlSink, foyerMiddleware, ensureSessionId.

Anything not re-exported from one of these two entry points is an internal implementation detail and may change without notice.

Breaking change (0.4.0): loadFlowConfigFile moved off "foyer-ax" onto "foyer-ax/node" (it reads from disk, so it can't run on the Edge runtime). Update import { loadFlowConfigFile } from "foyer-ax" to import { loadFlowConfigFile } from "foyer-ax/node".

The bot list

src/bots.ts reads known AI-bot user agents + their operators from data/bots.data.json, a small vendored snapshot of the community-maintained ai-robots-txt project. It's committed to the repo (and bundled directly into the built JS) so the classifier stays offline and deterministic - no network calls at runtime, and tests never hit the network. Refresh the snapshot with:

npm run update-bots     # or: foyer update-bots

Anything not (yet) in the community list can be added by hand in the CUSTOM_BOTS map at the top of src/bots.ts - it's merged in on top of the fetched list, so a manual addition survives the next update-bots run.

Adding another log source

The engine only depends on RequestRecord[], so plugging in a new source (e.g. Cloudflare) means:

  1. Write a function that turns the raw export into RequestRecord[] (see src/analyze/vercel.ts for a minimal example). Every field on RequestRecord must be populated - use "unknown", 0, or null for whatever the source doesn't carry, matching how vercel.ts handles a missing client IP.
  2. Wire it into src/cli.ts (or add a format flag, once there's more than one source worth branching on).
  3. Nothing downstream (sessionize, classify, detectFriction, computeFunnel) needs to change - that's the point of keeping ingestion separate from the engine.

Development

npm install
npm test              # full suite
npm run test:watch    # re-run on save
npm run typecheck     # strict tsc --noEmit
npm run lint          # biome check
npm run build         # emit dist/ (JS + .d.ts) via tsup

npm run analyze -- <vercel-log-export.json>

See CONTRIBUTING.md for the full contributor workflow and release steps.

Fixtures & examples

  • examples/sample-vercel.json - a synthetic log with a mix of human and agent sessions moving through a checkout flow, some agents stalling before ordering. Used in the "Example output" section above and as an end-to-end test fixture.
  • examples/foyer.config.json - a starter flow config (checkout, signup, search).
  • examples/sample-report.html - the HTML report rendered from the two fixtures above (foyer analyze examples/sample-vercel.json --flows examples/foyer.config.json --html ... --source shop.example.com). Open it directly in a browser to see the full editorial report.

License

MIT