Fix incorrect usage of TelemetryProcessor (#221)

* Fix incorrect usage of TelemetryProcessor

* Address feedback
This commit is contained in:
Tanuj Mittal 2020-09-22 12:19:06 -07:00 committed by GitHub
parent 3f2c67af23
commit 7c5c8ddb7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 132 additions and 104 deletions

View File

@ -562,3 +562,11 @@ export class AnalyticalStorageTtl {
public static readonly Infinite: number = -1; public static readonly Infinite: number = -1;
public static readonly Disabled: number = 0; public static readonly Disabled: number = 0;
} }
export class TerminalQueryParams {
public static readonly Terminal = "terminal";
public static readonly Server = "server";
public static readonly Token = "token";
public static readonly SubscriptionId = "subscriptionId";
public static readonly TerminalEndpoint = "terminalEndpoint";
}

View File

@ -25,34 +25,4 @@ describe("Message Handler", () => {
MessageHandler.runGarbageCollector(); MessageHandler.runGarbageCollector();
expect(MessageHandler.RequestMap["123"]).toBeUndefined(); expect(MessageHandler.RequestMap["123"]).toBeUndefined();
}); });
describe("getDataExplorerWindow", () => {
it("should return current window if current window has dataExplorerPlatform property", () => {
const currentWindow: Window = { dataExplorerPlatform: 0 } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toEqual(currentWindow);
});
it("should return current window's parent if current window's parent has dataExplorerPlatform property", () => {
const parentWindow: Window = { dataExplorerPlatform: 0 } as any;
const currentWindow: Window = { parent: parentWindow } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toEqual(parentWindow);
});
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is reference to itself", () => {
const parentWindow: Window = {} as any;
(parentWindow as any).parent = parentWindow; // If a window does not have a parent, its parent property is a reference to itself.
const currentWindow: Window = { parent: parentWindow } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toBeUndefined();
});
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is not defined", () => {
const parentWindow: Window = {} as any;
const currentWindow: Window = { parent: parentWindow } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toBeUndefined();
});
});
}); });

View File

@ -2,6 +2,7 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
import Q from "q"; import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "./Constants"; import * as Constants from "./Constants";
import { getDataExplorerWindow } from "../Utils/WindowUtils";
export interface CachedDataPromise<T> { export interface CachedDataPromise<T> {
deferred: Q.Deferred<T>; deferred: Q.Deferred<T>;
@ -61,25 +62,6 @@ export function sendMessage(data: any): void {
} }
} }
// Only exported for unit tests
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
// Start with the current window and traverse up the parent hierarchy to find a window
// with `dataExplorerPlatform` property
let dataExplorerWindow: Window | undefined = currentWindow;
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
// eslint-disable-next-line @typescript-eslint/no-explicit-any
while (dataExplorerWindow && (dataExplorerWindow as any).dataExplorerPlatform == undefined) {
// If a window does not have a parent, its parent property is a reference to itself.
if (dataExplorerWindow.parent == dataExplorerWindow) {
dataExplorerWindow = undefined;
} else {
dataExplorerWindow = dataExplorerWindow.parent;
}
}
return dataExplorerWindow;
};
export function canSendMessage(): boolean { export function canSendMessage(): boolean {
return window.parent !== window; return window.parent !== window;
} }

View File

