agent-analytics

Verified·Scanned 2/17/2026

Agent Analytics provides a drop-in tracking snippet and server-side handler to collect and query site events. The skill instructs running npx commands, requires env vars like API_KEYS and PROJECT_TOKENS, and sends events to https://api.agentanalytics.sh.

from clawhub.ai·v1.0.0·74.5 KB·0 installs
Scanned from 1.0.1 at 1a79f0b · Transparency log ↗
$ vett add clawhub.ai/dannyshmueli/agent-analytics

@agent-analytics/core

Platform-agnostic analytics engine. Zero dependencies — uses only Web APIs. Plug in your own database and auth to get a full analytics API on any runtime (Cloudflare Workers, Node.js, Deno, Bun, etc).

Install

npm install @agent-analytics/core

Quick Start: Cloudflare Workers

import { createAnalyticsHandler, D1Adapter } from '@agent-analytics/core';

export default {
  async fetch(request, env, ctx) {
    const db = new D1Adapter(env.DB);

    const validateWrite = (_request, body) => {
      const token = body?.token;
      if (!env.PROJECT_TOKENS) return { valid: true };
      if (!token || !env.PROJECT_TOKENS.split(',').includes(token))
        return { valid: false, error: 'invalid token' };
      return { valid: true };
    };

    const validateRead = (request, url) => {
      const key = request.headers.get('X-API-Key') || url.searchParams.get('key');
      if (!env.API_KEYS || !key || !env.API_KEYS.split(',').includes(key))
        return { valid: false };
      return { valid: true };
    };

    const handle = createAnalyticsHandler({ db, validateWrite, validateRead });
    const { response, writeOps } = await handle(request);

    if (writeOps) writeOps.forEach(op => ctx.waitUntil(op));
    return response;
  },
};

Initialize your D1 database with the included schema.sql, set API_KEYS and PROJECT_TOKENS as secrets, and deploy.

Quick Start: Node.js

import { createServer } from 'node:http';
import { createAnalyticsHandler } from '@agent-analytics/core';
// bring your own adapter — see "Database Adapter Interface" below
import { SqliteAdapter } from './db/sqlite.js';

const db = new SqliteAdapter('analytics.db');

const handleRequest = createAnalyticsHandler({
  db,
  validateWrite: (_req, body) => {
    const token = body?.token;
    if (!token || token !== process.env.PROJECT_TOKEN)
      return { valid: false, error: 'invalid token' };
    return { valid: true };
  },
  validateRead: (req, url) => {
    const key = req.headers.get('X-API-Key') || url.searchParams.get('key');
    if (!key || key !== process.env.API_KEY) return { valid: false };
    return { valid: true };
  },
});

createServer(async (req, res) => {
  const url = new URL(req.url, `http://localhost:8787`);
  let body = null;
  if (req.method === 'POST') {
    const chunks = [];
    for await (const chunk of req) chunks.push(chunk);
    body = Buffer.concat(chunks).toString();
  }
  const request = new Request(url.toString(), { method: req.method, headers: req.headers, body });
  const { response } = await handleRequest(request);
  res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
  res.end(await response.text());
}).listen(8787, () => console.log('Listening on :8787'));

Add Tracking to Your Site

Drop one line before </body>:

<script src="https://your-server.com/tracker.js" data-project="my-site" data-token="YOUR_TOKEN"></script>

This auto-tracks page views (including SPA route changes) with URL, referrer, screen size, browser, OS, device type, and UTM params.

// Track custom events
window.aa.track('signup', { plan: 'pro' });

// Identify a logged-in user (replaces anonymous ID)
window.aa.identify('user_123');

// Manually track a page view
window.aa.page('Dashboard');

Events are batched and flushed every 5 seconds, or immediately on page hide/unload via sendBeacon.

Query Your Data

# Aggregated stats (last 7 days by default)
curl "https://your-server.com/stats?project=my-site" \
  -H "X-API-Key: YOUR_KEY"

# Raw events
curl "https://your-server.com/events?project=my-site&event=page_view&limit=50" \
  -H "X-API-Key: YOUR_KEY"

# Flexible query
curl -X POST "https://your-server.com/query" \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "project": "my-site",
    "metrics": ["event_count", "unique_users"],
    "group_by": ["event"],
    "filters": [{ "field": "properties.browser", "op": "eq", "value": "Chrome" }],
    "date_from": "2025-01-01",
    "limit": 20
  }'

