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.
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
Approach 3: Hybrid Approach (Recommended)
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);
}