@ -8,6 +8,8 @@ import * as Logger from "../../../Common/Logger";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { StringUtils } from "../../../Utils/StringUtils"; import { StringUtils } from "../../../Utils/StringUtils";
import { userContext } from "../../../UserContext";
import { TerminalQueryParams } from "../../../Common/Constants";
export interface NotebookTerminalComponentProps { export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
@ -32,11 +34,11 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
public getTerminalParams(): Map<string, string> { public getTerminalParams(): Map<string, string> {
let params: Map<string, string> = new Map<string, string>(); let params: Map<string, string> = new Map<string, string>();
params.set("terminal", "true"); params.set(TerminalQueryParams.Terminal, "true");
const terminalEndpoint: string = this.tryGetTerminalEndpoint(); const terminalEndpoint: string = this.tryGetTerminalEndpoint();
if (terminalEndpoint) { if (terminalEndpoint) {
params.set("terminalEndpoint", terminalEndpoint); params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
} }
return params; return params;
@ -75,11 +77,13 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
return ""; return "";
} }
params.set("server", serverInfo.notebookServerEndpoint); params.set(TerminalQueryParams.Server, serverInfo.notebookServerEndpoint);
if (serverInfo.authToken && serverInfo.authToken.length > 0) { if (serverInfo.authToken && serverInfo.authToken.length > 0) {
params.set("token", serverInfo.authToken); params.set(TerminalQueryParams.Token, serverInfo.authToken);
} }
params.set(TerminalQueryParams.SubscriptionId, userContext.subscriptionId);
let result: string = "terminal.html?"; let result: string = "terminal.html?";
for (let key of params.keys()) { for (let key of params.keys()) {
result += `${key}=${encodeURIComponent(params.get(key))}&`; result += `${key}=${encodeURIComponent(params.get(key))}&`;

View File

@ -134,11 +134,9 @@ describe("Delete Collection Confirmation Pane", () => {
expect(telemetryProcessorSpy.called).toBe(true); expect(telemetryProcessorSpy.called).toBe(true);
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback); let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
expect( expect(
telemetryProcessorSpy.calledWith( telemetryProcessorSpy.calledWith(Action.DeleteCollection, ActionModifiers.Mark, {
Action.DeleteCollection, message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
ActionModifiers.Mark, })
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
)
).toBe(true); ).toBe(true);
}); });
}); });

View File

@ -88,11 +88,9 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
this.containerDeleteFeedback() this.containerDeleteFeedback()
); );
TelemetryProcessor.trace( TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
Action.DeleteCollection, message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
ActionModifiers.Mark, });
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
);
this.containerDeleteFeedback(""); this.containerDeleteFeedback("");
} }

View File

@ -120,11 +120,9 @@ describe("Delete Database Confirmation Pane", () => {
return pane.submit().then(() => { return pane.submit().then(() => {
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback); let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
expect(TelemetryProcessor.trace).toHaveBeenCalledWith( expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
Action.DeleteDatabase, message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
ActionModifiers.Mark, });
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
);
}); });
}); });
}); });

View File

@ -97,11 +97,9 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
this.databaseDeleteFeedback() this.databaseDeleteFeedback()
); );
TelemetryProcessor.trace( TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, {
Action.DeleteDatabase, message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
ActionModifiers.Mark, });
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
);
this.databaseDeleteFeedback(""); this.databaseDeleteFeedback("");
} }

View File

@ -142,7 +142,7 @@ export default class MongoShellTab extends TabsBase {
return; return;
} }
const dataToLog: string = event.data.data.logData; const dataToLog = { message: event.data.data.logData };
const logType: string = event.data.data.logType; const logType: string = event.data.data.logType;
const shellTraceId: string = event.data.data.traceId || "none"; const shellTraceId: string = event.data.data.traceId || "none";

View File

