Skip to content

Commit

Permalink
Workspace recommendations based on telemetry (#42294)
Browse files Browse the repository at this point in the history
  • Loading branch information
ramya-rao-a authored Jan 30, 2018
1 parent 8b260fb commit 361f703
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 22 deletions.
1 change: 1 addition & 0 deletions src/vs/platform/node/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface IProductConfiguration {
serviceUrl: string;
itemUrl: string;
controlUrl: string;
recommendationsUrl: string;
};
extensionTips: { [id: string]: string; };
extensionImportantTips: { [id: string]: { name: string; pattern: string; }; };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { IChoiceService, IMessageService } from 'vs/platform/message/common/mess
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction, InstallRecommendedExtensionAction } from 'vs/workbench/parts/extensions/browser/extensionsActions';
import Severity from 'vs/base/common/severity';
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { Schemas } from 'vs/base/common/network';
import { IFileService } from 'vs/platform/files/common/files';
import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/parts/extensions/common/extensions';
Expand All @@ -32,6 +32,10 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
import { guessMimeTypes, MIME_UNKNOWN } from 'vs/base/common/mime';
import { ShowLanguageExtensionsAction } from 'vs/workbench/browser/parts/editor/editorStatus';
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { getHashedRemotesFromUri } from 'vs/workbench/parts/stats/node/workspaceStats';
import { IRequestService } from 'vs/platform/request/node/request';
import { asJson } from 'vs/base/node/request';
import { isNumber } from 'vs/base/common/types';

