diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 3a02907..6e3030e 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists + diff --git a/example/ios/.vscode/launch.json b/example/ios/.vscode/launch.json new file mode 100644 index 0000000..2ba986f --- /dev/null +++ b/example/ios/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 0000000..a88caf9 --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/example/ios/Flutter/ephemeral/flutter_lldbinit b/example/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 0000000..e3ba6fb --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/example/ios/Podfile b/example/ios/Podfile index eb8b0f9..728c145 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project - platform :ios, '12.0' + platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9442052..cb623e0 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -33,11 +33,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 -PODFILE CHECKSUM: ce13d36744da294d67f8e460dbb7aed7c09bd7f4 +PODFILE CHECKSUM: 30517025a2fecca2d72dac25f08abb5b9a8f1a56 COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 4399b21..11ffde3 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -342,7 +342,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -423,7 +423,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -472,7 +472,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e67b280..fc5ae03 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> @@ -45,11 +46,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/example/lib/Providers/fs_data.dart b/example/lib/Providers/fs_data.dart index d3a3b45..22a9cb4 100644 --- a/example/lib/Providers/fs_data.dart +++ b/example/lib/Providers/fs_data.dart @@ -1,12 +1,10 @@ -import 'dart:math'; - import 'package:flagship/tracking/tracking_manager_config.dart'; import 'package:flagship/utils/constants.dart'; import 'package:flutter/material.dart'; class FSData extends ChangeNotifier { // Apikey - String _apiKey = "apiKey"; // + String _apiKey = "DxAcxlnRB9yFBZYtLDue1q01dcXZCw6aM49CQB23"; // // EnvId String _envId = "bkk9glocmjcg0vtmdlng"; // // Mode @@ -65,7 +63,7 @@ class FSData extends ChangeNotifier { } class UserData extends ChangeNotifier { - String _visitorId = "flutter_user" + Random().nextInt(10000).toString(); + String _visitorId = "anonymousId"; //+ Random().nextInt(10000).toString(); Map context = { "testing_tracking_manager": true, diff --git a/example/lib/widgets/configuration.dart b/example/lib/widgets/configuration.dart index e2773f5..aafbcc1 100644 --- a/example/lib/widgets/configuration.dart +++ b/example/lib/widgets/configuration.dart @@ -406,8 +406,7 @@ class _ConfigurationState extends State with ShowDialog { GestureBinding.instance.pointerRouter .addGlobalRoute(_emotionAIGlobalPointerRoute); } catch (e) { - // Todo later add flagship logger - print(e); + Flagship.logger(Level.ERROR, e.toString()); } Flagship.sharedInstance() diff --git a/example/pubspec.lock b/example/pubspec.lock index 5008719..ddaa333 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -21,26 +21,26 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" crypto: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: @@ -103,7 +103,7 @@ packages: path: ".." relative: true source: path - version: "4.1.1-beta" + version: "4.2.0-beta" flutter: dependency: "direct main" description: flutter @@ -163,34 +163,34 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -203,10 +203,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" murmurhash: dependency: transitive description: @@ -227,10 +227,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: transitive description: @@ -424,18 +424,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: @@ -464,10 +464,10 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.6" typed_data: dependency: transitive description: @@ -488,10 +488,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -533,5 +533,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/lib/Storage/database_management.dart b/lib/Storage/database_management.dart index 52f1842..dafe6a7 100644 --- a/lib/Storage/database_management.dart +++ b/lib/Storage/database_management.dart @@ -159,4 +159,22 @@ class DatabaseManagement { return ''; } } + + // Check if visitor exists in database + Future visitorExists(String visitorId, String nameTable) async { + try { + final db = _visitorDatabase; + if (db == null) return false; + + List result = await db.rawQuery( + 'SELECT COUNT(*) as count FROM $nameTable WHERE id = ?', [visitorId]); + + int count = result.isNotEmpty ? (result.first['count'] ?? 0) : 0; + return count > 0; + } on Exception catch (e) { + Flagship.logger(Level.EXCEPTIONS, + "Error checking if visitor exists: ${e.toString()}"); + return false; + } + } } diff --git a/lib/cache/default_cache.dart b/lib/cache/default_cache.dart index 419fa7b..39ebdd5 100644 --- a/lib/cache/default_cache.dart +++ b/lib/cache/default_cache.dart @@ -95,4 +95,16 @@ class DefaultCacheVisitorImp with IVisitorCacheImplementation { await _checkDatabase(); return dbMgt.readVisitor(visitoId, 'table_visitors'); } + + @override + Future visitorExists(String visitorId) async { + // Reject empty strings + if (visitorId.isEmpty) { + return false; + } + Flagship.logger( + Level.DEBUG, "visitorExists from default cache Implementation"); + await _checkDatabase(); + return dbMgt.visitorExists(visitorId, 'table_visitors'); + } } diff --git a/lib/cache/interface_cache.dart b/lib/cache/interface_cache.dart index c9bd905..ec42ae5 100644 --- a/lib/cache/interface_cache.dart +++ b/lib/cache/interface_cache.dart @@ -9,6 +9,13 @@ mixin IVisitorCacheImplementation { // Called when a visitor set consent to false. Must erase visitor data related to the given visitor void flushVisitor(String visitorId); + +// Optional: Called to check if a visitor exists in cache without retrieving the data +// Returns true if visitor exists, false otherwise +// Default implementation returns false (no cache) + Future visitorExists(String visitorId) async { + return Future.value(false); + } } mixin IHitCacheImplementation { diff --git a/lib/decision/bucketing_manager.dart b/lib/decision/bucketing_manager.dart index baa8c5f..a8ddf1e 100644 --- a/lib/decision/bucketing_manager.dart +++ b/lib/decision/bucketing_manager.dart @@ -150,8 +150,10 @@ class BucketingManager extends DecisionManager { if (this.assignationHistory == null) { this.assignationHistory = Map.fromEntries(newAssign.entries); } else { - this.assignationHistory?.clear(); - this.assignationHistory?.addEntries(newAssign.entries); + this.assignationHistory = { + ...this.assignationHistory ?? {}, + ...newAssign + }; } } diff --git a/lib/flagship_version.dart b/lib/flagship_version.dart index 29b1e21..e1c906e 100644 --- a/lib/flagship_version.dart +++ b/lib/flagship_version.dart @@ -1,2 +1,2 @@ /// This file is automatically updated -const FlagshipVersion = "4.1.2-beta"; +const FlagshipVersion = "4.2.0-beta"; diff --git a/lib/model/exposed_flag.dart b/lib/model/exposed_flag.dart index cdfe62a..97ca8d5 100644 --- a/lib/model/exposed_flag.dart +++ b/lib/model/exposed_flag.dart @@ -15,8 +15,11 @@ class ExposedFlag implements IFlag { final T _value; // Metadata final FlagMetadata _metadata; + // If flag is already activated + bool alreadyActivatedCampaign = false; - ExposedFlag(this._key, this._value, this._defaultValue, this._metadata); + ExposedFlag(this._key, this._value, this._defaultValue, this._metadata, + {bool alreadyActivatedCampaign = false}); T get value { return _value; @@ -37,7 +40,8 @@ class ExposedFlag implements IFlag { "key": this.key, "value": this.value, "defaultValue": this.defaultValue, - "metadata": this.metadata().toJson() + "metadata": this.metadata().toJson(), + "alreadyActivatedCampaign": this.alreadyActivatedCampaign }; } } diff --git a/lib/visitor.dart b/lib/visitor.dart index e284a18..c0eada0 100644 --- a/lib/visitor.dart +++ b/lib/visitor.dart @@ -40,6 +40,8 @@ enum Instance { NEW_INSTANCE } +const Duration FSSessionVisitor = Duration(seconds: 1 * 60 * 30); // 30 min + class Visitor with EmotionAiDelegate { /// VisitorId String visitorId; @@ -77,6 +79,7 @@ class Visitor with EmotionAiDelegate { Map assignmentsHistory = {}; /// Delegate visitor + late VisitorDelegate _visitorDelegate; /// Delegate to update the status @@ -104,6 +107,9 @@ class Visitor with EmotionAiDelegate { // _onFlagStatusFetched OnFlagStatusFetched _onFlagStatusFetched; + // Add this flag to track if visitor lookup has been performed + bool _needLookupVisitor = true; + // Get flagStatus FlagStatus get flagStatus { return _flagStatus; @@ -137,6 +143,9 @@ class Visitor with EmotionAiDelegate { } } + // Init the sesssion + DateTime sessionDuration = DateTime.now(); + // Create new instance for visitor Visitor( this.config, @@ -192,12 +201,12 @@ class Visitor with EmotionAiDelegate { _visitorDelegate.lookupHits(); // Lookup for the cached visitor data - _visitorDelegate.lookupVisitor(this.visitorId).then((isLoadedFromCache) => { - this._fetchReasons = isLoadedFromCache - ? FetchFlagsRequiredStatusReason.FLAGS_FETCHED_FROM_CACHE - : FetchFlagsRequiredStatusReason.FLAGS_NEVER_FETCHED - }); - _visitorDelegate.lookupVisitor(this.visitorId).whenComplete(() {}); + // _visitorDelegate.lookupVisitor(this.visitorId).then((isLoadedFromCache) => { + // this._fetchReasons = isLoadedFromCache + // ? FetchFlagsRequiredStatusReason.FLAGS_FETCHED_FROM_CACHE + // : FetchFlagsRequiredStatusReason.FLAGS_NEVER_FETCHED + // }); + // _visitorDelegate.lookupVisitor(this.visitorId).whenComplete(() {}); /// Send the consent hit _visitorDelegate.sendHit(Consent(hasConsented: _hasConsented)); @@ -208,11 +217,13 @@ class Visitor with EmotionAiDelegate { // Update context directely with map for void clearContext() { + sessionDuration = DateTime.now(); _context.clear(); } // Update context directely with map for void updateContextWithMap(Map context) { + sessionDuration = DateTime.now(); var oldContext = Map.fromEntries(_context.entries); _context.addAll(context); if (mapEquals(oldContext, _context) == false) { @@ -241,6 +252,7 @@ class Visitor with EmotionAiDelegate { /// otherwise the update context skip with warnning log void updateContext(String key, T value) { + sessionDuration = DateTime.now(); var oldContext = Map.fromEntries(_context.entries); /// Delegate the action to strategy to update @@ -259,6 +271,7 @@ class Visitor with EmotionAiDelegate { /// Update with predefined context void updateFlagshipContext(FlagshipContext flagshipContext, T value) { + sessionDuration = DateTime.now(); if (FlagshipContextManager.chekcValidity(flagshipContext, value)) { updateContext(rawValue(flagshipContext), value); } else { @@ -270,6 +283,7 @@ class Visitor with EmotionAiDelegate { // Get Flag // - Return Flag instance Flag getFlag(String key) { + sessionDuration = DateTime.now(); if (_flagSyncStatus != FlagSyncStatus.FLAGS_FETCHED) { Flagship.logger( Level.ALL, _flagSyncStatus.warningMessage(visitorId, key)); @@ -280,6 +294,7 @@ class Visitor with EmotionAiDelegate { // Get the colllection flags /// - Returns: an instance of FSFlagCollection with flags FlagCollection getFlags() { + sessionDuration = DateTime.now(); Map ret = {}; this.modifications.forEach((keyItem, modifItem) { @@ -288,9 +303,53 @@ class Visitor with EmotionAiDelegate { return FlagCollection(this._visitorDelegate, ret); } + // Private function to handle visitor lookup logic + Future _performVisitorLookupIfNeeded() async { + if (!_needLookupVisitor) + return; // temporary disable to always lookup visitor + + String? idToLookup; + + // First check if visitorId exists in cache using config + bool visitorExists = + await config.visitorCacheImp?.visitorExists(visitorId) ?? false; + + if (visitorExists) { + idToLookup = visitorId; + } else if (anonymousId != null) { + // If visitorId doesn't exist but we have anonymousId, check if it exists + bool anonymousExists = + await config.visitorCacheImp?.visitorExists(anonymousId!) ?? false; + if (anonymousExists) { + idToLookup = anonymousId!; + } + } + // Only perform lookup if we found an existing ID + if (idToLookup != null) { + await _visitorDelegate + .lookupVisitor(idToLookup) + .then((isLoadedFromCache) { + this._fetchReasons = isLoadedFromCache + ? FetchFlagsRequiredStatusReason.FLAGS_FETCHED_FROM_CACHE + : FetchFlagsRequiredStatusReason.FLAGS_NEVER_FETCHED; + this._needLookupVisitor = false; + }); + } else { + // No existing visitor found, set appropriate fetch reason + this._fetchReasons = FetchFlagsRequiredStatusReason.FLAGS_NEVER_FETCHED; + this._needLookupVisitor = false; + } + } + Future fetchFlags() async { + sessionDuration = DateTime.now(); + /// Delegate the action to strategy this.flagStatus = FlagStatus.FETCHING; + + // Only lookup visitor if it is necessary + await _performVisitorLookupIfNeeded(); + return _visitorDelegate.fetchFlags().then((fetchResponse) { if (fetchResponse?.error == null) { _flagSyncStatus = FlagSyncStatus.FLAGS_FETCHED; @@ -308,12 +367,14 @@ class Visitor with EmotionAiDelegate { /// Send hit Future sendHit(BaseHit hit) async { + sessionDuration = DateTime.now(); // Delegate the action to strategy _visitorDelegate.sendHit(hit); } /// Set Consent void setConsent(bool newValue) { + sessionDuration = DateTime.now(); // flush the hits from the pool if (newValue == false) { this.trackingManager?.flushAllTracking(this.visitorId); @@ -343,6 +404,8 @@ class Visitor with EmotionAiDelegate { /// - Requires: Make sure that the experience continuity option is enabled on the flagship platform before using this method authenticate(String visitorId) { + sessionDuration = DateTime.now(); + _needLookupVisitor = true; _isAuthenticated = true; _visitorDelegate.getStrategy().authenticateVisitor(visitorId); this.flagStatus = FlagStatus.FETCH_REQUIRED; @@ -353,6 +416,8 @@ class Visitor with EmotionAiDelegate { /// Use authenticate methode to go from Logged in session to logged out session unauthenticate() { + sessionDuration = DateTime.now(); + _needLookupVisitor = true; _isAuthenticated = false; _visitorDelegate.getStrategy().unAuthenticateVisitor(); this.flagStatus = FlagStatus.FETCH_REQUIRED; @@ -368,11 +433,13 @@ class Visitor with EmotionAiDelegate { @visibleForTesting FlagSyncStatus getFlagSyncStatus() { + sessionDuration = DateTime.now(); return _flagSyncStatus; } // Add emotionAI function collectEmotionsAIEvents(String screenName) { + sessionDuration = DateTime.now(); if (Flagship.sharedInstance().eaiCollectEnabled == true) { if (eaiVisitorScored == true) { Flagship.logger(Level.INFO, diff --git a/lib/visitor/Ivisitor.dart b/lib/visitor/Ivisitor.dart index 419e924..fc5ae41 100644 --- a/lib/visitor/Ivisitor.dart +++ b/lib/visitor/Ivisitor.dart @@ -14,8 +14,6 @@ abstract class IVisitor { // Future synchronizeModifications(); // Fetch Flags Future fetchFlags(); -// Activate modification - Future activateModification(String key); // Activate flag Future activateFlag(Modification pModification); // Send Hits diff --git a/lib/visitor/strategy/default_strategy.dart b/lib/visitor/strategy/default_strategy.dart index ae066fb..3019596 100644 --- a/lib/visitor/strategy/default_strategy.dart +++ b/lib/visitor/strategy/default_strategy.dart @@ -22,7 +22,6 @@ import 'package:flagship/visitor/Ivisitor.dart'; // This class represent the default behaviour class DefaultStrategy implements IVisitor { final Visitor visitor; - DefaultStrategy(this.visitor); @override @@ -39,65 +38,85 @@ class DefaultStrategy implements IVisitor { } } - // Activate - Future _sendActivate(Modification pModification) async { - // Check if the callback is defined - String? exposedFlag; - String? exposedVisitor; - if (Flagship.sharedInstance().getConfiguration()?.onVisitorExposed != - null) { - exposedFlag = jsonEncode(ExposedFlag( - pModification.key, - pModification.value, - pModification.defaultValue, - FlagMetadata.withMap(pModification.toJsonInformation())) - .toJson()); - - exposedVisitor = jsonEncode(VisitorExposed( - visitor.visitorId, visitor.anonymousId, visitor.getContext()) - .toJson()); - } - // Build the activate hit - Activate activateHit = Activate( - pModification, + Future _sendActivate( + Modification modification, + bool isDuplicated, + ) async { + // Get config and callback + final config = Flagship.sharedInstance().getConfiguration(); + final onExposed = config?.onVisitorExposed; + + // Prepare exposure object + ExposedFlag? exposedFlag; + VisitorExposed? exposedVisitor; + if (onExposed != null) { + exposedFlag = ExposedFlag( + modification.key, + modification.value, + modification.defaultValue, + FlagMetadata.withMap(modification.toJsonInformation()), + ); + exposedVisitor = VisitorExposed( visitor.visitorId, visitor.anonymousId, - Flagship.sharedInstance().envId ?? "", - exposedFlag, - exposedVisitor); - // Process the troubleShooting - DataUsageTracking.sharedInstance().processTroubleShootingHits( - CriticalPoints.VISITOR_SEND_ACTIVATE.name, visitor, activateHit); - visitor.trackingManager?.sendActivate(activateHit).then((activateResponse) { - if (activateResponse.statusCode >= 200 && - activateResponse.statusCode < 300) { - } else { - Flagship.logger( - Level.ERROR, - ACTIVATE_FAILED + - " status code = ${activateResponse.statusCode.toString()}"); + visitor.getContext(), + ); + } + + // When deduplicated + if (isDuplicated) { + if (onExposed != null && exposedFlag != null && exposedVisitor != null) { + exposedFlag.alreadyActivatedCampaign = true; + onExposed(exposedVisitor, exposedFlag); } - }); - } + Flagship.logger(Level.INFO, " The campaign's flag already activated "); + return; + } - @override - Future activateModification(String key) async { - if (visitor.modifications.containsKey(key)) { - try { - var modification = visitor.modifications[key]; + // When not duplicated + final String? flagJson = + exposedFlag != null ? jsonEncode(exposedFlag) : null; + final String? visitorJson = + exposedVisitor != null ? jsonEncode(exposedVisitor) : null; + + final activateHit = Activate( + modification, + visitor.visitorId, + visitor.anonymousId, + Flagship.sharedInstance().envId ?? '', + flagJson, + visitorJson, + ); + + // Send troubleshooting + DataUsageTracking.sharedInstance().processTroubleShootingHits( + CriticalPoints.VISITOR_SEND_ACTIVATE.name, + visitor, + activateHit, + ); - if (modification != null) { - await _sendActivate(modification); - } - } catch (exp) { - Flagship.logger(Level.EXCEPTIONS, EXCEPTION.replaceFirst("%s", "$exp")); + // Send Activate hit + try { + final response = await visitor.trackingManager?.sendActivate(activateHit); + final status = response?.statusCode ?? -1; + if (status < 200 || status >= 300) { + Flagship.logger( + Level.ERROR, + 'ACTIVATE_FAILED: status code = $status', + ); } + } catch (e, stack) { + Flagship.logger( + Level.ERROR, + 'ACTIVATE_FAILED: exception = $e\n$stack', + ); } } @override - Future activateFlag(Modification pModification) async { - return _sendActivate(pModification); + Future activateFlag(Modification pModification, + {bool isDuplicated = false}) async { + return _sendActivate(pModification, isDuplicated); } @override @@ -147,7 +166,7 @@ class DefaultStrategy implements IVisitor { } if (activate && hasSameType) { // Send activate later - _sendActivate(modification); + this._sendActivate(modification, false); } } catch (exp) { Flagship.logger(Level.INFO, @@ -203,6 +222,7 @@ class DefaultStrategy implements IVisitor { } else { state = FSSdkStatus.SDK_INITIALIZED; var modif = visitor.decisionManager.getModifications(camp.campaigns); + visitor.modifications.addAll(modif); // Start Batching loop visitor.trackingManager?.startBatchingLoop(); @@ -215,8 +235,24 @@ class DefaultStrategy implements IVisitor { visitor.flagshipDelegate.onUpdateState(state); // Save the response for the visitor database - cacheVisitor(visitor.visitorId, - jsonEncode(VisitorCache.fromVisitor(this.visitor).toJson())); + String visitorCacheData = + jsonEncode(VisitorCache.fromVisitor(this.visitor).toJson()); + cacheVisitor(visitor.visitorId, visitorCacheData); + // In bucketing mode, if anonymousId exists and no cache exists for it, cache the same data + if (visitor.config.decisionMode == Mode.BUCKETING && + visitor.anonymousId != null) { + // Check if cache exists for anonymousId + bool anonymousExists = await visitor.config.visitorCacheImp + ?.visitorExists(visitor.anonymousId ?? "") ?? + false; + + if (!anonymousExists) { + // Cache the same visitor data with anonymousId as key + cacheVisitor(visitor.anonymousId!, visitorCacheData); + Flagship.logger(Level.DEBUG, + "Cached visitor data for anonymousId: ${visitor.anonymousId} in bucketing mode"); + } + } // Update the dataUsage tracking visitor.dataUsageTracking .updateTroubleshooting(camp.accountSettings?.troubleshooting); @@ -253,21 +289,16 @@ class DefaultStrategy implements IVisitor { @override authenticateVisitor(String pVisitorId) { - if (visitor.config.decisionMode == Mode.DECISION_API) { - if (visitor.anonymousId == null) { - visitor.anonymousId = visitor.visitorId; - visitor.visitorId = pVisitorId; - // Update fs_users - visitor.updateContext(FS_USERS, pVisitorId); - } - - DataUsageTracking.sharedInstance() - .processTSXpc(CriticalPoints.VISITOR_AUTHENTICATE.name, this.visitor); - } else { - Flagship.logger(Level.ALL, - "AuthenticateVisitor method will be ignored in Bucketing configuration"); + if (visitor.anonymousId == null) { + visitor.anonymousId = visitor.visitorId; + visitor.visitorId = pVisitorId; + // Update fs_users + visitor.updateContext(FS_USERS, pVisitorId); } + DataUsageTracking.sharedInstance() + .processTSXpc(CriticalPoints.VISITOR_AUTHENTICATE.name, this.visitor); + // Update the xpc info for the emotionAI this .visitor @@ -277,19 +308,15 @@ class DefaultStrategy implements IVisitor { @override unAuthenticateVisitor() { - if (visitor.config.decisionMode == Mode.DECISION_API) { - if (visitor.anonymousId != null) { - visitor.visitorId = visitor.anonymousId as String; - visitor.anonymousId = null; - // Update fs_users in context - visitor.updateContext(FS_USERS, visitor.visitorId); - } - DataUsageTracking.sharedInstance().processTSXpc( - CriticalPoints.VISITOR_UNAUTHENTICATE.name, this.visitor); - } else { - Flagship.logger(Level.ALL, - "unAuthenticateVisitor method will be ignored in Bucketing configuration"); + if (visitor.anonymousId != null) { + visitor.visitorId = visitor.anonymousId as String; + visitor.anonymousId = null; + // Update fs_users in context + visitor.updateContext(FS_USERS, visitor.visitorId); } + DataUsageTracking.sharedInstance() + .processTSXpc(CriticalPoints.VISITOR_UNAUTHENTICATE.name, this.visitor); + // Update the xpc info for the emotionAI this .visitor @@ -298,15 +325,15 @@ class DefaultStrategy implements IVisitor { } @override - void cacheVisitor(String visitorId, String jsonString) { - visitor.config.visitorCacheImp?.cacheVisitor(visitor.visitorId, jsonString); + void cacheVisitor(String pVisitorId, String jsonString) { + visitor.config.visitorCacheImp?.cacheVisitor(pVisitorId, jsonString); } @override // Called right at visitor creation, return a jsonString corresponding to visitor. Return a jsonString - Future lookupVisitor(String visitoId) async { + Future lookupVisitor(String visitorId) async { var resultFromCacheBis = await visitor.config.visitorCacheImp - ?.lookupVisitor(visitor.visitorId) + ?.lookupVisitor(visitorId) .timeout( Duration( milliseconds: diff --git a/lib/visitor/strategy/no_consent_strategy.dart b/lib/visitor/strategy/no_consent_strategy.dart index 12aad62..387bba5 100644 --- a/lib/visitor/strategy/no_consent_strategy.dart +++ b/lib/visitor/strategy/no_consent_strategy.dart @@ -10,14 +10,9 @@ import 'package:flagship/visitor/strategy/default_strategy.dart'; class NoConsentStrategy extends DefaultStrategy { NoConsentStrategy(Visitor visitor) : super(visitor); -// The activate modification is not allowed @override - Future activateModification(String key) async { - Flagship.logger(Level.INFO, CONSENT_ACTIVATE); - } - - @override - Future activateFlag(Modification pFlag) async { + Future activateFlag(Modification pFlag, + {bool isDuplicated = false}) async { Flagship.logger(Level.INFO, CONSENT_ACTIVATE); } diff --git a/lib/visitor/strategy/not_ready_strategy.dart b/lib/visitor/strategy/not_ready_strategy.dart index 3f2172b..2e6a054 100644 --- a/lib/visitor/strategy/not_ready_strategy.dart +++ b/lib/visitor/strategy/not_ready_strategy.dart @@ -18,12 +18,8 @@ class NotReadyStrategy extends DefaultStrategy { } @override - Future activateModification(String key) async { - Flagship.logger(Level.ERROR, ACTIVATE_NOT_READY); - } - - @override - Future activateFlag(Modification pFlag) async { + Future activateFlag(Modification pFlag, + {bool isDuplicated = false}) async { Flagship.logger(Level.ERROR, ACTIVATE_NOT_READY); } diff --git a/lib/visitor/strategy/panic_strategy.dart b/lib/visitor/strategy/panic_strategy.dart index 20742f6..9f52f1c 100644 --- a/lib/visitor/strategy/panic_strategy.dart +++ b/lib/visitor/strategy/panic_strategy.dart @@ -11,12 +11,8 @@ class PanicStrategy extends DefaultStrategy { PanicStrategy(Visitor visitor) : super(visitor); @override - Future activateModification(String key) async { - Flagship.logger(Level.INFO, PANIC_ACTIVATE); - } - - @override - Future activateFlag(Modification pFlag) async { + Future activateFlag(Modification pFlag, + {bool isDuplicated = false}) async { Flagship.logger(Level.INFO, PANIC_ACTIVATE); } diff --git a/lib/visitor/visitor_delegate.dart b/lib/visitor/visitor_delegate.dart index f2aa4dd..d9de211 100644 --- a/lib/visitor/visitor_delegate.dart +++ b/lib/visitor/visitor_delegate.dart @@ -13,6 +13,8 @@ import '../visitor.dart'; class VisitorDelegate implements IVisitor { final Visitor visitor; + + Map _activatedVariations = {}; VisitorDelegate(this.visitor); // Get the strategy DefaultStrategy getStrategy() { @@ -33,15 +35,11 @@ class VisitorDelegate implements IVisitor { } } -// Activate modification - @override - Future activateModification(String key) { - return getStrategy().activateModification(key); - } - @override Future activateFlag(Modification pModification) { - return getStrategy().activateFlag(pModification); + bool isDup = _isDeduplicatedFlag( + pModification.campaignId, pModification.variationGroupId); + return getStrategy().activateFlag(pModification, isDuplicated: isDup); } // Get modification @@ -141,4 +139,27 @@ class VisitorDelegate implements IVisitor { onAppScreenChange(String screenName) { getStrategy().onAppScreenChange(screenName); } + + /// Returns `true` if flag is already activated during visitor session + bool _isDeduplicatedFlag(String campId, String varGrpId) { + final DateTime now = DateTime.now(); + final Duration elapsed = now.difference(visitor.sessionDuration); + + try { + if (elapsed > FSSessionVisitor) { + _activatedVariations + ..clear() + ..[campId] = varGrpId; + return false; + } + + final bool isDup = _activatedVariations[campId] == varGrpId; + + _activatedVariations[campId] = varGrpId; + + return isDup; + } finally { + visitor.sessionDuration = now; + } + } } diff --git a/pubspec.lock b/pubspec.lock index feea711..e41961e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: "direct main" description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: @@ -332,26 +332,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" logging: dependency: transitive description: @@ -364,10 +364,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -380,10 +380,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -428,10 +428,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: @@ -697,18 +697,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -745,10 +745,10 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.6" timing: dependency: transitive description: @@ -777,10 +777,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -854,5 +854,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 41ed826..b324bfa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flagship description: Flutter SDK for Flagship Feature management & Experiment platform for modern engineering and product teams -version: 4.1.2-beta +version: 4.2.0-beta homepage: https://flagship.io environment: diff --git a/test/activate_test.dart b/test/activate_test.dart index 69ee8f8..0c4235f 100644 --- a/test/activate_test.dart +++ b/test/activate_test.dart @@ -1,23 +1,43 @@ +import 'package:flagship/api/service.dart'; +import 'package:flagship/cache/default_cache.dart'; +import 'package:flagship/decision/api_manager.dart'; import 'package:flagship/flagship.dart'; import 'package:flagship/flagship_config.dart'; +import 'package:flagship/flagship_version.dart'; import 'package:flagship/model/exposed_flag.dart'; import 'package:flagship/model/flag.dart'; import 'package:flagship/model/modification.dart'; import 'package:flagship/model/visitor_exposed.dart'; +import 'package:flagship/tracking/tracking_manager.dart'; +import 'package:flagship/tracking/tracking_manager_config.dart'; +import 'package:flagship/tracking/tracking_manager_continuous_strategies.dart'; import 'package:flagship/visitor/strategy/default_strategy.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flagship/hits/activate.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'fake_path_provider_platform.dart'; import 'test_tools.dart'; +import 'package:http/http.dart' as http; +import 'service_test.mocks.dart'; +@GenerateMocks([Service]) void main() { - WidgetsFlutterBinding.ensureInitialized(); + MockService fakeService = MockService(); + + MockService fakeTrackingService = MockService(); + + TrackingManager fakeTrackingMgr = TrackingManageContinuousStrategy( + fakeTrackingService, TrackingManagerConfig(), DefaultCacheHitImp()); + + ApiManager fakeApi = ApiManager(fakeService); PathProviderPlatform.instance = FakePathProviderPlatform(); ToolsTest.sqfliteTestInit(); + WidgetsFlutterBinding.ensureInitialized(); SharedPreferences.setMockInitialValues({}); test("Activate with Modification object ", () { @@ -49,6 +69,7 @@ void main() { if (v.id == "expoVisitor") { expect(f.metadata().campaignId, "campaignId"); expect(v.id, "expoVisitor"); + expect(f.alreadyActivatedCampaign, false); } }).build(); Flagship.start("bkk9glocmjcg0vtmdlrr", "apiKey", config: expoConfig); @@ -111,4 +132,52 @@ void main() { var eF = ExposedFlag("key", 12, 12, FlagMetadata.withMap({})); expect(eF.metadata().campaignId, ""); }); + + test(' Test is Deduplicated', () async { + /// prepare response + when(fakeTrackingService.sendHttpRequest(RequestType.Post, + 'https://decision.flagship.io/v2/activate', any, any, + timeoutMs: TIMEOUT_REQUEST)) + .thenAnswer((_) async { + return http.Response("mock", 200); + }); + + var testConfig = ConfigBuilder().withOnVisitorExposed((v, f) { + if (v.id == "testV") { + expect(f.metadata().campaignId, "campaignId"); + expect(v.id, "testV"); + expect(f.alreadyActivatedCampaign, true); + } + }).build(); + + Flagship.start("bkk9glocmjcg0vtmdlrr", "apiKey", config: testConfig); + + var testV = Flagship.newVisitor(visitorId: "testV", hasConsented: true) + .withContext({"expoKey": "expoVal"}).build(); + + testV.trackingManager = fakeTrackingMgr; + testV.config.decisionManager = fakeApi; + + // Create a default strategy + var dfltStrategy = DefaultStrategy(testV); + + Modification itemModif = Modification( + "key1", + "campaignId", + "campName", + "variationGroupId", + "vargName", + "variationId", + "varName", + true, + "ab", + "slug", + 12); + + dfltStrategy.activateFlag(itemModif, isDuplicated: true); + var tr = dfltStrategy.visitor.trackingManager + as TrackingManageContinuousStrategy; + expect(tr.activatePool.fsQueue.length, 0); + dfltStrategy.onExposure(itemModif); + }); } diff --git a/test/notConsent_test.dart b/test/notConsent_test.dart index a654b64..550bea8 100644 --- a/test/notConsent_test.dart +++ b/test/notConsent_test.dart @@ -50,33 +50,5 @@ void main() { .build(); v1.config.decisionManager = fakeApi; expect(v1.getConsent(), false); - // ignore: deprecated_member_use_from_same_package - await v1.fetchFlags().then((value) { - // expect(Flagship.getStatus(), FSSdkStatus.SDK_INITIALIZED); - - // /// Activate - // // ignore: deprecated_member_use_from_same_package - // v1.activateModification("key"); - - // /// Get Modification - // // ignore: deprecated_member_use_from_same_package - // expect(v1.getModification('key_A', 'default'), "val_A"); - - // /// Get infos - // // ignore: deprecated_member_use_from_same_package - // var infos = v1.getModificationInfo('alias'); - // expect(infos?.length, 9); - // expect(infos!['campaignId'], "bsffhle242b2l3igq4dg"); - // expect(infos['variationGroupId'], "bsffhle242b2l3igq4egaa"); - // expect(infos['variationId'], "bsffhle242b2l3igq4f0"); - // expect(infos['isReference'], true); - - /// Send hit - v1.sendHit( - Event(action: "action", category: EventCategory.Action_Tracking)); - - /// Send consent hit - v1.sendHit(Consent(hasConsented: false)); - }); }); } diff --git a/test/panic_test.dart b/test/panic_test.dart index 3b6e58e..717bfce 100644 --- a/test/panic_test.dart +++ b/test/panic_test.dart @@ -2,12 +2,15 @@ import 'package:flagship/decision/api_manager.dart'; import 'package:flagship/flagship.dart'; import 'package:flagship/flagship_version.dart'; import 'package:flagship/status.dart'; +import 'package:flagship/tracking/tracking_manager.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'fake_path_provider_platform.dart'; import 'service_test.mocks.dart'; import 'package:flagship/api/service.dart'; import 'package:flagship/flagship_config.dart'; @@ -16,16 +19,11 @@ import 'test_tools.dart'; @GenerateMocks([Service]) void main() { + PathProviderPlatform.instance = FakePathProviderPlatform(); ToolsTest.sqfliteTestInit(); WidgetsFlutterBinding.ensureInitialized(); SharedPreferences.setMockInitialValues({}); Flagship.start("bkk9glocmjcg0vtmdlrr", "apiKey"); - Map fsHeaders = { - "x-api-key": "apiKey", - "x-sdk-client": "flutter", - "x-sdk-version": FlagshipVersion, - "Content-type": "application/json" - }; MockService fakePanicService = MockService(); ApiManager fakePanicApi = ApiManager(fakePanicService); @@ -33,12 +31,12 @@ void main() { String fakeResponse = await ToolsTest.readFile('test_resources/decisionApiPanic.json') ?? ""; when(fakePanicService.sendHttpRequest( - RequestType.Post, - 'https://decision.flagship.io/v2/bkk9glocmjcg0vtmdlrr/campaigns/?exposeAllKeys=true&extras[]=accountSettings', - fsHeaders, - any, - timeoutMs: TIMEOUT)) - .thenAnswer((_) async { + RequestType.Post, + 'https://decision.flagship.io/v2/bkk9glocmjcg0vtmdlrr/campaigns/?exposeAllKeys=true&extras[]=accountSettings', + any, + any, + timeoutMs: TIMEOUT, + )).thenAnswer((_) async { return http.Response(fakeResponse, 200); }); diff --git a/test/tracking_manager_test.dart b/test/tracking_manager_test.dart index 601efc2..3f8296a 100644 --- a/test/tracking_manager_test.dart +++ b/test/tracking_manager_test.dart @@ -85,6 +85,8 @@ Future main() async { mockFlag = vMock.getFlag("key_A"); var mockVal = mockFlag.value("defaultValue", visitorExposed: false); expect(mockVal, "val_A"); + vMock.sessionDuration = + DateTime.now().subtract(const Duration(minutes: 35)); await mockFlag.visitorExposed(); } @@ -105,7 +107,10 @@ Future main() async { mockFlagBis = vMock.getFlag("key_A"); var mockValBis = mockFlagBis.value("defaultValue", visitorExposed: false); expect(mockValBis, "val_A"); + vMock.sessionDuration = + DateTime.now().subtract(const Duration(minutes: 35)); await mockFlagBis.visitorExposed(); + // After sucess the pool should be empty expect( (vMock.trackingManager as TrackingManageContinuousStrategy)