Skip to main content
Skip table of contents

GA4 Item List, Promotion & Search Term Attribution with JENTIS

What are Promotions and Item Lists in GA4?

In Google Analytics 4 (GA4), Promotions and Item Lists are reports related to e-commerce tracking that help businesses monitor and optimize their online marketing and product displays. 

Promotions

Promotions in GA4 refer to any special offers, deals, or marketing campaigns designed to increase sales or conversions. With it, you can track how your promotions (e.g., banners, and special offer sections on your site) are performing.

Some Metrics available are:

  • Promotion Views: The number of times users saw a promotion.

  • Promotion Clicks: The number of times a user clicks on a promotion.

  • CTR (Click-Through Rate): This shows the effectiveness of your promotion by dividing clicks by views.

Use Case: Track how a homepage banner promoting a seasonal sale impacts user behavior and sales.

Item Lists

Item lists in GA4 are collections or categories of products or services that you display on your site, such as product categories, search results, or recommended product sections. In this report, you have an Item List Name that identifies the context or category in which the products are shown (e.g., “Summer Collection,” “Trending Products,” or “Related Items”).

Some Metrics available are:

  • Item List Views: The number of times an item in the list was seen.

  • Item Clicks: The number of times a product in the list was clicked.

  • View-to-Click Ratio: Measures the interaction rate of items in the list.

Use Case: Analyze how products displayed under “Best Sellers” perform compared to those shown under “New Arrivals.”

Key Differences:

  • Promotions focus on marketing activities aimed at driving conversions or engagement (like special offers or discount banners).

  • Item Lists focus on tracking user interactions with product groupings and how different display contexts affect user behavior.


What is the current challenge?

Google Analytics 4 does not, by default, provide a predefined out-of-the-box tracking solution, not even in the basic GA4 client-side implementation. Instead, it allows clients to send parameters independently, and then Google works on the data, filling the reports.

For example, the user clicks on a promotion, goes down the purchase funnel and purchases that product.  In this case, the purchased product can be attributed to the clicked Promotion as an assumption that the Promotion leads the User to purchasing the product. This seamless attribution is not something GA4 offers at the moment.

To help with the correct attribution of events in these reports, JENTIS created this step-by-step guide on how to configure your GA4 tracking on our Server-side Tracking Platform.


How do the attribution properties in GA4 work?

Attribution properties in GA4 are tracked on two levels simultaneously:

Event Level: In GA4, attribution properties can be applied to an entire event without linking them to specific items. When this happens, the attribution applies to all items within that event. This is useful in a promotion click event that doesn’t include specific products. Another case is a search event: a search term can be linked to a later purchase, even though the search event itself doesn’t contain any items.

For example, a user clicks on a promotion banner on your homepage that advertises a site-wide discount. This click triggers an event called promotion_click, but the event does not specify any particular products. The promotion is for the entire site, so the attribution is applied to the whole event.

Item Level: You can also assign attribution properties to individual items within an event. GA4 prioritizes item-level properties over event-level ones because they provide more detailed, specific information, leading to more accurate attribution. If a user adds multiple items to their cart after interacting with several different promotions or item lists, using only event-level attribution would mask the true performance of each promotion or item list. Item-level attribution reveals how each interaction contributes to the final purchase.

For example, a user visits a product page after clicking on a specific product recommendation in a "You May Also Like" section and then adds the product to their cart. Later, they do the same for another product from a different recommendation list or promotion. Each click event can be attributed to the individual items they interacted with.

Key Difference:

  • Event Level: Attribution applies broadly to all items in the event, useful when the promotion or action isn't tied to a specific product.

  • Item Level: Attribution applies to individual items, giving detailed insights when users interact with multiple promotions or item lists.


Step-by-step guide on how to do your Promotion, Item List, and Search Term attribution with JENTIS

If a search_result (Search), select_promotion (Promotion click), or select_item (Product list click) event is sent to JENTIS without any associated products but includes event-level attribution properties, those properties won’t be linked to any products later in the purchase process.

