Merge pull request #1953 from github/robertbrignull/telemetry_ui

Report telemetry on actions taken in the UI
This commit is contained in:
Robert
2023-01-17 09:36:06 +00:00
committed by GitHub
13 changed files with 160 additions and 16 deletions

View File

@@ -413,7 +413,8 @@ export type FromRemoteQueriesMessage =
| RemoteQueryDownloadAnalysisResultsMessage | RemoteQueryDownloadAnalysisResultsMessage
| RemoteQueryDownloadAllAnalysesResultsMessage | RemoteQueryDownloadAllAnalysesResultsMessage
| RemoteQueryExportResultsMessage | RemoteQueryExportResultsMessage
| CopyRepoListMessage; | CopyRepoListMessage
| TelemetryMessage;
export type ToRemoteQueriesMessage = export type ToRemoteQueriesMessage =
| SetRemoteQueryResultMessage | SetRemoteQueryResultMessage
@@ -504,6 +505,11 @@ export interface CancelVariantAnalysisMessage {
t: "cancelVariantAnalysis"; t: "cancelVariantAnalysis";
} }
export interface TelemetryMessage {
t: "telemetry";
action: string;
}
export type ToVariantAnalysisMessage = export type ToVariantAnalysisMessage =
| SetVariantAnalysisMessage | SetVariantAnalysisMessage
| SetRepoResultsMessage | SetRepoResultsMessage
@@ -517,4 +523,5 @@ export type FromVariantAnalysisMessage =
| CopyRepositoryListMessage | CopyRepositoryListMessage
| ExportResultsMessage | ExportResultsMessage
| OpenLogsMessage | OpenLogsMessage
| CancelVariantAnalysisMessage; | CancelVariantAnalysisMessage
| TelemetryMessage;

View File

@@ -33,6 +33,7 @@ import { AnalysesResultsManager } from "./analyses-results-manager";
import { AnalysisResults } from "./shared/analysis-result"; import { AnalysisResults } from "./shared/analysis-result";
import { humanizeUnit } from "../pure/time"; import { humanizeUnit } from "../pure/time";
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview"; import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
import { telemetryListener } from "../telemetry";
export class RemoteQueriesView extends AbstractWebview< export class RemoteQueriesView extends AbstractWebview<
ToRemoteQueriesMessage, ToRemoteQueriesMessage,
@@ -167,6 +168,9 @@ export class RemoteQueriesView extends AbstractWebview<
msg.queryId, msg.queryId,
); );
break; break;
case "telemetry":
telemetryListener?.sendUIInteraction(msg.action);
break;
default: default:
assertNever(msg); assertNever(msg);
} }

View File

@@ -16,6 +16,7 @@ import {
VariantAnalysisViewManager, VariantAnalysisViewManager,
} from "./variant-analysis-view-manager"; } from "./variant-analysis-view-manager";
import { showAndLogWarningMessage } from "../helpers"; import { showAndLogWarningMessage } from "../helpers";
import { telemetryListener } from "../telemetry";
export class VariantAnalysisView export class VariantAnalysisView
extends AbstractWebview<ToVariantAnalysisMessage, FromVariantAnalysisMessage> extends AbstractWebview<ToVariantAnalysisMessage, FromVariantAnalysisMessage>
@@ -151,6 +152,9 @@ export class VariantAnalysisView
this.variantAnalysisId, this.variantAnalysisId,
); );
break; break;
case "telemetry":
telemetryListener?.sendUIInteraction(msg.action);
break;
default: default:
assertNever(msg); assertNever(msg);
} }

View File

