From 5913f279bc4acb3597b45822027ff890540632bb Mon Sep 17 00:00:00 2001 From: Muzahidul Islam <129880873+muzahidul-opti@users.noreply.github.com> Date: Fri, 3 Oct 2025 21:18:23 +0600 Subject: [PATCH 1/7] [FSSDK-11875] chore: add support for android 15 (#84) * build: update Android build configurations - Bump compile SDK version to 35 and build tools version to "35.0.0" - Update target SDK version to 35 - Add namespace 'com.optimizely.optimizely_flutter_sdk' - Enable buildConfig feature - Update kotlin-stdlib-jdk7 to version 2.1.0 - Update jackson-databind to version 2.17.2 - Set minSdkVersion and targetSdkVersion to 35 for example app - Update distributionUrl in gradle-wrapper.properties to Gradle 8.10.2 - Update android plugin version to 8.7.0 and kotlin-android version to 2.1.0 * build: update Android SDK versions - Update compile SDK version to 36 and build tools version to "36.0.0" - Change min SDK version to 21 and target SDK version to 36 in the Android build configuration - Update compile SDK version to 36 and target SDK version to 36 in the example app build.gradle file * fix: update android build configuration - Update compile_sdk_version and build_tools_version to 35 - Remove target_sdk_version from ext - Update compileSdkVersion and targetSdkVersion in app/build.gradle to 35 * refactor: update compile and target SDK versions - Update compile SDK version to use project property if available, fallback to default - Update target SDK version to use project property if available, fallback to default * fix: remove conditional expressions for compileSdkVersion and targetSdkVersion - Simplify specifying compileSdkVersion and targetSdkVersion directly from flutter configuration --- android/build.gradle | 19 +++++++++++-------- example/android/app/build.gradle | 10 ++++++---- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 4 ++-- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 5d4e3ff..c33dcee 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -24,19 +24,22 @@ rootProject.allprojects { ext { - compile_sdk_version = 32 - build_tools_version = "30.0.3" + compile_sdk_version = 35 min_sdk_version = 21 - target_sdk_version = 29 } android { - compileSdkVersion compile_sdk_version - buildToolsVersion build_tools_version + namespace 'com.optimizely.optimizely_flutter_sdk' + compileSdkVersion rootProject.hasProperty('flutter.compileSdkVersion') + ? rootProject.flutter.compileSdkVersion.toInteger() + : compile_sdk_version + + buildFeatures { + buildConfig true + } defaultConfig { minSdkVersion min_sdk_version - targetSdkVersion target_sdk_version versionCode 1 versionName version_name buildConfigField "String", "CLIENT_VERSION", "\"$version_name\"" @@ -73,9 +76,9 @@ dependencies { implementation 'com.github.tony19:logback-android:3.0.0' implementation 'org.slf4j:slf4j-api:2.0.7' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0" implementation "com.optimizely.ab:android-sdk:5.0.1" - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2' implementation ('com.google.guava:guava:19.0') { exclude group:'com.google.guava', module:'listenablefuture' } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 415ec79..bd22a12 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -24,8 +24,9 @@ if (flutterVersionName == null) { } android { - compileSdkVersion 32 - ndkVersion flutter.ndkVersion + namespace "com.optimizely.optimizely_flutter_sdk_example" + + compileSdkVersion flutter.compileSdkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -37,8 +38,9 @@ android { applicationId "com.optimizely.optimizely_flutter_sdk_example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion 21 - targetSdkVersion 32 + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index cc5527d..db18181 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 5710b01..97284d6 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -26,8 +26,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.2.1" apply false - id "org.jetbrains.kotlin.android" version "1.6.10" apply false + id "com.android.application" version "8.7.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" From 3970033dfb034dfe8ced878e6d166cac2bc2c1c0 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam <129880873+muzahidul-opti@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:50:36 +0600 Subject: [PATCH 2/7] [FSSDK-11888] fix: disable code minify to support R8 compatibility (#87) * build: update minifyEnabled setting in release build - Change minifyEnabled from true to false in the release buildType - Update the ProGuard configuration to exclude javax.mail and javax.activation. classes * chore: update ProGuard rules - Keep necessary classes for Optimizely and Jackson libraries - Keep Logback classes - Remove unnecessary warnings for javax.mail classes - Add ProGuard configuration to build.gradle for minification - Update proguard-rules.txt in app folder * build: enable resource shrinking in release build - Add configuration to shrink resources for reducing APK size * chore: update build configuration - Remove 'minifyEnabled' setting from release build type in android/build.gradle - Exclude javax.mail and javax.activation classes in proguard configuration - Delete deprecated mail classes warning in proguard-rules.txt --- android/build.gradle | 8 -------- android/proguard-rules.txt | 6 ++++++ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index c33dcee..7d1edec 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -56,14 +56,6 @@ android { packagingOptions { exclude 'androidsupportmultidexversion.txt' } - - buildTypes { - release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - } dependencies { diff --git a/android/proguard-rules.txt b/android/proguard-rules.txt index 1564ced..e9d6827 100644 --- a/android/proguard-rules.txt +++ b/android/proguard-rules.txt @@ -12,4 +12,10 @@ -keep class com.fasterxml.jackson.** {*;} # Logback -keep class ch.qos.** { *; } + +# Mail classes (Logback SMTP appender) +-dontwarn javax.mail.** +-dontwarn javax.mail.internet.** +-dontwarn javax.activation.** + ##---------------End: proguard configuration ---------- From 34d2a3ba36ab343fec55f62ea42414659f608b1e Mon Sep 17 00:00:00 2001 From: Muzahidul Islam <129880873+muzahidul-opti@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:02:42 +0600 Subject: [PATCH 3/7] chore: prepare for release 3.1.0 (#85) * feat: add Android 15 support and update versions - Update AGP version to 8.7.0 - Update gradle version to 8.10.2 - Update kotlin version to 2.1.0 - Update SDK version to 3.1.0 in README.md, package_info.dart, and pubspec.yaml * feat: add Android 15 support and update dependencies - Add Android 15 support - Update AGP version to 8.7.0 - Update gradle version to 8.10.2 - Update kotlin version to 2.1.0 * Update date --- CHANGELOG.md | 9 +++++++++ README.md | 2 +- lib/package_info.dart | 2 +- pubspec.yaml | 4 ++-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e530c5..bf7e8a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Optimizely Flutter SDK Changelog +## 3.1.0 +October 9th, 2025 + +This minor release added the following support: +* Android 15 support ([#84](https://github.com/optimizely/optimizely-flutter-sdk/pull/84)) +* Update AGP version to 8.7.0 +* Update gradle version to 8.10.2 +* Update kotlin version to 2.1.0 + ## 3.0.1 Jun 4th, 2025 diff --git a/README.md b/README.md index 1b125d5..cdf6f10 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Other Flutter platforms are not currently supported by this SDK. To add the flutter-sdk to your project dependencies, include the following in your app's pubspec.yaml: ``` - optimizely_flutter_sdk: ^3.0.1 + optimizely_flutter_sdk: ^3.1.0 ``` Then run diff --git a/lib/package_info.dart b/lib/package_info.dart index 0bdd780..6c8b054 100644 --- a/lib/package_info.dart +++ b/lib/package_info.dart @@ -3,5 +3,5 @@ class PackageInfo { static const String name = 'optimizely_flutter_sdk'; - static const String version = '3.0.1'; + static const String version = '3.1.0'; } diff --git a/pubspec.yaml b/pubspec.yaml index 188939d..83c113e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: optimizely_flutter_sdk description: This repository houses the Flutter SDK for use with Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts. -version: 3.0.1 +version: 3.1.0 homepage: https://github.com/optimizely/optimizely-flutter-sdk environment: - sdk: '>=2.16.2 <4.0.0' + sdk: ">=2.16.2 <4.0.0" flutter: ">=2.5.0" dependencies: From c47b746a5d9e66f874015b5b55e3a29a952b6c77 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam <129880873+muzahidul-opti@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:13:04 +0600 Subject: [PATCH 4/7] [FSSDK-11853] add swift logger support (#88) * build: add Optimizely SDK logger classes - Add FlutterOptimizelyLogger class implementing OPTLogger protocol - Add constants for customLogger and loggerChannel - Implement FlutterMethodChannel for invoking log method - Add logger field to OptimizelyFlutterSdk constructor - Initialize custom logger in OptimizelyClientWrapper - Create LoggerBridge to handle log method calls - Implement OptimizelyLogger interface and DefaultOptimizelyLogger class * fix: resolve logging inconsistencies - Update custom logger initialization in OptimizelyFlutterSdkPlugin - Adjust logger channel setup in onDetachedFromEngine - Refactor LoggerBridge to handle log calls from native Swift/Java code properly - Ensure proper logging when no custom logger is set * refactor: update logger imports - Rename 'OptimizelyLogger.dart' to 'flutter_logger.dart' - Rename 'LoggerBridge.dart' to 'logger_bridge.dart' - Modify imports in 'optimizely_client_wrapper.dart' to reflect changes in logger files * feat: add custom logger implementation - Implement a custom logger class - Define logLevel property with default debug level - Define log method to print log messages with custom format * refactor: rename logger classes in Android and iOS - Rename FlutterOptimizelyLogger to OptimizelyFlutterLogger in Android - Rename FlutterOptimizelyLogger to OptimizelyFlutterLogger in iOS * feat: update logging behavior for Optimizely SDK - Remove unused log level property in CustomLogger - Implement channel setter method in OptimizelyFlutterLogger - Add guard clauses for levels and logger channel availability in log method - Update channel invocation to happen on main thread - Set log level in SwiftOptimizelyFlutterSdkPlugin based on parameters - Simplify DefaultOptimizelyLogger log method - Update log message formatting in logger bridge for consistency * feat: add methods and tests for logger state management - Add methods to expose converting log level, checking if a custom logger is set, retrieving the current logger, and resetting the logger state - Implement a method for simulating method calls - Add tests for maintaining logger state across multiple operations - Include a test for handling logger replacement - Create tests for edge cases including handling empty messages and special characters * feat: add separate logger channel for outgoing log calls - Define LOGGER_CHANNEL constant for OptimizelyFlutterLogger class - Set up separate FlutterMethodChannel for outgoing log calls - Update SwiftOptimizelyFlutterSdkPlugin to use the new logger channel * refactor: improve main thread dispatch for Flutter method channel calls - Add DispatchQueue.main.async for each method call to ensure platform channel messages are sent on the correct thread * chore: clean up logger implementation - Remove unnecessary comments and TODOs - Replace direct print statements with AppLogger methods - Refactor AppLogger to enhance flexibility and ease of use * style: update comment in sendLogToFlutter method - Remove comparison to Swift's DispatchQueue.main.async * chore: remove unused import statement - Remove import statement for 'log_level' that is no longer used - Update import paths for 'flutter_logger' and 'optimizely_flutter_sdk' test: update test cases in logger_test.dart - Update test cases to use 'const MethodCall' for creating method calls - Change null and empty arguments to be created and handled correctly - Fix missing level or message argument handling scenarios - Improve handling of invalid level data types in method calls docs: add comments to improve code clarity in logger_test.dart - Add comments explaining the purpose of each test group and case - Include comments for the different scenarios being tested in each case * chore: update log messages and method channel handling - Update log message format in CustomLogger class - Refactor main thread dispatch logic for method channel calls in Utils.swift - Enhance error handling and main thread dispatch in OptimizelyFlutterLogger.swift - Modify method channel creation and task queue handling in SwiftOptimizelyFlutterSdkPlugin.swift * refactor: enhance logging functionalities - Replace usage of static AppLogger class with separate logging functions - Introduce individual logger functions for error, warning, info, and debug levels - Create a default stand-alone logger instance to handle logging operations * test: add global logging functions test cases - Test calling global logging functions without errors - Test handling empty messages in global functions - Test handling special characters in global functions - Test handling rapid calls to global functions * chore: remove custom logger functionality - Remove code related to setting up and using a custom logger in OptimizelyFlutterClient.java - Delete OptimizelyFlutterLogger.java file as it is no longer needed - Update references to the logger channel in OptimizelyFlutterSdkPlugin.java * refactor: rename customLogger to useCustomLogger - Update 'Constants.swift' to change 'customLogger' to 'useCustomLogger' - Update 'SwiftOptimizelyFlutterSdkPlugin.swift' to change 'customLogger' to 'useCustomLogger' - Update 'optimizely_flutter_sdk.dart' to change 'logger' parameter to 'OptimizelyLogger' - Update 'optimizely_client_wrapper.dart' to change 'customLogger' to 'useCustomLogger' - Update 'constants.dart' to change 'customLogger' to 'useCustomLogger' * clean up --- .../OptimizelyFlutterClient.java | 2 +- example/lib/custom_logger.dart | 11 + example/lib/main.dart | 9 +- ios/Classes/HelperClasses/Constants.swift | 1 + ios/Classes/OptimizelyFlutterLogger.swift | 39 ++ .../SwiftOptimizelyFlutterSdkPlugin.swift | 19 + lib/optimizely_flutter_sdk.dart | 51 +- lib/src/logger/flutter_logger.dart | 26 + lib/src/logger/logger_bridge.dart | 98 ++++ lib/src/optimizely_client_wrapper.dart | 4 +- lib/src/utils/constants.dart | 1 + test/logger_test.dart | 506 ++++++++++++++++++ 12 files changed, 749 insertions(+), 18 deletions(-) create mode 100644 example/lib/custom_logger.dart create mode 100644 ios/Classes/OptimizelyFlutterLogger.swift create mode 100644 lib/src/logger/flutter_logger.dart create mode 100644 lib/src/logger/logger_bridge.dart create mode 100644 test/logger_test.dart diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java index cf85e3d..a4f6ce4 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java @@ -25,7 +25,6 @@ import com.optimizely.ab.UnknownEventTypeException; import com.optimizely.ab.android.event_handler.DefaultEventHandler; import com.optimizely.ab.android.sdk.OptimizelyClient; - import java.util.HashMap; import java.util.Map; @@ -187,6 +186,7 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N if (enableVuid) { optimizelyManagerBuilder.withVuidEnabled(); } + OptimizelyManager optimizelyManager = optimizelyManagerBuilder.build(context); optimizelyManager.initialize(context, null, (OptimizelyClient client) -> { diff --git a/example/lib/custom_logger.dart b/example/lib/custom_logger.dart new file mode 100644 index 0000000..e031fff --- /dev/null +++ b/example/lib/custom_logger.dart @@ -0,0 +1,11 @@ +import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; +import 'package:flutter/foundation.dart'; + +class CustomLogger implements OptimizelyLogger { + @override + void log(OptimizelyLogLevel level, String message) { + if (kDebugMode) { + print('[OPTIMIZELY] ${level.name.toUpperCase()}: $message'); + } + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index e7db8fa..9b8f1eb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'dart:async'; import 'dart:math'; import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; +import 'package:optimizely_flutter_sdk_example/custom_logger.dart'; void main() { runApp(const MyApp()); @@ -28,16 +29,20 @@ class _MyAppState extends State { OptimizelyDecideOption.includeReasons, OptimizelyDecideOption.excludeVariables }; + final customLogger = CustomLogger(); + var flutterSDK = OptimizelyFlutterSdk("X9mZd2WDywaUL9hZXyh9A", datafilePeriodicDownloadInterval: 10 * 60, eventOptions: const EventOptions( batchSize: 1, timeInterval: 60, maxQueueSize: 10000), defaultLogLevel: OptimizelyLogLevel.debug, - defaultDecideOptions: defaultOptions); + defaultDecideOptions: defaultOptions, + logger: customLogger, + ); var response = await flutterSDK.initializeClient(); setState(() { - uiResponse = "Optimizely Client initialized: ${response.success} "; + uiResponse = "[Optimizely] Client initialized: ${response.success} "; }); var rng = Random(); diff --git a/ios/Classes/HelperClasses/Constants.swift b/ios/Classes/HelperClasses/Constants.swift index a29370a..1b0cbac 100644 --- a/ios/Classes/HelperClasses/Constants.swift +++ b/ios/Classes/HelperClasses/Constants.swift @@ -91,6 +91,7 @@ struct RequestParameterKey { static let reasons = "reasons" static let decideOptions = "optimizelyDecideOption" static let defaultLogLevel = "defaultLogLevel" + static let useCustomLogger = "useCustomLogger" static let eventBatchSize = "eventBatchSize" static let eventTimeInterval = "eventTimeInterval" static let eventMaxQueueSize = "eventMaxQueueSize" diff --git a/ios/Classes/OptimizelyFlutterLogger.swift b/ios/Classes/OptimizelyFlutterLogger.swift new file mode 100644 index 0000000..7b9217a --- /dev/null +++ b/ios/Classes/OptimizelyFlutterLogger.swift @@ -0,0 +1,39 @@ +import Flutter +import Optimizely + +public class OptimizelyFlutterLogger: NSObject, OPTLogger { + static var LOGGER_CHANNEL: String = "optimizely_flutter_sdk_logger"; + + public static var logLevel: OptimizelyLogLevel = .info + + private static var loggerChannel: FlutterMethodChannel? + + public required override init() { + super.init() + } + + public static func setChannel(_ channel: FlutterMethodChannel) { + loggerChannel = channel + } + + public func log(level: OptimizelyLogLevel, message: String) { + // Early return if level check fails + guard level.rawValue <= OptimizelyFlutterLogger.logLevel.rawValue else { + return + } + + // Ensure we have a valid channel + guard let channel = Self.loggerChannel else { + print("[OptimizelyFlutterLogger] ERROR: No logger channel available!") + return + } + + // https://docs.flutter.dev/platform-integration/platform-channels#jumping-to-the-main-thread-in-ios + DispatchQueue.main.async { + channel.invokeMethod("log", arguments: [ + "level": level.rawValue, + "message": message + ]) + } + } +} diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index 7c093c4..75ba8b9 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -41,6 +41,14 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { channel = FlutterMethodChannel(name: "optimizely_flutter_sdk", binaryMessenger: registrar.messenger()) let instance = SwiftOptimizelyFlutterSdkPlugin() registrar.addMethodCallDelegate(instance, channel: channel) + + // Separate logger channel for outgoing log calls + let taskQueue = registrar.messenger().makeBackgroundTaskQueue?() + let loggerChannel = FlutterMethodChannel(name: OptimizelyFlutterLogger.LOGGER_CHANNEL, + binaryMessenger: registrar.messenger(), + codec: FlutterStandardMethodCodec.sharedInstance(), + taskQueue: taskQueue) + OptimizelyFlutterLogger.setChannel(loggerChannel) } /// Part of FlutterPlugin protocol to handle communication with flutter sdk @@ -110,6 +118,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { var defaultLogLevel = OptimizelyLogLevel.info if let logLevel = parameters[RequestParameterKey.defaultLogLevel] as? String { defaultLogLevel = Utils.getDefaultLogLevel(logLevel) + OptimizelyFlutterLogger.logLevel = defaultLogLevel } // SDK Settings Default Values @@ -163,9 +172,19 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { notificationIdsTracker.removeValue(forKey: sdkKey) optimizelyClientsTracker.removeValue(forKey: sdkKey) + // Check if custom logger is requested + var logger: OPTLogger? + if let useCustomLogger = parameters[RequestParameterKey.useCustomLogger] as? Bool, useCustomLogger { + // OptimizelyFlutterLogger bridges iOS logs to Flutter via Method Channel + // When useCustomLogger = true: + // iOS SDK log β†’ OptimizelyFlutterLogger β†’ Flutter Method Channel β†’ Flutter console + logger = OptimizelyFlutterLogger() + } + // Creating new instance let optimizelyInstance = OptimizelyClient( sdkKey:sdkKey, + logger:logger, eventDispatcher: eventDispatcher, datafileHandler: datafileHandler, periodicDownloadInterval: datafilePeriodicDownloadInterval, diff --git a/lib/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index 51dc9af..a1ff583 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -28,6 +28,8 @@ import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_respon import 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart'; import 'package:optimizely_flutter_sdk/src/user_context/optimizely_user_context.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; +import 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart'; +import 'package:optimizely_flutter_sdk/src/logger/logger_bridge.dart'; export 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart' show ClientPlatform, ListenerType; @@ -53,6 +55,8 @@ export 'package:optimizely_flutter_sdk/src/data_objects/datafile_options.dart' show DatafileHostOptions; export 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart' show OptimizelyLogLevel; +export 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart' + show OptimizelyLogger; /// The main client class for the Optimizely Flutter SDK. /// @@ -68,20 +72,37 @@ class OptimizelyFlutterSdk { final Set _defaultDecideOptions; final OptimizelyLogLevel _defaultLogLevel; final SDKSettings _sdkSettings; + static OptimizelyLogger? _customLogger; + /// Set a custom logger for the SDK + static void setLogger(OptimizelyLogger logger) { + _customLogger = logger; + LoggerBridge.initialize(logger); + } + /// Get the current logger + static OptimizelyLogger? get logger { + return _customLogger; + } OptimizelyFlutterSdk(this._sdkKey, - {EventOptions eventOptions = const EventOptions(), - int datafilePeriodicDownloadInterval = - 10 * 60, // Default time interval in seconds - Map datafileHostOptions = const {}, - Set defaultDecideOptions = const {}, - OptimizelyLogLevel defaultLogLevel = OptimizelyLogLevel.info, - SDKSettings sdkSettings = const SDKSettings()}) - : _eventOptions = eventOptions, - _datafilePeriodicDownloadInterval = datafilePeriodicDownloadInterval, - _datafileHostOptions = datafileHostOptions, - _defaultDecideOptions = defaultDecideOptions, - _defaultLogLevel = defaultLogLevel, - _sdkSettings = sdkSettings; + {EventOptions eventOptions = const EventOptions(), + int datafilePeriodicDownloadInterval = 10 * 60, + Map datafileHostOptions = const {}, + Set defaultDecideOptions = const {}, + OptimizelyLogLevel defaultLogLevel = OptimizelyLogLevel.info, + SDKSettings sdkSettings = const SDKSettings(), + OptimizelyLogger? logger}) + : _eventOptions = eventOptions, + _datafilePeriodicDownloadInterval = datafilePeriodicDownloadInterval, + _datafileHostOptions = datafileHostOptions, + _defaultDecideOptions = defaultDecideOptions, + _defaultLogLevel = defaultLogLevel, + _sdkSettings = sdkSettings { + // Set the logger if provided + if (logger != null) { + setLogger(logger); + } else { + logWarning("Logger not provided."); + } + } /// Starts Optimizely SDK (Synchronous) with provided sdkKey. Future initializeClient() async { @@ -92,7 +113,9 @@ class OptimizelyFlutterSdk { _datafileHostOptions, _defaultDecideOptions, _defaultLogLevel, - _sdkSettings); + _sdkSettings, + _customLogger + ); } /// Use the activate method to start an experiment. diff --git a/lib/src/logger/flutter_logger.dart b/lib/src/logger/flutter_logger.dart new file mode 100644 index 0000000..ad3ec67 --- /dev/null +++ b/lib/src/logger/flutter_logger.dart @@ -0,0 +1,26 @@ +import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; + +abstract class OptimizelyLogger { + /// Log a message at a certain level + void log(OptimizelyLogLevel level, String message); +} + +class DefaultOptimizelyLogger implements OptimizelyLogger { + @override + void log(OptimizelyLogLevel level, String message) { + print('[OPTIMIZELY] [${level.name.toUpperCase()}]: $message'); + } +} + +/// App logger instance +final _appLogger = DefaultOptimizelyLogger(); + +/// App logging functions +void logError(String message) => + _appLogger.log(OptimizelyLogLevel.error, message); +void logWarning(String message) => + _appLogger.log(OptimizelyLogLevel.warning, message); +void logInfo(String message) => + _appLogger.log(OptimizelyLogLevel.info, message); +void logDebug(String message) => + _appLogger.log(OptimizelyLogLevel.debug, message); diff --git a/lib/src/logger/logger_bridge.dart b/lib/src/logger/logger_bridge.dart new file mode 100644 index 0000000..2e2271a --- /dev/null +++ b/lib/src/logger/logger_bridge.dart @@ -0,0 +1,98 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart'; +import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; + +class LoggerBridge { + static const MethodChannel _loggerChannel = + MethodChannel('optimizely_flutter_sdk_logger'); + static OptimizelyLogger? _customLogger; + + /// Initialize the logger bridge to receive calls from native + static void initialize(OptimizelyLogger? logger) { + logInfo('[LoggerBridge] Initializing with logger: ${logger != null}'); + _customLogger = logger; + _loggerChannel.setMethodCallHandler(_handleMethodCall); + } + + /// Handle incoming method calls from native Swift/Java code + static Future _handleMethodCall(MethodCall call) async { + try { + switch (call.method) { + case 'log': + await _handleLogCall(call); + break; + default: + logWarning('[LoggerBridge] Unknown method call: ${call.method}'); + } + } catch (e) { + logError('[LoggerBridge] Error handling method call: $e'); + } + } + + /// Process the log call from Swift/Java + static Future _handleLogCall(MethodCall call) async { + try { + final args = Map.from(call.arguments ?? {}); + + final levelRawValue = args['level'] as int?; + final message = args['message'] as String?; + + if (levelRawValue == null || message == null) { + logError('[LoggerBridge] Warning: Missing level or message in log call'); + return; + } + + final level = _convertLogLevel(levelRawValue); + + if (_customLogger != null) { + _customLogger!.log(level, message); + } else { + logInfo('[Optimizely ${level.name}] $message'); + } + } catch (e) { + logError('[LoggerBridge] Error processing log call: $e'); + } + } + + /// Convert native log level to Flutter enum + static OptimizelyLogLevel _convertLogLevel(int rawValue) { + switch (rawValue) { + case 1: + return OptimizelyLogLevel.error; + case 2: + return OptimizelyLogLevel.warning; + case 3: + return OptimizelyLogLevel.info; + case 4: + return OptimizelyLogLevel.debug; + default: + return OptimizelyLogLevel.info; + } + } + + /// Expose convertLogLevel + static OptimizelyLogLevel convertLogLevel(int rawValue) { + return _convertLogLevel(rawValue); + } + + /// Check if a custom logger is set + static bool hasLogger() { + return _customLogger != null; + } + + /// Get the current logger + static OptimizelyLogger? getCurrentLogger() { + return _customLogger; + } + + /// Reset logger state + static void reset() { + _customLogger = null; + } + + /// Simulate method calls + static Future handleMethodCallForTesting(MethodCall call) async { + await _handleMethodCall(call); + } +} diff --git a/lib/src/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index a0869b9..8caa22c 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -63,7 +63,8 @@ class OptimizelyClientWrapper { Map datafileHostOptions, Set defaultDecideOptions, OptimizelyLogLevel defaultLogLevel, - SDKSettings sdkSettings) async { + SDKSettings sdkSettings, + OptimizelyLogger? logger) async { _channel.setMethodCallHandler(methodCallHandler); final convertedOptions = Utils.convertDecideOptions(defaultDecideOptions); final convertedLogLevel = Utils.convertLogLevel(defaultLogLevel); @@ -79,6 +80,7 @@ class OptimizelyClientWrapper { Constants.eventBatchSize: eventOptions.batchSize, Constants.eventTimeInterval: eventOptions.timeInterval, Constants.eventMaxQueueSize: eventOptions.maxQueueSize, + Constants.useCustomLogger: logger != null, }; // Odp Request params diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 2bb5421..8a1586e 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -86,6 +86,7 @@ class Constants { static const String optimizelyDecideOption = "optimizelyDecideOption"; static const String optimizelySegmentOption = "optimizelySegmentOption"; static const String optimizelySdkSettings = "optimizelySdkSettings"; + static const String useCustomLogger = 'useCustomLogger'; static const String defaultLogLevel = "defaultLogLevel"; static const String payload = "payload"; static const String value = "value"; diff --git a/test/logger_test.dart b/test/logger_test.dart new file mode 100644 index 0000000..8c0161e --- /dev/null +++ b/test/logger_test.dart @@ -0,0 +1,506 @@ +import "package:flutter/services.dart"; +import "package:flutter_test/flutter_test.dart"; +import 'package:optimizely_flutter_sdk/src/logger/logger_bridge.dart'; +import 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; + +/// Test implementation of OptimizelyLogger for testing +class TestLogger implements OptimizelyLogger { + final List logs = []; + + @override + void log(OptimizelyLogLevel level, String message) { + logs.add(LogEntry(level, message)); + } + + void clear() { + logs.clear(); + } +} + +/// Data class for log entries +class LogEntry { + final OptimizelyLogLevel level; + final String message; + + LogEntry(this.level, this.message); + + @override + String toString() => '${level.name}: $message'; +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group("Logger Tests", () { + setUp(() { + // Reset logger state before each test + LoggerBridge.reset(); + }); + + test("should handle log method call from native", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + // Simulate native log call by directly invoking the method handler + final methodCall = const MethodCall('log', { + 'level': 3, // INFO + 'message': 'Test log message from native' + }); + + await LoggerBridge.handleMethodCallForTesting(methodCall); + + expect(testLogger.logs.length, equals(1)); + expect(testLogger.logs.first.level, equals(OptimizelyLogLevel.info)); + expect(testLogger.logs.first.message, equals('Test log message from native')); + }); + + test("should convert log levels correctly", () { + expect(LoggerBridge.convertLogLevel(1), equals(OptimizelyLogLevel.error)); + expect(LoggerBridge.convertLogLevel(2), equals(OptimizelyLogLevel.warning)); + expect(LoggerBridge.convertLogLevel(3), equals(OptimizelyLogLevel.info)); + expect(LoggerBridge.convertLogLevel(4), equals(OptimizelyLogLevel.debug)); + }); + + test("should default to info for invalid log levels", () { + expect(LoggerBridge.convertLogLevel(0), equals(OptimizelyLogLevel.info)); + expect(LoggerBridge.convertLogLevel(5), equals(OptimizelyLogLevel.info)); + expect(LoggerBridge.convertLogLevel(-1), equals(OptimizelyLogLevel.info)); + }); + + test("should reset state correctly", () { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + expect(LoggerBridge.hasLogger(), isTrue); + + LoggerBridge.reset(); + + expect(LoggerBridge.hasLogger(), isFalse); + expect(LoggerBridge.getCurrentLogger(), isNull); + }); + + group("Error Handling", () { + test("should handle null arguments gracefully", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + final methodCall = const MethodCall('log', null); + await LoggerBridge.handleMethodCallForTesting(methodCall); + + expect(testLogger.logs.isEmpty, isTrue); + }); + + test("should handle empty arguments gracefully", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + final methodCall = const MethodCall('log', {}); + await LoggerBridge.handleMethodCallForTesting(methodCall); + + expect(testLogger.logs.isEmpty, isTrue); + }); + + test("should handle missing level argument", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + final methodCall = const MethodCall('log', { + 'message': 'Test message without level' + }); + await LoggerBridge.handleMethodCallForTesting(methodCall); + + expect(testLogger.logs.isEmpty, isTrue); + }); + + test("should handle missing message argument", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + final methodCall = const MethodCall('log', { + 'level': 3 + }); + await LoggerBridge.handleMethodCallForTesting(methodCall); + + expect(testLogger.logs.isEmpty, isTrue); + }); + + test("should handle invalid level data types", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + // Test with string level + var methodCall = const MethodCall('log', { + 'level': 'invalid', + 'message': 'Test message' + }); + await LoggerBridge.handleMethodCallForTesting(methodCall); + + expect(testLogger.logs.isEmpty, isTrue); + + // Test with null level + methodCall = const MethodCall('log', { + 'level': null, + 'message': 'Test message' + }); + await LoggerBridge.handleMethodCallForTesting(methodCall); + + expect(testLogger.logs.isEmpty, isTrue); + + testLogger.clear(); + }); + + test("should handle unknown method calls", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + final methodCall = const MethodCall('unknownMethod', { + 'level': 3, + 'message': 'Test message' + }); + + // Should not throw + expect(() async { + await LoggerBridge.handleMethodCallForTesting(methodCall); + }, returnsNormally); + + expect(testLogger.logs.isEmpty, isTrue); + }); + }); + + group("Multiple Log Levels", () { + test("should handle all log levels in sequence", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + final testCases = [ + {'level': 1, 'message': 'Error message', 'expected': OptimizelyLogLevel.error}, + {'level': 2, 'message': 'Warning message', 'expected': OptimizelyLogLevel.warning}, + {'level': 3, 'message': 'Info message', 'expected': OptimizelyLogLevel.info}, + {'level': 4, 'message': 'Debug message', 'expected': OptimizelyLogLevel.debug}, + ]; + + for (var testCase in testCases) { + final methodCall = MethodCall('log', { + 'level': testCase['level'], + 'message': testCase['message'] + }); + + await LoggerBridge.handleMethodCallForTesting(methodCall); + } + + expect(testLogger.logs.length, equals(4)); + + for (int i = 0; i < testCases.length; i++) { + expect(testLogger.logs[i].level, equals(testCases[i]['expected'])); + expect(testLogger.logs[i].message, equals(testCases[i]['message'])); + } + }); + + test("should handle mixed valid and invalid log levels", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + final testCases = [ + {'level': 1, 'message': 'Valid error', 'shouldLog': true}, + {'level': 'invalid', 'message': 'Invalid level', 'shouldLog': false}, + {'level': 3, 'message': 'Valid info', 'shouldLog': true}, + {'level': 999, 'message': 'Out of range level', 'shouldLog': true}, // Maps to info + {'level': -1, 'message': 'Negative level', 'shouldLog': true}, // Maps to info + ]; + + for (var testCase in testCases) { + final methodCall = MethodCall('log', { + 'level': testCase['level'], + 'message': testCase['message'] + }); + + await LoggerBridge.handleMethodCallForTesting(methodCall); + } + + // Should have 4 logs (all except the 'invalid' string level) + expect(testLogger.logs.length, equals(4)); + expect(testLogger.logs[0].level, equals(OptimizelyLogLevel.error)); + expect(testLogger.logs[1].level, equals(OptimizelyLogLevel.info)); + expect(testLogger.logs[2].level, equals(OptimizelyLogLevel.info)); // 999 maps to info + expect(testLogger.logs[3].level, equals(OptimizelyLogLevel.info)); // -1 maps to info + }); + }); + + group("DefaultOptimizelyLogger", () { + test("should create default logger instance", () { + var defaultLogger = DefaultOptimizelyLogger(); + expect(defaultLogger, isNotNull); + }); + + test("should handle logging without throwing", () { + var defaultLogger = DefaultOptimizelyLogger(); + + expect(() { + defaultLogger.log(OptimizelyLogLevel.error, "Error message"); + defaultLogger.log(OptimizelyLogLevel.warning, "Warning message"); + defaultLogger.log(OptimizelyLogLevel.info, "Info message"); + defaultLogger.log(OptimizelyLogLevel.debug, "Debug message"); + }, returnsNormally); + }); + }); + group("Global Logging Functions", () { + test("should call global logging functions without error", () { + expect(() { + logError("Global error message"); + logWarning("Global warning message"); + logInfo("Global info message"); + logDebug("Global debug message"); + }, returnsNormally); + }); + + test("should handle empty messages in global functions", () { + expect(() { + logError(""); + logWarning(""); + logInfo(""); + logDebug(""); + }, returnsNormally); + }); + + test("should handle special characters in global functions", () { + var specialMessage = "Special: πŸš€ \n\t δΈ–η•Œ"; + + expect(() { + logError(specialMessage); + logWarning(specialMessage); + logInfo(specialMessage); + logDebug(specialMessage); + }, returnsNormally); + }); + + test("should handle rapid calls to global functions", () { + expect(() { + for (int i = 0; i < 25; i++) { + logError("Rapid error $i"); + logWarning("Rapid warning $i"); + logInfo("Rapid info $i"); + logDebug("Rapid debug $i"); + } + }, returnsNormally); + }); + }); + group("Concurrent Access", () { + test("should handle multiple concurrent log calls", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + // Create multiple concurrent log calls + var futures = []; + for (int i = 0; i < 25; i++) { + futures.add(LoggerBridge.handleMethodCallForTesting( + MethodCall('log', { + 'level': (i % 4) + 1, // Cycle through levels 1-4 + 'message': 'Concurrent message $i' + }) + )); + } + + await Future.wait(futures); + + expect(testLogger.logs.length, equals(25)); + + // Verify all messages are present + for (int i = 0; i < 25; i++) { + expect(testLogger.logs.any((log) => log.message == 'Concurrent message $i'), isTrue); + } + }); + + test("should handle logger reinitialization during concurrent access", () async { + var testLogger1 = TestLogger(); + var testLogger2 = TestLogger(); + + LoggerBridge.initialize(testLogger1); + + // Start some async operations + var futures = []; + for (int i = 0; i < 5; i++) { + futures.add(LoggerBridge.handleMethodCallForTesting( + MethodCall('log', { + 'level': 3, + 'message': 'Message before reinit $i' + }) + )); + } + + // Reinitialize with a different logger mid-flight + LoggerBridge.initialize(testLogger2); + + // Add more operations + for (int i = 0; i < 5; i++) { + futures.add(LoggerBridge.handleMethodCallForTesting( + MethodCall('log', { + 'level': 3, + 'message': 'Message after reinit $i' + }) + )); + } + + await Future.wait(futures); + + // The total logs should be distributed between the two loggers + var totalLogs = testLogger1.logs.length + testLogger2.logs.length; + expect(totalLogs, equals(10)); + expect(LoggerBridge.getCurrentLogger(), equals(testLogger2)); + }); + }); + + group("Performance", () { + test("should handle high volume of logs efficiently", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + var stopwatch = Stopwatch()..start(); + + // Send 100 log messages + for (int i = 0; i < 100; i++) { + await LoggerBridge.handleMethodCallForTesting( + MethodCall('log', { + 'level': (i % 4) + 1, + 'message': 'Performance test log $i' + }) + ); + } + + stopwatch.stop(); + + expect(testLogger.logs.length, equals(100)); + expect(stopwatch.elapsedMilliseconds, lessThan(2000)); // Should complete in < 2 seconds + + // Verify first and last messages + expect(testLogger.logs.first.message, equals('Performance test log 0')); + expect(testLogger.logs.last.message, equals('Performance test log 99')); + }); + + test("should handle large message content efficiently", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + // Create a large message (10KB) + var largeMessage = 'X' * 10240; + + var stopwatch = Stopwatch()..start(); + + await LoggerBridge.handleMethodCallForTesting( + MethodCall('log', { + 'level': 3, + 'message': largeMessage + }) + ); + + stopwatch.stop(); + + expect(testLogger.logs.length, equals(1)); + expect(testLogger.logs.first.message.length, equals(10240)); + expect(stopwatch.elapsedMilliseconds, lessThan(100)); // Should be very fast + }); + }); + + group("State Management", () { + test("should maintain state across multiple operations", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + // Perform various operations + await LoggerBridge.handleMethodCallForTesting( + const MethodCall('log', {'level': 1, 'message': 'First message'}) + ); + + expect(LoggerBridge.hasLogger(), isTrue); + expect(testLogger.logs.length, equals(1)); + + await LoggerBridge.handleMethodCallForTesting( + const MethodCall('log', {'level': 2, 'message': 'Second message'}) + ); + + expect(LoggerBridge.hasLogger(), isTrue); + expect(testLogger.logs.length, equals(2)); + + LoggerBridge.reset(); + + expect(LoggerBridge.hasLogger(), isFalse); + expect(testLogger.logs.length, equals(2)); // Logger keeps its own state + }); + + test("should handle logger replacement", () async { + var testLogger1 = TestLogger(); + var testLogger2 = TestLogger(); + + // Initialize with first logger + LoggerBridge.initialize(testLogger1); + await LoggerBridge.handleMethodCallForTesting( + const MethodCall('log', {'level': 3, 'message': 'Message to logger 1'}) + ); + + expect(testLogger1.logs.length, equals(1)); + expect(testLogger2.logs.length, equals(0)); + + // Replace with second logger + LoggerBridge.initialize(testLogger2); + await LoggerBridge.handleMethodCallForTesting( + const MethodCall('log', {'level': 3, 'message': 'Message to logger 2'}) + ); + + expect(testLogger1.logs.length, equals(1)); // Unchanged + expect(testLogger2.logs.length, equals(1)); // New message + expect(LoggerBridge.getCurrentLogger(), equals(testLogger2)); + }); + }); + + group("Edge Cases", () { + test("should handle empty message", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + await LoggerBridge.handleMethodCallForTesting( + const MethodCall('log', { + 'level': 3, + 'message': '' + }) + ); + + expect(testLogger.logs.length, equals(1)); + expect(testLogger.logs.first.message, equals('')); + expect(testLogger.logs.first.level, equals(OptimizelyLogLevel.info)); + }); + + test("should handle special characters in message", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + var specialMessage = 'Special chars: πŸš€ ñÑéíóú δΈ­ζ–‡ \n\t\r\\'; + + await LoggerBridge.handleMethodCallForTesting( + MethodCall('log', { + 'level': 3, + 'message': specialMessage + }) + ); + + expect(testLogger.logs.length, equals(1)); + expect(testLogger.logs.first.message, equals(specialMessage)); + }); + + test("should handle invalid data types gracefully", () async { + var testLogger = TestLogger(); + LoggerBridge.initialize(testLogger); + + // Test with double level - should fail gracefully + await LoggerBridge.handleMethodCallForTesting( + const MethodCall('log', { + 'level': 3.0, // Double instead of int + 'message': 'Message with double level' + }) + ); + + // Should not log anything due to type casting error + expect(testLogger.logs.length, equals(0)); + }); + }); + }); +} From 00fd215da2e8f2f481b23bc9df9713d06dc83f49 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam <129880873+muzahidul-opti@users.noreply.github.com> Date: Fri, 24 Oct 2025 21:21:58 +0600 Subject: [PATCH 5/7] prepare for release 3.2.0 (#89) --- CHANGELOG.md | 6 ++++++ README.md | 2 +- lib/package_info.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7e8a8..3b737f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Optimizely Flutter SDK Changelog +## 3.2.0 +October 24th, 2025 + +### New Feature +* Swift custom logger support added ([#88](https://github.com/optimizely/optimizely-flutter-sdk/pull/88)) + ## 3.1.0 October 9th, 2025 diff --git a/README.md b/README.md index cdf6f10..eccb976 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Other Flutter platforms are not currently supported by this SDK. To add the flutter-sdk to your project dependencies, include the following in your app's pubspec.yaml: ``` - optimizely_flutter_sdk: ^3.1.0 + optimizely_flutter_sdk: ^3.2.0 ``` Then run diff --git a/lib/package_info.dart b/lib/package_info.dart index 6c8b054..777415b 100644 --- a/lib/package_info.dart +++ b/lib/package_info.dart @@ -3,5 +3,5 @@ class PackageInfo { static const String name = 'optimizely_flutter_sdk'; - static const String version = '3.1.0'; + static const String version = '3.2.0'; } diff --git a/pubspec.yaml b/pubspec.yaml index 83c113e..519c2dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: optimizely_flutter_sdk description: This repository houses the Flutter SDK for use with Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts. -version: 3.1.0 +version: 3.2.0 homepage: https://github.com/optimizely/optimizely-flutter-sdk environment: From 9dfbcec9981cd0e7eadc10e04892afe0fcc5fc29 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam <129880873+muzahidul-opti@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:19:03 +0600 Subject: [PATCH 6/7] [FSSDK-11985] feat: add android logger support (#90) * chore: add new classes for logging - Add FlutterLogbackAppender.java for logging events in the Flutter app - Integrate OptimizelyFlutterSdkPlugin with FlutterLogbackAppender - Update Constants.swift, SwiftOptimizelyFlutterSdkPlugin.swift, and related classes * refactor: update log level conversion logic - Adjust method to correctly convert log level strings to integers - Refactor switch statement for better readability and maintainability * refactor: optimize log level conversion - Remove unnecessary default log level value initialization - Refactor switch statement to directly return log levels for each case - Simplify comparison for warning log levels to include both "WARN" and "WARNING" * style: update logback configuration and log levels - Comment out configuration block in logback.xml - Modify log level 'WARNING' to 'WARN' in FlutterLogbackAppender.java * fix: ensure debugging logs are only printed in debug mode - Add check for kDebugMode to control printing of logs * chore: update logback.xml - Comment out old logback configuration - Add note explaining the use of FlutterLogbackAppender for logging --- android/src/main/assets/logback.xml | 19 +----- .../FlutterLogbackAppender.java | 62 +++++++++++++++++++ .../OptimizelyFlutterSdkPlugin.java | 30 +++++++++ ios/Classes/HelperClasses/Constants.swift | 1 - .../SwiftOptimizelyFlutterSdkPlugin.swift | 13 ++-- lib/optimizely_flutter_sdk.dart | 12 +--- lib/src/logger/flutter_logger.dart | 5 +- lib/src/optimizely_client_wrapper.dart | 1 - lib/src/utils/constants.dart | 1 - 9 files changed, 103 insertions(+), 41 deletions(-) create mode 100644 android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java diff --git a/android/src/main/assets/logback.xml b/android/src/main/assets/logback.xml index 8e6e0d6..7f531a8 100644 --- a/android/src/main/assets/logback.xml +++ b/android/src/main/assets/logback.xml @@ -1,18 +1 @@ - - - - Optimizely - - - %msg - - - - - - - + diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java new file mode 100644 index 0000000..2252cdd --- /dev/null +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterLogbackAppender.java @@ -0,0 +1,62 @@ +package com.optimizely.optimizely_flutter_sdk; +import com.optimizely.optimizely_flutter_sdk.helper_classes.Constants; + +import android.os.Handler; +import android.os.Looper; + +import java.util.HashMap; +import java.util.Map; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import io.flutter.plugin.common.MethodChannel; + +public class FlutterLogbackAppender extends AppenderBase { + + public static final String CHANNEL_NAME = "optimizely_flutter_sdk_logger"; + public static MethodChannel channel; + private static final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + + public static void setChannel(MethodChannel channel) { + FlutterLogbackAppender.channel = channel; + } + + @Override + protected void append(ILoggingEvent event) { + if (channel == null) { + return; + } + + String message = event.getFormattedMessage(); + String level = event.getLevel().toString(); + int logLevel = convertLogLevel(level); + Map logData = new HashMap<>(); + logData.put("level", logLevel); + logData.put("message", message); + + mainThreadHandler.post(() -> { + if (channel != null) { + channel.invokeMethod("log", logData); + } + }); + } + + int convertLogLevel(String logLevel) { + if (logLevel == null || logLevel.isEmpty()) { + return 3; + } + + switch (logLevel.toUpperCase()) { + case "ERROR": + return 1; + case "WARN": + return 2; + case "INFO": + return 3; + case "DEBUG": + return 4; + default: + return 3; + } + } +} diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java index 89f787c..5ca2d8e 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java @@ -32,10 +32,19 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; + + /** OptimizelyFlutterSdkPlugin */ public class OptimizelyFlutterSdkPlugin extends OptimizelyFlutterClient implements FlutterPlugin, ActivityAware, MethodCallHandler { public static MethodChannel channel; + private Appender flutterLogbackAppender; @Override public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { @@ -157,11 +166,32 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { channel = new MethodChannel(binding.getBinaryMessenger(), "optimizely_flutter_sdk"); channel.setMethodCallHandler(this); context = binding.getApplicationContext(); + + MethodChannel loggerChannel = new MethodChannel(binding.getBinaryMessenger(), FlutterLogbackAppender.CHANNEL_NAME); + FlutterLogbackAppender.setChannel(loggerChannel); + + // Add appender to logback + flutterLogbackAppender = new FlutterLogbackAppender(); + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + flutterLogbackAppender.setContext(lc); + flutterLogbackAppender.start(); + Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.setLevel(ch.qos.logback.classic.Level.ALL); + rootLogger.addAppender(flutterLogbackAppender); } @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel.setMethodCallHandler(null); + // Stop and detach the appender + if (flutterLogbackAppender != null) { + Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.detachAppender(flutterLogbackAppender); + flutterLogbackAppender.stop(); + flutterLogbackAppender = null; + } + // Clean up the channel + FlutterLogbackAppender.setChannel(null); } @Override diff --git a/ios/Classes/HelperClasses/Constants.swift b/ios/Classes/HelperClasses/Constants.swift index 1b0cbac..a29370a 100644 --- a/ios/Classes/HelperClasses/Constants.swift +++ b/ios/Classes/HelperClasses/Constants.swift @@ -91,7 +91,6 @@ struct RequestParameterKey { static let reasons = "reasons" static let decideOptions = "optimizelyDecideOption" static let defaultLogLevel = "defaultLogLevel" - static let useCustomLogger = "useCustomLogger" static let eventBatchSize = "eventBatchSize" static let eventTimeInterval = "eventTimeInterval" static let eventMaxQueueSize = "eventMaxQueueSize" diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index 75ba8b9..be81576 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -172,15 +172,10 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { notificationIdsTracker.removeValue(forKey: sdkKey) optimizelyClientsTracker.removeValue(forKey: sdkKey) - // Check if custom logger is requested - var logger: OPTLogger? - if let useCustomLogger = parameters[RequestParameterKey.useCustomLogger] as? Bool, useCustomLogger { - // OptimizelyFlutterLogger bridges iOS logs to Flutter via Method Channel - // When useCustomLogger = true: - // iOS SDK log β†’ OptimizelyFlutterLogger β†’ Flutter Method Channel β†’ Flutter console - logger = OptimizelyFlutterLogger() - } - + // OptimizelyFlutterLogger bridges iOS logs to Flutter via Method Channel + // iOS SDK log β†’ OptimizelyFlutterLogger β†’ Flutter Method Channel β†’ Flutter console + var logger: OPTLogger = OptimizelyFlutterLogger() + // Creating new instance let optimizelyInstance = OptimizelyClient( sdkKey:sdkKey, diff --git a/lib/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index a1ff583..cc7d11d 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -73,11 +73,6 @@ class OptimizelyFlutterSdk { final OptimizelyLogLevel _defaultLogLevel; final SDKSettings _sdkSettings; static OptimizelyLogger? _customLogger; - /// Set a custom logger for the SDK - static void setLogger(OptimizelyLogger logger) { - _customLogger = logger; - LoggerBridge.initialize(logger); - } /// Get the current logger static OptimizelyLogger? get logger { return _customLogger; @@ -97,11 +92,8 @@ class OptimizelyFlutterSdk { _defaultLogLevel = defaultLogLevel, _sdkSettings = sdkSettings { // Set the logger if provided - if (logger != null) { - setLogger(logger); - } else { - logWarning("Logger not provided."); - } + _customLogger = logger ?? DefaultOptimizelyLogger(); + LoggerBridge.initialize(_customLogger); } /// Starts Optimizely SDK (Synchronous) with provided sdkKey. diff --git a/lib/src/logger/flutter_logger.dart b/lib/src/logger/flutter_logger.dart index ad3ec67..f561ed9 100644 --- a/lib/src/logger/flutter_logger.dart +++ b/lib/src/logger/flutter_logger.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; abstract class OptimizelyLogger { @@ -8,7 +9,9 @@ abstract class OptimizelyLogger { class DefaultOptimizelyLogger implements OptimizelyLogger { @override void log(OptimizelyLogLevel level, String message) { - print('[OPTIMIZELY] [${level.name.toUpperCase()}]: $message'); + if (kDebugMode) { + print('[OPTIMIZELY] [${level.name.toUpperCase()}]: $message'); + } } } diff --git a/lib/src/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index 8caa22c..a7c092d 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -80,7 +80,6 @@ class OptimizelyClientWrapper { Constants.eventBatchSize: eventOptions.batchSize, Constants.eventTimeInterval: eventOptions.timeInterval, Constants.eventMaxQueueSize: eventOptions.maxQueueSize, - Constants.useCustomLogger: logger != null, }; // Odp Request params diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 8a1586e..2bb5421 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -86,7 +86,6 @@ class Constants { static const String optimizelyDecideOption = "optimizelyDecideOption"; static const String optimizelySegmentOption = "optimizelySegmentOption"; static const String optimizelySdkSettings = "optimizelySdkSettings"; - static const String useCustomLogger = 'useCustomLogger'; static const String defaultLogLevel = "defaultLogLevel"; static const String payload = "payload"; static const String value = "value"; From 725ec50d6a96009629343cb7a1b91b8b05235f5b Mon Sep 17 00:00:00 2001 From: Muzahidul Islam <129880873+muzahidul-opti@users.noreply.github.com> Date: Wed, 29 Oct 2025 21:43:23 +0600 Subject: [PATCH 7/7] Release 3.3.0 (#91) --- CHANGELOG.md | 6 ++++++ README.md | 2 +- lib/package_info.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b737f7..c988a75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Optimizely Flutter SDK Changelog +## 3.3.0 +October 29th, 2025 + +### New Feature +* Android custom logger support added ([#90](https://github.com/optimizely/optimizely-flutter-sdk/pull/90)) + ## 3.2.0 October 24th, 2025 diff --git a/README.md b/README.md index eccb976..3f7d5c8 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Other Flutter platforms are not currently supported by this SDK. To add the flutter-sdk to your project dependencies, include the following in your app's pubspec.yaml: ``` - optimizely_flutter_sdk: ^3.2.0 + optimizely_flutter_sdk: ^3.3.0 ``` Then run diff --git a/lib/package_info.dart b/lib/package_info.dart index 777415b..df427cd 100644 --- a/lib/package_info.dart +++ b/lib/package_info.dart @@ -3,5 +3,5 @@ class PackageInfo { static const String name = 'optimizely_flutter_sdk'; - static const String version = '3.2.0'; + static const String version = '3.3.0'; } diff --git a/pubspec.yaml b/pubspec.yaml index 519c2dd..e48e93d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: optimizely_flutter_sdk description: This repository houses the Flutter SDK for use with Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts. -version: 3.2.0 +version: 3.3.0 homepage: https://github.com/optimizely/optimizely-flutter-sdk environment: