Skip to content

Commit bb78781

Browse files
committed
新增 PostHog 产品分析,支持用户行为统计(启动、点名、抽奖事件上报)及用户属性同步(抽取次数统计)
1 parent 7d4a80a commit bb78781

8 files changed

Lines changed: 107 additions & 98 deletions

File tree

CHANGELOG/v2.3.0/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ v2.3 - Shiroko (砂狼白子) release 1
1212
- 新增 **名单设置**,支持批量配置数量(仅抽奖),以及支持配置标签(点名、抽奖)
1313
- 新增 **名单导入**,支持批量导入名单,且能检测并自动重命名重复项
1414
- 新增 **URL/IPC接口**,支持打开计时器窗口
15+
- 新增 **PostHog 产品分析**,支持用户行为统计(启动、点名、抽奖事件上报)及用户属性同步(抽取次数统计)
1516

1617
## 💡 功能优化
1718

@@ -30,7 +31,7 @@ v2.3 - Shiroko (砂狼白子) release 1
3031

3132
## 🔧 其它变更
3233

33-
- 新增 **PostHog 产品分析**,支持用户行为统计(启动、点名、抽奖事件上报)及用户属性同步(抽取次数统计)
34+
-
3435

3536
---
3637

app/common/lottery/lottery_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
get_content_name_async,
3737
get_content_pushbutton_name_async,
3838
)
39-
from app.tools.variable import APP_INIT_DELAY, track_event
39+
from app.tools.variable import APP_INIT_DELAY
40+
from app.tools.config import track_event
4041

4142
system_random = SystemRandom()
4243

app/common/roll_call/roll_call_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
get_content_combo_name_async,
3838
)
3939
from app.tools.path_utils import get_data_path
40-
from app.tools.variable import APP_INIT_DELAY, track_event
40+
from app.tools.variable import APP_INIT_DELAY
41+
from app.tools.config import track_event
4142

4243
system_random = SystemRandom()
4344

app/tools/config.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,91 @@ def configure_logging():
7777
logger.debug(f"日志系统已配置,当前日志等级: {log_level}")
7878

7979

80+
def create_sentry_before_send_filter():
81+
"""创建 Sentry 事件过滤器
82+
83+
过滤掉不需要上报的错误,如第三方库错误和常见的无害错误
84+
"""
85+
86+
def before_send(event, hint):
87+
# 1. 检查是否有堆栈信息
88+
has_stacktrace = False
89+
if "exception" in event:
90+
values = event.get("exception", {}).get("values", [])
91+
for val in values:
92+
if val.get("stacktrace"):
93+
has_stacktrace = True
94+
break
95+
96+
if not has_stacktrace and "threads" in event:
97+
values = event.get("threads", {}).get("values", [])
98+
for val in values:
99+
if val.get("stacktrace"):
100+
has_stacktrace = True
101+
break
102+
103+
# 检查 loguru 的 log_record
104+
log_record = hint.get("log_record")
105+
if log_record:
106+
if getattr(log_record, "exception", None):
107+
has_stacktrace = True
108+
elif hasattr(log_record, "extra") and log_record.extra.get("exc_info"):
109+
has_stacktrace = True
110+
111+
# 如果没有堆栈信息,且是错误/严重级别,则丢弃 (logger.info 等低级别不会被丢弃,除非 event_level 设置)
112+
if not has_stacktrace and event.get("level") in ("error", "fatal"):
113+
return None
114+
115+
# 2. 过滤特定的错误类型或模块
116+
if "exception" in event:
117+
exceptions = event.get("exception", {}).get("values", [])
118+
for exc in exceptions:
119+
module = exc.get("module", "")
120+
type_ = exc.get("type", "")
121+
value = exc.get("value", "")
122+
123+
# 过滤 Qt 常见无害错误 (通常是由于对象在 C++ 侧已销毁但 Python 侧仍在尝试访问)
124+
if type_ == "RuntimeError" and (
125+
"Internal C++ object" in str(value)
126+
or "has been deleted" in str(value)
127+
):
128+
return None
129+
130+
# 过滤 COM 相关
131+
if type_ == "COMError" and "没有注册类" in str(value):
132+
return None
133+
134+
return event
135+
136+
return before_send
137+
138+
139+
posthog_client = None
140+
141+
142+
def set_posthog_client(client):
143+
"""设置全局 PostHog 客户端"""
144+
global posthog_client
145+
posthog_client = client
146+
147+
148+
def track_event(event_name: str):
149+
"""上报事件到 PostHog
150+
151+
Args:
152+
event_name: 事件名称
153+
"""
154+
if posthog_client is None:
155+
return
156+
from app.tools.settings_access import get_or_create_user_id
157+
158+
user_id = get_or_create_user_id()
159+
posthog_client.capture(
160+
distinct_id=user_id,
161+
event=event_name,
162+
)
163+
164+
80165
# ==================== 通知模块 ====================
81166

82167

app/tools/settings_default.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,18 @@ def manage_settings_file():
149149
for second_level_key, second_level_value in first_level_value.items():
150150
if second_level_key not in updated_settings[first_level_key]:
151151
# 处理两种格式:{"default_value": xxx} 或直接值
152-
if isinstance(second_level_value, dict) and "default_value" in second_level_value:
152+
if (
153+
isinstance(second_level_value, dict)
154+
and "default_value" in second_level_value
155+
):
153156
default_val = second_level_value["default_value"]
154157
else:
155158
default_val = second_level_value
156159

