Add telemetry to Chat Customizations editor (#301173)

* Add telemetry to Chat Customizations editor

Instrument 7 key user interactions in the AI Customization Management
Editor with GDPR-compliant publicLog2 telemetry events:

- chatCustomizationEditor.opened: tracks editor opens with initial section
- chatCustomizationEditor.sectionChanged: tracks sidebar navigation
- chatCustomizationEditor.itemSelected: tracks item selection with type/storage
- chatCustomizationEditor.createItem: tracks AI-guided and manual creation
- chatCustomizationEditor.saveItem: tracks save actions (builtin override + existing)
- chatCustomizationEditor.deleteItem: tracks confirmed deletions
- chatCustomizationEditor.search: tracks search usage with result counts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: improve search telemetry logging in Chat Customizations editor

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Josh Spicer
2026-03-12 11:10:58 -07:00
committed by GitHub
parent 6eabb60970
commit 197fc9911c
3 changed files with 159 additions and 2 deletions

View File

@@ -48,11 +48,28 @@ import { HookType, HOOK_METADATA } from '../../common/promptSyntax/hookTypes.js'
import { parse as parseJSONC } from '../../../../../base/common/json.js';
import { Schemas } from '../../../../../base/common/network.js';
import { OS } from '../../../../../base/common/platform.js';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
export { truncateToFirstSentence } from './aiCustomizationListWidgetUtils.js';
const $ = DOM.$;
//#region Telemetry
type CustomizationEditorSearchEvent = {
section: string;
resultCount: number;
};
type CustomizationEditorSearchClassification = {
section: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The active section when the search was performed.' };
resultCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of items matching the search query.' };
owner: 'joshspicer';
comment: 'Tracks search usage in the Chat Customizations editor.';
};
//#endregion
const ITEM_HEIGHT = 44;
const GROUP_HEADER_HEIGHT = 36;
const GROUP_HEADER_HEIGHT_WITH_SEPARATOR = 40;
@@ -403,6 +420,7 @@ export class AICustomizationListWidget extends Disposable {
@IHoverService private readonly hoverService: IHoverService,
@IFileService private readonly fileService: IFileService,
@IPathService private readonly pathService: IPathService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
) {
super();
this.element = $('.ai-customization-list-widget');
@@ -430,7 +448,15 @@ export class AICustomizationListWidget extends Disposable {
this._register(this.searchInput.onDidChange(() => {
this.searchQuery = this.searchInput.value;
this.delayedFilter.trigger(() => this.filterItems());
this.delayedFilter.trigger(() => {
const matchCount = this.filterItems();
if (this.searchQuery.trim()) {
this.telemetryService.publicLog2<CustomizationEditorSearchEvent, CustomizationEditorSearchClassification>('chatCustomizationEditor.search', {
section: this.currentSection,
resultCount: matchCount,
});
}
});
}));
// Add button container next to search
@@ -1011,7 +1037,7 @@ export class AICustomizationListWidget extends Disposable {
/**
* Filters items based on the current search query and builds grouped display entries.
*/
private filterItems(): void {
private filterItems(): number {
let matchedItems: IAICustomizationListItem[];
if (!this.searchQuery.trim()) {
@@ -1095,6 +1121,7 @@ export class AICustomizationListWidget extends Disposable {
this.list.splice(0, this.list.length, this.displayEntries);
this.updateEmptyState();
return matchedItems.length;
}
/**

View File

@@ -38,6 +38,23 @@ import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.j
import { basename, dirname } from '../../../../../base/common/resources.js';
import { Schemas } from '../../../../../base/common/network.js';
import { isWindows, isMacintosh } from '../../../../../base/common/platform.js';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
//#region Telemetry
type CustomizationEditorDeleteItemEvent = {
promptType: string;
storage: string;
};
type CustomizationEditorDeleteItemClassification = {
promptType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of customization being deleted.' };
storage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The storage location of the deleted item.' };
owner: 'joshspicer';
comment: 'Tracks item deletion in the Chat Customizations editor.';
};
//#endregion
//#region Editor Registration
@@ -230,6 +247,12 @@ registerAction2(class extends Action2 {
});
if (confirmation.confirmed) {
const telemetryService = accessor.get(ITelemetryService);
telemetryService.publicLog2<CustomizationEditorDeleteItemEvent, CustomizationEditorDeleteItemClassification>('chatCustomizationEditor.deleteItem', {
promptType: promptType ?? '',
storage: storage ?? '',
});
// For skills, delete the parent folder (e.g. .github/skills/my-skill/)
// since each skill is a folder containing SKILL.md.
const deleteTarget = isSkill ? dirname(uri) : uri;

View File

@@ -81,6 +81,74 @@ import { IAgentPluginItem } from '../agentPluginEditor/agentPluginItems.js';
const $ = DOM.$;
//#region Telemetry
type CustomizationEditorOpenedEvent = {
section: string;
};
type CustomizationEditorOpenedClassification = {
section: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The initially selected section when the editor opens.' };
owner: 'joshspicer';
comment: 'Tracks when the Chat Customizations editor is opened.';
};
type CustomizationEditorSectionChangedEvent = {
section: string;
};
type CustomizationEditorSectionChangedClassification = {
section: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The section the user navigated to.' };
owner: 'joshspicer';
comment: 'Tracks section navigation within the Chat Customizations editor.';
};
type CustomizationEditorItemSelectedEvent = {
section: string;
promptType: string;
storage: string;
};
type CustomizationEditorItemSelectedClassification = {
section: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The active section when the item was selected.' };
promptType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The prompt type of the selected item.' };
storage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The storage location of the selected item (local, user, extension, plugin, builtin).' };
owner: 'joshspicer';
comment: 'Tracks item selection in the Chat Customizations editor.';
};
type CustomizationEditorCreateItemEvent = {
section: string;
promptType: string;
creationMode: 'ai' | 'manual';
target: string;
};
type CustomizationEditorCreateItemClassification = {
section: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The active section when the item was created.' };
promptType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of customization being created.' };
creationMode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the item was created via AI-guided flow or manual creation.' };
target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The target storage for the new item (workspace, user).' };
owner: 'joshspicer';
comment: 'Tracks customization creation in the Chat Customizations editor.';
};
type CustomizationEditorSaveItemEvent = {
promptType: string;
storage: string;
saveTarget: string;
};
type CustomizationEditorSaveItemClassification = {
promptType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of customization being saved.' };
storage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The original storage location of the item.' };
saveTarget: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The target storage for the save (workspace, user, existing).' };
owner: 'joshspicer';
comment: 'Tracks save actions in the Chat Customizations editor.';
};
//#endregion
export const aiCustomizationManagementSashBorder = registerColor(
'aiCustomizationManagement.sashBorder',
PANEL_BORDER,
@@ -485,6 +553,11 @@ export class AICustomizationManagementEditor extends EditorPane {
// Handle item selection
this.editorDisposables.add(this.listWidget.onDidSelectItem(item => {
this.telemetryService.publicLog2<CustomizationEditorItemSelectedEvent, CustomizationEditorItemSelectedClassification>('chatCustomizationEditor.itemSelected', {
section: this.selectedSection,
promptType: item.promptType,
storage: item.storage,
});
const isWorkspaceFile = item.storage === PromptsStorage.local;
const isReadOnly = item.storage === PromptsStorage.extension || item.storage === PromptsStorage.plugin || item.storage === BUILTIN_STORAGE;
this.showEmbeddedEditor(item.uri, item.name, item.promptType, item.storage, isWorkspaceFile, isReadOnly);
@@ -576,6 +649,10 @@ export class AICustomizationManagementEditor extends EditorPane {
return;
}
this.telemetryService.publicLog2<CustomizationEditorSectionChangedEvent, CustomizationEditorSectionChangedClassification>('chatCustomizationEditor.sectionChanged', {
section,
});
if (this.viewMode === 'editor') {
this.goBackToList();
}
@@ -667,6 +744,12 @@ export class AICustomizationManagementEditor extends EditorPane {
* Creates a new customization using the AI-guided flow.
*/
private async createNewItemWithAI(type: PromptsType): Promise<void> {
this.telemetryService.publicLog2<CustomizationEditorCreateItemEvent, CustomizationEditorCreateItemClassification>('chatCustomizationEditor.createItem', {
section: this.selectedSection,
promptType: type,
creationMode: 'ai',
target: 'workspace',
});
if (this.input) {
this.group.closeEditor(this.input);
}
@@ -677,6 +760,12 @@ export class AICustomizationManagementEditor extends EditorPane {
* Creates a new prompt file and opens it in the embedded editor.
*/
private async createNewItemManual(type: PromptsType, target: 'workspace' | 'user'): Promise<void> {
this.telemetryService.publicLog2<CustomizationEditorCreateItemEvent, CustomizationEditorCreateItemClassification>('chatCustomizationEditor.createItem', {
section: this.selectedSection,
promptType: type,
creationMode: 'manual',
target,
});
if (type === PromptsType.hook) {
if (this.workspaceService.isSessionsWindow) {
@@ -741,6 +830,10 @@ export class AICustomizationManagementEditor extends EditorPane {
this.inEditorContextKey.set(true);
this.sectionContextKey.set(this.selectedSection);
this.telemetryService.publicLog2<CustomizationEditorOpenedEvent, CustomizationEditorOpenedClassification>('chatCustomizationEditor.opened', {
section: this.selectedSection,
});
await super.setInput(input, options, context, token);
if (this.dimension) {
@@ -966,6 +1059,13 @@ export class AICustomizationManagementEditor extends EditorPane {
private goBackToList(): void {
const fileUri = this.currentEditingUri;
const backgroundSaveRequest = this.createExistingCustomizationSaveRequest();
if (backgroundSaveRequest) {
this.telemetryService.publicLog2<CustomizationEditorSaveItemEvent, CustomizationEditorSaveItemClassification>('chatCustomizationEditor.saveItem', {
promptType: this.currentEditingPromptType ?? '',
storage: String(this.currentEditingStorage ?? ''),
saveTarget: 'existing',
});
}
if (fileUri && this.currentEditingStorage === BUILTIN_STORAGE) {
this.disposeBuiltinEditingSession(fileUri);
}
@@ -1134,6 +1234,13 @@ export class AICustomizationManagementEditor extends EditorPane {
}
backgroundSaveRequest = this.createBuiltinPromptSaveRequest(selection);
if (backgroundSaveRequest) {
this.telemetryService.publicLog2<CustomizationEditorSaveItemEvent, CustomizationEditorSaveItemClassification>('chatCustomizationEditor.saveItem', {
promptType: this.currentEditingPromptType ?? '',
storage: String(this.currentEditingStorage ?? ''),
saveTarget: selection.target,
});
}
}
this.goBackToList();