However, if a select_promotion or select_item event is sent with products, and the attribution properties are only applied at the event level, JENTIS will automatically assign those properties to all products in the event. These properties can be accessed at any point if the product appears in a later eCommerce event (e.g., Purchase).

JENTIS provides three types of variables to manage attribution properties throughout the user journey:

  • Collectors: Used in select_promotion and select_item events to collect attribution properties.

  • Readers: Used in any eCommerce event where you want to retrieve these properties.

  • Cleaners: Triggered in events (by default, the Purchase event) to clear the stored attribution properties.

Item-level Collectors, Readers, and Cleaners perform CRUD (Create, Read, Update, Delete) operations on the product collection object, which is stored for each user separately, based on their journey.

Example:

  1. Create: A user views Product A, and this action is recorded in their product collection.

  2. Read: Later, you can retrieve information about Product A when analyzing their activity or when they interact with related promotions.

  3. Update: If the user adds Product A to their cart, the Product A status in the collection is updated to reflect this action.

  4. Delete: When the user completes a purchase, the product data for Product A might be deleted from the collection if it’s no longer needed for attribution or analysis.


This step-by-step guide outlines configuring and managing these attribution properties, variables, and supplementary settings for promotions and item lists tracking with JENTIS:

1. Attribution Properties Overview

These properties must be sent to Google for accurate attribution for promotions and item lists reports:

  • Promotion ID

  • Promotion Name

  • Creative Name

  • Creative Slot

  • Item List ID

  • Item List Name

2. Attributor Variables

There are six different types of variables for each attribution property, categorized into two levels and one particular “Search term” Collector:

Item Level Variables:
  • Collector: Gathers item-level attribution properties.

Click to open the Item level Collector code snippet
JS
async function(args) {
    
  const ARRAYLIST_ITERATOR = args.iterator;
  const attributionEnabled = (typeof this.accountvars["enable_attribution"] !== "undefined") ? this.accountvars["enable_attribution"] : false;
  const attribution_window = (typeof this.accountvars["attribution_window"] !== "undefined") ? this.accountvars["attribution_window"] * 1 : 1000*60*60*24*7;
  const first_click_attribution = (typeof this.accountvars["first_click_attribution"] !== "undefined") ? this.accountvars["first_click_attribution"] : false;
  
  const ItemPropertyToStore = "productlist_name";
  const productsPushedToJENTIS = this.getFrontendVariable('product_objects') || [];
  const eventLevelProperty = this.getFrontendVariable('productlistclick_name') || "";
  
  let arraylistItems = [];
  let productCollection = this.userStorage.read("product_collection") || {products: []};
  let modifiedProductCollection = productCollection;
  
  let _collect = function(objProducts, id, propertyValue, propertyName, bUpdate){
    let productCollectionModify = objProducts;
    let newEntry = true;
    let newProduct = {};
    
    for(let productIter = 0;  productIter < productCollectionModify.products.length; productIter++){
      if(
        productCollectionModify.products.length > 0 &&
        typeof productCollectionModify.products[productIter] !== "undefined" &&
        typeof productCollectionModify.products[productIter]["id"] !== "undefined" &&
        productCollectionModify.products[productIter]["id"] === id
      ){
        newEntry = false;
        if(!bUpdate){
          productCollectionModify.products[productIter][propertyName] = propertyValue;
        }
        break;
      }
    }
    if(newEntry){
      newProduct["id"] = id.toString();
      newProduct[propertyName] = propertyValue;
      productCollectionModify.products.push(newProduct);
    }
    
  return productCollectionModify;
  }
  
  if (typeof productsPushedToJENTIS !== "undefined") {
    for (var i = 0; i < productsPushedToJENTIS.length; i++) {
      if ( typeof productsPushedToJENTIS[i]["id"] !== "undefined" && productsPushedToJENTIS[i]["id"] ) {

        if(typeof productsPushedToJENTIS[i][ItemPropertyToStore] !== "undefined"){
          modifiedProductCollection = _collect(productCollection, productsPushedToJENTIS[i]["id"], productsPushedToJENTIS[i][ItemPropertyToStore], ItemPropertyToStore, first_click_attribution);
          arraylistItems.push(productsPushedToJENTIS[i][ItemPropertyToStore]);
          
        }else if(eventLevelProperty !== ""){
            modifiedProductCollection = _collect(productCollection, productsPushedToJENTIS[i]["id"], eventLevelProperty, ItemPropertyToStore, first_click_attribution);
          arraylistItems.push(eventLevelProperty);
        }else{
          modifiedProductCollection = _collect(productCollection, productsPushedToJENTIS[i]["id"], "", ItemPropertyToStore, first_click_attribution);
          arraylistItems.push("");
        }
          
      }else{
          arraylistItems.push("");
      }
    }
  }
  
  if(attributionEnabled){
    this.userStorage.write("product_collection", modifiedProductCollection, Date.now() + attribution_window);
  }
  
  return arraylistItems[ARRAYLIST_ITERATOR];
}
  • Reader: Reads item-level attribution properties in subsequent events.

