Added per-app totals API (bug 933861)

This commit is contained in:
Rob Hudson 2013-11-27 15:08:58 -08:00
Родитель f5f9039397
Коммит f0a25bb1c6
4 изменённых файлов: 157 добавлений и 31 удалений

Просмотреть файл

@ -543,3 +543,40 @@ The gross revenue of app purchases over time.
...
],
}
Totals Statistics
=================
Statistical information about metrics tracked. The information includes
the total, minimum and maximum, and other statistical calculations for
various metrics tracked.
Metrics
-------
Provided are the following metrics.
Per-app totals
~~~~~~~~~~~~~~
Statistical information about per-app metrics.
.. http:get:: /api/v1/stats/app/(int:id)|(string:slug)/totals/
**Response**:
.. code-block:: json
{
"installs": {
"max": 224.0,
"mean": 184.80000000000001,
"min": 132.0,
"sum_of_squares": 692112.0,
"std_deviation": 21.320412753978232,
"total": 3696.0,
"variance": 454.55999999999767
},
...
}

Просмотреть файл

@ -94,6 +94,13 @@ APP_STATS = {
'coerce': {'count': lambda d: '{0:.2f}'.format(d)},
},
}
APP_STATS_TOTAL = {
'installs': {
'metric': 'app_installs',
},
# TODO: Add more metrics here as needed. The total API will iterate over
# them and return statistical totals information on them all.
}
def _get_monolith_data(stat, start, end, interval, dimensions):
@ -209,6 +216,69 @@ class AppStats(CORSMixin, SlugOrIdMixin, ListAPIView):
dimensions))
class AppStatsTotal(CORSMixin, SlugOrIdMixin, ListAPIView):
authentication_classes = (RestOAuthAuthentication,
RestSharedSecretAuthentication)
cors_allowed_methods = ['get']
permission_classes = [AnyOf(AllowAppOwner,
GroupPermission('Stats', 'View'))]
queryset = Webapp.objects.all()
slug_field = 'app_slug'
def get(self, request, pk):
app = self.get_object()
try:
client = get_monolith_client()
except requests.ConnectionError as e:
log.info('Monolith connection error: {0}'.format(e))
raise ServiceUnavailable
# Note: We have to do this as separate requests so that if one fails
# the rest can still be returned.
data = {}
for metric, stat in APP_STATS_TOTAL.items():
data[metric] = {}
query = {
'query': {
'match_all': {}
},
'facets': {
metric: {
'statistical': {
'field': stat['metric']
},
'facet_filter': {
'term': {
'app-id': app.id
}
}
}
},
'size': 0
}
try:
resp = client.raw(query)
except ValueError as e:
log.info('Received value error from monolith client: %s' % e)
continue
for metric, facet in resp.get('facets', {}).items():
count = facet.get('count', 0)
# We filter out facets with count=0 to avoid returning things
# like `'max': u'-Infinity'`.
if count > 0:
for field in ('max', 'mean', 'min', 'std_deviation',
'sum_of_squares', 'total', 'variance'):
value = facet.get(field)
if value is not None:
data[metric][field] = value
return Response(data)
class TransactionAPI(CORSMixin, APIView):
"""
API to query by transaction ID.

Просмотреть файл

@ -15,20 +15,7 @@ from mkt.site.fixtures import fixture
from mkt.stats.api import APP_STATS, STATS, _get_monolith_data
@mock.patch('monolith.client.Client')
@mock.patch.object(settings, 'MONOLITH_SERVER', 'http://0.0.0.0:0')
class TestGlobalStatsResource(RestOAuth):
def setUp(self):
super(TestGlobalStatsResource, self).setUp()
self.grant_permission(self.profile, 'Stats:View')
self.data = {'start': '2013-04-01',
'end': '2013-04-15',
'interval': 'day'}
def url(self, metric=None):
metric = metric or STATS.keys()[0]
return reverse('global_stats', kwargs={'metric': metric})
class StatsAPITestMixin(object):
def test_cors(self, mocked):
res = self.client.get(self.url(), data=self.data)
@ -46,6 +33,22 @@ class TestGlobalStatsResource(RestOAuth):
res = self.anon.get(self.url())
eq_(res.status_code, 403)
@mock.patch('monolith.client.Client')
@mock.patch.object(settings, 'MONOLITH_SERVER', 'http://0.0.0.0:0')
class TestGlobalStatsResource(RestOAuth, StatsAPITestMixin):
def setUp(self):
super(TestGlobalStatsResource, self).setUp()
self.grant_permission(self.profile, 'Stats:View')
self.data = {'start': '2013-04-01',
'end': '2013-04-15',
'interval': 'day'}
def url(self, metric=None):
metric = metric or STATS.keys()[0]
return reverse('global_stats', kwargs={'metric': metric})
def test_bad_metric(self, mocked):
res = self.client.get(self.url('foo'))
eq_(res.status_code, 404)
@ -114,7 +117,7 @@ class TestGlobalStatsResource(RestOAuth):
@mock.patch('monolith.client.Client')
@mock.patch.object(settings, 'MONOLITH_SERVER', 'http://0.0.0.0:0')
class TestAppStatsResource(RestOAuth):
class TestAppStatsResource(RestOAuth, StatsAPITestMixin):
fixtures = fixture('user_2519')
def setUp(self):
@ -129,22 +132,6 @@ class TestAppStatsResource(RestOAuth):
metric = metric or APP_STATS.keys()[0]
return reverse('app_stats', kwargs={'pk': pk, 'metric': metric})
def test_cors(self, mocked):
res = self.client.get(self.url(), data=self.data)
self.assertCORS(res, 'get')
def test_verbs(self, mocked):
self._allowed_verbs(self.url(), ['get'])
def test_monolith_down(self, mocked):
mocked.side_effect = requests.ConnectionError
res = self.client.get(self.url(), data=self.data)
eq_(res.status_code, 503)
def test_anon(self, mocked):
res = self.anon.get(self.url())
eq_(res.status_code, 403)
def test_owner(self, mocked):
res = self.client.get(self.url(), data=self.data)
eq_(res.status_code, 200)
@ -171,6 +158,36 @@ class TestAppStatsResource(RestOAuth):
eq_(data['detail'][f], ['This field is required.'])
@mock.patch('monolith.client.Client')
@mock.patch.object(settings, 'MONOLITH_SERVER', 'http://0.0.0.0:0')
class TestAppStatsTotalResource(RestOAuth, StatsAPITestMixin):
fixtures = fixture('user_2519')
def setUp(self):
super(TestAppStatsTotalResource, self).setUp()
self.app = amo.tests.app_factory(status=amo.STATUS_PUBLIC)
self.app.addonuser_set.create(user=self.user.get_profile())
self.data = None # For the mixin tests.
def url(self, pk=None, metric=None):
pk = pk or self.app.pk
return reverse('app_stats_total', kwargs={'pk': pk})
def test_owner(self, mocked):
res = self.client.get(self.url())
eq_(res.status_code, 200)
def test_perms(self, mocked):
self.app.addonuser_set.all().delete()
self.grant_permission(self.profile, 'Stats:View')
res = self.client.get(self.url())
eq_(res.status_code, 200)
def test_bad_app(self, mocked):
res = self.client.get(self.url(pk=99999999))
eq_(res.status_code, 404)
class TestTransactionResource(RestOAuth):
fixtures = fixture('prices', 'user_2519', 'webapp_337141')

Просмотреть файл

@ -14,6 +14,8 @@ series = dict((type, '%s-%s' % (type, series_re)) for type in views.SERIES)
stats_api_patterns = patterns('',
url(r'^stats/global/(?P<metric>[^/]+)/$', api.GlobalStats.as_view(),
name='global_stats'),
url(r'^stats/app/(?P<pk>[^/<>"\']+)/totals/$',
api.AppStatsTotal.as_view(), name='app_stats_total'),
url(r'^stats/app/(?P<pk>[^/<>"\']+)/(?P<metric>[^/]+)/$',
api.AppStats.as_view(), name='app_stats'),
)