fix: disable ph telemetry still calls posthog (#4203)
This commit is contained in:
@@ -7,6 +7,11 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-03-03" description="v1.0.5">
|
||||
- **Telemetry Fix**
|
||||
- Fixed an issue where the PostHog client was initialized even after telemetry was disabled. Although events were not captured, the client was unnecessarily initialized.
|
||||
</Update>
|
||||
|
||||
<Update label="2026-02-17" description="v1.0.4">
|
||||
|
||||
**New Features & Updates:**
|
||||
|
||||
@@ -24,14 +24,18 @@ logging.getLogger("urllib3").setLevel(logging.CRITICAL + 1)
|
||||
|
||||
class AnonymousTelemetry:
|
||||
def __init__(self, vector_store=None):
|
||||
self.posthog = Posthog(project_api_key=PROJECT_API_KEY, host=HOST)
|
||||
if not MEM0_TELEMETRY:
|
||||
self.posthog = None
|
||||
self.user_id = None
|
||||
return
|
||||
|
||||
self.posthog = Posthog(project_api_key=PROJECT_API_KEY, host=HOST)
|
||||
self.user_id = get_or_create_user_id(vector_store)
|
||||
|
||||
if not MEM0_TELEMETRY:
|
||||
self.posthog.disabled = True
|
||||
|
||||
def capture_event(self, event_name, properties=None, user_email=None):
|
||||
if self.posthog is None:
|
||||
return
|
||||
|
||||
if properties is None:
|
||||
properties = {}
|
||||
properties = {
|
||||
@@ -49,13 +53,17 @@ class AnonymousTelemetry:
|
||||
self.posthog.capture(distinct_id=distinct_id, event=event_name, properties=properties)
|
||||
|
||||
def close(self):
|
||||
self.posthog.shutdown()
|
||||
if self.posthog is not None:
|
||||
self.posthog.shutdown()
|
||||
|
||||
|
||||
client_telemetry = AnonymousTelemetry()
|
||||
|
||||
|
||||
def capture_event(event_name, memory_instance, additional_data=None):
|
||||
if not MEM0_TELEMETRY:
|
||||
return
|
||||
|
||||
oss_telemetry = AnonymousTelemetry(
|
||||
vector_store=memory_instance._telemetry_vector_store
|
||||
if hasattr(memory_instance, "_telemetry_vector_store")
|
||||
@@ -81,6 +89,9 @@ def capture_event(event_name, memory_instance, additional_data=None):
|
||||
|
||||
|
||||
def capture_client_event(event_name, instance, additional_data=None):
|
||||
if not MEM0_TELEMETRY:
|
||||
return
|
||||
|
||||
event_data = {
|
||||
"function": f"{instance.__class__.__module__}.{instance.__class__.__name__}",
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0ai"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
description = "Long-term memory for AI Agents"
|
||||
authors = [
|
||||
{ name = "Mem0", email = "founders@mem0.ai" }
|
||||
|
||||
+104
-25
@@ -1,35 +1,114 @@
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
import threading
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
MEM0_TELEMETRY = os.environ.get("MEM0_TELEMETRY", "True")
|
||||
|
||||
if isinstance(MEM0_TELEMETRY, str):
|
||||
MEM0_TELEMETRY = MEM0_TELEMETRY.lower() in ("true", "1", "yes")
|
||||
import mem0.memory.telemetry as telemetry_module
|
||||
|
||||
|
||||
def use_telemetry():
|
||||
if os.getenv("MEM0_TELEMETRY", "true").lower() == "true":
|
||||
return True
|
||||
return False
|
||||
class TestTelemetryDisabled:
|
||||
"""Verify PostHog is never instantiated when telemetry is disabled."""
|
||||
|
||||
def test_posthog_not_created_when_disabled(self):
|
||||
"""Posthog() constructor should never be called when MEM0_TELEMETRY=False."""
|
||||
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||
with patch("mem0.memory.telemetry.Posthog") as mock_posthog:
|
||||
at = telemetry_module.AnonymousTelemetry()
|
||||
mock_posthog.assert_not_called()
|
||||
assert at.posthog is None
|
||||
assert at.user_id is None
|
||||
|
||||
def test_capture_event_noop_when_disabled(self):
|
||||
"""capture_event() should return immediately without creating AnonymousTelemetry."""
|
||||
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||
with patch("mem0.memory.telemetry.AnonymousTelemetry") as mock_cls:
|
||||
telemetry_module.capture_event("test.event", MagicMock())
|
||||
mock_cls.assert_not_called()
|
||||
|
||||
def test_capture_client_event_noop_when_disabled(self):
|
||||
"""capture_client_event() should return immediately without calling posthog."""
|
||||
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||
mock_instance = MagicMock()
|
||||
mock_client_telemetry = MagicMock()
|
||||
with patch.object(telemetry_module, "client_telemetry", mock_client_telemetry):
|
||||
telemetry_module.capture_client_event("test.event", mock_instance)
|
||||
mock_client_telemetry.capture_event.assert_not_called()
|
||||
|
||||
def test_instance_capture_event_noop_when_posthog_is_none(self):
|
||||
"""AnonymousTelemetry.capture_event() should be a no-op when posthog is None."""
|
||||
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||
at = telemetry_module.AnonymousTelemetry()
|
||||
at.capture_event("test.event", {"key": "value"}) # should not raise
|
||||
|
||||
def test_close_noop_when_posthog_is_none(self):
|
||||
"""close() should not raise when posthog is None."""
|
||||
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||
at = telemetry_module.AnonymousTelemetry()
|
||||
at.close() # should not raise
|
||||
|
||||
def test_no_threads_spawned_when_disabled(self):
|
||||
"""No consumer threads should be created when telemetry is disabled."""
|
||||
with patch.object(telemetry_module, "MEM0_TELEMETRY", False):
|
||||
threads_before = threading.active_count()
|
||||
telemetry_module.AnonymousTelemetry()
|
||||
threads_after = threading.active_count()
|
||||
assert threads_after == threads_before
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_env():
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
yield
|
||||
class TestTelemetryEnabled:
|
||||
"""Verify PostHog works normally when telemetry is enabled."""
|
||||
|
||||
def test_posthog_created_when_enabled(self):
|
||||
"""Posthog() should be instantiated when MEM0_TELEMETRY=True."""
|
||||
with patch.object(telemetry_module, "MEM0_TELEMETRY", True):
|
||||
with patch("mem0.memory.telemetry.Posthog") as mock_posthog:
|
||||
with patch("mem0.memory.telemetry.get_or_create_user_id", return_value="test-user"):
|
||||
at = telemetry_module.AnonymousTelemetry()
|
||||
mock_posthog.assert_called_once()
|
||||
assert at.posthog is not None
|
||||
assert at.user_id == "test-user"
|
||||
|
||||
def test_capture_event_sends_when_enabled(self):
|
||||
"""capture_event() should create AnonymousTelemetry and call capture when enabled."""
|
||||
with patch.object(telemetry_module, "MEM0_TELEMETRY", True):
|
||||
with patch("mem0.memory.telemetry.AnonymousTelemetry") as mock_cls:
|
||||
mock_at = MagicMock()
|
||||
mock_cls.return_value = mock_at
|
||||
mock_memory = MagicMock()
|
||||
mock_memory.config.graph_store.config = None
|
||||
mock_memory.api_version = "v1"
|
||||
telemetry_module.capture_event("test.event", mock_memory)
|
||||
mock_at.capture_event.assert_called_once()
|
||||
|
||||
def test_capture_client_event_sends_when_enabled(self):
|
||||
"""capture_client_event() should call client_telemetry.capture_event when enabled."""
|
||||
with patch.object(telemetry_module, "MEM0_TELEMETRY", True):
|
||||
mock_client_telemetry = MagicMock()
|
||||
with patch.object(telemetry_module, "client_telemetry", mock_client_telemetry):
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.user_email = "test@example.com"
|
||||
telemetry_module.capture_client_event("test.event", mock_instance)
|
||||
mock_client_telemetry.capture_event.assert_called_once()
|
||||
|
||||
|
||||
def test_telemetry_enabled():
|
||||
with patch.dict(os.environ, {"MEM0_TELEMETRY": "true"}):
|
||||
assert use_telemetry() is True
|
||||
class TestTelemetryEnvVar:
|
||||
"""Verify the MEM0_TELEMETRY env var parsing logic."""
|
||||
|
||||
|
||||
def test_telemetry_disabled():
|
||||
with patch.dict(os.environ, {"MEM0_TELEMETRY": "false"}):
|
||||
assert use_telemetry() is False
|
||||
|
||||
|
||||
def test_telemetry_default_enabled():
|
||||
assert use_telemetry() is True
|
||||
@pytest.mark.parametrize(
|
||||
"value,expected",
|
||||
[
|
||||
("true", True),
|
||||
("True", True),
|
||||
("TRUE", True),
|
||||
("1", True),
|
||||
("yes", True),
|
||||
("false", False),
|
||||
("False", False),
|
||||
("0", False),
|
||||
("no", False),
|
||||
("anything_else", False),
|
||||
],
|
||||
)
|
||||
def test_env_var_parsing(self, value, expected):
|
||||
result = value.lower() in ("true", "1", "yes")
|
||||
assert result == expected
|
||||
|
||||
Reference in New Issue
Block a user