Skip to content

Commit

Permalink
Merge pull request #8 from growthbook/feature/sticky-bucket
Browse files Browse the repository at this point in the history
sticky bucket
  • Loading branch information
vazarkevych authored Apr 16, 2024
2 parents 46d0c1c + c691a3d commit f3d36f1
Show file tree
Hide file tree
Showing 14 changed files with 337 additions and 31 deletions.
27 changes: 23 additions & 4 deletions lib/src/Cache/caching_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import 'dart:developer';

import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';

class CachingManager {
abstract class CachingLayer {

GBFeatures? getContent(String key);
void saveContent(String key, String value);
}


class CachingManager extends CachingLayer {
// Create a static and final instance of the CachingManager class.
static final CachingManager _instance = CachingManager._internal();

Expand All @@ -17,17 +24,29 @@ class CachingManager {
// A map to hold the cached data.
final Map<String, dynamic> _cache = {};

getData(String fileName){
return getContent(fileName);
}

void putData(String fileName, dynamic content){
saveContent(fileName, content);
}

// Method to put data into the cache.
void putData(String key, dynamic data) {
@override
void saveContent(String key, dynamic value) {
// Store the data in the cache using the key.
_cache[key] = data;
_cache[key] = value;
log("_cache[key] ${_cache[key]}");
final test = _cache[key];
log(test.runtimeType.toString());
}



// Method to get data from the cache.
GBFeatures? getData(String key) {
@override
GBFeatures? getContent(String key) {
// Retrieve the data from the cache using the key.
return _cache[key];
}
Expand Down
51 changes: 51 additions & 0 deletions lib/src/Evaluator/feature_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';
/// Returns Calculated Feature Result against that key
class GBFeatureEvaluator {
final Map<String, dynamic> attributeOverrides;

GBFeatureEvaluator({required this.attributeOverrides});

static GBFeatureResult evaluateFeature(GBContext context, String featureKey) {
/// If we are not able to find feature on the basis of the passed featureKey
/// then we are going to return unKnownFeature.
Expand Down Expand Up @@ -158,4 +162,51 @@ class GBFeatureEvaluator {
experiment: experiment,
experimentResult: experimentResult);
}

Future<void> refreshStickyBuckets(GBContext context) async {
if (context.stickyBucketService == null) {
return;
}

var attributes = getStickyBucketAttributes(context);
context.stickyBucketAssignmentDocs =
await context.stickyBucketService?.getAllAssignments(attributes);
}

Map<String, String> getStickyBucketAttributes(GBContext context) {
var attributes = <String, String>{};

context.stickyBucketIdentifierAttributes =
context.stickyBucketIdentifierAttributes != null
? deriveStickyBucketIdentifierAttributes(context)
: context.stickyBucketIdentifierAttributes;

context.stickyBucketIdentifierAttributes?.forEach((attr) {
var hashValue = GBUtils.getHashAttribute(
context: context, attributeOverrides: attributeOverrides, attr: attr);
attributes[attr] = hashValue[1];
});

return attributes;
}

List<String> deriveStickyBucketIdentifierAttributes(
GBContext context) {
var attributes = <String>{};
var features = context.features;
for (var id in features.keys) {
var feature = features[id];
var rules = feature?.rules;
rules?.forEach((rule) {
var variations = rule.variations;
variations?.forEach((variation) {
attributes.add(rule.hashAttribute ?? "id");
if (rule.fallbackAttribute != null) {
attributes.add(rule.fallbackAttribute!);
}
});
});
}
return attributes.toList();
}
}
16 changes: 15 additions & 1 deletion lib/src/Model/context.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';
import 'package:growthbook_sdk_flutter/src/Model/sticky_assignments_document.dart';
import 'package:growthbook_sdk_flutter/src/StickyBucketService/sticky_bucket_service.dart';

/// Defines the GrowthBook context.
class GBContext {
Expand All @@ -9,10 +11,14 @@ class GBContext {
this.enabled,
this.attributes,
this.forcedVariation,
this.stickyBucketAssignmentDocs,
this.stickyBucketIdentifierAttributes,
this.stickyBucketService,
this.remoteEval,
this.qaMode,
this.trackingCallBack,
this.features = const {},
this.backgroundSync,
this.backgroundSync
});

/// Registered API key for GrowthBook SDK.
Expand All @@ -33,6 +39,14 @@ class GBContext {
/// Force specific experiments to always assign a specific variation (used for QA).
Map<String, dynamic>? forcedVariation;

Map<StickyAttributeKey, StickyAssignmentsDocument>? stickyBucketAssignmentDocs;

List<String>? stickyBucketIdentifierAttributes;

StickyBucketService? stickyBucketService;

bool? remoteEval;

/// If true, random assignment is disabled and only explicitly forced variations are used.
bool? qaMode;

Expand Down
24 changes: 23 additions & 1 deletion lib/src/Model/experiment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ class GBExperiment {
this.condition,
this.parentConditions,
this.hashAttribute,
this.fallbackAttribute,
this.weights,
this.active = true,
this.coverage,
this.force,
this.hashVersion,
this.disableStickyBucketing,
this.bucketVersion,
this.minBucketVersion,
this.ranges,
this.meta,
this.filters,
Expand All @@ -40,6 +44,9 @@ class GBExperiment {
/// All users included in the experiment will be forced into the specific variation index
String? hashAttribute;

/// When using sticky bucketing, can be used as a fallback to assign variations
String? fallbackAttribute;

/// How to weight traffic between variations. Must add to 1.
List? weights;

Expand All @@ -65,7 +72,16 @@ class GBExperiment {

//new properties v0.4.0
/// The hash version to use (default to 1)
int? hashVersion;
double? hashVersion;

/// If true, sticky bucketing will be disabled for this experiment. (Note: sticky bucketing is only available if a StickyBucketingService is provided in the Context)
bool? disableStickyBucketing;

/// An sticky bucket version number that can be used to force a re-bucketing of users (default to `0`)
int? bucketVersion;

/// Any users with a sticky bucket version less than this will be excluded from the experiment
int? minBucketVersion;

/// Array of ranges, one per variation
@Tuple2Converter()
Expand Down Expand Up @@ -105,6 +121,7 @@ class GBExperimentResult {
this.name,
this.bucket,
this.passthrough,
this.stickyBucketUsed,
});

/// Whether or not the user is part of the experiment
Expand All @@ -116,6 +133,7 @@ class GBExperimentResult {
/// The array value of the assigned variation
dynamic value;

/// If a hash was used to assign a variation
bool? hashUsed;

/// The user attribute used to assign a variation
Expand All @@ -124,6 +142,7 @@ class GBExperimentResult {
/// The value of that attribute
String? hashValue;

/// The id of the feature (if any) that the experiment came from
String? featureId;

//new properties v0.4.0
Expand All @@ -139,6 +158,9 @@ class GBExperimentResult {
/// Used for holdout groups
bool? passthrough;

/// If sticky bucketing was used to assign a variation
bool? stickyBucketUsed;

factory GBExperimentResult.fromJson(Map<String, dynamic> value) =>
_$GBExperimentResultFromJson(value);
}
7 changes: 6 additions & 1 deletion lib/src/Model/experiment.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions lib/src/Model/features.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class GBFeature {
@JsonSerializable(createToJson: false)
class GBFeatureRule {
GBFeatureRule({
this.id,
this.condition,
this.coverage,
this.force,
Expand All @@ -37,7 +38,11 @@ class GBFeatureRule {
this.weights,
this.namespace,
this.hashAttribute,
this.fallbackAttribute,
this.hashVersion,
this.disableStickyBucketing,
this.bucketVersion,
this.minBucketVersion,
this.range,
this.ranges,
this.meta,
Expand All @@ -49,6 +54,8 @@ class GBFeatureRule {
this.parentConditions,
});

String? id;

/// Optional targeting condition
GBCondition? condition;

Expand Down Expand Up @@ -78,10 +85,22 @@ class GBFeatureRule {
/// What user attribute should be used to assign variations (defaults to id)
String? hashAttribute;

/// When using sticky bucketing, can be used as a fallback to assign variations
String? fallbackAttribute;

// new properties v0.4.0
/// The hash version to use (default to 1)
int? hashVersion;

/// If true, sticky bucketing will be disabled for this experiment. (Note: sticky bucketing is only available if a StickyBucketingService is provided in the Context)
bool? disableStickyBucketing;

/// An sticky bucket version number that can be used to force a re-bucketing of users (default to '0')
int? bucketVersion;

/// Any users with a sticky bucket version less than this will be excluded from the experiment
int? minBucketVersion;

/// A more precise version of coverage
@Tuple2Converter()
GBBucketRange? range;
Expand Down
5 changes: 5 additions & 0 deletions lib/src/Model/features.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions lib/src/Model/gb_parent_condition.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart';
import 'package:json_annotation/json_annotation.dart';

part 'gb_parent_condition.g.dart';


@JsonSerializable(createToJson: false)
class GBParentCondition {
String id;
GBCondition condition;
bool? gate;
final String id;
final Map<String, dynamic> condition;
final bool? gate;

GBParentCondition({required this.id, required this.condition, this.gate});

Expand Down
2 changes: 1 addition & 1 deletion lib/src/Model/gb_parent_condition.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions lib/src/Model/sticky_assignments_document.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:json_annotation/json_annotation.dart';

part 'sticky_assignments_document.g.dart';

@JsonSerializable()
class StickyAssignmentsDocument {
const StickyAssignmentsDocument({
required this.attributeName,
required this.attributeValue,
required this.assignments,
});

final String attributeName;
final String attributeValue;
final StickyAssignments assignments;

factory StickyAssignmentsDocument.fromJson(Map<String, dynamic> value) =>
_$StickyAssignmentsDocumentFromJson(value);

Map<String, dynamic> toJson() => _$StickyAssignmentsDocumentToJson(this);
}

typedef StickyAssignments = Map<StickyExperimentKey, String>;

typedef StickyExperimentKey = String; // `${experimentId}__{version}`

typedef StickyAttributeKey = String; // `${attributeName}||${attributeValue}`

Loading

0 comments on commit f3d36f1

Please sign in to comment.