Initial assignment service work (#136238)
* Initial assignment service work * Modify useragent for experiment * minor refactor * remove extraneous comment Co-authored-by: SteVen Batten <sbatten@microsoft.com>
This commit is contained in:
parent
1775f26147
commit
78ea034530
|
@ -216,7 +216,8 @@
|
|||
"vs/nls",
|
||||
"**/vs/base/common/**",
|
||||
"**/vs/base/parts/*/common/**",
|
||||
"**/vs/platform/*/common/**"
|
||||
"**/vs/platform/*/common/**",
|
||||
"tas-client-umd"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
],
|
||||
"exclude": [
|
||||
"node_modules/*",
|
||||
"vs/platform/files/browser/htmlFileSystemProvider.ts"
|
||||
"vs/platform/files/browser/htmlFileSystemProvider.ts",
|
||||
"vs/platform/assignment/*"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -95,6 +95,7 @@ import { SharedProcessTunnelService } from 'vs/platform/remote/node/sharedProces
|
|||
import { ipcSharedProcessWorkerChannelName, ISharedProcessWorkerConfiguration, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService';
|
||||
import { SharedProcessWorkerService } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService';
|
||||
import { IUserConfigurationFileService, UserConfigurationFileServiceId } from 'vs/platform/configuration/common/userConfigurationFileService';
|
||||
import { AssignmentService } from 'vs/platform/assignment/common/assignmentService';
|
||||
|
||||
class SharedProcessMain extends Disposable {
|
||||
|
||||
|
@ -240,6 +241,9 @@ class SharedProcessMain extends Disposable {
|
|||
const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id));
|
||||
services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(this.server.getChannel('extensionRecommendationNotification', activeWindowRouter)));
|
||||
|
||||
// Assignment Service (Experiment service w/out scorecards)
|
||||
const assignmentService = new AssignmentService(this.configuration.machineId, configurationService, productService);
|
||||
|
||||
// Telemetry
|
||||
let telemetryService: ITelemetryService;
|
||||
const appenders: ITelemetryAppender[] = [];
|
||||
|
@ -250,7 +254,16 @@ class SharedProcessMain extends Disposable {
|
|||
|
||||
// Application Insights
|
||||
if (productService.aiConfig && productService.aiConfig.asimovKey) {
|
||||
const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey);
|
||||
const testCollector = await assignmentService.getTreatment<boolean>('vscode.telemetryMigration') ?? false;
|
||||
const insiders = productService.quality !== 'stable';
|
||||
// Insiders send to both collector and vortex if assigned.
|
||||
// Stable only send to one
|
||||
if (insiders && testCollector) {
|
||||
const collectorAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey, testCollector, true);
|
||||
this._register(toDisposable(() => collectorAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data
|
||||
appenders.push(collectorAppender);
|
||||
}
|
||||
const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey, insiders ? false : testCollector);
|
||||
this._register(toDisposable(() => appInsightsAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data
|
||||
appenders.push(appInsightsAppender);
|
||||
}
|
||||
|
|
116
src/vs/platform/assignment/common/assignment.ts
Normal file
116
src/vs/platform/assignment/common/assignment.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { IExperimentationFilterProvider } from 'tas-client-umd';
|
||||
|
||||
export const ASSIGNMENT_STORAGE_KEY = 'VSCode.ABExp.FeatureData';
|
||||
export const ASSIGNMENT_REFETCH_INTERVAL = 0; // no polling
|
||||
|
||||
export interface IAssignmentService {
|
||||
readonly _serviceBrand: undefined;
|
||||
getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined>;
|
||||
}
|
||||
|
||||
export enum TargetPopulation {
|
||||
Team = 'team',
|
||||
Internal = 'internal',
|
||||
Insiders = 'insider',
|
||||
Public = 'public',
|
||||
}
|
||||
|
||||
/*
|
||||
Based upon the official VSCode currently existing filters in the
|
||||
ExP backend for the VSCode cluster.
|
||||
https://experimentation.visualstudio.com/Analysis%20and%20Experimentation/_git/AnE.ExP.TAS.TachyonHost.Configuration?path=%2FConfigurations%2Fvscode%2Fvscode.json&version=GBmaster
|
||||
"X-MSEdge-Market": "detection.market",
|
||||
"X-FD-Corpnet": "detection.corpnet",
|
||||
"X-VSCode–AppVersion": "appversion",
|
||||
"X-VSCode-Build": "build",
|
||||
"X-MSEdge-ClientId": "clientid",
|
||||
"X-VSCode-ExtensionName": "extensionname",
|
||||
"X-VSCode-TargetPopulation": "targetpopulation",
|
||||
"X-VSCode-Language": "language"
|
||||
*/
|
||||
export enum Filters {
|
||||
/**
|
||||
* The market in which the extension is distributed.
|
||||
*/
|
||||
Market = 'X-MSEdge-Market',
|
||||
|
||||
/**
|
||||
* The corporation network.
|
||||
*/
|
||||
CorpNet = 'X-FD-Corpnet',
|
||||
|
||||
/**
|
||||
* Version of the application which uses experimentation service.
|
||||
*/
|
||||
ApplicationVersion = 'X-VSCode-AppVersion',
|
||||
|
||||
/**
|
||||
* Insiders vs Stable.
|
||||
*/
|
||||
Build = 'X-VSCode-Build',
|
||||
|
||||
/**
|
||||
* Client Id which is used as primary unit for the experimentation.
|
||||
*/
|
||||
ClientId = 'X-MSEdge-ClientId',
|
||||
|
||||
/**
|
||||
* Extension header.
|
||||
*/
|
||||
ExtensionName = 'X-VSCode-ExtensionName',
|
||||
|
||||
/**
|
||||
* The language in use by VS Code
|
||||
*/
|
||||
Language = 'X-VSCode-Language',
|
||||
|
||||
/**
|
||||
* The target population.
|
||||
* This is used to separate internal, early preview, GA, etc.
|
||||
*/
|
||||
TargetPopulation = 'X-VSCode-TargetPopulation',
|
||||
}
|
||||
|
||||
export class AssignmentFilterProvider implements IExperimentationFilterProvider {
|
||||
constructor(
|
||||
private version: string,
|
||||
private appName: string,
|
||||
private machineId: string,
|
||||
private targetPopulation: TargetPopulation
|
||||
) { }
|
||||
|
||||
getFilterValue(filter: string): string | null {
|
||||
switch (filter) {
|
||||
case Filters.ApplicationVersion:
|
||||
return this.version; // productService.version
|
||||
case Filters.Build:
|
||||
return this.appName; // productService.nameLong
|
||||
case Filters.ClientId:
|
||||
return this.machineId;
|
||||
case Filters.Language:
|
||||
return platform.language;
|
||||
case Filters.ExtensionName:
|
||||
return 'vscode-core'; // always return vscode-core for exp service
|
||||
case Filters.TargetPopulation:
|
||||
return this.targetPopulation;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
getFilters(): Map<string, any> {
|
||||
let filters: Map<string, any> = new Map<string, any>();
|
||||
let filterValues = Object.values(Filters);
|
||||
for (let value of filterValues) {
|
||||
filters.set(value, this.getFilterValue(value));
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
}
|
110
src/vs/platform/assignment/common/assignmentService.ts
Normal file
110
src/vs/platform/assignment/common/assignmentService.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { IExperimentationTelemetry, ExperimentationService as TASClient } from 'tas-client-umd';
|
||||
import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { AssignmentFilterProvider, ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, IAssignmentService, TargetPopulation } from 'vs/platform/assignment/common/assignment';
|
||||
|
||||
class AssignmentServiceTelemetry implements IExperimentationTelemetry {
|
||||
constructor(
|
||||
) { }
|
||||
|
||||
setSharedProperty(name: string, value: string): void {
|
||||
// noop due to lack of telemetry service
|
||||
}
|
||||
|
||||
postEvent(eventName: string, props: Map<string, string>): void {
|
||||
// noop due to lack of telemetry service
|
||||
}
|
||||
}
|
||||
|
||||
export class AssignmentService implements IAssignmentService {
|
||||
_serviceBrand: undefined;
|
||||
private tasClient: Promise<TASClient> | undefined;
|
||||
private telemetry: AssignmentServiceTelemetry | undefined;
|
||||
private networkInitialized = false;
|
||||
|
||||
private overrideInitDelay: Promise<void>;
|
||||
|
||||
private get experimentsEnabled(): boolean {
|
||||
return this.configurationService.getValue('workbench.enableExperiments') === true;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly machineId: string,
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
@IProductService private productService: IProductService
|
||||
) {
|
||||
|
||||
if (productService.tasConfig && this.experimentsEnabled && getTelemetryLevel(this.configurationService) === TelemetryLevel.USAGE) {
|
||||
this.tasClient = this.setupTASClient();
|
||||
}
|
||||
|
||||
// For development purposes, configure the delay until tas local tas treatment ovverrides are available
|
||||
const overrideDelaySetting = this.configurationService.getValue('experiments.overrideDelay');
|
||||
const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0;
|
||||
this.overrideInitDelay = new Promise(resolve => setTimeout(resolve, overrideDelay));
|
||||
}
|
||||
|
||||
async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
|
||||
// For development purposes, allow overriding tas assignments to test variants locally.
|
||||
await this.overrideInitDelay;
|
||||
const override = this.configurationService.getValue<T>('experiments.override.' + name);
|
||||
if (override !== undefined) {
|
||||
return override;
|
||||
}
|
||||
|
||||
if (!this.tasClient) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this.experimentsEnabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let result: T | undefined;
|
||||
const client = await this.tasClient;
|
||||
if (this.networkInitialized) {
|
||||
result = client.getTreatmentVariable<T>('vscode', name);
|
||||
} else {
|
||||
result = await client.getTreatmentVariableAsync<T>('vscode', name, true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async setupTASClient(): Promise<TASClient> {
|
||||
const targetPopulation = this.productService.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders;
|
||||
const machineId = this.machineId;
|
||||
const filterProvider = new AssignmentFilterProvider(
|
||||
this.productService.version,
|
||||
this.productService.nameLong,
|
||||
machineId,
|
||||
targetPopulation
|
||||
);
|
||||
|
||||
this.telemetry = new AssignmentServiceTelemetry();
|
||||
|
||||
const tasConfig = this.productService.tasConfig!;
|
||||
const tasClient = new (await import('tas-client-umd')).ExperimentationService({
|
||||
filterProviders: [filterProvider],
|
||||
telemetry: this.telemetry,
|
||||
storageKey: ASSIGNMENT_STORAGE_KEY,
|
||||
keyValueStorage: undefined,
|
||||
featuresTelemetryPropertyName: tasConfig.featuresTelemetryPropertyName,
|
||||
assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,
|
||||
telemetryEventName: tasConfig.telemetryEventName,
|
||||
endpoint: tasConfig.endpoint,
|
||||
refetchInterval: ASSIGNMENT_REFETCH_INTERVAL,
|
||||
});
|
||||
|
||||
await tasClient.initializePromise;
|
||||
|
||||
tasClient.initialFetch.then(() => this.networkInitialized = true);
|
||||
return tasClient;
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
|
|||
import { mixin } from 'vs/base/common/objects';
|
||||
import { ITelemetryAppender, validateTelemetryData } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
|
||||
async function getClient(aiKey: string): Promise<TelemetryClient> {
|
||||
async function getClient(aiKey: string, testCollector: boolean): Promise<TelemetryClient> {
|
||||
const appInsights = await import('applicationinsights');
|
||||
let client: TelemetryClient;
|
||||
if (appInsights.defaultClient) {
|
||||
|
@ -29,7 +29,7 @@ async function getClient(aiKey: string): Promise<TelemetryClient> {
|
|||
}
|
||||
|
||||
if (aiKey.indexOf('AIF-') === 0) {
|
||||
client.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1';
|
||||
client.config.endpointUrl = testCollector ? 'https://mobile.events.data.microsoft.com/collect/v1' : 'https://vortex.data.microsoft.com/collect/v1';
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
@ -44,6 +44,8 @@ export class AppInsightsAppender implements ITelemetryAppender {
|
|||
private _eventPrefix: string,
|
||||
private _defaultData: { [key: string]: any } | null,
|
||||
aiKeyOrClientFactory: string | (() => TelemetryClient), // allow factory function for testing
|
||||
private readonly testCollector?: boolean,
|
||||
private readonly mirrored?: boolean
|
||||
) {
|
||||
if (!this._defaultData) {
|
||||
this._defaultData = Object.create(null);
|
||||
|
@ -68,7 +70,7 @@ export class AppInsightsAppender implements ITelemetryAppender {
|
|||
}
|
||||
|
||||
if (!this._asyncAIClient) {
|
||||
this._asyncAIClient = getClient(this._aiClient);
|
||||
this._asyncAIClient = getClient(this._aiClient, this.testCollector ?? false);
|
||||
}
|
||||
|
||||
this._asyncAIClient.then(
|
||||
|
@ -89,6 +91,10 @@ export class AppInsightsAppender implements ITelemetryAppender {
|
|||
data = mixin(data, this._defaultData);
|
||||
data = validateTelemetryData(data);
|
||||
|
||||
if (this.testCollector) {
|
||||
data.properties['common.useragent'] = this.mirrored ? 'mirror-collector++' : 'collector++';
|
||||
}
|
||||
|
||||
this._withAIClient((aiClient) => aiClient.trackEvent({
|
||||
name: this._eventPrefix + '/' + eventName,
|
||||
properties: data.properties,
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import * as platform from 'vs/base/common/platform';
|
||||
import type { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client-umd';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import type { IKeyValueStorage, IExperimentationTelemetry, ExperimentationService as TASClient } from 'tas-client-umd';
|
||||
import { MementoObject, Memento } from 'vs/workbench/common/memento';
|
||||
import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
|
@ -12,18 +12,14 @@ import { ITelemetryData } from 'vs/base/common/actions';
|
|||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { AssignmentFilterProvider, ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, IAssignmentService, TargetPopulation } from 'vs/platform/assignment/common/assignment';
|
||||
|
||||
export const ITASExperimentService = createDecorator<ITASExperimentService>('TASExperimentService');
|
||||
|
||||
export interface ITASExperimentService {
|
||||
readonly _serviceBrand: undefined;
|
||||
getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined>;
|
||||
export interface ITASExperimentService extends IAssignmentService {
|
||||
getCurrentExperiments(): Promise<string[] | undefined>;
|
||||
}
|
||||
|
||||
const storageKey = 'VSCode.ABExp.FeatureData';
|
||||
const refetchInterval = 0; // no polling
|
||||
|
||||
class MementoKeyValueStorage implements IKeyValueStorage {
|
||||
private mementoObj: MementoObject;
|
||||
constructor(private memento: Memento) {
|
||||
|
@ -77,108 +73,6 @@ class ExperimentServiceTelemetry implements IExperimentationTelemetry {
|
|||
}
|
||||
}
|
||||
|
||||
class ExperimentServiceFilterProvider implements IExperimentationFilterProvider {
|
||||
constructor(
|
||||
private version: string,
|
||||
private appName: string,
|
||||
private machineId: string,
|
||||
private targetPopulation: TargetPopulation
|
||||
) { }
|
||||
|
||||
getFilterValue(filter: string): string | null {
|
||||
switch (filter) {
|
||||
case Filters.ApplicationVersion:
|
||||
return this.version; // productService.version
|
||||
case Filters.Build:
|
||||
return this.appName; // productService.nameLong
|
||||
case Filters.ClientId:
|
||||
return this.machineId;
|
||||
case Filters.Language:
|
||||
return platform.language;
|
||||
case Filters.ExtensionName:
|
||||
return 'vscode-core'; // always return vscode-core for exp service
|
||||
case Filters.TargetPopulation:
|
||||
return this.targetPopulation;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
getFilters(): Map<string, any> {
|
||||
let filters: Map<string, any> = new Map<string, any>();
|
||||
let filterValues = Object.values(Filters);
|
||||
for (let value of filterValues) {
|
||||
filters.set(value, this.getFilterValue(value));
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Based upon the official VSCode currently existing filters in the
|
||||
ExP backend for the VSCode cluster.
|
||||
https://experimentation.visualstudio.com/Analysis%20and%20Experimentation/_git/AnE.ExP.TAS.TachyonHost.Configuration?path=%2FConfigurations%2Fvscode%2Fvscode.json&version=GBmaster
|
||||
"X-MSEdge-Market": "detection.market",
|
||||
"X-FD-Corpnet": "detection.corpnet",
|
||||
"X-VSCode–AppVersion": "appversion",
|
||||
"X-VSCode-Build": "build",
|
||||
"X-MSEdge-ClientId": "clientid",
|
||||
"X-VSCode-ExtensionName": "extensionname",
|
||||
"X-VSCode-TargetPopulation": "targetpopulation",
|
||||
"X-VSCode-Language": "language"
|
||||
*/
|
||||
|
||||
enum Filters {
|
||||
/**
|
||||
* The market in which the extension is distributed.
|
||||
*/
|
||||
Market = 'X-MSEdge-Market',
|
||||
|
||||
/**
|
||||
* The corporation network.
|
||||
*/
|
||||
CorpNet = 'X-FD-Corpnet',
|
||||
|
||||
/**
|
||||
* Version of the application which uses experimentation service.
|
||||
*/
|
||||
ApplicationVersion = 'X-VSCode-AppVersion',
|
||||
|
||||
/**
|
||||
* Insiders vs Stable.
|
||||
*/
|
||||
Build = 'X-VSCode-Build',
|
||||
|
||||
/**
|
||||
* Client Id which is used as primary unit for the experimentation.
|
||||
*/
|
||||
ClientId = 'X-MSEdge-ClientId',
|
||||
|
||||
/**
|
||||
* Extension header.
|
||||
*/
|
||||
ExtensionName = 'X-VSCode-ExtensionName',
|
||||
|
||||
/**
|
||||
* The language in use by VS Code
|
||||
*/
|
||||
Language = 'X-VSCode-Language',
|
||||
|
||||
/**
|
||||
* The target population.
|
||||
* This is used to separate internal, early preview, GA, etc.
|
||||
*/
|
||||
TargetPopulation = 'X-VSCode-TargetPopulation',
|
||||
}
|
||||
|
||||
enum TargetPopulation {
|
||||
Team = 'team',
|
||||
Internal = 'internal',
|
||||
Insiders = 'insider',
|
||||
Public = 'public',
|
||||
}
|
||||
|
||||
export class ExperimentService implements ITASExperimentService {
|
||||
_serviceBrand: undefined;
|
||||
private tasClient: Promise<TASClient> | undefined;
|
||||
|
@ -274,7 +168,7 @@ export class ExperimentService implements ITASExperimentService {
|
|||
const telemetryInfo = await this.telemetryService.getTelemetryInfo();
|
||||
const targetPopulation = telemetryInfo.msftInternal ? TargetPopulation.Internal : (this.productService.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders);
|
||||
const machineId = telemetryInfo.machineId;
|
||||
const filterProvider = new ExperimentServiceFilterProvider(
|
||||
const filterProvider = new AssignmentFilterProvider(
|
||||
this.productService.version,
|
||||
this.productService.nameLong,
|
||||
machineId,
|
||||
|
@ -289,13 +183,13 @@ export class ExperimentService implements ITASExperimentService {
|
|||
const tasClient = new (await import('tas-client-umd')).ExperimentationService({
|
||||
filterProviders: [filterProvider],
|
||||
telemetry: this.telemetry,
|
||||
storageKey: storageKey,
|
||||
storageKey: ASSIGNMENT_STORAGE_KEY,
|
||||
keyValueStorage: keyValueStorage,
|
||||
featuresTelemetryPropertyName: tasConfig.featuresTelemetryPropertyName,
|
||||
assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,
|
||||
telemetryEventName: tasConfig.telemetryEventName,
|
||||
endpoint: tasConfig.endpoint,
|
||||
refetchInterval: refetchInterval,
|
||||
refetchInterval: ASSIGNMENT_REFETCH_INTERVAL,
|
||||
});
|
||||
|
||||
await tasClient.initializePromise;
|
||||
|
|
Loading…
Reference in a new issue