diff --git a/client-app/app-runner.ts b/client-app/app-runner.ts index aeb572dd0..cf7722342 100644 --- a/client-app/app-runner.ts +++ b/client-app/app-runner.ts @@ -75,7 +75,7 @@ export default async () => { await Promise.all([fetchThemeContext(store), fetchUser(), fallback.setMessage()]); - initializeGoogleAnalytics(); + void initializeGoogleAnalytics(); void initializeHotjar(); /** diff --git a/client-app/core/composables/useGoogleAnalytics.ts b/client-app/core/composables/useGoogleAnalytics.ts index 653768900..99ebce525 100644 --- a/client-app/core/composables/useGoogleAnalytics.ts +++ b/client-app/core/composables/useGoogleAnalytics.ts @@ -1,337 +1,47 @@ -import { canUseDOM } from "@apollo/client/utilities"; import { useScriptTag } from "@vueuse/core"; -import { sumBy } from "lodash"; import { useCurrency } from "@/core/composables/useCurrency"; -import { useThemeContext } from "@/core/composables/useThemeContext"; +import { useModuleSettings } from "@/core/composables/useModuleSettings"; import { IS_DEVELOPMENT } from "@/core/constants"; +import { globals } from "@/core/globals"; import { Logger } from "@/core/utilities"; -import { globals } from "../globals"; -import type { - Breadcrumb, - CartType, - CustomerOrderType, - LineItemType, - OrderLineItemType, - Product, - VariationType, -} from "@/core/api/graphql/types"; -/** - * Custom events. The items array can not be added - */ -type CustomEventNamesType = "place_order" | "clear_cart"; -type EventParamsType = Gtag.ControlParams & Gtag.EventParams & Gtag.CustomParams; -type EventParamsExtendedType = EventParamsType & { item_list_id?: string; item_list_name?: string }; +const MODULE_ID = "VirtoCommerce.GoogleEcommerceAnalytics"; +const IS_ENABLED_KEY = "GoogleAnalytics4.EnableTracking"; -const { currentCurrency } = useCurrency(); -const { modulesSettings } = useThemeContext(); - -const DEBUG_PREFIX = "[GA]"; - -const MODULE_KEYS = { - ID: "VirtoCommerce.GoogleEcommerceAnalytics", - ENABLE_STATE: "GoogleAnalytics4.EnableTracking", - TRACK_ID: "GoogleAnalytics4.MeasurementId", -}; - -function getCategories(breadcrumbs: Breadcrumb[] = []): Record { - const categories: Record = {}; - - breadcrumbs - .filter((breadcrumb) => breadcrumb.typeName !== "CatalogProduct") - .slice(0, 5) // first five, according to the documentation - .forEach((breadcrumb, i) => { - const number = i + 1; - categories[`item_category${number > 1 ? number : ""}`] = breadcrumb.title; - }); - - return categories; -} - -function productToGtagItem(item: Product | VariationType, index?: number): Gtag.Item { - const categories: Record = "breadcrumbs" in item ? getCategories(item.breadcrumbs) : {}; - - return { - index, - item_id: item.code, - item_name: item.name, - affiliation: item.vendor?.name, - price: item.price?.list?.amount, - discount: item.price?.discountAmount?.amount, - quantity: item.availabilityData?.availableQuantity, - ...categories, - }; -} - -function lineItemToGtagItem(item: LineItemType | OrderLineItemType, index?: number): Gtag.Item { - const categories: Record = getCategories(item.product?.breadcrumbs); - - return { - index, - item_id: item.sku, - item_name: item.name, - affiliation: item.vendor?.name || "?", - currency: item.placedPrice.currency.code, - discount: item.discountAmount?.amount || item.discountTotal?.amount, - price: "price" in item ? item.price.amount : item.listPrice.amount, - quantity: item.quantity, - ...categories, - }; -} - -/** @deprecated use direct mapping */ -function getCartEventParams(cart: CartType): EventParamsType { - return { - currency: globals.currencyCode, - value: cart.total.amount, - items: cart.items.map(lineItemToGtagItem), - items_count: cart.items.length, - }; -} - -function sendEvent(eventName: Gtag.EventNames | CustomEventNamesType, eventParams?: EventParamsType): void { - if (canUseDOM && window.gtag) { - window.gtag("event", eventName, eventParams); - } else { - Logger.debug(DEBUG_PREFIX, eventName, eventParams); - } -} - -function viewItemList(items: { code: string }[] = [], params?: EventParamsExtendedType): void { - sendEvent("view_item_list", { - ...params, - items_skus: items - .map((el) => el.code) - .join(", ") - .trim(), - items_count: items.length, - }); -} - -function selectItem(item: Product | LineItemType, params?: EventParamsExtendedType): void { - const gtagItem = "productId" in item ? lineItemToGtagItem(item) : productToGtagItem(item); - - sendEvent("select_item", { - ...params, - items: [gtagItem], - }); -} - -function viewItem(item: Product, params?: EventParamsExtendedType): void { - sendEvent("view_item", { - ...params, - currency: globals.currencyCode, - value: item.price?.actual?.amount, - items: [productToGtagItem(item)], - }); -} - -function addItemToWishList(item: Product, params?: EventParamsExtendedType): void { - sendEvent("add_to_wishlist", { - ...params, - currency: globals.currencyCode, - value: item.price?.actual?.amount, - items: [productToGtagItem(item)], - }); -} - -function addItemToCart(item: Product | VariationType, quantity = 1, params?: EventParamsExtendedType): void { - const inputItem = productToGtagItem(item); - - inputItem.quantity = quantity; - - sendEvent("add_to_cart", { - ...params, - currency: globals.currencyCode, - value: item.price?.actual?.amount * quantity, - items: [inputItem], - }); -} +const { getModuleSettings, hasModuleSettings, isEnabled } = useModuleSettings(MODULE_ID); -function addItemsToCart(items: (Product | VariationType)[], params?: EventParamsExtendedType): void { - const subtotal: number = sumBy(items, (item) => item?.price?.actual?.amount); - const inputItems = items.filter((item) => item).map((item) => productToGtagItem(item)); - - sendEvent("add_to_cart", { - ...params, - currency: globals.currencyCode, - value: subtotal, - items: inputItems, - items_count: inputItems.length, - }); -} - -function removeItemsFromCart(items: LineItemType[], params?: EventParamsExtendedType): void { - const subtotal: number = sumBy(items, (item) => item.extendedPrice?.amount); - const inputItems = items.map((item) => lineItemToGtagItem(item)); - - sendEvent("remove_from_cart", { - ...params, - currency: globals.currencyCode, - value: subtotal, - items: inputItems, - items_count: inputItems.length, - }); -} - -function viewCart(cart: CartType, params?: EventParamsExtendedType): void { - const cartEventParams: EventParamsType = getCartEventParams(cart); - - sendEvent("view_cart", { - ...params, - ...cartEventParams, - }); -} - -function clearCart(cart: CartType, params?: EventParamsExtendedType): void { - const cartEventParams: EventParamsType = getCartEventParams(cart); - - sendEvent("clear_cart", { - ...params, - ...cartEventParams, - }); -} - -function beginCheckout(cart: CartType, params?: EventParamsExtendedType): void { - try { - sendEvent("begin_checkout", { - ...params, - currency: cart.currency.code, - value: cart.total.amount, - items: cart.items.map(lineItemToGtagItem), - items_count: cart.items.length, - coupon: cart.coupons?.[0]?.code, - }); - } catch (e) { - Logger.error(DEBUG_PREFIX, beginCheckout.name, e); - } -} - -function addShippingInfo(cart?: CartType, params?: EventParamsExtendedType, shipmentMethodOption?: string): void { - try { - sendEvent("add_shipping_info", { - ...params, - shipping_tier: shipmentMethodOption, - currency: cart?.shippingPrice.currency.code, - value: cart?.shippingPrice.amount, - coupon: cart?.coupons?.[0]?.code, - items: cart?.items.map(lineItemToGtagItem), - items_count: cart?.items.length, - }); - } catch (e) { - Logger.error(DEBUG_PREFIX, addShippingInfo.name, e); - } -} - -function addPaymentInfo(cart?: CartType, params?: EventParamsExtendedType, paymentGatewayCode?: string): void { - try { - sendEvent("add_payment_info", { - ...params, - payment_type: paymentGatewayCode, - currency: cart?.currency?.code, - value: cart?.total?.amount, - coupon: cart?.coupons?.[0]?.code, - items: cart?.items.map(lineItemToGtagItem), - items_count: cart?.items.length, - }); - } catch (e) { - Logger.error(DEBUG_PREFIX, addPaymentInfo.name, e); - } -} - -function purchase(order: CustomerOrderType, transactionId?: string, params?: EventParamsExtendedType): void { - try { - sendEvent("purchase", { - ...params, - currency: order.currency?.code, - transaction_id: transactionId, - value: order.total!.amount, - coupon: order.coupons?.[0], - shipping: order.shippingTotal?.amount, - tax: order.taxTotal?.amount, - items: order.items!.map(lineItemToGtagItem), - items_count: order?.items?.length, - }); - } catch (e) { - Logger.error(DEBUG_PREFIX, purchase.name, e); - } -} - -function placeOrder(order: CustomerOrderType, params?: EventParamsExtendedType): void { - try { - sendEvent("place_order", { - ...params, - currency: order.currency?.code, - value: order.total?.amount, - coupon: order.coupons?.[0], - shipping: order.shippingTotal.amount, - tax: order.taxTotal.amount, - items_count: order.items?.length, - }); - } catch (e) { - Logger.error(DEBUG_PREFIX, placeOrder.name, e); - } -} - -function search(searchTerm: string, visibleItems: { code: string }[] = [], itemsCount: number = 0): void { - sendEvent("search", { - search_term: searchTerm, - items_count: itemsCount, - visible_items: visibleItems - .map((el) => el.code) - .join(", ") - .trim(), - }); -} - -function init() { - if (!canUseDOM) { - return; - } +const { currentCurrency } = useCurrency(); +const { currencyCode } = globals; - const moduleSettings = modulesSettings.value?.find((el) => el.moduleId === MODULE_KEYS.ID); - const isGoogleAnalyticsEnabled = !!moduleSettings?.settings?.find((el) => el.name === MODULE_KEYS.ENABLE_STATE) - ?.value; +type GoogleAnalyticsMethodsType = ReturnType< + typeof import("@virto-commerce/front-modules-google-ecommerce-analytics").useGoogleAnalyticsModule +>; +let googleAnalyticsMethods: Omit; - if (isGoogleAnalyticsEnabled) { - const id = moduleSettings?.settings?.find((el) => el.name === MODULE_KEYS.TRACK_ID)?.value as string; - if (!IS_DEVELOPMENT) { - useScriptTag(`https://www.googletagmanager.com/gtag/js?id=${id}`); - } else { - Logger.debug(DEBUG_PREFIX, "initialized without sync with google"); +export function useGoogleAnalytics() { + async function init(): Promise { + if (hasModuleSettings && isEnabled(IS_ENABLED_KEY)) { + try { + const { useGoogleAnalyticsModule } = await import("@virto-commerce/front-modules-google-ecommerce-analytics"); + const { initModule, ...methods } = useGoogleAnalyticsModule(); + + initModule({ + getModuleSettings, + isDevelopment: IS_DEVELOPMENT, + logger: Logger, + useScriptTag, + currentCurrency, + currencyCode, + }); + googleAnalyticsMethods = methods; + } catch (e) { + Logger.error(useGoogleAnalytics.name, e); + } } - - window.dataLayer = window.dataLayer || []; - window.gtag = function gtag() { - // is not working with rest - // eslint-disable-next-line prefer-rest-params - window.dataLayer.push(arguments); - }; - - window.gtag("js", new Date()); - window.gtag("config", id, { debug_mode: true }); - window.gtag("set", { currency: currentCurrency.value.code }); } -} -export function useGoogleAnalytics() { return { - sendEvent, - viewItemList, - selectItem, - viewItem, - addItemToWishList, - addItemToCart, - addItemsToCart, - removeItemsFromCart, - viewCart, - clearCart, - beginCheckout, - addShippingInfo, - addPaymentInfo, - purchase, - placeOrder, - search, init, + ...googleAnalyticsMethods, }; } diff --git a/client-app/core/composables/useModuleSettings.ts b/client-app/core/composables/useModuleSettings.ts index c52b1a97e..786670e9e 100644 --- a/client-app/core/composables/useModuleSettings.ts +++ b/client-app/core/composables/useModuleSettings.ts @@ -1,6 +1,6 @@ import { createGlobalState } from "@vueuse/core"; import { computed } from "vue"; -import { useThemeContext } from "@/core/composables"; +import { useThemeContext } from "@/core/composables/useThemeContext"; function _useModuleSettings(moduleId: string) { const { themeContext } = useThemeContext(); diff --git a/client-app/shared/catalog/components/product-price-block.vue b/client-app/shared/catalog/components/product-price-block.vue index a1b537694..991f23fd4 100644 --- a/client-app/shared/catalog/components/product-price-block.vue +++ b/client-app/shared/catalog/components/product-price-block.vue @@ -59,9 +59,7 @@