@graphcommerce/google-datalayer 8.0.4-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/Config.graphqls +21 -0
  3. package/README.md +6 -0
  4. package/components/AnalyticsItemList.tsx +63 -0
  5. package/events/add_payment_info/AddPaymentInfoFragment.graphql +36 -0
  6. package/events/add_payment_info/addPaymentInfo.ts +23 -0
  7. package/events/add_payment_info/index.ts +2 -0
  8. package/events/add_shipping_info/AddSchippingInfoFragment.graphql +38 -0
  9. package/events/add_shipping_info/addShippingInfo.ts +22 -0
  10. package/events/add_shipping_info/index.ts +2 -0
  11. package/events/add_to_cart/AddToCartFragment.graphql +30 -0
  12. package/events/add_to_cart/addToCart.ts +58 -0
  13. package/events/add_to_cart/index.ts +2 -0
  14. package/events/begin_checkout/BeginCheckoutFragment.graphql +38 -0
  15. package/events/begin_checkout/beginCheckout.ts +22 -0
  16. package/events/begin_checkout/index.ts +2 -0
  17. package/events/purchase/index.ts +1 -0
  18. package/events/purchase/purchase.ts +27 -0
  19. package/events/remove_from_cart/RemoveFromCartFragment.graphql +36 -0
  20. package/events/remove_from_cart/index.ts +2 -0
  21. package/events/remove_from_cart/removeFromCart.ts +21 -0
  22. package/events/select_item/index.ts +1 -0
  23. package/events/select_item/select_item.ts +8 -0
  24. package/events/view_cart/ViewCartFragment.graphql +35 -0
  25. package/events/view_cart/index.ts +2 -0
  26. package/events/view_cart/viewCart.ts +21 -0
  27. package/events/view_item/index.ts +1 -0
  28. package/events/view_item/view_item.ts +8 -0
  29. package/events/view_item_list/index.ts +1 -0
  30. package/events/view_item_list/view_item_list.ts +8 -0
  31. package/lib/event.ts +39 -0
  32. package/lib/index.ts +2 -0
  33. package/lib/productToItem/ProductToItem.graphql +20 -0
  34. package/lib/productToItem/productToItem.ts +38 -0
  35. package/package.json +44 -0
  36. package/plugins/GoogleDatalayerAddProductsToCartForm.tsx +23 -0
  37. package/plugins/GoogleDatalayerAllPagesPageview.tsx +37 -0
  38. package/plugins/GoogleDatalayerCartStartCheckout.tsx +21 -0
  39. package/plugins/GoogleDatalayerCartStartCheckoutLinkOrButton.tsx +35 -0
  40. package/plugins/GoogleDatalayerPaymentMethodButton.tsx +26 -0
  41. package/plugins/GoogleDatalayerPaymentMethodContextProvider.tsx +22 -0
  42. package/plugins/GoogleDatalayerProductListItem.tsx +28 -0
  43. package/plugins/GoogleDatalayerProductListItemsBase.tsx +17 -0
  44. package/plugins/GoogleDatalayerRemoveItemFromCart.tsx +29 -0
  45. package/plugins/GoogleDatalayerRemoveItemFromCartFab.tsx +29 -0
  46. package/plugins/GoogleDatalayerShippingMethodForm.tsx +22 -0
  47. package/plugins/GoogleDatalayerUpdateItemQuantity.tsx +65 -0
  48. package/plugins/GoogleDatalayerViewItem.tsx +29 -0
  49. package/tsconfig.json +5 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # @graphcommerce/google-datalayer
