Skip to content
Open
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
35 changes: 35 additions & 0 deletions ld_openfeature/provider.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import threading
from logging import getLogger
from typing import Any, List, Mapping, Optional, Sequence, Union

from ldclient.evaluation import EvaluationDetail
Expand All @@ -11,11 +12,15 @@
from openfeature.provider.metadata import Metadata
from openfeature.provider import AbstractProvider
from openfeature.event import ProviderEventDetails
from openfeature.track import TrackingEventDetails
Comment thread
cursor[bot] marked this conversation as resolved.

from ld_openfeature.impl.context_converter import EvaluationContextConverter
from ld_openfeature.impl.details_converter import ResolutionDetailsConverter


logger = getLogger("launchdarkly-openfeature-server")


class LaunchDarklyProvider(AbstractProvider):
def __init__(self, config: Config):
self.__client = LDClient(config)
Expand Down Expand Up @@ -94,6 +99,36 @@ def get_metadata(self) -> Metadata:
def get_provider_hooks(self) -> List[Hook]:
return []

def track(
self,
tracking_event_name: str,
evaluation_context: Optional[EvaluationContext] = None,
tracking_event_details: Optional[TrackingEventDetails] = None,
) -> None:
if evaluation_context is None:
logger.info(
"The 'track' method was called without an evaluation context. "
"No 'track' event will be sent to LaunchDarkly. "
"The LaunchDarkly SDK requires a context to associate the event with."
)
return

ld_context = self.__context_converter.to_ld_context(evaluation_context)

if tracking_event_details is None:
self.__client.track(tracking_event_name, ld_context)
return

data = tracking_event_details.attributes or None
metric_value = tracking_event_details.value

if metric_value is not None:
self.__client.track(tracking_event_name, ld_context, data, metric_value)
elif data is not None:
self.__client.track(tracking_event_name, ld_context, data)
else:
self.__client.track(tracking_event_name, ld_context)

def resolve_boolean_details(
self,
flag_key: str,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ packages = [

[tool.poetry.dependencies]
python = "^3.10"
openfeature-sdk = ">=0.8.0,<1"
openfeature-sdk = ">=0.9.0,<1"
launchdarkly-server-sdk = "<10"


Expand Down
56 changes: 56 additions & 0 deletions tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from openfeature.event import ProviderEvent, EventDetails
from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import Reason
from openfeature.track import TrackingEventDetails
from openfeature import api

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


def test_track_without_context_does_not_send_an_event(provider: LaunchDarklyProvider):
with patch.object(LDClient, 'track') as mock_track:
provider.track("metric-key", None, None)

mock_track.assert_not_called()


def test_track_without_details_sends_event_without_data(provider: LaunchDarklyProvider,
evaluation_context: EvaluationContext):
with patch.object(LDClient, 'track') as mock_track:
provider.track("metric-key", evaluation_context, None)

mock_track.assert_called_once()
name, context = mock_track.call_args.args
assert name == "metric-key"
assert context.key == 'user-key'


def test_track_sends_attributes_as_data(provider: LaunchDarklyProvider,
evaluation_context: EvaluationContext):
with patch.object(LDClient, 'track') as mock_track:
provider.track("metric-key", evaluation_context, TrackingEventDetails(attributes={'string': 'value'}))

mock_track.assert_called_once()
name, context, data = mock_track.call_args.args
assert name == "metric-key"
assert context.key == 'user-key'
assert data == {'string': 'value'}


def test_track_sends_value_as_metric_value(provider: LaunchDarklyProvider,
evaluation_context: EvaluationContext):
with patch.object(LDClient, 'track') as mock_track:
provider.track("metric-key", evaluation_context,
TrackingEventDetails(value=17, attributes={'string': 'value'}))

mock_track.assert_called_once()
name, context, data, metric_value = mock_track.call_args.args
assert name == "metric-key"
assert context.key == 'user-key'
assert data == {'string': 'value'}
assert metric_value == 17


def test_track_without_attributes_sends_metric_value_without_data(provider: LaunchDarklyProvider,
evaluation_context: EvaluationContext):
with patch.object(LDClient, 'track') as mock_track:
provider.track("metric-key", evaluation_context, TrackingEventDetails(value=17))

mock_track.assert_called_once()
name, context, data, metric_value = mock_track.call_args.args
assert data is None
assert metric_value == 17


def test_provider_emits_ready_event_when_immediately_ready():
emission_count = 0
lock = threading.Lock()
Expand Down
Loading