@@ -12,6 +12,7 @@ import {
GLOBAL_ENABLE_TELEMETRY, GLOBAL_ENABLE_TELEMETRY,
LOG_TELEMETRY, LOG_TELEMETRY,
isIntegrationTestMode, isIntegrationTestMode,
isCanary,
} from "./config"; } from "./config";
import * as appInsights from "applicationinsights"; import * as appInsights from "applicationinsights";
import { extLogger } from "./common"; import { extLogger } from "./common";
@@ -155,19 +156,32 @@ export class TelemetryListener extends ConfigListener {
? CommandCompletion.Cancelled ? CommandCompletion.Cancelled
: CommandCompletion.Failed; : CommandCompletion.Failed;
const isCanary = (!!CANARY_FEATURES.getValue<boolean>()).toString();
this.reporter.sendTelemetryEvent( this.reporter.sendTelemetryEvent(
"command-usage", "command-usage",
{ {
name, name,
status, status,
isCanary, isCanary: isCanary().toString(),
}, },
{ executionTime }, { executionTime },
); );
} }
sendUIInteraction(name: string) {
if (!this.reporter) {
return;
}
this.reporter.sendTelemetryEvent(
"ui-interaction",
{
name,
isCanary: isCanary().toString(),
},
{},
);
}
/** /**
* Displays a popup asking the user if they want to enable telemetry * Displays a popup asking the user if they want to enable telemetry
* for this extension. * for this extension.

View File

@@ -11,6 +11,7 @@ import {
ResultSeverity, ResultSeverity,
} from "../../../remote-queries/shared/analysis-result"; } from "../../../remote-queries/shared/analysis-result";
import { CodePathsOverlay } from "./CodePathsOverlay"; import { CodePathsOverlay } from "./CodePathsOverlay";
import { useTelemetryOnChange } from "../telemetry";
const ShowPathsLink = styled(VSCodeLink)` const ShowPathsLink = styled(VSCodeLink)`
cursor: pointer; cursor: pointer;
@@ -23,6 +24,8 @@ export type CodePathsProps = {
severity: ResultSeverity; severity: ResultSeverity;
}; };
const filterIsOpenTelemetry = (v: boolean) => v;
export const CodePaths = ({ export const CodePaths = ({
codeFlows, codeFlows,
ruleDescription, ruleDescription,
@@ -30,6 +33,9 @@ export const CodePaths = ({
severity, severity,
}: CodePathsProps) => { }: CodePathsProps) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
useTelemetryOnChange(isOpen, "code-path-is-open", {
filterTelemetryOnValue: filterIsOpenTelemetry,
});
const linkRef = useRef<HTMLAnchorElement>(null); const linkRef = useRef<HTMLAnchorElement>(null);

View File

@@ -7,6 +7,7 @@ import {
CodeFlow, CodeFlow,
ResultSeverity, ResultSeverity,
} from "../../../remote-queries/shared/analysis-result"; } from "../../../remote-queries/shared/analysis-result";
import { useTelemetryOnChange } from "../telemetry";
import { SectionTitle } from "../SectionTitle"; import { SectionTitle } from "../SectionTitle";
import { VerticalSpace } from "../VerticalSpace"; import { VerticalSpace } from "../VerticalSpace";
import { CodeFlowsDropdown } from "./CodeFlowsDropdown"; import { CodeFlowsDropdown } from "./CodeFlowsDropdown";
@@ -77,6 +78,7 @@ export const CodePathsOverlay = ({
onClose, onClose,
}: CodePathsOverlayProps) => { }: CodePathsOverlayProps) => {
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]); const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
useTelemetryOnChange(selectedCodeFlow, "code-flow-selected");
return ( return (
<OverlayContainer> <OverlayContainer>

View File

@@ -8,6 +8,7 @@ import {
} from "../../../remote-queries/shared/analysis-result"; } from "../../../remote-queries/shared/analysis-result";
import { createRemoteFileRef } from "../../../pure/location-link-utils"; import { createRemoteFileRef } from "../../../pure/location-link-utils";
import { VerticalSpace } from "../VerticalSpace"; import { VerticalSpace } from "../VerticalSpace";
import { sendTelemetry } from "../telemetry";
const getSeverityColor = (severity: ResultSeverity) => { const getSeverityColor = (severity: ResultSeverity) => {
switch (severity) { switch (severity) {
@@ -49,6 +50,8 @@ type CodeSnippetMessageProps = {
children: React.ReactNode; children: React.ReactNode;
}; };
const sendAlertMessageLinkTelemetry = () => sendTelemetry("alert-message-link");
export const CodeSnippetMessage = ({ export const CodeSnippetMessage = ({
message, message,
severity, severity,
@@ -65,6 +68,7 @@ export const CodeSnippetMessage = ({
return ( return (
<LocationLink <LocationLink
key={index} key={index}
onClick={sendAlertMessageLinkTelemetry}
href={createRemoteFileRef( href={createRemoteFileRef(
token.location.fileLink, token.location.fileLink,
token.location.highlightedRegion?.startLine, token.location.highlightedRegion?.startLine,

View File

@@ -12,6 +12,7 @@ import {
import { createRemoteFileRef } from "../../../pure/location-link-utils"; import { createRemoteFileRef } from "../../../pure/location-link-utils";
import { CodeSnippetMessage } from "./CodeSnippetMessage"; import { CodeSnippetMessage } from "./CodeSnippetMessage";
import { CodeSnippetLine } from "./CodeSnippetLine"; import { CodeSnippetLine } from "./CodeSnippetLine";
import { sendTelemetry } from "../telemetry";
const borderColor = "var(--vscode-editor-snippetFinalTabstopHighlightBorder)"; const borderColor = "var(--vscode-editor-snippetFinalTabstopHighlightBorder)";
@@ -46,6 +47,9 @@ type Props = {
messageChildren?: React.ReactNode; messageChildren?: React.ReactNode;
}; };
const sendCodeSnippetTitleLinkTelemetry = () =>
sendTelemetry("file-code-snippet-title-link");
export const FileCodeSnippet = ({ export const FileCodeSnippet = ({
fileLink, fileLink,
codeSnippet, codeSnippet,
@@ -67,7 +71,12 @@ export const FileCodeSnippet = ({
return ( return (
<Container> <Container>
<TitleContainer> <TitleContainer>
<VSCodeLink href={titleFileUri}>{fileLink.filePath}</VSCodeLink> <VSCodeLink
onClick={sendCodeSnippetTitleLinkTelemetry}
href={titleFileUri}
>
{fileLink.filePath}
</VSCodeLink>
</TitleContainer> </TitleContainer>
{message && severity && ( {message && severity && (
<CodeSnippetMessage message={message} severity={severity}> <CodeSnippetMessage message={message} severity={severity}>
@@ -83,7 +92,12 @@ export const FileCodeSnippet = ({
return ( return (
<Container> <Container>
<TitleContainer> <TitleContainer>
<VSCodeLink href={titleFileUri}>{fileLink.filePath}</VSCodeLink> <VSCodeLink
onClick={sendCodeSnippetTitleLinkTelemetry}
href={titleFileUri}
>
{fileLink.filePath}
</VSCodeLink>
</TitleContainer> </TitleContainer>
<CodeContainer> <CodeContainer>
{code.map((line, index) => ( {code.map((line, index) => (

View File

@@ -0,0 +1,61 @@
import { useEffect, useMemo, useRef } from "react";
import { vscode } from "../vscode-api";
/**
* A react effect that outputs telemetry events whenever the value changes.
*
* @param value Default value to pass to React.useState
* @param telemetryAction Name of the telemetry event to output
* @param options Extra optional arguments, including:
* filterTelemetryOnValue: If provided, only output telemetry events when the
* predicate returns true. If not provided always outputs telemetry.
* debounceTimeout: If provided, will not output telemetry events for every change
* but will wait until specified timeout happens with no new events ocurring.
*/
export function useTelemetryOnChange<S>(
value: S,
telemetryAction: string,
{
filterTelemetryOnValue,
debounceTimeoutMillis,
}: {
filterTelemetryOnValue?: (value: S) => boolean;
debounceTimeoutMillis?: number;
} = {},
) {
const previousValue = useRef(value);
const sendTelemetryFunc = useMemo<() => void>(() => {
if (debounceTimeoutMillis === undefined) {
return () => sendTelemetry(telemetryAction);
} else {
let timer: NodeJS.Timeout;
return () => {
clearTimeout(timer);
timer = setTimeout(() => {
sendTelemetry(telemetryAction);
}, debounceTimeoutMillis);
};
}
}, [telemetryAction, debounceTimeoutMillis]);
useEffect(() => {
if (value === previousValue.current) {
return;
}
previousValue.current = value;
if (filterTelemetryOnValue && !filterTelemetryOnValue(value)) {
return;
}
sendTelemetryFunc();
}, [sendTelemetryFunc, filterTelemetryOnValue, value, previousValue]);
}
export function sendTelemetry(telemetryAction: string) {
vscode.postMessage({
t: "telemetry",
action: telemetryAction,
});
}

