diff --git a/app/__init__.py b/app/__init__.py
index 958a324f8..fc9ad0a2d 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -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,
diff --git a/app/formatters.py b/app/formatters.py
index 2f9223a44..612e45113 100644
--- a/app/formatters.py
+++ b/app/formatters.py
@@ -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)
diff --git a/app/main/__init__.py b/app/main/__init__.py
index d5620b705..8e0542676 100644
--- a/app/main/__init__.py
+++ b/app/main/__init__.py
@@ -27,6 +27,7 @@ from app.main.views import ( # noqa isort:skip
new_password,
notifications,
organisations,
+ performance,
platform_admin,
providers,
register,
diff --git a/app/main/views/performance.py b/app/main/views/performance.py
new file mode 100644
index 000000000..86f3b8bc9
--- /dev/null
+++ b/app/main/views/performance.py
@@ -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
+ )
diff --git a/app/navigation.py b/app/navigation.py
index 58027c8c8..a3bd4c29f 100644
--- a/app/navigation.py
+++ b/app/navigation.py
@@ -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',
diff --git a/app/notify_client/performance_dashboard_api_client.py b/app/notify_client/performance_dashboard_api_client.py
new file mode 100644
index 000000000..c08734aff
--- /dev/null
+++ b/app/notify_client/performance_dashboard_api_client.py
@@ -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()
diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html
index a492c9716..f8de636a8 100644
--- a/app/templates/admin_template.html
+++ b/app/templates/admin_template.html
@@ -172,7 +172,7 @@
"text": "System status"
},
{
- "href": "https://www.gov.uk/performance/govuk-notify",
+ "href": url_for('main.performance'),
"text": "Performance data"
},
{
diff --git a/app/templates/views/features.html b/app/templates/views/features.html
index 21dcd8ee6..29c84c843 100644
--- a/app/templates/views/features.html
+++ b/app/templates/views/features.html
@@ -58,7 +58,7 @@
We send messages through several different providers. If one provider fails, Notify switches to another so that your messages are not affected.
-Visit GOV.UK Performance to see how Notify is performing.
+Visit our performance data page to see how Notify is performing.
Security
Notify protects and manages data to meet the needs of government services.
diff --git a/app/templates/views/performance.html b/app/templates/views/performance.html
new file mode 100644
index 000000000..d8ad5e34a
--- /dev/null
+++ b/app/templates/views/performance.html
@@ -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 %}
+
+
+
+ {{ page_header('Performance data') }}
+
+
+
+
+ Messages sent since May 2016
+
+
+
+
+
{{ total_notifications|format_billions }}
+ total
+
+
+
+
+ {{ big_number(
+ email_notifications|format_billions,
+ label=email_notifications|message_count_noun('email'),
+ smallest=True,
+ ) }}
+
+
+ {{ big_number(
+ sms_notifications|format_billions,
+ label=sms_notifications|message_count_noun('sms'),
+ smallest=True,
+ ) }}
+
+
+ {{ big_number(
+ letter_notifications|format_billions,
+ label=letter_notifications|message_count_noun('letter'),
+ smallest=True,
+ ) }}
+
+
+
+
+
+
+ {% 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 %}
+
+ Only showing the last {{ notifications_by_type|length }} days
+
+
+
+
+ Messages sent within 10 seconds
+
+
+
+ {{ big_number(
+ '{:.1f}%'.format(average_percentage_under_10_seconds),
+ label='on average',
+ ) }}
+
+
+
+ {% 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 %}
+
+ Only showing the last {{ processing_time|length }} days
+
+
+
+
+ Organisations using Notify
+
+
+
+
There are
+
{{ count_of_live_services_and_organisations.organisations|format_thousands }}
+ organisations
+
+
+
and
+
{{ count_of_live_services_and_organisations.services|format_thousands }}
+ services
+
using Notify.
+
+
+
+ {% 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 %}
+
+
+{% endblock %}
diff --git a/app/templates/views/signedout.html b/app/templates/views/signedout.html
index 4f6e6467a..c90321418 100644
--- a/app/templates/views/signedout.html
+++ b/app/templates/views/signedout.html
@@ -123,19 +123,20 @@
Who’s using GOV.UK Notify
-
Services
-
{{ counts.services|format_thousands }}
- services
-
-
-
Organisations
+
There are
{{ counts.organisations|format_thousands }}
organisations
+
+
and
+
{{ counts.services|format_thousands }}
+ services
+
using Notify.
+
See the
- list of services and organisations.
+ list of services and organisations.
diff --git a/app/templates/views/terms-of-use.html b/app/templates/views/terms-of-use.html
index 358b776a6..60e0ab6fc 100644
--- a/app/templates/views/terms-of-use.html
+++ b/app/templates/views/terms-of-use.html
@@ -31,7 +31,7 @@
- send all the messages you pass to us, as long as they meet our guidelines
-
- show how Notify is performing (through our performance and status pages)
+ show how Notify is performing (through our performance and status pages)
- keep your data secure
- give you one month’s notice by email if we change our terms of use or delivery providers
diff --git a/tests/app/main/views/test_index.py b/tests/app/main/views/test_index.py
index c4b0dcc5d..998ef65d5 100644
--- a/tests/app/main/views/test_index.py
+++ b/tests/app/main/views/test_index.py
@@ -34,14 +34,11 @@ def test_non_logged_in_user_can_see_homepage(
assert normalize_spaces(page.select_one('#whos-using-notify').text) == (
'Who’s 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'
)
diff --git a/tests/app/main/views/test_performance.py b/tests/app/main/views/test_performance.py
new file mode 100644
index 000000000..17519a7a9
--- /dev/null
+++ b/tests/app/main/views/test_performance.py
@@ -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'
+ )
diff --git a/tests/app/notify_client/test_performance_platform_api_client.py b/tests/app/notify_client/test_performance_platform_api_client.py
new file mode 100644
index 000000000..dbdfbd6a1
--- /dev/null
+++ b/tests/app/notify_client/test_performance_platform_api_client.py
@@ -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