microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
align-android-launch-command

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/extension/services/surveyService/surveyService.ts

241lines · modepreview

// 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<RemoteSurveyConfig>;

    private cancellationTokenSource: vscode.CancellationTokenSource =
        new vscode.CancellationTokenSource();
    private _surveyConfig: SurveyConfig | null = null;
    private extensionFirstTimeInstalled: boolean = false;
    private promptDelayer: Delayer<Promise<void>> = new Delayer();

    public static getInstance(): SurveyService {
        if (!SurveyService.instance) {
            SurveyService.instance = new SurveyService();
        }

        return SurveyService.instance;
    }

    private constructor() {
        this.downloadConfigRequest = retryDownloadConfig<RemoteSurveyConfig>(
            this.endpointURL,
            this.cancellationTokenSource,
        );
    }

    public async promptSurvey(): Promise<void> {
        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<void> {
        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<void> {
        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<SurveyConfig> {
        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);
    }
}