nonebot-plugin-sentry-transaction 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 呵呵です
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.3
2
+ Name: nonebot-plugin-sentry-transaction
3
+ Version: 0.1.0
4
+ Summary: Non-invasive Sentry tracing for NoneBot events and matchers
5
+ Author: shoucandanghehe
6
+ Author-email: shoucandanghehe <wallfjjd@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 呵呵です
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Requires-Dist: nonebot-plugin-sentry>=2.0.0
29
+ Requires-Dist: nonebot2>=2.4.4
30
+ Requires-Dist: sentry-sdk>=2.15.0
31
+ Requires-Python: >=3.10
32
+ Description-Content-Type: text/markdown
33
+
34
+ <!-- markdownlint-disable MD033 MD036 MD041 -->
35
+ <div align="center">
36
+ <a href="https://v2.nonebot.dev/store">
37
+ <img src="./assets/NoneBotPlugin.png" width="300" alt="logo" />
38
+ </a>
39
+
40
+ # nonebot-plugin-sentry-transaction
41
+
42
+ ✨ NoneBot2 事件与 Matcher 的非侵入式 Sentry 性能追踪 ✨
43
+
44
+ ![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)
45
+ ![PyPI](https://img.shields.io/pypi/v/nonebot-plugin-sentry-transaction)
46
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
47
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
48
+ ![License](https://img.shields.io/github/license/shoucandanghehe/nonebot-plugin-sentry-transaction)
49
+ ![CI](https://github.com/shoucandanghehe/nonebot-plugin-sentry-transaction/actions/workflows/test.yml/badge.svg)
50
+ [![Codecov](https://codecov.io/gh/shoucandanghehe/nonebot-plugin-sentry-transaction/graph/badge.svg)](https://codecov.io/gh/shoucandanghehe/nonebot-plugin-sentry-transaction)
51
+
52
+ ---
53
+
54
+ 简体中文 | [English](./README.en-us.md)
55
+
56
+ </div>
57
+
58
+ ## 📖 功能特性
59
+
60
+ - 每个事件产生一个 Sentry **Transaction**(`op: nonebot.event`)
61
+ - 每次 Matcher 运行产生一个 Sentry **Span**(`op: nonebot.matcher`)
62
+ - 并发事件拥有独立 trace,互不干扰
63
+ - 同优先级并发 Matcher 拥有独立 span
64
+ - httpx / SQLAlchemy / aiohttp 等集成的子 span 自动挂载
65
+ - Matcher 异常反映为 `internal_error` span 状态
66
+
67
+ ## 🔧 前置条件
68
+
69
+ 本插件**不负责**初始化 Sentry,请配合 [nonebot-plugin-sentry](https://github.com/nonebot/plugin-sentry) 使用:
70
+
71
+ ```env
72
+ SENTRY_DSN=https://<key>@<org>.ingest.sentry.io/<project>
73
+ SENTRY_TRACES_SAMPLE_RATE=1.0
74
+ ```
75
+
76
+ 其他 Sentry 配置项请参阅 [nonebot-plugin-sentry 文档](https://github.com/nonebot/plugin-sentry)。
77
+
78
+ ## 💿 安装
79
+
80
+ ### 🚀 使用 uv
81
+
82
+ ```bash
83
+ uv add nonebot-plugin-sentry-transaction
84
+ ```
85
+
86
+ ### 🚀 使用 PDM
87
+
88
+ ```bash
89
+ pdm add nonebot-plugin-sentry-transaction
90
+ ```
91
+
92
+ ### 🚀 使用 Poetry
93
+
94
+ ```bash
95
+ poetry add nonebot-plugin-sentry-transaction
96
+ ```
97
+
98
+ ## ⚙️ 配置
99
+
100
+ | 配置项 | 默认值 | 说明 |
101
+ |--------|--------|------|
102
+ | `SENTRY_NB_TRACE_ENABLED` | `true` | 设为 `false` 可关闭事件/Matcher 追踪,同时保留 Sentry 错误上报。 |
103
+
104
+ ## 📊 在 Sentry 中的呈现
105
+
106
+ **Transaction** 出现在 Performance 面板,名称来自 `event.get_event_name()`,例如 `message.private.friend`、`notice.group_upload`。
107
+
108
+ **Span** 以 Matcher 所属插件命名(`plugin_name`),回退到 `module_name` 或类名。
109
+
110
+ 各 transaction/span 携带以下 tag:
111
+
112
+ | Tag | 来源 |
113
+ |-----|------|
114
+ | `adapter` | `bot.type` |
115
+ | `event_type` | `event.get_type()` |
116
+ | `matcher.plugin` | `Matcher.plugin_name` |
117
+ | `matcher.module` | `Matcher.module_name` |
118
+
119
+ ## 🔍 工作原理
120
+
121
+ 插件在加载时 monkey-patch NoneBot 的两个内部函数:
122
+
123
+ - `nonebot.message.handle_event` — 包装 `sentry_sdk.isolation_scope()` + `start_transaction()`
124
+ - `nonebot.message._run_matcher` — 包装 `sentry_sdk.new_scope()` + `start_span()`
125
+
126
+ 同时注册 `run_postprocessor` 钩子,检测被 NoneBot 内部捕获的 Matcher 异常并将 span 状态设为 `internal_error`。
127
+
128
+ 不修改任何业务插件代码。
129
+
130
+ ## 📄 许可证
131
+
132
+ 本项目使用 [MIT](./LICENSE) 许可证开源。
@@ -0,0 +1,99 @@
1
+ <!-- markdownlint-disable MD033 MD036 MD041 -->
2
+ <div align="center">
3
+ <a href="https://v2.nonebot.dev/store">
4
+ <img src="./assets/NoneBotPlugin.png" width="300" alt="logo" />
5
+ </a>
6
+
7
+ # nonebot-plugin-sentry-transaction
8
+
9
+ ✨ NoneBot2 事件与 Matcher 的非侵入式 Sentry 性能追踪 ✨
10
+
11
+ ![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)
12
+ ![PyPI](https://img.shields.io/pypi/v/nonebot-plugin-sentry-transaction)
13
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
14
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
15
+ ![License](https://img.shields.io/github/license/shoucandanghehe/nonebot-plugin-sentry-transaction)
16
+ ![CI](https://github.com/shoucandanghehe/nonebot-plugin-sentry-transaction/actions/workflows/test.yml/badge.svg)
17
+ [![Codecov](https://codecov.io/gh/shoucandanghehe/nonebot-plugin-sentry-transaction/graph/badge.svg)](https://codecov.io/gh/shoucandanghehe/nonebot-plugin-sentry-transaction)
18
+
19
+ ---
20
+
21
+ 简体中文 | [English](./README.en-us.md)
22
+
23
+ </div>
24
+
25
+ ## 📖 功能特性
26
+
27
+ - 每个事件产生一个 Sentry **Transaction**(`op: nonebot.event`)
28
+ - 每次 Matcher 运行产生一个 Sentry **Span**(`op: nonebot.matcher`)
29
+ - 并发事件拥有独立 trace,互不干扰
30
+ - 同优先级并发 Matcher 拥有独立 span
31
+ - httpx / SQLAlchemy / aiohttp 等集成的子 span 自动挂载
32
+ - Matcher 异常反映为 `internal_error` span 状态
33
+
34
+ ## 🔧 前置条件
35
+
36
+ 本插件**不负责**初始化 Sentry,请配合 [nonebot-plugin-sentry](https://github.com/nonebot/plugin-sentry) 使用:
37
+
38
+ ```env
39
+ SENTRY_DSN=https://<key>@<org>.ingest.sentry.io/<project>
40
+ SENTRY_TRACES_SAMPLE_RATE=1.0
41
+ ```
42
+
43
+ 其他 Sentry 配置项请参阅 [nonebot-plugin-sentry 文档](https://github.com/nonebot/plugin-sentry)。
44
+
45
+ ## 💿 安装
46
+
47
+ ### 🚀 使用 uv
48
+
49
+ ```bash
50
+ uv add nonebot-plugin-sentry-transaction
51
+ ```
52
+
53
+ ### 🚀 使用 PDM
54
+
55
+ ```bash
56
+ pdm add nonebot-plugin-sentry-transaction
57
+ ```
58
+
59
+ ### 🚀 使用 Poetry
60
+
61
+ ```bash
62
+ poetry add nonebot-plugin-sentry-transaction
63
+ ```
64
+
65
+ ## ⚙️ 配置
66
+
67
+ | 配置项 | 默认值 | 说明 |
68
+ |--------|--------|------|
69
+ | `SENTRY_NB_TRACE_ENABLED` | `true` | 设为 `false` 可关闭事件/Matcher 追踪,同时保留 Sentry 错误上报。 |
70
+
71
+ ## 📊 在 Sentry 中的呈现
72
+
73
+ **Transaction** 出现在 Performance 面板,名称来自 `event.get_event_name()`,例如 `message.private.friend`、`notice.group_upload`。
74
+
75
+ **Span** 以 Matcher 所属插件命名(`plugin_name`),回退到 `module_name` 或类名。
76
+
77
+ 各 transaction/span 携带以下 tag:
78
+
79
+ | Tag | 来源 |
80
+ |-----|------|
81
+ | `adapter` | `bot.type` |
82
+ | `event_type` | `event.get_type()` |
83
+ | `matcher.plugin` | `Matcher.plugin_name` |
84
+ | `matcher.module` | `Matcher.module_name` |
85
+
86
+ ## 🔍 工作原理
87
+
88
+ 插件在加载时 monkey-patch NoneBot 的两个内部函数:
89
+
90
+ - `nonebot.message.handle_event` — 包装 `sentry_sdk.isolation_scope()` + `start_transaction()`
91
+ - `nonebot.message._run_matcher` — 包装 `sentry_sdk.new_scope()` + `start_span()`
92
+
93
+ 同时注册 `run_postprocessor` 钩子,检测被 NoneBot 内部捕获的 Matcher 异常并将 span 状态设为 `internal_error`。
94
+
95
+ 不修改任何业务插件代码。
96
+
97
+ ## 📄 许可证
98
+
99
+ 本项目使用 [MIT](./LICENSE) 许可证开源。
@@ -0,0 +1,19 @@
1
+ from nonebot import get_plugin_config, require
2
+ from nonebot.plugin import PluginMetadata
3
+
4
+ require('nonebot_plugin_sentry')
5
+
6
+ from .config import Config # noqa: E402
7
+ from .tracing import setup_tracing # noqa: E402
8
+
9
+ __plugin_meta__ = PluginMetadata(
10
+ name='nonebot-plugin-sentry-transaction',
11
+ description='NoneBot Event 与 Matcher 的非侵入式 Sentry tracing',
12
+ usage='配置 nonebot-plugin-sentry 完成 Sentry 初始化。设置 SENTRY_NB_TRACE_ENABLED=false 可关闭 tracing。',
13
+ type='library',
14
+ config=Config,
15
+ supported_adapters=None,
16
+ )
17
+
18
+ config = get_plugin_config(Config)
19
+ setup_tracing(config)
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Config(BaseModel):
5
+ """Sentry tracing 插件配置。"""
6
+
7
+ sentry_nb_trace_enabled: bool = True
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import sys
5
+ from typing import TYPE_CHECKING
6
+
7
+ import nonebot.message
8
+ import sentry_sdk
9
+ from nonebot.exception import StopPropagation
10
+ from nonebot.message import run_postprocessor
11
+ from sentry_sdk.tracing import Span as SentrySpan
12
+
13
+ if TYPE_CHECKING:
14
+ from contextlib import AsyncExitStack
15
+
16
+ from nonebot.adapters import Bot, Event
17
+ from nonebot.matcher import Matcher
18
+ from nonebot.typing import T_DependencyCache, T_State
19
+
20
+ from .config import Config
21
+
22
+
23
+ _original_handle_event = nonebot.message.handle_event
24
+
25
+
26
+ async def traced_handle_event(bot: Bot, event: Event) -> None:
27
+ # isolation_scope 为每个事件创建独立的隔离作用域,防止并发事件之间污染 trace 状态。
28
+ with sentry_sdk.isolation_scope() as scope:
29
+ scope.set_tag('adapter', bot.type)
30
+ scope.set_tag('event_type', event.get_type())
31
+
32
+ with sentry_sdk.start_transaction(op='nonebot.event', name=event.get_event_name()) as transaction:
33
+ transaction.set_tag('adapter', bot.type)
34
+ transaction.set_tag('event_type', event.get_type())
35
+ await _original_handle_event(bot, event)
36
+
37
+
38
+ def patch_handle_event() -> None:
39
+ nonebot.message.handle_event = traced_handle_event
40
+
41
+ # Adapter 在导入时通过 from nonebot.message import handle_event 绑定了本地引用,
42
+ # 仅替换模块属性无法覆盖这些引用,需遍历 sys.modules 逐一替换。
43
+ for module in sys.modules.values():
44
+ if module is nonebot.message:
45
+ continue
46
+ try:
47
+ module_dict = vars(module)
48
+ except TypeError:
49
+ continue
50
+ for attr_name, attr_value in module_dict.items():
51
+ if attr_value is _original_handle_event:
52
+ with contextlib.suppress(AttributeError, TypeError):
53
+ setattr(module, attr_name, traced_handle_event)
54
+
55
+
56
+ _original_run_matcher = nonebot.message._run_matcher # noqa: SLF001
57
+
58
+
59
+ async def traced_run_matcher( # noqa: PLR0913
60
+ Matcher: type[Matcher], # noqa: N803
61
+ bot: Bot,
62
+ event: Event,
63
+ state: T_State,
64
+ stack: AsyncExitStack | None = None,
65
+ dependency_cache: T_DependencyCache | None = None,
66
+ ) -> None:
67
+ # new_scope 分叉当前 scope,防止同优先级并发 matcher 互相覆盖 active span。
68
+ with sentry_sdk.new_scope() as scope:
69
+ span_name = Matcher.plugin_name or Matcher.module_name or Matcher.__name__
70
+ scope.set_tag('matcher.plugin', Matcher.plugin_name or 'unknown')
71
+ scope.set_tag('matcher.module', Matcher.module_name or 'unknown')
72
+
73
+ # 在 span 内部捕获 StopPropagation,确保 span 以 ok 状态正常结束,
74
+ # 随后重新抛出以保持 NoneBot 的事件传播语义。
75
+ stop_propagation = False
76
+ with sentry_sdk.start_span(op='nonebot.matcher', name=span_name):
77
+ try:
78
+ await _original_run_matcher(Matcher, bot, event, state, stack, dependency_cache)
79
+ except StopPropagation:
80
+ stop_propagation = True
81
+
82
+ if stop_propagation:
83
+ raise StopPropagation
84
+
85
+
86
+ def patch_run_matcher() -> None:
87
+ # check_and_run_matcher 通过模块全局名调用 _run_matcher(运行时解析),
88
+ # 因此只需替换模块属性,无需扫描 sys.modules。
89
+ nonebot.message._run_matcher = traced_run_matcher # noqa: SLF001
90
+
91
+
92
+ async def _on_matcher_done(exception: Exception | None = None) -> None:
93
+ """检测 matcher 内部异常并将 span 标记为错误。
94
+
95
+ _run_matcher 会捕获 matcher 异常但不重新抛出,导致 span 的 __exit__
96
+ 无法感知异常。此 hook 通过 exception 参数补充设置 span 状态。
97
+ """
98
+ span = sentry_sdk.get_current_span()
99
+ if isinstance(span, SentrySpan) and exception is not None:
100
+ span.set_status('internal_error')
101
+
102
+
103
+ def setup_tracing(config: Config) -> None:
104
+ """在插件加载时安装 tracing patch。"""
105
+ if not config.sentry_nb_trace_enabled or not sentry_sdk.is_initialized():
106
+ return
107
+ patch_handle_event()
108
+ patch_run_matcher()
109
+ run_postprocessor(_on_matcher_done)
@@ -0,0 +1,135 @@
1
+ [project]
2
+ name = "nonebot-plugin-sentry-transaction"
3
+ version = "0.1.0"
4
+ description = "Non-invasive Sentry tracing for NoneBot events and matchers"
5
+ readme = "README.md"
6
+ authors = [{ name = "shoucandanghehe", email = "wallfjjd@gmail.com" }]
7
+ license = { file = "LICENSE" }
8
+ requires-python = ">=3.10"
9
+ dependencies = [
10
+ "nonebot-plugin-sentry>=2.0.0",
11
+ "nonebot2>=2.4.4",
12
+ "sentry-sdk>=2.15.0",
13
+ ]
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.10.6,<0.11.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [tool.uv.build-backend]
20
+ module-name = "nonebot_plugin_sentry_transaction"
21
+ module-root = ""
22
+
23
+ [tool.ruff]
24
+ line-length = 120
25
+ target-version = "py310"
26
+
27
+ [tool.ruff.lint]
28
+ select = [
29
+ "F", # pyflakes
30
+ "E", # pycodestyle errors
31
+ "W", # pycodestyle warnings
32
+ "C90", # mccabe
33
+ "I", # isort
34
+ "N", # PEP8-naming
35
+ "UP", # pyupgrade
36
+ "YTT", # flake8-2020
37
+ "ANN", # flake8-annotations
38
+ "ASYNC", # flake8-async
39
+ "S", # flake8-bandit
40
+ "BLE", # flake8-blind-except
41
+ "FBT", # flake8-boolean-trap
42
+ "B", # flake8-bugbear
43
+ "A", # flake8-builtins
44
+ "COM", # flake8-commas
45
+ "C4", # flake8-comprehensions
46
+ "DTZ", # flake8-datetimez
47
+ "T10", # flake8-debugger
48
+ "EM", # flake8-errmsg
49
+ "FA", # flake8-future-annotations
50
+ "ISC", # flake8-implicit-str-concat
51
+ "ICN", # flake8-import-conventions
52
+ "PIE", # flake8-pie
53
+ "T20", # flake8-print
54
+ "PYI", # flake8-pyi
55
+ "Q", # flake8-quotes
56
+ "RSE", # flake8-raise
57
+ "RET", # flake8-return
58
+ "SLF", # flake8-self
59
+ "SLOT", # flake8-slots
60
+ "SIM", # flake8-simplify
61
+ "TID", # flake8-tidy-imports
62
+ "TC", # flake8-type-checking
63
+ "ARG", # flake8-unused-arguments
64
+ "PTH", # flake8-use-pathlib
65
+ "ERA", # eradicate
66
+ "PD", # pandas-vet
67
+ "PGH", # pygrep-hooks
68
+ "PL", # pylint
69
+ "TRY", # tryceratops
70
+ "FLY", # flynt
71
+ "FAST", # FastAPI
72
+ "PERF", # Perflint
73
+ "FURB", # refurb
74
+ "RUF", # Ruff-specific rules
75
+ ]
76
+ ignore = [
77
+ "E501", # 过长的行由 ruff format 处理, 剩余的都是字符串
78
+ "ANN202", # 向 NoneBot 注册的函数
79
+ "TRY003",
80
+ "COM812", # 强制尾随逗号
81
+ "TID252", # 相对导入
82
+ "ISC001", # format warning
83
+ "RUF002", # docstring 中的全角标点
84
+ "RUF003", # 注释中的全角标点
85
+ ]
86
+
87
+ [tool.ruff.lint.per-file-ignores]
88
+ "tests/**/*.py" = ["S101", "ANN", "PLR2004", "SIM117", "E402"]
89
+
90
+ [tool.ruff.lint.flake8-quotes]
91
+ inline-quotes = "single"
92
+ multiline-quotes = "double"
93
+
94
+ [tool.ruff.lint.flake8-annotations]
95
+ mypy-init-return = true
96
+
97
+ [tool.ruff.lint.flake8-builtins]
98
+ builtins-ignorelist = ["id"]
99
+
100
+ [tool.ruff.format]
101
+ quote-style = "single"
102
+
103
+ [tool.basedpyright]
104
+ pythonVersion = "3.10"
105
+ pythonPlatform = "All"
106
+ defineConstant = { PYDANTIC_V2 = true }
107
+ typeCheckingMode = "standard"
108
+ reportExplicitAny = 'hint'
109
+ reportUnnecessaryTypeIgnoreComment = 'error'
110
+ reportImplicitOverride = 'error'
111
+ reportUnnecessaryComparison = 'error'
112
+ reportImplicitAbstractClass = 'error'
113
+ enableTypeIgnoreComments = false
114
+
115
+ [dependency-groups]
116
+ dev = [
117
+ "basedpyright>=1.38.3",
118
+ "mypy>=1.19.1",
119
+ "pytest>=9.0.2",
120
+ "pytest-asyncio>=0.24",
121
+ "pytest-cov>=4.0",
122
+ "ruff>=0.15.7",
123
+ ]
124
+
125
+
126
+ [tool.pytest.ini_options]
127
+ asyncio_mode = "strict"
128
+
129
+ [tool.coverage.run]
130
+ source = ["nonebot_plugin_sentry_transaction"]
131
+
132
+ [tool.coverage.report]
133
+ exclude_lines = [
134
+ "if TYPE_CHECKING:",
135
+ ]