From acb7a81bc43f7c0eef94f78a2a605dfb8a8db20b Mon Sep 17 00:00:00 2001 From: Sergey Motornyuk Date: Wed, 14 Jun 2023 20:23:38 +0300 Subject: [PATCH] feat: track downloads via MeasurementProtocol --- ckanext/googleanalytics/config.py | 12 ++++- .../googleanalytics/config_declaration.yaml | 16 ++++--- ckanext/googleanalytics/tests/test_view.py | 22 +++++++++ ckanext/googleanalytics/utils.py | 45 +++++++++++++------ ckanext/googleanalytics/views.py | 9 +++- 5 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 ckanext/googleanalytics/tests/test_view.py diff --git a/ckanext/googleanalytics/config.py b/ckanext/googleanalytics/config.py index 929950b..b73b4d7 100644 --- a/ckanext/googleanalytics/config.py +++ b/ckanext/googleanalytics/config.py @@ -5,7 +5,9 @@ from werkzeug.utils import import_string import ckan.plugins.toolkit as tk +CONFIG_TRACKING_ID = "googleanalytics.id" CONFIG_HANDLER_PATH = "googleanalytics.download_handler" +CONFIG_TRACKING_MODE = "googleanalytics.tracking_mode" DEFAULT_RESOURCE_URL_TAG = "/downloads/" DEFAULT_RECENT_VIEW_DAYS = 14 @@ -30,10 +32,9 @@ def download_handler(): return handler - def tracking_mode(): # type: () -> Literal["ga", "gtag", "gtm"] - type_ = tk.config.get("googleanalytics.tracking_mode") + type_ = tk.config.get(CONFIG_TRACKING_MODE) if type_: return type_ @@ -75,6 +76,13 @@ def measurement_protocol_client_secret(): return tk.config.get("googleanalytics.measurement_protocol.client_secret") +def measurement_protocol_track_downloads(): + # type: () -> bool + return tk.asbool( + tk.config.get("googleanalytics.measurement_protocol.track_downloads") + ) + + def account(): return tk.config.get("googleanalytics.account") diff --git a/ckanext/googleanalytics/config_declaration.yaml b/ckanext/googleanalytics/config_declaration.yaml index 87920d3..9ef0c2e 100644 --- a/ckanext/googleanalytics/config_declaration.yaml +++ b/ckanext/googleanalytics/config_declaration.yaml @@ -7,8 +7,9 @@ groups: placeholder: UA-000000000-1 validators: not_empty description: | - Google tag ID(`G-*`) for Google Analytics 4 properties or the - Tracking ID(`UA-*`) for Universal Analytics properties. + Google tag ID(`G-*`) for Google Analytics 4, the Tracking ID(`UA-*`) + for Universal Analytics, or container ID(`GTM-*`) for Google Tag + Manager. - key: googleanalytics.download_handler default: ckan.views.resource:download @@ -22,13 +23,14 @@ groups: example: gtag description: | Defines the type of code snippet embedded into the page. Can be set - to either `ga`(enables `googleanalytics.js`) or `gtag`(enables - `gtag.js`). The plugin will check `googleanalytics.id` and set - appropriate value depending on the prefix: `G-` enables `gtag`, while - `UA-` enables `ga` mode. + to `ga`(enables `googleanalytics.js`), `gtag`(enables `gtag.js`), or + `gtm`(enables Google Tag Manager). The plugin will check + `googleanalytics.id` and set appropriate value depending on the + prefix: `G-` enables `gtag`, `UA-` enables `ga` mode, and `GTM-` + enables Google Tag Manager. Use this option only if you know better which snippet you need. In - future, after dropping UniversalAnalytics, this option will be + future, after dropping UniversalAnalytics, this option may be removed. - key: googleanalytics.account diff --git a/ckanext/googleanalytics/tests/test_view.py b/ckanext/googleanalytics/tests/test_view.py new file mode 100644 index 0000000..cb73da3 --- /dev/null +++ b/ckanext/googleanalytics/tests/test_view.py @@ -0,0 +1,22 @@ +import pytest + +import ckan.plugins.toolkit as tk +from ckanext.googleanalytics import config + + +@pytest.mark.usefixtures("with_plugins", "with_request_context") +class TestCodeSnippets: + @pytest.mark.parametrize("mode", ["ga", "gtag", "gtm"]) + @pytest.mark.parametrize("tracking_id", ["UA-123", "G-123", "GTM-123"]) + def test_tracking_(self, mode, tracking_id, app, ckan_config, monkeypatch): + snippet = tk.h.googleanalytics_header() + monkeypatch.setitem(ckan_config, config.CONFIG_TRACKING_ID, tracking_id) + monkeypatch.setitem(ckan_config, config.CONFIG_TRACKING_MODE, mode) + snippet = tk.render_snippet("googleanalytics/snippets/_{}.html".format(mode), { + "googleanalytics_id": tracking_id, + "googleanalytics_domain": config.domain(), + "googleanalytics_fields": config.fields(), + "googleanalytics_linked_domains": config.linked_domains() + }) + resp = app.get("/") + assert snippet in resp.body diff --git a/ckanext/googleanalytics/utils.py b/ckanext/googleanalytics/utils.py index 8f5368b..c241633 100644 --- a/ckanext/googleanalytics/utils.py +++ b/ckanext/googleanalytics/utils.py @@ -9,19 +9,24 @@ from ckanext.googleanalytics import config log = logging.getLogger(__name__) EVENT_API = "CKAN API Request" +EVENT_DOWNLOAD = "CKAN Resource Download Request" def send_event(data): if isinstance(data, MeasurementProtocolData): - if data["event"] != EVENT_API: - log.warning("Only API event supported by Measurement Protocol at the moment") - return + if data["event"] == EVENT_API: + return _mp_api_handler({ + "action": data["object"], + "payload": data["payload"], + }) - return _mp_api_handler({ - "action": data["object"], - "payload": data["payload"], - }) + if data["event"] == EVENT_DOWNLOAD: + return _mp_download_handler({"payload": { + "resource_id": data["id"], + }}) + log.warning("Only API and Download events supported by Measurement Protocol at the moment") + return return _ga_handler(data) @@ -32,11 +37,28 @@ class SafeJSONEncoder(json.JSONEncoder): def _mp_api_handler(data_dict): - log.debug( "Sending API event to Google Analytics using the Measurement Protocol: %s", data_dict ) + _mp_event({ + "name": data_dict["action"], + "params": data_dict["payload"] + }) + + +def _mp_download_handler(data_dict): + log.debug( + "Sending Downlaod event to Google Analytics using the Measurement Protocol: %s", + data_dict + ) + _mp_event({ + "name": "file_download", + "params": data_dict["payload"], + }) + + +def _mp_event(event): resp = requests.post( "https://www.google-analytics.com/mp/collect", params={ @@ -46,13 +68,10 @@ def _mp_api_handler(data_dict): data=json.dumps({ "client_id": config.measurement_protocol_client_id(), "non_personalized_ads": False, - "events":[{ - "name": data_dict["action"], - "params": data_dict["payload"] - }] + "events":[event] }, cls=SafeJSONEncoder) ) - # breakpoint() + if resp.status_code >= 300: log.error("Cannot post event: %s", resp) diff --git a/ckanext/googleanalytics/views.py b/ckanext/googleanalytics/views.py index 7f066f8..4b67c33 100644 --- a/ckanext/googleanalytics/views.py +++ b/ckanext/googleanalytics/views.py @@ -59,7 +59,7 @@ def download(id, resource_id, filename=None, package_type="dataset"): handler = resource.download _post_analytics( g.user, - "CKAN Resource Download Request", + utils.EVENT_DOWNLOAD, "Resource", "Download", resource_id, @@ -90,7 +90,11 @@ def _post_analytics( from ckanext.googleanalytics.plugin import GoogleAnalyticsPlugin if config.tracking_id(): - if config.measurement_protocol_client_id() and event_type == utils.EVENT_API: + mp_client_id = config.measurement_protocol_client_id() + if mp_client_id and ( + event_type == utils.EVENT_API + or (event_type == utils.EVENT_DOWNLOAD and config.measurement_protocol_track_downloads()) + ): data_dict = utils.MeasurementProtocolData({ "event": event_type, "object": request_obj_type, @@ -98,6 +102,7 @@ def _post_analytics( "id": request_id, "payload": request_payload, }) + else: data_dict = utils.UniversalAnalyticsData({ "v": 1,