interface IExtensionsContent {
recommendations: string[];
Expand All @@ -42,16 +46,22 @@ const milliSecondsInADay = 1000 * 60 * 60 * 24;
const choiceNever = localize('neverShowAgain', "Don't show again");
const choiceClose = localize('close', "Close");

interface IDynamicWorkspaceRecommendations {
remoteSet: string[];
recommendations: string[];
}

export class ExtensionTipsService extends Disposable implements IExtensionTipsService {

_serviceBrand: any;

private _fileBasedRecommendations: { [id: string]: number; } = Object.create(null);
private _exeBasedRecommendations: { [id: string]: string; } = Object.create(null);
private _availableRecommendations: { [pattern: string]: string[] } = Object.create(null);
private _disposables: IDisposable[] = [];

private _allWorkspaceRecommendedExtensions: string[] = [];
private _dynamicWorkspaceRecommendations: string[] = [];
private _extensionsRecommendationsUrl: string;
private _disposables: IDisposable[] = [];
public promptWorkspaceRecommendationsPromise: TPromise<any>;

constructor(
Expand All @@ -67,14 +77,19 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
@IMessageService private messageService: IMessageService,
@ITelemetryService private telemetryService: ITelemetryService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IExtensionService private extensionService: IExtensionService
@IExtensionService private extensionService: IExtensionService,
@IRequestService private requestService: IRequestService
) {
super();

if (!this.isEnabled()) {
return;
}

if (product.extensionsGallery && product.extensionsGallery.recommendationsUrl) {
this._extensionsRecommendationsUrl = product.extensionsGallery.recommendationsUrl;
}
this.getDynamicWorkspaceRecommendations();
this._suggestFileBasedRecommendations();

this.promptWorkspaceRecommendationsPromise = this._suggestWorkspaceRecommendations();
Expand All @@ -94,6 +109,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
this._allWorkspaceRecommendedExtensions.forEach(x => output[x.toLowerCase()] = localize('workspaceRecommendation', "This extension is recommended by users of the current workspace."));
Object.keys(this._fileBasedRecommendations).forEach(x => output[x.toLowerCase()] = output[x.toLowerCase()] || localize('fileBasedRecommendation', "This extension is recommended based on the files you recently opened."));
forEach(this._exeBasedRecommendations, entry => output[entry.key.toLowerCase()] = output[entry.key.toLowerCase()] || localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", entry.value));
this._dynamicWorkspaceRecommendations.forEach(x => output[x.toLowerCase()] = output[x.toLowerCase()] || localize('dynamicWorkspaceRecommendation', "This extension might interest you because many other users of the current workspace use it."));
return output;
}

Expand Down Expand Up @@ -202,7 +218,13 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
}

getOtherRecommendations(): string[] {
return Object.keys(this._exeBasedRecommendations);
if (!this._dynamicWorkspaceRecommendations || !this._dynamicWorkspaceRecommendations.length) {
return Object.keys(this._exeBasedRecommendations);
}
const coinToss = Math.round(Math.random());
return distinct(coinToss
? [...Object.keys(this._exeBasedRecommendations), ...this._dynamicWorkspaceRecommendations]
: [...this._dynamicWorkspaceRecommendations, ...Object.keys(this._exeBasedRecommendations)]);
}

getKeymapRecommendations(): string[] {
Expand Down Expand Up @@ -620,6 +642,70 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
}
}

private getDynamicWorkspaceRecommendations(): TPromise<void> {
if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER) {
return TPromise.as(null);
}

const storageKey = 'extensionsAssistant/dynamicWorkspaceRecommendations';
let storedRecommendationsJson = {};
try {
storedRecommendationsJson = JSON.parse(this.storageService.get(storageKey, StorageScope.WORKSPACE, '{}'));
} catch (e) {
this.storageService.remove(storageKey, StorageScope.WORKSPACE);
}

if (Array.isArray(storedRecommendationsJson['recommendations'])
&& isNumber(storedRecommendationsJson['timestamp'])
&& storedRecommendationsJson['timestamp'] > 0
&& (Date.now() - storedRecommendationsJson['timestamp']) / milliSecondsInADay < 14) {
this._dynamicWorkspaceRecommendations = storedRecommendationsJson['recommendations'];
return TPromise.as(null);
}

if (!this._extensionsRecommendationsUrl) {
return TPromise.as(null);
}

return getHashedRemotesFromUri(this.contextService.getWorkspace().folders[0].uri, this.fileService).then(hashedRemotes => {
if (!hashedRemotes || !hashedRemotes.length) {
return null;
}

return new TPromise((c, e) => {
setTimeout(() => {
this.requestService.request({ type: 'GET', url: this._extensionsRecommendationsUrl }).then(context => {
if (context.res.statusCode !== 200) {
return c(null);
}
return asJson(context).then((result) => {
const allRecommendations: IDynamicWorkspaceRecommendations[] = Array.isArray(result['workspaceRecommendations']) ? result['workspaceRecommendations'] : [];
if (!allRecommendations.length) {
return c(null);
}

let foundRemote = false;
for (let i = 0; i < hashedRemotes.length && !foundRemote; i++) {
for (let j = 0; j < allRecommendations.length && !foundRemote; j++) {
if (Array.isArray(allRecommendations[j].remoteSet) && allRecommendations[j].remoteSet.indexOf(hashedRemotes[i]) > -1) {
foundRemote = true;
this._dynamicWorkspaceRecommendations = allRecommendations[j].recommendations || [];
this.storageService.store(storageKey, JSON.stringify({
recommendations: this._dynamicWorkspaceRecommendations,
timestamp: Date.now()
}), StorageScope.WORKSPACE);
}
}
}

return c(null);
});
});
}, 10000);
});
});
}

