← Back to KPIs

Content Completion Rate

Also known as: Completion Rate, Video Completion Rate, Viewing Completion Rate, Playback Completion Rate, Content Finish Rate, Watch-Through Completion Rate

EngagementkpiMedia & Streaming

Measures the percentage of qualified playback attempts that reach the defined completion threshold for eligible on-demand content. It indicates how consistently viewers consume content through its intended end after starting playback.

Business Use Case

The Product organization reviews completion rate by title, episode, content length, device, and playback quality. If a series shows strong starts but completion falls below 55% on connected TVs, the team compares abandonment position, playback errors, and rebuffering by device model to determine whether to fix the playback experience, adjust episode structure, or reduce future promotion of the affected title.

Business Criticality

High

Journey / Navigation Area

Engagement

Owning Function

Product

Scope

Item

Formula

(Number of Qualified Playback Attempts Completed / Total Number of Qualified Playback Attempts Started) × 100

Calculation Notes

1️⃣ Count each playback_attempt_id once in the denominator. 2️⃣ Count each playback_attempt_id no more than once in the numerator. 3️⃣ Completion recognized by playback_completed event or playhead position ≥ 0.90. 4️⃣ Seeked/scrubbed intervals do not count as consumed time; must be followed by 2 seconds of normal playback for completion. 5️⃣ Pause, buffering, quality changes, and backgrounding do not create new attempts. 6️⃣ Close attempts after 30 minutes of inactivity; accept completions within 24 hours for the same playback_attempt_id. 7️⃣ Use content duration from the content master; recalculate periods when metadata is corrected.

Technical Details

Measure Typepercentage
Aggregation WindowTime based - Daily, with weekly and monthly rollups

Events

PlatformEvent
Google Analytics 4video_complete
Adobemedia.complete

Data Mappings

