Skip to content
This repository was archived by the owner on Mar 3, 2023. It is now read-only.
Merged
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
9 changes: 0 additions & 9 deletions packages/react/src/analytics-errors-utils.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ import {
ShopifyAnalyticsProduct,
} from './analytics-types.js';
import {AnalyticsPageType, ShopifyAppSource} from './analytics-constants.js';
import {
addDataIf,
schemaWrapper,
stripGId,
stripId,
} from './analytics-utils.js';
import {addDataIf, schemaWrapper, parseGid} from './analytics-utils.js';
import {buildUUID} from './cookies-utils.js';

const SCHEMA_ID = 'custom_storefront_customer_tracking/1.0';
Expand Down Expand Up @@ -101,14 +96,16 @@ export function addToCart(
payload: ShopifyAnalyticsPayload
): ShopifyMonorailPayload[] {
const addToCartPayload = payload as ShopifyAddToCartPayload;
const cartToken = parseGid(addToCartPayload.cartId);
const cart_token = cartToken?.id ? `${cartToken.id}` : null;
return [
schemaWrapper(
SCHEMA_ID,
addDataIf(
{
event_name: PRODUCT_ADDED_TO_CART_EVENT_NAME,
customerId: addToCartPayload.customerId,
cart_token: stripId(addToCartPayload.cartId),
cart_token,
total_value: addToCartPayload.totalValue,
products: formatProductPayload(addToCartPayload.products),
},
Expand All @@ -121,6 +118,10 @@ export function addToCart(
function formatPayload(
payload: ShopifyAnalyticsPayload
): ShopifyMonorailPayload {
const shop_id =
typeof payload.shopId === 'string'
? parseGid(payload.shopId).id
: payload.shopId;
return {
source: payload.shopifyAppSource || ShopifyAppSource.headless,
hydrogenSubchannelId: payload.storefrontId || '0',
Expand All @@ -138,7 +139,7 @@ function formatPayload(
navigation_type: payload.navigationType,
navigation_api: payload.navigationApi,

shop_id: stripGId(payload.shopId),
shop_id,
currency: payload.currency,
};
}
Expand All @@ -151,8 +152,8 @@ function formatProductPayload(products?: ShopifyAnalyticsProduct[]) {
variant_gid: p.variantGid,
category: p.category,
sku: p.sku,
product_id: stripGId(p.productGid),
variant_id: stripGId(p.variantGid),
product_id: parseGid(p.product_gid).id,
variant_id: parseGid(p.variant_gid).id,
},
{
product_gid: p.productGid,
Expand Down
26 changes: 15 additions & 11 deletions packages/react/src/analytics-schema-trekkie-storefront-page-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,27 @@ import {
ShopifyMonorailPayload,
} from './analytics-types.js';
import {ShopifyAppId} from './analytics-constants.js';
import {
addDataIf,
schemaWrapper,
stripGId,
getResourceType,
} from './analytics-utils.js';
import {addDataIf, schemaWrapper, parseGid} from './analytics-utils.js';
import {buildUUID} from './cookies-utils.js';

const SCHEMA_ID = 'trekkie_storefront_page_view/1.4';
const oxygenDomain = 'myshopify.dev';
const OXYGEN_DOMAIN = 'myshopify.dev';

export function pageView(
payload: ShopifyAnalyticsPayload
): ShopifyMonorailPayload[] {
const pageViewPayload = payload as ShopifyPageViewPayload;
const {id, resource} = parseGid(pageViewPayload.resourceId);
const resourceType = resource ? resource.toLowerCase() : undefined;
return [
schemaWrapper(
SCHEMA_ID,
addDataIf(
{
pageType: pageViewPayload.pageType,
customerId: pageViewPayload.customerId,
resourceType: getResourceType(pageViewPayload.resourceId),
resourceId: stripGId(pageViewPayload.resourceId),
resourceType,
resourceId: id,
},
formatPayload(pageViewPayload)
)
Expand All @@ -38,6 +35,10 @@ export function pageView(
function formatPayload(
payload: ShopifyAnalyticsPayload
): ShopifyMonorailPayload {
const shopId =
typeof payload.shopId === 'string'
? parseGid(payload.shopId).id
: payload.shopId;
return {
appClientId: payload.shopifyAppSource
? ShopifyAppId[payload.shopifyAppSource]
Expand All @@ -57,15 +58,18 @@ function formatPayload(
referrer: payload.referrer,
title: payload.title,

shopId: stripGId(payload.shopId),
shopId,
currency: payload.currency,
contentLanguage: payload.acceptedLanguage || 'en',
};
}

function isMerchantRequest(url: string): boolean {
if (typeof url !== 'string') {
return false;
}
const hostname = new URL(url).hostname;
if (hostname.indexOf(oxygenDomain) !== -1 || hostname === 'localhost') {
if (hostname.indexOf(OXYGEN_DOMAIN) !== -1 || hostname === 'localhost') {
return true;
}
return false;
Expand Down
108 changes: 108 additions & 0 deletions packages/react/src/analytics-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {parseGid, addDataIf, schemaWrapper} from './analytics-utils.js';

describe('analytic-utils', () => {
describe('parseGid', () => {
it('returns the id and resource type from a gid', () => {
const {id, resource} = parseGid('gid://shopify/Order/123');
expect(id).toBe(123);
expect(resource).toBe('Order');
});

it('returns null if the gid is not a string', () => {
//@ts-expect-error - testing invalid input
const {id, resource} = parseGid(123);
expect(id).toBe(null);
expect(resource).toBe(null);
});

it('returns null if the gid is not a valid gid', () => {
const {id, resource} = parseGid('gid://shopify/Order');
expect(id).toBe(null);
expect(resource).toBe(null);
});

it('returns the id and resource type from a gid with a query string', () => {
const {id, resource} = parseGid('gid://shopify/Order/123?namespace=123');
expect(id).toBe(123);
expect(resource).toBe('Order');
});

it('returns the id and resource type from a gid with a query string and a fragment', () => {
const {id, resource} = parseGid(
'gid://shopify/Order/123?namespace=123#fragment'
);
expect(id).toBe(123);
expect(resource).toBe('Order');
});

it('returns null if the resource is missing', () => {
const {id, resource} = parseGid('gid://shopify//123');
expect(id).toBe(null);
expect(resource).toBe(null);
});
});

describe('addDataIf', () => {
it('adds the key value pair when the value is truthy', () => {
const data = {foo: 'bar'};
const formattedData = {};

expect(addDataIf(data, formattedData)).toEqual({foo: 'bar'});
});

it('does not add the key value pair when the value is falsy', () => {
const data = {foo: null, bazz: ''};
const formattedData = {};

expect(addDataIf(data, formattedData)).toEqual({});
});

it('does not add the key value pair when the value is an empty string', () => {
const data = {foo: ''};
const formattedData = {};

expect(addDataIf(data, formattedData)).toEqual({});
});

it('returns and empty object if the key value pairs are not an object', () => {
const data = 'foo';
const formattedData = {};

//@ts-expect-error passing-and-invalid-type-for-testing
expect(addDataIf(data, formattedData)).toEqual({});
});
});

describe('schemaWrapper', () => {
it('returns a Shopify Monorail payload from a Shopify Analytics payload and a schema ID', () => {
const payload = {foo: 'bar'};
const schemaId = '123';

expect(schemaWrapper(schemaId, payload)).toEqual({
schema_id: '123',
payload: {foo: 'bar'},
metadata: {event_created_at_ms: expect.any(Number)},
});
});

it('throws an error if the schema ID is not a string', () => {
const payload = {foo: 'bar'};
const schemaId = 123;

//@ts-expect-error passing-and-invalid-type-for-testing
expect(() => schemaWrapper(schemaId, payload)).toThrow(
'`schemaId` must be a string'
);
});

it('throws an error if the payload is not an object', () => {
const payload = 'foo';
const schemaId = '123';

//@ts-expect-error passing-and-invalid-type-for-testing
expect(() => schemaWrapper(schemaId, payload)).toThrow(
'`payload` must be an object'
);
});
});
});
72 changes: 59 additions & 13 deletions packages/react/src/analytics-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import type {ShopifyMonorailPayload} from './analytics-types.js';

export function schemaWrapper(schemaId: string, payload: unknown) {
/**
* Builds a Shopify Monorail payload from a Shopify Analytics payload and a schema ID.
* @param payload - The payload to format
* @param schemaId - The schema ID to use
* @returns The formatted payload
**/
export function schemaWrapper(schemaId: string, payload: object) {
if (typeof schemaId !== 'string') {
throw new Error('`schemaId` must be a string');
}
if (typeof payload !== 'object' || payload === null) {
throw new Error('`payload` must be an object');
}
return {
schema_id: schemaId,
payload,
Expand All @@ -10,19 +22,45 @@ export function schemaWrapper(schemaId: string, payload: unknown) {
};
}

export function stripGId(text: number | string | undefined): number {
if (typeof text === 'number') return text;
return parseInt(stripId(text || ''));
}

export function stripId(text = ''): string {
return text.substring(text.lastIndexOf('/') + 1);
/**
* Parses global id (gid) and returns the resource type and id.
* @see https://shopify.dev/api/usage/gids
* @param gid - A shopify GID (string)
* @returns \{ id: number, resource: string \}
*
* @example
* ```ts
* const {id, resource} = parseGid('gid://shopify/Order/123')
* // => id = 123, resource = 'Order'
* ```
**/
export function parseGid(gid: string | undefined): {
id: number | null;
resource: string | null;
} {
if (typeof gid !== 'string') return {id: null, resource: null};
const matches = gid.match(/^gid:\/\/.hopify\/(\w+)\/(\d+)/);
if (!matches || matches.length === 1) {
return {id: null, resource: null};
}
const id = matches[2] ? parseInt(matches[2], 10) : null;
const resource = matches[1] ? matches[1] : null;
return {id, resource};
}

/**
* Filters properties from an object and returns a new object with only the properties that have a truthy value.
* @param keyValuePairs - An object of key-value pairs
* @param formattedData - An object which will hold the truthy values
* @returns The formatted object
**/
export function addDataIf(
keyValuePairs: ShopifyMonorailPayload,
formattedData: ShopifyMonorailPayload
): ShopifyMonorailPayload {
if (typeof keyValuePairs !== 'object') {
return {};
}
Object.entries(keyValuePairs).forEach(([key, value]) => {
if (value) {
formattedData[key] = value;
Expand All @@ -31,9 +69,17 @@ export function addDataIf(
return formattedData;
}

export function getResourceType(text = ''): string {
return text
.substring(0, text.lastIndexOf('/'))
.replace(/.*shopify\//, '')
.toLowerCase();
/**
* Utility that errors if a function is called on the server.
* @param fnName - The name of the function
* @returns A boolean
**/
export function errorIfServer(fnName: string): boolean {
if (!window) {
console.error(
`${fnName} should only be used within the useEffect callback or event handlers`
);
return true;
}
return false;
}
3 changes: 1 addition & 2 deletions packages/react/src/analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
ShopifyMonorailPayload,
} from './analytics-types.js';
import {AnalyticsEventName} from './analytics-constants.js';
import {errorIfServer} from './analytics-errors-utils.js';
import {errorIfServer} from './analytics-utils.js';
import {getShopifyCookies} from './cookies-utils.js';

import * as TrekkieStorefrontPageView from './analytics-schema-trekkie-storefront-page-view.js';
Expand Down Expand Up @@ -128,7 +128,6 @@ function getNavigationTypeLegacy() {
performance?.navigation?.type !== undefined
) {
// https://developer.mozilla.org/en-US/docs/Web/API/Performance/navigation

const rawType = performance.navigation.type;
switch (rawType) {
case PerformanceNavigation.TYPE_NAVIGATE:
Expand Down