diff --git a/allianceauth/analytics/middleware.py b/allianceauth/analytics/middleware.py index ea636b6c..4dd93685 100644 --- a/allianceauth/analytics/middleware.py +++ b/allianceauth/analytics/middleware.py @@ -1,5 +1,6 @@ from bs4 import BeautifulSoup +from django.conf import settings from django.utils.deprecation import MiddlewareMixin from .models import AnalyticsTokens, AnalyticsIdentifier from .tasks import send_ga_tracking_web_view @@ -10,6 +11,8 @@ import re class AnalyticsMiddleware(MiddlewareMixin): def process_response(self, request, response): """Django Middleware: Process Page Views and creates Analytics Celery Tasks""" + if getattr(settings, "ANALYTICS_DISABLED", False): + return response analyticstokens = AnalyticsTokens.objects.all() client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex try: diff --git a/allianceauth/analytics/signals.py b/allianceauth/analytics/signals.py index b3943220..91565c5e 100644 --- a/allianceauth/analytics/signals.py +++ b/allianceauth/analytics/signals.py @@ -1,7 +1,8 @@ -from allianceauth.analytics.tasks import analytics_event -from celery.signals import task_failure, task_success - import logging +from celery.signals import task_failure, task_success +from django.conf import settings +from allianceauth.analytics.tasks import analytics_event + logger = logging.getLogger(__name__) @@ -11,6 +12,8 @@ def process_failure_signal( sender, task_id, signal, args, kwargs, einfo, **kw): logger.debug("Celery task_failure signal %s" % sender.__class__.__name__) + if getattr(settings, "ANALYTICS_DISABLED", False): + return category = sender.__module__ @@ -30,6 +33,8 @@ def process_failure_signal( @task_success.connect def celery_success_signal(sender, result=None, **kw): logger.debug("Celery task_success signal %s" % sender.__class__.__name__) + if getattr(settings, "ANALYTICS_DISABLED", False): + return category = sender.__module__ diff --git a/allianceauth/analytics/tasks.py b/allianceauth/analytics/tasks.py index 7fe22477..c8708b75 100644 --- a/allianceauth/analytics/tasks.py +++ b/allianceauth/analytics/tasks.py @@ -40,13 +40,12 @@ def analytics_event(category: str, Send a Google Analytics Event for each token stored Includes check for if its enabled/disabled - Parameters - ------- - `category` (str): Celery Namespace - `action` (str): Task Name - `label` (str): Optional, Task Success/Exception - `value` (int): Optional, If bulk, Query size, can be a binary True/False - `event_type` (str): Optional, Celery or Stats only, Default to Celery + Args: + `category` (str): Celery Namespace + `action` (str): Task Name + `label` (str): Optional, Task Success/Exception + `value` (int): Optional, If bulk, Query size, can be a binary True/False + `event_type` (str): Optional, Celery or Stats only, Default to Celery """ analyticstokens = AnalyticsTokens.objects.all() client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex @@ -73,7 +72,8 @@ def analytics_event(category: str, def analytics_daily_stats(): """Celery Task: Do not call directly - Gathers a series of daily statistics and sends analytics events containing them""" + Gathers a series of daily statistics and sends analytics events containing them + """ users = install_stat_users() tokens = install_stat_tokens() addons = install_stat_addons() diff --git a/allianceauth/analytics/tests/test_integration.py b/allianceauth/analytics/tests/test_integration.py new file mode 100644 index 00000000..0fbe445b --- /dev/null +++ b/allianceauth/analytics/tests/test_integration.py @@ -0,0 +1,108 @@ +from unittest.mock import patch +from urllib.parse import parse_qs + +import requests_mock + +from django.test import TestCase, override_settings + +from allianceauth.analytics.tasks import ANALYTICS_URL +from allianceauth.eveonline.tasks import update_character +from allianceauth.tests.auth_utils import AuthUtils + + +@override_settings(CELERY_ALWAYS_EAGER=True) +@requests_mock.mock() +class TestAnalyticsForViews(TestCase): + @override_settings(ANALYTICS_DISABLED=False) + def test_should_run_analytics(self, requests_mocker): + # given + requests_mocker.post(ANALYTICS_URL) + user = AuthUtils.create_user("Bruce Wayne") + self.client.force_login(user) + # when + response = self.client.get("/dashboard/") + # then + self.assertEqual(response.status_code, 200) + self.assertTrue(requests_mocker.called) + + @override_settings(ANALYTICS_DISABLED=True) + def test_should_not_run_analytics(self, requests_mocker): + # given + requests_mocker.post(ANALYTICS_URL) + user = AuthUtils.create_user("Bruce Wayne") + self.client.force_login(user) + # when + response = self.client.get("/dashboard/") + # then + self.assertEqual(response.status_code, 200) + self.assertFalse(requests_mocker.called) + + +@override_settings(CELERY_ALWAYS_EAGER=True) +@requests_mock.mock() +class TestAnalyticsForTasks(TestCase): + @override_settings(ANALYTICS_DISABLED=False) + @patch("allianceauth.eveonline.models.EveCharacter.objects.update_character") + def test_should_run_analytics_for_successful_task( + self, requests_mocker, mock_update_character + ): + # given + requests_mocker.post(ANALYTICS_URL) + user = AuthUtils.create_user("Bruce Wayne") + character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001) + # when + update_character.delay(character.character_id) + # then + self.assertTrue(mock_update_character.called) + self.assertTrue(requests_mocker.called) + payload = parse_qs(requests_mocker.last_request.text) + self.assertListEqual(payload["el"], ["Success"]) + + @override_settings(ANALYTICS_DISABLED=True) + @patch("allianceauth.eveonline.models.EveCharacter.objects.update_character") + def test_should_not_run_analytics_for_successful_task( + self, requests_mocker, mock_update_character + ): + # given + requests_mocker.post(ANALYTICS_URL) + user = AuthUtils.create_user("Bruce Wayne") + character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001) + # when + update_character.delay(character.character_id) + # then + self.assertTrue(mock_update_character.called) + self.assertFalse(requests_mocker.called) + + @override_settings(ANALYTICS_DISABLED=False) + @patch("allianceauth.eveonline.models.EveCharacter.objects.update_character") + def test_should_run_analytics_for_failed_task( + self, requests_mocker, mock_update_character + ): + # given + requests_mocker.post(ANALYTICS_URL) + mock_update_character.side_effect = RuntimeError + user = AuthUtils.create_user("Bruce Wayne") + character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001) + # when + update_character.delay(character.character_id) + # then + self.assertTrue(mock_update_character.called) + self.assertTrue(requests_mocker.called) + payload = parse_qs(requests_mocker.last_request.text) + self.assertNotEqual(payload["el"], ["Success"]) + + @override_settings(ANALYTICS_DISABLED=True) + @patch("allianceauth.eveonline.models.EveCharacter.objects.update_character") + def test_should_not_run_analytics_for_failed_task( + self, requests_mocker, mock_update_character + ): + # given + requests_mocker.post(ANALYTICS_URL) + mock_update_character.side_effect = RuntimeError + user = AuthUtils.create_user("Bruce Wayne") + character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001) + # when + update_character.delay(character.character_id) + # then + self.assertTrue(mock_update_character.called) + self.assertFalse(requests_mocker.called) diff --git a/docs/conf.py b/docs/conf.py index 682a0653..de813c7e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,6 +42,7 @@ from recommonmark.transform import AutoStructify extensions = [ 'sphinx_rtd_theme', 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', 'recommonmark', ] diff --git a/docs/features/core/analytics.md b/docs/features/core/analytics.md index 7b750ada..0ee03f8a 100644 --- a/docs/features/core/analytics.md +++ b/docs/features/core/analytics.md @@ -10,6 +10,12 @@ To Opt-Out, modify our pre-loaded token using the Admin dashboard */admin/analyt Each of the three features Daily Stats, Celery Events and Page Views can be enabled/Disabled independently. +Alternatively, you can fully opt out of analytics with the following optional setting: + +```python +ANALYTICS_DISABLED = True +``` + ![Analytics Tokens](/_static/images/features/core/analytics/tokens.png) ## What @@ -58,6 +64,8 @@ This data is stored in a Team Google Analytics Dashboard. The Maintainers all ha ### Analytics Event +```eval_rst .. automodule:: allianceauth.analytics.tasks :members: analytics_event :undoc-members: +```