Skip to content

Commit 8b9bc52

Browse files
committed
refactor: unleash tracker
Signed-off-by: Kiki L Hakiem <[email protected]>
1 parent 6c97881 commit 8b9bc52

File tree

4 files changed

+277
-253
lines changed

4 files changed

+277
-253
lines changed

providers/openfeature-provider-unleash/src/openfeature/contrib/provider/unleash/__init__.py

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
import json
2-
import uuid
32
from typing import Any, Callable, List, Mapping, Optional, Sequence, Union
43

4+
from UnleashClient import UnleashClient
5+
from UnleashClient.events import BaseEvent, UnleashFetchedEvent, UnleashReadyEvent
56
from openfeature.evaluation_context import EvaluationContext
6-
from openfeature.flag_evaluation import FlagResolutionDetails, FlagValueType, Reason
7-
from openfeature.hook import Hook
8-
from openfeature.provider import AbstractProvider, Metadata, ProviderStatus
97
from openfeature.event import ProviderEvent
108
from openfeature.exception import (
9+
ErrorCode,
1110
FlagNotFoundError,
1211
GeneralError,
1312
ParseError,
1413
TypeMismatchError,
15-
ErrorCode,
1614
)
15+
from openfeature.flag_evaluation import FlagResolutionDetails, FlagValueType, Reason
16+
from openfeature.hook import Hook
17+
from openfeature.provider import AbstractProvider, Metadata, ProviderStatus
1718
import requests
18-
from UnleashClient import UnleashClient
19-
from UnleashClient.events import (
20-
BaseEvent,
21-
UnleashReadyEvent,
22-
UnleashFetchedEvent,
23-
UnleashEvent,
24-
UnleashEventType,
25-
)
19+
20+
from .tracking import Tracker
2621

2722
__all__ = ["UnleashProvider"]
2823

@@ -53,6 +48,7 @@ def __init__(
5348
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED: [],
5449
ProviderEvent.PROVIDER_STALE: [],
5550
}
51+
self._tracking_manager = Tracker(self)
5652

