diff --git a/extensions/ql-vscode/src/view/common/CodePaths/CodePaths.tsx b/extensions/ql-vscode/src/view/common/CodePaths/CodePaths.tsx index 6b61f50ee..3408339a2 100644 --- a/extensions/ql-vscode/src/view/common/CodePaths/CodePaths.tsx +++ b/extensions/ql-vscode/src/view/common/CodePaths/CodePaths.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { useRef, useState } from "react"; +import { useRef } from "react"; import styled from "styled-components"; import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; @@ -11,6 +11,7 @@ import { ResultSeverity, } from "../../../remote-queries/shared/analysis-result"; import { CodePathsOverlay } from "./CodePathsOverlay"; +import { useStateWithTelemetry } from "../Telemetry"; const ShowPathsLink = styled(VSCodeLink)` cursor: pointer; @@ -29,7 +30,11 @@ export const CodePaths = ({ message, severity, }: CodePathsProps) => { - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useStateWithTelemetry( + false, + "code-path-is-open", + (v) => v === true, + ); const linkRef = useRef(null); diff --git a/extensions/ql-vscode/src/view/common/CodePaths/CodePathsOverlay.tsx b/extensions/ql-vscode/src/view/common/CodePaths/CodePathsOverlay.tsx index a291574fd..9a7cc402c 100644 --- a/extensions/ql-vscode/src/view/common/CodePaths/CodePathsOverlay.tsx +++ b/extensions/ql-vscode/src/view/common/CodePaths/CodePathsOverlay.tsx @@ -1,5 +1,4 @@ import * as React from "react"; -import { useState } from "react"; import styled from "styled-components"; import { @@ -7,6 +6,7 @@ import { CodeFlow, ResultSeverity, } from "../../../remote-queries/shared/analysis-result"; +import { useStateWithTelemetry } from "../Telemetry"; import { SectionTitle } from "../SectionTitle"; import { VerticalSpace } from "../VerticalSpace"; import { CodeFlowsDropdown } from "./CodeFlowsDropdown"; @@ -76,7 +76,10 @@ export const CodePathsOverlay = ({ severity, onClose, }: CodePathsOverlayProps) => { - const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]); + const [selectedCodeFlow, setSelectedCodeFlow] = useStateWithTelemetry( + codeFlows[0], + "code-flow-selected", + ); return ( diff --git a/extensions/ql-vscode/src/view/common/Telemetry.ts b/extensions/ql-vscode/src/view/common/Telemetry.ts new file mode 100644 index 000000000..68da92cfe --- /dev/null +++ b/extensions/ql-vscode/src/view/common/Telemetry.ts @@ -0,0 +1,52 @@ +import * as React from "react"; +import { useState } from "react"; +import { vscode } from "../vscode-api"; + +/** + * Wraps `React.useState` to output telemetry events whenever the value changes. + * + * The only catch is that when using a predicate to filter which values output telemetry, + * the setter only accepts a raw value, instead of a `(prevState: S) => S` function. + * + * @param defaultValue Default value to pass to React.useState + * @param telemetryAction Name of the telemetry event to output + * @param filterTelemetryOnValue If provided, only output telemetry events when the predicate returns true. If not provided always outputs telemetry. + * @returns A value and a setter function, just as if from `React.useState` + */ +export function useStateWithTelemetry( + defaultValue: S | (() => S), + telemetryAction: string, +): [S, React.Dispatch>]; +export function useStateWithTelemetry( + defaultValue: S | (() => S), + telemetryAction: string, + filterTelemetryOnValue: (value: S) => boolean, +): [S, React.Dispatch]; +export function useStateWithTelemetry( + defaultValue: S | (() => S), + telemetryAction: string, + filterTelemetryOnValue?: (value: S) => boolean, +): [S, React.Dispatch | React.Dispatch>] { + const [value, setter] = useState(defaultValue); + if (filterTelemetryOnValue === undefined) { + const setterWithTelemetry = (x: React.SetStateAction) => { + vscode.postMessage({ + t: "telemetry", + action: telemetryAction, + }); + setter(x); + }; + return [value, setterWithTelemetry]; + } else { + const setterWithTelemetry = (x: S) => { + if (filterTelemetryOnValue(x)) { + vscode.postMessage({ + t: "telemetry", + action: telemetryAction, + }); + } + setter(x); + }; + return [value, setterWithTelemetry]; + } +} diff --git a/extensions/ql-vscode/src/view/remote-queries/RawResultsTable.tsx b/extensions/ql-vscode/src/view/remote-queries/RawResultsTable.tsx index 2d88a5c23..d09be9489 100644 --- a/extensions/ql-vscode/src/view/remote-queries/RawResultsTable.tsx +++ b/extensions/ql-vscode/src/view/remote-queries/RawResultsTable.tsx @@ -1,5 +1,4 @@ import * as React from "react"; -import { useState } from "react"; import styled from "styled-components"; import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; import { @@ -10,6 +9,7 @@ import { import { tryGetRemoteLocation } from "../../pure/bqrs-utils"; import TextButton from "./TextButton"; import { convertNonPrintableChars } from "../../text-utils"; +import { useStateWithTelemetry } from "../common/Telemetry"; const numOfResultsInContractedMode = 5; @@ -100,7 +100,11 @@ const RawResultsTable = ({ fileLinkPrefix, sourceLocationPrefix, }: RawResultsTableProps) => { - const [tableExpanded, setTableExpanded] = useState(false); + const [tableExpanded, setTableExpanded] = useStateWithTelemetry( + false, + "raw-results-table-expanded", + (v) => v === true, + ); const numOfResultsToShow = tableExpanded ? results.rows.length : numOfResultsInContractedMode; diff --git a/extensions/ql-vscode/src/view/remote-queries/RemoteQueries.tsx b/extensions/ql-vscode/src/view/remote-queries/RemoteQueries.tsx index c1a0dc4a3..69c8292a6 100644 --- a/extensions/ql-vscode/src/view/remote-queries/RemoteQueries.tsx +++ b/extensions/ql-vscode/src/view/remote-queries/RemoteQueries.tsx @@ -433,7 +433,7 @@ const AnalysesResults = ({ sort: Sort; }) => { const totalAnalysesResults = sumAnalysesResults(analysesResults); - const [filterValue, setFilterValue] = React.useState(""); + const [filterValue, setFilterValue] = useState(""); if (totalResults === 0) { return <>; diff --git a/extensions/ql-vscode/src/view/variant-analysis/RepoRow.tsx b/extensions/ql-vscode/src/view/variant-analysis/RepoRow.tsx index 91bb97fc4..d07eff098 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/RepoRow.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/RepoRow.tsx @@ -24,6 +24,7 @@ import { vscode } from "../vscode-api"; import { AnalyzedRepoItemContent } from "./AnalyzedRepoItemContent"; import StarCount from "../common/StarCount"; import { LastUpdated } from "../common/LastUpdated"; +import { useStateWithTelemetry } from "../common/Telemetry"; // This will ensure that these icons have a className which we can use in the TitleContainer const ExpandCollapseCodicon = styled(Codicon)``; @@ -167,7 +168,11 @@ export const RepoRow = ({ selected, onSelectedChange, }: RepoRowProps) => { - const [isExpanded, setExpanded] = useState(false); + const [isExpanded, setExpanded] = useStateWithTelemetry( + false, + "variant-analysis-repo-row-expanded", + (v) => v === true, + ); const resultsLoaded = !!interpretedResults || !!rawResults; const [resultsLoading, setResultsLoading] = useState(false); @@ -182,7 +187,7 @@ export const RepoRow = ({ downloadStatus !== VariantAnalysisScannedRepositoryDownloadStatus.Succeeded ) { - setExpanded((oldIsExpanded) => !oldIsExpanded); + setExpanded(!isExpanded); return; } @@ -198,6 +203,8 @@ export const RepoRow = ({ repository.fullName, status, downloadStatus, + isExpanded, + setExpanded, ]); useEffect(() => { diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx index 023f2d80a..4500f4a3b 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx @@ -16,6 +16,7 @@ import { defaultFilterSortState, RepositoriesFilterSortState, } from "../../pure/variant-analysis-filter-sort"; +import { useStateWithTelemetry } from "../common/Telemetry"; export type VariantAnalysisProps = { variantAnalysis?: VariantAnalysisDomainModel; @@ -60,11 +61,16 @@ export function VariantAnalysis({ const [repoResults, setRepoResults] = useState(initialRepoResults); - const [selectedRepositoryIds, setSelectedRepositoryIds] = useState( - [], - ); + const [selectedRepositoryIds, setSelectedRepositoryIds] = + useStateWithTelemetry( + [], + "variant-analysis-selected-repository-ids", + ); const [filterSortState, setFilterSortState] = - useState(defaultFilterSortState); + useStateWithTelemetry( + defaultFilterSortState, + "variant-analysis-filter-sort-state", + ); useEffect(() => { const listener = (evt: MessageEvent) => {