Skip to content

Commit

Permalink
chore: Add logger to measure performance metrics (#782)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanDeMicco authored Jan 18, 2019
1 parent 4a84274 commit 7aa67da
Show file tree
Hide file tree
Showing 35 changed files with 752 additions and 67 deletions.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
44 changes: 33 additions & 11 deletions flow-typed/box-ui-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ import {
TASK_COMPLETED,
TASK_INCOMPLETE,
TASK_REJECTED,
METRIC_TYPE_PREVIEW,
METRIC_TYPE_ELEMENTS_LOAD_METRIC,
METRIC_TYPE_ELEMENTS_PERFORMANCE_METRIC,
} from '../src/constants';

type Method =
Expand Down Expand Up @@ -688,25 +691,25 @@ type ErrorResponseData = {

type ElementsXhrError = $AxiosError<any> | ErrorResponseData;

type ErrorOrigins =
| ORIGIN_CONTENT_SIDEBAR
| ORIGIN_CONTENT_PREVIEW
| ORIGIN_PREVIEW
| ORIGIN_DETAILS_SIDEBAR
| ORIGIN_ACTIVITY_SIDEBAR
| ORIGIN_SKILLS_SIDEBAR
| ORIGIN_METADATA_SIDEBAR;
type ElementOrigin =
| typeof ORIGIN_CONTENT_SIDEBAR
| typeof ORIGIN_CONTENT_PREVIEW
| typeof ORIGIN_PREVIEW
| typeof ORIGIN_DETAILS_SIDEBAR
| typeof ORIGIN_ACTIVITY_SIDEBAR
| typeof ORIGIN_SKILLS_SIDEBAR
| typeof ORIGIN_METADATA_SIDEBAR;

type ElementsError = {
type: 'error',
code: string,
message: string,
origin: ErrorOrigins,
origin: ElementOrigin,
context_info: Object,
};

type ErrorContextProps = {
onError: (error: ElementsXhrError | Error, code: string, contextInfo?: Object, origin: ErrorOrigins) => void,
onError: (error: ElementsXhrError | Error, code: string, contextInfo?: Object, origin: ElementOrigin) => void,
};

type ElementsErrorCallback = (e: ElementsXhrError, code: string, contextInfo?: Object) => void;
Expand All @@ -715,6 +718,25 @@ type ClassificationInfo = {
Box__Security__Classification__Key?: string,
} & MetadataInstance;

type MetricType =
| typeof METRIC_TYPE_PREVIEW
| typeof METRIC_TYPE_ELEMENTS_LOAD_METRIC
| typeof METRIC_TYPE_ELEMENTS_PERFORMANCE_METRIC;

type ElementsLoadMetricData = {
startMarkName?: string,
endMarkName: string,
};

type LoggerProps = {
onPreviewMetric: (data: Object) => void,
onReadyMetric: (data: ElementsLoadMetricData) => void,
};

type WithLoggerProps = {
logger: LoggerProps,
};

type ActivityFeedFeatures = {
avatars?: boolean, // Show avatars
tasks?: {|
Expand All @@ -725,4 +747,4 @@ type ActivityFeedFeatures = {

type ContentSidebarFeatures = {
activityFeed?: ActivityFeedFeatures
} & FeatureConfig;
} & FeatureConfig;
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,21 +97,21 @@
"clearMocks": true,
"restoreMocks": true,
"moduleNameMapper": {
"react-intl": "<rootDir>/conf/react-intl-mocks.js",
"intl": "<rootDir>/conf/lib-intl-mock.js",
"react-intl": "<rootDir>/conf/jest/mocks/react-intl-mocks.js",
"intl": "<rootDir>/conf/jest/mocks/lib-intl-mock.js",
"react-intl-locale-data": "<rootDir>/node_modules/react-intl/locale-data/en.js",
"box-ui-elements-locale-data": "<rootDir>/i18n/en-US.js",
"box-react-ui-locale-data": "<rootDir>/node_modules/box-react-ui/i18n/en-US.js",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/conf/fileMock.js",
"\\.(css|less)$": "<rootDir>/conf/styleMock.js",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/conf/jest/fileMock.js",
"\\.(css|less)$": "<rootDir>/conf/jest/mocks/styleMock.js",
"react-virtualized/dist/es": "react-virtualized/dist/commonjs"
},
"transformIgnorePatterns": ["node_modules/(?!(box-react-ui|react-virtualized))", "react-virtualized"],
"collectCoverage": false,
"coverageDirectory": "<rootDir>/reports",
"collectCoverageFrom": ["src/**/*.js", "!**/node_modules/**", "!**/__tests__/**"],
"roots": ["src"],
"setupTestFrameworkScriptFile": "<rootDir>/conf/enzyme-adapter.js",
"setupTestFrameworkScriptFile": "<rootDir>/conf/jest/enzyme-adapter.js",
"snapshotSerializers": ["enzyme-to-json/serializer"]
},
"engines": {
Expand Down Expand Up @@ -223,6 +223,7 @@
"stylelint-config-standard": "^18.2.0",
"stylelint-order": "^1.0.0",
"tabbable": "^1.1.3",
"uuid": "^3.3.2",
"wait-on": "^3.2.0",
"webpack": "^4.19.0",
"webpack-bundle-analyzer": "^3.0.2",
Expand Down Expand Up @@ -255,6 +256,7 @@
"react-virtualized": "^9.20.1",
"regenerator-runtime": "^0.11.1",
"scroll-into-view-if-needed": "^1.5.0",
"tabbable": "^1.1.2"
"tabbable": "^1.1.2",
"uuid": "^3.3.2"
}
}
25 changes: 15 additions & 10 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,16 +228,21 @@ export const ERROR_CODE_UNEXPECTED_EXCEPTION = 'unexpected_exception_error';
export const ERROR_CODE_SEARCH = 'search_error';
export const ERROR_CODE_UNKNOWN = 'unknown_error';

/* ------------------ Error Origins ---------------------- */
export const ORIGIN_CONTENT_PREVIEW = 'content_preview';
export const ORIGIN_CONTENT_SIDEBAR = 'content_sidebar';
export const ORIGIN_ACTIVITY_SIDEBAR = 'activity_sidebar';
export const ORIGIN_DETAILS_SIDEBAR = 'details_sidebar';
export const ORIGIN_METADATA_SIDEBAR = 'metadata_sidebar';
export const ORIGIN_SKILLS_SIDEBAR = 'skills_sidebar';
export const ORIGIN_PREVIEW = 'preview';
export const ORIGIN_CONTENT_EXPLORER = 'content_explorer';
export const ORIGIN_OPEN_WITH = 'open_with';
/* ------------------ Origins ---------------------- */
export const ORIGIN_CONTENT_PREVIEW: 'content_preview' = 'content_preview';
export const ORIGIN_CONTENT_SIDEBAR: 'content_sidebar' = 'content_sidebar';
export const ORIGIN_ACTIVITY_SIDEBAR: 'activity_sidebar' = 'activity_sidebar';
export const ORIGIN_DETAILS_SIDEBAR: 'details_sidebar' = 'details_sidebar';
export const ORIGIN_METADATA_SIDEBAR: 'metadata_sidebar' = 'metadata_sidebar';
export const ORIGIN_SKILLS_SIDEBAR: 'skills_sidebar' = 'skills_sidebar';
export const ORIGIN_PREVIEW: 'preview' = 'preview';
export const ORIGIN_CONTENT_EXPLORER: 'content_explorer' = 'content_explorer';
export const ORIGIN_OPEN_WITH: 'open_with' = 'open_with';

/* ------------------ Metric Types ---------------------- */
export const METRIC_TYPE_PREVIEW: 'preview_metric' = 'preview_metric';
export const METRIC_TYPE_ELEMENTS_PERFORMANCE_METRIC: 'elements_performance_metric' = 'elements_performance_metric';
export const METRIC_TYPE_ELEMENTS_LOAD_METRIC: 'elements_load_metric' = 'elements_load_metric';

/* ------------------ Error Keys ---------------------- */
export const IS_ERROR_DISPLAYED = 'isErrorDisplayed'; // used to determine if user will see some error state or message
Expand Down
4 changes: 2 additions & 2 deletions src/elements/common/error-boundary/ErrorBoundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import DefaultError from './DefaultError';

type Props = {
errorComponent: React.Element,
errorOrigin: ErrorOrigins,
errorOrigin: ElementOrigin,
children: React.ChildrenArray<React.Element<any>>,
onError: (error: ElementsError) => void,
};
Expand Down Expand Up @@ -56,7 +56,7 @@ class ErrorBoundary extends React.Component<Props, State> {
error: ElementsXhrError | Error,
code: string,
contextInfo: Object = {},
origin: ErrorOrigins = this.props.errorOrigin,
origin: ElementOrigin = this.props.errorOrigin,
) => {
if (!error || !code || !origin) {
return;
Expand Down
2 changes: 1 addition & 1 deletion src/elements/common/error-boundary/withErrorBoundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import * as React from 'react';
import ErrorBoundary from './ErrorBoundary';

const withErrorBoundary = (errorOrigin: ErrorOrigins) => (WrappedComponent: React.ComponentType<any>) =>
const withErrorBoundary = (errorOrigin: ElementOrigin) => (WrappedComponent: React.ComponentType<any>) =>
// $FlowFixMe doesn't know about forwardRef (https://github.com/facebook/flow/issues/6103)
React.forwardRef((props: Object, ref: React.Ref<any>) => (
<ErrorBoundary {...props} errorOrigin={errorOrigin}>
Expand Down
163 changes: 163 additions & 0 deletions src/elements/common/logger/Logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// @flow
import * as React from 'react';
import noop from 'lodash/noop';
import uuidv4 from 'uuid/v4';
import { isMarkSupported } from '../../../utils/performance';
import { EVENT_JS_READY } from './constants';
import { METRIC_TYPE_PREVIEW, METRIC_TYPE_ELEMENTS_LOAD_METRIC } from '../../../constants';

type ElementsMetric = {
component: ElementOrigin,
name: string,
sessionId: string,
timestamp: string,
type: MetricType,
} & ElementsLoadMetricData;

type Props = {
fileId?: string,
onMetric: (data: Object) => void,
children: React.ChildrenArray<React.Element<any>>,
source: ElementOrigin,
startMarkName?: string,
};

const SESSION_ID = uuidv4();
const uniqueEvents: Set<string> = new Set();

class Logger extends React.Component<Props> {
static defaultProps = {
onMetric: noop,
};

constructor(props: Props) {
super(props);
this.loggerProps = {
onPreviewMetric: this.handlePreviewMetric,
onReadyMetric: this.handleReadyMetric,
};
}

loggerProps: LoggerProps;

get uniqueEvents(): Set<string> {
return uniqueEvents;
}

get sessionId(): string {
return SESSION_ID;
}

/**
* Creates an event name meant for use with an event which is unique and meant to be logged only once
*
* @param {string} name - The event name
* @returns {string} A string containing the component and event name
*/
createEventName(name: string): string {
const { source } = this.props;
return `${source}::${name}`;
}

/**
* Checks to see if the specified event for the component has already been fired.
*
* @param {string} component - the component name
* @param {string} name - the event name
* @returns {boolean} True if the event has already been fired
*/
hasLoggedEvent(name: string): boolean {
return this.uniqueEvents.has(name);
}

/**
* Invokes the provided metric logging callback.
*
* @param {string} type - the type of the event
* @param {string} name - the name of the event
* @param {Object} data - the event data
*/
logMetric(type: MetricType, name: string, data: Object): void {
const { onMetric, source } = this.props;
const metric: ElementsMetric = {
...data,
component: source,
name,
timestamp: this.getTimestamp(),
sessionId: this.sessionId,
type,
};

onMetric(metric);
}

/**
* Logs a unique metric event. Prevents duplicate events from being logged in the session.
*
* @param {string} type - the type of the event
* @param {string} name - the name of the event
* @param {Object} data - the event data
*/
logUniqueMetric(type: MetricType, name: string, data: Object): void {
const eventName = this.createEventName(name);
if (this.hasLoggedEvent(eventName)) {
return;
}

this.logMetric(type, name, data);
this.uniqueEvents.add(eventName);
}

/**
* Preview metric handler
*
* @param {Object} data - the metric data
* @returns {void}
*/
handlePreviewMetric = (data: Object) => {
const { onMetric } = this.props;
onMetric({
...data,
type: METRIC_TYPE_PREVIEW,
});
};

/**
* JS ready metric handler
*
* @param {Object} data - the metric data
* @returns {void}
*/
handleReadyMetric = (data: ElementsLoadMetricData) => {
if (!isMarkSupported) {
return;
}

const { startMarkName } = this.props;
const metricData = {
...data,
startMarkName,
};
this.logUniqueMetric(METRIC_TYPE_ELEMENTS_LOAD_METRIC, EVENT_JS_READY, metricData);
};

/**
* Create an ISO Timestamp for right now.
*
* @returns {string}
*/
getTimestamp(): string {
return new Date().toISOString();
}

render() {
const { children, onMetric, startMarkName, ...rest } = this.props;

return React.cloneElement(children, {
...rest,
logger: this.loggerProps,
});
}
}

export default Logger;
Loading

0 comments on commit 7aa67da

Please sign in to comment.