Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/browserView/common/browserView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from '../../../../platform/browserView/common/browserView.js';
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { isLocalhost } from '../../../../platform/tunnel/common/tunnel.js';
import { isLocalhostAuthority } from '../../../../platform/url/common/trustedDomains.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';

Expand Down Expand Up @@ -309,7 +309,7 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel {
private logNavigationTelemetry(navigationType: IntegratedBrowserNavigationEvent['navigationType'], url: string): void {
let localhost: boolean;
try {
localhost = isLocalhost(new URL(url).hostname);
localhost = isLocalhostAuthority(new URL(url).host);
} catch {
localhost = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { Codicon } from '../../../../base/common/codicons.js';
import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js';
import { IChatRequestVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js';
import { IBrowserTargetLocator, getDisplayNameFromOuterHTML } from '../../../../platform/browserElements/common/browserElements.js';
import { logBrowserOpen } from './browserViewTelemetry.js';

export const CONTEXT_BROWSER_CAN_GO_BACK = new RawContextKey<boolean>('browserCanGoBack', false, localize('browser.canGoBack', "Whether the browser can go back"));
export const CONTEXT_BROWSER_CAN_GO_FORWARD = new RawContextKey<boolean>('browserCanGoForward', false, localize('browser.canGoForward', "Whether the browser can go forward"));
Expand Down Expand Up @@ -337,22 +338,7 @@ export class BrowserEditor extends EditorPane {
}));

this._inputDisposables.add(this._model.onDidRequestNewPage(({ url, name, background }) => {
type IntegratedBrowserNewPageRequestEvent = {
background: boolean;
};

type IntegratedBrowserNewPageRequestClassification = {
background: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether page was requested to open in background' };
owner: 'kycutler';
comment: 'Tracks new page requests from integrated browser';
};

this.telemetryService.publicLog2<IntegratedBrowserNewPageRequestEvent, IntegratedBrowserNewPageRequestClassification>(
'integratedBrowser.newPageRequest',
{
background
}
);
logBrowserOpen(this.telemetryService, background ? 'browserLinkBackground' : 'browserLinkForeground');

// Open a new browser tab for the requested URL
const browserUri = BrowserViewUri.forUrl(url, name ? `${input.id}-${name}` : undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { IBrowserViewWorkbenchService, IBrowserViewModel } from '../common/brows
import { hasKey } from '../../../../base/common/types.js';
import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js';
import { BrowserEditor } from './browserEditor.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { logBrowserOpen } from './browserViewTelemetry.js';

const LOADING_SPINNER_SVG = (color: string | undefined) => `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
Expand Down Expand Up @@ -58,7 +60,8 @@ export class BrowserEditorInput extends EditorInput {
@IThemeService private readonly themeService: IThemeService,
@IBrowserViewWorkbenchService private readonly browserViewWorkbenchService: IBrowserViewWorkbenchService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IInstantiationService private readonly instantiationService: IInstantiationService
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ITelemetryService private readonly telemetryService: ITelemetryService
) {
super();
this._id = options.id;
Expand Down Expand Up @@ -212,6 +215,8 @@ export class BrowserEditorInput extends EditorInput {
* This is used during Copy into New Window.
*/
override copy(): EditorInput {
logBrowserOpen(this.telemetryService, 'copyToNewWindow');

const currentUrl = this._model?.url ?? this._initialData.url;
return this.instantiationService.createInstance(BrowserEditorInput, {
id: generateUuid(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ import { Schemas } from '../../../../base/common/network.js';
import { IBrowserViewWorkbenchService } from '../common/browserView.js';
import { BrowserViewWorkbenchService } from './browserViewWorkbenchService.js';
import { BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js';
import { IOpenerService, IOpener, OpenInternalOptions, OpenExternalOptions } from '../../../../platform/opener/common/opener.js';
import { isLocalhostAuthority } from '../../../../platform/url/common/trustedDomains.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { URI } from '../../../../base/common/uri.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { logBrowserOpen } from './browserViewTelemetry.js';

// Register actions
import './browserViewActions.js';
Expand Down Expand Up @@ -90,11 +98,64 @@ class BrowserEditorResolverContribution implements IWorkbenchContribution {

registerWorkbenchContribution2(BrowserEditorResolverContribution.ID, BrowserEditorResolverContribution, WorkbenchPhase.BlockStartup);

/**
* Opens localhost URLs in the Integrated Browser when the setting is enabled.
*/
class LocalhostLinkOpenerContribution extends Disposable implements IWorkbenchContribution, IOpener {
static readonly ID = 'workbench.contrib.localhostLinkOpener';

constructor(
@IOpenerService openerService: IOpenerService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
@ITelemetryService private readonly telemetryService: ITelemetryService
) {
super();

this._register(openerService.registerOpener(this));
}

async open(resource: URI | string, _options?: OpenInternalOptions | OpenExternalOptions): Promise<boolean> {
if (!this.configurationService.getValue<boolean>('workbench.browser.openLocalhostLinks')) {
return false;
}

const url = typeof resource === 'string' ? resource : resource.toString(true);
try {
const parsed = new URL(url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return false;
}
if (!isLocalhostAuthority(parsed.host)) {
return false;
}
} catch {
return false;
}

logBrowserOpen(this.telemetryService, 'localhostLinkOpener');

const browserUri = BrowserViewUri.forUrl(url);
await this.editorService.openEditor({ resource: browserUri, options: { pinned: true } });
return true;
}
}

registerWorkbenchContribution2(LocalhostLinkOpenerContribution.ID, LocalhostLinkOpenerContribution, WorkbenchPhase.BlockStartup);

registerSingleton(IBrowserViewWorkbenchService, BrowserViewWorkbenchService, InstantiationType.Delayed);

Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
...workbenchConfigurationNodeBase,
properties: {
'workbench.browser.openLocalhostLinks': {
type: 'boolean',
default: false,
markdownDescription: localize(
{ comment: ['This is the description for a setting.'], key: 'browser.openLocalhostLinks' },
'When enabled, localhost links from the terminal, chat, and other sources will open in the Integrated Browser instead of the system browser.'
)
},
'workbench.browser.dataStorage': {
type: 'string',
enum: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { BrowserViewStorageScope } from '../../../../platform/browserView/common
import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { logBrowserOpen } from './browserViewTelemetry.js';

// Context key expression to check if browser editor is active
const BROWSER_EDITOR_ACTIVE = ContextKeyExpr.equals('activeEditor', BrowserEditor.ID);
Expand All @@ -36,8 +38,11 @@ class OpenIntegratedBrowserAction extends Action2 {

async run(accessor: ServicesAccessor, url?: string): Promise<void> {
const editorService = accessor.get(IEditorService);
const telemetryService = accessor.get(ITelemetryService);
const resource = BrowserViewUri.forUrl(url);

logBrowserOpen(telemetryService, url ? 'commandWithUrl' : 'commandWithoutUrl');

await editorService.openEditor({ resource });
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';

/**
* Source of an Integrated Browser open event.
*
* - `'commandWithoutUrl'`: opened via the "Open Integrated Browser" command without a URL argument.
* This typically means the user ran the command manually from the Command Palette.
* - `'commandWithUrl'`: opened via the "Open Integrated Browser" command with a URL argument.
* This typically means another extension or component invoked the command programmatically.
* - `'localhostLinkOpener'`: opened via the localhost link opener when the
* `workbench.browser.openLocalhostLinks` setting is enabled. This happens when clicking
* localhost links from the terminal, chat, or other sources.
* - `'browserLinkForeground'`: opened when clicking a link inside the Integrated Browser that
* opens in a new focused editor (e.g., links with target="_blank").
* - `'browserLinkBackground'`: opened when clicking a link inside the Integrated Browser that
* opens in a new background editor (e.g., Ctrl/Cmd+click).
* - `'copyToNewWindow'`: opened when the user copies a browser editor to a new window
* via "Copy into New Window".
*/
export type IntegratedBrowserOpenSource = 'commandWithoutUrl' | 'commandWithUrl' | 'localhostLinkOpener' | 'browserLinkForeground' | 'browserLinkBackground' | 'copyToNewWindow';

type IntegratedBrowserOpenEvent = {
source: IntegratedBrowserOpenSource;
};

type IntegratedBrowserOpenClassification = {
source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the Integrated Browser was opened' };
owner: 'jruales';
comment: 'Tracks how users open the Integrated Browser';
};

export function logBrowserOpen(telemetryService: ITelemetryService, source: IntegratedBrowserOpenSource): void {
telemetryService.publicLog2<IntegratedBrowserOpenEvent, IntegratedBrowserOpenClassification>(
'integratedBrowser.open',
{ source }
);
}
Loading