blob: 5a8ddf636df1b3c4c0be9d453973f4a52528184f [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function() {
'use strict';
// Latency reporting constants.
const TIMING = {
TYPE: 'timing-report',
CATEGORY: 'UI Latency',
// Reported events - alphabetize below.
APP_STARTED: 'App Started',
PAGE_LOADED: 'Page Loaded',
};
// Navigation reporting constants.
const NAVIGATION = {
TYPE: 'nav-report',
CATEGORY: 'Location Changed',
PAGE: 'Page',
};
const ERROR = {
TYPE: 'error',
CATEGORY: 'exception',
};
const INTERACTION_TYPE = 'interaction';
const CHANGE_VIEW_REGEX = /^\/c\/\d+\/?\d*$/;
const DIFF_VIEW_REGEX = /^\/c\/\d+\/\d+\/.+$/;
const pending = [];
const onError = function(oldOnError, msg, url, line, column, error) {
if (oldOnError) {
oldOnError(msg, url, line, column, error);
}
if (error) {
line = line || error.lineNumber;
column = column || error.columnNumber;
msg = msg || error.toString();
}
const payload = {
url,
line,
column,
error,
};
GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
return true;
};
const catchErrors = function(opt_context) {
const context = opt_context || window;
context.onerror = onError.bind(null, context.onerror);
context.addEventListener('unhandledrejection', e => {
const msg = e.reason.message;
const payload = {
error: e.reason,
};
GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
});
};
catchErrors();
const GrReporting = Polymer({
is: 'gr-reporting',
properties: {
category: String,
_baselines: {
type: Array,
value() { return {}; },
},
},
behaviors: [
Gerrit.BaseUrlBehavior,
],
get performanceTiming() {
return window.performance.timing;
},
now() {
return Math.round(10 * window.performance.now()) / 10;
},
reporter(...args) {
const report = (Gerrit._arePluginsLoaded() && !pending.length) ?
this.defaultReporter : this.cachingReporter;
report.apply(this, args);
},
defaultReporter(type, category, eventName, eventValue) {
const detail = {
type,
category,
name: eventName,
value: eventValue,
};
document.dispatchEvent(new CustomEvent(type, {detail}));
if (type === ERROR.TYPE) {
console.error(eventValue.error || eventName);
} else {
console.log(eventName + (eventValue !== undefined ?
(': ' + eventValue) : ''));
}
},
cachingReporter(type, category, eventName, eventValue) {
if (type === ERROR.TYPE) {
console.error(eventValue.error || eventName);
}
if (Gerrit._arePluginsLoaded()) {
if (pending.length) {
for (const args of pending.splice(0)) {
this.reporter(...args);
}
}
this.reporter(type, category, eventName, eventValue);
} else {
pending.push([type, category, eventName, eventValue]);
}
},
/**
* User-perceived app start time, should be reported when the app is ready.
*/
appStarted() {
const startTime =
new Date().getTime() - this.performanceTiming.navigationStart;
this.reporter(
TIMING.TYPE, TIMING.CATEGORY, TIMING.APP_STARTED, startTime);
},
/**
* Page load time, should be reported at any time after navigation.
*/
pageLoaded() {
if (this.performanceTiming.loadEventEnd === 0) {
console.error('pageLoaded should be called after window.onload');
this.async(this.pageLoaded, 100);
} else {
const loadTime = this.performanceTiming.loadEventEnd -
this.performanceTiming.navigationStart;
this.reporter(
TIMING.TYPE, TIMING.CATEGORY, TIMING.PAGE_LOADED, loadTime);
}
},
locationChanged() {
let page = '';
const pathname = this._getPathname();
if (pathname.startsWith('/q/')) {
page = this.getBaseUrl() + '/q/';
} else if (pathname.match(CHANGE_VIEW_REGEX)) { // change view
page = this.getBaseUrl() + '/c/';
} else if (pathname.match(DIFF_VIEW_REGEX)) { // diff view
page = this.getBaseUrl() + '/c//COMMIT_MSG';
} else {
// Ignore other page changes.
return;
}
this.reporter(
NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
},
pluginsLoaded() {
this.timeEnd('PluginsLoaded');
},
_getPathname() {
return '/' + window.location.pathname.substring(this.getBaseUrl().length);
},
/**
* Reset named timer.
*/
time(name) {
this._baselines[name] = this.now();
},
/**
* Finish named timer and report it to server.
*/
timeEnd(name) {
const baseTime = this._baselines[name] || 0;
const time = Math.round(this.now() - baseTime) + 'ms';
this.reporter(TIMING.TYPE, TIMING.CATEGORY, name, time);
delete this._baselines[name];
},
reportInteraction(eventName, opt_msg) {
this.reporter(INTERACTION_TYPE, this.category, eventName, opt_msg);
},
});
window.GrReporting = GrReporting;
// Expose onerror installation so it would be accessible from tests.
window.GrReporting._catchErrors = catchErrors;
})();