5753
def initialize(
5854
self, evaluation_context: Optional[EvaluationContext] = None
@@ -192,30 +188,7 @@ def track(
192188
evaluation_context: Optional evaluation context
193189
event_details: Optional tracking event details
194190
"""
195-
if not self.client:
196-
return
197-
198-
unleash_context = self._build_unleash_context(evaluation_context) or {}
199-
200-
if event_details:
201-
unleash_context.update(
202-
{
203-
"tracking_value": event_details.get("value"),
204-
"tracking_details": event_details,
205-
}
206-
)
207-
208-
tracking_event = UnleashEvent(
209-
event_type=UnleashEventType.FEATURE_FLAG,
210-
event_id=uuid.uuid4(),
211-
context=unleash_context,
212-
enabled=True,
213-
feature_name=event_name,
214-
variant="tracking_event",
215-
)
216-
217-
if hasattr(self, "_unleash_event_callback"):
218-
self._unleash_event_callback(tracking_event)
191+
self._tracking_manager.track(event_name, evaluation_context, event_details)
219192

220193
def _build_unleash_context(
221194
self, evaluation_context: Optional[EvaluationContext] = None
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Tracking functionality for Unleash provider."""
2+
3+
from typing import Any, Optional, Protocol
4+
import uuid
5+
6+
from UnleashClient.events import UnleashEvent, UnleashEventType
7+
from openfeature.evaluation_context import EvaluationContext
8+
9+
10+
class UnleashProvider(Protocol):
11+
"""Protocol defining the interface needed by Tracker."""
12+
13+
@property
14+
def client(self) -> Optional[Any]: ...
15+
16+
def _build_unleash_context(
17+
self, evaluation_context: Optional[EvaluationContext] = None
18+
) -> Optional[dict[str, Any]]: ...
19+
20+
def _unleash_event_callback(self, event: Any) -> None: ...
21+
22+
23+
class Tracker:
24+
"""Manages tracking functionality for the Unleash provider."""
25+
26+
def __init__(self, provider: UnleashProvider) -> None:
27+
"""Initialize the tracking manager.
28+
29+
Args:
30+
provider: The parent UnleashProvider instance
31+
"""
32+
self._provider = provider
33+
34+
def track(
35+
self,
36+
event_name: str,
37+
evaluation_context: Optional[EvaluationContext] = None,
38+
event_details: Optional[dict] = None,
39+
) -> None:
40+
"""Track user actions or application states using Unleash impression events.
41+
42+
Args:
43+
event_name: The name of the tracking event
44+
evaluation_context: Optional evaluation context
45+
event_details: Optional tracking event details
46+
"""
47+
if not self._provider.client:
48+
return
49+
50+
unleash_context = (
51+
self._provider._build_unleash_context(evaluation_context) or {}
52+
)
53+
54+
if event_details:
55+
unleash_context.update(
56+
{
57+
"tracking_value": event_details.get("value"),
58+
"tracking_details": event_details,
59+
}
60+
)
61+
62+
tracking_event = UnleashEvent(
63+
event_type=UnleashEventType.FEATURE_FLAG,
64+
event_id=uuid.uuid4(),
65+
context=unleash_context,
66+
enabled=True,
67+
feature_name=event_name,
68+
variant="tracking_event",
69+
)
70+
71+
if hasattr(self._provider, "_unleash_event_callback"):
72+
self._provider._unleash_event_callback(tracking_event)

providers/openfeature-provider-unleash/tests/test_provider.py

Lines changed: 4 additions & 216 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import pytest
2-
import requests
31
from unittest.mock import Mock, patch
42

53
from openfeature.contrib.provider.unleash import UnleashProvider
64
from openfeature.evaluation_context import EvaluationContext
7-
from openfeature.flag_evaluation import Reason
8-
from openfeature.provider import ProviderStatus
95
from openfeature.event import ProviderEvent
106
from openfeature.exception import (
117
FlagNotFoundError,
128
GeneralError,
139
ParseError,
1410
TypeMismatchError,
1511
)
12+
from openfeature.flag_evaluation import Reason
13+
from openfeature.provider import ProviderStatus
14+
import pytest
15+
import requests
1616

1717

1818
def test_unleash_provider_import():
@@ -800,215 +800,3 @@ def test_unleash_provider_type_validation():
800800
assert isinstance(result.value, str)
801801

802802
provider.shutdown()
803-
804-
805-
def test_unleash_provider_track_basic():
806-
"""Test basic tracking functionality."""
807-
mock_client = Mock()
808-
mock_client.initialize_client.return_value = None
809-
mock_event_callback = Mock()
810-
811-
with patch(
812-
"openfeature.contrib.provider.unleash.UnleashClient"
813-
) as mock_unleash_client:
814-
mock_unleash_client.return_value = mock_client
815-
816-
provider = UnleashProvider(
817-
url="http://localhost:4242", app_name="test-app", api_token="test-token"
818-
)
819-
provider.initialize()
820-
821-
# Set the event callback
822-
provider._unleash_event_callback = mock_event_callback
823-
824-
# Track a basic event
825-
provider.track("user_action")
826-
827-
# Verify the tracking event was created and passed to callback
828-
assert mock_event_callback.call_count == 1
829-
tracking_event = mock_event_callback.call_args[0][0]
830-
assert tracking_event.feature_name == "user_action"
831-
assert tracking_event.enabled is True
832-
assert tracking_event.variant == "tracking_event"
833-
834-
provider.shutdown()
835-
836-
837-
def test_unleash_provider_track_with_context():
838-
"""Test tracking with evaluation context."""
839-
mock_client = Mock()
840-
mock_client.initialize_client.return_value = None
841-
mock_event_callback = Mock()
842-
843-
with patch(
844-
"openfeature.contrib.provider.unleash.UnleashClient"
845-
) as mock_unleash_client:
846-
mock_unleash_client.return_value = mock_client
847-
848-
provider = UnleashProvider(
849-
url="http://localhost:4242", app_name="test-app", api_token="test-token"
850-
)
851-
provider.initialize()
852-
853-
provider._unleash_event_callback = mock_event_callback
854-
855-
context = EvaluationContext(
856-
targeting_key="user123",
857-
attributes={"email": "[email protected]", "role": "admin"},
858-
)
859-
860-
provider.track("page_view", context)
861-
862-
tracking_event = mock_event_callback.call_args[0][0]
863-
assert tracking_event.context["userId"] == "user123"
864-
assert tracking_event.context["email"] == "[email protected]"
865-
assert tracking_event.context["role"] == "admin"
866-
867-
provider.shutdown()
868-
869-
870-
def test_unleash_provider_track_with_event_details():
871-
"""Test tracking with event details."""
872-
mock_client = Mock()
873-
mock_client.initialize_client.return_value = None
874-
mock_event_callback = Mock()
875-
876-
with patch(
877-
"openfeature.contrib.provider.unleash.UnleashClient"
878-
) as mock_unleash_client:
879-
mock_unleash_client.return_value = mock_client
880-
881-
provider = UnleashProvider(
882-
url="http://localhost:4242", app_name="test-app", api_token="test-token"
883-
)
884-
provider.initialize()
885-
886-
provider._unleash_event_callback = mock_event_callback
887-
888-
event_details = {"value": 99.99, "currency": "USD", "category": "purchase"}
889-
890-
provider.track("purchase_completed", event_details=event_details)
891-
892-
tracking_event = mock_event_callback.call_args[0][0]
893-
assert tracking_event.context["tracking_value"] == 99.99
894-
assert tracking_event.context["tracking_details"] == event_details
895-
896-
provider.shutdown()
897-
898-
899-
def test_unleash_provider_track_not_initialized():
900-
"""Test tracking when provider is not initialized."""
901-
provider = UnleashProvider(
902-
url="http://localhost:4242", app_name="test-app", api_token="test-token"
903-
)
904-
905-
# Should not raise any exception, just return
906-
provider.track("test_event")
907-
908-
909-
def test_unleash_provider_track_empty_event_name():
910-
"""Test tracking with empty event name."""
911-
mock_client = Mock()
912-
mock_client.initialize_client.return_value = None
913-
mock_event_callback = Mock()
914-
915-
with patch(
916-
"openfeature.contrib.provider.unleash.UnleashClient"
917-
) as mock_unleash_client:
918-
mock_unleash_client.return_value = mock_client
919-
920-
provider = UnleashProvider(
921-
url="http://localhost:4242", app_name="test-app", api_token="test-token"
922-
)
923-
provider.initialize()
924-
925-
provider._unleash_event_callback = mock_event_callback
926-
927-
provider.track("")
928-
929-
tracking_event = mock_event_callback.call_args[0][0]
930-
assert tracking_event.feature_name == ""
931-
932-
provider.shutdown()
933-
934-
935-
def test_unleash_provider_track_none_context():
936-
"""Test tracking with None context."""
937-
mock_client = Mock()
938-
mock_client.initialize_client.return_value = None
939-
mock_event_callback = Mock()
940-
941-
with patch(
942-
"openfeature.contrib.provider.unleash.UnleashClient"
943-
) as mock_unleash_client:
944-
mock_unleash_client.return_value = mock_client
945-
946-
provider = UnleashProvider(
947-
url="http://localhost:4242", app_name="test-app", api_token="test-token"
948-
)
949-
provider.initialize()
950-
951-
provider._unleash_event_callback = mock_event_callback
952-
953-
provider.track("test_event", None)
954-
955-
tracking_event = mock_event_callback.call_args[0][0]
956-
assert tracking_event.context == {}
957-
958-
provider.shutdown()
959-
960-
961-
def test_unleash_provider_track_none_event_details():
962-
"""Test tracking with None event details."""
963-
mock_client = Mock()
964-
mock_client.initialize_client.return_value = None
965-
mock_event_callback = Mock()
966-
967-
with patch(
968-
"openfeature.contrib.provider.unleash.UnleashClient"
969-
) as mock_unleash_client:
970-
mock_unleash_client.return_value = mock_client
971-
972-
provider = UnleashProvider(
973-
url="http://localhost:4242", app_name="test-app", api_token="test-token"
974-
)
975-
provider.initialize()
976-
977-
provider._unleash_event_callback = mock_event_callback
978-
979-
provider.track("test_event", event_details=None)
980-
981-
tracking_event = mock_event_callback.call_args[0][0]
982-
assert "tracking_value" not in tracking_event.context
983-
assert "tracking_details" not in tracking_event.context
984-
985-
provider.shutdown()
986-
987-
988-
def test_unleash_provider_track_event_details_without_value():
989-
"""Test tracking with event details that don't have a value field."""
990-
mock_client = Mock()
991-
mock_client.initialize_client.return_value = None
992-
mock_event_callback = Mock()
993-
994-
with patch(
995-
"openfeature.contrib.provider.unleash.UnleashClient"
996-
) as mock_unleash_client:
997-
mock_unleash_client.return_value = mock_client
998-
999-
provider = UnleashProvider(
1000-
url="http://localhost:4242", app_name="test-app", api_token="test-token"
1001-
)
1002-
provider.initialize()
1003-
1004-
provider._unleash_event_callback = mock_event_callback
1005-
1006-
event_details = {"category": "test", "action": "view"}
1007-
1008-
provider.track("test_event", event_details=event_details)
1009-
1010-
tracking_event = mock_event_callback.call_args[0][0]
1011-
assert tracking_event.context["tracking_value"] is None
1012-
assert tracking_event.context["tracking_details"] == event_details
1013-
1014-
provider.shutdown()

0 commit comments

Comments
 (0)