@ -4,12 +4,15 @@ import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { appInsights } from "../appInsights"; import { appInsights } from "../appInsights";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getDataExplorerWindow } from "../../Utils/WindowUtils";
/** /**
* Class that persists telemetry data to the portal tables. * Class that persists telemetry data to the portal tables.
*/ */
export function trace(action: Action, actionModifier: string = ActionModifiers.Mark, data?: unknown): void { type TelemetryData = { [key: string]: unknown };
export function trace(action: Action, actionModifier: string = ActionModifiers.Mark, data?: TelemetryData): void {
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
data: { data: {
@ -22,7 +25,7 @@ export function trace(action: Action, actionModifier: string = ActionModifiers.M
appInsights.trackEvent({ name: Action[action] }, getData(actionModifier, data)); appInsights.trackEvent({ name: Action[action] }, getData(actionModifier, data));
} }
export function traceStart(action: Action, data?: unknown): number { export function traceStart(action: Action, data?: TelemetryData): number {
const timestamp: number = Date.now(); const timestamp: number = Date.now();
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
@ -38,7 +41,7 @@ export function traceStart(action: Action, data?: unknown): number {
return timestamp; return timestamp;
} }
export function traceSuccess(action: Action, data?: unknown, timestamp?: number): void { export function traceSuccess(action: Action, data?: TelemetryData, timestamp?: number): void {
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
data: { data: {
@ -52,7 +55,7 @@ export function traceSuccess(action: Action, data?: unknown, timestamp?: number)
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Success, data)); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Success, data));
} }
export function traceFailure(action: Action, data?: unknown, timestamp?: number): void { export function traceFailure(action: Action, data?: TelemetryData, timestamp?: number): void {
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
data: { data: {
@ -66,7 +69,7 @@ export function traceFailure(action: Action, data?: unknown, timestamp?: number)
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Failed, data)); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Failed, data));
} }
export function traceCancel(action: Action, data?: unknown, timestamp?: number): void { export function traceCancel(action: Action, data?: TelemetryData, timestamp?: number): void {
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
data: { data: {
@ -80,7 +83,7 @@ export function traceCancel(action: Action, data?: unknown, timestamp?: number):
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Cancel, data)); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Cancel, data));
} }
export function traceOpen(action: Action, data?: unknown, timestamp?: number): number { export function traceOpen(action: Action, data?: TelemetryData, timestamp?: number): number {
const validTimestamp = timestamp || Date.now(); const validTimestamp = timestamp || Date.now();
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
@ -96,7 +99,7 @@ export function traceOpen(action: Action, data?: unknown, timestamp?: number): n
return validTimestamp; return validTimestamp;
} }
export function traceMark(action: Action, data?: unknown, timestamp?: number): number { export function traceMark(action: Action, data?: TelemetryData, timestamp?: number): number {
const validTimestamp = timestamp || Date.now(); const validTimestamp = timestamp || Date.now();
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
@ -112,15 +115,12 @@ export function traceMark(action: Action, data?: unknown, timestamp?: number): n
return validTimestamp; return validTimestamp;
} }
function getData(actionModifier: string, data: unknown = {}): { [key: string]: string } | undefined { function getData(actionModifier: string, data: TelemetryData = {}): { [key: string]: string } {
if (typeof data === "string") { const dataExplorerWindow = getDataExplorerWindow(window);
data = { message: data };
}
if (typeof data === "object") {
return { return {
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet // TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
authType: (window as any).authType, authType: dataExplorerWindow && (dataExplorerWindow as any).authType,
subscriptionId: userContext.subscriptionId as string, subscriptionId: userContext.subscriptionId as string,
platform: configContext.platform, platform: configContext.platform,
env: process.env.NODE_ENV as string, env: process.env.NODE_ENV as string,
@ -128,5 +128,3 @@ function getData(actionModifier: string, data: unknown = {}): { [key: string]: s
...data ...data
}; };
} }
return undefined;
}

View File

@ -6,6 +6,8 @@ import { ServerConnection } from "@jupyterlab/services";
import { JupyterLabAppFactory } from "./JupyterLabAppFactory"; import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
import { Action } from "../Shared/Telemetry/TelemetryConstants"; import { Action } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../UserContext";
import { TerminalQueryParams } from "../Common/Constants";
const getUrlVars = (): { [key: string]: string } => { const getUrlVars = (): { [key: string]: string } => {
const vars: { [key: string]: string } = {}; const vars: { [key: string]: string } = {};
@ -18,22 +20,22 @@ const getUrlVars = (): { [key: string]: string } => {
const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => { const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => {
let body: BodyInit; let body: BodyInit;
if (urlVars.hasOwnProperty("terminalEndpoint")) { if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
body = JSON.stringify({ body = JSON.stringify({
endpoint: urlVars["terminalEndpoint"] endpoint: urlVars[TerminalQueryParams.TerminalEndpoint]
}); });
} }
const server = urlVars["server"]; const server = urlVars[TerminalQueryParams.Server];
let options: Partial<ServerConnection.ISettings> = { let options: Partial<ServerConnection.ISettings> = {
baseUrl: server, baseUrl: server,
init: { body }, init: { body },
fetch: window.parent.fetch fetch: window.parent.fetch
}; };
if (urlVars.hasOwnProperty("token")) { if (urlVars.hasOwnProperty(TerminalQueryParams.Token)) {
options = { options = {
baseUrl: server, baseUrl: server,
token: urlVars["token"], token: urlVars[TerminalQueryParams.Token],
init: { body }, init: { body },
fetch: window.parent.fetch fetch: window.parent.fetch
}; };
@ -44,6 +46,12 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
const urlVars = getUrlVars(); const urlVars = getUrlVars();
// Initialize userContext. Currently only subscrptionId is required by TelemetryProcessor
updateUserContext({
subscriptionId: urlVars[TerminalQueryParams.SubscriptionId]
});
const serverSettings = createServerSettings(urlVars); const serverSettings = createServerSettings(urlVars);
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, { const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, {
@ -51,7 +59,7 @@ const main = async (): Promise<void> => {
}); });
try { try {
if (urlVars.hasOwnProperty("terminal")) { if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) {
await JupyterLabAppFactory.createTerminalApp(serverSettings); await JupyterLabAppFactory.createTerminalApp(serverSettings);
} else { } else {
throw new Error("Only terminal is supported"); throw new Error("Only terminal is supported");

View File

@ -0,0 +1,49 @@
import { getDataExplorerWindow } from "./WindowUtils";
const createWindow = (dataExplorerPlatform: unknown, parent: Window): Window => {
// TODO: Need to `any` here since we're creating a mock window object
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockWindow: any = {};
if (dataExplorerPlatform !== undefined) {
mockWindow.dataExplorerPlatform = dataExplorerPlatform;
}
if (parent) {
mockWindow.parent = parent;
}
return mockWindow;
};
describe("WindowUtils", () => {
describe("getDataExplorerWindow", () => {
it("should return current window if current window has dataExplorerPlatform property", () => {
const currentWindow = createWindow(0, undefined);
expect(getDataExplorerWindow(currentWindow)).toEqual(currentWindow);
});
it("should return current window's parent if current window's parent has dataExplorerPlatform property", () => {
const parentWindow = createWindow(0, undefined);
const currentWindow = createWindow(undefined, parentWindow);
expect(getDataExplorerWindow(currentWindow)).toEqual(parentWindow);
});
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is reference to itself", () => {
const parentWindow = createWindow(undefined, undefined);
// TODO: Need to `any` here since parent is a readonly property
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(parentWindow as any).parent = parentWindow; // If a window does not have a parent, its parent property is a reference to itself.
const currentWindow = createWindow(undefined, parentWindow);
expect(getDataExplorerWindow(currentWindow)).toBeUndefined();
});
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is not defined", () => {
const parentWindow = createWindow(undefined, undefined);
const currentWindow = createWindow(undefined, parentWindow);
expect(getDataExplorerWindow(currentWindow)).toBeUndefined();
});
});
});

17
src/Utils/WindowUtils.ts Normal file
View File

@ -0,0 +1,17 @@
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
// Start with the current window and traverse up the parent hierarchy to find a window
// with `dataExplorerPlatform` property
let dataExplorerWindow: Window | undefined = currentWindow;
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
// eslint-disable-next-line @typescript-eslint/no-explicit-any
while (dataExplorerWindow && (dataExplorerWindow as any).dataExplorerPlatform === undefined) {
// If a window does not have a parent, its parent property is a reference to itself.
if (dataExplorerWindow.parent === dataExplorerWindow) {
dataExplorerWindow = undefined;
} else {
dataExplorerWindow = dataExplorerWindow.parent;
}
}
return dataExplorerWindow;
};