Skip to content

Commit 5bd9ce7

Browse files
authored
feat: Add experimental plugin support. (#225)
The flutter SDK is arranged into a common-client package that is independent of the flutter package to allow for eventually supporting non-flutter usages such as dart command line applications. This means, similar to JS, that plugins are a concern of the leaf-node of the SDK because plugins depend on types that are not common to the leaf nodes. A plugin for the flutter client SDK is registered with an instance of the flutter client SDK and not an instance of the common client SDK. (In flutter the common client is actually composed into the leaf-node and not a base class, so this difference is even more meaningful.) The work-around to this is to make common types for all the meta-data, and then a generic base class to be extended by the leaf-node implementations. That extended version makes the client concrete instead of generic. The base operations that need to be done on plugins, such as getting a list of hooks from them, and registering them, are handled via generic methods in the common client package. ```mermaid graph TD subgraph "common_client package" A[PluginBase&lt;TClient&gt;] end subgraph "flutter_client_sdk package" B[Plugin] B --> |extends| A end subgraph "User Application" C[MyCustomPlugin] C --> |extends| B end A --> |Provides| D[register method<br/>hooks property<br/>abstract metadata property] B --> |Specializes for| E[LDClient type<br/>Flutter SDK context] C --> |Implements| F[Custom behavior via override of register<br/>Custom hooks via override of hooks property<br/>Concrete metadata property] ```
1 parent 6e7a26d commit 5bd9ce7

File tree

15 files changed

+1526
-14
lines changed

15 files changed

+1526
-14
lines changed

packages/common_client/lib/launchdarkly_common_client.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,17 @@ export 'src/hooks/hook.dart'
5454
EvaluationSeriesContext,
5555
IdentifySeriesContext,
5656
TrackSeriesContext;
57+
58+
export 'src/hooks/operations.dart' show combineHooks;
59+
60+
export 'src/plugins/plugin.dart'
61+
show
62+
PluginBase,
63+
PluginCredentialInfo,
64+
PluginEnvironmentMetadata,
65+
PluginMetadata,
66+
PluginSdkMetadata;
67+
68+
export 'src/plugins/operations.dart' show safeGetHooks, safeRegisterPlugins;
69+
70+
export 'src/config/defaults/credential_type.dart' show CredentialType;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
/// Represents the credential type used by the SDK. The credential type is
2+
/// determined by the platform the SDK is running on.
13
enum CredentialType {
4+
/// The SDK is using a mobile key credential.
25
mobileKey,
6+
7+
/// The SDK is using a client-side ID.
38
clientSideId,
49
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'hook.dart';
2+
3+
List<Hook>? combineHooks(List<Hook>? baseHooks, List<Hook>? extendedHooks) {
4+
if (baseHooks == null) {
5+
return extendedHooks;
6+
}
7+
if (extendedHooks == null) {
8+
return baseHooks;
9+
}
10+
List<Hook> combined = [];
11+
combined.addAll(baseHooks);
12+
combined.addAll(extendedHooks);
13+
return combined;
14+
}

packages/common_client/lib/src/ld_common_client.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,4 +850,8 @@ final class LDCommonClient {
850850

851851
/// Check if the initialization process is complete.
852852
bool get initialized => _startCompleter?.isCompleted ?? false;
853+
854+
/// Get the type of credential the SDK uses.
855+
CredentialType get credentialType =>
856+
DefaultConfig.credentialConfig.credentialType;
853857
}

packages/common_client/lib/src/ld_common_config.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:collection';
12
import 'dart:math';
23

34
import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart';
@@ -129,7 +130,7 @@ abstract class LDCommonConfig {
129130
final List<String> globalPrivateAttributes;
130131

131132
/// An initial list of hooks.
132-
final List<Hook>? hooks;
133+
final UnmodifiableListView<Hook>? hooks;
133134

134135
LDCommonConfig(this.sdkCredential, this.autoEnvAttributes,
135136
{this.applicationInfo,
@@ -142,7 +143,7 @@ abstract class LDCommonConfig {
142143
DataSourceConfig? dataSourceConfig,
143144
bool? allAttributesPrivate,
144145
List<String>? globalPrivateAttributes,
145-
this.hooks})
146+
List<Hook>? hooks})
146147
: httpProperties = httpProperties ?? HttpProperties(),
147148
serviceEndpoints =
148149
serviceEndpoints ?? client_endpoints.ServiceEndpoints(),
@@ -153,7 +154,8 @@ abstract class LDCommonConfig {
153154
dataSourceConfig = dataSourceConfig ?? DataSourceConfig(),
154155
allAttributesPrivate =
155156
allAttributesPrivate ?? DefaultConfig.allAttributesPrivate,
156-
globalPrivateAttributes = globalPrivateAttributes ?? [];
157+
globalPrivateAttributes = globalPrivateAttributes ?? [],
158+
hooks = hooks != null ? UnmodifiableListView(List.from(hooks)) : null;
157159
}
158160

159161
/// Enable / disable options for Auto Environment Attributes functionality. When enabled, the SDK will automatically
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart'
2+
show LDLogger;
3+
4+
import 'plugin.dart';
5+
import '../hooks/hook.dart';
6+
7+
const _unknownPlugin = 'unknown';
8+
9+
String safeGetPluginName<TClient>(PluginBase<TClient> plugin, LDLogger logger) {
10+
try {
11+
return plugin.metadata.name;
12+
} catch (err) {
13+
logger.warn('Exception thrown access the name of a registered plugin.');
14+
return _unknownPlugin;
15+
}
16+
}
17+
18+
List<Hook>? safeGetHooks<TClient>(
19+
List<PluginBase<TClient>>? plugins, LDLogger logger) {
20+
if (plugins == null) return null;
21+
22+
return plugins
23+
.map<List<Hook>>((plugin) {
24+
try {
25+
return plugin.hooks;
26+
} catch (err) {
27+
logger.warn(
28+
'Exception thrown getting hooks for plugin ${safeGetPluginName(plugin, logger)}. Unable to get hooks for plugin.');
29+
}
30+
return [];
31+
})
32+
.expand<Hook>((hooks) => hooks)
33+
.toList();
34+
}
35+
36+
void safeRegisterPlugins<TClient>(
37+
TClient client,
38+
PluginEnvironmentMetadata metadata,
39+
List<PluginBase<TClient>>? plugins,
40+
LDLogger logger) {
41+
plugins?.forEach((plugin) {
42+
try {
43+
plugin.register(client, metadata);
44+
} catch (err) {
45+
logger.warn(
46+
'Exception thrown when registering plugin ${safeGetPluginName(plugin, logger)}');
47+
}
48+
});
49+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart'
2+
show ApplicationInfo;
3+
4+
import '../config/defaults/credential_type.dart';
5+
import '../hooks/hook.dart' show Hook;
6+
7+
/// Metadata about a plugin implementation.
8+
///
9+
/// May be used in logs and analytics to identify the plugin.
10+
final class PluginMetadata {
11+
/// The name of the plugin.
12+
final String name;
13+
14+
const PluginMetadata({required this.name});
15+
16+
@override
17+
String toString() {
18+
return 'PluginMetadata{name: $name}';
19+
}
20+
}
21+
22+
/// Metadata about the SDK that is running the plugin.
23+
final class PluginSdkMetadata {
24+
/// The name of the SDK.
25+
final String name;
26+
27+
/// The version of the SDK.
28+
final String version;
29+
30+
/// If this is a wrapper SDK, then this is the name of the wrapper.
31+
final String? wrapperName;
32+
33+
/// If this is a wrapper SDK, then this is the version of the wrapper.
34+
final String? wrapperVersion;
35+
36+
const PluginSdkMetadata(
37+
{required this.name,
38+
required this.version,
39+
this.wrapperName,
40+
this.wrapperVersion});
41+
42+
@override
43+
String toString() {
44+
return 'PluginSdkMetadata{name: $name, version: $version,'
45+
' wrapperName: $wrapperName, wrapperVersion: $wrapperVersion}';
46+
}
47+
}
48+
49+
/// Information about the credential used to initialize the SDK.
50+
final class PluginCredentialInfo {
51+
/// The type of credential.
52+
final CredentialType type;
53+
54+
/// The value of the credential.
55+
final String value;
56+
57+
const PluginCredentialInfo({required this.type, required this.value});
58+
59+
@override
60+
String toString() {
61+
return 'PluginCredentialInfo{type: $type, value: $value}';
62+
}
63+
}
64+
65+
/// Metadata about the environment where the plugin is running.
66+
final class PluginEnvironmentMetadata {
67+
/// Metadata about the SDK that is running the plugin.
68+
final PluginSdkMetadata sdk;
69+
70+
/// Metadata about the application where the LaunchDarkly SDK is running.
71+
///
72+
/// Plugins only have access to application info collected during
73+
/// configuration. Application information collected by environment reporting
74+
/// is not available.
75+
///
76+
/// If access to the environment reporting information is required, then it
77+
/// is available via the [LDContext] by using hooks.
78+
///
79+
/// Only present if any application information is available.
80+
final ApplicationInfo? application;
81+
82+
/// Information about the credential used to initialize the SDK.
83+
final PluginCredentialInfo credential;
84+
85+
PluginEnvironmentMetadata(
86+
{required this.sdk, required this.credential, this.application});
87+
88+
@override
89+
String toString() {
90+
return 'PluginEnvironmentMetadata{sdk: $sdk, credential: $credential,'
91+
' application: $application}';
92+
}
93+
}
94+
95+
/// Base class from which all plugins must derive.
96+
///
97+
/// Implementation note: SDK packages must export a specialized version of this
98+
/// for their specific TClient type. This class cannot provide a type, because
99+
/// it would limit the API to methods available in the base client.
100+
abstract base class PluginBase<TClient> {
101+
/// Metadata associated with this plugin.
102+
///
103+
/// Plugin implementations must implement this property.
104+
/// ```dart
105+
/// final _metadata = PluginMetadata(name: 'MyPluginName');
106+
///
107+
/// @override
108+
/// PluginMetadata get metadata => _metadata;
109+
/// ```
110+
PluginMetadata get metadata;
111+
112+
/// Registers the plugin with the SDK. Called once during SDK initialization.
113+
///
114+
/// The SDK initialization will typically not have been completed at this
115+
/// point, so the plugin should take appropriate actions to ensure the SDK is
116+
/// ready before sending track events or evaluating flags. For example the
117+
/// plugin could wait for the [Hook.afterIdentify] stage to indicate success
118+
/// before tracking any events.
119+
///
120+
/// The [client] the plugin is registered with.
121+
void register(
122+
TClient client, PluginEnvironmentMetadata environmentMetadata) {}
123+
124+
/// Hooks which are bundled with this plugin.
125+
///
126+
/// Implementations should override this method to return their bundled
127+
/// hooks.
128+
/// ```dart
129+
/// @override
130+
/// List<Hook> get hooks => [MyBundledHook()];
131+
/// ```
132+
List<Hook> get hooks => [];
133+
134+
PluginBase();
135+
}

0 commit comments

Comments
 (0)