From ef42b49e806b4a4f1202ec2e31f441465a7047de Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Fri, 14 Jul 2023 11:53:47 -0700 Subject: [PATCH] Support proxies in the telemetry service (#187952) --- src/vs/code/node/cliProcessMain.ts | 5 +- .../node/sharedProcess/sharedProcessMain.ts | 5 +- src/vs/platform/telemetry/node/1dsAppender.ts | 89 +++++++++++++++---- src/vs/server/node/serverServices.ts | 5 +- .../contrib/debug/node/telemetryApp.ts | 2 +- 5 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 0065a8da62e..a003267c043 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -192,7 +192,8 @@ class CliMain extends Disposable { services.set(IUriIdentityService, new UriIdentityService(fileService)); // Request - services.set(IRequestService, new SyncDescriptor(RequestService, undefined, true)); + const requestService = new RequestService(configurationService, environmentService, logService, loggerService); + services.set(IRequestService, requestService); // Download Service services.set(IDownloadService, new SyncDescriptor(DownloadService, undefined, true)); @@ -212,7 +213,7 @@ class CliMain extends Disposable { const isInternal = isInternalTelemetry(productService, configurationService); if (supportsTelemetry(productService, environmentService)) { if (productService.aiConfig && productService.aiConfig.ariaKey) { - appenders.push(new OneDataSystemAppender(isInternal, 'monacoworkbench', null, productService.aiConfig.ariaKey)); + appenders.push(new OneDataSystemAppender(requestService, isInternal, 'monacoworkbench', null, productService.aiConfig.ariaKey)); } const config: ITelemetryServiceConfig = { diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index d8b6d791e74..f911b0da431 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -251,7 +251,8 @@ class SharedProcessMain extends Disposable { services.set(IUriIdentityService, uriIdentityService); // Request - services.set(IRequestService, new RequestChannelClient(mainProcessService.getChannel('request'))); + const requestService = new RequestChannelClient(mainProcessService.getChannel('request')); + services.set(IRequestService, requestService); // Checksum services.set(IChecksumService, new SyncDescriptor(ChecksumService, undefined, false /* proxied to other processes */)); @@ -279,7 +280,7 @@ class SharedProcessMain extends Disposable { const logAppender = new TelemetryLogAppender(logService, loggerService, environmentService, productService); appenders.push(logAppender); if (productService.aiConfig?.ariaKey) { - const collectorAppender = new OneDataSystemAppender(internalTelemetry, 'monacoworkbench', null, productService.aiConfig.ariaKey); + const collectorAppender = new OneDataSystemAppender(requestService, internalTelemetry, 'monacoworkbench', null, productService.aiConfig.ariaKey); this._register(toDisposable(() => collectorAppender.flush())); // Ensure the 1DS appender is disposed so that it flushes remaining data appenders.push(collectorAppender); } diff --git a/src/vs/platform/telemetry/node/1dsAppender.ts b/src/vs/platform/telemetry/node/1dsAppender.ts index 8d3d82ca50c..94f0dc30692 100644 --- a/src/vs/platform/telemetry/node/1dsAppender.ts +++ b/src/vs/platform/telemetry/node/1dsAppender.ts @@ -4,41 +4,100 @@ *--------------------------------------------------------------------------------------------*/ import type { IPayloadData, IXHROverride } from '@microsoft/1ds-post-js'; -import * as https from 'https'; +import { streamToBuffer } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IRequestOptions } from 'vs/base/parts/request/common/request'; +import { IRequestService } from 'vs/platform/request/common/request'; import { AbstractOneDataSystemAppender, IAppInsightsCore } from 'vs/platform/telemetry/common/1dsAppender'; +/** + * Completes a request to submit telemetry to the server utilizing the request service + * @param options The options which will be used to make the request + * @param requestService The request service + * @returns An object containing the headers, statusCode, and responseData + */ +async function makeTelemetryRequest(options: IRequestOptions, requestService: IRequestService) { + const response = await requestService.request(options, CancellationToken.None); + const responseData = (await streamToBuffer(response.stream)).toString(); + const statusCode = response.res.statusCode ?? 200; + const headers = response.res.headers as Record; + return { + headers, + statusCode, + responseData + }; +} + +/** + * Complete a request to submit telemetry to the server utilizing the https module. Only used when the request service is not available + * @param options The options which will be used to make the request + * @param httpsModule The https node module + * @returns An object containing the headers, statusCode, and responseData + */ +function makeLegacyTelemetryRequest(options: IRequestOptions, httpsModule: typeof import('https')) { + const httpsOptions = { + method: options.type, + headers: options.headers + }; + const req = httpsModule.request(options.url ?? '', httpsOptions, res => { + res.on('data', function (responseData) { + return { + headers: res.headers as Record, + statusCode: res.statusCode ?? 200, + responseData: responseData.toString() + }; + }); + // On response with error send status of 0 and a blank response to oncomplete so we can retry events + res.on('error', function (err) { + throw err; + }); + }); + req.write(options.data); + req.end(); + return; +} + export class OneDataSystemAppender extends AbstractOneDataSystemAppender { constructor( + requestService: IRequestService | undefined, isInternalTelemetry: boolean, eventPrefix: string, defaultData: { [key: string]: any } | null, iKeyOrClientFactory: string | (() => IAppInsightsCore), // allow factory function for testing ) { + let httpsModule: typeof import('https') | undefined; + if (!requestService) { + httpsModule = require('https'); + } // Override the way events get sent since node doesn't have XHTMLRequest const customHttpXHROverride: IXHROverride = { sendPOST: (payload: IPayloadData, oncomplete) => { - const options = { - method: 'POST', + + const telemetryRequestData = typeof payload.data === 'string' ? payload.data : new TextDecoder().decode(payload.data); + const requestOptions: IRequestOptions = { + type: 'POST', headers: { ...payload.headers, 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(payload.data) - } + 'Content-Length': Buffer.byteLength(payload.data).toString() + }, + url: payload.urlString, + data: telemetryRequestData }; + try { - const req = https.request(payload.urlString, options, res => { - res.on('data', function (responseData) { - oncomplete(res.statusCode ?? 200, res.headers as Record, responseData.toString()); + if (requestService) { + makeTelemetryRequest(requestOptions, requestService).then(({ statusCode, headers, responseData }) => { + oncomplete(statusCode, headers, responseData); }); - // On response with error send status of 0 and a blank response to oncomplete so we can retry events - res.on('error', function (err) { - oncomplete(0, {}); - }); - }); - req.write(payload.data); - req.end(); + } else { + if (!httpsModule) { + throw new Error('https module is undefined'); + } + makeLegacyTelemetryRequest(requestOptions, httpsModule); + } } catch { // If it errors out, send status of 0 and a blank response to oncomplete so we can retry events oncomplete(0, {}); diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 910bbe0e5d1..d291af5dd1c 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -144,13 +144,14 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IExtensionHostStatusService, extensionHostStatusService); // Request - services.set(IRequestService, new SyncDescriptor(RequestService)); + const requestService = new RequestService(configurationService, environmentService, logService, loggerService); + services.set(IRequestService, requestService); let oneDsAppender: ITelemetryAppender = NullAppender; const isInternal = isInternalTelemetry(productService, configurationService); if (supportsTelemetry(productService, environmentService)) { if (productService.aiConfig && productService.aiConfig.ariaKey) { - oneDsAppender = new OneDataSystemAppender(isInternal, eventPrefix, null, productService.aiConfig.ariaKey); + oneDsAppender = new OneDataSystemAppender(requestService, isInternal, eventPrefix, null, productService.aiConfig.ariaKey); disposables.add(toDisposable(() => oneDsAppender?.flush())); // Ensure the AI appender is disposed so that it flushes remaining data } diff --git a/src/vs/workbench/contrib/debug/node/telemetryApp.ts b/src/vs/workbench/contrib/debug/node/telemetryApp.ts index 6296aeb862e..802a806486f 100644 --- a/src/vs/workbench/contrib/debug/node/telemetryApp.ts +++ b/src/vs/workbench/contrib/debug/node/telemetryApp.ts @@ -7,7 +7,7 @@ import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; import { OneDataSystemAppender } from 'vs/platform/telemetry/node/1dsAppender'; -const appender = new OneDataSystemAppender(false, process.argv[2], JSON.parse(process.argv[3]), process.argv[4]); +const appender = new OneDataSystemAppender(undefined, false, process.argv[2], JSON.parse(process.argv[3]), process.argv[4]); process.once('exit', () => appender.flush()); const channel = new TelemetryAppenderChannel([appender]);