Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache initial telemetry events to avoid wrong value assumption #140651

Merged
merged 3 commits into from Jan 14, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 1 addition & 28 deletions src/vs/server/remoteAgentEnvironmentImpl.ts
Expand Up @@ -25,11 +25,9 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics
import { basename, isAbsolute, join, normalize } from 'vs/base/common/path';
import { ProcessItem } from 'vs/base/common/processes';
import { ILog, Translations } from 'vs/workbench/services/extensions/common/extensionPoints';
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import { IBuiltInExtension } from 'vs/base/common/product';
import { IExtensionManagementCLIService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { cwd } from 'vs/base/common/process';
import { IRemoteTelemetryService } from 'vs/server/remoteTelemetryService';
import { Promises } from 'vs/base/node/pfs';
import { IProductService } from 'vs/platform/product/common/productService';

Expand Down Expand Up @@ -60,8 +58,6 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
private readonly environmentService: IServerEnvironmentService,
extensionManagementCLIService: IExtensionManagementCLIService,
private readonly logService: ILogService,
private readonly telemetryService: IRemoteTelemetryService,
private readonly telemetryAppender: ITelemetryAppender | null,
private readonly productService: IProductService
) {
this._logger = new class implements ILog {
Expand Down Expand Up @@ -98,11 +94,8 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
}

async call(_: any, command: string, arg?: any): Promise<any> {
console.log(`Command received: ${command}`);
switch (command) {
case 'disableTelemetry': {
this.telemetryService.permanentlyDisableTelemetry();
return;
}

case 'getEnvironmentData': {
const args = <IGetEnvironmentDataArguments>arg;
Expand Down Expand Up @@ -196,26 +189,6 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
return diagnosticInfo;
});
}

case 'logTelemetry': {
const { eventName, data } = arg;
// Logging is done directly to the appender instead of through the telemetry service
// as the data sent from the client has already had common properties added to it and
// has already been sent to the telemetry output channel
if (this.telemetryAppender) {
return this.telemetryAppender.log(eventName, data);
}

return Promise.resolve();
}

case 'flushTelemetry': {
if (this.telemetryAppender) {
return this.telemetryAppender.flush();
}

return Promise.resolve();
}
}

throw new Error(`IPC Command ${command} not found`);
Expand Down
8 changes: 6 additions & 2 deletions src/vs/server/remoteExtensionHostAgentServer.ts
Expand Up @@ -83,6 +83,7 @@ import { ICredentialsService } from 'vs/platform/credentials/common/credentials'
import { CredentialsMainService } from 'vs/platform/credentials/node/credentialsMainService';
import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService';
import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMainService';
import { RemoteTelemetryChannel } from 'vs/server/remoteTelemetryChannel';

const SHUTDOWN_TIMEOUT = 5 * 60 * 1000;

Expand Down Expand Up @@ -305,7 +306,7 @@ export class RemoteExtensionHostAgentServer extends Disposable {
piiPaths: [this._environmentService.appRoot]
};

