Skip to main content

View Products Attribution Handling

This document details the challenges and solutions for handling View Products events with proper attribution, ensuring reliable event delivery while maintaining accurate analytics.

important

Data Source for View Products Events:

  • Use current slot's data if the user navigated from a recommendation slot
  • Send without attribution if the view was not initiated from a slot
  • View Products events should be sent for ALL product views, regardless of attribution

The Challenge

View Products events present unique challenges for attribution tracking:

Problem 1: Event Timing Issues

When users click on recommendations and navigate to product pages, attempting to send View Products events immediately after the click can fail because:

  • Navigation starts before event completes - User begins navigating to the new page
  • Request cancellation - Browser cancels pending HTTP requests during navigation
  • Race conditions - Event might not complete before page unload
  • Lost attribution data - Event fails to send with proper attribution

Problem 2: Attribution Data Source Confusion

There's confusion about when to use current slot's data vs. stored data:

1. User clicks recommendation → Current slot's data should be used for View Products ✅ (correct)
2. User navigates to product page → View Products uses current slot's data ✅ (correct)
3. User returns via menu → View Products should NOT use stored data ❌ (incorrect!)
4. User returns via menu → View Products should be sent without attribution ✅ (correct)

Problem 3: Complete Analytics Requirements

View Products events must track ALL product views for analytics, not just attributed ones:

  • Total product view counts - Needed for business analytics
  • Attribution when available - Recommendation impact measurement (both sponsored and organic attribution)
  • Non-attributed views - Views from non-recommendation sources (direct navigation, menu, etc.)

Solution Approaches

Approach 1: URL Parameters

Pass attribution data through URL parameters when navigating from recommendations.

// When user clicks on recommendation
function handleProductClick(productRefId, clickData) {
const clickId = generateUniqueId();

// Store click data and send click event
// ... existing storage logic ...

// Pass attribution data via URL parameters
const attributionParams = new URLSearchParams({
pa_click_id: clickId,
pa_route_id: clickData.routeId,
pa_widget_id: clickData.widgetId,
pa_recommender_id: clickData.recommenderId,
pa_campaign_id: clickData.campaignId,
pa_tactic_id: clickData.tacticId,
pa_tactic_label: clickData.tacticLabel,
pa_placement_id: clickData.placementId,
pa_banner_id: clickData.bannerId,
pa_ad_set_id: clickData.adSetId,
pa_ad_set_version: clickData.adSetVersion,
pa_cost_per_click: clickData.costPerClick,
pa_cost_per_action: clickData.costPerAction,
pa_cost_per_mille: clickData.costPerMille,
pa_timestamp: clickData.timeStamp,
pa_hmac_salt: clickData.hmacSalt,
pa_hmac: clickData.hmac,
pa_supplier_id: clickData.supplierId,
pa_retail_boost_campaign_id: clickData.retailBoostCollectionCampaignId,
pa_timestamp: Date.now().toString()
});

// Navigate to product page with attribution data
const productUrl = `https://www.example.com/product/${productRefId}?${attributionParams.toString()}`;
window.location.href = productUrl;
}

// On product page load
function handleProductPageView(productRefId) {
const urlParams = new URLSearchParams(window.location.search);

const eventData = {
refId: productRefId,
currentUrl: window.location.href,
eventTime: new Date().toISOString()
};

// Check for attribution parameters
const clickId = urlParams.get('pa_click_id');
if (clickId) {
const timestamp = parseInt(urlParams.get('pa_timestamp'));
const timeDiff = Date.now() - timestamp;
const isRecent = timeDiff <= 5 * 60 * 1000; // 5 minutes

if (isRecent) {
Object.assign(eventData, {
clickId: clickId,
routeId: urlParams.get('pa_route_id'),
widgetId: urlParams.get('pa_widget_id'),
recommenderId: urlParams.get('pa_recommender_id'),
campaignId: urlParams.get('pa_campaign_id'),
tacticId: urlParams.get('pa_tactic_id'),
tacticLabel: urlParams.get('pa_tactic_label'),
placementId: urlParams.get('pa_placement_id'),
bannerId: urlParams.get('pa_banner_id'),
adSetId: urlParams.get('pa_ad_set_id'),
adSetVersion: urlParams.get('pa_ad_set_version'),
costPerClick: urlParams.get('pa_cost_per_click'),
costPerAction: urlParams.get('pa_cost_per_action'),
costPerMille: urlParams.get('pa_cost_per_mille'),
timeStamp: urlParams.get('pa_timestamp'),
hmacSalt: urlParams.get('pa_hmac_salt'),
hmac: urlParams.get('pa_hmac'),
supplierId: urlParams.get('pa_supplier_id'),
retailBoostCollectionCampaignId: urlParams.get('pa_retail_boost_campaign_id')
});
}
}

// Always send View Products event
sendViewProductsEvent(eventData);
}

