microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
revert-2693-dev/lucygramley/networkIsolationPolicy

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/services/surveyService/surveyService.ts

241lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import * as vscode from "vscode";
5import * as nls from "vscode-nls";
6import { IConfig, retryDownloadConfig } from "../remoteConfigHelper";
7import { ExtensionConfigManager } from "../../extensionConfigManager";
8import { TelemetryHelper } from "../../../common/telemetryHelper";
9import { Telemetry } from "../../../common/telemetry";
10import { areSameDates, getRandomIntInclusive } from "../../../common/utils";
11import { Delayer } from "../../../common/node/promise";
12
13nls.config({
14 messageFormat: nls.MessageFormat.bundle,
15 bundleFormat: nls.BundleFormat.standalone,
16})();
17const localize = nls.loadMessageBundle();
18
19enum SurveyNotificationReaction {
20 ACCEPT = "accept",
21 CANCEL = "cancel",
22}
23
24interface RemoteSurveyConfig extends IConfig {
25 shortPeriodToRemind: number;
26 longPeriodToRemind: number;
27 popCoveragePercent: number;
28 surveyName: string;
29 surveyUrl: string;
30 enabled: boolean;
31}
32
33export interface SurveyConfig extends RemoteSurveyConfig {
34 daysLeftBeforeSurvey: number;
35 lastExtensionUsageDate?: Date;
36}
37
38export class SurveyService implements vscode.Disposable {
39 private static instance: SurveyService;
40
41 private readonly SURVEY_CONFIG_NAME: string = "surveyConfig";
42 private readonly MAX_WAIT_TIME_TO_SHOW_SURVEY_IN_MINUTES: number = 30;
43 private readonly MIN_WAIT_TIME_TO_SHOW_SURVEY_IN_MINUTES: number = 5;
44 private readonly endpointURL: string =
45 "https://microsoft.github.io/vscode-react-native/surveys/surveyConfig.json";
46 private readonly downloadConfigRequest: Promise<RemoteSurveyConfig>;
47
48 private cancellationTokenSource: vscode.CancellationTokenSource =
49 new vscode.CancellationTokenSource();
50 private _surveyConfig: SurveyConfig | null = null;
51 private extensionFirstTimeInstalled: boolean = false;
52 private promptDelayer: Delayer<Promise<void>> = new Delayer();
53
54 public static getInstance(): SurveyService {
55 if (!SurveyService.instance) {
56 SurveyService.instance = new SurveyService();
57 }
58
59 return SurveyService.instance;
60 }
61
62 private constructor() {
63 this.downloadConfigRequest = retryDownloadConfig<RemoteSurveyConfig>(
64 this.endpointURL,
65 this.cancellationTokenSource,
66 );
67 }
68
69 public async promptSurvey(): Promise<void> {
70 await this.initializeSurveyConfig();
71
72 if (!this.surveyConfig.enabled) {
73 return;
74 }
75
76 const curDate: Date = new Date();
77
78 if (this.surveyConfig.daysLeftBeforeSurvey === 0) {
79 if (this.isCandidate()) {
80 void this.promptDelayer.runWihtDelay(async () => {
81 await this.showSurveyNotification();
82 this.surveyConfig.daysLeftBeforeSurvey = this.surveyConfig.longPeriodToRemind;
83 this.saveSurveyConfig(this.surveyConfig);
84 }, this.calculateSurveyNotificationDelay());
85 } else {
86 this.surveyConfig.daysLeftBeforeSurvey = this.surveyConfig.shortPeriodToRemind;
87 }
88 } else if (
89 this.surveyConfig.lastExtensionUsageDate &&
90 !areSameDates(curDate, this.surveyConfig.lastExtensionUsageDate) &&
91 this.surveyConfig.daysLeftBeforeSurvey > 0
92 ) {
93 this.surveyConfig.daysLeftBeforeSurvey--;
94 }
95
96 this.surveyConfig.lastExtensionUsageDate = curDate;
97 this.saveSurveyConfig(this.surveyConfig);
98 }
99
100 public setExtensionFirstTimeInstalled(extensionFirstTimeInstalled: boolean): void {
101 this.extensionFirstTimeInstalled = extensionFirstTimeInstalled;
102 }
103
104 public dispose(): void {
105 this.cancellationTokenSource.cancel();
106 this.cancellationTokenSource.dispose();
107 this.promptDelayer.dispose();
108 }
109
110 private async initializeSurveyConfig(): Promise<void> {
111 if (this._surveyConfig) {
112 return;
113 }
114
115 const surveyConfigLocal: SurveyConfig = ExtensionConfigManager.config.has(
116 this.SURVEY_CONFIG_NAME,
117 )
118 ? this.prepareRawConfig(ExtensionConfigManager.config.get(this.SURVEY_CONFIG_NAME))
119 : {
120 shortPeriodToRemind: 30,
121 longPeriodToRemind: 90,
122 popCoveragePercent: 0.1,
123 enabled: false,
124 daysLeftBeforeSurvey: this.extensionFirstTimeInstalled ? 30 : 3,
125 surveyName: "none",
126 surveyUrl: "",
127 };
128
129 const surveyConfig = await this.mergeRemoteConfigToLocal(surveyConfigLocal);
130
131 this.saveSurveyConfig(surveyConfig);
132
133 this._surveyConfig = surveyConfig;
134 }
135
136 private saveSurveyConfig(surveyConfig: SurveyConfig): void {
137 ExtensionConfigManager.config.set(this.SURVEY_CONFIG_NAME, surveyConfig);
138 }
139
140 private calculateSurveyNotificationDelay(): number {
141 return (
142 getRandomIntInclusive(
143 this.MIN_WAIT_TIME_TO_SHOW_SURVEY_IN_MINUTES,
144 this.MAX_WAIT_TIME_TO_SHOW_SURVEY_IN_MINUTES,
145 ) *
146 60 *
147 1000
148 );
149 }
150
151 private async showSurveyNotification(): Promise<void> {
152 const giveFeedbackButtonText = localize("giveFeedback", "Give Feedback");
153 const remindLaterButtonText = localize("remindLater", "Remind Me later");
154 const notificationText = localize(
155 "surveyNotificationText",
156 "Got a moment to help the React Native Tools team? Please tell us about your experience with the extension so far.",
157 );
158
159 this.sendPromptSurveyTelemetry(this.surveyConfig.surveyName);
160
161 const selection = await vscode.window.showInformationMessage(
162 notificationText,
163 giveFeedbackButtonText,
164 remindLaterButtonText,
165 );
166
167 if (!selection || selection === remindLaterButtonText) {
168 this.sendSurveyNotificationReactionTelemetry(
169 this.surveyConfig.surveyName,
170 SurveyNotificationReaction.CANCEL,
171 );
172 }
173 if (selection === giveFeedbackButtonText && this.surveyConfig.surveyUrl) {
174 void vscode.env.openExternal(vscode.Uri.parse(this.surveyConfig.surveyUrl));
175 this.sendSurveyNotificationReactionTelemetry(
176 this.surveyConfig.surveyName,
177 SurveyNotificationReaction.ACCEPT,
178 );
179 }
180 }
181
182 private get surveyConfig(): SurveyConfig {
183 if (!this._surveyConfig) {
184 if (!ExtensionConfigManager.config.has(this.SURVEY_CONFIG_NAME)) {
185 throw new Error("Could not find Survey config in the config store.");
186 } else {
187 this._surveyConfig = this.prepareRawConfig(
188 ExtensionConfigManager.config.get(this.SURVEY_CONFIG_NAME),
189 );
190 }
191 }
192 return this._surveyConfig as SurveyConfig;
193 }
194
195 private isCandidate(): boolean {
196 return this.surveyConfig.popCoveragePercent > Math.random();
197 }
198
199 private prepareRawConfig(rawSurveyConfig: SurveyConfig): SurveyConfig {
200 if (rawSurveyConfig.lastExtensionUsageDate) {
201 rawSurveyConfig.lastExtensionUsageDate = new Date(
202 rawSurveyConfig.lastExtensionUsageDate,
203 );
204 }
205 return rawSurveyConfig;
206 }
207
208 private async mergeRemoteConfigToLocal(surveyConfig: SurveyConfig): Promise<SurveyConfig> {
209 const remoteConfig = await this.downloadConfigRequest;
210 surveyConfig.shortPeriodToRemind = remoteConfig.shortPeriodToRemind;
211 surveyConfig.longPeriodToRemind = remoteConfig.longPeriodToRemind;
212 surveyConfig.popCoveragePercent = remoteConfig.popCoveragePercent;
213 surveyConfig.surveyUrl = remoteConfig.surveyUrl;
214 surveyConfig.surveyName = remoteConfig.surveyName;
215 surveyConfig.enabled = remoteConfig.enabled;
216 return surveyConfig;
217 }
218
219 private sendPromptSurveyTelemetry(surveyName: string): void {
220 const promptUserSurveyEvent = TelemetryHelper.createTelemetryEvent("promptUserSurvey", {
221 surveyName,
222 });
223
224 Telemetry.send(promptUserSurveyEvent);
225 }
226
227 private sendSurveyNotificationReactionTelemetry(
228 surveyName: string,
229 surveyNotificationReaction: SurveyNotificationReaction,
230 ): void {
231 const surveyNotificationReactionEvent = TelemetryHelper.createTelemetryEvent(
232 "surveyNotificationReaction",
233 {
234 surveyName,
235 userReaction: surveyNotificationReaction,
236 },
237 );
238
239 Telemetry.send(surveyNotificationReactionEvent);
240 }
241}
242