Click to open the Item level Reader code snippet
JS
async function(args) {
  const ARRAYLIST_ITERATOR = args.iterator;
  const attributionEnabled = (typeof this.accountvars["enable_attribution"] !== "undefined") ? this.accountvars["enable_attribution"] : false;
  const ItemPropertyToStore = "promotion_id";
  const productsPushedToJENTIS = this.getFrontendVariable('product_objects') || [];

  let arraylistItems = [];
  let productCollection = this.userStorage.read("product_collection") || {products: []};

  let _retrieve = function(id){
    let prodID = id;
    let productProperty = "";

    for(let productIter = 0;  productIter < productCollection.products.length; productIter++){
      if(
        typeof productCollection.products[productIter]["id"] !== "undefined" &&
        productCollection.products[productIter]["id"] === prodID &&
        typeof productCollection.products[productIter][ItemPropertyToStore] !== "undefined"
      ){
        productProperty = productCollection.products[productIter][ItemPropertyToStore];
        break;
      }
    }

    return productProperty;
  }

  if (typeof productsPushedToJENTIS !== "undefined") {
    for (var i = 0; i < productsPushedToJENTIS.length; i++) {

      if(attributionEnabled){
        if ( typeof productsPushedToJENTIS[i]["id"] !== "undefined" ) {
          
        if(_retrieve(productsPushedToJENTIS[i]["id"]) !== ""){
          
          arraylistItems.push(_retrieve(productsPushedToJENTIS[i]["id"]));
          
        }else if(typeof productsPushedToJENTIS[i][ItemPropertyToStore] !== "undefined"){
  
          arraylistItems.push(productsPushedToJENTIS[i][ItemPropertyToStore]);
          
        }else{
          
          arraylistItems.push("");
        }
        }
          
      }else if (typeof productsPushedToJENTIS[i][ItemPropertyToStore] !== "undefined"){
          
        arraylistItems.push(productsPushedToJENTIS[i][ItemPropertyToStore]);
        
      }else{
  
        arraylistItems.push("");
        
      }
    }
  }

  return arraylistItems[ARRAYLIST_ITERATOR];
}
  • Cleaner: Clears item-level attribution properties when no longer needed.

