mirror of
https://github.com/langgenius/dify.git
synced 2026-07-27 23:18:33 +08:00
334 lines
12 KiB
Python
334 lines
12 KiB
Python
import uuid
|
|
from datetime import datetime
|
|
from unittest.mock import Mock
|
|
|
|
import httpx
|
|
import pytest
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from models.model import DifySetup
|
|
from services import telemetry_service
|
|
from services.telemetry_service import CommunityTelemetryService
|
|
|
|
|
|
@pytest.fixture
|
|
def telemetry_enabled(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr(telemetry_service.dify_config, "EDITION", "SELF_HOSTED")
|
|
monkeypatch.setattr(telemetry_service.dify_config, "ENTERPRISE_ENABLED", False)
|
|
monkeypatch.setattr(telemetry_service.dify_config, "DISABLE_TELEMETRY", False)
|
|
monkeypatch.setattr(telemetry_service.dify_config, "DO_NOT_TRACK", False)
|
|
monkeypatch.setattr(telemetry_service.dify_config, "CI", False)
|
|
monkeypatch.setattr(telemetry_service.dify_config, "TELEMETRY_ENDPOINT", "https://telemetry.example.test/v1/events")
|
|
monkeypatch.setattr(
|
|
telemetry_service.dify_config,
|
|
"TELEMETRY_FALLBACK_ENDPOINT",
|
|
"https://telemetry-cn.example.test/v1/events",
|
|
)
|
|
monkeypatch.setattr(telemetry_service.dify_config, "TELEMETRY_TIMEOUT_SECONDS", 2)
|
|
|
|
|
|
def test_telemetry_is_disabled_for_enterprise(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr(telemetry_service.dify_config, "EDITION", "SELF_HOSTED")
|
|
monkeypatch.setattr(telemetry_service.dify_config, "ENTERPRISE_ENABLED", True)
|
|
|
|
assert CommunityTelemetryService._is_enabled() is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("setting", "value"),
|
|
[
|
|
("EDITION", "CLOUD"),
|
|
("DISABLE_TELEMETRY", True),
|
|
("DO_NOT_TRACK", True),
|
|
("CI", True),
|
|
("TELEMETRY_ENDPOINT", ""),
|
|
],
|
|
)
|
|
def test_telemetry_is_disabled_when_a_required_condition_is_not_met(
|
|
telemetry_enabled, monkeypatch: pytest.MonkeyPatch, setting: str, value: str | bool
|
|
):
|
|
monkeypatch.setattr(telemetry_service.dify_config, setting, value)
|
|
|
|
assert CommunityTelemetryService._is_enabled() is False
|
|
|
|
|
|
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
|
def test_reporting_without_setup_is_skipped(sqlite_session: Session, telemetry_enabled):
|
|
assert CommunityTelemetryService.report_install(session=sqlite_session) is False
|
|
assert CommunityTelemetryService.report_heartbeat(session=sqlite_session) is False
|
|
|
|
|
|
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
|
def test_report_install_marks_reported_at(sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch):
|
|
setup = DifySetup(version="installed-version", instance_id="d246c3a1-350b-406c-92c7-6043df680758")
|
|
sqlite_session.add(setup)
|
|
sqlite_session.commit()
|
|
monkeypatch.setattr(telemetry_service.dify_config.project, "version", "running-version")
|
|
|
|
sent_payloads: list[dict[str, str | int]] = []
|
|
|
|
def fake_post(url: str, json: dict[str, str | int], timeout: int):
|
|
sent_payloads.append(json)
|
|
return httpx.Response(204, request=httpx.Request("POST", url))
|
|
|
|
monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)
|
|
|
|
assert CommunityTelemetryService.report_install(session=sqlite_session) is True
|
|
|
|
saved_setup = sqlite_session.scalar(select(DifySetup))
|
|
assert saved_setup is not None
|
|
assert saved_setup.install_reported_at is not None
|
|
assert sent_payloads[0]["event"] == "install"
|
|
assert sent_payloads[0]["instance_id"] == setup.instance_id
|
|
assert sent_payloads[0]["version"] == "installed-version"
|
|
assert "installed_at" in sent_payloads[0]
|
|
|
|
|
|
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
|
def test_report_install_generates_missing_instance_id(
|
|
sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
setup = DifySetup(version="installed-version")
|
|
sqlite_session.add(setup)
|
|
sqlite_session.commit()
|
|
monkeypatch.setattr(
|
|
telemetry_service.httpx,
|
|
"post",
|
|
lambda url, json, timeout: httpx.Response(204, request=httpx.Request("POST", url)),
|
|
)
|
|
|
|
assert CommunityTelemetryService.report_install(session=sqlite_session) is True
|
|
|
|
assert setup.instance_id is not None
|
|
assert str(uuid.UUID(setup.instance_id)) == setup.instance_id
|
|
|
|
|
|
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
|
def test_report_heartbeat_generates_missing_instance_id(
|
|
sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
setup = DifySetup(version="1.0.0", install_reported_at=datetime(2026, 7, 12, 8, 0, 0))
|
|
sqlite_session.add(setup)
|
|
sqlite_session.commit()
|
|
monkeypatch.setattr(
|
|
telemetry_service.httpx,
|
|
"post",
|
|
lambda url, json, timeout: httpx.Response(204, request=httpx.Request("POST", url)),
|
|
)
|
|
|
|
assert (
|
|
CommunityTelemetryService.report_heartbeat(session=sqlite_session, now=datetime(2026, 7, 13, 12, 0, 0)) is True
|
|
)
|
|
|
|
assert setup.instance_id is not None
|
|
assert str(uuid.UUID(setup.instance_id)) == setup.instance_id
|
|
|
|
|
|
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
|
def test_report_install_failure_keeps_install_pending(
|
|
sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
setup = DifySetup(version="1.0.0", instance_id="d246c3a1-350b-406c-92c7-6043df680758")
|
|
sqlite_session.add(setup)
|
|
sqlite_session.commit()
|
|
|
|
def fake_post(url: str, json: dict[str, str | int], timeout: int):
|
|
raise httpx.ConnectError("offline", request=httpx.Request("POST", url))
|
|
|
|
monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)
|
|
|
|
assert CommunityTelemetryService.report_install(session=sqlite_session) is False
|
|
|
|
saved_setup = sqlite_session.scalar(select(DifySetup))
|
|
assert saved_setup is not None
|
|
assert saved_setup.install_reported_at is None
|
|
|
|
|
|
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
|
def test_report_install_uses_fallback_endpoint_after_network_failure(
|
|
sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
setup = DifySetup(version="1.0.0", instance_id="d246c3a1-350b-406c-92c7-6043df680758")
|
|
sqlite_session.add(setup)
|
|
sqlite_session.commit()
|
|
|
|
urls: list[str] = []
|
|
|
|
def fake_post(url: str, json: dict[str, str | int], timeout: int):
|
|
urls.append(url)
|
|
if url == telemetry_service.dify_config.TELEMETRY_ENDPOINT:
|
|
raise httpx.ConnectError("offline", request=httpx.Request("POST", url))
|
|
return httpx.Response(204, request=httpx.Request("POST", url))
|
|
|
|
monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)
|
|
|
|
assert CommunityTelemetryService.report_install(session=sqlite_session) is True
|
|
assert urls == [
|
|
telemetry_service.dify_config.TELEMETRY_ENDPOINT,
|
|
telemetry_service.dify_config.TELEMETRY_FALLBACK_ENDPOINT,
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
|
def test_report_install_does_not_use_fallback_endpoint_after_http_error(
|
|
sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
setup = DifySetup(version="1.0.0", instance_id="d246c3a1-350b-406c-92c7-6043df680758")
|
|
sqlite_session.add(setup)
|
|
sqlite_session.commit()
|
|
|
|
post_mock = Mock(
|
|
return_value=httpx.Response(
|
|
500,
|
|
request=httpx.Request("POST", telemetry_service.dify_config.TELEMETRY_ENDPOINT),
|
|
)
|
|
)
|
|
monkeypatch.setattr(telemetry_service.httpx, "post", post_mock)
|
|
|
|
assert CommunityTelemetryService.report_install(session=sqlite_session) is False
|
|
post_mock.assert_called_once()
|
|
|
|
|
|
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
|
def test_report_heartbeat_retries_pending_install_before_heartbeat(
|
|
sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
setup = DifySetup(version="installed-version", instance_id="d246c3a1-350b-406c-92c7-6043df680758")
|
|
sqlite_session.add(setup)
|
|
sqlite_session.commit()
|
|
monkeypatch.setattr(telemetry_service.dify_config.project, "version", "running-version")
|
|
|
|
sent_payloads: list[dict[str, str | int]] = []
|
|
|
|
def fake_post(url: str, json: dict[str, str | int], timeout: int):
|
|
sent_payloads.append(json)
|
|
return httpx.Response(204, request=httpx.Request("POST", url))
|
|
|
|
monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)
|
|
now = datetime(2026, 7, 13, 0, 0, 0)
|
|
assert CommunityTelemetryService.report_heartbeat(session=sqlite_session, now=now) is True
|
|
|
|
saved_setup = sqlite_session.scalar(select(DifySetup))
|
|
assert saved_setup is not None
|
|
assert saved_setup.install_reported_at is not None
|
|
assert saved_setup.last_heartbeat_at == now
|
|
assert [(payload["event"], payload["version"]) for payload in sent_payloads] == [
|
|
("install", "installed-version"),
|
|
("heartbeat", "running-version"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
|
def test_report_heartbeat_skips_when_already_sent_today(
|
|
sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
setup = DifySetup(
|
|
version="1.0.0",
|
|
instance_id="d246c3a1-350b-406c-92c7-6043df680758",
|
|
install_reported_at=datetime(2026, 7, 13, 8, 0, 0),
|
|
last_heartbeat_at=datetime(2026, 7, 13, 9, 0, 0),
|
|
)
|
|
sqlite_session.add(setup)
|
|
sqlite_session.commit()
|
|
|
|
post_mock = Mock()
|
|
monkeypatch.setattr(telemetry_service.httpx, "post", post_mock)
|
|
|
|
assert (
|
|
CommunityTelemetryService.report_heartbeat(session=sqlite_session, now=datetime(2026, 7, 13, 12, 0, 0)) is False
|
|
)
|
|
post_mock.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
|
def test_report_heartbeat_failure_does_not_mark_the_day_reported(
|
|
sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
setup = DifySetup(
|
|
version="1.0.0",
|
|
instance_id="d246c3a1-350b-406c-92c7-6043df680758",
|
|
install_reported_at=datetime(2026, 7, 13, 8, 0, 0),
|
|
)
|
|
sqlite_session.add(setup)
|
|
sqlite_session.commit()
|
|
|
|
def fake_post(url: str, json: dict[str, str | int], timeout: int):
|
|
raise httpx.ConnectError("offline", request=httpx.Request("POST", url))
|
|
|
|
monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)
|
|
|
|
assert (
|
|
CommunityTelemetryService.report_heartbeat(session=sqlite_session, now=datetime(2026, 7, 13, 12, 0, 0)) is False
|
|
)
|
|
assert setup.last_heartbeat_at is None
|
|
|
|
|
|
def test_send_event_skips_when_telemetry_is_disabled(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr(telemetry_service.dify_config, "DISABLE_TELEMETRY", True)
|
|
post_mock = Mock()
|
|
monkeypatch.setattr(telemetry_service.httpx, "post", post_mock)
|
|
|
|
assert CommunityTelemetryService._send_event({"event": "heartbeat"}) is False
|
|
post_mock.assert_not_called()
|
|
|
|
|
|
def test_send_event_skips_an_empty_fallback_endpoint(telemetry_enabled, monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr(telemetry_service.dify_config, "TELEMETRY_FALLBACK_ENDPOINT", "")
|
|
|
|
def fake_post(url: str, json: dict[str, str], timeout: int):
|
|
raise httpx.ConnectError("offline", request=httpx.Request("POST", url))
|
|
|
|
monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)
|
|
|
|
assert CommunityTelemetryService._send_event({"event": "heartbeat"}) is False
|
|
|
|
|
|
def test_send_event_does_not_retry_the_same_endpoint(telemetry_enabled, monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr(
|
|
telemetry_service.dify_config,
|
|
"TELEMETRY_FALLBACK_ENDPOINT",
|
|
telemetry_service.dify_config.TELEMETRY_ENDPOINT,
|
|
)
|
|
post_mock = Mock(
|
|
return_value=httpx.Response(
|
|
204,
|
|
request=httpx.Request("POST", telemetry_service.dify_config.TELEMETRY_ENDPOINT),
|
|
)
|
|
)
|
|
monkeypatch.setattr(telemetry_service.httpx, "post", post_mock)
|
|
|
|
assert CommunityTelemetryService._send_event({"event": "heartbeat"}) is True
|
|
post_mock.assert_called_once()
|
|
|
|
|
|
def test_heartbeat_is_not_due_without_instance_id():
|
|
setup = DifySetup(version="1.0.0")
|
|
|
|
assert CommunityTelemetryService._is_heartbeat_due(setup, datetime(2026, 7, 13, 12, 0, 0)) is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "expected"),
|
|
[
|
|
("Linux", "linux"),
|
|
("Plan9", "unknown"),
|
|
],
|
|
)
|
|
def test_normalize_os(value: str, expected: str):
|
|
assert CommunityTelemetryService._normalize_os(value) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "expected"),
|
|
[
|
|
("x86_64", "amd64"),
|
|
("aarch64", "arm64"),
|
|
("armv7l", "arm"),
|
|
("i686", "386"),
|
|
("riscv64", "unknown"),
|
|
],
|
|
)
|
|
def test_normalize_arch(value: str, expected: str):
|
|
assert CommunityTelemetryService._normalize_arch(value) == expected
|