Skip to content

Commit

Permalink
Update welcome page to support featured extension experiments
Browse files Browse the repository at this point in the history
  • Loading branch information
bhavyaus committed Mar 2, 2023
1 parent 7e5c00f commit 78afd61
Show file tree
Hide file tree
Showing 5 changed files with 436 additions and 13 deletions.
25 changes: 25 additions & 0 deletions src/vs/base/common/product.ts
Expand Up @@ -13,6 +13,29 @@ export interface IBuiltInExtension {
readonly metadata: any;
}

export interface IProductWalkthrough {
id: string;
steps: IProductWalkthroughStep[];
}

export interface IProductWalkthroughStep {
id: string;
title: string;
when: string;
description: string;
media:
| { type: 'image'; path: string | { hc: string; hcLight?: string; light: string; dark: string }; altText: string }
| { type: 'svg'; path: string; altText: string }
| { type: 'markdown'; path: string };
}

export interface IFeaturedExtension {
readonly id: string;
readonly title: string;
readonly description: string;
readonly imagePath: string;
}

export type ConfigurationSyncStore = {
url: string;
insidersUrl: string;
Expand Down Expand Up @@ -50,6 +73,8 @@ export interface IProductConfiguration {
readonly dataFolderName: string; // location for extensions (e.g. ~/.vscode-insiders)

readonly builtInExtensions?: IBuiltInExtension[];
readonly walkthroughMetadata?: IProductWalkthrough[];
readonly featuredExtensions?: IFeaturedExtension[];

readonly downloadUrl?: string;
readonly updateUrl?: string;
Expand Down
@@ -0,0 +1,200 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Disposable } from 'vs/base/common/lifecycle';
import { IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IProductService } from 'vs/platform/product/common/productService';
import { IFeaturedExtension } from 'vs/base/common/product';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { localize } from 'vs/nls';
import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';

type FeaturedExtensionTreatment = { extensions: string[]; showAsList?: string };
type FeaturedExtensionStorageData = { title: string; description: string; imagePath: string; date: number };

export const IFeaturedExtensionsService = createDecorator<IFeaturedExtensionsService>('featuredExtensionsService');

export interface IFeaturedExtensionsService {
_serviceBrand: undefined;

getExtensions(): Promise<IFeaturedExtension[]>;
title: string;
}

const enum FeaturedExtensionMetadataType {
Title,
Description,
ImagePath
}

export class FeaturedExtensionsService extends Disposable implements IFeaturedExtensionsService {
declare readonly _serviceBrand: undefined;

private ignoredExtensions: Set<string> = new Set<string>();
private treatment: FeaturedExtensionTreatment | undefined;
private _isInitialized: boolean = false;

private static readonly STORAGE_KEY = 'workbench.welcomePage.extensionMetadata';

constructor(
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IWorkbenchAssignmentService private readonly tasExperimentService: IWorkbenchAssignmentService,
@IStorageService private readonly storageService: IStorageService,
@IProductService private readonly productService: IProductService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
) {
super();
this.title = localize('gettingStarted.featuredTitle', 'Featured');
}

title: string;

async getExtensions(): Promise<IFeaturedExtension[]> {

await this._init();

let treatments = this.treatment?.extensions.filter(extension => !this.ignoredExtensions.has(extension)) ?? new Array<string>();
const featuredExtensions: IFeaturedExtension[] = new Array();

if (this.treatment?.showAsList !== 'true' && treatments.length > 0) {
// pick a random extensionId for display
const treatment = treatments[Math.floor(Math.random() * treatments.length)];
treatments = [treatment];
}

for (const treatment of treatments) {
const extension = await this.resolveExtension(treatment);
if (extension) {
featuredExtensions.push(extension);
}
}

return featuredExtensions;
}

private async _init(): Promise<void> {

if (this._isInitialized) {
return;
}

const extensions = await Promise.race([
this.tasExperimentService?.getTreatment<string>('welcome.featured.item'),
new Promise<string | undefined>(resolve => setTimeout(() => resolve(''), 2000))
]);

const extensionListTitle = await Promise.race([
this.tasExperimentService?.getTreatment<string>('welcome.featured.title'),
new Promise<string | undefined>(resolve => setTimeout(() => resolve(''), 2000))
]);

this.treatment = extensions ? JSON.parse(extensions) : { extensions: [] };
this.title = extensionListTitle ?? localize('gettingStarted.featuredTitle', 'Featured');

if (this.treatment) {
const installed = await this.extensionManagementService.getInstalled();

for (const extension of this.treatment.extensions) {
if (installed.some(e => ExtensionIdentifier.equals(e.identifier.id, extension))) {
this.ignoredExtensions.add(extension);
}
}
}

this._isInitialized = true;
}

private async resolveExtension(extensionId: string): Promise<IFeaturedExtension | undefined> {

const productMetadata = this.productService.featuredExtensions?.find(e => ExtensionIdentifier.equals(e.id, extensionId));

const title = productMetadata?.title ?? await this.getMetadata(extensionId, FeaturedExtensionMetadataType.Title);
const description = productMetadata?.description ?? await this.getMetadata(extensionId, FeaturedExtensionMetadataType.Description);
const imagePath = productMetadata?.imagePath ?? await this.getMetadata(extensionId, FeaturedExtensionMetadataType.ImagePath);

if (title && description && imagePath) {
return {
id: extensionId,
title: title,
description: description,
imagePath: imagePath,
};
}
return undefined;
}

private async getMetadata(extensionId: string, key: FeaturedExtensionMetadataType): Promise<string | undefined> {

const storageMetadata = this.getStorageData(extensionId);
if (storageMetadata) {
switch (key) {
case FeaturedExtensionMetadataType.Title: {
return storageMetadata.title;
}
case FeaturedExtensionMetadataType.Description: {
return storageMetadata.description;
}
case FeaturedExtensionMetadataType.ImagePath: {
return storageMetadata.imagePath;
}
default:
return undefined;
}
}

return await this.getGalleryMetadata(extensionId, key);
}

private getStorageData(extensionId: string): FeaturedExtensionStorageData | undefined {
const metadata = this.storageService.get(FeaturedExtensionsService.STORAGE_KEY + '.' + extensionId, StorageScope.APPLICATION);
if (metadata) {
const value = JSON.parse(metadata) as FeaturedExtensionStorageData;
const lastUpdateDate = new Date().getTime() - value.date;
if (lastUpdateDate < 1000 * 60 * 60 * 24 * 7) {
return value;
}
}
return undefined;
}

private async getGalleryMetadata(extensionId: string, key: FeaturedExtensionMetadataType): Promise<string | undefined> {

const storageKey = FeaturedExtensionsService.STORAGE_KEY + '.' + extensionId;
this.storageService.remove(storageKey, StorageScope.APPLICATION);

const galleryExtension = (await this.galleryService.getExtensions([{ id: extensionId }], CancellationToken.None))[0];
let metadata: string | undefined;
if (galleryExtension) {
switch (key) {
case FeaturedExtensionMetadataType.Title: {
metadata = galleryExtension.displayName;
break;
}
case FeaturedExtensionMetadataType.Description: {
metadata = galleryExtension.description;
break;
}
case FeaturedExtensionMetadataType.ImagePath: {
metadata = galleryExtension.assets.icon?.uri;
break;
}
}

this.storageService.store(storageKey, JSON.stringify({
title: galleryExtension.displayName,
description: galleryExtension.description,
imagePath: galleryExtension.assets.icon?.uri,
date: new Date().getTime()
}), StorageScope.APPLICATION, StorageTarget.MACHINE);
}
return metadata;
}
}

registerSingleton(IFeaturedExtensionsService, FeaturedExtensionsService, InstantiationType.Delayed);

0 comments on commit 78afd61

Please sign in to comment.