Skip to content
This repository has been archived by the owner on Dec 15, 2018. It is now read-only.

Commit

Permalink
Add analytics_notifier to post build results.
Browse files Browse the repository at this point in the history
Summary:
This adds an optional mechanism to post build results as a JSON
dictionary to an HTTP address to allow builds to be sent in realtime
to external analytics services.

Test Plan: Light unit.

Reviewers: jukka

Reviewed By: jukka

Subscribers: changesbot, wwu, jukka

Differential Revision: https://tails.corp.dropbox.com/D90885
  • Loading branch information
kylec1 committed Mar 11, 2015
1 parent 3191da9 commit 1d139a5
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 0 deletions.
1 change: 1 addition & 0 deletions changes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def create_app(_read_config=True, **config):
('changes.listeners.hipchat.build_finished_handler', 'build.finished'),
('changes.listeners.build_revision.revision_created_handler', 'revision.created'),
('changes.listeners.phabricator_listener.build_finished_handler', 'build.finished'),
('changes.listeners.analytics_notifier.build_finished_handler', 'build.finished'),
)

# restrict outbound notifications to the given domains
Expand Down
67 changes: 67 additions & 0 deletions changes/listeners/analytics_notifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Posts JSON with basic information for each completed build to the URL specified
in the config variable ANALYTICS_POST_URL, if that value is set.
The data posted to ANALYTICS_POST_URL will be a JSON array of objects, with each
object representing a completed build.
"""

import logging
import json
import requests
from datetime import datetime

from flask import current_app

from changes.models import Build

logger = logging.getLogger('analytics_notifier')


def _datetime_to_timestamp(dt):
"""Convert a datetime to unix epoch time in seconds."""
return int((dt - datetime.utcfromtimestamp(0)).total_seconds())


def build_finished_handler(build_id, **kwargs):
url = current_app.config.get('ANALYTICS_POST_URL')
if not url:
return
build = Build.query.get(build_id)
if build is None:
return

def maybe_ts(dt):
if dt:
return _datetime_to_timestamp(dt)
return None

data = {
'build_id': build.id.hex,
'result': unicode(build.result),
'project_slug': build.project.slug,
'is_commit': bool(build.source.is_commit()),
'label': build.label,
'number': build.number,
'duration': build.duration,
'target': build.target,
'date_created': maybe_ts(build.date_created),
'date_started': maybe_ts(build.date_started),
'date_finished': maybe_ts(build.date_finished),
}
if build.author:
data['author'] = build.author.email

post_build_data(url, data)


def post_build_data(url, data):
try:
# NB: We send an array of JSON objects rather than a single object
# so the interface doesn't need to change if we later want to do batch
# posting.
resp = requests.post(url, data=json.dumps([data]))
resp.raise_for_status()
# Should probably retry here so that transient failures don't result in
# missing data.
except Exception:
logger.exception("Failed to post to Analytics")
2 changes: 2 additions & 0 deletions changes/models/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ class Build(db.Model):
source_id = Column(GUID, ForeignKey('source.id', ondelete="CASCADE"))
author_id = Column(GUID, ForeignKey('author.id', ondelete="CASCADE"))
cause = Column(EnumType(Cause), nullable=False, default=Cause.unknown)
# label is a short description, typically from the title of the change that triggered the build.
label = Column(String(128), nullable=False)
# short indicator of what is being built, typically the sha or the Phabricator revision ID like 'D90885'.
target = Column(String(128))
tags = Column(ARRAY(String(16)), nullable=True)
status = Column(EnumType(Status), nullable=False, default=Status.unknown)
Expand Down
62 changes: 62 additions & 0 deletions tests/changes/listeners/test_analytics_notifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from __future__ import absolute_import

from datetime import datetime

from flask import current_app
import mock

from changes.constants import Result
from changes.testutils import TestCase
from changes.listeners.analytics_notifier import build_finished_handler


class AnalyticsNotifierTest(TestCase):

def setUp(self):
super(AnalyticsNotifierTest, self).setUp()

def _set_config_url(self, url):
current_app.config['ANALYTICS_POST_URL'] = url

@mock.patch('changes.listeners.analytics_notifier.post_build_data')
def test_no_url(self, post_fn):
self._set_config_url(None)
project = self.create_project(name='test', slug='test')
build = self.create_build(project, result=Result.failed)
build_finished_handler(build_id=build.id.hex)
self.assertEquals(post_fn.call_count, 0)

@mock.patch('changes.listeners.analytics_notifier.post_build_data')
def test_failed_build(self, post_fn):
URL = "https://analytics.example.com/report?source=changes"
self._set_config_url(URL)
project = self.create_project(name='test', slug='project-slug')
self.assertEquals(post_fn.call_count, 0)
duration = 1234
created = 1424998888
started = created + 10
finished = started + duration

def ts_to_datetime(ts):
return datetime.utcfromtimestamp(ts)

build = self.create_build(project, result=Result.failed, target='D1',
label='Some sweet diff', duration=duration,
date_created=ts_to_datetime(created), date_started=ts_to_datetime(started),
date_finished=ts_to_datetime(finished))
build_finished_handler(build_id=build.id.hex)

expected_data = {
'build_id': build.id.hex,
'number': 1,
'target': 'D1',
'project_slug': 'project-slug',
'result': 'Failed',
'label': 'Some sweet diff',
'is_commit': True,
'duration': 1234,
'date_created': created,
'date_started': started,
'date_finished': finished,
}
post_fn.assert_called_once_with(URL, expected_data)

0 comments on commit 1d139a5

Please sign in to comment.