From 32c957b8b9d7ede02c446678beaa07e011fe61e5 Mon Sep 17 00:00:00 2001 From: Volodymyr Nazarkevych Date: Thu, 19 Sep 2024 14:44:24 +0300 Subject: [PATCH] changelog 3.9.5 --- CHANGELOG.md | 6 ++ lib/src/Evaluator/experiment_evaluator.dart | 2 +- lib/src/Features/features_view_model.dart | 35 ++++++++--- lib/src/Model/context.dart | 4 +- lib/src/Utils/constant.dart | 2 + lib/src/Utils/gb_utils.dart | 4 +- lib/src/Utils/gb_variation_meta.dart | 11 ++++ lib/src/growth_book_sdk.dart | 68 +++++++++++++++++---- pubspec.yaml | 2 +- test/common_test/sdk_builder_test.dart | 22 +++---- 10 files changed, 115 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aca0411..3b89dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 3.9.5 +- Add subscription logic +- Fix issue with null handling +- Fix hashing on web +- Update cache saving logic + # 3.9.4 - New Operators $inGroup and $notInGroup to check Saved Groups by reference - Add argument to evalCondition for definition of Saved Groups diff --git a/lib/src/Evaluator/experiment_evaluator.dart b/lib/src/Evaluator/experiment_evaluator.dart index 35e5ecb..146ec2c 100644 --- a/lib/src/Evaluator/experiment_evaluator.dart +++ b/lib/src/Evaluator/experiment_evaluator.dart @@ -85,7 +85,7 @@ class ExperimentEvaluator { minExperimentBucketVersion: experiment.minBucketVersion ?? 0, meta: experiment.meta ?? [], expHashAttribute: experiment.hashAttribute ?? "id", - expFallBackAttribute: experiment.fallbackAttribute!, + expFallBackAttribute: experiment.fallbackAttribute, attributeOverrides: attributeOverrides, ); foundStickyBucket = stickyBucketResult.variation >= 0; diff --git a/lib/src/Features/features_view_model.dart b/lib/src/Features/features_view_model.dart index ea73f86..c87555d 100644 --- a/lib/src/Features/features_view_model.dart +++ b/lib/src/Features/features_view_model.dart @@ -64,8 +64,8 @@ class FeatureViewModel { } else { String receivedDataJson = utf8Decoder.convert(receivedData); final receiveFeatureJsonMap = json.decode(receivedDataJson); - Map featureMap = {}; + GBFeatures featureMap = {}; if (encryptionKey.isNotEmpty) { receiveFeatureJsonMap.forEach((key, value) { if (value is Map) { @@ -126,12 +126,25 @@ class FeatureViewModel { } void handleValidFeatures(FeaturedDataModel data) { - if (data.features != null && data.features!.isNotEmpty) { + if (data.features != null && data.encryptedFeatures == null) { delegate.featuresAPIModelSuccessfully(data); - String jsonString = json.encode(data.toJson()); - final bytes = utf8Encoder.convert(jsonString); - manager.putData(fileName: Constant.featureCache, content: bytes); delegate.featuresFetchedSuccessfully(gbFeatures: data.features!, isRemote: true); + final featureData = utf8Encoder.convert(jsonEncode(data.features)); + final featureDataOnUint8List = Uint8List.fromList(featureData); + manager.putData( + fileName: Constant.featureCache, + content: featureDataOnUint8List, + ); + + if (data.savedGroups != null) { + delegate.savedGroupsFetchedSuccessfully(savedGroups: data.savedGroups!, isRemote: true); + final savedGroupsData = utf8Encoder.convert(jsonEncode(data.savedGroups)); + final savedGroupsDataOnUint8List = Uint8List.fromList(savedGroupsData); + manager.putData( + fileName: Constant.savedGroupsCache, + content: savedGroupsDataOnUint8List, + ); + } } else { if (data.encryptedFeatures != null) { handleEncryptedFeatures(data.encryptedFeatures!); @@ -161,7 +174,7 @@ class FeatureViewModel { ); if (extractedFeatures != null) { - delegate.featuresFetchedSuccessfully(gbFeatures: extractedFeatures, isRemote: false); + delegate.featuresFetchedSuccessfully(gbFeatures: extractedFeatures, isRemote: true); final featureData = utf8Encoder.convert(jsonEncode(extractedFeatures)); final featureDataOnUint8List = Uint8List.fromList(featureData); manager.putData( @@ -237,9 +250,11 @@ class FeatureViewModel { } void cacheFeatures(FeaturedDataModel data) { - String jsonString = json.encode(data.toJson()); - final bytes = utf8Encoder.convert(jsonString); - - manager.putData(fileName: Constant.featureCache, content: bytes); + final featureData = utf8Encoder.convert(jsonEncode(data.features)); + final featureDataOnUint8List = Uint8List.fromList(featureData); + manager.putData( + fileName: Constant.featureCache, + content: featureDataOnUint8List, + ); } } diff --git a/lib/src/Model/context.dart b/lib/src/Model/context.dart index 7faade0..dba0d2d 100644 --- a/lib/src/Model/context.dart +++ b/lib/src/Model/context.dart @@ -69,7 +69,7 @@ class GBContext { String? getFeaturesURL() { if (hostURL != null && apiKey != null) { - return '${hostURL}api/features/$apiKey'; + return '${hostURL}/api/features/$apiKey'; } else { return null; } @@ -77,7 +77,7 @@ class GBContext { String? getRemoteEvalUrl() { if (hostURL != null && apiKey != null) { - return '${hostURL}api/eval/$apiKey'; + return '${hostURL}/api/eval/$apiKey'; } else { return null; } diff --git a/lib/src/Utils/constant.dart b/lib/src/Utils/constant.dart index caed860..453e687 100644 --- a/lib/src/Utils/constant.dart +++ b/lib/src/Utils/constant.dart @@ -44,6 +44,8 @@ typedef GBStickyBucketingService = LocalStorageStickyBucketService; /// A function that takes experiment and result as arguments. typedef TrackingCallBack = void Function(GBExperiment, GBExperimentResult); +typedef ExperimentRunCallback = void Function(GBExperiment, GBExperimentResult); + typedef GBFeatureUsageCallback = void Function(String, GBFeatureResult); typedef SavedGroupsValues = Map; diff --git a/lib/src/Utils/gb_utils.dart b/lib/src/Utils/gb_utils.dart index 1a3dc24..6bec298 100644 --- a/lib/src/Utils/gb_utils.dart +++ b/lib/src/Utils/gb_utils.dart @@ -11,7 +11,7 @@ class FNV { // Constants for FNV-1a 32-bit hash final int init32 = 0x811c9dc5; final int prime32 = 0x01000193; - final int mod32 = 1 << 32; // Equivalent to 2^32 + final int mod32 = 0x100000000; // Equivalent to 2^32 /// Fowler-Noll-Vo hash - 32 bit /// Returns an integer representing the hash. @@ -470,7 +470,7 @@ class GBUtils { required int minExperimentBucketVersion, required List meta, required String expHashAttribute, - required String expFallBackAttribute, + required String? expFallBackAttribute, required Map attributeOverrides, }) { // Get the assignment key for the given experiment key and version. diff --git a/lib/src/Utils/gb_variation_meta.dart b/lib/src/Utils/gb_variation_meta.dart index 2c8f898..3c3ce10 100644 --- a/lib/src/Utils/gb_variation_meta.dart +++ b/lib/src/Utils/gb_variation_meta.dart @@ -42,3 +42,14 @@ class GBTrackData { Map toJson() => _$GBTrackDataToJson(this); } + +@JsonSerializable() +class AssignedExperiment { + AssignedExperiment({ + required this.experiment, + required this.experimentResult, + }); + + final GBExperiment experiment; + final GBExperimentResult experimentResult; +} diff --git a/lib/src/growth_book_sdk.dart b/lib/src/growth_book_sdk.dart index aa441a7..f7b1176 100644 --- a/lib/src/growth_book_sdk.dart +++ b/lib/src/growth_book_sdk.dart @@ -1,11 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; import 'package:growthbook_sdk_flutter/growthbook_sdk_flutter.dart'; import 'package:growthbook_sdk_flutter/src/Model/remote_eval_model.dart'; import 'package:growthbook_sdk_flutter/src/Model/sticky_assignments_document.dart'; import 'package:growthbook_sdk_flutter/src/StickyBucketService/sticky_bucket_service.dart'; import 'package:growthbook_sdk_flutter/src/Utils/crypto.dart'; +import 'package:growthbook_sdk_flutter/src/Utils/gb_variation_meta.dart'; typedef VoidCallback = void Function(); @@ -28,10 +30,7 @@ class GBSDKBuilderApp { this.stickyBucketService, this.backgroundSync = false, this.remoteEval = false, - }) : assert( - hostURL.endsWith('/'), - 'Invalid host url: $hostURL. The hostUrl should be end with `/`, example: `https://example.growthbook.io/`', - ); + }); final String apiKey; final String? encryptionKey; @@ -132,6 +131,10 @@ class GrowthBookSDK extends FeaturesFlowDelegate { Map _attributeOverrides; + List subscriptions = []; + + Map assigned = {}; + /// The complete data regarding features & attributes etc. GBContext get context => _context; @@ -144,16 +147,20 @@ class GrowthBookSDK extends FeaturesFlowDelegate { required bool isRemote, }) { _context.features = gbFeatures; - if (_refreshHandler != null) { - _refreshHandler!(true); + if (isRemote) { + if (_refreshHandler != null) { + _refreshHandler!(true); + } } } @override void featuresFetchFailed({required GBError? error, required bool isRemote}) { _onInitializationFailure?.call(error); - if (_refreshHandler != null) { - _refreshHandler!(false); + if (isRemote) { + if (_refreshHandler != null) { + _refreshHandler!(false); + } } } @@ -179,10 +186,40 @@ class GrowthBookSDK extends FeaturesFlowDelegate { if (_context.remoteEval) { refreshForRemoteEval(); } else { + log(context.getFeaturesURL().toString()); await featureViewModel.fetchFeatures(context.getFeaturesURL()); } } + void fireSubscriptions(GBExperiment experiment, GBExperimentResult result) { + String key = experiment.key; + + // If assigned variation has changed, fire subscriptions + if (assigned.containsKey(key)) { + var assignedExperiment = assigned[key]; + + if (assignedExperiment!.experimentResult.inExperiment != result.inExperiment || + assignedExperiment.experimentResult.variationID != result.variationID) { + updateSubscriptions(key: key, experiment: experiment, result: result); + } + } + } + + void updateSubscriptions({required String key, required GBExperiment experiment, required GBExperimentResult result}) { + assigned[key] = AssignedExperiment(experiment: experiment, experimentResult: result); + for (var subscription in subscriptions) { + subscription(experiment, result); + } + } + + void subscribe(ExperimentRunCallback result) { + subscriptions.add(result); + } + + void clearSubscriptions() { + subscriptions.clear(); + } + GBFeatureResult feature(String id) { return FeatureEvaluator( attributeOverrides: _attributeOverrides, @@ -192,7 +229,8 @@ class GrowthBookSDK extends FeaturesFlowDelegate { } GBExperimentResult run(GBExperiment experiment) { - return ExperimentEvaluator(attributeOverrides: _attributeOverrides).evaluateExperiment(context, experiment); + final result = ExperimentEvaluator(attributeOverrides: _attributeOverrides).evaluateExperiment(context, experiment); + return result; } Map getStickyBucketAssignmentDocs() { @@ -285,16 +323,20 @@ class GrowthBookSDK extends FeaturesFlowDelegate { @override void savedGroupsFetchFailed({required GBError? error, required bool isRemote}) { _onInitializationFailure?.call(error); - if (_refreshHandler != null) { - _refreshHandler!(false); + if (isRemote) { + if (_refreshHandler != null) { + _refreshHandler!(false); + } } } @override void savedGroupsFetchedSuccessfully({required SavedGroupsValues savedGroups, required bool isRemote}) { _context.savedGroups = savedGroups; - if (_refreshHandler != null) { - _refreshHandler!(true); + if (isRemote) { + if (_refreshHandler != null) { + _refreshHandler!(true); + } } } } diff --git a/pubspec.yaml b/pubspec.yaml index d3dd326..59677f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: growthbook_sdk_flutter description: An open-source feature flagging and experimentation platform that makes it simple to alter features and execute A/B testing. -version: 3.9.4 +version: 3.9.5 homepage: https://github.com/alippo-com/GrowthBook-SDK-Flutter repository: https://github.com/growthbook/growthbook-flutter diff --git a/test/common_test/sdk_builder_test.dart b/test/common_test/sdk_builder_test.dart index 7a31d3d..60f3b8a 100644 --- a/test/common_test/sdk_builder_test.dart +++ b/test/common_test/sdk_builder_test.dart @@ -12,7 +12,7 @@ void main() { group('Initialization', () { const testApiKey = ''; const attr = {}; - const testHostURL = 'https://example.growthbook.io/'; + const testHostURL = 'https://example.growthbook.io'; const client = MockNetworkClient(); CachingManager manager = CachingManager(); @@ -66,18 +66,16 @@ void main() { manager.clearCache(); }); - test('- with initialization assertion cause of wrong host url', () async { - expect( - () => GBSDKBuilderApp( - apiKey: testApiKey, - hostURL: "https://example.growthbook.io", - client: client, - growthBookTrackingCallBack: (_, __) {}, - backgroundSync: false, - ), - throwsAssertionError, + test('- with initialization without throwing assertion error for wrong host url', () async { + final sdkInstance = GBSDKBuilderApp( + apiKey: testApiKey, + hostURL: testHostURL, + client: client, + growthBookTrackingCallBack: (_, __) {}, + backgroundSync: false, ); - manager.clearCache(); + expect(sdkInstance, isNotNull); + manager.clearCache(); }); test('- with network client', () async {