Skip to content

Commit 7a042c2

Browse files
authored
feat: Adds support for client-side prerequisite events (#172)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions Exception: Changes are not related to platforms. **Related issues** SDK-691
1 parent 3061517 commit 7a042c2

File tree

8 files changed

+170
-4
lines changed

8 files changed

+170
-4
lines changed

apps/flutter_client_contract_test_service/bin/contract_test_service.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class TestApiImpl extends SdkTestApi {
2424
'client-independence',
2525
'context-comparison',
2626
'inline-context',
27-
'anonymous-redaction'
27+
'anonymous-redaction',
28+
'client-prereq-events',
2829
];
2930

3031
static const clientUrlPrefix = '/client/';

packages/common/lib/src/events/default_event_processor.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ final class DefaultEventProcessor implements EventProcessor {
5050

5151
DefaultEventProcessor(
5252
{required LDLogger logger,
53-
bool indexEvents = false,
53+
required bool indexEvents,
5454
required int eventCapacity,
5555
required Duration flushInterval,
5656
required HttpClient client,

packages/common/lib/src/ld_evaluation_result.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ final class LDEvaluationResult {
1818
/// True if a client SDK should track reasons for this flag.
1919
final bool trackReason;
2020

21+
/// List of prerequisite flags that were evaluated as part of determining this [LDEvaluationResult]
22+
final List<String>? prerequisites;
23+
2124
/// A millisecond timestamp, which if the current time is before, a client SDK
2225
/// should send debug events for the flag.
2326
final int? debugEventsUntilDate;
@@ -31,6 +34,7 @@ final class LDEvaluationResult {
3134
required this.detail,
3235
this.trackEvents = false,
3336
this.trackReason = false,
37+
this.prerequisites,
3438
this.debugEventsUntilDate});
3539

3640
@override

packages/common/lib/src/serialization/ld_evaluation_result_serialization.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ final class LDEvaluationResultSerialization {
66
final flagVersion = json['flagVersion'] as num?;
77
final trackEvents = (json['trackEvents'] ?? false) as bool;
88
final trackReason = (json['trackReason'] ?? false) as bool;
9+
final prerequisites = (json['prerequisites'] as List<dynamic>?)
10+
?.map((e) => e as String)
11+
.toList();
912
final debugEventsUntilDateRaw = json['debugEventsUntilDate'] as num?;
1013
final value = LDValueSerialization.fromJson(json['value']);
1114
final jsonReason = json['reason'];
@@ -24,6 +27,7 @@ final class LDEvaluationResultSerialization {
2427
detail: LDEvaluationDetail(value, variationIndex, reason),
2528
trackEvents: trackEvents,
2629
trackReason: trackReason,
30+
prerequisites: prerequisites,
2731
debugEventsUntilDate: debugEventsUntilDateRaw?.toInt());
2832
}
2933

@@ -37,6 +41,9 @@ final class LDEvaluationResultSerialization {
3741
if (evaluationResult.trackReason) {
3842
result['trackReason'] = evaluationResult.trackReason;
3943
}
44+
if (evaluationResult.prerequisites?.isNotEmpty ?? false) {
45+
result['prerequisites'] = evaluationResult.prerequisites;
46+
}
4047
if (evaluationResult.debugEventsUntilDate != null) {
4148
result['debugEventsUntilDate'] = evaluationResult.debugEventsUntilDate;
4249
}

packages/common/test/events/event_processor_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import '../logging_test.dart';
2323
return (
2424
DefaultEventProcessor(
2525
logger: LDLogger(adapter: adapter),
26+
indexEvents: false,
2627
eventCapacity: 100,
2728
flushInterval: Duration(milliseconds: 100),
2829
client: client,
@@ -47,6 +48,7 @@ import '../logging_test.dart';
4748
return (
4849
DefaultEventProcessor(
4950
logger: LDLogger(adapter: adapter),
51+
indexEvents: false,
5052
eventCapacity: 100,
5153
flushInterval: Duration(milliseconds: 100),
5254
client: client,

packages/common_client/lib/src/ld_common_client.dart

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,48 @@ Map<ConnectionMode, DataSourceFactory> _defaultFactories(
7777
};
7878
}
7979

80+
typedef EventProcessorFactory = EventProcessor Function(
81+
{required LDLogger logger,
82+
required bool indexEvents,
83+
required int eventCapacity,
84+
required Duration flushInterval,
85+
required HttpClient client,
86+
required String analyticsEventsPath,
87+
required String diagnosticEventsPath,
88+
required ServiceEndpoints endpoints,
89+
required Duration diagnosticRecordingInterval,
90+
required bool allAttributesPrivate,
91+
required Set<AttributeReference> globalPrivateAttributes,
92+
DiagnosticsManager? diagnosticsManager});
93+
94+
EventProcessor _defaultEventProcessorFactory(
95+
{required LDLogger logger,
96+
required bool indexEvents,
97+
required int eventCapacity,
98+
required Duration flushInterval,
99+
required HttpClient client,
100+
required String analyticsEventsPath,
101+
required String diagnosticEventsPath,
102+
required ServiceEndpoints endpoints,
103+
required Duration diagnosticRecordingInterval,
104+
required bool allAttributesPrivate,
105+
required Set<AttributeReference> globalPrivateAttributes,
106+
DiagnosticsManager? diagnosticsManager}) {
107+
return DefaultEventProcessor(
108+
logger: logger,
109+
indexEvents: indexEvents,
110+
eventCapacity: eventCapacity,
111+
flushInterval: flushInterval,
112+
client: client,
113+
analyticsEventsPath: analyticsEventsPath,
114+
diagnosticEventsPath: diagnosticEventsPath,
115+
diagnosticsManager: diagnosticsManager,
116+
endpoints: endpoints,
117+
allAttributesPrivate: allAttributesPrivate,
118+
globalPrivateAttributes: globalPrivateAttributes,
119+
diagnosticRecordingInterval: diagnosticRecordingInterval);
120+
}
121+
80122
final class LDCommonClient {
81123
final LDCommonConfig _config;
82124
final Persistence _persistence;
@@ -96,6 +138,8 @@ final class LDCommonClient {
96138
// If there are cross-dependent modifiers, then this must be considered.
97139
late final List<ContextModifier> _modifiers;
98140

141+
final EventProcessorFactory _eventProcessorFactory;
142+
99143
/// The event processor is not constructed during LDCommonClient construction
100144
/// because it requires the HTTP properties which must be determined
101145
/// asynchronously.
@@ -127,7 +171,8 @@ final class LDCommonClient {
127171

128172
LDCommonClient(LDCommonConfig commonConfig, CommonPlatform platform,
129173
LDContext context, DiagnosticSdkData sdkData,
130-
{DataSourceFactoriesFn? dataSourceFactories})
174+
{DataSourceFactoriesFn? dataSourceFactories,
175+
EventProcessorFactory? eventProcessorFactory})
131176
: _config = commonConfig,
132177
_platform = platform,
133178
_persistence = ValidatingPersistence(
@@ -143,6 +188,8 @@ final class LDCommonClient {
143188
_initialUndecoratedContext = context,
144189
// Data source factories is primarily a mechanism for testing.
145190
_dataSourceFactories = dataSourceFactories ?? _defaultFactories,
191+
_eventProcessorFactory =
192+
eventProcessorFactory ?? _defaultEventProcessorFactory,
146193
_sdkData = sdkData {
147194
final dataSourceEventHandler = DataSourceEventHandler(
148195
flagManager: _flagManager,
@@ -273,8 +320,9 @@ final class LDCommonClient {
273320
final osInfo = _envReport.osInfo;
274321
DiagnosticsManager? diagnosticsManager = _makeDiagnosticsManager(osInfo);
275322

276-
_eventProcessor = DefaultEventProcessor(
323+
_eventProcessor = _eventProcessorFactory(
277324
logger: _logger,
325+
indexEvents: false,
278326
eventCapacity: _config.events.eventCapacity,
279327
flushInterval: _config.events.flushInterval,
280328
client: HttpClient(httpProperties: httpProperties),
@@ -528,6 +576,10 @@ final class LDCommonClient {
528576
LDEvaluationDetail<LDValue> detail;
529577

530578
if (evalResult != null && evalResult.flag != null) {
579+
evalResult.flag?.prerequisites?.forEach((prereq) {
580+
_variationInternal(prereq, LDValue.ofNull(), isDetailed: isDetailed);
581+
});
582+
531583
if (type == null || type == evalResult.flag!.detail.value.type) {
532584
detail = evalResult.flag!.detail;
533585
} else {

packages/common_client/test/ld_dart_client_test.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:launchdarkly_common_client/launchdarkly_common_client.dart';
66
import 'package:launchdarkly_common_client/src/data_sources/data_source.dart';
77
import 'package:test/test.dart';
88

9+
import 'mock_eventprocessor.dart';
910
import 'mock_persistence.dart';
1011

1112
final class TestConfig extends LDCommonConfig {
@@ -284,4 +285,66 @@ void main() {
284285
expect(res, 'datasource');
285286
});
286287
});
288+
289+
group('given mock flag data with prerequisites', () {
290+
late LDCommonClient client;
291+
late MockPersistence mockPersistence;
292+
late MockEventProcessor mockEventProcessor;
293+
final sdkKey = 'the-sdk-key';
294+
final sdkKeyPersistence =
295+
'LaunchDarkly_${sha256.convert(utf8.encode(sdkKey))}';
296+
297+
setUp(() {
298+
mockPersistence = MockPersistence();
299+
mockEventProcessor = MockEventProcessor();
300+
client = LDCommonClient(
301+
TestConfig(sdkKey, AutoEnvAttributes.disabled),
302+
CommonPlatform(persistence: mockPersistence),
303+
LDContextBuilder().kind('user', 'bob').build(),
304+
DiagnosticSdkData(name: '', version: ''),
305+
dataSourceFactories: (LDCommonConfig config, LDLogger logger,
306+
HttpProperties properties) {
307+
return {
308+
ConnectionMode.streaming: (LDContext context) {
309+
return TestDataSource();
310+
},
311+
ConnectionMode.polling: (LDContext context) {
312+
return TestDataSource();
313+
},
314+
};
315+
},
316+
eventProcessorFactory: (
317+
{required allAttributesPrivate,
318+
required analyticsEventsPath,
319+
required client,
320+
required diagnosticEventsPath,
321+
required diagnosticRecordingInterval,
322+
diagnosticsManager,
323+
required endpoints,
324+
required eventCapacity,
325+
required flushInterval,
326+
required globalPrivateAttributes,
327+
required indexEvents,
328+
required logger}) =>
329+
mockEventProcessor,
330+
);
331+
});
332+
333+
test('it reports events for each prerequisite', () async {
334+
final contextPersistenceKey =
335+
sha256.convert(utf8.encode('bob')).toString();
336+
mockPersistence.storage[sdkKeyPersistence] = {
337+
contextPersistenceKey:
338+
'{"flagA":{"version":1,"value":"storage","variation":0,"reason":{"kind":"OFF"},"prerequisites":["flagAB"]},"flagAB":{"version":1,"value":"storage","variation":0,"reason":{"kind":"OFF"}}}'
339+
};
340+
341+
await client
342+
.start(); // note no call to wait for network results here so we get the storage values
343+
final res = client.stringVariation('flagA', 'default');
344+
expect(res, 'storage');
345+
expect(mockEventProcessor.evalEvents.length, 2);
346+
expect(mockEventProcessor.evalEvents[0].flagKey, 'flagAB');
347+
expect(mockEventProcessor.evalEvents[1].flagKey, 'flagA');
348+
});
349+
});
287350
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart';
2+
3+
final class MockEventProcessor implements EventProcessor {
4+
final customEvents = <CustomEvent>[];
5+
final evalEvents = <EvalEvent>[];
6+
final identifyEvents = <IdentifyEvent>[];
7+
8+
@override
9+
Future<void> flush() async {
10+
// no-op in this mock
11+
}
12+
13+
@override
14+
void processCustomEvent(CustomEvent event) {
15+
customEvents.add(event);
16+
}
17+
18+
@override
19+
void processEvalEvent(EvalEvent event) {
20+
evalEvents.add(event);
21+
}
22+
23+
@override
24+
void processIdentifyEvent(IdentifyEvent event) {
25+
identifyEvents.add(event);
26+
}
27+
28+
@override
29+
void start() {
30+
// no-op in this mock
31+
}
32+
33+
@override
34+
void stop() {
35+
// no-op in this mock
36+
}
37+
}

0 commit comments

Comments
 (0)