Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions .github/workflows/pull-requests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,80 @@ jobs:
run: black --check confidence --exclude="telemetry_pb2.py|_version.py"

- name: Run flake8 formatter check
run: flake8 confidence --exclude=telemetry_pb2.py,_version.py
run: flake8 confidence --exclude=telemetry_pb2.py,_version.py,telemetry.py

- name: Run type linter check
run: mypy confidence --follow-imports=skip --exclude=telemetry_pb2.py
run: mypy confidence --follow-imports=skip --exclude telemetry_pb2.py --exclude telemetry.py

- name: Run tests with pytest
run: pytest

test-installation-scenarios:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
python-version: ["3.11"]
installation-type: ["full", "minimal"]

steps:
- name: Check out src from Git
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Upgrade pip
run: pip install --upgrade pip

- name: Install full dependencies (with protobuf)
if: matrix.installation-type == 'full'
run: |
pip install -e ".[dev]"

- name: Install minimal dependencies (without protobuf)
if: matrix.installation-type == 'minimal'
run: |
pip install --no-deps -e .
pip install requests==2.32.4 openfeature-sdk==0.4.2 typing_extensions==4.9.0 httpx==0.27.2
pip install pytest==7.4.2 pytest-mock==3.11.1

- name: Test telemetry functionality
run: |
python -c "
from confidence.telemetry import Telemetry, PROTOBUF_AVAILABLE, ProtoTraceId, ProtoStatus
print(f'Installation: ${{ matrix.installation-type }}')
print(f'Protobuf available: {PROTOBUF_AVAILABLE}')

telemetry = Telemetry('test-version')
telemetry.add_trace(ProtoTraceId.PROTO_TRACE_ID_RESOLVE_LATENCY, 100, ProtoStatus.PROTO_STATUS_SUCCESS)
header = telemetry.get_monitoring_header()

if '${{ matrix.installation-type }}' == 'full':
assert PROTOBUF_AVAILABLE == True, 'Protobuf should be available in full installation'
assert len(header) > 0, 'Header should not be empty in full installation'
print('✅ Full installation: Telemetry enabled')
else:
assert PROTOBUF_AVAILABLE == False, 'Protobuf should not be available in minimal installation'
assert header == '', 'Header should be empty in minimal installation'
print('✅ Minimal installation: Telemetry disabled')
"

- name: Run core functionality tests
run: |
# Run a subset of tests to verify core functionality works in both scenarios
python -c "
from confidence.confidence import Confidence
from openfeature import api
from confidence.openfeature_provider import ConfidenceOpenFeatureProvider