W3 Data Layer
{
  "digitalData": {
    "event": [
      {
        "eventInfo": {
          "eventId": "evt-550e8400-e29b-41d4-a716-446655440000",
          "eventName": "video_complete",
          "eventAction": "complete",
          "eventTimestamp": "2025-03-08T14:30:00.000Z"
        },
        "attributes": {
          "muted": false,
          "userId": "user-12345",
          "country": "US",
          "autoplay": false,
          "platform": "web",
          "videoUrl": "https://stream.example.com/content/content-10001",
          "contentId": "content-10001",
          "pageTitle": "Example Episode | Example",
          "sessionId": "session-67890",
          "appVersion": "web-2025.03.08",
          "deviceType": "desktop",
          "isEligible": true,
          "playerName": "web-player",
          "streamType": "vod",
          "contentName": "Example Episode",
          "contentType": "episode",
          "isCompleted": true,
          "contentGenre": "documentary",
          "deliveryType": "on_demand",
          "pageLocation": "https://www.example.com/watch/content-10001",
          "pageReferrer": "https://www.example.com/series/example-series",
          "seasonNumber": 2,
          "contentRating": "TV-14",
          "episodeNumber": 5,
          "playerVersion": "5.4.1",
          "contentCategory": "entertainment",
          "contentLanguage": "en-US",
          "contentProvider": "Example Studio",
          "contentSeriesId": "series-20001",
          "operatingSystem": "Windows 11",
          "playbackPercent": 95,
          "completionReason": "threshold_reached",
          "consentAnalytics": "granted",
          "subscriptionTier": "premium",
          "contentSeriesName": "Example Series",
          "eligibilityReason": "eligible_on_demand_content",
          "entitlementStatus": "entitled",
          "playbackAttemptId": "attempt-def456",
          "playbackSessionId": "playback-session-abc123",
          "contentReleaseDate": "2025-02-15",
          "isQualifiedAttempt": true,
          "qualificationReason": "minimum_playback_requirement_met",
          "contentDurationSeconds": 1800,
          "playbackPositionSeconds": 1710,
          "completionThresholdPercent": 95,
          "completionThresholdSeconds": 1710
        }
      }
    ]
  }
}
GA4 Data Layer
{
  "event": "video_complete",
  "muted": false,
  "country": "US",
  "user_id": "user-12345",
  "autoplay": false,
  "event_id": "evt-550e8400-e29b-41d4-a716-446655440000",
  "platform": "web",
  "video_url": "https://stream.example.com/content/content-10001",
  "content_id": "content-10001",
  "page_title": "Example Episode | Example",
  "session_id": "session-67890",
  "app_version": "web-2025.03.08",
  "device_type": "desktop",
  "is_eligible": true,
  "player_name": "web-player",
  "stream_type": "vod",
  "video_title": "Example Episode",
  "content_type": "episode",
  "is_completed": true,
  "content_genre": "documentary",
  "delivery_type": "on_demand",
  "page_location": "https://www.example.com/watch/content-10001",
  "page_referrer": "https://www.example.com/series/example-series",
  "season_number": 2,
  "video_percent": 95,
  "content_rating": "TV-14",
  "episode_number": 5,
  "player_version": "5.4.1",
  "video_duration": 1800,
  "video_provider": "Example Studio",
  "event_timestamp": "2025-03-08T14:30:00.000Z",
  "content_category": "entertainment",
  "content_language": "en-US",
  "operating_system": "Windows 11",
  "completion_reason": "threshold_reached",
  "consent_analytics": "granted",
  "content_series_id": "series-20001",
  "subscription_tier": "premium",
  "eligibility_reason": "eligible_on_demand_content",
  "entitlement_status": "entitled",
  "video_current_time": 1710,
  "content_series_name": "Example Series",
  "playback_attempt_id": "attempt-def456",
  "playback_session_id": "playback-session-abc123",
  "content_release_date": "2025-02-15",
  "is_qualified_attempt": true,
  "qualification_reason": "minimum_playback_requirement_met",
  "completion_threshold_percent": 95,
  "completion_threshold_seconds": 1710
}
Adobe Client Data Layer
{
  "data": {
    "muted": false,
    "userId": "user-12345",
    "country": "US",
    "eventId": "evt-550e8400-e29b-41d4-a716-446655440000",
    "autoplay": false,
    "platform": "web",
    "videoUrl": "https://stream.example.com/content/content-10001",
    "contentId": "content-10001",
    "pageTitle": "Example Episode | Example",
    "sessionId": "session-67890",
    "appVersion": "web-2025.03.08",
    "deviceType": "desktop",
    "isEligible": true,
    "playerName": "web-player",
    "streamType": "vod",
    "contentName": "Example Episode",
    "contentType": "episode",
    "isCompleted": true,
    "contentGenre": "documentary",
    "deliveryType": "on_demand",
    "pageLocation": "https://www.example.com/watch/content-10001",
    "pageReferrer": "https://www.example.com/series/example-series",
    "seasonNumber": 2,
    "contentRating": "TV-14",
    "episodeNumber": 5,
    "playerVersion": "5.4.1",
    "eventTimestamp": "2025-03-08T14:30:00.000Z",
    "contentCategory": "entertainment",
    "contentLanguage": "en-US",
    "contentProvider": "Example Studio",
    "contentSeriesId": "series-20001",
    "operatingSystem": "Windows 11",
    "playbackPercent": 95,
    "completionReason": "threshold_reached",
    "consentAnalytics": "granted",
    "subscriptionTier": "premium",
    "contentSeriesName": "Example Series",
    "eligibilityReason": "eligible_on_demand_content",
    "entitlementStatus": "entitled",
    "playbackAttemptId": "attempt-def456",
    "playbackSessionId": "playback-session-abc123",
    "contentReleaseDate": "2025-02-15",
    "isQualifiedAttempt": true,
    "qualificationReason": "minimum_playback_requirement_met",
    "contentDurationSeconds": 1800,
    "playbackPositionSeconds": 1710,
    "completionThresholdPercent": 95,
    "completionThresholdSeconds": 1710
  },
  "event": "media.complete"
}
XDM Mapping
{
  "web": {
    "webReferrer": {
      "URL": "https://www.example.com/series/example-series"
    },
    "webPageDetails": {
      "URL": "https://www.example.com/watch/content-10001",
      "name": "Example Episode | Example"
    }
  },
  "device": {
    "type": "desktop"
  },
  "consents": {
    "collect": {
      "val": "y"
    }
  },
  "eventType": "media.sessionComplete",
  "timestamp": "2025-03-08T14:30:00.000Z",
  "environment": {
    "operatingSystem": "Windows 11"
  },
  "identityMap": {
    "CRMID": [
      {
        "id": "user-12345",
        "primary": true,
        "authenticatedState": "authenticated"
      }
    ]
  },
  "eventMergeId": "evt-550e8400-e29b-41d4-a716-446655440000",
  "placeContext": {
    "geo": {
      "countryCode": "US"
    }
  },
  "mediaCollection": {
    "playhead": 1710,
    "sessionDetails": {
      "ID": "content-10001",
      "name": "content-10001",
      "show": "Example Series",
      "genre": "documentary",
      "length": 1800,
      "rating": "TV-14",
      "season": "2",
      "channel": "Example Studio",
      "episode": "5",
      "authorized": true,
      "originator": "Example Studio",
      "playerName": "web-player",
      "streamType": "vod",
      "contentType": "episode",
      "friendlyName": "Example Episode"
    }
  },
  "_contentCompletion": {
    "muted": false,
    "autoplay": false,
    "platform": "web",
    "videoUrl": "https://stream.example.com/content/content-10001",
    "sessionId": "session-67890",
    "appVersion": "web-2025.03.08",
    "isEligible": true,
    "isCompleted": true,
    "deliveryType": "on_demand",
    "playerVersion": "5.4.1",
    "contentCategory": "entertainment",
    "contentLanguage": "en-US",
    "contentSeriesId": "series-20001",
    "playbackPercent": 95,
    "completionReason": "threshold_reached",
    "subscriptionTier": "premium",
    "eligibilityReason": "eligible_on_demand_content",
    "entitlementStatus": "entitled",
    "playbackAttemptId": "attempt-def456",
    "playbackSessionId": "playback-session-abc123",
    "contentReleaseDate": "2025-02-15",
    "isQualifiedAttempt": true,
    "qualificationReason": "minimum_playback_requirement_met",
    "completionThresholdPercent": 95,
    "completionThresholdSeconds": 1710
  }
}

