diff --git a/apps/parsley/.gitignore b/apps/parsley/.gitignore index d261bd3dd..8c11be549 100644 --- a/apps/parsley/.gitignore +++ b/apps/parsley/.gitignore @@ -6,6 +6,7 @@ cypress/screenshots/ /src/**/*.css build.txt build.txt-e +vite.config.ts.timestamp-* #env variables .env-cmdrc.json diff --git a/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts b/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts index 6f4cc124d..60dc51c43 100644 --- a/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts +++ b/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts @@ -1,5 +1,6 @@ import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; import { AnalyticsIdentifier } from "analytics/types"; +import { SectionStatus } from "constants/logs"; import { DIRECTION } from "context/LogContext/types"; import { Filter } from "types/logs"; @@ -32,10 +33,30 @@ type Action = | { name: "Toggled expanded lines"; open: false } | { name: "Used search result pagination"; direction: DIRECTION } | { - name: "Toggled section"; + name: "Toggled section caret"; "section.name": string; "section.type": "command" | "function"; - open: boolean; + "section.open": boolean; + "section.status"?: SectionStatus; + "section.nested": boolean; + } + | { name: "Clicked open all sections button" } + | { name: "Clicked close all sections button" } + | { + name: "Clicked open subsections button"; + "function.name": string; + "function.status": SectionStatus; + "was.function.closed": boolean; + } + | { + name: "Clicked close subsections button"; + "function.name": string; + "function.status": SectionStatus; + } + | { + name: "Viewed log with sections and jump to failing line"; + "settings.sections.enabled": boolean; + "settings.jump_to_failing_line.enabled": boolean; }; export const useLogWindowAnalytics = () => diff --git a/apps/parsley/src/components/LogPane/index.tsx b/apps/parsley/src/components/LogPane/index.tsx index 8da1694f5..f5a8dce72 100644 --- a/apps/parsley/src/components/LogPane/index.tsx +++ b/apps/parsley/src/components/LogPane/index.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef } from "react"; import { css } from "@leafygreen-ui/emotion"; import Cookies from "js-cookie"; +import { useLogWindowAnalytics } from "analytics"; import PaginatedVirtualList from "components/PaginatedVirtualList"; import { PRETTY_PRINT_BOOKMARKS, WRAP } from "constants/cookies"; import { QueryParams } from "constants/queryParams"; @@ -17,6 +18,7 @@ interface LogPaneProps { const LogPane: React.FC = ({ rowCount, rowRenderer }) => { const { failingLine, listRef, preferences, processedLogLines, scrollToLine } = useLogContext(); + const { sendEvent } = useLogWindowAnalytics(); const { setPrettyPrint, setWrap, zebraStriping } = preferences; const { settings } = useParsleySettings(); const [shareLine] = useQueryParam( @@ -56,6 +58,12 @@ const LogPane: React.FC = ({ rowCount, rowRenderer }) => { setPrettyPrint(true); } performedScroll.current = true; + sendEvent({ + name: "Viewed log with sections and jump to failing line", + "settings.jump_to_failing_line.enabled": + settings.jumpToFailingLineEnabled, + "settings.sections.enabled": settings.sectionsEnabled, + }); }, 100); return () => clearTimeout(timeoutId); } diff --git a/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx b/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx index cda9791b4..ff0dde0ff 100644 --- a/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx +++ b/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx @@ -1,3 +1,4 @@ +import * as analytics from "analytics"; import { SectionStatus } from "constants/logs"; import * as logContext from "context/LogContext"; import { logContextWrapper } from "context/LogContext/test_utils"; @@ -63,8 +64,13 @@ describe("SectionHeader", () => { ); }); - it("should call onOpen function when 'open' button is clicked", async () => { + it("should call onOpen function when 'open' button is clicked and send analytics events", async () => { const user = userEvent.setup(); + const mockedAnalytics = vi.spyOn(analytics, "useLogWindowAnalytics"); + const sendEventMock = vi.fn(); + mockedAnalytics.mockImplementation(() => ({ + sendEvent: sendEventMock, + })); const mockedLogContext = vi.spyOn(logContext, "useLogContext"); const toggleFunctionSectionMock = vi.fn(); mockedLogContext.mockImplementation(() => ({ @@ -78,6 +84,15 @@ describe("SectionHeader", () => { }); const openButton = screen.getByDataCy("caret-toggle"); await user.click(openButton); + expect(sendEventMock).toHaveBeenCalledTimes(1); + expect(sendEventMock).toHaveBeenCalledWith({ + name: "Toggled section caret", + "section.name": "load_data", + "section.nested": false, + "section.open": true, + "section.status": "pass", + "section.type": "function", + }); expect(toggleFunctionSectionMock).toHaveBeenCalledTimes(1); expect(toggleFunctionSectionMock).toHaveBeenCalledWith({ functionID: "function-4", diff --git a/apps/parsley/src/components/LogRow/SectionHeader/SubsectionControls.test.tsx b/apps/parsley/src/components/LogRow/SectionHeader/SubsectionControls.test.tsx new file mode 100644 index 000000000..a8cdf217c --- /dev/null +++ b/apps/parsley/src/components/LogRow/SectionHeader/SubsectionControls.test.tsx @@ -0,0 +1,116 @@ +import * as analytics from "analytics"; +import { SectionStatus } from "constants/logs"; +import * as logContext from "context/LogContext"; +import { logContextWrapper } from "context/LogContext/test_utils"; +import { RenderFakeToastContext as InitializeFakeToastContext } from "context/toast/__mocks__"; +import { + sectionStateAllClosed, + sectionStateAllOpen, + sectionStateSomeOpen, +} from "hooks/useSections/testData"; +import { renderWithRouterMatch, screen, userEvent, waitFor } from "test_utils"; +import { SubsectionControls } from "./SubsectionControls"; + +const wrapper = logContextWrapper(); + +describe("SectionControls", () => { + beforeEach(() => { + InitializeFakeToastContext(); + }); + + it("Should not render 'Open subsections' when all subsections belonging to the function are open", () => { + const mockedLogContext = vi.spyOn(logContext, "useLogContext"); + mockedLogContext.mockImplementation(() => ({ + // @ts-expect-error - Only mocking a subset of useLogContext needed for this test + sectioning: { + sectionState: sectionStateAllOpen, + sectioningEnabled: true, + toggleAllSections: vi.fn(), + }, + })); + renderWithRouterMatch(, { wrapper }); + expect(screen.queryByText("Open subsections")).toBeNull(); + expect(screen.queryByText("Close subsections")).toBeVisible(); + }); + it("Should not render 'Close subsections' when all subsections belonging to the function are closed", () => { + const mockedLogContext = vi.spyOn(logContext, "useLogContext"); + mockedLogContext.mockImplementation(() => ({ + // @ts-expect-error - Only mocking a subset of useLogContext needed for this test. + sectioning: { + sectionState: sectionStateAllClosed, + sectioningEnabled: true, + toggleAllCommandsInFunction: vi.fn(), + }, + })); + renderWithRouterMatch(, { wrapper }); + expect(screen.queryByText("Open subsections")).toBeVisible(); + expect(screen.queryByText("Close subsections")).toBeNull(); + }); + it("Should render 'Open subsections' and 'Close subsections' buttons when some sections are open and some are closed", () => { + const mockedLogContext = vi.spyOn(logContext, "useLogContext"); + mockedLogContext.mockImplementation(() => ({ + // @ts-expect-error - Only mocking a subset of useLogContext needed for this test. + sectioning: { + sectionState: sectionStateSomeOpen, + sectioningEnabled: true, + toggleAllCommandsInFunction: vi.fn(), + }, + })); + renderWithRouterMatch(, { wrapper }); + expect(screen.queryByText("Open subsections")).toBeVisible(); + expect(screen.queryByText("Close subsections")).toBeVisible(); + }); + + it("Clicking on buttons calls 'toggleAllCommandsInFunction' with correct parameters and sends analytics events", async () => { + const user = userEvent.setup(); + const mockedLogContext = vi.spyOn(logContext, "useLogContext"); + const mockedAnalytics = vi.spyOn(analytics, "useLogWindowAnalytics"); + const sendEventMock = vi.fn(); + mockedAnalytics.mockImplementation(() => ({ + sendEvent: sendEventMock, + })); + const toggleAllCommandsInFunctionMock = vi.fn(); + mockedLogContext.mockImplementation(() => ({ + // @ts-expect-error - Only mocking a subset of useLogContext needed for this test. + sectioning: { + sectionState: sectionStateSomeOpen, + sectioningEnabled: true, + toggleAllCommandsInFunction: toggleAllCommandsInFunctionMock, + }, + })); + renderWithRouterMatch(, { wrapper }); + expect(screen.getByDataCy("open-subsections-btn")).toBeVisible(); + await user.click(screen.getByDataCy("open-subsections-btn")); + expect(sendEventMock).toHaveBeenCalledOnce(); + expect(sendEventMock).toHaveBeenCalledWith({ + "function.name": "funcName", + "function.status": "pass", + name: "Clicked open subsections button", + "was.function.closed": false, + }); + expect(toggleAllCommandsInFunctionMock).toHaveBeenCalledOnce(); + expect(toggleAllCommandsInFunctionMock).toHaveBeenCalledWith( + "function-1", + true, + ); + await user.click(screen.getByDataCy("close-subsections-btn")); + await waitFor(() => + expect(toggleAllCommandsInFunctionMock).toHaveBeenCalledWith( + "function-1", + false, + ), + ); + expect(sendEventMock).toHaveBeenCalledTimes(2); + expect(sendEventMock).toHaveBeenCalledWith({ + "function.name": "funcName", + "function.status": "pass", + name: "Clicked close subsections button", + }); + }); +}); + +const props = { + functionID: "function-1", + functionName: "funcName", + status: SectionStatus.Pass, +}; diff --git a/apps/parsley/src/components/LogRow/SectionHeader/SubsectionControls.tsx b/apps/parsley/src/components/LogRow/SectionHeader/SubsectionControls.tsx index dbcecfc7d..3218f89ce 100644 --- a/apps/parsley/src/components/LogRow/SectionHeader/SubsectionControls.tsx +++ b/apps/parsley/src/components/LogRow/SectionHeader/SubsectionControls.tsx @@ -1,9 +1,14 @@ import Button from "@leafygreen-ui/button"; +import { useLogWindowAnalytics } from "analytics"; +import { SectionStatus } from "constants/logs"; import { useLogContext } from "context/LogContext"; -const SubsectionControls: React.FC<{ functionID: string }> = ({ - functionID, -}) => { +const SubsectionControls: React.FC<{ + functionID: string; + functionName: string; + status: SectionStatus; +}> = ({ functionID, functionName, status }) => { + const { sendEvent } = useLogWindowAnalytics(); const { sectioning } = useLogContext(); const { sectionState, toggleAllCommandsInFunction } = sectioning; const showExpandButton = @@ -23,7 +28,15 @@ const SubsectionControls: React.FC<{ functionID: string }> = ({ {showExpandButton && (