# Discover event names and property keys
curl "https://your-server.com/properties?project=my-site" \
  -H "X-API-Key: YOUR_KEY"

# Discover which property keys are used by which events
curl "https://your-server.com/properties/received?project=my-site" \
  -H "X-API-Key: YOUR_KEY"

API Reference

Ingestion (project token required)

POST /track

Track a single event.

{
  "project": "my-site",
  "token": "pt_your_token",
  "event": "page_view",
  "properties": { "path": "/home", "browser": "Chrome" },
  "user_id": "user_123",
  "session_id": "sess_abc",
  "timestamp": 1706745600000
}
FieldRequiredDescription
projectyesProject identifier
tokenyes*Project token (*optional if auth is open)
eventyesEvent name
propertiesArbitrary JSON object
user_idUser identifier
session_idSession identifier (enables session tracking)
timestampUnix ms (defaults to Date.now())

Response: { "ok": true }

POST /track/batch

Track up to 100 events at once.

{
  "events": [
    { "project": "my-site", "token": "pt_abc", "event": "click", "user_id": "u1" },
    { "project": "my-site", "token": "pt_abc", "event": "scroll", "user_id": "u2" }
  ]
}

Response: { "ok": true, "count": 2 }

Query (API key required)

Pass your key via X-API-Key header or ?key= query parameter.

GET /stats

Aggregated overview with time series, top events, and session metrics.

ParamDefaultDescription
projectrequiredProject identifier
since7 days agoISO timestamp or date string
groupBydayhour, day, week, month

Response:

{
  "project": "my-site",
  "period": { "from": "2025-01-24", "to": "2025-01-31", "groupBy": "day" },
  "totals": { "unique_users": 1203, "total_events": 4821 },
  "timeSeries": [
    { "bucket": "2025-01-24", "unique_users": 180, "total_events": 712 }
  ],
  "events": [
    { "event": "page_view", "count": 3920, "unique_users": 1100 }
  ],
  "sessions": {
    "total_sessions": 1500,
    "bounce_rate": 0.42,
    "avg_duration": 185000,
    "pages_per_session": 3.2,
    "sessions_per_user": 1.2
  }
}

GET /sessions

List sessions with optional filters.

ParamDefaultDescription
projectrequiredProject identifier
since7 days agoISO timestamp or date string
user_idFilter by user
is_bounce0 or 1
limit100Max 1000

GET /events

Raw event log.

ParamDefaultDescription
projectrequiredProject identifier
eventFilter by event name
session_idFilter by session
since7 days agoISO timestamp or date string
limit100Max 1000

POST /query

Flexible analytics query with metrics, grouping, filtering, and sorting.

{
  "project": "my-site",
  "metrics": ["event_count", "unique_users"],
  "group_by": ["event", "date"],
  "filters": [
    { "field": "event", "op": "eq", "value": "page_view" },
    { "field": "properties.browser", "op": "eq", "value": "Chrome" }
  ],
  "date_from": "2025-01-01",
  "date_to": "2025-01-31",
  "order_by": "event_count",
  "order": "desc",
  "limit": 50
}
ParameterDescription
metricsevent_count, unique_users, session_count, bounce_rate, avg_duration
group_byevent, date, user_id, session_id
filters[].fieldevent, user_id, date, or properties.* for JSON property filters
filters[].opeq, neq, gt, lt, gte, lte
order_byAny metric or group_by field
limitMax 1000 (default: 100)

GET /properties

Discover event names and property keys for a project.

ParamDefaultDescription
projectrequiredProject identifier
since7 days agoISO timestamp or date string

Response:

{
  "project": "my-site",
  "events": [
    { "event": "page_view", "count": 3920, "unique_users": 1100, "first_seen": "2025-01-01", "last_seen": "2025-01-31" }
  ],
  "property_keys": ["browser", "device", "hostname", "os", "path", "referrer", "screen", "title", "url"]
}

GET /properties/received

Discover which property keys are used by which event types. Samples recent events for fast, bounded queries. Useful for AI agents to reuse consistent property naming.

ParamDefaultDescription
projectrequiredProject identifier
since7 days agoISO timestamp or date string
sample5000Max events to sample (100-10000)

Response:

{
  "project": "my-site",
  "sample_size": 5000,
  "since": "2025-01-24",
  "properties": [
    { "key": "path", "event": "page_view" },
    { "key": "browser", "event": "page_view" },
    { "key": "plan", "event": "signup" }
  ]
}

GET /projects

List all projects (derived from events data).

Response:

