Merge pull request #1919 from alphagov/new-platform-stats-endpoint

New endpoint for the updated platform admin stats page
This commit is contained in:
Katie Smith
2018-07-02 15:37:24 +01:00
committed by GitHub
11 changed files with 271 additions and 7 deletions

View File

@@ -113,6 +113,7 @@ def register_blueprint(application):
from app.organisation.rest import organisation_blueprint from app.organisation.rest import organisation_blueprint
from app.organisation.invite_rest import organisation_invite_blueprint from app.organisation.invite_rest import organisation_invite_blueprint
from app.complaint.complaint_rest import complaint_blueprint from app.complaint.complaint_rest import complaint_blueprint
from app.platform_stats.rest import platform_stats_blueprint
service_blueprint.before_request(requires_admin_auth) service_blueprint.before_request(requires_admin_auth)
application.register_blueprint(service_blueprint, url_prefix='/service') application.register_blueprint(service_blueprint, url_prefix='/service')
@@ -192,6 +193,9 @@ def register_blueprint(application):
complaint_blueprint.before_request(requires_admin_auth) complaint_blueprint.before_request(requires_admin_auth)
application.register_blueprint(complaint_blueprint) application.register_blueprint(complaint_blueprint)
platform_stats_blueprint.before_request(requires_admin_auth)
application.register_blueprint(platform_stats_blueprint, url_prefix='/platform-stats')
def register_v2_blueprints(application): def register_v2_blueprints(application):
from app.v2.inbound_sms.get_inbound_sms import v2_inbound_sms_blueprint as get_inbound_sms from app.v2.inbound_sms.get_inbound_sms import v2_inbound_sms_blueprint as get_inbound_sms

View File

@@ -609,3 +609,31 @@ def guess_notification_type(search_term):
return EMAIL_TYPE return EMAIL_TYPE
else: else:
return SMS_TYPE return SMS_TYPE
@statsd(namespace='dao')
def fetch_aggregate_stats_by_date_range_for_all_services(start_date, end_date):
start_date = get_london_midnight_in_utc(start_date)
end_date = get_london_midnight_in_utc(end_date + timedelta(days=1))
table = NotificationHistory
if end_date >= datetime.utcnow() - timedelta(days=7):
table = Notification
query = db.session.query(
table.notification_type,
table.status,
table.key_type,
func.count(table.id).label('count')
).filter(
table.created_at >= start_date,
table.created_at < end_date
).group_by(
table.notification_type,
table.key_type,
table.status
).order_by(
table.notification_type,
)
return query.all()

View File

View File

@@ -0,0 +1,10 @@
platform_stats_request = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "platform stats request schema",
"type": "object",
"title": "Platform stats request",
"properties": {
"start_date": {"type": ["string", "null"], "format": "date"},
"end_date": {"type": ["string", "null"], "format": "date"},
}
}

View File

@@ -0,0 +1,29 @@
from datetime import datetime
from flask import Blueprint, jsonify, request
from app.dao.notifications_dao import fetch_aggregate_stats_by_date_range_for_all_services
from app.errors import register_errors
from app.platform_stats.platform_stats_schema import platform_stats_request
from app.service.statistics import format_admin_stats
from app.schema_validation import validate
platform_stats_blueprint = Blueprint('platform_stats', __name__)
register_errors(platform_stats_blueprint)
@platform_stats_blueprint.route('')
def get_platform_stats():
if request.args:
validate(request.args, platform_stats_request)
# If start and end date are not set, we are expecting today's stats.
today = str(datetime.utcnow().date())
start_date = datetime.strptime(request.args.get('start_date', today), '%Y-%m-%d').date()
end_date = datetime.strptime(request.args.get('end_date', today), '%Y-%m-%d').date()
data = fetch_aggregate_stats_by_date_range_for_all_services(start_date=start_date, end_date=end_date)
stats = format_admin_stats(data)
return jsonify(stats)

View File

