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 }