diff --git a/packages/hydrogen-react/src/analytics-schema-custom-storefront-customer-tracking.test.ts b/packages/hydrogen-react/src/analytics-schema-custom-storefront-customer-tracking.test.ts index f5ecd189b3..dc63d16464 100644 --- a/packages/hydrogen-react/src/analytics-schema-custom-storefront-customer-tracking.test.ts +++ b/packages/hydrogen-react/src/analytics-schema-custom-storefront-customer-tracking.test.ts @@ -15,6 +15,7 @@ import type { ShopifyPageViewPayload, } from './analytics-types.js'; import {version} from '../package.json'; +import {getProductsValue, getProductValue} from './analytics-utils.js'; describe(`analytics schema - custom storefront customer tracking`, () => { describe('page view', () => { @@ -141,7 +142,7 @@ describe(`analytics schema - custom storefront customer tracking`, () => { const pageViewPayload = { ...BASE_PAYLOAD, pageType: 'product', - totalValue: 100, + totalValue: getProductsValue(BASE_PAYLOAD.products), }; const events = pageView(pageViewPayload); @@ -169,7 +170,7 @@ describe(`analytics schema - custom storefront customer tracking`, () => { ...BASE_PAYLOAD, pageType: 'product', products: [productPayload], - totalValue: 100, + totalValue: getProductValue(productPayload), }; const events = pageView(pageViewPayload); @@ -184,7 +185,6 @@ describe(`analytics schema - custom storefront customer tracking`, () => { expect(events[1]).toEqual( getExpectedPayload(pageViewPayload, { event_name: 'product_page_rendered', - total_value: pageViewPayload.totalValue, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment products: expect.anything(), canonical_url: pageViewPayload.url, @@ -217,7 +217,7 @@ describe(`analytics schema - custom storefront customer tracking`, () => { ...BASE_PAYLOAD, pageType: 'product', products: [productPayload], - totalValue: 100, + totalValue: getProductValue(productPayload), }; const events = pageView(pageViewPayload); @@ -293,7 +293,7 @@ describe(`analytics schema - custom storefront customer tracking`, () => { ...BASE_PAYLOAD, cartId: 'gid://shopify/Cart/abc123', products: [productPayload], - totalValue: 100, + totalValue: getProductValue(productPayload), }; const events = addToCart(addToCartPayload); @@ -303,7 +303,6 @@ describe(`analytics schema - custom storefront customer tracking`, () => { getExpectedPayload(addToCartPayload, { event_name: 'product_added_to_cart', cart_token: 'abc123', - total_value: addToCartPayload.totalValue, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment products: expect.anything(), }), @@ -322,6 +321,45 @@ describe(`analytics schema - custom storefront customer tracking`, () => { price: parseFloat(productPayload.price), }); }); + + it(`with base product payload quantity 1`, () => { + const productPayload = { + ...BASE_PRODUCT_PAYLOAD, + quantity: 1, + }; + const addToCartPayload = { + ...BASE_PAYLOAD, + cartId: 'gid://shopify/Cart/abc123', + products: [productPayload], + totalValue: getProductValue(productPayload), + }; + const events = addToCart(addToCartPayload); + + expectType(events); + expect(events.length).toBe(1); + expect(events[0]).toEqual( + getExpectedPayload(addToCartPayload, { + event_name: 'product_added_to_cart', + cart_token: 'abc123', + total_value: addToCartPayload.totalValue, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + products: expect.anything(), + }), + ); + const productEventPayload = events[0].payload; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const product = JSON.parse( + (productEventPayload.products && productEventPayload.products[0]) || + '{}', + ); + expect(product).toEqual({ + ...getForwardedProductPayload(productPayload), + variant: '', + quantity: 1, + product_id: 1, + price: parseFloat(productPayload.price), + }); + }); }); }); diff --git a/packages/hydrogen-react/src/analytics-utils.test.ts b/packages/hydrogen-react/src/analytics-utils.test.ts index c718187bc1..03f7844115 100644 --- a/packages/hydrogen-react/src/analytics-utils.test.ts +++ b/packages/hydrogen-react/src/analytics-utils.test.ts @@ -1,5 +1,18 @@ import {describe, it, expect} from 'vitest'; -import {parseGid, addDataIf, schemaWrapper} from './analytics-utils.js'; +import { + parseGid, + addDataIf, + schemaWrapper, + getProductsValue, + randomNatural, + getProductValue, +} from './analytics-utils.js'; +import {ShopifyAnalyticsProduct} from './analytics-types.js'; +import {expectType} from 'ts-expect'; +import { + BASE_PAYLOAD, + BASE_PRODUCT_PAYLOAD, +} from './analytics-schema.test.helpers.js'; describe('analytic-utils', () => { describe('parseGid', () => { @@ -151,4 +164,91 @@ describe('analytic-utils', () => { }); }); }); + + describe(`getProductValue`, () => { + it(`gets single quantity product value`, () => { + const productPayload = { + ...BASE_PRODUCT_PAYLOAD, + quantity: 1, + }; + const calculatedValue = getProductValue(productPayload); + + expectType(productPayload); + expect(calculatedValue).toEqual(parseFloat(productPayload.price)); + }); + + it(`gets arbitrary quantity product value`, () => { + const productPayload = { + ...BASE_PRODUCT_PAYLOAD, + quantity: randomNatural(), + }; + const calculatedValue = getProductValue(productPayload); + + expectType(productPayload); + expect(calculatedValue).toBeCloseTo( + parseFloat(productPayload.price) * productPayload.quantity, + ); + }); + }); + + describe(`getProductsValue`, () => { + it(`gets singleton products value`, () => { + const productPayload = { + ...BASE_PRODUCT_PAYLOAD, + quantity: randomNatural(), + }; + const productsPayload = [productPayload]; + const addToCartPayload = { + ...BASE_PAYLOAD, + cartId: 'gid://shopify/Cart/abc123', + products: productsPayload, + totalValue: getProductsValue(productsPayload), + }; + + expectType(productPayload); + expect(addToCartPayload.totalValue).toBeCloseTo( + parseFloat(productPayload.price) * productPayload.quantity, + ); + }); + + it(`gets tuple products value`, () => { + const productPayload = { + ...BASE_PRODUCT_PAYLOAD, + quantity: randomNatural(), + }; + const productsPayload = [productPayload, productPayload]; + const addToCartPayload = { + ...BASE_PAYLOAD, + cartId: 'gid://shopify/Cart/abc123', + products: productsPayload, + totalValue: getProductsValue(productsPayload), + }; + + expectType(productPayload); + expect(addToCartPayload.totalValue).toBeCloseTo( + parseFloat(productPayload.price) * productPayload.quantity * 2, + ); + }); + + it(`gets N products value`, () => { + const productPayload = { + ...BASE_PRODUCT_PAYLOAD, + quantity: randomNatural(), + }; + const productsPayload = new Array(randomNatural()).fill(productPayload); + const addToCartPayload = { + ...BASE_PAYLOAD, + cartId: 'gid://shopify/Cart/abc123', + products: productsPayload, + totalValue: getProductsValue(productsPayload), + }; + + expectType(productPayload); + expect(addToCartPayload.totalValue).toBeCloseTo( + parseFloat(productPayload.price) * + productPayload.quantity * + productsPayload.length, + ); + }); + }); }); diff --git a/packages/hydrogen-react/src/analytics-utils.ts b/packages/hydrogen-react/src/analytics-utils.ts index 615fe4201a..f979ba9e0e 100644 --- a/packages/hydrogen-react/src/analytics-utils.ts +++ b/packages/hydrogen-react/src/analytics-utils.ts @@ -2,7 +2,9 @@ import type { ShopifyMonorailPayload, ShopifyMonorailEvent, ShopifyGid, + ShopifyAnalyticsProduct, } from './analytics-types.js'; +import {faker} from '@faker-js/faker'; /** * Builds a Shopify Monorail event from a Shopify Monorail payload and a schema ID. @@ -106,3 +108,34 @@ export function errorIfServer(fnName: string): boolean { } return false; } + +/** + * Get a random number [1,1000] + * @returns A random number + */ +export function randomNatural() { + return faker.number.int({min: 1, max: 1000}); +} + +/** + * Calculate product price * quantity + * @param product - The product + * @returns A number + */ +export const getProductValue = (product: ShopifyAnalyticsProduct): number => + parseFloat(product.price) * (product.quantity || 0); + +/** + * Reduce all products and get their total value + * @param products - The products + * @returns A number + */ +export function getProductsValue( + products?: ShopifyAnalyticsProduct[], +): number | undefined { + return products?.reduce( + (previousValue, currentProduct) => + previousValue + getProductValue(currentProduct), + 0, + ); +}