11import pytest
22from ldclient import Config , Context , LDClient
3+ from ldclient .evaluation import EvaluationDetail
4+ from ldclient .hook import EvaluationSeriesContext
35from ldclient .integrations .test_data import TestData
46from opentelemetry .sdk .trace import TracerProvider
5- from opentelemetry .sdk .trace .export import SimpleSpanProcessor
6- from opentelemetry .sdk .trace .export .in_memory_span_exporter import (
7- InMemorySpanExporter , SpanExporter )
7+ from opentelemetry .sdk .trace .export import SimpleSpanProcessor , SpanExporter
8+ from opentelemetry .sdk .trace .export .in_memory_span_exporter import \
9+ InMemorySpanExporter
810from opentelemetry .trace import (Tracer , get_tracer_provider ,
911 set_tracer_provider )
1012
@@ -59,9 +61,11 @@ def test_records_basic_span_event(self, client: LDClient, exporter: SpanExporter
5961 event = spans [0 ].events [0 ]
6062 assert event .name == 'feature_flag'
6163 assert event .attributes ['feature_flag.key' ] == 'boolean'
62- assert event .attributes ['feature_flag.provider_name' ] == 'LaunchDarkly'
63- assert event .attributes ['feature_flag.context.key' ] == 'org:org-key'
64- assert 'feature_flag.variant' not in event .attributes
64+ assert event .attributes ['feature_flag.provider.name' ] == 'LaunchDarkly'
65+ assert event .attributes ['feature_flag.context.id' ] == 'org:org-key'
66+ assert event .attributes ['feature_flag.result.variationIndex' ] == '0'
67+ assert 'feature_flag.result.value' not in event .attributes
68+ assert 'feature_flag.result.reason.inExperiment' not in event .attributes
6569
6670 def test_can_include_variant (self , client : LDClient , exporter : SpanExporter , tracer : Tracer ):
6771 client .add_hook (Hook (HookOptions (include_variant = True )))
@@ -75,9 +79,42 @@ def test_can_include_variant(self, client: LDClient, exporter: SpanExporter, tra
7579 event = spans [0 ].events [0 ]
7680 assert event .name == 'feature_flag'
7781 assert event .attributes ['feature_flag.key' ] == 'boolean'
78- assert event .attributes ['feature_flag.provider_name' ] == 'LaunchDarkly'
79- assert event .attributes ['feature_flag.context.key' ] == 'org:org-key'
80- assert event .attributes ['feature_flag.variant' ] == 'True'
82+ assert event .attributes ['feature_flag.provider.name' ] == 'LaunchDarkly'
83+ assert event .attributes ['feature_flag.context.id' ] == 'org:org-key'
84+ assert event .attributes ['feature_flag.result.variationIndex' ] == '0'
85+ assert event .attributes ['feature_flag.result.value' ] == 'true'
86+ assert 'feature_flag.result.reason.inExperiment' not in event .attributes
87+
88+ @pytest .mark .parametrize ("flag_key, variations, variation_index, expected_value" , [
89+ ("string-flag" , ["alpha" , "beta" ], 1 , "beta" ),
90+ ("number-flag" , [42 , 99 ], 0 , 42 ),
91+ ("array-flag" , [[1 , 2 ], [3 , 4 ]], 1 , [3 , 4 ]),
92+ ("object-flag" , [{"a" : 1 }, {"b" : 2 }], 0 , {"a" : 1 }),
93+ ])
94+ def test_can_include_value_types (self , flag_key , variations , variation_index , expected_value , exporter : SpanExporter , tracer : Tracer ):
95+ td = TestData .data_source ()
96+ td .update (td .flag (flag_key ).variations (* variations ).variation_for_all (variation_index ))
97+ config = Config ('sdk-key' , update_processor_class = td , send_events = False )
98+ client = LDClient (config = config )
99+ client .add_hook (Hook (HookOptions (include_value = True )))
100+
101+ with tracer .start_as_current_span (f"test_can_include_value_types_{ flag_key } " ):
102+ context = Context .create ('org-key' , 'org' )
103+ client .variation (flag_key , context , None )
104+
105+ spans = exporter .get_finished_spans () # type: ignore[attr-defined]
106+ assert len (spans ) == 1
107+ assert len (spans [0 ].events ) == 1
108+
109+ import json
110+ event = spans [0 ].events [0 ]
111+ assert event .name == 'feature_flag'
112+ assert event .attributes ['feature_flag.key' ] == flag_key
113+ assert event .attributes ['feature_flag.provider.name' ] == 'LaunchDarkly'
114+ assert event .attributes ['feature_flag.context.id' ] == 'org:org-key'
115+ assert event .attributes ['feature_flag.result.variationIndex' ] == str (variation_index )
116+ assert event .attributes ['feature_flag.result.value' ] == json .dumps (expected_value )
117+ assert 'feature_flag.result.reason.inExperiment' not in event .attributes
81118
82119 def test_add_span_creates_span_if_one_not_active (self , client : LDClient , exporter : SpanExporter , tracer : Tracer ):
83120 client .add_hook (Hook (HookOptions (add_spans = True )))
@@ -86,7 +123,7 @@ def test_add_span_creates_span_if_one_not_active(self, client: LDClient, exporte
86123 spans = exporter .get_finished_spans () # type: ignore[attr-defined]
87124 assert len (spans ) == 1
88125
89- assert spans [0 ].attributes ['feature_flag.context.key ' ] == 'org:org-key'
126+ assert spans [0 ].attributes ['feature_flag.context.id ' ] == 'org:org-key'
90127 assert spans [0 ].attributes ['feature_flag.key' ] == 'boolean'
91128 assert len (spans [0 ].events ) == 0
92129
@@ -101,15 +138,17 @@ def test_add_span_leaves_events_on_top_level_span(self, client: LDClient, export
101138 ld_span = spans [0 ]
102139 toplevel = spans [1 ]
103140
104- assert ld_span .attributes ['feature_flag.context.key ' ] == 'org:org-key'
141+ assert ld_span .attributes ['feature_flag.context.id ' ] == 'org:org-key'
105142 assert ld_span .attributes ['feature_flag.key' ] == 'boolean'
106143
107144 event = toplevel .events [0 ]
108145 assert event .name == 'feature_flag'
109146 assert event .attributes ['feature_flag.key' ] == 'boolean'
110- assert event .attributes ['feature_flag.provider_name' ] == 'LaunchDarkly'
111- assert event .attributes ['feature_flag.context.key' ] == 'org:org-key'
112- assert 'feature_flag.variant' not in event .attributes
147+ assert event .attributes ['feature_flag.provider.name' ] == 'LaunchDarkly'
148+ assert event .attributes ['feature_flag.context.id' ] == 'org:org-key'
149+ assert event .attributes ['feature_flag.result.variationIndex' ] == '0'
150+ assert 'feature_flag.result.value' not in event .attributes
151+ assert 'feature_flag.result.reason.inExperiment' not in event .attributes
113152
114153 def test_hook_makes_its_span_active (self , client : LDClient , exporter : SpanExporter , tracer : Tracer ):
115154 client .add_hook (Hook (HookOptions (add_spans = True )))
@@ -125,20 +164,90 @@ def test_hook_makes_its_span_active(self, client: LDClient, exporter: SpanExport
125164 middle = spans [1 ]
126165 top = spans [2 ]
127166
128- assert inner .attributes ['feature_flag.context.key ' ] == 'org:org-key'
167+ assert inner .attributes ['feature_flag.context.id ' ] == 'org:org-key'
129168 assert inner .attributes ['feature_flag.key' ] == 'boolean'
130169 assert len (inner .events ) == 0
131170
132- assert middle .attributes ['feature_flag.context.key ' ] == 'org:org-key'
171+ assert middle .attributes ['feature_flag.context.id ' ] == 'org:org-key'
133172 assert middle .attributes ['feature_flag.key' ] == 'boolean'
134173 assert middle .events [0 ].name == 'feature_flag'
135174 assert middle .events [0 ].attributes ['feature_flag.key' ] == 'boolean'
136- assert middle .events [0 ].attributes ['feature_flag.provider_name' ] == 'LaunchDarkly'
137- assert middle .events [0 ].attributes ['feature_flag.context.key' ] == 'org:org-key'
138- assert 'feature_flag.variant' not in middle .events [0 ].attributes
175+ assert middle .events [0 ].attributes ['feature_flag.provider.name' ] == 'LaunchDarkly'
176+ assert middle .events [0 ].attributes ['feature_flag.context.id' ] == 'org:org-key'
177+ assert middle .events [0 ].attributes ['feature_flag.result.variationIndex' ] == '0'
178+ assert 'feature_flag.result.value' not in middle .events [0 ].attributes
179+ assert 'feature_flag.result.reason.inExperiment' not in middle .events [0 ].attributes
139180
140181 assert top .events [0 ].name == 'feature_flag'
141182 assert top .events [0 ].attributes ['feature_flag.key' ] == 'boolean'
142- assert top .events [0 ].attributes ['feature_flag.provider_name' ] == 'LaunchDarkly'
143- assert top .events [0 ].attributes ['feature_flag.context.key' ] == 'org:org-key'
144- assert 'feature_flag.variant' not in top .events [0 ].attributes
183+ assert top .events [0 ].attributes ['feature_flag.provider.name' ] == 'LaunchDarkly'
184+ assert top .events [0 ].attributes ['feature_flag.context.id' ] == 'org:org-key'
185+ assert top .events [0 ].attributes ['feature_flag.result.variationIndex' ] == '0'
186+ assert 'feature_flag.result.value' not in top .events [0 ].attributes
187+ assert 'feature_flag.result.reason.inExperiment' not in top .events [0 ].attributes
188+
189+ def test_records_in_experiment_attribute (self , exporter : SpanExporter , tracer : Tracer ):
190+ series_context = EvaluationSeriesContext (
191+ key = 'experiment-flag' ,
192+ context = Context .create ('org-key' , 'org' ),
193+ default_value = False ,
194+ method = 'variation' ,
195+ )
196+
197+ # Create an EvaluationDetail with inExperiment=True in the reason
198+ detail = EvaluationDetail (
199+ value = True ,
200+ variation_index = 1 ,
201+ reason = {"inExperiment" : True }
202+ )
203+
204+ hook = Hook ()
205+ with tracer .start_as_current_span ("test_records_in_experiment_attribute" ):
206+ data = hook .before_evaluation (series_context , {}) # type: ignore
207+ hook .after_evaluation (series_context , data , detail ) # type: ignore
208+
209+ spans = exporter .get_finished_spans () # type: ignore[attr-defined]
210+ assert len (spans ) == 1
211+ assert len (spans [0 ].events ) == 1
212+
213+ event = spans [0 ].events [0 ]
214+ assert event .name == 'feature_flag'
215+ assert event .attributes ['feature_flag.key' ] == 'experiment-flag'
216+ assert event .attributes ['feature_flag.provider.name' ] == 'LaunchDarkly'
217+ assert event .attributes ['feature_flag.context.id' ] == 'org:org-key'
218+ assert event .attributes ['feature_flag.result.variationIndex' ] == '1'
219+ assert event .attributes ['feature_flag.result.reason.inExperiment' ] == 'true'
220+ assert 'feature_flag.result.value' not in event .attributes
221+
222+ def test_does_not_include_variation_index_when_none (self , exporter : SpanExporter , tracer : Tracer ):
223+ series_context = EvaluationSeriesContext (
224+ key = 'flag-without-variation' ,
225+ context = Context .create ('org-key' , 'org' ),
226+ default_value = False ,
227+ method = 'variation' ,
228+ )
229+
230+ detail = EvaluationDetail (
231+ value = False ,
232+ variation_index = None ,
233+ reason = {"kind" : "FALLTHROUGH" }
234+ )
235+
236+ hook = Hook ()
237+ with tracer .start_as_current_span ("test_does_not_include_variation_index_when_none" ):
238+ data = hook .before_evaluation (series_context , {}) # type: ignore
239+ hook .after_evaluation (series_context , data , detail ) # type: ignore
240+
241+ spans = exporter .get_finished_spans () # type: ignore[attr-defined]
242+ assert len (spans ) == 1
243+ assert len (spans [0 ].events ) == 1
244+
245+ event = spans [0 ].events [0 ]
246+ assert event .name == 'feature_flag'
247+ assert event .attributes ['feature_flag.key' ] == 'flag-without-variation'
248+ assert event .attributes ['feature_flag.provider.name' ] == 'LaunchDarkly'
249+ assert event .attributes ['feature_flag.context.id' ] == 'org:org-key'
250+ # variationIndex should not be present when variation_index is None
251+ assert 'feature_flag.result.variationIndex' not in event .attributes
252+ assert 'feature_flag.result.reason.inExperiment' not in event .attributes
253+ assert 'feature_flag.result.value' not in event .attributes
0 commit comments