From 2ccc9676faeb7ae7c3b4188a8d061072864de3ab Mon Sep 17 00:00:00 2001 From: Chad Brokaw <36685920+chadbrokaw@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:00:00 -0700 Subject: [PATCH] feat: send user id and timestamp --- .../__tests__/hooks/useGTMDataLayer.test.tsx | 120 ++++++++++++++---- .../pages/listings/ListingDetail.test.tsx | 5 + .../authentication/context/UserProvider.tsx | 8 +- .../hooks/analytics/useGTMDataLayer.tsx | 48 +++++-- 4 files changed, 137 insertions(+), 44 deletions(-) diff --git a/app/javascript/__tests__/hooks/useGTMDataLayer.test.tsx b/app/javascript/__tests__/hooks/useGTMDataLayer.test.tsx index cf73c9cb1..f9bd1cd2a 100644 --- a/app/javascript/__tests__/hooks/useGTMDataLayer.test.tsx +++ b/app/javascript/__tests__/hooks/useGTMDataLayer.test.tsx @@ -1,59 +1,127 @@ +import React from "react" import { renderHook } from "@testing-library/react" -import { useGTMDataLayer } from "../../hooks/analytics/useGTMDataLayer" +import { + useGTMDataLayer, + useGTMDataLayerWithoutUserContext, +} from "../../hooks/analytics/useGTMDataLayer" import TagManager from "react-gtm-module" +import UserContext from "../../authentication/context/UserContext" +import { mockProfileStub } from "../__util__/accountUtils" describe("useGTMDataLayer", () => { let consoleSpy + const mockedDate = new Date() beforeEach(() => { consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {}) + jest.useFakeTimers() + jest.setSystemTime(mockedDate) }) afterEach(() => { consoleSpy.mockRestore() }) - it("pushes data to the data layer", () => { - const event = "testEvent" - const data = { test: "data" } + describe("User Information is available", () => { + let pushToDataLayer - const { result } = renderHook(() => useGTMDataLayer()) + beforeEach(() => { + const wrapper = ({ children }) => ( + {children} + ) - result.current.pushToDataLayer(event, data) + const { result } = renderHook(() => useGTMDataLayer(), { wrapper }) - expect(TagManager.dataLayer).toHaveBeenCalledWith({ dataLayer: { event, ...data } }) - }) + pushToDataLayer = result.current.pushToDataLayer + }) + + it("pushes data to the data layer", () => { + const event = "testEvent" + const data = { test: "data" } + + pushToDataLayer(event, data) + + expect(TagManager.dataLayer).toHaveBeenCalledWith({ + dataLayer: { + event, + event_timestamp: mockedDate.toISOString(), + ...data, + user_id: mockProfileStub.id, + }, + }) + }) + + it("errors out when no event is provided", () => { + const event = undefined + const data = { test: "data" } + + pushToDataLayer(event, data) - it("errors out when no data is provided", () => { - const event = "testEvent" - const data = undefined + expect(consoleSpy).toHaveBeenCalled() + }) - const { result } = renderHook(() => useGTMDataLayer()) + it("errors out when an event property is provided in the data object", () => { + const event = "testEvent" + const data = { test: "data", event: "testEvent" } - result.current.pushToDataLayer(event, data) + pushToDataLayer(event, data) - expect(consoleSpy).toHaveBeenCalled() + expect(consoleSpy).toHaveBeenCalled() + }) }) - it("errors out when no event is provided", () => { - const event = undefined - const data = { test: "data" } + describe("User Information is not available", () => { + let pushToDataLayer - const { result } = renderHook(() => useGTMDataLayer()) + beforeEach(() => { + const wrapper = ({ children }) => ( + {children} + ) - result.current.pushToDataLayer(event, data) + const { result } = renderHook(() => useGTMDataLayer(), { wrapper }) - expect(consoleSpy).toHaveBeenCalled() + pushToDataLayer = result.current.pushToDataLayer + }) + + it("pushed data to the data layer but with an undefined user_id", () => { + const event = "testEvent" + const data = { test: "data" } + + pushToDataLayer(event, data) + + expect(TagManager.dataLayer).toHaveBeenCalledWith({ + dataLayer: { + event, + event_timestamp: mockedDate.toISOString(), + ...data, + user_id: undefined, // when a value is undefined it is ignored by GTM + }, + }) + }) }) - it("errors out when an event property is provided in the data object", () => { - const event = "testEvent" - const data = { test: "data", event: "testEvent" } + describe("useGTMDataLayerWithoutUserContext", () => { + let pushToDataLayer + + beforeEach(() => { + const { result } = renderHook(() => useGTMDataLayerWithoutUserContext()) + + pushToDataLayer = result.current.pushToDataLayer + }) - const { result } = renderHook(() => useGTMDataLayer()) + it("pushed data to the data layer but without a user_id", () => { + const event = "testEvent" + const data = { test: "data" } - result.current.pushToDataLayer(event, data) + pushToDataLayer(event, data) - expect(consoleSpy).toHaveBeenCalled() + expect(TagManager.dataLayer).toHaveBeenCalledWith({ + dataLayer: { + event, + event_timestamp: mockedDate.toISOString(), + ...data, + }, + }) + }) }) }) diff --git a/app/javascript/__tests__/pages/listings/ListingDetail.test.tsx b/app/javascript/__tests__/pages/listings/ListingDetail.test.tsx index 7ce750ded..e7df73ba0 100644 --- a/app/javascript/__tests__/pages/listings/ListingDetail.test.tsx +++ b/app/javascript/__tests__/pages/listings/ListingDetail.test.tsx @@ -80,9 +80,12 @@ describe("Listing Detail", () => { }) it("initializes Google Tag Manager for a sales listing", async () => { + const mockedDate = new Date() axios.get.mockResolvedValue({ data: { listing: habitatListing, units: habitatListing.Units, ami: [] }, }) + jest.useFakeTimers() + jest.setSystemTime(mockedDate) await renderAndLoadAsync() expect(TagManager.dataLayer).toHaveBeenCalledWith({ @@ -95,6 +98,8 @@ describe("Listing Detail", () => { listing_record_type: "Ownership", listing_status: "Active", listing_tenure: "New sale", + event_timestamp: mockedDate.toISOString(), + listingType: undefined, }, }) }) diff --git a/app/javascript/authentication/context/UserProvider.tsx b/app/javascript/authentication/context/UserProvider.tsx index e91ad078b..275eec614 100644 --- a/app/javascript/authentication/context/UserProvider.tsx +++ b/app/javascript/authentication/context/UserProvider.tsx @@ -13,8 +13,8 @@ import { } from "./userActions" import UserContext, { ContextProps } from "./UserContext" import UserReducer from "./UserReducer" -import { useGTMDataLayer } from "../../hooks/analytics/useGTMDataLayer" import { AxiosError } from "axios" +import { useGTMDataLayerWithoutUserContext } from "../../hooks/analytics/useGTMDataLayer" interface UserProviderProps { children?: React.ReactNode @@ -26,7 +26,7 @@ const UserProvider = (props: UserProviderProps) => { initialStateLoaded: false, }) - const { pushToDataLayer } = useGTMDataLayer() + const { pushToDataLayer } = useGTMDataLayerWithoutUserContext() // Load our profile as soon as we have an access token available useEffect(() => { @@ -47,7 +47,7 @@ const UserProvider = (props: UserProviderProps) => { .catch((error) => { if (error?.message === "Token expired") { pushToDataLayer("logout", { - user_id: state?.profile?.id || undefined, + user_id: undefined, reason: "Token expire", }) @@ -81,7 +81,7 @@ const UserProvider = (props: UserProviderProps) => { }) .catch((error: AxiosError<{ error: string; email: string }>) => { pushToDataLayer("login_failed", { - user_id: null, + user_id: undefined, origin, error_reason: error.response?.data.error, }) diff --git a/app/javascript/hooks/analytics/useGTMDataLayer.tsx b/app/javascript/hooks/analytics/useGTMDataLayer.tsx index 7187d8a8d..178553dc7 100644 --- a/app/javascript/hooks/analytics/useGTMDataLayer.tsx +++ b/app/javascript/hooks/analytics/useGTMDataLayer.tsx @@ -1,21 +1,41 @@ -import { useCallback } from "react" +import { useCallback, useContext } from "react" import TagManager from "react-gtm-module" +import UserContext from "../../authentication/context/UserContext" +const validateAndPushData = (event, data) => { + if (!data || typeof data !== "object") { + console.error("Data must be an object when pushing to the data layer.") + return + } + if (!event) { + console.error("An event must be provided when pushing to the data layer.") + return + } + if (data?.event || data?.event_timestamp) { + console.error("Data object cannot contain an 'event' or 'event_timestamp' key.") + return + } + TagManager.dataLayer({ dataLayer: { event, event_timestamp: new Date().toISOString(), ...data } }) +} + +// Only use this hook if it is within the UserContext export const useGTMDataLayer = () => { + const { profile } = useContext(UserContext) + + const pushToDataLayer = useCallback( + (event, data) => { + validateAndPushData(event, { user_id: profile?.id || undefined, ...data }) + }, + [profile?.id] + ) + + return { pushToDataLayer } +} + +// A pure version of the hook that does not rely on any context +export const useGTMDataLayerWithoutUserContext = () => { const pushToDataLayer = useCallback((event, data) => { - if (!data || typeof data !== "object") { - console.error("Data must be an object when pushing to the data layer.") - return - } - if (!event) { - console.error("An event must be provided when pushing to the data layer.") - return - } - if (data?.event) { - console.error("Data object cannot contain an 'event' key.") - return - } - TagManager.dataLayer({ dataLayer: { event, ...data } }) + validateAndPushData(event, data) }, []) return { pushToDataLayer }