Analytics Engine as cookie-less telemetry: blobs are positional

Log allowlisted campaign fields from a passthrough Worker with writeDataPoint(); blob order is the schema, and reads must sum _sample_interval, not count rows.

Analytics Engine

· Chapter

16

·

3

min read

The answer. Analytics Engine is the cheapest way to get first-party, consent-independent telemetry that identifies nobody: a passthrough Worker on the zone route calls env.DATASET.writeDataPoint() with an allowlist of fields, then returns fetch(request). The write is fire-and-forget, so the visitor never waits. Two things aren't obvious from the docs. Blobs have no names — blob1..blobN are positional, so the order in the writer is the schema; keep the read-out next to the writer and document the positions in both. And the dataset is sampled under load: a query must sum(_sample_interval) to get the real count, because count() returns stored rows, which is fewer.

The pattern.

[[analytics_engine_datasets]]
binding = "UTM"
dataset = "my_utm_landings"
// Blob allowlist — ORDER IS THE SCHEMA (the read-out reads by position):
// blob1 source, blob2 campaign, blob3 content, blob4 medium, blob5 path, blob6 country, blob7 ua_class.
// Never the full query string. No cookies, no identity, no PII.
export default {
  async fetch(request, env) {
    try {
      const url = new URL(request.url), p = (k) => url.searchParams.get(k) || "";
      if (p("utm_source")) env.UTM.writeDataPoint({
        indexes: [p("utm_source").slice(0, 96)],
        blobs: [p("utm_source"), p("utm_campaign"), p("utm_content"), p("utm_medium"),
                url.pathname, request.cf?.country || "", uaClass(request)],
        doubles: [1],
      });
    } catch {}                       // fail open: analytics must never break the site
    return fetch(request);
  },
};
-- POST as text/plain to /accounts/<id>/analytics_engine/sql with an "Account Analytics Read" token
SELECT blob1 AS source, blob2 AS campaign, blob7 AS ua_class,
       sum(_sample_interval) AS hits, max(timestamp) AS last_hit
FROM my_utm_landings WHERE timestamp > now() - INTERVAL '30' DAY
GROUP BY source, campaign, ua_class ORDER BY hits DESC

The receipt. On the HarperFlow marketing zone, route live since 2026-08-16 (owner sign-off the same day): the Worker logs only utm_* landings on our own links, plus a count-only consent-choice beacon (POST /edge/consent-choice → 204) that made the cookie banner's accept rate measurable for the first time. Every row carries a ua_class (human, bot-ua, dc-asn — a UA regex plus a small datacenter-ASN set) because in an earlier outreach wave a datacenter scanner in Ashburn read as a warm lead. The Python read-out sums _sample_interval, groups by position and prints bot-classed hits separately; it is now the retargeting source of truth for cold-email waves (distinct utm_content where ua_class = human). Landing volumes are not recorded in the repo — unmeasured here.

Watch out.

  • Ingestion lags about a minute; an empty read-out right after deploy is not a bug.
  • More specific routes win: an existing /ingest/* Worker keeps precedence over your new /* route. Verify both after enabling.
  • On a workers.dev hostname fetch(request) recurses into yourself; return a stub in test mode.

Related: cache-api-poisoned-by-transient-error · worker-fetch-own-zone-error-1042