Skip to content

Commit 0cb3115

Browse files
committed
Adds support for sending telemetry in a test environment
1 parent e412a10 commit 0cb3115

File tree

4 files changed

+217
-45
lines changed

4 files changed

+217
-45
lines changed

lib/telemetryReporter.d.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface TelemetryEventMeasurements {
1717
/**
1818
* A replacement option for the app insights client. This allows the appender to filter out any sensitive or unnecessary information from the telemetry server.
1919
*/
20-
export interface ReplacementOption {
20+
export interface ReplacementOption {
2121

2222
/**
2323
* A regular expression matching any property to be removed or replaced from the telemetry server.
@@ -62,6 +62,16 @@ export default class TelemetryReporter {
6262
*/
6363
sendRawTelemetryEvent(eventName: string, properties?: RawTelemetryEventProperties, measurements?: TelemetryEventMeasurements): void;
6464

65+
/**
66+
* **DANGEROUS** Given an event name, some properties, and measurements sends a telemetry event without checking telemetry setting
67+
* Do not use unless in a controlled environment i.e. sending telmetry from a CI pipeline or testing during development
68+
* @param eventName The name of the event
69+
* @param properties The properties to send with the event
70+
* @param measurements The measurements (numeric values) to send with the event
71+
* @param sanitize Whether or not to sanitize to the properties and measures, defaults to true
72+
*/
73+
sendDangerousTelemetryEvent(eventName: string, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements, sanitize?: boolean): void;
74+
6575
/**
6676
* Sends a telemetry error event with the given properties, measurements, and errorProps
6777
* @param eventName The name of the event
@@ -71,6 +81,17 @@ export default class TelemetryReporter {
7181
*/
7282
sendTelemetryErrorEvent(eventName: string, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements, errorProps?: string[]): void;
7383

84+
/**
85+
* **DANGEROUS** Given an event name, some properties, and measurements sends a telemetry error event without checking telemetry setting
86+
* Do not use unless in a controlled environment i.e. sending telmetry from a CI pipeline or testing during development
87+
* @param eventName The name of the event
88+
* @param properties The properties to send with the event
89+
* @param measurements The measurements (numeric values) to send with the event
90+
* @param errorProps If not present then we assume all properties belong to the error prop and will be anonymized
91+
* @param sanitize Whether or not to run the properties and measures through sanitiziation, defaults to true
92+
*/
93+
sendDangerousTelemetryErrorEvent(eventName: string, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements, errorProps?: string[], sanitize?: boolean): void;
94+
7495
/**
7596
* Sends an exception which includes the error stack, properties, and measurements
7697
* @param error The error to send
@@ -79,6 +100,16 @@ export default class TelemetryReporter {
79100
*/
80101
sendTelemetryException(error: Error, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements): void;
81102

103+
/**
104+
* **DANGEROUS** Given an error, properties, and measurements. Sends an exception event without checking the telemetry setting
105+
* Do not use unless in a controlled environment i.e. sending telmetry from a CI pipeline or testing during development
106+
* @param eventName The name of the event
107+
* @param properties The properties to send with the event
108+
* @param measurements The measurements (numeric values) to send with the event
109+
* @param sanitize Whether or not to sanitize to the properties and measures, defaults to true
110+
*/
111+
sendDangerousTelemetryException(error: Error, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements, sanitize?: boolean): void
112+
82113
/**
83114
* Disposes of the telemetry reporter. This flushes the remaining events and disposes of the telemetry client.
84115
*/

src/browser/telemetryReporter.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import type { ApplicationInsights } from "@microsoft/applicationinsights-web";
66
import { BaseTelemetryAppender, BaseTelemetryClient } from "../common/baseTelemetryAppender";
77
import { AppenderData, BaseTelemetryReporter, ReplacementOption } from "../common/baseTelemetryReporter";
8-
import { applyReplacements, getTelemetryLevel, TelemetryLevel } from "../common/util";
8+
import { applyReplacements } from "../common/util";
99

1010

1111
const webAppInsightsClientFactory = async (key: string, replacementOptions?: ReplacementOption[]): Promise<BaseTelemetryClient> => {
@@ -33,8 +33,7 @@ const webAppInsightsClientFactory = async (key: string, replacementOptions?: Rep
3333
appInsightsClient.loadAppInsights();
3434
// If we cannot access the endpoint this most likely means it's being blocked
3535
// and we should not attempt to send any telemetry.
36-
const telemetryLevel = getTelemetryLevel();
37-
if (endpointUrl && telemetryLevel !== TelemetryLevel.OFF) {
36+
if (endpointUrl) {
3837
fetch(endpointUrl).catch(() => (appInsightsClient = undefined));
3938
}
4039
} catch (e) {

src/common/baseTelemetryAppender.ts

+45-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
44

5-
import { AppenderData, ITelemetryAppender } from "./baseTelemetryReporter";
5+
import { AppenderData } from "./baseTelemetryReporter";
66
import { getTelemetryLevel, TelemetryLevel } from "./util";
77

88
export interface BaseTelemetryClient {
@@ -11,6 +11,15 @@ export interface BaseTelemetryClient {
1111
flush(): void | Promise<void>;
1212
}
1313

14+
export interface ITelemetryAppender {
15+
logEvent(eventName: string, data?: AppenderData): void;
16+
logEventDangerously(eventName: string, data?: AppenderData): void;
17+
logException(exception: Error, data?: AppenderData): void;
18+
logExceptionDangerously(exception: Error, data?: AppenderData): void;
19+
flush(): void | Promise<void>;
20+
instantiateAppender(): void;
21+
}
22+
1423
export class BaseTelemetryAppender implements ITelemetryAppender {
1524
// Whether or not the client has been instantiated
1625
private _isInstantiated = false;
@@ -34,7 +43,7 @@ export class BaseTelemetryAppender implements ITelemetryAppender {
3443

3544
/**
3645
* Sends the event to the passed in telemetry client
37-
* @param eventName The named of the event to log
46+
* @param eventName The name of the event to log
3847
* @param data The data contanied in the event
3948
*/
4049
logEvent(eventName: string, data?: AppenderData): void {
@@ -47,6 +56,40 @@ export class BaseTelemetryAppender implements ITelemetryAppender {
4756
this._telemetryClient.logEvent(eventName, data);
4857
}
4958

59+
/**
60+
* **DANGEROUS**: Logs an event regardless of telemetry level.
61+
* This should only be used in controlled environments i.e. CI pipelines
62+
* @param eventName The named of the event to log
63+
* @param data The data contained in the event
64+
*/
65+
logEventDangerously(eventName: string, data?: AppenderData): void {
66+
if (!this._telemetryClient) {
67+
return;
68+
}
69+
if (!this._isInstantiated) {
70+
this._eventQueue.push({ eventName, data });
71+
} else {
72+
this._telemetryClient.logEvent(eventName, data);
73+
}
74+
}
75+
76+
/**
77+
* **DANGEROUS**: Logs an exception regardless of telemetry level.
78+
* This should only be used in controlled environments i.e. CI pipelines
79+
* @param exception The exception to log
80+
* @param data The data associated with the exception
81+
*/
82+
logExceptionDangerously(exception: Error, data?: AppenderData): void {
83+
if (!this._telemetryClient) {
84+
return;
85+
}
86+
if (!this._isInstantiated) {
87+
this._exceptionQueue.push({ exception, data });
88+
} else {
89+
this._telemetryClient.logException(exception, data);
90+
}
91+
}
92+
5093
/**
5194
* Sends an exception to the passed in telemetry client
5295
* @param exception The exception to collect

src/common/baseTelemetryReporter.ts

+138-39
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,18 @@
44

55
import * as vscode from "vscode";
66
import type { TelemetryEventMeasurements, TelemetryEventProperties, RawTelemetryEventProperties } from "../../lib/telemetryReporter";
7+
import { ITelemetryAppender } from "./baseTelemetryAppender";
78
import { getTelemetryLevel, TelemetryLevel } from "./util";
89

910
export interface AppenderData {
1011
properties?: RawTelemetryEventProperties,
1112
measurements?: TelemetryEventMeasurements
1213
}
13-
export interface ITelemetryAppender {
14-
logEvent(eventName: string, data?: AppenderData): void;
15-
logException(exception: Error, data?: AppenderData): void;
16-
flush(): void | Promise<void>;
17-
instantiateAppender(): void;
18-
}
1914

2015
/**
2116
* A replacement option for the app insights client. This allows the appender to filter out any sensitive or unnecessary information from the telemetry server.
2217
*/
23-
export interface ReplacementOption {
18+
export interface ReplacementOption {
2419

2520
/**
2621
* A regular expression matching any property to be removed or replaced from the telemetry server.
@@ -287,6 +282,32 @@ export class BaseTelemetryReporter {
287282
}
288283
}
289284

285+
/**
286+
* Internal function which logs telemetry events and takes extra options.
287+
* @param eventName The name of the event
288+
* @param properties The properties of the event
289+
* @param measurements The measurements (numeric values) to send with the event
290+
* @param sanitize Whether or not to sanitize to the properties and measures
291+
* @param dangerous Whether or not to ignore telemetry level
292+
*/
293+
private internalSendTelemetryEvent(
294+
eventName: string,
295+
properties: TelemetryEventProperties | undefined,
296+
measurements: TelemetryEventMeasurements | undefined,
297+
sanitize: boolean,
298+
dangerous: boolean
299+
): void {
300+
if ((this.userOptIn || dangerous) && eventName !== "") {
301+
properties = { ...properties, ...this.getCommonProperties() };
302+
if (sanitize) {
303+
const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => this.anonymizeFilePaths(prop, this.firstParty));
304+
properties = this.removePropertiesWithPossibleUserInfo(cleanProperties);
305+
}
306+
eventName = `${this.extensionId}/${eventName}`;
307+
dangerous ? this.telemetryAppender.logEventDangerously(eventName, { properties, measurements }) : this.telemetryAppender.logEvent(eventName, { properties, measurements });
308+
}
309+
}
310+
290311
/**
291312
* Given an event name, some properties, and measurements sends a telemetry event.
292313
* Properties are sanitized on best-effort basis to remove sensitive data prior to sending.
@@ -295,11 +316,7 @@ export class BaseTelemetryReporter {
295316
* @param measurements The measurements (numeric values) to send with the event
296317
*/
297318
public sendTelemetryEvent(eventName: string, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements): void {
298-
if (this.userOptIn && eventName !== "") {
299-
properties = { ...properties, ...this.getCommonProperties() };
300-
const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => this.anonymizeFilePaths(prop, this.firstParty));
301-
this.telemetryAppender.logEvent(`${this.extensionId}/${eventName}`, { properties: this.removePropertiesWithPossibleUserInfo(cleanProperties), measurements: measurements });
302-
}
319+
this.internalSendTelemetryEvent(eventName, properties, measurements, true, false);
303320
}
304321

305322
/**
@@ -309,9 +326,58 @@ export class BaseTelemetryReporter {
309326
* @param measurements The measurements (numeric values) to send with the event
310327
*/
311328
public sendRawTelemetryEvent(eventName: string, properties?: RawTelemetryEventProperties, measurements?: TelemetryEventMeasurements): void {
312-
if (this.userOptIn && eventName !== "") {
329+
this.internalSendTelemetryEvent(eventName, properties, measurements, false, false);
330+
}
331+
332+
/**
333+
* **DANGEROUS** Given an event name, some properties, and measurements sends a telemetry event without checking telemetry setting
334+
* Do not use unless in a controlled environment i.e. sending telmetry from a CI pipeline or testing during development
335+
* @param eventName The name of the event
336+
* @param properties The properties to send with the event
337+
* @param measurements The measurements (numeric values) to send with the event
338+
* @param sanitize Whether or not to sanitize to the properties and measures, defaults to true
339+
*/
340+
public sendDangerousTelemetryEvent(eventName: string, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements, sanitize = true): void {
341+
// Since telemetry is probably off when sending dangerously, we must start the appender
342+
this.telemetryAppender.instantiateAppender();
343+
this.internalSendTelemetryEvent(eventName, properties, measurements, sanitize, true);
344+
}
345+
346+
/**
347+
* Internal function which logs telemetry error events and takes extra options.
348+
* @param eventName The name of the event
349+
* @param properties The properties of the event
350+
* @param measurements The measurements (numeric values) to send with the event
351+
* @param errorProps Properties to readct. If undefined then we assume all properties belong to the error prop and will be anonymized
352+
* @param sanitize Whether or not to sanitize to the properties and measures
353+
* @param dangerous Whether or not to ignore telemetry level
354+
*/
355+
private internalSendTelemetryErrorEvent(
356+
eventName: string,
357+
properties: TelemetryEventProperties | undefined,
358+
measurements: TelemetryEventMeasurements | undefined,
359+
errorProps: string[] | undefined,
360+
sanitize: boolean,
361+
dangerous: boolean
362+
): void {
363+
if ((this.shouldSendErrorTelemetry() || dangerous) && eventName !== "") {
364+
313365
properties = { ...properties, ...this.getCommonProperties() };
314-
this.telemetryAppender.logEvent(`${this.extensionId}/${eventName}`, { properties, measurements });
366+
if (sanitize) {
367+
// always clean the properties if first party
368+
// do not send any error properties if we shouldn't send error telemetry
369+
// if we have no errorProps, assume all are error props
370+
const cleanProperties = this.cloneAndChange(properties, (key: string, prop: string) => {
371+
372+
if (errorProps === undefined || errorProps.indexOf(key) !== -1) {
373+
return "REDACTED";
374+
}
375+
376+
return this.anonymizeFilePaths(prop, this.firstParty);
377+
});
378+
properties = this.removePropertiesWithPossibleUserInfo(cleanProperties);
379+
}
380+
dangerous ? this.telemetryAppender.logEventDangerously(eventName, { properties, measurements }) : this.telemetryAppender.logEvent(eventName, { properties, measurements });
315381
}
316382
}
317383

@@ -322,24 +388,51 @@ export class BaseTelemetryReporter {
322388
* @param measurements The measurements (numeric values) to send with the event
323389
* @param errorProps If not present then we assume all properties belong to the error prop and will be anonymized
324390
*/
325-
public sendTelemetryErrorEvent(eventName: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number }, errorProps?: string[]): void {
326-
if (this.errorOptIn && eventName !== "") {
327-
// always clean the properties if first party
328-
// do not send any error properties if we shouldn't send error telemetry
329-
// if we have no errorProps, assume all are error props
330-
properties = { ...properties, ...this.getCommonProperties() };
331-
const cleanProperties = this.cloneAndChange(properties, (key: string, prop: string) => {
332-
if (this.shouldSendErrorTelemetry()) {
333-
return this.anonymizeFilePaths(prop, this.firstParty);
334-
}
391+
public sendTelemetryErrorEvent(eventName: string, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements, errorProps?: string[]): void {
392+
this.internalSendTelemetryErrorEvent(eventName, properties, measurements, errorProps, true, false);
393+
}
335394

336-
if (errorProps === undefined || errorProps.indexOf(key) !== -1) {
337-
return "REDACTED";
338-
}
395+
/**
396+
* **DANGEROUS** Given an event name, some properties, and measurements sends a telemetry error event without checking telemetry setting
397+
* Do not use unless in a controlled environment i.e. sending telmetry from a CI pipeline or testing during development
398+
* @param eventName The name of the event
399+
* @param properties The properties to send with the event
400+
* @param measurements The measurements (numeric values) to send with the event
401+
* @param errorProps If not present then we assume all properties belong to the error prop and will be anonymized
402+
* @param sanitize Whether or not to run the properties and measures through sanitiziation, defaults to true
403+
*/
404+
public sendDangerousTelemetryErrorEvent(eventName: string, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements, errorProps?: string[], sanitize = true): void {
405+
// Since telemetry is probably off when sending dangerously, we must start the appender
406+
this.telemetryAppender.instantiateAppender();
407+
this.internalSendTelemetryErrorEvent(eventName, properties, measurements, errorProps, sanitize, true);
408+
}
339409

340-
return this.anonymizeFilePaths(prop, this.firstParty);
341-
});
342-
this.telemetryAppender.logEvent(`${this.extensionId}/${eventName}`, { properties: this.removePropertiesWithPossibleUserInfo(cleanProperties), measurements: measurements });
410+
/**
411+
* Internal function which logs telemetry exceptions and takes extra options
412+
* @param error: The error to send
413+
* @param properties The properties of the event
414+
* @param measurements The measurements (numeric values) to send with the event
415+
* @param sanitize Whether or not to sanitize to the properties and measures
416+
* @param dangerous Whether or not to ignore telemetry level
417+
*/
418+
private internalSendTelemetryException(
419+
error: Error,
420+
properties: TelemetryEventProperties | undefined,
421+
measurements: TelemetryEventMeasurements | undefined,
422+
sanitize: boolean,
423+
dangerous: boolean
424+
): void {
425+
if ((this.shouldSendErrorTelemetry() || dangerous) && error) {
426+
properties = { ...properties, ...this.getCommonProperties() };
427+
if (sanitize) {
428+
const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => this.anonymizeFilePaths(prop, this.firstParty));
429+
// Also run the error stack through the anonymizer
430+
if (error.stack) {
431+
error.stack = this.anonymizeFilePaths(error.stack, this.firstParty);
432+
}
433+
properties = this.removePropertiesWithPossibleUserInfo(cleanProperties);
434+
}
435+
dangerous ? this.telemetryAppender.logExceptionDangerously(error, { properties, measurements }) : this.telemetryAppender.logException(error, { properties, measurements });
343436
}
344437
}
345438

@@ -350,15 +443,21 @@ export class BaseTelemetryReporter {
350443
* @param measurements The measurements (numeric values) to send with the event
351444
*/
352445
public sendTelemetryException(error: Error, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements): void {
353-
if (this.shouldSendErrorTelemetry() && this.errorOptIn && error) {
354-
properties = { ...properties, ...this.getCommonProperties() };
355-
const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => this.anonymizeFilePaths(prop, this.firstParty));
356-
// Also run the error stack through the anonymizer
357-
if (error.stack) {
358-
error.stack = this.anonymizeFilePaths(error.stack, this.firstParty);
359-
}
360-
this.telemetryAppender.logException(error, { properties: this.removePropertiesWithPossibleUserInfo(cleanProperties), measurements: measurements });
361-
}
446+
this.internalSendTelemetryException(error, properties, measurements, true, false);
447+
}
448+
449+
/**
450+
* **DANGEROUS** Given an error, properties, and measurements. Sends an exception event without checking the telemetry setting
451+
* Do not use unless in a controlled environment i.e. sending telmetry from a CI pipeline or testing during development
452+
* @param eventName The name of the event
453+
* @param properties The properties to send with the event
454+
* @param measurements The measurements (numeric values) to send with the event
455+
* @param sanitize Whether or not to sanitize to the properties and measures, defaults to true
456+
*/
457+
public sendDangerousTelemetryException(error: Error, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements, sanitize = true): void {
458+
// Since telemetry is probably off when sending dangerously, we must start the appender
459+
this.telemetryAppender.instantiateAppender();
460+
this.internalSendTelemetryException(error, properties, measurements, sanitize, true);
362461
}
363462

364463
/**

0 commit comments

Comments
 (0)