Why GA4 says your authors get 40 seconds — and how we get to the real number
GA4 has three different scopes for data — event, session, and user. Mix them wrong and your best author looks like your worst. Here is exactly how we navigate that limitation to show real per-author engagement time.
userEngagementDuration / sessions grouped by customEvent:post_author. Returns ~one article's read time. Looks broken.
Landing-page attribution: when a session starts on an author's article, we credit the entire session's engagement to them.
GA4 has three scopes. Mixing them is where it all goes wrong.
Every dimension and metric in GA4 lives in exactly one scope. If you join something event-scoped (like a custom event parameter) with something session-scoped (like total session count), the math is silently misleading.
Session scope
session_source, session_medium, landing_page, session_campaign — set when the session begins, then attached to every later event.
Perfect for "this whole visit started on X" or "came from Y".
Cannot change mid-session, even if the user navigates somewhere different.
If post_author were truly session-scoped, naive division would work. But GA4 only treats it that way for the LANDING page — not for every author the visitor reads.
One session. Three authors. GA4 splits the time.
Watch what happens when a single visitor reads articles by three different authors in one visit. Press play and see how the naive query buries the author who actually earned the click.
Lands on Author A
Author A's post
Clicks related: Author B
Author B's post
Back to Author A
Author A's post
Skims Author C
Author C's post
Naive: post_author × engagement
SELECT customEvent:post_author, SUM(userEngagementDuration) / sessionsEach author only gets credit for the seconds spent on their own page. Author A — who earned the click — looks no better than Author B. Author C, who got 12 lazy seconds, gets the same per-event treatment.
Glarify: landing-session attribution
landingPagePlusQueryString → post_author map → SUM(engagement) / sessionsAuthor A landed the visitor and gets credit for the entire stickiness of the session — including the time the visitor spent on B and C. That is the question content teams actually ask: "who pulls people in?"
How we actually compute it
Five steps, one batched call per property, deterministic joins. No fuzzy matching, no machine guessing — just careful schema design around GA4's scope rules.
- Step 1
Query 1 — Landing-page engagement
For each property, ask GA4: per landing page, total sessions and total userEngagementDuration. This is the only metric in GA4 that legitimately spans a whole session, anchored to where it began.
dimensions: [landingPagePlusQueryString] metrics: [sessions, userEngagementDuration]
- Step 2
Query 2 — Path → author map
Separately, fetch pagePath × customEvent:post_author × screenPageViews. For every URL we know who wrote it (using the highest-view author when reassignments happen).
dimensions: [pagePath, customEvent:post_author] metrics: [screenPageViews]
- Step 3
Normalize both sides
GA4 returns landing paths with query strings, AMP variants, pagination and casing differences. Our normalizer (src/lib/author-attribution.ts) collapses all of that to one canonical key on both sides so the join actually matches.
/Author/Bio?utm=x → /author/bio /post/amp/ → /post /page/2/ → /
- Step 4
Join + aggregate
Join landing rows to the author of that path. Sum sessions and engagement per author across all properties. The final number is total engaged seconds ÷ landing sessions for that author.
avgTime[author] = Σ engagementDuration / Σ sessions (over landings owned by author) - Step 5
Background fetch + merge
Landing attribution is heavier than a vanilla GA4 call, so we run it in the background after the page paints. Per-page reading time shows immediately; landing-session time slots in once ready, no opt-in required.
See the gap with your own assumptions
Drag the sliders to model an author. The naive number is what GA4 hands you by default. The Glarify number is what landing attribution surfaces.
GA4 default (post_author scope)
Just one article's read time.
Glarify (landing-session)
Whole visit credited to the author who earned the landing.
The four things this still cannot do
Landing attribution is a much better answer than the naive query — but it is not magic. Here is exactly where it bends.
Untagged pages drop out
If post_author was added to your site after some posts were live, those older pages have no author parameter. Their landing sessions don't get credited to anyone. We surface this as a coverage badge so you know what fraction of traffic is attributable.
GA4 thresholding on small audiences
When an author has fewer than ~50 sessions in the date range, GA4's privacy thresholding can return zero or aggregate the row away. We mark those rows as "low data" rather than implying zero engagement.
Reassigned posts
If an article changes author over time, GA4 will return rows for both. We pick the author with the most pageviews in the date range to own the path. Aliases (configured in admin) take precedence so admins can merge variants.
Single-page sessions
GA4 sometimes records zero engagement for sessions that fired only one event before the user left. Those still count toward the session denominator, which can pull the average down. This is GA4's definition, not a bug — we report what GA4 reports.