SQL Queries

Google Analytics 4 (BigQuery)
-- ==========================================
-- 1. GOOGLE BIGQUERY SQL
-- ==========================================
WITH event_parameter_pivot AS (
  SELECT
    PARSE_DATE('%Y%m%d', e.event_date) AS event_date,
    TIMESTAMP_MICROS(e.event_timestamp) AS event_timestamp,
    e.event_name,
    e.user_pseudo_id,
    e.platform AS collected_platform,
    e.device.category AS collected_device_type,
    e.device.operating_system AS collected_operating_system,
    e.geo.country AS collected_country,
    MAX(IF(ep.key = 'event_id', COALESCE(ep.value.string_value, CAST(ep.value.int_value AS STRING)), NULL)) AS event_id,
    MAX(IF(ep.key = 'user_id', COALESCE(ep.value.string_value, CAST(ep.value.int_value AS STRING)), NULL)) AS event_user_id,
    MAX(IF(ep.key = 'session_id', COALESCE(ep.value.string_value, CAST(ep.value.int_value AS STRING)), NULL)) AS session_id,
    MAX(IF(ep.key = 'playback_session_id', ep.value.string_value, NULL)) AS playback_session_id,
    MAX(IF(ep.key = 'playback_attempt_id', ep.value.string_value, NULL)) AS playback_attempt_id,
    MAX(IF(ep.key = 'content_id', ep.value.string_value, NULL)) AS content_id,
    MAX(IF(ep.key = 'video_title', ep.value.string_value, NULL)) AS content_name,
    MAX(IF(ep.key = 'content_type', ep.value.string_value, NULL)) AS content_type,
    MAX(IF(ep.key = 'content_genre', ep.value.string_value, NULL)) AS content_genre,
    MAX(IF(ep.key = 'content_series_id', ep.value.string_value, NULL)) AS content_series_id,
    MAX(IF(ep.key = 'content_series_name', ep.value.string_value, NULL)) AS content_series_name,
    MAX(IF(ep.key = 'season_number', COALESCE(ep.value.string_value, CAST(ep.value.int_value AS STRING)), NULL)) AS season_number,
    MAX(IF(ep.key = 'episode_number', COALESCE(ep.value.string_value, CAST(ep.value.int_value AS STRING)), NULL)) AS episode_number,
    MAX(IF(ep.key = 'video_duration', COALESCE(ep.value.int_value, CAST(ep.value.double_value AS INT64), CAST(ep.value.float_value AS INT64)), NULL)) AS content_duration_seconds,
    MAX(IF(ep.key = 'video_provider', ep.value.string_value, NULL)) AS content_provider,
    MAX(IF(ep.key = 'content_language', ep.value.string_value, NULL)) AS content_language,
    MAX(IF(ep.key = 'content_rating', ep.value.string_value, NULL)) AS content_rating,
    MAX(IF(ep.key = 'content_release_date', ep.value.string_value, NULL)) AS content_release_date,
    MAX(IF(ep.key = 'content_category', ep.value.string_value, NULL)) AS content_category,
    MAX(IF(ep.key = 'delivery_type', ep.value.string_value, NULL)) AS delivery_type,
    MAX(IF(ep.key = 'stream_type', ep.value.string_value, NULL)) AS stream_type,
    MAX(IF(ep.key = 'player_name', ep.value.string_value, NULL)) AS player_name,
    MAX(IF(ep.key = 'player_version', ep.value.string_value, NULL)) AS player_version,
    MAX(IF(ep.key = 'video_current_time', COALESCE(ep.value.double_value, ep.value.float_value, CAST(ep.value.int_value AS FLOAT64)), NULL)) AS playback_position_seconds,
    MAX(IF(ep.key = 'video_percent', COALESCE(ep.value.double_value, ep.value.float_value, CAST(ep.value.int_value AS FLOAT64)), NULL)) AS playback_percent,
    MAX(IF(ep.key = 'completion_threshold_percent', COALESCE(ep.value.double_value, ep.value.float_value, CAST(ep.value.int_value AS FLOAT64)), NULL)) AS completion_threshold_percent,
    MAX(IF(ep.key = 'completion_threshold_seconds', COALESCE(ep.value.double_value, ep.value.float_value, CAST(ep.value.int_value AS FLOAT64)), NULL)) AS completion_threshold_seconds,
    MAX(IF(ep.key = 'completion_reason', ep.value.string_value, NULL)) AS completion_reason,
    MAX(IF(ep.key = 'is_completed', LOWER(COALESCE(ep.value.string_value, CAST(ep.value.int_value AS STRING))), NULL)) AS is_completed,
    MAX(IF(ep.key = 'is_eligible', LOWER(COALESCE(ep.value.string_value, CAST(ep.value.int_value AS STRING))), NULL)) AS is_eligible,
    MAX(IF(ep.key = 'eligibility_reason', ep.value.string_value, NULL)) AS eligibility_reason,
    MAX(IF(ep.key = 'is_qualified_attempt', LOWER(COALESCE(ep.value.string_value, CAST(ep.value.int_value AS STRING))), NULL)) AS is_qualified_attempt,
    MAX(IF(ep.key = 'qualification_reason', ep.value.string_value, NULL)) AS qualification_reason,
    MAX(IF(ep.key = 'autoplay', LOWER(COALESCE(ep.value.string_value, CAST(ep.value.int_value AS STRING))), NULL)) AS autoplay,
    MAX(IF(ep.key = 'muted', LOWER(COALESCE(ep.value.string_value, CAST(ep.value.int_value AS STRING))), NULL)) AS muted,
    MAX(IF(ep.key = 'entitlement_status', ep.value.string_value, NULL)) AS entitlement_status,
    MAX(IF(ep.key = 'subscription_tier', ep.value.string_value, NULL)) AS subscription_tier,
    MAX(IF(ep.key = 'platform', ep.value.string_value, NULL)) AS event_platform,
    MAX(IF(ep.key = 'device_type', ep.value.string_value, NULL)) AS event_device_type,
    MAX(IF(ep.key = 'operating_system', ep.value.string_value, NULL)) AS event_operating_system,
    MAX(IF(ep.key = 'app_version', ep.value.string_value, NULL)) AS app_version,
    MAX(IF(ep.key = 'video_url', ep.value.string_value, NULL)) AS video_url,
    MAX(IF(ep.key = 'page_location', ep.value.string_value, NULL)) AS page_location,
    MAX(IF(ep.key = 'page_title', ep.value.string_value, NULL)) AS page_title,
    MAX(IF(ep.key = 'page_referrer', ep.value.string_value, NULL)) AS page_referrer,
    MAX(IF(ep.key = 'country', ep.value.string_value, NULL)) AS event_country,
    MAX(IF(ep.key = 'consent_analytics', ep.value.string_value, NULL)) AS consent_analytics
  FROM `your-project.analytics_123456789.events_*` AS e
  LEFT JOIN UNNEST(e.event_params) AS ep
  WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE('UTC'), INTERVAL 30 DAY))
                          AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE('UTC'), INTERVAL 1 DAY))
    AND e.event_name IN ('video_start', 'video_progress', 'video_complete')
  GROUP BY
    e.event_date,
    e.event_timestamp,
    e.event_name,
    e.user_pseudo_id,
    e.platform,
    e.device.category,
    e.device.operating_system,
    e.geo.country
),
deduplicated_events AS (
  SELECT * EXCEPT(event_deduplication_rank)
  FROM (
    SELECT
      p.*,
      ROW_NUMBER() OVER (
        PARTITION BY COALESCE(
          p.event_id,
          CONCAT(
            CAST(p.event_timestamp AS STRING), '|',
            p.event_name, '|',
            COALESCE(p.playback_attempt_id, ''), '|',
            COALESCE(p.content_id, '')
          )
        )
        ORDER BY p.event_timestamp
      ) AS event_deduplication_rank
    FROM event_parameter_pivot AS p
  )
  WHERE event_deduplication_rank = 1
),
normalized_events AS (
  SELECT
    *,
    COALESCE(
      NULLIF(playback_attempt_id, ''),
      NULLIF(CONCAT(COALESCE(playback_session_id, ''), '|', COALESCE(content_id, '')), '|')
    ) AS attempt_key,
    COALESCE(NULLIF(event_user_id, ''), user_pseudo_id) AS user_id,
    COALESCE(NULLIF(event_platform, ''), collected_platform) AS platform,
    COALESCE(NULLIF(event_device_type, ''), collected_device_type) AS device_type,
    COALESCE(NULLIF(event_operating_system, ''), collected_operating_system) AS operating_system,
    COALESCE(NULLIF(event_country, ''), collected_country) AS country,
    IF(is_eligible IN ('true', '1', 'yes'), 1, 0) AS eligible_event_flag,
    IF(is_qualified_attempt IN ('true', '1', 'yes'), 1, 0) AS qualified_event_flag,
    IF(is_completed IN ('true', '1', 'yes'), 1, 0) AS completed_event_flag,
    IF(autoplay IN ('true', '1', 'yes'), TRUE, FALSE) AS autoplay_flag,
    IF(muted IN ('true', '1', 'yes'), TRUE, FALSE) AS muted_flag
  FROM deduplicated_events
),
attempt_facts AS (
  SELECT
    attempt_key,
    MIN(event_date) AS activity_date,
    MAX(user_id) AS user_id,
    MAX(session_id) AS session_id,
    MAX(playback_session_id) AS playback_session_id,
    MAX(playback_attempt_id) AS playback_attempt_id,
    MAX(content_id) AS content_id,
    MAX(content_name) AS content_name,
    MAX(content_type) AS content_type,
    MAX(content_genre) AS content_genre,
    MAX(content_series_id) AS content_series_id,
    MAX(content_series_name) AS content_series_name,
    MAX(SAFE_CAST(season_number AS INT64)) AS season_number,
    MAX(SAFE_CAST(episode_number AS INT64)) AS episode_number,
    MAX(content_duration_seconds) AS content_duration_seconds,
    MAX(content_provider) AS content_provider,
    MAX(content_language) AS content_language,
    MAX(content_rating) AS content_rating,
    MAX(SAFE_CAST(content_release_date AS DATE)) AS content_release_date,
    MAX(content_category) AS content_category,
    MAX(delivery_type) AS delivery_type,
    MAX(stream_type) AS stream_type,
    MAX(player_name) AS player_name,
    MAX(player_version) AS player_version,
    MAX(playback_position_seconds) AS maximum_playback_position_seconds,
    MAX(playback_percent) AS maximum_playback_percent,
    MAX(completion_threshold_percent) AS completion_threshold_percent,
    MAX(completion_threshold_seconds) AS completion_threshold_seconds,
    MAX(completion_reason) AS completion_reason,
    MAX(eligibility_reason) AS eligibility_reason,
    MAX(qualification_reason) AS qualification_reason,
    LOGICAL_OR(autoplay_flag) AS autoplay,
    LOGICAL_OR(muted_flag) AS muted,
    MAX(entitlement_status) AS entitlement_status,
    MAX(subscription_tier) AS subscription_tier,
    MAX(platform) AS platform,
    MAX(device_type) AS device_type,
    MAX(operating_system) AS operating_system,
    MAX(app_version) AS app_version,
    MAX(video_url) AS video_url,
    MAX(page_location) AS page_location,
    MAX(page_title) AS page_title,
    MAX(page_referrer) AS page_referrer,
    MAX(country) AS country,
    MAX(consent_analytics) AS consent_analytics,
    MAX(eligible_event_flag) AS eligible_attempt_flag,
    MAX(qualified_event_flag) AS qualified_attempt_flag,
    MAX(IF(event_name = 'video_start', 1, 0)) AS started_attempt_flag,
    MAX(
      IF(
        event_name = 'video_complete'
        OR completed_event_flag = 1
        OR (
          playback_percent IS NOT NULL
          AND playback_percent >= COALESCE(completion_threshold_percent, 95.0)
        )
        OR (
          playback_position_seconds IS NOT NULL
          AND playback_position_seconds >= completion_threshold_seconds
        ),
        1,
        0
      )
    ) AS completed_attempt_flag
  FROM normalized_events
  WHERE attempt_key IS NOT NULL
  GROUP BY attempt_key
),
qualified_eligible_attempts AS (
  SELECT *
  FROM attempt_facts
  WHERE started_attempt_flag = 1
    AND eligible_attempt_flag = 1
    AND qualified_attempt_flag = 1
    AND LOWER(COALESCE(consent_analytics, 'granted')) IN ('granted', 'y', 'yes', 'true', '1')
)
SELECT
  activity_date,
  content_id,
  content_name,
  content_type,
  content_genre,
  content_series_id,
  content_series_name,
  season_number,
  episode_number,
  content_duration_seconds,
  content_provider,
  content_language,
  content_rating,
  content_release_date,
  content_category,
  delivery_type,
  stream_type,
  player_name,
  player_version,
  eligibility_reason,
  qualification_reason,
  completion_threshold_percent,
  completion_threshold_seconds,
  autoplay,
  muted,
  entitlement_status,
  subscription_tier,
  platform,
  device_type,
  operating_system,
  app_version,
  video_url,
  page_location,
  page_title,
  page_referrer,
  country,
  consent_analytics,
  COUNT(*) AS qualified_eligible_attempts,
  COUNTIF(completed_attempt_flag = 1) AS completed_attempts,
  ROUND(
    100 * SAFE_DIVIDE(COUNTIF(completed_attempt_flag = 1), COUNT(*)),
    4
  ) AS content_completion_rate_percent
