Add more Telemetry to Data Explorer (#242)

* Add Telemetry to command bar buttons

* Count and report # of files/notebooks/directories in myNotebook to telemetry

* Add resource tree clicks to Telemetry

* Log to Telemetry: opened notebook cell counts by type, kernelspec name

* Fix unit test

* Move Telemetry processor call in notebook traceNotebookTelemetry action from reducer to epic. Use action to trace other info.

* Fix react duplicate key error

* Log notebook cell context menu actions

* Reformat and cleanup

* Move resource tree tracing code out of render(). Only call once when tree is updated

* Fix build issues
This commit is contained in:
Laurent Nguyen
2020-10-08 10:53:01 +02:00
committed by GitHub
parent ff03c79399
commit b69174788d
10 changed files with 389 additions and 204 deletions

View File

@@ -32,24 +32,27 @@ import {
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
import { sessions, kernels } from "rx-jupyter";
import { RecordOf } from "immutable";
import { AnyAction } from "redux";
import * as Constants from "../../../Common/Constants";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as CdbActions from "./actions";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action as TelemetryAction } from "../../../Shared/Telemetry/TelemetryConstants";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { CdbAppState } from "./types";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
import * as TextFile from "./contents/file/text-file";
import { NotebookUtil } from "../NotebookUtil";
import { FileSystemUtil } from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions";
import { Areas } from "../../../Common/Constants";
interface NotebookServiceConfig extends JupyterServerConfig {
userPuid?: string;
}
const logToTelemetry = (state: CdbAppState, title: string, error?: string) => {
const logFailureToTelemetry = (state: CdbAppState, title: string, error?: string) => {
TelemetryProcessor.traceFailure(TelemetryAction.NotebookErrorNotification, {
databaseAccountName: state.cdb.databaseAccountName,
defaultExperience: state.cdb.defaultExperience,
@@ -311,7 +314,7 @@ export const launchWebSocketKernelEpic = (
kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logToTelemetry(state$.value, "Launching alternate kernel", msg);
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
} else {
return of(
actions.launchKernelFailed({
@@ -337,7 +340,7 @@ export const launchWebSocketKernelEpic = (
msg += ` Using default kernel: ${kernelSpecToLaunch}`;
}
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logToTelemetry(state$.value, "Launching alternate kernel", msg);
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
}
const sessionPayload = {
@@ -634,7 +637,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
const title = "Kernel restart";
const msg = "Kernel successfully restarted";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logToTelemetry(state$.value, title, msg);
logFailureToTelemetry(state$.value, title, msg);
break;
}
case actions.RESTART_KERNEL_FAILED:
@@ -645,7 +648,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
const title = "Save failure";
const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logToTelemetry(state$.value, title, msg);
logFailureToTelemetry(state$.value, title, msg);
break;
}
case actions.FETCH_CONTENT_FAILED: {
@@ -654,7 +657,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
const title = "Fetching content failure";
const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logToTelemetry(state$.value, title, msg);
logFailureToTelemetry(state$.value, title, msg);
break;
}
}
@@ -679,7 +682,7 @@ const handleKernelConnectionLostEpic = (
const msg = "Notebook was disconnected from kernel";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logToTelemetry(state, "Error", "Kernel connection error");
logFailureToTelemetry(state, "Error", "Kernel connection error");
const host = selectors.currentHost(state);
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host as RecordOf<JupyterHostRecordProps>);
@@ -692,7 +695,7 @@ const handleKernelConnectionLostEpic = (
const msg =
"Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically.";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logToTelemetry(state, "Kernel restart error", msg);
logFailureToTelemetry(state, "Kernel restart error", msg);
const explorer = window.dataExplorer;
if (explorer) {
@@ -844,6 +847,105 @@ const closeContentFailedToFetchEpic = (
);
};
const traceNotebookTelemetryEpic = (
action$: Observable<cdbActions.TraceNotebookTelemetryAction>,
state$: StateObservable<CdbAppState>
): Observable<{}> => {
return action$.pipe(
ofType(cdbActions.TRACE_NOTEBOOK_TELEMETRY),
mergeMap((action: cdbActions.TraceNotebookTelemetryAction) => {
const state = state$.value;
TelemetryProcessor.trace(action.payload.action, action.payload.actionModifier, {
...action.payload.data,
databaseAccountName: state.cdb.databaseAccountName,
defaultExperience: state.cdb.defaultExperience,
dataExplorerArea: Areas.Notebook
});
return EMPTY;
})
);
};
/**
* Log notebook information to telemetry
* # raw cells, # markdown cells, # code cells, total
* @param action$
* @param state$
*/
const traceNotebookInfoEpic = (
action$: Observable<actions.FetchContentFulfilled>,
state$: StateObservable<AppState>
): Observable<{} | cdbActions.TraceNotebookTelemetryAction> => {
return action$.pipe(
ofType(actions.FETCH_CONTENT_FULFILLED),
mergeMap((action: { payload: any }) => {
const state = state$.value;
const contentRef = action.payload.contentRef;
const model = selectors.model(state, { contentRef });
// If it's not a notebook, we shouldn't be here
if (!model || model.type !== "notebook") {
return EMPTY;
}
const dataToLog = {
nbCodeCells: 0,
nbRawCells: 0,
nbMarkdownCells: 0,
nbCells: 0
};
for (let [id, cell] of selectors.notebook.cellMap(model)) {
switch (cell.cell_type) {
case "code":
dataToLog.nbCodeCells++;
break;
case "markdown":
dataToLog.nbMarkdownCells++;
break;
case "raw":
dataToLog.nbRawCells++;
break;
}
dataToLog.nbCells++;
}
return of(
cdbActions.traceNotebookTelemetry({
action: TelemetryAction.NotebooksFetched,
actionModifier: ActionModifiers.Mark,
data: dataToLog
})
);
})
);
};
/**
* Log Kernel spec to start
* @param action$
* @param state$
*/
const traceNotebookKernelEpic = (
action$: Observable<AnyAction>,
state$: StateObservable<AppState>
): Observable<cdbActions.TraceNotebookTelemetryAction> => {
return action$.pipe(
ofType(actions.LAUNCH_KERNEL_SUCCESSFUL),
mergeMap((action: { payload: any; type: string }) => {
return of(
cdbActions.traceNotebookTelemetry({
action: TelemetryAction.NotebooksKernelSpecName,
actionModifier: ActionModifiers.Mark,
data: {
kernelSpecName: action.payload.kernel.name
}
})
);
})
);
};
export const allEpics = [
addInitialCodeCellEpic,
focusInitialCodeCellEpic,
@@ -856,5 +958,8 @@ export const allEpics = [
executeFocusedCellAndFocusNextEpic,
closeUnsupportedMimetypesEpic,
closeContentFailedToFetchEpic,
restartWebSocketKernelEpic
restartWebSocketKernelEpic,
traceNotebookTelemetryEpic,
traceNotebookInfoEpic,
traceNotebookKernelEpic
];

View File

@@ -1,7 +1,5 @@
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
import { Action } from "redux";
import { Areas } from "../../../Common/Constants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as cdbActions from "./actions";
import { CdbRecord } from "./types";
@@ -72,17 +70,6 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
return state.set("hoveredCellId", typedAction.payload.cellId);
}
case cdbActions.TRACE_NOTEBOOK_TELEMETRY: {
const typedAction = action as cdbActions.TraceNotebookTelemetryAction;
TelemetryProcessor.trace(typedAction.payload.action, typedAction.payload.actionModifier, {
...typedAction.payload.data,
databaseAccountName: state.databaseAccountName,
defaultExperience: state.defaultExperience,
dataExplorerArea: Areas.Notebook
});
return state;
}
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
var parentEltsMap = state.get("currentNotebookParentElements");