Skip to content

Commit af431bd

Browse files
committed
add stats uploader
1 parent db795d9 commit af431bd

3 files changed

Lines changed: 240 additions & 0 deletions

File tree

examples/test_stats_uploader.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Test script for the stats uploader module"""
2+
3+
import logging
4+
5+
from telemetric.statswrapper import stats_deco
6+
from telemetric.ga4.stats_uploader import StatsUploader
7+
8+
logging.basicConfig(level=logging.DEBUG)
9+
10+
11+
# Create some test functions with statistics
12+
@stats_deco(None, b=("a", 3), c=None)
13+
def test_func(a, b=None, c=None):
14+
return a, b, c
15+
16+
17+
@stats_deco(x=None, y=(True, False))
18+
def another_func(x=1, y=True):
19+
if y:
20+
return x * 2
21+
return x
22+
23+
24+
# Call the functions to generate some statistics
25+
test_func(1, 3)
26+
test_func("hello", "a", "world")
27+
test_func(42)
28+
29+
another_func(5)
30+
another_func(10, False)
31+
another_func(15, True)
32+
33+
34+
# Print current statistics
35+
print("=== Current Statistics ===")
36+
print("test_func counts:", test_func._get_counts())
37+
print("test_func param stats:", test_func._get_param_stats())
38+
print()
39+
print("another_func counts:", another_func._get_counts())
40+
print("another_func param stats:", another_func._get_param_stats())
41+
print()
42+
43+
44+
# Test the stats uploader (this will try to upload to GA4)
45+
print("=== Testing Stats Uploader ===")
46+
47+
# Use a dummy proxy URL for testing (won't actually send data unless configured)
48+
uploader = StatsUploader(proxy_url="https://analytics-proxy-production-665e.up.railway.app")
49+
50+
print("Analytics client enabled:", uploader.analytics.enabled)
51+
52+
# Test uploading individual function stats
53+
print("Uploading test_func stats...")
54+
result1 = uploader.upload_function_stats(test_func, package_name="test_package")
55+
print("Upload result:", result1)
56+
57+
print("Uploading another_func stats...")
58+
result2 = uploader.upload_function_stats(another_func, package_name="test_package")
59+
print("Upload result:", result2)
60+
61+
# Test uploading all stats
62+
print("\nUploading all stats...")
63+
summary = uploader.upload_all_stats(package_name="test_package")
64+
print("Upload summary:", summary)
65+
66+
# Test custom stats upload
67+
print("\nUploading custom stats...")
68+
custom_result = uploader.upload_custom_stats("custom_metric", {
69+
"metric_name": "test_metric",
70+
"value": 42,
71+
"category": "testing"
72+
})
73+
print("Custom upload result:", custom_result)

src/telemetric/ga4/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Google Analytics 4 integration for telemetric"""
2+
3+
from .analytics import AnalyticsClient
4+
from .stats_uploader import StatsUploader
5+
6+
__all__ = ["AnalyticsClient", "StatsUploader"]
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Module for uploading statswrapper statistics to Google Analytics 4"""
2+
3+
from typing import Optional, Dict, Any
4+
from telemetric.ga4.analytics import AnalyticsClient
5+
from telemetric.statswrapper import _wrapped
6+
7+
8+
class StatsUploader:
9+
"""
10+
Uploads statswrapper function usage statistics to Google Analytics 4.
11+
12+
Example usage:
13+
uploader = StatsUploader(proxy_url="https://your-project.up.railway.app")
14+
uploader.upload_all_stats()
15+
"""
16+
17+
def __init__(self, proxy_url: str):
18+
"""
19+
Initialize the stats uploader.
20+
21+
Args:
22+
proxy_url: The URL of the GA4 proxy server
23+
"""
24+
self.analytics = AnalyticsClient(proxy_url)
25+
26+
def upload_function_stats(self, wrapped_func, package_name: Optional[str] = None) -> bool:
27+
"""
28+
Upload statistics for a single wrapped function to GA4.
29+
30+
Args:
31+
wrapped_func: A statswrapper-wrapped function
32+
package_name: Optional package name to include in the event
33+
34+
Returns:
35+
bool: True if upload was attempted (regardless of success), False if disabled
36+
"""
37+
if not self.analytics.enabled:
38+
return False
39+
40+
# Get function counts: (total_calls, error_calls, invalid_args)
41+
counts = wrapped_func._get_counts()
42+
total_calls, error_calls, invalid_args = counts
43+
44+
# Skip functions that haven't been called
45+
if total_calls == 0:
46+
return True
47+
48+
# Get parameter statistics
49+
param_stats = wrapped_func._get_param_stats()
50+
51+
# Build event parameters
52+
event_params = {
53+
'function_name': f"{wrapped_func.__module__}.{wrapped_func.__name__}",
54+
'total_calls': total_calls,
55+
'error_calls': error_calls,
56+
'invalid_args': invalid_args,
57+
'success_rate': round((total_calls - error_calls) / total_calls, 3) if total_calls > 0 else 0
58+
}
59+
60+
if package_name:
61+
event_params['package_name'] = package_name
62+
63+
# Add parameter usage statistics
64+
param_data = {}
65+
for name, n_uses, known_params, param_counts in param_stats:
66+
if name is None:
67+
# Positional argument
68+
param_key = f"pos_arg_uses"
69+
param_data[param_key] = n_uses
70+
else:
71+
# Named argument
72+
param_key = f"arg_{name}_uses"
73+
param_data[param_key] = n_uses
74+
75+
# Add specific parameter value counts if available
76+
if known_params is not None and param_counts is not None:
77+
for i, param_val in enumerate(known_params):
78+
if i < len(param_counts):
79+
count_key = f"arg_{name}_{param_val}_count"
80+
param_data[count_key] = param_counts[i]
81+
82+
# Merge parameter data into event params (limit to reasonable size)
83+
if len(param_data) <= 20: # GA4 has parameter limits
84+
event_params.update(param_data)
85+
else:
86+
# If too many parameters, just include summary
87+
event_params['total_params_tracked'] = len(param_data)
88+
89+
# Send to GA4
90+
self.analytics.track_event('function_usage_stats', event_params)
91+
return True
92+
93+
def upload_all_stats(self, package_name: Optional[str] = None, skip_uncalled: bool = True) -> Dict[str, Any]:
94+
"""
95+
Upload statistics for all wrapped functions to GA4.
96+
97+
Args:
98+
package_name: Optional package name to include in events
99+
skip_uncalled: Whether to skip functions that haven't been called
100+
101+
Returns:
102+
dict: Summary of upload results
103+
"""
104+
if not self.analytics.enabled:
105+
return {
106+
'uploaded': 0,
107+
'skipped': 0,
108+
'total_functions': len(_wrapped),
109+
'status': 'disabled'
110+
}
111+
112+
uploaded = 0
113+
skipped = 0
114+
115+
for wrapped_func in _wrapped:
116+
counts = wrapped_func._get_counts()
117+
total_calls = counts[0]
118+
119+
if skip_uncalled and total_calls == 0:
120+
skipped += 1
121+
continue
122+
123+
self.upload_function_stats(wrapped_func, package_name)
124+
uploaded += 1
125+
126+
# Upload summary statistics
127+
summary_params = {
128+
'total_wrapped_functions': len(_wrapped),
129+
'functions_called': sum(1 for f in _wrapped if f._get_counts()[0] > 0),
130+
'total_function_calls': sum(f._get_counts()[0] for f in _wrapped),
131+
'total_errors': sum(f._get_counts()[1] for f in _wrapped),
132+
}
133+
134+
if package_name:
135+
summary_params['package_name'] = package_name
136+
137+
self.analytics.track_event('package_usage_summary', summary_params)
138+
139+
return {
140+
'uploaded': uploaded,
141+
'skipped': skipped,
142+
'total_functions': len(_wrapped),
143+
'status': 'completed'
144+
}
145+
146+
def upload_custom_stats(self, event_name: str, stats_data: Dict[str, Any]) -> bool:
147+
"""
148+
Upload custom statistics data to GA4.
149+
150+
Args:
151+
event_name: Name of the GA4 event
152+
stats_data: Dictionary of statistics to upload
153+
154+
Returns:
155+
bool: True if upload was attempted, False if disabled
156+
"""
157+
if not self.analytics.enabled:
158+
return False
159+
160+
self.analytics.track_event(event_name, stats_data)
161+
return True

0 commit comments

Comments
 (0)