Coverage for portality / lib / plausible.py: 77%
61 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 00:09 +0100
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 00:09 +0100
1# ~~ PlausibleAnalytics:ExternalService~~
2import json
3import logging
4import os
5import requests
7from functools import wraps
8from threading import Thread
10from portality.core import app
11from flask import request
13logger = logging.getLogger(__name__)
15# Keep track of when this is misconfigured so we don't spam the logs with skip messages
16_failstate = False
19def create_logfile(log_dir=None):
20 filepath = __name__ + '.log'
21 if log_dir is not None:
22 if not os.path.exists(log_dir):
23 os.makedirs(log_dir)
24 filepath = os.path.join(log_dir, filepath)
25 fh = logging.FileHandler(filepath)
26 fh.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
27 logger.addHandler(fh)
30def send_event(goal: str, on_completed=None, **props_kwargs):
31 """ Send event data to Plausible Analytics. (ref: https://plausible.io/docs/events-api )
32 """
34 plausible_api_url = app.config.get('PLAUSIBLE_API_URL', '')
35 if not app.config.get('PLAUSIBLE_URL', '') and not plausible_api_url:
36 global _failstate
37 if not _failstate:
38 logger.warning('skip send_event, PLAUSIBLE_URL undefined')
39 _failstate = True
40 return
42 # prepare request payload
43 payload = {'name': goal,
44 'url': app.config.get('BASE_URL', 'http://localhost'),
45 'domain': app.config.get('PLAUSIBLE_SITE_NAME', 'localhost'), }
46 if props_kwargs:
47 payload['props'] = props_kwargs
49 # headers for plausible API
50 headers = {'Content-Type': 'application/json'}
51 if request:
52 # Add IP from CloudFlare header or remote_addr - this works because we have ProxyFix on the app
53 headers["X-Forwarded-For"] = request.headers.get("cf-connecting-ip", request.remote_addr)
54 user_agent_key = 'User-Agent'
55 user_agent_val = request.headers.get(user_agent_key)
56 if user_agent_val:
57 headers[user_agent_key] = user_agent_val
59 # Supply detailed URL if we have it from the request context
60 payload['url'] = request.base_url
62 def _send():
63 resp = requests.post(plausible_api_url, json=payload, headers=headers)
64 if resp.status_code >= 300:
65 logger.warning(f'Send plausible event API fail. snd: [{resp.url}] [{headers}] [{payload}] rcv: [{resp.status_code}] [{resp.text}]')
66 if on_completed:
67 on_completed(resp)
69 try:
70 Thread(target=_send).start()
71 except RuntimeError as e:
72 # When we can't create a thread; don't escalate further since we'd rather the app works than the analytics
73 logger.error(str(e))
76def pa_event(goal, action, label='',
77 record_value_of_which_arg='', **prop_kwargs):
78 """
79 Decorator for Flask view functions, sending event data to Plausible
80 Analytics.
81 """
83 def decorator(fn):
84 @wraps(fn)
85 def decorated_view(*args, **kwargs):
86 # define event label
87 el = label
88 if record_value_of_which_arg in kwargs:
89 el = kwargs[record_value_of_which_arg]
91 # prepare event props payload
92 event_payload = {
93 'action': action,
94 'label': el,
95 }
96 if prop_kwargs:
97 event_payload.update(prop_kwargs)
99 # send event
100 send_event(goal, **event_payload)
102 return fn(*args, **kwargs)
104 return decorated_view
106 return decorator