Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 16 additions & 0 deletions packages/common/test/ld_context_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ void main() {
expect(LDContextBuilder().build().canonicalKey, '');
});

test('can modify an invalid context to become valid', () {
// Start with an invalid context (no kind specified)
final invalidContext = LDContextBuilder().build();
expect(invalidContext.valid, false);
expect(invalidContext.canonicalKey, '');

// Modify it to become valid by adding a valid kind
final validContext = LDContextBuilder.fromContext(invalidContext)
.kind('user', 'user-key')
.build();

expect(validContext.valid, true);
expect(validContext.canonicalKey, 'user-key');
expect(validContext.keys, <String, String>{'user': 'user-key'});
});

test('can change the key of a context during build', () {
final context = LDContextBuilder()
.kind('user', 'user-key')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ const _anonContextKeyNamespace = 'LaunchDarkly_AnonContextKey';

final class AnonymousContextModifier implements ContextModifier {
final Persistence _persistence;
final LDLogger _logger;

AnonymousContextModifier(Persistence persistence)
: _persistence = persistence;
AnonymousContextModifier(Persistence persistence, LDLogger logger)
: _persistence = persistence,
_logger = logger;

/// For any anonymous contexts, which do not have keys specified, generate
/// or read a persisted key for the anonymous kinds present. If persistence
/// is available, then the key will be stable.
@override
Future<LDContext> decorate(LDContext context) async {
if (!context.valid) {
return context;
_logger.info(
'AnonymousContextModifier was asked to modify an invalid context and will attempt to do so. This is expected if starting with an empty context.');
}
// Before we make a builder we should check if any anonymous contexts
// without keys exist.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ final class AutoEnvContextModifier implements ContextModifier {

@override
Future<LDContext> decorate(LDContext context) async {
if (!context.valid) {
_logger.info(
'AutoEnvContextModifier was asked to modify an invalid context and will attempt to do so. This is expected if starting with an empty context.');
}

final builder = LDContextBuilder.fromContext(context);

for (final recipe in _recipes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import 'data_source_status.dart';

typedef MessageHandler = void Function(MessageEvent);
typedef ErrorHandler = void Function(dynamic);
typedef SseClientFactory = SSEClient Function(Uri uri,
HttpProperties httpProperties, String? body, SseHttpMethod? method);
typedef SseClientFactory = SSEClient Function(
Uri uri,
HttpProperties httpProperties,
String? body,
SseHttpMethod? method,
EventSourceLogger? logger);

SSEClient _defaultClientFactory(Uri uri, HttpProperties httpProperties,
String? body, SseHttpMethod? method) {
String? body, SseHttpMethod? method, EventSourceLogger? logger) {
return SSEClient(uri, {'put', 'patch', 'delete'},
headers: httpProperties.baseHeaders,
body: body,
httpMethod: method ?? SseHttpMethod.get);
httpMethod: method ?? SseHttpMethod.get,
logger: logger);
}

final class StreamingDataSource implements DataSource {
Expand Down Expand Up @@ -109,7 +114,8 @@ final class StreamingDataSource implements DataSource {
_uri,
_httpProperties,
_useReport ? _contextString : null,
_useReport ? SseHttpMethod.report : SseHttpMethod.get);
_useReport ? SseHttpMethod.report : SseHttpMethod.get,
LDLoggerToEventSourceAdapter(_logger));

_subscription = _client!.stream.listen((event) async {
if (_stopped) {
Expand Down Expand Up @@ -147,3 +153,22 @@ final class StreamingDataSource implements DataSource {
_dataController.close();
}
}

/// Adapter to convert LDLogger to EventSourceLogger
class LDLoggerToEventSourceAdapter implements EventSourceLogger {
final LDLogger _logger;

LDLoggerToEventSourceAdapter(this._logger);

@override
void debug(String message) => _logger.debug(message);

@override
void info(String message) => _logger.info(message);

@override
void warn(String message) => _logger.warn(message);

@override
void error(String message) => _logger.error(message);
}
56 changes: 35 additions & 21 deletions packages/common_client/lib/src/ld_common_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ final class LDCommonClient {

late final DataSourceManager _dataSourceManager;
late final EnvironmentReport _envReport;
late final AsyncSingleQueue<void> _identifyQueue = AsyncSingleQueue();
late final AsyncSingleQueue<IdentifyResult> _identifyQueue =
AsyncSingleQueue();
late final DataSourceFactoriesFn _dataSourceFactories;

// Modifications will happen in the order they are specified in this list.
Expand Down Expand Up @@ -274,7 +275,7 @@ final class LDCommonClient {
// having been set resulting in a crash.
_identifyQueue.execute(() async {
await _startInternal();
await _identifyInternal(_initialUndecoratedContext,
return _identifyInternal(_initialUndecoratedContext,
waitForNetworkResults: waitForNetworkResults);
}).then((res) {
_startCompleter!.complete(_mapIdentifyResult(res));
Expand Down Expand Up @@ -308,7 +309,7 @@ final class LDCommonClient {
_envReport = await _makeEnvironmentReport();

// set up context modifiers, adding the auto env modifier if turned on
_modifiers = [AnonymousContextModifier(_persistence)];
_modifiers = [AnonymousContextModifier(_persistence, _logger)];
if (_config.autoEnvAttributes == AutoEnvAttributes.enabled) {
_modifiers.add(
AutoEnvContextModifier(_envReport, _persistence, _config.logger));
Expand Down Expand Up @@ -421,39 +422,52 @@ final class LDCommonClient {
return IdentifyError(Exception(message));
}
final res = await _identifyQueue.execute(() async {
await _identifyInternal(context,
return _identifyInternal(context,
waitForNetworkResults: waitForNetworkResults);
});
return _mapIdentifyResult(res);
}

Future<IdentifyResult> _mapIdentifyResult(TaskResult<void> res) async {
Future<IdentifyResult> _mapIdentifyResult(
TaskResult<IdentifyResult> res) async {
switch (res) {
case TaskComplete<void>():
return IdentifyComplete();
case TaskShed<void>():
case TaskComplete<IdentifyResult>(result: var result):
return result ?? IdentifyComplete();
case TaskShed<IdentifyResult>():
return IdentifySuperseded();
case TaskError<void>(error: var error):
case TaskError<IdentifyResult>(error: var error):
return IdentifyError(error);
}
}

Future<void> _identifyInternal(LDContext context,
Future<IdentifyResult> _identifyInternal(LDContext context,
{bool waitForNetworkResults = false}) async {
await _setAndDecorateContext(context);
final completer = Completer<void>();
_eventProcessor?.processIdentifyEvent(IdentifyEvent(context: _context));
final loadedFromCache = await _flagManager.loadCached(_context);

if (_config.offline) {
return;
if (!context.valid) {
const message =
'LDClient was provided an invalid context. The context will be ignored. Existing flags will be used for evaluations until identify is called with a valid context.';
_logger.warn(message);
return IdentifyError(Exception(message));
}
_dataSourceManager.identify(_context, completer);

if (loadedFromCache && !waitForNetworkResults) {
return;
try {
await _setAndDecorateContext(context);
final completer = Completer<void>();
_eventProcessor?.processIdentifyEvent(IdentifyEvent(context: _context));
final loadedFromCache = await _flagManager.loadCached(_context);

if (_config.offline) {
return IdentifyComplete();
}
_dataSourceManager.identify(_context, completer);

if (loadedFromCache && !waitForNetworkResults) {
return IdentifyComplete();
}
await completer.future;
return IdentifyComplete();
} catch (error) {
return IdentifyError(error);
}
return completer.future;
}

/// Returns the value of flag [flagKey] for the current context as a bool.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import 'package:launchdarkly_common_client/launchdarkly_common_client.dart';
import 'package:launchdarkly_common_client/src/context_modifiers/anonymous_context_modifier.dart';
import 'package:launchdarkly_common_client/src/persistence/persistence.dart';
import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart';
import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';

import '../mock_persistence.dart';

class MockAdapter extends Mock implements LDLogAdapter {}

void main() {
setUpAll(() {
registerFallbackValue(LDLogRecord(
level: LDLogLevel.debug,
message: '',
time: DateTime.now(),
logTag: ''));
});
group('without persistence', () {
test('it populates keys for anonymous contexts that lack them', () async {
final context = LDContextBuilder()
Expand All @@ -16,7 +27,8 @@ void main() {
.anonymous(true)
.build();

final decorator = AnonymousContextModifier(InMemoryPersistence());
final decorator =
AnonymousContextModifier(InMemoryPersistence(), LDLogger());
final decoratedContext = await decorator.decorate(context);

expect(decoratedContext.attributesByKind['user']!.key, isNotEmpty);
Expand All @@ -33,7 +45,8 @@ void main() {
.anonymous(true)
.build();

final decorator = AnonymousContextModifier(InMemoryPersistence());
final decorator =
AnonymousContextModifier(InMemoryPersistence(), LDLogger());
final decoratedContext = await decorator.decorate(context);

expect(decoratedContext.attributesByKind['user']!.key,
Expand All @@ -49,7 +62,8 @@ void main() {
.anonymous(true)
.build();

final decorator = AnonymousContextModifier(InMemoryPersistence());
final decorator =
AnonymousContextModifier(InMemoryPersistence(), LDLogger());
final decoratedContext = await decorator.decorate(context);
final decoratedContext2 = await decorator.decorate(context);

Expand All @@ -74,7 +88,7 @@ void main() {
encodePersistenceKey('user'): 'the-user-key',
encodePersistenceKey('company'): 'the-company-key',
};
final decorator = AnonymousContextModifier(mockPersistence);
final decorator = AnonymousContextModifier(mockPersistence, LDLogger());

final decoratedContext = await decorator.decorate(context);

Expand All @@ -92,7 +106,7 @@ void main() {
.build();

final mockPersistence = MockPersistence();
final decorator = AnonymousContextModifier(mockPersistence);
final decorator = AnonymousContextModifier(mockPersistence, LDLogger());

final decoratedContext = await decorator.decorate(context);

Expand All @@ -106,4 +120,25 @@ void main() {
encodePersistenceKey('company')]);
});
});

group('invalid context handling', () {
test('it logs a info log when asked to modify an invalid context',
() async {
final invalidContext =
LDContextBuilder().build(); // This creates an invalid context
final mockAdapter = MockAdapter();
final logger = LDLogger(adapter: mockAdapter, level: LDLogLevel.info);
final decorator = AnonymousContextModifier(InMemoryPersistence(), logger);

final result = await decorator.decorate(invalidContext);

expect(result.valid, false);

final logRecord = verify(() => mockAdapter.log(captureAny())).captured[0]
as LDLogRecord;
expect(logRecord.level, LDLogLevel.info);
expect(logRecord.message,
'AnonymousContextModifier was asked to modify an invalid context and will attempt to do so. This is expected if starting with an empty context.');
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@ import 'package:launchdarkly_common_client/src/context_modifiers/env_context_mod
import 'package:launchdarkly_common_client/src/persistence/persistence.dart';
import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart';
import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';

class MockAdapter extends Mock implements LDLogAdapter {}

void main() {
setUpAll(() {
registerFallbackValue(LDLogRecord(
level: LDLogLevel.debug,
message: '',
time: DateTime.now(),
logTag: ''));
});

group('env reporter with various configurations', () {
test('reporter has all attributes', () async {
final mockPersistence = InMemoryPersistence();
Expand Down Expand Up @@ -449,4 +460,66 @@ void main() {
expect(key1, key2);
});
});

group('invalid context handling', () {
test('it logs an info log when asked to modify an invalid context',
() async {
final invalidContext =
LDContextBuilder().build(); // This creates an invalid context
final mockPersistence = InMemoryPersistence();
final mockAdapter = MockAdapter();
final logger = LDLogger(adapter: mockAdapter, level: LDLogLevel.info);
final envReporter = ConcreteEnvReporter(
applicationInfo: Future.value(null),
osInfo: Future.value(null),
deviceInfo: Future.value(null),
locale: Future.value(null));

final report = await PrioritizedEnvReportBuilder()
.setConfigLayer(envReporter)
.build();

final decorator = AutoEnvContextModifier(report, mockPersistence, logger);

final result = await decorator.decorate(invalidContext);

expect(result.valid, false);

final logRecord = verify(() => mockAdapter.log(captureAny())).captured[0]
as LDLogRecord;
expect(logRecord.level, LDLogLevel.info);
expect(logRecord.message,
'AutoEnvContextModifier was asked to modify an invalid context and will attempt to do so. This is expected if starting with an empty context.');
});

test('it makes an invalid context valid by adding environment attributes',
() async {
final invalidContext = LDContextBuilder().build();
final mockPersistence = InMemoryPersistence();
final logger = LDLogger();
final envReporter = ConcreteEnvReporter(
applicationInfo: Future.value(ApplicationInfo(
applicationId: 'mockID',
applicationName: 'mockName',
applicationVersion: 'mockVersion',
applicationVersionName: 'mockVersionName')),
osInfo: Future.value(OsInfo(
family: 'mockFamily',
name: 'mockOsName',
version: 'mockOsVersion')),
deviceInfo: Future.value(
DeviceInfo(model: 'mockModel', manufacturer: 'mockManufacturer')),
locale: Future.value('mockLocale'));

final report = await PrioritizedEnvReportBuilder()
.setConfigLayer(envReporter)
.build();

final decorator = AutoEnvContextModifier(report, mockPersistence, logger);

final result = await decorator.decorate(invalidContext);

expect(result.valid, true);
});
});
}
Loading
Loading