From a03268c80fb92b3e3917a238a64aa731ae7db0ef Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Tue, 18 May 2021 06:29:24 -0700 Subject: [PATCH 1/6] First version from Hackweek 2020 mostly done by yasir --- labs/optimizely-agent-client-dart/.gitignore | 4 + labs/optimizely-agent-client-dart/README.md | 100 +++++++++++++ .../example/example.dart | 43 ++++++ .../lib/optimizely_agent.dart | 109 ++++++++++++++ .../lib/src/models/activate_response.dart | 54 +++++++ .../lib/src/models/decision_types.dart | 17 +++ .../optimizely_config/optimizely_config.dart | 58 ++++++++ .../optimizely_experiment.dart | 52 +++++++ .../optimizely_config/optimizely_feature.dart | 61 ++++++++ .../optimizely_variable.dart | 46 ++++++ .../optimizely_variation.dart | 56 ++++++++ .../lib/src/models/override_response.dart | 50 +++++++ .../lib/src/network/http_manager.dart | 40 ++++++ .../lib/src/request_manager.dart | 135 ++++++++++++++++++ .../optimizely-agent-client-dart/pubspec.yaml | 14 ++ 15 files changed, 839 insertions(+) create mode 100644 labs/optimizely-agent-client-dart/.gitignore create mode 100644 labs/optimizely-agent-client-dart/README.md create mode 100644 labs/optimizely-agent-client-dart/example/example.dart create mode 100644 labs/optimizely-agent-client-dart/lib/optimizely_agent.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/activate_response.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/decision_types.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_config.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_experiment.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_feature.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_variable.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_variation.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/override_response.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/network/http_manager.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/request_manager.dart create mode 100644 labs/optimizely-agent-client-dart/pubspec.yaml diff --git a/labs/optimizely-agent-client-dart/.gitignore b/labs/optimizely-agent-client-dart/.gitignore new file mode 100644 index 0000000..b0ef3b0 --- /dev/null +++ b/labs/optimizely-agent-client-dart/.gitignore @@ -0,0 +1,4 @@ +.packages +pubspec.lock +.dart_tool/* +launch.json \ No newline at end of file diff --git a/labs/optimizely-agent-client-dart/README.md b/labs/optimizely-agent-client-dart/README.md new file mode 100644 index 0000000..f1987ec --- /dev/null +++ b/labs/optimizely-agent-client-dart/README.md @@ -0,0 +1,100 @@ +# Dart Client for Optimizely Agent +This is a dart client to facilitate communication with Optimizely Agent. + +## Initialization +``` +OptimizelyAgent(String sdkKey, String url) +``` +The client can be initialized buy providing `sdkKey` and url where `agent` is deployed. + +#### Example +``` +OptimizelyAgent agent = new OptimizelyAgent('{sdkKey}', 'http://localhost:8080'); +``` + +## Activate +``` +activate({ + @required String userId, + Map userAttributes, + List featureKey, + List experimentKey, + bool disableTracking, + DecisionType type, + bool enabled +}) → Future> +``` + +Activate takes `userId` as a required argument and a combination of optional arguments and returns a list of decisions represented by `OptimizelyDecision`. + +#### Example +``` +List optimizelyDecisions = await agent.activate(userId: 'user1', type: DecisionType.experiment, enabled: true); +if (optimizelyDecisions != null) { + print('Total Decisions ${optimizelyDecisions.length}'); + optimizelyDecisions.forEach((OptimizelyDecision decision) { + print(decision.variationKey); + }); +} +``` + +## Track +``` +track({ + @required String eventKey, + String userId, + Map eventTags, + Map userAttributes +}) → Future +``` + +Track takes `eventKey` as a required argument and a combination of optional arguments and returns nothing. + +#### Example +``` +await agent.track(eventKey: 'button1_click', userId: 'user1'); +``` + +## Optimizely Config +``` +getOptimizelyConfig() → Future +``` + +Returns `OptimizelyConfig` object which contains revision, a map of experiments and a map of features. + +#### Example +``` +OptimizelyConfig config = await agent.getOptimizelyConfig(); +if (config != null) { + print('Revision ${config.revision}'); + config.experimentsMap.forEach((String key, OptimizelyExperiment experiment) { + print('Experiment Key: $key'); + print('Experiment Id: ${experiment.id}'); + experiment.variationsMap.forEach((String key, OptimizelyVariation variation) { + print(' Variation Key: $key'); + print(' Variation Id: ${variation.id}'); + }); + }); +} +``` + +## Override Decision +``` +overrideDecision({ + @required String userId, + @required String experimentKey, + @required String variationKey +}) → Future +``` + +overrideDecision requires all the parameters and returns on `OverrideResponse` object which contains previous variation, new variation, messages and some more information. + +#### Example +``` +OverrideResponse overrideResponse = await agent.overrideDecision(userId: 'user1', experimentKey: 'playground-test', variationKey: 'variation_5'); +if (overrideResponse != null) { + print('Previous Variation: ${overrideResponse.prevVariationKey}'); + print('New Variation: ${overrideResponse.variationKey}'); + overrideResponse.messages.forEach((String message) => print('Message: $message')); +} +``` \ No newline at end of file diff --git a/labs/optimizely-agent-client-dart/example/example.dart b/labs/optimizely-agent-client-dart/example/example.dart new file mode 100644 index 0000000..ba97dc6 --- /dev/null +++ b/labs/optimizely-agent-client-dart/example/example.dart @@ -0,0 +1,43 @@ +import 'package:optimizely_agent_client/optimizely_agent.dart'; + +void main() async { + OptimizelyAgent agent = new OptimizelyAgent('{SDK_KEY}', '{AGENT_URL}'); + + print('---- Calling OptimizelyConfig API ----'); + OptimizelyConfig config = await agent.getOptimizelyConfig(); + if (config != null) { + print('Revision ${config.revision}'); + config.experimentsMap.forEach((String key, OptimizelyExperiment experiment) { + print('Experiment Key: $key'); + print('Experiment Id: ${experiment.id}'); + experiment.variationsMap.forEach((String key, OptimizelyVariation variation) { + print(' Variation Key: $key'); + print(' Variation Id: ${variation.id}'); + }); + }); + } + print(''); + + print('---- Calling Activate API ----'); + List optimizelyDecisions = await agent.activate(userId: 'user1', type: DecisionType.experiment, enabled: true); + if (optimizelyDecisions != null) { + print('Total Decisions ${optimizelyDecisions.length}'); + optimizelyDecisions.forEach((OptimizelyDecision decision) { + print(decision.toJson()); + }); + } + print(''); + + print('---- Calling Track API ----'); + await agent.track(eventKey: 'button1_click', userId: 'user1'); + print(''); + + print('---- Calling Override API ----'); + OverrideResponse overrideResponse = await agent.overrideDecision(userId: 'user1', experimentKey: 'playground-test', variationKey: 'variation_5'); + if (overrideResponse != null) { + print('Previous Variation: ${overrideResponse.prevVariationKey}'); + print('New Variation: ${overrideResponse.variationKey}'); + overrideResponse.messages.forEach((String message) => print('Message: $message')); + } + print(''); +} diff --git a/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart b/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart new file mode 100644 index 0000000..fb26b3b --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart @@ -0,0 +1,109 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +import 'package:meta/meta.dart'; +import 'package:dio/dio.dart'; + +import './src/models/activate_response.dart'; +import './src/models/decision_types.dart'; + +import './src/models/optimizely_config/optimizely_config.dart'; +import './src/models/override_response.dart'; +import './src/request_manager.dart'; + +export './src/models/decision_types.dart'; +export './src/models/activate_response.dart'; +export './src/models/override_response.dart'; + +// Exporting all OptimizelyConfig entities +export './src/models/optimizely_config/optimizely_config.dart'; +export './src/models/optimizely_config/optimizely_experiment.dart'; +export './src/models/optimizely_config/optimizely_feature.dart'; +export './src/models/optimizely_config/optimizely_variable.dart'; +export './src/models/optimizely_config/optimizely_variation.dart'; + +class OptimizelyAgent { + RequestManager _requestmanager; + + OptimizelyAgent(String sdkKey, String url) { + _requestmanager = RequestManager(sdkKey, url); + } + + /// Returns status code and OptimizelyConfig object + Future getOptimizelyConfig() async { + Response resp = await _requestmanager.getOptimizelyConfig(); + return resp.statusCode == 200 ? OptimizelyConfig.fromJson(resp.data) : null; + } + + /// Tracks an event and returns nothing. + Future track({ + @required String eventKey, + String userId, + Map eventTags, + Map userAttributes + }) { + return _requestmanager.track( + eventKey: eventKey, + userId: userId, + eventTags: eventTags, + userAttributes: userAttributes + ); + } + + /// Overrides a decision for the user and returns OverrideResponse object. + Future overrideDecision({ + @required String userId, + @required String experimentKey, + @required String variationKey + }) async { + Response resp = await _requestmanager.overrideDecision( + userId: userId, + experimentKey: experimentKey, + variationKey: variationKey + ); + return resp.statusCode == 200 ? OverrideResponse.fromJson(resp.data) : null; + } + + /// Activate makes feature and experiment decisions for the selected query parameters + /// and returns list of OptimizelyDecision + Future> activate({ + @required String userId, + Map userAttributes, + List featureKey, + List experimentKey, + bool disableTracking, + DecisionType type, + bool enabled + }) async { + Response resp = await _requestmanager.activate( + userId: userId, + userAttributes: userAttributes, + featureKey: featureKey, + experimentKey: experimentKey, + disableTracking: disableTracking, + type: type, + enabled: enabled + ); + if (resp.statusCode == 200) { + List optimizelyDecisions = []; + resp.data.forEach((element) { + optimizelyDecisions.add(OptimizelyDecision.fromJson(element)); + }); + return optimizelyDecisions; + } + return null; + } +} diff --git a/labs/optimizely-agent-client-dart/lib/src/models/activate_response.dart b/labs/optimizely-agent-client-dart/lib/src/models/activate_response.dart new file mode 100644 index 0000000..2c919b7 --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/activate_response.dart @@ -0,0 +1,54 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +class OptimizelyDecision { + OptimizelyDecision(this.userId, this.experimentKey, this.error); + + String userId; + String experimentKey; + String featureKey; + String variationKey; + String type; + Map variables; + bool enabled; + String error; + + factory OptimizelyDecision.fromJson(Map json) { + return OptimizelyDecision( + json['userId'] as String, + json['experimentKey'] as String, + json['error'] as String ?? '', + ) + ..featureKey = json['featureKey'] as String + ..variationKey = json['variationKey'] as String + ..type = json['type'] as String + ..variables = json['variables'] as Map ?? {} + ..enabled = json['enabled'] as bool; + } + + Map toJson() { + return { + 'userId': this.userId, + 'experimentKey': this.experimentKey, + 'featureKey': this.featureKey, + 'variationKey': this.variationKey, + 'type': this.type, + 'variables': this.variables, + 'enabled': this.enabled, + 'error': this.error, + }; + } +} diff --git a/labs/optimizely-agent-client-dart/lib/src/models/decision_types.dart b/labs/optimizely-agent-client-dart/lib/src/models/decision_types.dart new file mode 100644 index 0000000..c54fb94 --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/decision_types.dart @@ -0,0 +1,17 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +enum DecisionType { feature, experiment } diff --git a/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_config.dart b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_config.dart new file mode 100644 index 0000000..76b5883 --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_config.dart @@ -0,0 +1,58 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +import './optimizely_experiment.dart'; +import './optimizely_feature.dart'; + +class OptimizelyConfig { + String revision; + Map experimentsMap; + Map featuresMap; + + OptimizelyConfig(this.revision, this.experimentsMap, this.featuresMap); + + factory OptimizelyConfig.fromJson(Map json) => + _$OptimizelyConfigFromJson(json); + + Map toJson() => _$OptimizelyConfigToJson(this); +} + +OptimizelyConfig _$OptimizelyConfigFromJson(Map json) { + return OptimizelyConfig( + json['revision'] as String, + (json['experimentsMap'] as Map)?.map( + (k, e) => MapEntry( + k, + e == null + ? null + : OptimizelyExperiment.fromJson(e as Map)), + ), + (json['featuresMap'] as Map)?.map( + (k, e) => MapEntry( + k, + e == null + ? null + : OptimizelyFeature.fromJson(e as Map)), + ), + ); +} + +Map _$OptimizelyConfigToJson(OptimizelyConfig instance) => + { + 'revision': instance.revision, + 'experimentsMap': instance.experimentsMap, + 'featuresMap': instance.featuresMap, + }; diff --git a/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_experiment.dart b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_experiment.dart new file mode 100644 index 0000000..7499390 --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_experiment.dart @@ -0,0 +1,52 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +import './optimizely_variation.dart'; + +class OptimizelyExperiment { + String id; + String key; + Map variationsMap; + + OptimizelyExperiment(this.id, this.key, this.variationsMap); + + factory OptimizelyExperiment.fromJson(Map json) => + _$OptimizelyExperimentFromJson(json); + + Map toJson() => _$OptimizelyExperimentToJson(this); +} + +OptimizelyExperiment _$OptimizelyExperimentFromJson(Map json) { + return OptimizelyExperiment( + json['id'] as String, + json['key'] as String, + (json['variationsMap'] as Map)?.map( + (k, e) => MapEntry( + k, + e == null + ? null + : OptimizelyVariation.fromJson(e as Map)), + ), + ); +} + +Map _$OptimizelyExperimentToJson( + OptimizelyExperiment instance) => + { + 'id': instance.id, + 'key': instance.key, + 'variationsMap': instance.variationsMap, + }; diff --git a/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_feature.dart b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_feature.dart new file mode 100644 index 0000000..1bb536b --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_feature.dart @@ -0,0 +1,61 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +import './optimizely_experiment.dart'; +import './optimizely_variable.dart'; + +class OptimizelyFeature { + String id; + String key; + Map experimentsMap; + Map variablesMap; + + OptimizelyFeature(this.id, this.key, this.experimentsMap, this.variablesMap); + + factory OptimizelyFeature.fromJson(Map json) => + _$OptimizelyFeatureFromJson(json); + + Map toJson() => _$OptimizelyFeatureToJson(this); +} + +OptimizelyFeature _$OptimizelyFeatureFromJson(Map json) { + return OptimizelyFeature( + json['id'] as String, + json['key'] as String, + (json['experimentsMap'] as Map)?.map( + (k, e) => MapEntry( + k, + e == null + ? null + : OptimizelyExperiment.fromJson(e as Map)), + ), + (json['variablesMap'] as Map)?.map( + (k, e) => MapEntry( + k, + e == null + ? null + : OptimizelyVariable.fromJson(e as Map)), + ), + ); +} + +Map _$OptimizelyFeatureToJson(OptimizelyFeature instance) => + { + 'id': instance.id, + 'key': instance.key, + 'experimentsMap': instance.experimentsMap, + 'variablesMap': instance.variablesMap, + }; diff --git a/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_variable.dart b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_variable.dart new file mode 100644 index 0000000..e1b9496 --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_variable.dart @@ -0,0 +1,46 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +class OptimizelyVariable { + String id; + String key; + String type; + String value; + + OptimizelyVariable(this.id, this.key, this.type, this.value); + + factory OptimizelyVariable.fromJson(Map json) => + _$OptimizelyVariableFromJson(json); + + Map toJson() => _$OptimizelyVariableToJson(this); +} + +OptimizelyVariable _$OptimizelyVariableFromJson(Map json) { + return OptimizelyVariable( + json['id'] as String, + json['key'] as String, + json['type'] as String, + json['value'] as String, + ); +} + +Map _$OptimizelyVariableToJson(OptimizelyVariable instance) => + { + 'id': instance.id, + 'key': instance.key, + 'type': instance.type, + 'value': instance.value, + }; diff --git a/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_variation.dart b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_variation.dart new file mode 100644 index 0000000..09e2fa5 --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_config/optimizely_variation.dart @@ -0,0 +1,56 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +import './optimizely_variable.dart'; + +class OptimizelyVariation { + String id; + String key; + bool featureEnabled; + Map variablesMap; + + OptimizelyVariation( + this.id, this.key, this.featureEnabled, this.variablesMap); + + factory OptimizelyVariation.fromJson(Map json) => + _$OptimizelyVariationFromJson(json); + + Map toJson() => _$OptimizelyVariationToJson(this); +} + +OptimizelyVariation _$OptimizelyVariationFromJson(Map json) { + return OptimizelyVariation( + json['id'] as String, + json['key'] as String, + json['featureEnabled'] as bool, + (json['variablesMap'] as Map)?.map( + (k, e) => MapEntry( + k, + e == null + ? null + : OptimizelyVariable.fromJson(e as Map)), + ), + ); +} + +Map _$OptimizelyVariationToJson( + OptimizelyVariation instance) => + { + 'id': instance.id, + 'key': instance.key, + 'featureEnabled': instance.featureEnabled, + 'variablesMap': instance.variablesMap, + }; diff --git a/labs/optimizely-agent-client-dart/lib/src/models/override_response.dart b/labs/optimizely-agent-client-dart/lib/src/models/override_response.dart new file mode 100644 index 0000000..ad2839a --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/override_response.dart @@ -0,0 +1,50 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +class OverrideResponse { + OverrideResponse(this.userId, this.experimentKey, this.variationKey, + this.prevVariationKey, this.messages); + + String userId; + String experimentKey; + String variationKey; + String prevVariationKey; + List messages; + + factory OverrideResponse.fromJson(Map json) => + _$OverrideResponseFromJson(json); + + Map toJson() => _$OverrideResponseToJson(this); +} + +OverrideResponse _$OverrideResponseFromJson(Map json) { + return OverrideResponse( + json['userId'] as String, + json['experimentKey'] as String, + json['variationKey'] as String, + json['prevVariationKey'] as String, + (json['messages'] as List)?.map((e) => e as String)?.toList(), + ); +} + +Map _$OverrideResponseToJson(OverrideResponse instance) => + { + 'userId': instance.userId, + 'experimentKey': instance.experimentKey, + 'variationKey': instance.variationKey, + 'prevVariationKey': instance.prevVariationKey, + 'messages': instance.messages, + }; diff --git a/labs/optimizely-agent-client-dart/lib/src/network/http_manager.dart b/labs/optimizely-agent-client-dart/lib/src/network/http_manager.dart new file mode 100644 index 0000000..e110ae3 --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/network/http_manager.dart @@ -0,0 +1,40 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +import 'dart:io'; +import 'package:dio/dio.dart'; + +class HttpManager { + final String _sdkKey; + final String _url; + final _client = Dio(); + + HttpManager(this._sdkKey, this._url) { + _client.options.baseUrl = _url; + _client.options.headers = { + "X-Optimizely-SDK-Key": _sdkKey, + HttpHeaders.contentTypeHeader: "application/json" + }; + } + + Future getRequest(String endpoint) { + return _client.get('$_url$endpoint'); + } + + Future postRequest(String endpoint, Object body, [Map queryParams]) { + return _client.post(endpoint, data: body, queryParameters: queryParams); + } +} diff --git a/labs/optimizely-agent-client-dart/lib/src/request_manager.dart b/labs/optimizely-agent-client-dart/lib/src/request_manager.dart new file mode 100644 index 0000000..35b5e5b --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/request_manager.dart @@ -0,0 +1,135 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +import 'package:meta/meta.dart'; +import 'package:dio/dio.dart'; + +import './models/decision_types.dart'; +import './network/http_manager.dart'; + +class RequestManager { + HttpManager _manager; + + RequestManager(String sdkKey, url) { + _manager = HttpManager(sdkKey, url); + } + + Future getOptimizelyConfig() async { + Response resp; + try { + resp = await _manager.getRequest("/v1/config"); + } on DioError catch(err) { + resp = err.response != null ? err.response : new Response(statusCode: 0, statusMessage: err.message); + } + return resp; + } + + Future track({ + @required String eventKey, + String userId, + Map eventTags, + Map userAttributes + }) async { + Map body = {}; + + if (userId != null) { + body["userId"] = userId; + } + + if (eventTags != null) { + body["eventTags"] = eventTags; + } + + if (userAttributes != null) { + body["userAttributes"] = userAttributes; + } + + Response resp; + try { + resp = await _manager.postRequest("/v1/track", body, {"eventKey": eventKey}); + } on DioError catch(err) { + resp = err.response != null ? err.response : new Response(statusCode: 0, statusMessage: err.message); + } + return resp; + } + + Future overrideDecision({ + @required String userId, + @required String experimentKey, + @required String variationKey + }) async { + Map body = { + "userId": userId, + "experimentKey": experimentKey, + "variationKey": variationKey + }; + + Response resp; + try { + resp = await _manager.postRequest("/v1/override", body); + } on DioError catch(err) { + print(err.message); + resp = err.response != null ? err.response : new Response(statusCode: 0, statusMessage: err.message); + } + return resp; + } + + Future activate({ + @required String userId, + Map userAttributes, + List featureKey, + List experimentKey, + bool disableTracking, + DecisionType type, + bool enabled, + }) async { + Map body = { "userId": userId }; + + if (userAttributes != null) { + body["userAttributes"] = userAttributes; + } + + Map queryParams = {}; + + if (featureKey != null) { + queryParams["featureKey"] = featureKey.join(','); + } + + if (experimentKey != null) { + queryParams["experimentKey"] = experimentKey.join(','); + } + + if (disableTracking != null) { + queryParams["disableTracking"] = disableTracking.toString(); + } + + if (type != null) { + queryParams["type"] = type.toString().split('.').last; + } + + if (enabled != null) { + queryParams["enabled"] = enabled.toString(); + } + + Response resp; + try { + resp = await _manager.postRequest("/v1/activate", body, queryParams); + } on DioError catch(err) { + resp = err.response != null ? err.response : new Response(statusCode: 0, statusMessage: err.message); + } + return resp; + } +} diff --git a/labs/optimizely-agent-client-dart/pubspec.yaml b/labs/optimizely-agent-client-dart/pubspec.yaml new file mode 100644 index 0000000..e0062fb --- /dev/null +++ b/labs/optimizely-agent-client-dart/pubspec.yaml @@ -0,0 +1,14 @@ +name: optimizely_agent_client +description: Optimizely Agent Client. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 0.1.0 + +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + dio: ^3.0.10 From d934b686e7b05fdcc09f14bf0dbd1f92f8b017b6 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Tue, 18 May 2021 22:30:05 -0700 Subject: [PATCH 2/6] 1. Added decide and decideAll methods 2. Added memoization of userContext --- .../example/example.dart | 32 +++++++-- .../lib/optimizely_agent.dart | 67 +++++++++++++++++-- .../src/models/optimizely_decide_option.dart | 23 +++++++ .../lib/src/models/optimizely_decision.dart | 48 +++++++++++++ ...e.dart => optimizely_decision_legacy.dart} | 8 +-- .../lib/src/models/user_context.dart | 35 ++++++++++ .../lib/src/network/http_manager.dart | 2 +- .../lib/src/request_manager.dart | 31 +++++++++ 8 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/optimizely_decide_option.dart create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/optimizely_decision.dart rename labs/optimizely-agent-client-dart/lib/src/models/{activate_response.dart => optimizely_decision_legacy.dart} (90%) create mode 100644 labs/optimizely-agent-client-dart/lib/src/models/user_context.dart diff --git a/labs/optimizely-agent-client-dart/example/example.dart b/labs/optimizely-agent-client-dart/example/example.dart index ba97dc6..9c9626b 100644 --- a/labs/optimizely-agent-client-dart/example/example.dart +++ b/labs/optimizely-agent-client-dart/example/example.dart @@ -1,8 +1,26 @@ import 'package:optimizely_agent_client/optimizely_agent.dart'; void main() async { - OptimizelyAgent agent = new OptimizelyAgent('{SDK_KEY}', '{AGENT_URL}'); + OptimizelyAgent agent = new OptimizelyAgent('JY3jkLmiQiAqHd866edA3', 'http://127.0.0.1:8080', new UserContext('zee')); + print('---- Calling DecideAll API ----'); + var decisions = await agent.decideAll( + [ + OptimizelyDecideOption.DISABLE_DECISION_EVENT, + OptimizelyDecideOption.INCLUDE_REASONS + ], + ); + decisions?.forEach((decision) { + print(decision.toJson()); + }); + print(''); + + var decision = await agent.decide('product_sort', [ + OptimizelyDecideOption.DISABLE_DECISION_EVENT, + OptimizelyDecideOption.INCLUDE_REASONS + ]); + print(decision.toJson()); + print('---- Calling OptimizelyConfig API ----'); OptimizelyConfig config = await agent.getOptimizelyConfig(); if (config != null) { @@ -15,14 +33,18 @@ void main() async { print(' Variation Id: ${variation.id}'); }); }); + + config.featuresMap.forEach((String key, OptimizelyFeature feature) { + print('Feature Key: $key'); + }); } print(''); print('---- Calling Activate API ----'); - List optimizelyDecisions = await agent.activate(userId: 'user1', type: DecisionType.experiment, enabled: true); - if (optimizelyDecisions != null) { - print('Total Decisions ${optimizelyDecisions.length}'); - optimizelyDecisions.forEach((OptimizelyDecision decision) { + List optimizelyDecisionsLegacy = await agent.activate(userId: 'user1', type: DecisionType.experiment, enabled: true); + if (optimizelyDecisionsLegacy != null) { + print('Total Decisions ${optimizelyDecisionsLegacy.length}'); + optimizelyDecisionsLegacy.forEach((OptimizelyDecisionLegacy decision) { print(decision.toJson()); }); } diff --git a/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart b/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart index fb26b3b..f63c9b6 100644 --- a/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart +++ b/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart @@ -17,16 +17,22 @@ import 'package:meta/meta.dart'; import 'package:dio/dio.dart'; -import './src/models/activate_response.dart'; +import './src/models/optimizely_decision_legacy.dart'; import './src/models/decision_types.dart'; - +import './src/models/optimizely_decide_option.dart'; +import './src/models/optimizely_decision.dart'; +import './src/models/user_context.dart'; import './src/models/optimizely_config/optimizely_config.dart'; import './src/models/override_response.dart'; import './src/request_manager.dart'; +// Exporting all the required classes +export './src/models/optimizely_decision.dart'; export './src/models/decision_types.dart'; -export './src/models/activate_response.dart'; +export './src/models/optimizely_decision_legacy.dart'; export './src/models/override_response.dart'; +export './src/models/optimizely_decide_option.dart'; +export './src/models/user_context.dart'; // Exporting all OptimizelyConfig entities export './src/models/optimizely_config/optimizely_config.dart'; @@ -37,9 +43,11 @@ export './src/models/optimizely_config/optimizely_variation.dart'; class OptimizelyAgent { RequestManager _requestmanager; + UserContext userContext; - OptimizelyAgent(String sdkKey, String url) { + OptimizelyAgent(String sdkKey, String url, UserContext userContext) { _requestmanager = RequestManager(sdkKey, url); + this.userContext = userContext; } /// Returns status code and OptimizelyConfig object @@ -79,7 +87,7 @@ class OptimizelyAgent { /// Activate makes feature and experiment decisions for the selected query parameters /// and returns list of OptimizelyDecision - Future> activate({ + Future> activate({ @required String userId, Map userAttributes, List featureKey, @@ -98,6 +106,53 @@ class OptimizelyAgent { enabled: enabled ); if (resp.statusCode == 200) { + List optimizelyDecisions = []; + resp.data.forEach((element) { + optimizelyDecisions.add(OptimizelyDecisionLegacy.fromJson(element)); + }); + return optimizelyDecisions; + } + return null; + } + + Future decide( + String key, + [ + List optimizelyDecideOptions = const [], + UserContext overrideUserContext + ] + ) async { + UserContext resolvedUserContext = userContext; + if (overrideUserContext != null) { + resolvedUserContext = overrideUserContext; + } + if (!isUserContextValid(resolvedUserContext)) { + print('Invalid User Context, Failing `decide`'); + return null; + } + Response resp = await _requestmanager.decide(userContext: resolvedUserContext, key: key, optimizelyDecideOptions: optimizelyDecideOptions); + if (resp.statusCode == 200) { + return OptimizelyDecision.fromJson(resp.data); + } + return null; + } + + Future> decideAll( + [ + List optimizelyDecideOptions = const [], + UserContext overrideUserContext + ] + ) async { + UserContext resolvedUserContext = userContext; + if (overrideUserContext != null) { + resolvedUserContext = overrideUserContext; + } + if (!isUserContextValid(resolvedUserContext)) { + print('Invalid User Context, Failing `decideAll`'); + return null; + } + Response resp = await _requestmanager.decide(userContext: resolvedUserContext, optimizelyDecideOptions: optimizelyDecideOptions); + if (resp.statusCode == 200) { List optimizelyDecisions = []; resp.data.forEach((element) { optimizelyDecisions.add(OptimizelyDecision.fromJson(element)); @@ -106,4 +161,6 @@ class OptimizelyAgent { } return null; } + + isUserContextValid(UserContext userContext) => userContext?.userId != null && userContext?.userId != ''; } diff --git a/labs/optimizely-agent-client-dart/lib/src/models/optimizely_decide_option.dart b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_decide_option.dart new file mode 100644 index 0000000..b9892c8 --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_decide_option.dart @@ -0,0 +1,23 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +enum OptimizelyDecideOption { + DISABLE_DECISION_EVENT, + ENABLED_FLAGS_ONLY, + IGNORE_USER_PROFILE_SERVICE, + EXCLUDE_VARIABLES, + INCLUDE_REASONS +} diff --git a/labs/optimizely-agent-client-dart/lib/src/models/optimizely_decision.dart b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_decision.dart new file mode 100644 index 0000000..389bb23 --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_decision.dart @@ -0,0 +1,48 @@ +/**************************************************************************** + * Copyright 2021, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +import './user_context.dart'; + +class OptimizelyDecision { + Map variables; + String variationKey; + bool enabled; + String ruleKey; + String flagKey; + UserContext userContext; + List reasons; + + OptimizelyDecision.fromJson(Map json) + : variables = json['variables'] as Map ?? {}, + variationKey = json['variationKey'], + enabled = json['enabled'], + ruleKey = json['ruleKey'], + flagKey = json['flagKey'], + userContext = UserContext.fromJson(json['userContext']), + reasons = (json['reasons'] as List).map((r) => r.toString()).toList(); + + Map toJson() { + return { + 'userContext': this.userContext.toJson(), + 'ruleKey': this.ruleKey, + 'flagKey': this.flagKey, + 'variationKey': this.variationKey, + 'variables': this.variables, + 'enabled': this.enabled, + 'reasons': this.reasons, + }; + } +} diff --git a/labs/optimizely-agent-client-dart/lib/src/models/activate_response.dart b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_decision_legacy.dart similarity index 90% rename from labs/optimizely-agent-client-dart/lib/src/models/activate_response.dart rename to labs/optimizely-agent-client-dart/lib/src/models/optimizely_decision_legacy.dart index 2c919b7..5b742fe 100644 --- a/labs/optimizely-agent-client-dart/lib/src/models/activate_response.dart +++ b/labs/optimizely-agent-client-dart/lib/src/models/optimizely_decision_legacy.dart @@ -14,8 +14,8 @@ * limitations under the License. * ***************************************************************************/ -class OptimizelyDecision { - OptimizelyDecision(this.userId, this.experimentKey, this.error); +class OptimizelyDecisionLegacy { + OptimizelyDecisionLegacy(this.userId, this.experimentKey, this.error); String userId; String experimentKey; @@ -26,8 +26,8 @@ class OptimizelyDecision { bool enabled; String error; - factory OptimizelyDecision.fromJson(Map json) { - return OptimizelyDecision( + factory OptimizelyDecisionLegacy.fromJson(Map json) { + return OptimizelyDecisionLegacy( json['userId'] as String, json['experimentKey'] as String, json['error'] as String ?? '', diff --git a/labs/optimizely-agent-client-dart/lib/src/models/user_context.dart b/labs/optimizely-agent-client-dart/lib/src/models/user_context.dart new file mode 100644 index 0000000..a600031 --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/models/user_context.dart @@ -0,0 +1,35 @@ +/**************************************************************************** + * Copyright 2021, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +class UserContext { + String userId; + Map attributes = new Map(); + + UserContext(userId, [attributes]) + : this.userId = userId, + this.attributes = attributes; + + UserContext.fromJson(Map json) + : userId = json['userId'], + attributes = (json['attributes'] as Map).map((k, e) => MapEntry(k, e as String)); + + Map toJson() { + return { + "userId": this.userId, + "attributes": this.attributes + }; + } +} \ No newline at end of file diff --git a/labs/optimizely-agent-client-dart/lib/src/network/http_manager.dart b/labs/optimizely-agent-client-dart/lib/src/network/http_manager.dart index e110ae3..2d808bb 100644 --- a/labs/optimizely-agent-client-dart/lib/src/network/http_manager.dart +++ b/labs/optimizely-agent-client-dart/lib/src/network/http_manager.dart @@ -34,7 +34,7 @@ class HttpManager { return _client.get('$_url$endpoint'); } - Future postRequest(String endpoint, Object body, [Map queryParams]) { + Future postRequest(String endpoint, Object body, [Map queryParams]) { return _client.post(endpoint, data: body, queryParameters: queryParams); } } diff --git a/labs/optimizely-agent-client-dart/lib/src/request_manager.dart b/labs/optimizely-agent-client-dart/lib/src/request_manager.dart index 35b5e5b..ee6e66d 100644 --- a/labs/optimizely-agent-client-dart/lib/src/request_manager.dart +++ b/labs/optimizely-agent-client-dart/lib/src/request_manager.dart @@ -17,7 +17,9 @@ import 'package:meta/meta.dart'; import 'package:dio/dio.dart'; +import './models/user_context.dart'; import './models/decision_types.dart'; +import './models/optimizely_decide_option.dart'; import './network/http_manager.dart'; class RequestManager { @@ -132,4 +134,33 @@ class RequestManager { } return resp; } + + Future decide({ + @required UserContext userContext, + String key, + List optimizelyDecideOptions = const [] + }) async { + Map body = { + "userId": userContext.userId, + "decideOptions": optimizelyDecideOptions.map((option) => option.toString().split('.').last).toList(), + }; + + if (userContext.attributes != null) { + body["userAttributes"] = userContext.attributes; + } + + Map queryParams = {}; + + if (key != null) { + queryParams['keys'] = key; + } + + Response resp; + try { + resp = await _manager.postRequest("/v1/decide", body, queryParams); + } on DioError catch(err) { + resp = err.response != null ? err.response : new Response(statusCode: 0, statusMessage: err.message); + } + return resp; + } } From 95cee1f949b2160004c3d00f62dd40ebff0debef Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Tue, 18 May 2021 23:15:04 -0700 Subject: [PATCH 3/6] added a decisionCache --- .../example/example.dart | 4 +- .../lib/optimizely_agent.dart | 23 +++++++++++ .../lib/src/decision_cache.dart | 39 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 labs/optimizely-agent-client-dart/lib/src/decision_cache.dart diff --git a/labs/optimizely-agent-client-dart/example/example.dart b/labs/optimizely-agent-client-dart/example/example.dart index 9c9626b..02b536b 100644 --- a/labs/optimizely-agent-client-dart/example/example.dart +++ b/labs/optimizely-agent-client-dart/example/example.dart @@ -3,6 +3,8 @@ import 'package:optimizely_agent_client/optimizely_agent.dart'; void main() async { OptimizelyAgent agent = new OptimizelyAgent('JY3jkLmiQiAqHd866edA3', 'http://127.0.0.1:8080', new UserContext('zee')); + await agent.loadAndCacheDecisions(); + print('---- Calling DecideAll API ----'); var decisions = await agent.decideAll( [ @@ -19,7 +21,7 @@ void main() async { OptimizelyDecideOption.DISABLE_DECISION_EVENT, OptimizelyDecideOption.INCLUDE_REASONS ]); - print(decision.toJson()); + print(decision.toJson()); print('---- Calling OptimizelyConfig API ----'); OptimizelyConfig config = await agent.getOptimizelyConfig(); diff --git a/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart b/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart index f63c9b6..8c43e21 100644 --- a/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart +++ b/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart @@ -25,6 +25,7 @@ import './src/models/user_context.dart'; import './src/models/optimizely_config/optimizely_config.dart'; import './src/models/override_response.dart'; import './src/request_manager.dart'; +import './src/decision_cache.dart'; // Exporting all the required classes export './src/models/optimizely_decision.dart'; @@ -44,6 +45,7 @@ export './src/models/optimizely_config/optimizely_variation.dart'; class OptimizelyAgent { RequestManager _requestmanager; UserContext userContext; + DecisionCache decisionCache = new DecisionCache(); OptimizelyAgent(String sdkKey, String url, UserContext userContext) { _requestmanager = RequestManager(sdkKey, url); @@ -130,6 +132,14 @@ class OptimizelyAgent { print('Invalid User Context, Failing `decide`'); return null; } + OptimizelyDecision cachedDecision = decisionCache.getDecision(resolvedUserContext, key); + if (cachedDecision != null) { + print('--- Cache Hit!!! Returning Cached decision ---'); + return cachedDecision; + } else { + print('--- Cache Miss!!! Making a call to agent ---'); + } + Response resp = await _requestmanager.decide(userContext: resolvedUserContext, key: key, optimizelyDecideOptions: optimizelyDecideOptions); if (resp.statusCode == 200) { return OptimizelyDecision.fromJson(resp.data); @@ -163,4 +173,17 @@ class OptimizelyAgent { } isUserContextValid(UserContext userContext) => userContext?.userId != null && userContext?.userId != ''; + + Future loadAndCacheDecisions([UserContext overrideUserContext]) async { + UserContext resolvedUserContext = userContext; + if (overrideUserContext != null) { + resolvedUserContext = overrideUserContext; + } + if (!isUserContextValid(resolvedUserContext)) { + print('Invalid User Context, Failing `loadAndCacheDecisions`'); + return null; + } + List decisions = await decideAll([OptimizelyDecideOption.DISABLE_DECISION_EVENT], resolvedUserContext); + decisions.forEach((decision) => decisionCache.addDecision(resolvedUserContext, decision.flagKey, decision)); + } } diff --git a/labs/optimizely-agent-client-dart/lib/src/decision_cache.dart b/labs/optimizely-agent-client-dart/lib/src/decision_cache.dart new file mode 100644 index 0000000..0d02d3d --- /dev/null +++ b/labs/optimizely-agent-client-dart/lib/src/decision_cache.dart @@ -0,0 +1,39 @@ +/**************************************************************************** + * Copyright 2021, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +import './models/optimizely_decision.dart'; +import './models/user_context.dart'; + +class DecisionCache { + Map> cache = {}; + + addDecision(UserContext userContext, String flagKey, OptimizelyDecision decision) { + String userId = userContext.userId; + if (cache.containsKey(userId)) { + cache[userId][flagKey] = decision; + } else { + cache[userId] = { flagKey: decision}; + } + } + + OptimizelyDecision getDecision(UserContext userContext, String flagKey) { + String userId = userContext.userId; + if (cache[userId] != null && cache[userId][flagKey] != null) { + return cache[userId][flagKey]; + } + return null; + } +} From 55d2efb4a6e982604407c89586535086b32084ce Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Wed, 19 May 2021 00:09:28 -0700 Subject: [PATCH 4/6] Added cache flush --- .../lib/optimizely_agent.dart | 48 ++++++++++--------- .../lib/src/decision_cache.dart | 4 +- .../lib/src/request_manager.dart | 23 +-------- 3 files changed, 29 insertions(+), 46 deletions(-) diff --git a/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart b/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart index 8c43e21..c96544f 100644 --- a/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart +++ b/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart @@ -61,46 +61,46 @@ class OptimizelyAgent { /// Tracks an event and returns nothing. Future track({ @required String eventKey, - String userId, Map eventTags, - Map userAttributes + UserContext overrideUserContext }) { + UserContext resolvedUserContext = userContext; + if (overrideUserContext != null) { + resolvedUserContext = overrideUserContext; + } + if (!isUserContextValid(resolvedUserContext)) { + print('Invalid User Context, Failing `track`'); + return null; + } return _requestmanager.track( eventKey: eventKey, - userId: userId, + userId: resolvedUserContext.userId, eventTags: eventTags, - userAttributes: userAttributes + userAttributes: resolvedUserContext.attributes ); } - /// Overrides a decision for the user and returns OverrideResponse object. - Future overrideDecision({ - @required String userId, - @required String experimentKey, - @required String variationKey - }) async { - Response resp = await _requestmanager.overrideDecision( - userId: userId, - experimentKey: experimentKey, - variationKey: variationKey - ); - return resp.statusCode == 200 ? OverrideResponse.fromJson(resp.data) : null; - } - /// Activate makes feature and experiment decisions for the selected query parameters /// and returns list of OptimizelyDecision Future> activate({ - @required String userId, - Map userAttributes, List featureKey, List experimentKey, bool disableTracking, DecisionType type, - bool enabled + bool enabled, + UserContext overrideUserContext }) async { + UserContext resolvedUserContext = userContext; + if (overrideUserContext != null) { + resolvedUserContext = overrideUserContext; + } + if (!isUserContextValid(resolvedUserContext)) { + print('Invalid User Context, Failing `activate`'); + return null; + } Response resp = await _requestmanager.activate( - userId: userId, - userAttributes: userAttributes, + userId: resolvedUserContext.userId, + userAttributes: resolvedUserContext.attributes, featureKey: featureKey, experimentKey: experimentKey, disableTracking: disableTracking, @@ -186,4 +186,6 @@ class OptimizelyAgent { List decisions = await decideAll([OptimizelyDecideOption.DISABLE_DECISION_EVENT], resolvedUserContext); decisions.forEach((decision) => decisionCache.addDecision(resolvedUserContext, decision.flagKey, decision)); } + + resetCache() => decisionCache.reset(); } diff --git a/labs/optimizely-agent-client-dart/lib/src/decision_cache.dart b/labs/optimizely-agent-client-dart/lib/src/decision_cache.dart index 0d02d3d..11872f4 100644 --- a/labs/optimizely-agent-client-dart/lib/src/decision_cache.dart +++ b/labs/optimizely-agent-client-dart/lib/src/decision_cache.dart @@ -20,7 +20,7 @@ import './models/user_context.dart'; class DecisionCache { Map> cache = {}; - addDecision(UserContext userContext, String flagKey, OptimizelyDecision decision) { + void addDecision(UserContext userContext, String flagKey, OptimizelyDecision decision) { String userId = userContext.userId; if (cache.containsKey(userId)) { cache[userId][flagKey] = decision; @@ -36,4 +36,6 @@ class DecisionCache { } return null; } + + void reset() => cache = {}; } diff --git a/labs/optimizely-agent-client-dart/lib/src/request_manager.dart b/labs/optimizely-agent-client-dart/lib/src/request_manager.dart index ee6e66d..4a8fd8c 100644 --- a/labs/optimizely-agent-client-dart/lib/src/request_manager.dart +++ b/labs/optimizely-agent-client-dart/lib/src/request_manager.dart @@ -68,27 +68,6 @@ class RequestManager { return resp; } - Future overrideDecision({ - @required String userId, - @required String experimentKey, - @required String variationKey - }) async { - Map body = { - "userId": userId, - "experimentKey": experimentKey, - "variationKey": variationKey - }; - - Response resp; - try { - resp = await _manager.postRequest("/v1/override", body); - } on DioError catch(err) { - print(err.message); - resp = err.response != null ? err.response : new Response(statusCode: 0, statusMessage: err.message); - } - return resp; - } - Future activate({ @required String userId, Map userAttributes, @@ -142,7 +121,7 @@ class RequestManager { }) async { Map body = { "userId": userContext.userId, - "decideOptions": optimizelyDecideOptions.map((option) => option.toString().split('.').last).toList(), + "decideOptions": optimizelyDecideOptions?.map((option) => option.toString().split('.').last)?.toList(), }; if (userContext.attributes != null) { From 4d5305cfb1032e7240ff9854e579301ac697c885 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Wed, 19 May 2021 11:53:43 -0700 Subject: [PATCH 5/6] some cleanup --- .../example/example.dart | 15 ++---- .../lib/optimizely_agent.dart | 2 - .../lib/src/models/override_response.dart | 50 ------------------- 3 files changed, 3 insertions(+), 64 deletions(-) delete mode 100644 labs/optimizely-agent-client-dart/lib/src/models/override_response.dart diff --git a/labs/optimizely-agent-client-dart/example/example.dart b/labs/optimizely-agent-client-dart/example/example.dart index 02b536b..ecde079 100644 --- a/labs/optimizely-agent-client-dart/example/example.dart +++ b/labs/optimizely-agent-client-dart/example/example.dart @@ -43,7 +43,7 @@ void main() async { print(''); print('---- Calling Activate API ----'); - List optimizelyDecisionsLegacy = await agent.activate(userId: 'user1', type: DecisionType.experiment, enabled: true); + List optimizelyDecisionsLegacy = await agent.activate(type: DecisionType.experiment, enabled: true); if (optimizelyDecisionsLegacy != null) { print('Total Decisions ${optimizelyDecisionsLegacy.length}'); optimizelyDecisionsLegacy.forEach((OptimizelyDecisionLegacy decision) { @@ -53,15 +53,6 @@ void main() async { print(''); print('---- Calling Track API ----'); - await agent.track(eventKey: 'button1_click', userId: 'user1'); - print(''); - - print('---- Calling Override API ----'); - OverrideResponse overrideResponse = await agent.overrideDecision(userId: 'user1', experimentKey: 'playground-test', variationKey: 'variation_5'); - if (overrideResponse != null) { - print('Previous Variation: ${overrideResponse.prevVariationKey}'); - print('New Variation: ${overrideResponse.variationKey}'); - overrideResponse.messages.forEach((String message) => print('Message: $message')); - } - print(''); + await agent.track(eventKey: 'button1_click'); + print('Done!'); } diff --git a/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart b/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart index c96544f..3622dee 100644 --- a/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart +++ b/labs/optimizely-agent-client-dart/lib/optimizely_agent.dart @@ -23,7 +23,6 @@ import './src/models/optimizely_decide_option.dart'; import './src/models/optimizely_decision.dart'; import './src/models/user_context.dart'; import './src/models/optimizely_config/optimizely_config.dart'; -import './src/models/override_response.dart'; import './src/request_manager.dart'; import './src/decision_cache.dart'; @@ -31,7 +30,6 @@ import './src/decision_cache.dart'; export './src/models/optimizely_decision.dart'; export './src/models/decision_types.dart'; export './src/models/optimizely_decision_legacy.dart'; -export './src/models/override_response.dart'; export './src/models/optimizely_decide_option.dart'; export './src/models/user_context.dart'; diff --git a/labs/optimizely-agent-client-dart/lib/src/models/override_response.dart b/labs/optimizely-agent-client-dart/lib/src/models/override_response.dart deleted file mode 100644 index ad2839a..0000000 --- a/labs/optimizely-agent-client-dart/lib/src/models/override_response.dart +++ /dev/null @@ -1,50 +0,0 @@ -/**************************************************************************** - * Copyright 2020, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -class OverrideResponse { - OverrideResponse(this.userId, this.experimentKey, this.variationKey, - this.prevVariationKey, this.messages); - - String userId; - String experimentKey; - String variationKey; - String prevVariationKey; - List messages; - - factory OverrideResponse.fromJson(Map json) => - _$OverrideResponseFromJson(json); - - Map toJson() => _$OverrideResponseToJson(this); -} - -OverrideResponse _$OverrideResponseFromJson(Map json) { - return OverrideResponse( - json['userId'] as String, - json['experimentKey'] as String, - json['variationKey'] as String, - json['prevVariationKey'] as String, - (json['messages'] as List)?.map((e) => e as String)?.toList(), - ); -} - -Map _$OverrideResponseToJson(OverrideResponse instance) => - { - 'userId': instance.userId, - 'experimentKey': instance.experimentKey, - 'variationKey': instance.variationKey, - 'prevVariationKey': instance.prevVariationKey, - 'messages': instance.messages, - }; From caf8e15179b7d12712e5725d81a2226886666dc4 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Wed, 19 May 2021 17:59:12 -0700 Subject: [PATCH 6/6] Updated readme --- labs/optimizely-agent-client-dart/README.md | 92 +++++++++++-------- .../example/example.dart | 6 +- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/labs/optimizely-agent-client-dart/README.md b/labs/optimizely-agent-client-dart/README.md index f1987ec..8b9037f 100644 --- a/labs/optimizely-agent-client-dart/README.md +++ b/labs/optimizely-agent-client-dart/README.md @@ -1,38 +1,74 @@ -# Dart Client for Optimizely Agent -This is a dart client to facilitate communication with Optimizely Agent. +# Dart / FlutterClient for Optimizely Agent +This is a dart / flutter client to facilitate communication with Optimizely Agent. ## Initialization ``` -OptimizelyAgent(String sdkKey, String url) +OptimizelyAgent(String sdkKey, String url, UserContext userContext) ``` -The client can be initialized buy providing `sdkKey` and url where `agent` is deployed. +The client can be initialized by providing `sdkKey`, url where `agent` is deployed and `userContext` which contains `userId` and `attributes`. The client memoizes the user information and reuses it for subsequent calls. #### Example ``` -OptimizelyAgent agent = new OptimizelyAgent('{sdkKey}', 'http://localhost:8080'); +OptimizelyAgent agent = new OptimizelyAgent('{sdkKey}', 'http://localhost:8080', UserContext('user1', {'group': 'premium'})); ``` -## Activate +## Decision Caching +By default, the client makes a new http call to the agent for every decision. Optionally, to avoid latency, all decisions can be loaded and cached for a userContext. + +``` +loadAndCacheDecisions([UserContext overrideUserContext]) → Future +``` + +When no arguments are provided, it will load decisions for the memoized user. An optional`overrideUserContext` can be provided to load and cache decisions for a different user. + +#### +``` +await agent.loadAndCacheDecisions(); +``` + +## Decide + +``` +decide( + String key, + [List optimizelyDecideOptions = const [], + UserContext overrideUserContext +]) → Future +``` + +`decide` takes flag Key as a required parameter and evaluates the decision for the memoized user. It can also optionally take decide options or override User. `decide` returns a cached decision if available otherwise it makes an API call to the agent. + +## Decide All + +``` +decideAll( + [List optimizelyDecideOptions = const [], + UserContext overrideUserContext +]) → Future> +``` + +`decideAll` evaluates all the decisions for the memoized user. It can also optionally take decide options or override User. `decideAll` does not make use of the cache and always makes a new API call to agent. + +## Activate (Legacy) ``` activate({ - @required String userId, - Map userAttributes, List featureKey, List experimentKey, bool disableTracking, DecisionType type, - bool enabled -}) → Future> + bool enabled, + UserContext overrideUserContext +}) → Future> ``` -Activate takes `userId` as a required argument and a combination of optional arguments and returns a list of decisions represented by `OptimizelyDecision`. +Activate is a Legacy API and should only be used with legacy experiments. I uses memoized user and takes a combination of optional arguments and returns a list of decisions. Activate does not leverage decision caching. #### Example ``` -List optimizelyDecisions = await agent.activate(userId: 'user1', type: DecisionType.experiment, enabled: true); +List optimizelyDecisions = await agent.activate(type: DecisionType.experiment, enabled: true); if (optimizelyDecisions != null) { print('Total Decisions ${optimizelyDecisions.length}'); - optimizelyDecisions.forEach((OptimizelyDecision decision) { + optimizelyDecisions.forEach((OptimizelyDecisionLegacy decision) { print(decision.variationKey); }); } @@ -41,18 +77,17 @@ if (optimizelyDecisions != null) { ## Track ``` track({ - @required String eventKey, - String userId, + @required String eventKey, Map eventTags, - Map userAttributes + UserContext overrideUserContext }) → Future ``` -Track takes `eventKey` as a required argument and a combination of optional arguments and returns nothing. +Track takes `eventKey` as a required argument and a combination of optional arguments and sends an event. #### Example ``` -await agent.track(eventKey: 'button1_click', userId: 'user1'); +await agent.track(eventKey: 'button1_click'); ``` ## Optimizely Config @@ -77,24 +112,3 @@ if (config != null) { }); } ``` - -## Override Decision -``` -overrideDecision({ - @required String userId, - @required String experimentKey, - @required String variationKey -}) → Future -``` - -overrideDecision requires all the parameters and returns on `OverrideResponse` object which contains previous variation, new variation, messages and some more information. - -#### Example -``` -OverrideResponse overrideResponse = await agent.overrideDecision(userId: 'user1', experimentKey: 'playground-test', variationKey: 'variation_5'); -if (overrideResponse != null) { - print('Previous Variation: ${overrideResponse.prevVariationKey}'); - print('New Variation: ${overrideResponse.variationKey}'); - overrideResponse.messages.forEach((String message) => print('Message: $message')); -} -``` \ No newline at end of file diff --git a/labs/optimizely-agent-client-dart/example/example.dart b/labs/optimizely-agent-client-dart/example/example.dart index ecde079..dc25cb8 100644 --- a/labs/optimizely-agent-client-dart/example/example.dart +++ b/labs/optimizely-agent-client-dart/example/example.dart @@ -1,7 +1,7 @@ import 'package:optimizely_agent_client/optimizely_agent.dart'; void main() async { - OptimizelyAgent agent = new OptimizelyAgent('JY3jkLmiQiAqHd866edA3', 'http://127.0.0.1:8080', new UserContext('zee')); + OptimizelyAgent agent = new OptimizelyAgent('{SDK_KEY}', '{AGENT_URL}', UserContext('{USER_ID}')); await agent.loadAndCacheDecisions(); @@ -17,7 +17,7 @@ void main() async { }); print(''); - var decision = await agent.decide('product_sort', [ + var decision = await agent.decide('{FLAG_KEY}', [ OptimizelyDecideOption.DISABLE_DECISION_EVENT, OptimizelyDecideOption.INCLUDE_REASONS ]); @@ -53,6 +53,6 @@ void main() async { print(''); print('---- Calling Track API ----'); - await agent.track(eventKey: 'button1_click'); + await agent.track(eventKey: '{EVENT_NAME}'); print('Done!'); }