services.set(IRemoteTelemetryService, new SyncDescriptor(RemoteTelemetryService, [config]));
services.set(IRemoteTelemetryService, new SyncDescriptor(RemoteTelemetryService, [config, undefined]));
} else {
services.set(IRemoteTelemetryService, RemoteNullTelemetryService);
}
Expand Down Expand Up @@ -340,9 +341,12 @@ export class RemoteExtensionHostAgentServer extends Disposable {
services.set(ICredentialsService, credentialsService);

return instantiationService.invokeFunction(accessor => {
const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(this._connectionToken, this._environmentService, extensionManagementCLIService, this._logService, accessor.get(IRemoteTelemetryService), appInsightsAppender, this._productService);
const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(this._connectionToken, this._environmentService, extensionManagementCLIService, this._logService, this._productService);
this._socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel);

const telemetryChannel = new RemoteTelemetryChannel(accessor.get(IRemoteTelemetryService), appInsightsAppender);
this._socketServer.registerChannel('telemetry', telemetryChannel);

this._socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(this._environmentService, this._logService, ptyService, this._productService));

const remoteFileSystemChannel = new RemoteAgentFileSystemProviderChannel(this._logService, this._environmentService);
Expand Down
65 changes: 65 additions & 0 deletions src/vs/server/remoteTelemetryChannel.ts
@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import { IRemoteTelemetryService } from 'vs/server/remoteTelemetryService';

export class RemoteTelemetryChannel extends Disposable implements IServerChannel {
constructor(
private readonly telemetryService: IRemoteTelemetryService,
private readonly telemetryAppender: ITelemetryAppender | null
) {
super();
}


async call(_: any, command: string, arg?: any): Promise<any> {
switch (command) {
case 'updateTelemetryLevel': {
const { telemetryLevel } = arg;
return this.telemetryService.updateInjectedTelemetryLevel(telemetryLevel);
}

case 'logTelemetry': {
const { eventName, data } = arg;
// Logging is done directly to the appender instead of through the telemetry service
// as the data sent from the client has already had common properties added to it and
// has already been sent to the telemetry output channel
if (this.telemetryAppender) {
return this.telemetryAppender.log(eventName, data);
}

return Promise.resolve();
}

case 'flushTelemetry': {
if (this.telemetryAppender) {
return this.telemetryAppender.flush();
}

return Promise.resolve();
}
}
// Command we cannot handle so we throw an error
throw new Error(`IPC Command ${command} not found`);
}

listen(_: any, event: string, arg: any): Event<any> {
throw new Error('Not supported');
}

/**
* Disposing the channel also disables the telemetryService as there is
* no longer a way to control it
*/
public override dispose(): void {
this.telemetryService.updateInjectedTelemetryLevel(TelemetryLevel.NONE);
super.dispose();
}
}
77 changes: 61 additions & 16 deletions src/vs/server/remoteTelemetryService.ts
Expand Up @@ -6,59 +6,104 @@
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings';
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITelemetryData, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
import { NullTelemetryServiceShape } from 'vs/platform/telemetry/common/telemetryUtils';

export interface IRemoteTelemetryService extends ITelemetryService {
permanentlyDisableTelemetry(): void
updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void>
}

interface CachedTelemetryEvent {
eventName: string;
data?: ITelemetryData;
anonymizeFilePaths?: boolean;
eventType: 'usage' | 'error';
}

export class RemoteTelemetryService extends TelemetryService implements IRemoteTelemetryService {
private _isDisabled = false;
private _telemetryCache: CachedTelemetryEvent[] = [];
// Because we cannot read the workspace config on the remote site
// the RemoteTeelemtryService is respeonsible for knowing its telemetry level
// this is done through IPC calls and initial value injections
private _injectedTelemetryLevel: TelemetryLevel | undefined;
constructor(
config: ITelemetryServiceConfig,
injectedTelemetryLevel: TelemetryLevel | undefined,
@IConfigurationService _configurationService: IConfigurationService
) {
super(config, _configurationService);
this._injectedTelemetryLevel = injectedTelemetryLevel;
}

override publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise<void> {
if (this._isDisabled) {
if (this._injectedTelemetryLevel === undefined) {
// Undefined safety with cache in case super class calls log before cache is initialized in subclass constructor
this._telemetryCache?.push({ eventName, data, anonymizeFilePaths, eventType: 'usage' });
return Promise.resolve();
}
if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
return Promise.resolve(undefined);
}
console.log(`Logging telemetry: ${eventName}`);
return super.publicLog(eventName, data, anonymizeFilePaths);
}

override publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>, anonymizeFilePaths?: boolean): Promise<void> {
if (this._isDisabled) {
return Promise.resolve(undefined);
}
return super.publicLog2(eventName, data, anonymizeFilePaths);
return this.publicLog(eventName, data as ITelemetryData | undefined, anonymizeFilePaths);
}

override publicLogError(errorEventName: string, data?: ITelemetryData): Promise<void> {
if (this._isDisabled) {
if (this._injectedTelemetryLevel === undefined) {
// Undefined safety with cache in case super class calls log before cache is initialized in subclass constructor
this._telemetryCache?.push({ eventName: errorEventName, data, eventType: 'error' });
return Promise.resolve();
}
if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
return Promise.resolve(undefined);
}
console.log(`Logging telemetry: ${errorEventName}`);
return super.publicLogError(errorEventName, data);
}

override publicLogError2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>): Promise<void> {
if (this._isDisabled) {
return Promise.resolve(undefined);
return this.publicLogError(eventName, data as ITelemetryData | undefined);
}

// Flushes all the cached events with the new level
async flushTelemetryCache(): Promise<void> {
if (this._telemetryCache?.length === 0) {
return;
}
return super.publicLogError2(eventName, data);
for (const cacheItem of this._telemetryCache) {
if (cacheItem.eventType === 'usage') {
await this.publicLog(cacheItem.eventName, cacheItem.data, cacheItem.anonymizeFilePaths);
} else {
await this.publicLogError(cacheItem.eventName, cacheItem.data);
}
}
this._telemetryCache = [];
}

permanentlyDisableTelemetry(): void {
this._isDisabled = true;
this.dispose();
async updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> {
if (telemetryLevel === undefined) {
this._injectedTelemetryLevel = TelemetryLevel.NONE;
throw new Error('Telemetry level cannot be undefined. This will cause infinite looping!');
}
// We always take the most restrictive level because we don't want multiple clients to connect and send data when one client does not consent
this._injectedTelemetryLevel = this._injectedTelemetryLevel ? Math.min(this._injectedTelemetryLevel, telemetryLevel) : telemetryLevel;
if (this._injectedTelemetryLevel === TelemetryLevel.NONE) {
this._telemetryCache = [];
this.dispose();
} else {
// Level was set we're no longer in a pending state we flush the telemetry cache.
return this.flushTelemetryCache();
}
}
}

export const RemoteNullTelemetryService = new class extends NullTelemetryServiceShape implements IRemoteTelemetryService {
permanentlyDisableTelemetry(): void { return; } // No-op, telemetry is already disabled
async updateInjectedTelemetryLevel(): Promise<void> { return; } // No-op, telemetry is already disabled
};

export const IRemoteTelemetryService = refineServiceDecorator<ITelemetryService, IRemoteTelemetryService>(ITelemetryService);
Expand Up @@ -29,7 +29,7 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot
import { IDownloadService } from 'vs/platform/download/common/download';
import { OpenLocalFileFolderCommand, OpenLocalFileCommand, OpenLocalFolderCommand, SaveLocalFileCommand, RemoteFileDialogContext } from 'vs/workbench/services/dialogs/browser/simpleFileDialog';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { TelemetryLevel, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry';
import { TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry';
import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils';

class RemoteChannelsContribution implements IWorkbenchContribution {
Expand Down Expand Up @@ -113,11 +113,7 @@ class RemoteTelemetryEnablementUpdater extends Disposable implements IWorkbenchC
}

private updateRemoteTelemetryEnablement(): Promise<void> {
if (getTelemetryLevel(this.configurationService) === TelemetryLevel.NONE) {
return this.remoteAgentService.disableTelemetry();
}

return Promise.resolve();
return this.remoteAgentService.updateTelemetryLevel(getTelemetryLevel(this.configurationService));
}
}

Expand Down
Expand Up @@ -16,7 +16,7 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics
import { Emitter } from 'vs/base/common/event';
import { ISignService } from 'vs/platform/sign/common/sign';
import { ILogService } from 'vs/platform/log/common/log';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { IProductService } from 'vs/platform/product/common/productService';
import { URI } from 'vs/base/common/uri';
Expand Down Expand Up @@ -97,22 +97,22 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I
);
}

