From 6e1bfb49f69cabeaeb4a70f1553e1af526251f95 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:03:22 +0000 Subject: [PATCH 1/6] docs: clarify anonymous context key generation behavior Update documentation on LDAttributesBuilder.anonymous(), LDContextBuilder.kind(), LDClient.start(), and LDClient.identify() to explain that the SDK automatically generates and persists stable keys for anonymous contexts without keys. Co-Authored-By: rlamb@launchdarkly.com --- packages/common/lib/src/ld_context.dart | 23 +++++++++++++++++-- .../flutter_client_sdk/lib/src/ld_client.dart | 11 +++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/common/lib/src/ld_context.dart b/packages/common/lib/src/ld_context.dart index 9e0bcf0..fbd00e0 100644 --- a/packages/common/lib/src/ld_context.dart +++ b/packages/common/lib/src/ld_context.dart @@ -140,6 +140,20 @@ final class LDAttributesBuilder { /// It does not exclude it from analytics event data, so it is not the same as /// making attributes private; all non-private attributes will still be /// included in events and data export. + /// + /// When a context is anonymous and no key has been provided (i.e. the key + /// parameter was omitted from [LDContextBuilder.kind]), the SDK will + /// automatically generate a key for the context during [LDClient.start] or + /// [LDClient.identify]. The generated key is a UUID that is persisted + /// on the device, so it will remain stable across application restarts. + /// + /// Example of creating an anonymous context with a generated key: + /// ```dart + /// final context = LDContextBuilder() + /// .kind('user') + /// .anonymous(true) + /// .build(); + /// ``` LDAttributesBuilder anonymous(bool anonymous) { _anonymous = anonymous; return this; @@ -461,8 +475,13 @@ final class LDContextBuilder { /// non-empty. Calling this method again with the same kind returns the same /// [LDAttributesBuilder] as before. /// - /// If key is omitted, this will create an anonymous context with a generated key. - /// The generated key will be persisted and reused for future application runs. + /// If [key] is omitted and [LDAttributesBuilder.anonymous] is set to true, + /// the SDK will automatically generate a stable key for this context during + /// [LDClient.start] or [LDClient.identify]. The generated key will be + /// persisted on the device and reused for future application runs. + /// + /// If [key] is omitted and anonymous is not set to true, the context will + /// be invalid. LDAttributesBuilder kind(String kind, [String? key]) { LDAttributesBuilder builder = _buildersByKind.putIfAbsent( kind, () => LDAttributesBuilder._internal(this, kind)); diff --git a/packages/flutter_client_sdk/lib/src/ld_client.dart b/packages/flutter_client_sdk/lib/src/ld_client.dart index d5cc2f9..a272d6e 100644 --- a/packages/flutter_client_sdk/lib/src/ld_client.dart +++ b/packages/flutter_client_sdk/lib/src/ld_client.dart @@ -112,6 +112,11 @@ interface class LDClient { /// `runApp` is called, you must ensure the binding is initialized with /// `WidgetsFlutterBinding.ensureInitialized`. /// + /// During startup, if the initial context contains any anonymous contexts + /// without keys (i.e. [LDAttributesBuilder.anonymous] was set to true and + /// no key was provided to [LDContextBuilder.kind]), the SDK will + /// automatically generate and persist a stable key for each such context. + /// /// The [start] function can take an indeterminate amount of time to /// complete. For instance if the SDK is started while a device is in airplane /// mode, then it may not complete until some time in the future when the @@ -144,6 +149,12 @@ interface class LDClient { /// service containing the public [LDContext] fields for indexing on the /// dashboard. /// + /// If the provided context contains any anonymous contexts without keys + /// (i.e. [LDAttributesBuilder.anonymous] was set to true and no key was + /// provided to [LDContextBuilder.kind]), the SDK will automatically generate + /// and persist a stable key for each such context before processing the + /// identify. + /// /// A context with the same kinds and same keys will use the same cached /// context. /// From fd3bc9eb92ca8a4e7a062beaa9da3224bbf58477 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:12:56 +0000 Subject: [PATCH 2/6] feat: auto-set anonymous when key is omitted from context builder When a context kind is added without a key, the context is now automatically set to anonymous at build time. This means LDContextBuilder().kind('user').build() produces a valid anonymous context rather than an invalid one. The SDK will generate and persist a stable key for the context during start() or identify(). Updated documentation on anonymous(), kind(), start(), and identify() to explain this behavior. Co-Authored-By: rlamb@launchdarkly.com --- packages/common/lib/src/ld_context.dart | 28 +++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/common/lib/src/ld_context.dart b/packages/common/lib/src/ld_context.dart index fbd00e0..4cb8798 100644 --- a/packages/common/lib/src/ld_context.dart +++ b/packages/common/lib/src/ld_context.dart @@ -141,18 +141,26 @@ final class LDAttributesBuilder { /// making attributes private; all non-private attributes will still be /// included in events and data export. /// - /// When a context is anonymous and no key has been provided (i.e. the key - /// parameter was omitted from [LDContextBuilder.kind]), the SDK will + /// A context is also automatically set to anonymous at build time if no key + /// was provided to [LDContextBuilder.kind]. + /// + /// When a context is anonymous and no key has been provided, the SDK will /// automatically generate a key for the context during [LDClient.start] or /// [LDClient.identify]. The generated key is a UUID that is persisted /// on the device, so it will remain stable across application restarts. /// /// Example of creating an anonymous context with a generated key: /// ```dart + /// // Explicitly anonymous: /// final context = LDContextBuilder() /// .kind('user') /// .anonymous(true) /// .build(); + /// + /// // Also anonymous (automatically, because no key was provided): + /// final context2 = LDContextBuilder() + /// .kind('user') + /// .build(); /// ``` LDAttributesBuilder anonymous(bool anonymous) { _anonymous = anonymous; @@ -301,8 +309,9 @@ final class LDAttributesBuilder { LDContextAttributes? _build() { final key = _key ?? ''; if (key == '' && !_anonymous) { - // If the context is not anonymous, then the key cannot be empty. - return null; + // If no key was provided, the context is automatically anonymous. + // The SDK will generate a stable key during start() or identify(). + _anonymous = true; } if (_validKind(_kind)) { return LDContextAttributes._internal( @@ -475,13 +484,10 @@ final class LDContextBuilder { /// non-empty. Calling this method again with the same kind returns the same /// [LDAttributesBuilder] as before. /// - /// If [key] is omitted and [LDAttributesBuilder.anonymous] is set to true, - /// the SDK will automatically generate a stable key for this context during - /// [LDClient.start] or [LDClient.identify]. The generated key will be - /// persisted on the device and reused for future application runs. - /// - /// If [key] is omitted and anonymous is not set to true, the context will - /// be invalid. + /// If [key] is omitted, the context will automatically be set to anonymous + /// at build time, and the SDK will generate a stable key for this context + /// during [LDClient.start] or [LDClient.identify]. The generated key will + /// be persisted on the device and reused for future application runs. LDAttributesBuilder kind(String kind, [String? key]) { LDAttributesBuilder builder = _buildersByKind.putIfAbsent( kind, () => LDAttributesBuilder._internal(this, kind)); From 9016b96a3a6639fed7593ee99bb7b3139dbd1715 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:22:19 +0000 Subject: [PATCH 3/6] fix: track explicit anonymous setting to avoid mutating builder state Address review feedback: _build() no longer mutates _anonymous as a side effect. Instead, an _anonymousExplicitlySet flag tracks whether anonymous() was called. When no key is provided and anonymous was not explicitly set, the context is automatically anonymous. When anonymous(false) was explicitly called with no key, the context remains invalid. Suppress 4 contract tests that expect invalid contexts for inputs without keys, as these now produce valid anonymous contexts per the new auto-anonymous behavior. Co-Authored-By: rlamb@launchdarkly.com --- .../testharness-suppressions.txt | 4 ++++ packages/common/lib/src/ld_context.dart | 21 ++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/flutter_client_contract_test_service/testharness-suppressions.txt b/apps/flutter_client_contract_test_service/testharness-suppressions.txt index 5a19026..e08322d 100644 --- a/apps/flutter_client_contract_test_service/testharness-suppressions.txt +++ b/apps/flutter_client_contract_test_service/testharness-suppressions.txt @@ -1,3 +1,7 @@ context type/convert/invalid context/{"kind": null, "key": "x"} +context type/convert/invalid context/{"kind": "org", "key": null} +context type/convert/invalid context/{"kind": "org", "name": 3} +context type/convert/invalid context/{"kind": "org", "anonymous": null} +context type/convert/invalid context/{"kind": "org", "anonymous": "yes"} tags/disallowed characters autoEnvAttributes/ diff --git a/packages/common/lib/src/ld_context.dart b/packages/common/lib/src/ld_context.dart index 4cb8798..a9c236e 100644 --- a/packages/common/lib/src/ld_context.dart +++ b/packages/common/lib/src/ld_context.dart @@ -106,6 +106,7 @@ final class LDAttributesBuilder { String? _key; String? _name; bool _anonymous = false; + bool _anonymousExplicitlySet = false; final Set _privateAttributes = {}; // map for tracking attributes of the context @@ -164,6 +165,7 @@ final class LDAttributesBuilder { /// ``` LDAttributesBuilder anonymous(bool anonymous) { _anonymous = anonymous; + _anonymousExplicitlySet = true; return this; } @@ -308,10 +310,19 @@ final class LDAttributesBuilder { /// any subsequent actions on the [LDAttributesBuilder]. LDContextAttributes? _build() { final key = _key ?? ''; - if (key == '' && !_anonymous) { - // If no key was provided, the context is automatically anonymous. - // The SDK will generate a stable key during start() or identify(). - _anonymous = true; + // Determine the effective anonymous value. + // If no key was provided and anonymous was not explicitly set, + // the context is automatically anonymous. + // If anonymous was explicitly set to false, the context is invalid + // without a key. + final bool effectiveAnonymous; + if (key == '' && !_anonymous && !_anonymousExplicitlySet) { + effectiveAnonymous = true; + } else if (key == '' && !_anonymous) { + // Explicitly set anonymous(false) with no key = invalid context. + return null; + } else { + effectiveAnonymous = _anonymous; } if (_validKind(_kind)) { return LDContextAttributes._internal( @@ -319,7 +330,7 @@ final class LDAttributesBuilder { Map.unmodifiable(_attributes), _kind, _key ?? '', - _anonymous, + effectiveAnonymous, _privateAttributes, _name); } From 061529264bd964b6e32dcd99d0b54ecbcdd386cf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:27:29 +0000 Subject: [PATCH 4/6] fix: set anonymous(false) in contract test JSON conversion Instead of suppressing contract tests, explicitly set anonymous(false) in _flattenedListToContext when the input doesn't have anonymous=true. This ensures the contract tests pass as before (keyless contexts without explicit anonymous=true remain invalid during JSON conversion), while the builder API auto-anonymous behavior is preserved for users who simply call kind() without a key. Co-Authored-By: rlamb@launchdarkly.com --- .../bin/contract_test_service.dart | 7 +++++++ .../testharness-suppressions.txt | 4 ---- 2 files changed, 7 insertions(+), 4 deletions(-) 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 0c2c3e0..2d7e233 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 @@ -422,6 +422,13 @@ class TestApiImpl extends SdkTestApi { attrsBuilder.setValue( a.key, common.LDValueSerialization.fromJson(a.value)); } + // When converting from JSON, explicitly set anonymous(false) if it + // wasn't set to true. This prevents the builder from auto-setting + // anonymous for keyless contexts during JSON deserialization, reserving + // that behavior for the builder API. + if (attributes['anonymous'] != true) { + attrsBuilder.anonymous(false); + } final Map meta = attributes['_meta'] ?? {}; final List privateAttrs = ((meta['privateAttributes'] ?? []) as List) diff --git a/apps/flutter_client_contract_test_service/testharness-suppressions.txt b/apps/flutter_client_contract_test_service/testharness-suppressions.txt index e08322d..5a19026 100644 --- a/apps/flutter_client_contract_test_service/testharness-suppressions.txt +++ b/apps/flutter_client_contract_test_service/testharness-suppressions.txt @@ -1,7 +1,3 @@ context type/convert/invalid context/{"kind": null, "key": "x"} -context type/convert/invalid context/{"kind": "org", "key": null} -context type/convert/invalid context/{"kind": "org", "name": 3} -context type/convert/invalid context/{"kind": "org", "anonymous": null} -context type/convert/invalid context/{"kind": "org", "anonymous": "yes"} tags/disallowed characters autoEnvAttributes/ From 3635f6c947249db87f6c70ceb63edc369680ee12 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:46:25 +0000 Subject: [PATCH 5/6] refactor: address review feedback from tanderson-ld - Refactor _anonymous from bool + _anonymousExplicitlySet to bool? (null = never set, true = anonymous, false = not anonymous). This simplifies the _build() logic per reviewer suggestion. - Reorder examples: implicit anonymous first, explicit second - Improve example comments per reviewer suggestions - Add persistence caveat to doc comment - Fix _build() comment to accurately describe all three states Co-Authored-By: rlamb@launchdarkly.com --- packages/common/lib/src/ld_context.dart | 31 ++++++++++++------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/common/lib/src/ld_context.dart b/packages/common/lib/src/ld_context.dart index a9c236e..07a936c 100644 --- a/packages/common/lib/src/ld_context.dart +++ b/packages/common/lib/src/ld_context.dart @@ -105,8 +105,7 @@ final class LDAttributesBuilder { final LDContextBuilder _parent; String? _key; String? _name; - bool _anonymous = false; - bool _anonymousExplicitlySet = false; + bool? _anonymous; final Set _privateAttributes = {}; // map for tracking attributes of the context @@ -149,23 +148,24 @@ final class LDAttributesBuilder { /// automatically generate a key for the context during [LDClient.start] or /// [LDClient.identify]. The generated key is a UUID that is persisted /// on the device, so it will remain stable across application restarts. + /// If persistence is not available, the generated key will not be stable + /// across application restarts. /// - /// Example of creating an anonymous context with a generated key: + /// Examples of creating an anonymous context: /// ```dart - /// // Explicitly anonymous: + /// // Implicitly anonymous and will get a generated key (no key was provided): /// final context = LDContextBuilder() /// .kind('user') - /// .anonymous(true) /// .build(); /// - /// // Also anonymous (automatically, because no key was provided): + /// // Explicitly anonymous and will get a generated key (no key was provided): /// final context2 = LDContextBuilder() /// .kind('user') + /// .anonymous(true) /// .build(); /// ``` LDAttributesBuilder anonymous(bool anonymous) { _anonymous = anonymous; - _anonymousExplicitlySet = true; return this; } @@ -310,19 +310,18 @@ final class LDAttributesBuilder { /// any subsequent actions on the [LDAttributesBuilder]. LDContextAttributes? _build() { final key = _key ?? ''; - // Determine the effective anonymous value. - // If no key was provided and anonymous was not explicitly set, - // the context is automatically anonymous. - // If anonymous was explicitly set to false, the context is invalid - // without a key. + // Determine the effective anonymous value: + // - _anonymous == null (never set): auto-anonymous if no key, else false + // - _anonymous == true: anonymous + // - _anonymous == false (explicitly set): not anonymous, requires a key final bool effectiveAnonymous; - if (key == '' && !_anonymous && !_anonymousExplicitlySet) { + if (_anonymous == null && key == '') { effectiveAnonymous = true; - } else if (key == '' && !_anonymous) { - // Explicitly set anonymous(false) with no key = invalid context. + } else if ((_anonymous ?? false) == false && key == '') { + // Not anonymous (explicitly set to false) with no key = invalid context. return null; } else { - effectiveAnonymous = _anonymous; + effectiveAnonymous = _anonymous ?? false; } if (_validKind(_kind)) { return LDContextAttributes._internal( From 2e1d62ed2a76fe359d8932722af0df5db4e668ce Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:53:15 +0000 Subject: [PATCH 6/6] revert: remove behavioral changes, keep docs-only Reverts auto-anonymous logic, bool? refactor, and contract test service changes. Keeps only documentation updates explaining how to get generated keys by setting anonymous(true). Adds persistence caveat per reviewer feedback. Co-Authored-By: rlamb@launchdarkly.com --- .../bin/contract_test_service.dart | 7 ---- packages/common/lib/src/ld_context.dart | 42 +++++++------------ 2 files changed, 14 insertions(+), 35 deletions(-) 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 2d7e233..0c2c3e0 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 @@ -422,13 +422,6 @@ class TestApiImpl extends SdkTestApi { attrsBuilder.setValue( a.key, common.LDValueSerialization.fromJson(a.value)); } - // When converting from JSON, explicitly set anonymous(false) if it - // wasn't set to true. This prevents the builder from auto-setting - // anonymous for keyless contexts during JSON deserialization, reserving - // that behavior for the builder API. - if (attributes['anonymous'] != true) { - attrsBuilder.anonymous(false); - } final Map meta = attributes['_meta'] ?? {}; final List privateAttrs = ((meta['privateAttributes'] ?? []) as List) diff --git a/packages/common/lib/src/ld_context.dart b/packages/common/lib/src/ld_context.dart index 07a936c..f9fe912 100644 --- a/packages/common/lib/src/ld_context.dart +++ b/packages/common/lib/src/ld_context.dart @@ -105,7 +105,7 @@ final class LDAttributesBuilder { final LDContextBuilder _parent; String? _key; String? _name; - bool? _anonymous; + bool _anonymous = false; final Set _privateAttributes = {}; // map for tracking attributes of the context @@ -141,26 +141,18 @@ final class LDAttributesBuilder { /// making attributes private; all non-private attributes will still be /// included in events and data export. /// - /// A context is also automatically set to anonymous at build time if no key - /// was provided to [LDContextBuilder.kind]. - /// - /// When a context is anonymous and no key has been provided, the SDK will + /// When a context is anonymous and no key has been provided (i.e. the key + /// parameter was omitted from [LDContextBuilder.kind]), the SDK will /// automatically generate a key for the context during [LDClient.start] or /// [LDClient.identify]. The generated key is a UUID that is persisted /// on the device, so it will remain stable across application restarts. /// If persistence is not available, the generated key will not be stable /// across application restarts. /// - /// Examples of creating an anonymous context: + /// Example of creating an anonymous context with a generated key: /// ```dart - /// // Implicitly anonymous and will get a generated key (no key was provided): /// final context = LDContextBuilder() /// .kind('user') - /// .build(); - /// - /// // Explicitly anonymous and will get a generated key (no key was provided): - /// final context2 = LDContextBuilder() - /// .kind('user') /// .anonymous(true) /// .build(); /// ``` @@ -310,18 +302,9 @@ final class LDAttributesBuilder { /// any subsequent actions on the [LDAttributesBuilder]. LDContextAttributes? _build() { final key = _key ?? ''; - // Determine the effective anonymous value: - // - _anonymous == null (never set): auto-anonymous if no key, else false - // - _anonymous == true: anonymous - // - _anonymous == false (explicitly set): not anonymous, requires a key - final bool effectiveAnonymous; - if (_anonymous == null && key == '') { - effectiveAnonymous = true; - } else if ((_anonymous ?? false) == false && key == '') { - // Not anonymous (explicitly set to false) with no key = invalid context. + if (key == '' && !_anonymous) { + // If the context is not anonymous, then the key cannot be empty. return null; - } else { - effectiveAnonymous = _anonymous ?? false; } if (_validKind(_kind)) { return LDContextAttributes._internal( @@ -329,7 +312,7 @@ final class LDAttributesBuilder { Map.unmodifiable(_attributes), _kind, _key ?? '', - effectiveAnonymous, + _anonymous, _privateAttributes, _name); } @@ -494,10 +477,13 @@ final class LDContextBuilder { /// non-empty. Calling this method again with the same kind returns the same /// [LDAttributesBuilder] as before. /// - /// If [key] is omitted, the context will automatically be set to anonymous - /// at build time, and the SDK will generate a stable key for this context - /// during [LDClient.start] or [LDClient.identify]. The generated key will - /// be persisted on the device and reused for future application runs. + /// If [key] is omitted and [LDAttributesBuilder.anonymous] is set to true, + /// the SDK will automatically generate a stable key for this context during + /// [LDClient.start] or [LDClient.identify]. The generated key will be + /// persisted on the device and reused for future application runs. + /// + /// If [key] is omitted and anonymous is not set to true, the context will + /// be invalid. LDAttributesBuilder kind(String kind, [String? key]) { LDAttributesBuilder builder = _buildersByKind.putIfAbsent( kind, () => LDAttributesBuilder._internal(this, kind));