diff --git a/.changeset/flat-wombats-lie.md b/.changeset/flat-wombats-lie.md new file mode 100644 index 0000000000..e1568078d7 --- /dev/null +++ b/.changeset/flat-wombats-lie.md @@ -0,0 +1,7 @@ +--- +"@graphcommerce/google-datalayer": patch +"@graphcommerce/googletagmanager": patch +"@graphcommerce/googleanalytics": patch +--- + +Extracted the datalayer from the googleanalytics package and moved to google-datalayer package. Make sure Google Analytics and Google Tagmanager both can send events individually. Be able to configure the datalayer will send as GA4 or legacy GA3 events. diff --git a/docs/framework/config.md b/docs/framework/config.md index d3568fffc1..3d114ebdce 100644 --- a/docs/framework/config.md +++ b/docs/framework/config.md @@ -1,4 +1,12 @@ +### AnalyticsConfig + +AnalyticsConfig will contain all configuration values for the analytics in GraphCommerce. + +#### eventFormat: GA3 | GA4[] + +eventFormat contains the list of fired and formatted events + # GraphCommerce configuration system Global GraphCommerce configuration can be configured in your `graphcommerce.config.js` file @@ -105,6 +113,8 @@ Examples: All storefront configuration for the project +#### analytics: [AnalyticsConfig](#AnalyticsConfig) + #### cartDisplayPricesInclTax: boolean Due to a limitation of the GraphQL API it is not possible to determine if a cart should be displayed including or excluding tax. diff --git a/packages/google-datalayer/Config.graphqls b/packages/google-datalayer/Config.graphqls new file mode 100644 index 0000000000..4308d07de8 --- /dev/null +++ b/packages/google-datalayer/Config.graphqls @@ -0,0 +1,21 @@ +extend input GraphCommerceConfig { + analytics: AnalyticsConfig +} + +""" +EventFormat is an enumatation of different event formats. This decides what the format of the event data will be. +""" +enum EventFormat { + GA3 + GA4 +} + +""" +AnalyticsConfig will contain all configuration values for the analytics in GraphCommerce. +""" +input AnalyticsConfig { + """ + eventFormat contains the list of fired and formatted events + """ + eventFormat: [EventFormat!] +} diff --git a/packages/google-datalayer/README.md b/packages/google-datalayer/README.md new file mode 100644 index 0000000000..93cbb9cbe7 --- /dev/null +++ b/packages/google-datalayer/README.md @@ -0,0 +1,6 @@ +# @graphcommerce/google-datalayer + +This package contains utilities that can be used in different app analytics +trackers. + +add_payment_info add_shipping_info diff --git a/packages/google-datalayer/components/AnalyticsItemList.tsx b/packages/google-datalayer/components/AnalyticsItemList.tsx new file mode 100644 index 0000000000..5033d91963 --- /dev/null +++ b/packages/google-datalayer/components/AnalyticsItemList.tsx @@ -0,0 +1,63 @@ +import { nonNullable, useMemoObject } from '@graphcommerce/next-ui' +import { useEventCallback } from '@mui/material' +import React, { useContext, useEffect, useMemo } from 'react' +import { Item, ProductToItemFragment, productToItem } from '../lib' +import { event } from '../lib/event' + +export type UseViewItemListProps

= { + title: string + items?: (P | null | undefined)[] | null + listId?: string +} + +export type ViewItemList = { + item_list_id: string + item_list_name: string + items: Item[] +} + +const GoogleTagManagerItemListContext = React.createContext<{ + item_list_id: string + item_list_name: string +}>({ item_list_id: '', item_list_name: '' }) + +export function useListItemHandler(item: ProductToItemFragment) { + const { item_list_id, item_list_name } = useContext(GoogleTagManagerItemListContext) + return useEventCallback(() => + event('select_item', { + item_list_id, + item_list_name, + items: productToItem(item), + }), + ) +} + +export function ItemList

