Project output written to --out. Default is out/analytics.json for pend-analytics audit. Shape is AuditRun in src/types.ts.
When a PageSpeed key is set, Lighthouse screenshots go under dirname(--out)/psi-shots/ and each lab row records the relative path as analytics.psi.pages[].screenshot. Paths only - image bytes never go into the JSON.
Input · Output (this page) · Analytics · Security
This engine does not rate traffic. The score object is kept so consumers that already read a Pendulum envelope still parse. Do not treat the band as a readiness claim.
pages and findings are always empty arrays. There is no check catalog.
| name | value example | description / info |
|---|---|---|
| Automated readiness | 0 | Always zero. |
| Worst page score | 0 | Always zero. |
| Band | "Good" | Always this label so the envelope does not look like a failed audit. Not a traffic rating. |
| Issue burden | all zeros | Always zeros. |
| Manual completion | 0 / 0 | Always empty. |
Fields written to --out (default out/analytics.json) for pend-analytics audit. Missing values are omitted, never invented defaults.
The payload is run.analytics. Provider failures stay in errors[] and the process still exits 0. Setup and troubleshooting: analytics.md.
| name | value example | description / info |
|---|---|---|
version | 1 | AuditRun envelope. Not the package version. |
tool | "@pendulumdev/analytics" | Producer id. |
generatedAt | "2026-09-11T10:00:00.000Z" | ISO timestamp when the pull finished. |
project | "Example Project" | From config project. |
standard | "Pendulum_Analytics_v1" | From config standard. |
baseUrl | "https://example.com" | From config when set. |
score | Unscored zeros. See above. | |
pages | [] | Always empty. |
findings | [] | Always empty. |
analytics.range | Resolved primary window, plus previous window when compare is on. | |
start / end | "2026-01-01" | Inclusive YYYY-MM-DD. |
previousStart / previousEnd | "2025-12-04" | Present when comparePrevious is on. |
analytics.gsc | Search Console daily rows, top pages, top queries, totals. | |
analytics.ga4 | Sessions, landing pages, channels, devices, new vs returning. | |
analytics.googleSetup | Public tags. Unique snippets (each with destinations and the pages they appear on), optional GTM containers, collisions. | |
googleSetup.urls | ["https://example.com/"] | Pages fetched for the tag scan, homepage first. |
googleSetup.snippets | Deduped by fingerprint. Destinations are the IDs read from that snippet. | |
kind | "gtag" | gtm, gtag, ua, or other. |
location | "head" | head, body, or unknown. |
text | "https://www.googletagmanager.com/gtag/js?id=G-…" | Truncated snippet or loader URL. |
fingerprint | "8b417677" | Stable hash of text. |
destinations | [{ "family": "ga4", "id": "G-CDEC0MPC01" }] | IDs found in this snippet. |
pages | ["https://example.com/", "https://example.com/about"] | Scanned URLs this snippet was found on. |
analytics.events | Declared vs received (and optional observed) event names. | |
analytics.crux | Origin Chrome UX Report field vitals, when a PageSpeed key is set. | |
reason | "not-found" | Present when vitals are missing. not-found (no field record), empty (record with no p75s), or error. |
detail | "chrome ux report data not found" | Google or transport message for not-found and error. |
analytics.psi | Homepage lab rows (mobile and desktop), when a PageSpeed key is set. | |
analytics.rivals | Named rival origins: homepage CrUX and PageSpeed lab, when a PageSpeed key is set. | |
analytics.insights | Period signals. Omitted rows mean the threshold was not met. See the table below. | |
id | "gsc-clicks-drop-action" | Stable insight id. |
response | "action" | watch (monitor the next window) or action (take steps now). Not a score or how bad the number is. |
title | "Organic clicks dropped sharply" | Short label. |
detail | Period numbers for this run. | |
remediation | engineer and client copy. | |
analytics.errors | Provider, auth, tags, observe, CrUX, or PSI soft-failures. | |
source | "psi" | gsc, ga4, auth, tags, observe, crux, psi, or rival. |
message | "https://…/ (mobile): Timed out" | Human-readable failure. |
host | "https://pendulumdev.co.uk" | Origin for psi, crux, and rival rows so the site and named rivals stay distinguishable. |
timing | Wall time for the pull. Browser and crawl times stay zero. |
Period signals, not a check catalog. A row appears only when its threshold is crossed. An empty list means nothing notable this window, not that everything passed.
response is what to do next, not how severe the metric is. watch means monitor over the next window. action means take steps now.
| id | response | title |
|---|---|---|
gsc-clicks-drop-action | action | Organic clicks dropped sharply |
gsc-clicks-drop-watch | watch | Organic clicks trended down |
gsc-impressions-drop-action | action | Search impressions dropped sharply |
gsc-impressions-drop-watch | watch | Search impressions trended down |
gsc-low-ctr-queries | action | High-impression queries with low CTR |
gsc-position-worse | watch | Average position worsened |
ga4-sessions-drop-action | action | Sessions dropped sharply |
ga4-sessions-drop-watch | watch | Sessions trended down |
Period drop rows need comparePrevious and a previous window. action is a 25%+ fall vs the prior period; watch is a 10-25% fall. Low-CTR needs queries in the top 10 with at least 100 impressions and CTR under 2%. Position watch fires when average position is 1.5 or more worse and impressions stayed within 10%.
readRunJson() requires score, analytics, and pages, then hands back a typed AuditRun. Re-reading a run written by an older version is the common failure, and it is better to fail with a clear message than to render half a report.