Analytics

What it does

The packages/events cartridge36 is a package for one job, with its implementation and required infrastructure. It records one JSON event for each HTTP request when capture is enabled.

It follows capture first, shape later7. Each event is stored unchanged. Queries select and interpret its fields later.

A stream receives events. A pipeline copies them to a sink, the destination that writes the stored files.

  1. withEventCapture wraps the Worker’s fetch handler.
  2. It sends the event in the background after building the response.
  3. A Worker binding23, a declared connection to a resource, sends the event to an unstructured Cloudflare Pipelines stream.
  4. The pipeline runs INSERT INTO <sink> SELECT * FROM <stream>.
  5. A Basin Catalog sink writes Parquet files to the Iceberg table default.events_raw in R2.

R2 is Cloudflare’s object storage. Parquet stores data by column in files. Iceberg organizes those files as a table. Basin Catalog makes the table queryable with SQL.

Sending events through the public HTTP endpoint is disabled. The sink writes files every 300 seconds by default. See Basin Catalog sink.

Each event holds:

The standard

Worker logs

The three HTTP Worker declarations share privateObservability in apps/site/src/observability.ts:

The policy disables per-request invocation logs and traces. Application console and Effect error logs remain enabled. Effect is the library that describes work with typed failures36 and required services. A service is a named interface for one job.

500s log their cause with an incident id.

Cloudflare invocation logs contain request, response, and related metadata. They are separate from custom application messages. Alchemy cannot set redactQueryString, so invocation logs would store confirm tokens and device codes from query strings. Mischief therefore keeps invocation logs off and writes one HTTP request log per request with method, path without query, status, and milliseconds. joinInterest logs one outcome label (accepted, busy, saved_mail_delayed, not_open, or failed) and nothing else. See Workers Logs.

Client IPs reach Basin only as salted hashes. The Worker policy prevents automatic request-header dumps in stored invocation logs. Application logs must not print raw IPs or request headers.

These settings apply after deployment. They do not erase stored logs from earlier deployments.

Crash archive

Mischief declares MischiefCrashTail as an Alchemy tailConsumers entry. registerCrashTail registers a native tail listener through the host Worker’s listen method. Returning a tail method from the Effect Worker shape does not register this listener.

The tail Worker receives execution traces after Mischief finishes an invocation. It records failed outcomes and traces with exceptions, including exceptions after a successful response.

Successful traces without exceptions produce no object. Initialization exceptions keep their event time and script version even when the trace has no fetch event.

The events cartridge36 owns the CrashArchive port and its R2 adapter. A private MischiefCrashBucket holds one immutable JSON object per failed trace. Keys use crashes/<UTC-day>/<random-id>.json. Separate objects prevent concurrent deliveries from overwriting a daily file.

There is no daily aggregate or second store.

Each record contains only:

Exception messages keep ordinary diagnostic text, including quoted property names. Redaction removes credential-shaped values, emails, IP addresses, and URL query strings. Credential rules cover bearer tokens, key assignments, known token prefixes, JWTs, private keys, and long opaque values. Custom exception names become [redacted name].

Credential rules apply even when Cloudflare removes sensitive header values before delivery. They cannot identify every secret written as ordinary prose.

Stack locations retain a source filename, line, and column. They exclude function names, directory paths, URLs, and the first error line.

Known credential-header, cookie, and query values are removed from exception and console text at token boundaries. Ordinary header values do not participate in redaction. Paths use credential-shape rules instead of substitutions from unrelated headers or query values.

Console records keep the level and redacted string arguments for exception outcomes or traces with exceptions. Structured arguments and serialized request payloads are omitted. Missing levels stay null; older records can have no console field.

The archive excludes request headers, bodies, and diagnostic-channel payloads. Redaction removes recognized private values from diagnostic text.

The tail Worker has no public HTTP route and disables its own observability. Mischief keeps invocation logs and traces disabled. Crash capture operates independently of EVENTS_ENABLED.

R2 permits direct object reads without a catalog, pipeline, or sink token. The archive supplies best-effort evidence. Missing records do not prove health. Cloudflare delivery limits, missing stack data, and archive failures can leave gaps.

