Merge pull request #3832 from alphagov/performance-platform-page

Add a page to show performance data
This commit is contained in:
Chris Hill-Scott
2021-03-15 09:20:47 +00:00
committed by GitHub
14 changed files with 484 additions and 16 deletions

View File

@@ -38,6 +38,7 @@ from app.config import configs
from app.extensions import antivirus_client, redis_client, zendesk_client
from app.formatters import (
convert_to_boolean,
format_billions,
format_date,
format_date_human,
format_date_normal,
@@ -100,6 +101,9 @@ from app.notify_client.letter_jobs_client import letter_jobs_client
from app.notify_client.notification_api_client import notification_api_client
from app.notify_client.org_invite_api_client import org_invite_api_client
from app.notify_client.organisations_api_client import organisations_client
from app.notify_client.performance_dashboard_api_client import (
performance_dashboard_api_client,
)
from app.notify_client.platform_stats_api_client import (
platform_stats_api_client,
)
@@ -185,6 +189,7 @@ def create_app(application):
notification_api_client,
org_invite_api_client,
organisations_client,
performance_dashboard_api_client,
platform_stats_api_client,
provider_client,
service_api_client,
@@ -523,6 +528,7 @@ def setup_event_handlers():
def add_template_filters(application):
for fn in [
format_billions,
format_datetime,
format_datetime_24h,
format_datetime_normal,

View File

@@ -515,3 +515,7 @@ def format_mobile_network(network):
if network in ('three', 'vodafone', 'o2'):
return network.capitalize()
return 'EE'
def format_billions(count):
return humanize.intword(count)

View File

@@ -27,6 +27,7 @@ from app.main.views import ( # noqa isort:skip
new_password,
notifications,
organisations,
performance,
platform_admin,
providers,
register,

View File

@@ -0,0 +1,41 @@
from datetime import datetime, timedelta
from itertools import groupby
from operator import itemgetter
from statistics import mean
from flask import render_template
from app import performance_dashboard_api_client, status_api_client
from app.main import main
@main.route("/performance")
def performance():
stats = performance_dashboard_api_client.get_performance_dashboard_stats(
start_date=(datetime.utcnow() - timedelta(days=7)).date(),
end_date=datetime.utcnow().date(),
)
stats['organisations_using_notify'] = sorted(
[
{
'organisation_name': organisation_name,
'count_of_live_services': len(list(group)),
}
for organisation_name, group in groupby(
stats['services_using_notify'],
itemgetter('organisation_name'),
)
],
key=itemgetter('organisation_name'),
)
stats['average_percentage_under_10_seconds'] = mean([
row['percentage_under_10_seconds']
for row in stats['processing_time']
] or [0])
stats['count_of_live_services_and_organisations'] = (
status_api_client.get_count_of_live_services_and_organisations()
)
return render_template(
'views/performance.html',
**stats
)

View File

@@ -246,6 +246,7 @@ class HeaderNavigation(Navigation):
'organisation_settings',
'organisation_preview_email_branding',
'organisation_preview_letter_branding',
'performance',
'privacy',
'public_agreement',
'public_download_agreement',
@@ -648,6 +649,7 @@ class MainNavigation(Navigation):
'organisation_preview_letter_branding',
'organisation_settings',
'organisations',
'performance',
'performance_platform_xlsx',
'platform_admin',
'platform_admin_list_complaints',
@@ -916,6 +918,7 @@ class CaseworkNavigation(Navigation):
'organisation_preview_letter_branding',
'organisation_settings',
'organisations',
'performance',
'performance_platform_xlsx',
'platform_admin_list_complaints',
'platform_admin_reports',
@@ -1235,6 +1238,7 @@ class OrgNavigation(Navigation):
'old_terms',
'old_using_notify',
'organisations',
'performance',
'performance_platform_xlsx',
'platform_admin',
'platform_admin_list_complaints',

View File

@@ -0,0 +1,22 @@
from app.notify_client import NotifyAdminAPIClient, cache
class PerformanceDashboardAPIClient(NotifyAdminAPIClient):
@cache.set('performance-stats-{start_date}-to-{end_date}')
def get_performance_dashboard_stats(
self,
*,
start_date,
end_date,
):
return self.get(
'/performance-dashboard',
params={
'start_date': str(start_date),
'end_date': str(end_date),
}
)
performance_dashboard_api_client = PerformanceDashboardAPIClient()

View File

@@ -172,7 +172,7 @@
"text": "System status"
},
{
"href": "https://www.gov.uk/performance/govuk-notify",
"href": url_for('main.performance'),
"text": "Performance data"
},
{

View File

@@ -58,7 +58,7 @@
<p class="govuk-body">We send messages through several different providers. If one provider fails, Notify switches to another so that your messages are not affected.</p>
<p class="govuk-body">Visit GOV.UK Performance to <a class="govuk-link govuk-link--no-visited-state" href="https://www.gov.uk/performance/govuk-notify">see how Notify is performing</a>.</p>
<p class="govuk-body">Visit our <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.performance') }}">performance data</a> page to see how Notify is performing.</p>
<h2 class="heading-medium" id="security">Security</h2>
<p class="govuk-body">Notify protects and manages data to meet the needs of government services.</p>

View File

@@ -0,0 +1,153 @@
{% extends "withoutnav_template.html" %}
{% from "components/big-number.html" import big_number %}
{% from "components/page-header.html" import page_header %}
{% from "components/table.html" import field, list_table %}
{% block per_page_title %}
Performance data
{% endblock %}
{% block maincolumn_content %}
<div class="govuk-grid-row govuk-!-margin-bottom-8">
<div class="govuk-grid-column-two-thirds">
{{ page_header('Performance data') }}
</div>
</div>
<h2 class="govuk-heading-m">
Messages sent since May 2016
</h2>
<div class="govuk-grid-row">
<div class="govuk-grid-column-one-half">
<div class="product-page-big-number">{{ total_notifications|format_billions }}</div>
total
</div>
<div class="govuk-grid-column-one-half">
<div class="govuk-grid-row govuk-!-padding-top-4">
<div class="govuk-grid-column-one-third">
{{ big_number(
email_notifications|format_billions,
label=email_notifications|message_count_noun('email'),
smallest=True,
) }}
</div>
<div class="govuk-grid-column-one-third">
{{ big_number(
sms_notifications|format_billions,
label=sms_notifications|message_count_noun('sms'),
smallest=True,
) }}
</div>
<div class="govuk-grid-column-one-third">
{{ big_number(
letter_notifications|format_billions,
label=letter_notifications|message_count_noun('letter'),
smallest=True,
) }}
</div>
</div>
</div>
</div>
<div class="dashboard-table">
{% call(item, row_number) list_table(
notifications_by_type,
caption='Messages sent since May 2016',
caption_visible=False,
field_headings=[
'Date',
99|message_count_noun('email')|capitalize,
99|message_count_noun('sms')|capitalize,
99|message_count_noun('letter')|capitalize,
],
empty_message='No data to show'
) %}
{% call field() %}
{{ item.date | format_date_normal }}
{% endcall %}
{% call field() %}
{{ item.emails|format_thousands }}
{% endcall %}
{% call field() %}
{{ item.sms|format_thousands }}
{% endcall %}
{% call field() %}
{{ item.letters|format_thousands }}
{% endcall %}
{% endcall %}
<p class="table-show-more-link">
Only showing the last {{ notifications_by_type|length }} days
</p>
</div>
<h2 class="govuk-heading-m">
Messages sent within 10 seconds
</h2>
<div class="govuk-grid-row">
<div class="govuk-grid-column-one-half">
{{ big_number(
'{:.1f}%'.format(average_percentage_under_10_seconds),
label='on average',
) }}
</div>
</div>
<div class="dashboard-table">
{% call(item, row_number) list_table(
processing_time | reverse,
caption='Messages sent within 10 seconds',
caption_visible=False,
field_headings=[
'Date', 'Percentage'
],
empty_message='No data to show'
) %}
{% call field() %}
{{ item.date | format_date_normal }}
{% endcall %}
{% call field() %}
{{ item.percentage_under_10_seconds }}%
{% endcall %}
{% endcall %}
<p class="table-show-more-link">
Only showing the last {{ processing_time|length }} days
</p>
</div>
<h2 class="govuk-heading-m">
Organisations using Notify
</h2>
<div class="govuk-grid-row bottom-gutter">
<div class="govuk-grid-column-one-half">
<span class="govuk-visually-hidden">There are</span>
<div class="product-page-big-number">{{ count_of_live_services_and_organisations.organisations|format_thousands }}</div>
organisations
</div>
<div class="govuk-grid-column-one-half">
<span class="govuk-visually-hidden">and</span>
<div class="product-page-big-number">{{ count_of_live_services_and_organisations.services|format_thousands }}</div>
services
<span class="govuk-visually-hidden">using Notify.</span>
</div>
</div>
<div class="dashboard-table">
{% call(item, row_number) list_table(
organisations_using_notify,
caption='Organisations using Notify',
caption_visible=False,
field_headings=[
'Organisation', 'Number of live services'
],
empty_message='No data to show'
) %}
{% call field() %}
{{ item.organisation_name }}
{% endcall %}
{% call field() %}
{{ item.count_of_live_services }}
{% endcall %}
{% endcall %}
</div>
{% endblock %}

View File

@@ -123,19 +123,20 @@
<h2>Whos using GOV.UK Notify</h2>
<div class="govuk-grid-row bottom-gutter">
<div class="govuk-grid-column-one-half">
<h3 class="govuk-visually-hidden">Services</h3>
<div class="product-page-big-number">{{ counts.services|format_thousands }}</div>
services
</div>
<div class="govuk-grid-column-one-half">
<h3 class="govuk-visually-hidden">Organisations</h3>
<span class="govuk-visually-hidden">There are</span>
<div class="product-page-big-number">{{ counts.organisations|format_thousands }}</div>
organisations
</div>
<div class="govuk-grid-column-one-half">
<span class="govuk-visually-hidden">and</span>
<div class="product-page-big-number">{{ counts.services|format_thousands }}</div>
services
<span class="govuk-visually-hidden">using Notify.</span>
</div>
</div>
<p class="govuk-body">
See the
<a class="govuk-link govuk-link--no-visited-state" href="https://www.gov.uk/performance/govuk-notify/government-services">list of services and organisations</a>.
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.performance') }}">list of services and organisations</a>.
</p>
</div>
</div>

View File

@@ -31,7 +31,7 @@
<ul class="list list-bullet">
<li>send all the messages you pass to us, as long as they meet our guidelines</li>
<li>
show how Notify is performing (through our <a class="govuk-link govuk-link--no-visited-state" href="https://www.gov.uk/performance/govuk-notify">performance</a> and <a class="govuk-link govuk-link--no-visited-state" href="https://status.notifications.service.gov.uk/">status</a> pages)
show how Notify is performing (through our <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.performance') }}">performance</a> and <a class="govuk-link govuk-link--no-visited-state" href="https://status.notifications.service.gov.uk/">status</a> pages)
</li>
<li>keep your data <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('.security') }}">secure</a></li>
<li>give you one months notice by email if we change our terms of use or delivery providers</li>

View File

@@ -34,14 +34,11 @@ def test_non_logged_in_user_can_see_homepage(
assert normalize_spaces(page.select_one('#whos-using-notify').text) == (
'Whos using GOV.UK Notify '
'Services '
'9,999 services '
'Organisations '
'111 organisations '
'There are 111 organisations and 9,999 services using Notify. '
'See the list of services and organisations.'
)
assert page.select_one('#whos-using-notify a')['href'] == (
'https://www.gov.uk/performance/govuk-notify/government-services'
assert page.select_one('#whos-using-notify a')['href'] == url_for(
'main.performance'
)

View File

@@ -0,0 +1,154 @@
import random
import uuid
from datetime import date
from freezegun import freeze_time
from tests.conftest import normalize_spaces
def _get_example_performance_data():
return {
"total_notifications": 1_789_000_000,
"email_notifications": 1_123_000_000,
"sms_notifications": 987_654_321,
"letter_notifications": 1_234_567,
"live_service_count": random.randrange(1, 1000),
"notifications_by_type": [
{
"date": "2021-02-21",
"emails": 1_234_567, "sms": 123_456, "letters": 123,
},
{
"date": "2021-02-22",
"emails": 1, "sms": 2, "letters": 3,
},
{
"date": "2021-02-23",
"emails": 1, "sms": 2, "letters": 3,
},
{
"date": "2021-02-24",
"emails": 1, "sms": 2, "letters": 3,
},
{
"date": "2021-02-25",
"emails": 1, "sms": 2, "letters": 3,
},
{
"date": "2021-02-26",
"emails": 1, "sms": 2, "letters": 3,
},
{
"date": "2021-02-27",
"emails": 1, "sms": 2, "letters": 3,
},
],
"processing_time": [
{
"date": "2021-02-21",
"percentage_under_10_seconds": 99.2
},
{
"date": "2021-02-22",
"percentage_under_10_seconds": 95.3
},
{
"date": "2021-02-23",
"percentage_under_10_seconds": 95.6
},
{
"date": "2021-02-24",
"percentage_under_10_seconds": 96.7
},
{
"date": "2021-02-25",
"percentage_under_10_seconds": 95.7
},
{
"date": "2021-02-26",
"percentage_under_10_seconds": 96.5
},
{
"date": "2021-02-27",
"percentage_under_10_seconds": 98.6
},
],
"services_using_notify": [
{
"organisation_id": uuid.uuid4(),
"organisation_name": "Department of Examples and Patterns",
"service_id": uuid.uuid4(),
"service_name": "Example service"
},
{
"organisation_id": uuid.uuid4(),
"organisation_name": "Department of Examples and Patterns",
"service_id": uuid.uuid4(),
"service_name": "Example service 2"
},
{
"organisation_id": uuid.uuid4(),
"organisation_name": "Department of One Service",
"service_id": uuid.uuid4(),
"service_name": "Example service 3"
},
],
}
@freeze_time('2021-01-01')
def test_should_render_performance_page(
mocker,
client_request,
mock_get_service_and_organisation_counts,
):
mock_get_performance_data = mocker.patch(
'app.performance_dashboard_api_client.get_performance_dashboard_stats',
return_value=_get_example_performance_data(),
)
page = client_request.get('main.performance')
mock_get_performance_data.assert_called_once_with(
start_date=date(2020, 12, 25),
end_date=date(2021, 1, 1),
)
assert normalize_spaces(page.select_one('main').text) == (
'Performance data '
''
'Messages sent since May 2016 '
'1.8 billion total '
'1.1 billion emails '
'987.7 million text messages '
'1.2 million letters '
''
'Messages sent since May 2016 '
'Date Emails Text messages Letters '
'21 February 2021 1,234,567 123,456 123 '
'22 February 2021 1 2 3 '
'23 February 2021 1 2 3 '
'24 February 2021 1 2 3 '
'25 February 2021 1 2 3 '
'26 February 2021 1 2 3 '
'27 February 2021 1 2 3 '
'Only showing the last 7 days '
''
'Messages sent within 10 seconds '
'96.8% on average '
'Messages sent within 10 seconds '
'Date Percentage '
'27 February 2021 98.6% '
'26 February 2021 96.5% '
'25 February 2021 95.7% '
'24 February 2021 96.7% '
'23 February 2021 95.6% '
'22 February 2021 95.3% '
'21 February 2021 99.2% '
'Only showing the last 7 days '
''
'Organisations using Notify '
'There are 111 organisations and 9,999 services using Notify. '
'Organisations using Notify '
'Organisation Number of live services '
'Department of Examples and Patterns 2 '
'Department of One Service 1'
)

View File

@@ -0,0 +1,85 @@
from datetime import date
from unittest.mock import call
from app.notify_client.performance_dashboard_api_client import (
PerformanceDashboardAPIClient,
)
def test_get_aggregate_platform_stats(mocker):
mocker.patch('app.extensions.RedisClient.get', return_value=None)
client = PerformanceDashboardAPIClient()
mock = mocker.patch.object(client, 'get', return_value={})
client.get_performance_dashboard_stats(
start_date=date(2021, 3, 1),
end_date=date(2021, 3, 31),
)
mock.assert_called_once_with('/performance-dashboard', params={
'start_date': '2021-03-01',
'end_date': '2021-03-31'
})
def test_sets_value_in_cache(mocker):
client = PerformanceDashboardAPIClient()
mock_redis_get = mocker.patch(
'app.extensions.RedisClient.get',
return_value=None,
)
mock_api_get = mocker.patch(
'app.notify_client.NotifyAdminAPIClient.get',
return_value={'data_from': 'api'},
)
mock_redis_set = mocker.patch(
'app.extensions.RedisClient.set',
)
assert client.get_performance_dashboard_stats(
start_date=date(2021, 1, 1),
end_date=date(2022, 2, 2),
) == {'data_from': 'api'}
assert mock_redis_get.call_args_list == [
call('performance-stats-2021-01-01-to-2022-02-02'),
]
assert mock_api_get.call_args_list == [
call('/performance-dashboard', params={
'start_date': '2021-01-01', 'end_date': '2022-02-02'
}),
]
assert mock_redis_set.call_args_list == [
call(
'performance-stats-2021-01-01-to-2022-02-02',
'{"data_from": "api"}',
ex=604800,
),
]
def test_returns_value_from_cache(mocker):
client = PerformanceDashboardAPIClient()
mock_redis_get = mocker.patch(
'app.extensions.RedisClient.get',
return_value=b'{"data_from": "cache"}',
)
mock_api_get = mocker.patch(
'app.notify_client.NotifyAdminAPIClient.get',
)
mock_redis_set = mocker.patch(
'app.extensions.RedisClient.set',
)
assert client.get_performance_dashboard_stats(
start_date=date(2021, 1, 1),
end_date=date(2022, 2, 2),
) == {'data_from': 'cache'}
assert mock_redis_get.call_args_list == [
call('performance-stats-2021-01-01-to-2022-02-02'),
]
assert mock_api_get.called is False
assert mock_redis_set.called is False