/**
* Copyright 2009 Google Inc.
*
* 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.
*/
import("fastJSON");
import("netutils");
import("funhtml.*");
import("stringutils.{html,sprintf,startsWith,md5}");
import("jsutils.*");
import("sqlbase.sqlbase");
import("sqlbase.sqlcommon");
import("sqlbase.sqlobj");
import("dispatch.{Dispatcher,PrefixMatcher,DirMatcher,forward}");
import("etherpad.globals.*");
import("etherpad.utils.*");
import("etherpad.sessions.getSession");
import("etherpad.sessions");
import("etherpad.statistics.statistics");
import("etherpad.log");
import("etherpad.usage_stats.usage_stats");
import("etherpad.helpers");
//----------------------------------------------------------------
// Usagestats
//----------------------------------------------------------------
var _defaultPrefs = {
topNCount: 5,
granularity: 1440
}
function onRequest() {
keys(_defaultPrefs).forEach(function(prefName) {
if (request.params[prefName]) {
_prefs()[prefName] = request.params[prefName];
}
});
if (request.isPost) {
response.redirect(
request.path+
(request.query ? "?"+request.query : "")+
(request.params.fragment ? "#"+request.params.fragment : ""));
}
}
function _prefs() {
if (! sessions.getSession().statsPrefs) {
sessions.getSession().statsPrefs = {}
}
return sessions.getSession().statsPrefs;
}
function _pref(pname) {
return _prefs()[pname] || _defaultPrefs[pname];
}
function _topN() {
return _pref('topNCount');
}
function _showLiveStats() {
return _timescale() < 1440;
// return _pref('granularity') == 'live';
}
function _showHistStats() {
return _timescale() >= 1440
// return _pref('showLiveOrHistorical') == 'hist';
}
function _timescale() {
return Number(_pref('granularity')) || 1;
}
// types:
// compare - compare one or more single-value stats
// top - show top values over time
// histogram - show histogram over time
var statDisplays = {
users: [
{ name: "visitors",
description: "User visits, total over a %t period",
type: "compare",
stats: [ {stat: "site_pageviews",
description: "Page views",
color: "FFA928" },
{stat: "site_unique_ips",
description: "Unique IPs",
color: "00FF00" } ] },
// free pad usage
{ name: "free pad usage, 1 day",
description: "Free pad.spline.inf.fu-berlin.de users, total over a %t period",
type: "compare",
stats: [ {stat: "active_user_ids",
description: "All users",
color: "FFA928" },
{stat: "users_1day_returning_7days",
description: "Users returning after 7 days",
color: "00FF00"},
{stat: "users_1day_returning_30days",
description: "Users returning after 30 days",
color: "FF0000"} ] },
{ name: "free pad usage, 7 day",
description: "Free pad.spline.inf.fu-berlin.de users over the last 7 days",
type: "compare",
options: { hideLive: true, latestUseHistorical: true},
stats: [ {stat: "active_user_ids_7days",
description: "All users",
color: "FFA928" },
{stat: "users_7day_returning_7days",
description: "Users returning after 7 days",
color: "00FF00"},
{stat: "users_7day_returning_30days",
description: "Users returning after 30 days",
color: "FF0000"} ] },
{ name: "free pad usage, 30 day",
description: "Free pad.spline.inf.fu-berlin.de users over the last 30 days",
type: "compare",
options: { hideLive: true, latestUseHistorical: true},
stats: [ {stat: "active_user_ids_30days",
description: "All users",
color: "FFA928" },
{stat: "users_30day_returning_7days",
description: "Users returning after 7 days",
color: "00FF00"},
{stat: "users_30day_returning_30days",
description: "Users returning after 30 days",
color: "FF0000"} ] },
// pro pad usage
{ name: "active pro accounts, 1 day",
description: "Active pro accounts, total over a %t period",
type: "compare",
stats: [ {stat: "active_pro_accounts",
description: "All accounts",
color: "FFA928" },
{stat: "pro_accounts_1day_returning_7days",
description: "Accounts older than 7 days",
color: "00FF00"},
{stat: "pro_accounts_1day_returning_30days",
description: "Accounts older than 30 days",
color: "FF0000"} ] },
{ name: "active pro accounts, 7 day",
description: "Active pro accounts over the last 7 days",
type: "compare",
options: { hideLive: true, latestUseHistorical: true},
stats: [ {stat: "active_pro_accounts_7days",
description: "All accounts",
color: "FFA928" },
{stat: "pro_accounts_7day_returning_7days",
description: "Accounts older than 7 days",
color: "00FF00"},
{stat: "pro_accounts_7day_returning_30days",
description: "Accounts older than 30 days",
color: "FF0000"} ] },
{ name: "active pro accounts, 30 day",
description: "Active pro accounts over the last 30 days",
type: "compare",
options: { hideLive: true, latestUseHistorical: true},
stats: [ {stat: "active_pro_accounts_30days",
description: "All accounts",
color: "FFA928" },
{stat: "pro_accounts_30day_returning_7days",
description: "Accounts older than 7 days",
color: "00FF00"},
{stat: "pro_accounts_30day_returning_30days",
description: "Accounts older than 30 days",
color: "FF0000"} ] },
// other stats
{ name: "pad connections",
description: "Number of active comet connections, mean over a %t period",
type: "top",
options: {showOthers: false},
stats: ["streaming_connections"] },
{ name: "referers",
description: "Referers, number of hits over a %t period",
type: "top",
options: {showOthers: false},
stats: ["top_referers"] },
],
product: [
{ name: "pads",
description: "Newly-created and active pads, total over a %t period",
type: "compare",
stats: [ {stat: "active_pads",
description: "Active pads",
color: "FFA928" },
{stat: "new_pads",
description: "New pads",
color: "FF0000" }] },
{ name: "chats",
description: "Chat messages and active chatters, total over a %t period",
type: "compare",
stats: [ {stat: "chat_messages",
description: "Messages",
color: "FFA928" },
{stat: "active_chatters",
description: "Chatters",
color: "FF0000" }] },
{ name: "import/export",
description: "Imports and Exports, total over a %t period",
type: "compare",
stats: [ {stat: {f: '+', args: ["imports_exports_counts:export", "imports_exports_counts:import"]},
description: "Total",
color: "FFA928" },
{stat: "imports_exports_counts:export",
description: "Exports",
color: "FF0000"},
{stat: "imports_exports_counts:import",
description: "Imports",
color: "00FF00"}] },
{ name: "revenue",
description: "Revenue, total over a %t period",
type: "compare",
stats: [ {stat: "revenue",
description: "Revenue",
color: "FFA928"}] }
],
performance: [
{ name: "dynamic page latencies",
description: "Slowest dynamic pages: mean load time in milliseconds over a %t period",
type: "top",
options: {showOthers: false},
stats: ["execution_latencies"] },
{ name: "pad startup latencies",
description: "Pad startup times: percent load time in milliseconds over a %t period",
type: "histogram",
stats: ["pad_startup_times"] },
{ name: "stream post latencies",
description: "Comet post latencies, percentiles in milliseconds over a %t period",
type: "histogram",
stats: ["streaming_latencies"] },
],
health: [
{ name: "disconnect causes",
description: "Causes of disconnects, total over a %t period",
type: "top",
stats: ["disconnect_causes"] },
{ name: "paths with 404s",
description: "'Not found' responses, by path, number served over a %t period",
type: "top",
stats: ["paths_404"] },
{ name: "exceptions",
description: "Total number of server exceptions over a %t period",
type: "compare",
stats: [ {stat: "exceptions",
description: "Exceptions",
color: "FF1928" } ] },
{ name: "paths with 500s",
type: "top",
description: "'500' responses, by path, number served over a %t period",
type: "top",
stats: ["paths_500"] },
{ name: "paths with exceptions",
description: "responses with exceptions, by path, number served over a %t period",
type: "top",
stats: ["paths_exception"] },
{ name: "disconnects with client-side errors",
description: "user disconnects with an error on the client side, number over a %t period",
type: "compare",
stats: [ { stat: "disconnects_with_clientside_errors",
description: "Disconnects with errors",
color: "FFA928" } ] },
{ name: "unnecessary disconnects",
description: "disconnects that were avoidable, number over a %t period",
type: "compare",
stats: [ { stat: "streaming_disconnects:disconnected_userids",
description: "Number of unique users disconnected",
color: "FFA928" },
{ stat: "streaming_disconnects:total_disconnects",
description: "Total number of disconnects",
color: "FF0000" } ] },
]
}
function getUsedStats(statStructure) {
var stats = {};
function getStructureValues(statStructure) {
if (typeof(statStructure) == 'string') {
stats[statStructure] = true;
} else {
statStructure.args.forEach(getStructureValues);
}
}
getStructureValues(statStructure);
return keys(stats);
}
function getStatData(statStructure, values_f) {
function getStructureValues(statStructure) {
if (typeof(statStructure) == 'string') {
return values_f(statStructure);
} else if (typeof(statStructure) == 'number') {
return statStructure;
} else {
var args = statStructure.args.map(getStructureValues);
return {
f: statStructure.f,
args: args
}
}
}
var mappedStructure = getStructureValues(statStructure);
function evalStructure(statStructure) {
if ((typeof(statStructure) == 'number') || (statStructure instanceof Array)) {
return statStructure;
} else {
var merge_f = statStructure.f;
if (typeof(merge_f) == 'string') {
switch (merge_f) {
case '+':
merge_f = function() {
var sum = 0;
for (var i = 0; i < arguments.length; ++i) {
sum += arguments[i];
}
return sum;
}
break;
case '*':
merge_f = function() {
var product = 0;
for (var i = 0; i < arguments.length; ++i) {
product *= arguments[i];
}
return product;
}
break;
case '/':
merge_f = function(a, b) { return a / b; }
break;
case '-':
merge_f = function(a, b) { return a - b; }
break;
}
}
var evaluatedArguments = statStructure.args.map(evalStructure);
var length = -1;
evaluatedArguments.forEach(function(arg) {
if (typeof(arg) == 'object' && (arg instanceof Array)) {
length = arg.length;
}
});
evaluatedArguments = evaluatedArguments.map(function(arg) {
if (typeof(arg) == 'number') {
var newArg = new Array(length);
for (var i = 0; i < newArg.length; ++i) {
newArg[i] = arg;
}
return newArg
} else {
return arg;
}
});
return mergeArrays.apply(this, [merge_f].concat(evaluatedArguments));
}
}
return evalStructure(mappedStructure);
}
var googleChartSimpleEncoding = "ABCDEFGHIJLKMNOPQRSTUVQXYZabcdefghijklmnopqrstuvwxyz0123456789-.";
function _enc(value) {
return googleChartSimpleEncoding[Math.floor(value/64)] + googleChartSimpleEncoding[value%64];
}
function drawSparkline(dataSets, labels, colors, minutes) {
var max = 1;
var maxLength = 0;
dataSets.forEach(function(dataSet, i) {
if (dataSet.length > maxLength) {
maxLength = dataSet.length;
}
dataSet.forEach(function(point) {
if (point > max) {
max = point;
}
});
});
var data = dataSets.map(function(dataSet) {
var chars = dataSet.map(function(x) {
if (x !== undefined) {
return _enc(Math.round(x/max*4095));
} else {
return "__";
}
}).join("");
while (chars.length < maxLength*2) {
chars = "__"+chars;
}
return chars;
}).join(",");
var timeLabels;
if (minutes < 60*24) {
timeLabels = [4,3,2,1,0].map(function(t) {
var minutesPerTick = minutes/4;
var d = new Date(Date.now() - minutesPerTick*60000*t);
return (d.getHours()%12 || 12)+":"+(d.getMinutes() < 10 ? "0" : "")+d.getMinutes()+(d.getHours() < 12 ? "am":"pm");
}).join("|");
} else {
timeLabels = [4,3,2,1,0].map(function(t) {
var daysPerTick = (minutes/(60*24))/4;
var d = new Date(Date.now() - t*daysPerTick*24*60*60*1000);
return (d.getMonth()+1)+"/"+d.getDate();
}).join("|");
}
var pointLabels = dataSets.map(function(dataSet, i) {
return ["t"+dataSet[dataSet.length-1],colors[i],i,maxLength-1,12,0].join(",");
}).join("|");
labels = labels.map(function(label) {
return encodeURIComponent((label.length > 73) ? label.slice(0, 70) + "..." : label);
});
var step = Math.round(max/10);
step = Math.round(step/Math.pow(10, String(step).length-1))*Math.pow(10, String(step).length-1);
var srcUrl =
"http://chart.apis.google.com/chart?chs=600x300&cht=lc&chd=e:"+data+
"&chxt=y,x&chco="+colors.join(",")+"&chxr=0,0,"+max+","+step+"&chxl=1:|"+timeLabels+
"&chdl="+labels.join("|")+"&chdlp=b&chm="+pointLabels;
return toHTML(IMG({src: srcUrl}));
}
var liveDataNumSamples = 20;
function extractStatValuesFunction(nameToValues_f) {
return function(statName) {
var value;
if (statName.indexOf(":") >= 0) {
[statName, value] = statName.split(":");
}
var h = nameToValues_f(statName);
if (value) {
h = h.map(function(topValues) {
if (! topValues) { return; }
var tv = topValues.topValues;
for (var i = 0; i < tv.length; ++i) {
if (tv[i].value == value) {
return tv[i].count;
}
}
return 0;
});
}
return h;
}
}
function sparkline_compare(history_f, minutesPerSample, stat) {
var histories = stat.stats.map(function(stat) {
var samples = getStatData(stat.stat, extractStatValuesFunction(history_f));
return [samples, stat.description, stat.color];
});
return drawSparkline(histories.map(function(history) { return history[0] }),
histories.map(function(history) { return history[1] }),
histories.map(function(history) { return history[2] }),
minutesPerSample*histories[0][0].length);
}
function sparkline_top(history_f, minutesPerSample, stat) {
var showOthers = ! stat.options || stat.options.showOthers != false;
var history = stat.stats.map(history_f)[0];
if (history.length == 0) {
return "no data";
}
var topRecents = {};
var topRecents_arr = [];
history.forEach(function(tv) {
if (! tv) { return; }
if (tv.topValues.length > 0) {
topRecents_arr = tv.topValues.map(function(x) { return x.value; });
}
});
if (topRecents_arr.length == 0) {
return "no data";
}
topRecents_arr = topRecents_arr.slice(0, _topN());
topRecents_arr.forEach(function(value, i) {
topRecents[value] = i;
});
if (showOthers) {
topRecents_arr.push("Other");
}
var max = 1;
var values = topRecents_arr.map(function() { return history.map(function() { return 0 }); });
history.forEach(function(tv, i) {
if (! tv) { return; }
tv.topValues.forEach(function(entry) {
if (entry.count > max) {
max = entry.count;
}
if (entry.value in topRecents) {
values[topRecents[entry.value]][i] = entry.count;
} else if (showOthers) {
values[values.length-1][i] += entry.count;
}
});
});
return drawSparkline(
values,
topRecents_arr,
["FF0000", "00FF00", "0000FF", "FF00FF", "00FFFF"].slice(0, topRecents_arr.length-1).concat("FFA928"),
minutesPerSample*history.length);
}
function sparkline_histogram(history_f, minutesPerSample, stat) {
var history = stat.stats.map(history_f)[0];
if (history.length == 0) {
return "no data";
}
var percentiles = [50, 90, 95, 99];
var data = percentiles.map(function() { return []; })
history.forEach(function(hist) {
percentiles.forEach(function(pct, i) {
data[i].push((hist ? hist[""+pct] : undefined));
});
});
return drawSparkline(
data,
percentiles.map(function(pct) { return ""+pct+"%"; }),
["FF0000","FF00FF","FFA928","00FF00"].reverse(),
minutesPerSample*history.length);
}
function liveHistoryFunction(minutesPerSample) {
return function(statName) {
return statistics.liveSnapshot(statName).history(minutesPerSample, liveDataNumSamples);
}
}
function _listStats(statName, count) {
var options = { orderBy: '-timestamp,id' };
if (count !== undefined) {
options.limit = count;
}
return sqlobj.selectMulti('statistics', {name: statName}, options);
}
function ancientHistoryFunction(time) {
return function(statName) {
var seenDates = {};
var samples = _listStats(statName);
samples = samples.reverse().map(function(json) {
if (seenDates[""+json.timestamp]) { return; }
seenDates[""+json.timestamp] = true;
return {timestamp: json.timestamp, json: json.value};
}).filter(function(x) { return x !== undefined });
samples = samples.reverse().slice(0, Math.round(time/(24*60)));
var samplesWithEmptyValues = [];
for (var i = 0; i < samples.length-1; ++i) {
var current = samples[i];
var next = samples[i+1];
samplesWithEmptyValues.push(current.json);
for (var j = current.timestamp+86400*1000; j < next.timestamp; j += 86400*1000) {
samplesWithEmptyValues.push(undefined);
}
}
if (samples.length > 0) {
samplesWithEmptyValues.push(samples[samples.length-1].json);
}
samplesWithEmptyValues = samplesWithEmptyValues.map(function(json) {
if (! json) { return; }
var obj = fastJSON.parse(json);
if (keys(obj).length == 1 && 'value' in obj) {
obj = obj.value;
}
return obj;
});
return samplesWithEmptyValues.reverse();
}
}
function sparkline(history_f, minutesPerSample, stat) {
if (this["sparkline_"+stat.type]) {
return this["sparkline_"+stat.type](history_f, minutesPerSample, stat);
} else {
return "No sparkline handler!";
}
}
function liveLatestFunction(minutesPerSample) {
return function(statName) {
return [statistics.liveSnapshot(statName).latest(minutesPerSample)];
}
}
function liveTotal(statName) {
return [statistics.liveSnapshot(statName).total];
}
function historyLatest(statName) {
return _listStats(statName, 1).map(function(x) {
var value = fastJSON.parse(x.value);
if (keys(value).length == 1 && 'value' in value) {
value = value.value;
}
return value;
});
}
function latest_compare(latest_f, stat) {
return stat.stats.map(function(stat) {
var sample = getStatData(stat.stat, extractStatValuesFunction(latest_f))[0];
return { value: sample, description: stat.description };
});
}
function latest_top(latest_f, stat) {
var showOthers = ! stat.options || stat.options.showOthers != false;
var sample = stat.stats.map(latest_f)[0][0];
if (! sample) {
return [];
}
var total = sample.count;
var values = sample.topValues.slice(0, _topN()).map(function(v) {
total -= v.count;
return { value: v.count, description: v.value };
});
if (showOthers) {
values.push({value: total, description: "Other"});
}
return values;
}
function latest_histogram(latest_f, stat) {
var sample = stat.stats.map(latest_f)[0][0];
if (! sample) {
return "no data";
}
var percentiles = [0, 1, 5, 10, 25, 50, 75, 90, 95, 99, 100].filter(function(pct) { return ((""+pct) in sample) });
var xpos = percentiles.map(function(x, i) { return sample[x] });
var xMax = 0;
var xMin = 1e12;
xpos.forEach(function(x) { xMax = (x > xMax ? x : xMax); xMin = (x < xMin ? x : xMin); });
xposNormalized = xpos.map(function(x) { return Math.round((x-xMin)/(xMax-xMin || 1)*100); });
var ypos = percentiles.slice(1).map(function(y, i) { return (y-percentiles[i])/(xpos[i+1] || 1); });
var yMax = 0;
ypos.forEach(function(y) { yMax = (y > yMax ? y : yMax); });
yposNormalized = ypos.map(function(y) { return Math.round(y/yMax*100); });
// var proposedLabels = mergeArrays(function(x, y) { return {pos: x, label: y}; }, xposNormalized, xpos);
// var keepLabels = [{pos: 0, label: 0}];
// proposedLabels.forEach(function(label) {
// if (label.pos - keepLabels[keepLabels.length-1].pos > 10) {
// keepLabels.push(label);
// }
// });
//
// var labelPos = keepLabels.map(function(x) { return x.pos });
// var labels = keepLabels.map(function(x) { return x.label });
return toHTML(IMG({src:
"http://chart.apis.google.com/chart?chs=340x100&cht=lxy&chd=t:"+xposNormalized.join(",")+"|0,"+yposNormalized.join(",")+
"&chxt=x&chxr=0,"+xMin+","+xMax+","+Math.floor((xMax-xMin)/5) // "l=0:|"+labels.join("|")+"&chxp=0,"+labelPos.join(",")
}));
}
function latest(latest_f, stat) {
if (this["latest_"+stat.type]) {
return this["latest_"+stat.type](latest_f, stat);
} else {
return "No latest handler!";
}
}
function dropdown(name, options, selected) {
var select;
if (typeof(name) == 'string') {
select = SELECT({name: name});
} else {
select = SELECT(name);
}
function addOption(value, content) {
var opt = OPTION({value: value}, content || value);
if (value == selected) {
opt.attribs.selected = "selected";
}
select.push(opt);
}
if (options instanceof Array) {
options.forEach(f_limitArgs(this, addOption, 1));
} else {
eachProperty(options, addOption);
}
return select;
}
function render_main() {
var categoriesToStats = {};
eachProperty(statDisplays, function(catName, statArray) {
categoriesToStats[catName] = statArray.map(_renderableStat);
});
renderHtml('statistics/stat_page.ejs',
{eachProperty: eachProperty,
statCategoryNames: keys(categoriesToStats),
categoriesToStats: categoriesToStats,
optionsForm: _optionsForm() });
}
function _optionsForm() {
return FORM({id: "statprefs", method: "POST"}, "Show data with granularity: ",
// dropdown({name: 'showLiveOrHistorical', onchange: 'formChanged();'},
// {live: 'live', hist: 'historical'},
// _pref('showLiveOrHistorical')),
// (_showLiveStats() ?
// SPAN(" with granularity ",
dropdown({name: 'granularity', onchange: 'formChanged();'},
{"1": '1 minute', "5": '5 minutes', "60": '1 hour', "1440": '1 day'},
_pref('granularity')), // ),
// : ""),
" top N:",
INPUT({type: "text", name: "topNCount", value: _topN()}),
INPUT({type: "submit", name: "Set", value: "set N"}),
INPUT({type: "hidden", name: "fragment", id: "fragment", value: "health"}));
}
// function render_main() {
// var body = BODY();
//
// var cat = request.params.cat;
// if (!cat) {
// cat = 'health';
// }
//
// body.push(A({id: "backtoadmin", href: "/ep/admin/"}, html("«"), " back to admin"));
// body.push(_renderTopnav(cat));
//
// body.push(form);
//
// if (request.params.stat) {
// body.push(A({className: "viewall",
// href: qpath({stat: null})}, html("«"), " view all"));
// }
//
// var statNames = statDisplays[cat];
// statNames.forEach(function(sn) {
// if (!request.params.stat || (request.params.stat == sn)) {
// body.push(_renderableStat(sn));
// }
// });
//
// helpers.includeCss('admin/admin-stats.css');
// response.write(HTML(HEAD(html(helpers.cssIncludes())), body));
// }
function _getLatest(stat) {
var minutesPerSample = _timescale();
if (_showLiveStats()) {
return latest(liveLatestFunction(minutesPerSample), stat);
} else {
return latest(liveTotal, stat);
}
}
function _getGraph(stat) {
var minutesPerSample = _timescale();
if (_showLiveStats()) {
return html(sparkline(liveHistoryFunction(minutesPerSample), minutesPerSample, stat));
} else {
return html(sparkline(ancientHistoryFunction(60*24*60), 24*60, stat));
}
}
function _getDataLinks(stat) {
if (_showLiveStats()) {
return;
}
function listToLinks(list) {
var links = []; //SPAN({className: "datalink"}, "(data for ");
list.forEach(function(statName) {
links.push(toHTML(A({href: "/ep/admin/usagestats/data?statName="+statName}, statName)));
});
// links.push(")");
return links;
}
switch (stat.type) {
case 'compare':
var stats = [];
stat.stats.map(function(stat) { return getUsedStats(stat.stat); }).forEach(function(list) {
stats = stats.concat(list);
});
return listToLinks(stats);
case 'top':
return listToLinks(stat.stats);
case 'histogram':
return listToLinks(stat.stats);
}
}
function _renderableStat(stat) {
var minutesPerSample = _timescale();
var period = (_showLiveStats() ? minutesPerSample : 24*60);
if (period < 24*60 && stat.hideLive) {
return;
}
if (period < 60) {
period = ""+period+"-minute";
} else if (period < 24*60) {
period = ""+period/(60)+"-hour";
} else if (period >= 24*60) {
period = ""+period/(24*60)+"-day";
}
var graph = _getGraph(stat);
var id = stat.name.replace(/[^a-zA-Z0-9]/g, "");
var displayName = stat.description.replace("%t", period);
var latest = _getLatest(stat);
var dataLinks = _getDataLinks(stat);
return {
id: id,
specialState: "",
displayName: displayName,
name: stat.name,
graph: graph,
latest: latest,
dataLinks: dataLinks
}
}
function render_data() {
var sn = request.params.statName;
var t = TABLE({border: 1, cellpadding: 2, style: "font-family: monospace;"});
_listStats(sn).forEach(function(s) {
var tr = TR();
tr.push(TD((s.id)));
tr.push(TD((new Date(s.timestamp * 1000)).toString()));
tr.push(TD(s.value));
t.push(tr);
});
response.write(HTML(BODY(t)));
}
// function renderStat(body, statName) {
// var div = DIV({className: 'statbox'});
// div.push(A({className: "stat-title", href: qpath({stat: statName})},
// statName, descriptions[statName] || ""));
// if (_showHistStats()) {
// div.push(
// DIV({className: "stat-graph"},
// A({href: '/ep/admin/usagestats/graph?size=1080x420&statName='+statName},
// IMG({src: '/ep/admin/usagestats/graph?size=400x200&statName='+statName,
// style: 'border: 1px solid #ccc; margin: 10px 0 0 20px;'})),
// BR(),
// DIV({style: 'text-align: right;'},
// A({style: 'text-decoration: none; font-size: .8em;',
// href: '/ep/admin/usagestats/data?statName='+statName}, "(data)")))
// );
// }
// if (_showLiveStats()) {
// var data = statistics.getStatData(statName);
// var displayData = statistics.liveSnapshot(data);
// var t = TABLE({border: 0});
// var tcount = 0;
// ["minute", "hour", "fourHour", "day"].forEach(function(timescale) {
// if (! _showTimescale(timescale)) { return; }
// var tr = TR();
// t.push(tr);
// tr.push(TD({valign: "top"}, B("Last ", timescale)));
// var td = TD();
// var cell = SPAN();
// tr.push(td);
// td.push(cell);
// switch (data.plotType) {
// case 'line':
// cell.push(B(displayData[timescale])); break;
// case 'topValues':
// var top = displayData[timescale].topValues;
// if (tcount != 0) {
// tr[0].attribs.style = cell.attribs.style = "border-top: 2px solid black;";
// }
// // println(statName+" / top length: "+top.length);
// for (var i = 0; i < Math.min(_topN(), top.length); ++i) {
// cell.push(B(top[i].count), ": ", top[i].value, BR());
// }
// break;
// case 'histogram':
// var percentiles = displayData[timescale];
// var pcts = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
// var max = percentiles["100"] || 1000;
// cell.push(IMG({src: "http://chart.apis.google.com/chart?chs=340x100&cht=bvs&chd=t:"+
// pcts.map(function(pct) { return Math.round(percentiles[""+pct]/max*100); }).join(",")+
// "&chxt=x,y&chxl=0:|"+
// pcts.map(function(pct) { return ""+pct+"%"; }).join("|")+
// "&chxr=0,0,100|1,0,"+max+""}))
// // td.push("50%: ", B(percentiles["50"]), " ",
// // "90%: ", B(percentiles["90"]), " ",
// // "max: ", B(percentiles["100"]));
// break;
// }
// tcount++;
// });
// div.push(DIV({className: "stat-table"}, t));
// div.push(html(helpers.clearFloats()));
// }
// body.push(div);
// }
// =======
// >>>>>>> Stashed changes:etherpad/src/etherpad/control/statscontrol.js
// old output.
//
// function getStatsForCategory(category) {
// var statnames = statistics.getAllStatNames();
//
// var matchingStatNames = [];
// statnames.forEach(function(sn) {
// if (statistics.getStatData(sn).category == category) {
// matchingStatNames.push(sn);
// }
// });
//
// return matchingStatNames;
// }
//
// function renderCategoryList() {
// var body = BODY();
//
// catNames = getCategoryNames();
// body.push(P("Please select a statistics category:"));
// catNames.sort().forEach(function(catname) {
// body.push(P(A({href: "/ep/admin/usagestats/?cat="+catname}, catname)));
// });
// response.write(body);
// }
//
// function getCategoryNames() {
// var statnames = statistics.getAllStatNames();
// var catNames = {};
// statnames.forEach(function(sn) {
// catNames[statistics.getStatData(sn).category] = true;
// });
// return keys(catNames);
// }
//
// function dropdown(name, options, selected) {
// var select;
// if (typeof(name) == 'string') {
// select = SELECT({name: name});
// } else {
// select = SELECT(name);
// }
//
// function addOption(value, content) {
// var opt = OPTION({value: value}, content || value);
// if (value == selected) {
// opt.attribs.selected = "selected";
// }
// select.push(opt);
// }
//
// if (options instanceof Array) {
// options.forEach(f_limitArgs(this, addOption, 1));
// } else {
// eachProperty(options, addOption);
// }
// return select;
// }
//
// function getCategorizedStats() {
// var statnames = statistics.getAllStatNames();
// var categories = {}
// statnames.forEach(function(sn) {
// var category = statistics.getStatData(sn).category
// if (! categories[category]) {
// categories[category] = [];
// }
// categories[category].push(statistics.getStatData(sn));
// });
// return categories;
// }
//
// function render_ajax() {
// var categoriesToStats = getCategorizedStats();
//
// eachProperty(categoriesToStats, function(catName, statArray) {
// categoriesToStats[catName] = statArray.map(function(statObject) {
// return {
// specialState: "",
// displayName: statObject.name,
// name: statObject.name,
// data: liveStatDisplayHtml(statObject)
// }
// })
// });
//
// renderHtml('statistics/stat_page.ejs',
// {eachProperty: eachProperty,
// statCategoryNames: keys(categoriesToStats),
// categoriesToStats: categoriesToStats });
// }
// function render_main() {
// var body = BODY();
//
// var statNames = statistics.getAllStatNames(); //getStatsForCategory(request.params.cat);
// statNames.forEach(function(sn) {
// renderStat(body, sn);
// });
// response.write(body);
// }
//
// var descriptions = {
// execution_latencies: ", mean response time in milliseconds",
// static_file_latencies: ", mean response time in milliseconds",
// pad_startup_times: ", max response time in milliseconds of fastest N% of requests"
// };
//
// function liveStatDisplayHtml(statObject) {
// var displayData = statistics.liveSnapshot(statObject);
// switch (statObject.plotType) {
// case 'line':
// return displayData;
// case 'topValues':
// var data = {}
// eachProperty(displayData, function(timescale, tsdata) {
// data[timescale] = ""
// var top = tsdata.topValues;
// for (var i = 0; i < Math.min(_topN(), top.length); ++i) {
// data[timescale] += [B(top[i].count), ": ", top[i].value, BR()].map(toHTML).join("");
// }
// if (data[timescale] == "") {
// data[timescale] = "(no data)";
// }
// });
// return data;
// case 'histogram':
// var imgs = {}
// eachProperty(displayData, function(timescale, tsdata) {
// var percentiles = tsdata;
// var pcts = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
// var max = percentiles["100"] || 1000;
// imgs[timescale] =
// toHTML(IMG({src: "http://chart.apis.google.com/chart?chs=400x100&cht=bvs&chd=t:"+
// pcts.map(function(pct) { return Math.round(percentiles[""+pct]/max*100); }).join(",")+
// "&chxt=x,y&chxl=0:|"+
// pcts.map(function(pct) { return ""+pct+"%"; }).join("|")+
// "&chxr=0,0,100|1,0,"+max+""}));
// });
// return imgs;
// }
// }
//
// function renderStat(body, statName) {
// var div = DIV({style: 'float: left; text-align: center; margin: 3px; border: 1px solid black;'})
// div.push(P(statName, descriptions[statName] || ""));
// if (_showLiveStats()) {
// var data = statistics.getStatData(statName);
// var displayData = statistics.liveSnapshot(data);
// var t = TABLE();
// var tcount = 0;
// ["minute", "hour", "fourHour", "day"].forEach(function(timescale) {
// if (! _showTimescale(timescale)) { return; }
// var tr = TR();
// t.push(tr);
// tr.push(TD("last ", timescale));
// var td = TD();
// tr.push(td);
// switch (data.plotType) {
// case 'line':
// td.push(B(displayData[timescale])); break;
// case 'topValues':
// var top = displayData[timescale].topValues;
// if (tcount != 0) {
// tr[0].attribs.style = td.attribs.style = "border-top: 1px solid gray;";
// }
// // println(statName+" / top length: "+top.length);
// for (var i = 0; i < Math.min(_topN(), top.length); ++i) {
// td.push(B(top[i].count), ": ", top[i].value, BR());
// }
// break;
// case 'histogram':
// var percentiles = displayData[timescale];
// var pcts = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
// var max = percentiles["100"] || 1000;
// td.push(IMG({src: "http://chart.apis.google.com/chart?chs=340x100&cht=bvs&chd=t:"+
// pcts.map(function(pct) { return Math.round(percentiles[""+pct]/max*100); }).join(",")+
// "&chxt=x,y&chxl=0:|"+
// pcts.map(function(pct) { return ""+pct+"%"; }).join("|")+
// "&chxr=0,0,100|1,0,"+max+""}))
// // td.push("50%: ", B(percentiles["50"]), " ",
// // "90%: ", B(percentiles["90"]), " ",
// // "max: ", B(percentiles["100"]));
// break;
// }
// tcount++;
// });
// div.push(t)
// }
// if (_showHistStats()) {
// div.push(A({href: '/ep/admin/usagestats/graph?size=1080x420&statName='+statName},
// IMG({src: '/ep/admin/usagestats/graph?size=400x200&statName='+statName,
// style: 'border: 1px solid #ccc; margin: 10px 0 0 20px;'})),
// BR(),
// DIV({style: 'text-align: right;'},
// A({style: 'text-decoration: none; font-size: .8em;',
// href: '/ep/admin/usagestats/data?statName='+statName}, "(data)")));
// }
// body.push(div);
// }
//
// function render_graph() {
// var sn = request.params.statName;
// if (!sn) {
// render404();
// }
// usage_stats.respondWithGraph(sn);
// }
//
//
// function render_exceptions() {
// var logNames = ["frontend/exception", "backend/exceptions"];
// }
// function render_updatehistory() {
//
// sqlcommon.withConnection(function(conn) {
// var stmnt = "delete from statistics;";
// var s = conn.createStatement();
// sqlcommon.closing(s, function() {
// s.execute(stmnt);
// });
// });
//
// var processed = {};
//
// function _domonth(y, m) {
// for (var i = 0; i < 32; i++) {
// _processStatsDay(y, m, i, processed);
// }
// }
//
// _domonth(2008, 10);
// _domonth(2008, 11);
// _domonth(2008, 12);
// _domonth(2009, 1);
// _domonth(2009, 2);
// _domonth(2009, 3);
// _domonth(2009, 4);
// _domonth(2009, 5);
// _domonth(2009, 6);
// _domonth(2009, 7);
//
// response.redirect('/ep/admin/usagestats');
// }
// function _processStatsDay(year, month, date, processed) {
// var now = new Date();
// var day = new Date();
//
// for (var i = 0; i < 10; i++) {
// day.setFullYear(year);
// day.setDate(date);
// day.setMonth(month-1);
// }
//
// if ((+day < +now) &&
// (!((day.getFullYear() == now.getFullYear()) &&
// (day.getMonth() == now.getMonth()) &&
// (day.getDate() == now.getDate())))) {
//
// var dayNoon = statistics.noon(day);
//
// if (processed[dayNoon]) {
// return;
// } else {
// statistics.processLogDay(new Date(dayNoon));
// processed[dayNoon] = true;
// }
// } else {
// /* nothing */
// }
// }