From dc6cbfa6b8ead1144731bf13b4cef6348e08ca92 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 30 Oct 2024 12:09:47 -0500 Subject: [PATCH 1/4] feat: Adds support for client-side prerequisite events --- .../bin/contract_test_service.dart | 3 +- .../src/events/default_event_processor.dart | 2 +- .../common/lib/src/ld_evaluation_result.dart | 4 ++ .../ld_evaluation_result_serialization.dart | 7 +++ .../test/events/event_processor_test.dart | 2 + .../lib/src/ld_common_client.dart | 59 ++++++++++++++++- .../test/ld_dart_client_test.dart | 63 +++++++++++++++++++ .../test/mock_eventprocessor.dart | 37 +++++++++++ 8 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 packages/common_client/test/mock_eventprocessor.dart diff --git a/apps/flutter_client_contract_test_service/bin/contract_test_service.dart b/apps/flutter_client_contract_test_service/bin/contract_test_service.dart index 5f174e60..9b327545 100644 --- a/apps/flutter_client_contract_test_service/bin/contract_test_service.dart +++ b/apps/flutter_client_contract_test_service/bin/contract_test_service.dart @@ -24,7 +24,8 @@ class TestApiImpl extends SdkTestApi { 'client-independence', 'context-comparison', 'inline-context', - 'anonymous-redaction' + 'anonymous-redaction', + 'client-prereq-events', ]; static const clientUrlPrefix = '/client/'; diff --git a/packages/common/lib/src/events/default_event_processor.dart b/packages/common/lib/src/events/default_event_processor.dart index 9a471d1b..e68e496b 100644 --- a/packages/common/lib/src/events/default_event_processor.dart +++ b/packages/common/lib/src/events/default_event_processor.dart @@ -50,7 +50,7 @@ final class DefaultEventProcessor implements EventProcessor { DefaultEventProcessor( {required LDLogger logger, - bool indexEvents = false, + required bool indexEvents, required int eventCapacity, required Duration flushInterval, required HttpClient client, diff --git a/packages/common/lib/src/ld_evaluation_result.dart b/packages/common/lib/src/ld_evaluation_result.dart index 376db8fe..07a581a2 100644 --- a/packages/common/lib/src/ld_evaluation_result.dart +++ b/packages/common/lib/src/ld_evaluation_result.dart @@ -18,6 +18,9 @@ final class LDEvaluationResult { /// True if a client SDK should track reasons for this flag. final bool trackReason; + /// List of prerequisite flags that were evaluated as part of determining this [LDEvaluationResult] + final List? prerequisites; + /// A millisecond timestamp, which if the current time is before, a client SDK /// should send debug events for the flag. final int? debugEventsUntilDate; @@ -31,6 +34,7 @@ final class LDEvaluationResult { required this.detail, this.trackEvents = false, this.trackReason = false, + this.prerequisites, this.debugEventsUntilDate}); @override diff --git a/packages/common/lib/src/serialization/ld_evaluation_result_serialization.dart b/packages/common/lib/src/serialization/ld_evaluation_result_serialization.dart index 493f4dfd..f0fecdd5 100644 --- a/packages/common/lib/src/serialization/ld_evaluation_result_serialization.dart +++ b/packages/common/lib/src/serialization/ld_evaluation_result_serialization.dart @@ -6,6 +6,9 @@ final class LDEvaluationResultSerialization { final flagVersion = json['flagVersion'] as num?; final trackEvents = (json['trackEvents'] ?? false) as bool; final trackReason = (json['trackReason'] ?? false) as bool; + final prerequisites = (json['prerequisites'] as List?) + ?.map((e) => e as String) + .toList(); final debugEventsUntilDateRaw = json['debugEventsUntilDate'] as num?; final value = LDValueSerialization.fromJson(json['value']); final jsonReason = json['reason']; @@ -24,6 +27,7 @@ final class LDEvaluationResultSerialization { detail: LDEvaluationDetail(value, variationIndex, reason), trackEvents: trackEvents, trackReason: trackReason, + prerequisites: prerequisites, debugEventsUntilDate: debugEventsUntilDateRaw?.toInt()); } @@ -37,6 +41,9 @@ final class LDEvaluationResultSerialization { if (evaluationResult.trackReason) { result['trackReason'] = evaluationResult.trackReason; } + if (evaluationResult.prerequisites?.isNotEmpty ?? false) { + result['prerequisites'] = evaluationResult.prerequisites; + } if (evaluationResult.debugEventsUntilDate != null) { result['debugEventsUntilDate'] = evaluationResult.debugEventsUntilDate; } diff --git a/packages/common/test/events/event_processor_test.dart b/packages/common/test/events/event_processor_test.dart index ebd3bed0..6e9c26a3 100644 --- a/packages/common/test/events/event_processor_test.dart +++ b/packages/common/test/events/event_processor_test.dart @@ -23,6 +23,7 @@ import '../logging_test.dart'; return ( DefaultEventProcessor( logger: LDLogger(adapter: adapter), + indexEvents: false, eventCapacity: 100, flushInterval: Duration(milliseconds: 100), client: client, @@ -47,6 +48,7 @@ import '../logging_test.dart'; return ( DefaultEventProcessor( logger: LDLogger(adapter: adapter), + indexEvents: false, eventCapacity: 100, flushInterval: Duration(milliseconds: 100), client: client, diff --git a/packages/common_client/lib/src/ld_common_client.dart b/packages/common_client/lib/src/ld_common_client.dart index 5a17d7c0..5fc08693 100644 --- a/packages/common_client/lib/src/ld_common_client.dart +++ b/packages/common_client/lib/src/ld_common_client.dart @@ -77,6 +77,49 @@ Map _defaultFactories( }; } +typedef EventProcessorFactory = EventProcessor Function( + {required LDLogger logger, + required bool indexEvents, + required int eventCapacity, + required Duration flushInterval, + required HttpClient client, + required String analyticsEventsPath, + required String diagnosticEventsPath, + required ServiceEndpoints endpoints, + required Duration diagnosticRecordingInterval, + required bool allAttributesPrivate, + required Set globalPrivateAttributes, + DiagnosticsManager? diagnosticsManager} + ); + +EventProcessor _defaultEventProcessorFactory( + {required LDLogger logger, + required bool indexEvents, + required int eventCapacity, + required Duration flushInterval, + required HttpClient client, + required String analyticsEventsPath, + required String diagnosticEventsPath, + required ServiceEndpoints endpoints, + required Duration diagnosticRecordingInterval, + required bool allAttributesPrivate, + required Set globalPrivateAttributes, + DiagnosticsManager? diagnosticsManager}) { + return DefaultEventProcessor( + logger: logger, + indexEvents: indexEvents, + eventCapacity: eventCapacity, + flushInterval: flushInterval, + client: client, + analyticsEventsPath: analyticsEventsPath, + diagnosticEventsPath: diagnosticEventsPath, + diagnosticsManager: diagnosticsManager, + endpoints: endpoints, + allAttributesPrivate: allAttributesPrivate, + globalPrivateAttributes: globalPrivateAttributes, + diagnosticRecordingInterval: diagnosticRecordingInterval); +} + final class LDCommonClient { final LDCommonConfig _config; final Persistence _persistence; @@ -96,6 +139,8 @@ final class LDCommonClient { // If there are cross-dependent modifiers, then this must be considered. late final List _modifiers; + EventProcessorFactory _eventProcessorFactory; + /// The event processor is not constructed during LDCommonClient construction /// because it requires the HTTP properties which must be determined /// asynchronously. @@ -127,7 +172,8 @@ final class LDCommonClient { LDCommonClient(LDCommonConfig commonConfig, CommonPlatform platform, LDContext context, DiagnosticSdkData sdkData, - {DataSourceFactoriesFn? dataSourceFactories}) + {DataSourceFactoriesFn? dataSourceFactories, + EventProcessorFactory? eventProcessorFactory}) : _config = commonConfig, _platform = platform, _persistence = ValidatingPersistence( @@ -143,6 +189,8 @@ final class LDCommonClient { _initialUndecoratedContext = context, // Data source factories is primarily a mechanism for testing. _dataSourceFactories = dataSourceFactories ?? _defaultFactories, + _eventProcessorFactory = + eventProcessorFactory ?? _defaultEventProcessorFactory, _sdkData = sdkData { final dataSourceEventHandler = DataSourceEventHandler( flagManager: _flagManager, @@ -273,8 +321,9 @@ final class LDCommonClient { final osInfo = _envReport.osInfo; DiagnosticsManager? diagnosticsManager = _makeDiagnosticsManager(osInfo); - _eventProcessor = DefaultEventProcessor( + _eventProcessor = _eventProcessorFactory( logger: _logger, + indexEvents: false, eventCapacity: _config.events.eventCapacity, flushInterval: _config.events.flushInterval, client: HttpClient(httpProperties: httpProperties), @@ -528,6 +577,12 @@ final class LDCommonClient { LDEvaluationDetail detail; if (evalResult != null && evalResult.flag != null) { + if (evalResult.flag?.prerequisites != null) { + evalResult.flag?.prerequisites?.forEach((prereq) { + _variationInternal(prereq, LDValue.ofNull(), isDetailed: isDetailed); + }); + } + if (type == null || type == evalResult.flag!.detail.value.type) { detail = evalResult.flag!.detail; } else { diff --git a/packages/common_client/test/ld_dart_client_test.dart b/packages/common_client/test/ld_dart_client_test.dart index f4ee75ee..18b66454 100644 --- a/packages/common_client/test/ld_dart_client_test.dart +++ b/packages/common_client/test/ld_dart_client_test.dart @@ -6,6 +6,7 @@ import 'package:launchdarkly_common_client/launchdarkly_common_client.dart'; import 'package:launchdarkly_common_client/src/data_sources/data_source.dart'; import 'package:test/test.dart'; +import 'mock_eventprocessor.dart'; import 'mock_persistence.dart'; final class TestConfig extends LDCommonConfig { @@ -284,4 +285,66 @@ void main() { expect(res, 'datasource'); }); }); + + group('given mock flag data with prerequisites', () { + late LDCommonClient client; + late MockPersistence mockPersistence; + late MockEventProcessor mockEventProcessor; + final sdkKey = 'the-sdk-key'; + final sdkKeyPersistence = + 'LaunchDarkly_${sha256.convert(utf8.encode(sdkKey))}'; + + setUp(() { + mockPersistence = MockPersistence(); + mockEventProcessor = MockEventProcessor(); + client = LDCommonClient( + TestConfig(sdkKey, AutoEnvAttributes.disabled), + CommonPlatform(persistence: mockPersistence), + LDContextBuilder().kind('user', 'bob').build(), + DiagnosticSdkData(name: '', version: ''), + dataSourceFactories: (LDCommonConfig config, LDLogger logger, + HttpProperties properties) { + return { + ConnectionMode.streaming: (LDContext context) { + return TestDataSource(); + }, + ConnectionMode.polling: (LDContext context) { + return TestDataSource(); + }, + }; + }, + eventProcessorFactory: ( + {required allAttributesPrivate, + required analyticsEventsPath, + required client, + required diagnosticEventsPath, + required diagnosticRecordingInterval, + diagnosticsManager, + required endpoints, + required eventCapacity, + required flushInterval, + required globalPrivateAttributes, + required indexEvents, + required logger}) => + mockEventProcessor, + ); + }); + + test('it includes reports events for each prerequisite', () async { + final contextPersistenceKey = + sha256.convert(utf8.encode('bob')).toString(); + mockPersistence.storage[sdkKeyPersistence] = { + contextPersistenceKey: + '{"flagA":{"version":1,"value":"storage","variation":0,"reason":{"kind":"OFF"},"prerequisites":["flagAB"]},"flagAB":{"version":1,"value":"storage","variation":0,"reason":{"kind":"OFF"}}}' + }; + + await client + .start(); // note no call to wait for network results here so we get the storage values + final res = client.stringVariation('flagA', 'default'); + expect(res, 'storage'); + expect(mockEventProcessor.evalEvents.length, 2); + expect(mockEventProcessor.evalEvents[0].flagKey, 'flagAB'); + expect(mockEventProcessor.evalEvents[1].flagKey, 'flagA'); + }); + }); } diff --git a/packages/common_client/test/mock_eventprocessor.dart b/packages/common_client/test/mock_eventprocessor.dart new file mode 100644 index 00000000..fa30ccfb --- /dev/null +++ b/packages/common_client/test/mock_eventprocessor.dart @@ -0,0 +1,37 @@ +import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart'; + +final class MockEventProcessor implements EventProcessor { + final customEvents = []; + final evalEvents = []; + final identifyEvents = []; + + @override + Future flush() async { + // no-op in this mock + } + + @override + void processCustomEvent(CustomEvent event) { + customEvents.add(event); + } + + @override + void processEvalEvent(EvalEvent event) { + evalEvents.add(event); + } + + @override + void processIdentifyEvent(IdentifyEvent event) { + identifyEvents.add(event); + } + + @override + void start() { + // no-op in this mock + } + + @override + void stop() { + // no-op in this mock + } +} From dc1eb116d6ba44e89765cdd10596f7f4ecf5ff34 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 30 Oct 2024 12:15:59 -0500 Subject: [PATCH 2/4] fixing lint issue --- packages/common_client/lib/src/ld_common_client.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common_client/lib/src/ld_common_client.dart b/packages/common_client/lib/src/ld_common_client.dart index 5fc08693..d756140e 100644 --- a/packages/common_client/lib/src/ld_common_client.dart +++ b/packages/common_client/lib/src/ld_common_client.dart @@ -139,7 +139,7 @@ final class LDCommonClient { // If there are cross-dependent modifiers, then this must be considered. late final List _modifiers; - EventProcessorFactory _eventProcessorFactory; + final EventProcessorFactory _eventProcessorFactory; /// The event processor is not constructed during LDCommonClient construction /// because it requires the HTTP properties which must be determined From 0008871931ab86b60fb04e2561fdc78bcf04f89e Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 30 Oct 2024 13:33:38 -0500 Subject: [PATCH 3/4] dart format --- .../ld_evaluation_result_serialization.dart | 4 ++-- packages/common_client/lib/src/ld_common_client.dart | 3 +-- packages/common_client/test/mock_eventprocessor.dart | 12 ++++++------ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/common/lib/src/serialization/ld_evaluation_result_serialization.dart b/packages/common/lib/src/serialization/ld_evaluation_result_serialization.dart index f0fecdd5..61b20b69 100644 --- a/packages/common/lib/src/serialization/ld_evaluation_result_serialization.dart +++ b/packages/common/lib/src/serialization/ld_evaluation_result_serialization.dart @@ -7,8 +7,8 @@ final class LDEvaluationResultSerialization { final trackEvents = (json['trackEvents'] ?? false) as bool; final trackReason = (json['trackReason'] ?? false) as bool; final prerequisites = (json['prerequisites'] as List?) - ?.map((e) => e as String) - .toList(); + ?.map((e) => e as String) + .toList(); final debugEventsUntilDateRaw = json['debugEventsUntilDate'] as num?; final value = LDValueSerialization.fromJson(json['value']); final jsonReason = json['reason']; diff --git a/packages/common_client/lib/src/ld_common_client.dart b/packages/common_client/lib/src/ld_common_client.dart index d756140e..f0749acb 100644 --- a/packages/common_client/lib/src/ld_common_client.dart +++ b/packages/common_client/lib/src/ld_common_client.dart @@ -89,8 +89,7 @@ typedef EventProcessorFactory = EventProcessor Function( required Duration diagnosticRecordingInterval, required bool allAttributesPrivate, required Set globalPrivateAttributes, - DiagnosticsManager? diagnosticsManager} - ); + DiagnosticsManager? diagnosticsManager}); EventProcessor _defaultEventProcessorFactory( {required LDLogger logger, diff --git a/packages/common_client/test/mock_eventprocessor.dart b/packages/common_client/test/mock_eventprocessor.dart index fa30ccfb..9ae8d574 100644 --- a/packages/common_client/test/mock_eventprocessor.dart +++ b/packages/common_client/test/mock_eventprocessor.dart @@ -4,32 +4,32 @@ final class MockEventProcessor implements EventProcessor { final customEvents = []; final evalEvents = []; final identifyEvents = []; - + @override Future flush() async { // no-op in this mock } - + @override void processCustomEvent(CustomEvent event) { customEvents.add(event); } - + @override void processEvalEvent(EvalEvent event) { evalEvents.add(event); } - + @override void processIdentifyEvent(IdentifyEvent event) { identifyEvents.add(event); } - + @override void start() { // no-op in this mock } - + @override void stop() { // no-op in this mock From 207ba2db5c349faab2029882004589bf4bbf3c23 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 31 Oct 2024 09:42:29 -0500 Subject: [PATCH 4/4] removing unnecessary if condition --- packages/common_client/lib/src/ld_common_client.dart | 8 +++----- packages/common_client/test/ld_dart_client_test.dart | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/common_client/lib/src/ld_common_client.dart b/packages/common_client/lib/src/ld_common_client.dart index f0749acb..211d4937 100644 --- a/packages/common_client/lib/src/ld_common_client.dart +++ b/packages/common_client/lib/src/ld_common_client.dart @@ -576,11 +576,9 @@ final class LDCommonClient { LDEvaluationDetail detail; if (evalResult != null && evalResult.flag != null) { - if (evalResult.flag?.prerequisites != null) { - evalResult.flag?.prerequisites?.forEach((prereq) { - _variationInternal(prereq, LDValue.ofNull(), isDetailed: isDetailed); - }); - } + evalResult.flag?.prerequisites?.forEach((prereq) { + _variationInternal(prereq, LDValue.ofNull(), isDetailed: isDetailed); + }); if (type == null || type == evalResult.flag!.detail.value.type) { detail = evalResult.flag!.detail; diff --git a/packages/common_client/test/ld_dart_client_test.dart b/packages/common_client/test/ld_dart_client_test.dart index 18b66454..e719631e 100644 --- a/packages/common_client/test/ld_dart_client_test.dart +++ b/packages/common_client/test/ld_dart_client_test.dart @@ -330,7 +330,7 @@ void main() { ); }); - test('it includes reports events for each prerequisite', () async { + test('it reports events for each prerequisite', () async { final contextPersistenceKey = sha256.convert(utf8.encode('bob')).toString(); mockPersistence.storage[sdkKeyPersistence] = {