@graphcommerce/google-datalayer 9.0.0-canary.72 → 9.0.0-canary.73

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 (35) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/api/googleEventNames.ts +116 -0
  3. package/api/sendEvent.ts +15 -7
  4. package/components/DatalayerViewItemList.tsx +7 -4
  5. package/mapping/cartItemToDatalayerItem/cartItemToDatalayerItem.ts +3 -2
  6. package/mapping/cartItemToRemoveFromCart/cartToRemoveFromCart.ts +2 -5
  7. package/mapping/cartToAddPaymentInfo/Cart_AddPaymentInfo.graphql +1 -1
  8. package/mapping/cartToAddPaymentInfo/cartToAddPaymentInfo.ts +10 -3
  9. package/mapping/cartToAddShippingInfo/Cart_AddShippingInfo.graphql +1 -1
  10. package/mapping/cartToAddShippingInfo/cartToAddShippingInfo.ts +14 -6
  11. package/mapping/cartToBeginCheckout/Cart_BeginCheckout.graphql +1 -1
  12. package/mapping/cartToBeginCheckout/cartToBeginCheckout.ts +7 -3
  13. package/mapping/cartToPurchase/Cart_PurchaseEvent.graphql +18 -0
  14. package/mapping/cartToPurchase/cartToPurchase.ts +28 -0
  15. package/mapping/cartToViewCart/Cart_ViewCart.graphql +8 -1
  16. package/mapping/cartToViewCart/cartToViewCart.ts +12 -3
  17. package/mapping/datalayerItemsToCurrencyValue/datalayerItemsToCurrencyValue.ts +9 -2
  18. package/mapping/productItemsToViewItemList/productItemsToViewItemList.ts +28 -6
  19. package/mapping/productToDatalayerItem/productToDatalayerItem.ts +11 -4
  20. package/mapping/productToViewItem/productToViewItem.ts +13 -8
  21. package/package.json +12 -9
  22. package/plugins/GoogleDatalayerAddProductsToCartForm.tsx +7 -3
  23. package/plugins/GoogleDatalayerCartStartCheckout.tsx +3 -2
  24. package/plugins/GoogleDatalayerCartStartCheckoutLinkOrButton.tsx +2 -1
  25. package/plugins/GoogleDatalayerPaymentMethodButton.tsx +2 -1
  26. package/plugins/GoogleDatalayerPaymentMethodContextProvider.tsx +5 -3
  27. package/plugins/GoogleDatalayerRemoveItemFromCart.tsx +5 -3
  28. package/plugins/GoogleDatalayerShippingMethodForm.tsx +4 -2
  29. package/plugins/GoogleDatalayerUseRemoveItemFromCart.tsx +3 -2
  30. package/plugins/GoogleDatalayerUseSignInForm.tsx +0 -0
  31. package/plugins/GoogleDatalayerViewItem.tsx +4 -3
  32. package/plugins/GoogleDatalayerWebVitals.tsx +2 -1
  33. package/mapping/cartToDatalayerItems/Cart_DatalayerItems.graphql +0 -11
  34. package/mapping/cartToDatalayerItems/cartToDatalayerItems.ts +0 -10
  35. package/mapping/orderToPurchase/orderToPurchase.ts +0 -17
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @graphcommerce/google-datalayer
2
2
 