@@ -14,6 +14,37 @@ def format_statistics(statistics):
return counts return counts
def format_admin_stats(statistics):
counts = create_stats_dict()
for row in statistics:
if row.key_type == 'test':
counts[row.notification_type]['test-key'] += row.count
else:
counts[row.notification_type]['total'] += row.count
if row.status in ('technical-failure', 'permanent-failure', 'temporary-failure', 'virus-scan-failed'):
counts[row.notification_type]['failures'][row.status] += row.count
return counts
def create_stats_dict():
stats_dict = {}
for template in TEMPLATE_TYPES:
stats_dict[template] = {}
for status in ('total', 'test-key'):
stats_dict[template][status] = 0
stats_dict[template]['failures'] = {
'technical-failure': 0,
'permanent-failure': 0,
'temporary-failure': 0,
'virus-scan-failed': 0,
}
return stats_dict
def format_monthly_template_notification_stats(year, rows): def format_monthly_template_notification_stats(year, rows):
stats = { stats = {
datetime.strftime(date, '%Y-%m'): {} datetime.strftime(date, '%Y-%m'): {}

View File

@@ -33,7 +33,8 @@ from app.dao.notifications_dao import (
dao_get_notification_by_reference, dao_get_notification_by_reference,
dao_get_notifications_by_references, dao_get_notifications_by_references,
dao_get_notification_history_by_reference, dao_get_notification_history_by_reference,
notifications_not_yet_sent notifications_not_yet_sent,
fetch_aggregate_stats_by_date_range_for_all_services,
) )
from app.dao.services_dao import dao_update_service from app.dao.services_dao import dao_update_service
from app.models import ( from app.models import (
@@ -1933,3 +1934,65 @@ def test_notifications_not_yet_sent_return_no_rows(sample_service, notification_
results = notifications_not_yet_sent(older_than, notification_type) results = notifications_not_yet_sent(older_than, notification_type)
assert len(results) == 0 assert len(results) == 0
def test_fetch_aggregate_stats_by_date_range_for_all_services_returns_empty_list_when_no_stats(notify_db_session):
start_date = date(2018, 1, 1)
end_date = date(2018, 1, 5)
result = fetch_aggregate_stats_by_date_range_for_all_services(start_date, end_date)
assert result == []
@freeze_time('2018-01-08')
def test_fetch_aggregate_stats_by_date_range_for_all_services_groups_stats(
sample_template,
sample_email_template,
sample_letter_template,
):
today = datetime.now().date()
for i in range(3):
create_notification(template=sample_email_template, status='permanent-failure',
created_at=today)
create_notification(template=sample_email_template, status='sent', created_at=today)
create_notification(template=sample_template, status='sent', created_at=today)
create_notification(template=sample_template, status='sent', created_at=today,
key_type=KEY_TYPE_TEAM)
create_notification(template=sample_letter_template, status='virus-scan-failed',
created_at=today)
result = fetch_aggregate_stats_by_date_range_for_all_services(today, today)
assert len(result) == 5
assert result[0] == ('email', 'permanent-failure', 'normal', 3)
assert result[1] == ('email', 'sent', 'normal', 1)
assert result[2] == ('sms', 'sent', 'normal', 1)
assert result[3] == ('sms', 'sent', 'team', 1)
assert result[4] == ('letter', 'virus-scan-failed', 'normal', 1)
def test_fetch_aggregate_stats_by_date_range_for_all_services_uses_bst_date(sample_template):
query_day = datetime(2018, 6, 5).date()
create_notification(sample_template, status='sent', created_at=datetime(2018, 6, 4, 23, 59))
create_notification(sample_template, status='created', created_at=datetime(2018, 6, 5, 23, 00))
result = fetch_aggregate_stats_by_date_range_for_all_services(query_day, query_day)
assert len(result) == 1
assert result[0].status == 'sent'
@freeze_time('2018-01-08T12:00:00')
def test_fetch_aggregate_stats_by_date_range_for_all_services_gets_test_notifications_when_start_date_over_7_days_ago(
sample_template
):
ten_days_ago = datetime.utcnow().date() - timedelta(days=10)
today = datetime.utcnow().date()
create_notification(sample_template, key_type=KEY_TYPE_TEST, created_at=datetime.utcnow())
result = fetch_aggregate_stats_by_date_range_for_all_services(ten_days_ago, today)
assert len(result) == 1

View File

@@ -674,7 +674,7 @@ def test_fetch_monthly_historical_stats_separates_months(notify_db, notify_db_se
result = dao_fetch_monthly_historical_stats_for_service(sample_template.service_id, 2016) result = dao_fetch_monthly_historical_stats_for_service(sample_template.service_id, 2016)
for date, status, count in ( for day, status, count in (
('2016-04', 'sending', 0), ('2016-04', 'sending', 0),
('2016-04', 'delivered', 0), ('2016-04', 'delivered', 0),
('2016-04', 'pending', 0), ('2016-04', 'pending', 0),
@@ -692,9 +692,9 @@ def test_fetch_monthly_historical_stats_separates_months(notify_db, notify_db_se
('2017-03', 'created', 2), ('2017-03', 'created', 2),
): ):
assert result[date]['sms'][status] == count assert result[day]['sms'][status] == count
assert result[date]['email'][status] == 0 assert result[day]['email'][status] == 0
assert result[date]['letter'][status] == 0 assert result[day]['letter'][status] == 0
assert result.keys() == { assert result.keys() == {
'2016-04', '2016-05', '2016-06', '2016-04', '2016-05', '2016-06',

View File

View File

@@ -0,0 +1,37 @@
from datetime import date, datetime
from freezegun import freeze_time
@freeze_time('2018-06-01')
def test_get_platform_stats_uses_todays_date_if_no_start_or_end_date_is_provided(admin_request, mocker):
today = datetime.now().date()
dao_mock = mocker.patch('app.platform_stats.rest.fetch_aggregate_stats_by_date_range_for_all_services')
mocker.patch('app.service.rest.statistics.format_statistics')
admin_request.get('platform_stats.get_platform_stats')
dao_mock.assert_called_once_with(start_date=today, end_date=today)
def test_get_platform_stats_can_filter_by_date(admin_request, mocker):
start_date = date(2017, 1, 1)
end_date = date(2018, 1, 1)
dao_mock = mocker.patch('app.platform_stats.rest.fetch_aggregate_stats_by_date_range_for_all_services')
mocker.patch('app.service.rest.statistics.format_statistics')
admin_request.get('platform_stats.get_platform_stats', start_date=start_date, end_date=end_date)
dao_mock.assert_called_once_with(start_date=start_date, end_date=end_date)
def test_get_platform_stats_validates_the_date(admin_request):
start_date = '1234-56-78'
response = admin_request.get(
'platform_stats.get_platform_stats', start_date=start_date,
_expected_status=400
)
assert response['errors'][0]['message'] == 'start_date time data {} does not match format %Y-%m-%d'.format(
start_date)

View File

@@ -3,11 +3,14 @@ import collections
import pytest import pytest
from app.service.statistics import ( from app.service.statistics import (
format_admin_stats,
format_statistics, format_statistics,
create_stats_dict,
create_zeroed_stats_dicts, create_zeroed_stats_dicts,
) )
StatsRow = collections.namedtuple('row', ('notification_type', 'status', 'count')) StatsRow = collections.namedtuple('row', ('notification_type', 'status', 'count'))
NewStatsRow = collections.namedtuple('row', ('notification_type', 'status', 'key_type', 'count'))
# email_counts and sms_counts are 3-tuple of requested, delivered, failed # email_counts and sms_counts are 3-tuple of requested, delivered, failed
@@ -65,5 +68,64 @@ def test_create_zeroed_stats_dicts():
} }
def _stats(requested, delivered, failed): def test_create_stats_dict():
return {'requested': requested, 'delivered': delivered, 'failed': failed} assert create_stats_dict() == {
'sms': {'total': 0,
'test-key': 0,
'failures': {'technical-failure': 0,
'permanent-failure': 0,
'temporary-failure': 0,
'virus-scan-failed': 0}},
'email': {'total': 0,
'test-key': 0,
'failures': {'technical-failure': 0,
'permanent-failure': 0,
'temporary-failure': 0,
'virus-scan-failed': 0}},
'letter': {'total': 0,
'test-key': 0,
'failures': {'technical-failure': 0,
'permanent-failure': 0,
'temporary-failure': 0,
'virus-scan-failed': 0}}
}
def test_format_admin_stats_only_includes_test_key_notifications_in_test_key_section():
rows = [
NewStatsRow('email', 'technical-failure', 'test', 3),
NewStatsRow('sms', 'permanent-failure', 'test', 4),
NewStatsRow('letter', 'virus-scan-failed', 'test', 5),
]
stats_dict = format_admin_stats(rows)
assert stats_dict['email']['total'] == 0
assert stats_dict['email']['failures']['technical-failure'] == 0
assert stats_dict['email']['test-key'] == 3
assert stats_dict['sms']['total'] == 0
assert stats_dict['sms']['failures']['permanent-failure'] == 0
assert stats_dict['sms']['test-key'] == 4
assert stats_dict['letter']['total'] == 0
assert stats_dict['letter']['failures']['virus-scan-failed'] == 0
assert stats_dict['letter']['test-key'] == 5
def test_format_admin_stats_counts_non_test_key_notifications_correctly():
rows = [
NewStatsRow('email', 'technical-failure', 'normal', 1),
NewStatsRow('email', 'created', 'team', 3),
NewStatsRow('sms', 'temporary-failure', 'normal', 6),
NewStatsRow('sms', 'sent', 'normal', 2),
NewStatsRow('letter', 'pending-virus-check', 'normal', 1),
]
stats_dict = format_admin_stats(rows)
assert stats_dict['email']['total'] == 4
assert stats_dict['email']['failures']['technical-failure'] == 1
assert stats_dict['sms']['total'] == 8
assert stats_dict['sms']['failures']['permanent-failure'] == 0
assert stats_dict['letter']['total'] == 1