Null fields stay unknown; they do not acquire guessed values. Replayed deliveries can create duplicate records with different object keys. The bucket retains its objects when removed from the Stack.

Read crashes without a dashboard

Use an R2 read-only S3 profile for the archive. Get the physical bucket name from the production plan for MischiefCrashBucket. Use a UTC-day prefix to bound the listing. The following commands need AWS CLI and an authenticated R2 S3 profile.

aws --profile <r2-read-profile> \
  --endpoint-url https://<account-id>.r2.cloudflarestorage.com \
  s3api list-objects-v2 --bucket <crash-bucket> \
  --prefix crashes/2026-10-08/ --max-keys 20 --no-paginate

Read Contents[].Key from the result. If IsTruncated is true, pass NextContinuationToken with --continuation-token for the next page. Fetch one exact key; do not sync the entire archive.

aws --profile <r2-read-profile> \
  --endpoint-url https://<account-id>.r2.cloudflarestorage.com \
  s3api get-object --bucket <crash-bucket> \
  --key <listed-key> crash.json

Compare eventTime, path, and scriptVersion with the failed deployment probe. Read stack locations against that script version’s source. A Cloudflare ray id is not stored, so this comparison is a correlation rather than an exact join. Fix the observed cause in a separate change.

Before deployment, run pnpm --filter @rat-stack/events exec vitest run test/crash.test.ts. Run pnpm --filter @rat-stack/site exec vitest run test/crash-tail.test.ts to exercise Alchemy’s exported tail dispatcher. After deployment, verify the tail consumer attachment and read an actual failed trace from R2. Local tests prove projection and archive writes; they do not prove Cloudflare delivery.

Deleting stored events

A delete from events_raw writes a new Iceberg snapshot. Older snapshots keep their Parquet files, and those files still hold the deleted rows.

Snapshot expiry and compaction remove the old files. Cloudflare runs this maintenance on its own schedule. In one test run, no snapshot expired within 38 minutes.

The cartridge does not prove that a delete physically erases a row by a known time. Consider this before you enable capture for visitors who can request erasure, for example under GDPR in the EU or UK:

Switches

SettingDefaultWhat it does
EVENTS_ENABLEDfalseOff retains the bucket, stream, and salt, and records nothing. On adds the catalog, sink, and pipeline.
EVENTS_SINK_TOKENunsetRequires R2 Admin Read & Write when capture is on. Export it in the deploy shell.
EVENTS_IDENTITY_MODEpersistent in the site schemapersistent or daily visitor ids. The Worker falls back to daily when absent.

The sink token is read from the process environment. A value in .env or an --env-file is not seen. The deploy credential cannot create API tokens, so the sink token is supplied separately. See Basin Catalog sink.

How to check

  1. Check an HTML response in persistent mode without a rat_vid cookie. The response must set one.
  2. Check pipelinesUserErrorsAdaptiveGroups for zero pipeline errors.
  3. Check pipelinesSinkAdaptiveGroups for records and files written. See Metrics and analytics.
  4. Find the queryable catalog with wrangler basin catalog get <bucket>.
  5. Query it with wrangler basin sql query <warehouse> "<sql>". The command calls the catalog a warehouse. Wrangler reads WRANGLER_BASIN_SQL_AUTH_TOKEN. See Query data.
  6. Run pnpm --filter @rat-stack/site exec vitest run test/worker-observability.test.ts.
  7. Review the deployed Worker settings before claiming the invocation-log policy is active.

A cookie proves that the persistent capture wrapper is enabled. It does not prove that an event is stored in the table.

SELECT json_get_str(value, 'body', 'path') AS path, count(*) AS requests
FROM default.events_raw
GROUP BY path
ORDER BY requests DESC
LIMIT 20;

The query reads each path from JSON in the value column.

Queries use JSON functions such as json_get_str. See Scalar functions.

Tests

The cartridge uses property-based testing18 and model-based testing14.

Properties cover:

The visitor model follows ids across devices, cookies, and days. The observability test checks every application Worker declaration against a named privacy policy. HTTP Workers use privateObservability; the tail Worker uses the stricter silentObservability.

Removing it

The README’s Keep or cut table lists every file and setting under “No analytics.”

