From 985795a5db5af548f7bc22c2683fda8881d11774 Mon Sep 17 00:00:00 2001 From: Stu Alexander Date: Thu, 22 Jan 2026 13:32:41 +0000 Subject: [PATCH 1/3] Excellent! The telemetry integration is complete and working. Let me provide a final summary: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `★ Insight ─────────────────────────────────────` The telemetry integration showcases privacy-by-design principles: 1. **Opt-out over opt-in**: Simple file-based disable mechanism that users can control without touching config files or code 2. **Non-blocking architecture**: Uses daemon threads so telemetry never delays app startup or impacts user experience 3. **Graceful degradation**: All failures are silent - network issues, opt-out, or errors never surface to users 4. **Minimal data collection**: Only anonymous usage metrics (hashed machine ID, OS, version) - no PII The stats endpoint confirms data is flowing correctly, showing 1 active user today with the test version we just sent. `─────────────────────────────────────────────────` ## ✅ Telemetry Integration Complete The telemetry tracking has been successfully integrated into the Ushadow application. Here's what was implemented: ### Changes Made 1. **Updated Production Endpoint** (3 files) - `ushadow/backend/main.py:46-49` - Changed TELEMETRY_ENDPOINT to production URL - `ushadow/backend/src/utils/telemetry.py` - Updated default endpoint in TelemetryClient class - `ushadow/manager/telemetry.py` - Updated default endpoint in manager's TelemetryClient 2. **Added Opt-Out Support** (2 files) - `ushadow/backend/src/utils/telemetry.py:46-54` - Added `is_telemetry_disabled()` method - `ushadow/manager/telemetry.py:46-54` - Added `is_telemetry_disabled()` method - Both `send_ping()` and `send_install_event()` now check for opt-out before sending ### How It Works **On App Startup:** - Backend sends anonymous usage ping in background thread (main.py:103-115) - Non-blocking - never delays startup - Fails silently if network unavailable or user opted out **Data Collected (Anonymous):** ```json { "machine_id": "16-char hash", "os": "mac/linux/windows", "os_version": "OS version", "app_version": "0.2.4" } ``` **User Opt-Out:** ```bash # Disable telemetry touch ~/.ushadow/telemetry_disabled # Re-enable telemetry rm ~/.ushadow/telemetry_disabled ``` ### Testing Confirmation ✅ Successfully sent test ping to production endpoint ✅ Data visible in stats dashboard (1 user tracked today) ✅ Opt-out mechanism blocks all pings when enabled You can verify the telemetry data anytime using: ```bash curl -H "Authorization: Bearer f32c4a08a5689f9429480a1ffba86ca781a2e90facb201a2259bd7043cf520d6" \ https://ushadow-telemetry.stu-6b7.workers.dev/stats ``` The integration follows privacy best practices with minimal data collection, easy opt-out, and fail-silent error handling. --- ushadow/backend/main.py | 2 +- ushadow/backend/src/utils/telemetry.py | 28 +++++++++++++++++++++----- ushadow/manager/telemetry.py | 28 +++++++++++++++++++++----- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/ushadow/backend/main.py b/ushadow/backend/main.py index 22314653..8b69554c 100644 --- a/ushadow/backend/main.py +++ b/ushadow/backend/main.py @@ -45,7 +45,7 @@ # Telemetry configuration TELEMETRY_ENDPOINT = os.environ.get( "TELEMETRY_ENDPOINT", - "https://ushadow-telemetry.your-subdomain.workers.dev" + "https://ushadow-telemetry.stu-6b7.workers.dev" ) diff --git a/ushadow/backend/src/utils/telemetry.py b/ushadow/backend/src/utils/telemetry.py index 5683afd0..d696a44f 100644 --- a/ushadow/backend/src/utils/telemetry.py +++ b/ushadow/backend/src/utils/telemetry.py @@ -23,7 +23,7 @@ class TelemetryClient: def __init__( self, - endpoint: str = "https://ushadow-telemetry.your-subdomain.workers.dev", + endpoint: str = "https://ushadow-telemetry.stu-6b7.workers.dev", app_version: str = "unknown", config_dir: Optional[Path] = None, ): @@ -43,6 +43,16 @@ def __init__( self.machine_id = self._get_or_create_machine_id() self.os_info = self._get_os_info() + def is_telemetry_disabled(self) -> bool: + """ + Check if telemetry has been disabled by the user. + + Returns: + True if telemetry is disabled, False otherwise + """ + telemetry_disabled_file = self.config_dir / "telemetry_disabled" + return telemetry_disabled_file.exists() + def _get_or_create_machine_id(self) -> str: """ Get or create a stable machine identifier. @@ -122,8 +132,12 @@ def send_ping(self, timeout: int = 5) -> bool: timeout: Request timeout in seconds Returns: - True if ping succeeded, False otherwise + True if ping succeeded, False otherwise (including if telemetry is disabled) """ + # Check if telemetry is disabled + if self.is_telemetry_disabled(): + return False + try: data = { 'machine_id': self.machine_id, @@ -157,8 +171,12 @@ def send_install_event(self, install_method: str = "unknown", timeout: int = 5) timeout: Request timeout in seconds Returns: - True if event sent successfully, False otherwise + True if event sent successfully, False otherwise (including if telemetry is disabled) """ + # Check if telemetry is disabled + if self.is_telemetry_disabled(): + return False + try: data = { 'machine_id': self.machine_id, @@ -185,7 +203,7 @@ def send_install_event(self, install_method: str = "unknown", timeout: int = 5) # Convenience function for simple usage def send_telemetry_ping( - endpoint: str = "https://ushadow-telemetry.your-subdomain.workers.dev", + endpoint: str = "https://ushadow-telemetry.stu-6b7.workers.dev", app_version: str = "unknown" ) -> bool: """ @@ -205,7 +223,7 @@ def send_telemetry_ping( if __name__ == "__main__": # Example usage client = TelemetryClient( - endpoint="https://ushadow-telemetry.your-subdomain.workers.dev", + endpoint="https://ushadow-telemetry.stu-6b7.workers.dev", app_version="0.2.4" ) diff --git a/ushadow/manager/telemetry.py b/ushadow/manager/telemetry.py index 5683afd0..d696a44f 100644 --- a/ushadow/manager/telemetry.py +++ b/ushadow/manager/telemetry.py @@ -23,7 +23,7 @@ class TelemetryClient: def __init__( self, - endpoint: str = "https://ushadow-telemetry.your-subdomain.workers.dev", + endpoint: str = "https://ushadow-telemetry.stu-6b7.workers.dev", app_version: str = "unknown", config_dir: Optional[Path] = None, ): @@ -43,6 +43,16 @@ def __init__( self.machine_id = self._get_or_create_machine_id() self.os_info = self._get_os_info() + def is_telemetry_disabled(self) -> bool: + """ + Check if telemetry has been disabled by the user. + + Returns: + True if telemetry is disabled, False otherwise + """ + telemetry_disabled_file = self.config_dir / "telemetry_disabled" + return telemetry_disabled_file.exists() + def _get_or_create_machine_id(self) -> str: """ Get or create a stable machine identifier. @@ -122,8 +132,12 @@ def send_ping(self, timeout: int = 5) -> bool: timeout: Request timeout in seconds Returns: - True if ping succeeded, False otherwise + True if ping succeeded, False otherwise (including if telemetry is disabled) """ + # Check if telemetry is disabled + if self.is_telemetry_disabled(): + return False + try: data = { 'machine_id': self.machine_id, @@ -157,8 +171,12 @@ def send_install_event(self, install_method: str = "unknown", timeout: int = 5) timeout: Request timeout in seconds Returns: - True if event sent successfully, False otherwise + True if event sent successfully, False otherwise (including if telemetry is disabled) """ + # Check if telemetry is disabled + if self.is_telemetry_disabled(): + return False + try: data = { 'machine_id': self.machine_id, @@ -185,7 +203,7 @@ def send_install_event(self, install_method: str = "unknown", timeout: int = 5) # Convenience function for simple usage def send_telemetry_ping( - endpoint: str = "https://ushadow-telemetry.your-subdomain.workers.dev", + endpoint: str = "https://ushadow-telemetry.stu-6b7.workers.dev", app_version: str = "unknown" ) -> bool: """ @@ -205,7 +223,7 @@ def send_telemetry_ping( if __name__ == "__main__": # Example usage client = TelemetryClient( - endpoint="https://ushadow-telemetry.your-subdomain.workers.dev", + endpoint="https://ushadow-telemetry.stu-6b7.workers.dev", app_version="0.2.4" ) From 355f3dddbb3b98e1617682046a86e02653dd22a8 Mon Sep 17 00:00:00 2001 From: Stu Alexander Date: Sat, 24 Jan 2026 00:55:09 +0000 Subject: [PATCH 2/3] =?UTF-8?q?##=20=E2=9C=85=20Docker=20Build=20Issue=20F?= =?UTF-8?q?ixed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Docker build errors have been resolved by switching from the GitHub Container Registry (ghcr.io) approach to the official uv installation script. ### What Was Changed **File: `ushadow/backend/Dockerfile`** **Before (causing errors):** ```dockerfile COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/ ``` **After (working):** ```dockerfile # Install build dependencies including curl RUN apt-get update && apt-get install -y \ gcc \ python3-dev \ libyaml-dev \ curl \ && rm -rf /var/lib/apt/lists/* # Install uv using official installation script RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ ln -s /root/.local/bin/uv /usr/local/bin/uv && \ ln -s /root/.local/bin/uvx /usr/local/bin/uvx ``` This change was applied to both the **builder stage** and **runtime stage** of the multi-stage Dockerfile. ### Why This Works 1. **No Registry Authentication** - The official script downloads directly from astral.sh, bypassing the ghcr.io authentication issues 2. **Recommended Method** - This is the officially supported installation approach from the uv maintainers 3. **Always Up-to-Date** - Gets the latest stable version (currently 0.9.26) 4. **Reliable** - Eliminates dependency on GitHub Container Registry availability ### Test Results ✅ Docker build completed successfully ✅ Image created: `ushadow-backend` ✅ uv version confirmed: `0.9.26` The build now works without any registry authentication errors and follows the official installation best practices. --- ushadow/backend/Dockerfile | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ushadow/backend/Dockerfile b/ushadow/backend/Dockerfile index ba1ff299..a30a20f5 100644 --- a/ushadow/backend/Dockerfile +++ b/ushadow/backend/Dockerfile @@ -8,16 +8,20 @@ FROM python:3.12-slim AS builder WORKDIR /app -# Install uv for fast Python package management -COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/ - -# Install build dependencies for Python packages (gcc, python3-dev, libyaml-dev) +# Install build dependencies for Python packages (gcc, python3-dev, libyaml-dev, curl for uv install) RUN apt-get update && apt-get install -y \ gcc \ python3-dev \ libyaml-dev \ + curl \ && rm -rf /var/lib/apt/lists/* +# Install uv for fast Python package management +# Using official installation script (recommended method) +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + ln -s /root/.local/bin/uv /usr/local/bin/uv && \ + ln -s /root/.local/bin/uvx /usr/local/bin/uvx + # Copy dependency files first for better layer caching COPY pyproject.toml uv.lock ./ @@ -32,10 +36,7 @@ FROM python:3.12-slim WORKDIR /app -# Install uv for running the app -COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/ - -# Install only runtime dependencies (curl for healthcheck, docker-cli for volume cleanup, tailscale) +# Install runtime dependencies (curl for healthcheck and uv install, docker-cli for volume cleanup, tailscale) RUN apt-get update && apt-get install -y \ curl \ ca-certificates \ @@ -49,6 +50,12 @@ RUN apt-get update && apt-get install -y \ && curl -fsSL https://tailscale.com/install.sh | sh \ && rm -rf /var/lib/apt/lists/* +# Install uv for running the app +# Using official installation script (recommended method) +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + ln -s /root/.local/bin/uv /usr/local/bin/uv && \ + ln -s /root/.local/bin/uvx /usr/local/bin/uvx + # Copy virtual environment from builder COPY --from=builder /app/.venv /app/.venv From 8247f6857ff4534727a2d733a5408e16d80d0022 Mon Sep 17 00:00:00 2001 From: Stu Alexander Date: Sat, 24 Jan 2026 01:43:24 +0000 Subject: [PATCH 3/3] Update uv.lock with pytest-env dependency Updated lockfile to include pytest-env>=1.1.0 that was already specified in pyproject.toml dev dependencies. This enables better environment variable management during test runs. Co-Authored-By: Claude Sonnet 4.5 --- ushadow/backend/uv.lock | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ushadow/backend/uv.lock b/ushadow/backend/uv.lock index 7da1fad6..6ec6ad49 100644 --- a/ushadow/backend/uv.lock +++ b/ushadow/backend/uv.lock @@ -1982,6 +1982,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] +[[package]] +name = "pytest-env" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/12/9c87d0ca45d5992473208bcef2828169fa7d39b8d7fc6e3401f5c08b8bf7/pytest_env-1.2.0.tar.gz", hash = "sha256:475e2ebe8626cee01f491f304a74b12137742397d6c784ea4bc258f069232b80", size = 8973, upload-time = "2025-10-09T19:15:47.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl", hash = "sha256:d7e5b7198f9b83c795377c09feefa45d56083834e60d04767efd64819fc9da00", size = 6251, upload-time = "2025-10-09T19:15:46.077Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2653,6 +2665,7 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-env" }, { name = "ruff" }, ] @@ -2689,6 +2702,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.3" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0.0" }, + { name = "pytest-env", marker = "extra == 'dev'", specifier = ">=1.1.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-jose", extras = ["cryptography"], specifier = ">=3.3.0" }, { name = "python-multipart", specifier = ">=0.0.20" },