mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-16 02:56:37 +01:00
263 lines
12 KiB
TypeScript
263 lines
12 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import assert from 'assert';
|
|
import { VSBuffer } from '../../../../base/common/buffer.js';
|
|
import { DisposableStore } from '../../../../base/common/lifecycle.js';
|
|
import { Schemas } from '../../../../base/common/network.js';
|
|
import { URI } from '../../../../base/common/uri.js';
|
|
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
|
import type { INativeEnvironmentService } from '../../../environment/common/environment.js';
|
|
import { FileService } from '../../../files/common/fileService.js';
|
|
import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js';
|
|
import { NullLoggerService, NullLogService } from '../../../log/common/log.js';
|
|
import type { IProductService } from '../../../product/common/productService.js';
|
|
import { ITelemetryData, ITelemetryService, TelemetryLevel } from '../../../telemetry/common/telemetry.js';
|
|
import { AgentHostTelemetryLevelConfigKey, telemetryLevelToAgentHostConfigValue } from '../../common/agentHostSchema.js';
|
|
import { AgentHostRestrictedTelemetrySender, IAgentHostRestrictedTelemetry, IAgentHostInternalTelemetryContext, IAgentHostRestrictedTelemetryContext, TelemetryProps } from '../../node/agentHostRestrictedTelemetry.js';
|
|
import { AgentHostTelemetryService, createAgentHostTelemetryService, updateAgentHostTelemetryLevelFromConfig } from '../../node/agentHostTelemetryService.js';
|
|
import { AgentHostInternalTelemetrySender } from '../../node/agentHostMicrosoftTelemetry.js';
|
|
|
|
class TestTelemetryService implements ITelemetryService {
|
|
declare readonly _serviceBrand: undefined;
|
|
|
|
telemetryLevel = TelemetryLevel.USAGE;
|
|
sendErrorTelemetry = true;
|
|
sessionId = 'sessionId';
|
|
machineId = 'machineId';
|
|
sqmId = 'sqmId';
|
|
devDeviceId = 'devDeviceId';
|
|
firstSessionDate = 'firstSessionDate';
|
|
readonly events: { eventName: string; data: ITelemetryData | undefined }[] = [];
|
|
readonly errorEvents: { eventName: string; data: ITelemetryData | undefined }[] = [];
|
|
|
|
publicLog(eventName: string, data?: ITelemetryData): void {
|
|
this.events.push({ eventName, data });
|
|
}
|
|
|
|
publicLogError(eventName: string, data?: ITelemetryData): void {
|
|
this.errorEvents.push({ eventName, data });
|
|
}
|
|
|
|
publicLog2(eventName: string, data?: ITelemetryData): void {
|
|
this.events.push({ eventName, data });
|
|
}
|
|
|
|
publicLogError2(eventName: string, data?: ITelemetryData): void {
|
|
this.errorEvents.push({ eventName, data });
|
|
}
|
|
|
|
setExperimentProperty(): void { }
|
|
setCommonProperty(): void { }
|
|
}
|
|
|
|
class TestRestrictedSink implements IAgentHostRestrictedTelemetry {
|
|
readonly enhanced: string[] = [];
|
|
readonly standard: string[] = [];
|
|
readonly trackingIds: (string | undefined)[] = [];
|
|
readonly endpoints: (string | undefined)[] = [];
|
|
readonly enabledFlags: boolean[] = [];
|
|
readonly internal: string[] = [];
|
|
readonly internalContexts: (IAgentHostInternalTelemetryContext | undefined)[] = [];
|
|
|
|
sendGHTelemetryEvent(eventName: string, _properties?: TelemetryProps): void {
|
|
this.standard.push(eventName);
|
|
}
|
|
sendEnhancedGHTelemetryEvent(eventName: string, _properties?: TelemetryProps): void {
|
|
this.enhanced.push(eventName);
|
|
}
|
|
sendEnhancedGHTelemetryEventForContext(_context: IAgentHostRestrictedTelemetryContext, eventName: string): void {
|
|
this.enhanced.push(eventName);
|
|
}
|
|
sendInternalMSFTTelemetryEvent(eventName: string): void {
|
|
this.internal.push(eventName);
|
|
}
|
|
sendInternalMSFTTelemetryEventForContext(_context: IAgentHostInternalTelemetryContext, eventName: string): void {
|
|
this.internal.push(eventName);
|
|
}
|
|
setCopilotTrackingId(trackingId: string | undefined): void {
|
|
this.trackingIds.push(trackingId);
|
|
}
|
|
setRestrictedTelemetryEndpoint(endpointUrl: string | undefined): void {
|
|
this.endpoints.push(endpointUrl);
|
|
}
|
|
setRestrictedTelemetryEnabled(enabled: boolean): void {
|
|
this.enabledFlags.push(enabled);
|
|
}
|
|
setInternalTelemetryContext(context: IAgentHostInternalTelemetryContext | undefined): void {
|
|
this.internalContexts.push(context);
|
|
}
|
|
}
|
|
|
|
suite('AgentHostTelemetryService', () => {
|
|
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
|
|
|
|
test('logging-only builds do not create restricted network senders', async () => {
|
|
const localDisposables = disposables.add(new DisposableStore());
|
|
const logService = new NullLogService();
|
|
const fileService = localDisposables.add(new FileService(logService));
|
|
const fileSystemProvider = localDisposables.add(new InMemoryFileSystemProvider());
|
|
localDisposables.add(fileService.registerProvider(Schemas.inMemory, fileSystemProvider));
|
|
const service = await createAgentHostTelemetryService({
|
|
environmentService: {
|
|
isBuilt: false,
|
|
disableTelemetry: false,
|
|
appRoot: '/app',
|
|
extensionsPath: '/extensions',
|
|
userHome: URI.from({ scheme: Schemas.inMemory, path: '/home' }),
|
|
tmpDir: URI.from({ scheme: Schemas.inMemory, path: '/tmp' }),
|
|
userDataPath: '/user-data',
|
|
appSettingsHome: URI.from({ scheme: Schemas.inMemory, path: '/User' }),
|
|
} as INativeEnvironmentService,
|
|
productService: { _serviceBrand: undefined, version: '1.130.0' } as IProductService,
|
|
fileService,
|
|
loggerService: localDisposables.add(new NullLoggerService()),
|
|
logService,
|
|
disposables: localDisposables,
|
|
});
|
|
assert.strictEqual((service as unknown as { _restricted: AgentHostRestrictedTelemetrySender | undefined })._restricted, undefined);
|
|
});
|
|
|
|
test('uses the built-in Copilot manifest version for internal telemetry', async () => {
|
|
const localDisposables = disposables.add(new DisposableStore());
|
|
const logService = new NullLogService();
|
|
const fileService = localDisposables.add(new FileService(logService));
|
|
const fileSystemProvider = localDisposables.add(new InMemoryFileSystemProvider());
|
|
localDisposables.add(fileService.registerProvider(Schemas.file, fileSystemProvider));
|
|
await fileService.createFolder(URI.file('/extensions/copilot'));
|
|
await fileService.writeFile(URI.file('/extensions/copilot/package.json'), VSBuffer.fromString(JSON.stringify({ version: '0.58.0' })));
|
|
|
|
const service = await createAgentHostTelemetryService({
|
|
environmentService: {
|
|
isBuilt: true,
|
|
disableTelemetry: false,
|
|
appRoot: '/app',
|
|
extensionsPath: '/extensions',
|
|
builtinExtensionsPath: '/extensions',
|
|
userHome: URI.file('/home'),
|
|
tmpDir: URI.file('/tmp'),
|
|
userDataPath: '/user-data',
|
|
appSettingsHome: URI.file('/User'),
|
|
} as INativeEnvironmentService,
|
|
productService: {
|
|
_serviceBrand: undefined,
|
|
version: '1.130.0',
|
|
enableTelemetry: true,
|
|
aiConfig: { ariaKey: 'test-key' },
|
|
} as IProductService,
|
|
fileService,
|
|
loggerService: localDisposables.add(new NullLoggerService()),
|
|
logService,
|
|
disposables: localDisposables,
|
|
});
|
|
const restricted = (service as unknown as { _restricted: AgentHostRestrictedTelemetrySender })._restricted;
|
|
const internalSender = (restricted as unknown as { _internalSink: AgentHostInternalTelemetrySender })._internalSink;
|
|
|
|
assert.strictEqual((internalSender as unknown as { _options: { extensionVersion: string | undefined } })._options.extensionVersion, '0.58.0');
|
|
});
|
|
|
|
test('permanently disables usage and error telemetry after TelemetryLevel.NONE', async () => {
|
|
const delegate = new TestTelemetryService();
|
|
const service = disposables.add(new AgentHostTelemetryService(delegate));
|
|
|
|
service.publicLog('beforeDisable', { count: 1 });
|
|
service.updateTelemetryLevel(TelemetryLevel.NONE);
|
|
service.updateTelemetryLevel(TelemetryLevel.USAGE);
|
|
service.publicLog2('afterDisable');
|
|
service.publicLogError2('afterDisableError');
|
|
service.publicLog('afterDisableAsync', { count: 4 });
|
|
service.publicLogError('afterDisableErrorAsync', { count: 5 });
|
|
|
|
assert.deepStrictEqual({
|
|
telemetryLevel: service.telemetryLevel,
|
|
sendErrorTelemetry: service.sendErrorTelemetry,
|
|
events: delegate.events,
|
|
errorEvents: delegate.errorEvents,
|
|
}, {
|
|
telemetryLevel: TelemetryLevel.NONE,
|
|
sendErrorTelemetry: false,
|
|
events: [{ eventName: 'beforeDisable', data: { count: 1 } }],
|
|
errorEvents: [],
|
|
});
|
|
});
|
|
|
|
test('uses most restrictive client telemetry level', () => {
|
|
const service = disposables.add(new AgentHostTelemetryService(new TestTelemetryService()));
|
|
|
|
service.updateTelemetryLevel(TelemetryLevel.ERROR);
|
|
service.updateTelemetryLevel(TelemetryLevel.USAGE);
|
|
|
|
assert.strictEqual(service.telemetryLevel, TelemetryLevel.ERROR);
|
|
});
|
|
|
|
test('updates telemetry level from root config string enum', () => {
|
|
const service = disposables.add(new AgentHostTelemetryService(new TestTelemetryService()));
|
|
|
|
updateAgentHostTelemetryLevelFromConfig(service, {
|
|
[AgentHostTelemetryLevelConfigKey]: telemetryLevelToAgentHostConfigValue(TelemetryLevel.ERROR),
|
|
});
|
|
|
|
assert.strictEqual(service.telemetryLevel, TelemetryLevel.ERROR);
|
|
});
|
|
|
|
test('enhanced GH telemetry is gated on the restricted (rt) opt-in; standard GH telemetry is not', () => {
|
|
const restricted = new TestRestrictedSink();
|
|
const service = disposables.add(new AgentHostTelemetryService(new TestTelemetryService(), restricted));
|
|
|
|
service.sendEnhancedGHTelemetryEvent('request.options.tools'); // dropped: rt disabled by default
|
|
service.sendGHTelemetryEvent('completion'); // sent: standard GH telemetry is not rt-gated
|
|
service.setRestrictedTelemetryEnabled(true);
|
|
service.sendEnhancedGHTelemetryEvent('request.options.tools'); // sent: rt now enabled
|
|
service.setCopilotTrackingId('tid-1');
|
|
service.setRestrictedTelemetryEndpoint('https://ghe.example/telemetry');
|
|
|
|
assert.deepStrictEqual({
|
|
enhanced: restricted.enhanced,
|
|
standard: restricted.standard,
|
|
trackingIds: restricted.trackingIds,
|
|
endpoints: restricted.endpoints,
|
|
}, {
|
|
enhanced: ['request.options.tools'],
|
|
standard: ['completion'],
|
|
trackingIds: ['tid-1'],
|
|
endpoints: ['https://ghe.example/telemetry'],
|
|
});
|
|
// The rt opt-in is mirrored onto the sender (defense in depth), matching the extension's
|
|
// opted-in-only restricted reporter.
|
|
assert.deepStrictEqual(restricted.enabledFlags, [true]);
|
|
});
|
|
|
|
test('enhanced GH telemetry stays suppressed when telemetry is disabled, even for an rt=1 user', () => {
|
|
const delegate = new TestTelemetryService();
|
|
delegate.telemetryLevel = TelemetryLevel.ERROR; // user opted below USAGE
|
|
const restricted = new TestRestrictedSink();
|
|
const service = disposables.add(new AgentHostTelemetryService(delegate, restricted));
|
|
|
|
service.setRestrictedTelemetryEnabled(true); // rt=1
|
|
service.sendEnhancedGHTelemetryEvent('request.options.tools');
|
|
service.sendGHTelemetryEvent('completion');
|
|
|
|
// Neither standard nor enhanced GH telemetry is delegated below USAGE, regardless of rt.
|
|
assert.deepStrictEqual({ enhanced: restricted.enhanced, standard: restricted.standard }, { enhanced: [], standard: [] });
|
|
});
|
|
|
|
test('internal telemetry is independently gated and identity is cleared on account changes', () => {
|
|
const restricted = new TestRestrictedSink();
|
|
const service = disposables.add(new AgentHostTelemetryService(new TestTelemetryService(), restricted));
|
|
const internalContext = { isInternal: true, trackingId: 'tid-1', userName: 'octocat', isVscodeTeamMember: true };
|
|
|
|
service.sendInternalMSFTTelemetryEvent('beforeIdentity');
|
|
service.setInternalTelemetryContext(internalContext);
|
|
service.sendInternalMSFTTelemetryEvent('internal');
|
|
service.setInternalTelemetryContext(undefined);
|
|
service.sendInternalMSFTTelemetryEvent('afterClear');
|
|
|
|
assert.deepStrictEqual({ internal: restricted.internal, contexts: restricted.internalContexts }, {
|
|
internal: ['internal'],
|
|
contexts: [internalContext, undefined],
|
|
});
|
|
});
|
|
});
|