{
  "projects": [
    { "id": "my-site", "created": "2025-01-01", "last_active": "2025-01-31", "event_count": 4821 }
  ]
}

Utility

GET /health

{ "status": "ok", "service": "agent-analytics" }

GET /tracker.js

Serves the client-side tracking script. See Add Tracking to Your Site.

createAnalyticsHandler()

The main factory function. Returns an async request handler.

import { createAnalyticsHandler } from '@agent-analytics/core';

const handleRequest = createAnalyticsHandler({
  db,              // DbAdapter — your database implementation
  validateWrite,   // (request: Request, body: object) => { valid: boolean, error?: string }
  validateRead,    // (request: Request, url: URL) => { valid: boolean }
  useQueue,        // boolean (default: false) — return queueMessages instead of writeOps
  healthExtra,     // object (default: {}) — extra fields merged into /health response
});

const { response, writeOps, queueMessages } = await handleRequest(request);
ParameterRequiredDescription
dbyesDatabase adapter implementing the DbAdapter interface
validateWriteyesAuth function for ingestion endpoints (/track, /track/batch)
validateReadyesAuth function for query endpoints (/stats, /events, /query, etc)
useQueuenoWhen true, returns queueMessages array instead of writeOps promises
healthExtranoExtra fields merged into the /health JSON response

Return value:

FieldDescription
responseStandard Response object — return this to the client
writeOpsArray of Promise — database write operations (when useQueue is false)
queueMessagesArray of event objects to enqueue (when useQueue is true)

Database Adapter Interface

All adapters must implement these methods:

MethodSignatureDescription
trackEvent({ project, event, properties, user_id, session_id, timestamp }) => PromiseInsert a single event (+ upsert session if session_id provided)
trackBatch(events[]) => PromiseInsert multiple events atomically
getStats({ project, since?, groupBy? }) => PromiseAggregated stats with time series
getEvents({ project, event?, session_id?, since?, limit? }) => PromiseRaw event query
query({ project, metrics?, filters?, date_from?, date_to?, group_by?, order_by?, order?, limit? }) => PromiseFlexible analytics query
getProperties({ project, since? }) => PromiseDiscover event names and property keys
getPropertiesReceived({ project, since?, sample? }) => PromiseProperty keys mapped to event types
listProjects() => PromiseList all projects
getSessions({ project, since?, user_id?, is_bounce?, limit? }) => PromiseList sessions with filters
getSessionStats({ project, since? }) => PromiseAggregate session metrics
upsertSession(sessionData) => PromiseUpsert a session row
cleanupSessions({ project, before_date }) => PromiseDelete old sessions

The included D1Adapter implements this interface for Cloudflare D1. See src/db/d1.js.

Schema

Initialize your database with schema.sql:

CREATE TABLE IF NOT EXISTS events (
  id TEXT PRIMARY KEY,
  project_id TEXT NOT NULL,
  event TEXT NOT NULL,
  properties TEXT,
  user_id TEXT,
  session_id TEXT,
  timestamp INTEGER NOT NULL,
  date TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_events_project_date ON events(project_id, date);
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);

CREATE TABLE IF NOT EXISTS sessions (
  session_id TEXT PRIMARY KEY,
  user_id TEXT,
  project_id TEXT NOT NULL,
  start_time INTEGER NOT NULL,
  end_time INTEGER NOT NULL,
  duration INTEGER DEFAULT 0,
  entry_page TEXT,
  exit_page TEXT,
  event_count INTEGER DEFAULT 1,
  is_bounce INTEGER DEFAULT 1,
  date TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_sessions_project_date ON sessions(project_id, date);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(project_id, user_id);

Exports

// Main entry: @agent-analytics/core
import {
  createAnalyticsHandler,  // Handler factory
  D1Adapter,               // Cloudflare D1 database adapter
  validatePropertyKey,     // Validates property keys (alphanumeric + underscores, 1-128 chars)
  today,                   // () => 'YYYY-MM-DD'
  daysAgo,                 // (n) => 'YYYY-MM-DD'
  parseSince,              // (since?) => 'YYYY-MM-DD' (defaults to 7 days ago)
  parseSinceMs,            // (since?) => epoch ms (defaults to 7 days ago)
  TRACKER_JS,              // Client-side tracking script source
} from '@agent-analytics/core';

// Sub-path export: @agent-analytics/core/ulid
import { ulid } from '@agent-analytics/core/ulid';  // ULID generator (time-sortable, 26 chars)

License

MIT