mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
150
src/vs/platform/telemetry/browser/errorTelemetry.ts
Normal file
150
src/vs/platform/telemetry/browser/errorTelemetry.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { binarySearch } from 'vs/base/common/arrays';
|
||||
import { globals } from 'vs/base/common/platform';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import Errors = require('vs/base/common/errors');
|
||||
import { safeStringify } from 'vs/base/common/objects';
|
||||
|
||||
interface ErrorEvent {
|
||||
stack: string;
|
||||
message?: string;
|
||||
filename?: string;
|
||||
line?: number;
|
||||
column?: number;
|
||||
error?: { name: string; message: string; };
|
||||
|
||||
count?: number;
|
||||
}
|
||||
|
||||
namespace ErrorEvent {
|
||||
export function compare(a: ErrorEvent, b: ErrorEvent) {
|
||||
if (a.stack < b.stack) {
|
||||
return -1;
|
||||
} else if (a.stack > b.stack) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ErrorTelemetry {
|
||||
|
||||
public static ERROR_FLUSH_TIMEOUT: number = 5 * 1000;
|
||||
|
||||
private _telemetryService: ITelemetryService;
|
||||
private _flushDelay: number;
|
||||
private _flushHandle = -1;
|
||||
private _buffer: ErrorEvent[] = [];
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
constructor(telemetryService: ITelemetryService, flushDelay = ErrorTelemetry.ERROR_FLUSH_TIMEOUT) {
|
||||
this._telemetryService = telemetryService;
|
||||
this._flushDelay = flushDelay;
|
||||
|
||||
// (1) check for unexpected but handled errors
|
||||
const unbind = Errors.errorHandler.addListener((err) => this._onErrorEvent(err));
|
||||
this._disposables.push(toDisposable(unbind));
|
||||
|
||||
// (2) check for uncaught global errors
|
||||
let oldOnError: Function;
|
||||
let that = this;
|
||||
if (typeof globals.onerror === 'function') {
|
||||
oldOnError = globals.onerror;
|
||||
}
|
||||
globals.onerror = function (message: string, filename: string, line: number, column?: number, e?: any) {
|
||||
that._onUncaughtError(message, filename, line, column, e);
|
||||
if (oldOnError) {
|
||||
oldOnError.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
this._disposables.push(toDisposable(function () {
|
||||
if (oldOnError) {
|
||||
globals.onerror = oldOnError;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
clearTimeout(this._flushHandle);
|
||||
this._flushBuffer();
|
||||
this._disposables = dispose(this._disposables);
|
||||
}
|
||||
|
||||
private _onErrorEvent(err: any): void {
|
||||
|
||||
if (!err) {
|
||||
return;
|
||||
}
|
||||
|
||||
// unwrap nested errors from loader
|
||||
if (err.detail && err.detail.stack) {
|
||||
err = err.detail;
|
||||
}
|
||||
|
||||
// work around behavior in workerServer.ts that breaks up Error.stack
|
||||
let stack = Array.isArray(err.stack) ? err.stack.join('\n') : err.stack;
|
||||
let message = err.message ? err.message : safeStringify(err);
|
||||
|
||||
// errors without a stack are not useful telemetry
|
||||
if (!stack) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._enqueue({ message, stack });
|
||||
}
|
||||
|
||||
private _onUncaughtError(message: string, filename: string, line: number, column?: number, err?: any): void {
|
||||
|
||||
let data: ErrorEvent = {
|
||||
stack: message,
|
||||
message,
|
||||
filename,
|
||||
line,
|
||||
column
|
||||
};
|
||||
|
||||
if (err) {
|
||||
let { name, message, stack } = err;
|
||||
data.error = { name, message };
|
||||
if (stack) {
|
||||
data.stack = Array.isArray(err.stack)
|
||||
? err.stack = err.stack.join('\n')
|
||||
: err.stack;
|
||||
}
|
||||
}
|
||||
|
||||
this._enqueue(data);
|
||||
}
|
||||
|
||||
private _enqueue(e: ErrorEvent): void {
|
||||
|
||||
const idx = binarySearch(this._buffer, e, ErrorEvent.compare);
|
||||
if (idx < 0) {
|
||||
e.count = 1;
|
||||
this._buffer.splice(~idx, 0, e);
|
||||
} else {
|
||||
this._buffer[idx].count += 1;
|
||||
}
|
||||
|
||||
if (this._flushHandle === -1) {
|
||||
this._flushHandle = setTimeout(() => {
|
||||
this._flushBuffer();
|
||||
this._flushHandle = -1;
|
||||
}, this._flushDelay);
|
||||
}
|
||||
}
|
||||
|
||||
private _flushBuffer(): void {
|
||||
for (let error of this._buffer) {
|
||||
this._telemetryService.publicLog('UnhandledError', error);
|
||||
}
|
||||
this._buffer.length = 0;
|
||||
}
|
||||
}
|
||||
78
src/vs/platform/telemetry/browser/idleMonitor.ts
Normal file
78
src/vs/platform/telemetry/browser/idleMonitor.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TimeoutTimer } from 'vs/base/common/async';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
|
||||
export enum UserStatus {
|
||||
Idle,
|
||||
Active
|
||||
}
|
||||
|
||||
export class IdleMonitor extends Disposable {
|
||||
|
||||
private _lastActiveTime: number;
|
||||
private _idleCheckTimeout: TimeoutTimer;
|
||||
private _status: UserStatus;
|
||||
private _idleTime: number;
|
||||
|
||||
private _onStatusChange: Emitter<UserStatus>;
|
||||
get onStatusChange(): Event<UserStatus> { return this._onStatusChange.event; }
|
||||
|
||||
constructor(idleTime) {
|
||||
super();
|
||||
|
||||
this._status = null;
|
||||
this._idleCheckTimeout = this._register(new TimeoutTimer());
|
||||
this._lastActiveTime = -1;
|
||||
this._idleTime = idleTime;
|
||||
this._onStatusChange = new Emitter<UserStatus>();
|
||||
|
||||
this._register(dom.addDisposableListener(document, 'mousemove', () => this._onUserActive()));
|
||||
this._register(dom.addDisposableListener(document, 'keydown', () => this._onUserActive()));
|
||||
this._onUserActive();
|
||||
}
|
||||
|
||||
get status(): UserStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
private _onUserActive(): void {
|
||||
this._lastActiveTime = (new Date()).getTime();
|
||||
|
||||
if (this._status !== UserStatus.Active) {
|
||||
this._status = UserStatus.Active;
|
||||
this._scheduleIdleCheck();
|
||||
this._onStatusChange.fire(this._status);
|
||||
}
|
||||
}
|
||||
|
||||
private _onUserIdle(): void {
|
||||
if (this._status !== UserStatus.Idle) {
|
||||
this._status = UserStatus.Idle;
|
||||
this._onStatusChange.fire(this._status);
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleIdleCheck(): void {
|
||||
const minimumTimeWhenUserCanBecomeIdle = this._lastActiveTime + this._idleTime;
|
||||
const timeout = minimumTimeWhenUserCanBecomeIdle - (new Date()).getTime();
|
||||
|
||||
this._idleCheckTimeout.setIfNotSet(() => this._checkIfUserIsIdle(), timeout);
|
||||
}
|
||||
|
||||
private _checkIfUserIsIdle(): void {
|
||||
const actualIdleTime = (new Date()).getTime() - this._lastActiveTime;
|
||||
|
||||
if (actualIdleTime >= this._idleTime) {
|
||||
this._onUserIdle();
|
||||
} else {
|
||||
this._scheduleIdleCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/vs/platform/telemetry/common/experiments.ts
Normal file
89
src/vs/platform/telemetry/common/experiments.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export interface IExperiments {
|
||||
deployToAzureQuickLink: boolean;
|
||||
}
|
||||
|
||||
export const IExperimentService = createDecorator<IExperimentService>('experimentService');
|
||||
|
||||
export interface IExperimentService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
getExperiments(): IExperiments;
|
||||
}
|
||||
|
||||
export class ExperimentService implements IExperimentService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private experiments: IExperiments;
|
||||
|
||||
constructor(
|
||||
@IStorageService private storageService: IStorageService,
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
) { }
|
||||
|
||||
getExperiments() {
|
||||
if (!this.experiments) {
|
||||
this.experiments = loadExperiments(this.storageService, this.configurationService);
|
||||
}
|
||||
return this.experiments;
|
||||
}
|
||||
}
|
||||
|
||||
function loadExperiments(storageService: IStorageService, configurationService: IConfigurationService): IExperiments {
|
||||
const experiments = splitExperimentsRandomness(storageService);
|
||||
return applyOverrides(experiments, configurationService);
|
||||
}
|
||||
|
||||
function applyOverrides(experiments: IExperiments, configurationService: IConfigurationService): IExperiments {
|
||||
const experimentsConfig = getExperimentsOverrides(configurationService);
|
||||
Object.keys(experiments).forEach(key => {
|
||||
if (key in experimentsConfig) {
|
||||
experiments[key] = experimentsConfig[key];
|
||||
}
|
||||
});
|
||||
return experiments;
|
||||
}
|
||||
|
||||
function splitExperimentsRandomness(storageService: IStorageService): IExperiments {
|
||||
const random1 = getExperimentsRandomness(storageService);
|
||||
const [random2, /* showTaskDocumentation */] = splitRandom(random1);
|
||||
const [/* random3 */, deployToAzureQuickLink] = splitRandom(random2);
|
||||
// const [random4, /* mergeQuickLinks */] = splitRandom(random3);
|
||||
// const [random5, /* enableWelcomePage */] = splitRandom(random4);
|
||||
return {
|
||||
deployToAzureQuickLink
|
||||
};
|
||||
}
|
||||
|
||||
function getExperimentsRandomness(storageService: IStorageService) {
|
||||
const key = 'experiments.randomness';
|
||||
let valueString = storageService.get(key);
|
||||
if (!valueString) {
|
||||
valueString = Math.random().toString();
|
||||
storageService.store(key, valueString);
|
||||
}
|
||||
|
||||
return parseFloat(valueString);
|
||||
}
|
||||
|
||||
function splitRandom(random: number): [number, boolean] {
|
||||
const scaled = random * 2;
|
||||
const i = Math.floor(scaled);
|
||||
return [scaled - i, i === 1];
|
||||
}
|
||||
|
||||
function getExperimentsOverrides(configurationService: IConfigurationService): IExperiments {
|
||||
const config: any = configurationService.getConfiguration('telemetry');
|
||||
return config && config.experiments || {};
|
||||
}
|
||||
37
src/vs/platform/telemetry/common/telemetry.ts
Normal file
37
src/vs/platform/telemetry/common/telemetry.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const ITelemetryService = createDecorator<ITelemetryService>('telemetryService');
|
||||
|
||||
export interface ITelemetryInfo {
|
||||
sessionId: string;
|
||||
machineId: string;
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
export interface ITelemetryData {
|
||||
from?: string;
|
||||
target?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ITelemetryService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
/**
|
||||
* Sends a telemetry event that has been privacy approved.
|
||||
* Do not call this unless you have been given approval.
|
||||
*/
|
||||
publicLog(eventName: string, data?: ITelemetryData): TPromise<void>;
|
||||
|
||||
getTelemetryInfo(): TPromise<ITelemetryInfo>;
|
||||
|
||||
isOptedIn: boolean;
|
||||
}
|
||||
43
src/vs/platform/telemetry/common/telemetryIpc.ts
Normal file
43
src/vs/platform/telemetry/common/telemetryIpc.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
|
||||
export interface ITelemetryLog {
|
||||
eventName: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export interface ITelemetryAppenderChannel extends IChannel {
|
||||
call(command: 'log', data: ITelemetryLog): TPromise<void>;
|
||||
call(command: string, arg: any): TPromise<any>;
|
||||
}
|
||||
|
||||
export class TelemetryAppenderChannel implements ITelemetryAppenderChannel {
|
||||
|
||||
constructor(private appender: ITelemetryAppender) { }
|
||||
|
||||
call(command: string, { eventName, data }: ITelemetryLog): TPromise<any> {
|
||||
this.appender.log(eventName, data);
|
||||
return TPromise.as(null);
|
||||
}
|
||||
}
|
||||
|
||||
export class TelemetryAppenderClient implements ITelemetryAppender {
|
||||
|
||||
constructor(private channel: ITelemetryAppenderChannel) { }
|
||||
|
||||
log(eventName: string, data?: any): any {
|
||||
return this.channel.call('log', { eventName, data });
|
||||
}
|
||||
|
||||
dispose(): any {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
150
src/vs/platform/telemetry/common/telemetryService.ts
Normal file
150
src/vs/platform/telemetry/common/telemetryService.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { escapeRegExpCharacters } from 'vs/base/common/strings';
|
||||
import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { optional } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { cloneAndChange, mixin } from 'vs/base/common/objects';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
export interface ITelemetryServiceConfig {
|
||||
appender: ITelemetryAppender;
|
||||
commonProperties?: TPromise<{ [name: string]: any }>;
|
||||
piiPaths?: string[];
|
||||
userOptIn?: boolean;
|
||||
}
|
||||
|
||||
export class TelemetryService implements ITelemetryService {
|
||||
|
||||
static IDLE_START_EVENT_NAME = 'UserIdleStart';
|
||||
static IDLE_STOP_EVENT_NAME = 'UserIdleStop';
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private _appender: ITelemetryAppender;
|
||||
private _commonProperties: TPromise<{ [name: string]: any; }>;
|
||||
private _piiPaths: string[];
|
||||
private _userOptIn: boolean;
|
||||
|
||||
private _disposables: IDisposable[] = [];
|
||||
private _cleanupPatterns: [RegExp, string][] = [];
|
||||
|
||||
constructor(
|
||||
config: ITelemetryServiceConfig,
|
||||
@optional(IConfigurationService) private _configurationService: IConfigurationService
|
||||
) {
|
||||
this._appender = config.appender;
|
||||
this._commonProperties = config.commonProperties || TPromise.as({});
|
||||
this._piiPaths = config.piiPaths || [];
|
||||
this._userOptIn = typeof config.userOptIn === 'undefined' ? true : config.userOptIn;
|
||||
|
||||
// static cleanup patterns for:
|
||||
// #1 `file:///DANGEROUS/PATH/resources/app/Useful/Information`
|
||||
// #2 // Any other file path that doesn't match the approved form above should be cleaned.
|
||||
// #3 "Error: ENOENT; no such file or directory" is often followed with PII, clean it
|
||||
this._cleanupPatterns.push(
|
||||
[/file:\/\/\/.*?\/resources\/app\//gi, ''],
|
||||
[/file:\/\/\/.*/gi, ''],
|
||||
[/ENOENT: no such file or directory.*?\'([^\']+)\'/gi, 'ENOENT: no such file or directory']
|
||||
);
|
||||
|
||||
for (let piiPath of this._piiPaths) {
|
||||
this._cleanupPatterns.push([new RegExp(escapeRegExpCharacters(piiPath), 'gi'), '']);
|
||||
}
|
||||
|
||||
if (this._configurationService) {
|
||||
this._updateUserOptIn();
|
||||
this._configurationService.onDidUpdateConfiguration(this._updateUserOptIn, this, this._disposables);
|
||||
this.publicLog('optInStatus', { optIn: this._userOptIn });
|
||||
}
|
||||
}
|
||||
|
||||
private _updateUserOptIn(): void {
|
||||
const config = this._configurationService.getConfiguration<any>(TELEMETRY_SECTION_ID);
|
||||
this._userOptIn = config ? config.enableTelemetry : this._userOptIn;
|
||||
}
|
||||
|
||||
get isOptedIn(): boolean {
|
||||
return this._userOptIn;
|
||||
}
|
||||
|
||||
getTelemetryInfo(): TPromise<ITelemetryInfo> {
|
||||
return this._commonProperties.then(values => {
|
||||
// well known properties
|
||||
let sessionId = values['sessionID'];
|
||||
let instanceId = values['common.instanceId'];
|
||||
let machineId = values['common.machineId'];
|
||||
|
||||
return { sessionId, instanceId, machineId };
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables = dispose(this._disposables);
|
||||
}
|
||||
|
||||
publicLog(eventName: string, data?: ITelemetryData): TPromise<any> {
|
||||
// don't send events when the user is optout
|
||||
if (!this._userOptIn) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
|
||||
return this._commonProperties.then(values => {
|
||||
|
||||
// (first) add common properties
|
||||
data = mixin(data, values);
|
||||
|
||||
// (last) remove all PII from data
|
||||
data = cloneAndChange(data, value => {
|
||||
if (typeof value === 'string') {
|
||||
return this._cleanupInfo(value);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
this._appender.log(eventName, data);
|
||||
|
||||
}, err => {
|
||||
// unsure what to do now...
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
private _cleanupInfo(stack: string): string {
|
||||
|
||||
// sanitize with configured cleanup patterns
|
||||
for (let tuple of this._cleanupPatterns) {
|
||||
let [regexp, replaceValue] = tuple;
|
||||
stack = stack.replace(regexp, replaceValue);
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const TELEMETRY_SECTION_ID = 'telemetry';
|
||||
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
|
||||
'id': TELEMETRY_SECTION_ID,
|
||||
'order': 110,
|
||||
'type': 'object',
|
||||
'title': localize('telemetryConfigurationTitle', "Telemetry"),
|
||||
'properties': {
|
||||
'telemetry.enableTelemetry': {
|
||||
'type': 'boolean',
|
||||
'description': localize('telemetry.enableTelemetry', "Enable usage data and errors to be sent to Microsoft."),
|
||||
'default': true
|
||||
}
|
||||
}
|
||||
});
|
||||
247
src/vs/platform/telemetry/common/telemetryUtils.ts
Normal file
247
src/vs/platform/telemetry/common/telemetryUtils.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { guessMimeTypes } from 'vs/base/common/mime';
|
||||
import paths = require('vs/base/common/paths');
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { ConfigurationSource, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IKeybindingService, KeybindingSource } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export const NullTelemetryService = {
|
||||
_serviceBrand: undefined,
|
||||
publicLog(eventName: string, data?: ITelemetryData) {
|
||||
return TPromise.as<void>(null);
|
||||
},
|
||||
isOptedIn: true,
|
||||
getTelemetryInfo(): TPromise<ITelemetryInfo> {
|
||||
return TPromise.as({
|
||||
instanceId: 'someValue.instanceId',
|
||||
sessionId: 'someValue.sessionId',
|
||||
machineId: 'someValue.machineId'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export interface ITelemetryAppender {
|
||||
log(eventName: string, data: any): void;
|
||||
}
|
||||
|
||||
export function combinedAppender(...appenders: ITelemetryAppender[]): ITelemetryAppender {
|
||||
return { log: (e, d) => appenders.forEach(a => a.log(e, d)) };
|
||||
}
|
||||
|
||||
export const NullAppender: ITelemetryAppender = { log: () => null };
|
||||
|
||||
// --- util
|
||||
|
||||
export function anonymize(input: string): string {
|
||||
if (!input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
let r = '';
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
let ch = input[i];
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
r += '0';
|
||||
continue;
|
||||
}
|
||||
if (ch >= 'a' && ch <= 'z') {
|
||||
r += 'a';
|
||||
continue;
|
||||
}
|
||||
if (ch >= 'A' && ch <= 'Z') {
|
||||
r += 'A';
|
||||
continue;
|
||||
}
|
||||
r += ch;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
export interface URIDescriptor {
|
||||
mimeType?: string;
|
||||
ext?: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export function telemetryURIDescriptor(uri: URI): URIDescriptor {
|
||||
const fsPath = uri && uri.fsPath;
|
||||
return fsPath ? { mimeType: guessMimeTypes(fsPath).join(', '), ext: paths.extname(fsPath), path: anonymize(fsPath) } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Only add settings that cannot contain any personal/private information of users (PII).
|
||||
*/
|
||||
const configurationValueWhitelist = [
|
||||
'editor.tabCompletion',
|
||||
'editor.fontFamily',
|
||||
'editor.fontWeight',
|
||||
'editor.fontSize',
|
||||
'editor.lineHeight',
|
||||
'editor.letterSpacing',
|
||||
'editor.lineNumbers',
|
||||
'editor.rulers',
|
||||
'editor.wordSeparators',
|
||||
'editor.tabSize',
|
||||
'editor.insertSpaces',
|
||||
'editor.detectIndentation',
|
||||
'editor.roundedSelection',
|
||||
'editor.scrollBeyondLastLine',
|
||||
'editor.minimap.enabled',
|
||||
'editor.minimap.renderCharacters',
|
||||
'editor.minimap.maxColumn',
|
||||
'editor.find.seedSearchStringFromSelection',
|
||||
'editor.find.autoFindInSelection',
|
||||
'editor.wordWrap',
|
||||
'editor.wordWrapColumn',
|
||||
'editor.wrappingIndent',
|
||||
'editor.mouseWheelScrollSensitivity',
|
||||
'editor.multiCursorModifier',
|
||||
'editor.quickSuggestions',
|
||||
'editor.quickSuggestionsDelay',
|
||||
'editor.parameterHints',
|
||||
'editor.autoClosingBrackets',
|
||||
'editor.autoIndent',
|
||||
'editor.formatOnType',
|
||||
'editor.formatOnPaste',
|
||||
'editor.suggestOnTriggerCharacters',
|
||||
'editor.acceptSuggestionOnEnter',
|
||||
'editor.acceptSuggestionOnCommitCharacter',
|
||||
'editor.snippetSuggestions',
|
||||
'editor.emptySelectionClipboard',
|
||||
'editor.wordBasedSuggestions',
|
||||
'editor.suggestFontSize',
|
||||
'editor.suggestLineHeight',
|
||||
'editor.selectionHighlight',
|
||||
'editor.occurrencesHighlight',
|
||||
'editor.overviewRulerLanes',
|
||||
'editor.overviewRulerBorder',
|
||||
'editor.cursorBlinking',
|
||||
'editor.cursorStyle',
|
||||
'editor.mouseWheelZoom',
|
||||
'editor.fontLigatures',
|
||||
'editor.hideCursorInOverviewRuler',
|
||||
'editor.renderWhitespace',
|
||||
'editor.renderControlCharacters',
|
||||
'editor.renderIndentGuides',
|
||||
'editor.renderLineHighlight',
|
||||
'editor.codeLens',
|
||||
'editor.folding',
|
||||
'editor.showFoldingControls',
|
||||
'editor.matchBrackets',
|
||||
'editor.glyphMargin',
|
||||
'editor.useTabStops',
|
||||
'editor.trimAutoWhitespace',
|
||||
'editor.stablePeek',
|
||||
'editor.dragAndDrop',
|
||||
'editor.formatOnSave',
|
||||
'editor.colorDecorators',
|
||||
|
||||
'window.zoomLevel',
|
||||
'files.autoSave',
|
||||
'files.hotExit',
|
||||
'files.associations',
|
||||
'workbench.statusBar.visible',
|
||||
'files.trimTrailingWhitespace',
|
||||
'git.confirmSync',
|
||||
'workbench.sideBar.location',
|
||||
'window.openFilesInNewWindow',
|
||||
'javascript.validate.enable',
|
||||
'window.reopenFolders',
|
||||
'window.restoreWindows',
|
||||
'extensions.autoUpdate',
|
||||
'files.eol',
|
||||
'explorer.openEditors.visible',
|
||||
'workbench.editor.enablePreview',
|
||||
'files.autoSaveDelay',
|
||||
'workbench.editor.showTabs',
|
||||
'files.encoding',
|
||||
'files.autoGuessEncoding',
|
||||
'git.enabled',
|
||||
'http.proxyStrictSSL',
|
||||
'terminal.integrated.fontFamily',
|
||||
'workbench.editor.enablePreviewFromQuickOpen',
|
||||
'workbench.editor.swipeToNavigate',
|
||||
'php.builtInCompletions.enable',
|
||||
'php.validate.enable',
|
||||
'php.validate.run',
|
||||
'workbench.welcome.enabled',
|
||||
'workbench.startupEditor',
|
||||
];
|
||||
|
||||
export function configurationTelemetry(telemetryService: ITelemetryService, configurationService: IConfigurationService): IDisposable {
|
||||
return configurationService.onDidUpdateConfiguration(event => {
|
||||
if (event.source !== ConfigurationSource.Default) {
|
||||
telemetryService.publicLog('updateConfiguration', {
|
||||
configurationSource: ConfigurationSource[event.source],
|
||||
configurationKeys: flattenKeys(event.sourceConfig)
|
||||
});
|
||||
telemetryService.publicLog('updateConfigurationValues', {
|
||||
configurationSource: ConfigurationSource[event.source],
|
||||
configurationValues: flattenValues(event.sourceConfig, configurationValueWhitelist)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function lifecycleTelemetry(telemetryService: ITelemetryService, lifecycleService: ILifecycleService): IDisposable {
|
||||
return lifecycleService.onShutdown(event => {
|
||||
telemetryService.publicLog('shutdown', { reason: ShutdownReason[event] });
|
||||
});
|
||||
}
|
||||
|
||||
export function keybindingsTelemetry(telemetryService: ITelemetryService, keybindingService: IKeybindingService): IDisposable {
|
||||
return keybindingService.onDidUpdateKeybindings(event => {
|
||||
if (event.source === KeybindingSource.User && event.keybindings) {
|
||||
telemetryService.publicLog('updateKeybindings', {
|
||||
bindings: event.keybindings.map(binding => ({
|
||||
key: binding.key,
|
||||
command: binding.command,
|
||||
when: binding.when,
|
||||
args: binding.args ? true : undefined
|
||||
}))
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function flattenKeys(value: Object): string[] {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
const result: string[] = [];
|
||||
flatKeys(result, '', value);
|
||||
return result;
|
||||
}
|
||||
|
||||
function flatKeys(result: string[], prefix: string, value: Object): void {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
Object.keys(value)
|
||||
.forEach(key => flatKeys(result, prefix ? `${prefix}.${key}` : key, value[key]));
|
||||
} else {
|
||||
result.push(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
function flattenValues(value: Object, keys: string[]): { [key: string]: any }[] {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return keys.reduce((array, key) => {
|
||||
const v = key.split('.')
|
||||
.reduce((tmp, k) => tmp && typeof tmp === 'object' ? tmp[k] : undefined, value);
|
||||
if (typeof v !== 'undefined') {
|
||||
array.push({ [key]: v });
|
||||
}
|
||||
return array;
|
||||
}, []);
|
||||
}
|
||||
152
src/vs/platform/telemetry/node/appInsightsAppender.ts
Normal file
152
src/vs/platform/telemetry/node/appInsightsAppender.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as appInsights from 'applicationinsights';
|
||||
import { isObject } from 'vs/base/common/types';
|
||||
import { safeStringify, mixin } from 'vs/base/common/objects';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
|
||||
let _initialized = false;
|
||||
|
||||
function ensureAIEngineIsInitialized(): void {
|
||||
if (_initialized === false) {
|
||||
// we need to pass some fake key, otherwise AI throws an exception
|
||||
appInsights.setup('2588e01f-f6c9-4cd6-a348-143741f8d702')
|
||||
.setAutoCollectConsole(false)
|
||||
.setAutoCollectExceptions(false)
|
||||
.setAutoCollectPerformance(false)
|
||||
.setAutoCollectRequests(false);
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
function getClient(aiKey: string): typeof appInsights.client {
|
||||
|
||||
ensureAIEngineIsInitialized();
|
||||
|
||||
const client = appInsights.getClient(aiKey);
|
||||
client.channel.setOfflineMode(true);
|
||||
client.context.tags[client.context.keys.deviceMachineName] = ''; //prevent App Insights from reporting machine name
|
||||
if (aiKey.indexOf('AIF-') === 0) {
|
||||
client.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1';
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
interface Properties {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface Measurements {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
export class AppInsightsAppender implements ITelemetryAppender {
|
||||
|
||||
private _aiClient: typeof appInsights.client;
|
||||
|
||||
constructor(
|
||||
private _eventPrefix: string,
|
||||
private _defaultData: { [key: string]: any },
|
||||
aiKeyOrClientFactory: string | (() => typeof appInsights.client) // allow factory function for testing
|
||||
) {
|
||||
if (!this._defaultData) {
|
||||
this._defaultData = Object.create(null);
|
||||
}
|
||||
|
||||
if (typeof aiKeyOrClientFactory === 'string') {
|
||||
this._aiClient = getClient(aiKeyOrClientFactory);
|
||||
} else if (typeof aiKeyOrClientFactory === 'function') {
|
||||
this._aiClient = aiKeyOrClientFactory();
|
||||
}
|
||||
}
|
||||
|
||||
private static _getData(data?: any): { properties: Properties, measurements: Measurements } {
|
||||
|
||||
const properties: Properties = Object.create(null);
|
||||
const measurements: Measurements = Object.create(null);
|
||||
|
||||
const flat = Object.create(null);
|
||||
AppInsightsAppender._flaten(data, flat);
|
||||
|
||||
for (let prop in flat) {
|
||||
// enforce property names less than 150 char, take the last 150 char
|
||||
prop = prop.length > 150 ? prop.substr(prop.length - 149) : prop;
|
||||
var value = flat[prop];
|
||||
|
||||
if (typeof value === 'number') {
|
||||
measurements[prop] = value;
|
||||
|
||||
} else if (typeof value === 'boolean') {
|
||||
measurements[prop] = value ? 1 : 0;
|
||||
|
||||
} else if (typeof value === 'string') {
|
||||
//enforce property value to be less than 1024 char, take the first 1024 char
|
||||
properties[prop] = value.substring(0, 1023);
|
||||
|
||||
} else if (typeof value !== 'undefined' && value !== null) {
|
||||
properties[prop] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
properties,
|
||||
measurements
|
||||
};
|
||||
}
|
||||
|
||||
private static _flaten(obj: any, result: { [key: string]: any }, order: number = 0, prefix?: string): void {
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var item of Object.getOwnPropertyNames(obj)) {
|
||||
const value = obj[item];
|
||||
const index = prefix ? prefix + item : item;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
result[index] = safeStringify(value);
|
||||
|
||||
} else if (value instanceof Date) {
|
||||
// TODO unsure why this is here and not in _getData
|
||||
result[index] = value.toISOString();
|
||||
|
||||
} else if (isObject(value)) {
|
||||
if (order < 2) {
|
||||
AppInsightsAppender._flaten(value, result, order + 1, index + '.');
|
||||
} else {
|
||||
result[index] = safeStringify(value);
|
||||
}
|
||||
} else {
|
||||
result[index] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log(eventName: string, data?: any): void {
|
||||
if (!this._aiClient) {
|
||||
return;
|
||||
}
|
||||
data = mixin(data, this._defaultData);
|
||||
let { properties, measurements } = AppInsightsAppender._getData(data);
|
||||
this._aiClient.trackEvent(this._eventPrefix + '/' + eventName, properties, measurements);
|
||||
}
|
||||
|
||||
dispose(): TPromise<any> {
|
||||
if (this._aiClient) {
|
||||
return new TPromise(resolve => {
|
||||
this._aiClient.sendPendingData(() => {
|
||||
// all data flushed
|
||||
this._aiClient = undefined;
|
||||
resolve(void 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
50
src/vs/platform/telemetry/node/commonProperties.ts
Normal file
50
src/vs/platform/telemetry/node/commonProperties.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as Platform from 'vs/base/common/platform';
|
||||
import * as os from 'os';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import * as uuid from 'vs/base/common/uuid';
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
import product from 'vs/platform/node/product';
|
||||
|
||||
export const machineIdStorageKey = 'telemetry.machineId';
|
||||
export const machineIdIpcChannel = 'vscode:machineId';
|
||||
|
||||
export function resolveCommonProperties(commit: string, version: string): TPromise<{ [name: string]: string; }> {
|
||||
const result: { [name: string]: string; } = Object.create(null);
|
||||
|
||||
result['sessionID'] = uuid.generateUuid() + Date.now();
|
||||
result['commitHash'] = commit;
|
||||
result['version'] = version;
|
||||
result['common.osVersion'] = os.release();
|
||||
result['common.platform'] = Platform.Platform[Platform.platform];
|
||||
result['common.nodePlatform'] = process.platform;
|
||||
result['common.nodeArch'] = process.arch;
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
result['common.application.name'] = product.nameLong;
|
||||
|
||||
// dynamic properties which value differs on each call
|
||||
let seq = 0;
|
||||
const startTime = Date.now();
|
||||
Object.defineProperties(result, {
|
||||
'timestamp': {
|
||||
get: () => new Date(),
|
||||
enumerable: true
|
||||
},
|
||||
'common.timesincesessionstart': {
|
||||
get: () => Date.now() - startTime,
|
||||
enumerable: true
|
||||
},
|
||||
'common.sequence': {
|
||||
get: () => seq++,
|
||||
enumerable: true
|
||||
}
|
||||
});
|
||||
|
||||
return TPromise.as(result);
|
||||
}
|
||||
144
src/vs/platform/telemetry/node/workbenchCommonProperties.ts
Normal file
144
src/vs/platform/telemetry/node/workbenchCommonProperties.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as winreg from 'winreg';
|
||||
import * as os from 'os';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import * as uuid from 'vs/base/common/uuid';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { getMachineId } from 'vs/base/node/id';
|
||||
import { resolveCommonProperties, machineIdStorageKey } from '../node/commonProperties';
|
||||
|
||||
// {{ SQL CARBON EDIT }}
|
||||
import product from 'vs/platform/node/product';
|
||||
import * as Utils from 'sql/common/telemetryUtilities';
|
||||
|
||||
const SQM_KEY: string = '\\Software\\Microsoft\\SQMClient';
|
||||
|
||||
export function resolveWorkbenchCommonProperties(storageService: IStorageService, commit: string, version: string): TPromise<{ [name: string]: string }> {
|
||||
return resolveCommonProperties(commit, version).then(result => {
|
||||
result['common.version.shell'] = process.versions && (<any>process).versions['electron'];
|
||||
result['common.version.renderer'] = process.versions && (<any>process).versions['chrome'];
|
||||
result['common.osVersion'] = os.release();
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
result['common.application.name'] = product.nameLong;
|
||||
getUserId(storageService).then(value => result['common.userId'] = value);
|
||||
|
||||
const lastSessionDate = storageService.get('telemetry.lastSessionDate');
|
||||
const firstSessionDate = storageService.get('telemetry.firstSessionDate') || new Date().toUTCString();
|
||||
storageService.store('telemetry.firstSessionDate', firstSessionDate);
|
||||
storageService.store('telemetry.lastSessionDate', new Date().toUTCString());
|
||||
|
||||
result['common.firstSessionDate'] = firstSessionDate;
|
||||
result['common.lastSessionDate'] = lastSessionDate;
|
||||
result['common.isNewSession'] = !lastSessionDate ? '1' : '0';
|
||||
|
||||
const promises: TPromise<any>[] = [];
|
||||
promises.push(getOrCreateInstanceId(storageService).then(value => result['common.instanceId'] = value));
|
||||
promises.push(getOrCreateMachineId(storageService).then(value => result['common.machineId'] = value));
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
promises.push(getSqmUserId(storageService).then(value => result['common.sqm.userid'] = value));
|
||||
promises.push(getSqmMachineId(storageService).then(value => result['common.sqm.machineid'] = value));
|
||||
}
|
||||
|
||||
return TPromise.join(promises).then(() => result);
|
||||
});
|
||||
}
|
||||
|
||||
function getOrCreateInstanceId(storageService: IStorageService): TPromise<string> {
|
||||
let result = storageService.get('telemetry.instanceId') || uuid.generateUuid();
|
||||
storageService.store('telemetry.instanceId', result);
|
||||
return TPromise.as(result);
|
||||
}
|
||||
|
||||
export function getOrCreateMachineId(storageService: IStorageService): TPromise<string> {
|
||||
let result = storageService.get(machineIdStorageKey);
|
||||
|
||||
if (result) {
|
||||
return TPromise.as(result);
|
||||
}
|
||||
|
||||
return getMachineId().then(result => {
|
||||
storageService.store(machineIdStorageKey, result);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
function getSqmUserId(storageService: IStorageService): TPromise<string> {
|
||||
const sqmUserId = storageService.get('telemetry.sqm.userId');
|
||||
if (sqmUserId) {
|
||||
return TPromise.as(sqmUserId);
|
||||
}
|
||||
return getWinRegKeyData(SQM_KEY, 'UserId', winreg.HKCU).then(result => {
|
||||
if (result) {
|
||||
storageService.store('telemetry.sqm.userId', result);
|
||||
return result;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function getSqmMachineId(storageService: IStorageService): TPromise<string> {
|
||||
let sqmMachineId = storageService.get('telemetry.sqm.machineId');
|
||||
if (sqmMachineId) {
|
||||
return TPromise.as(sqmMachineId);
|
||||
}
|
||||
return getWinRegKeyData(SQM_KEY, 'MachineId', winreg.HKLM).then(result => {
|
||||
if (result) {
|
||||
storageService.store('telemetry.sqm.machineId', result);
|
||||
return result;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function getWinRegKeyData(key: string, name: string, hive: string): TPromise<string> {
|
||||
return new TPromise<string>((resolve, reject) => {
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
const reg = new winreg({ hive, key });
|
||||
reg.get(name, (e, result) => {
|
||||
if (e || !result) {
|
||||
reject(null);
|
||||
} else {
|
||||
resolve(result.value);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
}).then(undefined, err => {
|
||||
// we only want success
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
// Get the unique ID for the current user
|
||||
function getUserId(storageService: IStorageService): Promise<string> {
|
||||
var userId = storageService.get('common.userId');
|
||||
return new Promise<string>(resolve => {
|
||||
// Generate the user id if it has not been created already
|
||||
if (typeof userId === 'undefined') {
|
||||
let id = Utils.generateUserId();
|
||||
id.then( newId => {
|
||||
userId = newId;
|
||||
resolve(userId);
|
||||
//store the user Id in the storage service
|
||||
storageService.store('common.userId', userId);
|
||||
});
|
||||
} else {
|
||||
resolve(userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
|
||||
|
||||
interface IAppInsightsEvent {
|
||||
eventName: string;
|
||||
properties?: { string?: string; };
|
||||
measurements?: { string?: number; };
|
||||
}
|
||||
|
||||
class AppInsightsMock {
|
||||
|
||||
public events: IAppInsightsEvent[] = [];
|
||||
public IsTrackingPageView: boolean = false;
|
||||
public exceptions: any[] = [];
|
||||
|
||||
public trackEvent(eventName: string, properties?: { string?: string; }, measurements?: { string?: number; }): void {
|
||||
this.events.push({
|
||||
eventName,
|
||||
properties,
|
||||
measurements
|
||||
});
|
||||
}
|
||||
public trackPageView(): void {
|
||||
this.IsTrackingPageView = true;
|
||||
}
|
||||
|
||||
public trackException(exception: any): void {
|
||||
this.exceptions.push(exception);
|
||||
}
|
||||
|
||||
public sendPendingData(callback): void {
|
||||
// called on dispose
|
||||
}
|
||||
}
|
||||
|
||||
suite('AIAdapter', () => {
|
||||
var appInsightsMock: AppInsightsMock;
|
||||
var adapter: AppInsightsAppender;
|
||||
var prefix = 'prefix';
|
||||
|
||||
setup(() => {
|
||||
appInsightsMock = new AppInsightsMock();
|
||||
adapter = new AppInsightsAppender(prefix, undefined, () => appInsightsMock);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
adapter.dispose();
|
||||
});
|
||||
|
||||
test('Simple event', () => {
|
||||
adapter.log('testEvent');
|
||||
|
||||
assert.equal(appInsightsMock.events.length, 1);
|
||||
assert.equal(appInsightsMock.events[0].eventName, `${prefix}/testEvent`);
|
||||
});
|
||||
|
||||
test('addional data', () => {
|
||||
adapter = new AppInsightsAppender(prefix, { first: '1st', second: 2, third: true }, () => appInsightsMock);
|
||||
adapter.log('testEvent');
|
||||
|
||||
assert.equal(appInsightsMock.events.length, 1);
|
||||
let [first] = appInsightsMock.events;
|
||||
assert.equal(first.eventName, `${prefix}/testEvent`);
|
||||
assert.equal(first.properties['first'], '1st');
|
||||
assert.equal(first.measurements['second'], '2');
|
||||
assert.equal(first.measurements['third'], 1);
|
||||
});
|
||||
|
||||
test('property limits', () => {
|
||||
var reallyLongPropertyName = 'abcdefghijklmnopqrstuvwxyz';
|
||||
for (var i = 0; i < 6; i++) {
|
||||
reallyLongPropertyName += 'abcdefghijklmnopqrstuvwxyz';
|
||||
}
|
||||
assert(reallyLongPropertyName.length > 150);
|
||||
|
||||
var reallyLongPropertyValue = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123';
|
||||
for (var i = 0; i < 21; i++) {
|
||||
reallyLongPropertyValue += 'abcdefghijklmnopqrstuvwxyz012345678901234567890123';
|
||||
}
|
||||
assert(reallyLongPropertyValue.length > 1024);
|
||||
|
||||
var data = {};
|
||||
data[reallyLongPropertyName] = '1234';
|
||||
data['reallyLongPropertyValue'] = reallyLongPropertyValue;
|
||||
adapter.log('testEvent', data);
|
||||
|
||||
assert.equal(appInsightsMock.events.length, 1);
|
||||
|
||||
for (var prop in appInsightsMock.events[0].properties) {
|
||||
assert(prop.length < 150);
|
||||
assert(appInsightsMock.events[0].properties[prop].length < 1024);
|
||||
}
|
||||
});
|
||||
|
||||
test('Different data types', () => {
|
||||
var date = new Date();
|
||||
adapter.log('testEvent', { favoriteDate: date, likeRed: false, likeBlue: true, favoriteNumber: 1, favoriteColor: 'blue', favoriteCars: ['bmw', 'audi', 'ford'] });
|
||||
|
||||
assert.equal(appInsightsMock.events.length, 1);
|
||||
assert.equal(appInsightsMock.events[0].eventName, `${prefix}/testEvent`);
|
||||
assert.equal(appInsightsMock.events[0].properties['favoriteColor'], 'blue');
|
||||
assert.equal(appInsightsMock.events[0].measurements['likeRed'], 0);
|
||||
assert.equal(appInsightsMock.events[0].measurements['likeBlue'], 1);
|
||||
assert.equal(appInsightsMock.events[0].properties['favoriteDate'], date.toISOString());
|
||||
assert.equal(appInsightsMock.events[0].properties['favoriteCars'], JSON.stringify(['bmw', 'audi', 'ford']));
|
||||
assert.equal(appInsightsMock.events[0].measurements['favoriteNumber'], 1);
|
||||
});
|
||||
|
||||
test('Nested data', () => {
|
||||
adapter.log('testEvent', {
|
||||
window: {
|
||||
title: 'some title',
|
||||
measurements: {
|
||||
width: 100,
|
||||
height: 200
|
||||
}
|
||||
},
|
||||
nestedObj: {
|
||||
nestedObj2: {
|
||||
nestedObj3: {
|
||||
testProperty: 'test',
|
||||
}
|
||||
},
|
||||
testMeasurement: 1
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(appInsightsMock.events.length, 1);
|
||||
assert.equal(appInsightsMock.events[0].eventName, `${prefix}/testEvent`);
|
||||
|
||||
assert.equal(appInsightsMock.events[0].properties['window.title'], 'some title');
|
||||
assert.equal(appInsightsMock.events[0].measurements['window.measurements.width'], 100);
|
||||
assert.equal(appInsightsMock.events[0].measurements['window.measurements.height'], 200);
|
||||
|
||||
assert.equal(appInsightsMock.events[0].properties['nestedObj.nestedObj2.nestedObj3'], JSON.stringify({ 'testProperty': 'test' }));
|
||||
assert.equal(appInsightsMock.events[0].measurements['nestedObj.testMeasurement'], 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { resolveWorkbenchCommonProperties } from 'vs/platform/telemetry/node/workbenchCommonProperties';
|
||||
import { StorageService, InMemoryLocalStorage } from 'vs/platform/storage/common/storageService';
|
||||
import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace';
|
||||
|
||||
suite('Telemetry - common properties', function () {
|
||||
|
||||
const commit = void 0;
|
||||
const version = void 0;
|
||||
let storageService;
|
||||
|
||||
setup(() => {
|
||||
storageService = new StorageService(new InMemoryLocalStorage(), null, TestWorkspace.id);
|
||||
});
|
||||
|
||||
test('default', function () {
|
||||
|
||||
return resolveWorkbenchCommonProperties(storageService, commit, version).then(props => {
|
||||
|
||||
assert.ok('commitHash' in props);
|
||||
assert.ok('sessionID' in props);
|
||||
assert.ok('timestamp' in props);
|
||||
assert.ok('common.platform' in props);
|
||||
assert.ok('common.nodePlatform' in props);
|
||||
assert.ok('common.nodeArch' in props);
|
||||
assert.ok('common.timesincesessionstart' in props);
|
||||
assert.ok('common.sequence' in props);
|
||||
|
||||
// assert.ok('common.version.shell' in first.data); // only when running on electron
|
||||
// assert.ok('common.version.renderer' in first.data);
|
||||
assert.ok('common.osVersion' in props, 'osVersion');
|
||||
assert.ok('version' in props);
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
assert.ok('common.application.name' in props);
|
||||
|
||||
assert.ok('common.firstSessionDate' in props, 'firstSessionDate');
|
||||
assert.ok('common.lastSessionDate' in props, 'lastSessionDate'); // conditional, see below, 'lastSessionDate'ow
|
||||
assert.ok('common.isNewSession' in props, 'isNewSession');
|
||||
|
||||
// machine id et al
|
||||
assert.ok('common.instanceId' in props, 'instanceId');
|
||||
assert.ok('common.machineId' in props, 'machineId');
|
||||
if (process.platform === 'win32') { // SQM only on windows
|
||||
assert.ok('common.sqm.userid' in props, 'userid');
|
||||
assert.ok('common.sqm.machineid' in props, 'machineid');
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
test('lastSessionDate when aviablale', function () {
|
||||
|
||||
storageService.store('telemetry.lastSessionDate', new Date().toUTCString());
|
||||
|
||||
return resolveWorkbenchCommonProperties(storageService, commit, version).then(props => {
|
||||
|
||||
assert.ok('common.lastSessionDate' in props); // conditional, see below
|
||||
assert.ok('common.isNewSession' in props);
|
||||
assert.equal(props['common.isNewSession'], 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('values chance on ask', function () {
|
||||
return resolveWorkbenchCommonProperties(storageService, commit, version).then(props => {
|
||||
let value1 = props['common.sequence'];
|
||||
let value2 = props['common.sequence'];
|
||||
assert.ok(value1 !== value2, 'seq');
|
||||
|
||||
value1 = props['timestamp'];
|
||||
value2 = props['timestamp'];
|
||||
assert.ok(value1 !== value2, 'timestamp');
|
||||
|
||||
value1 = props['common.timesincesessionstart'];
|
||||
return TPromise.timeout(10).then(_ => {
|
||||
value2 = props['common.timesincesessionstart'];
|
||||
assert.ok(value1 !== value2, 'timesincesessionstart');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,716 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
|
||||
import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry';
|
||||
import { NullAppender, ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import Errors = require('vs/base/common/errors');
|
||||
import * as sinon from 'sinon';
|
||||
import { getConfigurationValue } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
const optInStatusEventName: string = 'optInStatus';
|
||||
|
||||
class TestTelemetryAppender implements ITelemetryAppender {
|
||||
|
||||
public events: any[];
|
||||
public isDisposed: boolean;
|
||||
|
||||
constructor() {
|
||||
this.events = [];
|
||||
this.isDisposed = false;
|
||||
}
|
||||
|
||||
public log(eventName: string, data?: any): void {
|
||||
this.events.push({ eventName, data });
|
||||
}
|
||||
|
||||
public getEventsCount() {
|
||||
return this.events.length;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorTestingSettings {
|
||||
public personalInfo;
|
||||
public importantInfo;
|
||||
public filePrefix;
|
||||
public dangerousPathWithoutImportantInfo;
|
||||
public dangerousPathWithImportantInfo;
|
||||
public missingModelPrefix;
|
||||
public missingModelMessage;
|
||||
public noSuchFilePrefix;
|
||||
public noSuchFileMessage;
|
||||
public stack;
|
||||
|
||||
constructor() {
|
||||
this.personalInfo = 'DANGEROUS/PATH';
|
||||
this.importantInfo = 'important/information';
|
||||
this.filePrefix = 'file:///';
|
||||
this.dangerousPathWithImportantInfo = this.filePrefix + this.personalInfo + '/resources/app/' + this.importantInfo;
|
||||
this.dangerousPathWithoutImportantInfo = this.filePrefix + this.personalInfo;
|
||||
|
||||
this.missingModelPrefix = 'Received model events for missing model ';
|
||||
this.missingModelMessage = this.missingModelPrefix + ' ' + this.dangerousPathWithoutImportantInfo;
|
||||
|
||||
this.noSuchFilePrefix = 'ENOENT: no such file or directory';
|
||||
this.noSuchFileMessage = this.noSuchFilePrefix + ' \'' + this.personalInfo + '\'';
|
||||
|
||||
this.stack = ['at e._modelEvents (a/path/that/doesnt/contain/code/names.js:11:7309)',
|
||||
' at t.AllWorkers (a/path/that/doesnt/contain/code/names.js:6:8844)',
|
||||
' at e.(anonymous function) [as _modelEvents] (a/path/that/doesnt/contain/code/names.js:5:29552)',
|
||||
' at Function.<anonymous> (a/path/that/doesnt/contain/code/names.js:6:8272)',
|
||||
' at e.dispatch (a/path/that/doesnt/contain/code/names.js:5:26931)',
|
||||
' at e.request (a/path/that/doesnt/contain/code/names.js:14:1745)',
|
||||
' at t._handleMessage (another/path/that/doesnt/contain/code/names.js:14:17447)',
|
||||
' at t._onmessage (another/path/that/doesnt/contain/code/names.js:14:16976)',
|
||||
' at t.onmessage (another/path/that/doesnt/contain/code/names.js:14:15854)',
|
||||
' at DedicatedWorkerGlobalScope.self.onmessage',
|
||||
this.dangerousPathWithImportantInfo,
|
||||
this.dangerousPathWithoutImportantInfo,
|
||||
this.missingModelMessage,
|
||||
this.noSuchFileMessage];
|
||||
}
|
||||
}
|
||||
|
||||
suite('TelemetryService', () => {
|
||||
|
||||
test('Disposing', sinon.test(function () {
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
|
||||
return service.publicLog('testPrivateEvent').then(() => {
|
||||
assert.equal(testAppender.getEventsCount(), 1);
|
||||
|
||||
service.dispose();
|
||||
assert.equal(!testAppender.isDisposed, true);
|
||||
});
|
||||
}));
|
||||
|
||||
// event reporting
|
||||
test('Simple event', sinon.test(function () {
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
|
||||
return service.publicLog('testEvent').then(_ => {
|
||||
assert.equal(testAppender.getEventsCount(), 1);
|
||||
assert.equal(testAppender.events[0].eventName, 'testEvent');
|
||||
assert.notEqual(testAppender.events[0].data, null);
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
}));
|
||||
|
||||
test('Event with data', sinon.test(function () {
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
|
||||
return service.publicLog('testEvent', {
|
||||
'stringProp': 'property',
|
||||
'numberProp': 1,
|
||||
'booleanProp': true,
|
||||
'complexProp': {
|
||||
'value': 0
|
||||
}
|
||||
}).then(() => {
|
||||
assert.equal(testAppender.getEventsCount(), 1);
|
||||
assert.equal(testAppender.events[0].eventName, 'testEvent');
|
||||
assert.notEqual(testAppender.events[0].data, null);
|
||||
assert.equal(testAppender.events[0].data['stringProp'], 'property');
|
||||
assert.equal(testAppender.events[0].data['numberProp'], 1);
|
||||
assert.equal(testAppender.events[0].data['booleanProp'], true);
|
||||
assert.equal(testAppender.events[0].data['complexProp'].value, 0);
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
test('common properties added to *all* events, simple event', function () {
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({
|
||||
appender: testAppender,
|
||||
commonProperties: TPromise.as({ foo: 'JA!', get bar() { return Math.random(); } })
|
||||
}, undefined);
|
||||
|
||||
return service.publicLog('testEvent').then(_ => {
|
||||
let [first] = testAppender.events;
|
||||
|
||||
assert.equal(Object.keys(first.data).length, 2);
|
||||
assert.equal(typeof first.data['foo'], 'string');
|
||||
assert.equal(typeof first.data['bar'], 'number');
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('common properties added to *all* events, event with data', function () {
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({
|
||||
appender: testAppender,
|
||||
commonProperties: TPromise.as({ foo: 'JA!', get bar() { return Math.random(); } })
|
||||
}, undefined);
|
||||
|
||||
return service.publicLog('testEvent', { hightower: 'xl', price: 8000 }).then(_ => {
|
||||
let [first] = testAppender.events;
|
||||
|
||||
assert.equal(Object.keys(first.data).length, 4);
|
||||
assert.equal(typeof first.data['foo'], 'string');
|
||||
assert.equal(typeof first.data['bar'], 'number');
|
||||
assert.equal(typeof first.data['hightower'], 'string');
|
||||
assert.equal(typeof first.data['price'], 'number');
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('TelemetryInfo comes from properties', function () {
|
||||
let service = new TelemetryService({
|
||||
appender: NullAppender,
|
||||
commonProperties: TPromise.as({
|
||||
sessionID: 'one',
|
||||
['common.instanceId']: 'two',
|
||||
['common.machineId']: 'three',
|
||||
})
|
||||
}, undefined);
|
||||
|
||||
return service.getTelemetryInfo().then(info => {
|
||||
assert.equal(info.sessionId, 'one');
|
||||
assert.equal(info.instanceId, 'two');
|
||||
assert.equal(info.machineId, 'three');
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('enableTelemetry on by default', sinon.test(function () {
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
|
||||
return service.publicLog('testEvent').then(() => {
|
||||
assert.equal(testAppender.getEventsCount(), 1);
|
||||
assert.equal(testAppender.events[0].eventName, 'testEvent');
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
}));
|
||||
|
||||
test('Error events', sinon.test(function () {
|
||||
|
||||
let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler();
|
||||
Errors.setUnexpectedErrorHandler(() => { });
|
||||
|
||||
try {
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
|
||||
let e: any = new Error('This is a test.');
|
||||
// for Phantom
|
||||
if (!e.stack) {
|
||||
e.stack = 'blah';
|
||||
}
|
||||
|
||||
Errors.onUnexpectedError(e);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
assert.equal(testAppender.getEventsCount(), 1);
|
||||
assert.equal(testAppender.events[0].eventName, 'UnhandledError');
|
||||
assert.equal(testAppender.events[0].data.message, 'This is a test.');
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
} finally {
|
||||
Errors.setUnexpectedErrorHandler(origErrorHandler);
|
||||
}
|
||||
}));
|
||||
|
||||
// test('Unhandled Promise Error events', sinon.test(function() {
|
||||
//
|
||||
// let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler();
|
||||
// Errors.setUnexpectedErrorHandler(() => {});
|
||||
//
|
||||
// try {
|
||||
// let service = new MainTelemetryService();
|
||||
// let testAppender = new TestTelemetryAppender();
|
||||
// service.addTelemetryAppender(testAppender);
|
||||
//
|
||||
// winjs.Promise.wrapError(new Error('This should not get logged'));
|
||||
// winjs.TPromise.as(true).then(() => {
|
||||
// throw new Error('This should get logged');
|
||||
// });
|
||||
// // prevent console output from failing the test
|
||||
// this.stub(console, 'log');
|
||||
// // allow for the promise to finish
|
||||
// this.clock.tick(MainErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
//
|
||||
// assert.equal(testAppender.getEventsCount(), 1);
|
||||
// assert.equal(testAppender.events[0].eventName, 'UnhandledError');
|
||||
// assert.equal(testAppender.events[0].data.message, 'This should get logged');
|
||||
//
|
||||
// service.dispose();
|
||||
// } finally {
|
||||
// Errors.setUnexpectedErrorHandler(origErrorHandler);
|
||||
// }
|
||||
// }));
|
||||
|
||||
test('Handle global errors', sinon.test(function () {
|
||||
let errorStub = sinon.stub();
|
||||
window.onerror = errorStub;
|
||||
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let testError = new Error('test');
|
||||
(<any>window.onerror)('Error Message', 'file.js', 2, 42, testError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.equal(errorStub.alwaysCalledWithExactly('Error Message', 'file.js', 2, 42, testError), true);
|
||||
assert.equal(errorStub.callCount, 1);
|
||||
|
||||
assert.equal(testAppender.getEventsCount(), 1);
|
||||
assert.equal(testAppender.events[0].eventName, 'UnhandledError');
|
||||
assert.equal(testAppender.events[0].data.message, 'Error Message');
|
||||
assert.equal(testAppender.events[0].data.filename, 'file.js');
|
||||
assert.equal(testAppender.events[0].data.line, 2);
|
||||
assert.equal(testAppender.events[0].data.column, 42);
|
||||
assert.equal(testAppender.events[0].data.error.message, 'test');
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
}));
|
||||
|
||||
test('Uncaught Error Telemetry removes PII from filename', sinon.test(function () {
|
||||
let errorStub = sinon.stub();
|
||||
window.onerror = errorStub;
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let dangerousFilenameError: any = new Error('dangerousFilename');
|
||||
dangerousFilenameError.stack = settings.stack;
|
||||
(<any>window.onerror)('dangerousFilename', settings.dangerousPathWithImportantInfo + '/test.js', 2, 42, dangerousFilenameError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.equal(errorStub.callCount, 1);
|
||||
assert.equal(testAppender.events[0].data.filename.indexOf(settings.dangerousPathWithImportantInfo), -1);
|
||||
|
||||
dangerousFilenameError = new Error('dangerousFilename');
|
||||
dangerousFilenameError.stack = settings.stack;
|
||||
(<any>window.onerror)('dangerousFilename', settings.dangerousPathWithImportantInfo + '/test.js', 2, 42, dangerousFilenameError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.equal(errorStub.callCount, 2);
|
||||
assert.equal(testAppender.events[0].data.filename.indexOf(settings.dangerousPathWithImportantInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.filename, settings.importantInfo + '/test.js');
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
}));
|
||||
|
||||
test('Unexpected Error Telemetry removes PII', sinon.test(function () {
|
||||
let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler();
|
||||
Errors.setUnexpectedErrorHandler(() => { });
|
||||
try {
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let dangerousPathWithoutImportantInfoError: any = new Error(settings.dangerousPathWithoutImportantInfo);
|
||||
dangerousPathWithoutImportantInfoError.stack = settings.stack;
|
||||
Errors.onUnexpectedError(dangerousPathWithoutImportantInfoError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1);
|
||||
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length);
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
}
|
||||
finally {
|
||||
Errors.setUnexpectedErrorHandler(origErrorHandler);
|
||||
}
|
||||
}));
|
||||
|
||||
test('Uncaught Error Telemetry removes PII', sinon.test(function () {
|
||||
let errorStub = sinon.stub();
|
||||
window.onerror = errorStub;
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let dangerousPathWithoutImportantInfoError: any = new Error('dangerousPathWithoutImportantInfo');
|
||||
dangerousPathWithoutImportantInfoError.stack = settings.stack;
|
||||
(<any>window.onerror)(settings.dangerousPathWithoutImportantInfo, 'test.js', 2, 42, dangerousPathWithoutImportantInfoError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.equal(errorStub.callCount, 1);
|
||||
// Test that no file information remains, esp. personal info
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length);
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
}));
|
||||
|
||||
test('Unexpected Error Telemetry removes PII but preserves Code file path', sinon.test(function () {
|
||||
|
||||
let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler();
|
||||
Errors.setUnexpectedErrorHandler(() => { });
|
||||
|
||||
try {
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo);
|
||||
dangerousPathWithImportantInfoError.stack = settings.stack;
|
||||
|
||||
// Test that important information remains but personal info does not
|
||||
Errors.onUnexpectedError(dangerousPathWithImportantInfoError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.notEqual(testAppender.events[0].data.message.indexOf(settings.importantInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.importantInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length);
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
}
|
||||
finally {
|
||||
Errors.setUnexpectedErrorHandler(origErrorHandler);
|
||||
}
|
||||
}));
|
||||
|
||||
test('Uncaught Error Telemetry removes PII but preserves Code file path', sinon.test(function () {
|
||||
let errorStub = sinon.stub();
|
||||
window.onerror = errorStub;
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo');
|
||||
dangerousPathWithImportantInfoError.stack = settings.stack;
|
||||
(<any>window.onerror)(settings.dangerousPathWithImportantInfo, 'test.js', 2, 42, dangerousPathWithImportantInfoError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.equal(errorStub.callCount, 1);
|
||||
// Test that important information remains but personal info does not
|
||||
assert.notEqual(testAppender.events[0].data.message.indexOf(settings.importantInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.importantInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length);
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
}));
|
||||
|
||||
test('Unexpected Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinon.test(function () {
|
||||
|
||||
let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler();
|
||||
Errors.setUnexpectedErrorHandler(() => { });
|
||||
|
||||
try {
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender, piiPaths: [settings.personalInfo + '/resources/app/'] }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo);
|
||||
dangerousPathWithImportantInfoError.stack = settings.stack;
|
||||
|
||||
// Test that important information remains but personal info does not
|
||||
Errors.onUnexpectedError(dangerousPathWithImportantInfoError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.notEqual(testAppender.events[0].data.message.indexOf(settings.importantInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.importantInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length);
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
}
|
||||
finally {
|
||||
Errors.setUnexpectedErrorHandler(origErrorHandler);
|
||||
}
|
||||
}));
|
||||
|
||||
test('Uncaught Error Telemetry removes PII but preserves Code file path when PIIPath is configured', sinon.test(function () {
|
||||
let errorStub = sinon.stub();
|
||||
window.onerror = errorStub;
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender, piiPaths: [settings.personalInfo + '/resources/app/'] }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo');
|
||||
dangerousPathWithImportantInfoError.stack = settings.stack;
|
||||
(<any>window.onerror)(settings.dangerousPathWithImportantInfo, 'test.js', 2, 42, dangerousPathWithImportantInfoError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.equal(errorStub.callCount, 1);
|
||||
// Test that important information remains but personal info does not
|
||||
assert.notEqual(testAppender.events[0].data.message.indexOf(settings.importantInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.importantInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length);
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
}));
|
||||
|
||||
test('Unexpected Error Telemetry removes PII but preserves Missing Model error message', sinon.test(function () {
|
||||
|
||||
let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler();
|
||||
Errors.setUnexpectedErrorHandler(() => { });
|
||||
|
||||
try {
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let missingModelError: any = new Error(settings.missingModelMessage);
|
||||
missingModelError.stack = settings.stack;
|
||||
|
||||
// Test that no file information remains, but this particular
|
||||
// error message does (Received model events for missing model)
|
||||
Errors.onUnexpectedError(missingModelError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.notEqual(testAppender.events[0].data.message.indexOf(settings.missingModelPrefix), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.missingModelPrefix), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length);
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
} finally {
|
||||
Errors.setUnexpectedErrorHandler(origErrorHandler);
|
||||
}
|
||||
}));
|
||||
|
||||
test('Uncaught Error Telemetry removes PII but preserves Missing Model error message', sinon.test(function () {
|
||||
let errorStub = sinon.stub();
|
||||
window.onerror = errorStub;
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let missingModelError: any = new Error('missingModelMessage');
|
||||
missingModelError.stack = settings.stack;
|
||||
(<any>window.onerror)(settings.missingModelMessage, 'test.js', 2, 42, missingModelError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.equal(errorStub.callCount, 1);
|
||||
// Test that no file information remains, but this particular
|
||||
// error message does (Received model events for missing model)
|
||||
assert.notEqual(testAppender.events[0].data.message.indexOf(settings.missingModelPrefix), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.missingModelPrefix), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length);
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
}));
|
||||
|
||||
test('Unexpected Error Telemetry removes PII but preserves No Such File error message', sinon.test(function () {
|
||||
|
||||
let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler();
|
||||
Errors.setUnexpectedErrorHandler(() => { });
|
||||
|
||||
try {
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let noSuchFileError: any = new Error(settings.noSuchFileMessage);
|
||||
noSuchFileError.stack = settings.stack;
|
||||
|
||||
// Test that no file information remains, but this particular
|
||||
// error message does (ENOENT: no such file or directory)
|
||||
Errors.onUnexpectedError(noSuchFileError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.notEqual(testAppender.events[0].data.message.indexOf(settings.noSuchFilePrefix), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.noSuchFilePrefix), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length);
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
} finally {
|
||||
Errors.setUnexpectedErrorHandler(origErrorHandler);
|
||||
}
|
||||
}));
|
||||
|
||||
test('Uncaught Error Telemetry removes PII but preserves No Such File error message', sinon.test(function () {
|
||||
let origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler();
|
||||
Errors.setUnexpectedErrorHandler(() => { });
|
||||
|
||||
try {
|
||||
let errorStub = sinon.stub();
|
||||
window.onerror = errorStub;
|
||||
let settings = new ErrorTestingSettings();
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ appender: testAppender }, undefined);
|
||||
const errorTelemetry = new ErrorTelemetry(service);
|
||||
|
||||
let noSuchFileError: any = new Error('noSuchFileMessage');
|
||||
noSuchFileError.stack = settings.stack;
|
||||
(<any>window.onerror)(settings.noSuchFileMessage, 'test.js', 2, 42, noSuchFileError);
|
||||
this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT);
|
||||
|
||||
assert.equal(errorStub.callCount, 1);
|
||||
// Test that no file information remains, but this particular
|
||||
// error message does (ENOENT: no such file or directory)
|
||||
Errors.onUnexpectedError(noSuchFileError);
|
||||
assert.notEqual(testAppender.events[0].data.message.indexOf(settings.noSuchFilePrefix), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.message.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.noSuchFilePrefix), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.personalInfo), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.indexOf(settings.filePrefix), -1);
|
||||
assert.notEqual(testAppender.events[0].data.stack.indexOf(settings.stack[4]), -1);
|
||||
assert.equal(testAppender.events[0].data.stack.split('\n').length, settings.stack.length);
|
||||
|
||||
errorTelemetry.dispose();
|
||||
service.dispose();
|
||||
} finally {
|
||||
Errors.setUnexpectedErrorHandler(origErrorHandler);
|
||||
}
|
||||
}));
|
||||
|
||||
test('Telemetry Service respects user opt-in settings', sinon.test(function () {
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ userOptIn: false, appender: testAppender }, undefined);
|
||||
|
||||
return service.publicLog('testEvent').then(() => {
|
||||
assert.equal(testAppender.getEventsCount(), 0);
|
||||
service.dispose();
|
||||
});
|
||||
}));
|
||||
|
||||
test('Telemetry Service does not sent optInStatus when user opted out', sinon.test(function () {
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ userOptIn: false, appender: testAppender }, undefined);
|
||||
|
||||
return service.publicLog(optInStatusEventName, { optIn: false }).then(() => {
|
||||
assert.equal(testAppender.getEventsCount(), 0);
|
||||
service.dispose();
|
||||
});
|
||||
}));
|
||||
|
||||
test('Telemetry Service sends events when enableTelemetry is on even user optin is on', sinon.test(function () {
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({ userOptIn: true, appender: testAppender }, undefined);
|
||||
|
||||
return service.publicLog('testEvent').then(() => {
|
||||
assert.equal(testAppender.getEventsCount(), 1);
|
||||
service.dispose();
|
||||
});
|
||||
}));
|
||||
|
||||
test('Telemetry Service checks with config service', function () {
|
||||
|
||||
let enableTelemetry = false;
|
||||
let emitter = new Emitter<any>();
|
||||
|
||||
let testAppender = new TestTelemetryAppender();
|
||||
let service = new TelemetryService({
|
||||
appender: testAppender
|
||||
}, {
|
||||
_serviceBrand: undefined,
|
||||
getConfiguration() {
|
||||
return {
|
||||
enableTelemetry: enableTelemetry
|
||||
} as any;
|
||||
},
|
||||
getConfigurationData(): any {
|
||||
return null;
|
||||
},
|
||||
reloadConfiguration() {
|
||||
return TPromise.as(this.getConfiguration());
|
||||
},
|
||||
lookup(key: string) {
|
||||
return {
|
||||
value: getConfigurationValue(this.getConfiguration(), key),
|
||||
default: getConfigurationValue(this.getConfiguration(), key),
|
||||
user: getConfigurationValue(this.getConfiguration(), key),
|
||||
workspace: null,
|
||||
folder: null
|
||||
};
|
||||
},
|
||||
keys() { return { default: [], user: [], workspace: [], folder: [] }; },
|
||||
values() { return {}; },
|
||||
onDidUpdateConfiguration: emitter.event
|
||||
});
|
||||
|
||||
assert.equal(service.isOptedIn, false);
|
||||
|
||||
enableTelemetry = true;
|
||||
emitter.fire({});
|
||||
assert.equal(service.isOptedIn, true);
|
||||
|
||||
enableTelemetry = false;
|
||||
emitter.fire({});
|
||||
assert.equal(service.isOptedIn, false);
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user