Click to open the Item level Cleaner code snippet
JS
async function(args) {
  const ARRAYLIST_ITERATOR = args.iterator;
  const cleanOnPurchase = (typeof this.accountvars["delete_on_purchase"] !== "undefined") ? this.accountvars["delete_on_purchase"] : false;
  const attributionEnabled = (typeof this.accountvars["enable_attribution"] !== "undefined") ? this.accountvars["enable_attribution"] : false;
  const attribution_window = (typeof this.accountvars["attribution_window"] !== "undefined") ? this.accountvars["attribution_window"] * 1 : 1000*60*60*24*7;
  const ItemPropertyToStore = "promotion_creative";
  const productsPushedToJENTIS = this.getFrontendVariable('product_objects') || [];

  let arraylistItems = [];
  let arrIndexesToClean = [];
  let retrievedProductProperty = "";
  let productCollection = this.userStorage.read("product_collection") || {products: []};

  let _findProduct = function(arrProducts, productId){
    let keyToFind = "id";
    let valueToFind = productId;
    let foundIndex = -1;
    
    for(let productIter = 0;  productIter < arrProducts.length; productIter++){
      if (keyToFind in arrProducts[productIter] && arrProducts[productIter][keyToFind] === valueToFind){
        foundIndex = productIter;
        break;
      }
    }
    
    return foundIndex; 
  };
  let _retrieveProductProperty = function(arrProducts, property, index){
      let storedProperty = "";

      if(typeof arrProducts[index][property] !== "undefined" ){
          storedProperty = arrProducts[index][property];
      }
    
      return storedProperty;
  };
  let _cleanProductProperty = function(objProducts, property, currentIndex){
    let productFeed = objProducts;
      if ( 
        typeof productFeed !== "undefined" &&
        typeof productFeed.products !== "undefined" &&
        typeof property !== "undefined" &&
        typeof currentIndex !== "undefined" &&
        typeof productFeed.products[currentIndex] !== "undefined" &&
        typeof productFeed.products[currentIndex][property] !== "undefined"
      ){
        productFeed.products[currentIndex][property] = "";
      }
  
    return productFeed;
  };

  if (typeof productsPushedToJENTIS !== "undefined") {
      for (var i = 0; i < productsPushedToJENTIS.length; i++) {

        if(attributionEnabled){
          if ( typeof productsPushedToJENTIS[i]["id"] !== "undefined" ) {

            let productIndex = _findProduct(productCollection.products, productsPushedToJENTIS[i]["id"]);
            
            if(productIndex > -1){
              
              retrievedProductProperty = _retrieveProductProperty(productCollection.products, ItemPropertyToStore, productIndex);
              arraylistItems.push(retrievedProductProperty);
              
              if(cleanOnPurchase){
                arrIndexesToClean.push(productIndex);
              }
              
            }else if(typeof productsPushedToJENTIS[i][ItemPropertyToStore] !== "undefined"){

              arraylistItems.push(productsPushedToJENTIS[i][ItemPropertyToStore]);
              
            }else{
              arraylistItems.push("");
            }

          }
          
        }else if (typeof productsPushedToJENTIS[i][ItemPropertyToStore] !== "undefined"){
            
          arraylistItems.push(productsPushedToJENTIS[i][ItemPropertyToStore]);
          
        }else{
          
          arraylistItems.push("");
          
        }
      }
  }
          
  if(attributionEnabled && arrIndexesToClean.length > 0){
    let productCollectionClean = _cleanProductProperty(productCollection, ItemPropertyToStore, arrIndexesToClean[ARRAYLIST_ITERATOR]);
    this.userStorage.write("product_collection", productCollectionClean, Date.now() + attribution_window);
  }

  return arraylistItems[ARRAYLIST_ITERATOR];
}
Event Level Variables:
  • Collector: Collects event-level attribution properties.

Click to open the Event level Collector code snippet
JS
async function() {
  
  const attributionEnabled = (typeof this.accountvars["enable_attribution"] !== "undefined") ? this.accountvars["enable_attribution"] : false;
  const attribution_window = (typeof this.accountvars["attribution_window"] !== "undefined" && this.accountvars["attribution_window"] !== "") ? this.accountvars["attribution_window"] * 1 : 1000*60*60*24*7;
  const first_click_attribution = (typeof this.accountvars["first_click_attribution"] !== "undefined") ? this.accountvars["first_click_attribution"] : false;
  
  const productCollection = this.userStorage.read("product_collection") || {products: []};
  const storedEventLevelProperty = this.userStorage.read("event_level_item_list_id");
  const eventLevelProperty = this.getFrontendVariable('productlist_id') || "";
 
  if(attributionEnabled){
    if(
      !storedEventLevelProperty ||
      ( storedEventLevelProperty && !first_click_attribution ) 
    ){
      this.userStorage.write("event_level_item_list_id", eventLevelProperty, Date.now() + attribution_window);
    }
  }

  return eventLevelProperty;
}
  • Reader: Reads event-level attribution properties in subsequent events.

