From b183cc08da1249460e299c0fcdb80e722f3226da Mon Sep 17 00:00:00 2001 From: Armando Andini Date: Mon, 29 Jan 2024 14:16:28 -0300 Subject: [PATCH] chore: report telemetry consent --- client/src/popups/showAnalyticsAllowPopup.ts | 16 ++--- server/src/analytics/GoogleAnalytics.ts | 59 ++++++++++++++++++- server/src/analytics/types.ts | 29 +++++---- server/src/server.ts | 7 ++- server/src/telemetry/SentryServerTelemetry.ts | 2 +- server/src/telemetry/types.ts | 3 + server/test/helpers/setupMockAnalytics.ts | 2 + server/test/helpers/setupMockTelemetry.ts | 2 + 8 files changed, 94 insertions(+), 26 deletions(-) diff --git a/client/src/popups/showAnalyticsAllowPopup.ts b/client/src/popups/showAnalyticsAllowPopup.ts index a2fb82a8..e2e1c86c 100644 --- a/client/src/popups/showAnalyticsAllowPopup.ts +++ b/client/src/popups/showAnalyticsAllowPopup.ts @@ -1,18 +1,12 @@ -import { - workspace, - window, - ExtensionContext, - ConfigurationTarget, - ExtensionMode, -} from "vscode"; +import { workspace, window, ConfigurationTarget, ExtensionMode } from "vscode"; +import { ExtensionState } from "../types"; const PREVIOUSLY_SHOWN_TELEMETRY_LABEL = "previouslyShownTelemetryMessage"; export async function showAnalyticsAllowPopup({ context, -}: { - context: ExtensionContext; -}): Promise { + client, +}: ExtensionState): Promise { if (context.extensionMode === ExtensionMode.Test) { // Dialog messages are prohibited in tests: // https://github.com/microsoft/vscode/blob/36fefc828e4c496a7bbb64c63f3eb3052a650f8f/src/vs/workbench/services/dialogs/common/dialogService.ts#L56 @@ -44,4 +38,6 @@ export async function showAnalyticsAllowPopup({ await config.update("telemetry", isAccepted, ConfigurationTarget.Global); await context.globalState.update(PREVIOUSLY_SHOWN_TELEMETRY_LABEL, true); + + client?.sendNotification("custom/telemetryConsent", isAccepted); } diff --git a/server/src/analytics/GoogleAnalytics.ts b/server/src/analytics/GoogleAnalytics.ts index 08832328..432532d5 100644 --- a/server/src/analytics/GoogleAnalytics.ts +++ b/server/src/analytics/GoogleAnalytics.ts @@ -6,6 +6,7 @@ import { ServerState } from "../types"; import { Analytics, AnalyticsPayload } from "./types"; const GA_URL = "https://www.google-analytics.com/mp/collect"; +const TELEMETRY_USER_ID = "hh_vscode_telemetry_consent"; export class GoogleAnalytics implements Analytics { private readonly measurementID: string; @@ -48,7 +49,7 @@ export class GoogleAnalytics implements Analytics { return; } - const payload = this._buildPayloadFrom(taskName, this.machineId); + const payload = this._buildTaskPayload(taskName, this.machineId); await this._sendHit(payload); } catch { @@ -57,7 +58,29 @@ export class GoogleAnalytics implements Analytics { } } - private _buildPayloadFrom( + // Meant for the initial response to the telemetry consent popup + public async sendTelemetryResponse(userConsent: boolean): Promise { + try { + const payload = this._buildTelemetryResponsePayload(userConsent); + + await this._sendHit(payload); + } catch { + return; + } + } + + // Meant for subsequent changes to the telemetry setting + public async sendTelemetryChange(userConsent: boolean): Promise { + try { + const payload = this._buildTelemetryChangePayload(userConsent); + + await this._sendHit(payload); + } catch { + return; + } + } + + private _buildTaskPayload( taskName: string, machineId: string ): AnalyticsPayload { @@ -81,6 +104,38 @@ export class GoogleAnalytics implements Analytics { }; } + private _buildTelemetryResponsePayload(userConsent: boolean) { + return { + client_id: TELEMETRY_USER_ID, + user_id: TELEMETRY_USER_ID, + user_properties: {}, + events: [ + { + name: "TelemetryConsentResponse", + params: { + userConsent: userConsent ? "yes" : "no", + }, + }, + ], + }; + } + + private _buildTelemetryChangePayload(userConsent: boolean) { + return { + client_id: TELEMETRY_USER_ID, + user_id: TELEMETRY_USER_ID, + user_properties: {}, + events: [ + { + name: "TelemetryConsentChange", + params: { + userConsent: userConsent ? "yes" : "no", + }, + }, + ], + }; + } + private _sendHit(payload: AnalyticsPayload) { return got.post(GA_URL, { headers: { diff --git a/server/src/analytics/types.ts b/server/src/analytics/types.ts index 24f8f1f1..07402d08 100644 --- a/server/src/analytics/types.ts +++ b/server/src/analytics/types.ts @@ -6,23 +6,26 @@ export interface AnalyticsPayload { client_id: string; user_id: string; - user_properties: { - extensionVersion: { - value?: string; - }; - languageClient: { - value?: string; - }; - operatingSystem: { - value?: string; - }; - }; + user_properties: + | { + extensionVersion: { + value?: string; + }; + languageClient: { + value?: string; + }; + operatingSystem: { + value?: string; + }; + } + | {}; events: Array<{ name: string; params: { - engagement_time_msec: string; + engagement_time_msec?: string; session_id?: string; + userConsent?: string; }; }>; } @@ -36,4 +39,6 @@ export interface Analytics { ): void; sendPageView(taskName: string): Promise; + sendTelemetryResponse(userConsent: boolean): Promise; + sendTelemetryChange(userConsent: boolean): Promise; } diff --git a/server/src/server.ts b/server/src/server.ts index c2e78b46..b4f56044 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -109,7 +109,7 @@ function attachCustomHooks(serverState: ServerState) { connection.onNotification( "custom/didChangeTelemetryEnabled", - ({ enabled }: { enabled: boolean }) => { + async ({ enabled }: { enabled: boolean }) => { if (enabled) { logger.info(`Telemetry enabled`); } else { @@ -117,6 +117,7 @@ function attachCustomHooks(serverState: ServerState) { } serverState.telemetryEnabled = enabled; + await serverState.telemetry.analytics.sendTelemetryChange(enabled); } ); @@ -126,4 +127,8 @@ function attachCustomHooks(serverState: ServerState) { serverState.extensionConfig = extensionConfig; } ); + + connection.onNotification("custom/telemetryConsent", async (payload) => { + await serverState.telemetry.analytics.sendTelemetryResponse(payload); + }); } diff --git a/server/src/telemetry/SentryServerTelemetry.ts b/server/src/telemetry/SentryServerTelemetry.ts index 34917058..efdf2106 100644 --- a/server/src/telemetry/SentryServerTelemetry.ts +++ b/server/src/telemetry/SentryServerTelemetry.ts @@ -14,7 +14,7 @@ const SENTRY_CLOSE_TIMEOUT = 2000; export class SentryServerTelemetry implements Telemetry { private dsn: string; private serverState: ServerState | null; - private analytics: Analytics; + public analytics: Analytics; private actionTaken: boolean; private heartbeatInterval: NodeJS.Timeout | null; private heartbeatPeriod: number; diff --git a/server/src/telemetry/types.ts b/server/src/telemetry/types.ts index c0406389..bc9789f2 100644 --- a/server/src/telemetry/types.ts +++ b/server/src/telemetry/types.ts @@ -1,6 +1,7 @@ import { SpanStatusType } from "@sentry/tracing"; import type { Primitive, Transaction } from "@sentry/types"; import { ServerState } from "../types"; +import { Analytics } from "../analytics/types"; export interface TrackingResult { status: SpanStatusType; @@ -8,6 +9,8 @@ export interface TrackingResult { } export interface Telemetry { + analytics: Analytics; + init( machineId: string | undefined, extensionName: string | undefined, diff --git a/server/test/helpers/setupMockAnalytics.ts b/server/test/helpers/setupMockAnalytics.ts index 33077fe2..e782bc81 100644 --- a/server/test/helpers/setupMockAnalytics.ts +++ b/server/test/helpers/setupMockAnalytics.ts @@ -5,5 +5,7 @@ export function setupMockAnalytics(): Analytics { return { init: sinon.spy(), sendPageView: sinon.spy(), + sendTelemetryResponse: sinon.spy(), + sendTelemetryChange: sinon.spy(), }; } diff --git a/server/test/helpers/setupMockTelemetry.ts b/server/test/helpers/setupMockTelemetry.ts index aa074a42..5c690717 100644 --- a/server/test/helpers/setupMockTelemetry.ts +++ b/server/test/helpers/setupMockTelemetry.ts @@ -1,11 +1,13 @@ import { Transaction } from "@sentry/types"; import * as sinon from "sinon"; import { Telemetry } from "../../src/telemetry/types"; +import { setupMockAnalytics } from "./setupMockAnalytics"; export function setupMockTelemetry(): Telemetry { return { init: sinon.spy(), captureException: sinon.spy(), + analytics: setupMockAnalytics(), trackTiming: async (_taskName: string, action) => { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any