disableTelemetry(): Promise<void> {
return this._withChannel(
channel => RemoteExtensionEnvironmentChannelClient.disableTelemetry(channel),
updateTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> {
return this._withTelemetryChannel(
channel => RemoteExtensionEnvironmentChannelClient.updateTelemetryLevel(channel, telemetryLevel),
undefined
);
}

logTelemetry(eventName: string, data: ITelemetryData): Promise<void> {
return this._withChannel(
return this._withTelemetryChannel(
channel => RemoteExtensionEnvironmentChannelClient.logTelemetry(channel, eventName, data),
undefined
);
}

flushTelemetry(): Promise<void> {
return this._withChannel(
return this._withTelemetryChannel(
channel => RemoteExtensionEnvironmentChannelClient.flushTelemetry(channel),
undefined
);
Expand All @@ -125,6 +125,14 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I
}
return connection.withChannel('remoteextensionsenvironment', (channel) => callback(channel, connection));
}

private _withTelemetryChannel<R>(callback: (channel: IChannel, connection: IRemoteAgentConnection) => Promise<R>, fallback: R): Promise<R> {
const connection = this.getConnection();
if (!connection) {
return Promise.resolve(fallback);
}
return connection.withChannel('telemetry', (channel) => callback(channel, connection));
}
}

export class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection {
Expand Down
Expand Up @@ -10,7 +10,7 @@ import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';

export interface IGetEnvironmentDataArguments {
remoteAuthority: string;
Expand Down Expand Up @@ -111,8 +111,8 @@ export class RemoteExtensionEnvironmentChannelClient {
return channel.call<IDiagnosticInfo>('getDiagnosticInfo', options);
}

static disableTelemetry(channel: IChannel): Promise<void> {
return channel.call<void>('disableTelemetry');
static updateTelemetryLevel(channel: IChannel, telemetryLevel: TelemetryLevel): Promise<void> {
return channel.call<void>('updateTelemetryLevel', { telemetryLevel });
}

static logTelemetry(channel: IChannel, eventName: string, data: ITelemetryData): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/services/remote/common/remoteAgentService.ts
Expand Up @@ -9,7 +9,7 @@ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics';
import { Event } from 'vs/base/common/event';
import { PersistentConnectionEvent, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { URI } from 'vs/base/common/uri';

Expand Down Expand Up @@ -42,7 +42,7 @@ export interface IRemoteAgentService {
*/
scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise<IExtensionDescription | null>;
getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise<IDiagnosticInfo | undefined>;
disableTelemetry(): Promise<void>;
updateTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void>;
logTelemetry(eventName: string, data?: ITelemetryData): Promise<void>;
flushTelemetry(): Promise<void>;
}
Expand Down