universal handling of retrieving totals from datastore, incl. uniques.

This commit is contained in:
Danny 2018-05-02 14:52:52 +02:00
parent 50556c44e9
commit 47d8347ef1
28 changed files with 395 additions and 246 deletions

View File

@ -30,19 +30,19 @@ class CountWidget extends Component {
}
componentWillReceiveProps(newProps, prevState) {
this.setState({
before: newProps.before,
after: newProps.after,
});
if(newProps.before == prevState.before && newProps.after == prevState.after) {
return;
}
if(newProps.before != prevState.before || newProps.after != prevState.after) {
this.fetchData();
}
this.setState({
before: newProps.before,
after: newProps.after,
});
this.fetchData();
}
@bind
fetchData() {
console.log(this.state);
this.setState({ loading: true })
Client.request(`${this.props.endpoint}/count?before=${this.state.before}&after=${this.state.after}`)

View File

@ -14,7 +14,7 @@ class Table extends Component {
this.state = {
records: [],
limit: 100,
limit: 15,
loading: true,
before: props.before,
after: props.after,
@ -26,14 +26,15 @@ class Table extends Component {
}
componentWillReceiveProps(newProps, prevState) {
if(newProps.before == prevState.before && newProps.after == prevState.after) {
return;
}
this.setState({
before: newProps.before,
after: newProps.after,
});
if(newProps.before != prevState.before || newProps.after != prevState.after) {
this.fetchRecords();
}
this.fetchRecords();
}
@bind
@ -44,19 +45,27 @@ class Table extends Component {
.then((d) => {
this.setState({
loading: false,
records: d
records: d,
});
});
}
render(props, state) {
const tableRows = state.records !== null ? state.records.map((p, i) => (
<div class="table-row">
<div class="cell main-col"><a href={"http://"+p.hostname+p.path}>{p.path||p.label}</a></div>
<div class="cell">{p.count||p.value}</div>
<div class="cell">{p.count_unique||p.unique_value||"-"}</div>
const tableRows = state.records !== null ? state.records.map((p, i) => {
let ahref = document.createElement('a'); ahref.href = p.value;
let classes = "table-row w" + Math.round(p.percentage_of_total);
let label = ahref.pathname;
if( props.showHostname ) {
label = ahref.hostname.replace('www.', '') + (ahref.pathname.length > 1 ? ahref.pathname : '');
}
return(
<div class={classes}>
<div class="cell main-col"><a href={ahref.href}>{label}</a></div>
<div class="cell">{p.count}</div>
<div class="cell">{p.count_unique||"-"}</div>
</div>
)) : <div class="table-row">Nothing here, yet.</div>;
)}) : <div class="table-row">Nothing here, yet.</div>;
const loadingOverlay = state.loading ? <div class="loading-overlay"><div></div></div> : '';

View File

@ -20,15 +20,23 @@ Client.request = function(resource, args) {
}
return fetch(`/api/${resource}`, args)
.then(handleRequestErrors)
.then(parseJSON)
.then(checkData)
.then(parseData)
}
function parseJSON(r) {
return r.json()
}
function checkData(d) {
function handleRequestErrors(r) {
if (!r.ok) {
throw new Error(r.statusText);
}
return r;
}
function parseData(d) {
if(d.Error) {
throw new Error(d.Error)
}

View File

@ -22,14 +22,13 @@ class Dashboard extends Component {
@bind
changePeriod(s) {
console.log(s)
this.setState({ period: s.period, before: s.before, after: s.after })
window.history.replaceState(this.state, null, `#!${s.period}`)
}
render(props, state) {
return (
<div class="rapper">
<div class="wrapper">
<header class="section">
<nav class="main-nav animated fadeInDown">
@ -56,7 +55,7 @@ class Dashboard extends Component {
</div>
<Table endpoint="pageviews" headers={["Top pages", "Views", "Uniques"]} before={state.before} after={state.after} />
<Table endpoint="referrers" headers={["Top referrers", "Views", "Uniques"]} before={state.before} after={state.after} />
<Table endpoint="referrers" headers={["Top referrers", "Views", "Uniques"]} before={state.before} after={state.after} showHostname="true" />
</div>
</section>

View File

@ -1,114 +1,151 @@
/*
TODO: move fonts to local hosting - only host the needed weights/styles not all weights styles
overpass 200, 500, 600
purple #533feb
green #88ffc6
dark #46494d
medium #98a0a6
light #f5f7fa
padding 8, 16, 20, 32, 64, 128, 256, 512, 1024
font size 12, 16, 64
*/
@import url('//overpass-30e2.kxcdn.com/overpass.css');
* { font: 400 16px/1 'overpass', sans-serif; padding: 0; margin: 0; border: 0; outline: 0; border-radius: 0; border: none; vertical-align: baseline; -webkit-appearance: none; appearance: none; list-style: none; box-sizing: border-box; }
::selection { background: #a0ffd1; }
::-moz-selection { background: #a0ffd1; }
TODO: move fonts to local hosting - only host the needed weights/styles not all weights styles
@keyframes fadeInUp {
0% { opacity: 0; transform: translateY(20px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInDown {
0% { opacity: 0; transform: translateY(-20px); }
100% { opacity: 1; transform: translateY(0); }
}
.animated { animation-duration: .4s; animation-fill-mode: both; }
.delayed_02s { animation-delay: .2s; }
.delayed_03s { animation-delay: .3s; }
.delayed_04s { animation-delay: .4s; }
.delayed_05s { animation-delay: .5s; }
.delayed_06s { animation-delay: .6s; }
.fadeInUp { animation-name: fadeInUp; }
.fadeInDown { animation-name: fadeInDown; }
html {}
body { background: #f5f7fa; text-align: center; padding: 8px; }
.rapper { max-width: 1180px; margin: 0 auto; text-align: left; }
.section { margin-bottom: 32px; }
header {}
section {}
footer {}
.boxes { display: flex; margin: 8px 0; flex-wrap: wrap; flex-direction: row; justify-content: flex-start; align-items: stretch; width: 100%; }
.box { border-radius: 4px; margin-bottom: 8px; box-shadow: 0 2px 8px 0 rgba(70,73,77,.16); padding: 24px 0; flex: 1; flex-basis: 100%; }
.box-totals { background: #46494d; color: #fff; padding: 32px 16px 0 16px; }
.box-pages { background: #fff; }
.box-referrers { background: #fff; }
nav.main-nav ul { width: 100%; text-align: right; margin-top: 4px; }
nav li { display: inline-block; }
nav li a { transition: color .2s ease; position: relative; display: inline-block; padding: 0 8px 0 0; }
nav.main-nav li a { padding: 6px 8px 6px 0; }
nav li a:hover { color: #98a0a6; }
nav.date-nav li.active a:after { content:""; background: #88ffc6; display: block; width: 100%; height: 3px; position: absolute; top: 4px; z-index: -1; margin: 0 0 0 -4px; transition: all .4s ease; }
overpass 200, 500, 600
purple #533feb
green #88ffc6
nav.date-nav li a { color: #46494d; }
nav.date-nav li a:hover { color: #98a0a6; }
nav.date-nav li.active a:hover { color: #46494d; }
dark #46494d
medium #98a0a6
light #f5f7fa
padding 8, 16, 20, 32, 64, 128, 256, 512, 1024
font size 12, 16, 64
*/
@import url('//overpass-30e2.kxcdn.com/overpass.css');
* {
padding: 0;
margin: 0;
border: 0;
outline: 0;
border-radius: 0;
border: none;
vertical-align: baseline;
-webkit-appearance: none;
appearance: none;
list-style: none;
box-sizing: border-box;
}
::selection { background: #a0ffd1; }
::-moz-selection { background: #a0ffd1; }
@keyframes fadeInUp {
0% { opacity: 0; transform: translateY(20px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInDown {
0% { opacity: 0; transform: translateY(-20px); }
100% { opacity: 1; transform: translateY(0); }
}
.animated { animation-duration: .4s; animation-fill-mode: both; }
.delayed_02s { animation-delay: .2s; }
.delayed_03s { animation-delay: .3s; }
.delayed_04s { animation-delay: .4s; }
.delayed_05s { animation-delay: .5s; }
.delayed_06s { animation-delay: .6s; }
.fadeInUp { animation-name: fadeInUp; }
.fadeInDown { animation-name: fadeInDown; }
html {}
body {
font: 400 16px/1 'overpass', sans-serif;
background: #f5f7fa;
text-align: center;
padding: 8px;
}
.wrapper { max-width: 1180px; margin: 0 auto; text-align: left; }
.section { margin-bottom: 32px; }
header {}
section {}
footer {}
.boxes {
display: flex;
margin: 8px 0;
flex-wrap: wrap;
flex-direction: row;
justify-content:
flex-start; align-items:
stretch; width: 100%;
}
.box {
border-radius: 4px;
margin-bottom: 8px;
box-shadow: 0 2px 8px 0 rgba(70,73,77,.16);
padding: 24px 0;
flex: 1;
flex-basis: 100%;
min-width: 40px;
}
.box-totals { background: #46494d; color: #fff; padding: 32px 16px 0 16px; }
.box-pages { background: #fff; }
.box-referrers { background: #fff; }
nav.main-nav ul { width: 100%; text-align: right; margin-top: 4px; }
nav li { display: inline-block; }
nav li a { transition: color .2s ease; position: relative; display: inline-block; padding: 0 8px 0 0; }
nav.main-nav li a { padding: 6px 8px 6px 0; }
nav li a:hover { color: #98a0a6; }
nav.date-nav li.active a:after { content:""; background: #88ffc6; display: block; width: 100%; height: 3px; position: absolute; top: 4px; z-index: -1; margin: 0 0 0 -4px; transition: all .4s ease; }
nav.date-nav li a { color: #46494d; }
nav.date-nav li a:hover { color: #98a0a6; }
nav.date-nav li.active a:hover { color: #46494d; }
nav li.visitors { color: #533feb; }
nav li.signout a { padding-right: 0; }
nav li.logo { float: left; }
nav li.logo a { color: #533feb; display: inline-block; background: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 48 48' xml:space='preserve'%3e%3cpath style='fill:%23533feb;' d='M47.882,26.381C47.96,25.598,48,24.804,48,24c0.001-6.623-2.688-12.632-7.029-16.971 C36.632,2.688,30.623-0.001,24,0C17.377-0.001,11.368,2.688,7.029,7.029C2.688,11.368-0.001,17.377,0,24 c0,3.917,0.941,7.624,2.609,10.892c0,0,0,0,0,0c1.985,3.891,4.998,7.165,8.682,9.47C14.975,46.667,19.338,48.001,24,48 c6.221,0.001,11.901-2.372,16.162-6.258C44.424,37.858,47.284,32.45,47.882,26.381C47.882,26.381,47.882,26.381,47.882,26.381 C47.882,26.381,47.882,26.381,47.882,26.381C47.882,26.381,47.882,26.381,47.882,26.381C47.882,26.381,47.882,26.381,47.882,26.381 z M24,2.824c5.852,0.001,11.137,2.368,14.974,6.202c3.596,3.599,5.902,8.472,6.175,13.891l-8.386-8.386 c-0.263-0.263-0.627-0.414-0.998-0.414s-0.735,0.151-0.998,0.413L22.588,26.709l-5.59-5.59c-0.551-0.551-1.445-0.551-1.997,0 l-10.69,10.69C3.353,29.394,2.824,26.762,2.824,24c0.001-5.852,2.368-11.137,6.202-14.974C12.863,5.192,18.148,2.824,24,2.824z'/%3e%3cpath style='fill:%23fff;' d='M4.312,31.809l10.69-10.69c0.551-0.551,1.445-0.551,1.997,0l5.59,5.59l12.178-12.178 c0.263-0.263,0.626-0.413,0.998-0.413s0.735,0.151,0.998,0.414l8.386,8.386c-0.273-5.42-2.579-10.293-6.175-13.891 C35.137,5.192,29.852,2.824,24,2.824C18.148,2.824,12.863,5.192,9.026,9.026C5.192,12.863,2.824,18.148,2.824,24 C2.824,26.762,3.353,29.394,4.312,31.809z'/%3e%3c/svg%3e") top left no-repeat; background-size: 24px 24px; height: 24px; padding: 6px 0 6px 32px; }
.main-nav ul { display: inline-block; }
.spacer { color: #98a0a6; padding: 0 8px; }
svg { width: 24px; height: 24px; display: inline-block; vertical-align: top; }
.header div, .date-nav a, .total-heading { font-size: 12px; text-transform: uppercase; color: #98a0a6; }
p, li, .cell { }
.total-numbers { font-size: 44px; letter-spacing: -3px; margin-bottom: 32px; font-weight: 200; }
.totals-detail { width: 48%; display: inline-block; }
.total-heading { color: #fff; opacity: .6; }
.table-row { display: flex; flex-direction: row; flex-grow: 0; flex-wrap: wrap; width: 100%; position: relative; margin-bottom: 2px; padding: 0 16px; }
.cell { flex-grow: 1; width: 20%; text-align: left; padding: 8px 0; position: relative; z-index: 2; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.main-col { width: 56%; margin-right: 4%; }
.header:after { display: none; }
.table-row:after { content: ""; background: #88ffc6; position: absolute; height: 30px; top: 0; left: 0; opacity: .2; border-right: 2px solid #45ce8c; }
.w100:after{width:99%}.w99:after{width:99%}.w98:after{width:98%}.w97:after{width:97%}.w96:after{width:96%}.w95:after{width:95%}.w94:after{width:94%}.w93:after{width:93%}.w92:after{width:92%}.w91:after{width:91%}.w90:after{width:90%}.w89:after{width:89%}.w88:after{width:88%}.w87:after{width:87%}.w86:after{width:86%}.w85:after{width:85%}.w84:after{width:84%}.w83:after{width:83%}.w82:after{width:82%}.w81:after{width:81%}.w80:after{width:80%}.w79:after{width:79%}.w78:after{width:78%}.w77:after{width:77%}.w76:after{width:76%}.w75:after{width:75%}.w74:after{width:74%}.w73:after{width:73%}.w72:after{width:72%}.w71:after{width:71%}.w70:after{width:70%}.w69:after{width:69%}.w68:after{width:68%}.w67:after{width:67%}.w66:after{width:66%}.w65:after{width:65%}.w64:after{width:64%}.w63:after{width:63%}.w62:after{width:62%}.w61:after{width:61%}.w60:after{width:60%}.w59:after{width:59%}.w58:after{width:58%}.w57:after{width:57%}.w56:after{width:56%}.w55:after{width:55%}.w54:after{width:54%}.w53:after{width:53%}.w52:after{width:52%}.w51:after{width:51%}.w50:after{width:50%}.w49:after{width:49%}.w48:after{width:48%}.w47:after{width:47%}.w46:after{width:46%}.w45:after{width:45%}.w44:after{width:44%}.w43:after{width:43%}.w42:after{width:42%}.w41:after{width:41%}.w40:after{width:40%}.w39:after{width:39%}.w38:after{width:38%}.w37:after{width:37%}.w36:after{width:36%}.w35:after{width:35%}.w34:after{width:34%}.w33:after{width:33%}.w32:after{width:32%}.w31:after{width:31%}.w30:after{width:30%}.w29:after{width:29%}.w28:after{width:28%}.w27:after{width:27%}.w26:after{width:26%}.w25:after{width:25%}.w24:after{width:24%}.w23:after{width:23%}.w22:after{width:22%}.w21:after{width:21%}.w20:after{width:20%}.w19:after{width:19%}.w18:after{width:18%}.w17:after{width:17%}.w16:after{width:16%}.w15:after{width:15%}.w14:after{width:14%}.w13:after{width:13%}.w12:after{width:12%}.w11:after{width:11%}.w10:after{width:10%}.w09:after{width:9%}.w08:after{width:8%}.w07:after{width:7%}.w06:after{width:6%}.w05:after{width:5%}.w04:after{width:4%}.w03:after{width:3%}.w02:after{width:2%}.w01:after{width:1%}.w00:after{width:0}
a { color: #46494d; text-decoration: none; transition: all .4s ease; }
a:hover {}
.cell a:hover { color: #533feb; }
nav li.visitors { color: #533feb; }
nav li.signout a { padding-right: 0; }
nav li.logo { float: left; }
nav li.logo a { color: #533feb; display: inline-block; background: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 48 48' xml:space='preserve'%3e%3cpath style='fill:%23533feb;' d='M47.882,26.381C47.96,25.598,48,24.804,48,24c0.001-6.623-2.688-12.632-7.029-16.971 C36.632,2.688,30.623-0.001,24,0C17.377-0.001,11.368,2.688,7.029,7.029C2.688,11.368-0.001,17.377,0,24 c0,3.917,0.941,7.624,2.609,10.892c0,0,0,0,0,0c1.985,3.891,4.998,7.165,8.682,9.47C14.975,46.667,19.338,48.001,24,48 c6.221,0.001,11.901-2.372,16.162-6.258C44.424,37.858,47.284,32.45,47.882,26.381C47.882,26.381,47.882,26.381,47.882,26.381 C47.882,26.381,47.882,26.381,47.882,26.381C47.882,26.381,47.882,26.381,47.882,26.381C47.882,26.381,47.882,26.381,47.882,26.381 z M24,2.824c5.852,0.001,11.137,2.368,14.974,6.202c3.596,3.599,5.902,8.472,6.175,13.891l-8.386-8.386 c-0.263-0.263-0.627-0.414-0.998-0.414s-0.735,0.151-0.998,0.413L22.588,26.709l-5.59-5.59c-0.551-0.551-1.445-0.551-1.997,0 l-10.69,10.69C3.353,29.394,2.824,26.762,2.824,24c0.001-5.852,2.368-11.137,6.202-14.974C12.863,5.192,18.148,2.824,24,2.824z'/%3e%3cpath style='fill:%23fff;' d='M4.312,31.809l10.69-10.69c0.551-0.551,1.445-0.551,1.997,0l5.59,5.59l12.178-12.178 c0.263-0.263,0.626-0.413,0.998-0.413s0.735,0.151,0.998,0.414l8.386,8.386c-0.273-5.42-2.579-10.293-6.175-13.891 C35.137,5.192,29.852,2.824,24,2.824C18.148,2.824,12.863,5.192,9.026,9.026C5.192,12.863,2.824,18.148,2.824,24 C2.824,26.762,3.353,29.394,4.312,31.809z'/%3e%3c/svg%3e") top left no-repeat; background-size: 24px 24px; height: 24px; padding: 6px 0 6px 32px; }
.main-nav ul { display: inline-block; }
.spacer { color: #98a0a6; padding: 0 8px; }
svg { width: 24px; height: 24px; display: inline-block; vertical-align: top; }
.header div, .date-nav a, .total-heading { font-size: 12px; text-transform: uppercase; color: #98a0a6; }
p, li, .cell { }
.total-numbers { font-size: 44px; letter-spacing: -3px; margin-bottom: 32px; font-weight: 200; }
.totals-detail { width: 48%; display: inline-block; }
.total-heading { color: #fff; opacity: .6; }
.table-row { display: flex; flex-direction: row; flex-grow: 0; flex-wrap: wrap; width: 100%; position: relative; margin-bottom: 2px; padding: 0 16px; }
.cell { flex-grow: 1; width: 20%; text-align: left; padding: 8px 0; position: relative; z-index: 2; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.main-col { width: 56%; margin-right: 4%; }
.header:after { display: none; }
.table-row:after { content: ""; background: #88ffc6; position: absolute; height: 30px; top: 0; left: 0; opacity: .2; border-right: 2px solid #45ce8c; }
a { color: #46494d; text-decoration: none; transition: all .4s ease; }
a:hover {}
.cell a:hover { color: #533feb; }
@media ( min-width: 1220px ) {
nav.main-nav ul { margin-top: 24px; }
.boxes { justify-content: space-between; flex-wrap: nowrap; }
.box { margin: 0 4px; }
.box-totals { max-width: 230px; margin-left: 0; }
.box-referrers { margin-right: 0; }
.totals-detail { width: 100%; }
.total-numbers { font-size: 64px; }
}
.w100:after{width:99%}.w99:after{width:99%}.w98:after{width:98%}.w97:after{width:97%}.w96:after{width:96%}.w95:after{width:95%}.w94:after{width:94%}.w93:after{width:93%}.w92:after{width:92%}.w91:after{width:91%}.w90:after{width:90%}.w89:after{width:89%}.w88:after{width:88%}.w87:after{width:87%}.w86:after{width:86%}.w85:after{width:85%}.w84:after{width:84%}.w83:after{width:83%}.w82:after{width:82%}.w81:after{width:81%}.w80:after{width:80%}.w79:after{width:79%}.w78:after{width:78%}.w77:after{width:77%}.w76:after{width:76%}.w75:after{width:75%}.w74:after{width:74%}.w73:after{width:73%}.w72:after{width:72%}.w71:after{width:71%}.w70:after{width:70%}.w69:after{width:69%}.w68:after{width:68%}.w67:after{width:67%}.w66:after{width:66%}.w65:after{width:65%}.w64:after{width:64%}.w63:after{width:63%}.w62:after{width:62%}.w61:after{width:61%}.w60:after{width:60%}.w59:after{width:59%}.w58:after{width:58%}.w57:after{width:57%}.w56:after{width:56%}.w55:after{width:55%}.w54:after{width:54%}.w53:after{width:53%}.w52:after{width:52%}.w51:after{width:51%}.w50:after{width:50%}.w49:after{width:49%}.w48:after{width:48%}.w47:after{width:47%}.w46:after{width:46%}.w45:after{width:45%}.w44:after{width:44%}.w43:after{width:43%}.w42:after{width:42%}.w41:after{width:41%}.w40:after{width:40%}.w39:after{width:39%}.w38:after{width:38%}.w37:after{width:37%}.w36:after{width:36%}.w35:after{width:35%}.w34:after{width:34%}.w33:after{width:33%}.w32:after{width:32%}.w31:after{width:31%}.w30:after{width:30%}.w29:after{width:29%}.w28:after{width:28%}.w27:after{width:27%}.w26:after{width:26%}.w25:after{width:25%}.w24:after{width:24%}.w23:after{width:23%}.w22:after{width:22%}.w21:after{width:21%}.w20:after{width:20%}.w19:after{width:19%}.w18:after{width:18%}.w17:after{width:17%}.w16:after{width:16%}.w15:after{width:15%}.w14:after{width:14%}.w13:after{width:13%}.w12:after{width:12%}.w11:after{width:11%}.w10:after{width:10%}.w09:after{width:9%}.w08:after{width:8%}.w07:after{width:7%}.w06:after{width:6%}.w05:after{width:5%}.w04:after{width:4%}.w03:after{width:3%}.w02:after{width:2%}.w01:after{width:1%}.w00:after{width:0}
@media ( min-width: 1220px ) {
nav.main-nav ul { margin-top: 24px; }
.boxes { justify-content: space-between; flex-wrap: nowrap; }
.box { margin: 0 4px; }
.box-totals { max-width: 230px; margin-left: 0; }
.box-referrers { margin-right: 0; }
.totals-detail { width: 100%; }
.total-numbers { font-size: 64px; }
}

View File

@ -20,25 +20,25 @@ if( ! debug ) {
gulp.task('default', defaultTasks);
gulp.task('browserify', function () {
return browserify({
entries: './assets/js/script.js',
debug: debug
})
.transform("babelify", {
presets: ["es2015"],
plugins: [
"transform-decorators-legacy",
["transform-react-jsx", { "pragma":"h" } ]
]
})
.bundle()
.on('error', function(err){
console.log(err.message);
this.emit('end');
})
.pipe(source('script.js'))
.pipe(buffer())
.pipe(gulp.dest('./build/js/'))
return browserify({
entries: './assets/js/script.js',
debug: debug
})
.transform("babelify", {
presets: ["es2015"],
plugins: [
"transform-decorators-legacy",
["transform-react-jsx", { "pragma":"h" } ]
]
})
.bundle()
.on('error', function(err){
console.log(err.message);
this.emit('end');
})
.pipe(source('script.js'))
.pipe(buffer())
.pipe(gulp.dest('./build/js/'))
});
gulp.task('minify', function(cb) {
@ -79,5 +79,5 @@ gulp.task('watch', ['default'], function() {
gulp.watch(['./assets/js/**/*.js'], ['browserify', 'tracker'] );
gulp.watch(['./assets/sass/**/**/*.scss'], ['sass'] );
gulp.watch(['./assets/**/*.html'], ['html'] );
gulp.watch(['./assets/img/**/*'], ['img'] );
gulp.watch(['./assets/img/**/*'], ['img'] );
});

View File

@ -3,13 +3,14 @@ package api
import (
"net/http"
"github.com/usefathom/fathom/pkg/count"
"github.com/usefathom/fathom/pkg/datastore"
)
// URL: /api/pageviews
var GetPageviewsHandler = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
before, after := getRequestedPeriods(r)
results, err := datastore.TotalPageviewsPerPage(before, after, defaultLimit)
results, err := count.Pageviews(before, after, defaultLimit)
if err != nil {
return err
}

View File

@ -9,18 +9,18 @@ import (
)
// Browsers returns a point slice containing browser data per browser name
func Browsers(before int64, after int64, limit int64) ([]*models.Point, error) {
func Browsers(before int64, after int64, limit int64) ([]*models.Total, error) {
points, err := datastore.TotalsPerBrowser(before, after, limit)
if err != nil {
return nil, err
}
total, err := datastore.TotalUniqueBrowsers(before, after)
total, err := datastore.TotalBrowsers(before, after)
if err != nil {
return nil, err
}
points = calculatePointPercentages(points, total)
points = calculatePercentagesOfTotal(points, total)
return points, nil
}

View File

@ -35,11 +35,11 @@ func Archive() {
log.Infof("finished aggregating metrics. ran for %dms.", (end.UnixNano()-start.UnixNano())/1000000)
}
func calculatePointPercentages(points []*models.Point, total int) []*models.Point {
func calculatePercentagesOfTotal(totals []*models.Total, total int) []*models.Total {
// calculate percentage values for each point
for _, p := range points {
p.PercentageValue = float64(p.Value) / float64(total) * 100.00
for _, p := range totals {
p.PercentageOfTotal = float64(p.Count) / float64(total) * 100.00
}
return points
return totals
}

View File

@ -8,16 +8,16 @@ import (
)
func TestCalculatePointPercentages(t *testing.T) {
points := []*models.Point{
&models.Point{
Label: "Foo",
Value: 5,
totals := []*models.Total{
&models.Total{
Value: "Foo",
Count: 5,
},
}
points = calculatePointPercentages(points, 100)
totals = calculatePercentagesOfTotal(totals, 100)
if points[0].PercentageValue != 5.00 {
t.Errorf("Percentage value should be 5.00, is %.2f", points[0].PercentageValue)
if totals[0].PercentageOfTotal != 5.00 {
t.Errorf("Percentage value should be 5.00, is %.2f", totals[0].PercentageOfTotal)
}
}

View File

@ -9,18 +9,18 @@ import (
)
// Languages returns a point slice containing language data per language
func Languages(before int64, after int64, limit int64) ([]*models.Point, error) {
func Languages(before int64, after int64, limit int64) ([]*models.Total, error) {
points, err := datastore.TotalsPerLanguage(before, after, limit)
if err != nil {
return nil, err
}
total, err := datastore.TotalUniqueLanguages(before, after)
total, err := datastore.TotalLanguages(before, after)
if err != nil {
return nil, err
}
points = calculatePointPercentages(points, total)
points = calculatePercentagesOfTotal(points, total)
return points, nil
}

View File

@ -5,8 +5,25 @@ import (
"time"
"github.com/usefathom/fathom/pkg/datastore"
"github.com/usefathom/fathom/pkg/models"
)
// Pageviews returns a point slice containing language data per language
func Pageviews(before int64, after int64, limit int64) ([]*models.Total, error) {
points, err := datastore.TotalPageviewsPerPage(before, after, limit)
if err != nil {
return nil, err
}
total, err := datastore.TotalPageviews(before, after)
if err != nil {
return nil, err
}
points = calculatePercentagesOfTotal(points, total)
return points, nil
}
// CreatePageviewTotals aggregates pageview data for each page into daily totals
func CreatePageviewTotals(since string) {
tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02")

View File

@ -9,7 +9,7 @@ import (
)
// Referrers returns a point slice containing browser data per browser name
func Referrers(before int64, after int64, limit int64) ([]*models.Point, error) {
func Referrers(before int64, after int64, limit int64) ([]*models.Total, error) {
points, err := datastore.TotalsPerReferrer(before, after, limit)
if err != nil {
return nil, err
@ -20,7 +20,7 @@ func Referrers(before int64, after int64, limit int64) ([]*models.Point, error)
return nil, err
}
points = calculatePointPercentages(points, total)
points = calculatePercentagesOfTotal(points, total)
return points, nil
}

View File

@ -9,18 +9,18 @@ import (
)
// Screens returns a point slice containing screen data per size
func Screens(before int64, after int64, limit int64) ([]*models.Point, error) {
func Screens(before int64, after int64, limit int64) ([]*models.Total, error) {
points, err := datastore.TotalsPerScreen(before, after, limit)
if err != nil {
return nil, err
}
total, err := datastore.TotalUniqueScreens(before, after)
total, err := datastore.TotalScreens(before, after)
if err != nil {
return nil, err
}
points = calculatePointPercentages(points, total)
points = calculatePercentagesOfTotal(points, total)
return points, nil
}

View File

@ -0,0 +1,7 @@
-- +migrate Up
ALTER TABLE pages ADD COLUMN scheme ENUM("http", "https") DEFAULT "http";
ALTER TABLE pages DROP COLUMN title;
-- +migrate Down
ALTER TABLE pages DROP COLUMN scheme;
ALTER TABLE pages ADD COLUMN title VARCHAR(255) NULL;

View File

@ -23,8 +23,8 @@ func GetPageByHostnameAndPath(hostname, path string) (*models.Page, error) {
// SavePage inserts the page model in the connected database
func SavePage(p *models.Page) error {
query := dbx.Rebind(`INSERT INTO pages(hostname, path, title) VALUES(?, ?, ?)`)
result, err := dbx.Exec(query, p.Hostname, p.Path, p.Title)
query := dbx.Rebind(`INSERT INTO pages(scheme, hostname, path) VALUES(?, ?, ?)`)
result, err := dbx.Exec(query, p.Scheme, p.Hostname, p.Path)
if err != nil {
return err
}

View File

@ -7,10 +7,8 @@ import (
)
func SaveTotals(metric string, totals []*models.Total) error {
query := dbx.Rebind(fmt.Sprintf(`
INSERT INTO total_%s( value, count, count_unique, date)
VALUES( ?, ?, ?, ? ) ON DUPLICATE KEY UPDATE count = ?, count_unique = ?
`, metric))
query := fmt.Sprintf(`INSERT INTO total_%s( value, count, count_unique, date) VALUES( ?, ?, ?, ? ) ON DUPLICATE KEY UPDATE count = ?, count_unique = ?`, metric)
query = dbx.Rebind(query)
tx, err := dbx.Begin()
if err != nil {

View File

@ -2,6 +2,19 @@ package datastore
import "github.com/usefathom/fathom/pkg/models"
// TotalBrowsers returns the total # of browsers between two given timestamps
func TotalBrowsers(before int64, after int64) (int, error) {
var total int
query := dbx.Rebind(`
SELECT
COALESCE(SUM(t.count), 0)
FROM total_browser_names t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?`)
err := dbx.Get(&total, query, before, after)
return total, err
}
// TotalUniqueBrowsers returns the total # of unique browsers between two given timestamps
func TotalUniqueBrowsers(before int64, after int64) (int, error) {
var total int
@ -15,17 +28,18 @@ func TotalUniqueBrowsers(before int64, after int64) (int, error) {
return total, err
}
func TotalsPerBrowser(before int64, after int64, limit int64) ([]*models.Point, error) {
var results []*models.Point
func TotalsPerBrowser(before int64, after int64, limit int64) ([]*models.Total, error) {
var results []*models.Total
query := dbx.Rebind(`
SELECT
t.value AS label,
COALESCE(SUM(t.count_unique), 0) AS value
t.value AS value,
COALESCE(SUM(t.count), 0) AS count,
COALESCE(SUM(t.count_unique), 0) AS count_unique
FROM total_browser_names t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?
GROUP BY label
ORDER BY value DESC
GROUP BY t.value
ORDER BY count DESC
LIMIT ?`)
err := dbx.Select(&results, query, before, after, limit)

View File

@ -2,6 +2,20 @@ package datastore
import "github.com/usefathom/fathom/pkg/models"
// TotalLanguages returns the total # of browser languages between two given timestamps
func TotalLanguages(before int64, after int64) (int, error) {
var total int
query := dbx.Rebind(`
SELECT
COALESCE(SUM(t.count), 0)
FROM total_browser_languages t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?`)
err := dbx.Get(&total, query, before, after)
return total, err
}
// TotalUniqueLanguages returns the total # of unique browser languages between two given timestamps
func TotalUniqueLanguages(before int64, after int64) (int, error) {
var total int
@ -16,17 +30,18 @@ func TotalUniqueLanguages(before int64, after int64) (int, error) {
return total, err
}
func TotalsPerLanguage(before int64, after int64, limit int64) ([]*models.Point, error) {
var results []*models.Point
func TotalsPerLanguage(before int64, after int64, limit int64) ([]*models.Total, error) {
var results []*models.Total
query := dbx.Rebind(`
SELECT
t.value AS label,
COALESCE(SUM(t.count_unique), 0) AS value
t.value AS value,
COALESCE(SUM(t.count), 0) AS count,
COALESCE(SUM(t.count_unique), 0) AS count_unique
FROM total_browser_languages t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?
GROUP BY label
ORDER BY value DESC
GROUP BY t.value
ORDER BY count DESC
LIMIT ?`)
err := dbx.Select(&results, query, before, after, limit)

View File

@ -20,17 +20,35 @@ func TotalPageviews(before int64, after int64) (int, error) {
return total, nil
}
// TotalUniquePageviews returns the total number of unique pageviews between the given timestamps
func TotalUniquePageviews(before int64, after int64) (int, error) {
var total int
query := dbx.Rebind(`
SELECT COALESCE(SUM(t.count_unique), 0)
FROM total_pageviews t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?`)
err := dbx.Get(&total, query, before, after)
if err != nil {
return 0, err
}
return total, nil
}
// TotalPageviewsPerDay returns a slice of data points representing the number of pageviews per day
func TotalPageviewsPerDay(before int64, after int64) ([]*models.Point, error) {
var results []*models.Point
func TotalPageviewsPerDay(before int64, after int64) ([]*models.Total, error) {
var results []*models.Total
query := dbx.Rebind(`
SELECT
COALESCE(SUM(t.count), 0) AS value,
CONCAT(p.scheme, "://", p.hostname, p.path) AS value,
COALESCE(SUM(t.count), 0) AS count,
COALESCE(SUM(t.count_unique), 0) AS count_unique,
DATE_FORMAT(t.date, '%Y-%m-%d') AS label
FROM total_pageviews t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?
GROUP BY label`)
GROUP BY label, p.hostname, p.path, p.scheme`)
err := dbx.Select(&results, query, before, after)
if err != nil {
@ -41,18 +59,17 @@ func TotalPageviewsPerDay(before int64, after int64) ([]*models.Point, error) {
}
// TotalPageviewsPerPage returns a set of pageview counts, grouped by page (hostname + path)
func TotalPageviewsPerPage(before int64, after int64, limit int64) ([]*models.PageviewCount, error) {
var results []*models.PageviewCount
func TotalPageviewsPerPage(before int64, after int64, limit int64) ([]*models.Total, error) {
var results []*models.Total
query := dbx.Rebind(`
SELECT
p.hostname,
p.path,
CONCAT(p.scheme, "://", p.hostname, p.path) AS value,
COALESCE(SUM(t.count), 0) AS count,
COALESCE(SUM(t.count_unique), 0) AS countunique
COALESCE(SUM(t.count_unique), 0) AS count_unique
FROM total_pageviews t
LEFT JOIN pages p ON p.id = t.page_id
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?
GROUP BY p.path, p.hostname
GROUP BY p.hostname, p.path, p.scheme
ORDER BY count DESC
LIMIT ?`)
err := dbx.Select(&results, query, before, after, limit)
@ -63,6 +80,8 @@ func TotalPageviewsPerPage(before int64, after int64, limit int64) ([]*models.Pa
return results, nil
}
// SavePageviewTotals saves the given totals in the connected database
// Differs slightly from the metric specific totals because of the normalized pages (to save storage)
func SavePageviewTotals(totals []*models.Total) error {
tx, err := dbx.Begin()
if err != nil {

View File

@ -16,17 +16,32 @@ func TotalReferrers(before int64, after int64) (int, error) {
return total, err
}
func TotalsPerReferrer(before int64, after int64, limit int64) ([]*models.Point, error) {
var results []*models.Point
// TotalUniqueReferrers returns the total # of unique referrers between two given timestamps
func TotalUniqueReferrers(before int64, after int64) (int, error) {
var total int
query := dbx.Rebind(`
SELECT
COALESCE(SUM(t.count_unique), 0)
FROM total_referrers t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?`)
err := dbx.Get(&total, query, before, after)
return total, err
}
func TotalsPerReferrer(before int64, after int64, limit int64) ([]*models.Total, error) {
var results []*models.Total
query := dbx.Rebind(`
SELECT
t.value AS label,
COALESCE(SUM(t.count), 0) AS value
t.value,
COALESCE(SUM(t.count), 0) AS count,
COALESCE(SUM(t.count_unique), 0) AS count_unique
FROM total_referrers t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?
GROUP BY label
ORDER BY value DESC
GROUP BY t.value
ORDER BY count DESC
LIMIT ?`)
err := dbx.Select(&results, query, before, after, limit)

View File

@ -2,7 +2,20 @@ package datastore
import "github.com/usefathom/fathom/pkg/models"
// TotalUniqueScreens returns the total # of screens between two given timestamps
// TotalScreens returns the total # of screens between two given timestamps
func TotalScreens(before int64, after int64) (int, error) {
var total int
query := dbx.Rebind(`
SELECT
COALESCE(SUM(t.count), 0)
FROM total_screens t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?`)
err := dbx.Get(&total, query, before, after)
return total, err
}
// TotalUniqueScreens returns the total # of unique screens between two given timestamps
func TotalUniqueScreens(before int64, after int64) (int, error) {
var total int
@ -15,17 +28,18 @@ func TotalUniqueScreens(before int64, after int64) (int, error) {
return total, err
}
func TotalsPerScreen(before int64, after int64, limit int64) ([]*models.Point, error) {
var results []*models.Point
func TotalsPerScreen(before int64, after int64, limit int64) ([]*models.Total, error) {
var results []*models.Total
query := dbx.Rebind(`
SELECT
t.value AS label,
COALESCE(SUM(t.count_unique), 0) AS value
t.value AS value,
COALESCE(SUM(t.count), 0) AS count,
COALESCE(SUM(t.count_unique), 0) AS count_unique
FROM total_screens t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?
GROUP BY t.value
ORDER BY value DESC
ORDER BY count DESC
LIMIT ?`)
err := dbx.Select(&results, query, before, after, limit)

View File

@ -15,11 +15,12 @@ func TotalVisitors(before int64, after int64) (int, error) {
}
// TotalVisitorsPerDay returns a point slice containing visitor data per day
func TotalVisitorsPerDay(before int64, after int64) ([]*models.Point, error) {
var results []*models.Point
func TotalVisitorsPerDay(before int64, after int64) ([]*models.Total, error) {
var results []*models.Total
query := dbx.Rebind(`SELECT
COALESCE(SUM(t.count), 0) AS value,
COALESCE(SUM(t.count), 0) AS count,
COALESCE(SUM(t.count_unique), 0) AS count_unique,
DATE_FORMAT(t.date, '%Y-%m-%d') AS label
FROM total_visitors t
WHERE UNIX_TIMESTAMP(t.date) <= ? AND UNIX_TIMESTAMP(t.date) >= ?
@ -29,6 +30,7 @@ func TotalVisitorsPerDay(before int64, after int64) ([]*models.Point, error) {
return results, err
}
// SaveVisitorTotals saves the given totals in the connected datastore
func SaveVisitorTotals(totals []*models.Total) error {
tx, err := dbx.Begin()
if err != nil {

8
pkg/models/count.go Normal file
View File

@ -0,0 +1,8 @@
package models
type Count struct {
URL string `json:"url"`
Views int64 `json:"views"`
Uniques int64 `json:"uniques"`
PercentOfTotal float64 `json:"percent_of_total"`
}

View File

@ -1,8 +1,9 @@
package models
type Page struct {
ID int64
Hostname string
Path string
Title string
ID int64 `json:"-"`
Scheme string `json:"scheme"`
Hostname string `json:"hostname"`
Path string `json:"path"`
Title string `json:"title"`
}

View File

@ -8,10 +8,3 @@ type Pageview struct {
ReferrerUrl string
Timestamp string
}
type PageviewCount struct {
Hostname string `json:"hostname"`
Path string `json:"path"`
Count int `json:"count"`
CountUnique int `json:"count_unique"`
}

View File

@ -1,9 +0,0 @@
package models
// Point represents a data point, will always have a Label and Value
type Point struct {
Label string `json:"label"`
Value int `json:"value"`
PercentageValue float64 `json:"perc_value,omitempty"`
UniqueValue int `json:"unique_value,omitempty"`
}

View File

@ -2,10 +2,11 @@ package models
// Total represents a daily aggregated total for a metric
type Total struct {
ID int64
PageID int64 `db:"page_id"`
Value string
Count int64
CountUnique int64 `db:"count_unique"`
Date string `db:"date_group"`
ID int64 `json:"-"`
PageID int64 `db:"page_id" json:"-"`
Value string `db:"value" json:"value"`
Count int64 `db:"count" json:"count"`
CountUnique int64 `db:"count_unique" json:"count_unique"`
PercentageOfTotal float64 `db:"-" json:"percentage_of_total"`
Date string `db:"date_group" json:"date,omitempty"`
}