Skip to content

Dart / Flutter Client for Optimizely Agent #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions labs/optimizely-agent-client-dart/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.packages
pubspec.lock
.dart_tool/*
launch.json
114 changes: 114 additions & 0 deletions labs/optimizely-agent-client-dart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Dart / FlutterClient for Optimizely Agent
This is a dart / flutter client to facilitate communication with Optimizely Agent.

## Initialization
```
OptimizelyAgent(String sdkKey, String url, UserContext userContext)
```
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', UserContext('user1', {'group': 'premium'}));
```

## 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<void>
```

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<OptimizelyDecideOption> optimizelyDecideOptions = const [],
UserContext overrideUserContext
]) → Future<OptimizelyDecision>
```

`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<OptimizelyDecideOption> optimizelyDecideOptions = const [],
UserContext overrideUserContext
]) → Future<List<OptimizelyDecision>>
```

`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({
List<String> featureKey,
List<String> experimentKey,
bool disableTracking,
DecisionType type,
bool enabled,
UserContext overrideUserContext
}) → Future<List<OptimizelyDecisionLegacy>>
```

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<OptimizelyDecisionLegacy> optimizelyDecisions = await agent.activate(type: DecisionType.experiment, enabled: true);
if (optimizelyDecisions != null) {
print('Total Decisions ${optimizelyDecisions.length}');
optimizelyDecisions.forEach((OptimizelyDecisionLegacy decision) {
print(decision.variationKey);
});
}
```

## Track
```
track({
@required String eventKey,
Map<String, dynamic> eventTags,
UserContext overrideUserContext
}) → Future<void>
```

Track takes `eventKey` as a required argument and a combination of optional arguments and sends an event.

#### Example
```
await agent.track(eventKey: 'button1_click');
```

## Optimizely Config
```
getOptimizelyConfig() → Future<OptimizelyConfig>
```

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}');
});
});
}
```
58 changes: 58 additions & 0 deletions labs/optimizely-agent-client-dart/example/example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:optimizely_agent_client/optimizely_agent.dart';

void main() async {
OptimizelyAgent agent = new OptimizelyAgent('{SDK_KEY}', '{AGENT_URL}', UserContext('{USER_ID}'));

await agent.loadAndCacheDecisions();

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('{FLAG_KEY}', [
OptimizelyDecideOption.DISABLE_DECISION_EVENT,
OptimizelyDecideOption.INCLUDE_REASONS
]);
print(decision.toJson());

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}');
});
});

config.featuresMap.forEach((String key, OptimizelyFeature feature) {
print('Feature Key: $key');
});
}
print('');

print('---- Calling Activate API ----');
List<OptimizelyDecisionLegacy> optimizelyDecisionsLegacy = await agent.activate(type: DecisionType.experiment, enabled: true);
if (optimizelyDecisionsLegacy != null) {
print('Total Decisions ${optimizelyDecisionsLegacy.length}');
optimizelyDecisionsLegacy.forEach((OptimizelyDecisionLegacy decision) {
print(decision.toJson());
});
}
print('');

print('---- Calling Track API ----');
await agent.track(eventKey: '{EVENT_NAME}');
print('Done!');
}
189 changes: 189 additions & 0 deletions labs/optimizely-agent-client-dart/lib/optimizely_agent.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/****************************************************************************
* 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/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/request_manager.dart';
import './src/decision_cache.dart';

// Exporting all the required classes
export './src/models/optimizely_decision.dart';
export './src/models/decision_types.dart';
export './src/models/optimizely_decision_legacy.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';
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;
UserContext userContext;
DecisionCache decisionCache = new DecisionCache();

OptimizelyAgent(String sdkKey, String url, UserContext userContext) {
_requestmanager = RequestManager(sdkKey, url);
this.userContext = userContext;
}

/// Returns status code and OptimizelyConfig object
Future<OptimizelyConfig> getOptimizelyConfig() async {
Response resp = await _requestmanager.getOptimizelyConfig();
return resp.statusCode == 200 ? OptimizelyConfig.fromJson(resp.data) : null;
}

/// Tracks an event and returns nothing.
Future<void> track({
@required String eventKey,
Map<String, dynamic> eventTags,
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: resolvedUserContext.userId,
eventTags: eventTags,
userAttributes: resolvedUserContext.attributes
);
}

/// Activate makes feature and experiment decisions for the selected query parameters
/// and returns list of OptimizelyDecision
Future<List<OptimizelyDecisionLegacy>> activate({
List<String> featureKey,
List<String> experimentKey,
bool disableTracking,
DecisionType type,
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: resolvedUserContext.userId,
userAttributes: resolvedUserContext.attributes,
featureKey: featureKey,
experimentKey: experimentKey,
disableTracking: disableTracking,
type: type,
enabled: enabled
);
if (resp.statusCode == 200) {
List<OptimizelyDecisionLegacy> optimizelyDecisions = [];
resp.data.forEach((element) {
optimizelyDecisions.add(OptimizelyDecisionLegacy.fromJson(element));
});
return optimizelyDecisions;
}
return null;
}

Future<OptimizelyDecision> decide(
String key,
[
List<OptimizelyDecideOption> optimizelyDecideOptions = const [],
UserContext overrideUserContext
]
) async {
UserContext resolvedUserContext = userContext;
if (overrideUserContext != null) {
resolvedUserContext = overrideUserContext;
}
if (!isUserContextValid(resolvedUserContext)) {
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);
}
return null;
}

Future<List<OptimizelyDecision>> decideAll(
[
List<OptimizelyDecideOption> 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<OptimizelyDecision> optimizelyDecisions = [];
resp.data.forEach((element) {
optimizelyDecisions.add(OptimizelyDecision.fromJson(element));
});
return optimizelyDecisions;
}
return null;
}

isUserContextValid(UserContext userContext) => userContext?.userId != null && userContext?.userId != '';

Future<void> loadAndCacheDecisions([UserContext overrideUserContext]) async {
UserContext resolvedUserContext = userContext;
if (overrideUserContext != null) {
resolvedUserContext = overrideUserContext;
}
if (!isUserContextValid(resolvedUserContext)) {
print('Invalid User Context, Failing `loadAndCacheDecisions`');
return null;
}
List<OptimizelyDecision> decisions = await decideAll([OptimizelyDecideOption.DISABLE_DECISION_EVENT], resolvedUserContext);
decisions.forEach((decision) => decisionCache.addDecision(resolvedUserContext, decision.flagKey, decision));
}

resetCache() => decisionCache.reset();
}
Loading