Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/vs/base/parts/ipc/node/ipc.cp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export class Client implements IChannelClient, IDisposable {
this._client = null;
}

get isConnected(): boolean {
return this.child?.connected ?? false;
}

getChannel<T extends IChannel>(channelName: string): T {
const that = this;

Expand Down
8 changes: 5 additions & 3 deletions src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,9 +755,11 @@ export class CodeApplication extends Disposable {
// This manager self-disposes after its lifecycle join; CodeApplication disposes before later shutdown listeners run.
appInstantiationService.createInstance(AgentHostProcessManager, agentHostStarter, process.platform);

// Metered Connection
// Metered connection telemetry
appInstantiationService.invokeFunction(accessor => {
(accessor.get(IMeteredConnectionService) as MeteredConnectionMainService).start();
const meteredConnectionService = accessor.get(IMeteredConnectionService) as MeteredConnectionMainService;
meteredConnectionService.setTelemetryService(accessor.get(ITelemetryService));
meteredConnectionService.start();
});

// Auth Handler
Expand Down Expand Up @@ -1305,7 +1307,7 @@ export class CodeApplication extends Disposable {
const appender = new TelemetryAppenderClient(channel);
const commonProperties = resolveCommonProperties(release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, sqmId, devDeviceId, isInternal, this.productService.date);
const piiPaths = getPiiPathsFromEnvironment(this.environmentMainService);
const config: ITelemetryServiceConfig = { appenders: [appender], commonProperties, piiPaths, sendErrorTelemetry: true };
const config: ITelemetryServiceConfig = { appenders: [appender], commonProperties, piiPaths, sendErrorTelemetry: true, meteredConnectionService };

services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [config], false));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
const logAppender = new TelemetryLogAppender('', false, loggerService, environmentService, productService);
appenders.push(logAppender);
if (!isLoggingOnly(productService, environmentService) && productService.aiConfig?.ariaKey) {
const collectorAppender = new OneDataSystemAppender(requestService, internalTelemetry, 'monacoworkbench', null, productService.aiConfig.ariaKey);
const collectorAppender = new OneDataSystemAppender(requestService, internalTelemetry, 'monacoworkbench', null, productService.aiConfig.ariaKey, meteredConnectionService);
this._register(toDisposable(() => collectorAppender.flush())); // Ensure the 1DS appender is disposed so that it flushes remaining data
appenders.push(collectorAppender);
}
Expand All @@ -343,6 +343,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, this.configuration.sqmId, this.configuration.devDeviceId, internalTelemetry, productService.date),
sendErrorTelemetry: true,
piiPaths: getPiiPathsFromEnvironment(environmentService),
meteredConnectionService,
}, configurationService, productService);
} else {
telemetryService = NullTelemetryService;
Expand All @@ -354,7 +355,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
services.set(ITelemetryService, telemetryService);

// Custom Endpoint Telemetry
const customEndpointTelemetryService = new CustomEndpointTelemetryService(configurationService, telemetryService, loggerService, environmentService, productService);
const customEndpointTelemetryService = this._register(new CustomEndpointTelemetryService(configurationService, telemetryService, loggerService, environmentService, productService, meteredConnectionService));
services.set(ICustomEndpointTelemetryService, customEndpointTelemetryService);

// Extension Management
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { MeteredConnectionMonitor, MeteredConnectionState } from '@vscode/m
import { DeferredPromise, raceTimeout } from '../../../base/common/async.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { ILogService } from '../../log/common/log.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { AbstractMeteredConnectionService } from '../common/meteredConnection.js';

type MonitorFactory = () => Promise<MeteredConnectionMonitor>;
Expand All @@ -27,6 +28,7 @@ async function createMonitor(): Promise<MeteredConnectionMonitor> {
* This implementation receives metered connection updates from the operating system.
*/
export class MeteredConnectionMainService extends AbstractMeteredConnectionService {
private telemetryService: ITelemetryService | undefined;
private readonly monitorFactory: MonitorFactory;
private readonly initialized = new DeferredPromise<void>();
private readonly initializationTimeout: number;
Expand All @@ -43,6 +45,10 @@ export class MeteredConnectionMainService extends AbstractMeteredConnectionServi
this.initializationTimeout = options?.initializationTimeout ?? INITIALIZATION_TIMEOUT;
}

public setTelemetryService(telemetryService: ITelemetryService): void {
this.telemetryService = telemetryService;
}

public start(): void {
if (!this.started) {
this.started = true;
Expand Down Expand Up @@ -100,4 +106,28 @@ export class MeteredConnectionMainService extends AbstractMeteredConnectionServi
return false;
}
}

protected override onChangeUnderlyingConnection() {
// Fire event after sending telemetry if switching to metered since telemetry will be paused.
const fireAfter = this.isUnderlyingConnectionMetered;
if (!fireAfter) {
super.onChangeUnderlyingConnection();
}

type MeteredConnectionStateChangeEvent = {
connectionState: boolean;
};
type MeteredConnectionStateChangeClassification = {
owner: 'dmitrivMS';
comment: 'Tracks metered network connection state changes to understand usage patterns.';
connectionState: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the underlying network connection is metered according to the OS.' };
};
this.telemetryService?.publicLog2<MeteredConnectionStateChangeEvent, MeteredConnectionStateChangeClassification>('meteredConnectionStateChange', {
connectionState: this.isUnderlyingConnectionMetered,
});

if (fireAfter) {
super.onChangeUnderlyingConnection();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Emitter } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IMeteredConnectionService } from '../../common/meteredConnection.js';

export class TestMeteredConnectionService extends Disposable implements IMeteredConnectionService {
declare readonly _serviceBrand: undefined;

private readonly _onDidChangeIsConnectionMetered = this._register(new Emitter<boolean>());
readonly onDidChangeIsConnectionMetered = this._onDidChangeIsConnectionMetered.event;

constructor(
public isConnectionMetered: boolean,
readonly whenInitialized: Promise<void> = Promise.resolve(),
) {
super();
}

setIsConnectionMetered(isMetered: boolean): void {
this.isConnectionMetered = isMetered;
this._onDidChangeIsConnectionMetered.fire(isMetered);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Emitter } from '../../../../base/common/event.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js';
import { NullLogService } from '../../../log/common/log.js';
import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js';
import { MeteredConnectionCommand } from '../../common/meteredConnectionIpc.js';
import { MeteredConnectionChannel } from '../../electron-main/meteredConnectionChannel.js';
import { MeteredConnectionMainService } from '../../electron-main/meteredConnectionMainService.js';
Expand Down Expand Up @@ -46,6 +47,7 @@ suite('MeteredConnectionMainService', () => {
store.add(configurationService.onDidChangeConfigurationEmitter);
const monitor = new TestMeteredConnectionMonitor();
const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService()));
service.setTelemetryService(NullTelemetryService);
service.start();
let initialized = false;
void service.whenInitialized.then(() => initialized = true);
Expand Down Expand Up @@ -73,6 +75,7 @@ suite('MeteredConnectionMainService', () => {
store.add(configurationService.onDidChangeConfigurationEmitter);
const monitor = new TestMeteredConnectionMonitor();
const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService()));
service.setTelemetryService(NullTelemetryService);
service.start();
const changes: boolean[] = [];
store.add(service.onDidChangeIsConnectionMetered(state => changes.push(state)));
Expand Down Expand Up @@ -108,6 +111,7 @@ suite('MeteredConnectionMainService', () => {
store.add(configurationService.onDidChangeConfigurationEmitter);
const monitor = new TestMeteredConnectionMonitor();
const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService()));
service.setTelemetryService(NullTelemetryService);
service.start();
const channel = new MeteredConnectionChannel(service);
let resolved = false;
Expand Down Expand Up @@ -135,6 +139,7 @@ suite('MeteredConnectionMainService', () => {
monitorFactory: async () => monitor,
initializationTimeout: 0,
}, configurationService, new NullLogService()));
service.setTelemetryService(NullTelemetryService);
service.start();

await service.whenInitialized;
Expand All @@ -158,6 +163,7 @@ suite('MeteredConnectionMainService', () => {
monitorFactory: () => monitorPromise.p,
initializationTimeout: 0,
}, configurationService, new NullLogService()));
service.setTelemetryService(NullTelemetryService);
service.start();

await service.whenInitialized;
Expand All @@ -176,6 +182,7 @@ suite('MeteredConnectionMainService', () => {
store.add(configurationService.onDidChangeConfigurationEmitter);
const monitor = new TestMeteredConnectionMonitor();
const service = store.add(new MeteredConnectionMainService({ monitorFactory: async () => monitor }, configurationService, new NullLogService()));
service.setTelemetryService(NullTelemetryService);
service.start();
await timeout(0);

Expand All @@ -199,6 +206,7 @@ suite('MeteredConnectionMainService', () => {
const monitor = new TestMeteredConnectionMonitor();
const monitorPromise = new DeferredPromise<MeteredConnectionMonitor>();
const service = store.add(new MeteredConnectionMainService({ monitorFactory: () => monitorPromise.p }, configurationService, new NullLogService()));
service.setTelemetryService(NullTelemetryService);
service.start();

service.dispose();
Expand Down
57 changes: 52 additions & 5 deletions src/vs/platform/telemetry/common/1dsAppender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export interface IAppInsightsCore {
unload(isAsync: boolean, unloadComplete: (unloadState: ITelemetryUnloadState) => void): void;
}

interface IAppInsightsClient {
readonly core: IAppInsightsCore;
readonly transmissionController: Pick<PostChannel, 'pause' | 'resume'>;
}

const endpointUrl = 'https://mobile.events.data.microsoft.com/OneCollector/1.0';
const endpointHealthUrl = 'https://mobile.events.data.microsoft.com/ping';

Expand All @@ -39,7 +44,7 @@ export function applyEnvelopeDefaults(envelope: ITelemetryItem, isInternalMachin
}
}

async function getClient(instrumentationKey: string, addInternalFlag?: boolean, xhrOverride?: IXHROverride): Promise<IAppInsightsCore> {
async function getClient(instrumentationKey: string, addInternalFlag?: boolean, xhrOverride?: IXHROverride): Promise<IAppInsightsClient> {
// eslint-disable-next-line local/code-amd-node-module
const oneDs = isWeb ? await importAMDNodeModule<typeof import('@microsoft/1ds-core-js')>('@microsoft/1ds-core-js', 'bundle/ms.core.min.js') : await import('@microsoft/1ds-core-js');
// eslint-disable-next-line local/code-amd-node-module
Expand Down Expand Up @@ -76,14 +81,19 @@ async function getClient(instrumentationKey: string, addInternalFlag?: boolean,

appInsightsCore.addTelemetryInitializer(envelope => applyEnvelopeDefaults(envelope, addInternalFlag));

return appInsightsCore;
return {
core: appInsightsCore,
transmissionController: collectorChannelPlugin,
};
}

// TODO @lramos15 maybe make more in line with src/vs/platform/telemetry/browser/appInsightsAppender.ts with caching support
export abstract class AbstractOneDataSystemAppender implements ITelemetryAppender {

protected _aiCoreOrKey: IAppInsightsCore | string | undefined;
private _asyncAiCore: Promise<IAppInsightsCore> | null;
private _transmissionController: Pick<PostChannel, 'pause' | 'resume'> | undefined;
private _isTransmissionPaused = false;
protected readonly endPointUrl = endpointUrl;
protected readonly endPointHealthUrl = endpointHealthUrl;

Expand All @@ -106,6 +116,30 @@ export abstract class AbstractOneDataSystemAppender implements ITelemetryAppende
this._asyncAiCore = null;
}

protected get isTransmissionPaused(): boolean {
return this._isTransmissionPaused;
}

protected setTransmissionController(transmissionController: Pick<PostChannel, 'pause' | 'resume'>): void {
this._transmissionController = transmissionController;
if (this.isTransmissionPaused) {
transmissionController.pause();
}
}

protected setTransmissionPaused(isPaused: boolean): void {
if (this.isTransmissionPaused === isPaused) {
return;
}

this._isTransmissionPaused = isPaused;
if (isPaused) {
this._transmissionController?.pause();
} else {
this._transmissionController?.resume();
}
}

private _withAIClient(callback: (aiCore: IAppInsightsCore) => void): void {
if (!this._aiCoreOrKey) {
return;
Expand All @@ -117,7 +151,10 @@ export abstract class AbstractOneDataSystemAppender implements ITelemetryAppende
}

if (!this._asyncAiCore) {
this._asyncAiCore = getClient(this._aiCoreOrKey, this._isInternalTelemetry, this._xhrOverride);
this._asyncAiCore = getClient(this._aiCoreOrKey, this._isInternalTelemetry, this._xhrOverride).then(client => {
this.setTransmissionController(client.transmissionController);
return client.core;
});
}

this._asyncAiCore.then(
Expand All @@ -132,7 +169,7 @@ export abstract class AbstractOneDataSystemAppender implements ITelemetryAppende
}

log(eventName: string, data?: unknown): void {
if (!this._aiCoreOrKey) {
if (!this._aiCoreOrKey || this.isTransmissionPaused) {
return;
}
data = mixin(data, this._defaultData);
Expand All @@ -141,6 +178,10 @@ export abstract class AbstractOneDataSystemAppender implements ITelemetryAppende

try {
this._withAIClient((aiClient) => {
if (this.isTransmissionPaused) {
return;
}

aiClient.pluginVersionString = validatedData?.properties.version ?? 'Unknown';
aiClient.track({
name,
Expand All @@ -151,11 +192,17 @@ export abstract class AbstractOneDataSystemAppender implements ITelemetryAppende
}

flush(): Promise<void> {
if (this._aiCoreOrKey) {
if (this._aiCoreOrKey && !this.isTransmissionPaused) {
return new Promise(resolve => {
this._withAIClient((aiClient) => {
if (this.isTransmissionPaused) {
resolve();
return;
}

aiClient.unload(true, () => {
this._aiCoreOrKey = undefined;
this._transmissionController = undefined;
resolve(undefined);
});
});
Expand Down
Loading