-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(analytics): Implement usage analytics with react-ga4 (#1847)
* feat(analytics): Implement usage analytics with react-ga4 * feat(analytics): Fix syntax for analytics with react-ga4 * feat(testing): Replace mocking store with renderWrappedHook state * feat(analytics): Set custom dimensions with react-ga4 * feat(analytics): Send login event in poller * feat(analytics): Enable debug view in development * test(analytics): Add unit tests
- Loading branch information
1 parent
602b277
commit 10e6d25
Showing
11 changed files
with
256 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,74 +1,99 @@ | ||
import { renderHook } from "@testing-library/react"; | ||
import * as reactGA from "react-ga"; | ||
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 { connectionInfoFactory } from "testing/factories/juju/jujulib"; | ||
import { renderWrappedHook } from "testing/utils"; | ||
import * as analyticsUtils from "utils/analytics"; | ||
|
||
import useAnalytics from "./useAnalytics"; | ||
|
||
vi.mock("react-ga", () => ({ | ||
event: vi.fn(), | ||
pageview: vi.fn(), | ||
})); | ||
|
||
describe("useAnalytics", () => { | ||
let pageviewSpy: MockInstance; | ||
let eventSpy: MockInstance; | ||
|
||
beforeEach(() => { | ||
vi.stubEnv("PROD", true); | ||
eventSpy = vi.spyOn(reactGA, "event"); | ||
pageviewSpy = vi.spyOn(reactGA, "pageview"); | ||
vi.spyOn(analyticsUtils, "default").mockImplementation(() => vi.fn()); | ||
}); | ||
|
||
afterEach(() => { | ||
localStorage.clear(); | ||
}); | ||
|
||
afterAll(() => { | ||
vi.unstubAllEnvs(); | ||
}); | ||
|
||
it("does not send events in development", () => { | ||
vi.spyOn(store, "useAppSelector").mockImplementation( | ||
vi.fn().mockReturnValue(true), | ||
); | ||
vi.stubEnv("PROD", false); | ||
const { result } = renderHook(() => useAnalytics()); | ||
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 } = renderHook(() => 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(); | ||
expect(analyticsUtils.default).toHaveBeenCalledWith( | ||
false, | ||
{ controllerVersion: "", dashboardVersion: "", isJuju: "false" }, | ||
{ path: "/some/path" }, | ||
); | ||
}); | ||
|
||
it("can send pageview events", () => { | ||
vi.spyOn(store, "useAppSelector").mockImplementation( | ||
vi.fn().mockReturnValue(true), | ||
); | ||
const { result } = renderHook(() => useAnalytics()); | ||
const { result } = renderWrappedHook(() => useAnalytics(), { | ||
state: rootStateFactory.build({ | ||
general: generalStateFactory.build({ | ||
config: configFactory.build({ | ||
analyticsEnabled: true, | ||
}), | ||
}), | ||
}), | ||
}); | ||
result.current({ path: "/some/path" }); | ||
expect(pageviewSpy).toHaveBeenCalledWith("/some/path"); | ||
expect(analyticsUtils.default).toHaveBeenCalledWith( | ||
true, | ||
{ controllerVersion: "", dashboardVersion: "", isJuju: "false" }, | ||
{ path: "/some/path" }, | ||
); | ||
}); | ||
|
||
it("can send events", () => { | ||
vi.spyOn(store, "useAppSelector").mockImplementation( | ||
vi.fn().mockReturnValue(true), | ||
); | ||
const { result } = renderHook(() => 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", | ||
action: "toggle", | ||
expect(analyticsUtils.default).toHaveBeenCalledWith( | ||
true, | ||
{ controllerVersion: "", dashboardVersion: "", isJuju: "false" }, | ||
{ 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" }, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,29 @@ | ||
import { pageview, event } from "react-ga"; | ||
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; | ||
}; | ||
import analytics, { type AnalyticsMessage } from "utils/analytics"; | ||
|
||
export default function useAnalytics() { | ||
const analyticsEnabled = useAppSelector(getAnalyticsEnabled); | ||
return ({ path, category = "", action = "" }: AnalyticMessage) => { | ||
const isProduction = import.meta.env.PROD; | ||
if (!isProduction || !analyticsEnabled) { | ||
return; | ||
} | ||
if (path) { | ||
pageview(path); | ||
} else { | ||
event({ | ||
category, | ||
action, | ||
}); | ||
} | ||
const isJuju = useSelector(getIsJuju); | ||
const appVersion = useSelector(getAppVersion); | ||
const wsControllerURL = useAppSelector(getWSControllerURL); | ||
const controllerVersion = useAppSelector((state) => | ||
getControllerConnection(state, wsControllerURL), | ||
)?.serverVersion; | ||
const eventParams = { | ||
dashboardVersion: appVersion ?? "", | ||
controllerVersion: controllerVersion ?? "", | ||
isJuju: (!!isJuju).toString(), | ||
}; | ||
|
||
return (props: AnalyticsMessage) => | ||
analytics(!!analyticsEnabled, eventParams, props); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.