From 593dd30e87bd64f9b8b9b42bfc631482039a480a Mon Sep 17 00:00:00 2001 From: Helen Lin Date: Fri, 14 Jun 2024 11:10:10 -0700 Subject: [PATCH] Fix analytics bugs (#2249) --- .changeset/dirty-humans-remember.md | 7 ++ .../src/useShopifyCookies.test.tsx | 97 +++++++++++++++++++ .../hydrogen-react/src/useShopifyCookies.tsx | 25 ++++- .../src/analytics-manager/CartAnalytics.tsx | 6 +- .../analytics-manager/ShopifyAnalytics.tsx | 3 +- 5 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 .changeset/dirty-humans-remember.md diff --git a/.changeset/dirty-humans-remember.md b/.changeset/dirty-humans-remember.md new file mode 100644 index 0000000000..f937c1f7cf --- /dev/null +++ b/.changeset/dirty-humans-remember.md @@ -0,0 +1,7 @@ +--- +'@shopify/create-hydrogen': patch +'@shopify/hydrogen-react': patch +'@shopify/hydrogen': patch +--- + +Fix shopify cookie creation and add auto top-level domain detection diff --git a/packages/hydrogen-react/src/useShopifyCookies.test.tsx b/packages/hydrogen-react/src/useShopifyCookies.test.tsx index 06a58219ce..cf04dde985 100644 --- a/packages/hydrogen-react/src/useShopifyCookies.test.tsx +++ b/packages/hydrogen-react/src/useShopifyCookies.test.tsx @@ -238,4 +238,101 @@ describe(`useShopifyCookies`, () => { expect(Object.keys(cookieJar).length).toBe(0); }); + + it('sets domain to top level domain when checkoutDomain is supplied', () => { + const cookieJar: MockCookieJar = mockCookie(); + const domain = 'myshop.com'; + const checkoutDomain = 'checkout.myshop.com'; + + renderHook(() => + useShopifyCookies({hasUserConsent: true, domain, checkoutDomain}), + ); + + const cookies = getShopifyCookies(document.cookie); + + expect(cookies).toEqual({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + _shopify_s: expect.any(String), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + _shopify_y: expect.any(String), + }); + expect(cookies['_shopify_s']).not.toBe(''); + expect(cookies['_shopify_y']).not.toBe(''); + + expect(cookieJar['_shopify_s'].value).not.toBe( + cookieJar['_shopify_y'].value, + ); + expect(cookieJar['_shopify_s']).toMatchObject({ + domain: `.myshop.com`, + maxage: 1800, + }); + expect(cookieJar['_shopify_y']).toMatchObject({ + domain: `.myshop.com`, + maxage: 31104000, + }); + }); + + it('sets domain to top level domain when domain and checkoutDomain are both subdomains', () => { + const cookieJar: MockCookieJar = mockCookie(); + const domain = 'ca.myshop.com'; + const checkoutDomain = 'checkout.myshop.com'; + + renderHook(() => + useShopifyCookies({hasUserConsent: true, domain, checkoutDomain}), + ); + + const cookies = getShopifyCookies(document.cookie); + + expect(cookies).toEqual({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + _shopify_s: expect.any(String), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + _shopify_y: expect.any(String), + }); + expect(cookies['_shopify_s']).not.toBe(''); + expect(cookies['_shopify_y']).not.toBe(''); + + expect(cookieJar['_shopify_s'].value).not.toBe( + cookieJar['_shopify_y'].value, + ); + expect(cookieJar['_shopify_s']).toMatchObject({ + domain: `.myshop.com`, + maxage: 1800, + }); + expect(cookieJar['_shopify_y']).toMatchObject({ + domain: `.myshop.com`, + maxage: 31104000, + }); + }); + + it('does not set domain on localhost if checkoutDomain is supplied', () => { + const cookieJar: MockCookieJar = mockCookie(); + const domain = 'localhost:3000'; + const checkoutDomain = 'checkout.myshop.com'; + + renderHook(() => + useShopifyCookies({hasUserConsent: true, domain, checkoutDomain}), + ); + + const cookies = getShopifyCookies(document.cookie); + + expect(cookies).toEqual({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + _shopify_s: expect.any(String), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + _shopify_y: expect.any(String), + }); + expect(cookies['_shopify_s']).not.toBe(''); + expect(cookies['_shopify_y']).not.toBe(''); + + expect(cookieJar['_shopify_s'].value).not.toBe( + cookieJar['_shopify_y'].value, + ); + expect(cookieJar['_shopify_s']).toMatchObject({ + maxage: 1800, + }); + expect(cookieJar['_shopify_y']).toMatchObject({ + maxage: 31104000, + }); + }); }); diff --git a/packages/hydrogen-react/src/useShopifyCookies.tsx b/packages/hydrogen-react/src/useShopifyCookies.tsx index 187819fd71..69046e62a3 100644 --- a/packages/hydrogen-react/src/useShopifyCookies.tsx +++ b/packages/hydrogen-react/src/useShopifyCookies.tsx @@ -17,10 +17,18 @@ type UseShopifyCookiesOptions = { * The domain scope of the cookie. Defaults to empty string. **/ domain?: string; + /** + * The checkout domain of the shop. Defaults to empty string. If set, the cookie domain will check if it can be set with the checkout domain. + */ + checkoutDomain?: string; }; export function useShopifyCookies(options?: UseShopifyCookiesOptions): void { - const {hasUserConsent = false, domain = ''} = options || {}; + const { + hasUserConsent = false, + domain = '', + checkoutDomain = '', + } = options || {}; useEffect(() => { const cookies = getShopifyCookies(document.cookie); @@ -34,6 +42,19 @@ export function useShopifyCookies(options?: UseShopifyCookiesOptions): void { // Use override domain or current host let currentDomain = domain || window.document.location.host; + if (checkoutDomain) { + const checkoutDomainParts = checkoutDomain.split('.').reverse(); + const currentDomainParts = currentDomain.split('.').reverse(); + const sameDomainParts: Array = []; + checkoutDomainParts.forEach((part, index) => { + if (part === currentDomainParts[index]) { + sameDomainParts.push(part); + } + }); + + currentDomain = sameDomainParts.reverse().join('.'); + } + // Reset domain if localhost if (/^localhost/.test(currentDomain)) currentDomain = ''; @@ -64,7 +85,7 @@ export function useShopifyCookies(options?: UseShopifyCookiesOptions): void { setCookie(SHOPIFY_Y, '', 0, domainWithLeadingDot); setCookie(SHOPIFY_S, '', 0, domainWithLeadingDot); } - }, [options, hasUserConsent, domain]); + }, [options, hasUserConsent, domain, checkoutDomain]); } function setCookie( diff --git a/packages/hydrogen/src/analytics-manager/CartAnalytics.tsx b/packages/hydrogen/src/analytics-manager/CartAnalytics.tsx index 6ef4a73412..6e713f5bb5 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] Unable to set up cart analytics events: ${fieldName} is missing.`, + `[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.`, ); } @@ -35,11 +35,11 @@ export function CartAnalytics({ Promise.resolve(currentCart).then((updatedCart) => { if (updatedCart && updatedCart.lines) { if (!updatedCart.id) { - logMissingField('cart.id'); + logMissingField('id'); return; } if (!updatedCart.updatedAt) { - logMissingField('cart.updatedAt'); + logMissingField('updatedAt'); return; } } diff --git a/packages/hydrogen/src/analytics-manager/ShopifyAnalytics.tsx b/packages/hydrogen/src/analytics-manager/ShopifyAnalytics.tsx index e1c32d0eb4..86298d5f20 100644 --- a/packages/hydrogen/src/analytics-manager/ShopifyAnalytics.tsx +++ b/packages/hydrogen/src/analytics-manager/ShopifyAnalytics.tsx @@ -93,8 +93,9 @@ export function ShopifyAnalytics({ }); useShopifyCookies({ - hasUserConsent: canTrack(), + hasUserConsent: shopifyReady && privacyReady ? canTrack() : true, domain, + checkoutDomain, }); useEffect(() => {