fix: document telemetry opt-out and silence delivery failures (#2618)

* fix: document telemetry opt-out and silence delivery failures

* style: format telemetry debug logging
This commit is contained in:
RockChinQ
2026-09-30 22:27:05 +08:00
committed by GitHub
parent 461b483977
commit d3e4bd55cd
5 changed files with 30 additions and 19 deletions
+4
View File
@@ -188,3 +188,7 @@ Thanks to all [contributors](https://github.com/langbot-app/LangBot/graphs/contr
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>
## Optional telemetry
Telemetry is enabled by default. To opt out, set `space.disable_telemetry: true` in your instance’s `data/config.yaml`, preserve other settings, and restart the instance. This disables usage, heartbeat and execution telemetry. In Cloud, the instance operator controls this setting for every workspace in the instance. Delivery is best-effort in background tasks; connection failures, timeouts and server errors do not block normal operations. Failure details are logged only at DEBUG level.
+4
View File
@@ -212,3 +212,7 @@ LangBot **从设计上就对 Agent 友好** —— 你的编码 Agent(Claude C
If you're a code agent based on LLM, you should read [AGENTS.md](AGENTS.md) to understand the project structure and development process before you start any development.
-->
## 可选遥测
遥测默认开启。可以在实例 `data/config.yaml` 中将 `space.disable_telemetry` 改为 `true`,保留其他配置,然后重启实例。此开关关闭使用统计、实例心跳和功能执行遥测;Cloud 由实例运维方配置,对该实例所有工作区生效。遥测在后台尽力发送,网络故障、超时或服务端错误不会阻断正常操作;失败详情仅记录在 DEBUG 日志中。
+8 -10
View File
@@ -170,9 +170,7 @@ class TelemetryManager:
if resp.status_code >= 400:
body = await httpclient.response_text(resp, max_chars=200)
self.ap.logger.warning(
f'Telemetry post to {url} returned status {resp.status_code} - {body}'
)
self.ap.logger.debug(f'Telemetry post to {url} returned status {resp.status_code} - {body}')
else:
# Detect application-level errors inside HTTP 200 responses
app_err = False
@@ -181,7 +179,7 @@ class TelemetryManager:
app_code = j.get('code') if isinstance(j, dict) else None
if app_code is not None and int(app_code) >= 400:
app_err = True
self.ap.logger.warning(
self.ap.logger.debug(
f'Telemetry post to {url} returned application error code {j.get("code")} - {j.get("msg")}'
)
except Exception:
@@ -189,7 +187,7 @@ class TelemetryManager:
if app_err:
body = await httpclient.response_text(resp, max_chars=200)
self.ap.logger.warning(
self.ap.logger.debug(
f'Telemetry post to {url} returned app-level error - response: {body}'
)
else:
@@ -200,19 +198,19 @@ class TelemetryManager:
if not app_err:
return True
except asyncio.TimeoutError:
self.ap.logger.warning(f'Telemetry post to {url} timed out')
self.ap.logger.debug(f'Telemetry post to {url} timed out')
except Exception as e:
self.ap.logger.warning(f'Failed to post telemetry to {url}: {e}', exc_info=True)
self.ap.logger.debug(f'Failed to post telemetry to {url}: {e}', exc_info=True)
except Exception as e:
try:
self.ap.logger.warning(
self.ap.logger.debug(
f'Failed to create HTTP client for telemetry or sanitize payload: {e}', exc_info=True
)
except Exception:
pass
except Exception as e:
# Never raise from telemetry; surface as warning for visibility
# Never raise from telemetry; diagnostics are debug-only.
try:
self.ap.logger.warning(f'Unexpected telemetry error: {e}', exc_info=True)
self.ap.logger.debug(f'Unexpected telemetry error: {e}', exc_info=True)
except Exception:
pass
+2 -1
View File
@@ -426,5 +426,6 @@ space:
# OAuth authorization page URL (user will be redirected here)
oauth_authorize_url: 'https://space.langbot.app/auth/authorize'
disable_models_service: false
# Disable usage telemetry.
# Optional telemetry is enabled by default. Set true to disable usage,
# heartbeat and execution reporting; restart after changing this setting.
disable_telemetry: false
+12 -8
View File
@@ -431,6 +431,7 @@ class TestHTTPScenarios:
with patch.object(httpx, 'AsyncClient', return_value=mock_client):
await manager.send({'query_id': 'test'})
mock_app.logger.warning.assert_not_called()
mock_app.logger.debug.assert_called()
# Verify debug message contains URL and status
debug_call_args = mock_app.logger.debug.call_args[0][0]
@@ -459,8 +460,9 @@ class TestHTTPScenarios:
with patch.object(httpx, 'AsyncClient', return_value=mock_client):
await manager.send({'query_id': 'test'})
mock_app.logger.warning.assert_called()
warning_call_args = mock_app.logger.warning.call_args[0][0]
mock_app.logger.warning.assert_not_called()
mock_app.logger.debug.assert_called()
warning_call_args = mock_app.logger.debug.call_args[0][0]
assert 'status 500' in warning_call_args
@pytest.mark.asyncio
@@ -488,9 +490,9 @@ class TestHTTPScenarios:
await manager.send({'query_id': 'test'})
# Source code calls warning twice for application errors
assert mock_app.logger.warning.call_count >= 1
assert mock_app.logger.debug.call_count >= 1
# Check that one of the calls contains application error info
all_warnings = [call[0][0] for call in mock_app.logger.warning.call_args_list]
all_warnings = [call[0][0] for call in mock_app.logger.debug.call_args_list]
assert any('400' in w for w in all_warnings), f'No warning contained error code 400: {all_warnings}'
@pytest.mark.asyncio
@@ -516,8 +518,9 @@ class TestHTTPScenarios:
with patch.object(httpx, 'AsyncClient', return_value=mock_client):
await manager.send({'query_id': 'test'})
mock_app.logger.warning.assert_called()
warning_call_args = mock_app.logger.warning.call_args[0][0]
mock_app.logger.warning.assert_not_called()
mock_app.logger.debug.assert_called()
warning_call_args = mock_app.logger.debug.call_args[0][0]
assert 'timed out' in warning_call_args
@pytest.mark.asyncio
@@ -542,7 +545,8 @@ class TestHTTPScenarios:
# Should not raise exception
await manager.send({'query_id': 'test'})
mock_app.logger.warning.assert_called()
mock_app.logger.warning.assert_not_called()
mock_app.logger.debug.assert_called()
@pytest.mark.asyncio
async def test_send_never_raises_exception(self):
@@ -551,7 +555,7 @@ class TestHTTPScenarios:
mock_app = Mock()
# Even logger may fail
mock_app.logger = Mock()
mock_app.logger.warning = Mock(side_effect=Exception('Logger failed'))
mock_app.logger.debug = Mock(side_effect=Exception('Logger failed'))
manager = telemetry.TelemetryManager(mock_app)
manager.telemetry_config = {'url': 'https://example.com'}