← Back to KPIs

Product List Click-Through Rate

Also known as: Item List Click-Through Rate, Item List CTR, Item-list click-through rate, Product Listing CTR, PLP Click-Through Rate

EngagementkpiE-commerce: Product Discovery & Merchandising (Search/Filters/PDP)E-commerce

Product List Click-Through Rate is the share of product list views (category pages, search results, recommendation carousels, cross-sell modules) that produce a click on a product, measured as select_item events divided by view_item_list events and broken down by item_list_id and item_list_name. It is event-based and differs from GA4's built-in Item-list click-through rate, which is user-based (users who selected an item from a list divided by users who viewed that list), so the two numbers will not reconcile. It shows how well each list's ranking, product mix and tile content turn browsing into product-detail visits, which makes it the main health signal for merchandising and discovery surfaces before add-to-cart and purchase.

Business Use Case

1️⃣ The Merchandising team reviews category-page list CTR every week. When a category's rate falls more than 15% below its trailing four-week baseline, a merchandiser re-ranks the grid (boosting in-stock, high-margin and top-rated SKUs, and demoting out-of-stock SKUs) and checks recovery in the following week's read. 2️⃣ The Personalization product manager uses recommendation-carousel CTR, sliced by item_list_name and placement, as the primary metric for algorithm A/B tests. The variant ships only when it shows a statistically significant CTR lift and no drop in downstream item-view-to-cart rate. 3️⃣ The Site Search team monitors search-results list CTR for the top 200 queries by volume. Queries with high result counts but CTR below the site median are triaged for synonym, boosting or relevance-rule changes in the search engine. 4️⃣ The UX and Product Design team compares list CTR by device category on product listing pages. When mobile trails desktop by more than 20% relative, they open a tile-redesign test (image ratio, price and promo badge placement, rating display) and track CTR as the success metric.

Business Criticality

High

Journey / Navigation Area

Discovery

Owning Function

Merchandising

Scope

Event

Formula

(Item List Clicks [count of select_item events] / Item List Views [count of view_item_list events]) × 100, where both counts are grouped by the same item_list_id / item_list_name over the same date range

Calculation Notes

1️⃣ Grain and slicing: the KPI uses list-view grain. The numerator is select_item events and the denominator is view_item_list events, and both are joined on item_list_id, falling back to item_list_name when no ID is sent. Recommendation, search-results and category lists are slices of this KPI by item_list_name or recommendation placement, so they do not get separate specs. The search-result-click-through-rate metric covers search-result lists only, and it must reconcile to this KPI filtered to those lists. 2️⃣ Do not mix grains: the item-impression ratio (select_item divided by the number of items[] entries in view_item_list) answers a different question and must not be blended with this KPI or shown under the same name. Report it separately if position-level analysis is needed. A list view counts once however many products it contains. 3️⃣ Denominator hygiene: view_item_list fires once per list render that meets the viewability rule (at least 50% of the list container in the viewport for at least 1 second). Below-the-fold carousels that are never scrolled into view are excluded. Re-renders of the same list within the same page_view (SPA re-hydration, sort toggle that returns identical results, carousel auto-rotation) are de-duplicated. Each new infinite-scroll batch or pagination page counts as a new list view. 4️⃣ Numerator hygiene: count only clicks on product tiles, including quick-view opens and swatch clicks that navigate to the PDP. Exclude banners, editorial or content cards, filter chips and sponsored placements that do not carry an items[] payload. A select_item without item_list_id or item_list_name goes into a '(not set)' bucket. That bucket is included in the site-level total but kept out of list-level rankings, and a (not set) share above 5% is treated as a tagging defect. 5️⃣ Ratios above 100%: several clicks from one list view are all counted, so a list-level value above 100% usually means a back-navigation restore from bfcache that did not re-fire view_item_list, or select_item firing on hover or touchstart. Fix this in the tag by re-firing view_item_list on pageshow when event.persisted is true. Never cap the value in reporting, because capping hides the defect. 6️⃣ Exclusions and session rules: remove known bots and spiders (GA4 excludes known bots at collection; AEP filters them by user agent), internal and developer traffic (traffic_type / _tenant.trafficType, fed by the internal IP rules), debug-mode and QA traffic, and any hostname outside the production allow-list (which removes staging) before aggregating. The KPI is event-scoped, so the GA4 30-minute inactivity timeout does not affect the ratio. Session- or user-scoped cuts (for example, the share of sessions with at least one list click) are separate reads and must use distinct session or user counts in both numerator and denominator. 7️⃣ Timing and significance: events are dated by event_timestamp in the property reporting time zone. Daily values stay provisional for 72 hours to allow for late-arriving GA4 export hits. Compare lists or test variants only when each has at least 1,000 list views in the window, and use a two-proportion z-test before acting on differences below 2 percentage points.

