From d282e5d8cc59603549cb0d06b04080798da549a7 Mon Sep 17 00:00:00 2001 From: Urvashi Sharma Date: Thu, 6 Feb 2025 16:32:20 +0530 Subject: [PATCH 1/7] feat(analytics): Implement usage analytics with react-ga4 --- package.json | 2 +- src/components/App/App.test.tsx | 20 +++++++++-------- src/components/App/App.tsx | 9 +++++--- .../CaptureRoutes/CaptureRoutes.test.tsx | 9 +++++--- src/components/LogIn/LogIn.tsx | 22 +++++++++++++++++++ src/hooks/useAnalytics.test.ts | 20 +++++++++++------ src/hooks/useAnalytics.tsx | 15 +++++++++---- src/testing/setup.ts | 2 +- yarn.lock | 13 +++++------ 9 files changed, 76 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index dbabc6d01..4d634379c 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "prismjs": "1.29.0", "react": "19.0.0", "react-dom": "19.0.0", - "react-ga": "3.3.1", + "react-ga4": "2.1.0", "react-hot-toast": "2.5.1", "react-json-tree": "0.19.0", "react-redux": "9.2.0", diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx index 9c546435c..4f0767082 100644 --- a/src/components/App/App.test.tsx +++ b/src/components/App/App.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from "@testing-library/react"; -import * as reactGA from "react-ga"; +import ReactGA from "react-ga4"; import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; import { vi } from "vitest"; @@ -14,9 +14,8 @@ vi.mock("components/Routes", () => ({ default: vi.fn(), })); -vi.mock("react-ga", () => ({ - initialize: vi.fn(), - pageview: vi.fn(), +vi.mock("react-ga4", () => ({ + default: { initialize: vi.fn(), send: vi.fn() }, })); vi.mock("react-router", async () => { @@ -63,8 +62,8 @@ describe("App", () => { it("sends pageview events", () => { vi.stubEnv("PROD", true); window.happyDOM.setURL("/models"); - const initializeSpy = vi.spyOn(reactGA, "initialize"); - const pageviewSpy = vi.spyOn(reactGA, "pageview"); + const initializeSpy = vi.spyOn(ReactGA, "initialize"); + const pageviewSpy = vi.spyOn(ReactGA, "send"); const state = rootStateFactory.withGeneralConfig().build(); const store = mockStore(state); render( @@ -73,14 +72,17 @@ describe("App", () => { , ); expect(initializeSpy).toHaveBeenCalled(); - expect(pageviewSpy).toHaveBeenCalledWith("/models"); + expect(pageviewSpy).toHaveBeenCalledWith({ + hitType: "page_view", + page: "/models", + }); }); it("does not send pageview events if analytics is disabled", () => { vi.stubEnv("PROD", true); window.happyDOM.setURL("/models"); - const initializeSpy = vi.spyOn(reactGA, "initialize"); - const pageviewSpy = vi.spyOn(reactGA, "pageview"); + const initializeSpy = vi.spyOn(ReactGA, "initialize"); + const pageviewSpy = vi.spyOn(ReactGA, "send"); const state = rootStateFactory.build({ general: generalStateFactory.build({ config: configFactory.build({ diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 46e6fa219..781f0b0e1 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -1,4 +1,4 @@ -import { initialize, pageview } from "react-ga"; +import ReactGA from "react-ga4"; import ConnectionError from "components/ConnectionError"; import ErrorBoundary from "components/ErrorBoundary"; @@ -12,8 +12,11 @@ function App() { const isProduction = import.meta.env.PROD; const analyticsEnabled = useAppSelector(getAnalyticsEnabled); if (isProduction && analyticsEnabled) { - initialize("UA-1018242-68"); - pageview(window.location.href.replace(window.location.origin, "")); + ReactGA.initialize("UA-1018242-68"); // TODO: should use the GA4 Measurement ID (which starts with "G-") + ReactGA.send({ + hitType: "page_view", + page: window.location.href.replace(window.location.origin, ""), + }); } return ( diff --git a/src/components/CaptureRoutes/CaptureRoutes.test.tsx b/src/components/CaptureRoutes/CaptureRoutes.test.tsx index 99e6a7c70..7804e14f0 100644 --- a/src/components/CaptureRoutes/CaptureRoutes.test.tsx +++ b/src/components/CaptureRoutes/CaptureRoutes.test.tsx @@ -1,4 +1,4 @@ -import * as reactGA from "react-ga"; +import ReactGA from "react-ga4"; import { BrowserRouter, Route, Routes } from "react-router"; import type { MockInstance } from "vitest"; import { vi } from "vitest"; @@ -17,7 +17,7 @@ describe("CaptureRoutes", () => { beforeEach(() => { vi.stubEnv("PROD", true); - pageviewSpy = vi.spyOn(reactGA, "pageview"); + pageviewSpy = vi.spyOn(ReactGA, "send"); state = rootStateFactory.build({ general: generalStateFactory.build({ config: configFactory.build({ @@ -43,6 +43,9 @@ describe("CaptureRoutes", () => { , ); - expect(pageviewSpy).toHaveBeenCalledWith("/new/path"); + expect(pageviewSpy).toHaveBeenCalledWith({ + hitType: "page_view", + page: "/new/path", + }); }); }); diff --git a/src/components/LogIn/LogIn.tsx b/src/components/LogIn/LogIn.tsx index 82fa0f26f..a5943c000 100644 --- a/src/components/LogIn/LogIn.tsx +++ b/src/components/LogIn/LogIn.tsx @@ -9,6 +9,7 @@ import AuthenticationButton from "components/AuthenticationButton"; import Logo from "components/Logo"; import ToastCard from "components/ToastCard"; import type { ToastInstance } from "components/ToastCard"; +import useAnalytics from "hooks/useAnalytics"; import { getConfig, getLoginError, @@ -16,6 +17,8 @@ import { getWSControllerURL, isLoggedIn, getIsJuju, + getAppVersion, + getControllerConnection, } from "store/general/selectors"; import { AuthMethod } from "store/general/types"; import { useAppSelector } from "store/store"; @@ -27,9 +30,14 @@ import { ErrorResponse, Label, TestId } from "./types"; export default function LogIn() { const viewedAuthRequests = useRef([]); + const sendAnalytics = useAnalytics(); const config = useSelector(getConfig); const isJuju = useSelector(getIsJuju); + const appVersion = useSelector(getAppVersion); const wsControllerURL = useAppSelector(getWSControllerURL); + const controllerVersion = useAppSelector((state) => + getControllerConnection(state, wsControllerURL), + )?.serverVersion; const userIsLoggedIn = useAppSelector((state) => isLoggedIn(state, wsControllerURL), ); @@ -71,6 +79,20 @@ export default function LogIn() { }); }, [visitURLs]); + useEffect(() => { + if (userIsLoggedIn) { + sendAnalytics({ + category: "Authentication", + action: "User Login", + eventParams: { + dashboardVersion: appVersion ?? "", + controllerVersion: controllerVersion ?? "", + isJuju: (!!isJuju).toString(), + }, + }); + } + }, [userIsLoggedIn, appVersion, controllerVersion, isJuju, sendAnalytics]); + let form: ReactNode = null; switch (config?.authMethod) { case AuthMethod.CANDID: diff --git a/src/hooks/useAnalytics.test.ts b/src/hooks/useAnalytics.test.ts index c7bb3a1d5..1eafe842b 100644 --- a/src/hooks/useAnalytics.test.ts +++ b/src/hooks/useAnalytics.test.ts @@ -1,5 +1,5 @@ import { renderHook } from "@testing-library/react"; -import * as reactGA from "react-ga"; +import ReactGA from "react-ga4"; import type { MockInstance } from "vitest"; import { vi } from "vitest"; @@ -7,9 +7,12 @@ import * as store from "store/store"; import useAnalytics from "./useAnalytics"; -vi.mock("react-ga", () => ({ - event: vi.fn(), - pageview: vi.fn(), +vi.mock("react-ga4", () => ({ + default: { + initialize: vi.fn(), + send: vi.fn(), + event: vi.fn(), + }, })); describe("useAnalytics", () => { @@ -18,8 +21,8 @@ describe("useAnalytics", () => { beforeEach(() => { vi.stubEnv("PROD", true); - eventSpy = vi.spyOn(reactGA, "event"); - pageviewSpy = vi.spyOn(reactGA, "pageview"); + eventSpy = vi.spyOn(ReactGA, "event"); + pageviewSpy = vi.spyOn(ReactGA, "send"); }); afterEach(() => { @@ -57,7 +60,10 @@ describe("useAnalytics", () => { ); const { result } = renderHook(() => useAnalytics()); result.current({ path: "/some/path" }); - expect(pageviewSpy).toHaveBeenCalledWith("/some/path"); + expect(pageviewSpy).toHaveBeenCalledWith({ + hitType: "page_view", + page: "/some/path", + }); }); it("can send events", () => { diff --git a/src/hooks/useAnalytics.tsx b/src/hooks/useAnalytics.tsx index 9874ffb94..dd687cb1f 100644 --- a/src/hooks/useAnalytics.tsx +++ b/src/hooks/useAnalytics.tsx @@ -1,4 +1,4 @@ -import { pageview, event } from "react-ga"; +import ReactGA from "react-ga4"; import { getAnalyticsEnabled } from "store/general/selectors"; import { useAppSelector } from "store/store"; @@ -7,21 +7,28 @@ type AnalyticMessage = { path?: string; category?: string; action?: string; + eventParams?: { [key: string]: string }; }; export default function useAnalytics() { const analyticsEnabled = useAppSelector(getAnalyticsEnabled); - return ({ path, category = "", action = "" }: AnalyticMessage) => { + return ({ + path, + category = "", + action = "", + eventParams = {}, + }: AnalyticMessage) => { const isProduction = import.meta.env.PROD; if (!isProduction || !analyticsEnabled) { return; } if (path) { - pageview(path); + ReactGA.send({ hitType: "page_view", page: path }); } else { - event({ + ReactGA.event({ category, action, + ...eventParams, }); } }; diff --git a/src/testing/setup.ts b/src/testing/setup.ts index 4485e8993..428fefa39 100644 --- a/src/testing/setup.ts +++ b/src/testing/setup.ts @@ -5,7 +5,7 @@ import createFetchMock from "vitest-fetch-mock"; import { logger } from "utils/logger"; -vi.mock("react-ga"); +vi.mock("react-ga4"); const fetchMocker = createFetchMock(vi); // sets globalThis.fetch and globalThis.fetchMock to our mocked version diff --git a/yarn.lock b/yarn.lock index 93ef5c23e..f578ed9c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7530,7 +7530,7 @@ __metadata: react: "npm:19.0.0" react-anchorme: "npm:4.0.1" react-dom: "npm:19.0.0" - react-ga: "npm:3.3.1" + react-ga4: "npm:2.1.0" react-hot-toast: "npm:2.5.1" react-json-tree: "npm:0.19.0" react-redux: "npm:9.2.0" @@ -9194,13 +9194,10 @@ __metadata: languageName: node linkType: hard -"react-ga@npm:3.3.1": - version: 3.3.1 - resolution: "react-ga@npm:3.3.1" - peerDependencies: - prop-types: ^15.6.0 - react: ^15.6.2 || ^16.0 || ^17 || ^18 - checksum: 10c0/a2a61c138887c651a59b49669141246e9a01803bd67c5d7581016949d2c8acdbef14db1a0039b855dcd3c979c711b4c45d910fcaa49e27165c13cdc685b4c2e0 +"react-ga4@npm:2.1.0": + version: 2.1.0 + resolution: "react-ga4@npm:2.1.0" + checksum: 10c0/314aa86dd7cb868535f26bfb8b537d3b3c20649c66b2b942fba72e081295441446932a4ae96499231c8a4836ab0a222a97b1bd03633b8cc1477991efe93444cd languageName: node linkType: hard From a4422243190dbd1727f9247278c1331e9692a00d Mon Sep 17 00:00:00 2001 From: Urvashi Sharma Date: Sat, 8 Feb 2025 00:14:28 +0530 Subject: [PATCH 2/7] feat(analytics): Fix syntax for analytics with react-ga4 --- src/components/App/App.test.tsx | 2 +- src/components/App/App.tsx | 12 +++++-- .../CaptureRoutes/CaptureRoutes.test.tsx | 2 +- src/components/LogIn/LogIn.tsx | 13 +------- src/hooks/useAnalytics.test.ts | 15 +++++---- src/hooks/useAnalytics.tsx | 32 +++++++++++++------ 6 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx index 4f0767082..c1e000202 100644 --- a/src/components/App/App.test.tsx +++ b/src/components/App/App.test.tsx @@ -73,7 +73,7 @@ describe("App", () => { ); expect(initializeSpy).toHaveBeenCalled(); expect(pageviewSpy).toHaveBeenCalledWith({ - hitType: "page_view", + hitType: "pageview", page: "/models", }); }); diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 781f0b0e1..242d138b2 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -12,9 +12,17 @@ function App() { const isProduction = import.meta.env.PROD; const analyticsEnabled = useAppSelector(getAnalyticsEnabled); if (isProduction && analyticsEnabled) { - ReactGA.initialize("UA-1018242-68"); // TODO: should use the GA4 Measurement ID (which starts with "G-") + ReactGA.initialize("G-JHXHM8VXJ1", { + gtagOptions: { + custom_map: { + dimension1: "dashboardVersion", + dimension2: "controllerVersion", + dimension3: "isJuju", + }, + }, + }); ReactGA.send({ - hitType: "page_view", + hitType: "pageview", page: window.location.href.replace(window.location.origin, ""), }); } diff --git a/src/components/CaptureRoutes/CaptureRoutes.test.tsx b/src/components/CaptureRoutes/CaptureRoutes.test.tsx index 7804e14f0..db3ce2b94 100644 --- a/src/components/CaptureRoutes/CaptureRoutes.test.tsx +++ b/src/components/CaptureRoutes/CaptureRoutes.test.tsx @@ -44,7 +44,7 @@ describe("CaptureRoutes", () => { , ); expect(pageviewSpy).toHaveBeenCalledWith({ - hitType: "page_view", + hitType: "pageview", page: "/new/path", }); }); diff --git a/src/components/LogIn/LogIn.tsx b/src/components/LogIn/LogIn.tsx index a5943c000..49411f9a8 100644 --- a/src/components/LogIn/LogIn.tsx +++ b/src/components/LogIn/LogIn.tsx @@ -17,8 +17,6 @@ import { getWSControllerURL, isLoggedIn, getIsJuju, - getAppVersion, - getControllerConnection, } from "store/general/selectors"; import { AuthMethod } from "store/general/types"; import { useAppSelector } from "store/store"; @@ -33,11 +31,7 @@ export default function LogIn() { const sendAnalytics = useAnalytics(); const config = useSelector(getConfig); const isJuju = useSelector(getIsJuju); - const appVersion = useSelector(getAppVersion); const wsControllerURL = useAppSelector(getWSControllerURL); - const controllerVersion = useAppSelector((state) => - getControllerConnection(state, wsControllerURL), - )?.serverVersion; const userIsLoggedIn = useAppSelector((state) => isLoggedIn(state, wsControllerURL), ); @@ -84,14 +78,9 @@ export default function LogIn() { sendAnalytics({ category: "Authentication", action: "User Login", - eventParams: { - dashboardVersion: appVersion ?? "", - controllerVersion: controllerVersion ?? "", - isJuju: (!!isJuju).toString(), - }, }); } - }, [userIsLoggedIn, appVersion, controllerVersion, isJuju, sendAnalytics]); + }, [userIsLoggedIn, sendAnalytics]); let form: ReactNode = null; switch (config?.authMethod) { diff --git a/src/hooks/useAnalytics.test.ts b/src/hooks/useAnalytics.test.ts index 1eafe842b..1cf20746b 100644 --- a/src/hooks/useAnalytics.test.ts +++ b/src/hooks/useAnalytics.test.ts @@ -1,9 +1,9 @@ -import { renderHook } from "@testing-library/react"; import ReactGA from "react-ga4"; import type { MockInstance } from "vitest"; import { vi } from "vitest"; import * as store from "store/store"; +import { renderWrappedHook } from "testing/utils"; import useAnalytics from "./useAnalytics"; @@ -38,7 +38,7 @@ describe("useAnalytics", () => { vi.fn().mockReturnValue(true), ); vi.stubEnv("PROD", false); - const { result } = renderHook(() => useAnalytics()); + const { result } = renderWrappedHook(() => useAnalytics()); result.current({ path: "/some/path" }); expect(eventSpy).not.toHaveBeenCalled(); expect(pageviewSpy).not.toHaveBeenCalled(); @@ -48,7 +48,7 @@ describe("useAnalytics", () => { vi.spyOn(store, "useAppSelector").mockImplementation( vi.fn().mockReturnValue(false), ); - const { result } = renderHook(() => useAnalytics()); + const { result } = renderWrappedHook(() => useAnalytics()); result.current({ path: "/some/path" }); expect(eventSpy).not.toHaveBeenCalled(); expect(pageviewSpy).not.toHaveBeenCalled(); @@ -58,10 +58,10 @@ describe("useAnalytics", () => { vi.spyOn(store, "useAppSelector").mockImplementation( vi.fn().mockReturnValue(true), ); - const { result } = renderHook(() => useAnalytics()); + const { result } = renderWrappedHook(() => useAnalytics()); result.current({ path: "/some/path" }); expect(pageviewSpy).toHaveBeenCalledWith({ - hitType: "page_view", + hitType: "pageview", page: "/some/path", }); }); @@ -70,11 +70,14 @@ describe("useAnalytics", () => { vi.spyOn(store, "useAppSelector").mockImplementation( vi.fn().mockReturnValue(true), ); - const { result } = renderHook(() => useAnalytics()); + const { result } = renderWrappedHook(() => useAnalytics()); result.current({ category: "sidebar", action: "toggle" }); expect(eventSpy).toHaveBeenCalledWith({ category: "sidebar", action: "toggle", + controllerVersion: "", + dashboardVersion: "", + isJuju: "false", }); }); }); diff --git a/src/hooks/useAnalytics.tsx b/src/hooks/useAnalytics.tsx index dd687cb1f..4e3d44db1 100644 --- a/src/hooks/useAnalytics.tsx +++ b/src/hooks/useAnalytics.tsx @@ -1,34 +1,46 @@ import ReactGA from "react-ga4"; +import { useSelector } from "react-redux"; -import { getAnalyticsEnabled } from "store/general/selectors"; +import { + getAnalyticsEnabled, + getAppVersion, + getControllerConnection, + getIsJuju, + getWSControllerURL, +} from "store/general/selectors"; import { useAppSelector } from "store/store"; type AnalyticMessage = { path?: string; category?: string; action?: string; - eventParams?: { [key: string]: string }; }; export default function useAnalytics() { const analyticsEnabled = useAppSelector(getAnalyticsEnabled); - return ({ - path, - category = "", - action = "", - eventParams = {}, - }: AnalyticMessage) => { + const isJuju = useSelector(getIsJuju); + const appVersion = useSelector(getAppVersion); + const wsControllerURL = useAppSelector(getWSControllerURL); + const controllerVersion = useAppSelector((state) => + getControllerConnection(state, wsControllerURL), + )?.serverVersion; + + return ({ path, category = "", action = "" }: AnalyticMessage) => { const isProduction = import.meta.env.PROD; if (!isProduction || !analyticsEnabled) { return; } if (path) { - ReactGA.send({ hitType: "page_view", page: path }); + ReactGA.send({ hitType: "pageview", page: path }); } else { ReactGA.event({ category, action, - ...eventParams, + ...{ + dashboardVersion: appVersion ?? "", + controllerVersion: controllerVersion ?? "", + isJuju: (!!isJuju).toString(), + }, }); } }; From 141c9663cf2cebea148c8d4c3e39e36311d3347f Mon Sep 17 00:00:00 2001 From: Urvashi Sharma Date: Tue, 18 Feb 2025 12:44:29 +0530 Subject: [PATCH 3/7] feat(testing): Replace mocking store with renderWrappedHook state --- src/components/LogIn/LogIn.tsx | 4 +-- src/hooks/useAnalytics.test.ts | 55 +++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/components/LogIn/LogIn.tsx b/src/components/LogIn/LogIn.tsx index 49411f9a8..25a80b1f9 100644 --- a/src/components/LogIn/LogIn.tsx +++ b/src/components/LogIn/LogIn.tsx @@ -77,10 +77,10 @@ export default function LogIn() { if (userIsLoggedIn) { sendAnalytics({ category: "Authentication", - action: "User Login", + action: `User Login (${config?.authMethod})`, }); } - }, [userIsLoggedIn, sendAnalytics]); + }, [userIsLoggedIn, sendAnalytics, config]); let form: ReactNode = null; switch (config?.authMethod) { diff --git a/src/hooks/useAnalytics.test.ts b/src/hooks/useAnalytics.test.ts index 1cf20746b..33920dcf6 100644 --- a/src/hooks/useAnalytics.test.ts +++ b/src/hooks/useAnalytics.test.ts @@ -2,7 +2,8 @@ import ReactGA from "react-ga4"; import type { MockInstance } from "vitest"; import { vi } from "vitest"; -import * as store from "store/store"; +import { rootStateFactory } from "testing/factories"; +import { configFactory, generalStateFactory } from "testing/factories/general"; import { renderWrappedHook } from "testing/utils"; import useAnalytics from "./useAnalytics"; @@ -34,31 +35,46 @@ describe("useAnalytics", () => { }); it("does not send events in development", () => { - vi.spyOn(store, "useAppSelector").mockImplementation( - vi.fn().mockReturnValue(true), - ); vi.stubEnv("PROD", false); - const { result } = renderWrappedHook(() => useAnalytics()); + const { result } = renderWrappedHook(() => useAnalytics(), { + state: rootStateFactory.build({ + general: generalStateFactory.build({ + config: configFactory.build({ + analyticsEnabled: true, + }), + }), + }), + }); result.current({ path: "/some/path" }); expect(eventSpy).not.toHaveBeenCalled(); expect(pageviewSpy).not.toHaveBeenCalled(); }); it("does not send events if analytics are disabled", () => { - vi.spyOn(store, "useAppSelector").mockImplementation( - vi.fn().mockReturnValue(false), - ); - const { result } = renderWrappedHook(() => useAnalytics()); + const { result } = renderWrappedHook(() => useAnalytics(), { + state: rootStateFactory.build({ + general: generalStateFactory.build({ + config: configFactory.build({ + analyticsEnabled: false, + }), + }), + }), + }); result.current({ path: "/some/path" }); expect(eventSpy).not.toHaveBeenCalled(); expect(pageviewSpy).not.toHaveBeenCalled(); }); it("can send pageview events", () => { - vi.spyOn(store, "useAppSelector").mockImplementation( - vi.fn().mockReturnValue(true), - ); - const { result } = renderWrappedHook(() => useAnalytics()); + const { result } = renderWrappedHook(() => useAnalytics(), { + state: rootStateFactory.build({ + general: generalStateFactory.build({ + config: configFactory.build({ + analyticsEnabled: true, + }), + }), + }), + }); result.current({ path: "/some/path" }); expect(pageviewSpy).toHaveBeenCalledWith({ hitType: "pageview", @@ -67,10 +83,15 @@ describe("useAnalytics", () => { }); it("can send events", () => { - vi.spyOn(store, "useAppSelector").mockImplementation( - vi.fn().mockReturnValue(true), - ); - const { result } = renderWrappedHook(() => useAnalytics()); + const { result } = renderWrappedHook(() => useAnalytics(), { + state: rootStateFactory.build({ + general: generalStateFactory.build({ + config: configFactory.build({ + analyticsEnabled: true, + }), + }), + }), + }); result.current({ category: "sidebar", action: "toggle" }); expect(eventSpy).toHaveBeenCalledWith({ category: "sidebar", From 22e68022ffdb1b8d86cf0ac5196d4366edeb183d Mon Sep 17 00:00:00 2001 From: Urvashi Sharma Date: Tue, 18 Feb 2025 15:21:10 +0530 Subject: [PATCH 4/7] feat(analytics): Set custom dimensions with react-ga4 --- src/components/App/App.tsx | 10 ++++------ src/hooks/useAnalytics.test.ts | 4 ++-- src/hooks/useAnalytics.tsx | 11 ++++------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 242d138b2..5ec50222e 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -13,12 +13,10 @@ function App() { const analyticsEnabled = useAppSelector(getAnalyticsEnabled); if (isProduction && analyticsEnabled) { ReactGA.initialize("G-JHXHM8VXJ1", { - gtagOptions: { - custom_map: { - dimension1: "dashboardVersion", - dimension2: "controllerVersion", - dimension3: "isJuju", - }, + gaOptions: { + dimension1: "dashboardVersion", + dimension2: "controllerVersion", + dimension3: "isJuju", }, }); ReactGA.send({ diff --git a/src/hooks/useAnalytics.test.ts b/src/hooks/useAnalytics.test.ts index 33920dcf6..8e3e734c8 100644 --- a/src/hooks/useAnalytics.test.ts +++ b/src/hooks/useAnalytics.test.ts @@ -13,6 +13,7 @@ vi.mock("react-ga4", () => ({ initialize: vi.fn(), send: vi.fn(), event: vi.fn(), + set: vi.fn(), }, })); @@ -93,9 +94,8 @@ describe("useAnalytics", () => { }), }); result.current({ category: "sidebar", action: "toggle" }); - expect(eventSpy).toHaveBeenCalledWith({ + expect(eventSpy).toHaveBeenCalledWith("toggle", { category: "sidebar", - action: "toggle", controllerVersion: "", dashboardVersion: "", isJuju: "false", diff --git a/src/hooks/useAnalytics.tsx b/src/hooks/useAnalytics.tsx index 4e3d44db1..9bc5d91d9 100644 --- a/src/hooks/useAnalytics.tsx +++ b/src/hooks/useAnalytics.tsx @@ -33,14 +33,11 @@ export default function useAnalytics() { if (path) { ReactGA.send({ hitType: "pageview", page: path }); } else { - ReactGA.event({ + ReactGA.event(action, { category, - action, - ...{ - dashboardVersion: appVersion ?? "", - controllerVersion: controllerVersion ?? "", - isJuju: (!!isJuju).toString(), - }, + dashboardVersion: appVersion ?? "", + controllerVersion: controllerVersion ?? "", + isJuju: (!!isJuju).toString(), }); } }; From a5951687dc5fd4aa3aa01406235f16a574c70d61 Mon Sep 17 00:00:00 2001 From: Urvashi Sharma Date: Fri, 21 Feb 2025 00:33:14 +0530 Subject: [PATCH 5/7] feat(analytics): Send login event in poller --- src/components/App/App.tsx | 8 +------- src/components/LogIn/LogIn.tsx | 11 ---------- src/hooks/useAnalytics.tsx | 26 +++++++++--------------- src/store/middleware/model-poller.ts | 24 +++++++++++++++++----- src/utils/analytics.ts | 30 ++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 40 deletions(-) create mode 100644 src/utils/analytics.ts diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 5ec50222e..a5de21151 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -12,13 +12,7 @@ function App() { const isProduction = import.meta.env.PROD; const analyticsEnabled = useAppSelector(getAnalyticsEnabled); if (isProduction && analyticsEnabled) { - ReactGA.initialize("G-JHXHM8VXJ1", { - gaOptions: { - dimension1: "dashboardVersion", - dimension2: "controllerVersion", - dimension3: "isJuju", - }, - }); + ReactGA.initialize("G-JHXHM8VXJ1"); // send { gaOptions: { debug_mode: true } } as second param to enable debugView ReactGA.send({ hitType: "pageview", page: window.location.href.replace(window.location.origin, ""), diff --git a/src/components/LogIn/LogIn.tsx b/src/components/LogIn/LogIn.tsx index 25a80b1f9..82fa0f26f 100644 --- a/src/components/LogIn/LogIn.tsx +++ b/src/components/LogIn/LogIn.tsx @@ -9,7 +9,6 @@ import AuthenticationButton from "components/AuthenticationButton"; import Logo from "components/Logo"; import ToastCard from "components/ToastCard"; import type { ToastInstance } from "components/ToastCard"; -import useAnalytics from "hooks/useAnalytics"; import { getConfig, getLoginError, @@ -28,7 +27,6 @@ import { ErrorResponse, Label, TestId } from "./types"; export default function LogIn() { const viewedAuthRequests = useRef([]); - const sendAnalytics = useAnalytics(); const config = useSelector(getConfig); const isJuju = useSelector(getIsJuju); const wsControllerURL = useAppSelector(getWSControllerURL); @@ -73,15 +71,6 @@ export default function LogIn() { }); }, [visitURLs]); - useEffect(() => { - if (userIsLoggedIn) { - sendAnalytics({ - category: "Authentication", - action: `User Login (${config?.authMethod})`, - }); - } - }, [userIsLoggedIn, sendAnalytics, config]); - let form: ReactNode = null; switch (config?.authMethod) { case AuthMethod.CANDID: diff --git a/src/hooks/useAnalytics.tsx b/src/hooks/useAnalytics.tsx index 9bc5d91d9..fb09d98e6 100644 --- a/src/hooks/useAnalytics.tsx +++ b/src/hooks/useAnalytics.tsx @@ -1,4 +1,3 @@ -import ReactGA from "react-ga4"; import { useSelector } from "react-redux"; import { @@ -9,6 +8,7 @@ import { getWSControllerURL, } from "store/general/selectors"; import { useAppSelector } from "store/store"; +import analytics from "utils/analytics"; type AnalyticMessage = { path?: string; @@ -18,27 +18,19 @@ type AnalyticMessage = { export default function useAnalytics() { const analyticsEnabled = useAppSelector(getAnalyticsEnabled); + const isProduction = import.meta.env.PROD; const isJuju = useSelector(getIsJuju); const appVersion = useSelector(getAppVersion); const wsControllerURL = useAppSelector(getWSControllerURL); const controllerVersion = useAppSelector((state) => getControllerConnection(state, wsControllerURL), )?.serverVersion; - - return ({ path, category = "", action = "" }: AnalyticMessage) => { - const isProduction = import.meta.env.PROD; - if (!isProduction || !analyticsEnabled) { - return; - } - if (path) { - ReactGA.send({ hitType: "pageview", page: path }); - } else { - ReactGA.event(action, { - category, - dashboardVersion: appVersion ?? "", - controllerVersion: controllerVersion ?? "", - isJuju: (!!isJuju).toString(), - }); - } + const eventParams = { + dashboardVersion: appVersion ?? "", + controllerVersion: controllerVersion ?? "", + isJuju: (!!isJuju).toString(), }; + + return (props: AnalyticMessage) => + analytics(!!analyticsEnabled, isProduction, eventParams, props); } diff --git a/src/store/middleware/model-poller.ts b/src/store/middleware/model-poller.ts index 8e7f5c26a..550fd2345 100644 --- a/src/store/middleware/model-poller.ts +++ b/src/store/middleware/model-poller.ts @@ -16,13 +16,14 @@ import { whoami } from "juju/jimm/thunks"; import type { ConnectionWithFacades } from "juju/types"; import { actions as appActions, thunks as appThunks } from "store/app"; import { actions as generalActions } from "store/general"; -import { isLoggedIn } from "store/general/selectors"; +import { getAppVersion, getIsJuju, isLoggedIn } from "store/general/selectors"; import { AuthMethod } from "store/general/types"; import { actions as jujuActions } from "store/juju"; import type { RootState, Store } from "store/store"; import { isSpecificAction } from "types"; import { toErrorString } from "utils"; import { logger } from "utils/logger"; +import analytics from "utils/analytics"; export enum LoginError { LOG = "Unable to log into controller.", @@ -134,11 +135,24 @@ export const modelPollerMiddleware: Middleware< // XXX Now that we can register multiple controllers this needs // to be sent per controller. - if ( - import.meta.env.PROD && - window.jujuDashboardConfig?.analyticsEnabled - ) { + const isProduction = import.meta.env.PROD; + const analyticsEnabled = window.jujuDashboardConfig?.analyticsEnabled; + if (isProduction && analyticsEnabled) { + const isJuju = (!!getIsJuju(reduxStore.getState())).toString(); + const dashboardVersion = getAppVersion(reduxStore.getState()) ?? ""; + const controllerVersion = conn.info.serverVersion ?? ""; + Sentry.setTag("jujuVersion", conn.info.serverVersion); + + analytics( + analyticsEnabled, + isProduction, + { dashboardVersion, controllerVersion, isJuju }, + { + category: "Authentication", + action: `User Login (${authMethod})`, + }, + ); } // Remove the getFacade function as this doesn't need to be stored in Redux. diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts new file mode 100644 index 000000000..2e375e967 --- /dev/null +++ b/src/utils/analytics.ts @@ -0,0 +1,30 @@ +import ReactGA from "react-ga4"; + +type EventParams = { + dashboardVersion: string; + controllerVersion: string; + isJuju: string; +}; + +type AnalyticsMessage = { path?: string; category?: string; action?: string }; + +const analytics = ( + analyticsEnabled: boolean, + isProduction: boolean, + eventParams: EventParams, + { path, category = "", action = "" }: AnalyticsMessage, +) => { + if (!isProduction || !analyticsEnabled) { + return; + } + if (path) { + ReactGA.send({ hitType: "pageview", page: path }); + } else { + ReactGA.event(action, { + category, + ...eventParams, + }); + } +}; + +export default analytics; From e1bd8c914ce30cd781bde1450b4bbdcca9c87e47 Mon Sep 17 00:00:00 2001 From: Urvashi Sharma Date: Fri, 21 Feb 2025 13:19:59 +0530 Subject: [PATCH 6/7] feat(analytics): Enable debug view in development --- src/components/App/App.tsx | 7 ++- src/hooks/useAnalytics.test.ts | 66 +++++++--------------------- src/hooks/useAnalytics.tsx | 3 +- src/store/middleware/model-poller.ts | 33 +++++++------- src/utils/analytics.test.ts | 64 +++++++++++++++++++++++++++ src/utils/analytics.ts | 3 +- 6 files changed, 104 insertions(+), 72 deletions(-) create mode 100644 src/utils/analytics.test.ts diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index a5de21151..36118aa2f 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -11,8 +11,11 @@ import "../../scss/index.scss"; function App() { const isProduction = import.meta.env.PROD; const analyticsEnabled = useAppSelector(getAnalyticsEnabled); - if (isProduction && analyticsEnabled) { - ReactGA.initialize("G-JHXHM8VXJ1"); // send { gaOptions: { debug_mode: true } } as second param to enable debugView + if (analyticsEnabled) { + ReactGA.initialize( + "G-JHXHM8VXJ1", + isProduction ? {} : { gaOptions: { debug_mode: true } }, + ); ReactGA.send({ hitType: "pageview", page: window.location.href.replace(window.location.origin, ""), diff --git a/src/hooks/useAnalytics.test.ts b/src/hooks/useAnalytics.test.ts index 8e3e734c8..9a73acb3d 100644 --- a/src/hooks/useAnalytics.test.ts +++ b/src/hooks/useAnalytics.test.ts @@ -1,56 +1,21 @@ -import ReactGA from "react-ga4"; -import type { MockInstance } from "vitest"; import { vi } from "vitest"; import { rootStateFactory } from "testing/factories"; import { configFactory, generalStateFactory } from "testing/factories/general"; import { renderWrappedHook } from "testing/utils"; +import * as analyticsUtils from "utils/analytics"; import useAnalytics from "./useAnalytics"; -vi.mock("react-ga4", () => ({ - default: { - initialize: vi.fn(), - send: vi.fn(), - event: vi.fn(), - set: vi.fn(), - }, -})); - describe("useAnalytics", () => { - let pageviewSpy: MockInstance; - let eventSpy: MockInstance; - beforeEach(() => { - vi.stubEnv("PROD", true); - eventSpy = vi.spyOn(ReactGA, "event"); - pageviewSpy = vi.spyOn(ReactGA, "send"); + vi.spyOn(analyticsUtils, "default").mockImplementation(() => vi.fn()); }); afterEach(() => { localStorage.clear(); }); - afterAll(() => { - vi.unstubAllEnvs(); - }); - - it("does not send events in development", () => { - vi.stubEnv("PROD", false); - const { result } = renderWrappedHook(() => useAnalytics(), { - state: rootStateFactory.build({ - general: generalStateFactory.build({ - config: configFactory.build({ - analyticsEnabled: true, - }), - }), - }), - }); - result.current({ path: "/some/path" }); - expect(eventSpy).not.toHaveBeenCalled(); - expect(pageviewSpy).not.toHaveBeenCalled(); - }); - it("does not send events if analytics are disabled", () => { const { result } = renderWrappedHook(() => useAnalytics(), { state: rootStateFactory.build({ @@ -62,8 +27,11 @@ describe("useAnalytics", () => { }), }); result.current({ path: "/some/path" }); - expect(eventSpy).not.toHaveBeenCalled(); - expect(pageviewSpy).not.toHaveBeenCalled(); + expect(analyticsUtils.default).toHaveBeenCalledWith( + false, + { controllerVersion: "", dashboardVersion: "", isJuju: "false" }, + { path: "/some/path" }, + ); }); it("can send pageview events", () => { @@ -77,10 +45,11 @@ describe("useAnalytics", () => { }), }); result.current({ path: "/some/path" }); - expect(pageviewSpy).toHaveBeenCalledWith({ - hitType: "pageview", - page: "/some/path", - }); + expect(analyticsUtils.default).toHaveBeenCalledWith( + true, + { controllerVersion: "", dashboardVersion: "", isJuju: "false" }, + { path: "/some/path" }, + ); }); it("can send events", () => { @@ -94,11 +63,10 @@ describe("useAnalytics", () => { }), }); result.current({ category: "sidebar", action: "toggle" }); - expect(eventSpy).toHaveBeenCalledWith("toggle", { - category: "sidebar", - controllerVersion: "", - dashboardVersion: "", - isJuju: "false", - }); + expect(analyticsUtils.default).toHaveBeenCalledWith( + true, + { controllerVersion: "", dashboardVersion: "", isJuju: "false" }, + { category: "sidebar", action: "toggle" }, + ); }); }); diff --git a/src/hooks/useAnalytics.tsx b/src/hooks/useAnalytics.tsx index fb09d98e6..8be73b51a 100644 --- a/src/hooks/useAnalytics.tsx +++ b/src/hooks/useAnalytics.tsx @@ -18,7 +18,6 @@ type AnalyticMessage = { export default function useAnalytics() { const analyticsEnabled = useAppSelector(getAnalyticsEnabled); - const isProduction = import.meta.env.PROD; const isJuju = useSelector(getIsJuju); const appVersion = useSelector(getAppVersion); const wsControllerURL = useAppSelector(getWSControllerURL); @@ -32,5 +31,5 @@ export default function useAnalytics() { }; return (props: AnalyticMessage) => - analytics(!!analyticsEnabled, isProduction, eventParams, props); + analytics(!!analyticsEnabled, eventParams, props); } diff --git a/src/store/middleware/model-poller.ts b/src/store/middleware/model-poller.ts index 550fd2345..63dc0fc1d 100644 --- a/src/store/middleware/model-poller.ts +++ b/src/store/middleware/model-poller.ts @@ -22,8 +22,8 @@ import { actions as jujuActions } from "store/juju"; import type { RootState, Store } from "store/store"; import { isSpecificAction } from "types"; import { toErrorString } from "utils"; -import { logger } from "utils/logger"; import analytics from "utils/analytics"; +import { logger } from "utils/logger"; export enum LoginError { LOG = "Unable to log into controller.", @@ -133,26 +133,25 @@ export const modelPollerMiddleware: Middleware< return; } - // XXX Now that we can register multiple controllers this needs - // to be sent per controller. const isProduction = import.meta.env.PROD; const analyticsEnabled = window.jujuDashboardConfig?.analyticsEnabled; - if (isProduction && analyticsEnabled) { - const isJuju = (!!getIsJuju(reduxStore.getState())).toString(); - const dashboardVersion = getAppVersion(reduxStore.getState()) ?? ""; - const controllerVersion = conn.info.serverVersion ?? ""; + const isJuju = (!!getIsJuju(reduxStore.getState())).toString(); + const dashboardVersion = getAppVersion(reduxStore.getState()) ?? ""; + const controllerVersion = conn.info.serverVersion ?? ""; - Sentry.setTag("jujuVersion", conn.info.serverVersion); + analytics( + !!analyticsEnabled, + { dashboardVersion, controllerVersion, isJuju }, + { + category: "Authentication", + action: `User Login (${authMethod})`, + }, + ); - analytics( - analyticsEnabled, - isProduction, - { dashboardVersion, controllerVersion, isJuju }, - { - category: "Authentication", - action: `User Login (${authMethod})`, - }, - ); + // XXX Now that we can register multiple controllers this needs + // to be sent per controller. + if (isProduction && analyticsEnabled) { + Sentry.setTag("jujuVersion", conn.info.serverVersion); } // Remove the getFacade function as this doesn't need to be stored in Redux. diff --git a/src/utils/analytics.test.ts b/src/utils/analytics.test.ts new file mode 100644 index 000000000..41dce8eb6 --- /dev/null +++ b/src/utils/analytics.test.ts @@ -0,0 +1,64 @@ +import ReactGA from "react-ga4"; +import type { MockInstance } from "vitest"; +import { vi } from "vitest"; + +import analytics from "./analytics"; + +vi.mock("react-ga4", () => ({ + default: { + initialize: vi.fn(), + send: vi.fn(), + event: vi.fn(), + set: vi.fn(), + }, +})); + +describe("analytics", () => { + let pageviewSpy: MockInstance; + let eventSpy: MockInstance; + + beforeEach(() => { + eventSpy = vi.spyOn(ReactGA, "event"); + pageviewSpy = vi.spyOn(ReactGA, "send"); + }); + + afterEach(() => { + localStorage.clear(); + }); + + it("does not send events if analytics are disabled", () => { + analytics( + false, + { dashboardVersion: "1.0.0", controllerVersion: "1.0.0", isJuju: "true" }, + { path: "/some/path" }, + ); + expect(eventSpy).not.toHaveBeenCalled(); + expect(pageviewSpy).not.toHaveBeenCalled(); + }); + + it("can send pageview events", () => { + analytics( + true, + { dashboardVersion: "1.0.0", controllerVersion: "1.0.0", isJuju: "true" }, + { path: "/some/path" }, + ); + expect(pageviewSpy).toHaveBeenCalledWith({ + hitType: "pageview", + page: "/some/path", + }); + }); + + it("can send events", () => { + analytics( + true, + { dashboardVersion: "1.0.0", controllerVersion: "1.0.0", isJuju: "true" }, + { category: "sidebar", action: "toggle" }, + ); + expect(eventSpy).toHaveBeenCalledWith("toggle", { + category: "sidebar", + controllerVersion: "1.0.0", + dashboardVersion: "1.0.0", + isJuju: "true", + }); + }); +}); diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index 2e375e967..18fc445b1 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -10,11 +10,10 @@ type AnalyticsMessage = { path?: string; category?: string; action?: string }; const analytics = ( analyticsEnabled: boolean, - isProduction: boolean, eventParams: EventParams, { path, category = "", action = "" }: AnalyticsMessage, ) => { - if (!isProduction || !analyticsEnabled) { + if (!analyticsEnabled) { return; } if (path) { From 35afec27eb4a423ac9f7e8ba4ef08b371499c687 Mon Sep 17 00:00:00 2001 From: Urvashi Sharma Date: Tue, 25 Feb 2025 11:55:12 +0530 Subject: [PATCH 7/7] test(analytics): Add unit tests --- .../CaptureRoutes/CaptureRoutes.test.tsx | 3 +++ src/hooks/useAnalytics.test.ts | 27 +++++++++++++++++++ src/hooks/useAnalytics.tsx | 10 ++----- src/store/middleware/model-poller.ts | 9 +++++-- src/utils/analytics.test.ts | 3 +++ src/utils/analytics.ts | 9 +++++-- 6 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/components/CaptureRoutes/CaptureRoutes.test.tsx b/src/components/CaptureRoutes/CaptureRoutes.test.tsx index db3ce2b94..edbe7f7f9 100644 --- a/src/components/CaptureRoutes/CaptureRoutes.test.tsx +++ b/src/components/CaptureRoutes/CaptureRoutes.test.tsx @@ -44,7 +44,10 @@ describe("CaptureRoutes", () => { , ); expect(pageviewSpy).toHaveBeenCalledWith({ + controllerVersion: "", + dashboardVersion: "", hitType: "pageview", + isJuju: "false", page: "/new/path", }); }); diff --git a/src/hooks/useAnalytics.test.ts b/src/hooks/useAnalytics.test.ts index 9a73acb3d..fbd607173 100644 --- a/src/hooks/useAnalytics.test.ts +++ b/src/hooks/useAnalytics.test.ts @@ -2,6 +2,7 @@ import { vi } from "vitest"; import { rootStateFactory } from "testing/factories"; import { configFactory, generalStateFactory } from "testing/factories/general"; +import { connectionInfoFactory } from "testing/factories/juju/jujulib"; import { renderWrappedHook } from "testing/utils"; import * as analyticsUtils from "utils/analytics"; @@ -69,4 +70,30 @@ describe("useAnalytics", () => { { category: "sidebar", action: "toggle" }, ); }); + + it("can send events with correct event params", () => { + const { result } = renderWrappedHook(() => useAnalytics(), { + state: rootStateFactory.build({ + general: generalStateFactory.build({ + appVersion: "1.0.0", + controllerConnections: { + "wss://example.com/api": connectionInfoFactory.build({ + serverVersion: "1.2.3", + }), + }, + config: configFactory.build({ + analyticsEnabled: true, + isJuju: true, + controllerAPIEndpoint: "wss://example.com/api", + }), + }), + }), + }); + result.current({ category: "sidebar", action: "toggle" }); + expect(analyticsUtils.default).toHaveBeenCalledWith( + true, + { controllerVersion: "1.2.3", dashboardVersion: "1.0.0", isJuju: "true" }, + { category: "sidebar", action: "toggle" }, + ); + }); }); diff --git a/src/hooks/useAnalytics.tsx b/src/hooks/useAnalytics.tsx index 8be73b51a..c845a4b5d 100644 --- a/src/hooks/useAnalytics.tsx +++ b/src/hooks/useAnalytics.tsx @@ -8,13 +8,7 @@ import { getWSControllerURL, } from "store/general/selectors"; import { useAppSelector } from "store/store"; -import analytics from "utils/analytics"; - -type AnalyticMessage = { - path?: string; - category?: string; - action?: string; -}; +import analytics, { type AnalyticsMessage } from "utils/analytics"; export default function useAnalytics() { const analyticsEnabled = useAppSelector(getAnalyticsEnabled); @@ -30,6 +24,6 @@ export default function useAnalytics() { isJuju: (!!isJuju).toString(), }; - return (props: AnalyticMessage) => + return (props: AnalyticsMessage) => analytics(!!analyticsEnabled, eventParams, props); } diff --git a/src/store/middleware/model-poller.ts b/src/store/middleware/model-poller.ts index 63dc0fc1d..39d9b6e67 100644 --- a/src/store/middleware/model-poller.ts +++ b/src/store/middleware/model-poller.ts @@ -16,7 +16,12 @@ import { whoami } from "juju/jimm/thunks"; import type { ConnectionWithFacades } from "juju/types"; import { actions as appActions, thunks as appThunks } from "store/app"; import { actions as generalActions } from "store/general"; -import { getAppVersion, getIsJuju, isLoggedIn } from "store/general/selectors"; +import { + getAnalyticsEnabled, + getAppVersion, + getIsJuju, + isLoggedIn, +} from "store/general/selectors"; import { AuthMethod } from "store/general/types"; import { actions as jujuActions } from "store/juju"; import type { RootState, Store } from "store/store"; @@ -134,7 +139,7 @@ export const modelPollerMiddleware: Middleware< } const isProduction = import.meta.env.PROD; - const analyticsEnabled = window.jujuDashboardConfig?.analyticsEnabled; + const analyticsEnabled = getAnalyticsEnabled(reduxStore.getState()); const isJuju = (!!getIsJuju(reduxStore.getState())).toString(); const dashboardVersion = getAppVersion(reduxStore.getState()) ?? ""; const controllerVersion = conn.info.serverVersion ?? ""; diff --git a/src/utils/analytics.test.ts b/src/utils/analytics.test.ts index 41dce8eb6..421b12ec8 100644 --- a/src/utils/analytics.test.ts +++ b/src/utils/analytics.test.ts @@ -43,7 +43,10 @@ describe("analytics", () => { { path: "/some/path" }, ); expect(pageviewSpy).toHaveBeenCalledWith({ + controllerVersion: "1.0.0", + dashboardVersion: "1.0.0", hitType: "pageview", + isJuju: "true", page: "/some/path", }); }); diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index 18fc445b1..a1d0a1b96 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -6,8 +6,13 @@ type EventParams = { isJuju: string; }; -type AnalyticsMessage = { path?: string; category?: string; action?: string }; +export type AnalyticsMessage = { + path?: string; + category?: string; + action?: string; +}; +// In components, the useAnalytics hook can be used to fetch additional event data const analytics = ( analyticsEnabled: boolean, eventParams: EventParams, @@ -17,7 +22,7 @@ const analytics = ( return; } if (path) { - ReactGA.send({ hitType: "pageview", page: path }); + ReactGA.send({ hitType: "pageview", page: path, ...eventParams }); } else { ReactGA.event(action, { category,