Pros:

  • Simple implementation
  • Works with any navigation method
  • No storage dependencies

Cons:

  • URL pollution
  • Limited parameter length
  • Visible in browser history

Approach 2: Session Storage (Temporary)

Store attribution data temporarily in sessionStorage for the next page load.

// When user clicks on recommendation
function handleProductClick(productRefId, clickData) {
const clickId = generateUniqueId();

// Store click data and send click event
// ... existing storage logic ...

// Store temporary attribution data for next page load
sessionStorage.setItem('pa_pending_attribution', JSON.stringify({
productRefId: productRefId,
clickId: clickId,
routeId: clickData.routeId,
widgetId: clickData.widgetId,
recommenderId: clickData.recommenderId,
campaignId: clickData.campaignId,
tacticId: clickData.tacticId,
tacticLabel: clickData.tacticLabel,
placementId: clickData.placementId,
bannerId: clickData.bannerId,
adSetId: clickData.adSetId,
adSetVersion: clickData.adSetVersion,
costPerClick: clickData.costPerClick,
costPerAction: clickData.costPerAction,
costPerMille: clickData.costPerMille,
timeStamp: clickData.timeStamp,
hmacSalt: clickData.hmacSalt,
hmac: clickData.hmac,
supplierId: clickData.supplierId,
retailBoostCollectionCampaignId: clickData.retailBoostCollectionCampaignId,
timestamp: Date.now()
}));

// Navigate to product page
window.location.href = `https://www.example.com/product/${productRefId}`;
}

// On product page load
function handleProductPageView(productRefId) {
// Check for pending attribution data
const pendingAttribution = sessionStorage.getItem('pa_pending_attribution');

const eventData = {
refId: productRefId,
currentUrl: window.location.href,
eventTime: new Date().toISOString()
};

if (pendingAttribution) {
const attribution = JSON.parse(pendingAttribution);

// Only use attribution if it's for this product and recent
const timeDiff = Date.now() - attribution.timestamp;
const isRecent = timeDiff <= 5 * 60 * 1000; // 5 minutes

if (attribution.productRefId === productRefId && isRecent) {
Object.assign(eventData, {
clickId: attribution.clickId,
routeId: attribution.routeId,
widgetId: attribution.widgetId,
recommenderId: attribution.recommenderId,
campaignId: attribution.campaignId,
tacticId: attribution.tacticId
});

// Clear the pending attribution after use
sessionStorage.removeItem('pa_pending_attribution');
}
}

// Always send View Products event
sendViewProductsEvent(eventData);
}

Pros:

  • Clean URLs
  • No size limitations
  • Automatic cleanup on session end
  • Reliable data transfer

Cons:

  • Session-dependent
  • Requires JavaScript
  • Single-use only

Combine sessionStorage with timestamp validation and automatic cleanup.

// When user clicks on recommendation
function handleProductClick(productRefId, clickData) {
const clickId = generateUniqueId();

// Store click data as before
let storedData = getStoredRecommendationData(customerId, productRefId);
if (!storedData) {
storedData = {
refId: productRefId,
sponsored: null,
organic: null,
recommendationTime: new Date().toISOString()
};
}

// Determine if this is a sponsored or organic click
const isSponsored = clickData.adSetId && clickData.adSetId.length > 0;

// Store click data in appropriate slot (only for recommendation-based clicks)
if (isSponsored) {
storedData.sponsored = {
clickId: clickId,
eventTime: new Date().toISOString(),
...clickData
};
} else if (clickData.routeId && clickData.widgetId) { // Only store organic clicks from recommendations
storedData.organic = {
clickId: clickId,
eventTime: new Date().toISOString(),
...clickData
};
}
// Native button clicks (contextType: NativeButton) are not stored

storedData.lastUpdated = new Date().toISOString();
storeRecommendationData(customerId, productRefId, storedData);

// Send click event
sendClickEvent({
clickId: clickId,
refId: productRefId,
...clickData
});

// Store temporary attribution data for immediate use
sessionStorage.setItem('pa_pending_attribution', JSON.stringify({
productRefId: productRefId,
clickId: clickId,
routeId: clickData.routeId,
widgetId: clickData.widgetId,
recommenderId: clickData.recommenderId,
campaignId: clickData.campaignId,
tacticId: clickData.tacticId,
tacticLabel: clickData.tacticLabel,
placementId: clickData.placementId,
bannerId: clickData.bannerId,
adSetId: clickData.adSetId,
adSetVersion: clickData.adSetVersion,
costPerClick: clickData.costPerClick,
costPerAction: clickData.costPerAction,
costPerMille: clickData.costPerMille,
timeStamp: clickData.timeStamp,
hmacSalt: clickData.hmacSalt,
hmac: clickData.hmac,
supplierId: clickData.supplierId,
retailBoostCollectionCampaignId: clickData.retailBoostCollectionCampaignId,
timestamp: Date.now()
}));

// Navigate to product page
window.location.href = `https://www.example.com/product/${productRefId}`;
}

