diff --git a/src/vs/sessions/common/sessionsTelemetry.ts b/src/vs/sessions/common/sessionsTelemetry.ts new file mode 100644 index 0000000000000..0b2974d9b52c5 --- /dev/null +++ b/src/vs/sessions/common/sessionsTelemetry.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +// --- Titlebar button interactions --- + +export type SessionsInteractionButton = + | 'newSession' + | 'runPrimaryTask' + | 'addTask' + | 'generateNewTask' + | 'openTerminal' + | 'openInVSCode'; + +type SessionsInteractionEvent = { + button: string; +}; + +type SessionsInteractionClassification = { + owner: 'osortega'; + comment: 'Tracks user interactions with buttons in the Agents window'; + button: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the button that was clicked' }; +}; + +/** + * Log a titlebar button interaction in the Agents window. + */ +export function logSessionsInteraction(telemetryService: ITelemetryService, button: SessionsInteractionButton): void { + telemetryService.publicLog2('vscodeAgents.interaction', { button }); +} + +// --- Changes panel interactions --- + +type ChangesViewTogglePanelEvent = { + visible: boolean; +}; + +type ChangesViewTogglePanelClassification = { + owner: 'osortega'; + comment: 'Tracks when the user toggles the Changes panel open or closed.'; + visible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the Changes panel is now visible.' }; +}; + +export function logChangesViewToggle(telemetryService: ITelemetryService, visible: boolean): void { + telemetryService.publicLog2('vscodeAgents.changesView/togglePanel', { visible }); +} + +type ChangesViewVersionModeChangeEvent = { + mode: string; +}; + +type ChangesViewVersionModeChangeClassification = { + owner: 'osortega'; + comment: 'Tracks when the user switches the version mode in the Changes panel (Branch Changes, All Changes, Last Turn).'; + mode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version mode selected by the user.' }; +}; + +export function logChangesViewVersionModeChange(telemetryService: ITelemetryService, mode: string): void { + telemetryService.publicLog2('vscodeAgents.changesView/versionModeChange', { mode }); +} + +type ChangesViewFileSelectEvent = { + changeType: string; +}; + +type ChangesViewFileSelectClassification = { + owner: 'osortega'; + comment: 'Tracks when the user selects a changed file in the Changes panel.'; + changeType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of change (added, modified, deleted).' }; +}; + +export function logChangesViewFileSelect(telemetryService: ITelemetryService, changeType: string): void { + telemetryService.publicLog2('vscodeAgents.changesView/fileSelect', { changeType }); +} + +type ChangesViewViewModeChangeEvent = { + mode: string; +}; + +type ChangesViewViewModeChangeClassification = { + owner: 'osortega'; + comment: 'Tracks when the user switches between list and tree view modes in the Changes panel.'; + mode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The view mode selected by the user (list or tree).' }; +}; + +export function logChangesViewViewModeChange(telemetryService: ITelemetryService, mode: string): void { + telemetryService.publicLog2('vscodeAgents.changesView/viewModeChange', { mode }); +} + +type ChangesViewReviewCommentAddedEvent = { + hasExistingFeedback: boolean; + hasSuggestion: boolean; + isFromPRReview: boolean; +}; + +type ChangesViewReviewCommentAddedClassification = { + owner: 'osortega'; + comment: 'Tracks when a user adds a review comment (feedback) to a file in the Changes panel.'; + hasExistingFeedback: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether there was already feedback on this file.' }; + hasSuggestion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the feedback includes a code suggestion.' }; + isFromPRReview: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the feedback was converted from a PR review comment.' }; +}; + +export function logChangesViewReviewCommentAdded(telemetryService: ITelemetryService, data: { hasExistingFeedback: boolean; hasSuggestion: boolean; isFromPRReview: boolean }): void { + telemetryService.publicLog2('vscodeAgents.changesView/reviewCommentAdded', data); +} diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts index cbd0f3beca843..dd85dd2da6a94 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackService.ts @@ -20,6 +20,8 @@ import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/c import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ICodeReviewSuggestion } from '../../codeReview/browser/codeReviewService.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { logChangesViewReviewCommentAdded } from '../../../common/sessionsTelemetry.js'; // --- Types -------------------------------------------------------------------- @@ -144,6 +146,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @ICommandService private readonly _commandService: ICommandService, @ILogService private readonly _logService: ILogService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { super(); } @@ -201,6 +204,12 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe this._onDidChangeFeedback.fire({ sessionResource, feedbackItems }); + logChangesViewReviewCommentAdded(this._telemetryService, { + hasExistingFeedback: hasExistingForFile, + hasSuggestion: !!suggestion, + isFromPRReview: !!sourcePRReviewCommentId, + }); + return feedback; } diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts index de89dd854bcf8..d042a6eade196 100644 --- a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts @@ -13,6 +13,8 @@ import { AgentFeedbackService, IAgentFeedbackService } from '../../browser/agent import { IChatEditingService } from '../../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { IAgentSessionsService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; function r(startLine: number, endLine: number = startLine): Range { return new Range(startLine, 1, endLine, 1); @@ -36,6 +38,7 @@ suite('AgentFeedbackService - Ordering', () => { instantiationService.stub(IChatEditingService, new class extends mock() { }); instantiationService.stub(IAgentSessionsService, new class extends mock() { }); + instantiationService.stub(ITelemetryService, NullTelemetryService); service = store.add(instantiationService.createInstance(AgentFeedbackService)); session = URI.parse('test://session/1'); diff --git a/src/vs/sessions/contrib/changes/browser/changesTitleBarWidget.ts b/src/vs/sessions/contrib/changes/browser/changesTitleBarWidget.ts index 21e66ef9c4059..68c30f8cedb09 100644 --- a/src/vs/sessions/contrib/changes/browser/changesTitleBarWidget.ts +++ b/src/vs/sessions/contrib/changes/browser/changesTitleBarWidget.ts @@ -24,9 +24,11 @@ import { IsAuxiliaryWindowContext, AuxiliaryBarVisibleContext } from '../../../. import { getAgentChangesSummary } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/layout/browser/layoutService.js'; import { IPaneCompositePartService } from '../../../../workbench/services/panecomposite/browser/panecomposite.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ViewContainerLocation } from '../../../../workbench/common/views.js'; import { Menus } from '../../../browser/menus.js'; import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; +import { logChangesViewToggle } from '../../../common/sessionsTelemetry.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { CHANGES_VIEW_CONTAINER_ID } from './changesView.js'; @@ -182,11 +184,14 @@ registerAction2(class extends Action2 { run(accessor: ServicesAccessor): void { const layoutService = accessor.get(IWorkbenchLayoutService); const paneCompositeService = accessor.get(IPaneCompositePartService); + const telemetryService = accessor.get(ITelemetryService); const isVisible = !layoutService.isVisible(Parts.AUXILIARYBAR_PART); layoutService.setPartHidden(!isVisible, Parts.AUXILIARYBAR_PART); if (isVisible) { paneCompositeService.openPaneComposite(CHANGES_VIEW_CONTAINER_ID, ViewContainerLocation.AuxiliaryBar); } + + logChangesViewToggle(telemetryService, isVisible); } }); diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 2cfab1d2ca5ff..0ba98489913d2 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -74,6 +74,7 @@ import { IView, Sizing, SplitView } from '../../../../base/browser/ui/splitview/ import { Color } from '../../../../base/common/color.js'; import { PANEL_SECTION_BORDER } from '../../../../workbench/common/theme.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../../workbench/common/editor.js'; +import { logChangesViewFileSelect, logChangesViewVersionModeChange, logChangesViewViewModeChange } from '../../../common/sessionsTelemetry.js'; const $ = dom.$; @@ -551,7 +552,8 @@ export class ChangesViewPane extends ViewPane { @ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService, @ILabelService private readonly labelService: ILabelService, @ICodeReviewService private readonly codeReviewService: ICodeReviewService, - @IGitHubService private readonly gitHubService: IGitHubService + @IGitHubService private readonly gitHubService: IGitHubService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super({ ...options, titleMenuId: MenuId.ChatEditingSessionTitleToolbar }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -1119,6 +1121,8 @@ export class ChangesViewPane extends ViewPane { return; } + logChangesViewFileSelect(this.telemetryService, e.element.changeType); + const items = combinedEntriesObs.get(); openFileItem(e.element, items, e.sideBySide, !!e.editorOptions?.preserveFocus, !!e.editorOptions?.pinned, items.length > 1); })); @@ -1748,7 +1752,8 @@ class SetChangesListViewModeAction extends ViewAction { }); } - async runInView(_: ServicesAccessor, view: ChangesViewPane): Promise { + async runInView(accessor: ServicesAccessor, view: ChangesViewPane): Promise { + logChangesViewViewModeChange(accessor.get(ITelemetryService), ChangesViewMode.List); view.viewModel.setViewMode(ChangesViewMode.List); } } @@ -1770,7 +1775,8 @@ class SetChangesTreeViewModeAction extends ViewAction { }); } - async runInView(_: ServicesAccessor, view: ChangesViewPane): Promise { + async runInView(accessor: ServicesAccessor, view: ChangesViewPane): Promise { + logChangesViewViewModeChange(accessor.get(ITelemetryService), ChangesViewMode.Tree); view.viewModel.setViewMode(ChangesViewMode.Tree); } } @@ -1810,7 +1816,7 @@ class ChangesPickerActionItem extends ActionWidgetDropdownActionViewItem { @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, @ISessionsManagementService sessionManagementService: ISessionsManagementService, - @ITelemetryService telemetryService: ITelemetryService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { const actionProvider: IActionWidgetDropdownActionProvider = { getActions: () => { @@ -1829,6 +1835,7 @@ class ChangesPickerActionItem extends ActionWidgetDropdownActionViewItem { category: { label: 'changes', order: 1, showHeader: false }, run: async () => { viewModel.setVersionMode(ChangesVersionMode.BranchChanges); + logChangesViewVersionModeChange(this.telemetryService, ChangesVersionMode.BranchChanges); if (this.element) { this.renderLabel(this.element); } @@ -1845,6 +1852,7 @@ class ChangesPickerActionItem extends ActionWidgetDropdownActionViewItem { viewModel.activeSessionLastCheckpointRefObs.get() !== undefined, run: async () => { viewModel.setVersionMode(ChangesVersionMode.AllChanges); + logChangesViewVersionModeChange(this.telemetryService, ChangesVersionMode.AllChanges); if (this.element) { this.renderLabel(this.element); } @@ -1861,6 +1869,7 @@ class ChangesPickerActionItem extends ActionWidgetDropdownActionViewItem { viewModel.activeSessionLastCheckpointRefObs.get() !== undefined, run: async () => { viewModel.setVersionMode(ChangesVersionMode.LastTurn); + logChangesViewVersionModeChange(this.telemetryService, ChangesVersionMode.LastTurn); if (this.element) { this.renderLabel(this.element); } diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index 314bd21d348e5..187e281ea6ff8 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -12,6 +12,8 @@ import { Schemas } from '../../../../base/common/network.js'; import { URI } from '../../../../base/common/uri.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { logSessionsInteraction } from '../../../common/sessionsTelemetry.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { IViewContainersRegistry, IViewsRegistry, ViewContainerLocation, Extensions as ViewExtensions, WindowVisibility } from '../../../../workbench/common/views.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; @@ -60,6 +62,9 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { } override async run(accessor: ServicesAccessor): Promise { + const telemetryService = accessor.get(ITelemetryService); + logSessionsInteraction(telemetryService, 'openInVSCode'); + const openerService = accessor.get(IOpenerService); const productService = accessor.get(IProductService); const sessionsManagementService = accessor.get(ISessionsManagementService); diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index f9e720ff92162..ec9dfff06917f 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -26,6 +26,7 @@ import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keyb import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; +import { logSessionsInteraction } from '../../../common/sessionsTelemetry.js'; import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js'; import { SessionsCategories } from '../../../common/categories.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; @@ -121,6 +122,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr @ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService, @IActionViewItemService private readonly _actionViewItemService: IActionViewItemService, @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { super(); @@ -194,6 +196,8 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr return; } + logSessionsInteraction(that._telemetryService, 'runPrimaryTask'); + const { tasks, session } = activeState; if (tasks.length === 0) { const task = await that._showConfigureQuickPick(session); @@ -238,6 +242,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr } async run(): Promise { + logSessionsInteraction(that._telemetryService, 'addTask'); const task = await that._showConfigureQuickPick(session); if (task) { await that._sessionsConfigService.runTask(task, session); @@ -261,6 +266,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr } async run(): Promise { + logSessionsInteraction(that._telemetryService, 'generateNewTask'); await that._sessionManagementService.sendAndCreateChat(session, { query: '/generate-run-commands' }); } })); diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts index 7896a32e5377b..628bc4212cc3a 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts @@ -30,7 +30,9 @@ import { Action2, MenuId, registerAction2 } from '../../../../../platform/action import { Button } from '../../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IHostService } from '../../../../../workbench/services/host/browser/host.js'; +import { logSessionsInteraction } from '../../../../common/sessionsTelemetry.js'; const $ = DOM.$; export const SessionsViewId = 'sessions.workbench.view.sessionsView'; @@ -70,6 +72,7 @@ export class SessionsView extends ViewPane { @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, @IHostService private readonly hostService: IHostService, @IStorageService private readonly storageService: IStorageService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -133,7 +136,10 @@ export class SessionsView extends ViewPane { supportIcons: true, })); newSessionButton.label = `$(${Codicon.plus.id}) ${localize('sessionLabel', "Session")}`; - this._register(newSessionButton.onDidClick(() => this.sessionsManagementService.openNewSessionView())); + this._register(newSessionButton.onDidClick(() => { + logSessionsInteraction(this.telemetryService, 'newSession'); + this.sessionsManagementService.openNewSessionView(); + })); const buttonLabel = $('.new-session-button-label'); const keybindingHint = $('span.new-session-keybinding-hint'); diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index 805a6b5e7ab48..b51b89aba175e 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -20,7 +20,9 @@ import { ISessionsManagementService } from '../../sessions/browser/sessionsManag import { ISession } from '../../sessions/common/sessionData.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; +import { logSessionsInteraction } from '../../../common/sessionsTelemetry.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; import { TERMINAL_VIEW_ID } from '../../../../workbench/contrib/terminal/common/terminal.js'; import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/layout/browser/layoutService.js'; @@ -319,6 +321,9 @@ class OpenSessionInTerminalAction extends Action2 { } override async run(_accessor: ServicesAccessor): Promise { + const telemetryService = _accessor.get(ITelemetryService); + logSessionsInteraction(telemetryService, 'openTerminal'); + const layoutService = _accessor.get(IWorkbenchLayoutService); const viewsService = _accessor.get(IViewsService);