Skip to content

Commit

Permalink
feat: send user id and timestamp
Browse files Browse the repository at this point in the history
  • Loading branch information
chadbrokaw committed Jan 29, 2025
1 parent 41f7347 commit 2ccc967
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 44 deletions.
120 changes: 94 additions & 26 deletions app/javascript/__tests__/hooks/useGTMDataLayer.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<UserContext.Provider value={{ profile: mockProfileStub }}>{children}</UserContext.Provider>
)

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 }) => (
<UserContext.Provider value={{ profile: undefined }}>{children}</UserContext.Provider>
)

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,
},
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ListingDetail assetPaths="/" />)

expect(TagManager.dataLayer).toHaveBeenCalledWith({
Expand All @@ -95,6 +98,8 @@ describe("Listing Detail", () => {
listing_record_type: "Ownership",
listing_status: "Active",
listing_tenure: "New sale",
event_timestamp: mockedDate.toISOString(),
listingType: undefined,
},
})
})
Expand Down
8 changes: 4 additions & 4 deletions app/javascript/authentication/context/UserProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(() => {
Expand All @@ -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",
})

Expand Down Expand Up @@ -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,
})
Expand Down
48 changes: 34 additions & 14 deletions app/javascript/hooks/analytics/useGTMDataLayer.tsx
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down

0 comments on commit 2ccc967

Please sign in to comment.