// On product page load
function handleProductPageView(productRefId) {
// Check for pending attribution data
const pendingAttribution = sessionStorage.getItem('pa_pending_attribution');

const eventData = {
refId: productRefId,
currentUrl: window.location.href,
eventTime: new Date().toISOString()
};

if (pendingAttribution) {
const attribution = JSON.parse(pendingAttribution);

// Only use attribution if it's for this product and recent
const timeDiff = Date.now() - attribution.timestamp;
const isRecent = timeDiff <= 5 * 60 * 1000; // 5 minutes

if (attribution.productRefId === productRefId && isRecent) {
Object.assign(eventData, {
clickId: attribution.clickId,
routeId: attribution.routeId,
widgetId: attribution.widgetId,
recommenderId: attribution.recommenderId,
campaignId: attribution.campaignId,
tacticId: attribution.tacticId
});

// Clear the pending attribution after use
sessionStorage.removeItem('pa_pending_attribution');
}
}

// Always send View Products event
sendViewProductsEvent(eventData);
}

Pros:

  • Reliable event delivery
  • Clean URLs
  • Automatic cleanup
  • Timestamp validation
  • Handles all scenarios

Cons:

  • More complex implementation
  • Requires JavaScript

Implementation Best Practices

1. Timestamp Validation

Always validate attribution timestamps to prevent stale data usage:

const MAX_ATTRIBUTION_AGE = 5 * 60 * 1000; // 5 minutes

function isAttributionValid(timestamp) {
const timeDiff = Date.now() - timestamp;
return timeDiff <= MAX_ATTRIBUTION_AGE;
}

2. Automatic Cleanup

Clear temporary attribution data after use to prevent reuse:

// Clear after successful use
sessionStorage.removeItem('pa_pending_attribution');

// Clear on page unload (fallback)
window.addEventListener('beforeunload', () => {
sessionStorage.removeItem('pa_pending_attribution');
});

3. Fallback Handling

Always send View Products events even without attribution:

// Always send View Products event, with or without attribution
sendViewProductsEvent(eventData);

4. Error Handling

Handle cases where attribution data is malformed:

try {
const attribution = JSON.parse(pendingAttribution);
// Use attribution data
} catch (error) {
console.warn('Invalid attribution data:', error);
// Send event without attribution
}

Alternative Approaches

Time-Based Attribution Window

Use stored recommendation data with time validation:

function handleProductPageView(productRefId) {
const attributionData = getAttributionData(customerId, productRefId);
const attributionWindow = 30; // minutes

let shouldUseAttribution = false;

if (attributionData) {
const clickTime = new Date(attributionData.eventTime);
const now = new Date();
const minutesDiff = (now - clickTime) / (1000 * 60);

shouldUseAttribution = minutesDiff <= attributionWindow;
}

// Send event with or without attribution
sendViewProductsEvent(eventData);
}

Referrer-Based Attribution

Use document.referrer to determine attribution:

function handleProductPageView(productRefId) {
const attributionData = getAttributionData(customerId, productRefId);
const referrer = document.referrer;

let shouldUseAttribution = false;

if (attributionData && referrer) {
const isFromRecommendation = referrer.includes('recommendation') ||
referrer.includes('widget');
shouldUseAttribution = isFromRecommendation;
}

// Send event with or without attribution
sendViewProductsEvent(eventData);
}

Summary

The recommended approach for View Products attribution handling is:

  1. Use sessionStorage for temporary attribution data transfer
  2. Include timestamp validation to prevent stale data usage
  3. Clear data after use to prevent reuse
  4. Always send events regardless of attribution availability
  5. Handle all user journey patterns including direct navigation
  6. Use current slot's data for View Products (not stored data)
important

Key Principle: View Products events should use the current slot's attribution data when navigating from a recommendation slot, not stored data from previous interactions. This ensures accurate attribution for the specific slot that led to the product view.

This ensures reliable event delivery while maintaining accurate attribution tracking and complete analytics coverage.