Behaviour events in the nightly digest: the existing GA4 read-only token already covered them

On 25 July I wanted to know whether the GA4 read-only token I already held covered event reports. The ga-telemetry skill was pulling page views, sessions, and top landing pages via the Reporting API. What I needed to check: did the analytics.readonly scope extend to events?

It does. eventName and eventCount are available under the same scope. Adding them to the nightly digest was a server-side change only: extend the ga-telemetry query, format the output, surface it in the digest. No new OAuth flow, no new Keychain entry, no instrumentation on the site, no site deployment. GA4’s default collection was already firing these events; the gap was the query.

That’s the mechanism. The rest of this post is why it took until 25 July to get there, and what the three-layer structure means for what comes next.

The setup

UK Calculators runs a nightly cron estate. GSC pulls search performance. GA4 pulls traffic. AdSense pulls account and earnings status. Results land in a digest; the digest feeds a daily ops email.

The ops email is newer; it landed on 22 July. A single styled HTML email from .editorial/scripts/ops_report.py, written in stdlib only, covering cycle-run status (stage completions, wall times, outcome), heartbeat, awaiting-approval items, and both telemetry snapshots side by side. One glance to cover overnight estate health without opening a terminal. Before the ops email existed, the digest output sat in a log file. Useful if you went looking; invisible otherwise.

The silent period

There’s a gap in that story. The nightly GA and GSC crons ran from late June and produced nothing. Not errors — nothing. Silent success returns with empty output.

I ran a telemetry review on the morning of Saturday 4 July and traced the cause: every cron run since 20 June had failed to read from the external volume due to launchd TCC restrictions. The launchd-spawned process had no Full Disk Access grant; the script sat on a volume that required it. The fix was a Terminal-spawn wrapper. The cron spawns an osascript call that opens a Terminal session with the correct user environment, and that session runs the actual script. Terminal inherits FDA; the launchd daemon does not need to.

The TCC diagnosis and fix is covered separately. Worth noting here only because the silent period meant the telemetry pipeline had no live validation for five weeks. After 4 July, the crons ran clean. By 22 July the ops email was wiring the output up. By 25 July event data was in.

What “freeze-safe” means

The devlog entry for 25 July flags the change as “freeze-safe: server-side only, zero site changes, zero new authorisation.” That flag exists because UK Calculators was mid-AdSense review on 25 July, with the account in GETTING_READY and the review in progress.

Any site change during an active AdSense review is a risk worth avoiding. Adding telemetry that required new client-side tracking code, a modified GA4 configuration, or a new script tag would have been deferred. The event query requires none of that. GA4’s default event collection fires automatically on most standard interactions; no custom instrumentation was needed for the first pass. The events already existed in the GA4 property. Adding the digest query just surfaced them.

This is explicitly Part 1 of the behaviour-telemetry plan. Part 2 will require site changes. Part 1 did not.

What the read-only scope covers

The GA4 token is scoped to analytics.readonly. That scope covers the Reporting API v1 (runReport, batchRunReports, runPivotReport, runRealtimeReport) and the dimensions and metrics those endpoints expose, including eventName, eventCount, and eventCountPerUser. It does not cover configuration: you cannot read custom dimension definitions or modify event parameters via this scope. For the digest use case, that constraint is irrelevant. The query is “what events fired and how many times,” which is a runReport call.

The token already existed for the page-view queries. Reusing it for event queries cost nothing new: no additional consent screen, no new Keychain entry, no second OAuth flow.

The three layers

There are three layers to behaviour telemetry on a site like UK Calculators, and the cost profile differs at each.

Layer 1: default GA4 events. Page views, scrolls, outbound clicks, session starts. Collected automatically. Available immediately under analytics.readonly. This is what landed on 25 July. Freeze-safe.

Layer 2: Enhanced Measurement events. Form interactions, video engagement, file downloads, in-page search. Configurable in GA4 admin without touching site code. Still analytics.readonly at query time. The site change is a GA4 admin toggle, not a deployment.

Layer 3: custom events. Calculator-specific interactions: a submit button tap, a result copy, a share action. These require a gtag event call in the site code. Site deployment required. Not freeze-safe during an active review.

Most teams jump straight to Layer 3 because they want granular calculator-specific data. Layer 1 is available for free and gets you the event distribution immediately. UK Calculators is a tool-first site where most pages are self-contained calculators. The ratio of click events to scroll events is a rough proxy for whether users are completing interactions or scanning content. Not precise enough to act on alone, but directionally useful. Custom events at Layer 3 will sharpen that signal. Layer 1 establishes the baseline.

Where this goes

Layer 1 is in the digest. The nightly run now surfaces eventName and eventCount alongside sessions and page views. The ops email includes both snapshots.

Layer 2 is next: a GA4 admin change, no site deployment, no new auth. When that lands it goes in the devlog in the same format: what changed, what it cost, what it surfaces.

Layer 3 waits for the AdSense review to resolve. Custom events scoped to calculator interactions require site changes; those are off the table while the review is active.

The pattern worth extracting: before assuming behaviour telemetry requires a new integration or a new token, check what the scope you already hold covers. analytics.readonly extends further than the initial page-view use case implies. The event reports were always available. The first query just took until 25 July.

All writing