Technical Details

Measure TypePercentage
Aggregation WindowDaily, Weekly, Monthly

Events

PlatformEvent
Google Analytics 4select_item
Adobeselect_item

Data Mappings

W3 Data Layer
{
  "page": {
    "pageInfo": {
      "pageID": "plp-womens-running-shoes",
      "pageName": "category:women:running-shoes",
      "pageType": "Product Listing"
    },
    "category": {
      "primaryCategory": "Women",
      "subCategory1": "Running Shoes"
    }
  },
  "productList": [
    {
      "listInfo": {
        "listID": "plp_womens_running_shoes",
        "listName": "Category Grid - Women's Running Shoes",
        "placement": "main_grid"
      },
      "item": [
        {
          "productInfo": {
            "productID": "SKU_12345",
            "productName": "Trail Runner 5",
            "manufacturer": "Acme Sports"
          },
          "category": {
            "primaryCategory": "Footwear",
            "subCategory1": "Running Shoes"
          },
          "price": {
            "basePrice": 129.99,
            "currency": "USD"
          },
          "attributes": {
            "index": 0
          }
        },
        {
          "productInfo": {
            "productID": "SKU_67890",
            "productName": "Road Racer 3",
            "manufacturer": "Acme Sports"
          },
          "category": {
            "primaryCategory": "Footwear",
            "subCategory1": "Running Shoes"
          },
          "price": {
            "basePrice": 109.99,
            "currency": "USD"
          },
          "attributes": {
            "index": 1
          }
        }
      ]
    }
  ],
  "event": [
    {
      "eventInfo": {
        "eventName": "view_item_list",
        "eventAction": "list_view",
        "type": "impression"
      },
      "category": {
        "primaryCategory": "ecommerce"
      },
      "attributes": {
        "listID": "plp_womens_running_shoes",
        "listName": "Category Grid - Women's Running Shoes",
        "itemCount": 2
      }
    },
    {
      "eventInfo": {
        "eventName": "select_item",
        "eventAction": "list_click",
        "type": "interaction"
      },
      "category": {
        "primaryCategory": "ecommerce"
      },
      "attributes": {
        "listID": "plp_womens_running_shoes",
        "listName": "Category Grid - Women's Running Shoes",
        "productID": "SKU_67890",
        "index": 1
      }
    }
  ]
}
GA4 Data Layer
{
  "event": "select_item",
  "page_type": "product_listing",
  "ecommerce": {
    "item_list_id": "plp_womens_running_shoes",
    "item_list_name": "Category Grid - Women's Running Shoes",
    "items": [
      {
        "item_id": "SKU_67890",
        "item_name": "Road Racer 3",
        "item_brand": "Acme Sports",
        "item_category": "Footwear",
        "item_category2": "Running Shoes",
        "item_list_id": "plp_womens_running_shoes",
        "item_list_name": "Category Grid - Women's Running Shoes",
        "index": 1,
        "price": 109.99,
        "quantity": 1
      }
    ]
  }
}
Adobe Client Data Layer
{
  "event": "select_item",
  "eventInfo": {
    "path": "component.productlist-grid"
  },
  "page": {
    "pageType": "Product Listing",
    "pageName": "category:women:running-shoes"
  },
  "productList": {
    "listId": "plp_womens_running_shoes",
    "listName": "Category Grid - Women's Running Shoes",
    "placement": "main_grid"
  },
  "product": [
    {
      "productId": "SKU_67890",
      "productName": "Road Racer 3",
      "brand": "Acme Sports",
      "category": "Footwear/Running Shoes",
      "position": 1,
      "price": 109.99,
      "currency": "USD"
    }
  ]
}
XDM Mapping
{
  "eventType": "web.webinteraction.linkClicks",
  "web": {
    "webPageDetails": {
      "name": "category:women:running-shoes",
      "URL": "https://www.example.com/women/running-shoes",
      "siteSection": "Product Listing"
    },
    "webInteraction": {
      "name": "select_item",
      "type": "other",
      "linkClicks": {
        "value": 1
      }
    }
  },
  "productListItems": [
    {
      "SKU": "SKU_67890",
      "name": "Road Racer 3",
      "productCategories": [
        {
          "categoryID": "footwear-running-shoes",
          "categoryName": "Footwear/Running Shoes"
        }
      ],
      "priceTotal": 109.99,
      "currencyCode": "USD",
      "quantity": 1
    }
  ],
  "_tenant": {
    "pageType": "Product Listing",
    "trafficType": "external",
    "itemList": {
      "eventName": "select_item",
      "listID": "plp_womens_running_shoes",
      "listName": "Category Grid - Women's Running Shoes",
      "placement": "main_grid"
    }
  },
  "_experience": {
    "analytics": {
      "customDimensions": {
        "eVars": {
          "eVar10": "plp_womens_running_shoes",
          "eVar11": "Category Grid - Women's Running Shoes",
          "eVar12": "1"
        }
      }
    }
  },
  "view_item_list_event": {
    "eventType": "itemList.view",
    "web": {
      "webPageDetails": {
        "name": "category:women:running-shoes",
        "URL": "https://www.example.com/women/running-shoes",
        "siteSection": "Product Listing"
      }
    },
    "productListItems": [
      {
        "SKU": "SKU_12345",
        "name": "Trail Runner 5",
        "priceTotal": 129.99,
        "currencyCode": "USD"
      },
      {
        "SKU": "SKU_67890",
        "name": "Road Racer 3",
        "priceTotal": 109.99,
        "currencyCode": "USD"
      }
    ],
    "_tenant": {
      "pageType": "Product Listing",
      "trafficType": "external",
      "itemList": {
        "eventName": "view_item_list",
        "listID": "plp_womens_running_shoes",
        "listName": "Category Grid - Women's Running Shoes",
        "placement": "main_grid",
        "itemCount": 2
      }
    },
    "note": "view_item_list is sent as its own experience event with the custom eventType itemList.view and no web.webInteraction / linkClicks object, so list views never inflate link clicks. Both list events carry _tenant.itemList (eventName, listID, listName, placement), populated from the ACDL productList object (listId, listName, placement); _tenant.pageType comes from ACDL page.pageType. _tenant.trafficType mirrors the GA4 traffic_type rule (internal, developer) and is set to developer when debug mode is on."
  }
}