View File

@@ -10,6 +10,7 @@ import {
import { tryGetRemoteLocation } from "../../pure/bqrs-utils"; import { tryGetRemoteLocation } from "../../pure/bqrs-utils";
import TextButton from "./TextButton"; import TextButton from "./TextButton";
import { convertNonPrintableChars } from "../../text-utils"; import { convertNonPrintableChars } from "../../text-utils";
import { sendTelemetry, useTelemetryOnChange } from "../common/telemetry";
const numOfResultsInContractedMode = 5; const numOfResultsInContractedMode = 5;
@@ -45,6 +46,8 @@ type CellProps = {
sourceLocationPrefix: string; sourceLocationPrefix: string;
}; };
const sendRawResultsLinkTelemetry = () => sendTelemetry("raw-results-link");
const Cell = ({ value, fileLinkPrefix, sourceLocationPrefix }: CellProps) => { const Cell = ({ value, fileLinkPrefix, sourceLocationPrefix }: CellProps) => {
switch (typeof value) { switch (typeof value) {
case "string": case "string":
@@ -59,7 +62,11 @@ const Cell = ({ value, fileLinkPrefix, sourceLocationPrefix }: CellProps) => {
); );
const safeLabel = convertNonPrintableChars(value.label); const safeLabel = convertNonPrintableChars(value.label);
if (url) { if (url) {
return <VSCodeLink href={url}>{safeLabel}</VSCodeLink>; return (
<VSCodeLink onClick={sendRawResultsLinkTelemetry} href={url}>
{safeLabel}
</VSCodeLink>
);
} else { } else {
return <span>{safeLabel}</span>; return <span>{safeLabel}</span>;
} }
@@ -94,6 +101,8 @@ type RawResultsTableProps = {
sourceLocationPrefix: string; sourceLocationPrefix: string;
}; };
const filterTableExpandedTelemetry = (v: boolean) => v;
const RawResultsTable = ({ const RawResultsTable = ({
schema, schema,
results, results,
@@ -101,6 +110,9 @@ const RawResultsTable = ({
sourceLocationPrefix, sourceLocationPrefix,
}: RawResultsTableProps) => { }: RawResultsTableProps) => {
const [tableExpanded, setTableExpanded] = useState(false); const [tableExpanded, setTableExpanded] = useState(false);
useTelemetryOnChange(tableExpanded, "raw-results-table-expanded", {
filterTelemetryOnValue: filterTableExpandedTelemetry,
});
const numOfResultsToShow = tableExpanded const numOfResultsToShow = tableExpanded
? results.rows.length ? results.rows.length
: numOfResultsInContractedMode; : numOfResultsInContractedMode;

View File

@@ -433,7 +433,7 @@ const AnalysesResults = ({
sort: Sort; sort: Sort;
}) => { }) => {
const totalAnalysesResults = sumAnalysesResults(analysesResults); const totalAnalysesResults = sumAnalysesResults(analysesResults);
const [filterValue, setFilterValue] = React.useState(""); const [filterValue, setFilterValue] = useState("");
if (totalResults === 0) { if (totalResults === 0) {
return <></>; return <></>;

View File

@@ -24,6 +24,7 @@ import { vscode } from "../vscode-api";
import { AnalyzedRepoItemContent } from "./AnalyzedRepoItemContent"; import { AnalyzedRepoItemContent } from "./AnalyzedRepoItemContent";
import StarCount from "../common/StarCount"; import StarCount from "../common/StarCount";
import { LastUpdated } from "../common/LastUpdated"; import { LastUpdated } from "../common/LastUpdated";
import { useTelemetryOnChange } from "../common/telemetry";
// This will ensure that these icons have a className which we can use in the TitleContainer // This will ensure that these icons have a className which we can use in the TitleContainer
const ExpandCollapseCodicon = styled(Codicon)``; const ExpandCollapseCodicon = styled(Codicon)``;
@@ -157,6 +158,8 @@ const isExpandableContentLoaded = (
return resultsLoaded; return resultsLoaded;
}; };
const filterRepoRowExpandedTelemetry = (v: boolean) => v;
export const RepoRow = ({ export const RepoRow = ({
repository, repository,
status, status,
@@ -168,6 +171,9 @@ export const RepoRow = ({
onSelectedChange, onSelectedChange,
}: RepoRowProps) => { }: RepoRowProps) => {
const [isExpanded, setExpanded] = useState(false); const [isExpanded, setExpanded] = useState(false);
useTelemetryOnChange(isExpanded, "variant-analysis-repo-row-expanded", {
filterTelemetryOnValue: filterRepoRowExpandedTelemetry,
});
const resultsLoaded = !!interpretedResults || !!rawResults; const resultsLoaded = !!interpretedResults || !!rawResults;
const [resultsLoading, setResultsLoading] = useState(false); const [resultsLoading, setResultsLoading] = useState(false);
@@ -198,6 +204,7 @@ export const RepoRow = ({
repository.fullName, repository.fullName,
status, status,
downloadStatus, downloadStatus,
setExpanded,
]); ]);
useEffect(() => { useEffect(() => {
@@ -205,7 +212,7 @@ export const RepoRow = ({
setResultsLoading(false); setResultsLoading(false);
setExpanded(true); setExpanded(true);
} }
}, [resultsLoaded, resultsLoading]); }, [resultsLoaded, resultsLoading, setExpanded]);
const onClickCheckbox = useCallback((e: React.MouseEvent) => { const onClickCheckbox = useCallback((e: React.MouseEvent) => {
// Prevent calling the onClick event of the container, which would toggle the expanded state // Prevent calling the onClick event of the container, which would toggle the expanded state

View File

@@ -12,10 +12,8 @@ import { VariantAnalysisOutcomePanels } from "./VariantAnalysisOutcomePanels";
import { VariantAnalysisLoading } from "./VariantAnalysisLoading"; import { VariantAnalysisLoading } from "./VariantAnalysisLoading";
import { ToVariantAnalysisMessage } from "../../pure/interface-types"; import { ToVariantAnalysisMessage } from "../../pure/interface-types";
import { vscode } from "../vscode-api"; import { vscode } from "../vscode-api";
import { import { defaultFilterSortState } from "../../pure/variant-analysis-filter-sort";
defaultFilterSortState, import { useTelemetryOnChange } from "../common/telemetry";
RepositoriesFilterSortState,
} from "../../pure/variant-analysis-filter-sort";
export type VariantAnalysisProps = { export type VariantAnalysisProps = {
variantAnalysis?: VariantAnalysisDomainModel; variantAnalysis?: VariantAnalysisDomainModel;
@@ -63,8 +61,19 @@ export function VariantAnalysis({
const [selectedRepositoryIds, setSelectedRepositoryIds] = useState<number[]>( const [selectedRepositoryIds, setSelectedRepositoryIds] = useState<number[]>(
[], [],
); );
const [filterSortState, setFilterSortState] = useTelemetryOnChange(
useState<RepositoriesFilterSortState>(defaultFilterSortState); selectedRepositoryIds,
"variant-analysis-selected-repository-ids",
{
debounceTimeoutMillis: 1000,
},
);
const [filterSortState, setFilterSortState] = useState(
defaultFilterSortState,
);
useTelemetryOnChange(filterSortState, "variant-analysis-filter-sort-state", {
debounceTimeoutMillis: 1000,
});
useEffect(() => { useEffect(() => {
const listener = (evt: MessageEvent) => { const listener = (evt: MessageEvent) => {