Sources

  1. Tail Workers. Cloudflare docs. Tail consumers receive producer execution traces, including uncaught exceptions. Accessed 2026-10-08.
  2. AWS CLI with R2. Cloudflare docs. S3-compatible object listing and reads through an authenticated R2 endpoint. Accessed 2026-10-08.
  3. Manage streams. Cloudflare docs. Stream modes; used for unstructured JSON ingestion. Accessed 2026-10-01.
  4. Basin Catalog. Cloudflare docs. Sink configuration; used for the R2 Data Catalog sink and its permissions. Accessed 2026-10-01.
  5. Metrics and analytics. Cloudflare docs. Pipeline metrics; used to check dropped records and sink writes. Accessed 2026-10-01.
  6. Query data. Cloudflare docs. Query commands; used to verify rows land in the warehouse. Accessed 2026-10-01.
  7. Scalar functions. Cloudflare docs. JSON scalar functions; used to read request paths from the value column. Accessed 2026-10-01.
  8. Workers Logs. Cloudflare docs. Invocation logs contain request metadata; invocation_logs disables them without disabling custom logs. Accessed 2026-10-02.
  9. rat-stack/packages/events at main · joelhooks/rat-stack · GitHub. GitHub joelhooks/rat-stack. Events cartridge; used for the capture envelope, identity modes, and tests. Accessed 2026-10-01.

Last changed 2026-10-12 in 4533d33. Source on GitHub. Change log.

Linked from

  1. A clean playground. rat-stack does real work with packages that stay easy to add and remove.

    Analytics supplies another working package, with an explicit removal checklist.

  2. Agent guide. An Effect stack so pure (aspirational) Kit Langton will blush.

    Analytics — One event per HTTP request, stored unchanged for queries to select and interpret later.

  3. Agent rules for rat-stack (AGENTS.md). What you may change, which commands to run, and which changes need approval.
  4. An agent applied through Markdown and HTTP. One observed application used the Markdown page and llms.txt, then one POST; confirmation followed 88 seconds later.

    This is one counts-only observation from our request analytics.

  5. Build and run rat-stack (README.md). What is in the repo, how the example works, and how to run it.
  6. Capture first, shape later. Store events without rejecting them for an evolving event shape, then decide their shape when you query.

    Analytics documents that boundary.¶

  7. Cartridges. A cartridge owns one job and brings the infrastructure its service needs.

    Analytics is a second running example: packages/events owns capture and its Basin resources.

  8. Change log. What changed in the files served here, newest first.

    Analytics — edited: feat: log one path-only request line per Mischief request and joinInterest outcome

  9. Feature flags. Typed feature flags with owners, removal dates, and stable subject rollouts.

    See Analytics for capture and identity behavior.¶

  10. Full agent guide. An Effect stack so pure (aspirational) Kit Langton will blush.
  11. Interest signup. Agent-only applications start email confirmation through interfaces owned by core and delivery implemented in a separate package.

    Request analytics, trace spans, and logs never receive those values.

  12. One capability, every surface. A schema-typed contract and one handler feed every interface rat-stack keeps.

    Analytics shows a separate infrastructure cartridge; interest signup shows core ports with a delivery adapter outside core.

  13. Property-based testing. State a rule that must hold for every input, then let a generator hunt for the smallest input that breaks it.

    Analytics documents cookie behavior and what the checks establish about storage in Basin.¶

  14. Purpose and boundaries of rat-stack (VISION.md). What this starter is for and what a useful copy should keep.

    Request analytics runs with a server-set persistent rat_vid cookie; event bodies exclude request bodies and sensitive query keys.

  15. Rat Stack systems | rat-stack. The systems rat-stack ships, the standard each keeps, and how to check it.

    Analytics — One event per HTTP request, stored unchanged for queries to select and interpret later.

  16. rat-stack. An Effect stack so pure (aspirational) Kit Langton will blush.
  17. Source change history (log.md). The generated change log as Markdown.

    Analytics — edited: feat: log one path-only request line per Mischief request and joinInterest outcome

  18. Trace operations where they mean something. Name useful operations, add safe context, and distinguish creating a span from delivering it to a backend.

    Request analytics captures request events through its own sink.