A complete reference for content and media analytics. All events — from page engagement to video completion — with parameters, data types, and data layer examples you can copy into your tracking plan.
Content sites live and die by engagement metrics. But most teams track nothing beyond page views. That tells you what people clicked on — not what they actually consumed. Did they read the article or bounce after the headline? Did they watch 10 seconds of your video or the full 8 minutes? This reference gives you a structured event schema that turns “visitors” into measurable engagement data.
I’ve used this schema across news publishers, corporate blogs, and media platforms in Europe. It works for any content-driven site — whether you’re monetizing through ads, subscriptions, or lead generation from your content.
Content Engagement Funnel
Content analytics tracks users through four engagement levels. Each level has distinct events that map to specific content performance metrics.
| Level | Key Events | Business Metric |
|---|---|---|
| Reach | page_view, content_impression |
Traffic, impressions |
| Consumption | scroll_depth, read_complete, video_progress |
Engagement rate, avg. consumption |
| Interaction | content_share, comment_posted, reaction_added |
Social amplification, community |
| Conversion | newsletter_subscribe, paywall_hit, subscription_started |
Subscriber rate, revenue |
Page Engagement Events
Page views tell you someone arrived. Engagement events tell you they stayed. The gap between the two is where most content analytics fails.
| Event | Parameters | Type | When It Fires |
|---|---|---|---|
scroll_depth |
percent_scrolled |
number | User scrolls past threshold (25, 50, 75, 100) |
content_type |
string | ||
content_id |
string | ||
word_count |
number | ||
read_complete |
content_id |
string | User reaches end of article content |
content_type |
string | ||
time_on_content |
number | ||
word_count |
number | ||
time_on_content |
engaged_seconds |
number | Fires at intervals (30s, 60s, 120s, 300s) |
content_id |
string | ||
content_visible |
boolean | ||
content_impression |
content_id |
string | Content card visible in viewport (lists, feeds) |
content_title |
string | ||
list_position |
number | ||
list_name |
string |
The content_type parameter is your segmentation key. Use consistent values: article, guide, case_study, news, tutorial, landing_page, or blog_post. This lets you compare engagement across content formats without separate events for each.
For scroll_depth, fire at 25% intervals. Avoid tracking every pixel — it creates noise and burns through GA4’s event quotas. The 25/50/75/100 pattern gives you enough granularity to spot drop-off points. When I set this up for a tech publisher in Berlin, we discovered that 70% of readers dropped off before 50% scroll on long-form articles — which led to restructuring content with key takeaways at the top.
Important: the content_visible parameter on time_on_content distinguishes active reading from tab-in-background. Use the Page Visibility API to track whether the tab is actually focused. Without this, your time metrics will be inflated by users who opened your article and switched to another tab.
Data Layer Example: Scroll Depth
window.dataLayer.push({ "event": "scroll_depth", "percent_scrolled": 50, "content_type": "article", "content_id": "post-1234", "word_count": 2400 });
Video Tracking Events
Video is the hardest content type to track well. The challenge isn’t firing events — it’s firing them consistently across YouTube embeds, self-hosted players, and third-party platforms. This schema works with any player that exposes JavaScript events.
| Event | Parameters | Type | When It Fires |
|---|---|---|---|
video_start |
video_title |
string | User initiates video playback |
video_id |
string | ||
video_provider |
string | ||
video_duration |
number | ||
is_autoplay |
boolean | ||
video_progress |
video_title |
string | Video reaches 25%, 50%, 75% mark |
video_id |
string | ||
percent_watched |
number | ||
video_duration |
number | ||
video_complete |
video_title |
string | Video reaches 95%+ of duration |
video_id |
string | ||
video_duration |
number | ||
watch_time |
number | ||
video_pause |
video_title |
string | User pauses playback |
percent_watched |
number | ||
video_seek |
video_title |
string | User jumps to different position |
seek_from |
number | ||
seek_to |
number |
Use video_provider to distinguish between player types: youtube, vimeo, wistia, html5, or jwplayer. This matters because YouTube embeds fire events differently than self-hosted HTML5 players, and your GTM triggers need to account for both.
Fire video_complete at 95%, not 100%. Most users skip the last few seconds — especially when an end screen or outro appears. Setting the threshold at 95% gives you a more accurate completion rate. The watch_time parameter (actual seconds watched, excluding paused time) is more valuable than video_duration for measuring real engagement.
The is_autoplay parameter on video_start is critical. Without it, autoplay views inflate your start counts and destroy your start-to-complete ratio. Always separate autoplay from user-initiated plays in your reports.
Data Layer Example: Video Progress
window.dataLayer.push({ "event": "video_progress", "video_title": "GA4 Migration Walkthrough", "video_id": "dQw4w9WgXcQ", "video_provider": "youtube", "percent_watched": 50, "video_duration": 480 });
Audio & Podcast Events
Audio tracking follows the same pattern as video but with podcast-specific parameters. If you host episodes on your site or embed players from Spotify, Apple Podcasts, or a custom player, these events capture listening behavior.
| Event | Parameters | Type | When It Fires |
|---|---|---|---|
audio_start |
audio_title |
string | User initiates audio playback |
audio_id |
string | ||
audio_type |
string | ||
audio_duration |
number | ||
episode_number |
number | ||
audio_progress |
audio_title |
string | Audio reaches 25%, 50%, 75% |
percent_listened |
number | ||
audio_complete |
audio_title |
string | Audio reaches 95%+ of duration |
audio_id |
string | ||
listen_time |
number | ||
audio_speed_change |
playback_speed |
number | User changes playback speed |
audio_title |
string |
Use audio_type to distinguish between podcast_episode, audiobook, music, or voice_article. The audio_speed_change event is surprisingly useful — when a significant portion of listeners use 1.5x or 2x speed, it often signals that your content could be more concise.
Social Sharing & Interaction Events
Sharing events measure content amplification — when readers become distributors. These are the events that tie content quality to organic reach.
| Event | Parameters | Type | When It Fires |
|---|---|---|---|
content_share |
share_method |
string | User clicks a share button |
content_id |
string | ||
content_type |
string | ||
content_title |
string | ||
copy_link |
content_id |
string | User copies article URL |
copy_source |
string | ||
comment_posted |
content_id |
string | User submits a comment |
comment_length |
number | ||
is_reply |
boolean | ||
reaction_added |
reaction_type |
string | User clicks like, bookmark, or emoji |
content_id |
string | ||
text_selected |
selected_text_length |
number | User highlights text (engagement signal) |
content_id |
string |
Use share_method values like twitter, linkedin, facebook, email, whatsapp, or native_share (for the browser’s Web Share API). The copy_link event captures dark social — people who share by pasting URLs directly into Slack, email, or messages. In my experience, copy-link sharing accounts for 40-60% of all shares on B2B content, yet most analytics setups miss it entirely.
The text_selected event is a lightweight engagement signal — people who highlight text are actively reading and often preparing to quote or reference your content. Don’t overtrack it: fire once per selection, and only when the selection is longer than 20 characters to avoid noise from accidental clicks.
Newsletter & Subscription Events
For content businesses, email subscribers and paid subscribers are the conversion events that map directly to revenue and audience growth.
| Event | Parameters | Type | When It Fires |
|---|---|---|---|
newsletter_subscribe |
subscribe_location |
string | User submits email for newsletter |
newsletter_name |
string | ||
content_id |
string | ||
newsletter_unsubscribe |
newsletter_name |
string | User unsubscribes (server-side) |
days_subscribed |
number | ||
paywall_hit |
content_id |
string | User encounters paywall/gate |
content_type |
string | ||
paywall_type |
string | ||
articles_read |
number | ||
subscription_started |
plan_name |
string | User starts paid subscription |
plan_price |
number | ||
currency |
string | ||
billing_period |
string | ||
subscription_cancelled |
plan_name |
string | User cancels paid subscription |
cancel_reason |
string | ||
days_subscribed |
number |
Use subscribe_location to track which placements convert: inline, end_of_article, popup, sidebar, exit_intent, or header_bar. Combined with content_id, you can see which specific articles drive the most newsletter signups.
The paywall_hit event is essential for metered paywall models. Track articles_read to see how many free articles users consume before hitting the wall — and how that number correlates with conversion to paid. Use paywall_type values like hard, metered, freemium, or registration_wall.
Data Layer Example: Newsletter Subscribe
window.dataLayer.push({ "event": "newsletter_subscribe", "subscribe_location": "end_of_article", "newsletter_name": "weekly_digest", "content_id": "post-1234" });
Search & Navigation Events
Internal search and navigation events reveal what users want to find — and whether your content architecture helps or hinders discovery.
| Event | Parameters | Type | When It Fires |
|---|---|---|---|
internal_search |
search_term |
string | User submits a search query |
results_count |
number | ||
search_location |
string | ||
search_result_click |
search_term |
string | User clicks a search result |
result_position |
number | ||
result_content_id |
string | ||
category_view |
category_name |
string | User browses a category/tag page |
content_count |
number | ||
related_content_click |
source_content_id |
string | User clicks a recommended article |
target_content_id |
string | ||
recommendation_type |
string |
The internal_search event with results_count = 0 is a gold mine for content strategy. These zero-result searches tell you exactly what your audience wants but can’t find. Export them monthly and use them to prioritize new articles.
Complete Event Map
Here’s every content and media event at a glance, organized by engagement level with recommended GA4 key event status.
| Level | Event Name | Source | Key Event? |
|---|---|---|---|
| Reach | content_impression |
Client | No |
| Consumption | scroll_depth |
Client | No |
read_complete |
Client | Yes | |
time_on_content |
Client | No | |
video_start |
Client | No | |
video_progress |
Client | No | |
video_complete |
Client | Yes | |
video_pause |
Client | No | |
video_seek |
Client | No | |
audio_start |
Client | No | |
audio_progress |
Client | No | |
audio_complete |
Client | Yes | |
audio_speed_change |
Client | No | |
| Interaction | content_share |
Client | No |
copy_link |
Client | No | |
comment_posted |
Client | No | |
reaction_added |
Client | No | |
text_selected |
Client | No | |
| Navigation | internal_search |
Client | No |
search_result_click |
Client | No | |
category_view |
Client | No | |
related_content_click |
Client | No | |
| Conversion | newsletter_subscribe |
Client | Yes |
newsletter_unsubscribe |
Server | No | |
paywall_hit |
Client | No | |
subscription_started |
Client/Server | Yes | |
subscription_cancelled |
Server | No |
Key events should be reserved for actions that represent genuine content engagement milestones or business value. Mark read_complete, video_complete, audio_complete, newsletter_subscribe, and subscription_started as key events. Everything else stays as regular events — valuable for analysis but not conversions.
GTM Implementation Guide
Here’s how to set up the core content tracking events in Google Tag Manager.
| Event | GTM Tag Name | Trigger Type | Notes |
|---|---|---|---|
scroll_depth |
GA4 Event – scroll_depth | Scroll Depth | Set 25, 50, 75, 100 thresholds |
read_complete |
GA4 Event – read_complete | Element Visibility | Target: end-of-article marker element |
video_start |
GA4 Event – video_start | YouTube Video / Custom Event | Use built-in YouTube trigger or dataLayer |
video_progress |
GA4 Event – video_progress | YouTube Video / Custom Event | Set 25, 50, 75 progress thresholds |
video_complete |
GA4 Event – video_complete | YouTube Video / Custom Event | Use Complete status or 95% threshold |
content_share |
GA4 Event – content_share | Click – All Elements | Filter by share button class/data attribute |
newsletter_subscribe |
GA4 Event – newsletter_subscribe | Custom Event (dataLayer) | Fire on success callback from email provider |
internal_search |
GA4 Event – internal_search | Form Submission / Custom Event | Capture search term from input field |
YouTube tracking tip: GTM has a built-in YouTube video trigger that handles start, progress, pause, and complete events automatically. Enable it in your trigger settings and you get video tracking with zero custom code. For non-YouTube players (Vimeo, Wistia, HTML5), you’ll need custom JavaScript that listens to the player’s API and pushes events to the data layer.
Parameter Reuse Strategy
Consistent parameter naming across all content events conserves your GA4 custom dimension slots and simplifies reporting.
| Parameter | Reused Across | Example Values |
|---|---|---|
content_id |
All content events | post-1234, video-567, episode-89 |
content_type |
scroll_depth, read_complete, content_share, paywall_hit | article, guide, case_study, news, tutorial |
video_title |
video_start, video_progress, video_complete, video_pause, video_seek | Human-readable video title |
audio_title |
audio_start, audio_progress, audio_complete, audio_speed_change | Human-readable audio/episode title |
search_term |
internal_search, search_result_click | User’s search query text |
Five shared parameters cover 20+ events. The content_id parameter is the most important — it lets you join engagement data across event types. You can answer questions like “which articles that people read completely also generated the most newsletter signups?” by filtering on the same content_id.
Validation Checklist
Before going live with your content tracking, verify each item:
Implementation Checklist
scroll_depthfires only at 25% thresholds (not every pixel)read_completetargets the correct end-of-content element (not page footer)time_on_contentpauses when tab is not visible (Page Visibility API)video_startdistinguishes autoplay from user-initiated playsvideo_completefires at 95%, not 100%content_sharecaptures the actual platform, not just “clicked share button”newsletter_subscribefires on success callback, not on form submitinternal_searchlogs zero-result searches (results_count = 0)- All
content_idvalues are consistent across events for the same content - No PII leaks in search terms or content parameters
- Test in GA4 DebugView across desktop and mobile
FAQ
Should I use GA4’s built-in scroll and video events or custom ones?
GA4’s enhanced measurement includes basic scroll (90% only) and YouTube video events. They’re a good start, but limited. The built-in scroll event fires at just one threshold. Custom scroll tracking at 25% intervals gives you much more granular data. For video, GA4’s enhanced measurement only handles YouTube — if you use other players, you need custom events anyway. I recommend disabling the built-in events and implementing custom ones for consistency.
How do I track read completion accurately?
Use GTM’s Element Visibility trigger pointed at an invisible marker element placed at the end of your article content — after the last paragraph, before comments or related posts. Set it to fire when 100% of the element is visible, with a minimum on-screen duration of 1 second. This prevents false completions from fast-scrolling users who zip past the bottom without reading.
What’s a good read completion rate for articles?
Average read completion rates vary widely by content type. News articles typically see 20-30%, long-form guides 15-25%, and tutorials or how-to content 30-45%. Don’t compare across types — compare within the same content format and similar word count. A 25% completion rate on a 5,000-word deep dive may be excellent, while the same rate on a 500-word news piece signals a problem.
How should I handle tracking across paginated content?
For paginated articles (multi-page or infinite scroll), treat each page/section as a separate scroll_depth context with the same content_id but different page_number parameter. Fire read_complete only when the user reaches the end of the final page. For infinite scroll feeds, use content_impression to track which items enter the viewport instead of scroll_depth.
Is tracking text selection a privacy concern?
Track the length of selected text, not the text itself. Recording actual highlighted text could capture PII if users select their own name, email, or sensitive content. The selected_text_length parameter gives you the engagement signal without the privacy risk. Under GDPR, this qualifies as non-essential tracking and requires consent in most implementations.
Related Guides on EU-Medin
Further Reading
- GA4 Recommended Events Reference — Google Analytics
- Enhanced Measurement Events — Google Analytics Help
- YouTube IFrame Player API — Google Developers
- YouTube Video Tracking with GTM — Analytics Mania
Product Analytics Specialist based in Munich. Helping teams implement clean, actionable analytics since 2015.