Wishlist Additions
Also known as: Add to Wishlist Events, Wishlist Adds, Items Added to Wishlist, Save for Later Additions, Favourites Added
Wishlist Additions counts the confirmed add_to_wishlist events recorded when a shopper saves a product to a wishlist, favourites or save-for-later list, reported at event grain and at item grain from the items[] array. It is the numerator of Add-to-List Rate and a leading indicator of deferred purchase intent, showing which products attract consideration but have not yet been carted.
Business Use Case
1️⃣ Merchandising: each week the merchandising team reviews SKUs in the top decile for item-level wishlist additions whose Item View-to-Cart Rate is below category median, then checks price point, size or colour availability and imagery before choosing to mark the item down or refresh the PDP.,2️⃣ CRM & Lifecycle Marketing: when a wishlisted SKU gets a price drop of 10% or more or comes back in stock, the lifecycle team sends a triggered email or push to consented customers who saved it in the last 90 days, and measures the result as wishlist-to-purchase conversion.,3️⃣ Buying & Inventory Planning: before a replenishment or pre-order buy is committed, the planners use item-level wishlist additions for out-of-stock and not-yet-launched SKUs as a demand signal to size initial quantities and choose which store or DC allocation to prioritise.,4️⃣ Product (PDP & Listing UX): when a release changes where the wishlist control appears on product listing or product detail pages, the product team A/B tests wishlist additions per 1,000 product views and checks that add-to-cart volume holds steady before rolling the change out to all traffic.
Business Criticality
Medium
Journey / Navigation Area
Discovery
Owning Function
Merchandising
Scope
Event
Formula
Calculation Notes
1️⃣ Two grains, never mixed: the event-level count (one per add_to_wishlist hit) feeds Add-to-List Rate against view_item events, while the item-level count (one per items[] entry with a non-null item_id) feeds SKU, brand and category reporting. A multi-item save, such as moving a whole cart to the wishlist, is 1 event but N items; items[].quantity is ignored because a wishlist holds a product, not units.,2️⃣ Count only confirmed saves: the event must fire on the wishlist service's success response, not on the heart-icon click. Adds rejected for an unauthenticated user, a full list or an invalid variant are excluded. Double-fires are deduplicated to one: a save is counted only when it is the first save by that user (user_pseudo_id in GA4, ECID in AEP) for the same sorted set of item_id values, or arrives more than 2 seconds after the previous save with that same user and item set.,3️⃣ Guest-to-account merges are not new additions: when items saved as a guest in local storage are synced to the account at login, the sync must not emit add_to_wishlist. A save that forces a login is counted once, after authentication completes. An add, remove and re-add of the same item_id in one session counts as each confirmed add, because GA4 has no standard remove_from_wishlist event to net against.,4️⃣ Exclusions: internal traffic and staging, QA, preview and dev hostnames are removed in both SQL fields. GA4 removes known bots and spiders before the BigQuery export, so ga4_sql needs no bot filter; aep_sql filters bot, spider, crawler and headless user agents. Events without items[] or a non-null item_id are excluded from item grain only: they still count at event grain and are surfaced as events_missing_items in the data-quality monitor, which should stay below 1% of volume.,5️⃣ Timing: attribute to event_date in the reporting-property timezone. Sessions follow the GA4 30-minute inactivity timeout, so an add after 30 idle minutes belongs to a new session for session-scoped segments. Daily figures are treated as provisional for 72 hours to absorb late-arriving hits.,6️⃣ The value parameter is the sum of item prices at the moment of saving, in the event's currency. It is intent value, not revenue, and must never be added to purchase revenue. Aggregate it only after converting to the reporting currency at the event_date rate; saves with no price, such as pre-launch SKUs, carry value 0 or null. A daily FX rates table (fx_daily_rates_dataset) is a required input for aep_sql: a non-USD save with no rate for its event_date keeps a NULL value, is left out of the value sum and is counted in fx_rate_missing_events.,7️⃣ Context split: save-for-later actions started from the cart (item_list_name = 'cart' or equivalent) are a cart-abandonment signal, not a discovery signal. Report them as a separate row and exclude them from the Product Discovery numerator of Add-to-List Rate, so the rate stays comparable with product-view-based denominators.
Technical Details
Events
| Platform | Event |
|---|---|
| Google Analytics 4 | add_to_wishlist |
| Adobe | addToWishlist |
Data Mappings
W3 Data Layer
{
"digitalData": {
"page": {
"pageInfo": {
"pageName": "pdp:SKU-10482",
"pageType": "product detail"
}
},
"event": [
{
"eventInfo": {
"eventName": "add_to_wishlist",
"eventAction": "save",
"type": "wishlist",
"timeStamp": "2026-09-14T00:59:00Z"
},
"attributes": {
"currency": "USD",
"value": 89.99,
"listName": "product_detail"
}
}
],
"product": [
{
"productInfo": {
"productID": "SKU-10482",
"productName": "Trail Running Shoe",
"manufacturer": "Northpeak"
},
"category": {
"primaryCategory": "Footwear"
},
"price": {
"basePrice": 89.99,
"currency": "USD"
},
"quantity": 1
}
]
}
}GA4 Data Layer
{
"event": "add_to_wishlist",
"ecommerce": {
"currency": "USD",
"value": 89.99,
"items": [
{
"item_id": "SKU-10482",
"item_name": "Trail Running Shoe",
"item_brand": "Northpeak",
"item_category": "Footwear",
"item_list_name": "product_detail",
"price": 89.99,
"quantity": 1
}
]
}
}Adobe Client Data Layer
{
"event": "addToWishlist",
"eventInfo": {
"path": "component.wishlist-button",
"linkName": "Add to Wishlist",
"linkType": "o"
},
"commerce": {
"currency": "USD",
"value": 89.99,
"listName": "product_detail",
"products": [
{
"sku": "SKU-10482",
"name": "Trail Running Shoe",
"brand": "Northpeak",
"category": "Footwear",
"price": 89.99,
"quantity": 1
}
]
}
}XDM Mapping
{
"eventType": "commerce.saveForLaters",
"commerce": {
"saveForLaters": {
"value": 1
}
},
"productListItems": [
{
"SKU": "SKU-10482",
"name": "Trail Running Shoe",
"productCategories": [
{
"categoryName": "Footwear"
}
],
"priceTotal": 89.99,
"quantity": 1,
"currencyCode": "USD"
}
],
"_experience": {
"analytics": {
"customDimensions": {
"eVars": {
"eVar1": "product_detail"
}
}
}
},
"web": {
"webInteraction": {
"name": "Add to Wishlist",
"type": "other",
"linkClicks": {
"value": 1
}
}
}
}SQL Queries
Google Analytics 4 (BigQuery)
-- ==========================================
-- 1. GOOGLE BIGQUERY SQL
-- ==========================================
-- Wishlist Additions: confirmed add_to_wishlist events per day (event grain) and saved items with a non-null item_id (item grain),
-- split by save context (product discovery vs cart save-for-later). Intent value uses event_value_in_usd and is never revenue.
-- Add-to-List Rate (discovery context only) = discovery wishlist additions / all view_item events for the same day.
-- Exclusions: internal traffic, staging / QA / preview / dev hostnames, double-fires (same user, same sorted item_id set, within 2 seconds).
-- Known bots are removed by GA4 before export, so no bot filter is needed here.
-- event_date is already in the reporting-property timezone; the last 3 days are flagged provisional.
WITH base AS (
SELECT
PARSE_DATE('%Y%m%d', e.event_date) AS event_date,
e.event_name,
e.user_pseudo_id,
e.event_timestamp,
e.event_value_in_usd,
e.items,
LOWER(COALESCE(e.device.web_info.hostname, '')) AS hostname,
(SELECT ep.value.int_value FROM UNNEST(e.event_params) AS ep WHERE ep.key = 'ga_session_id') AS ga_session_id,
(SELECT ep.value.string_value FROM UNNEST(e.event_params) AS ep WHERE ep.key = 'traffic_type') AS traffic_type,
(SELECT ep.value.string_value FROM UNNEST(e.event_params) AS ep WHERE ep.key = 'item_list_name') AS param_item_list_name,
(SELECT MAX(it.item_list_name) FROM UNNEST(e.items) AS it) AS items_item_list_name,
(SELECT COUNT(1) FROM UNNEST(e.items) AS it WHERE it.item_id IS NOT NULL) AS item_count,
(SELECT STRING_AGG(it.item_id, '|' ORDER BY it.item_id) FROM UNNEST(e.items) AS it WHERE it.item_id IS NOT NULL) AS item_signature
FROM `your-project.analytics_123456789.events_*` AS e
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY))
AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY))
AND e.event_name IN ('add_to_wishlist', 'view_item')
),
filtered AS (
SELECT
b.*,
CASE
WHEN LOWER(COALESCE(b.items_item_list_name, b.param_item_list_name, '')) IN ('cart', 'shopping_cart', 'mini_cart', 'cart_save_for_later')
THEN 'cart_save_for_later'
ELSE 'product_discovery'
END AS save_context
FROM base AS b
WHERE COALESCE(b.traffic_type, '') <> 'internal'
AND NOT REGEXP_CONTAINS(b.hostname, r'^(staging|stage|qa|uat|preview|dev)[.-]|localhost')
),
wishlist_sequenced AS (
SELECT
f.*,
LAG(f.event_timestamp) OVER (
PARTITION BY f.user_pseudo_id, COALESCE(f.item_signature, '(no_items)')
ORDER BY f.event_timestamp
) AS prev_event_timestamp
FROM filtered AS f
WHERE f.event_name = 'add_to_wishlist'
),
wishlist_confirmed AS (
SELECT *
FROM wishlist_sequenced
WHERE prev_event_timestamp IS NULL
OR event_timestamp - prev_event_timestamp > 2000000
),
wishlist_daily AS (
SELECT
event_date,
save_context,
COUNT(*) AS wishlist_additions,
SUM(item_count) AS wishlist_item_additions,
COUNT(DISTINCT CONCAT(user_pseudo_id, '-', CAST(ga_session_id AS STRING))) AS sessions_with_wishlist_addition,
COUNTIF(item_count = 0) AS events_missing_items,
ROUND(SUM(COALESCE(event_value_in_usd, 0)), 2) AS wishlist_intent_value_usd
FROM wishlist_confirmed
GROUP BY event_date, save_context
),
product_views_daily AS (
SELECT
event_date,
COUNT(*) AS view_item_events
FROM filtered
WHERE event_name = 'view_item'
GROUP BY event_date
)
SELECT
w.event_date,
w.save_context,
w.wishlist_additions,
w.wishlist_item_additions,
w.sessions_with_wishlist_addition,
w.wishlist_intent_value_usd,
w.events_missing_items,
SAFE_DIVIDE(w.events_missing_items, w.wishlist_additions) AS missing_items_share,
v.view_item_events,
CASE
WHEN w.save_context = 'product_discovery' THEN SAFE_DIVIDE(w.wishlist_additions, v.view_item_events)
END AS add_to_list_rate,
w.event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY) AS is_provisional
FROM wishlist_daily AS w
LEFT JOIN product_views_daily AS v
ON v.event_date = w.event_date
ORDER BY w.event_date DESC, w.save_context;Adobe Experience Platform (AEP Query Service)
-- ==========================================
-- 2. ADOBE AEP QUERY SERVICE SQL
-- ==========================================
-- Wishlist Additions: confirmed commerce.saveForLaters ExperienceEvents per day (event grain) and saved productListItems with a SKU (item grain),
-- split by save context from eVar1 list name (product discovery vs cart save-for-later).
-- Add-to-List Rate (discovery context only) = discovery wishlist additions / all commerce.productViews events for the same day.
-- Intent value = sum of productListItems.priceTotal converted to USD at the event_date rate from fx_daily_rates_dataset (required input); never added to revenue.
-- Non-USD saves without a rate keep a NULL value (excluded from the sum) and are counted in fx_rate_missing_events.
-- Exclusions: bot / spider user agents, staging / QA / preview / dev hosts, double-fires (same ECID, same SKU set, within 2 seconds).
-- Dates use the reporting timezone; the last 3 days are flagged provisional.
WITH base AS (
SELECT
to_date(from_utc_timestamp(e.timestamp, 'America/New_York')) AS event_date,
e.timestamp AS event_ts,
e.eventType AS event_type,
COALESCE(e.endUserIDs._experience.ecid.id, e.identityMap['ECID'][0].id) AS ecid,
e._experience.analytics.session.num AS visit_num,
lower(COALESCE(e._experience.analytics.customDimensions.eVars.eVar1, '')) AS list_name,
CASE WHEN e.productListItems IS NULL THEN 0
ELSE size(filter(e.productListItems, x -> x.SKU IS NOT NULL)) END AS item_count,
CASE WHEN e.productListItems IS NULL THEN NULL
ELSE NULLIF(array_join(array_sort(transform(filter(e.productListItems, x -> x.SKU IS NOT NULL), x -> x.SKU)), '|'), '') END AS item_signature,
CASE WHEN e.productListItems IS NULL THEN CAST(0 AS DOUBLE)
ELSE aggregate(e.productListItems, CAST(0 AS DOUBLE), (acc, x) -> acc + COALESCE(CAST(x.priceTotal AS DOUBLE), CAST(0 AS DOUBLE))) END AS intent_value_local,
upper(element_at(e.productListItems, 1).currencyCode) AS currency_code
FROM web_experience_events_dataset AS e
WHERE e.timestamp >= date_sub(current_date(), 31)
AND e.timestamp < current_date()
AND (
(e.eventType = 'commerce.saveForLaters' AND COALESCE(e.commerce.saveForLaters.value, 0) > 0)
OR (e.eventType = 'commerce.productViews')
)
AND lower(COALESCE(e.web.webPageDetails.URL, '')) NOT RLIKE '^https?://(staging|stage|qa|uat|preview|dev)[.-]|localhost'
AND lower(COALESCE(e.environment.browserDetails.userAgent, '')) NOT RLIKE 'bot|spider|crawler|headless'
),
classified AS (
SELECT
b.*,
CASE
WHEN b.list_name IN ('cart', 'shopping_cart', 'mini_cart', 'cart_save_for_later') THEN 'cart_save_for_later'
ELSE 'product_discovery'
END AS save_context
FROM base AS b
WHERE b.event_date >= date_sub(current_date(), 30)
),
wishlist_sequenced AS (
SELECT
c.*,
LAG(c.event_ts) OVER (
PARTITION BY c.ecid, COALESCE(c.item_signature, '(no_items)')
ORDER BY c.event_ts
) AS prev_event_ts
FROM classified AS c
WHERE c.event_type = 'commerce.saveForLaters'
),
wishlist_confirmed AS (
SELECT *
FROM wishlist_sequenced
WHERE prev_event_ts IS NULL
OR CAST(event_ts AS DOUBLE) - CAST(prev_event_ts AS DOUBLE) > 2
),
wishlist_daily AS (
SELECT
w.event_date,
w.save_context,
COUNT(*) AS wishlist_additions,
SUM(w.item_count) AS wishlist_item_additions,
COUNT(DISTINCT concat(w.ecid, '-', CAST(w.visit_num AS STRING))) AS visits_with_wishlist_addition,
SUM(CASE WHEN w.item_count = 0 THEN 1 ELSE 0 END) AS events_missing_items,
ROUND(SUM(w.intent_value_local * CASE WHEN w.currency_code = 'USD' THEN CAST(1.0 AS DOUBLE) ELSE fx.rate_to_usd END), 2) AS wishlist_intent_value_usd,
SUM(CASE WHEN COALESCE(w.currency_code, '') <> 'USD' AND fx.rate_to_usd IS NULL THEN 1 ELSE 0 END) AS fx_rate_missing_events
FROM wishlist_confirmed AS w
LEFT JOIN fx_daily_rates_dataset AS fx
ON fx.rate_date = w.event_date
AND upper(fx.currency_code) = w.currency_code
GROUP BY w.event_date, w.save_context
),
product_views_daily AS (
SELECT
event_date,
COUNT(*) AS product_view_events
FROM classified
WHERE event_type = 'commerce.productViews'
GROUP BY event_date
)
SELECT
w.event_date,
w.save_context,
w.wishlist_additions,
w.wishlist_item_additions,
w.visits_with_wishlist_addition,
w.wishlist_intent_value_usd,
w.fx_rate_missing_events,
w.events_missing_items,
CAST(w.events_missing_items AS DOUBLE) / NULLIF(w.wishlist_additions, 0) AS missing_items_share,
v.product_view_events,
CASE
WHEN w.save_context = 'product_discovery'
THEN CAST(w.wishlist_additions AS DOUBLE) / NULLIF(v.product_view_events, 0)
END AS add_to_list_rate,
w.event_date >= date_sub(current_date(), 3) AS is_provisional
FROM wishlist_daily AS w
LEFT JOIN product_views_daily AS v
ON v.event_date = w.event_date
ORDER BY w.event_date DESC, w.save_context;Source Data
Digital Analytics (GA4)
Dependencies
Prerequisite: List the dependencies required for this Metric
Events:
Dimensions:
Report Attributes
Item ID / SKU Item Name Item Category Item Brand Price Band Stock Status Item List Name Device Category Default Channel Grouping New vs Returning User
Recommended Reporting Views
Segment Eligibility
true
Governance
Derived KPIs
Governance
GitHub
catalog/metrics/wishlist-additions.yml