getKeywordsForExtension(extension: string): string[] {
const keywords = product.extensionKeywords || {};
return keywords[extension] || [];
Expand Down
26 changes: 15 additions & 11 deletions src/vs/workbench/parts/stats/node/workspaceStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,21 @@ export function getRemotes(text: string): string[] {
return remotes;
}

export function getHashedRemotes(text: string): string[] {
export function getHashedRemotesFromConfig(text: string): string[] {
return getRemotes(text).map(r => {
return crypto.createHash('sha1').update(r).digest('hex');
});
}

export function getHashedRemotesFromUri(workspaceUri: URI, fileService: IFileService): TPromise<string[]> {
let path = workspaceUri.path;
let uri = workspaceUri.with({ path: `${path !== '/' ? path : ''}/.git/config` });
return fileService.resolveContent(uri, { acceptTextOnly: true }).then(
content => getHashedRemotesFromConfig(content.value),
err => [] // ignore missing or binary file
);
}

export class WorkspaceStats implements IWorkbenchContribution {
constructor(
@IFileService private fileService: IFileService,
Expand Down Expand Up @@ -329,18 +338,13 @@ export class WorkspaceStats implements IWorkbenchContribution {

private reportRemotes(workspaceUris: URI[]): void {
TPromise.join<string[]>(workspaceUris.map(workspaceUri => {
let path = workspaceUri.path;
let uri = workspaceUri.with({ path: `${path !== '/' ? path : ''}/.git/config` });
return this.fileService.resolveContent(uri, { acceptTextOnly: true }).then(
content => getHashedRemotes(content.value),
err => [] // ignore missing or binary file
);
return getHashedRemotesFromUri(workspaceUri, this.fileService);
})).then(hashedRemotes => {
/* __GDPR__
"workspace.hashedRemotes" : {
"remotes" : { "classification": "CustomerContent", "purpose": "FeatureInsight" }
}
*/
"workspace.hashedRemotes" : {
"remotes" : { "classification": "CustomerContent", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workspace.hashedRemotes', { remotes: hashedRemotes });
}, onUnexpectedError);
}
Expand Down
12 changes: 6 additions & 6 deletions src/vs/workbench/parts/stats/test/workspaceStats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import * as assert from 'assert';
import * as crypto from 'crypto';
import { getDomainsOfRemotes, getRemotes, getHashedRemotes } from 'vs/workbench/parts/stats/node/workspaceStats';
import { getDomainsOfRemotes, getRemotes, getHashedRemotesFromConfig } from 'vs/workbench/parts/stats/node/workspaceStats';

function hash(value: string): string {
return crypto.createHash('sha1').update(value.toString()).digest('hex');
Expand Down Expand Up @@ -90,15 +90,15 @@ suite('Telemetry - WorkspaceStats', () => {
});

test('Single remote hashed', function () {
assert.deepStrictEqual(getHashedRemotes(remote('https://username:[email protected]/username/repository.git')), [hash('github3.com/username/repository.git')]);
assert.deepStrictEqual(getHashedRemotes(remote('ssh://[email protected]/project.git')), [hash('git.server.org/project.git')]);
assert.deepStrictEqual(getHashedRemotes(remote('[email protected]:project.git')), [hash('git.server.org/project.git')]);
assert.deepStrictEqual(getHashedRemotes(remote('/opt/git/project.git')), []);
assert.deepStrictEqual(getHashedRemotesFromConfig(remote('https://username:[email protected]/username/repository.git')), [hash('github3.com/username/repository.git')]);
assert.deepStrictEqual(getHashedRemotesFromConfig(remote('ssh://[email protected]/project.git')), [hash('git.server.org/project.git')]);
assert.deepStrictEqual(getHashedRemotesFromConfig(remote('[email protected]:project.git')), [hash('git.server.org/project.git')]);
assert.deepStrictEqual(getHashedRemotesFromConfig(remote('/opt/git/project.git')), []);
});

test('Multiple remotes hashed', function () {
const config = ['https://github.com/Microsoft/vscode.git', 'https://git.example.com/gitproject.git'].map(remote).join(' ');
assert.deepStrictEqual(getHashedRemotes(config), [hash('github.com/Microsoft/vscode.git'), hash('git.example.com/gitproject.git')]);
assert.deepStrictEqual(getHashedRemotesFromConfig(config), [hash('github.com/Microsoft/vscode.git'), hash('git.example.com/gitproject.git')]);
});

function remote(url: string): string {
Expand Down

0 comments on commit 361f703

Please sign in to comment.