@webgrip/edge-analytics (0.1.0)

Published 2026-09-05 04:55:22 +00:00 by webgrip-ci in webgrip/frontend-toolkit

Installation

@webgrip:registry=
npm install @webgrip/edge-analytics@0.1.0
"@webgrip/edge-analytics": "0.1.0"

About this package

@webgrip/edge-analytics

Counterscale reporting from the edge of a Workers Static Assets site. No script in the page, no cookie, and nothing for a blocker to stop.

What this is, and what it is not

Counterscale is the analytics product: MIT, self-hosted on Cloudflare Workers and Analytics Engine, cookieless. It is the collector and the dashboard.

This package is the ten lines that both Webgrip sites would otherwise each carry their own copy of: a Worker that reports the pageview and hands the request straight back to the asset binding.

It uses Counterscale's server module rather than the browser snippet. A script in the page is blocked for exactly the audience these sites care about; on twente.dev the existing RUM beacon is blocked by the maintainer's own browser. Reporting from inside the request that serves the page leaves nothing to block.

Two fields are deliberately never sent even though the tracker accepts them: ip and userAgent. Neither is needed to answer which channel filled the room, and not collecting is the only durable way to not leak.

Deploying the collector, once

Counterscale is one Worker with its own Analytics Engine dataset and dashboard, deployed once for every site that reports to it. Webgrip runs it at counterscale.webgrip.dev, named after the tool rather than after what it does, so analytics.webgrip.dev stays free for whatever comes later.

npx @counterscale/cli

The installer asks for a Cloudflare API token with Workers and Analytics Engine permissions, and for a dashboard password. Point the Worker at the hostname above, then note the reporting endpoint: https://counterscale.webgrip.dev/collect.

It cannot run on a homelab. Analytics Engine is a Cloudflare product with no self-hosted build, and that is the right side of the trade anyway: the collector sits in the request path of a public site, so it has to be somewhere that is always up. The homelab is where the archive belongs, not the intake.

Each site reports with its own siteId, so one deployment serves several sites and the dashboard filters between them.

Wiring a site

1. src/worker.mjs:

import { createAnalyticsFetch } from '@webgrip/edge-analytics';

export default {
  fetch: createAnalyticsFetch({
    siteId: 'twente-dev',
    reporterUrl: 'https://analytics.webgrip.dev/collect',
  }),
};

2. wrangler.toml:

main = "src/worker.mjs"

[assets]
directory = "./dist"
binding = "ASSETS"
html_handling = "auto-trailing-slash"
not_found_handling = "404-page"

# Page paths only. Every path listed here costs a Worker invocation; every path
# excluded stays a free, unmetered static asset request. A pageview pulls one
# document and a dozen assets, so excluding the asset directories is the
# difference between one invocation per view and thirteen. This is the cost
# knob; get it wrong and the free tier stops being free.
run_worker_first = [
  "/*",
  "!/_astro/*",
  "!/brand/*",
  "!/pagefind/*",
  "!/fonts/*",
]

3. pnpm add @counterscale/tracker — it is a peer dependency, so the site pins the tracker version it deploys.

What it costs

Nothing, with room. The Workers free plan allows 100,000 invocations a day and Analytics Engine 100,000 data points and 10,000 read queries a day. With run_worker_first scoped as above that is 100,000 pageviews a day.

Grafana

Counterscale ships a dashboard, and its data also sits in a plain Analytics Engine dataset, which exposes a SQL API over HTTP that Grafana can query directly. Use Counterscale's dashboard for the standard traffic questions and Grafana where the numbers have to sit beside everything else you already watch.

SELECT blob4 AS source, blob5 AS medium, blob6 AS campaign, SUM(_sample_interval) AS visits
FROM metricsDataset
WHERE timestamp > NOW() - INTERVAL '30' DAY
GROUP BY source, medium, campaign
ORDER BY visits DESC

Confirm the blob positions against Counterscale's own schema before trusting a panel; they are ordered by its collector, not by this package. And use SUM(_sample_interval) rather than COUNT(*): Analytics Engine samples under load and the sample interval is the weight that undoes it. At these volumes nothing is sampled and the two agree, which is exactly why the wrong one is easy to ship and hard to notice.

Retention is ninety days

Analytics Engine keeps data for ninety days, and that is shorter than the questions these sites ask. Anything meant to outlive the window has to be copied into a store you own before it closes. Plan for that when you wire the dashboards, not when the first quarter falls off the end.

Dependencies

Peer dependencies

ID Version
@counterscale/tracker ^3.4.0
Details
npm
2026-09-05 04:55:22 +00:00
5
MIT
latest
4.3 KiB
Assets (1)
Versions (1) View all
0.1.0 2026-09-05