( + props: UseViewItemListProps

& { children: React.ReactNode }, +) { + const { title, items, listId, children } = props + + const eventData: ViewItemList = useMemoObject({ + item_list_id: listId ?? title?.toLowerCase().replace(/\s/g, '_'), + item_list_name: title, + items: items?.map((item) => (item ? productToItem(item) : null)).filter(nonNullable) ?? [], + }) + + useEffect(() => { + event('view_item_list', eventData) + }, [eventData]) + + const value = useMemo( + () => ({ + item_list_id: listId ?? title?.toLowerCase().replace(/\s/g, '_'), + item_list_name: title ?? listId, + }), + [listId, title], + ) + + return ( + + {children} + + ) +} diff --git a/packages/googleanalytics/events/gtagAddPaymentInfo/GtagAddPaymentInfo.graphql b/packages/google-datalayer/events/add_payment_info/AddPaymentInfoFragment.graphql similarity index 91% rename from packages/googleanalytics/events/gtagAddPaymentInfo/GtagAddPaymentInfo.graphql rename to packages/google-datalayer/events/add_payment_info/AddPaymentInfoFragment.graphql index 7e8ef3e5d9..24266ff9d7 100644 --- a/packages/googleanalytics/events/gtagAddPaymentInfo/GtagAddPaymentInfo.graphql +++ b/packages/google-datalayer/events/add_payment_info/AddPaymentInfoFragment.graphql @@ -1,4 +1,4 @@ -fragment GtagAddPaymentInfo on Cart +fragment AddPaymentInfoFragment on Cart @inject(into: ["PaymentMethodContext", "PaymentMethodUpdated"]) { items { uid diff --git a/packages/googleanalytics/events/gtagAddPaymentInfo/gtagAddPaymentInfo.ts b/packages/google-datalayer/events/add_payment_info/addPaymentInfo.ts similarity index 74% rename from packages/googleanalytics/events/gtagAddPaymentInfo/gtagAddPaymentInfo.ts rename to packages/google-datalayer/events/add_payment_info/addPaymentInfo.ts index 454e237bf0..46b29c9a6a 100644 --- a/packages/googleanalytics/events/gtagAddPaymentInfo/gtagAddPaymentInfo.ts +++ b/packages/google-datalayer/events/add_payment_info/addPaymentInfo.ts @@ -1,7 +1,9 @@ -import { GtagAddPaymentInfoFragment } from './GtagAddPaymentInfo.gql' +import { event } from '../../lib/event' +import { AddPaymentInfoFragment } from './AddPaymentInfoFragment.gql' -export function gtagAddPaymentInfo(cart?: C | null) { - globalThis.gtag?.('event', 'add_payment_info', { +export const addPaymentInfo = (cart?: C | null) => + event('add_payment_info', { + ...cart, currency: cart?.prices?.grand_total?.currency, value: cart?.prices?.grand_total?.value, coupon: cart?.applied_coupons?.map((coupon) => coupon?.code), @@ -19,4 +21,3 @@ export function gtagAddPaymentInfo(cart?: quantity: item?.quantity, })), }) -} diff --git a/packages/google-datalayer/events/add_payment_info/index.ts b/packages/google-datalayer/events/add_payment_info/index.ts new file mode 100644 index 0000000000..22b1e9c648 --- /dev/null +++ b/packages/google-datalayer/events/add_payment_info/index.ts @@ -0,0 +1,2 @@ +export * from './addPaymentInfo' +export * from './AddPaymentInfoFragment.gql' diff --git a/packages/googleanalytics/events/gtagAddShippingInfo/GtagAddShippingInfo.graphql b/packages/google-datalayer/events/add_shipping_info/AddSchippingInfoFragment.graphql similarity index 83% rename from packages/googleanalytics/events/gtagAddShippingInfo/GtagAddShippingInfo.graphql rename to packages/google-datalayer/events/add_shipping_info/AddSchippingInfoFragment.graphql index f6dfea2b25..497c600e9c 100644 --- a/packages/googleanalytics/events/gtagAddShippingInfo/GtagAddShippingInfo.graphql +++ b/packages/google-datalayer/events/add_shipping_info/AddSchippingInfoFragment.graphql @@ -1,4 +1,4 @@ -fragment GtagAddShippingInfo on Cart @inject(into: ["ShippingMethodSelected"]) { +fragment AddShippingInfoFragment on Cart @inject(into: ["ShippingMethodSelected"]) { prices { grand_total { currency diff --git a/packages/googleanalytics/events/gtagAddShippingInfo/gtagAddShippingInfo.ts b/packages/google-datalayer/events/add_shipping_info/addShippingInfo.ts similarity index 61% rename from packages/googleanalytics/events/gtagAddShippingInfo/gtagAddShippingInfo.ts rename to packages/google-datalayer/events/add_shipping_info/addShippingInfo.ts index 76bdf1a950..12548aeeca 100644 --- a/packages/googleanalytics/events/gtagAddShippingInfo/gtagAddShippingInfo.ts +++ b/packages/google-datalayer/events/add_shipping_info/addShippingInfo.ts @@ -1,9 +1,9 @@ -import { GtagAddShippingInfoFragment } from './GtagAddShippingInfo.gql' +import { event } from '../../lib/event' +import { AddShippingInfoFragment } from './AddSchippingInfoFragment.gql' -export function gtagAddShippingInfo(cart?: C | null) { - if (!cart) return - - globalThis.gtag?.('event', 'add_shipping_info', { +export const addShippingInfo = (cart?: C) => + event('add_shipping_info', { + ...cart, currency: cart?.prices?.grand_total?.currency, value: cart?.prices?.grand_total?.value, coupon: cart?.applied_coupons?.map((coupon) => coupon?.code), @@ -15,9 +15,8 @@ export function gtagAddShippingInfo(cart? (sum, discount) => sum + (discount?.amount?.value ?? 0), 0, ), - item_variant: item?.__typename === 'ConfigurableCartItem' ? item.configured_variant.sku : '', + item_variant: item && 'configured_variant' in item ? item.configured_variant.sku : '', price: item?.prices?.price.value, quantity: item?.quantity, })), }) -} diff --git a/packages/google-datalayer/events/add_shipping_info/index.ts b/packages/google-datalayer/events/add_shipping_info/index.ts new file mode 100644 index 0000000000..e6a2cf1ae3 --- /dev/null +++ b/packages/google-datalayer/events/add_shipping_info/index.ts @@ -0,0 +1,2 @@ +export * from './addShippingInfo' +export * from './AddSchippingInfoFragment.gql' diff --git a/packages/googleanalytics/events/gtagAddToCart/GtagAddToCart.graphql b/packages/google-datalayer/events/add_to_cart/AddToCartFragment.graphql similarity index 57% rename from packages/googleanalytics/events/gtagAddToCart/GtagAddToCart.graphql rename to packages/google-datalayer/events/add_to_cart/AddToCartFragment.graphql index 18ae30a040..abe8330c49 100644 --- a/packages/googleanalytics/events/gtagAddToCart/GtagAddToCart.graphql +++ b/packages/google-datalayer/events/add_to_cart/AddToCartFragment.graphql @@ -1,5 +1,10 @@ -fragment GtagAddToCart on Cart @inject(into: ["CartItemCountChanged"]) { +fragment AddToCartFragment on Cart @inject(into: ["CartItemCountChanged"]) { items { + quantity + product { + sku + name + } prices { discounts { amount { @@ -17,4 +22,9 @@ fragment GtagAddToCart on Cart @inject(into: ["CartItemCountChanged"]) { } } } + prices { + grand_total { + currency + } + } } diff --git a/packages/google-datalayer/events/add_to_cart/addToCart.ts b/packages/google-datalayer/events/add_to_cart/addToCart.ts new file mode 100644 index 0000000000..b643354146 --- /dev/null +++ b/packages/google-datalayer/events/add_to_cart/addToCart.ts @@ -0,0 +1,58 @@ +import type { FetchResult } from '@graphcommerce/graphql' +import { + AddProductsToCartFields, + AddProductsToCartMutation, + findAddedItems, + toUserErrors, +} from '@graphcommerce/magento-product' +import { nonNullable } from '@graphcommerce/next-ui' +import { event } from '../../lib/event' + +export const addToCart = ( + result: FetchResult, + variables: AddProductsToCartFields, +) => { + const { data, errors } = result + const cart = data?.addProductsToCart?.cart + + const addedItems = findAddedItems(data, variables) + + const items = addedItems + .map(({ itemVariable, itemInCart }) => { + if (!itemInCart) return null + const { product, prices } = itemInCart + return { + item_id: product.sku, + item_name: product.name, + currency: prices?.price.currency, + price: (prices?.row_total_including_tax.value ?? 1) / itemInCart.quantity, + quantity: itemVariable.quantity, + discount: prices?.discounts?.reduce( + (sum, discount) => sum + (discount?.amount?.value ?? 0) / itemVariable.quantity, + 0, + ), + } + }) + .filter(nonNullable) + + const userErrors = toUserErrors(result.data) + if ((errors && errors.length > 0) || userErrors.length > 0) { + event('add_to_cart_error', { + userErrors: userErrors?.map((e) => e.message), + errors: errors?.map((e) => e.message), + variables, + }) + } + + if (!items.length) return + + event('add_to_cart', { + currency: cart?.prices?.grand_total?.currency, + value: addedItems.reduce( + (sum, { itemVariable, itemInCart }) => + sum + (itemInCart?.prices?.row_total_including_tax.value ?? 1) / itemVariable.quantity, + 0, + ), + items, + }) +} diff --git a/packages/google-datalayer/events/add_to_cart/index.ts b/packages/google-datalayer/events/add_to_cart/index.ts new file mode 100644 index 0000000000..6fdd3cecf8 --- /dev/null +++ b/packages/google-datalayer/events/add_to_cart/index.ts @@ -0,0 +1,2 @@ +export * from './addToCart' +export * from './AddToCartFragment.gql' diff --git a/packages/googleanalytics/events/gtagBeginCheckout/GtagBeginCheckout.graphql b/packages/google-datalayer/events/begin_checkout/BeginCheckoutFragment.graphql similarity index 85% rename from packages/googleanalytics/events/gtagBeginCheckout/GtagBeginCheckout.graphql rename to packages/google-datalayer/events/begin_checkout/BeginCheckoutFragment.graphql index 1bdde9ff8e..dd7e65c2ec 100644 --- a/packages/googleanalytics/events/gtagBeginCheckout/GtagBeginCheckout.graphql +++ b/packages/google-datalayer/events/begin_checkout/BeginCheckoutFragment.graphql @@ -1,4 +1,4 @@ -fragment GtagBeginCheckout on Cart @inject(into: ["CartStartCheckout"]) { +fragment BeginCheckoutFragment on Cart @inject(into: ["CartStartCheckout"]) { prices { grand_total { currency diff --git a/packages/googleanalytics/events/gtagBeginCheckout/gtagBeginCheckout.ts b/packages/google-datalayer/events/begin_checkout/beginCheckout.ts similarity index 73% rename from packages/googleanalytics/events/gtagBeginCheckout/gtagBeginCheckout.ts rename to packages/google-datalayer/events/begin_checkout/beginCheckout.ts index 3343462536..208a881872 100644 --- a/packages/googleanalytics/events/gtagBeginCheckout/gtagBeginCheckout.ts +++ b/packages/google-datalayer/events/begin_checkout/beginCheckout.ts @@ -1,7 +1,9 @@ -import { GtagBeginCheckoutFragment } from './GtagBeginCheckout.gql' +import { event } from '../../lib/event' +import { BeginCheckoutFragment } from './BeginCheckoutFragment.gql' -export function gtagBeginCheckout(cart?: C | null) { - globalThis.gtag?.('event', 'begin_checkout', { +export const beginCheckout = (cart?: C | null) => + event('begin_checkout', { + ...cart, currency: cart?.prices?.grand_total?.currency, value: cart?.prices?.grand_total?.value, coupon: cart?.applied_coupons?.map((coupon) => coupon?.code), @@ -18,4 +20,3 @@ export function gtagBeginCheckout(cart?: C quantity: item?.quantity, })), }) -} diff --git a/packages/google-datalayer/events/begin_checkout/index.ts b/packages/google-datalayer/events/begin_checkout/index.ts new file mode 100644 index 0000000000..7e8893210c --- /dev/null +++ b/packages/google-datalayer/events/begin_checkout/index.ts @@ -0,0 +1,2 @@ +export * from './beginCheckout' +export * from './BeginCheckoutFragment.gql' diff --git a/packages/google-datalayer/events/purchase/index.ts b/packages/google-datalayer/events/purchase/index.ts new file mode 100644 index 0000000000..c75468140d --- /dev/null +++ b/packages/google-datalayer/events/purchase/index.ts @@ -0,0 +1 @@ +export * from './purchase' diff --git a/packages/googleanalytics/events/gtagAddPurchaseInfo/gtagAddPurchaseInfo.ts b/packages/google-datalayer/events/purchase/purchase.ts similarity index 79% rename from packages/googleanalytics/events/gtagAddPurchaseInfo/gtagAddPurchaseInfo.ts rename to packages/google-datalayer/events/purchase/purchase.ts index 5a5b117cfc..381e3ddfa4 100644 --- a/packages/googleanalytics/events/gtagAddPurchaseInfo/gtagAddPurchaseInfo.ts +++ b/packages/google-datalayer/events/purchase/purchase.ts @@ -1,14 +1,16 @@ import { PaymentMethodContextFragment } from '@graphcommerce/magento-cart-payment-method/Api/PaymentMethodContext.gql' +import { event } from '../../lib/event' -export function gtagAddPurchaseInfo( +export const purchase = ( orderNumber: string, - cart: PaymentMethodContextFragment | null | undefined, -) { - globalThis.gtag?.('event', 'purchase', { + cart: C | null | undefined, +) => + event('purchase', { + ...cart, transaction_id: orderNumber, currency: cart?.prices?.grand_total?.currency, value: cart?.prices?.grand_total?.value, - coupon: cart?.applied_coupons?.map((coupon) => coupon?.code), + coupon: cart?.applied_coupons?.map((coupon) => coupon?.code).join(' '), payment_type: cart?.selected_payment_method?.code, tax: cart?.prices?.applied_taxes?.reduce((sum, tax) => sum + (tax?.amount?.value ?? 0), 0), items: cart?.items?.map((item) => ({ @@ -16,7 +18,6 @@ export function gtagAddPurchaseInfo( item_name: item?.product.name, currency: item?.prices?.price.currency, discount: item?.prices?.discounts?.reduce( - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands (sum, discount) => sum + (discount?.amount?.value ?? 0), 0, ), @@ -24,4 +25,3 @@ export function gtagAddPurchaseInfo( quantity: item?.quantity, })), }) -} diff --git a/packages/googleanalytics/events/gtagRemoveFromCart/GtagRemoveFromCart.graphql b/packages/google-datalayer/events/remove_from_cart/RemoveFromCartFragment.graphql similarity index 83% rename from packages/googleanalytics/events/gtagRemoveFromCart/GtagRemoveFromCart.graphql rename to packages/google-datalayer/events/remove_from_cart/RemoveFromCartFragment.graphql index f7989d9e80..021eb624bd 100644 --- a/packages/googleanalytics/events/gtagRemoveFromCart/GtagRemoveFromCart.graphql +++ b/packages/google-datalayer/events/remove_from_cart/RemoveFromCartFragment.graphql @@ -1,4 +1,4 @@ -fragment GtagRemoveFromCart on Cart @inject(into: ["CartItemCountChanged"]) { +fragment RemoveFromCartFragment on Cart @inject(into: ["CartItemCountChanged"]) { __typename prices { grand_total { diff --git a/packages/google-datalayer/events/remove_from_cart/index.ts b/packages/google-datalayer/events/remove_from_cart/index.ts new file mode 100644 index 0000000000..7931331aa9 --- /dev/null +++ b/packages/google-datalayer/events/remove_from_cart/index.ts @@ -0,0 +1,2 @@ +export * from './removeFromCart' +export * from './RemoveFromCartFragment.gql' diff --git a/packages/googleanalytics/events/gtagRemoveFromCart/gtagRemoveFromCart.ts b/packages/google-datalayer/events/remove_from_cart/removeFromCart.ts similarity index 70% rename from packages/googleanalytics/events/gtagRemoveFromCart/gtagRemoveFromCart.ts rename to packages/google-datalayer/events/remove_from_cart/removeFromCart.ts index abfef53046..7506f56272 100644 --- a/packages/googleanalytics/events/gtagRemoveFromCart/gtagRemoveFromCart.ts +++ b/packages/google-datalayer/events/remove_from_cart/removeFromCart.ts @@ -1,9 +1,9 @@ -import { GtagRemoveFromCartFragment } from './GtagRemoveFromCart.gql' +import { event } from '../../lib/event' +import { RemoveFromCartFragment } from './RemoveFromCartFragment.gql' -export function gtagRemoveFromCart(cart?: C | null) { - if (!cart) return - - globalThis.gtag?.('event', 'remove_from_cart', { +export const removeFromCart = (cart: C | null | undefined) => + event('remove_from_cart', { + ...cart, currency: cart?.prices?.grand_total?.currency, value: cart?.prices?.grand_total?.value, items: cart?.items?.map((item) => ({ @@ -19,4 +19,3 @@ export function gtagRemoveFromCart(cart?: quantity: item?.quantity, })), }) -} diff --git a/packages/google-datalayer/events/select_item/index.ts b/packages/google-datalayer/events/select_item/index.ts new file mode 100644 index 0000000000..a4f7241a94 --- /dev/null +++ b/packages/google-datalayer/events/select_item/index.ts @@ -0,0 +1 @@ +export * from './select_item' diff --git a/packages/google-datalayer/events/select_item/select_item.ts b/packages/google-datalayer/events/select_item/select_item.ts new file mode 100644 index 0000000000..73c3136e28 --- /dev/null +++ b/packages/google-datalayer/events/select_item/select_item.ts @@ -0,0 +1,8 @@ +import { event } from '../../lib/event' + +export const selectItem = (itemListId: string, itemListName: string, items: unknown[]) => + event('select_item', { + item_list_id: itemListId, + item_list_name: itemListName, + items, + }) diff --git a/packages/googleanalytics/events/gtagViewCart/GtagViewCart.graphql b/packages/google-datalayer/events/view_cart/ViewCartFragment.graphql similarity index 84% rename from packages/googleanalytics/events/gtagViewCart/GtagViewCart.graphql rename to packages/google-datalayer/events/view_cart/ViewCartFragment.graphql index 522f932ce9..442a711b3b 100644 --- a/packages/googleanalytics/events/gtagViewCart/GtagViewCart.graphql +++ b/packages/google-datalayer/events/view_cart/ViewCartFragment.graphql @@ -1,4 +1,4 @@ -fragment GtagViewCart on Cart @inject(into: ["CartItemCountChanged"]) { +fragment ViewCartFragment on Cart @inject(into: ["CartItemCountChanged"]) { prices { grand_total { currency diff --git a/packages/google-datalayer/events/view_cart/index.ts b/packages/google-datalayer/events/view_cart/index.ts new file mode 100644 index 0000000000..1731a6bf56 --- /dev/null +++ b/packages/google-datalayer/events/view_cart/index.ts @@ -0,0 +1,2 @@ +export * from './viewCart' +export * from './ViewCartFragment.gql' diff --git a/packages/googleanalytics/events/gtagViewCart/gtagViewCart.ts b/packages/google-datalayer/events/view_cart/viewCart.ts similarity index 72% rename from packages/googleanalytics/events/gtagViewCart/gtagViewCart.ts rename to packages/google-datalayer/events/view_cart/viewCart.ts index e42c24a5a0..8989a9d768 100644 --- a/packages/googleanalytics/events/gtagViewCart/gtagViewCart.ts +++ b/packages/google-datalayer/events/view_cart/viewCart.ts @@ -1,9 +1,9 @@ -import { GtagViewCartFragment } from './GtagViewCart.gql' +import { event } from '../../lib/event' +import { ViewCartFragment } from './ViewCartFragment.gql' -export function gtagViewCart(cart?: C | null) { - if (!cart) return - - globalThis.gtag?.('event', 'view_cart', { +export const viewCart = (cart: C | null | undefined) => + event('view_cart', { + ...cart, currency: cart?.prices?.grand_total?.currency, value: cart?.prices?.grand_total?.value, items: cart?.items?.map((item) => ({ @@ -19,4 +19,3 @@ export function gtagViewCart(cart?: C | null) { quantity: item?.quantity, })), }) -} diff --git a/packages/google-datalayer/events/view_item/index.ts b/packages/google-datalayer/events/view_item/index.ts new file mode 100644 index 0000000000..e834da439b --- /dev/null +++ b/packages/google-datalayer/events/view_item/index.ts @@ -0,0 +1 @@ +export * from './view_item' diff --git a/packages/google-datalayer/events/view_item/view_item.ts b/packages/google-datalayer/events/view_item/view_item.ts new file mode 100644 index 0000000000..0dbae337fe --- /dev/null +++ b/packages/google-datalayer/events/view_item/view_item.ts @@ -0,0 +1,8 @@ +import { event } from '../../lib/event' + +export const viewItem = (itemListId: string, itemListName: string, items: unknown[]) => + event('view_item', { + item_list_id: itemListId, + item_list_name: itemListName, + items, + }) diff --git a/packages/google-datalayer/events/view_item_list/index.ts b/packages/google-datalayer/events/view_item_list/index.ts new file mode 100644 index 0000000000..3efae2b72e --- /dev/null +++ b/packages/google-datalayer/events/view_item_list/index.ts @@ -0,0 +1 @@ +export * from './view_item_list' diff --git a/packages/google-datalayer/events/view_item_list/view_item_list.ts b/packages/google-datalayer/events/view_item_list/view_item_list.ts new file mode 100644 index 0000000000..c1ced45b8b --- /dev/null +++ b/packages/google-datalayer/events/view_item_list/view_item_list.ts @@ -0,0 +1,8 @@ +import { event } from '../../lib/event' + +export const viewItemList = (itemListId: string, itemListName: string, items: unknown[]) => + event('view_item_list', { + item_list_id: itemListId, + item_list_name: itemListName, + items, + }) diff --git a/packages/google-datalayer/lib/event.ts b/packages/google-datalayer/lib/event.ts new file mode 100644 index 0000000000..34f9ea002b --- /dev/null +++ b/packages/google-datalayer/lib/event.ts @@ -0,0 +1,39 @@ +import { EventFormatSchema } from '@graphcommerce/next-config' + +type EventMapFunctionType = ( + eventName: Gtag.EventNames | (string & Record), + eventData: { + [key: string]: unknown + }, +) => void + +type EventType = keyof (typeof EventFormatSchema)['Enum'] + +const eventMap: { [key in EventType]: EventMapFunctionType } = { + GA4: (eventName, eventData) => { + if (import.meta.graphCommerce.googleAnalyticsId) { + globalThis.gtag('event', eventName, eventData) + } + + if (import.meta.graphCommerce.googleTagmanagerId) { + globalThis.dataLayer.push({ event: eventName, ...eventData }) + } + }, + GA3: (eventName, eventData) => { + if (import.meta.graphCommerce.googleAnalyticsId) { + console.warn( + "Google Analytics 3 format is not supported for Google Analytics 4. Please update your event format to 'GA4'.", + ) + } + + if (import.meta.graphCommerce.googleTagmanagerId) { + globalThis.dataLayer.push({ event: eventName, ecommerce: eventData }) + } + }, +} + +const eventsToBeFired = import.meta.graphCommerce.analytics?.eventFormat ?? ['GA4'] + +export const event: EventMapFunctionType = (eventName, eventData) => { + eventsToBeFired.map((e) => eventMap[e](eventName, eventData)) +} diff --git a/packages/google-datalayer/lib/index.ts b/packages/google-datalayer/lib/index.ts new file mode 100644 index 0000000000..2b49b3e886 --- /dev/null +++ b/packages/google-datalayer/lib/index.ts @@ -0,0 +1,2 @@ +export * from './productToItem/productToItem' +export * from './productToItem/ProductToItem.gql' diff --git a/packages/googleanalytics/events/productToGtagItem/ProductToGtagItem.graphql b/packages/google-datalayer/lib/productToItem/ProductToItem.graphql similarity index 81% rename from packages/googleanalytics/events/productToGtagItem/ProductToGtagItem.graphql rename to packages/google-datalayer/lib/productToItem/ProductToItem.graphql index 73c663a477..5c6af3fbcd 100644 --- a/packages/googleanalytics/events/productToGtagItem/ProductToGtagItem.graphql +++ b/packages/google-datalayer/lib/productToItem/ProductToItem.graphql @@ -1,4 +1,4 @@ -fragment ProductToGtagItem on ProductInterface { +fragment ProductToItem on ProductInterface { name sku price_range { diff --git a/packages/googleanalytics/events/productToGtagItem/productToGtagItem.ts b/packages/google-datalayer/lib/productToItem/productToItem.ts similarity index 84% rename from packages/googleanalytics/events/productToGtagItem/productToGtagItem.ts rename to packages/google-datalayer/lib/productToItem/productToItem.ts index 866a877138..ae84752139 100644 --- a/packages/googleanalytics/events/productToGtagItem/productToGtagItem.ts +++ b/packages/google-datalayer/lib/productToItem/productToItem.ts @@ -1,6 +1,6 @@ -import { ProductToGtagItemFragment } from './ProductToGtagItem.gql' +import { ProductToItemFragment } from './ProductToItem.gql' -export type GtagItem = { +export type Item = { item_id: string item_name: string affiliation?: string @@ -22,7 +22,7 @@ export type GtagItem = { quantity?: number } -export function productToGtagItem

(item: P): GtagItem { +export function productToItem

(item: P): Item { return { item_id: item.sku ?? '', item_name: item.name ?? '', diff --git a/packages/google-datalayer/package.json b/packages/google-datalayer/package.json new file mode 100644 index 0000000000..01563c6ea3 --- /dev/null +++ b/packages/google-datalayer/package.json @@ -0,0 +1,44 @@ +{ + "name": "@graphcommerce/google-datalayer", + "homepage": "https://www.graphcommerce.org/", + "repository": "github:graphcommerce-org/graphcommerce", + "version": "8.0.4-canary.0", + "sideEffects": false, + "prettier": "@graphcommerce/prettier-config-pwa", + "eslintConfig": { + "extends": "@graphcommerce/eslint-config-pwa", + "parserOptions": { + "project": "./tsconfig.json" + } + }, + "peerDependencies": { + "@graphcommerce/eslint-config-pwa": "^8.0.0-canary.74", + "@graphcommerce/graphql-mesh": "^8.0.0-canary.74", + "@graphcommerce/magento-cart": "^8.0.0-canary.74", + "@graphcommerce/magento-cart-payment-method": "^8.0.0-canary.74", + "@graphcommerce/magento-cart-shipping-method": "^8.0.0-canary.74", + "@graphcommerce/magento-product": "^8.0.0-canary.74", + "@graphcommerce/next-config": "^8.0.0-canary.74", + "@graphcommerce/next-ui": "^8.0.0-canary.74", + "@graphcommerce/prettier-config-pwa": "^8.0.0-canary.74", + "@graphcommerce/typescript-config-pwa": "^8.0.0-canary.74", + "@mui/material": "^5.14.20", + "next": "^14", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "peerDependenciesMeta": { + "@graphcommerce/magento-cart": { + "optional": true + }, + "@graphcommerce/magento-cart-payment-method": { + "optional": true + }, + "@graphcommerce/magento-cart-shipping-method": { + "optional": true + }, + "@graphcommerce/magento-product": { + "optional": true + } + } +} diff --git a/packages/google-datalayer/plugins/GoogleDatalayerAddProductsToCartForm.tsx b/packages/google-datalayer/plugins/GoogleDatalayerAddProductsToCartForm.tsx new file mode 100644 index 0000000000..3cc3079bb2 --- /dev/null +++ b/packages/google-datalayer/plugins/GoogleDatalayerAddProductsToCartForm.tsx @@ -0,0 +1,23 @@ +import { AddProductsToCartFormProps } from '@graphcommerce/magento-product' +import { PluginProps } from '@graphcommerce/next-config' +import { addToCart } from '../events/add_to_cart' + +export const component = 'AddProductsToCartForm' +export const exported = '@graphcommerce/magento-product' + +/** When a product is added to the Cart, send a Google Analytics event */ +function GoogleDatalayerAddProductsToCartForm(props: PluginProps) { + const { Prev, onComplete, ...rest } = props + + return ( + { + addToCart(result, variables) + return onComplete?.(result, variables) + }} + /> + ) +} + +export const Plugin = GoogleDatalayerAddProductsToCartForm diff --git a/packages/google-datalayer/plugins/GoogleDatalayerAllPagesPageview.tsx b/packages/google-datalayer/plugins/GoogleDatalayerAllPagesPageview.tsx new file mode 100644 index 0000000000..0b9b5bc278 --- /dev/null +++ b/packages/google-datalayer/plugins/GoogleDatalayerAllPagesPageview.tsx @@ -0,0 +1,37 @@ +import type { PagesProps } from '@graphcommerce/framer-next-pages' +import type { PluginProps } from '@graphcommerce/next-config' +import { useRouter } from 'next/router' +import { useEffect } from 'react' +import { event } from '../lib/event' + +export const component = 'FramerNextPages' +export const exported = '@graphcommerce/framer-next-pages' + +function GoogleDatalayerAllPagesPageview(props: PluginProps) { + const { Prev, ...rest } = props + + const { events } = useRouter() + + useEffect(() => { + const onRouteChangeComplete = (url: string) => { + /** + * Todo: the actual page_view event is currently disabled, because we run the risk of double counting page views. + * https://developers.google.com/analytics/devguides/collection/ga4/views?client_type=gtag#manually_send_page_view_events + * + * https://developers.google.com/analytics/devguides/collection/ga4/single-page-applications?implementation=event + */ + // event('page_view', { + // page_title: '', + // page_location: '', + // }) + // event('pageview', { page: url }) + } + + events.on('routeChangeComplete', onRouteChangeComplete) + return () => events.off('routeChangeComplete', onRouteChangeComplete) + }, [events]) + + return +} + +export const Plugin = GoogleDatalayerAllPagesPageview diff --git a/packages/google-datalayer/plugins/GoogleDatalayerCartStartCheckout.tsx b/packages/google-datalayer/plugins/GoogleDatalayerCartStartCheckout.tsx new file mode 100644 index 0000000000..372df8c8cc --- /dev/null +++ b/packages/google-datalayer/plugins/GoogleDatalayerCartStartCheckout.tsx @@ -0,0 +1,21 @@ +import { CartStartCheckoutProps } from '@graphcommerce/magento-cart' +import { PluginProps } from '@graphcommerce/next-config' +import { beginCheckout } from '../events/begin_checkout' + +export const component = 'CartStartCheckout' +export const exported = '@graphcommerce/magento-cart' + +export function GoogleDatalayerCartStartCheckout(props: PluginProps) { + const { Prev, onStart, ...rest } = props + return ( + { + beginCheckout(cart) + return onStart?.(e, cart) + }} + /> + ) +} + +export const Plugin = GoogleDatalayerCartStartCheckout diff --git a/packages/googleanalytics/plugins/GaCartStartCheckoutLinkOrButton.tsx b/packages/google-datalayer/plugins/GoogleDatalayerCartStartCheckoutLinkOrButton.tsx similarity index 60% rename from packages/googleanalytics/plugins/GaCartStartCheckoutLinkOrButton.tsx rename to packages/google-datalayer/plugins/GoogleDatalayerCartStartCheckoutLinkOrButton.tsx index 7acbe64d8a..5e19648ff2 100644 --- a/packages/googleanalytics/plugins/GaCartStartCheckoutLinkOrButton.tsx +++ b/packages/google-datalayer/plugins/GoogleDatalayerCartStartCheckoutLinkOrButton.tsx @@ -1,15 +1,14 @@ import { CartStartCheckoutLinkOrButtonProps } from '@graphcommerce/magento-cart' -import { IfConfig, PluginProps } from '@graphcommerce/next-config' +import { PluginProps } from '@graphcommerce/next-config' import { useMemoObject } from '@graphcommerce/next-ui' import { useEffect } from 'react' -import { gtagBeginCheckout } from '../events/gtagBeginCheckout/gtagBeginCheckout' -import { gtagViewCart } from '../events/gtagViewCart/gtagViewCart' +import { beginCheckout } from '../events/begin_checkout' +import { viewCart } from '../events/view_cart' export const component = 'CartStartCheckoutLinkOrButton' export const exported = '@graphcommerce/magento-cart' -export const ifConfig: IfConfig = 'googleAnalyticsId' -export function GaCartStartCheckoutLinkOrButton( +export function GoogleDatalayerCartStartCheckoutLinkOrButton( props: PluginProps, ) { const { Prev, onStart, ...rest } = props @@ -18,7 +17,7 @@ export function GaCartStartCheckoutLinkOrButton( useEffect(() => { if (cartObject.items) { - gtagViewCart(cartObject) + viewCart(cartObject) } }, [cartObject]) @@ -26,11 +25,11 @@ export function GaCartStartCheckoutLinkOrButton( { - gtagBeginCheckout(cart) + beginCheckout(cart) return onStart?.(e, cart) }} /> ) } -export const Plugin = GaCartStartCheckoutLinkOrButton +export const Plugin = GoogleDatalayerCartStartCheckoutLinkOrButton diff --git a/packages/googleanalytics/plugins/GaPaymentMethodButton.tsx b/packages/google-datalayer/plugins/GoogleDatalayerPaymentMethodButton.tsx similarity index 66% rename from packages/googleanalytics/plugins/GaPaymentMethodButton.tsx rename to packages/google-datalayer/plugins/GoogleDatalayerPaymentMethodButton.tsx index 9388e6a134..a8be0a075e 100644 --- a/packages/googleanalytics/plugins/GaPaymentMethodButton.tsx +++ b/packages/google-datalayer/plugins/GoogleDatalayerPaymentMethodButton.tsx @@ -1,15 +1,14 @@ import { useCartQuery } from '@graphcommerce/magento-cart' import { PaymentMethodButtonProps } from '@graphcommerce/magento-cart-payment-method' import { GetPaymentMethodContextDocument } from '@graphcommerce/magento-cart-payment-method/PaymentMethodContext/GetPaymentMethodContext.gql' -import { IfConfig, PluginProps } from '@graphcommerce/next-config' -import { gtagAddPaymentInfo } from '../events/gtagAddPaymentInfo/gtagAddPaymentInfo' +import { PluginProps } from '@graphcommerce/next-config' +import { addPaymentInfo } from '../events/add_payment_info' export const component = 'PaymentMethodButton' export const exported = '@graphcommerce/magento-cart-payment-method' -export const ifConfig: IfConfig = 'googleAnalyticsId' // @todo This plugin can probably be migrated to the actual form that is submitted. -function GaPaymentMethodButton(props: PluginProps) { +function GoogleDatalayerPaymentMethodButton(props: PluginProps) { const { Prev, onSubmitSuccessful, ...rest } = props const methodContext = useCartQuery(GetPaymentMethodContextDocument) @@ -17,11 +16,11 @@ function GaPaymentMethodButton(props: PluginProps) { { - gtagAddPaymentInfo(methodContext.data?.cart) + addPaymentInfo(methodContext.data?.cart) return onSubmitSuccessful?.() }} /> ) } -export const Plugin = GaPaymentMethodButton +export const Plugin = GoogleDatalayerPaymentMethodButton diff --git a/packages/googleanalytics/plugins/GaPaymentMethodContextProvider.tsx b/packages/google-datalayer/plugins/GoogleDatalayerPaymentMethodContextProvider.tsx similarity index 50% rename from packages/googleanalytics/plugins/GaPaymentMethodContextProvider.tsx rename to packages/google-datalayer/plugins/GoogleDatalayerPaymentMethodContextProvider.tsx index 5c95de61d3..89b3500f96 100644 --- a/packages/googleanalytics/plugins/GaPaymentMethodContextProvider.tsx +++ b/packages/google-datalayer/plugins/GoogleDatalayerPaymentMethodContextProvider.tsx @@ -1,21 +1,22 @@ import type { PaymentMethodContextProviderProps } from '@graphcommerce/magento-cart-payment-method' -import type { IfConfig, PluginProps } from '@graphcommerce/next-config' -import { gtagAddPurchaseInfo } from '../events/gtagAddPurchaseInfo/gtagAddPurchaseInfo' +import type { PluginProps } from '@graphcommerce/next-config' +import { purchase } from '../events/purchase' export const component = 'PaymentMethodContextProvider' export const exported = '@graphcommerce/magento-cart-payment-method' -export const ifConfig: IfConfig = 'googleAnalyticsId' -function GaPaymentMethodContextProvider(props: PluginProps) { +function GoogleDatalayerPaymentMethodContextProvider( + props: PluginProps, +) { const { Prev, onSuccess, ...rest } = props return ( { - gtagAddPurchaseInfo(orderNumber, cart) + purchase(orderNumber, cart) return onSuccess?.(orderNumber, cart) }} /> ) } -export const Plugin = GaPaymentMethodContextProvider +export const Plugin = GoogleDatalayerPaymentMethodContextProvider diff --git a/packages/google-datalayer/plugins/GoogleDatalayerProductListItem.tsx b/packages/google-datalayer/plugins/GoogleDatalayerProductListItem.tsx new file mode 100644 index 0000000000..cb0fbef648 --- /dev/null +++ b/packages/google-datalayer/plugins/GoogleDatalayerProductListItem.tsx @@ -0,0 +1,28 @@ +import { ProductListItemReal } from '@graphcommerce/magento-product' +import { PluginProps } from '@graphcommerce/next-config' +import { useEventCallback } from '@mui/material' +import { ComponentProps } from 'react' +import { useListItemHandler } from '../components/AnalyticsItemList' + +export const component = 'ProductListItemReal' +export const exported = '@graphcommerce/magento-product' + +function GoogleDatalayerProductListItem( + props: PluginProps>, +) { + const { Prev, onClick, ...rest } = props + const { sku, price_range, name } = rest + const handle = useListItemHandler({ sku, price_range, name }) + + return ( + >((e, item) => { + handle() + return onClick?.(e, item) + })} + /> + ) +} + +export const Plugin = GoogleDatalayerProductListItem diff --git a/packages/google-datalayer/plugins/GoogleDatalayerProductListItemsBase.tsx b/packages/google-datalayer/plugins/GoogleDatalayerProductListItemsBase.tsx new file mode 100644 index 0000000000..be39d69aa8 --- /dev/null +++ b/packages/google-datalayer/plugins/GoogleDatalayerProductListItemsBase.tsx @@ -0,0 +1,17 @@ +import type { ProductItemsGridProps } from '@graphcommerce/magento-product' +import { PluginProps } from '@graphcommerce/next-config' +import { ItemList } from '../components/AnalyticsItemList' + +export const component = 'ProductListItemsBase' +export const exported = '@graphcommerce/magento-product' + +export function GoogleDatalayerProductListItemsBase(props: PluginProps) { + const { Prev, ...rest } = props + return ( + + + + ) +} + +export const Plugin = GoogleDatalayerProductListItemsBase diff --git a/packages/google-datalayer/plugins/GoogleDatalayerRemoveItemFromCart.tsx b/packages/google-datalayer/plugins/GoogleDatalayerRemoveItemFromCart.tsx new file mode 100644 index 0000000000..4809bd1a7e --- /dev/null +++ b/packages/google-datalayer/plugins/GoogleDatalayerRemoveItemFromCart.tsx @@ -0,0 +1,29 @@ +import type { RemoveItemFromCart as Original } from '@graphcommerce/magento-cart-items' +import { ReactPlugin } from '@graphcommerce/next-config' +import { removeFromCart } from '../events/remove_from_cart' + +export const component = 'RemoveItemFromCart' +export const exported = + '@graphcommerce/magento-cart-items/components/RemoveItemFromCart/RemoveItemFromCart' + +export const GoogleDatalayerRemoveItemFromCart: ReactPlugin = (props) => { + const { Prev, uid, quantity, prices, product, buttonProps } = props + + return ( + { + removeFromCart({ + __typename: 'Cart', + items: [{ uid, __typename: 'SimpleCartItem', product, quantity, prices }], + }) + buttonProps?.onClick?.(e) + }, + }} + /> + ) +} + +export const Plugin = GoogleDatalayerRemoveItemFromCart diff --git a/packages/google-datalayer/plugins/GoogleDatalayerRemoveItemFromCartFab.tsx b/packages/google-datalayer/plugins/GoogleDatalayerRemoveItemFromCartFab.tsx new file mode 100644 index 0000000000..82d687c5a0 --- /dev/null +++ b/packages/google-datalayer/plugins/GoogleDatalayerRemoveItemFromCartFab.tsx @@ -0,0 +1,29 @@ +import type { RemoveItemFromCartFab as Original } from '@graphcommerce/magento-cart-items' +import { ReactPlugin } from '@graphcommerce/next-config' +import { removeFromCart } from '../events/remove_from_cart' + +export const component = 'RemoveItemFromCartFab' +export const exported = + '@graphcommerce/magento-cart-items/components/RemoveItemFromCart/RemoveItemFromCartFab' + +export const GoogleDatalayerRemoveItemFromCartFab: ReactPlugin = (props) => { + const { Prev, uid, quantity, prices, product, fabProps } = props + + return ( + { + removeFromCart({ + __typename: 'Cart', + items: [{ uid, __typename: 'SimpleCartItem', product, quantity, prices }], + }) + fabProps?.onClick?.(e) + }, + }} + /> + ) +} + +export const Plugin = GoogleDatalayerRemoveItemFromCartFab diff --git a/packages/googleanalytics/plugins/GaShippingMethodForm.tsx b/packages/google-datalayer/plugins/GoogleDatalayerShippingMethodForm.tsx similarity index 54% rename from packages/googleanalytics/plugins/GaShippingMethodForm.tsx rename to packages/google-datalayer/plugins/GoogleDatalayerShippingMethodForm.tsx index 1ec359ab7d..c6076d864a 100644 --- a/packages/googleanalytics/plugins/GaShippingMethodForm.tsx +++ b/packages/google-datalayer/plugins/GoogleDatalayerShippingMethodForm.tsx @@ -1,23 +1,22 @@ import { ShippingMethodFormProps } from '@graphcommerce/magento-cart-shipping-method' -import { IfConfig, PluginProps } from '@graphcommerce/next-config' -import { gtagAddShippingInfo } from '../events/gtagAddShippingInfo/gtagAddShippingInfo' +import { PluginProps } from '@graphcommerce/next-config' +import { addShippingInfo } from '../events/add_shipping_info' export const component = 'ShippingMethodForm' export const exported = '@graphcommerce/magento-cart-shipping-method' -export const ifConfig: IfConfig = 'googleAnalyticsId' /** When the ShippingMethod is submitted the result is sent to Google Analytics */ -export function GaShippingMethodForm(props: PluginProps) { +export function GoogleDatalayerShippingMethodForm(props: PluginProps) { const { Prev, onComplete, ...rest } = props return ( { - gtagAddShippingInfo(result.data?.setShippingMethodsOnCart?.cart) + addShippingInfo(result.data?.setShippingMethodsOnCart?.cart) return onComplete?.(result, variables) }} /> ) } -export const Plugin = GaShippingMethodForm +export const Plugin = GoogleDatalayerShippingMethodForm diff --git a/packages/google-datalayer/plugins/GoogleDatalayerUpdateItemQuantity.tsx b/packages/google-datalayer/plugins/GoogleDatalayerUpdateItemQuantity.tsx new file mode 100644 index 0000000000..d400b5651d --- /dev/null +++ b/packages/google-datalayer/plugins/GoogleDatalayerUpdateItemQuantity.tsx @@ -0,0 +1,65 @@ +import type { UpdateItemQuantityProps } from '@graphcommerce/magento-cart-items' +import { PluginProps } from '@graphcommerce/next-config' +import { event } from '../lib/event' + +export const component = 'UpdateItemQuantity' +export const exported = + '@graphcommerce/magento-cart-items/components/UpdateItemQuantity/UpdateItemQuantity' + +/** + * When a product is added to the Cart, by using the + button on cart page, send a Google Analytics + * event + */ +function GoogleDatalayerUpdateItemQuantity(props: PluginProps) { + const { Prev, formOptions, quantity, ...rest } = props + + return ( + { + const original = formOptions?.onComplete?.(data, variables) + const diffQuantity = variables.quantity - quantity + if (diffQuantity === 0) return original + + const itemId = variables.uid + const addedItem = data.data?.updateCartItems?.cart.items?.find( + (item) => item?.uid === itemId, + ) + + if (addedItem && addedItem.prices && addedItem.prices.row_total_including_tax.value) { + // we need to manually calculate pricePerItemInclTax (https://github.com/magento/magento2/issues/33848) + const pricePerItemInclTax = + addedItem.prices.row_total_including_tax.value / addedItem.quantity + const addToCartValue = pricePerItemInclTax * diffQuantity + + event('add_to_cart', { + currency: addedItem?.prices?.price.currency, + value: addToCartValue, + items: [ + { + item_id: addedItem?.product.sku, + item_name: addedItem?.product.name, + currency: addedItem?.prices?.price.currency, + price: pricePerItemInclTax, + quantity: variables.quantity, + discount: addedItem?.prices?.discounts?.reduce( + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + (sum, discount) => sum + (discount?.amount?.value ?? 0), + 0, + ), + }, + ], + }) + } + + return original + }, + }} + /> + ) +} + +export const Plugin = GoogleDatalayerUpdateItemQuantity diff --git a/packages/googleanalytics/plugins/GaViewItem.tsx b/packages/google-datalayer/plugins/GoogleDatalayerViewItem.tsx similarity index 56% rename from packages/googleanalytics/plugins/GaViewItem.tsx rename to packages/google-datalayer/plugins/GoogleDatalayerViewItem.tsx index 3dad858841..d4ae87ab9d 100644 --- a/packages/googleanalytics/plugins/GaViewItem.tsx +++ b/packages/google-datalayer/plugins/GoogleDatalayerViewItem.tsx @@ -1,27 +1,29 @@ import type { ProductPageMeta } from '@graphcommerce/magento-product' -import { IfConfig, PluginProps } from '@graphcommerce/next-config' +import { PluginProps } from '@graphcommerce/next-config' import { useMemoObject } from '@graphcommerce/next-ui' -import { useEffect } from 'react' -import { productToGtagItem } from '../events/productToGtagItem/productToGtagItem' +import React, { useEffect } from 'react' +import { productToItem } from '../lib' +import { event } from '../lib/event' export const component = 'ProductPageMeta' export const exported = '@graphcommerce/magento-product' -export const ifConfig: IfConfig = 'googleAnalyticsId' /** When a product is added to the Cart, send a Google Analytics event */ -function GaViewItem(props: PluginProps>) { +function GoogleDatalayerViewItem(props: PluginProps>) { const { Prev, product } = props const { price_range } = product const viewItem = useMemoObject({ currency: price_range.minimum_price.final_price.currency, value: price_range.minimum_price.final_price.value, - items: [productToGtagItem(product)], + items: [productToItem(product)], }) // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - useEffect(() => globalThis.gtag?.('event', 'view_item', viewItem), [viewItem]) + useEffect(() => { + event('view_item', viewItem) + }, [viewItem]) return } -export const Plugin = GaViewItem +export const Plugin = GoogleDatalayerViewItem diff --git a/packages/google-datalayer/tsconfig.json b/packages/google-datalayer/tsconfig.json new file mode 100644 index 0000000000..7398153dd6 --- /dev/null +++ b/packages/google-datalayer/tsconfig.json @@ -0,0 +1,5 @@ +{ + "exclude": ["**/node_modules", "**/.*/"], + "include": ["**/*.ts", "**/*.tsx"], + "extends": "@graphcommerce/typescript-config-pwa/nextjs.json" +} diff --git a/packages/googleanalytics/README.md b/packages/googleanalytics/README.md index 16a08a7422..00ccb5217a 100644 --- a/packages/googleanalytics/README.md +++ b/packages/googleanalytics/README.md @@ -20,3 +20,7 @@ Besides the GA4 integration it also tracks the following events: Configure the following ([configuration values](./Config.graphqls)) in your graphcommerce.config.js + +Make sure you also configure the 'Page changes based on browser history events.' +configuration in Google Analytics, see +[docs](https://developers.google.com/analytics/devguides/collection/ga4/single-page-applications?implementation=browser-history#implement_single-page_application_measurement). diff --git a/packages/googleanalytics/components/GoogleAnalyticsItemList.tsx b/packages/googleanalytics/components/GoogleAnalyticsItemList.tsx deleted file mode 100644 index 52f6d7db73..0000000000 --- a/packages/googleanalytics/components/GoogleAnalyticsItemList.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { nonNullable, useMemoObject } from '@graphcommerce/next-ui' -import { useEventCallback } from '@mui/material' -import React, { useContext, useEffect, useMemo } from 'react' -import { ProductToGtagItemFragment } from '../events/productToGtagItem/ProductToGtagItem.gql' -import { GtagItem, productToGtagItem } from '../events/productToGtagItem/productToGtagItem' - -export type UseGtagViewItemListProps< - P extends ProductToGtagItemFragment = ProductToGtagItemFragment, -> = { - title: string - items?: (P | null | undefined)[] | null - listId?: string -} - -export type ViewItemList = { - item_list_id: string - item_list_name: string - items: GtagItem[] -} - -const GoogleAnalyticsItemListContext = React.createContext<{ - item_list_id: string - item_list_name: string -}>({ item_list_id: '', item_list_name: '' }) - -export function useGoogleAnalyticsListItemHandler(item: ProductToGtagItemFragment) { - const { item_list_id, item_list_name } = useContext(GoogleAnalyticsItemListContext) - return useEventCallback(() => - globalThis.gtag?.('event', 'select_item', { - item_list_id, - item_list_name, - items: productToGtagItem(item), - }), - ) -} - -export function GoogleAnalyticsItemList< - P extends ProductToGtagItemFragment = ProductToGtagItemFragment, ->(props: UseGtagViewItemListProps

& { children: React.ReactNode }) { - const { title, items, listId, children } = props - - const eventData: ViewItemList = useMemoObject({ - item_list_id: listId ?? title?.toLowerCase().replace(/\s/g, '_'), - item_list_name: title, - items: items?.map((item) => (item ? productToGtagItem(item) : null)).filter(nonNullable) ?? [], - }) - - useEffect(() => globalThis.gtag?.('event', 'view_item_list', eventData), [eventData]) - - const value = useMemo( - () => ({ - item_list_id: listId ?? title?.toLowerCase().replace(/\s/g, '_'), - item_list_name: title ?? listId, - }), - [listId, title], - ) - - return ( - - {children} - - ) -} diff --git a/packages/googleanalytics/components/index.ts b/packages/googleanalytics/components/index.ts index aa493a4775..94af146d46 100644 --- a/packages/googleanalytics/components/index.ts +++ b/packages/googleanalytics/components/index.ts @@ -1,2 +1 @@ export * from './GoogleAnalyticsScript' -export * from './GoogleAnalyticsItemList' diff --git a/packages/googleanalytics/events/gtagAddToCart/gtagAddToCart.ts b/packages/googleanalytics/events/gtagAddToCart/gtagAddToCart.ts deleted file mode 100644 index 2a96fc25e2..0000000000 --- a/packages/googleanalytics/events/gtagAddToCart/gtagAddToCart.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { FetchResult } from '@graphcommerce/graphql' -import { AddProductsToCartMutation, AddProductsToCartFields } from '@graphcommerce/magento-product' - -export const gtagAddToCart = ( - result: FetchResult, - variables: AddProductsToCartFields, -) => { - const addedItem = result.data?.addProductsToCart?.cart.items?.slice(-1)[0] - - if (addedItem && addedItem.prices && addedItem.prices.row_total_including_tax.value) { - // we need to manually calculate pricePerItemInclTax (https://github.com/magento/magento2/issues/33848) - const pricePerItemInclTax = addedItem.prices.row_total_including_tax.value / addedItem.quantity - const addToCartValue = pricePerItemInclTax * variables.cartItems[0].quantity - - globalThis.gtag?.('event', 'add_to_cart', { - currency: addedItem?.prices?.price.currency, - value: addToCartValue, - items: [ - { - item_id: addedItem?.product.sku, - item_name: addedItem?.product.name, - currency: addedItem?.prices?.price.currency, - price: pricePerItemInclTax, - quantity: variables.cartItems[0].quantity, - discount: addedItem?.prices?.discounts?.reduce( - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - (sum, discount) => sum + (discount?.amount?.value ?? 0), - 0, - ), - }, - ], - }) - } -} diff --git a/packages/googleanalytics/index.ts b/packages/googleanalytics/index.ts deleted file mode 100644 index 4a9d3e721e..0000000000 --- a/packages/googleanalytics/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './components' -export * from './events/gtagBeginCheckout/gtagBeginCheckout' -export * from './events/gtagAddPaymentInfo/gtagAddPaymentInfo' -export * from './events/gtagAddPurchaseInfo/gtagAddPurchaseInfo' -export * from './events/gtagAddShippingInfo/gtagAddShippingInfo' -export * from './events/gtagAddToCart/gtagAddToCart' diff --git a/packages/googleanalytics/package.json b/packages/googleanalytics/package.json index 78a574de3c..592f8b6439 100644 --- a/packages/googleanalytics/package.json +++ b/packages/googleanalytics/package.json @@ -14,6 +14,9 @@ "devDependencies": { "@types/gtag.js": "^0.0.18" }, + "dependencies": { + "@graphcommerce/google-datalayer": "8.0.4-canary.0" + }, "peerDependencies": { "@graphcommerce/eslint-config-pwa": "^8.0.4-canary.0", "@graphcommerce/graphql-mesh": "^8.0.4-canary.0", diff --git a/packages/googleanalytics/plugins/GaAddProductsToCartForm.tsx b/packages/googleanalytics/plugins/GaAddProductsToCartForm.tsx deleted file mode 100644 index 542df4df66..0000000000 --- a/packages/googleanalytics/plugins/GaAddProductsToCartForm.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { AddProductsToCartFormProps } from '@graphcommerce/magento-product' -import { IfConfig, PluginProps } from '@graphcommerce/next-config' -import { gtagAddToCart } from '../events/gtagAddToCart/gtagAddToCart' - -export const component = 'AddProductsToCartForm' -export const exported = '@graphcommerce/magento-product' -export const ifConfig: IfConfig = 'googleAnalyticsId' - -/** When a product is added to the Cart, send a Google Analytics event */ -function GaAddProductsToCartForm(props: PluginProps) { - const { Prev, onComplete, ...rest } = props - - return ( - { - gtagAddToCart(data, variables) - return onComplete?.(data, variables) - }} - /> - ) -} - -export const Plugin = GaAddProductsToCartForm diff --git a/packages/googleanalytics/plugins/GaCartStartCheckout.tsx b/packages/googleanalytics/plugins/GaCartStartCheckout.tsx deleted file mode 100644 index fb6f0cdeb8..0000000000 --- a/packages/googleanalytics/plugins/GaCartStartCheckout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { CartStartCheckoutProps } from '@graphcommerce/magento-cart' -import { IfConfig, PluginProps } from '@graphcommerce/next-config' -import { gtagBeginCheckout } from '../events/gtagBeginCheckout/gtagBeginCheckout' - -export const component = 'CartStartCheckout' -export const exported = '@graphcommerce/magento-cart' -export const ifConfig: IfConfig = 'googleAnalyticsId' - -export function GaCartStartCheckout(props: PluginProps) { - const { Prev, onStart, ...rest } = props - return ( - { - gtagBeginCheckout(cart) - return onStart?.(e, cart) - }} - /> - ) -} - -export const Plugin = GaCartStartCheckout diff --git a/packages/googleanalytics/plugins/GaProductListItem.tsx b/packages/googleanalytics/plugins/GaProductListItem.tsx deleted file mode 100644 index 042f63aefa..0000000000 --- a/packages/googleanalytics/plugins/GaProductListItem.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { ProductListItemProps } from '@graphcommerce/magento-product' -import { IfConfig, PluginProps } from '@graphcommerce/next-config' -import { useEventCallback } from '@mui/material' -import { useGoogleAnalyticsListItemHandler } from '../components/GoogleAnalyticsItemList' - -export const component = 'ProductListItem' -export const exported = '@graphcommerce/magento-product' -export const ifConfig: IfConfig = 'googleAnalyticsId' - -function GaProductListItemsBase(props: PluginProps) { - const { Prev, onClick, ...rest } = props - const handle = useGoogleAnalyticsListItemHandler(rest) - - return ( - { - handle() - onClick?.(e, item) - })} - /> - ) -} - -export const Plugin = GaProductListItemsBase diff --git a/packages/googleanalytics/plugins/GaProductListItemsBase.tsx b/packages/googleanalytics/plugins/GaProductListItemsBase.tsx deleted file mode 100644 index 8390a7b8db..0000000000 --- a/packages/googleanalytics/plugins/GaProductListItemsBase.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { ProductItemsGridProps } from '@graphcommerce/magento-product' -import { IfConfig, PluginProps } from '@graphcommerce/next-config' -import { GoogleAnalyticsItemList } from '../components/GoogleAnalyticsItemList' - -export const component = 'ProductListItemsBase' -export const exported = '@graphcommerce/magento-product' -export const ifConfig: IfConfig = 'googleAnalyticsId' - -export function GaProductListItemsBase(props: PluginProps) { - const { Prev, ...rest } = props - return ( - - - - ) -} - -export const Plugin = GaProductListItemsBase diff --git a/packages/googleanalytics/plugins/GaRemoveItemFromCart.tsx b/packages/googleanalytics/plugins/GaRemoveItemFromCart.tsx deleted file mode 100644 index b224d8111e..0000000000 --- a/packages/googleanalytics/plugins/GaRemoveItemFromCart.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { RemoveItemFromCart } from '@graphcommerce/magento-cart-items' -import { IfConfig, ReactPlugin } from '@graphcommerce/next-config' -import { gtagRemoveFromCart } from '../events/gtagRemoveFromCart/gtagRemoveFromCart' - -export const component = 'RemoveItemFromCart' -export const exported = '@graphcommerce/magento-cart-items/components/RemoveItemFromCart/RemoveItemFromCart' -export const ifConfig: IfConfig = 'googleAnalyticsId' - -export const GaRemoveItemFromCart: ReactPlugin = (props) => { - const { Prev, uid, quantity, prices, product, buttonProps } = props - - return ( - { - gtagRemoveFromCart({ - __typename: 'Cart', - items: [{ uid, __typename: 'SimpleCartItem', product, quantity, prices }], - }) - buttonProps?.onClick?.(e) - }, - }} - /> - ) -} - -export const Plugin = GaRemoveItemFromCart diff --git a/packages/googleanalytics/plugins/GaRemoveItemFromCartFab.tsx b/packages/googleanalytics/plugins/GaRemoveItemFromCartFab.tsx deleted file mode 100644 index 5677bc912b..0000000000 --- a/packages/googleanalytics/plugins/GaRemoveItemFromCartFab.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { RemoveItemFromCartFab } from '@graphcommerce/magento-cart-items' -import { IfConfig, ReactPlugin } from '@graphcommerce/next-config' -import { gtagRemoveFromCart } from '../events/gtagRemoveFromCart/gtagRemoveFromCart' - -export const component = 'RemoveItemFromCartFab' -export const exported = '@graphcommerce/magento-cart-items/components/RemoveItemFromCart/RemoveItemFromCartFab' -export const ifConfig: IfConfig = 'googleAnalyticsId' - -export const GaRemoveItemFromCartFab: ReactPlugin = (props) => { - const { Prev, uid, quantity, prices, product, fabProps } = props - - return ( - { - gtagRemoveFromCart({ - __typename: 'Cart', - items: [{ uid, __typename: 'SimpleCartItem', product, quantity, prices }], - }) - fabProps?.onClick?.(e) - }, - }} - /> - ) -} - -export const Plugin = GaRemoveItemFromCartFab diff --git a/packages/googleanalytics/plugins/GaUpdateItemQuantity.tsx b/packages/googleanalytics/plugins/GaUpdateItemQuantity.tsx deleted file mode 100644 index 821b5499ee..0000000000 --- a/packages/googleanalytics/plugins/GaUpdateItemQuantity.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type { UpdateItemQuantityProps } from '@graphcommerce/magento-cart-items' -import { IfConfig, PluginProps } from '@graphcommerce/next-config' - -export const component = 'UpdateItemQuantity' -export const exported = '@graphcommerce/magento-cart-items/components/UpdateItemQuantity/UpdateItemQuantity' -export const ifConfig: IfConfig = 'googleAnalyticsId' - -/** - * When a product is added to the Cart, by using the + button on cart page, send a Google Analytics - * event - */ -function GaUpdateItemQuantity(props: PluginProps) { - const { Prev, onComplete, quantity, ...rest } = props - - return ( - { - const original = onComplete?.(data, variables) - const diffQuantity = variables.quantity - quantity - if (diffQuantity === 0) return original - - const itemId = variables.uid - const addedItem = data.data?.updateCartItems?.cart.items?.find( - (item) => item?.uid === itemId, - ) - - if (addedItem && addedItem.prices && addedItem.prices.row_total_including_tax.value) { - // we need to manually calculate pricePerItemInclTax (https://github.com/magento/magento2/issues/33848) - const pricePerItemInclTax = - addedItem.prices.row_total_including_tax.value / addedItem.quantity - const addToCartValue = pricePerItemInclTax * diffQuantity - - globalThis.gtag?.('event', 'add_to_cart', { - currency: addedItem?.prices?.price.currency, - value: addToCartValue, - items: [ - { - item_id: addedItem?.product.sku, - item_name: addedItem?.product.name, - currency: addedItem?.prices?.price.currency, - price: pricePerItemInclTax, - quantity: variables.quantity, - discount: addedItem?.prices?.discounts?.reduce( - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - (sum, discount) => sum + (discount?.amount?.value ?? 0), - 0, - ), - }, - ], - }) - } - - return original - }} - /> - ) -} - -export const Plugin = GaUpdateItemQuantity diff --git a/packages/googletagmanager/components/GoogleTagManagerScript.tsx b/packages/googletagmanager/components/GoogleTagManagerScript.tsx index 19bec20dbf..81288e7952 100644 --- a/packages/googletagmanager/components/GoogleTagManagerScript.tsx +++ b/packages/googletagmanager/components/GoogleTagManagerScript.tsx @@ -1,23 +1,10 @@ import { useStorefrontConfig } from '@graphcommerce/next-ui' -import { useRouter } from 'next/router' import Script from 'next/script' -import { useEffect } from 'react' export function GoogleTagManagerScript() { - const { events } = useRouter() const id = useStorefrontConfig().googleTagmanagerId ?? import.meta.graphCommerce.googleTagmanagerId - useEffect(() => { - const onRouteChangeComplete = (url: string) => { - const dataLayer = globalThis.dataLayer as Record[] | undefined - dataLayer?.push({ event: 'pageview', page: url }) - } - - events.on('routeChangeComplete', onRouteChangeComplete) - return () => events.off('routeChangeComplete', onRouteChangeComplete) - }, [events]) - return ( <>