157160
if default_val is not None:
158-
updated_settings[first_level_key][second_level_key] = default_val
161+
updated_settings[first_level_key][second_level_key] = (
162+
default_val
163+
)
159164
settings_updated = True
160165

161166
# 移除多余的设置项

app/tools/settings_default_storage.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,9 @@
275275
"floating_window_opacity": {"default_value": 0.8},
276276
"floating_window_topmost_mode": {"default_value": 1},
277277
"extend_quick_draw_component": {"default_value": False},
278-
"floating_window_button_control": {"default_value": ["roll_call", "quick_draw"]},
278+
"floating_window_button_control": {
279+
"default_value": ["roll_call", "quick_draw"]
280+
},
279281
"floating_window_placement": {"default_value": 1},
280282
"floating_window_display_style": {"default_value": 0},
281283
"floating_window_theme": {"default_value": 0},

app/tools/variable.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -350,33 +350,3 @@ def _normalize_arch(machine: str) -> str:
350350
# ==================================================
351351

352352
main_window = None # 全局主窗口引用
353-
354-
355-
# ==================================================
356-
# PostHog 产品分析
357-
# ==================================================
358-
359-
posthog_client = None # 全局 PostHog 客户端
360-
361-
362-
def set_posthog_client(client):
363-
"""设置全局 PostHog 客户端"""
364-
global posthog_client
365-
posthog_client = client
366-
367-
368-
def track_event(event_name: str):
369-
"""上报事件到 PostHog
370-
371-
Args:
372-
event_name: 事件名称
373-
"""
374-
if posthog_client is None:
375-
return
376-
from app.tools.settings_access import get_or_create_user_id
377-
378-
user_id = get_or_create_user_id()
379-
posthog_client.capture(
380-
distinct_id=user_id,
381-
event=event_name,
382-
)

main.py

Lines changed: 6 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@
1313
from loguru import logger
1414

1515
from app.tools.path_utils import get_app_root
16-
from app.tools.config import configure_logging
16+
from app.tools.config import (
17+
configure_logging,
18+
set_posthog_client,
19+
create_sentry_before_send_filter,
20+
)
1721
from app.tools.settings_default import manage_settings_file
18-
from app.tools.settings_access import readme_settings_async, get_or_create_user_id, update_settings
22+
from app.tools.settings_access import readme_settings_async, get_or_create_user_id
1923
from app.core.app_init import calculate_total_draw_counts
2024
from app.tools.variable import (
2125
APP_QUIT_ON_LAST_WINDOW_CLOSED,
@@ -29,7 +33,6 @@
2933
PROCESS_EXIT_WAIT_SECONDS,
3034
POSTHOG_API_KEY,
3135
POSTHOG_HOST,
32-
set_posthog_client,
3336
)
3437
from app.core.single_instance import (
3538
check_single_instance,
@@ -53,65 +56,6 @@
5356
# ==================================================
5457

5558

56-
def create_sentry_before_send_filter():
57-
"""创建 Sentry 事件过滤器
58-
59-
过滤掉不需要上报的错误,如第三方库错误和常见的无害错误
60-
"""
61-
62-
def before_send(event, hint):
63-
# 1. 检查是否有堆栈信息
64-
has_stacktrace = False
65-
if "exception" in event:
66-
values = event.get("exception", {}).get("values", [])
67-
for val in values:
68-
if val.get("stacktrace"):
69-
has_stacktrace = True
70-
break
71-
72-
if not has_stacktrace and "threads" in event:
73-
values = event.get("threads", {}).get("values", [])
74-
for val in values:
75-
if val.get("stacktrace"):
76-
has_stacktrace = True
77-
break
78-
79-
# 检查 loguru 的 log_record
80-
log_record = hint.get("log_record")
81-
if log_record:
82-
if getattr(log_record, "exception", None):
83-
has_stacktrace = True
84-
elif hasattr(log_record, "extra") and log_record.extra.get("exc_info"):
85-
has_stacktrace = True
86-
87-
# 如果没有堆栈信息,且是错误/严重级别,则丢弃 (logger.info 等低级别不会被丢弃,除非 event_level 设置)
88-
if not has_stacktrace and event.get("level") in ("error", "fatal"):
89-
return None
90-
91-
# 2. 过滤特定的错误类型或模块
92-
if "exception" in event:
93-
exceptions = event.get("exception", {}).get("values", [])
94-
for exc in exceptions:
95-
module = exc.get("module", "")
96-
type_ = exc.get("type", "")
97-
value = exc.get("value", "")
98-
99-
# 过滤 Qt 常见无害错误 (通常是由于对象在 C++ 侧已销毁但 Python 侧仍在尝试访问)
100-
if type_ == "RuntimeError" and (
101-
"Internal C++ object" in str(value)
102-
or "has been deleted" in str(value)
103-
):
104-
return None
105-
106-
# 过滤 COM 相关
107-
if type_ == "COMError" and "没有注册类" in str(value):
108-
return None
109-
110-
return event
111-
112-
return before_send
113-
114-
11559
def initialize_sentry():
11660
"""初始化 Sentry 错误监控系统"""
11761
sentry_sdk.init(

0 commit comments

Comments
 (0)