3
+ ## 9.0.0-canary.73
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2337](https://github.com/graphcommerce-org/graphcommerce/pull/2337) [`18898df`](https://github.com/graphcommerce-org/graphcommerce/commit/18898df44b786dd68d8e6fec538e3db947c157e4) - All sendEvent calls are now the return type of useSendEvent, to allow plugins to use hooks themselves ([@Renzovh](https://github.com/Renzovh))
8
+
3
9
  ## 9.0.0-canary.72
4
10
 
5
11
  ## 9.0.0-canary.71
@@ -1,3 +1,17 @@
1
+ import { Metric } from 'web-vitals'
2
+ import type { AddPaymentInfo } from '../mapping/cartToAddPaymentInfo/cartToAddPaymentInfo'
3
+ import type { AddShippingInfo } from '../mapping/cartToAddShippingInfo/cartToAddShippingInfo'
4
+ import type { BeginCheckout } from '../mapping/cartToBeginCheckout/cartToBeginCheckout'
5
+ import type { PurchaseOrRefund } from '../mapping/cartToPurchase/cartToPurchase'
6
+ import type { ViewCart } from '../mapping/cartToViewCart/cartToViewCart'
7
+ import type { DataLayerCurrencyValue } from '../mapping/datalayerItemsToCurrencyValue/datalayerItemsToCurrencyValue'
8
+ import type {
9
+ SelectItem,
10
+ ViewItemList,
11
+ } from '../mapping/productItemsToViewItemList/productItemsToViewItemList'
12
+ import type { GoogleDatalayerItem } from '../mapping/productToDatalayerItem/productToDatalayerItem'
13
+ import type { ViewItem } from '../mapping/productToViewItem/productToViewItem'
14
+
1
15
  export const googleEventNames = [
2
16
  'add_payment_info',
3
17
  'add_shipping_info',
@@ -37,3 +51,105 @@ export const googleEventNames = [
37
51
  'view_promotion',
38
52
  'view_search_results',
39
53
  ] as const
54
+
55
+ /**
56
+ * @see https://developers.google.com/tag-platform/gtagjs/reference/events
57
+ */
58
+ export type GoogleEventTypes = {
59
+ exception: { description?: string; fatal?: boolean }
60
+ share: { method?: string; content_type?: string; item_id?: string }
61
+
62
+ add_payment_info: AddPaymentInfo
63
+ add_shipping_info: AddShippingInfo
64
+ add_to_cart: ViewCart
65
+ add_to_wishlist: DataLayerCurrencyValue & { items: GoogleDatalayerItem[] }
66
+ begin_checkout: BeginCheckout
67
+
68
+ login: { method?: string }
69
+ sign_up: {
70
+ method?: string
71
+ }
72
+
73
+ page_view: {
74
+ page_location?: string
75
+ client_id?: string
76
+ language?: string
77
+ page_encoding?: string
78
+ page_title?: string
79
+ user_agent?: string
80
+ }
81
+
82
+ purchase: PurchaseOrRefund
83
+ refund: PurchaseOrRefund
84
+ remove_from_cart: ViewCart
85
+
86
+ select_content: {
87
+ content_type?: string
88
+ content_id?: string
89
+ data?: object
90
+ }
91
+ select_item: SelectItem
92
+
93
+ view_cart: ViewCart
94
+ view_item: ViewItem
95
+ view_item_list: ViewItemList
96
+
97
+ search: { search_term?: string }
98
+ view_search_results: { search_term?: string }
99
+
100
+ view_promotion: {
101
+ creative_name?: string
102
+ creative_slot?: string
103
+ promotion_id?: string
104
+ promotion_name?: string
105
+ items?: GoogleDatalayerItem[]
106
+ }
107
+ select_promotion: {
108
+ creative_name?: string
109
+ creative_slot?: string
110
+ promotion_id?: string
111
+ promotion_name?: string
112
+ items?: GoogleDatalayerItem[]
113
+ }
114
+
115
+ // Tutorial
116
+ tutorial_begin: object
117
+ tutorial_complete: object
118
+
119
+ // Gaming
120
+ earn_virtual_currency: { value?: string; virtual_currency_name?: string }
121
+ join_group: { group_id?: string }
122
+ level_end: { level_name?: string; success?: boolean }
123
+ level_start: { level_name?: string }
124
+ level_up: { level?: number; character?: string }
125
+ post_score: { score?: number; level?: number; character?: string }
126
+ spend_virtual_currency: {
127
+ value?: string
128
+ virtual_currency_name?: string
129
+ item_name?: string
130
+ }
131
+ unlock_achievement: { achievement_id: string }
132
+
133
+ // Leads
134
+ close_convert_lead: DataLayerCurrencyValue
135
+ close_unconvert_lead: DataLayerCurrencyValue & { unconvert_lead_reason?: string }
136
+ disqualify_lead: DataLayerCurrencyValue & { disqualified_lead_reason?: string }
137
+ generate_lead: DataLayerCurrencyValue & { lead_source?: string }
138
+ qualify_lead: DataLayerCurrencyValue
139
+ working_lead: DataLayerCurrencyValue & { lead_status?: string }
140
+
141
+ // Custom events
142
+ add_to_cart_error: {
143
+ userErrors?: string[]
144
+ errors?: string[]
145
+ variables?: object
146
+ }
147
+
148
+ // Core web vitals tracking.
149
+ [key: `cwv_${string}`]: {
150
+ value: Metric['delta']
151
+ debug_target?: string
152
+ } & {
153
+ [K in keyof Metric as K extends string ? `metric_${K}` : never]?: Metric[K]
154
+ }
155
+ }
package/api/sendEvent.ts CHANGED
@@ -1,12 +1,20 @@
1
- import { googleEventNames } from './googleEventNames'
1
+ import { GoogleEventTypes } from './googleEventNames'
2
2
 
3
- export type EventMapFunctionType = (
4
- eventName: (typeof googleEventNames)[number] | (string & Record<never, never>),
5
- eventData: {
6
- [key: string]: unknown
7
- },
3
+ export type SendEvent = (
4
+ eventName: Event | (string & Record<never, never>),
5
+ eventData: { [key: string]: unknown },
8
6
  ) => void
9
7
 
10
- export const sendEvent: EventMapFunctionType = (eventName, eventData) => {
8
+ export function sendEvent<Event extends keyof GoogleEventTypes>(
9
+ eventName: Event,
10
+ eventData: GoogleEventTypes[Event],
11
+ ) {
11
12
  // This is a generic event handler and is plugins from google-analytics and google datalayer
12
13
  }
14
+
15
+ export function useSendEvent() {
16
+ return <Event extends keyof GoogleEventTypes>(
17
+ eventName: Event,
18
+ eventData: GoogleEventTypes[Event],
19
+ ) => sendEvent<Event>(eventName, eventData)
20
+ }
@@ -1,8 +1,8 @@
1
- import { ProductItemsGridProps } from '@graphcommerce/magento-product'
1
+ import { ProductItemsGridProps, useProductFiltersPro } from '@graphcommerce/magento-product'
2
2
  import { useMemoObject } from '@graphcommerce/next-ui'
3
3
  import { useEventCallback } from '@mui/material'
4
4
  import React, { useContext, useEffect } from 'react'
5
- import { sendEvent } from '../api/sendEvent'
5
+ import { useSendEvent } from '../api/sendEvent'
6
6
  import {
7
7
  productItemsToViewItemList,
8
8
  viewItemListToSelectItem,
@@ -28,10 +28,13 @@ export function DatalayerViewItemList(
28
28
  const { title: item_list_name, items, children } = props
29
29
  const item_list_id = item_list_name.toLowerCase().replace(/\s/g, '_')
30
30
 
31
+ const params = useProductFiltersPro(true)?.params
32
+
33
+ const sendEvent = useSendEvent()
31
34
  const viewItemList = useMemoObject(
32
- productItemsToViewItemList(item_list_id, item_list_name, items),
35
+ productItemsToViewItemList(item_list_id, item_list_name, items, params),
33
36
  )
34
- useEffect(() => sendEvent('view_item_list', viewItemList), [viewItemList])
37
+ useEffect(() => sendEvent('view_item_list', viewItemList), [sendEvent, viewItemList])
35
38
 
36
39
  const selectItem = useEventCallback((itemId: string) => {
37
40
  sendEvent('select_item', viewItemListToSelectItem(viewItemList, itemId))
@@ -6,6 +6,7 @@ import { CartItem_DatalayerItemFragment } from './CartItem_DatalayerItem.gql'
6
6
 
7
7
  export function cartItemToDatalayerItem<P extends CartItem_DatalayerItemFragment>(
8
8
  item: P,
9
+ index: number,
9
10
  ): GoogleDatalayerItem {
10
11
  const discount = item.prices?.total_item_discount?.value
11
12
  ? item.prices.total_item_discount.value / item.quantity
@@ -14,14 +15,14 @@ export function cartItemToDatalayerItem<P extends CartItem_DatalayerItemFragment
14
15
  const price = (item?.prices?.price_including_tax?.value ?? 0) - discount
15
16
 
16
17
  return {
17
- ...productToDatalayerItem(item.product),
18
+ ...productToDatalayerItem(item.product, index),
18
19
  currency: item.prices?.price.currency as string,
19
20
  discount,
20
21
  price,
21
22
  quantity: item.quantity,
22
23
  item_variant:
23
24
  item.__typename === 'ConfigurableCartItem'
24
- ? item.configured_variant.sku ?? undefined
25
+ ? (item.configured_variant.sku ?? undefined)
25
26
  : undefined,
26
27
  }
27
28
  }
@@ -5,9 +5,6 @@ import { CartItem_RemoveFromCartFragment } from './CartItem_RemoveFromCart.gql'
5
5
  export const cartItemToRemoveFromCart = <C extends CartItem_RemoveFromCartFragment>(
6
6
  cartItem: C,
7
7
  ) => {
8
- const items = [cartItemToDatalayerItem(cartItem)]
9
- return {
10
- ...datalayerItemsToCurrencyValue(items),
11
- items,
12
- }
8
+ const items = [cartItemToDatalayerItem(cartItem, 0)]
9
+ return { ...datalayerItemsToCurrencyValue(items), items }
13
10
  }
@@ -1,10 +1,10 @@
1
1
  fragment Cart_AddPaymentInfo on Cart
2
2
  @inject(into: ["PaymentMethodContext", "PaymentMethodUpdated"]) {
3
+ ...Cart_ViewCart
3
4
  applied_coupons {
4
5
  code
5
6
  }
6
7
  selected_payment_method {
7
8
  code
8
9
  }
9
- ...Cart_DatalayerItems
10
10
  }
@@ -1,10 +1,17 @@
1
- import { cartToDatalayerItems } from '../cartToDatalayerItems/cartToDatalayerItems'
1
+ import { cartToViewCart, ViewCart } from '../cartToViewCart/cartToViewCart'
2
2
  import { Cart_AddPaymentInfoFragment } from './Cart_AddPaymentInfo.gql'
3
3
 
4
- export function cartToAddPaymentInfo<C extends Cart_AddPaymentInfoFragment>(cart: C) {
4
+ export type AddPaymentInfo = ViewCart & {
5
+ coupon?: string
6
+ payment_type?: string
7
+ }
8
+
9
+ export function cartToAddPaymentInfo<C extends Cart_AddPaymentInfoFragment>(
10
+ cart: C,
11
+ ): AddPaymentInfo {
5
12
  return {
13
+ ...cartToViewCart(cart),
6
14
  coupon: cart?.applied_coupons?.map((coupon) => coupon?.code).join(' '),
7
15
  payment_type: cart?.selected_payment_method?.code,
8
- ...cartToDatalayerItems(cart),
9
16
  }
10
17
  }
@@ -8,5 +8,5 @@ fragment Cart_AddShippingInfo on Cart @inject(into: ["ShippingMethodSelected"])
8
8
  method_code
9
9
  }
10
10
  }
11
- ...Cart_DatalayerItems
11
+ ...Cart_ViewCart
12
12
  }
@@ -1,15 +1,23 @@
1
- import { cartToDatalayerItems } from '../cartToDatalayerItems/cartToDatalayerItems'
1
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
2
+ import { cartToViewCart, ViewCart } from '../cartToViewCart/cartToViewCart'
2
3
  import { Cart_AddShippingInfoFragment } from './Cart_AddShippingInfo.gql'
3
4
 
4
- export function cartToAddShippingInfo<C extends Cart_AddShippingInfoFragment>(cart: C) {
5
+ export type AddShippingInfo = ViewCart & {
6
+ coupon?: string
7
+ shipping_tier?: string
8
+ }
9
+
10
+ export function cartToAddShippingInfo<C extends Cart_AddShippingInfoFragment>(
11
+ cart: C,
12
+ ): AddShippingInfo {
5
13
  return {
6
14
  coupon: cart?.applied_coupons?.map((coupon) => coupon?.code).join(' '),
7
- shipping_tier: cart?.shipping_addresses
15
+ shipping_tier: filterNonNullableKeys(cart?.shipping_addresses, ['selected_shipping_method'])
8
16
  .map(
9
- (address) =>
10
- `${address?.selected_shipping_method?.carrier_code}_${address?.selected_shipping_method?.method_code}`,
17
+ ({ selected_shipping_method: { carrier_code, method_code } }) =>
18
+ `${carrier_code}_${method_code}`,
11
19
  )
12
20
  .join(' '),
13
- ...cartToDatalayerItems(cart),
21
+ ...cartToViewCart(cart),
14
22
  }
15
23
  }
@@ -1,6 +1,6 @@
1
1
  fragment Cart_BeginCheckout on Cart @inject(into: ["CartStartCheckout"]) {
2
+ ...Cart_ViewCart
2
3
  applied_coupons {
3
4
  code
4
5
  }
5
- ...Cart_DatalayerItems
6
6
  }
@@ -1,9 +1,13 @@
1
- import { cartToDatalayerItems } from '../cartToDatalayerItems/cartToDatalayerItems'
1
+ import { cartToViewCart, ViewCart } from '../cartToViewCart/cartToViewCart'
2
2
  import { Cart_BeginCheckoutFragment } from './Cart_BeginCheckout.gql'
3
3
 
4
- export function cartToBeginCheckout<C extends Cart_BeginCheckoutFragment>(cart: C) {
4
+ export type BeginCheckout = ViewCart & {
5
+ coupon?: string
6
+ }
7
+
8
+ export function cartToBeginCheckout<C extends Cart_BeginCheckoutFragment>(cart: C): BeginCheckout {
5
9
  return {
6
10
  coupon: cart?.applied_coupons?.map((coupon) => coupon?.code).join(' '),
7
- ...cartToDatalayerItems(cart),
11
+ ...cartToViewCart(cart),
8
12
  }
9
13
  }
@@ -0,0 +1,18 @@
1
+ fragment Cart_PurchaseEvent on Cart
2
+ @inject(into: ["PaymentMethodContext", "PaymentMethodUpdated"]) {
3
+ ...Cart_AddPaymentInfo
4
+ shipping_addresses {
5
+ selected_shipping_method {
6
+ price_excl_tax {
7
+ ...Money
8
+ }
9
+ }
10
+ }
11
+ prices {
12
+ applied_taxes {
13
+ amount {
14
+ ...Money
15
+ }
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,28 @@
1
+ import { cartToViewCart, ViewCart } from '../cartToViewCart/cartToViewCart'
2
+ import { Cart_PurchaseEventFragment } from './Cart_PurchaseEvent.gql'
3
+
4
+ export type PurchaseOrRefund = ViewCart & {
5
+ transaction_id: string
6
+ coupon?: string
7
+ shipping?: number
8
+ tax?: number
9
+ }
10
+
11
+ export function cartToPurchase<C extends Cart_PurchaseEventFragment>(
12
+ orderNumber: string,
13
+ cart: C | null | undefined,
14
+ ): PurchaseOrRefund {
15
+ // Although the fallback information is wrong, we find it to be more important to register a purchase even in the cart object is missing.
16
+ const base = cart ? cartToViewCart(cart) : { items: [], currency: '', value: 0 }
17
+
18
+ return {
19
+ transaction_id: orderNumber,
20
+ coupon: cart?.applied_coupons?.map((coupon) => coupon?.code).join(' '),
21
+ shipping: cart?.shipping_addresses.reduce(
22
+ (sum, address) => sum + (address?.selected_shipping_method?.price_excl_tax.value ?? 0),
23
+ 0,
24
+ ),
25
+ tax: cart?.prices?.applied_taxes?.reduce((sum, tax) => sum + (tax?.amount?.value ?? 0), 0),
26
+ ...base,
27
+ }
28
+ }
@@ -1,4 +1,11 @@
1
1
  fragment Cart_ViewCart on Cart @inject(into: ["CartItemCountChanged"]) {
2
2
  __typename
3
- ...Cart_DatalayerItems
3
+ items {
4
+ ...CartItem_DatalayerItem
5
+ }
6
+ prices {
7
+ subtotal_including_tax {
8
+ currency
9
+ }
10
+ }
4
11
  }
@@ -1,6 +1,15 @@
1
- import { cartToDatalayerItems } from '../cartToDatalayerItems/cartToDatalayerItems'
1
+ import { nonNullable } from '@graphcommerce/next-ui'
2
+ import { cartItemToDatalayerItem } from '../cartItemToDatalayerItem/cartItemToDatalayerItem'
3
+ import {
4
+ DataLayerCurrencyValue,
5
+ datalayerItemsToCurrencyValue,
6
+ } from '../datalayerItemsToCurrencyValue/datalayerItemsToCurrencyValue'
7
+ import { GoogleDatalayerItem } from '../productToDatalayerItem/productToDatalayerItem'
2
8
  import { Cart_ViewCartFragment } from './Cart_ViewCart.gql'
3
9
 
4
- export function cartToViewCart<C extends Cart_ViewCartFragment>(cart: C) {
5
- return cartToDatalayerItems(cart)
10
+ export type ViewCart = DataLayerCurrencyValue & { items: GoogleDatalayerItem[] }
11
+
12
+ export function cartToViewCart<C extends Cart_ViewCartFragment>(cart: C): ViewCart {
13
+ const items = cart?.items?.filter(nonNullable).map(cartItemToDatalayerItem) ?? []
14
+ return { ...datalayerItemsToCurrencyValue(items), items }
6
15
  }
@@ -1,8 +1,15 @@
1
1
  import { GoogleDatalayerItem } from '../productToDatalayerItem/productToDatalayerItem'
2
2
 
3
- export function datalayerItemsToCurrencyValue(items: GoogleDatalayerItem[]) {
3
+ export type DataLayerCurrencyValue = {
4
+ currency: string
5
+ value: number
6
+ }
7
+
8
+ export function datalayerItemsToCurrencyValue(
9
+ items: GoogleDatalayerItem[],
10
+ ): DataLayerCurrencyValue {
4
11
  return {
5
- currency: items[0].currency,
12
+ currency: items[0]?.currency ?? '',
6
13
  value: items.reduce((acc, item) => acc + (item.price ?? 0) * item.quantity, 0),
7
14
  }
8
15
  }
@@ -1,22 +1,44 @@
1
- import { ProductListItemFragment } from '@graphcommerce/magento-product'
1
+ import { ProductFilterParams, ProductListItemFragment } from '@graphcommerce/magento-product'
2
2
  import { nonNullable } from '@graphcommerce/next-ui'
3
- import { productToDatalayerItem } from '../productToDatalayerItem/productToDatalayerItem'
3
+ import {
4
+ GoogleDatalayerItem,
5
+ productToDatalayerItem,
6
+ } from '../productToDatalayerItem/productToDatalayerItem'
7
+
8
+ export type ViewItemList = {
9
+ item_list_id: string
10
+ item_list_name: string
11
+ items: GoogleDatalayerItem[]
12
+ filter_params?: ProductFilterParams | null
13
+ }
14
+
15
+ export type SelectItem = {
16
+ item_list_id: string
17
+ item_list_name: string
18
+ items: GoogleDatalayerItem[]
19
+ filter_params?: ProductFilterParams | null
20
+ }
4
21
 
5
22
  export function productItemsToViewItemList<P extends ProductListItemFragment>(
6
23
  item_list_id: string,
7
24
  item_list_name: string,
8
25
  items: Array<P | null | undefined> | null | undefined,
9
- ) {
26
+ filter_params: ProductFilterParams | null | undefined,
27
+ ): ViewItemList {
10
28
  return {
11
29
  item_list_id,
12
30
  item_list_name,
13
- items: items?.filter(nonNullable)?.map((item) => productToDatalayerItem(item)) ?? [],
31
+ items: (items ?? [])?.filter(nonNullable)?.map(productToDatalayerItem),
32
+ filter_params,
14
33
  }
15
34
  }
16
35
 
17
36
  export function viewItemListToSelectItem(
18
37
  viewItemList: ReturnType<typeof productItemsToViewItemList>,
19
38
  itemId: string,
20
- ) {
21
- return { ...viewItemList, items: viewItemList.items.filter((i) => i.item_id === itemId) }
39
+ ): SelectItem {
40
+ return {
41
+ ...viewItemList,
42
+ items: viewItemList.items.filter((i) => i.item_id === itemId),
43
+ }
22
44
  }
@@ -2,14 +2,18 @@ import { productPageCategory } from '@graphcommerce/magento-product'
2
2
  import { nonNullable } from '@graphcommerce/next-ui'
3
3
  import { Product_DatalayerItemFragment } from './Product_DatalayerItem.gql'
4
4
 
5
+ /**
6
+ * https://developers.google.com/tag-platform/gtagjs/reference/events#add_to_cart_item
7
+ */
5
8
  export type GoogleDatalayerItem = {
6
9
  item_id: string
10
+ item_uid: string
7
11
  item_name: string
8
12
  affiliation?: string
9
13
  coupon?: string
10
14
  currency?: string
11
15
  discount?: number
12
- index?: number
16
+ index: number
13
17
  item_brand?: string
14
18
  item_category?: string
15
19
  item_category2?: string
@@ -20,27 +24,30 @@ export type GoogleDatalayerItem = {
20
24
  item_list_name?: string
21
25
  item_variant?: string
22
26
  location_id?: string
23
- price?: number
27
+ price: number
24
28
  quantity: number
25
29
  }
26
30
 
27
31
  export function productToDatalayerItem<P extends Product_DatalayerItemFragment>(
28
32
  item: P,
33
+ index: number,
29
34
  ): GoogleDatalayerItem {
30
35
  const category = productPageCategory(item)
31
36
  const item_categories = Object.fromEntries(
32
37
  [...(category?.breadcrumbs?.map((b) => b?.category_name) ?? []), category?.name]
33
38
  .filter(nonNullable)
34
- .map((name, index) => [`item_category${index > 0 ? index + 1 : ''}`, name]),
39
+ .map((name, idx) => [`item_category${idx > 0 ? idx + 1 : ''}`, name]),
35
40
  )
36
41
 
37
42
  return {
38
43
  item_id: item.sku ?? '',
44
+ item_uid: item.uid,
39
45
  item_name: item.name ?? '',
40
- price: item.price_range?.minimum_price.final_price.value ?? undefined,
46
+ price: item.price_range?.minimum_price.final_price.value ?? 0,
41
47
  currency: item.price_range?.minimum_price.final_price.currency ?? undefined,
42
48
  discount: item.price_range?.minimum_price.discount?.amount_off ?? undefined,
43
49
  quantity: 1,
50
+ index,
44
51
  ...item_categories,
45
52
  }
46
53
  }
@@ -1,11 +1,16 @@
1
- import { datalayerItemsToCurrencyValue } from '../datalayerItemsToCurrencyValue/datalayerItemsToCurrencyValue'
2
- import { productToDatalayerItem } from '../productToDatalayerItem/productToDatalayerItem'
1
+ import {
2
+ DataLayerCurrencyValue,
3
+ datalayerItemsToCurrencyValue,
4
+ } from '../datalayerItemsToCurrencyValue/datalayerItemsToCurrencyValue'
5
+ import {
6
+ GoogleDatalayerItem,
7
+ productToDatalayerItem,
8
+ } from '../productToDatalayerItem/productToDatalayerItem'
3
9
  import { Product_ViewItemFragment } from './Product_ViewItem.gql'
4
10
 
5
- export function productToViewItem<C extends Product_ViewItemFragment>(product: C) {
6
- const items = [productToDatalayerItem(product)]
7
- return {
8
- ...datalayerItemsToCurrencyValue(items),
9
- items,
10
- }
11
+ export type ViewItem = { items: GoogleDatalayerItem[] } & DataLayerCurrencyValue
12
+
13
+ export function productToViewItem<C extends Product_ViewItemFragment>(product: C): ViewItem {
14
+ const items = [productToDatalayerItem(product, 0)]
15
+ return { ...datalayerItemsToCurrencyValue(items), items }
11
16
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/google-datalayer",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "9.0.0-canary.72",
5
+ "version": "9.0.0-canary.73",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -12,14 +12,14 @@
12
12
  }
13
13
  },
14
14
  "peerDependencies": {
15
- "@graphcommerce/eslint-config-pwa": "^9.0.0-canary.72",
16
- "@graphcommerce/magento-cart": "^9.0.0-canary.72",
17
- "@graphcommerce/magento-cart-payment-method": "^9.0.0-canary.72",
18
- "@graphcommerce/magento-cart-shipping-method": "^9.0.0-canary.72",
19
- "@graphcommerce/magento-product": "^9.0.0-canary.72",
20
- "@graphcommerce/next-ui": "^9.0.0-canary.72",
21
- "@graphcommerce/prettier-config-pwa": "^9.0.0-canary.72",
22
- "@graphcommerce/typescript-config-pwa": "^9.0.0-canary.72",
15
+ "@graphcommerce/eslint-config-pwa": "^9.0.0-canary.73",
16
+ "@graphcommerce/magento-cart": "^9.0.0-canary.73",
17
+ "@graphcommerce/magento-cart-payment-method": "^9.0.0-canary.73",
18
+ "@graphcommerce/magento-cart-shipping-method": "^9.0.0-canary.73",
19
+ "@graphcommerce/magento-product": "^9.0.0-canary.73",
20
+ "@graphcommerce/next-ui": "^9.0.0-canary.73",
21
+ "@graphcommerce/prettier-config-pwa": "^9.0.0-canary.73",
22
+ "@graphcommerce/typescript-config-pwa": "^9.0.0-canary.73",
23
23
  "@mui/material": "^5.14.20",
24
24
  "next": "^14",
25
25
  "react": "^18.2.0",
@@ -37,6 +37,9 @@
37
37
  },
38
38
  "@graphcommerce/magento-product": {
39
39
  "optional": true
40
+ },
41
+ "@graphcommerce/magento-product-configurable": {
42
+ "optional": true
40
43
  }
41
44
  },
42
45
  "dependencies": {
@@ -5,7 +5,7 @@ import {
5
5
  } from '@graphcommerce/magento-product'
6
6
  import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
7
7
  import { nonNullable } from '@graphcommerce/next-ui'
8
- import { sendEvent } from '../api/sendEvent'
8
+ import { useSendEvent } from '../api/sendEvent'
9
9
  import { cartItemToDatalayerItem } from '../mapping/cartItemToDatalayerItem/cartItemToDatalayerItem'
10
10
  import { datalayerItemsToCurrencyValue } from '../mapping/datalayerItemsToCurrencyValue/datalayerItemsToCurrencyValue'
11
11
 
@@ -18,6 +18,7 @@ export const config: PluginConfig = {
18
18
  export function AddProductsToCartForm(props: PluginProps<AddProductsToCartFormProps>) {
19
19
  const { Prev, onComplete, ...rest } = props
20
20
 
21
+ const sendEvent = useSendEvent()
21
22
  return (
22
23
  <Prev
23
24
  {...rest}
@@ -26,9 +27,12 @@ export function AddProductsToCartForm(props: PluginProps<AddProductsToCartFormPr
26
27
  const addedItems = findAddedItems(data, variables)
27
28
 
28
29
  const items = addedItems
29
- .map(({ itemVariable, itemInCart }) => {
30
+ .map(({ itemVariable, itemInCart }, index) => {
30
31
  if (!itemInCart) return null
31
- return { ...cartItemToDatalayerItem(itemInCart), quantity: itemVariable.quantity }
32
+ return {
33
+ ...cartItemToDatalayerItem(itemInCart, index),
34
+ quantity: itemVariable.quantity,
35
+ }
32
36
  })
33
37
  .filter(nonNullable)
34
38
 
@@ -2,7 +2,7 @@ import { CartStartCheckoutProps } from '@graphcommerce/magento-cart'
2
2
  import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
3
  import { useMemoObject } from '@graphcommerce/next-ui'
4
4
  import { useEffect, useRef } from 'react'
5
- import { sendEvent } from '../api/sendEvent'
5
+ import { useSendEvent } from '../api/sendEvent'
6
6
  import { cartToBeginCheckout } from '../mapping/cartToBeginCheckout/cartToBeginCheckout'
7
7
  import { cartToViewCart } from '../mapping/cartToViewCart/cartToViewCart'
8
8
 
@@ -15,13 +15,14 @@ export function CartStartCheckout(props: PluginProps<CartStartCheckoutProps>) {
15
15
  const { Prev, onStart, ...rest } = props
16
16
 
17
17
  const send = useRef(false)
18
+ const sendEvent = useSendEvent()
18
19
  const viewCart = useMemoObject(cartToViewCart({ __typename: 'Cart', ...props }))
19
20
  useEffect(() => {
20
21
  if (!send.current) {
21
22
  sendEvent('view_cart', viewCart)
22
23
  send.current = true
23
24
  }
24
- }, [viewCart])
25
+ }, [sendEvent, viewCart])
25
26
 
26
27
  return (
27
28
  <Prev
@@ -1,6 +1,6 @@
1
1
  import { CartStartCheckoutLinkOrButtonProps } from '@graphcommerce/magento-cart'
2
2
  import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
- import { sendEvent } from '../api/sendEvent'
3
+ import { useSendEvent } from '../api/sendEvent'
4
4
  import { cartToBeginCheckout } from '../mapping/cartToBeginCheckout/cartToBeginCheckout'
5
5
 
6
6
  export const config: PluginConfig = {
@@ -13,6 +13,7 @@ export function CartStartCheckoutLinkOrButton(
13
13
  ) {
14
14
  const { Prev, onStart, ...rest } = props
15
15
 
16
+ const sendEvent = useSendEvent()
16
17
  return (
17
18
  <Prev
18
19
  {...rest}
@@ -2,7 +2,7 @@ import { useCartQuery } from '@graphcommerce/magento-cart'
2
2
  import { PaymentMethodButtonProps } from '@graphcommerce/magento-cart-payment-method'
3
3
  import { GetPaymentMethodContextDocument } from '@graphcommerce/magento-cart-payment-method/PaymentMethodContext/GetPaymentMethodContext.gql'
4
4
  import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
5
- import { sendEvent } from '../api/sendEvent'
5
+ import { useSendEvent } from '../api/sendEvent'
6
6
  import { cartToAddPaymentInfo } from '../mapping/cartToAddPaymentInfo/cartToAddPaymentInfo'
7
7
 
8
8
  export const config: PluginConfig = {
@@ -14,6 +14,7 @@ export function PaymentMethodButton(props: PluginProps<PaymentMethodButtonProps>
14
14
  const { Prev, onSubmitSuccessful, ...rest } = props
15
15
  const methodContext = useCartQuery(GetPaymentMethodContextDocument)
16
16
 
17
+ const sendEvent = useSendEvent()
17
18
  return (
18
19
  <Prev
19
20
  {...rest}
@@ -1,7 +1,7 @@
1
1
  import type { PaymentMethodContextProviderProps } from '@graphcommerce/magento-cart-payment-method'
2
2
  import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
- import { sendEvent } from '../api/sendEvent'
4
- import { orderToPurchase } from '../mapping/orderToPurchase/orderToPurchase'
3
+ import { useSendEvent } from '../api/sendEvent'
4
+ import { cartToPurchase } from '../mapping/cartToPurchase/cartToPurchase'
5
5
 
6
6
  export const config: PluginConfig = {
7
7
  module: '@graphcommerce/magento-cart-payment-method',
@@ -12,11 +12,13 @@ export function PaymentMethodContextProvider(
12
12
  props: PluginProps<PaymentMethodContextProviderProps>,
13
13
  ) {
14
14
  const { Prev, onSuccess, ...rest } = props
15
+
16
+ const sendEvent = useSendEvent()
15
17
  return (
16
18
  <Prev
17
19
  {...rest}
18
20
  onSuccess={(orderNumber, cart) => {
19
- sendEvent('purchase', orderToPurchase(orderNumber, cart))
21
+ sendEvent('purchase', cartToPurchase(orderNumber, cart))
20
22
  return onSuccess?.(orderNumber, cart)
21
23
  }}
22
24
  />
@@ -1,6 +1,6 @@
1
1
  import type { useRemoveItemFromCart as useRemoveItemFromCartBase } from '@graphcommerce/magento-cart-items'
2
2
  import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
3
- import { sendEvent } from '../api/sendEvent'
3
+ import { useSendEvent } from '../api/sendEvent'
4
4
  import { cartItemToRemoveFromCart } from '../mapping/cartItemToRemoveFromCart/cartToRemoveFromCart'
5
5
 
6
6
  export const config: PluginConfig = {
@@ -11,8 +11,9 @@ export const config: PluginConfig = {
11
11
  export const useRemoveItemFromCart: FunctionPlugin<typeof useRemoveItemFromCartBase> = (
12
12
  usePrev,
13
13
  props,
14
- ) =>
15
- usePrev({
14
+ ) => {
15
+ const sendEvent = useSendEvent()
16
+ return usePrev({
16
17
  ...props,
17
18
  onComplete: (result, variables) => {
18
19
  if (!result.errors) {
@@ -24,3 +25,4 @@ export const useRemoveItemFromCart: FunctionPlugin<typeof useRemoveItemFromCartB
24
25
  return props.onComplete?.(result, variables)
25
26
  },
26
27
  })
28
+ }
@@ -1,6 +1,6 @@
1
1
  import { ShippingMethodFormProps } from '@graphcommerce/magento-cart-shipping-method'
2
2
  import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
- import { sendEvent } from '../api/sendEvent'
3
+ import { useSendEvent } from '../api/sendEvent'
4
4
  import { cartToAddShippingInfo } from '../mapping/cartToAddShippingInfo/cartToAddShippingInfo'
5
5
 
6
6
  export const config: PluginConfig = {
@@ -10,6 +10,8 @@ export const config: PluginConfig = {
10
10
 
11
11
  export function ShippingMethodForm(props: PluginProps<ShippingMethodFormProps>) {
12
12
  const { Prev, onComplete, ...rest } = props
13
+
14
+ const sendEvent = useSendEvent()
13
15
  return (
14
16
  <Prev
15
17
  {...rest}
@@ -17,7 +19,7 @@ export function ShippingMethodForm(props: PluginProps<ShippingMethodFormProps>)
17
19
  if (result.data?.setShippingMethodsOnCart?.cart) {
18
20
  sendEvent(
19
21
  'add_shipping_info',
20
- cartToAddShippingInfo(result.data?.setShippingMethodsOnCart?.cart),
22
+ cartToAddShippingInfo(result.data.setShippingMethodsOnCart.cart),
21
23
  )
22
24
  }
23
25
 
@@ -1,6 +1,6 @@
1
1
  import type { UpdateItemQuantityProps } from '@graphcommerce/magento-cart-items'
2
2
  import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
- import { sendEvent } from '../api/sendEvent'
3
+ import { useSendEvent } from '../api/sendEvent'
4
4
  import { cartItemToDatalayerItem } from '../mapping/cartItemToDatalayerItem/cartItemToDatalayerItem'
5
5
  import { datalayerItemsToCurrencyValue } from '../mapping/datalayerItemsToCurrencyValue/datalayerItemsToCurrencyValue'
6
6
 
@@ -16,6 +16,7 @@ export const config: PluginConfig = {
16
16
  export function UpdateItemQuantity(props: PluginProps<UpdateItemQuantityProps>) {
17
17
  const { Prev, formOptions, quantity, ...rest } = props
18
18
 
19
+ const sendEvent = useSendEvent()
19
20
  return (
20
21
  <Prev
21
22
  {...rest}
@@ -33,7 +34,7 @@ export function UpdateItemQuantity(props: PluginProps<UpdateItemQuantityProps>)
33
34
 
34
35
  if (!itemInCart?.quantity || diffQuantity === 0) return original
35
36
 
36
- const items = [{ ...cartItemToDatalayerItem(itemInCart), quantity: absQuantity }]
37
+ const items = [{ ...cartItemToDatalayerItem(itemInCart, 0), quantity: absQuantity }]
37
38
  sendEvent(diffQuantity < 0 ? 'remove_from_cart' : 'add_to_cart', {
38
39
  ...datalayerItemsToCurrencyValue(items),
39
40
  items,
File without changes
@@ -2,7 +2,7 @@ import type { ProductPageMetaProps } from '@graphcommerce/magento-product'
2
2
  import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
3
  import { useMemoObject } from '@graphcommerce/next-ui'
4
4
  import { useEffect } from 'react'
5
- import { sendEvent } from '../api/sendEvent'
5
+ import { useSendEvent } from '../api/sendEvent'
6
6
  import { productToViewItem } from '../mapping/productToViewItem/productToViewItem'
7
7
 
8
8
  export const config: PluginConfig = {
@@ -10,12 +10,13 @@ export const config: PluginConfig = {
10
10
  type: 'component',
11
11
  }
12
12
 
13
- /** When a product is added to the Cart, send a Google Analytics event */
13
+ /** When a product detail page is viewed, send a Google Analytics event */
14
14
  export function ProductPageMeta(props: PluginProps<ProductPageMetaProps>) {
15
15
  const { Prev, product } = props
16
16
 
17
+ const sendEvent = useSendEvent()
17
18
  const viewItem = useMemoObject(productToViewItem(product))
18
- useEffect(() => sendEvent('view_item', viewItem), [viewItem])
19
+ useEffect(() => sendEvent('view_item', viewItem), [sendEvent, viewItem])
19
20
 
20
21
  return <Prev {...props} />
21
22
  }
@@ -3,7 +3,7 @@ import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
3
3
  import { useEventCallback } from '@mui/material'
4
4
  import { useEffect } from 'react'
5
5
  import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB, Metric } from 'web-vitals/attribution'
6
- import { sendEvent } from '../api/sendEvent'
6
+ import { useSendEvent } from '../api/sendEvent'
7
7
 
8
8
  export const config: PluginConfig = {
9
9
  type: 'component',
@@ -19,6 +19,7 @@ export const config: PluginConfig = {
19
19
  export function FramerNextPages(props: PluginProps<PagesProps>) {
20
20
  const { Prev, ...rest } = props
21
21
 
22
+ const sendEvent = useSendEvent()
22
23
  const sendCoreWebVitals = useEventCallback((m: Metric, debug_target?: string | undefined) => {
23
24
  sendEvent(`cwv_${m.name.toLowerCase()}`, {
24
25
  value: m.delta,
@@ -1,11 +0,0 @@
1
- fragment Cart_DatalayerItems on Cart {
2
- __typename
3
- items {
4
- ...CartItem_DatalayerItem
5
- }
6
- prices {
7
- subtotal_including_tax {
8
- currency
9
- }
10
- }
11
- }
@@ -1,10 +0,0 @@
1
- import { nonNullable } from '@graphcommerce/next-ui'
2
- import { cartItemToDatalayerItem } from '../cartItemToDatalayerItem/cartItemToDatalayerItem'
3
- import { datalayerItemsToCurrencyValue } from '../datalayerItemsToCurrencyValue/datalayerItemsToCurrencyValue'
4
- import { Cart_DatalayerItemsFragment } from './Cart_DatalayerItems.gql'
5
-
6
- export function cartToDatalayerItems<C extends Cart_DatalayerItemsFragment>(cart: C) {
7
- const items = cart.items?.filter(nonNullable).map(cartItemToDatalayerItem)
8
- if (!items) return {}
9
- return { ...datalayerItemsToCurrencyValue(items), items }
10
- }
@@ -1,17 +0,0 @@
1
- import { PaymentMethodContextFragment } from '@graphcommerce/magento-cart-payment-method/Api/PaymentMethodContext.gql'
2
- import { cartToDatalayerItems } from '../cartToDatalayerItems/cartToDatalayerItems'
3
-
4
- export function orderToPurchase<C extends PaymentMethodContextFragment>(
5
- orderNumber: string,
6
- cart: C | null | undefined,
7
- ) {
8
- if (!cart) return { transaction_id: orderNumber }
9
-
10
- return {
11
- transaction_id: orderNumber,
12
- coupon: cart.applied_coupons?.map((coupon) => coupon?.code).join(' '),
13
- payment_type: cart.selected_payment_method?.code,
14
- tax: cart.prices?.applied_taxes?.reduce((sum, tax) => sum + (tax?.amount?.value ?? 0), 0),
15
- ...cartToDatalayerItems(cart),
16
- }
17
- }