SQL Queries

Google Analytics 4 (BigQuery)
-- ==========================================
-- 1. GOOGLE BIGQUERY SQL
-- ==========================================
-- Product List Click-Through Rate = select_item events / view_item_list events x 100, at list-view grain.
-- item_list_id / item_list_name are read from event_params first, then from items[] (GA4 export stores them there for most web streams).
-- Exclusions: known bots are removed by GA4 at collection; internal/developer traffic_type, debug_mode hits and hostnames outside the production allow-list are removed here.
-- meets_min_volume applies the 1,000 list-view minimum over the whole window per list, not per row.
WITH list_events AS (
  SELECT
    PARSE_DATE('%Y%m%d', event_date) AS event_date,
    event_name,
    device.category AS device_category,
    (SELECT MAX(IF(ep.key = 'item_list_id', ep.value.string_value, NULL)) FROM UNNEST(event_params) AS ep) AS param_list_id,
    (SELECT MAX(IF(ep.key = 'item_list_name', ep.value.string_value, NULL)) FROM UNNEST(event_params) AS ep) AS param_list_name,
    (SELECT MAX(IF(ep.key = 'page_type', ep.value.string_value, NULL)) FROM UNNEST(event_params) AS ep) AS page_type,
    (SELECT MAX(IF(ep.key = 'page_location', ep.value.string_value, NULL)) FROM UNNEST(event_params) AS ep) AS page_location,
    (SELECT MAX(IF(ep.key = 'traffic_type', ep.value.string_value, NULL)) FROM UNNEST(event_params) AS ep) AS traffic_type,
    (SELECT MAX(IF(ep.key = 'debug_mode', COALESCE(ep.value.int_value, SAFE_CAST(ep.value.string_value AS INT64)), NULL)) FROM UNNEST(event_params) AS ep) AS debug_mode,
    (SELECT MAX(NULLIF(it.item_list_id, '(not set)')) FROM UNNEST(items) AS it) AS items_list_id,
    (SELECT MAX(NULLIF(it.item_list_name, '(not set)')) FROM UNNEST(items) AS it) AS items_list_name
  FROM `your-project.analytics_123456789.events_*`
  WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 31 DAY))
                          AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY))
    AND event_name IN ('view_item_list', 'select_item')
),
normalized AS (
  SELECT
    event_date,
    event_name,
    device_category,
    page_type,
    COALESCE(NULLIF(param_list_id, ''), NULLIF(items_list_id, ''), NULLIF(param_list_name, ''), NULLIF(items_list_name, ''), '(not set)') AS item_list_id,
    COALESCE(NULLIF(param_list_name, ''), NULLIF(items_list_name, ''), '(not set)') AS item_list_name
  FROM list_events
  WHERE COALESCE(traffic_type, '') NOT IN ('internal', 'developer')
    AND COALESCE(debug_mode, 0) = 0
    AND LOWER(NET.HOST(page_location)) IN ('www.example.com', 'example.com')  -- production hostname allow-list
),
daily AS (
  SELECT
    event_date,
    item_list_id,
    item_list_name,
    device_category,
    COUNTIF(event_name = 'select_item') AS item_list_clicks,
    COUNTIF(event_name = 'view_item_list') AS item_list_views
  FROM normalized
  GROUP BY event_date, item_list_id, item_list_name, device_category
)
SELECT
  event_date,
  item_list_id,
  item_list_name,
  device_category,
  item_list_clicks,
  item_list_views,
  ROUND(100 * SAFE_DIVIDE(item_list_clicks, item_list_views), 2) AS product_list_ctr_pct,
  item_list_id = '(not set)' AS is_not_set_bucket,
  SUM(item_list_views) OVER (PARTITION BY item_list_id, item_list_name) >= 1000 AS meets_min_volume
