Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/log/vscode-node/loggingActions.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dns from 'dns';
7
import * as http from 'http';
8
import * as https from 'https';
9
import * as os from 'os';
10
import * as path from 'path';
11
import * as tls from 'tls';
12
import * as util from 'util';
13
import * as vscode from 'vscode';
14
15
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
16
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
17
import { ICAPIClientService } from '../../../platform/endpoint/common/capiClient';
18
import { IEnvService, isScenarioAutomation } from '../../../platform/env/common/envService';
19
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
20
import { collectErrorMessages, collectSingleLineErrorMessage, ILogService, sanitizeNetworkErrorForTelemetry } from '../../../platform/log/common/logService';
21
import { outputChannel } from '../../../platform/log/vscode/outputChannelLogTarget';
22
import { FetchEvent, IFetcherService } from '../../../platform/networking/common/fetcherService';
23
import { IFetcher, userAgentLibraryHeader } from '../../../platform/networking/common/networking';
24
import { NodeFetcher } from '../../../platform/networking/node/nodeFetcher';
25
import { NodeFetchFetcher } from '../../../platform/networking/node/nodeFetchFetcher';
26
import { ElectronFetcher } from '../../../platform/networking/vscode-node/electronFetcher';
27
import { getShadowedConfig } from '../../../platform/networking/vscode-node/fetcherServiceImpl';
28
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
29
30
import { shuffle } from '../../../util/vs/base/common/arrays';
31
import { timeout } from '../../../util/vs/base/common/async';
32
import { Disposable, MutableDisposable } from '../../../util/vs/base/common/lifecycle';
33
import { generateUuid } from '../../../util/vs/base/common/uuid';
34
import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation';
35
import { EXTENSION_ID } from '../../common/constants';
36
37
interface ProxyAgentLog {
38
trace(message: string, ...args: any[]): void;
39
debug(message: string, ...args: any[]): void;
40
info(message: string, ...args: any[]): void;
41
warn(message: string, ...args: any[]): void;
42
error(message: string | Error, ...args: any[]): void;
43
}
44
45
interface ProxyAgentParams {
46
log: ProxyAgentLog;
47
loadSystemCertificatesFromNode: () => boolean | undefined;
48
}
49
50
interface ProxyAgent {
51
loadSystemCertificates?(params: ProxyAgentParams): Promise<string[]>;
52
resolveProxyURL?(url: string): Promise<string | undefined>;
53
}
54
55
export class LoggingActionsContrib {
56
constructor(
57
@IVSCodeExtensionContext private readonly _context: IVSCodeExtensionContext,
58
@IEnvService private envService: IEnvService,
59
@IConfigurationService private readonly configurationService: IConfigurationService,
60
@IExperimentationService private readonly experimentationService: IExperimentationService,
61
@IAuthenticationService private readonly authService: IAuthenticationService,
62
@ICAPIClientService private readonly capiClientService: ICAPIClientService,
63
@IFetcherService private readonly fetcherService: IFetcherService,
64
@ILogService private logService: ILogService,
65
) {
66
const collectDiagnostics = async () => {
67
const document = await vscode.workspace.openTextDocument({ language: 'markdown' });
68
const editor = await vscode.window.showTextDocument(document);
69
const electronConfig = getShadowedConfig<boolean>(this.configurationService, this.experimentationService, ConfigKey.Shared.DebugUseElectronFetcher, ConfigKey.TeamInternal.DebugExpUseElectronFetcher);
70
const nodeConfig = getShadowedConfig<boolean>(this.configurationService, this.experimentationService, ConfigKey.Shared.DebugUseNodeFetcher, ConfigKey.TeamInternal.DebugExpUseNodeFetcher);
71
const nodeFetchConfig = getShadowedConfig<boolean>(this.configurationService, this.experimentationService, ConfigKey.Shared.DebugUseNodeFetchFetcher, ConfigKey.TeamInternal.DebugExpUseNodeFetchFetcher);
72
const ext = vscode.extensions.getExtension(EXTENSION_ID);
73
const product = require(path.join(vscode.env.appRoot, 'product.json'));
74
await appendText(editor, `## GitHub Copilot Chat
75
76
- Extension: ${this.envService.getVersion()} (${this.envService.getBuildType()})
77
- VS Code: ${vscode.version} (${product.commit || 'out-of-source'})
78
- OS: ${os.platform()} ${os.release()} ${os.arch()}${vscode.env.remoteName ? `
79
- Remote Name: ${vscode.env.remoteName}` : ''}${vscode.env.remoteName && ext ? `
80
- Extension Kind: ${vscode.ExtensionKind[ext.extensionKind]}` : ''}
81
- GitHub Account: ${this.authService.anyGitHubSession?.account.label || 'Signed Out'}
82
83
## Network
84
85
User Settings:
86
\`\`\`json${getNetworkSettings()}
87
"github.copilot.advanced.debug.useElectronFetcher": ${electronConfig},
88
"github.copilot.advanced.debug.useNodeFetcher": ${nodeConfig},
89
"github.copilot.advanced.debug.useNodeFetchFetcher": ${nodeFetchConfig}
90
\`\`\`${getProxyEnvVariables()}
91
`);
92
const proxyAgent = loadVSCodeModule<ProxyAgent>('@vscode/proxy-agent');
93
const loadSystemCertificatesFromNode = this.configurationService.getNonExtensionConfig<boolean>('http.systemCertificatesNode');
94
const osCertificates = proxyAgent?.loadSystemCertificates ? await loadSystemCertificates(proxyAgent.loadSystemCertificates, loadSystemCertificatesFromNode, this.logService) : undefined;
95
const urls = [
96
this.capiClientService.dotcomAPIURL,
97
this.capiClientService.capiPingURL,
98
this.capiClientService.proxyBaseURL + '/_ping',
99
];
100
const isGHEnterprise = this.capiClientService.dotcomAPIURL !== 'https://api.github.com';
101
const timeoutSeconds = 10;
102
const electronFetcher = ElectronFetcher.create(this.envService);
103
const electronCurrent = !!electronFetcher && electronConfig;
104
const nodeCurrent = !electronCurrent && nodeConfig;
105
const nodeFetchCurrent = !electronCurrent && !nodeCurrent && nodeFetchConfig;
106
const nodeCurrentFallback = !electronCurrent && !nodeFetchCurrent;
107
const activeFetcher = this.fetcherService.getUserAgentLibrary();
108
const nodeFetcher = new NodeFetcher(this.envService);
109
const fetchers = {
110
['Electron fetch']: {
111
fetcher: electronFetcher,
112
current: electronCurrent,
113
},
114
['Node.js https']: {
115
fetcher: nodeFetcher,
116
current: nodeCurrent || nodeCurrentFallback,
117
},
118
['Node.js fetch']: {
119
fetcher: new NodeFetchFetcher(this.envService),
120
current: nodeFetchCurrent,
121
},
122
};
123
const dnsLookup = util.promisify(dns.lookup);
124
for (const url of urls) {
125
const authHeaders = await this.getAuthHeaders(isGHEnterprise, url);
126
const host = new URL(url).hostname;
127
await appendText(editor, `\nConnecting to ${url}:\n`);
128
for (const family of [4, 6]) {
129
await appendText(editor, `- DNS ipv${family} Lookup: `);
130
const start = Date.now();
131
try {
132
const dnsResult = await Promise.race([dnsLookup(host, { family }), timeout(timeoutSeconds * 1000)]);
133
if (dnsResult) {
134
await appendText(editor, `${dnsResult.address} (${Date.now() - start} ms)\n`);
135
} else {
136
await appendText(editor, `timed out after ${timeoutSeconds} seconds\n`);
137
}
138
} catch (err) {
139
await appendText(editor, `Error (${Date.now() - start} ms): ${err?.message}\n`);
140
}
141
}
142
let probeProxyURL: string | undefined;
143
if (proxyAgent?.resolveProxyURL) {
144
await appendText(editor, `- Proxy URL: `);
145
const start = Date.now();
146
try {
147
const proxyURL = await Promise.race([proxyAgent.resolveProxyURL(url), timeoutAfter(timeoutSeconds * 1000)]);
148
if (proxyURL === 'timeout') {
149
await appendText(editor, `timed out after ${timeoutSeconds} seconds\n`);
150
} else {
151
await appendText(editor, `${proxyURL || 'None'} (${Date.now() - start} ms)\n`);
152
probeProxyURL = proxyURL;
153
}
154
} catch (err) {
155
await appendText(editor, `Error (${Date.now() - start} ms): ${err?.message}\n`);
156
}
157
}
158
if (proxyAgent?.loadSystemCertificates && probeProxyURL?.startsWith('https:')) {
159
const tlsOrig: typeof tls | undefined = (tls as any).__vscodeOriginal;
160
if (tlsOrig) {
161
await appendText(editor, `- Proxy TLS: `);
162
if (!osCertificates) {
163
await appendText(editor, `(failed to load system certificates) `);
164
}
165
const start = Date.now();
166
try {
167
const result = await Promise.race([tlsConnect(tlsOrig, probeProxyURL, [...tls.rootCertificates, ...(osCertificates || [])]), timeout(timeoutSeconds * 1000)]);
168
if (result) {
169
await appendText(editor, `${result} (${Date.now() - start} ms)\n`);
170
} else {
171
await appendText(editor, `timed out after ${timeoutSeconds} seconds\n`);
172
}
173
} catch (err) {
174
await appendText(editor, `Error (${Date.now() - start} ms): ${err?.message}\n`);
175
}
176
}
177
}
178
if (probeProxyURL) {
179
const httpx: typeof https | typeof http | undefined = probeProxyURL.startsWith('https:') ? (https as any).__vscodeOriginal : (http as any).__vscodeOriginal;
180
if (httpx) {
181
await appendText(editor, `- Proxy Connection: `);
182
const start = Date.now();
183
try {
184
const result = await Promise.race([proxyConnect(httpx, probeProxyURL, url), timeout(timeoutSeconds * 1000)]);
185
if (result) {
186
const headers = Object.keys(result.headers).map(header => `\n ${header}: ${result.headers[header]}`);
187
const text = `${result.statusCode} ${result.statusMessage}${headers.join('')}`;
188
await appendText(editor, `${text} (${Date.now() - start} ms)\n`);
189
} else {
190
await appendText(editor, `timed out after ${timeoutSeconds} seconds\n`);
191
}
192
} catch (err) {
193
await appendText(editor, `Error (${Date.now() - start} ms): ${err?.message}\n`);
194
}
195
}
196
}
197
for (const [name, fetcher] of Object.entries(fetchers)) {
198
await appendText(editor, `- ${name}${fetcher.current ? ' (configured)' : fetcher.fetcher?.getUserAgentLibrary() === activeFetcher ? ' (active)' : ''}: `);
199
if (fetcher.fetcher) {
200
const start = Date.now();
201
try {
202
const response = await Promise.race([fetcher.fetcher.fetch(url, { headers: authHeaders, callSite: 'diagnostics-fetcher-probe' }), timeout(timeoutSeconds * 1000)]);
203
if (response) {
204
await appendText(editor, `HTTP ${response.status} (${Date.now() - start} ms)\n`);
205
} else {
206
await appendText(editor, `timed out after ${timeoutSeconds} seconds\n`);
207
}
208
} catch (err) {
209
await appendText(editor, `Error (${Date.now() - start} ms): ${collectErrorMessages(err)}\n`);
210
}
211
} else {
212
await appendText(editor, 'Unavailable\n');
213
}
214
}
215
}
216
217
const currentFetcher = Object.values(fetchers).find(fetcher => fetcher.current)?.fetcher || nodeFetcher;
218
const secondaryUrls = [
219
{ url: 'https://mobile.events.data.microsoft.com', fetcher: currentFetcher },
220
{ url: 'https://dc.services.visualstudio.com', fetcher: currentFetcher },
221
{ url: 'https://copilot-telemetry.githubusercontent.com/_ping', fetcher: nodeFetcher },
222
{ url: vscode.Uri.parse(this.capiClientService.copilotTelemetryURL).with({ path: '/_ping' }).toString(), fetcher: nodeFetcher },
223
{ url: 'https://default.exp-tas.com', fetcher: nodeFetcher },
224
];
225
await appendText(editor, `\n`);
226
for (const { url, fetcher } of secondaryUrls) {
227
const authHeaders = await this.getAuthHeaders(isGHEnterprise, url);
228
await appendText(editor, `Connecting to ${url}: `);
229
const start = Date.now();
230
try {
231
const response = await Promise.race([fetcher.fetch(url, { headers: authHeaders, callSite: 'diagnostics-secondary-probe' }), timeout(timeoutSeconds * 1000)]);
232
if (response) {
233
await appendText(editor, `HTTP ${response.status} (${Date.now() - start} ms)\n`);
234
} else {
235
await appendText(editor, `timed out after ${timeoutSeconds} seconds\n`);
236
}
237
} catch (err) {
238
await appendText(editor, `Error (${Date.now() - start} ms): ${collectErrorMessages(err)}\n`);
239
}
240
}
241
await appendText(editor, `\nNumber of system certificates: ${osCertificates?.length ?? 'failed to load'}\n`);
242
await appendText(editor, `
243
## Documentation
244
245
In corporate networks: [Troubleshooting firewall settings for GitHub Copilot](https://docs.github.com/en/copilot/troubleshooting-github-copilot/troubleshooting-firewall-settings-for-github-copilot).`);
246
247
return document.getText();
248
};
249
this._context.subscriptions.push(vscode.commands.registerCommand('github.copilot.debug.collectDiagnostics', collectDiagnostics));
250
// Internal command is not declared in package.json so it can be used from the welcome views while the extension is being activated.
251
this._context.subscriptions.push(vscode.commands.registerCommand('github.copilot.debug.collectDiagnostics.internal', collectDiagnostics));
252
this._context.subscriptions.push(vscode.commands.registerCommand('github.copilot.debug.showOutputChannel.internal', () => outputChannel.show()));
253
this._context.subscriptions.push(new NetworkStatus(this.fetcherService, this.configurationService, this.experimentationService));
254
}
255
256
private async getAuthHeaders(isGHEnterprise: boolean, url: string) {
257
const authHeaders: Record<string, string> = {};
258
if (isGHEnterprise) {
259
let token = '';
260
if (url === this.capiClientService.dotcomAPIURL) {
261
token = this.authService.anyGitHubSession?.accessToken || '';
262
} else {
263
try {
264
token = (await this.authService.getCopilotToken()).token;
265
} catch (_err) {
266
// Ignore error
267
token = '';
268
}
269
}
270
authHeaders['Authorization'] = `Bearer ${token}`;
271
}
272
return authHeaders;
273
}
274
}
275
276
async function appendText(editor: vscode.TextEditor, string: string) {
277
await editor.edit(builder => {
278
builder.insert(editor.document.lineAt(editor.document.lineCount - 1).range.end, string);
279
});
280
}
281
282
function timeoutAfter(ms: number) {
283
return new Promise<'timeout'>(resolve => setTimeout(() => resolve('timeout'), ms));
284
}
285
286
function loadVSCodeModule<T>(moduleName: string): T | undefined {
287
const appRoot = vscode.env.appRoot;
288
try {
289
return require(`${appRoot}/node_modules.asar/${moduleName}`);
290
} catch (err) {
291
// Not in ASAR.
292
}
293
try {
294
return require(`${appRoot}/node_modules/${moduleName}`);
295
} catch (err) {
296
// Not available.
297
}
298
return undefined;
299
}
300
301
async function loadSystemCertificates(load: NonNullable<ProxyAgent['loadSystemCertificates']>, loadSystemCertificatesFromNode: boolean | undefined, logService: ILogService): Promise<(string | Buffer)[] | undefined> {
302
try {
303
const certificates = await load({
304
log: {
305
trace(message: string, ..._args: any[]) {
306
logService.trace(message);
307
},
308
debug(message: string, ..._args: any[]) {
309
logService.debug(message);
310
},
311
info(message: string, ..._args: any[]) {
312
logService.info(message);
313
},
314
warn(message: string, ..._args: any[]) {
315
logService.warn(message);
316
},
317
error(message: string | Error, ..._args: any[]) {
318
logService.error(typeof message === 'string' ? message : String(message));
319
},
320
} satisfies ProxyAgentLog,
321
loadSystemCertificatesFromNode: () => loadSystemCertificatesFromNode,
322
});
323
return Array.isArray(certificates) ? certificates : undefined;
324
} catch (err) {
325
logService.error(err);
326
return undefined;
327
}
328
}
329
330
async function tlsConnect(tlsOrig: typeof tls, proxyURL: string, ca: (string | Buffer)[]) {
331
return new Promise<string>((resolve, reject) => {
332
const proxyUrlObj = new URL(proxyURL);
333
const socket = tlsOrig.connect({
334
host: proxyUrlObj.hostname,
335
port: parseInt(proxyUrlObj.port, 10),
336
servername: proxyUrlObj.hostname,
337
ca,
338
}, () => {
339
socket.end();
340
resolve('Succeeded');
341
});
342
socket.on('error', reject);
343
});
344
}
345
346
async function proxyConnect(httpx: typeof https | typeof http, proxyUrl: string, targetUrl: string, sanitize = false) {
347
return new Promise<{ statusCode: number | undefined; statusMessage: string | undefined; headers: Record<string, string | string[]> }>((resolve, reject) => {
348
const proxyUrlObj = new URL(proxyUrl);
349
const targetUrlObj = new URL(targetUrl);
350
const targetHost = `${targetUrlObj.hostname}:${targetUrlObj.port || (targetUrlObj.protocol === 'https:' ? 443 : 80)}`;
351
const options = {
352
method: 'CONNECT',
353
host: proxyUrlObj.hostname,
354
port: proxyUrlObj.port,
355
path: targetHost,
356
headers: {
357
Host: targetHost,
358
},
359
rejectUnauthorized: false,
360
};
361
const req = httpx.request(options);
362
req.on('connect', (res, socket, head) => {
363
const headers = ['proxy-authenticate', 'proxy-agent', 'server', 'via'].reduce((acc, header) => {
364
const value = res.headers[header];
365
if (value) {
366
const doSanitize = sanitize && !['proxy-agent', 'server'].includes(header);
367
acc[header] = doSanitize ? Array.isArray(value) ? value.map(sanitizeValue) : sanitizeValue(value) : value;
368
}
369
return acc;
370
}, {} as Record<string, string | string[]>);
371
socket.end();
372
resolve({ statusCode: res.statusCode, statusMessage: res.statusMessage, headers });
373
});
374
req.on('error', reject);
375
req.end();
376
});
377
}
378
379
const networkSettingsIds = [
380
'http.proxy',
381
'http.noProxy',
382
'http.proxyAuthorization',
383
'http.proxyStrictSSL',
384
'http.proxySupport',
385
'http.electronFetch',
386
'http.fetchAdditionalSupport',
387
'http.proxyKerberosServicePrincipal',
388
'http.systemCertificates',
389
'http.systemCertificatesNode',
390
'http.experimental.systemCertificatesV2',
391
'http.useLocalProxyConfiguration',
392
];
393
const alwaysShowSettingsIds = [
394
'http.systemCertificatesNode',
395
];
396
397
function getNetworkSettings() {
398
const configuration = vscode.workspace.getConfiguration();
399
return networkSettingsIds.map(key => {
400
const i = configuration.inspect(key);
401
const v = configuration.get(key, i?.defaultValue);
402
if (alwaysShowSettingsIds.includes(key) || v !== i?.defaultValue && !(Array.isArray(v) && Array.isArray(i?.defaultValue) && v.length === 0 && i?.defaultValue.length === 0)) {
403
return `\n "${key}": ${JSON.stringify(v)},`;
404
}
405
return '';
406
}).join('');
407
}
408
409
function getProxyEnvVariables() {
410
const res = [];
411
const envVars = ['http_proxy', 'https_proxy', 'ftp_proxy', 'all_proxy', 'no_proxy'];
412
for (const env in process.env) {
413
if (envVars.includes(env.toLowerCase())) {
414
res.push(`\n- ${env}=${process.env[env]}`);
415
}
416
}
417
return res.length ? `\n\nEnvironment Variables:${res.join('')}` : '';
418
}
419
420
export class FetcherTelemetryContribution {
421
constructor(
422
@IInstantiationService instantiationService: IInstantiationService,
423
) {
424
instantiationService.invokeFunction(collectFetcherTelemetry);
425
}
426
}
427
428
function collectFetcherTelemetry(accessor: ServicesAccessor): void {
429
const extensionContext = accessor.get(IVSCodeExtensionContext);
430
const envService = accessor.get(IEnvService);
431
const logService = accessor.get(ILogService);
432
const configurationService = accessor.get(IConfigurationService);
433
const expService = accessor.get(IExperimentationService);
434
435
if (!vscode.env.isTelemetryEnabled || extensionContext.extensionMode !== vscode.ExtensionMode.Production || isScenarioAutomation) {
436
return;
437
}
438
439
if (!configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.DebugCollectFetcherTelemetry, expService)) {
440
return;
441
}
442
443
const now = Date.now();
444
const previous = extensionContext.globalState.get<number>('lastCollectFetcherTelemetryTime', 0);
445
if (now - previous < 5 * 60 * 1000) {
446
logService.debug(`Send fetcher telemetry: Skipped.`);
447
return;
448
}
449
450
(async () => {
451
await extensionContext.globalState.update('lastCollectFetcherTelemetryTime', now);
452
453
logService.debug(`Send fetcher telemetry: Exclude other windows.`);
454
const windowUUID = generateUuid();
455
await extensionContext.globalState.update('lastCollectFetcherTelemetryUUID', windowUUID);
456
await timeout(5000);
457
if (extensionContext.globalState.get<string>('lastCollectFetcherTelemetryUUID') !== windowUUID) {
458
logService.debug(`Send fetcher telemetry: Other window won.`);
459
return;
460
}
461
logService.debug(`Send fetcher telemetry: This window won.`);
462
463
const fetchers = [
464
ElectronFetcher.create(envService),
465
new NodeFetchFetcher(envService),
466
new NodeFetcher(envService),
467
].filter(fetcher => fetcher) as IFetcher[];
468
469
// Randomize to offset any order dependency in telemetry.
470
shuffle(fetchers);
471
472
// First loop: probe each fetcher with an empty body to collect connectivity results.
473
const probeResults: Record<string, string> = {};
474
for (const fetcher of fetchers) {
475
const library = fetcher.getUserAgentLibrary();
476
const key = library.replace(/-/g, '');
477
const requestStartTime = Date.now();
478
try {
479
const response = await sendRawTelemetry(fetcher, envService, extensionContext, 'GitHub.copilot-chat/fetcherTelemetryProbe', {});
480
probeResults[key] = `Status: ${response.status}`;
481
logService.debug(`Fetcher telemetry probe: ${library} ${probeResults[key]} (${Date.now() - requestStartTime}ms)`);
482
} catch (e) {
483
probeResults[key] = `Error: ${sanitizeNetworkErrorForTelemetry(collectSingleLineErrorMessage(e, true))}`;
484
logService.debug(`Fetcher telemetry probe: ${library} ${probeResults[key]} (${Date.now() - requestStartTime}ms)`);
485
}
486
}
487
488
// Second loop: send the actual telemetry event including probe results.
489
const requestGroupId = generateUuid();
490
const extensionKind = extensionContext.extension.extensionKind === vscode.ExtensionKind.UI ? 'local' : 'remote';
491
for (const fetcher of fetchers) {
492
const requestStartTime = Date.now();
493
try {
494
/* __GDPR__
495
"fetcherTelemetry" : {
496
"owner": "chrmarti",
497
"comment": "Telemetry event to test connectivity of different fetcher implementations.",
498
"requestGroupId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id to group requests from the same run." },
499
"clientLibrary": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The fetcher library used for this request." },
500
"extensionKind": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the extension runs locally or remotely." },
501
"remoteName": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The remote name, if any." },
502
"electronfetch": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Probe result for the electron-fetch fetcher." },
503
"nodefetch": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Probe result for the node-fetch fetcher." },
504
"nodehttp": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Probe result for the node-http fetcher." }
505
}
506
*/
507
const properties: Record<string, string> = {
508
requestGroupId,
509
clientLibrary: fetcher.getUserAgentLibrary(),
510
extensionKind,
511
remoteName: vscode.env.remoteName ?? 'none',
512
...probeResults,
513
};
514
const response = await sendRawTelemetry(fetcher, envService, extensionContext, 'GitHub.copilot-chat/fetcherTelemetry', properties);
515
516
logService.debug(`Fetcher telemetry: Succeeded in ${Date.now() - requestStartTime}ms using ${fetcher.getUserAgentLibrary()} with status ${response.status} (${response.statusText}).`);
517
} catch (e) {
518
logService.debug(`Fetcher telemetry: Failed in ${Date.now() - requestStartTime}ms using ${fetcher.getUserAgentLibrary()}.`);
519
}
520
}
521
})().catch(err => {
522
logService.error(err);
523
});
524
}
525
526
async function sendRawTelemetry(fetcher: IFetcher, envService: IEnvService, extensionContext: IVSCodeExtensionContext, eventName: string, properties: Record<string, string>) {
527
const url = 'https://mobile.events.data.microsoft.com/OneCollector/1.0?cors=true&content-type=application/x-json-stream';
528
const product = require(path.join(vscode.env.appRoot, 'product.json'));
529
const vscodeCommitHash: string = product.commit || '';
530
const ariaKey = (extensionContext.extension.packageJSON as { ariaKey?: string }).ariaKey ?? '';
531
const iKey = `o:${ariaKey.split('-')[0]}`;
532
const sdkVer = '1DS-Web-JS-4.3.10';
533
const eventTime = new Date(Date.now() - 10).toISOString();
534
const event = {
535
name: eventName,
536
time: eventTime,
537
ver: '4.0',
538
iKey,
539
ext: {
540
sdk: { ver: sdkVer },
541
web: { consentDetails: '{"GPC_DataSharingOptIn":false}' },
542
},
543
data: {
544
baseData: {
545
name: eventName,
546
properties: {
547
...properties,
548
'abexp.assignmentcontext': '',
549
'common.os': os.platform(),
550
'common.nodeArch': os.arch(),
551
'common.platformversion': os.release(),
552
'common.telemetryclientversion': '1.5.0',
553
'common.extname': EXTENSION_ID,
554
'common.extversion': envService.getVersion(),
555
'common.vscodemachineid': envService.machineId,
556
'common.vscodesessionid': envService.sessionId,
557
'common.vscodecommithash': vscodeCommitHash,
558
'common.sqmid': '',
559
'common.devDeviceId': envService.devDeviceId,
560
'common.vscodeversion': envService.vscodeVersion,
561
'common.vscodereleasedate': product.date || 'unknown',
562
'common.isnewappinstall': vscode.env.isNewAppInstall,
563
'common.product': envService.uiKind,
564
'common.uikind': envService.uiKind,
565
'common.remotename': envService.remoteName ?? 'none',
566
'version': 'PostChannel=4.3.10',
567
},
568
},
569
},
570
};
571
const body = JSON.stringify(event);
572
const headers: Record<string, string> = {
573
'Client-Id': 'NO_AUTH',
574
'client-version': sdkVer,
575
'apikey': ariaKey,
576
'upload-time': String(Date.now()),
577
'time-delta-to-apply-millis': 'use-collector-delta',
578
'cache-control': 'no-cache, no-store',
579
'content-type': 'application/x-json-stream',
580
'User-Agent': `GitHubCopilotChat/${envService.getVersion()}`,
581
[userAgentLibraryHeader]: fetcher.getUserAgentLibrary(),
582
};
583
if (fetcher.getUserAgentLibrary() === NodeFetcher.ID) {
584
headers['content-length'] = String(Buffer.byteLength(body));
585
}
586
const response = await fetcher.fetch(url, {
587
method: 'POST',
588
headers,
589
body,
590
callSite: 'diagnostics-telemetry-probe',
591
});
592
await response.text();
593
return response;
594
}
595
596
const ids_paths = /(^|\b)[\p{L}\p{Nd}]+((=""?[^"]+""?)|(([.:=/"_-]+[\p{L}\p{Nd}]+)+))(\b|$)/giu;
597
export function sanitizeValue(input: string | undefined): string {
598
return (input || '').replace(ids_paths, (m) => maskByClass(m));
599
}
600
601
function maskByClass(s: string): string {
602
if (/^net::[A-Z_]+$/.test(s) || ['dev-container', 'attached-container', 'k8s-container', 'ssh-remote'].includes(s)) {
603
return s;
604
}
605
return s.replace(/\p{Lu}|\p{Ll}|\p{Nd}/gu, (ch) => {
606
if (/\p{Lu}/u.test(ch)) {
607
return 'A';
608
}
609
if (/\p{Ll}/u.test(ch)) {
610
return 'a';
611
}
612
return '0';
613
});
614
}
615
616
class NetworkStatus extends Disposable {
617
618
private readonly _statusBarItem: vscode.StatusBarItem;
619
private readonly _events: FetchEvent[] = [];
620
private readonly _fetchSubscription = this._register(new MutableDisposable());
621
622
constructor(private readonly _fetcherService: IFetcherService, private readonly _configurationService: IConfigurationService, private readonly _experimentationService: IExperimentationService) {
623
super();
624
this._statusBarItem = this._register(vscode.window.createStatusBarItem('copilot.networkStatus', vscode.StatusBarAlignment.Right, -1000));
625
this._statusBarItem.name = 'Copilot Network Status';
626
this._register(_configurationService.onDidChangeConfiguration(e => {
627
if (e.affectsConfiguration(ConfigKey.TeamInternal.DebugShowNetworkStatus.fullyQualifiedId)) {
628
this._update();
629
}
630
}));
631
this._update();
632
}
633
634
private _isEnabled(): boolean {
635
return this._configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.DebugShowNetworkStatus, this._experimentationService);
636
}
637
638
private _onEvent(event: FetchEvent): void {
639
this._events.push(event);
640
const cutoff = Date.now() - 5 * 60 * 1000;
641
while (this._events.length > 0 && this._events[0].timestamp < cutoff) {
642
this._events.shift();
643
}
644
this._update();
645
}
646
647
private _update(): void {
648
const enabled = this._isEnabled();
649
if (enabled && !this._fetchSubscription.value) {
650
this._fetchSubscription.value = this._fetcherService.onDidFetch(e => this._onEvent(e));
651
} else if (!enabled) {
652
this._fetchSubscription.value = undefined;
653
this._events.length = 0;
654
this._statusBarItem.hide();
655
return;
656
}
657
const latestById = new Map<string, FetchEvent>();
658
for (const e of this._events) {
659
latestById.set(e.internalId, e);
660
}
661
const latest = [...latestById.values()];
662
const errors = latest.filter(e => e.outcome === 'error');
663
this._statusBarItem.text = `Copilot Network: ${errors.length} errors / ${latest.length} total`;
664
665
const byHostname = new Map<string, { total: number; errors: number; cancellations: number }>();
666
for (const e of latest) {
667
let entry = byHostname.get(e.hostname);
668
if (!entry) {
669
entry = { total: 0, errors: 0, cancellations: 0 };
670
byHostname.set(e.hostname, entry);
671
}
672
entry.total++;
673
if (e.outcome === 'error') {
674
entry.errors++;
675
} else if (e.outcome === 'cancel') {
676
entry.cancellations++;
677
}
678
}
679
const tooltip = new vscode.MarkdownString();
680
tooltip.appendMarkdown(`| Hostname | Errors | Cancellations | Total |\n`);
681
tooltip.appendMarkdown(`|:--|--:|--:|--:|\n`);
682
for (const [hostname, { total, errors, cancellations }] of [...byHostname].sort((a, b) => b[1].total - a[1].total)) {
683
tooltip.appendMarkdown(`| ${hostname} | ${errors} | ${cancellations} | ${total} |\n`);
684
}
685
tooltip.appendMarkdown(`\n**${errors.length}** of **${latest.length}** network requests failed in the last 5 minutes`);
686
this._statusBarItem.tooltip = tooltip;
687
688
this._statusBarItem.show();
689
}
690
}
691
692