Click to open the Event level Reader code snippet
JS
async function() {
  
  const attributionEnabled = (typeof this.accountvars["enable_attribution"] !== "undefined") ? this.accountvars["enable_attribution"] : false;
  const storedEventLevelProperty = this.userStorage.read("event_level_promotion_creative");
  let eventLevelProperty = "";
  
  if(attributionEnabled){
    if(storedEventLevelProperty){
      eventLevelProperty = storedEventLevelProperty;
    }
  }
  
  return eventLevelProperty;
}
  • Cleaner: Clears event-level attribution properties when required.

Click to open the Event level Cleaner code snippet
JS
async function() {
  
  const attributionEnabled = (typeof this.accountvars["enable_attribution"] !== "undefined") ? this.accountvars["enable_attribution"] : false;
  const cleanOnPurchase = (typeof this.accountvars["delete_on_purchase"] !== "undefined") ? this.accountvars["delete_on_purchase"] : false;
  
  const storedEventLevelProperty = this.userStorage.read("event_level_promotion_creativeslot");
  let eventLevelProperty = "";
  
  if(attributionEnabled){
    if(storedEventLevelProperty){
      eventLevelProperty = storedEventLevelProperty;
    }

    if(cleanOnPurchase){
      this.userStorage.delete("event_level_promotion_creativeslot");
    }
  }

  return eventLevelProperty;
}
Special Collector:
  • Search Term Collector: Specifically affects the "Item List Name" attribution property based on user searches.

Click to open the Search Term Collector code snippet
JS
async function() {
  
  const attributionEnabled = (typeof this.accountvars["enable_attribution"] !== "undefined") ? this.accountvars["enable_attribution"] : false;
  const searchtermAttributionEnabled = (typeof this.accountvars["enable_searchterm_attribution"] !== "undefined" && this.accountvars["attribution_window"] !== "") ? this.accountvars["enable_searchterm_attribution"] : false;
  const attribution_window = (typeof this.accountvars["attribution_window"] !== "undefined" && this.accountvars["attribution_window"] !== "") ? this.accountvars["attribution_window"] * 1 : 1000*60*60*24*7;
  const first_click_attribution = (typeof this.accountvars["first_click_attribution"] !== "undefined") ? this.accountvars["first_click_attribution"] : false;
  
  const storedEventLevelProperty = this.userStorage.read("event_level_item_list_name") || false;
  const productCollection = this.userStorage.read("product_collection") || {products: []};
  const eventLevelProperty = this.getFrontendVariable('search_term') || "";
    
  let _cleanProductLevelProperties = function(arrProducts){

    let productCollectionToClean = arrProducts;

    for(let productIter = 0; productIter < productCollectionToClean.products.length; productIter++){
      productCollectionToClean.products[productIter]["productlist_id"] = "";
      productCollectionToClean.products[productIter]["productlist_name"] = "";
    }
    
    return productCollectionToClean;
  }
  
  if(attributionEnabled && searchtermAttributionEnabled){

    if(
      !storedEventLevelProperty ||
      ( storedEventLevelProperty && !first_click_attribution ) 
    ){
      this.userStorage.write("event_level_item_list_name", eventLevelProperty, Date.now() + attribution_window);
      this.userStorage.delete("event_level_item_list_id"); //delete the id if we set a name to avoid mixing attribution properties.
      
      let productCollectionCleaned = _cleanProductLevelProperties(productCollection);
      this.userStorage.write("product_collection", productCollectionCleaned, Date.now() + attribution_window);
    }
    
  }

  return eventLevelProperty;
}

3. Event Level Placeholders

All promotion- and item-list-specific parameters must be added as Event Scoped Custom Dimensions at the event level.

