From d25ea61e0cdfef641b31deca1858af997ed8d5d4 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 22 Mar 2019 17:03:01 -0700 Subject: [PATCH] Add telemetry around localhost loading inside of webview This is needed to understand which extension should enable the new port forwarding feature. Only logs that some extension is accessing localhost in a webview, not which port/resource is accessed --- .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/electron-browser/mainThreadWebview.ts | 18 +++++++++-- .../api/node/extHostLanguageFeatures.ts | 2 +- .../webview/electron-browser/webviewEditor.ts | 8 ++--- .../electron-browser/webviewEditorInput.ts | 20 +++++++++--- .../webviewEditorInputFactory.ts | 11 +++++-- .../electron-browser/webviewEditorService.ts | 27 ++++++++++++---- .../electron-browser/webviewElement.ts | 32 ++++++++++++++++--- 8 files changed, 92 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 97e91c1f7d1..2590c31ace2 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -506,7 +506,7 @@ export interface WebviewPanelShowOptions { export interface MainThreadWebviewsShape extends IDisposable { $createWebviewPanel(handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void; - $createWebviewCodeInset(handle: WebviewInsetHandle, symbolId: string, options: modes.IWebviewOptions, extensionLocation: UriComponents | undefined): void; + $createWebviewCodeInset(handle: WebviewInsetHandle, symbolId: string, options: modes.IWebviewOptions, extensionId: ExtensionIdentifier | undefined, extensionLocation: UriComponents | undefined): void; $disposeWebview(handle: WebviewPanelHandle): void; $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void; $setTitle(handle: WebviewPanelHandle, value: string): void; diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index 16433f0a226..a9202f1f5e5 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -94,7 +94,10 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn); } - const webview = this._webviewService.createWebview(this.getInternalWebviewId(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), URI.revive(extensionLocation), this.createWebviewEventDelegate(handle)); + const webview = this._webviewService.createWebview(this.getInternalWebviewId(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), { + location: URI.revive(extensionLocation), + id: extensionId + }, this.createWebviewEventDelegate(handle)); webview.state = { viewType: viewType, state: undefined @@ -111,7 +114,13 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extensionId.value }); } - $createWebviewCodeInset(handle: WebviewInsetHandle, symbolId: string, options: IWebviewOptions, extensionLocation: UriComponents): void { + $createWebviewCodeInset( + handle: WebviewInsetHandle, + symbolId: string, + options: IWebviewOptions, + extensionId: ExtensionIdentifier, + extensionLocation: UriComponents + ): void { // todo@joh main is for the lack of a code-inset service // which we maybe wanna have... this is how it now works // 1) create webview element @@ -122,7 +131,10 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews WebviewElement, this._layoutService.getContainer(Parts.EDITOR_PART), { - extensionLocation: URI.revive(extensionLocation), + extension: { + location: URI.revive(extensionLocation), + id: extensionId + }, enableFindWidget: false, }, { diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index d99356d64d9..328f1b2b02c 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -1211,7 +1211,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { const webviewHandle = Math.random(); const webview = new ExtHostWebview(webviewHandle, this._webviewProxy, { enableScripts: true }); return this._withAdapter(handle, CodeInsetAdapter, async (adapter, extension) => { - await this._webviewProxy.$createWebviewCodeInset(webviewHandle, symbol.id, { enableCommandUris: true, enableScripts: true }, extension ? extension.extensionLocation : undefined); + await this._webviewProxy.$createWebviewCodeInset(webviewHandle, symbol.id, { enableCommandUris: true, enableScripts: true }, extension ? extension.identifier : undefined, extension ? extension.extensionLocation : undefined); return adapter.resolveCodeInset(symbol, webview, token); }, symbol); } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts index c99ae0e8e48..ff987ed90e2 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts @@ -257,9 +257,9 @@ export class WebviewEditor extends BaseEditor { private getDefaultLocalResourceRoots(): URI[] { const rootPaths = this._contextService.getWorkspace().folders.map(x => x.uri); - const extensionLocation = (this.input as WebviewEditorInput).extensionLocation; - if (extensionLocation) { - rootPaths.push(extensionLocation); + const extension = (this.input as WebviewEditorInput).extension; + if (extension) { + rootPaths.push(extension.location); } return rootPaths; } @@ -283,7 +283,7 @@ export class WebviewEditor extends BaseEditor { this._layoutService.getContainer(Parts.EDITOR_PART), { allowSvgs: true, - extensionLocation: input.extensionLocation, + extension: input.extension, enableFindWidget: input.options.enableFindWidget }, {}); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts index 93b7532ed29..791a9b9ca3c 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts @@ -7,6 +7,7 @@ import { Emitter } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { EditorInput, EditorModel, GroupIdentifier, IEditorInput } from 'vs/workbench/common/editor'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { WebviewEvents, WebviewInputOptions } from './webviewEditorService'; @@ -63,7 +64,10 @@ export class WebviewEditorInput extends EditorInput { private _scrollYPercentage: number = 0; private _state: any; - public readonly extensionLocation: URI | undefined; + public readonly extension?: { + readonly location: URI; + readonly id: ExtensionIdentifier; + }; private readonly _id: number; constructor( @@ -73,7 +77,10 @@ export class WebviewEditorInput extends EditorInput { options: WebviewInputOptions, state: any, events: WebviewEvents, - extensionLocation: URI | undefined, + extension: undefined | { + readonly location: URI; + readonly id: ExtensionIdentifier; + }, @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, ) { super(); @@ -89,7 +96,7 @@ export class WebviewEditorInput extends EditorInput { this._options = options; this._events = events; this._state = state; - this.extensionLocation = extensionLocation; + this.extension = extension; } public getTypeId(): string { @@ -313,11 +320,14 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput { options: WebviewInputOptions, state: any, events: WebviewEvents, - extensionLocation: URI | undefined, + extension: undefined | { + readonly location: URI; + readonly id: ExtensionIdentifier + }, public readonly reviver: (input: WebviewEditorInput) => Promise, @IWorkbenchLayoutService partService: IWorkbenchLayoutService, ) { - super(viewType, id, name, options, state, events, extensionLocation, partService); + super(viewType, id, name, options, state, events, extension, partService); } public async resolve(): Promise { diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory.ts index cd2b716c90a..d79b00c8037 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory.ts @@ -8,6 +8,7 @@ import { IEditorInputFactory } from 'vs/workbench/common/editor'; import { WebviewEditorInput } from './webviewEditorInput'; import { IWebviewEditorService, WebviewInputOptions } from './webviewEditorService'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; interface SerializedIconPath { light: string | UriComponents; @@ -20,6 +21,7 @@ interface SerializedWebview { readonly title: string; readonly options: WebviewInputOptions; readonly extensionLocation: string | UriComponents | undefined; + readonly extensionId: string | undefined; readonly state: any; readonly iconPath: SerializedIconPath | undefined; readonly group?: number; @@ -45,7 +47,8 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { id: input.getId(), title: input.getName(), options: input.options, - extensionLocation: input.extensionLocation, + extensionLocation: input.extension ? input.extension.location : undefined, + extensionId: input.extension ? input.extension.id.value : undefined, state: input.state, iconPath: input.iconPath ? { light: input.iconPath.light, dark: input.iconPath.dark, } : undefined, group: input.group @@ -64,8 +67,12 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { ): WebviewEditorInput { const data: SerializedWebview = JSON.parse(serializedEditorInput); const extensionLocation = reviveUri(data.extensionLocation); + const extensionId = data.extensionId ? new ExtensionIdentifier(data.extensionId) : undefined; const iconPath = reviveIconPath(data.iconPath); - return this._webviewService.reviveWebview(data.viewType, data.id, data.title, iconPath, data.state, data.options, extensionLocation, data.group); + return this._webviewService.reviveWebview(data.viewType, data.id, data.title, iconPath, data.state, data.options, extensionLocation ? { + location: extensionLocation, + id: extensionId + } : undefined, data.group); } } function reviveIconPath(data: SerializedIconPath | undefined) { diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts index 623eb976b40..8581f7b8975 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts @@ -7,12 +7,13 @@ import { equals } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { values } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; +import { IWebviewOptions, IWebviewPanelOptions } from 'vs/editor/common/modes'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { RevivedWebviewEditorInput, WebviewEditorInput } from './webviewEditorInput'; -import { IWebviewOptions, IWebviewPanelOptions } from 'vs/editor/common/modes'; export const IWebviewEditorService = createDecorator('webviewEditorService'); @@ -29,7 +30,10 @@ export interface IWebviewEditorService { title: string, showOptions: ICreateWebViewShowOptions, options: WebviewInputOptions, - extensionLocation: URI | undefined, + extension: undefined | { + location: URI, + id: ExtensionIdentifier + }, events: WebviewEvents ): WebviewEditorInput; @@ -40,7 +44,10 @@ export interface IWebviewEditorService { iconPath: { light: URI, dark: URI } | undefined, state: any, options: WebviewInputOptions, - extensionLocation: URI | undefined, + extension: undefined | { + readonly location: URI, + readonly id?: ExtensionIdentifier + }, group: number | undefined ): WebviewEditorInput; @@ -130,10 +137,13 @@ export class WebviewEditorService implements IWebviewEditorService { title: string, showOptions: ICreateWebViewShowOptions, options: IWebviewOptions, - extensionLocation: URI | undefined, + extension: undefined | { + location: URI, + id: ExtensionIdentifier + }, events: WebviewEvents ): WebviewEditorInput { - const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, undefined, title, options, {}, events, extensionLocation); + const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, undefined, title, options, {}, events, extension); this._editorService.openEditor(webviewInput, { pinned: true, preserveFocus: showOptions.preserveFocus }, showOptions.group); return webviewInput; } @@ -160,10 +170,13 @@ export class WebviewEditorService implements IWebviewEditorService { iconPath: { light: URI, dark: URI } | undefined, state: any, options: WebviewInputOptions, - extensionLocation: URI, + extension: undefined | { + readonly location: URI, + readonly id?: ExtensionIdentifier + }, group: number | undefined, ): WebviewEditorInput { - const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, viewType, id, title, options, state, {}, extensionLocation, async (webview: WebviewEditorInput): Promise => { + const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, viewType, id, title, options, state, {}, extension, async (webview: WebviewEditorInput): Promise => { const didRevive = await this.tryRevive(webview); if (didRevive) { return Promise.resolve(undefined); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index d6dc03875ec..3e56ee15843 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -19,6 +19,8 @@ import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/the import { registerFileProtocol, WebviewProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols'; import { areWebviewInputOptionsEqual } from './webviewEditorService'; import { WebviewFindWidget } from './webviewFindWidget'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export interface WebviewPortMapping { readonly port: number; @@ -32,7 +34,10 @@ export interface WebviewPortMapping { export interface WebviewOptions { readonly allowSvgs?: boolean; - readonly extensionLocation?: URI; + readonly extension?: { + readonly location: URI; + readonly id?: ExtensionIdentifier; + }; readonly enableFindWidget?: boolean; } @@ -145,10 +150,14 @@ class WebviewPortMappingProvider extends Disposable { constructor( session: WebviewSession, - mappings: () => ReadonlyArray + mappings: () => ReadonlyArray, + extensionId: ExtensionIdentifier | undefined, + @ITelemetryService telemetryService: ITelemetryService, ) { super(); + let hasLogged = false; + session.onBeforeRequest(async (details) => { const uri = URI.parse(details.url); if (uri.scheme !== 'http' && uri.scheme !== 'https') { @@ -157,6 +166,17 @@ class WebviewPortMappingProvider extends Disposable { const localhostMatch = /^localhost:(\d+)$/.exec(uri.authority); if (localhostMatch) { + if (!hasLogged && extensionId) { + hasLogged = true; + + /* __GDPR__ + "webview.accessLocalhost" : { + "extension" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + telemetryService.publicLog('webview.accessLocalhost', { extension: extensionId.value }); + } + const port = +localhostMatch[1]; for (const mapping of mappings()) { if (mapping.port === port && mapping.port !== mapping.resolvedPort) { @@ -317,6 +337,7 @@ export class WebviewElement extends Disposable { @IThemeService themeService: IThemeService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, + @ITelemetryService telemetryService: ITelemetryService, ) { super(); this._webview = document.createElement('webview'); @@ -348,15 +369,16 @@ export class WebviewElement extends Disposable { this._register(new WebviewProtocolProvider( this._webview, - this._options.extensionLocation, + this._options.extension ? this._options.extension.location : undefined, () => (this._contentOptions.localResourceRoots || []), environmentService, fileService)); this._register(new WebviewPortMappingProvider( session, - () => (this._contentOptions.portMappings || []) - )); + () => (this._contentOptions.portMappings || []), + _options.extension ? _options.extension.id : undefined, + telemetryService)); if (!this._options.allowSvgs) {