Skip to content
Browse files

track stats with redis

  • Loading branch information...
1 parent 3331e1d commit 6a478b8d434082f66ce1ec49d3c26f2326a43651 Jeff Balogh committed Apr 2, 2012
Showing with 130 additions and 8 deletions.
  1. +88 −2 app.py
  2. +1 −0 requirements.txt
  3. +41 −6 static/app.js
View
90 app.py
@@ -1,9 +1,10 @@
from datetime import datetime
import json
import os
-
+import time
import urlparse
+import redis as redislib
import requests
from flask import Flask, request, redirect, abort, session, jsonify
@@ -21,6 +22,11 @@
app.secret_key = os.environ.get('SECRET_KEY', 'secret key')
+redis_url = urlparse.urlparse(os.environ.get('REDISTOGO_URL',
+ 'redis://localhost:6379'))
+redis = redislib.Redis(host=redis_url.hostname, port=redis_url.port,
+ password=redis_url.password)
+
class Model(object):
@@ -54,34 +60,83 @@ def __repr__(self):
return self.repo
+def stat(name, bucket='stats'):
+ start = time.time()
+ now = datetime.now()
+ pipe = redis.pipeline()
+ pipe.hincrby(bucket, name, 1)
+ pipe.incr(name + now.strftime(':%Y-%m-%d:%H:%M'))
+ pipe.incr(name + now.strftime(':%Y-%m-%d:%H'))
+ pipe.incr(name + now.strftime(':%Y-%m-%d'))
+ pipe.execute()
+ print 'redis: %.2f' % (time.time() - start)
+
+
+STATS = {
+ 'http-redirect': 'Redirect HTTP to HTTPS',
+ 'homepage': '/',
+ 'queue': '/queue',
+ 'new-queue': 'New Queue',
+ 'update-queue': 'Update Queue',
+ 'oauth': '/oauth',
+ 'new-user': 'New User',
+ 'hook': '/hook',
+ 'notify': 'Send Notification',
+ 'subscribe': '/subscribe',
+ 'add-subscription': 'Add subscription',
+ 'unsubscribe': 'Unsubscribe',
+ 'stat': '/stat',
+ 'test-hook': 'Test Hook',
+ 'add-hook': 'Add Hook',
+ 'hook-fail': 'Adding Hook Failed',
+ 'remove-hook': 'Remove Hook',
+ 'step-1': 'Get Add-on',
+ 'step-2': 'Authorize',
+ 'step-3': 'Add Repos',
+ 'check-perm': 'Check Permission succeeded',
+ 'request-perm': 'Request Permission succeeded',
+ 'push-url': 'Have push URL',
+ 'oauthd': 'OAuth Success',
+ 'nav-timing': 'Navigation Timing',
+ 'no-nav-timing': 'No Navigation Timer',
+}
+
+
@app.route('/', methods=['GET'])
def root():
if not app.debug and request.headers['X-Forwarded-Proto'] != 'https':
+ stat('http-redirect')
response = redirect('https://github-notifications.herokuapp.com', 301)
response.headers['Strict-Transport-Security'] = 'max-age=15768000'
return response
+ stat('homepage')
return open('index.html').read() % {'STATIC': STATIC_URL}
@app.route('/queue', methods=['POST'])
def add_queue():
+ stat('queue')
queue = request.form['queue']
username = session['username']
user = User.query.filter_by(username=username).first_or_404()
new_user = user.push_url is None
if user.push_url != queue:
+ if not new_user:
+ stat('update-queue')
print ('Adding push URL.' if new_user else 'Updating push URL.')
user.push_url = queue
db.session.add(user)
db.session.commit()
if new_user:
+ stat('new-queue')
notify(queue, 'Welcome to Github Notifications!',
'So glad to have you %s.' % user.username)
return ''
@app.route('/oauth', methods=['GET'])
def oauth():
+ stat('oauth')
if 'code' in request.args:
code = request.args['code']
r = requests.post('https://github.com/login/oauth/access_token',
@@ -93,6 +148,7 @@ def oauth():
r = requests.get('https://api.github.com/user?access_token=%s' % token)
username = json.loads(r.text)['login']
if not User.query.filter_by(username=username).first():
+ stat('new-user')
print 'New user:', username
user = User(username=username)
db.session.add(user)
@@ -101,14 +157,15 @@ def oauth():
session['username'] = username
response = redirect('/')
response.set_cookie('access_token', token)
- response.set_cookie('username', username);
+ response.set_cookie('username', username)
return response
return redirect('/')
@app.route('/hook', methods=['POST'])
def hook():
+ stat('hook')
payload = json.loads(request.form['payload'])
repo = payload['repository']
if not payload['commits']:
@@ -125,6 +182,7 @@ def hook():
action = payload['compare']
repo_slug = normalize(repo['url'])
+ stat(repo_slug, bucket='hooks')
print 'Sending hook for:', repo_slug
q = User.query.join(User.subscriptions).filter(Subscription.repo == repo_slug)
for user in q.all():
@@ -139,6 +197,7 @@ def normalize(repo_url):
@app.route('/subscribe', methods=['POST'])
def subscribe():
+ stat('subscribe')
repo = request.form['repo']
username = session['username']
@@ -147,6 +206,7 @@ def subscribe():
if r.status_code == 204:
repo = normalize(repo)
if not Subscription.query.filter_by(user=user, repo=repo).first():
+ stat('add-subscription')
print 'Adding a subscription for:', repo
sub = Subscription(repo=repo, user=user)
db.session.add(sub)
@@ -157,6 +217,7 @@ def subscribe():
@app.route('/unsubscribe', methods=['POST'])
def unsubscribe():
+ stat('unsubscribe')
repo = normalize(request.form['repo'])
username = session['username']
@@ -167,11 +228,36 @@ def unsubscribe():
return jsonify(count=Subscription.query.filter_by(repo=repo).count())
+@app.route('/stat', methods=['POST'])
+def add_stat():
+ stat('add-stat')
+ stat(request.form['name'])
+ return ''
+
+
+@app.route('/nav-timing', methods=['POST'])
+def nav_timing():
+ start = time.time()
+ pipe = redis.pipeline()
+ pipe.hincrby('stats', 'nav-timing', 1)
+ pipe.rpush('dns', request.form['dns'])
+ pipe.rpush('connect', request.form['connect'])
+ pipe.rpush('response', request.form['response'])
+ pipe.rpush('interactive', request.form['interactive'])
+ pipe.rpush('loaded', request.form['loaded'])
+ pipe.rpush('total', request.form['total'])
+ pipe.execute()
+ print 'redis: %.2f' % (time.time() - start)
+ return ''
+
+
def notify(queue, title, text, action=None):
+ stat('notify')
msg = {'title': title, 'body': text, 'actionUrl': action}
msg = dict((k, v) for k, v in msg.items() if v)
response = requests.post(queue, msg)
print 'Sent notification:', response
+ stat('notify:%s' % response.status_code)
if __name__ == '__main__':
View
1 requirements.txt
@@ -10,3 +10,4 @@ gunicorn==0.14.2
Flask-SQLAlchemy==0.15
SQLAlchemy==0.7.5
boto==2.3.0
+redis==2.4.11
View
47 static/app.js
@@ -10,6 +10,10 @@ function bakeCookies() {
return rv;
}
+function stat(name) {
+ $.post('/stat', {name: name});
+}
+
function getUserData() {
var promise = $.Deferred();
if (localStorage.getItem('userData')) {
@@ -81,6 +85,7 @@ function getHooks() {
}
function addHook(repo) {
+ stat('add-hook');
var url = repo.url + '/hooks?access_token=' + token,
data = {name: 'web',
active: true,
@@ -104,16 +109,18 @@ function addHook(repo) {
type: 'POST',
data: JSON.stringify(data),
dataType: 'json',
- contentType: 'application/json',
+ contentType: 'application/json'
}).done(function(d){ promise.resolve(d); });
}
}).fail(function() {
+ stat('hook-fail');
repoMap[repo.id].denied = true;
render();
});
}
function removeHook(repo) {
+ stat('remove-hook');
var hooks = getHooks(),
hook = hooks[repo.id];
$.post('/unsubscribe', {repo: repo.url}, function(d) {
@@ -143,6 +150,7 @@ function main() {
var id = $(this).parent().attr('data-id');
var hook = getHooks()[id];
if (hook) {
+ stat('test-hook');
$.post(hook + '/test?access_token=' + token);
}
}).on('click', 'button.disconnect', function() {
@@ -185,24 +193,29 @@ function step2() {
check.onsuccess = function() {
if (check.result.url) {
pushUrl = check.result.url;
+ stat('check-perm');
promise.resolve();
} else {
var request = notification.requestRemotePermission();
request.onsuccess = function() {
+ stat('request-perm');
pushUrl = request.result.url;
promise.resolve();
};
request.onerror = function() {
+ stat('request-error');
alert('error requesting remote permission');
};
}
};
check.onerror = function() {
+ stat('check-error');
alert('error checking remote permission');
- }
+ };
promise.done(function() {
- console.log('Your push URL:', pushUrl);
- $('#step-2 li').toggleClass('selected');
+ stat('push-url');
+ console.log('Your push URL:', pushUrl);
+ $('#step-2 li').toggleClass('selected');
});
return promise;
}
@@ -212,6 +225,7 @@ function step3() {
var promise = $.Deferred(),
cookies = bakeCookies();
if (cookies.username && cookies.access_token) {
+ stat('oauthd');
token = localStorage.token = cookies.access_token;
username = localStorage.username = cookies.username;
@@ -244,7 +258,7 @@ function step4() {
function render() {
var hooks = getHooks(),
allRepos = repos.concat(orgRepos);
- for (idx in allRepos) {
+ for (var idx in allRepos) {
allRepos[idx].hasHook = (allRepos[idx].id in hooks);
}
$('#repos').html(Mustache.render($('#repos-template').text(),
@@ -263,11 +277,32 @@ $(document).bind('step', function(e, step) {
$('.selected').each(function(idx, el) {
if (el.classList)
el.classList.remove('selected');
- })
+ });
$('#omgsvg,#nav,#steps').children(child).each(function(idx, el) {
if (el.classList)
el.classList.add('selected');
});
+ stat('step-' + step);
});
+function navTiming() {
+ if (performance.timing) {
+ setTimeout(function() {
+ var t = performance.timing,
+ start = t.navigationStart;
+ $.post('/nav-timing', {
+ dns: t.domainLookupEnd - start,
+ connect: t.connectEnd - start,
+ response: t.responseEnd - start,
+ interactive: t.domInteractive - start,
+ loaded: t.domContentLoadedEventEnd - start,
+ total: t.loadEventEnd - start
+ });
+ }, 3000);
+ } else {
+ stat('no-nav-timing');
+ }
+}
+
$(document).ready(main);
+$(document).ready(navTiming);

0 comments on commit 6a478b8

Please sign in to comment.
Something went wrong with that request. Please try again.