FROM daily
ORDER BY event_date DESC, item_list_views DESC;
Adobe Experience Platform (AEP Query Service)
-- ==========================================
-- 2. ADOBE AEP QUERY SERVICE SQL
-- ==========================================
-- Product List Click-Through Rate = select_item events / view_item_list events x 100, at list-view grain.
-- List events are identified by _tenant.itemList.eventName (view_item_list is sent with eventType itemList.view, select_item with web.webinteraction.linkClicks).
-- List ID and name come from _tenant.itemList.listID / listName (mapped from the ACDL productList object). Replace _tenant with your tenant namespace.
-- Exclusions: bot user agents, internal/developer _tenant.trafficType and hostnames outside the production allow-list.
-- meets_min_volume applies the 1,000 list-view minimum over the whole window per list, not per row.
WITH list_events AS (
  SELECT
    to_date(timestamp) AS event_date,
    _tenant.itemList.eventName AS list_event_name,
    _tenant.pageType AS page_type,
    device.type AS device_type,
    COALESCE(NULLIF(_tenant.itemList.listID, ''),
             NULLIF(_tenant.itemList.listName, ''),
             '(not set)') AS item_list_id,
    COALESCE(NULLIF(_tenant.itemList.listName, ''), '(not set)') AS item_list_name
  FROM aep_web_events_dataset
  WHERE timestamp >= date_sub(current_date(), 31)
    AND timestamp < current_date()
    AND _tenant.itemList.eventName IN ('view_item_list', 'select_item')
    AND COALESCE(_tenant.trafficType, '') NOT IN ('internal', 'developer')
    AND lower(parse_url(web.webPageDetails.URL, 'HOST')) IN ('www.example.com', 'example.com')  -- production hostname allow-list
    AND NOT (lower(COALESCE(environment.browserDetails.userAgent, '')) RLIKE 'bot|crawler|spider|slurp|headless|lighthouse')
),
daily AS (
  SELECT
    event_date,
    item_list_id,
    item_list_name,
    device_type,
    SUM(CASE WHEN list_event_name = 'select_item' THEN 1 ELSE 0 END) AS item_list_clicks,
    SUM(CASE WHEN list_event_name = 'view_item_list' THEN 1 ELSE 0 END) AS item_list_views
  FROM list_events
  GROUP BY event_date, item_list_id, item_list_name, device_type
)
SELECT
  event_date,
  item_list_id,
  item_list_name,
  device_type,
  item_list_clicks,
  item_list_views,
  ROUND(100.0 * item_list_clicks / NULLIF(item_list_views, 0), 2) AS product_list_ctr_pct,
  item_list_id = '(not set)' AS is_not_set_bucket,
  SUM(item_list_views) OVER (PARTITION BY item_list_id, item_list_name) >= 1000 AS meets_min_volume
FROM daily
ORDER BY event_date DESC, item_list_views DESC;

Source Data

Digital Analytics (GA4)

Dependencies

Prerequisite: List the dependencies required for this KPI

Metrics:

item-list-clicksitem-list-views

Report Attributes

Item List Name Item List ID Recommendation Placement Page Type Item Category Device Category Traffic Source Internal Search Term

Recommended Reporting Views

Views
Merchandising Performance DashboardProduct Discovery & Site Search DashboardPersonalization & Recommendations Test ReadoutE-commerce Funnel Overview

Segment Eligibility

true

Governance

Data SensitivityInternal

Governance

Created bydevyendarm
Created on9/16/2026
Last modified bydevyendarm
Last modified on9/18/2026
Version1.0.0
Validation stateqa passed
StatusPUBLISHED

GitHub

View related Pull Requestcatalog/kpis/product-list-click-through-rate.yml

Community Discussion