Publishing & Editorial Event Spec
This page defines all standard events for news sites, blogs, magazines, and content publishers. These events cover the full reader lifecycle — content discovery, article reading and media engagement, search, registration, newsletter subscriptions, paywall interactions, subscription conversion, and retention. Implement these events to enable accurate server-side delivery to advertising and analytics platforms.
Where an event overlaps with e-commerce (search, sign_up, login, generate_lead, view_promotion, select_promotion), Signal reuses GA4’s recommended event name so existing Google Tag Manager event names work as-is. Publishing-specific events use the article_*, content_*, and domain-named conventions below.
All events are sent via datafly.track(). Properties are passed as a flat object in the second argument.
Never pass raw personal data (email address, phone number, full name) in event properties. Hash identifiers with SHA-256 (lowercase, trimmed) before sending — Signal forwards the hashed value to Meta CAPI, TikTok, and other platforms for identity matching.
Content Discovery Events
view_item_list
Fired when a user views a section front, topic hub, homepage feed, or list of article cards.
datafly.track('view_item_list', {
item_list_id: 'technology-front',
item_list_name: 'Technology — Section Front',
items: [
{ item_id: 'art-20260311-001', item_name: 'The Future of Server-Side Tracking', index: 0, content_category: 'Technology', author: 'Jane Holloway' },
{ item_id: 'art-20260311-002', item_name: 'Inside the Cookieless Web', index: 1, content_category: 'Technology', author: 'Sam Reed' }
]
})| Property | Type | Required | Description |
|---|---|---|---|
item_list_id | string | Optional | Machine-readable identifier for the list, section, or feed |
item_list_name | string | Optional | Human-readable name of the list or section being browsed |
items | array | Optional | Array of content items displayed in the list (see Content Items Schema) |
select_content
Fired when a user clicks an article card, headline, or recommendation within a list or feed.
datafly.track('select_content', {
content_type: 'article',
item_list_id: 'technology-front',
item_list_name: 'Technology — Section Front',
items: [
{ item_id: 'art-20260311-001', item_name: 'The Future of Server-Side Tracking', index: 0, content_category: 'Technology' }
]
})| Property | Type | Required | Description |
|---|---|---|---|
content_type | string | Optional | Type of content selected (e.g. article, video, gallery, podcast) |
item_list_id | string | Optional | Identifier of the list the content was selected from |
item_list_name | string | Optional | Name of the list the content was selected from |
items | array | Optional | Array containing the selected content item (see Content Items Schema) |
category_viewed
Fired when a user browses a section, topic, or category listing page (as distinct from a homepage feed).
datafly.track('category_viewed', {
category: 'Technology',
page: 2
})| Property | Type | Required | Description |
|---|---|---|---|
category | string | Required | Section or category name |
page | number | Optional | Pagination page number (1-indexed). Omit or set to 1 for the first page. |
view_promotion
Fired when a promotional unit — a sponsored placement, house ad, or featured-story banner — becomes visible to the user.
datafly.track('view_promotion', {
promotion_id: 'SUB-DRIVE-Q2',
promotion_name: 'Spring Subscription Drive',
creative_name: 'sub-drive-hero-v2.jpg',
creative_slot: 'homepage-hero'
})| Property | Type | Required | Description |
|---|---|---|---|
promotion_id | string | Optional | Identifier for the promotion |
promotion_name | string | Optional | Human-readable name of the promotion |
creative_name | string | Optional | Name or URL of the creative asset shown |
creative_slot | string | Optional | Slot or placement where the promotion appeared |
select_promotion
Fired when a user clicks a promotional unit or house-ad creative.
datafly.track('select_promotion', {
promotion_id: 'SUB-DRIVE-Q2',
promotion_name: 'Spring Subscription Drive',
creative_name: 'sub-drive-hero-v2.jpg',
creative_slot: 'homepage-hero'
})| Property | Type | Required | Description |
|---|---|---|---|
promotion_id | string | Optional | Identifier for the promotion |
promotion_name | string | Optional | Human-readable name of the promotion |
creative_name | string | Optional | Name or URL of the creative asset shown |
creative_slot | string | Optional | Slot or placement where the promotion appeared |
Search Events
search
Fired when a user submits a search query on the site. This is the canonical GA4 search event.
datafly.track('search', {
search_term: 'climate policy',
results_count: 47,
filters: {
date_range: 'last_30_days',
category: 'Environment'
}
})| Property | Type | Required | Description |
|---|---|---|---|
search_term | string | Required | The search term entered by the user |
results_count | number | Optional | Number of results returned for the query |
filters | object | Optional | Key-value map of any filters applied to the search |
search_performed
Publishing-specific search event for site searches you want to track distinctly from the canonical GA4 search (e.g. archive search, paywalled-content search). Use this when your blueprints route editorial search separately from product search.
datafly.track('search_performed', {
search_term: 'climate policy',
results_count: 47,
search_type: 'archive'
})| Property | Type | Required | Description |
|---|---|---|---|
search_term | string | Required | The search term entered by the user |
results_count | number | Optional | Number of results returned for the query |
search_type | string | Optional | Search surface (e.g. archive, site, topic) |
Article Engagement Events
article_viewed
Fired when a user lands on an article or content detail page. Send this as soon as the page renders, regardless of scroll depth. Also accepted as content_viewed and view_content.
datafly.track('article_viewed', {
content_id: 'art-20260311-001',
content_type: 'article',
content_name: 'The Future of Server-Side Tracking',
content_category: 'Technology',
author: 'Jane Holloway',
content_tags: ['tracking', 'privacy', 'adtech'],
published_at: '2026-03-11T09:00:00Z',
word_count: 1840,
paywall: false
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the content |
content_type | string | Optional | Type of content (e.g. article, video, gallery, podcast) |
content_name | string | Optional | Content headline or title |
content_category | string | Optional | Primary section or category (e.g. Technology, Sport) |
author | string | Optional | Author name or byline |
content_tags | string[] | Optional | Array of topic tags associated with the content |
published_at | string | Optional | ISO 8601 publication date (e.g. 2026-03-11T09:00:00Z) |
word_count | number | Optional | Total word count of the article body |
paywall | boolean | Optional | Whether the content is behind a paywall or content gate |
article_read
Fired once when a user has consumed at least 80% of the article (scrolled to the read threshold, or played a video/podcast to near-completion). Fire a maximum of once per content item per session.
datafly.track('article_read', {
content_id: 'art-20260311-001',
content_type: 'article',
content_name: 'The Future of Server-Side Tracking',
read_time_seconds: 312,
scroll_depth: 87
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the content |
content_type | string | Optional | Type of content read |
content_name | string | Optional | Content headline or title |
read_time_seconds | number | Optional | Time in seconds from article_viewed to reaching the read threshold |
scroll_depth | number | Optional | Maximum scroll depth reached, expressed as a percentage (0–100) |
Use article_read (not article_viewed) as the signal for engaged reads when optimising campaigns. A user who scrolls to 80% is a meaningfully different signal from a page bounce.
article_shared
Fired when a user shares an article via a share button or copy-link action.
datafly.track('article_shared', {
content_id: 'art-20260311-001',
content_type: 'article',
share_method: 'twitter'
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the content being shared |
content_type | string | Optional | Type of content being shared |
share_method | string | Optional | Share channel. One of: twitter, facebook, whatsapp, email, copy_link |
article_bookmarked
Fired when a user bookmarks an article or saves it to their reading list or “save for later” collection.
datafly.track('article_bookmarked', {
content_id: 'art-20260311-001',
content_type: 'article',
content_name: 'The Future of Server-Side Tracking',
collection: 'read-later'
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the bookmarked content |
content_type | string | Optional | Type of content being bookmarked |
content_name | string | Optional | Content headline or title |
collection | string | Optional | Name of the list or collection the content was saved to |
content_shared
Generic share event for non-article content (videos, galleries, podcasts, interactives) shared via a share button or copy-link action.
datafly.track('content_shared', {
content_id: 'vid-20260311-009',
content_type: 'video',
share_method: 'whatsapp'
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the content being shared |
content_type | string | Optional | Type of content being shared |
share_method | string | Optional | Share channel. One of: twitter, facebook, whatsapp, email, copy_link |
content_liked
Fired when a user likes, reacts to, or upvotes a piece of content.
datafly.track('content_liked', {
content_id: 'art-20260311-001',
content_type: 'article',
reaction: 'like'
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the content being liked |
content_type | string | Optional | Type of content being liked |
reaction | string | Optional | Reaction type if multiple are offered (e.g. like, love, insightful) |
content_saved
Fired when a user saves non-article content to their bookmarks, reading list, or “save for later” collection.
datafly.track('content_saved', {
content_id: 'pod-ep-142',
content_type: 'podcast',
content_name: 'The Privacy Briefing — Episode 142',
collection: 'listen-later'
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the saved content |
content_type | string | Optional | Type of content being saved |
content_name | string | Optional | Content headline or title |
collection | string | Optional | Name of the list or collection the content was saved to |
content_downloaded
Fired when a user downloads a piece of content — a PDF article, whitepaper, report, or offline copy.
datafly.track('content_downloaded', {
content_id: 'report-2026-adtech',
content_type: 'report',
content_name: 'State of AdTech 2026',
file_format: 'pdf'
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the downloaded content |
content_type | string | Optional | Type of content being downloaded (e.g. report, whitepaper, article) |
content_name | string | Optional | Content title |
file_format | string | Optional | File format of the download (e.g. pdf, epub, mp3) |
comment_posted
Fired when a user successfully submits a comment on an article.
datafly.track('comment_posted', {
content_id: 'art-20260311-001',
comment_id: 'cmt-77241',
reply_to: 'cmt-77190'
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Content the comment was posted on |
comment_id | string | Optional | Unique identifier for the new comment |
reply_to | string | Optional | comment_id of the parent comment, if this is a reply |
author_followed
Fired when a user follows or subscribes to updates from an author.
datafly.track('author_followed', {
author_id: 'author-jane-holloway',
author_name: 'Jane Holloway'
})| Property | Type | Required | Description |
|---|---|---|---|
author_id | string | Required | Unique identifier for the author |
author_name | string | Optional | Display name of the author |
topic_followed
Fired when a user follows a topic, tag, or section to personalise their feed.
datafly.track('topic_followed', {
topic_id: 'topic-climate',
topic_name: 'Climate',
source: 'topic_hub'
})| Property | Type | Required | Description |
|---|---|---|---|
topic_id | string | Required | Unique identifier for the topic or tag |
topic_name | string | Optional | Display name of the topic |
source | string | Optional | Where the follow was triggered (e.g. topic_hub, article, onboarding) |
Video & Audio Events
video_start
Fired when an embedded video begins playback.
datafly.track('video_start', {
content_id: 'vid-20260311-009',
content_name: 'Explainer: How Server-Side Tagging Works',
content_category: 'Technology',
video_duration: 184,
video_provider: 'jwplayer'
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the video |
content_name | string | Optional | Video title |
content_category | string | Optional | Section or category of the video |
video_duration | number | Optional | Total duration of the video in seconds |
video_provider | string | Optional | Player or hosting provider (e.g. jwplayer, youtube, brightcove) |
video_progress
Fired when playback crosses a progress milestone (typically 25%, 50%, 75%).
datafly.track('video_progress', {
content_id: 'vid-20260311-009',
content_name: 'Explainer: How Server-Side Tagging Works',
percent_complete: 50,
video_duration: 184
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the video |
content_name | string | Optional | Video title |
percent_complete | number | Optional | Progress milestone reached (e.g. 25, 50, 75) |
video_duration | number | Optional | Total duration of the video in seconds |
video_complete
Fired when a user finishes watching a video (reaches the end).
datafly.track('video_complete', {
content_id: 'vid-20260311-009',
content_name: 'Explainer: How Server-Side Tagging Works',
video_duration: 184
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the video |
content_name | string | Optional | Video title |
video_duration | number | Optional | Total duration of the video in seconds |
audio_start
Fired when a podcast episode or audio article begins playback.
datafly.track('audio_start', {
content_id: 'pod-ep-142',
content_name: 'The Privacy Briefing — Episode 142',
content_category: 'Podcasts',
audio_duration: 2640
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Unique identifier for the audio content |
content_name | string | Optional | Episode or audio title |
content_category | string | Optional | Section, show, or category |
audio_duration | number | Optional | Total duration of the audio in seconds |
Account Events
sign_up
Fired when a user completes account registration (free registered reader).
datafly.track('sign_up', {
method: 'email'
})| Property | Type | Required | Description |
|---|---|---|---|
method | string | Optional | Registration method (e.g. email, google, apple) |
login
Fired when a user signs in to their account.
datafly.track('login', {
method: 'email'
})| Property | Type | Required | Description |
|---|---|---|---|
method | string | Optional | Login method (e.g. email, google, apple) |
generate_lead
Fired when a registered or anonymous reader submits interest in a product, demo, or gated download — for example completing a lead-gen form on a B2B publication. Never pass raw email or phone here; the contact PII is hashed server-side via Signal’s identity blueprint.
datafly.track('generate_lead', {
lead_type: 'gated_report',
content_id: 'report-2026-adtech',
value: 0,
currency: 'GBP'
})| Property | Type | Required | Description |
|---|---|---|---|
lead_type | string | Optional | The kind of lead captured (e.g. gated_report, demo_request, event_signup) |
content_id | string | Optional | Content or asset that generated the lead |
value | number | Optional | Estimated monetary value of the lead |
currency | string | Optional | ISO 4217 currency code |
Newsletter Events
newsletter_subscribed
Fired when a user successfully subscribes to a newsletter or mailing list.
datafly.track('newsletter_subscribed', {
list_id: 'daily-briefing',
email_hash: 'b4c9a...f1e2',
source: 'article'
})| Property | Type | Required | Description |
|---|---|---|---|
list_id | string | Optional | Identifier of the newsletter or list subscribed to |
email_hash | string | Optional | SHA-256 hash of the subscriber’s email address. Never send raw email addresses in event properties. |
source | string | Optional | Where the subscription was triggered. One of: inline, popup, footer, article |
Never pass a raw email address in event properties. Always hash with SHA-256 (lowercase, trimmed) before sending. This hashed value can be used by Meta CAPI and other platforms for identity matching.
newsletter_unsubscribed
Fired when a user unsubscribes from a newsletter or mailing list.
datafly.track('newsletter_unsubscribed', {
list_id: 'daily-briefing',
reason: 'too_frequent'
})| Property | Type | Required | Description |
|---|---|---|---|
list_id | string | Optional | Identifier of the newsletter or list unsubscribed from |
reason | string | Optional | Reason provided by the user for unsubscribing |
Paywall & Subscription Events
paywall_viewed
Fired when a user encounters a content gate or paywall prompt, whether inline (content cut-off) or as a modal overlay.
datafly.track('paywall_viewed', {
content_id: 'art-20260311-044',
subscription_type: 'digital',
position: 'modal'
})| Property | Type | Required | Description |
|---|---|---|---|
content_id | string | Required | Content that triggered the paywall |
subscription_type | string | Optional | Subscription tier or product required to access the content |
position | string | Optional | How the gate was presented. One of: inline, modal |
subscription_started
Fired when a user successfully starts a subscription (paid or trial). For Signal’s canonical revenue mapping this aligns with the GA4 purchase conversion.
datafly.track('subscription_started', {
transaction_id: 'SUB-10042',
plan_id: 'digital-monthly',
plan_name: 'Digital Monthly',
value: 9.99,
currency: 'GBP',
trial: false
})| Property | Type | Required | Description |
|---|---|---|---|
transaction_id | string | Required | Unique subscription order identifier — used for deduplication |
plan_id | string | Optional | Machine-readable plan identifier |
plan_name | string | Optional | Human-readable plan name |
value | number | Optional | Subscription price charged at start |
currency | string | Optional | ISO 4217 currency code |
trial | boolean | Optional | Whether this is the start of a free trial |
Always send transaction_id on subscription_started events for deduplication. Platforms like Meta CAPI and GA4 use it to prevent double-counting alongside browser-side events.
subscription_renewed
Fired when an existing subscription renews and the reader is charged again.
datafly.track('subscription_renewed', {
transaction_id: 'SUB-10042-R7',
plan_id: 'digital-monthly',
value: 9.99,
currency: 'GBP'
})| Property | Type | Required | Description |
|---|---|---|---|
transaction_id | string | Required | Unique identifier for the renewal charge — used for deduplication |
plan_id | string | Optional | Identifier of the renewing plan |
value | number | Optional | Amount charged on renewal |
currency | string | Optional | ISO 4217 currency code |
subscription_cancelled
Fired when a user cancels their subscription.
datafly.track('subscription_cancelled', {
plan_id: 'digital-monthly',
reason: 'too_expensive'
})| Property | Type | Required | Description |
|---|---|---|---|
plan_id | string | Optional | Identifier of the cancelled plan |
reason | string | Optional | Cancellation reason provided by the user |
Content Items Schema
Several discovery events accept an items array. Each object can include the following properties. The items[] array uses GA4-style field names. Signal’s server-side blueprints automatically transform these to vendor-specific formats (e.g., Meta’s contents[] with id/title fields).
| Property | Type | Description |
|---|---|---|
item_id | string | Your internal content ID |
item_name | string | Content headline or title |
content_category | string | Section or category of the content |
content_type | string | Type of content (e.g. article, video, gallery, podcast) |
author | string | Author name or byline |
index | number | Position in list (0-indexed, relevant in list/feed contexts) |
item_list_id | string | Identifier of the list the item was shown in |
item_list_name | string | Name of the list the item was shown in |
Vendor Mapping
The table below shows how Datafly Signal maps publishing events to the native event names expected by each vendor’s server-side API. Signal uses the GA4-style event names as its canonical format, and blueprints handle the transformation to vendor-specific payloads.
| Signal Event | GA4 | Meta CAPI | TikTok Events API |
|---|---|---|---|
view_item_list | view_item_list | ViewContent | ViewContent |
select_content | select_content | ViewContent | ClickButton |
category_viewed | view_item_list | ViewContent | ViewContent |
view_promotion | view_promotion | (custom) | (custom) |
select_promotion | select_promotion | (custom) | (custom) |
search | search | Search | Search |
search_performed | search | Search | Search |
article_viewed | view_item | ViewContent | ViewContent |
article_read | select_content | ViewContent | ViewContent |
article_shared | share | (custom) | (custom) |
article_bookmarked | add_to_wishlist | AddToWishlist | AddToWishlist |
content_shared | share | (custom) | (custom) |
content_liked | select_content | (custom) | (custom) |
content_saved | add_to_wishlist | AddToWishlist | AddToWishlist |
content_downloaded | file_download | (custom) | Download |
comment_posted | select_content | (custom) | (custom) |
author_followed | join_group | Subscribe | Subscribe |
topic_followed | join_group | Subscribe | Subscribe |
video_start | video_start | (custom) | (custom) |
video_progress | video_progress | (custom) | (custom) |
video_complete | video_complete | (custom) | (custom) |
audio_start | select_content | (custom) | (custom) |
sign_up | sign_up | CompleteRegistration | CompleteRegistration |
login | login | (custom) | (custom) |
generate_lead | generate_lead | Lead | SubmitForm |
newsletter_subscribed | sign_up | Subscribe | Subscribe |
newsletter_unsubscribed | (custom) | (custom) | (custom) |
paywall_viewed | view_promotion | ViewContent | ViewContent |
subscription_started | purchase | Subscribe | Subscribe |
subscription_renewed | purchase | Subscribe | Subscribe |
subscription_cancelled | refund | (custom) | (custom) |