未验证 提交 361f7030 编写于 作者: R Ramya Rao 提交者: GitHub

Workspace recommendations based on telemetry (#42294)

上级 8b260fb6
......@@ -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; }; };
......
......@@ -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';
......@@ -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[];
......@@ -42,6 +46,11 @@ 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;
......@@ -49,9 +58,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
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(
......@@ -67,7 +77,8 @@ 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();
......@@ -75,6 +86,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
return;
}
if (product.extensionsGallery && product.extensionsGallery.recommendationsUrl) {
this._extensionsRecommendationsUrl = product.extensionsGallery.recommendationsUrl;
}
this.getDynamicWorkspaceRecommendations();
this._suggestFileBasedRecommendations();
this.promptWorkspaceRecommendationsPromise = this._suggestWorkspaceRecommendations();
......@@ -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;
}
......@@ -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[] {
......@@ -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] || [];
......
......@@ -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,
......@@ -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);
}
......
......@@ -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');
......@@ -90,15 +90,15 @@ suite('Telemetry - WorkspaceStats', () => {
});
test('Single remote hashed', function () {
assert.deepStrictEqual(getHashedRemotes(remote('https://username:password@github3.com/username/repository.git')), [hash('github3.com/username/repository.git')]);
assert.deepStrictEqual(getHashedRemotes(remote('ssh://user@git.server.org/project.git')), [hash('git.server.org/project.git')]);
assert.deepStrictEqual(getHashedRemotes(remote('user@git.server.org:project.git')), [hash('git.server.org/project.git')]);
assert.deepStrictEqual(getHashedRemotes(remote('/opt/git/project.git')), []);
assert.deepStrictEqual(getHashedRemotesFromConfig(remote('https://username:password@github3.com/username/repository.git')), [hash('github3.com/username/repository.git')]);
assert.deepStrictEqual(getHashedRemotesFromConfig(remote('ssh://user@git.server.org/project.git')), [hash('git.server.org/project.git')]);
assert.deepStrictEqual(getHashedRemotesFromConfig(remote('user@git.server.org: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 {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册