Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand All @@ -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,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

total_value is actually expected to not exist when product quantity is 0 or undefined:

addDataIf(
{
event_name: PRODUCT_ADDED_TO_CART_EVENT_NAME,
customerId: addToCartPayload.customerId,
cart_token: cartToken?.id ? `${cartToken.id}` : null,
total_value: addToCartPayload.totalValue,

^ removes total_value if totalValue is 0/undefined.

This is test case where totalValue is 0, since all products here are quantity undefined. products array is derived from BASE_PRODUCT_PAYLOAD, which does not define quantity.

I add a test below to test a payload where products has a product with quantity 1.

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
products: expect.anything(),
canonical_url: pageViewPayload.url,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand All @@ -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(),
}),
Expand All @@ -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<ShopifyMonorailPayload[]>(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),
});
});
});
});

Expand Down
102 changes: 101 additions & 1 deletion packages/hydrogen-react/src/analytics-utils.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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<ShopifyAnalyticsProduct>(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<ShopifyAnalyticsProduct>(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<ShopifyAnalyticsProduct>(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<ShopifyAnalyticsProduct>(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<ShopifyAnalyticsProduct>(productPayload);
expect(addToCartPayload.totalValue).toBeCloseTo(
parseFloat(productPayload.price) *
productPayload.quantity *
productsPayload.length,
);
});
});
});
33 changes: 33 additions & 0 deletions packages/hydrogen-react/src/analytics-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
);
}