|
27 | 27 |
|
28 | 28 | # idna encoding import required to prevent bug (unknown encoding: idna) |
29 | 29 | import encodings.idna |
30 | | -import requests |
| 30 | +import base64 |
31 | 31 | import platform |
32 | | -import threading |
| 32 | +import requests |
33 | 33 | import time |
34 | | -import urllib.parse |
35 | | -import json |
36 | 34 |
|
37 | 35 | from classes import info |
38 | 36 | from classes import language |
|
79 | 77 | # Build user-agent |
80 | 78 | user_agent = "Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36" % os_version |
81 | 79 |
|
82 | | -params = { |
83 | | - "cid": s.get("unique_install_id"), # Unique install ID |
84 | | - "v": 1, # Google Measurement API version |
85 | | - "tid": "UA-4381101-5", # Google Analytic Tracking ID |
86 | | - "an": info.PRODUCT_NAME, # App Name |
87 | | - "aip": 1, # Anonymize IP |
88 | | - "aid": "org.openshot.%s" % info.NAME, # App ID |
89 | | - "av": info.VERSION, # App Version |
90 | | - "ul": language.get_current_locale().replace('_', '-').lower(), # Current Locale |
91 | | - "ua": user_agent, # Custom User Agent (for OS, Processor, and OS version) |
92 | | - "cd1": openshot.OPENSHOT_VERSION_FULL, # Dimension 1: libopenshot version |
93 | | - "cd2": platform.python_version(), # Dimension 2: python version (i.e. 3.4.3) |
94 | | - "cd3": QT_VERSION_STR, # Dimension 3: qt5 version (i.e. 5.2.1) |
95 | | - "cd4": PYQT_VERSION_STR, # Dimension 4: pyqt5 version (i.e. 5.2.1) |
96 | | - "cd5": os_distro |
97 | | -} |
| 80 | +GA4_ENDPOINT = "https://www.google-analytics.com/mp/collect" |
| 81 | +GA4_MID = "G-YGF4YGWPE2" |
| 82 | +GA4_MPS = "aG9qVmV0MllURXVPMlVoQm11RWRDQQ==" |
| 83 | +GA4_SESSION_ID = int(time.time()) |
| 84 | +METRIC_QUEUE_MAX = 100 |
98 | 85 |
|
99 | 86 | # Queue for metrics (incase things are disabled... just queue it up |
100 | 87 | # incase the user enables metrics later |
101 | 88 | metric_queue = [] |
102 | 89 |
|
103 | 90 |
|
| 91 | +def _d(value): |
| 92 | + """Decode a stored string value.""" |
| 93 | + return base64.b64decode(value.encode("ascii")).decode("ascii") |
| 94 | + |
| 95 | + |
| 96 | +def _base_event_params(): |
| 97 | + """Shared GA4 params for all events (used as custom dimensions).""" |
| 98 | + return { |
| 99 | + "engagement_time_msec": 1, |
| 100 | + "session_id": GA4_SESSION_ID, |
| 101 | + "os_app_name": info.PRODUCT_NAME, |
| 102 | + "os_app_version": info.VERSION, |
| 103 | + "os_libopenshot_version": openshot.OPENSHOT_VERSION_FULL, |
| 104 | + "os_python_version": platform.python_version(), |
| 105 | + "os_qt_version": QT_VERSION_STR, |
| 106 | + "os_pyqt_version": PYQT_VERSION_STR, |
| 107 | + "os_locale": language.get_current_locale().replace('_', '-').lower(), |
| 108 | + "os_platform": os_version, |
| 109 | + "os_distro": os_distro, |
| 110 | + } |
| 111 | + |
| 112 | + |
| 113 | +def _build_event(name, extra_params=None): |
| 114 | + """Build a GA4 event payload.""" |
| 115 | + event_params = _base_event_params() |
| 116 | + if extra_params: |
| 117 | + event_params.update(extra_params) |
| 118 | + return { |
| 119 | + "name": name, |
| 120 | + "params": event_params, |
| 121 | + } |
| 122 | + |
| 123 | + |
104 | 124 | def track_metric_screen(screen_name): |
105 | 125 | """Track a GUI screen being shown""" |
106 | | - metric_params = json.loads(json.dumps(params)) |
107 | | - metric_params["t"] = "screenview" |
108 | | - metric_params["cd"] = screen_name |
109 | | - metric_params["cid"] = s.get("unique_install_id") |
110 | | - QTimer.singleShot(0, partial(send_metric, metric_params)) |
| 126 | + event = _build_event("screen_view", {"screen_name": screen_name}) |
| 127 | + QTimer.singleShot(0, partial(send_metric, event)) |
111 | 128 |
|
112 | 129 |
|
113 | 130 | def track_metric_event(event_action, event_label, event_category="General", event_value=0): |
114 | | - """Track a GUI screen being shown""" |
115 | | - metric_params = json.loads(json.dumps(params)) |
116 | | - metric_params["t"] = "event" |
117 | | - metric_params["ec"] = event_category |
118 | | - metric_params["ea"] = event_action |
119 | | - metric_params["el"] = event_label |
120 | | - metric_params["ev"] = event_value |
121 | | - metric_params["cid"] = s.get("unique_install_id") |
122 | | - QTimer.singleShot(0, partial(send_metric, metric_params)) |
| 131 | + """Track a UI event.""" |
| 132 | + event = _build_event( |
| 133 | + "ui_event", |
| 134 | + { |
| 135 | + "event_category": event_category, |
| 136 | + "event_action": event_action, |
| 137 | + "event_label": event_label, |
| 138 | + "value": event_value, |
| 139 | + }, |
| 140 | + ) |
| 141 | + QTimer.singleShot(0, partial(send_metric, event)) |
123 | 142 |
|
124 | 143 |
|
125 | 144 | def track_metric_error(error_name, is_fatal=False): |
126 | 145 | """Track an error has occurred""" |
127 | | - metric_params = json.loads(json.dumps(params)) |
128 | | - metric_params["t"] = "exception" |
129 | | - metric_params["exd"] = error_name |
130 | | - metric_params["exf"] = 0 |
131 | | - if is_fatal: |
132 | | - metric_params["exf"] = 1 |
133 | | - QTimer.singleShot(0, partial(send_metric, metric_params)) |
| 146 | + event = _build_event( |
| 147 | + "exception", |
| 148 | + { |
| 149 | + "description": error_name, |
| 150 | + "fatal": 1 if is_fatal else 0, |
| 151 | + }, |
| 152 | + ) |
| 153 | + QTimer.singleShot(0, partial(send_metric, event)) |
134 | 154 |
|
135 | 155 |
|
136 | 156 | def track_metric_session(is_start=True): |
137 | | - """Track a GUI screen being shown""" |
138 | | - metric_params = json.loads(json.dumps(params)) |
139 | | - metric_params["t"] = "screenview" |
140 | | - metric_params["sc"] = "start" |
141 | | - metric_params["cd"] = "launch-app" |
142 | | - metric_params["cid"] = s.get("unique_install_id") |
143 | | - if not is_start: |
144 | | - metric_params["sc"] = "end" |
145 | | - metric_params["cd"] = "close-app" |
146 | | - QTimer.singleShot(0, partial(send_metric, metric_params)) |
| 157 | + """Track application session start/end.""" |
| 158 | + event_name = "session_start" if is_start else "session_end" |
| 159 | + event = _build_event(event_name, {"screen_name": "launch-app" if is_start else "close-app"}) |
| 160 | + QTimer.singleShot(0, partial(send_metric, event)) |
147 | 161 |
|
148 | 162 |
|
149 | | -def send_metric(params): |
150 | | - """Send anonymous metric over HTTP for tracking""" |
| 163 | +def send_metric(event): |
| 164 | + """Send anonymous GA4 Measurement Protocol events over HTTP.""" |
151 | 165 |
|
152 | 166 | # Add to queue and *maybe* send if the user allows it |
153 | | - metric_queue.append(params) |
| 167 | + metric_queue.append(event) |
| 168 | + if len(metric_queue) > METRIC_QUEUE_MAX: |
| 169 | + metric_queue.pop(0) |
154 | 170 |
|
155 | 171 | # Check if the user wants to send metrics and errors |
156 | 172 | if s.get("send_metrics"): |
| 173 | + if not GA4_MID or not GA4_MPS: |
| 174 | + log.warning("GA4 metrics disabled: missing GA4 configuration") |
| 175 | + return |
157 | 176 |
|
158 | | - for metric_params in metric_queue: |
159 | | - url_params = urllib.parse.urlencode(metric_params) |
160 | | - url = "https://www.google-analytics.com/collect?%s" % url_params |
161 | | - |
162 | | - # Send metric HTTP data |
163 | | - try: |
164 | | - r = requests.get(url, headers={"user-agent": user_agent}) |
165 | | - except Exception: |
166 | | - log.warning("Failed to track metric", exc_info=1) |
167 | | - |
168 | | - # Wait a moment, so we don't spam the requests |
169 | | - time.sleep(0.25) |
170 | | - |
171 | | - # All metrics have been sent (or attempted to send) |
172 | | - # Clear the queue |
| 177 | + events_to_send = list(metric_queue) |
173 | 178 | metric_queue.clear() |
| 179 | + |
| 180 | + url = "%s?measurement_id=%s&api_secret=%s" % ( |
| 181 | + GA4_ENDPOINT, |
| 182 | + GA4_MID, |
| 183 | + _d(GA4_MPS), |
| 184 | + ) |
| 185 | + payload = { |
| 186 | + "client_id": s.get("unique_install_id"), |
| 187 | + "events": events_to_send, |
| 188 | + } |
| 189 | + |
| 190 | + # Send metric HTTP data |
| 191 | + try: |
| 192 | + r = requests.post(url, json=payload, headers={"user-agent": user_agent}, timeout=5) |
| 193 | + if r.status_code >= 300: |
| 194 | + log.warning("Failed to track metric (status=%s)", r.status_code) |
| 195 | + except Exception: |
| 196 | + log.warning("Failed to track metric", exc_info=1) |
| 197 | + |
| 198 | + # Wait a moment, so we don't spam the requests |
| 199 | + time.sleep(0.25) |
0 commit comments