From 3aaf810eee46689824bbf124f81e67bf9c291dde Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 29 Sep 2021 16:14:30 -0700 Subject: [PATCH] experiments: allow multiple activation events --- .../experiments/common/experimentService.ts | 59 +++++++++------- .../experimentService.test.ts | 67 +++++++++++++------ 2 files changed, 83 insertions(+), 43 deletions(-) diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index a544ac4c6e5..2737127f12b 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -3,26 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { distinct } from 'vs/base/common/arrays'; +import { RunOnceWorker } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; -import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { match } from 'vs/base/common/glob'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { equals } from 'vs/base/common/objects'; +import { language, OperatingSystem, OS } from 'vs/base/common/platform'; +import { isDefined } from 'vs/base/common/types'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { language, OperatingSystem, OS } from 'vs/base/common/platform'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { match } from 'vs/base/common/glob'; -import { IRequestService, asJson } from 'vs/platform/request/common/request'; -import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { distinct } from 'vs/base/common/arrays'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProductService } from 'vs/platform/product/common/productService'; +import { asJson, IRequestService } from 'vs/platform/request/common/request'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags'; -import { RunOnceWorker } from 'vs/base/common/async'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { equals } from 'vs/base/common/objects'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; export const enum ExperimentState { Evaluating, @@ -93,7 +94,7 @@ interface IExperimentStorageState { * be incremented when adding a condition, otherwise experiments might activate * on older versions of VS Code where not intended. */ -export const currentSchemaVersion = 4; +export const currentSchemaVersion = 5; interface IRawExperiment { id: string; @@ -107,7 +108,7 @@ interface IRawExperiment { userSetting?: { [key: string]: unknown; }; // Start the experiment if the number of activation events have happened over the last week: activationEvent?: { - event: string; + event: string | string[]; uniqueDays?: number; minEvents: number; }; @@ -285,7 +286,8 @@ export class ExperimentService extends Disposable implements IExperimentService this.storageService.remove('allExperiments', StorageScope.GLOBAL); } - const activationEvents = new Set(rawExperiments.map(exp => exp.condition?.activationEvent?.event).filter(evt => !!evt)); + const activationEvents = new Set(rawExperiments.map(exp => exp.condition?.activationEvent?.event) + .filter(isDefined).flatMap(evt => typeof evt === 'string' ? [evt] : [])); if (activationEvents.size) { this._register(this.extensionService.onWillActivateByEvent(evt => { if (activationEvents.has(evt.event)) { @@ -402,7 +404,14 @@ export class ExperimentService extends Disposable implements IExperimentService this.storageService.store(key, JSON.stringify(record), StorageScope.GLOBAL, StorageTarget.MACHINE); this._experiments - .filter(e => e.state === ExperimentState.Evaluating && e.raw?.condition?.activationEvent?.event === event) + .filter(e => { + const lookingFor = e.raw?.condition?.activationEvent?.event; + if (e.state !== ExperimentState.Evaluating || !lookingFor) { + return false; + } + + return typeof lookingFor === 'string' ? lookingFor === event : lookingFor?.includes(event); + }) .forEach(e => this.evaluateExperiment(e.raw!)); } @@ -412,14 +421,18 @@ export class ExperimentService extends Disposable implements IExperimentService return true; } - const { count } = getCurrentActivationRecord(safeParse(this.storageService.get(experimentEventStorageKey(setting.event), StorageScope.GLOBAL), undefined)); - let total = 0; let uniqueDays = 0; - for (const entry of count) { - if (entry > 0) { - uniqueDays++; - total += entry; + + const events = typeof setting.event === 'string' ? [setting.event] : setting.event; + for (const event of events) { + const { count } = getCurrentActivationRecord(safeParse(this.storageService.get(experimentEventStorageKey(event), StorageScope.GLOBAL), undefined)); + + for (const entry of count) { + if (entry > 0) { + uniqueDays++; + total += entry; + } } } diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts index 2be5c1bcc57..fb038316d5e 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts @@ -5,34 +5,32 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { ExperimentActionType, ExperimentState, IExperiment, ExperimentService, getCurrentActivationRecord, currentSchemaVersion } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { - IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, ILocalExtension, InstallExtensionResult -} from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { timeout } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; -import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; -import { NativeURLService } from 'vs/platform/url/common/urlService'; -import { IURLService } from 'vs/platform/url/common/url'; -import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { OS } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, ILocalExtension, InstallExtensionEvent, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IWillActivateEvent, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { timeout } from 'vs/base/common/async'; -import { TestExtensionService } from 'vs/workbench/test/common/workbenchTestServices'; -import { OS } from 'vs/base/common/platform'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IURLService } from 'vs/platform/url/common/url'; +import { NativeURLService } from 'vs/platform/url/common/urlService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { currentSchemaVersion, ExperimentActionType, ExperimentService, ExperimentState, getCurrentActivationRecord, IExperiment } from 'vs/workbench/contrib/experiments/common/experimentService'; +import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; +import { IExtensionService, IWillActivateEvent } from 'vs/workbench/services/extensions/common/extensions'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { TestWorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; +import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestExtensionService } from 'vs/workbench/test/common/workbenchTestServices'; interface ExperimentSettings { enabled?: boolean; @@ -407,6 +405,35 @@ suite('Experiment Service', () => { }); }); + test('Activation event allows multiple', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + activationEvent: { + event: ['other:event', 'my:event'], + minEvents: 5, + } + } + } + ] + }; + + instantiationService.stub(IStorageService, 'get', (a: string, b: StorageScope, c?: string) => { + return a === 'experimentEventRecord-my-event' + ? JSON.stringify({ count: [10], mostRecentBucket: Date.now() }) + : undefined; + }); + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.strictEqual(result.enabled, true); + assert.strictEqual(result.state, ExperimentState.Run); + }); + }); + test('Activation event does not work with old data', () => { experimentData = { experiments: [ -- GitLab