Parameters to include:

  • ep.promotion_id

  • ep.promotion_name

  • ep.creative_name

  • ep.creative_slot

  • ep.item_list_id

  • ep.item_list_name

These parameters should be added to the following GA4 tags to submit the Event Level attributes:

view_item

add_to_cart

begin_checkout

add_payment_info

add_shipping_info

view_cart

purchase


4. Promotion Type Variable Update

The promotion_type variable, used to filter promotion and product arrays in View Promotion and Select Promotion events, must be updated. This change will allow it to read both promotion and product data from the JENTIS Data Layer, ensuring backward compatibility with the promotion objects. Without this update, there is no way to submit promotions along with products.

Click to open the promotion_type code snippet
JS
function() {
    var property = "type";
    var arrItemData = [];
    var items = [];
    var promotions = window.jentis.tracker.getDatalayer(true, "promotion") || [];
    var products = window.jentis.tracker.getDatalayer(true, "product") || [];

    if (typeof promotions !== "undefined" && promotions.length > 0) {
      items = promotions;
    }else if(typeof products !== "undefined" && products.length > 0){
      items = products;
    }
  
    for (var i = 0; i < items.length; i++) {
        if (typeof items[i][property] !== "undefined") {
            arrItemData.push(items[i][property]);
        } else {
            arrItemData.push("");
        }
    }
    return arrItemData;
}


5. New Product Promotion Variables

Four new Product Promotion variables must be added to read promotion attributes from product objects in array lists. These include:

  • Promotion ID

  • Promotion Name

  • Creative Name

  • Creative Slot

Click to open the Product Promotion ID code snippet
JS
function() {
    var prodProperty = "promotion_id";
    var arrProdData = [];
    var prodz = window.jentis.tracker.getDatalayer(true, "product");

    if (typeof prodz !== "undefined") {
        for (var i = 0; i < prodz.length; i++) {
            if (typeof prodz[i][prodProperty] !== "undefined") {
                arrProdData.push(prodz[i][prodProperty]);
            } else {
                arrProdData.push("");
            }
        }
    }
    return arrProdData;
}

If the setup has promotion events that sometimes submit only promotion attributes but other times submit promotion attributes with product properties, the previous four variables (Promotion ID, Promotion Name, Creative, Creative Slot) must be added to the Promotion Impression tag next to the original Promotion object-based variables.

As shown in the image below, both Promotion ID Arraylist and Product Promotion ID can be submitted:

image-20240523-124534.png

EITHER promotion objects are pushed with a Promotion impression event OR product objects.


6. New Product Objects Variable

A supplementary variable is needed to retrieve all products pushed to the system, which can then be used in Collector variables:

Click to open the Product objects code snippet
JS
function() {
    var prodz = window.jentis.tracker.getDatalayer(true, "product");
    return prodz;
}


7. Tool Constants to Control Attribution

New constants must be configured to control attribution behavior:

  • Enable Attribution (Constant name: enable_attribution):

    • Set to true/false.

    • If true, enables Item List and Promotion attribution.

  • Enable Search Term Attribution (Constant name: enable_searchterm_attribution):

    • Set to true/false.

    • If true, search terms can be attributed as Item List Names.

  • Attribution Window (Constant name: attribution_window):

    • Defines how long Item Lists, Promotions, or Search Terms should be stored for attribution (in milliseconds).

  • First Click Attribution (Constant name: first_click_attribution):

    • Set to true/false.

    • If true, only the first captured properties will be attributed, and all subsequent properties will be ignored.

  • Delete on Purchase (Constant name: delete_on_purchase):

    • Set to true/false.

    • If true, all attributed properties will be cleared from storage upon purchase. Item-level properties will only be deleted for purchased items.


8. Testing

The main way to test this setup is by checking the following GA4 reports:

  • Reports > Monetization > Promotions

  • Reports > Monetization > Overview (Look at the "Items purchased by Item list name" graph)

Then, use the JENTIS Datalayer push commands to send send promotion and/or productlist related data.


If you have any questions or feedback, please open a request on our Helpdesk.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.