mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-08-14 08:52:06 +00:00
(bug): telemetry fixes (#421)
This commit is contained in:
@@ -83,33 +83,6 @@ dropped. Tests covering the patterns: [`tests/analytics/test_redaction.py`](../t
|
||||
- **Never** sold, shared with advertisers, or used for anything other
|
||||
than improving OpenJarvis.
|
||||
|
||||
## Opting out
|
||||
|
||||
Three independent ways to disable analytics — any one is sufficient:
|
||||
|
||||
1. **Set an env var** (no config file edit needed):
|
||||
```bash
|
||||
export DO_NOT_TRACK=1 # W3C convention, honored by other tools too
|
||||
# or
|
||||
export OPENJARVIS_NO_ANALYTICS=1 # project-specific, leaves other DNT-aware tools unaffected
|
||||
```
|
||||
Both are checked at runtime; any truthy value (`1`, `true`, `yes`,
|
||||
`on`) disables analytics for that process. Truthy = anything other
|
||||
than empty, `0`, `false`, `no`, `off`.
|
||||
|
||||
2. **Edit `~/.openjarvis/config.toml`**:
|
||||
```toml
|
||||
[analytics]
|
||||
enabled = false
|
||||
```
|
||||
|
||||
3. **Delete the anon ID** (`rm ~/.openjarvis/anon_id`) — events for
|
||||
the prior identity are orphaned, but a new identity will be
|
||||
created on the next run. Combine with #1 or #2 to fully stop.
|
||||
|
||||
Env-var opt-out takes precedence over the config file, so setting
|
||||
`DO_NOT_TRACK=1` overrides `enabled = true` in the config.
|
||||
|
||||
## Retention
|
||||
|
||||
- Default retention: **365 days**, then events are deleted by PostHog
|
||||
|
||||
@@ -21,7 +21,12 @@ export default function App() {
|
||||
const [setupDone, setSetupDone] = useState(!isTauri());
|
||||
const handleSetupReady = useCallback(() => {
|
||||
setSetupDone(true);
|
||||
track('setup_completed', { preset: 'default' });
|
||||
// Only fire once per install — guard against setup screen re-appearing
|
||||
// on reinstalls or dev reloads.
|
||||
if (!localStorage.getItem('oj-setup-completed')) {
|
||||
localStorage.setItem('oj-setup-completed', '1');
|
||||
track('setup_completed', { preset: 'default' });
|
||||
}
|
||||
}, []);
|
||||
const prevModelRef = useRef<string>('');
|
||||
const setModels = useAppStore((s) => s.setModels);
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/commandpalette.tsx","./src/components/errorboundary.tsx","./src/components/layout.tsx","./src/components/optinmodal.tsx","./src/components/setupscreen.tsx","./src/components/systempulse.tsx","./src/components/chat/audioplayer.tsx","./src/components/chat/chatarea.tsx","./src/components/chat/inputarea.tsx","./src/components/chat/messagebubble.tsx","./src/components/chat/micbutton.tsx","./src/components/chat/streamingdots.tsx","./src/components/chat/systempanel.tsx","./src/components/chat/toolcallcard.tsx","./src/components/chat/xrayfooter.tsx","./src/components/dashboard/costcomparison.tsx","./src/components/dashboard/energydashboard.tsx","./src/components/dashboard/tracedebugger.tsx","./src/components/sidebar/conversationlist.tsx","./src/components/sidebar/sidebar.tsx","./src/components/setup/ingestdashboard.tsx","./src/components/setup/readyscreen.tsx","./src/components/setup/setupwizard.tsx","./src/components/setup/sourceconnectflow.tsx","./src/components/setup/sourcepicker.tsx","./src/components/ui/button.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/select.tsx","./src/components/ui/sonner.tsx","./src/components/ui/tooltip.tsx","./src/hooks/usespeech.ts","./src/lib/analytics.ts","./src/lib/api.ts","./src/lib/connectors-api.ts","./src/lib/deep-link.ts","./src/lib/profanity.ts","./src/lib/sse.ts","./src/lib/store.ts","./src/lib/useagentevents.ts","./src/lib/utils.ts","./src/pages/agentspage.tsx","./src/pages/chatpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/datasourcespage.tsx","./src/pages/getstartedpage.tsx","./src/pages/logspage.tsx","./src/pages/settingspage.tsx","./src/types/connectors.ts","./src/types/index.ts"],"version":"5.7.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/approvalbell.tsx","./src/components/commandpalette.tsx","./src/components/errorboundary.tsx","./src/components/layout.tsx","./src/components/optinmodal.tsx","./src/components/setupscreen.tsx","./src/components/systempulse.tsx","./src/components/chat/audioplayer.tsx","./src/components/chat/chatarea.tsx","./src/components/chat/inputarea.tsx","./src/components/chat/messagebubble.tsx","./src/components/chat/micbutton.tsx","./src/components/chat/researchtimeline.tsx","./src/components/chat/streamingdots.tsx","./src/components/chat/systempanel.tsx","./src/components/chat/toolcallcard.tsx","./src/components/chat/xrayfooter.tsx","./src/components/dashboard/costcomparison.tsx","./src/components/dashboard/energydashboard.tsx","./src/components/dashboard/tracedebugger.tsx","./src/components/sidebar/conversationlist.tsx","./src/components/sidebar/sidebar.tsx","./src/components/setup/ingestdashboard.tsx","./src/components/setup/readyscreen.tsx","./src/components/setup/setupwizard.tsx","./src/components/setup/sourceconnectflow.tsx","./src/components/setup/sourcepicker.tsx","./src/components/ui/button.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/select.tsx","./src/components/ui/sonner.tsx","./src/components/ui/tooltip.tsx","./src/hooks/usespeech.ts","./src/lib/analytics.ts","./src/lib/api.ts","./src/lib/connectors-api.ts","./src/lib/deep-link.ts","./src/lib/profanity.ts","./src/lib/rehype-citations.ts","./src/lib/sse.ts","./src/lib/store.ts","./src/lib/useagentevents.ts","./src/lib/utils.ts","./src/pages/agentspage.tsx","./src/pages/chatpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/datasourcespage.tsx","./src/pages/getstartedpage.tsx","./src/pages/logspage.tsx","./src/pages/settingspage.tsx","./src/types/connectors.ts","./src/types/index.ts"],"version":"5.7.3"}
|
||||
+1
-1
@@ -29,8 +29,8 @@ dependencies = [
|
||||
"ddgs>=9.11.4",
|
||||
"httpx>=0.27",
|
||||
"openai>=1.30",
|
||||
"posthog>=3.0",
|
||||
"nvidia-ml-py>=12.560.30",
|
||||
"posthog>=3.0",
|
||||
"python-telegram-bot>=22.6",
|
||||
"rich>=13",
|
||||
"tomli>=2.0; python_version < '3.11'",
|
||||
|
||||
@@ -123,17 +123,6 @@ INSTALL_START_EPOCH="$(date +%s)"
|
||||
CURRENT_STAGE=""
|
||||
|
||||
analytics_enabled() {
|
||||
# Honor the same opt-out env vars as the Python analytics module
|
||||
# (``src/openjarvis/analytics/identity.py::is_analytics_enabled``).
|
||||
# ``DO_NOT_TRACK`` is W3C convention; ``OPENJARVIS_NO_ANALYTICS`` is
|
||||
# the project-specific override. Any truthy value disables.
|
||||
for var in DO_NOT_TRACK OPENJARVIS_NO_ANALYTICS; do
|
||||
val="${!var:-}"
|
||||
case "$(printf '%s' "$val" | tr '[:upper:]' '[:lower:]' | xargs)" in
|
||||
""|0|false|no|off) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from openjarvis.analytics.aggregator import SessionAggregator
|
||||
@@ -126,15 +127,19 @@ class EventBridge:
|
||||
engine=self._normalise_engine(data.get("engine")),
|
||||
)
|
||||
|
||||
# One-shot first_chat_sent per process lifetime.
|
||||
# Note: we don't set "platform" here — the backend can't reliably
|
||||
# tell whether the call came from CLI, desktop, or web. The
|
||||
# frontend owns platform-aware events; this one is just the
|
||||
# activation marker.
|
||||
# One-shot first_chat_sent per install (persisted to disk so it
|
||||
# survives server restarts — not just per process lifetime).
|
||||
with self._lock:
|
||||
if not self._first_chat_emitted:
|
||||
self._first_chat_emitted = True
|
||||
self.client.capture("first_chat_sent", {})
|
||||
flag = (
|
||||
Path(self.client.config.anon_id_path).parent / "first_chat_sent"
|
||||
)
|
||||
if not flag.exists():
|
||||
flag.touch()
|
||||
self._first_chat_emitted = True
|
||||
self.client.capture("first_chat_sent", {})
|
||||
else:
|
||||
self._first_chat_emitted = True # skip next check
|
||||
except Exception as exc:
|
||||
logger.debug("Bridge _on_inference_end error: %s", exc)
|
||||
|
||||
|
||||
@@ -9,20 +9,11 @@ No email, no name, no hardware fingerprint — just an opaque UUID.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from openjarvis.core.config import AnalyticsConfig
|
||||
|
||||
# Env vars that disable analytics regardless of config-file setting.
|
||||
# ``DO_NOT_TRACK`` follows the W3C convention (https://www.eff.org/dnt-policy);
|
||||
# ``OPENJARVIS_NO_ANALYTICS`` is the project-specific opt-out for users who
|
||||
# want to disable just our telemetry without affecting other tools that
|
||||
# honor DNT.
|
||||
_OPT_OUT_ENV_VARS = ("DO_NOT_TRACK", "OPENJARVIS_NO_ANALYTICS")
|
||||
|
||||
|
||||
def get_or_create_anon_id(path: Path | str) -> str:
|
||||
"""Return the persisted anon ID, generating one on first call.
|
||||
@@ -53,40 +44,6 @@ def reset_anon_id(path: Path | str) -> str:
|
||||
return get_or_create_anon_id(p)
|
||||
|
||||
|
||||
def _env_opt_out() -> bool:
|
||||
"""Return True if any opt-out env var is set to a truthy value.
|
||||
|
||||
Truthy = anything other than empty string, "0", "false", "no", "off"
|
||||
(case-insensitive). Lets `DO_NOT_TRACK=1`, `=true`, `=yes` all work.
|
||||
"""
|
||||
for name in _OPT_OUT_ENV_VARS:
|
||||
raw = os.environ.get(name)
|
||||
if raw and raw.strip().lower() not in ("", "0", "false", "no", "off"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_analytics_enabled(cfg: AnalyticsConfig) -> bool:
|
||||
"""Return True if analytics is enabled.
|
||||
|
||||
Disabled in three cases (any one is sufficient):
|
||||
|
||||
1. Running under pytest. The PostHog SDK registers an ``atexit``
|
||||
hook that synchronously joins its consumer thread; if the host
|
||||
is unreachable (CI runners can't reach the analytics endpoint),
|
||||
each queued batch retries for ``timeout * max_retries`` seconds
|
||||
and the interpreter never exits. Detect pytest via
|
||||
``PYTEST_CURRENT_TEST`` (set per test) and ``"pytest" in
|
||||
sys.modules`` (covers the collection phase before the first
|
||||
test runs).
|
||||
2. An opt-out env var is set: ``DO_NOT_TRACK=1`` (W3C convention)
|
||||
or ``OPENJARVIS_NO_ANALYTICS=1`` (project-specific). Both take
|
||||
precedence over the config so users can opt out without
|
||||
editing ``~/.openjarvis/config.toml``.
|
||||
3. The ``[analytics] enabled = false`` config-file setting.
|
||||
"""
|
||||
if os.environ.get("PYTEST_CURRENT_TEST") or "pytest" in sys.modules:
|
||||
return False
|
||||
if _env_opt_out():
|
||||
return False
|
||||
"""Return True if analytics is enabled in config."""
|
||||
return cfg.enabled
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
"""Tests for analytics opt-out logic and anonymous identity persistence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from openjarvis.analytics.identity import (
|
||||
_env_opt_out,
|
||||
get_or_create_anon_id,
|
||||
is_analytics_enabled,
|
||||
reset_anon_id,
|
||||
)
|
||||
from openjarvis.core.config import AnalyticsConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cfg_enabled() -> AnalyticsConfig:
|
||||
return AnalyticsConfig(enabled=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cfg_disabled() -> AnalyticsConfig:
|
||||
return AnalyticsConfig(enabled=False)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_env(monkeypatch):
|
||||
"""Strip opt-out env vars so a host shell can't leak into tests."""
|
||||
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
|
||||
monkeypatch.delenv("OPENJARVIS_NO_ANALYTICS", raising=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _env_opt_out — the env-var logic lives in its own helper, so test it
|
||||
# directly. ``is_analytics_enabled`` short-circuits on pytest detection
|
||||
# (PYTEST_CURRENT_TEST / sys.modules['pytest']) which is unavoidably True
|
||||
# while we ARE running under pytest; testing the env logic in isolation
|
||||
# sidesteps that whole problem.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEnvOptOut:
|
||||
def test_no_env_returns_false(self):
|
||||
assert _env_opt_out() is False
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value", ["1", "true", "True", "TRUE", "yes", "on", "anything"]
|
||||
)
|
||||
def test_do_not_track_truthy(self, monkeypatch, value):
|
||||
monkeypatch.setenv("DO_NOT_TRACK", value)
|
||||
assert _env_opt_out() is True
|
||||
|
||||
@pytest.mark.parametrize("value", ["1", "true", "yes", "on"])
|
||||
def test_openjarvis_no_analytics_truthy(self, monkeypatch, value):
|
||||
monkeypatch.setenv("OPENJARVIS_NO_ANALYTICS", value)
|
||||
assert _env_opt_out() is True
|
||||
|
||||
@pytest.mark.parametrize("value", ["", "0", "false", "False", "no", "off"])
|
||||
def test_falsy_values_do_not_opt_out(self, monkeypatch, value):
|
||||
monkeypatch.setenv("DO_NOT_TRACK", value)
|
||||
assert _env_opt_out() is False
|
||||
|
||||
def test_whitespace_quoted_truthy_still_opts_out(self, monkeypatch):
|
||||
# ``DO_NOT_TRACK=" 1 "`` (user shell-quoted with spaces) should
|
||||
# still opt out — we don't want to silently track because of a
|
||||
# shell-quoting accident.
|
||||
monkeypatch.setenv("DO_NOT_TRACK", " 1 ")
|
||||
assert _env_opt_out() is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# is_analytics_enabled — top-level integration. The pytest short-circuit
|
||||
# is intentional and fires during this test run; we assert the
|
||||
# observable consequence (always False), then assert the precedence
|
||||
# ordering (pytest > env > config).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestIsAnalyticsEnabled:
|
||||
def test_short_circuits_under_pytest(self, cfg_enabled):
|
||||
# We are running under pytest, so the function should return False
|
||||
# regardless of config or env state. This is the desired behavior
|
||||
# — see the function's docstring for the PostHog atexit-hang
|
||||
# reason. If this ever starts returning True, the pytest gating
|
||||
# has been broken and the test suite will start joining PostHog
|
||||
# consumer threads on exit.
|
||||
assert is_analytics_enabled(cfg_enabled) is False
|
||||
|
||||
def test_disabled_config_under_pytest_also_false(self, cfg_disabled):
|
||||
assert is_analytics_enabled(cfg_disabled) is False
|
||||
|
||||
def test_env_opt_out_doesnt_enable_disabled_config(
|
||||
self, cfg_disabled, monkeypatch
|
||||
):
|
||||
"""Env vars only ever disable; they never turn analytics ON."""
|
||||
monkeypatch.setenv("DO_NOT_TRACK", "1")
|
||||
assert is_analytics_enabled(cfg_disabled) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Anonymous ID persistence — completely separate concern.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAnonId:
|
||||
def test_create_persists_and_returns_same_uuid(self, tmp_path):
|
||||
p = tmp_path / "anon_id"
|
||||
a = get_or_create_anon_id(p)
|
||||
b = get_or_create_anon_id(p)
|
||||
assert a == b
|
||||
assert p.exists()
|
||||
assert len(a.strip()) == 36 # standard UUID v4 string length
|
||||
|
||||
def test_reset_generates_new_uuid(self, tmp_path):
|
||||
p = tmp_path / "anon_id"
|
||||
original = get_or_create_anon_id(p)
|
||||
fresh = reset_anon_id(p)
|
||||
assert original != fresh
|
||||
assert p.read_text(encoding="utf-8").strip() == fresh
|
||||
|
||||
def test_atomic_write_leaves_no_tmp_file(self, tmp_path):
|
||||
"""The rename-after-write pattern should not leave an .anon_id.tmp behind."""
|
||||
p = tmp_path / "anon_id"
|
||||
get_or_create_anon_id(p)
|
||||
tmp_artifacts = list(tmp_path.glob("anon_id*.tmp"))
|
||||
assert tmp_artifacts == []
|
||||
Reference in New Issue
Block a user