Coverage for portality / lib / plausible.py: 77%

61 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-04 09:41 +0100

1# ~~ PlausibleAnalytics:ExternalService~~ 

2import json 

3import logging 

4import os 

5import requests 

6 

7from functools import wraps 

8from threading import Thread 

9 

10from portality.core import app 

11from flask import request 

12 

13logger = logging.getLogger(__name__) 

14 

15# Keep track of when this is misconfigured so we don't spam the logs with skip messages 

16_failstate = False 

17 

18 

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) 

28 

29 

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

33 

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 

41 

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 

48 

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 

58 

59 # Supply detailed URL if we have it from the request context 

60 payload['url'] = request.base_url 

61 

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) 

68 

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

74 

75 

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

82 

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] 

90 

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) 

98 

99 # send event 

100 send_event(goal, **event_payload) 

101 

102 return fn(*args, **kwargs) 

103 

104 return decorated_view 

105 

106 return decorator