Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add frontend logging functions and log loading time for Dashboard and explore view #4226

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions superset/assets/javascripts/chart/Chart.jsx
Expand Up @@ -7,6 +7,7 @@ import { Tooltip } from 'react-bootstrap';
import { d3format } from '../modules/utils';
import ChartBody from './ChartBody';
import Loading from '../components/Loading';
import { Logger, LOG_ACTIONS_RENDER_EVENT } from '../logger';
import StackTraceMessage from '../components/StackTraceMessage';
import visMap from '../../visualizations/main';
import sandboxedEval from '../modules/sandbox';
Expand Down Expand Up @@ -171,13 +172,21 @@ class Chart extends React.PureComponent {
const viz = visMap[this.props.vizType];
const fd = this.props.formData;
const qr = this.props.queryResponse;
const renderStart = Logger.getTimestamp();
try {
// Executing user-defined data mutator function
if (fd.js_data) {
qr.data = sandboxedEval(fd.js_data)(qr.data);
}
// [re]rendering the visualization
viz(this, qr, this.props.setControlValue);
Logger.append(LOG_ACTIONS_RENDER_EVENT, {
label: this.props.chartKey,
vis_type: this.props.vizType,
start_offset: renderStart,
duration: Logger.getTimestamp() - renderStart,
});
this.props.actions.chartRenderingSucceeded(this.props.chartKey);
} catch (e) {
this.props.actions.chartRenderingFailed(e, this.props.chartKey);
}
Expand Down
29 changes: 28 additions & 1 deletion superset/assets/javascripts/chart/chartAction.js
@@ -1,5 +1,6 @@
import { getExploreUrl, getAnnotationJsonUrl } from '../explore/exploreUtils';
import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../modules/AnnotationTypes';
import { Logger, LOG_ACTIONS_LOAD_EVENT } from '../logger';

const $ = window.$ = require('jquery');

Expand Down Expand Up @@ -36,6 +37,11 @@ export function chartRenderingFailed(error, key) {
return { type: CHART_RENDERING_FAILED, error, key };
}

export const CHART_RENDERING_SUCCEEDED = 'CHART_RENDERING_SUCCEEDED';
export function chartRenderingSucceeded(key) {
return { type: CHART_RENDERING_SUCCEEDED, key };
}

export const REMOVE_CHART = 'REMOVE_CHART';
export function removeChart(key) {
return { type: REMOVE_CHART, key };
Expand Down Expand Up @@ -107,16 +113,37 @@ export const RUN_QUERY = 'RUN_QUERY';
export function runQuery(formData, force = false, timeout = 60, key) {
return (dispatch) => {
const url = getExploreUrl(formData, 'json', force);
let logStart;
const queryRequest = $.ajax({
url,
dataType: 'json',
timeout: timeout * 1000,
beforeSend: () => {
logStart = Logger.getTimestamp();
},
});

const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, key)))
.then(() => queryRequest)
.then(queryResponse => dispatch(chartUpdateSucceeded(queryResponse, key)))
.then((queryResponse) => {
Logger.append(LOG_ACTIONS_LOAD_EVENT, {
label: key,
is_cached: queryResponse.is_cached,
row_count: queryResponse.rowcount,
datasource: formData.datasource,
start_offset: logStart,
duration: Logger.getTimestamp() - logStart,
});
return dispatch(chartUpdateSucceeded(queryResponse, key));
})
.catch((err) => {
Logger.append(LOG_ACTIONS_LOAD_EVENT, {
label: key,
has_err: true,
datasource: formData.datasource,
start_offset: logStart,
duration: Logger.getTimestamp() - logStart,
});
if (err.statusText === 'timeout') {
dispatch(chartUpdateTimeout(err.statusText, timeout, key));
} else if (err.statusText !== 'abort') {
Expand Down
5 changes: 5 additions & 0 deletions superset/assets/javascripts/chart/chartReducer.js
Expand Up @@ -55,6 +55,11 @@ export default function chartReducer(charts = {}, action) {
chartAlert: t('Updating chart was stopped'),
};
},
[actions.CHART_RENDERING_SUCCEEDED](state) {
return { ...state,
chartStatus: 'rendered',
};
},
[actions.CHART_RENDERING_FAILED](state) {
return { ...state,
chartStatus: 'failed',
Expand Down
22 changes: 21 additions & 1 deletion superset/assets/javascripts/dashboard/components/Dashboard.jsx
Expand Up @@ -5,6 +5,8 @@ import AlertsWrapper from '../../components/AlertsWrapper';
import GridLayout from './GridLayout';
import Header from './Header';
import { areObjectsEqual } from '../../reduxUtils';
import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD,
LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger';
import { t } from '../../locales';

import '../../../stylesheets/dashboard.css';
Expand All @@ -21,6 +23,7 @@ const propTypes = {
userId: PropTypes.string,
isStarred: PropTypes.bool,
editMode: PropTypes.bool,
impressionId: PropTypes.string,
};

const defaultProps = {
Expand All @@ -41,6 +44,14 @@ class Dashboard extends React.PureComponent {
super(props);
this.refreshTimer = null;
this.firstLoad = true;
this.loadingLog = new ActionLog({
impressionId: props.impressionId,
actionType: LOG_ACTIONS_PAGE_LOAD,
source: 'dashboard',
sourceId: props.dashboard.id,
eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT],
});
Logger.start(this.loadingLog);

// alert for unsaved changes
this.state = { unsavedChanges: false };
Expand Down Expand Up @@ -68,10 +79,19 @@ class Dashboard extends React.PureComponent {
}

componentDidMount() {
this.firstLoad = false;
window.addEventListener('resize', this.rerenderCharts);
}

componentWillReceiveProps(nextProps) {
if (this.firstLoad &&
Object.values(nextProps.slices)
.every(slice => (['rendered', 'failed', 'stopped'].indexOf(slice.chartStatus) > -1))
) {
Logger.end(this.loadingLog);
this.firstLoad = false;
}
}

componentDidUpdate(prevProps) {
if (!areObjectsEqual(prevProps.filters, this.props.filters) && this.props.refresh) {
const currentFilterKeys = Object.keys(this.props.filters);
Expand Down
Expand Up @@ -5,7 +5,7 @@ import * as dashboardActions from '../actions';
import * as chartActions from '../../chart/chartAction';
import Dashboard from './Dashboard';

function mapStateToProps({ charts, dashboard }) {
function mapStateToProps({ charts, dashboard, impressionId }) {
return {
initMessages: dashboard.common.flash_messages,
timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
Expand All @@ -17,6 +17,7 @@ function mapStateToProps({ charts, dashboard }) {
userId: dashboard.userId,
isStarred: !!dashboard.isStarred,
editMode: dashboard.editMode,
impressionId,
};
}

Expand Down
2 changes: 2 additions & 0 deletions superset/assets/javascripts/dashboard/reducers.js
@@ -1,5 +1,6 @@
import { combineReducers } from 'redux';
import d3 from 'd3';
import shortid from 'shortid';

import charts, { chart } from '../chart/chartReducer';
import * as actions from './actions';
Expand Down Expand Up @@ -200,4 +201,5 @@ export const dashboard = function (state = {}, action) {
export default combineReducers({
charts,
dashboard,
impressionId: () => (shortid.generate()),
});
Expand Up @@ -15,23 +15,36 @@ import { chartPropType } from '../../chart/chartReducer';
import * as exploreActions from '../actions/exploreActions';
import * as saveModalActions from '../actions/saveModalActions';
import * as chartActions from '../../chart/chartAction';
import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD,
LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger';

const propTypes = {
actions: PropTypes.object.isRequired,
datasource_type: PropTypes.string.isRequired,
isDatasourceMetaLoading: PropTypes.bool.isRequired,
chartStatus: PropTypes.string,
chart: PropTypes.shape(chartPropType).isRequired,
slice: PropTypes.object,
controls: PropTypes.object.isRequired,
forcedHeight: PropTypes.string,
form_data: PropTypes.object.isRequired,
standalone: PropTypes.bool.isRequired,
timeout: PropTypes.number,
impressionId: PropTypes.string,
};

class ExploreViewContainer extends React.Component {
constructor(props) {
super(props);
this.firstLoad = true;
this.loadingLog = new ActionLog({
impressionId: props.impressionId,
actionType: LOG_ACTIONS_PAGE_LOAD,
source: 'slice',
sourceId: props.slice ? props.slice.slice_id : 0,
eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT],
});
Logger.start(this.loadingLog);

this.state = {
height: this.getHeight(),
width: this.getWidth(),
Expand All @@ -44,6 +57,11 @@ class ExploreViewContainer extends React.Component {
}

componentWillReceiveProps(np) {
if (this.firstLoad &&
['rendered', 'failed', 'stopped'].indexOf(np.chart.chartStatus) > -1) {
Logger.end(this.loadingLog);
this.firstLoad = false;
}
if (np.controls.viz_type.value !== this.props.controls.viz_type.value) {
this.props.actions.resetControls();
this.props.actions.triggerQuery(true, this.props.chart.chartKey);
Expand Down Expand Up @@ -197,7 +215,7 @@ class ExploreViewContainer extends React.Component {

ExploreViewContainer.propTypes = propTypes;

function mapStateToProps({ explore, charts }) {
function mapStateToProps({ explore, charts, impressionId }) {
const form_data = getFormDataFromControls(explore.controls);
const chartKey = Object.keys(charts)[0];
const chart = charts[chartKey];
Expand All @@ -220,6 +238,7 @@ function mapStateToProps({ explore, charts }) {
forcedHeight: explore.forced_height,
chart,
timeout: explore.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
impressionId,
};
}

Expand Down
2 changes: 2 additions & 0 deletions superset/assets/javascripts/explore/index.jsx
Expand Up @@ -5,6 +5,7 @@ import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';

import shortid from 'shortid';
import { now } from '../modules/dates';
import { initEnhancer } from '../reduxUtils';
import { getChartKey } from './exploreUtils';
Expand Down Expand Up @@ -64,6 +65,7 @@ const initState = {
saveModalAlert: null,
},
explore: bootstrappedState,
impressionId: shortid.generate(),
};
const store = createStore(rootReducer, initState,
compose(applyMiddleware(thunk), initEnhancer(false)),
Expand Down
2 changes: 2 additions & 0 deletions superset/assets/javascripts/explore/reducers/index.js
@@ -1,4 +1,5 @@
import { combineReducers } from 'redux';
import shortid from 'shortid';

import charts from '../../chart/chartReducer';
import saveModal from './saveModalReducer';
Expand All @@ -8,4 +9,5 @@ export default combineReducers({
charts,
saveModal,
explore,
impressionId: () => (shortid.generate()),
});
114 changes: 114 additions & 0 deletions superset/assets/javascripts/logger.js
@@ -0,0 +1,114 @@
import $ from 'jquery';

export const LOG_ACTIONS_PAGE_LOAD = 'page_load_perf';
export const LOG_ACTIONS_LOAD_EVENT = 'load_events';
export const LOG_ACTIONS_RENDER_EVENT = 'render_events';

const handlers = {};

export const Logger = {
start(log) {
log.setAttribute('startAt', new Date().getTime() - this.getTimestamp());
log.eventNames.forEach((eventName) => {
if (!handlers[eventName]) {
handlers[eventName] = [];
}
handlers[eventName].push(log.addEvent.bind(log));
});
},

append(eventName, eventBody) {
return handlers[eventName].length &&
handlers[eventName].forEach(handler => (handler(eventName, eventBody)));
},

end(log) {
log.setAttribute('duration', new Date().getTime() - log.startAt);
this.send(log);

log.eventNames.forEach((eventName) => {
if (handlers[eventName].length) {
const index = handlers[eventName]
.findIndex(handler => (handler === log.addEvent));
handlers[eventName].splice(index, 1);
}
});
},

send(log) {
const { impressionId, actionType, source, sourceId, events, startAt, duration } = log;
const requestPrams = [];
requestPrams.push(['impression_id', impressionId]);
switch (source) {
case 'dashboard':
requestPrams.push(['dashboard_id', sourceId]);
break;
case 'slice':
requestPrams.push(['slice_id', sourceId]);
break;
default:
break;
}
let url = '/superset/log/';
if (requestPrams.length) {
url += '?' + requestPrams.map(([k, v]) => (k + '=' + v)).join('&');
}
const eventData = {};
for (const eventName in events) {
eventData[eventName] = [];
events[eventName].forEach((event) => {
eventData[eventName].push(event);
});
}

$.ajax({
url,
method: 'POST',
dataType: 'json',
data: {
source: 'client',
type: actionType,
started_time: startAt,
duration,
events: JSON.stringify(eventData),
},
});
},

getTimestamp() {
return Math.round(window.performance.now());
},
};

export class ActionLog {
constructor({ impressionId, actionType, source, sourceId, eventNames, sendNow }) {
this.impressionId = impressionId;
this.source = source;
this.sourceId = sourceId;
this.actionType = actionType;
this.eventNames = eventNames;
this.sendNow = sendNow || false;
this.startAt = 0;
this.duration = 0;
this.events = {};

this.addEvent = this.addEvent.bind(this);
}

setAttribute(name, value) {
this[name] = value;
}

addEvent(eventName, eventBody) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(eventBody);

if (this.sendNow) {
this.setAttribute('duration', new Date().getTime() - this.startAt);
Logger.send(this);
this.events = {};
}
}
}
1 change: 1 addition & 0 deletions superset/assets/spec/helpers/browser.js
Expand Up @@ -38,4 +38,5 @@ global.sinon.useFakeXMLHttpRequest();

global.window.XMLHttpRequest = global.XMLHttpRequest;
global.window.location = { href: 'about:blank' };
global.window.performance = { now: () => (new Date().getTime()) };
global.$ = require('jquery')(global.window);