Skip to content

Commit 7c96501

Browse files
Add events with metadata
1 parent cf0b910 commit 7c96501

File tree

5 files changed

+153
-4
lines changed

5 files changed

+153
-4
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
CHANGES
22

3+
1.1.0 (Feb 27 2026)
4+
- Split SDK 10.5.1 remains supported. Provider lifecycle events (PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED, PROVIDER_ERROR) require Split SDK 10.6.0 or later; on 10.5.1 the provider works as before without emitting those events.
5+
- Provider now emits OpenFeature provider events (PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED, PROVIDER_ERROR) when Split SDK 10.6+ fires ready/update/timeout. Event details include OpenFeature-friendly metadata (see docs/EVENTS_MAPPING.md).
6+
37
1.0.0 (Nov 10 2025)
48
- BREAKING CHANGE: Passing the SplitClient object to Provider constructor is now only through the initialization context dictionary
59
- BREAKING CHANGE: Provider will throw exception when ObjectDetail and ObjectValue evaluation is used, since it will attempt to parse the treatment as a JSON structure.

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
This Provider is designed to allow the use of OpenFeature with Split, the platform for controlled rollouts, serving features to your users via the Split feature flag to manage your complete customer experience.
66

77
## Compatibility
8-
This SDK is compatible with Python 3.9 and higher.
8+
- Python 3.9 and higher.
9+
- **Split SDK**: [Split Python SDK](https://github.com/splitio/python-client) **10.5.1 or later**. Provider lifecycle events (PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED, PROVIDER_ERROR) require **10.6.0 or later**; on 10.5.1 the provider works without emitting those events.
910

1011
## Getting started
1112

1213
This package replaces the previous `split-openfeature-provider` Python provider in [Pypi](https://pypi.org/project/split-openfeature-provider/).
1314

1415
### Pip Installation
1516
```python
16-
pip install split-openfeature-provider==1.0.0
17+
pip install split-openfeature-provider==1.1.0
1718
```
1819
### Configure it
1920
Below is a simple example that describes using the Split Provider. Please see the [OpenFeature Documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api) for details on how to use the OpenFeature SDK.

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
openfeature_sdk==0.8.3
2-
splitio_client[cpphash,asyncio]==10.5.1
2+
splitio_client[cpphash,asyncio]>=10.5.1

split_openfeature_provider/split_client_wrapper.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22
from splitio.exceptions import TimeoutException
33
import logging
44

5+
try:
6+
from splitio.models.events import SdkEvent
7+
except ImportError:
8+
SdkEvent = None # type: ignore # Split < 10.6: no events API
9+
510
_LOGGER = logging.getLogger(__name__)
611

12+
# Sentinel for block_until_ready timeout (not a Split SdkEvent)
13+
SPLIT_EVENT_BUR_TIMEOUT = "block_until_ready_timeout"
14+
15+
716
class SplitClientWrapper():
817

918
def __init__(self, initial_context):
1019
self.sdk_ready = False
1120
self.split_client = None
21+
self._event_receiver = None
1222

1323
if not self._validate_context(initial_context):
1424
raise AttributeError()
@@ -39,13 +49,15 @@ def __init__(self, initial_context):
3949
self.sdk_ready = True
4050
except TimeoutException:
4151
_LOGGER.debug("Split SDK timed out")
52+
self._notify_receiver(SPLIT_EVENT_BUR_TIMEOUT, None)
4253

4354
self.split_client = self._factory.client()
4455

4556
async def create(self):
4657
if self._initial_context.get("SplitClient") != None:
4758
self.split_client = self._initial_context.get("SplitClient")
4859
self._factory = self.split_client._factory
60+
await self._register_split_events_async()
4961
return
5062

5163
try:
@@ -54,8 +66,10 @@ async def create(self):
5466
self.sdk_ready = True
5567
except TimeoutException:
5668
_LOGGER.debug("Split SDK timed out")
69+
self._notify_receiver(SPLIT_EVENT_BUR_TIMEOUT, None)
5770

5871
self.split_client = self._factory.client()
72+
await self._register_split_events_async()
5973

6074
def is_sdk_ready(self):
6175
if self.sdk_ready:
@@ -69,9 +83,59 @@ def is_sdk_ready(self):
6983

7084
return self.sdk_ready
7185

86+
def set_event_receiver(self, receiver):
87+
"""Set the receiver that will be notified of Split SDK events (e.g. the provider)."""
88+
self._event_receiver = receiver
89+
90+
def register_for_split_events(self):
91+
"""Register for Split SDK events (SDK_READY, SDK_UPDATE). Pass the provider as receiver (or call set_event_receiver first)."""
92+
self._register_split_events()
93+
94+
def unregister_for_split_events(self):
95+
"""Stop receiving Split SDK events."""
96+
self._event_receiver = None
97+
98+
def _notify_receiver(self, split_event, event_metadata):
99+
if self._event_receiver is None:
100+
_LOGGER.debug("Split event %s: no receiver registered", split_event)
101+
return
102+
try:
103+
self._event_receiver._on_split_event(split_event, event_metadata)
104+
except Exception as ex:
105+
_LOGGER.debug("Split event callback error: %s", ex)
106+
107+
def _register_split_events(self):
108+
if self._factory is None:
109+
_LOGGER.warning("SplitClientWrapper: _factory is None, cannot register for SDK events")
110+
return
111+
if SdkEvent is None:
112+
_LOGGER.debug("SplitClientWrapper: SdkEvent not available (Split SDK < 10.6?), skipping event registration")
113+
return
114+
try:
115+
em = self._factory._events_manager
116+
if not hasattr(em, "register"):
117+
_LOGGER.warning("SplitClientWrapper: events_manager has no register method")
118+
return
119+
em.register(SdkEvent.SDK_READY, lambda m: self._notify_receiver(SdkEvent.SDK_READY, m))
120+
em.register(SdkEvent.SDK_UPDATE, lambda m: self._notify_receiver(SdkEvent.SDK_UPDATE, m))
121+
_LOGGER.info("SplitClientWrapper: registered for SDK_READY and SDK_UPDATE")
122+
except Exception as ex:
123+
_LOGGER.warning("Could not register Split events: %s", ex)
124+
72125
def destroy(self, destroy_event=None):
73126
self._factory.destroy(destroy_event)
74127

128+
async def _register_split_events_async(self):
129+
if self._factory is None or SdkEvent is None:
130+
return
131+
try:
132+
em = self._factory._events_manager
133+
if hasattr(em, "register"):
134+
await em.register(SdkEvent.SDK_READY, lambda m: self._notify_receiver(SdkEvent.SDK_READY, m))
135+
await em.register(SdkEvent.SDK_UPDATE, lambda m: self._notify_receiver(SdkEvent.SDK_UPDATE, m))
136+
except Exception as ex:
137+
_LOGGER.debug("Could not register Split events: %s", ex)
138+
75139
async def destroy_async(self):
76140
await self._factory.destroy()
77141

split_openfeature_provider/split_provider.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,95 @@
77
from openfeature.exception import ErrorCode, GeneralError, ParseError, OpenFeatureError, TargetingKeyMissingError
88
from openfeature.flag_evaluation import Reason, FlagResolutionDetails
99
from openfeature.provider import AbstractProvider, Metadata
10-
from split_openfeature_provider.split_client_wrapper import SplitClientWrapper
10+
from openfeature.event import ProviderEventDetails
11+
from split_openfeature_provider.split_client_wrapper import SplitClientWrapper, SPLIT_EVENT_BUR_TIMEOUT
1112

1213
_LOGGER = logging.getLogger(__name__)
1314

15+
try:
16+
from splitio.models.events import SdkEvent
17+
except ImportError:
18+
SdkEvent = None # type: ignore
19+
20+
21+
def _flags_changed_from_sdk_update(event_metadata):
22+
"""
23+
Extract list of updated flag/split names from Split SDK_UPDATE event metadata.
24+
OpenFeature expects flags_changed: list[str] for PROVIDER_CONFIGURATION_CHANGED.
25+
Handles: dict with "names", object with .metadata, or object with get_names() (Split EventsMetadata).
26+
"""
27+
if event_metadata is None:
28+
return None
29+
if hasattr(event_metadata, "metadata") and getattr(event_metadata, "metadata", None) is not None:
30+
event_metadata = getattr(event_metadata, "metadata")
31+
if isinstance(event_metadata, dict):
32+
val = event_metadata.get("names")
33+
if isinstance(val, list):
34+
return [str(x) for x in val if x is not None]
35+
return None
36+
if hasattr(event_metadata, "get_names"):
37+
names = event_metadata.get_names()
38+
if names is not None:
39+
return [str(x) for x in names if x is not None]
40+
return None
41+
42+
43+
def _metadata_from_split(split_event, event_metadata):
44+
"""Build OpenFeature event metadata dict from Split event (and optional Split metadata)."""
45+
meta = {"split_event": getattr(split_event, "value", str(split_event))}
46+
if event_metadata is not None and isinstance(event_metadata, dict):
47+
for k, v in event_metadata.items():
48+
if isinstance(v, (bool, str, int, float)):
49+
meta["split_%s" % k] = v
50+
# Split may pass an object with get_type/get_names (e.g. EventsMetadata)
51+
if event_metadata is not None and hasattr(event_metadata, "get_type"):
52+
t = event_metadata.get_type()
53+
meta["split_type"] = getattr(t, "value", str(t))
54+
if event_metadata is not None and hasattr(event_metadata, "get_names"):
55+
names = event_metadata.get_names()
56+
meta["split_names"] = list(names) if names is not None else []
57+
return meta
58+
59+
1460
class SplitProviderBase(AbstractProvider):
1561

1662
def get_metadata(self) -> Metadata:
1763
return Metadata("Split")
1864

65+
def attach(self, on_emit):
66+
super().attach(on_emit)
67+
self._split_client_wrapper.set_event_receiver(self)
68+
self._split_client_wrapper.register_for_split_events()
69+
70+
def detach(self):
71+
self._split_client_wrapper.unregister_for_split_events()
72+
super().detach()
73+
74+
def _on_split_event(self, split_event, event_metadata):
75+
"""Map Split SDK events to OpenFeature provider events with OpenFeature-friendly details."""
76+
_LOGGER.debug("SplitProvider: _on_split_event received %s", split_event)
77+
if split_event == SPLIT_EVENT_BUR_TIMEOUT:
78+
self.emit_provider_error(ProviderEventDetails(
79+
message="Block until ready timed out",
80+
error_code=ErrorCode.PROVIDER_NOT_READY,
81+
metadata=_metadata_from_split(split_event, event_metadata),
82+
))
83+
return
84+
if SdkEvent is None:
85+
return
86+
if split_event == SdkEvent.SDK_READY:
87+
self.emit_provider_ready(ProviderEventDetails(
88+
metadata=_metadata_from_split(split_event, event_metadata),
89+
))
90+
elif split_event == SdkEvent.SDK_UPDATE:
91+
flags_changed = _flags_changed_from_sdk_update(event_metadata)
92+
details = ProviderEventDetails(
93+
flags_changed=flags_changed,
94+
metadata=_metadata_from_split(split_event, event_metadata),
95+
)
96+
_LOGGER.info("SplitProvider: emitting PROVIDER_CONFIGURATION_CHANGED flags_changed=%s", flags_changed)
97+
self.emit_provider_configuration_changed(details)
98+
1999
def get_provider_hooks(self) -> typing.List[Hook]:
20100
return []
21101

0 commit comments

Comments
 (0)