Content analytics should explain whether people find the right material, consume enough of it to make progress, and complete the task the content was designed to support. Page views alone cannot answer those questions.
This recipe applies to documentation, blogs, guides, changelogs, learning centers, and in-product help. It combines Web analytics, Product search analytics, and Survey and feedback analytics.
Build a content catalog#
Give each published item stable metadata:
{
"content_id": "docs:reliable_event_delivery",
"content_version": 7,
"content_type": "documentation",
"section": "guides",
"topic": "event_delivery",
"audience": "backend_engineer",
"owner_team": "developer_experience",
"published_at": "2026-07-26T00:00:00Z",
"last_reviewed_month": "2026-07",
"status": "published"
}
Keep titles and full text in the content system. Send bounded identifiers and categories to GraphJSON.
Stable content IDs let a page move without breaking its history. Increment
content_version when meaning changes materially, not for every typo.
Instrument content views#
{
"event": "content_viewed",
"view_id": "view_01J...",
"session_id": "sess_42",
"content_id": "docs:reliable_event_delivery",
"content_version": 7,
"content_type": "documentation",
"section": "guides",
"topic": "event_delivery",
"route_template": "/docs/Guides/:slug",
"entry_method": "internal_search",
"referrer_type": "same_site",
"release": "web-2026.07.26.1"
}
Generate view_id for the rendered content instance. Exclude full referrer
URLs, query strings, and page paths containing user input.
Distinguish a server request, client render, and view that became visible. Automated previews and prefetches are not human views.
Measure engaged reading carefully#
Time on page is not automatically reading. A tab can remain open in the background.
Use:
- page visible and focused
- periodic bounded heartbeats
- content scroll progress
- interaction with examples or navigation
- terminal page-hide or route-change event
Emit one derived terminal summary:
{
"event": "content_engagement_completed",
"view_id": "view_01J...",
"session_id": "sess_42",
"content_id": "docs:reliable_event_delivery",
"content_version": 7,
"engaged_seconds": 184,
"maximum_progress_bucket": "75_99",
"example_interactions": 2,
"outcome": "continued_to_related_guide",
"engagement_definition_version": 3
}
Use progress buckets rather than precise scroll percentages. Long pages and short pages are not directly comparable.
Do not claim that engaged time proves comprehension. A confusing article can produce long sessions.
Track high-intent content actions#
Useful controlled events:
- code example copied
- API request example expanded
- related guide opened
- external reference followed
- download started
- changelog entry opened
- edit or issue link selected
- CTA selected
{
"event": "content_action_completed",
"view_id": "view_01J...",
"content_id": "docs:reliable_event_delivery",
"action": "code_example_copied",
"example_key": "typescript_outbox_worker",
"destination_type": "clipboard"
}
Do not send copied text or arbitrary link URLs.
Connect internal search#
Preserve one search journey:
search request
→ results returned
→ content selected
→ content engagement
→ next task or product outcome
Attach the originating search_id and selected rank to the content view. This
supports:
- zero-result topics
- selected-result rank
- reformulation after reading
- search exits
- downstream success
Do not log raw queries by default. Use controlled topic classes and privacy-safe diagnostics from the search guide.
Define content success by intent#
Different content types need different outcomes:
| Content | Useful outcome |
|---|---|
| Quickstart | First accepted event |
| API reference | Successful request or resolved integration error |
| Troubleshooting | Problem resolved without repeated search or support contact |
| Concept guide | Related implementation guide started |
| Changelog | Affected migration or review action completed |
| Blog article | Qualified continuation, signup, or later product activation |
Avoid one universal content-conversion metric.
Link product outcomes conservatively#
When permitted, carry a content_view_id or session-level content touch to a
later product fact:
{
"event": "first_event_accepted",
"account_id": "acct_7",
"user_id": "usr_42",
"content_view_id": "view_01J...",
"assistance_type": "quickstart",
"result": "success"
}
Define the attribution window and competing touches. Call the result content-assisted, not content-caused.
Anonymous-to-known identity must follow the documented consent and identity-stitching policy.
Query content-assisted outcomes#
WITH views AS (
SELECT
JSONExtractString(json, 'view_id') AS view_id,
argMin(JSONExtractString(json, 'content_id'), timestamp) AS content_id,
argMin(JSONExtractString(json, 'session_id'), timestamp) AS session_id,
min(toDateTime(timestamp)) AS viewed_at
FROM content_events
WHERE JSONExtractString(json, 'event') = 'content_viewed'
GROUP BY view_id
),
outcomes AS (
SELECT
JSONExtractString(json, 'session_id') AS session_id,
min(toDateTime(timestamp)) AS outcome_at
FROM product_events
WHERE
JSONExtractString(json, 'event') = 'first_event_accepted'
AND JSONExtractString(json, 'result') = 'success'
GROUP BY session_id
)
SELECT
views.content_id,
uniqExact(views.session_id) AS mature_view_sessions,
uniqExactIf(
views.session_id,
outcomes.outcome_at >= views.viewed_at
AND outcomes.outcome_at < views.viewed_at + INTERVAL 7 DAY
) AS assisted_sessions,
assisted_sessions / nullIf(mature_view_sessions, 0) AS assisted_rate
FROM views
LEFT JOIN outcomes USING session_id
WHERE views.viewed_at < now() - INTERVAL 7 DAY
GROUP BY views.content_id
ORDER BY mature_view_sessions DESC
One session can view several pages before the outcome. This query gives each page assistance credit; it is not a single-touch causal attribution model.
Collect feedback with context#
{
"event": "content_feedback_submitted",
"feedback_id": "fb_81",
"view_id": "view_01J...",
"content_id": "docs:reliable_event_delivery",
"content_version": 7,
"response": "not_helpful",
"reason_category": "missing_example",
"surface": "page_footer"
}
Use controlled reason categories. Keep free-text comments in a dedicated, access-controlled feedback system.
Show feedback response rate. A small self-selected group is not representative of all readers.
Measure content paths#
Useful journeys:
- landing page → concept → quickstart → first event
- error search → troubleshooting → API reference → successful request
- blog → docs → signup → activation
- changelog → migration guide → implementation completion
Remove global navigation and repeated refresh noise. Use content_id, not page
title, in path arrays.
Do not assume the last page was the only helpful page.
Find stale and missing content#
Join catalog metadata with behavior:
- high traffic with old review date
- high search exits
- repeated reformulation after a page
- low example interaction on implementation pages
- increased negative feedback after a product change
- support categories with no corresponding guide
- broken or removed destination actions
Traffic is not the only priority. Low-traffic security, billing, recovery, and incident documentation can still be critical.
Use the Event catalog pattern for ownership and lifecycle.
Filter invalid traffic#
Exclude or separate:
- search-engine crawlers
- link unfurlers
- uptime checks
- accessibility and visual regression bots
- documentation build previews
- employees and test accounts
- repeated reloads from development
Use server-verified flags and maintained reference data. User-agent filtering alone is incomplete.
Build the content dashboard#
Show:
- eligible human views by content type and topic
- entry source and internal-search contribution
- engaged-time and progress distributions
- high-intent content actions
- search reformulation and exit after content
- intent-specific assisted outcomes
- feedback rate and controlled reasons
- common learning paths
- high-impact stale or missing content
- bot-filter and instrumentation coverage
Keep content version and review month available during investigations.
Launch checklist#
- Every item has a stable content ID, owner, type, topic, and version.
- Views represent rendered visible content, not server requests.
- Full URLs, raw searches, copied text, and feedback text are excluded.
- Engaged time stops for background or hidden pages.
- Progress uses bounded buckets and page-length context.
- Content outcomes reflect the item’s intended job.
- Assisted outcomes use explicit windows and causal language.
- Search, feedback, and product identities follow consent policy.
- Bots, previews, and internal traffic are separated.
- Staleness review includes critical low-traffic content.