feat: track downloads via MeasurementProtocol

This commit is contained in:
Sergey Motornyuk 2023-06-14 20:23:38 +03:00
parent 5268985575
commit acb7a81bc4
5 changed files with 80 additions and 24 deletions

View File

@ -5,7 +5,9 @@ from werkzeug.utils import import_string
import ckan.plugins.toolkit as tk import ckan.plugins.toolkit as tk
CONFIG_TRACKING_ID = "googleanalytics.id"
CONFIG_HANDLER_PATH = "googleanalytics.download_handler" CONFIG_HANDLER_PATH = "googleanalytics.download_handler"
CONFIG_TRACKING_MODE = "googleanalytics.tracking_mode"
DEFAULT_RESOURCE_URL_TAG = "/downloads/" DEFAULT_RESOURCE_URL_TAG = "/downloads/"
DEFAULT_RECENT_VIEW_DAYS = 14 DEFAULT_RECENT_VIEW_DAYS = 14
@ -30,10 +32,9 @@ def download_handler():
return handler return handler
def tracking_mode(): def tracking_mode():
# type: () -> Literal["ga", "gtag", "gtm"] # type: () -> Literal["ga", "gtag", "gtm"]
type_ = tk.config.get("googleanalytics.tracking_mode") type_ = tk.config.get(CONFIG_TRACKING_MODE)
if type_: if type_:
return type_ return type_
@ -75,6 +76,13 @@ def measurement_protocol_client_secret():
return tk.config.get("googleanalytics.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(): def account():
return tk.config.get("googleanalytics.account") return tk.config.get("googleanalytics.account")

View File

@ -7,8 +7,9 @@ groups:
placeholder: UA-000000000-1 placeholder: UA-000000000-1
validators: not_empty validators: not_empty
description: | description: |
Google tag ID(`G-*`) for Google Analytics 4 properties or the Google tag ID(`G-*`) for Google Analytics 4, the Tracking ID(`UA-*`)
Tracking ID(`UA-*`) for Universal Analytics properties. for Universal Analytics, or container ID(`GTM-*`) for Google Tag
Manager.
- key: googleanalytics.download_handler - key: googleanalytics.download_handler
default: ckan.views.resource:download default: ckan.views.resource:download
@ -22,13 +23,14 @@ groups:
example: gtag example: gtag
description: | description: |
Defines the type of code snippet embedded into the page. Can be set Defines the type of code snippet embedded into the page. Can be set
to either `ga`(enables `googleanalytics.js`) or `gtag`(enables to `ga`(enables `googleanalytics.js`), `gtag`(enables `gtag.js`), or
`gtag.js`). The plugin will check `googleanalytics.id` and set `gtm`(enables Google Tag Manager). The plugin will check
appropriate value depending on the prefix: `G-` enables `gtag`, while `googleanalytics.id` and set appropriate value depending on the
`UA-` enables `ga` mode. 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 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. removed.
- key: googleanalytics.account - key: googleanalytics.account

View File

@ -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

View File

@ -9,19 +9,24 @@ from ckanext.googleanalytics import config
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
EVENT_API = "CKAN API Request" EVENT_API = "CKAN API Request"
EVENT_DOWNLOAD = "CKAN Resource Download Request"
def send_event(data): def send_event(data):
if isinstance(data, MeasurementProtocolData): if isinstance(data, MeasurementProtocolData):
if data["event"] != EVENT_API: if data["event"] == EVENT_API:
log.warning("Only API event supported by Measurement Protocol at the moment") return _mp_api_handler({
return "action": data["object"],
"payload": data["payload"],
})
return _mp_api_handler({ if data["event"] == EVENT_DOWNLOAD:
"action": data["object"], return _mp_download_handler({"payload": {
"payload": data["payload"], "resource_id": data["id"],
}) }})
log.warning("Only API and Download events supported by Measurement Protocol at the moment")
return
return _ga_handler(data) return _ga_handler(data)
@ -32,11 +37,28 @@ class SafeJSONEncoder(json.JSONEncoder):
def _mp_api_handler(data_dict): def _mp_api_handler(data_dict):
log.debug( log.debug(
"Sending API event to Google Analytics using the Measurement Protocol: %s", "Sending API event to Google Analytics using the Measurement Protocol: %s",
data_dict 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( resp = requests.post(
"https://www.google-analytics.com/mp/collect", "https://www.google-analytics.com/mp/collect",
params={ params={
@ -46,13 +68,10 @@ def _mp_api_handler(data_dict):
data=json.dumps({ data=json.dumps({
"client_id": config.measurement_protocol_client_id(), "client_id": config.measurement_protocol_client_id(),
"non_personalized_ads": False, "non_personalized_ads": False,
"events":[{ "events":[event]
"name": data_dict["action"],
"params": data_dict["payload"]
}]
}, cls=SafeJSONEncoder) }, cls=SafeJSONEncoder)
) )
# breakpoint()
if resp.status_code >= 300: if resp.status_code >= 300:
log.error("Cannot post event: %s", resp) log.error("Cannot post event: %s", resp)

View File

@ -59,7 +59,7 @@ def download(id, resource_id, filename=None, package_type="dataset"):
handler = resource.download handler = resource.download
_post_analytics( _post_analytics(
g.user, g.user,
"CKAN Resource Download Request", utils.EVENT_DOWNLOAD,
"Resource", "Resource",
"Download", "Download",
resource_id, resource_id,
@ -90,7 +90,11 @@ def _post_analytics(
from ckanext.googleanalytics.plugin import GoogleAnalyticsPlugin from ckanext.googleanalytics.plugin import GoogleAnalyticsPlugin
if config.tracking_id(): 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({ data_dict = utils.MeasurementProtocolData({
"event": event_type, "event": event_type,
"object": request_obj_type, "object": request_obj_type,
@ -98,6 +102,7 @@ def _post_analytics(
"id": request_id, "id": request_id,
"payload": request_payload, "payload": request_payload,
}) })
else: else:
data_dict = utils.UniversalAnalyticsData({ data_dict = utils.UniversalAnalyticsData({
"v": 1, "v": 1,