# Test basic SDK initialization (should work in both scenarios)
confidence = Confidence('fake-token', disable_telemetry=True)
provider = ConfidenceOpenFeatureProvider(confidence)
print('✅ Core SDK functionality works')
"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ dist/
build/
confidence/_version.py
.env
.claude/
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,19 @@ the [OpenFeature reference documentation](https://openfeature.dev/docs/reference
pip install spotify-confidence-sdk==2.0.2
```

This installs the full SDK including telemetry support and is the suggested .

#### Minimal installation (without telemetry)
For environments where you cannot use protobuf, you can install without protobuf (which disables telemetry):

```bash
pip install spotify-confidence-sdk==2.0.1 --no-deps
pip install requests==2.32.4 openfeature-sdk==0.4.2 typing_extensions==4.9.0 httpx==0.27.2
```

#### requirements.txt
```python
```txt
# Full installation (recommended)
spotify-confidence-sdk==2.0.2

pip install -r requirements.txt
Expand Down Expand Up @@ -117,7 +128,15 @@ confidence = Confidence("CLIENT_TOKEN", logger=quiet_logger)

The SDK includes telemetry functionality that helps monitor SDK performance and usage. By default, telemetry is enabled and collects metrics (anonymously) such as resolve latency and request status. This data is used by the Confidence team to improve the product, and in certain cases it is also available to the SDK adopters.

You can disable telemetry by setting `disable_telemetry=True` when initializing the Confidence client:
### Telemetry behavior

- **Default installation**: Telemetry is enabled automatically when protobuf dependencies are available
- **Minimal installation**: Telemetry is automatically disabled when protobuf is not installed (see [minimal installation](#minimal-installation-without-telemetry))
- **Manual control**: You can explicitly disable telemetry even when dependencies are available

### Disabling telemetry

You can explicitly disable telemetry by setting `disable_telemetry=True` when initializing the Confidence client:

```python
confidence = Confidence("CLIENT_TOKEN",
Expand Down
67 changes: 54 additions & 13 deletions confidence/telemetry.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,60 @@
import base64
from queue import Queue
from typing import Optional
from typing import Optional, Union, Any
from typing_extensions import TypeAlias
from enum import IntEnum

from confidence.telemetry_pb2 import (
ProtoMonitoring,
ProtoLibraryTraces,
ProtoPlatform,
)
# Try to import protobuf components, fallback to mock types if unavailable
try:
from confidence.telemetry_pb2 import (
ProtoMonitoring,
ProtoLibraryTraces,
ProtoPlatform,
)

# Define type aliases for the protobuf classes
ProtoTrace: TypeAlias = ProtoLibraryTraces.ProtoTrace
ProtoLibrary: TypeAlias = ProtoLibraryTraces.ProtoLibrary
ProtoTraceId: TypeAlias = ProtoLibraryTraces.ProtoTraceId
ProtoStatus: TypeAlias = ProtoLibraryTraces.ProtoTrace.ProtoRequestTrace.ProtoStatus
# Define type aliases for the protobuf classes
ProtoTrace: TypeAlias = ProtoLibraryTraces.ProtoTrace
ProtoLibrary: TypeAlias = ProtoLibraryTraces.ProtoLibrary
ProtoTraceId: TypeAlias = ProtoLibraryTraces.ProtoTraceId
ProtoStatus: TypeAlias = ProtoLibraryTraces.ProtoTrace.ProtoRequestTrace.ProtoStatus

PROTOBUF_AVAILABLE = True
except ImportError:
PROTOBUF_AVAILABLE = False

# Fallback enum classes that match protobuf enum values
class ProtoLibrary(IntEnum):
PROTO_LIBRARY_UNSPECIFIED = 0
PROTO_LIBRARY_CONFIDENCE = 1
PROTO_LIBRARY_OPEN_FEATURE = 2
PROTO_LIBRARY_REACT = 3

class ProtoTraceId(IntEnum):
PROTO_TRACE_ID_UNSPECIFIED = 0
PROTO_TRACE_ID_RESOLVE_LATENCY = 1
PROTO_TRACE_ID_STALE_FLAG = 2
PROTO_TRACE_ID_FLAG_TYPE_MISMATCH = 3
PROTO_TRACE_ID_WITH_CONTEXT = 4

class ProtoStatus(IntEnum):
PROTO_STATUS_UNSPECIFIED = 0
PROTO_STATUS_SUCCESS = 1
PROTO_STATUS_ERROR = 2
PROTO_STATUS_TIMEOUT = 3
PROTO_STATUS_CACHED = 4

class ProtoPlatform(IntEnum):
PROTO_PLATFORM_UNSPECIFIED = 0
PROTO_PLATFORM_JS_WEB = 4
PROTO_PLATFORM_JS_SERVER = 5
PROTO_PLATFORM_PYTHON = 6
PROTO_PLATFORM_GO = 7

# Mock trace class for type compatibility
class ProtoTrace:
def __init__(self):
self.id = None
self.request_trace = None


class Telemetry:
Expand All @@ -40,7 +81,7 @@ def __init__(self, version: str, disabled: bool = False) -> None:
def add_trace(
self, trace_id: ProtoTraceId, duration_ms: int, status: ProtoStatus
) -> None:
if self._disabled:
if self._disabled or not PROTOBUF_AVAILABLE:
return
trace = ProtoTrace()
trace.id = trace_id
Expand All @@ -51,7 +92,7 @@ def add_trace(
self._traces_queue.put(trace)

def get_monitoring_header(self) -> str:
if self._disabled:
if self._disabled or not PROTOBUF_AVAILABLE:
return ""
current_traces = []
while not self._traces_queue.empty():
Expand Down
60 changes: 38 additions & 22 deletions tests/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,28 @@
import base64
import time
from unittest.mock import patch, MagicMock
from confidence.telemetry_pb2 import ProtoMonitoring, ProtoLibraryTraces, ProtoPlatform
from confidence.telemetry import Telemetry
from confidence.telemetry import Telemetry, PROTOBUF_AVAILABLE
from confidence.confidence import Confidence, Region
import requests

# Get the nested classes from ProtoLibraryTraces
ProtoTrace = ProtoLibraryTraces.ProtoTrace
ProtoRequestTrace = ProtoTrace.ProtoRequestTrace
ProtoStatus = ProtoRequestTrace.ProtoStatus
ProtoLibrary = ProtoLibraryTraces.ProtoLibrary
ProtoTraceId = ProtoLibraryTraces.ProtoTraceId
# Import protobuf types if available, otherwise use fallback types
if PROTOBUF_AVAILABLE:
from confidence.telemetry_pb2 import ProtoMonitoring, ProtoLibraryTraces, ProtoPlatform
# Get the nested classes from ProtoLibraryTraces
ProtoTrace = ProtoLibraryTraces.ProtoTrace
ProtoRequestTrace = ProtoTrace.ProtoRequestTrace
ProtoStatus = ProtoRequestTrace.ProtoStatus
ProtoLibrary = ProtoLibraryTraces.ProtoLibrary
ProtoTraceId = ProtoLibraryTraces.ProtoTraceId
else:
from confidence.telemetry import (
ProtoLibrary, ProtoTraceId, ProtoStatus, ProtoPlatform, ProtoTrace
)


def requires_protobuf(test_func):
"""Decorator to skip tests that require protobuf when it's not available"""
return unittest.skipUnless(PROTOBUF_AVAILABLE, "protobuf not available")(test_func)


class TestTelemetry(unittest.TestCase):
Expand All @@ -30,21 +41,26 @@ def test_add_trace(self):
)

header = telemetry.get_monitoring_header()
monitoring = ProtoMonitoring()
monitoring.ParseFromString(base64.b64decode(header))

self.assertEqual(monitoring.platform, ProtoPlatform.PROTO_PLATFORM_PYTHON)
self.assertEqual(len(monitoring.library_traces), 1)

library_trace = monitoring.library_traces[0]
self.assertEqual(library_trace.library, ProtoLibrary.PROTO_LIBRARY_CONFIDENCE)
self.assertEqual(library_trace.library_version, "1.0.0")

self.assertEqual(len(library_trace.traces), 1)
trace = library_trace.traces[0]
self.assertEqual(trace.id, ProtoTraceId.PROTO_TRACE_ID_RESOLVE_LATENCY)
self.assertEqual(trace.request_trace.millisecond_duration, 100)
self.assertEqual(trace.request_trace.status, ProtoStatus.PROTO_STATUS_SUCCESS)
if PROTOBUF_AVAILABLE:
monitoring = ProtoMonitoring()
monitoring.ParseFromString(base64.b64decode(header))

self.assertEqual(monitoring.platform, ProtoPlatform.PROTO_PLATFORM_PYTHON)
self.assertEqual(len(monitoring.library_traces), 1)

library_trace = monitoring.library_traces[0]
self.assertEqual(library_trace.library, ProtoLibrary.PROTO_LIBRARY_CONFIDENCE)
self.assertEqual(library_trace.library_version, "1.0.0")

self.assertEqual(len(library_trace.traces), 1)
trace = library_trace.traces[0]
self.assertEqual(trace.id, ProtoTraceId.PROTO_TRACE_ID_RESOLVE_LATENCY)
self.assertEqual(trace.request_trace.millisecond_duration, 100)
self.assertEqual(trace.request_trace.status, ProtoStatus.PROTO_STATUS_SUCCESS)
else:
# When protobuf is not available, telemetry should return empty header
self.assertEqual(header, "")

def test_traces_are_consumed(self):
telemetry = Telemetry("1.0.0")
Expand Down