diff --git a/.eslint-plugin-local/code-no-telemetry-common-property.ts b/.eslint-plugin-local/code-no-telemetry-common-property.ts index 2627a09c0a4..6de65febb89 100644 --- a/.eslint-plugin-local/code-no-telemetry-common-property.ts +++ b/.eslint-plugin-local/code-no-telemetry-common-property.ts @@ -46,6 +46,7 @@ const commonTelemetryProperties = new Set([ 'common.cli', 'common.useragent', 'common.istouchdevice', + 'common.copilottrackingid', ]); export default new class NoTelemetryCommonProperty implements eslint.Rule.RuleModule { diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index e12a15076e4..ace6ffc3bfd 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -803,6 +803,7 @@ class StandaloneTelemetryService implements ITelemetryService { readonly sendErrorTelemetry = false; setEnabled(): void { } setExperimentProperty(): void { } + setCommonProperty(): void { } publicLog() { } publicLog2() { } publicLogError() { } diff --git a/src/vs/platform/dataChannel/browser/forwardingTelemetryService.ts b/src/vs/platform/dataChannel/browser/forwardingTelemetryService.ts index e27a067eaf8..9150e178e99 100644 --- a/src/vs/platform/dataChannel/browser/forwardingTelemetryService.ts +++ b/src/vs/platform/dataChannel/browser/forwardingTelemetryService.ts @@ -70,6 +70,10 @@ export class InterceptingTelemetryService implements ITelemetryService { setExperimentProperty(name: string, value: string): void { this._baseService.setExperimentProperty(name, value); } + + setCommonProperty(name: string, value: string): void { + this._baseService.setCommonProperty(name, value); + } } export interface IEditTelemetryData { diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 11fd408cda0..5cd0d8cbdc6 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -51,6 +51,12 @@ export interface ITelemetryService { publicLogError2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): void; setExperimentProperty(name: string, value: string): void; + + /** + * Sets a common property that will be attached to all telemetry events. + * Common properties are added after PII cleaning and cannot be overridden by event data. + */ + setCommonProperty(name: string, value: string): void; } export function telemetryLevelEnabled(service: ITelemetryService, level: TelemetryLevel): boolean { diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 445609fb7d3..e84542599b0 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -135,6 +135,10 @@ export class TelemetryService implements ITelemetryService { } } + setCommonProperty(name: string, value: string): void { + this._commonProperties[name] = value; + } + private _flushPendingEvents(): void { if (this._isExperimentPropertySet) { return; diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index d1acea8ff5d..dac6a8dfa8a 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -40,6 +40,7 @@ export class NullTelemetryServiceShape implements ITelemetryService { publicLogError() { } publicLogError2() { } setExperimentProperty() { } + setCommonProperty() { } } export const NullTelemetryService = new NullTelemetryServiceShape(); diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index cf93a8cc0cd..ae80446734f 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -212,6 +212,22 @@ suite('TelemetryService', () => { service.dispose(); }); + test('setCommonProperty adds property to all subsequent events', function () { + const testAppender = new TestTelemetryAppender(); + const service = new TelemetryService({ + appenders: [testAppender], + }, new TestConfigurationService(), TestProductService); + + service.publicLog('eventBeforeSet'); + service.setCommonProperty('common.copilotTrackingId', 'test-tracking-id'); + service.publicLog('eventAfterSet'); + + assert.strictEqual(testAppender.events[0].data['common.copilotTrackingId'], undefined); + assert.strictEqual(testAppender.events[1].data['common.copilotTrackingId'], 'test-tracking-id'); + + service.dispose(); + }); + test('telemetry on by default', function () { const testAppender = new TestTelemetryAppender(); const service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService(), TestProductService); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 045810171de..06e553b2528 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -23,6 +23,7 @@ import product from '../../../../platform/product/common/product.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js'; import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; @@ -1526,6 +1527,32 @@ class ChatResolverContribution extends Disposable { } } +class CopilotTelemetryContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.copilotTelemetry'; + + constructor( + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + ) { + super(); + + this.updateCopilotTrackingId(); + + this._register(this.chatEntitlementService.onDidChangeEntitlement(() => { + this.updateCopilotTrackingId(); + })); + } + + private updateCopilotTrackingId(): void { + const copilotTrackingId = this.chatEntitlementService.copilotTrackingId; + if (copilotTrackingId) { + // __GDPR__COMMON__ "common.copilotTrackingId" : { "endPoint": "GoogleAnalyticsID", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight", "comment": "The anonymized Copilot analytics tracking ID from the entitlement API." } + this.telemetryService.setCommonProperty('common.copilotTrackingId', copilotTrackingId); + } + } +} + class ChatDebugResolverContribution implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatDebugResolver'; @@ -1849,6 +1876,7 @@ registerEditorFeature(ChatInputBoxContentProvider); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatDebugEditorInput.ID, ChatDebugEditorInputSerializer); +registerWorkbenchContribution2(CopilotTelemetryContribution.ID, CopilotTelemetryContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribution, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(ChatDebugResolverContribution.ID, ChatDebugResolverContribution, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(PromptsDebugContribution.ID, PromptsDebugContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 2b6541ee231..b59a8bc40e4 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -135,6 +135,10 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.setExperimentProperty(name, value); } + setCommonProperty(name: string, value: string): void { + this.impl.setCommonProperty(name, value); + } + get telemetryLevel(): TelemetryLevel { return this.impl.telemetryLevel; } diff --git a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts index 2104d6b254c..8eef56b42c1 100644 --- a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts @@ -101,6 +101,10 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.setExperimentProperty(name, value); } + setCommonProperty(name: string, value: string): void { + this.impl.setCommonProperty(name, value); + } + get telemetryLevel(): TelemetryLevel { return this.impl.telemetryLevel; } diff --git a/src/vs/workbench/test/electron-browser/treeSitterTokenizationFeature.test.ts b/src/vs/workbench/test/electron-browser/treeSitterTokenizationFeature.test.ts index 4abd6dce6de..f261adee2f0 100644 --- a/src/vs/workbench/test/electron-browser/treeSitterTokenizationFeature.test.ts +++ b/src/vs/workbench/test/electron-browser/treeSitterTokenizationFeature.test.ts @@ -69,6 +69,8 @@ class MockTelemetryService implements ITelemetryService { } setExperimentProperty(name: string, value: string): void { } + setCommonProperty(name: string, value: string): void { + } }