Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/ui-spa/src/TelemetryLogger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,17 @@ describe("TelemetryService", () => {
});

it("trackPageView sends a PageView payload with pageName and additional props", async () => {
await telemetryService.trackPageView("HomePage", [{ foo: "bar" } as any]);
await telemetryService.trackPageView("HomePage", [
{ foo: "bar" },
{ id: "123" },
]);

expect(logTelemetryEvent).toHaveBeenCalledTimes(1);
const sent = getSentPayload();

const expected = {
telemetryType: TelemetryType.PageView,
properties: [{ pageName: "HomePage" }, { foo: "bar" }],
properties: [{ pageName: "HomePage" }, { foo: "bar" }, { id: "123" }],
};

expect(sent).toEqual(expected);
Expand Down
3 changes: 2 additions & 1 deletion src/ui-spa/src/TelemetryLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ export type TelemetryPayload = {

export type TelemetryEventPropsMap = {
JourneyStarted: [{ journeyId: string }];
JourneyCancelled: [{ journeyId: string }, { cancelledFrom: string }];
};
export type CustomEventName = keyof TelemetryEventPropsMap;

export type TelemetryEventProps<T extends CustomEventName> =
TelemetryEventPropsMap[T];

export type TelemetryPageViewProps = [Record<string, unknown>];
export type TelemetryPageViewProps = Record<string, unknown>[];

export class TelemetryService {
async trackEvent<T extends CustomEventName>(
Expand Down
38 changes: 38 additions & 0 deletions src/ui-spa/src/common/constants/pageTitles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export const pageTitles: Record<string, string> = {
"/case-registration": "Home",
"/areas": "Case Areas",
"/case-details": "Case Details",
"/first-hearing": "First Hearing Details",
"/case-complexity": "Case Complexity",
"/case-monitoring-codes": "Case Monitoring Codes",
"/case-assignee": "Case Assignee",
"/case-summary": "Case Summary",
"/case-registration-confirmation": "Case Registration Confirmation",
"/add-suspect": "Add Suspect",
"/suspect-dob": "Suspect Date of Birth",
"/suspect-gender": "Suspect Gender",
"/suspect-ethnicity": "Suspect Ethnicity",
"/suspect-religion": "Suspect Religion",
"/suspect-disability": "Suspect Disability",
"/suspect-add-aliases": "Suspect Add Aliases",
"/suspect-aliases-summary": "Suspect Aliases Summary",
"/suspect-asn": "Suspect ASN",
"/suspect-offender": "Suspect Offender Type",
"/suspect-summary": "Suspect Summary",
"/suspect-remove-confirmation": "Suspect Remove Confirmation",
"/want-to-add-charges": "Want To Add Charges",
"/add-charge-suspect": "Add Charge Suspect",
"/charges-offence-search": "Charges Offence Search",
"/add-charge-details": "Add Charge Details",
"/charges-summary": "Charges Summary",
"/add-charge-victim": "Add Charge Victim",
"/charge-remove-confirmation": "Charge Remove Confirmation",
"/charges-victim-duplicate-confirmation":
"Charges Victim Duplicate Confirmation",
"/change-area-confirmation": "Change Area Confirmation",
"/change-registering-unit-confirmation":
"Change Registering Unit Confirmation",
"/remove-all-suspects-confirmation": "Remove All Suspects Confirmation",
"/cancel-case-registration-confirmation":
"Cancel Case Registration Confirmation",
};
109 changes: 109 additions & 0 deletions src/ui-spa/src/common/hooks/usePageView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from "react";
import { render } from "@testing-library/react";
import { type Mock } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

vi.mock("../../TelemetryLogger", () => ({
telemetryService: {
trackPageView: vi.fn(),
},
}));

vi.mock("react-router", () => ({
useLocation: vi.fn(),
}));

import { telemetryService } from "../../TelemetryLogger";
import { useLocation } from "react-router";
import { CaseRegistrationFormContext } from "../providers/CaseRegistrationProvider";
import usePageView from "./usePageView";

function TestComponent() {
usePageView();
return null;
}

describe("usePageView", () => {
const mockedUseLocation = useLocation as Mock;

beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.clearAllMocks();
});

it("calls telemetryService.trackPageView with the expected payload when pathname changes and journeyId is present", () => {
const pathname = "/case-registration";
mockedUseLocation.mockImplementation(() => ({
pathname,
}));

const providerValue = {
state: { telemetryData: { journeyId: "J1" } },
} as unknown as React.ContextType<typeof CaseRegistrationFormContext>;

render(
<CaseRegistrationFormContext.Provider value={providerValue}>
<TestComponent />
</CaseRegistrationFormContext.Provider>,
);

expect(telemetryService.trackPageView).toHaveBeenCalledTimes(1);

const call = (telemetryService.trackPageView as Mock).mock.calls[0];

const expectedPageName = "Home";
const expectedProperties = [{ journeyId: "J1" }, { path: pathname }];

expect(call).toEqual([expectedPageName, expectedProperties]);
});

it("does not call trackPageView when journeyId is missing", () => {
mockedUseLocation.mockImplementation(() => ({
pathname: "/case-registration/step-1",
}));

const providerValue = {
state: { telemetryData: { journeyId: "" } },
} as unknown as React.ContextType<typeof CaseRegistrationFormContext>;

render(
<CaseRegistrationFormContext.Provider value={providerValue}>
<TestComponent />
</CaseRegistrationFormContext.Provider>,
);

expect(telemetryService.trackPageView).not.toHaveBeenCalled();
});

it("does not call trackPageView again if pathname hasn't changed", () => {
mockedUseLocation.mockImplementation(() => ({
pathname: "/case-registration/",
}));

const providerValue = {
state: { telemetryData: { journeyId: "J1" } },
} as unknown as React.ContextType<typeof CaseRegistrationFormContext>;

const { rerender } = render(
<CaseRegistrationFormContext.Provider value={providerValue}>
<TestComponent />
</CaseRegistrationFormContext.Provider>,
);

expect(telemetryService.trackPageView).toHaveBeenCalledTimes(1);

mockedUseLocation.mockImplementation(() => ({
pathname: "/case-registration/",
}));
rerender(
<CaseRegistrationFormContext.Provider value={providerValue}>
<TestComponent />
</CaseRegistrationFormContext.Provider>,
);

expect(telemetryService.trackPageView).toHaveBeenCalledTimes(1);
});
});
36 changes: 36 additions & 0 deletions src/ui-spa/src/common/hooks/usePageView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect, useRef, useContext } from "react";
import { useLocation } from "react-router";
import { telemetryService } from "../../TelemetryLogger";
import { CaseRegistrationFormContext } from "../providers/CaseRegistrationProvider";
import { pageTitles } from "../constants/pageTitles";

const usePageView = () => {
const { state } = useContext(CaseRegistrationFormContext);
const location = useLocation();
const lastPath = useRef<string | null>(null);
const cleanPath = location.pathname.replace(/\/+$/, "") || "/";

Check warning on line 11 in src/ui-spa/src/common/hooks/usePageView.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its runtime, as it has super-linear performance due to backtracking.

See more on https://sonarcloud.io/project/issues?id=CPS-Innovation_Case-Management-Register-a-Case&issues=AaC0Y_ZjswDuyNWsCwpO&open=AaC0Y_ZjswDuyNWsCwpO&pullRequest=239
useEffect(() => {
if (lastPath.current === cleanPath) return;

if (!state.telemetryData.journeyId) {
return;
}

const lastSlashIndex = cleanPath.lastIndexOf("/");
const lastSegment = cleanPath.substring(lastSlashIndex);
const pageName = pageTitles[lastSegment] || "Unknown Page";

telemetryService.trackPageView(pageName, [
{
journeyId: state.telemetryData.journeyId,
},
{
path: cleanPath,
},
]);

lastPath.current = cleanPath;
}, [cleanPath, state.telemetryData.journeyId]);
};

export default usePageView;
42 changes: 2 additions & 40 deletions src/ui-spa/src/common/hooks/useRouteDocumentTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,6 @@
import { useEffect, useState } from "react";
import { useLocation } from "react-router";

const routeTitles: Record<string, string> = {
"/case-registration": "Home",
"/areas": "Case Areas",
"/case-details": "Case Details",
"/first-hearing": "First Hearing Details",
"/case-complexity": "Case Complexity",
"/case-monitoring-codes": "Case Monitoring Codes",
"/case-assignee": "Case Assignee",
"/case-summary": "Case Summary",
"/case-registration-confirmation": "Case Registration Confirmation",
"/add-suspect": "Add Suspect",
"/suspect-dob": "Suspect Date of Birth",
"/suspect-gender": "Suspect Gender",
"/suspect-ethnicity": "Suspect Ethnicity",
"/suspect-religion": "Suspect Religion",
"/suspect-disability": "Suspect Disability",
"/suspect-add-aliases": "Suspect Add Aliases",
"/suspect-aliases-summary": "Suspect Aliases Summary",
"/suspect-asn": "Suspect ASN",
"/suspect-offender": "Suspect Offender Type",
"/suspect-summary": "Suspect Summary",
"/suspect-remove-confirmation": "Suspect Remove Confirmation",
"/want-to-add-charges": "Want To Add Charges",
"/add-charge-suspect": "Add Charge Suspect",
"/charges-offence-search": "Charges Offence Search",
"/add-charge-details": "Add Charge Details",
"/charges-summary": "Charges Summary",
"/add-charge-victim": "Add Charge Victim",
"/charge-remove-confirmation": "Charge Remove Confirmation",
"/charges-victim-duplicate-confirmation":
"Charges Victim Duplicate Confirmation",
"/change-area-confirmation": "Change Area Confirmation",
"/change-registering-unit-confirmation":
"Change Registering Unit Confirmation",
"/remove-all-suspects-confirmation": "Remove All Suspects Confirmation",
"/cancel-case-registration-confirmation":
"Cancel Case Registration Confirmation",
};
import { pageTitles } from "../constants/pageTitles";

const DEFAULT_TITLE = "Home";

Expand All @@ -48,7 +10,7 @@ export const useRouteDocumentTitle = (): { title: string } => {
useEffect(() => {
const lastSlashIndex = pathname.lastIndexOf("/");
const lastSegment = pathname.substring(lastSlashIndex);
const title = `${routeTitles[lastSegment] ?? DEFAULT_TITLE} - Register A Case`;
const title = `${pageTitles[lastSegment] ?? DEFAULT_TITLE} - Register A Case`;
document.title = title;
setTitle(title);
}, [pathname]);
Expand Down
22 changes: 22 additions & 0 deletions src/ui-spa/src/common/reducers/caseRegistrationReducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ describe("caseRegistrationReducer", () => {
},
},
apiData: apiData,
telemetryData: {
journeyId: "",
},
};
const action: CaseRegistrationActions = { type: "RESET_FORM_DATA" };
const state = caseRegistrationReducer(modifiedState, action);
Expand Down Expand Up @@ -1587,6 +1590,25 @@ describe("caseRegistrationReducer", () => {
const state = caseRegistrationReducer(modifiedState, action);
expect(state).toEqual(modifiedState);
});

it("SET_TELEMETRY_JOURNEY_ID should set the telemetryData journeyId", () => {
const startState = {
...initialState,
formData: {
...initialState.formData,
},
};

const action = {
type: "SET_TELEMETRY_JOURNEY_ID" as const,
payload: { journeyId: "J1" },
};

const next = caseRegistrationReducer(startState, action);

expect(next.telemetryData.journeyId).toBe("J1");
expect(next.apiData).toEqual(startState.apiData);
});
});
describe("getResetFieldValues", () => {
it("should reset caseProsecutorText and caseCaseworkerText when caseProsecutorRadio is 'no'", () => {
Expand Down
23 changes: 23 additions & 0 deletions src/ui-spa/src/common/reducers/caseRegistrationReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ export type CaseRegistrationState = {
suspectOffenderTypes?: OffenderTypes | null;
offencesSearchResults?: Offences | null;
};
telemetryData: {
journeyId: string;
};
};

export const suspectInitialState: SuspectFormData = {
Expand Down Expand Up @@ -261,6 +264,10 @@ export const initialState: CaseRegistrationState = {
suspectReligions: null,
suspectOffenderTypes: null,
},

telemetryData: {
journeyId: "",
},
};

export type CaseRegistrationActions =
Expand Down Expand Up @@ -484,6 +491,12 @@ export type CaseRegistrationActions =
}
| {
type: "REMOVE_ALL_SUSPECTS";
}
| {
type: "SET_TELEMETRY_JOURNEY_ID";
payload: {
journeyId: string;
};
};

export type DispatchType = React.Dispatch<CaseRegistrationActions>;
Expand Down Expand Up @@ -879,6 +892,16 @@ export const caseRegistrationReducer = (
};
}

case "SET_TELEMETRY_JOURNEY_ID": {
return {
...state,
telemetryData: {
...state.telemetryData,
journeyId: action.payload.journeyId,
},
};
}

default:
return state;
}
Expand Down
Loading
Loading