Skip to content

Commit

Permalink
DEVPROD-5303: Track sectioning analytics (#328)
Browse files Browse the repository at this point in the history
  • Loading branch information
SupaJoon authored Aug 27, 2024
1 parent 9eef99c commit b00200d
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 19 deletions.
1 change: 1 addition & 0 deletions apps/parsley/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ cypress/screenshots/
/src/**/*.css
build.txt
build.txt-e
vite.config.ts.timestamp-*

#env variables
.env-cmdrc.json
Expand Down
25 changes: 23 additions & 2 deletions apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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 = () =>
Expand Down
8 changes: 8 additions & 0 deletions apps/parsley/src/components/LogPane/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -17,6 +18,7 @@ interface LogPaneProps {
const LogPane: React.FC<LogPaneProps> = ({ rowCount, rowRenderer }) => {
const { failingLine, listRef, preferences, processedLogLines, scrollToLine } =
useLogContext();
const { sendEvent } = useLogWindowAnalytics();
const { setPrettyPrint, setWrap, zebraStriping } = preferences;
const { settings } = useParsleySettings();
const [shareLine] = useQueryParam<number | undefined>(
Expand Down Expand Up @@ -56,6 +58,12 @@ const LogPane: React.FC<LogPaneProps> = ({ 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(() => ({
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<SubsectionControls {...props} />, { 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(<SubsectionControls {...props} />, { 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(<SubsectionControls {...props} />, { 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(<SubsectionControls {...props} />, { 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,
};
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -23,7 +28,15 @@ const SubsectionControls: React.FC<{ functionID: string }> = ({
{showExpandButton && (
<Button
data-cy="open-subsections-btn"
onClick={() => toggleAllCommandsInFunction(functionID, true)}
onClick={() => {
sendEvent({
"function.name": functionName,
"function.status": status,
name: "Clicked open subsections button",
"was.function.closed": !sectionState[functionID].isOpen,
});
toggleAllCommandsInFunction(functionID, true);
}}
size="xsmall"
>
Open subsections
Expand All @@ -32,7 +45,14 @@ const SubsectionControls: React.FC<{ functionID: string }> = ({
{showCloseButton && (
<Button
data-cy="close-subsections-btn"
onClick={() => toggleAllCommandsInFunction(functionID, false)}
onClick={() => {
toggleAllCommandsInFunction(functionID, false);
sendEvent({
"function.name": functionName,
"function.status": status,
name: "Clicked close subsections button",
});
}}
size="xsmall"
>
Close subsections
Expand Down
12 changes: 9 additions & 3 deletions apps/parsley/src/components/LogRow/SectionHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ const SectionHeader: React.FC<SectionHeaderProps> = ({
<CaretToggle
onClick={() => {
sendEvent({
name: "Toggled section",
open: !open,
name: "Toggled section caret",
"section.name": functionName,
"section.nested": false,
"section.open": !open,
"section.status": status,
"section.type": "function",
});
sectioning.toggleFunctionSection({ functionID, isOpen: !open });
Expand All @@ -52,7 +54,11 @@ const SectionHeader: React.FC<SectionHeaderProps> = ({
<Icon fill={gray.dark1} glyph={statusGlyph} />
<Body>Function: {functionName}</Body>
<ButtonWrapper>
<SubsectionControls functionID={functionID} />
<SubsectionControls
functionID={functionID}
functionName={functionName}
status={status}
/>
</ButtonWrapper>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -49,8 +50,13 @@ describe("SubsectionHeader", () => {
);
});

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(() => ({
Expand All @@ -70,6 +76,15 @@ describe("SubsectionHeader", () => {
functionID: "function-1",
isOpen: true,
});
expect(sendEventMock).toHaveBeenCalledTimes(1);
expect(sendEventMock).toHaveBeenCalledWith({
name: "Toggled section caret",
"section.name": "shell.exec",
"section.nested": true,
"section.open": true,
"section.status": undefined,
"section.type": "command",
});
});

it("open and close state is controlled by the 'open' prop", async () => {
Expand Down
6 changes: 4 additions & 2 deletions apps/parsley/src/components/LogRow/SubsectionHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ const SubsectionHeader: React.FC<SubsectionHeaderProps> = ({
<CaretToggle
onClick={() => {
sendEvent({
name: "Toggled section",
open: !open,
name: "Toggled section caret",
"section.name": commandName,
"section.nested": !isTopLevelCommand,
"section.open": !open,
"section.status": status,
"section.type": "command",
});
sectioning.toggleCommandSection({
Expand Down
16 changes: 15 additions & 1 deletion apps/parsley/src/components/SubHeader/SectionControls.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as analytics from "analytics";
import * as logContext from "context/LogContext";
import { logContextWrapper } from "context/LogContext/test_utils";
import { RenderFakeToastContext as InitializeFakeToastContext } from "context/toast/__mocks__";
Expand Down Expand Up @@ -73,9 +74,14 @@ describe("SectionControls", () => {
expect(screen.queryByText("Close all sections")).toBeVisible();
});

it("Clicking on buttons calls 'toggleAllSections' with correct parameters", async () => {
it("Clicking on buttons calls 'toggleAllSections' 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 toggleAllSectionsMock = vi.fn();
mockedLogContext.mockImplementation(() => ({
// @ts-expect-error - Only mocking a subset of useLogContext needed for this test.
Expand All @@ -88,10 +94,18 @@ describe("SectionControls", () => {
renderWithRouterMatch(<SectionControls />, { wrapper });
expect(screen.getByDataCy("open-all-sections-btn")).toBeVisible();
await user.click(screen.getByDataCy("open-all-sections-btn"));
expect(sendEventMock).toHaveBeenCalledOnce();
expect(sendEventMock).toHaveBeenCalledWith({
name: "Clicked open all sections button",
});
expect(toggleAllSectionsMock).toHaveBeenCalledOnce();
await user.click(screen.getByDataCy("close-all-sections-btn"));
await waitFor(() =>
expect(toggleAllSectionsMock).toHaveBeenCalledWith(false),
);
expect(sendEventMock).toHaveBeenCalledTimes(2);
expect(sendEventMock).toHaveBeenCalledWith({
name: "Clicked close all sections button",
});
});
});
Loading

0 comments on commit b00200d

Please sign in to comment.