FROM qualified_eligible_attempts
GROUP BY
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
  11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
  21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
  31, 32, 33, 34, 35, 36, 37
ORDER BY activity_date DESC, content_id;
Adobe Experience Platform (AEP Query Service)
-- ==========================================
-- 2. ADOBE AEP QUERY SERVICE SQL
-- ==========================================
WITH source_events AS (
  SELECT
    eventMergeId AS event_id,
    "timestamp" AS event_timestamp,
    eventType AS event_type,
    identityMap['CRMID'][0].id AS user_id,
    _contentCompletion.sessionId AS session_id,
    _contentCompletion.playbackSessionId AS playback_session_id,
    _contentCompletion.playbackAttemptId AS playback_attempt_id,
    mediaCollection.sessionDetails."ID" AS content_id,
    mediaCollection.sessionDetails.friendlyName AS content_name,
    mediaCollection.sessionDetails.contentType AS content_type,
    mediaCollection.sessionDetails.genre AS content_genre,
    _contentCompletion.contentSeriesId AS content_series_id,
    mediaCollection.sessionDetails.show AS content_series_name,
    mediaCollection.sessionDetails.season AS season_number,
    mediaCollection.sessionDetails.episode AS episode_number,
    mediaCollection.sessionDetails.length AS content_duration_seconds,
    mediaCollection.sessionDetails.originator AS content_provider,
    _contentCompletion.contentLanguage AS content_language,
    mediaCollection.sessionDetails.rating AS content_rating,
    _contentCompletion.contentReleaseDate AS content_release_date,
    _contentCompletion.contentCategory AS content_category,
    _contentCompletion.deliveryType AS delivery_type,
    mediaCollection.sessionDetails.streamType AS stream_type,
    mediaCollection.sessionDetails.playerName AS player_name,
    _contentCompletion.playerVersion AS player_version,
    mediaCollection.playhead AS playback_position_seconds,
    _contentCompletion.playbackPercent AS playback_percent,
    _contentCompletion.completionThresholdPercent AS completion_threshold_percent,
    _contentCompletion.completionThresholdSeconds AS completion_threshold_seconds,
    _contentCompletion.completionReason AS completion_reason,
    _contentCompletion.isCompleted AS is_completed,
    _contentCompletion.isEligible AS is_eligible,
    _contentCompletion.eligibilityReason AS eligibility_reason,
    _contentCompletion.isQualifiedAttempt AS is_qualified_attempt,
    _contentCompletion.qualificationReason AS qualification_reason,
    _contentCompletion.autoplay AS autoplay,
    _contentCompletion.muted AS muted,
    _contentCompletion.entitlementStatus AS entitlement_status,
    _contentCompletion.subscriptionTier AS subscription_tier,
    _contentCompletion.platform AS platform,
    device.type AS device_type,
    environment.operatingSystem AS operating_system,
    _contentCompletion.appVersion AS app_version,
    _contentCompletion.videoUrl AS video_url,
    web.webPageDetails.URL AS page_location,
    web.webPageDetails.name AS page_title,
    web.webReferrer.URL AS page_referrer,
    placeContext.geo.countryCode AS country,
    consents.collect.val AS consent_analytics
  FROM your_aep_dataset
  WHERE "timestamp" >= CURRENT_DATE - INTERVAL '30 days'
    AND "timestamp" < CURRENT_DATE
    AND eventType IN ('media.sessionStart', 'media.sessionComplete', 'media.playheadUpdate')
),
ranked_events AS (
  SELECT
    s.*,
    ROW_NUMBER() OVER (
      PARTITION BY COALESCE(
        event_id,
        CONCAT(
          CAST(event_timestamp AS VARCHAR), '|',
          COALESCE(event_type, ''), '|',
          COALESCE(playback_attempt_id, ''), '|',
          COALESCE(content_id, '')
        )
      )
      ORDER BY event_timestamp
    ) AS event_deduplication_rank
  FROM source_events s
),
deduplicated_events AS (
  SELECT *
  FROM ranked_events
  WHERE event_deduplication_rank = 1
),
normalized_events AS (
  SELECT
    d.*,
    COALESCE(
      NULLIF(playback_attempt_id, ''),
      NULLIF(CONCAT(COALESCE(playback_session_id, ''), '|', COALESCE(content_id, '')), '|')
    ) AS attempt_key,
    CASE WHEN is_eligible = TRUE THEN 1 ELSE 0 END AS eligible_event_flag,
    CASE WHEN is_qualified_attempt = TRUE THEN 1 ELSE 0 END AS qualified_event_flag,
    CASE WHEN is_completed = TRUE THEN 1 ELSE 0 END AS completed_event_flag,
    CASE WHEN autoplay = TRUE THEN 1 ELSE 0 END AS autoplay_flag,
    CASE WHEN muted = TRUE THEN 1 ELSE 0 END AS muted_flag
  FROM deduplicated_events d
),
attempt_facts AS (
  SELECT
    attempt_key,
    CAST(MIN(event_timestamp) AS DATE) AS activity_date,
    MAX(user_id) AS user_id,
    MAX(session_id) AS session_id,
    MAX(playback_session_id) AS playback_session_id,
    MAX(playback_attempt_id) AS playback_attempt_id,
    MAX(content_id) AS content_id,
    MAX(content_name) AS content_name,
    MAX(content_type) AS content_type,
    MAX(content_genre) AS content_genre,
    MAX(content_series_id) AS content_series_id,
    MAX(content_series_name) AS content_series_name,
    MAX(CAST(season_number AS INTEGER)) AS season_number,
    MAX(CAST(episode_number AS INTEGER)) AS episode_number,
    MAX(content_duration_seconds) AS content_duration_seconds,
    MAX(content_provider) AS content_provider,
    MAX(content_language) AS content_language,
    MAX(content_rating) AS content_rating,
    MAX(CAST(content_release_date AS DATE)) AS content_release_date,
    MAX(content_category) AS content_category,
    MAX(delivery_type) AS delivery_type,
    MAX(stream_type) AS stream_type,
    MAX(player_name) AS player_name,
    MAX(player_version) AS player_version,
    MAX(playback_position_seconds) AS maximum_playback_position_seconds,
    MAX(playback_percent) AS maximum_playback_percent,
    MAX(completion_threshold_percent) AS completion_threshold_percent,
    MAX(completion_threshold_seconds) AS completion_threshold_seconds,
    MAX(completion_reason) AS completion_reason,
    MAX(eligibility_reason) AS eligibility_reason,
    MAX(qualification_reason) AS qualification_reason,
    MAX(autoplay_flag) AS autoplay_flag,
    MAX(muted_flag) AS muted_flag,
    MAX(entitlement_status) AS entitlement_status,
    MAX(subscription_tier) AS subscription_tier,
    MAX(platform) AS platform,
    MAX(device_type) AS device_type,
    MAX(operating_system) AS operating_system,
    MAX(app_version) AS app_version,
    MAX(video_url) AS video_url,
    MAX(page_location) AS page_location,
    MAX(page_title) AS page_title,
    MAX(page_referrer) AS page_referrer,
    MAX(country) AS country,
    MAX(consent_analytics) AS consent_analytics,
    MAX(eligible_event_flag) AS eligible_attempt_flag,
    MAX(qualified_event_flag) AS qualified_attempt_flag,
    MAX(CASE WHEN event_type = 'media.sessionStart' THEN 1 ELSE 0 END) AS started_attempt_flag,
    MAX(
      CASE
        WHEN event_type = 'media.sessionComplete'
          OR completed_event_flag = 1
          OR (
            playback_percent IS NOT NULL
            AND playback_percent >= COALESCE(completion_threshold_percent, 95.0)
          )
          OR (
            playback_position_seconds IS NOT NULL
            AND playback_position_seconds >= completion_threshold_seconds
          )
        THEN 1
        ELSE 0
      END
    ) AS completed_attempt_flag
  FROM normalized_events
  WHERE attempt_key IS NOT NULL
  GROUP BY attempt_key
),
qualified_eligible_attempts AS (
  SELECT *
  FROM attempt_facts
  WHERE started_attempt_flag = 1
    AND eligible_attempt_flag = 1
    AND qualified_attempt_flag = 1
    AND LOWER(COALESCE(consent_analytics, 'y')) IN ('granted', 'y', 'yes', 'true', '1')
)
SELECT
  activity_date,
  content_id,
  content_name,
  content_type,
  content_genre,
  content_series_id,
  content_series_name,
  season_number,
  episode_number,
  content_duration_seconds,
  content_provider,
  content_language,
  content_rating,
  content_release_date,
  content_category,
  delivery_type,
  stream_type,
  player_name,
  player_version,
  eligibility_reason,
  qualification_reason,
  completion_threshold_percent,
  completion_threshold_seconds,
  CASE WHEN autoplay_flag = 1 THEN TRUE ELSE FALSE END AS autoplay,
  CASE WHEN muted_flag = 1 THEN TRUE ELSE FALSE END AS muted,
  entitlement_status,
  subscription_tier,
  platform,
  device_type,
  operating_system,
  app_version,
  video_url,
  page_location,
  page_title,
  page_referrer,
  country,
  consent_analytics,
  COUNT(*) AS qualified_eligible_attempts,
  SUM(completed_attempt_flag) AS completed_attempts,
  ROUND(
    100.0 * SUM(completed_attempt_flag) / NULLIF(COUNT(*), 0),
    4
  ) AS content_completion_rate_percent