2
+
3
+ ## 8.0.4-canary.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2158](https://github.com/graphcommerce-org/graphcommerce/pull/2158) [`34de808`](https://github.com/graphcommerce-org/graphcommerce/commit/34de8085e9352d1f3b20b26746685370ea10ab90) - 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.
8
+ ([@mikekeehnen](https://github.com/mikekeehnen))
@@ -0,0 +1,21 @@
1
+ extend input GraphCommerceConfig {
2
+ analytics: AnalyticsConfig
3
+ }
4
+
5
+ """
6
+ EventFormat is an enumatation of different event formats. This decides what the format of the event data will be.
7
+ """
8
+ enum EventFormat {
9
+ GA3
10
+ GA4
11
+ }
12
+
13
+ """
14
+ AnalyticsConfig will contain all configuration values for the analytics in GraphCommerce.
15
+ """
16
+ input AnalyticsConfig {
17
+ """
18
+ eventFormat contains the list of fired and formatted events
19
+ """
20
+ eventFormat: [EventFormat!]
21
+ }
package/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # @graphcommerce/google-datalayer
2
+
3
+ This package contains utilities that can be used in different app analytics
4
+ trackers.
5
+
6
+ add_payment_info add_shipping_info
@@ -0,0 +1,63 @@
1
+ import { nonNullable, useMemoObject } from '@graphcommerce/next-ui'
2
+ import { useEventCallback } from '@mui/material'
3
+ import React, { useContext, useEffect, useMemo } from 'react'
4
+ import { Item, ProductToItemFragment, productToItem } from '../lib'
5
+ import { event } from '../lib/event'
6
+
7
+ export type UseViewItemListProps<P extends ProductToItemFragment = ProductToItemFragment> = {
8
+ title: string
9
+ items?: (P | null | undefined)[] | null
10
+ listId?: string
11
+ }
12
+
13
+ export type ViewItemList = {
14
+ item_list_id: string
15
+ item_list_name: string
16
+ items: Item[]
17
+ }
18
+
19
+ const GoogleTagManagerItemListContext = React.createContext<{
20
+ item_list_id: string
21
+ item_list_name: string
22
+ }>({ item_list_id: '', item_list_name: '' })
23
+
24
+ export function useListItemHandler(item: ProductToItemFragment) {
25
+ const { item_list_id, item_list_name } = useContext(GoogleTagManagerItemListContext)
26
+ return useEventCallback(() =>
27
+ event('select_item', {
28
+ item_list_id,
29
+ item_list_name,
30
+ items: productToItem(item),
31
+ }),
32
+ )
33
+ }
34
+
35
+ export function ItemList<P extends ProductToItemFragment = ProductToItemFragment>(
36
+ props: UseViewItemListProps<P> & { children: React.ReactNode },
37
+ ) {
38
+ const { title, items, listId, children } = props
39
+
40
+ const eventData: ViewItemList = useMemoObject({
41
+ item_list_id: listId ?? title?.toLowerCase().replace(/\s/g, '_'),
42
+ item_list_name: title,
43
+ items: items?.map((item) => (item ? productToItem(item) : null)).filter(nonNullable) ?? [],
44
+ })
45
+
46
+ useEffect(() => {
47
+ event('view_item_list', eventData)
48
+ }, [eventData])
49
+
50
+ const value = useMemo(
51
+ () => ({
52
+ item_list_id: listId ?? title?.toLowerCase().replace(/\s/g, '_'),
53
+ item_list_name: title ?? listId,
54
+ }),
55
+ [listId, title],
56
+ )
57
+
58
+ return (
59
+ <GoogleTagManagerItemListContext.Provider value={value}>
60
+ {children}
61
+ </GoogleTagManagerItemListContext.Provider>
62
+ )
63
+ }
@@ -0,0 +1,36 @@
1
+ fragment AddPaymentInfoFragment on Cart
2
+ @inject(into: ["PaymentMethodContext", "PaymentMethodUpdated"]) {
3
+ items {
4
+ uid
5
+ prices {
6
+ price {
7
+ currency
8
+ value
9
+ }
10
+ discounts {
11
+ amount {
12
+ currency
13
+ value
14
+ }
15
+ }
16
+ }
17
+ quantity
18
+ product {
19
+ uid
20
+ name
21
+ sku
22
+ }
23
+ }
24
+ applied_coupons {
25
+ code
26
+ }
27
+ prices {
28
+ grand_total {
29
+ currency
30
+ value
31
+ }
32
+ }
33
+ selected_payment_method {
34
+ code
35
+ }
36
+ }
@@ -0,0 +1,23 @@
1
+ import { event } from '../../lib/event'
2
+ import { AddPaymentInfoFragment } from './AddPaymentInfoFragment.gql'
3
+
4
+ export const addPaymentInfo = <C extends AddPaymentInfoFragment>(cart?: C | null) =>
5
+ event('add_payment_info', {
6
+ ...cart,
7
+ currency: cart?.prices?.grand_total?.currency,
8
+ value: cart?.prices?.grand_total?.value,
9
+ coupon: cart?.applied_coupons?.map((coupon) => coupon?.code),
10
+ payment_type: cart?.selected_payment_method?.code,
11
+ items: cart?.items?.map((item) => ({
12
+ item_id: item?.product.sku,
13
+ item_name: item?.product.name,
14
+ currency: item?.prices?.price.currency,
15
+ discount: item?.prices?.discounts?.reduce(
16
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
17
+ (sum, discount) => sum + (discount?.amount?.value ?? 0),
18
+ 0,
19
+ ),
20
+ price: item?.prices?.price.value,
21
+ quantity: item?.quantity,
22
+ })),
23
+ })
@@ -0,0 +1,2 @@
1
+ export * from './addPaymentInfo'
2
+ export * from './AddPaymentInfoFragment.gql'
@@ -0,0 +1,38 @@
1
+ fragment AddShippingInfoFragment on Cart @inject(into: ["ShippingMethodSelected"]) {
2
+ prices {
3
+ grand_total {
4
+ currency
5
+ value
6
+ }
7
+ }
8
+ applied_coupons {
9
+ code
10
+ }
11
+ items {
12
+ __typename
13
+ uid
14
+ product {
15
+ uid
16
+ sku
17
+ name
18
+ }
19
+ prices {
20
+ price {
21
+ value
22
+ currency
23
+ }
24
+ discounts {
25
+ amount {
26
+ value
27
+ }
28
+ }
29
+ }
30
+ quantity
31
+ ... on ConfigurableCartItem {
32
+ configured_variant {
33
+ uid
34
+ sku
35
+ }
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,22 @@
1
+ import { event } from '../../lib/event'
2
+ import { AddShippingInfoFragment } from './AddSchippingInfoFragment.gql'
3
+
4
+ export const addShippingInfo = <C extends AddShippingInfoFragment>(cart?: C) =>
5
+ event('add_shipping_info', {
6
+ ...cart,
7
+ currency: cart?.prices?.grand_total?.currency,
8
+ value: cart?.prices?.grand_total?.value,
9
+ coupon: cart?.applied_coupons?.map((coupon) => coupon?.code),
10
+ items: cart?.items?.map((item) => ({
11
+ item_id: item?.product.sku,
12
+ item_name: item?.product.name,
13
+ currency: item?.prices?.price.currency,
14
+ discount: item?.prices?.discounts?.reduce(
15
+ (sum, discount) => sum + (discount?.amount?.value ?? 0),
16
+ 0,
17
+ ),
18
+ item_variant: item && 'configured_variant' in item ? item.configured_variant.sku : '',
19
+ price: item?.prices?.price.value,
20
+ quantity: item?.quantity,
21
+ })),
22
+ })
@@ -0,0 +1,2 @@
1
+ export * from './addShippingInfo'
2
+ export * from './AddSchippingInfoFragment.gql'
@@ -0,0 +1,30 @@
1
+ fragment AddToCartFragment on Cart @inject(into: ["CartItemCountChanged"]) {
2
+ items {
3
+ quantity
4
+ product {
5
+ sku
6
+ name
7
+ }
8
+ prices {
9
+ discounts {
10
+ amount {
11
+ currency
12
+ value
13
+ }
14
+ }
15
+ row_total_including_tax {
16
+ currency
17
+ value
18
+ }
19
+ price {
20
+ currency
21
+ value
22
+ }
23
+ }
24
+ }
25
+ prices {
26
+ grand_total {
27
+ currency
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,58 @@
1
+ import type { FetchResult } from '@graphcommerce/graphql'
2
+ import {
3
+ AddProductsToCartFields,
4
+ AddProductsToCartMutation,
5
+ findAddedItems,
6
+ toUserErrors,
7
+ } from '@graphcommerce/magento-product'
8
+ import { nonNullable } from '@graphcommerce/next-ui'
9
+ import { event } from '../../lib/event'
10
+
11
+ export const addToCart = (
12
+ result: FetchResult<AddProductsToCartMutation>,
13
+ variables: AddProductsToCartFields,
14
+ ) => {
15
+ const { data, errors } = result
16
+ const cart = data?.addProductsToCart?.cart
17
+
18
+ const addedItems = findAddedItems(data, variables)
19
+
20
+ const items = addedItems
21
+ .map(({ itemVariable, itemInCart }) => {
22
+ if (!itemInCart) return null
23
+ const { product, prices } = itemInCart
24
+ return {
25
+ item_id: product.sku,
26
+ item_name: product.name,
27
+ currency: prices?.price.currency,
28
+ price: (prices?.row_total_including_tax.value ?? 1) / itemInCart.quantity,
29
+ quantity: itemVariable.quantity,
30
+ discount: prices?.discounts?.reduce(
31
+ (sum, discount) => sum + (discount?.amount?.value ?? 0) / itemVariable.quantity,
32
+ 0,
33
+ ),
34
+ }
35
+ })
36
+ .filter(nonNullable)
37
+
38
+ const userErrors = toUserErrors(result.data)
39
+ if ((errors && errors.length > 0) || userErrors.length > 0) {
40
+ event('add_to_cart_error', {
41
+ userErrors: userErrors?.map((e) => e.message),
42
+ errors: errors?.map((e) => e.message),
43
+ variables,
44
+ })
45
+ }
46
+
47
+ if (!items.length) return
48
+
49
+ event('add_to_cart', {
50
+ currency: cart?.prices?.grand_total?.currency,
51
+ value: addedItems.reduce(
52
+ (sum, { itemVariable, itemInCart }) =>
53
+ sum + (itemInCart?.prices?.row_total_including_tax.value ?? 1) / itemVariable.quantity,
54
+ 0,
55
+ ),
56
+ items,
57
+ })
58
+ }
@@ -0,0 +1,2 @@
1
+ export * from './addToCart'
2
+ export * from './AddToCartFragment.gql'
@@ -0,0 +1,38 @@
1
+ fragment BeginCheckoutFragment on Cart @inject(into: ["CartStartCheckout"]) {
2
+ prices {
3
+ grand_total {
4
+ currency
5
+ value
6
+ }
7
+ }
8
+ applied_coupons {
9
+ code
10
+ }
11
+ items {
12
+ __typename
13
+ uid
14
+ product {
15
+ uid
16
+ sku
17
+ name
18
+ }
19
+ prices {
20
+ price {
21
+ value
22
+ currency
23
+ }
24
+ discounts {
25
+ amount {
26
+ value
27
+ }
28
+ }
29
+ }
30
+ quantity
31
+ ... on ConfigurableCartItem {
32
+ configured_variant {
33
+ uid
34
+ sku
35
+ }
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,22 @@
1
+ import { event } from '../../lib/event'
2
+ import { BeginCheckoutFragment } from './BeginCheckoutFragment.gql'
3
+
4
+ export const beginCheckout = <C extends BeginCheckoutFragment>(cart?: C | null) =>
5
+ event('begin_checkout', {
6
+ ...cart,
7
+ currency: cart?.prices?.grand_total?.currency,
8
+ value: cart?.prices?.grand_total?.value,
9
+ coupon: cart?.applied_coupons?.map((coupon) => coupon?.code),
10
+ items: cart?.items?.map((item) => ({
11
+ item_id: item?.product.sku,
12
+ item_name: item?.product.name,
13
+ currency: item?.prices?.price.currency,
14
+ discount: item?.prices?.discounts?.reduce(
15
+ (sum, discount) => sum + (discount?.amount?.value ?? 0),
16
+ 0,
17
+ ),
18
+ item_variant: item?.__typename === 'ConfigurableCartItem' ? item.configured_variant.sku : '',
19
+ price: item?.prices?.price.value,
20
+ quantity: item?.quantity,
21
+ })),
22
+ })
@@ -0,0 +1,2 @@
1
+ export * from './beginCheckout'
2
+ export * from './BeginCheckoutFragment.gql'
@@ -0,0 +1 @@
1
+ export * from './purchase'
@@ -0,0 +1,27 @@
1
+ import { PaymentMethodContextFragment } from '@graphcommerce/magento-cart-payment-method/Api/PaymentMethodContext.gql'
2
+ import { event } from '../../lib/event'
3
+
4
+ export const purchase = <C extends PaymentMethodContextFragment>(
5
+ orderNumber: string,
6
+ cart: C | null | undefined,
7
+ ) =>
8
+ event('purchase', {
9
+ ...cart,
10
+ transaction_id: orderNumber,
11
+ currency: cart?.prices?.grand_total?.currency,
12
+ value: cart?.prices?.grand_total?.value,
13
+ coupon: cart?.applied_coupons?.map((coupon) => coupon?.code).join(' '),
14
+ payment_type: cart?.selected_payment_method?.code,
15
+ tax: cart?.prices?.applied_taxes?.reduce((sum, tax) => sum + (tax?.amount?.value ?? 0), 0),
16
+ items: cart?.items?.map((item) => ({
17
+ item_id: item?.product.sku,
18
+ item_name: item?.product.name,
19
+ currency: item?.prices?.price.currency,
20
+ discount: item?.prices?.discounts?.reduce(
21
+ (sum, discount) => sum + (discount?.amount?.value ?? 0),
22
+ 0,
23
+ ),
24
+ price: item?.prices?.price.value,
25
+ quantity: item?.quantity,
26
+ })),
27
+ })
@@ -0,0 +1,36 @@
1
+ fragment RemoveFromCartFragment on Cart @inject(into: ["CartItemCountChanged"]) {
2
+ __typename
3
+ prices {
4
+ grand_total {
5
+ currency
6
+ value
7
+ }
8
+ }
9
+ items {
10
+ __typename
11
+ uid
12
+ product {
13
+ uid
14
+ sku
15
+ name
16
+ }
17
+ prices {
18
+ price {
19
+ value
20
+ currency
21
+ }
22
+ discounts {
23
+ amount {
24
+ value
25
+ }
26
+ }
27
+ }
28
+ quantity
29
+ ... on ConfigurableCartItem {
30
+ configured_variant {
31
+ uid
32
+ sku
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,2 @@
1
+ export * from './removeFromCart'
2
+ export * from './RemoveFromCartFragment.gql'
@@ -0,0 +1,21 @@
1
+ import { event } from '../../lib/event'
2
+ import { RemoveFromCartFragment } from './RemoveFromCartFragment.gql'
3
+
4
+ export const removeFromCart = <C extends RemoveFromCartFragment>(cart: C | null | undefined) =>
5
+ event('remove_from_cart', {
6
+ ...cart,
7
+ currency: cart?.prices?.grand_total?.currency,
8
+ value: cart?.prices?.grand_total?.value,
9
+ items: cart?.items?.map((item) => ({
10
+ item_id: item?.product.sku,
11
+ item_name: item?.product.name,
12
+ currency: item?.prices?.price.currency,
13
+ discount: item?.prices?.discounts?.reduce(
14
+ (sum, discount) => sum + (discount?.amount?.value ?? 0),
15
+ 0,
16
+ ),
17
+ item_variant: item?.__typename === 'ConfigurableCartItem' ? item.configured_variant.sku : '',
18
+ price: item?.prices?.price.value,
19
+ quantity: item?.quantity,
20
+ })),
21
+ })
@@ -0,0 +1 @@
1
+ export * from './select_item'
@@ -0,0 +1,8 @@
1
+ import { event } from '../../lib/event'
2
+
3
+ export const selectItem = (itemListId: string, itemListName: string, items: unknown[]) =>
4
+ event('select_item', {
5
+ item_list_id: itemListId,
6
+ item_list_name: itemListName,
7
+ items,
8
+ })
@@ -0,0 +1,35 @@
1
+ fragment ViewCartFragment on Cart @inject(into: ["CartItemCountChanged"]) {
2
+ prices {
3
+ grand_total {
4
+ currency
5
+ value
6
+ }
7
+ }
8
+ items {
9
+ __typename
10
+ uid
11
+ product {
12
+ uid
13
+ sku
14
+ name
15
+ }
16
+ prices {
17
+ price {
18
+ value
19
+ currency
20
+ }
21
+ discounts {
22
+ amount {
23
+ value
24
+ }
25
+ }
26
+ }
27
+ quantity
28
+ ... on ConfigurableCartItem {
29
+ configured_variant {
30
+ uid
31
+ sku
32
+ }
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,2 @@
1
+ export * from './viewCart'
2
+ export * from './ViewCartFragment.gql'
@@ -0,0 +1,21 @@
1
+ import { event } from '../../lib/event'
2
+ import { ViewCartFragment } from './ViewCartFragment.gql'
3
+
4
+ export const viewCart = <C extends ViewCartFragment>(cart: C | null | undefined) =>
5
+ event('view_cart', {
6
+ ...cart,
7
+ currency: cart?.prices?.grand_total?.currency,
8
+ value: cart?.prices?.grand_total?.value,
9
+ items: cart?.items?.map((item) => ({
10
+ item_id: item?.product.sku,
11
+ item_name: item?.product.name,
12
+ currency: item?.prices?.price.currency,
13
+ discount: item?.prices?.discounts?.reduce(
14
+ (sum, discount) => sum + (discount?.amount?.value ?? 0),
15
+ 0,
16
+ ),
17
+ item_variant: item?.__typename === 'ConfigurableCartItem' ? item.configured_variant.sku : '',
18
+ price: item?.prices?.price.value,
19
+ quantity: item?.quantity,
20
+ })),
21
+ })
@@ -0,0 +1 @@
1
+ export * from './view_item'
@@ -0,0 +1,8 @@
1
+ import { event } from '../../lib/event'
2
+
3
+ export const viewItem = (itemListId: string, itemListName: string, items: unknown[]) =>
4
+ event('view_item', {
5
+ item_list_id: itemListId,
6
+ item_list_name: itemListName,
7
+ items,
8
+ })
@@ -0,0 +1 @@
1
+ export * from './view_item_list'
@@ -0,0 +1,8 @@
1
+ import { event } from '../../lib/event'
2
+
3
+ export const viewItemList = (itemListId: string, itemListName: string, items: unknown[]) =>
4
+ event('view_item_list', {
5
+ item_list_id: itemListId,
6
+ item_list_name: itemListName,
7
+ items,
8
+ })
package/lib/event.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { EventFormatSchema } from '@graphcommerce/next-config'
2
+
3
+ type EventMapFunctionType = (
4
+ eventName: Gtag.EventNames | (string & Record<never, never>),
5
+ eventData: {
6
+ [key: string]: unknown
7
+ },
8
+ ) => void
9
+
10
+ type EventType = keyof (typeof EventFormatSchema)['Enum']
11
+
12
+ const eventMap: { [key in EventType]: EventMapFunctionType } = {
13
+ GA4: (eventName, eventData) => {
14
+ if (import.meta.graphCommerce.googleAnalyticsId) {
15
+ globalThis.gtag('event', eventName, eventData)
16
+ }
17
+
18
+ if (import.meta.graphCommerce.googleTagmanagerId) {
19
+ globalThis.dataLayer.push({ event: eventName, ...eventData })
20
+ }
21
+ },
22
+ GA3: (eventName, eventData) => {
23
+ if (import.meta.graphCommerce.googleAnalyticsId) {
24
+ console.warn(
25
+ "Google Analytics 3 format is not supported for Google Analytics 4. Please update your event format to 'GA4'.",
26
+ )
27
+ }
28
+
29
+ if (import.meta.graphCommerce.googleTagmanagerId) {
30
+ globalThis.dataLayer.push({ event: eventName, ecommerce: eventData })
31
+ }
32
+ },
33
+ }
34
+
35
+ const eventsToBeFired = import.meta.graphCommerce.analytics?.eventFormat ?? ['GA4']
36
+
37
+ export const event: EventMapFunctionType = (eventName, eventData) => {
38
+ eventsToBeFired.map((e) => eventMap[e](eventName, eventData))
39
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './productToItem/productToItem'
2
+ export * from './productToItem/ProductToItem.gql'
@@ -0,0 +1,20 @@
1
+ fragment ProductToItem on ProductInterface {
2
+ name
3
+ sku
4
+ price_range {
5
+ minimum_price {
6
+ final_price {
7
+ value
8
+ currency
9
+ }
10
+ discount {
11
+ amount_off
12
+ }
13
+ }
14
+ }
15
+ categories {
16
+ uid
17
+ url_key
18
+ name
19
+ }
20
+ }
@@ -0,0 +1,38 @@
1
+ import { ProductToItemFragment } from './ProductToItem.gql'
2
+
3
+ export type Item = {
4
+ item_id: string
5
+ item_name: string
6
+ affiliation?: string
7
+ coupon?: string
8
+ currency?: string
9
+ discount?: number
10
+ index?: number
11
+ item_brand?: string
12
+ item_category?: string
13
+ item_category2?: string
14
+ item_category3?: string
15
+ item_category4?: string
16
+ item_category5?: string
17
+ item_list_id?: string
18
+ item_list_name?: string
19
+ item_variant?: string
20
+ location_id?: string
21
+ price?: number
22
+ quantity?: number
23
+ }
24
+
25
+ export function productToItem<P extends ProductToItemFragment>(item: P): Item {
26
+ return {
27
+ item_id: item.sku ?? '',
28
+ item_name: item.name ?? '',
29
+ price: item.price_range?.minimum_price.final_price.value ?? undefined,
30
+ currency: item.price_range?.minimum_price.final_price.currency ?? undefined,
31
+ discount: item.price_range?.minimum_price.discount?.amount_off ?? undefined,
32
+ item_category: item.categories?.[0]?.name ?? undefined,
33
+ item_category2: item.categories?.[1]?.name ?? undefined,
34
+ item_category3: item.categories?.[2]?.name ?? undefined,
35
+ item_category4: item.categories?.[3]?.name ?? undefined,
36
+ item_category5: item.categories?.[4]?.name ?? undefined,
37
+ }
38
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@graphcommerce/google-datalayer",
3
+ "homepage": "https://www.graphcommerce.org/",
4
+ "repository": "github:graphcommerce-org/graphcommerce",
5
+ "version": "8.0.4-canary.1",
6
+ "sideEffects": false,
7
+ "prettier": "@graphcommerce/prettier-config-pwa",
8
+ "eslintConfig": {
9
+ "extends": "@graphcommerce/eslint-config-pwa",
10
+ "parserOptions": {
11
+ "project": "./tsconfig.json"
12
+ }
13
+ },
14
+ "peerDependencies": {
15
+ "@graphcommerce/eslint-config-pwa": "^8.0.4-canary.1",
16
+ "@graphcommerce/graphql-mesh": "^8.0.4-canary.1",
17
+ "@graphcommerce/magento-cart": "^8.0.4-canary.1",
18
+ "@graphcommerce/magento-cart-payment-method": "^8.0.4-canary.1",
19
+ "@graphcommerce/magento-cart-shipping-method": "^8.0.4-canary.1",
20
+ "@graphcommerce/magento-product": "^8.0.4-canary.1",
21
+ "@graphcommerce/next-config": "^8.0.4-canary.1",
22
+ "@graphcommerce/next-ui": "^8.0.4-canary.1",
23
+ "@graphcommerce/prettier-config-pwa": "^8.0.4-canary.1",
24
+ "@graphcommerce/typescript-config-pwa": "^8.0.4-canary.1",
25
+ "@mui/material": "^5.14.20",
26
+ "next": "^14",
27
+ "react": "^18.2.0",
28
+ "react-dom": "^18.2.0"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "@graphcommerce/magento-cart": {
32
+ "optional": true
33
+ },
34
+ "@graphcommerce/magento-cart-payment-method": {
35
+ "optional": true
36
+ },
37
+ "@graphcommerce/magento-cart-shipping-method": {
38
+ "optional": true
39
+ },
40
+ "@graphcommerce/magento-product": {
41
+ "optional": true
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,23 @@
1
+ import { AddProductsToCartFormProps } from '@graphcommerce/magento-product'
2
+ import { PluginProps } from '@graphcommerce/next-config'
3
+ import { addToCart } from '../events/add_to_cart'
4
+
5
+ export const component = 'AddProductsToCartForm'
6
+ export const exported = '@graphcommerce/magento-product'
7
+
8
+ /** When a product is added to the Cart, send a Google Analytics event */
9
+ function GoogleDatalayerAddProductsToCartForm(props: PluginProps<AddProductsToCartFormProps>) {
10
+ const { Prev, onComplete, ...rest } = props
11
+
12
+ return (
13
+ <Prev
14
+ {...rest}
15
+ onComplete={(result, variables) => {
16
+ addToCart(result, variables)
17
+ return onComplete?.(result, variables)
18
+ }}
19
+ />
20
+ )
21
+ }
22
+
23
+ export const Plugin = GoogleDatalayerAddProductsToCartForm
@@ -0,0 +1,37 @@
1
+ import type { PagesProps } from '@graphcommerce/framer-next-pages'
2
+ import type { PluginProps } from '@graphcommerce/next-config'
3
+ import { useRouter } from 'next/router'
4
+ import { useEffect } from 'react'
5
+ import { event } from '../lib/event'
6
+
7
+ export const component = 'FramerNextPages'
8
+ export const exported = '@graphcommerce/framer-next-pages'
9
+
10
+ function GoogleDatalayerAllPagesPageview(props: PluginProps<PagesProps>) {
11
+ const { Prev, ...rest } = props
12
+
13
+ const { events } = useRouter()
14
+
15
+ useEffect(() => {
16
+ const onRouteChangeComplete = (url: string) => {
17
+ /**
18
+ * Todo: the actual page_view event is currently disabled, because we run the risk of double counting page views.
19
+ * https://developers.google.com/analytics/devguides/collection/ga4/views?client_type=gtag#manually_send_page_view_events
20
+ *
21
+ * https://developers.google.com/analytics/devguides/collection/ga4/single-page-applications?implementation=event
22
+ */
23
+ // event('page_view', {
24
+ // page_title: '<Page Title>',
25
+ // page_location: '<Page Location>',
26
+ // })
27
+ // event('pageview', { page: url })
28
+ }
29
+
30
+ events.on('routeChangeComplete', onRouteChangeComplete)
31
+ return () => events.off('routeChangeComplete', onRouteChangeComplete)
32
+ }, [events])
33
+
34
+ return <Prev {...rest} />
35
+ }
36
+
37
+ export const Plugin = GoogleDatalayerAllPagesPageview
@@ -0,0 +1,21 @@
1
+ import { CartStartCheckoutProps } from '@graphcommerce/magento-cart'
2
+ import { PluginProps } from '@graphcommerce/next-config'
3
+ import { beginCheckout } from '../events/begin_checkout'
4
+
5
+ export const component = 'CartStartCheckout'
6
+ export const exported = '@graphcommerce/magento-cart'
7
+
8
+ export function GoogleDatalayerCartStartCheckout(props: PluginProps<CartStartCheckoutProps>) {
9
+ const { Prev, onStart, ...rest } = props
10
+ return (
11
+ <Prev
12
+ {...rest}
13
+ onStart={(e, cart) => {
14
+ beginCheckout(cart)
15
+ return onStart?.(e, cart)
16
+ }}
17
+ />
18
+ )
19
+ }
20
+
21
+ export const Plugin = GoogleDatalayerCartStartCheckout
@@ -0,0 +1,35 @@
1
+ import { CartStartCheckoutLinkOrButtonProps } from '@graphcommerce/magento-cart'
2
+ import { PluginProps } from '@graphcommerce/next-config'
3
+ import { useMemoObject } from '@graphcommerce/next-ui'
4
+ import { useEffect } from 'react'
5
+ import { beginCheckout } from '../events/begin_checkout'
6
+ import { viewCart } from '../events/view_cart'
7
+
8
+ export const component = 'CartStartCheckoutLinkOrButton'
9
+ export const exported = '@graphcommerce/magento-cart'
10
+
11
+ export function GoogleDatalayerCartStartCheckoutLinkOrButton(
12
+ props: PluginProps<CartStartCheckoutLinkOrButtonProps>,
13
+ ) {
14
+ const { Prev, onStart, ...rest } = props
15
+
16
+ const cartObject = useMemoObject({ items: rest.items, prices: rest.prices })
17
+
18
+ useEffect(() => {
19
+ if (cartObject.items) {
20
+ viewCart(cartObject)
21
+ }
22
+ }, [cartObject])
23
+
24
+ return (
25
+ <Prev
26
+ {...rest}
27
+ onStart={(e, cart) => {
28
+ beginCheckout(cart)
29
+ return onStart?.(e, cart)
30
+ }}
31
+ />
32
+ )
33
+ }
34
+
35
+ export const Plugin = GoogleDatalayerCartStartCheckoutLinkOrButton
@@ -0,0 +1,26 @@
1
+ import { useCartQuery } from '@graphcommerce/magento-cart'
2
+ import { PaymentMethodButtonProps } from '@graphcommerce/magento-cart-payment-method'
3
+ import { GetPaymentMethodContextDocument } from '@graphcommerce/magento-cart-payment-method/PaymentMethodContext/GetPaymentMethodContext.gql'
4
+ import { PluginProps } from '@graphcommerce/next-config'
5
+ import { addPaymentInfo } from '../events/add_payment_info'
6
+
7
+ export const component = 'PaymentMethodButton'
8
+ export const exported = '@graphcommerce/magento-cart-payment-method'
9
+
10
+ // @todo This plugin can probably be migrated to the actual form that is submitted.
11
+ function GoogleDatalayerPaymentMethodButton(props: PluginProps<PaymentMethodButtonProps>) {
12
+ const { Prev, onSubmitSuccessful, ...rest } = props
13
+ const methodContext = useCartQuery(GetPaymentMethodContextDocument)
14
+
15
+ return (
16
+ <Prev
17
+ {...rest}
18
+ onSubmitSuccessful={() => {
19
+ addPaymentInfo(methodContext.data?.cart)
20
+ return onSubmitSuccessful?.()
21
+ }}
22
+ />
23
+ )
24
+ }
25
+
26
+ export const Plugin = GoogleDatalayerPaymentMethodButton
@@ -0,0 +1,22 @@
1
+ import type { PaymentMethodContextProviderProps } from '@graphcommerce/magento-cart-payment-method'
2
+ import type { PluginProps } from '@graphcommerce/next-config'
3
+ import { purchase } from '../events/purchase'
4
+
5
+ export const component = 'PaymentMethodContextProvider'
6
+ export const exported = '@graphcommerce/magento-cart-payment-method'
7
+
8
+ function GoogleDatalayerPaymentMethodContextProvider(
9
+ props: PluginProps<PaymentMethodContextProviderProps>,
10
+ ) {
11
+ const { Prev, onSuccess, ...rest } = props
12
+ return (
13
+ <Prev
14
+ {...rest}
15
+ onSuccess={(orderNumber, cart) => {
16
+ purchase(orderNumber, cart)
17
+ return onSuccess?.(orderNumber, cart)
18
+ }}
19
+ />
20
+ )
21
+ }
22
+ export const Plugin = GoogleDatalayerPaymentMethodContextProvider
@@ -0,0 +1,28 @@
1
+ import { ProductListItemReal } from '@graphcommerce/magento-product'
2
+ import { PluginProps } from '@graphcommerce/next-config'
3
+ import { useEventCallback } from '@mui/material'
4
+ import { ComponentProps } from 'react'
5
+ import { useListItemHandler } from '../components/AnalyticsItemList'
6
+
7
+ export const component = 'ProductListItemReal'
8
+ export const exported = '@graphcommerce/magento-product'
9
+
10
+ function GoogleDatalayerProductListItem(
11
+ props: PluginProps<ComponentProps<typeof ProductListItemReal>>,
12
+ ) {
13
+ const { Prev, onClick, ...rest } = props
14
+ const { sku, price_range, name } = rest
15
+ const handle = useListItemHandler({ sku, price_range, name })
16
+
17
+ return (
18
+ <Prev
19
+ {...rest}
20
+ onClick={useEventCallback<NonNullable<typeof onClick>>((e, item) => {
21
+ handle()
22
+ return onClick?.(e, item)
23
+ })}
24
+ />
25
+ )
26
+ }
27
+
28
+ export const Plugin = GoogleDatalayerProductListItem
@@ -0,0 +1,17 @@
1
+ import type { ProductItemsGridProps } from '@graphcommerce/magento-product'
2
+ import { PluginProps } from '@graphcommerce/next-config'
3
+ import { ItemList } from '../components/AnalyticsItemList'
4
+
5
+ export const component = 'ProductListItemsBase'
6
+ export const exported = '@graphcommerce/magento-product'
7
+
8
+ export function GoogleDatalayerProductListItemsBase(props: PluginProps<ProductItemsGridProps>) {
9
+ const { Prev, ...rest } = props
10
+ return (
11
+ <ItemList {...rest}>
12
+ <Prev {...rest} />
13
+ </ItemList>
14
+ )
15
+ }
16
+
17
+ export const Plugin = GoogleDatalayerProductListItemsBase
@@ -0,0 +1,29 @@
1
+ import type { RemoveItemFromCart as Original } from '@graphcommerce/magento-cart-items'
2
+ import { ReactPlugin } from '@graphcommerce/next-config'
3
+ import { removeFromCart } from '../events/remove_from_cart'
4
+
5
+ export const component = 'RemoveItemFromCart'
6
+ export const exported =
7
+ '@graphcommerce/magento-cart-items/components/RemoveItemFromCart/RemoveItemFromCart'
8
+
9
+ export const GoogleDatalayerRemoveItemFromCart: ReactPlugin<typeof Original> = (props) => {
10
+ const { Prev, uid, quantity, prices, product, buttonProps } = props
11
+
12
+ return (
13
+ <Prev
14
+ {...props}
15
+ product={product}
16
+ buttonProps={{
17
+ onClick: (e) => {
18
+ removeFromCart({
19
+ __typename: 'Cart',
20
+ items: [{ uid, __typename: 'SimpleCartItem', product, quantity, prices }],
21
+ })
22
+ buttonProps?.onClick?.(e)
23
+ },
24
+ }}
25
+ />
26
+ )
27
+ }
28
+
29
+ export const Plugin = GoogleDatalayerRemoveItemFromCart
@@ -0,0 +1,29 @@
1
+ import type { RemoveItemFromCartFab as Original } from '@graphcommerce/magento-cart-items'
2
+ import { ReactPlugin } from '@graphcommerce/next-config'
3
+ import { removeFromCart } from '../events/remove_from_cart'
4
+
5
+ export const component = 'RemoveItemFromCartFab'
6
+ export const exported =
7
+ '@graphcommerce/magento-cart-items/components/RemoveItemFromCart/RemoveItemFromCartFab'
8
+
9
+ export const GoogleDatalayerRemoveItemFromCartFab: ReactPlugin<typeof Original> = (props) => {
10
+ const { Prev, uid, quantity, prices, product, fabProps } = props
11
+
12
+ return (
13
+ <Prev
14
+ {...props}
15
+ product={product}
16
+ fabProps={{
17
+ onClick: (e) => {
18
+ removeFromCart({
19
+ __typename: 'Cart',
20
+ items: [{ uid, __typename: 'SimpleCartItem', product, quantity, prices }],
21
+ })
22
+ fabProps?.onClick?.(e)
23
+ },
24
+ }}
25
+ />
26
+ )
27
+ }
28
+
29
+ export const Plugin = GoogleDatalayerRemoveItemFromCartFab
@@ -0,0 +1,22 @@
1
+ import { ShippingMethodFormProps } from '@graphcommerce/magento-cart-shipping-method'
2
+ import { PluginProps } from '@graphcommerce/next-config'
3
+ import { addShippingInfo } from '../events/add_shipping_info'
4
+
5
+ export const component = 'ShippingMethodForm'
6
+ export const exported = '@graphcommerce/magento-cart-shipping-method'
7
+
8
+ /** When the ShippingMethod is submitted the result is sent to Google Analytics */
9
+ export function GoogleDatalayerShippingMethodForm(props: PluginProps<ShippingMethodFormProps>) {
10
+ const { Prev, onComplete, ...rest } = props
11
+ return (
12
+ <Prev
13
+ {...rest}
14
+ onComplete={(result, variables) => {
15
+ addShippingInfo(result.data?.setShippingMethodsOnCart?.cart)
16
+ return onComplete?.(result, variables)
17
+ }}
18
+ />
19
+ )
20
+ }
21
+
22
+ export const Plugin = GoogleDatalayerShippingMethodForm
@@ -0,0 +1,65 @@
1
+ import type { UpdateItemQuantityProps } from '@graphcommerce/magento-cart-items'
2
+ import { PluginProps } from '@graphcommerce/next-config'
3
+ import { event } from '../lib/event'
4
+
5
+ export const component = 'UpdateItemQuantity'
6
+ export const exported =
7
+ '@graphcommerce/magento-cart-items/components/UpdateItemQuantity/UpdateItemQuantity'
8
+
9
+ /**
10
+ * When a product is added to the Cart, by using the + button on cart page, send a Google Analytics
11
+ * event
12
+ */
13
+ function GoogleDatalayerUpdateItemQuantity(props: PluginProps<UpdateItemQuantityProps>) {
14
+ const { Prev, formOptions, quantity, ...rest } = props
15
+
16
+ return (
17
+ <Prev
18
+ {...rest}
19
+ quantity={quantity}
20
+ formOptions={{
21
+ ...formOptions,
22
+ onComplete: (data, variables) => {
23
+ const original = formOptions?.onComplete?.(data, variables)
24
+ const diffQuantity = variables.quantity - quantity
25
+ if (diffQuantity === 0) return original
26
+
27
+ const itemId = variables.uid
28
+ const addedItem = data.data?.updateCartItems?.cart.items?.find(
29
+ (item) => item?.uid === itemId,
30
+ )
31
+
32
+ if (addedItem && addedItem.prices && addedItem.prices.row_total_including_tax.value) {
33
+ // we need to manually calculate pricePerItemInclTax (https://github.com/magento/magento2/issues/33848)
34
+ const pricePerItemInclTax =
35
+ addedItem.prices.row_total_including_tax.value / addedItem.quantity
36
+ const addToCartValue = pricePerItemInclTax * diffQuantity
37
+
38
+ event('add_to_cart', {
39
+ currency: addedItem?.prices?.price.currency,
40
+ value: addToCartValue,
41
+ items: [
42
+ {
43
+ item_id: addedItem?.product.sku,
44
+ item_name: addedItem?.product.name,
45
+ currency: addedItem?.prices?.price.currency,
46
+ price: pricePerItemInclTax,
47
+ quantity: variables.quantity,
48
+ discount: addedItem?.prices?.discounts?.reduce(
49
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
50
+ (sum, discount) => sum + (discount?.amount?.value ?? 0),
51
+ 0,
52
+ ),
53
+ },
54
+ ],
55
+ })
56
+ }
57
+
58
+ return original
59
+ },
60
+ }}
61
+ />
62
+ )
63
+ }
64
+
65
+ export const Plugin = GoogleDatalayerUpdateItemQuantity
@@ -0,0 +1,29 @@
1
+ import type { ProductPageMeta } from '@graphcommerce/magento-product'
2
+ import { PluginProps } from '@graphcommerce/next-config'
3
+ import { useMemoObject } from '@graphcommerce/next-ui'
4
+ import React, { useEffect } from 'react'
5
+ import { productToItem } from '../lib'
6
+ import { event } from '../lib/event'
7
+
8
+ export const component = 'ProductPageMeta'
9
+ export const exported = '@graphcommerce/magento-product'
10
+
11
+ /** When a product is added to the Cart, send a Google Analytics event */
12
+ function GoogleDatalayerViewItem(props: PluginProps<React.ComponentProps<typeof ProductPageMeta>>) {
13
+ const { Prev, product } = props
14
+ const { price_range } = product
15
+
16
+ const viewItem = useMemoObject({
17
+ currency: price_range.minimum_price.final_price.currency,
18
+ value: price_range.minimum_price.final_price.value,
19
+ items: [productToItem(product)],
20
+ })
21
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
22
+ useEffect(() => {
23
+ event('view_item', viewItem)
24
+ }, [viewItem])
25
+
26
+ return <Prev {...props} />
27
+ }
28
+
29
+ export const Plugin = GoogleDatalayerViewItem
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "exclude": ["**/node_modules", "**/.*/"],
3
+ "include": ["**/*.ts", "**/*.tsx"],
4
+ "extends": "@graphcommerce/typescript-config-pwa/nextjs.json"
5
+ }