diff --git a/.changeset/stupid-falcons-wash.md b/.changeset/stupid-falcons-wash.md new file mode 100644 index 0000000000..d4410c5a55 --- /dev/null +++ b/.changeset/stupid-falcons-wash.md @@ -0,0 +1,5 @@ +--- +'@shopify/hydrogen': patch +--- + +Auto cookie domain detection for customer privacy api and better error message for missing analytics fields diff --git a/packages/hydrogen/src/analytics-manager/CartAnalytics.tsx b/packages/hydrogen/src/analytics-manager/CartAnalytics.tsx index 6e713f5bb5..fad620b48f 100644 --- a/packages/hydrogen/src/analytics-manager/CartAnalytics.tsx +++ b/packages/hydrogen/src/analytics-manager/CartAnalytics.tsx @@ -10,7 +10,7 @@ import {flattenConnection} from '@shopify/hydrogen-react'; function logMissingField(fieldName: string) { // eslint-disable-next-line no-console console.error( - `[h2:error:CartAnalytics] Can't set up cart analytics events because the \`cart.${fieldName}\` value is missing from your GraphQL cart query. In standard Hydrogen projects, the cart query is contained in \`app/lib/fragments.js\`. Make sure it includes \`cart.${fieldName}\`. Check the Hydrogen Skeleton template for reference: https://github.com/Shopify/hydrogen/blob/main/templates/skeleton/app/lib/fragments.ts#L59.`, + `[h2:error:CartAnalytics] Can't set up cart analytics events because the \`cart.${fieldName}\` value is missing from your GraphQL cart query. In your project, search for where \`fragment CartApiQuery on Cart\` is defined and make sure \`${fieldName}\` is part of your cart query. Check the Hydrogen Skeleton template for reference: https://github.com/Shopify/hydrogen/blob/main/templates/skeleton/app/lib/fragments.ts#L59.`, ); } diff --git a/packages/hydrogen/src/analytics-manager/ShopifyAnalytics.tsx b/packages/hydrogen/src/analytics-manager/ShopifyAnalytics.tsx index 86298d5f20..62cf59e1eb 100644 --- a/packages/hydrogen/src/analytics-manager/ShopifyAnalytics.tsx +++ b/packages/hydrogen/src/analytics-manager/ShopifyAnalytics.tsx @@ -208,10 +208,7 @@ function productViewHandler(payload: ProductViewPayload) { if ( eventPayload && validateProducts({ - eventName: PRODUCT_VIEWED, - productField: 'products', - variantField: 'product.', - fromSource: 'product_viewed products array', + type: 'product', products: payload.products, }) ) { @@ -318,10 +315,7 @@ function sendCartAnalytics({ }; if ( validateProducts({ - eventName: ADD_TO_CART, - productField: 'merchandise.product', - variantField: 'merchandise', - fromSource: 'cart query', + type: 'cart', products: [product], }) ) { @@ -335,17 +329,27 @@ function sendCartAnalytics({ } } -const PRODUCT_VIEWED = 'Product viewed'; -const ADD_TO_CART = 'Add to cart'; function missingErrorMessage( - eventName: string, - missingFieldName: string, - fromSource: string, + type: 'cart' | 'product', + fieldName: string, + isVariantField: boolean, + viewKeyName?: string, ) { - // eslint-disable-next-line no-console - console.error( - `[h2:error:ShopifyAnalytics] ${eventName}: ${missingFieldName} is required from the ${fromSource}.`, - ); + if (type === 'cart') { + const name = `${ + isVariantField ? 'merchandise' : 'merchandise.product' + }.${fieldName}`; + // eslint-disable-next-line no-console + console.error( + `[h2:error:ShopifyAnalytics] Can't set up cart analytics events because the \`cart.lines[].${name}\` value is missing from your GraphQL cart query. In your project, search for where \`fragment CartLine on CartLine\` is defined and make sure \`${name}\` is part of your cart query. Check the Hydrogen Skeleton template for reference: https://github.com/Shopify/hydrogen/blob/main/templates/skeleton/app/lib/fragments.ts#L25-L56.`, + ); + } else { + const name = `${viewKeyName || fieldName}`; + // eslint-disable-next-line no-console + console.error( + `[h2:error:ShopifyAnalytics] Can't set up product view analytics events because the \`${name}\` is missing from your \`\`. Make sure \`${name}\` is part of your products data prop. Check the Hydrogen Skeleton template for reference: https://github.com/Shopify/hydrogen/blob/main/templates/skeleton/app/routes/products.%24handle.tsx#L159-L165.`, + ); + } } // Product expected field and types: @@ -360,50 +364,40 @@ function missingErrorMessage( // category: string, optional // quantity: float function validateProducts({ - eventName, - productField, - variantField, + type, products, - fromSource, }: { - eventName: string; - productField: string; - variantField: string; - fromSource: string; + type: 'cart' | 'product'; products: Array>; }) { if (!products || products.length === 0) { - missingErrorMessage(eventName, `${productField}`, fromSource); + missingErrorMessage(type, '', false, 'data.products'); return false; } products.forEach((product) => { if (!product.id) { - missingErrorMessage(eventName, `${productField}.id`, fromSource); + missingErrorMessage(type, 'id', false); return false; } if (!product.title) { - missingErrorMessage(eventName, `${productField}.title`, fromSource); + missingErrorMessage(type, 'title', false); return false; } if (!product.price) { - missingErrorMessage( - eventName, - `${variantField}.price.amount`, - fromSource, - ); + missingErrorMessage(type, 'price.amount', true, 'price'); return false; } if (!product.vendor) { - missingErrorMessage(eventName, `${productField}.vendor`, fromSource); + missingErrorMessage(type, 'vendor', false); return false; } if (!product.variantId) { - missingErrorMessage(eventName, `${variantField}.id`, fromSource); + missingErrorMessage(type, 'id', true, 'variantId'); return false; } if (!product.variantTitle) { - missingErrorMessage(eventName, `${variantField}.title`, fromSource); + missingErrorMessage(type, 'title', true, 'variantTitle'); return false; } }); diff --git a/packages/hydrogen/src/customer-privacy/ShopifyCustomerPrivacy.tsx b/packages/hydrogen/src/customer-privacy/ShopifyCustomerPrivacy.tsx index a396f2e93e..88ab65cb1c 100644 --- a/packages/hydrogen/src/customer-privacy/ShopifyCustomerPrivacy.tsx +++ b/packages/hydrogen/src/customer-privacy/ShopifyCustomerPrivacy.tsx @@ -143,14 +143,14 @@ export function useCustomerPrivacy(props: CustomerPrivacyApiProps) { if (scriptStatus !== 'done' || loadedEvent.current) return; loadedEvent.current = true; - if (!consentConfig.checkoutDomain) logMissingConfig('checkoutDomain'); - if (!consentConfig.storefrontAccessToken) - logMissingConfig('storefrontAccessToken'); + const {checkoutDomain, storefrontAccessToken} = consentConfig; + if (!checkoutDomain) logMissingConfig('checkoutDomain'); + if (!storefrontAccessToken) logMissingConfig('storefrontAccessToken'); // validate that the storefront access token is not a server API token if ( - consentConfig.storefrontAccessToken.startsWith('shpat_') || - consentConfig.storefrontAccessToken.length !== 32 + storefrontAccessToken.startsWith('shpat_') || + storefrontAccessToken.length !== 32 ) { // eslint-disable-next-line no-console console.error( @@ -158,11 +158,31 @@ export function useCustomerPrivacy(props: CustomerPrivacyApiProps) { ); } - if (withPrivacyBanner && window?.privacyBanner) { - window?.privacyBanner?.loadBanner({ - checkoutRootDomain: consentConfig.checkoutDomain, - storefrontAccessToken: consentConfig.storefrontAccessToken, + const config: CustomerPrivacyConsentConfig = { + checkoutRootDomain: checkoutDomain, + storefrontAccessToken, + }; + + if (checkoutDomain) { + let storefrontRootDomain = window.document.location.host; + const checkoutDomainParts = checkoutDomain.split('.').reverse(); + const currentDomainParts = storefrontRootDomain.split('.').reverse(); + const sameDomainParts: Array = []; + checkoutDomainParts.forEach((part, index) => { + if (part === currentDomainParts[index]) { + sameDomainParts.push(part); + } }); + + storefrontRootDomain = sameDomainParts.reverse().join('.'); + + if (storefrontRootDomain) { + config.storefrontRootDomain = storefrontRootDomain; + } + } + + if (withPrivacyBanner && window?.privacyBanner) { + window.privacyBanner?.loadBanner(config); } if (!window.Shopify?.customerPrivacy) return; @@ -179,8 +199,7 @@ export function useCustomerPrivacy(props: CustomerPrivacyApiProps) { { ...consent, headlessStorefront: true, - checkoutRootDomain: consentConfig.checkoutDomain, - storefrontAccessToken: consentConfig.storefrontAccessToken, + ...config, }, callback, );