Skip to content

Commit 79b09c0

Browse files
feat: Support OpenFeature tracking
1 parent 82de51d commit 79b09c0

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

ld_openfeature/provider.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import threading
2+
from logging import getLogger
23
from typing import Any, List, Mapping, Optional, Sequence, Union
34

45
from ldclient.evaluation import EvaluationDetail
@@ -11,11 +12,15 @@
1112
from openfeature.provider.metadata import Metadata
1213
from openfeature.provider import AbstractProvider
1314
from openfeature.event import ProviderEventDetails
15+
from openfeature.track import TrackingEventDetails
1416

1517
from ld_openfeature.impl.context_converter import EvaluationContextConverter
1618
from ld_openfeature.impl.details_converter import ResolutionDetailsConverter
1719

1820

21+
logger = getLogger("launchdarkly-openfeature-server")
22+
23+
1924
class LaunchDarklyProvider(AbstractProvider):
2025
def __init__(self, config: Config):
2126
self.__client = LDClient(config)
@@ -94,6 +99,36 @@ def get_metadata(self) -> Metadata:
9499
def get_provider_hooks(self) -> List[Hook]:
95100
return []
96101

102+
def track(
103+
self,
104+
tracking_event_name: str,
105+
evaluation_context: Optional[EvaluationContext] = None,
106+
tracking_event_details: Optional[TrackingEventDetails] = None,
107+
) -> None:
108+
if evaluation_context is None:
109+
logger.info(
110+
"The 'track' method was called without an evaluation context. "
111+
"No 'track' event will be sent to LaunchDarkly. "
112+
"The LaunchDarkly SDK requires a context to associate the event with."
113+
)
114+
return
115+
116+
ld_context = self.__context_converter.to_ld_context(evaluation_context)
117+
118+
if tracking_event_details is None:
119+
self.__client.track(tracking_event_name, ld_context)
120+
return
121+
122+
data = tracking_event_details.attributes or None
123+
metric_value = tracking_event_details.value
124+
125+
if metric_value is not None:
126+
self.__client.track(tracking_event_name, ld_context, data, metric_value)
127+
elif data is not None:
128+
self.__client.track(tracking_event_name, ld_context, data)
129+
else:
130+
self.__client.track(tracking_event_name, ld_context)
131+
97132
def resolve_boolean_details(
98133
self,
99134
flag_key: str,

tests/test_provider.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from openfeature.event import ProviderEvent, EventDetails
1212
from openfeature.exception import ErrorCode
1313
from openfeature.flag_evaluation import Reason
14+
from openfeature.track import TrackingEventDetails
1415
from openfeature import api
1516

1617
from ld_openfeature import LaunchDarklyProvider, Config
@@ -150,6 +151,61 @@ def test_logger_changes_should_cascade_to_evaluation_converter(provider: LaunchD
150151
assert caplog.records[0].message == "'kind' was set to a non-string value; defaulting to user"
151152

152153

154+
def test_track_without_context_does_not_send_an_event(provider: LaunchDarklyProvider):
155+
with patch.object(LDClient, 'track') as mock_track:
156+
provider.track("metric-key", None, None)
157+
158+
mock_track.assert_not_called()
159+
160+
161+
def test_track_without_details_sends_event_without_data(provider: LaunchDarklyProvider,
162+
evaluation_context: EvaluationContext):
163+
with patch.object(LDClient, 'track') as mock_track:
164+
provider.track("metric-key", evaluation_context, None)
165+
166+
mock_track.assert_called_once()
167+
name, context = mock_track.call_args.args
168+
assert name == "metric-key"
169+
assert context.key == 'user-key'
170+
171+
172+
def test_track_sends_attributes_as_data(provider: LaunchDarklyProvider,
173+
evaluation_context: EvaluationContext):
174+
with patch.object(LDClient, 'track') as mock_track:
175+
provider.track("metric-key", evaluation_context, TrackingEventDetails(attributes={'string': 'value'}))
176+
177+
mock_track.assert_called_once()
178+
name, context, data = mock_track.call_args.args
179+
assert name == "metric-key"
180+
assert context.key == 'user-key'
181+
assert data == {'string': 'value'}
182+
183+
184+
def test_track_sends_value_as_metric_value(provider: LaunchDarklyProvider,
185+
evaluation_context: EvaluationContext):
186+
with patch.object(LDClient, 'track') as mock_track:
187+
provider.track("metric-key", evaluation_context,
188+
TrackingEventDetails(value=17, attributes={'string': 'value'}))
189+
190+
mock_track.assert_called_once()
191+
name, context, data, metric_value = mock_track.call_args.args
192+
assert name == "metric-key"
193+
assert context.key == 'user-key'
194+
assert data == {'string': 'value'}
195+
assert metric_value == 17
196+
197+
198+
def test_track_without_attributes_sends_metric_value_without_data(provider: LaunchDarklyProvider,
199+
evaluation_context: EvaluationContext):
200+
with patch.object(LDClient, 'track') as mock_track:
201+
provider.track("metric-key", evaluation_context, TrackingEventDetails(value=17))
202+
203+
mock_track.assert_called_once()
204+
name, context, data, metric_value = mock_track.call_args.args
205+
assert data is None
206+
assert metric_value == 17
207+
208+
153209
def test_provider_emits_ready_event_when_immediately_ready():
154210
emission_count = 0
155211
lock = threading.Lock()

0 commit comments

Comments
 (0)