From 8c76c8a7f596bebba0ff8542881fb85350c21d7f Mon Sep 17 00:00:00 2001 From: Thomas Cristina de Carvalho Date: Wed, 15 May 2024 17:46:25 -0400 Subject: [PATCH] Migrate to new Analytics Add UNSTABLE_Analytics and remove useAnalytics hook Add PUBLIC_CHECKOUT_DOMAIN --- .env.template | 1 + app/components/CustomAnalytics.tsx | 32 ++++ app/components/product/AddToCartForm.tsx | 151 ++++++------------ app/entry.server.tsx | 1 + app/graphql/fragments.ts | 2 + app/hooks/useAnalytics.tsx | 67 -------- app/lib/env.server.ts | 6 +- app/root.tsx | 38 +++-- app/routes/($locale).$.tsx | 6 +- app/routes/($locale).cart.tsx | 7 +- ...$locale).collections.$collectionHandle.tsx | 40 +++-- .../($locale).products.$productHandle.tsx | 32 ++-- app/routes/_index.tsx | 5 +- remix.env.d.ts | 1 + storefrontapi.generated.d.ts | 9 +- 15 files changed, 165 insertions(+), 233 deletions(-) create mode 100644 app/components/CustomAnalytics.tsx delete mode 100644 app/hooks/useAnalytics.tsx diff --git a/.env.template b/.env.template index 398c3c6..4040d9a 100644 --- a/.env.template +++ b/.env.template @@ -6,6 +6,7 @@ PUBLIC_STOREFRONT_API_TOKEN="046fc93a591ca78ec9dc34657b660ac6" PRIVATE_STOREFRONT_API_TOKEN="shpat_•••••••••••••••••••••••••" PUBLIC_STOREFRONT_ID="22227" PUBLIC_STOREFRONT_API_VERSION="2024-01" +PUBLIC_CHECKOUT_DOMAIN="checkout.frontvibe.com" # Sanity project ID SANITY_STUDIO_PROJECT_ID="gbcm3da4" diff --git a/app/components/CustomAnalytics.tsx b/app/components/CustomAnalytics.tsx new file mode 100644 index 0000000..1f26db7 --- /dev/null +++ b/app/components/CustomAnalytics.tsx @@ -0,0 +1,32 @@ +import {unstable_useAnalytics as useAnalytics} from '@shopify/hydrogen'; +import {useEffect} from 'react'; + +import {useIsDev} from '~/hooks/useIsDev'; + +export function CustomAnalytics() { + const {subscribe} = useAnalytics(); + const isDev = useIsDev(); + + useEffect(() => { + if (!isDev) return; + + // Standard events + subscribe('page_viewed', (data) => { + console.log('CustomAnalytics - Page viewed:', data); + }); + subscribe('product_viewed', (data) => { + console.log('CustomAnalytics - Product viewed:', data); + }); + subscribe('collection_viewed', (data) => { + console.log('CustomAnalytics - Collection viewed:', data); + }); + subscribe('cart_viewed', (data) => { + console.log('CustomAnalytics - Cart viewed:', data); + }); + subscribe('cart_updated', (data) => { + console.log('CustomAnalytics - Cart updated:', data); + }); + }, [subscribe, isDev]); + + return null; +} diff --git a/app/components/product/AddToCartForm.tsx b/app/components/product/AddToCartForm.tsx index 367437f..8204a97 100644 --- a/app/components/product/AddToCartForm.tsx +++ b/app/components/product/AddToCartForm.tsx @@ -1,20 +1,10 @@ -import type {FetcherWithComponents} from '@remix-run/react'; -import type {ShopifyAddToCartPayload} from '@shopify/hydrogen'; import type {ProductVariantFragmentFragment} from 'storefrontapi.generated'; import {useNavigation} from '@remix-run/react'; -import { - AnalyticsEventName, - CartForm, - OptimisticInput, - ShopPayButton, - getClientBrowserParameters, - sendShopifyAnalytics, -} from '@shopify/hydrogen'; +import {CartForm, OptimisticInput, ShopPayButton} from '@shopify/hydrogen'; import {useEffect, useState} from 'react'; import {useIdle, useSessionStorage} from 'react-use'; -import {usePageAnalytics} from '~/hooks/useAnalytics'; import {useLocalePath} from '~/hooks/useLocalePath'; import {useSanityThemeContent} from '~/hooks/useSanityThemeContent'; import {useSelectedVariant} from '~/hooks/useSelectedVariant'; @@ -78,57 +68,55 @@ export function AddToCartForm(props: { // Button is also disabled if navigation is loading (new variant is being fetched) // to prevent adding the wrong variant to the cart. return ( - -
- + - - {showShopPay && selectedVariant.id && ( - + quantity, + }, + }} + id="cart-line-item" + /> +
-
+ + {showShopPay && selectedVariant.id && ( + + )} + ); }} @@ -193,51 +181,6 @@ function ShopPay(props: { ); } -function AddToCartAnalytics({ - children, - fetcher, -}: { - children: React.ReactNode; - fetcher: FetcherWithComponents; -}): JSX.Element { - const fetcherData = fetcher.data; - const formData = fetcher.formData; - const pageAnalytics = usePageAnalytics({hasUserConsent: true}); - - useEffect(() => { - if (formData) { - const cartData: Record = {}; - const cartInputs = CartForm.getFormInput(formData); - - try { - if (cartInputs.inputs.analytics) { - const dataInForm: unknown = JSON.parse( - String(cartInputs.inputs.analytics), - ); - Object.assign(cartData, dataInForm); - } - } catch { - // do nothing - } - - if (Object.keys(cartData).length && fetcherData) { - const addToCartPayload: ShopifyAddToCartPayload = { - ...getClientBrowserParameters(), - ...pageAnalytics, - ...cartData, - cartId: fetcherData.cart.id, - }; - - sendShopifyAnalytics({ - eventName: AnalyticsEventName.ADD_TO_CART, - payload: addToCartPayload, - }); - } - } - }, [fetcherData, formData, pageAnalytics]); - return <>{children}; -} - function ShopPayLogo() { return ( (''); - const pageAnalytics = usePageAnalytics({hasUserConsent}); - - // Page view analytics - // We want useEffect to execute only when location changes - // which represents a page view - useEffect(() => { - if (lastLocationKey.current === location.key) return; - - lastLocationKey.current = location.key; - - const payload: ShopifyPageViewPayload = { - ...getClientBrowserParameters(), - ...pageAnalytics, - }; - - sendShopifyAnalytics({ - eventName: AnalyticsEventName.PAGE_VIEW, - payload, - }); - }, [location, pageAnalytics]); -} - -export function usePageAnalytics({hasUserConsent}: {hasUserConsent: boolean}) { - const matches = useMatches(); - - return useMemo(() => { - const data: Record = {}; - - matches.forEach((event) => { - const eventData = event?.data as Record; - if (eventData) { - eventData['analytics'] && Object.assign(data, eventData['analytics']); - - const selectedLocale = - (eventData['selectedLocale'] as typeof DEFAULT_LOCALE) || - DEFAULT_LOCALE; - - Object.assign(data, { - acceptedLanguage: selectedLocale.language, - currency: selectedLocale.currency, - }); - } - }); - - return { - ...data, - hasUserConsent, - } as unknown as ShopifyPageViewPayload; - }, [matches, hasUserConsent]); -} diff --git a/app/lib/env.server.ts b/app/lib/env.server.ts index 14cac11..00061d7 100644 --- a/app/lib/env.server.ts +++ b/app/lib/env.server.ts @@ -3,7 +3,7 @@ * some are only available through the process.env object */ -import {AppLoadContext} from '@shopify/remix-oxygen'; +import type {AppLoadContext} from '@shopify/remix-oxygen'; export function envVariables(contextEnv: Env) { let env: Env | NodeJS.ProcessEnv = contextEnv; @@ -19,6 +19,10 @@ export function envVariables(contextEnv: Env) { env.PRIVATE_STOREFRONT_API_TOKEN, 'PRIVATE_STOREFRONT_API_TOKEN', ), + PUBLIC_CHECKOUT_DOMAIN: checkRequiredEnv( + env.PUBLIC_CHECKOUT_DOMAIN, + 'PUBLIC_CHECKOUT_DOMAIN', + ), PUBLIC_STORE_DOMAIN: checkRequiredEnv( env.PUBLIC_STORE_DOMAIN, 'PUBLIC_STORE_DOMAIN', diff --git a/app/root.tsx b/app/root.tsx index 5f9bcf0..7de7d9a 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -15,11 +15,16 @@ import { Scripts, ScrollRestoration, isRouteErrorResponse, + useLoaderData, useMatches, useNavigate, useRouteError, } from '@remix-run/react'; -import {useNonce} from '@shopify/hydrogen'; +import { + UNSTABLE_Analytics as Analytics, + getShopAnalytics, + useNonce, +} from '@shopify/hydrogen'; import {defer} from '@shopify/remix-oxygen'; import {DEFAULT_LOCALE} from 'countries'; @@ -29,10 +34,10 @@ import type {HydrogenSession} from './lib/hydrogen.session.server'; import faviconAsset from '../public/favicon.ico'; import {CssVars} from './components/CssVars'; +import {CustomAnalytics} from './components/CustomAnalytics'; import {Fonts} from './components/Fonts'; import {generateSanityImageUrl} from './components/sanity/SanityImage'; import {Button} from './components/ui/Button'; -import {useAnalytics} from './hooks/useAnalytics'; import {useLocalePath} from './hooks/useLocalePath'; import {useSanityThemeContent} from './hooks/useSanityThemeContent'; import {generateFontsPreloadLinks} from './lib/fonts'; @@ -151,12 +156,12 @@ export async function loader({context, request}: LoaderFunctionArgs) { return defer( { - analytics: { - shopId: layout.shop.id, - shopifySalesChannel: locale.salesChannel, - }, cart: cartPromise, collectionListPromise, + consent: { + checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, + storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, + }, env: { /* * Be careful not to expose any sensitive environment variables here. @@ -178,6 +183,10 @@ export async function loader({context, request}: LoaderFunctionArgs) { sanityPreviewMode, sanityRoot, seo, + shop: getShopAnalytics({ + publicStorefrontId: env.PUBLIC_STOREFRONT_ID, + storefront: storefront, + }), ...sanityPreviewPayload({ context, params: queryParams, @@ -191,9 +200,7 @@ export async function loader({context, request}: LoaderFunctionArgs) { export default function App() { const nonce = useNonce(); const {locale} = useRootLoaderData(); - const hasUserConsent = true; - - useAnalytics(hasUserConsent); + const data = useLoaderData(); return ( @@ -206,9 +213,16 @@ export default function App() { - - - + + + + + + diff --git a/app/routes/($locale).$.tsx b/app/routes/($locale).$.tsx index a90ff5e..ed304ee 100644 --- a/app/routes/($locale).$.tsx +++ b/app/routes/($locale).$.tsx @@ -1,7 +1,7 @@ import type {LoaderFunctionArgs, MetaArgs} from '@shopify/remix-oxygen'; import {useLoaderData} from '@remix-run/react'; -import {AnalyticsPageType, getSeoMeta} from '@shopify/hydrogen'; +import {getSeoMeta} from '@shopify/hydrogen'; import {defer} from '@shopify/remix-oxygen'; import {DEFAULT_LOCALE} from 'countries'; @@ -61,10 +61,6 @@ export async function loader({context, params, request}: LoaderFunctionArgs) { }); return defer({ - analytics: { - pageType: - handle === 'home' ? AnalyticsPageType.home : AnalyticsPageType.page, - }, collectionListPromise, featuredCollectionPromise, featuredProductPromise, diff --git a/app/routes/($locale).cart.tsx b/app/routes/($locale).cart.tsx index cc9b0b1..ae54009 100644 --- a/app/routes/($locale).cart.tsx +++ b/app/routes/($locale).cart.tsx @@ -6,7 +6,7 @@ import type { import type {CartApiQueryFragment} from 'storefrontapi.generated'; import {useLoaderData} from '@remix-run/react'; -import {CartForm} from '@shopify/hydrogen'; +import {UNSTABLE_Analytics as Analytics, CartForm} from '@shopify/hydrogen'; import {json, redirectDocument} from '@shopify/remix-oxygen'; import invariant from 'tiny-invariant'; @@ -64,7 +64,6 @@ export async function action({context, request}: ActionFunctionArgs) { /** * The Cart ID may change after each mutation. We need to update it each time in the session. */ - const cartId = result.cart.id; const headers = cart.setCartId(result.cart.id); const redirectTo = formData.get('redirectTo') ?? null; @@ -78,9 +77,6 @@ export async function action({context, request}: ActionFunctionArgs) { return json( { - analytics: { - cartId, - }, cart: cartResult, errors, }, @@ -100,6 +96,7 @@ export default function CartRoute() { return (
+
); } diff --git a/app/routes/($locale).collections.$collectionHandle.tsx b/app/routes/($locale).collections.$collectionHandle.tsx index 1f24e87..b20d989 100644 --- a/app/routes/($locale).collections.$collectionHandle.tsx +++ b/app/routes/($locale).collections.$collectionHandle.tsx @@ -2,8 +2,7 @@ import type {LoaderFunctionArgs, MetaArgs} from '@shopify/remix-oxygen'; import type {CollectionDetailsQuery} from 'storefrontapi.generated'; import {useLoaderData} from '@remix-run/react'; -import {getSeoMeta} from '@shopify/hydrogen'; -import {AnalyticsPageType} from '@shopify/hydrogen-react'; +import {UNSTABLE_Analytics as Analytics, getSeoMeta} from '@shopify/hydrogen'; import {defer} from '@shopify/remix-oxygen'; import {DEFAULT_LOCALE} from 'countries'; import invariant from 'tiny-invariant'; @@ -68,11 +67,6 @@ export async function loader({context, params, request}: LoaderFunctionArgs) { const seo = seoPayload.collection({collection, url: request.url}); return defer({ - analytics: { - collectionHandle, - pageType: AnalyticsPageType.collection, - resourceId: collection.id, - }, cmsCollection, collection, collectionListPromise, @@ -89,18 +83,30 @@ export async function loader({context, params, request}: LoaderFunctionArgs) { } export default function Collection() { - const {cmsCollection} = useLoaderData(); + const {cmsCollection, collection} = useLoaderData(); const {data, encodeDataAttribute} = useSanityData({initial: cmsCollection}); const template = data?.collection?.template || data?.defaultCollectionTemplate; - return template?.sections && template.sections.length > 0 - ? template.sections.map((section) => ( - - )) - : null; + return ( + <> + {template?.sections && template.sections.length > 0 + ? template.sections.map((section) => ( + + )) + : null} + + + ); } diff --git a/app/routes/($locale).products.$productHandle.tsx b/app/routes/($locale).products.$productHandle.tsx index b408407..8377691 100644 --- a/app/routes/($locale).products.$productHandle.tsx +++ b/app/routes/($locale).products.$productHandle.tsx @@ -1,10 +1,9 @@ -import type {ShopifyAnalyticsProduct} from '@shopify/hydrogen'; import type {LoaderFunctionArgs, MetaArgs} from '@shopify/remix-oxygen'; import type {ProductQuery} from 'storefrontapi.generated'; import {useLoaderData} from '@remix-run/react'; import { - AnalyticsPageType, + UNSTABLE_Analytics as Analytics, getSelectedProductOptions, getSeoMeta, } from '@shopify/hydrogen'; @@ -86,13 +85,6 @@ export async function loader({context, params, request}: LoaderFunctionArgs) { storefront, }); - const productAnalytics: ShopifyAnalyticsProduct = { - brand: product.vendor, - name: product.title, - price: product.priceRange.minVariantPrice.amount, - productGid: product.id, - }; - const seo = seoPayload.product({ product, selectedVariant: product.variants.nodes[0], @@ -100,12 +92,6 @@ export async function loader({context, params, request}: LoaderFunctionArgs) { }); return defer({ - analytics: { - pageType: AnalyticsPageType.product, - products: [productAnalytics], - resourceId: product.id, - totalValue: parseFloat(product.priceRange.minVariantPrice.amount), - }, cmsProduct, collectionListPromise, featuredCollectionPromise, @@ -126,6 +112,7 @@ export default function Product() { const {cmsProduct, product} = useLoaderData(); const {data, encodeDataAttribute} = useSanityData({initial: cmsProduct}); const template = data?.product?.template || data?.defaultProductTemplate; + const selectedVariant = product.variants.nodes[0]; return ( @@ -138,6 +125,21 @@ export default function Product() { key={section._key} /> ))} + ); } diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 8a99945..22ede7c 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,6 +1,6 @@ import type {LoaderFunctionArgs, MetaArgs} from '@shopify/remix-oxygen'; -import {AnalyticsPageType, getSeoMeta} from '@shopify/hydrogen'; +import {getSeoMeta} from '@shopify/hydrogen'; import {defer} from '@shopify/remix-oxygen'; import {DEFAULT_LOCALE} from 'countries'; @@ -55,9 +55,6 @@ export async function loader({context, request}: LoaderFunctionArgs) { }); return defer({ - analytics: { - pageType: AnalyticsPageType.home, - }, collectionListPromise, featuredCollectionPromise, featuredProductPromise, diff --git a/remix.env.d.ts b/remix.env.d.ts index b6a6f4e..37d5219 100644 --- a/remix.env.d.ts +++ b/remix.env.d.ts @@ -36,6 +36,7 @@ declare global { PUBLIC_STOREFRONT_API_TOKEN: string; PUBLIC_STOREFRONT_API_VERSION: string; PUBLIC_STOREFRONT_ID: string; + PUBLIC_CHECKOUT_DOMAIN: string; SANITY_STUDIO_API_VERSION: string; SANITY_STUDIO_DATASET: string; SANITY_STUDIO_PROJECT_ID: string; diff --git a/storefrontapi.generated.d.ts b/storefrontapi.generated.d.ts index ec3e81b..d5f56ff 100644 --- a/storefrontapi.generated.d.ts +++ b/storefrontapi.generated.d.ts @@ -138,7 +138,7 @@ export type CartLineFragment = Pick< 'id' | 'altText' | 'width' | 'height' | 'url' > & {thumbnail: StorefrontAPI.Image['url']} >; - product: Pick; + product: Pick; selectedOptions: Array< Pick >; @@ -147,7 +147,7 @@ export type CartLineFragment = Pick< export type CartApiQueryFragment = Pick< StorefrontAPI.Cart, - 'id' | 'checkoutUrl' | 'totalQuantity' | 'note' + 'id' | 'updatedAt' | 'checkoutUrl' | 'totalQuantity' | 'note' > & { buyerIdentity: Pick< StorefrontAPI.CartBuyerIdentity, @@ -188,7 +188,10 @@ export type CartApiQueryFragment = Pick< 'id' | 'altText' | 'width' | 'height' | 'url' > & {thumbnail: StorefrontAPI.Image['url']} >; - product: Pick; + product: Pick< + StorefrontAPI.Product, + 'handle' | 'vendor' | 'title' | 'id' + >; selectedOptions: Array< Pick >;