// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import * as vscode from "vscode"; import * as nls from "vscode-nls"; import { IConfig, retryDownloadConfig } from "../remoteConfigHelper"; import { ExtensionConfigManager } from "../../extensionConfigManager"; import { TelemetryHelper } from "../../../common/telemetryHelper"; import { Telemetry } from "../../../common/telemetry"; import { areSameDates, getRandomIntInclusive } from "../../../common/utils"; import { Delayer } from "../../../common/node/promise"; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone, })(); const localize = nls.loadMessageBundle(); enum SurveyNotificationReaction { ACCEPT = "accept", CANCEL = "cancel", } interface RemoteSurveyConfig extends IConfig { shortPeriodToRemind: number; longPeriodToRemind: number; popCoveragePercent: number; surveyName: string; surveyUrl: string; enabled: boolean; } export interface SurveyConfig extends RemoteSurveyConfig { daysLeftBeforeSurvey: number; lastExtensionUsageDate?: Date; } export class SurveyService implements vscode.Disposable { private static instance: SurveyService; private readonly SURVEY_CONFIG_NAME: string = "surveyConfig"; private readonly MAX_WAIT_TIME_TO_SHOW_SURVEY_IN_MINUTES: number = 30; private readonly MIN_WAIT_TIME_TO_SHOW_SURVEY_IN_MINUTES: number = 5; private readonly endpointURL: string = "https://microsoft.github.io/vscode-react-native/surveys/surveyConfig.json"; private readonly downloadConfigRequest: Promise; private cancellationTokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(); private _surveyConfig: SurveyConfig | null = null; private extensionFirstTimeInstalled: boolean = false; private promptDelayer: Delayer> = new Delayer(); public static getInstance(): SurveyService { if (!SurveyService.instance) { SurveyService.instance = new SurveyService(); } return SurveyService.instance; } private constructor() { this.downloadConfigRequest = retryDownloadConfig( this.endpointURL, this.cancellationTokenSource, ); } public async promptSurvey(): Promise { await this.initializeSurveyConfig(); if (!this.surveyConfig.enabled) { return; } const curDate: Date = new Date(); if (this.surveyConfig.daysLeftBeforeSurvey === 0) { if (this.isCandidate()) { void this.promptDelayer.runWihtDelay(async () => { await this.showSurveyNotification(); this.surveyConfig.daysLeftBeforeSurvey = this.surveyConfig.longPeriodToRemind; this.saveSurveyConfig(this.surveyConfig); }, this.calculateSurveyNotificationDelay()); } else { this.surveyConfig.daysLeftBeforeSurvey = this.surveyConfig.shortPeriodToRemind; } } else if ( this.surveyConfig.lastExtensionUsageDate && !areSameDates(curDate, this.surveyConfig.lastExtensionUsageDate) && this.surveyConfig.daysLeftBeforeSurvey > 0 ) { this.surveyConfig.daysLeftBeforeSurvey--; } this.surveyConfig.lastExtensionUsageDate = curDate; this.saveSurveyConfig(this.surveyConfig); } public setExtensionFirstTimeInstalled(extensionFirstTimeInstalled: boolean): void { this.extensionFirstTimeInstalled = extensionFirstTimeInstalled; } public dispose(): void { this.cancellationTokenSource.cancel(); this.cancellationTokenSource.dispose(); this.promptDelayer.dispose(); } private async initializeSurveyConfig(): Promise { if (this._surveyConfig) { return; } const surveyConfigLocal: SurveyConfig = ExtensionConfigManager.config.has( this.SURVEY_CONFIG_NAME, ) ? this.prepareRawConfig(ExtensionConfigManager.config.get(this.SURVEY_CONFIG_NAME)) : { shortPeriodToRemind: 30, longPeriodToRemind: 90, popCoveragePercent: 0.1, enabled: false, daysLeftBeforeSurvey: this.extensionFirstTimeInstalled ? 30 : 3, surveyName: "none", surveyUrl: "", }; const surveyConfig = await this.mergeRemoteConfigToLocal(surveyConfigLocal); this.saveSurveyConfig(surveyConfig); this._surveyConfig = surveyConfig; } private saveSurveyConfig(surveyConfig: SurveyConfig): void { ExtensionConfigManager.config.set(this.SURVEY_CONFIG_NAME, surveyConfig); } private calculateSurveyNotificationDelay(): number { return ( getRandomIntInclusive( this.MIN_WAIT_TIME_TO_SHOW_SURVEY_IN_MINUTES, this.MAX_WAIT_TIME_TO_SHOW_SURVEY_IN_MINUTES, ) * 60 * 1000 ); } private async showSurveyNotification(): Promise { const giveFeedbackButtonText = localize("giveFeedback", "Give Feedback"); const remindLaterButtonText = localize("remindLater", "Remind Me later"); const notificationText = localize( "surveyNotificationText", "Got a moment to help the React Native Tools team? Please tell us about your experience with the extension so far.", ); this.sendPromptSurveyTelemetry(this.surveyConfig.surveyName); const selection = await vscode.window.showInformationMessage( notificationText, giveFeedbackButtonText, remindLaterButtonText, ); if (!selection || selection === remindLaterButtonText) { this.sendSurveyNotificationReactionTelemetry( this.surveyConfig.surveyName, SurveyNotificationReaction.CANCEL, ); } if (selection === giveFeedbackButtonText && this.surveyConfig.surveyUrl) { void vscode.env.openExternal(vscode.Uri.parse(this.surveyConfig.surveyUrl)); this.sendSurveyNotificationReactionTelemetry( this.surveyConfig.surveyName, SurveyNotificationReaction.ACCEPT, ); } } private get surveyConfig(): SurveyConfig { if (!this._surveyConfig) { if (!ExtensionConfigManager.config.has(this.SURVEY_CONFIG_NAME)) { throw new Error("Could not find Survey config in the config store."); } else { this._surveyConfig = this.prepareRawConfig( ExtensionConfigManager.config.get(this.SURVEY_CONFIG_NAME), ); } } return this._surveyConfig as SurveyConfig; } private isCandidate(): boolean { return this.surveyConfig.popCoveragePercent > Math.random(); } private prepareRawConfig(rawSurveyConfig: SurveyConfig): SurveyConfig { if (rawSurveyConfig.lastExtensionUsageDate) { rawSurveyConfig.lastExtensionUsageDate = new Date( rawSurveyConfig.lastExtensionUsageDate, ); } return rawSurveyConfig; } private async mergeRemoteConfigToLocal(surveyConfig: SurveyConfig): Promise { const remoteConfig = await this.downloadConfigRequest; surveyConfig.shortPeriodToRemind = remoteConfig.shortPeriodToRemind; surveyConfig.longPeriodToRemind = remoteConfig.longPeriodToRemind; surveyConfig.popCoveragePercent = remoteConfig.popCoveragePercent; surveyConfig.surveyUrl = remoteConfig.surveyUrl; surveyConfig.surveyName = remoteConfig.surveyName; surveyConfig.enabled = remoteConfig.enabled; return surveyConfig; } private sendPromptSurveyTelemetry(surveyName: string): void { const promptUserSurveyEvent = TelemetryHelper.createTelemetryEvent("promptUserSurvey", { surveyName, }); Telemetry.send(promptUserSurveyEvent); } private sendSurveyNotificationReactionTelemetry( surveyName: string, surveyNotificationReaction: SurveyNotificationReaction, ): void { const surveyNotificationReactionEvent = TelemetryHelper.createTelemetryEvent( "surveyNotificationReaction", { surveyName, userReaction: surveyNotificationReaction, }, ); Telemetry.send(surveyNotificationReactionEvent); } }