FROM qualified_eligible_attempts
GROUP BY
  activity_date,
  content_id,
  content_name,
  content_type,
  content_genre,
  content_series_id,
  content_series_name,
  season_number,
  episode_number,
  content_duration_seconds,
  content_provider,
  content_language,
  content_rating,
  content_release_date,
  content_category,
  delivery_type,
  stream_type,
  player_name,
  player_version,
  eligibility_reason,
  qualification_reason,
  completion_threshold_percent,
  completion_threshold_seconds,
  autoplay_flag,
  muted_flag,
  entitlement_status,
  subscription_tier,
  platform,
  device_type,
  operating_system,
  app_version,
  video_url,
  page_location,
  page_title,
  page_referrer,
  country,
  consent_analytics
ORDER BY activity_date DESC, content_id;

Source Data

Digital Analytics playback telemetry, streaming platform logs, content management metadata, and entitlement records

Dependencies

[ "playback_attempt_id", "content_id", "playback_start_event", "playback_progress_event", "playback_completed_event", "event_timestamp", "playhead_position_seconds", "authoritative_content_duration_seconds", "content_type", "playback_environment", "autoplay_preview_flag", "test_traffic_flag" ]

Report Attributes

Content ID Content Title Series Season Episode Content Type Genre Content Duration Band Release Year Original or Licensed Subscription Tier Country Region Platform Device Type Device Model Operating System Application Version Playback Quality Acquisition Channel New or Returning Viewer Start Date

Recommended Reporting Views

Views
Executive Content Engagement DashboardTitle Performance DashboardStreaming Experience DashboardAudience Engagement DashboardSeries and Episode Performance Dashboard

Segment Eligibility

true

Governance

Data Sensitivityconfidential

Governance

Created bydevyendarm
Created on9/3/2026
Last modified bydevyendarm
StatusPUBLISHED

GitHub

View File on GitHubcatalog/kpis/content-completion-rate.yml

Community Discussion