From be95ef59adda5f783ab74a5cff8a916793fa72a8 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Fri, 19 Sep 2025 16:48:11 +0600 Subject: [PATCH 01/15] 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 --- ios/Classes/FlutterOptimizelyLogger.swift | 25 ++++++++++ ios/Classes/HelperClasses/Constants.swift | 1 + .../SwiftOptimizelyFlutterSdkPlugin.swift | 9 ++++ lib/optimizely_flutter_sdk.dart | 49 +++++++++++++------ lib/src/logger/LoggerBridge.dart | 26 ++++++++++ lib/src/logger/OptimizelyLogger.dart | 28 +++++++++++ lib/src/optimizely_client_wrapper.dart | 10 +++- lib/src/utils/constants.dart | 1 + 8 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 ios/Classes/FlutterOptimizelyLogger.swift create mode 100644 lib/src/logger/LoggerBridge.dart create mode 100644 lib/src/logger/OptimizelyLogger.dart diff --git a/ios/Classes/FlutterOptimizelyLogger.swift b/ios/Classes/FlutterOptimizelyLogger.swift new file mode 100644 index 0000000..b397b79 --- /dev/null +++ b/ios/Classes/FlutterOptimizelyLogger.swift @@ -0,0 +1,25 @@ +import Flutter +import Optimizely + +public class FlutterOptimizelyLogger: NSObject, OPTLogger { + public static var logLevel: OptimizelyLogLevel = .info + + private static let loggerChannel = FlutterMethodChannel( + name: "optimizely_flutter_sdk_logger", + binaryMessenger: SwiftOptimizelyFlutterSdkPlugin.registrar?.messenger() ?? FlutterEngine().binaryMessenger + ) + + public required override init() { + super.init() + } + + public func log(level: OptimizelyLogLevel, message: String) { + // Ensure we're on the main thread when calling Flutter + DispatchQueue.main.async { + Self.loggerChannel.invokeMethod("log", arguments: [ + "level": level.rawValue, + "message": message + ]) + } + } +} diff --git a/ios/Classes/HelperClasses/Constants.swift b/ios/Classes/HelperClasses/Constants.swift index a29370a..c7af4fc 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 customLogger = "customLogger" 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 7c093c4..2c56690 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -36,8 +36,10 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { return UUID().uuidString } + static var registrar: FlutterPluginRegistrar? /// Registers optimizely_flutter_sdk channel to communicate with the flutter sdk to receive requests and send responses public static func register(with registrar: FlutterPluginRegistrar) { + self.registrar = registrar channel = FlutterMethodChannel(name: "optimizely_flutter_sdk", binaryMessenger: registrar.messenger()) let instance = SwiftOptimizelyFlutterSdkPlugin() registrar.addMethodCallDelegate(instance, channel: channel) @@ -163,9 +165,16 @@ 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.customLogger] as? Bool, useCustomLogger { + logger = FlutterOptimizelyLogger() + } + // 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..51200e7 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/OptimizelyLogger.dart'; +import 'package:optimizely_flutter_sdk/src/logger/LoggerBridge.dart'; export 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart' show ClientPlatform, ListenerType; @@ -68,20 +70,37 @@ class OptimizelyFlutterSdk { final Set _defaultDecideOptions; final OptimizelyLogLevel _defaultLogLevel; final SDKSettings _sdkSettings; + final OptimizelyLogger? _logger; // Add logger field + static OptimizelyLogger? _customLogger; + /// Set a custom logger for the SDK + static void setLogger(OptimizelyLogger logger) { + _customLogger = logger; + LoggerBridge.initialize(); + } + /// 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}) // Add logger parameter + : _eventOptions = eventOptions, + _datafilePeriodicDownloadInterval = datafilePeriodicDownloadInterval, + _datafileHostOptions = datafileHostOptions, + _defaultDecideOptions = defaultDecideOptions, + _defaultLogLevel = defaultLogLevel, + _sdkSettings = sdkSettings, + _logger = logger { + // Set the logger if provided + if (logger != null) { + setLogger(logger); + } + } /// Starts Optimizely SDK (Synchronous) with provided sdkKey. Future initializeClient() async { @@ -92,7 +111,9 @@ class OptimizelyFlutterSdk { _datafileHostOptions, _defaultDecideOptions, _defaultLogLevel, - _sdkSettings); + _sdkSettings, + _logger + ); } /// Use the activate method to start an experiment. diff --git a/lib/src/logger/LoggerBridge.dart b/lib/src/logger/LoggerBridge.dart new file mode 100644 index 0000000..93e7aea --- /dev/null +++ b/lib/src/logger/LoggerBridge.dart @@ -0,0 +1,26 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; + +import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; + +class LoggerBridge { + static const MethodChannel _loggerChannel = + MethodChannel('optimizely_flutter_sdk_logger'); + + static void initialize() { + _loggerChannel.setMethodCallHandler(_handleMethodCall); + } + + static Future _handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'log': + final args = call.arguments as Map; + final level = OptimizelyLogLevel.values[args['level'] as int]; + final message = args['message'] as String; + + OptimizelyFlutterSdk.logger?.log(level, message); + break; + } + } +} diff --git a/lib/src/logger/OptimizelyLogger.dart b/lib/src/logger/OptimizelyLogger.dart new file mode 100644 index 0000000..4414780 --- /dev/null +++ b/lib/src/logger/OptimizelyLogger.dart @@ -0,0 +1,28 @@ +import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; + +abstract class OptimizelyLogger { + /// The log level for the logger + OptimizelyLogLevel get logLevel; + set logLevel(OptimizelyLogLevel level); + + /// Log a message at a certain level + void log(OptimizelyLogLevel level, String message); +} + +// enum OptimizelyLogLevel { error, warning, info, debug } + +class DefaultOptimizelyLogger implements OptimizelyLogger { + @override + OptimizelyLogLevel logLevel = OptimizelyLogLevel.info; + + @override + void log(OptimizelyLogLevel level, String message) { + if (_shouldLog(level)) { + print('[Optimizely ${level.name.toUpperCase()}] $message'); + } + } + + bool _shouldLog(OptimizelyLogLevel messageLevel) { + return messageLevel.index <= logLevel.index; + } +} diff --git a/lib/src/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index a0869b9..4a5ff85 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -27,6 +27,8 @@ import 'package:optimizely_flutter_sdk/src/data_objects/get_vuid_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; +import 'package:optimizely_flutter_sdk/src/logger/OptimizelyLogger.dart'; +import 'package:optimizely_flutter_sdk/src/logger/LoggerBridge.dart'; enum ListenerType { activate, track, decision, logEvent, projectConfigUpdate } @@ -63,8 +65,13 @@ class OptimizelyClientWrapper { Map datafileHostOptions, Set defaultDecideOptions, OptimizelyLogLevel defaultLogLevel, - SDKSettings sdkSettings) async { + SDKSettings sdkSettings, + OptimizelyLogger? logger) async { _channel.setMethodCallHandler(methodCallHandler); + // Initialize logger bridge if custom logger is provided + if (logger != null) { + LoggerBridge.initialize(); + } final convertedOptions = Utils.convertDecideOptions(defaultDecideOptions); final convertedLogLevel = Utils.convertLogLevel(defaultLogLevel); const sdkVersion = PackageInfo.version; @@ -79,6 +86,7 @@ class OptimizelyClientWrapper { Constants.eventBatchSize: eventOptions.batchSize, Constants.eventTimeInterval: eventOptions.timeInterval, Constants.eventMaxQueueSize: eventOptions.maxQueueSize, + Constants.customLogger: logger != null, }; // Odp Request params diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 2bb5421..4b502be 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 customLogger = 'customLogger'; static const String defaultLogLevel = "defaultLogLevel"; static const String payload = "payload"; static const String value = "value"; From 9b4c6fead21f1aaebcc7e479811d8ef3b57c52bf Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Fri, 19 Sep 2025 18:59:41 +0600 Subject: [PATCH 02/15] 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 --- .../FlutterOptimizelyLogger.java | 378 ++++++++++++++++++ .../OptimizelyFlutterClient.java | 11 +- .../OptimizelyFlutterSdkPlugin.java | 10 + .../helper_classes/ArgumentsParser.java | 4 + .../helper_classes/Constants.java | 1 + lib/optimizely_flutter_sdk.dart | 10 +- lib/src/logger/LoggerBridge.dart | 72 +++- lib/src/optimizely_client_wrapper.dart | 4 - 8 files changed, 470 insertions(+), 20 deletions(-) create mode 100644 android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterOptimizelyLogger.java diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterOptimizelyLogger.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterOptimizelyLogger.java new file mode 100644 index 0000000..751e0ab --- /dev/null +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterOptimizelyLogger.java @@ -0,0 +1,378 @@ +/**************************************************************************** + * Copyright 2022-2023, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * https://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +package com.optimizely.optimizely_flutter_sdk; + +import android.os.Handler; +import android.os.Looper; +import io.flutter.plugin.common.MethodChannel; +import org.slf4j.Logger; +import org.slf4j.Marker; +import java.util.HashMap; +import java.util.Map; + +public class FlutterOptimizelyLogger implements Logger { + static final String LOGGER_CHANNEL = "optimizely_flutter_sdk_logger"; + private static MethodChannel loggerChannel; + private final String tag; + + public FlutterOptimizelyLogger(String name) { + tag = name; + } + + public static void setChannel(MethodChannel channel) { + loggerChannel = channel; + } + + @Override + public String getName() { + return "OptimizelyLogger"; + } + + // Trace methods + @Override + public boolean isTraceEnabled() { + return false; + } + + @Override + public void trace(String msg) { + // Not implemented + } + + @Override + public void trace(String format, Object arg) { + // Not implemented + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + // Not implemented + } + + @Override + public void trace(String format, Object... arguments) { + // Not implemented + } + + @Override + public void trace(String msg, Throwable t) { + // Not implemented + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return false; + } + + @Override + public void trace(Marker marker, String msg) { + // Not implemented + } + + @Override + public void trace(Marker marker, String format, Object arg) { + // Not implemented + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) { + // Not implemented + } + + @Override + public void trace(Marker marker, String format, Object... argArray) { + // Not implemented + } + + @Override + public void trace(Marker marker, String msg, Throwable t) { + // Not implemented + } + + // Debug methods + @Override + public boolean isDebugEnabled() { + return true; + } + + @Override + public void debug(String msg) { + sendLogToFlutter(4, msg); + } + + @Override + public void debug(String format, Object arg) { + debug(formatMessage(format, arg)); + } + + @Override + public void debug(String format, Object arg1, Object arg2) { + debug(formatMessage(format, arg1, arg2)); + } + + @Override + public void debug(String format, Object... arguments) { + debug(formatMessage(format, arguments)); + } + + @Override + public void debug(String msg, Throwable t) { + debug(formatThrowable(msg, t)); + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return true; + } + + @Override + public void debug(Marker marker, String msg) { + debug(msg); + } + + @Override + public void debug(Marker marker, String format, Object arg) { + debug(format, arg); + } + + @Override + public void debug(Marker marker, String format, Object arg1, Object arg2) { + debug(format, arg1, arg2); + } + + @Override + public void debug(Marker marker, String format, Object... arguments) { + debug(format, arguments); + } + + @Override + public void debug(Marker marker, String msg, Throwable t) { + debug(msg, t); + } + + // Info methods + @Override + public boolean isInfoEnabled() { + return true; + } + + @Override + public void info(String msg) { + sendLogToFlutter(3, msg); + } + + @Override + public void info(String format, Object arg) { + info(formatMessage(format, arg)); + } + + @Override + public void info(String format, Object arg1, Object arg2) { + info(formatMessage(format, arg1, arg2)); + } + + @Override + public void info(String format, Object... arguments) { + info(formatMessage(format, arguments)); + } + + @Override + public void info(String msg, Throwable t) { + info(formatThrowable(msg, t)); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return true; + } + + @Override + public void info(Marker marker, String msg) { + info(msg); + } + + @Override + public void info(Marker marker, String format, Object arg) { + info(format, arg); + } + + @Override + public void info(Marker marker, String format, Object arg1, Object arg2) { + info(format, arg1, arg2); + } + + @Override + public void info(Marker marker, String format, Object... arguments) { + info(format, arguments); + } + + @Override + public void info(Marker marker, String msg, Throwable t) { + info(msg, t); + } + + // Warn methods + @Override + public boolean isWarnEnabled() { + return true; + } + + @Override + public void warn(String msg) { + sendLogToFlutter(2, msg); + } + + @Override + public void warn(String format, Object arg) { + warn(formatMessage(format, arg)); + } + + @Override + public void warn(String format, Object... arguments) { + warn(formatMessage(format, arguments)); + } + + @Override + public void warn(String format, Object arg1, Object arg2) { + warn(formatMessage(format, arg1, arg2)); + } + + @Override + public void warn(String msg, Throwable t) { + warn(formatThrowable(msg, t)); + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return true; + } + + @Override + public void warn(Marker marker, String msg) { + warn(msg); + } + + @Override + public void warn(Marker marker, String format, Object arg) { + warn(format, arg); + } + + @Override + public void warn(Marker marker, String format, Object arg1, Object arg2) { + warn(format, arg1, arg2); + } + + @Override + public void warn(Marker marker, String format, Object... arguments) { + warn(format, arguments); + } + + @Override + public void warn(Marker marker, String msg, Throwable t) { + warn(msg, t); + } + + // Error methods + @Override + public boolean isErrorEnabled() { + return true; + } + + @Override + public void error(String msg) { + sendLogToFlutter(1, msg); // ERROR level = 1 + } + + @Override + public void error(String format, Object arg) { + error(formatMessage(format, arg)); + } + + @Override + public void error(String format, Object arg1, Object arg2) { + error(formatMessage(format, arg1, arg2)); + } + + @Override + public void error(String format, Object... arguments) { + error(formatMessage(format, arguments)); + } + + @Override + public void error(String msg, Throwable t) { + error(formatThrowable(msg, t)); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return true; + } + + @Override + public void error(Marker marker, String msg) { + error(msg); + } + + @Override + public void error(Marker marker, String format, Object arg) { + error(format, arg); + } + + @Override + public void error(Marker marker, String format, Object arg1, Object arg2) { + error(format, arg1, arg2); + } + + @Override + public void error(Marker marker, String format, Object... arguments) { + error(format, arguments); + } + + @Override + public void error(Marker marker, String msg, Throwable t) { + error(msg, t); + } + + // Helper methods + private void sendLogToFlutter(int level, String message) { + if (loggerChannel == null) { + return; + } + + // Ensure we're on the main thread when calling Flutter (similar to Swift's DispatchQueue.main.async) + Handler mainHandler = new Handler(Looper.getMainLooper()); + mainHandler.post(() -> { + Map arguments = new HashMap<>(); + arguments.put("level", level); + arguments.put("message", message); + loggerChannel.invokeMethod("log", arguments); + }); + } + + private String formatMessage(String format, Object... args) { + try { + // SLF4J uses {} placeholders, replace with %s for String.format + String formatString = format.replace("{}", "%s"); + return String.format(formatString, args); + } catch (Exception e) { + return format; + } + } + + private String formatThrowable(String msg, Throwable t) { + return msg + " - " + t.getMessage(); + } +} \ No newline at end of file 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..93960ec 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,7 @@ import com.optimizely.ab.UnknownEventTypeException; import com.optimizely.ab.android.event_handler.DefaultEventHandler; import com.optimizely.ab.android.sdk.OptimizelyClient; - +import org.slf4j.Logger; import java.util.HashMap; import java.util.Map; @@ -187,6 +187,15 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N if (enableVuid) { optimizelyManagerBuilder.withVuidEnabled(); } + + // Check if custom logger is requested + Boolean useCustomLogger = argumentsParser.getCustomLogger(); + Logger customLogger = null; + if (useCustomLogger != null && useCustomLogger) { + customLogger = new FlutterOptimizelyLogger("OptimizelySDK"); + optimizelyManagerBuilder.withLogger(customLogger); + } + OptimizelyManager optimizelyManager = optimizelyManagerBuilder.build(context); optimizelyManager.initialize(context, null, (OptimizelyClient client) -> { 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..2c9f36c 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 @@ -36,6 +36,7 @@ public class OptimizelyFlutterSdkPlugin extends OptimizelyFlutterClient implements FlutterPlugin, ActivityAware, MethodCallHandler { public static MethodChannel channel; + private static MethodChannel loggerChannel; @Override public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { @@ -156,12 +157,21 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { channel = new MethodChannel(binding.getBinaryMessenger(), "optimizely_flutter_sdk"); channel.setMethodCallHandler(this); + + // Logger channel for custom logging + loggerChannel = new MethodChannel(binding.getBinaryMessenger(), FlutterOptimizelyLogger.LOGGER_CHANNEL); + + // Set the logger channel in your logger class + FlutterOptimizelyLogger.setChannel(loggerChannel); + context = binding.getApplicationContext(); } @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel.setMethodCallHandler(null); + loggerChannel.setMethodCallHandler(null); // Clean up logger channel + FlutterOptimizelyLogger.setChannel(null); } @Override diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java index 6d2741f..092ea64 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java @@ -80,6 +80,10 @@ public String getDefaultLogLevel() { return (String) arguments.get(Constants.RequestParameterKey.DEFAULT_LOG_LEVEL); } + public Boolean getCustomLogger() { + return (Boolean) arguments.get(Constants.RequestParameterKey.CUSTOM_LOGGER); + } + public String getFlagKey() { return (String) arguments.get(Constants.RequestParameterKey.FLAG_KEY); } diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java index 62f0ce9..ce67f74 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java @@ -69,6 +69,7 @@ public static class RequestParameterKey { public static final String DECIDE_KEYS = "keys"; public static final String DECIDE_OPTIONS = "optimizelyDecideOption"; public static final String DEFAULT_LOG_LEVEL = "defaultLogLevel"; + public static final String CUSTOM_LOGGER = "customLogger"; public static final String EVENT_BATCH_SIZE = "eventBatchSize"; public static final String EVENT_TIME_INTERVAL = "eventTimeInterval"; public static final String EVENT_MAX_QUEUE_SIZE = "eventMaxQueueSize"; diff --git a/lib/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index 51200e7..3caf903 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -70,12 +70,11 @@ class OptimizelyFlutterSdk { final Set _defaultDecideOptions; final OptimizelyLogLevel _defaultLogLevel; final SDKSettings _sdkSettings; - final OptimizelyLogger? _logger; // Add logger field static OptimizelyLogger? _customLogger; /// Set a custom logger for the SDK static void setLogger(OptimizelyLogger logger) { _customLogger = logger; - LoggerBridge.initialize(); + LoggerBridge.initialize(logger); } /// Get the current logger static OptimizelyLogger? get logger { @@ -94,11 +93,12 @@ class OptimizelyFlutterSdk { _datafileHostOptions = datafileHostOptions, _defaultDecideOptions = defaultDecideOptions, _defaultLogLevel = defaultLogLevel, - _sdkSettings = sdkSettings, - _logger = logger { + _sdkSettings = sdkSettings { // Set the logger if provided if (logger != null) { setLogger(logger); + } else { + print("Logger not provided."); } } @@ -112,7 +112,7 @@ class OptimizelyFlutterSdk { _defaultDecideOptions, _defaultLogLevel, _sdkSettings, - _logger + _customLogger ); } diff --git a/lib/src/logger/LoggerBridge.dart b/lib/src/logger/LoggerBridge.dart index 93e7aea..0ea6672 100644 --- a/lib/src/logger/LoggerBridge.dart +++ b/lib/src/logger/LoggerBridge.dart @@ -1,26 +1,78 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; - +import 'package:optimizely_flutter_sdk/src/logger/OptimizelyLogger.dart'; import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; class LoggerBridge { static const MethodChannel _loggerChannel = MethodChannel('optimizely_flutter_sdk_logger'); + static OptimizelyLogger? _customLogger; - static void initialize() { + /// Initialize the logger bridge to receive calls from native + static void initialize(OptimizelyLogger? logger) { + print('[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 { - switch (call.method) { - case 'log': - final args = call.arguments as Map; - final level = OptimizelyLogLevel.values[args['level'] as int]; - final message = args['message'] as String; - - OptimizelyFlutterSdk.logger?.log(level, message); - break; + print('[LoggerBridge] Received method call: ${call.method}'); + try { + switch (call.method) { + case 'log': + await _handleLogCall(call); + break; + default: + print('[LoggerBridge] Unknown method call: ${call.method}'); + } + } catch (e) { + print('[LoggerBridge] Error handling method call: $e'); + } + } + + /// Process the log call from Swift/Java + static Future _handleLogCall(MethodCall call) async { + try { + // Simple fix - just convert the map safely + final args = Map.from(call.arguments ?? {}); + + final levelRawValue = args['level'] as int?; + final message = args['message'] as String?; + + if (levelRawValue == null || message == null) { + print('[LoggerBridge] Warning: Missing level or message in log call'); + return; + } + + final level = _convertLogLevel(levelRawValue); + + print('[LoggerBridge] Processing log: level=$levelRawValue, message=$message'); + + if (_customLogger != null) { + _customLogger!.log(level, message); + } else { + print('[Optimizely ${level.name.toUpperCase()}] $message'); + } + } catch (e) { + print('[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; } } } diff --git a/lib/src/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index 4a5ff85..89581f4 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -68,10 +68,6 @@ class OptimizelyClientWrapper { SDKSettings sdkSettings, OptimizelyLogger? logger) async { _channel.setMethodCallHandler(methodCallHandler); - // Initialize logger bridge if custom logger is provided - if (logger != null) { - LoggerBridge.initialize(); - } final convertedOptions = Utils.convertDecideOptions(defaultDecideOptions); final convertedLogLevel = Utils.convertLogLevel(defaultLogLevel); const sdkVersion = PackageInfo.version; From 2f888403c6ef28b0d1a6e35baa145e921f8e9668 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Fri, 19 Sep 2025 19:24:10 +0600 Subject: [PATCH 03/15] 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 --- lib/optimizely_flutter_sdk.dart | 6 ++++-- .../logger/{OptimizelyLogger.dart => flutter_logger.dart} | 2 -- lib/src/logger/{LoggerBridge.dart => logger_bridge.dart} | 3 +-- lib/src/optimizely_client_wrapper.dart | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) rename lib/src/logger/{OptimizelyLogger.dart => flutter_logger.dart} (92%) rename lib/src/logger/{LoggerBridge.dart => logger_bridge.dart} (95%) diff --git a/lib/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index 3caf903..1247f49 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -28,8 +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/OptimizelyLogger.dart'; -import 'package:optimizely_flutter_sdk/src/logger/LoggerBridge.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; @@ -55,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. /// diff --git a/lib/src/logger/OptimizelyLogger.dart b/lib/src/logger/flutter_logger.dart similarity index 92% rename from lib/src/logger/OptimizelyLogger.dart rename to lib/src/logger/flutter_logger.dart index 4414780..225b22f 100644 --- a/lib/src/logger/OptimizelyLogger.dart +++ b/lib/src/logger/flutter_logger.dart @@ -9,8 +9,6 @@ abstract class OptimizelyLogger { void log(OptimizelyLogLevel level, String message); } -// enum OptimizelyLogLevel { error, warning, info, debug } - class DefaultOptimizelyLogger implements OptimizelyLogger { @override OptimizelyLogLevel logLevel = OptimizelyLogLevel.info; diff --git a/lib/src/logger/LoggerBridge.dart b/lib/src/logger/logger_bridge.dart similarity index 95% rename from lib/src/logger/LoggerBridge.dart rename to lib/src/logger/logger_bridge.dart index 0ea6672..a0bf5e7 100644 --- a/lib/src/logger/LoggerBridge.dart +++ b/lib/src/logger/logger_bridge.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; -import 'package:optimizely_flutter_sdk/src/logger/OptimizelyLogger.dart'; +import 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart'; import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; class LoggerBridge { @@ -35,7 +35,6 @@ class LoggerBridge { /// Process the log call from Swift/Java static Future _handleLogCall(MethodCall call) async { try { - // Simple fix - just convert the map safely final args = Map.from(call.arguments ?? {}); final levelRawValue = args['level'] as int?; diff --git a/lib/src/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index 89581f4..9c2ff99 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -27,8 +27,8 @@ import 'package:optimizely_flutter_sdk/src/data_objects/get_vuid_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; -import 'package:optimizely_flutter_sdk/src/logger/OptimizelyLogger.dart'; -import 'package:optimizely_flutter_sdk/src/logger/LoggerBridge.dart'; +import 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart'; +import 'package:optimizely_flutter_sdk/src/logger/logger_bridge.dart'; enum ListenerType { activate, track, decision, logEvent, projectConfigUpdate } From 6336b0c53be0b525f24f7cedd7a5df91c1a99c4c Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Fri, 19 Sep 2025 19:24:29 +0600 Subject: [PATCH 04/15] 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 --- example/lib/custom_logger.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 example/lib/custom_logger.dart diff --git a/example/lib/custom_logger.dart b/example/lib/custom_logger.dart new file mode 100644 index 0000000..7f1ed92 --- /dev/null +++ b/example/lib/custom_logger.dart @@ -0,0 +1,11 @@ +import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; + +class CustomLogger implements OptimizelyLogger { + @override + OptimizelyLogLevel logLevel = OptimizelyLogLevel.debug; + + @override + void log(OptimizelyLogLevel level, String message) { + print('[Flutter LOGGER] ${level.name}: $message'); + } +} From 40cfdaafddeb24964ab8c50f94de2d6e07a5b94a Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Fri, 19 Sep 2025 23:49:37 +0600 Subject: [PATCH 05/15] refactor: rename logger classes in Android and iOS - Rename FlutterOptimizelyLogger to OptimizelyFlutterLogger in Android - Rename FlutterOptimizelyLogger to OptimizelyFlutterLogger in iOS --- .../OptimizelyFlutterClient.java | 2 +- ...yLogger.java => OptimizelyFlutterLogger.java} | 4 ++-- .../OptimizelyFlutterSdkPlugin.java | 13 +++++-------- example/lib/custom_logger.dart | 5 ++++- example/lib/main.dart | 16 ++++++++++++++-- ...ogger.swift => OptimizelyFlutterLogger.swift} | 2 +- .../SwiftOptimizelyFlutterSdkPlugin.swift | 2 +- lib/src/optimizely_client_wrapper.dart | 2 -- 8 files changed, 28 insertions(+), 18 deletions(-) rename android/src/main/java/com/optimizely/optimizely_flutter_sdk/{FlutterOptimizelyLogger.java => OptimizelyFlutterLogger.java} (98%) rename ios/Classes/{FlutterOptimizelyLogger.swift => OptimizelyFlutterLogger.swift} (92%) 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 93960ec..9739082 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 @@ -192,7 +192,7 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N Boolean useCustomLogger = argumentsParser.getCustomLogger(); Logger customLogger = null; if (useCustomLogger != null && useCustomLogger) { - customLogger = new FlutterOptimizelyLogger("OptimizelySDK"); + customLogger = new OptimizelyFlutterLogger("OptimizelySDK"); optimizelyManagerBuilder.withLogger(customLogger); } diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterOptimizelyLogger.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterLogger.java similarity index 98% rename from android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterOptimizelyLogger.java rename to android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterLogger.java index 751e0ab..4c57f9b 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/FlutterOptimizelyLogger.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterLogger.java @@ -23,12 +23,12 @@ import java.util.HashMap; import java.util.Map; -public class FlutterOptimizelyLogger implements Logger { +public class OptimizelyFlutterLogger implements Logger { static final String LOGGER_CHANNEL = "optimizely_flutter_sdk_logger"; private static MethodChannel loggerChannel; private final String tag; - public FlutterOptimizelyLogger(String name) { + public OptimizelyFlutterLogger(String name) { tag = name; } 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 2c9f36c..7a68b36 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 @@ -157,12 +157,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { channel = new MethodChannel(binding.getBinaryMessenger(), "optimizely_flutter_sdk"); channel.setMethodCallHandler(this); - - // Logger channel for custom logging - loggerChannel = new MethodChannel(binding.getBinaryMessenger(), FlutterOptimizelyLogger.LOGGER_CHANNEL); - - // Set the logger channel in your logger class - FlutterOptimizelyLogger.setChannel(loggerChannel); + + loggerChannel = new MethodChannel(binding.getBinaryMessenger(), OptimizelyFlutterLogger.LOGGER_CHANNEL); + OptimizelyFlutterLogger.setChannel(loggerChannel); context = binding.getApplicationContext(); } @@ -170,8 +167,8 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel.setMethodCallHandler(null); - loggerChannel.setMethodCallHandler(null); // Clean up logger channel - FlutterOptimizelyLogger.setChannel(null); + loggerChannel.setMethodCallHandler(null); + OptimizelyFlutterLogger.setChannel(null); } @Override diff --git a/example/lib/custom_logger.dart b/example/lib/custom_logger.dart index 7f1ed92..9f5fb25 100644 --- a/example/lib/custom_logger.dart +++ b/example/lib/custom_logger.dart @@ -1,4 +1,5 @@ import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; +import 'package:flutter/foundation.dart'; class CustomLogger implements OptimizelyLogger { @override @@ -6,6 +7,8 @@ class CustomLogger implements OptimizelyLogger { @override void log(OptimizelyLogLevel level, String message) { - print('[Flutter LOGGER] ${level.name}: $message'); + if (kDebugMode) { + print('[Flutter LOGGER] ${level.name}: $message'); + } } } diff --git a/example/lib/main.dart b/example/lib/main.dart index e7db8fa..9591bbd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,14 @@ 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'; + +/** + * Logger TODO: + * Check thread safety + * Android logger fix + * Add unit test + */ void main() { runApp(const MyApp()); @@ -28,16 +36,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/FlutterOptimizelyLogger.swift b/ios/Classes/OptimizelyFlutterLogger.swift similarity index 92% rename from ios/Classes/FlutterOptimizelyLogger.swift rename to ios/Classes/OptimizelyFlutterLogger.swift index b397b79..1e64c5a 100644 --- a/ios/Classes/FlutterOptimizelyLogger.swift +++ b/ios/Classes/OptimizelyFlutterLogger.swift @@ -1,7 +1,7 @@ import Flutter import Optimizely -public class FlutterOptimizelyLogger: NSObject, OPTLogger { +public class OptimizelyFlutterLogger: NSObject, OPTLogger { public static var logLevel: OptimizelyLogLevel = .info private static let loggerChannel = FlutterMethodChannel( diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index 2c56690..17e9fe4 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -168,7 +168,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { // Check if custom logger is requested var logger: OPTLogger? if let useCustomLogger = parameters[RequestParameterKey.customLogger] as? Bool, useCustomLogger { - logger = FlutterOptimizelyLogger() + logger = OptimizelyFlutterLogger() } // Creating new instance diff --git a/lib/src/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index 9c2ff99..1e5ecf3 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -27,8 +27,6 @@ import 'package:optimizely_flutter_sdk/src/data_objects/get_vuid_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_response.dart'; import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; -import 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart'; -import 'package:optimizely_flutter_sdk/src/logger/logger_bridge.dart'; enum ListenerType { activate, track, decision, logEvent, projectConfigUpdate } From 8b401cac272279aa8f3c652d162651a67fecc92e Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Tue, 23 Sep 2025 23:36:55 +0600 Subject: [PATCH 06/15] 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 --- example/lib/custom_logger.dart | 3 -- ios/Classes/OptimizelyFlutterLogger.swift | 35 +++++++++++++++---- .../SwiftOptimizelyFlutterSdkPlugin.swift | 1 + lib/src/logger/flutter_logger.dart | 15 +------- lib/src/logger/logger_bridge.dart | 2 +- 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/example/lib/custom_logger.dart b/example/lib/custom_logger.dart index 9f5fb25..e8fa4e7 100644 --- a/example/lib/custom_logger.dart +++ b/example/lib/custom_logger.dart @@ -2,9 +2,6 @@ import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; import 'package:flutter/foundation.dart'; class CustomLogger implements OptimizelyLogger { - @override - OptimizelyLogLevel logLevel = OptimizelyLogLevel.debug; - @override void log(OptimizelyLogLevel level, String message) { if (kDebugMode) { diff --git a/ios/Classes/OptimizelyFlutterLogger.swift b/ios/Classes/OptimizelyFlutterLogger.swift index 1e64c5a..0154fc9 100644 --- a/ios/Classes/OptimizelyFlutterLogger.swift +++ b/ios/Classes/OptimizelyFlutterLogger.swift @@ -4,22 +4,43 @@ import Optimizely public class OptimizelyFlutterLogger: NSObject, OPTLogger { public static var logLevel: OptimizelyLogLevel = .info - private static let loggerChannel = FlutterMethodChannel( - name: "optimizely_flutter_sdk_logger", - binaryMessenger: SwiftOptimizelyFlutterSdkPlugin.registrar?.messenger() ?? FlutterEngine().binaryMessenger - ) + 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) { - // Ensure we're on the main thread when calling Flutter - DispatchQueue.main.async { - Self.loggerChannel.invokeMethod("log", arguments: [ + // 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 + } + + // Ensure logging happens on main thread as FlutterMethodChannel requires it + if Thread.isMainThread { + // Already on main thread + channel.invokeMethod("log", arguments: [ "level": level.rawValue, "message": message ]) + } else { + // Switch to main thread + DispatchQueue.main.sync { + channel.invokeMethod("log", arguments: [ + "level": level.rawValue, + "message": message + ]) + } } } } diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index 17e9fe4..20c22b2 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -112,6 +112,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 diff --git a/lib/src/logger/flutter_logger.dart b/lib/src/logger/flutter_logger.dart index 225b22f..0026dfc 100644 --- a/lib/src/logger/flutter_logger.dart +++ b/lib/src/logger/flutter_logger.dart @@ -1,26 +1,13 @@ import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'; abstract class OptimizelyLogger { - /// The log level for the logger - OptimizelyLogLevel get logLevel; - set logLevel(OptimizelyLogLevel level); - /// Log a message at a certain level void log(OptimizelyLogLevel level, String message); } class DefaultOptimizelyLogger implements OptimizelyLogger { - @override - OptimizelyLogLevel logLevel = OptimizelyLogLevel.info; - @override void log(OptimizelyLogLevel level, String message) { - if (_shouldLog(level)) { - print('[Optimizely ${level.name.toUpperCase()}] $message'); - } - } - - bool _shouldLog(OptimizelyLogLevel messageLevel) { - return messageLevel.index <= logLevel.index; + print('[Optimizely ${level.name}] $message'); } } diff --git a/lib/src/logger/logger_bridge.dart b/lib/src/logger/logger_bridge.dart index a0bf5e7..200643d 100644 --- a/lib/src/logger/logger_bridge.dart +++ b/lib/src/logger/logger_bridge.dart @@ -52,7 +52,7 @@ class LoggerBridge { if (_customLogger != null) { _customLogger!.log(level, message); } else { - print('[Optimizely ${level.name.toUpperCase()}] $message'); + print('[Optimizely ${level.name}] $message'); } } catch (e) { print('[LoggerBridge] Error processing log call: $e'); From bde2659cdd2faec7e5e0184ffdccc47778d784a0 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Sep 2025 15:32:00 +0600 Subject: [PATCH 07/15] 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 --- lib/src/logger/logger_bridge.dart | 25 ++ test/logger_test.dart | 466 ++++++++++++++++++++++++++++++ 2 files changed, 491 insertions(+) create mode 100644 test/logger_test.dart diff --git a/lib/src/logger/logger_bridge.dart b/lib/src/logger/logger_bridge.dart index 200643d..4f801fe 100644 --- a/lib/src/logger/logger_bridge.dart +++ b/lib/src/logger/logger_bridge.dart @@ -74,4 +74,29 @@ class LoggerBridge { 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/test/logger_test.dart b/test/logger_test.dart new file mode 100644 index 0000000..be8441a --- /dev/null +++ b/test/logger_test.dart @@ -0,0 +1,466 @@ +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 = 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 = 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 = 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 = 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 = 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 = MethodCall('log', { + 'level': 'invalid', + 'message': 'Test message' + }); + await LoggerBridge.handleMethodCallForTesting(methodCall); + + expect(testLogger.logs.isEmpty, isTrue); + + // Test with null level + methodCall = 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 = 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("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( + MethodCall('log', {'level': 1, 'message': 'First message'}) + ); + + expect(LoggerBridge.hasLogger(), isTrue); + expect(testLogger.logs.length, equals(1)); + + await LoggerBridge.handleMethodCallForTesting( + 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( + 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( + 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( + 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( + 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 6993bfc8c3505b9492eee74fa1269f3ee88d0df1 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Sep 2025 16:17:39 +0600 Subject: [PATCH 08/15] 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 --- ios/Classes/OptimizelyFlutterLogger.swift | 2 ++ ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/ios/Classes/OptimizelyFlutterLogger.swift b/ios/Classes/OptimizelyFlutterLogger.swift index 0154fc9..5ef559c 100644 --- a/ios/Classes/OptimizelyFlutterLogger.swift +++ b/ios/Classes/OptimizelyFlutterLogger.swift @@ -2,6 +2,8 @@ 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? diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index 20c22b2..0c7aabb 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -43,6 +43,10 @@ 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 loggerChannel = FlutterMethodChannel(name: OptimizelyFlutterLogger.LOGGER_CHANNEL, binaryMessenger: registrar.messenger()) + OptimizelyFlutterLogger.setChannel(loggerChannel) } /// Part of FlutterPlugin protocol to handle communication with flutter sdk From 9b26c398704484ccb870aac7994d629341f0469a Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Sep 2025 17:03:25 +0600 Subject: [PATCH 09/15] 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 --- ios/Classes/HelperClasses/Utils.swift | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/ios/Classes/HelperClasses/Utils.swift b/ios/Classes/HelperClasses/Utils.swift index 41b39c1..9fcaaf9 100644 --- a/ios/Classes/HelperClasses/Utils.swift +++ b/ios/Classes/HelperClasses/Utils.swift @@ -65,7 +65,10 @@ public class Utils: NSObject { "url" : url, "params" : logEvent as Any ] - SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.logEvent)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.logEvent, RequestParameterKey.notificationPayload: listenerDict]) + // Dispatch to main thread for Flutter method channel call. Platform channel messages must be sent on the platform thread to avoid data loss or crashes. + DispatchQueue.main.async { + SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.logEvent)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.logEvent, RequestParameterKey.notificationPayload: listenerDict]) + } } return listener @@ -94,7 +97,10 @@ public class Utils: NSObject { "attributes" : attributes as Any, "variation" : variation ] - SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.activate)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.activate, RequestParameterKey.notificationPayload: listenerDict]) + // Dispatch to main thread for Flutter method channel call + DispatchQueue.main.async { + SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.activate)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.activate, RequestParameterKey.notificationPayload: listenerDict]) + } } return listener } @@ -108,7 +114,10 @@ public class Utils: NSObject { "attributes" : attributes as Any, "decisionInfo": decisionInfo ] - SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.decision)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.decision, RequestParameterKey.notificationPayload: listenerDict]) + // Dispatch to main thread for Flutter method channel call + DispatchQueue.main.async { + SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.decision)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.decision, RequestParameterKey.notificationPayload: listenerDict]) + } } return listener } @@ -123,7 +132,9 @@ public class Utils: NSObject { "userId" : userId, // "event": event as Any, This is causing codec related exceptions on flutter side, need to debug ] - SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.track)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.track, RequestParameterKey.notificationPayload: listenerDict]) + DispatchQueue.main.async { + SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.track)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.track, RequestParameterKey.notificationPayload: listenerDict]) + } } return listener } From a0a9ca906998fd4ff07549d8eaa77356faec3740 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Sep 2025 20:43:48 +0600 Subject: [PATCH 10/15] 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 --- example/lib/main.dart | 9 +-------- lib/optimizely_flutter_sdk.dart | 2 +- lib/src/logger/flutter_logger.dart | 31 +++++++++++++++++++++++++++++- lib/src/logger/logger_bridge.dart | 16 +++++++-------- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 9591bbd..9b8f1eb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,13 +4,6 @@ import 'dart:math'; import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; import 'package:optimizely_flutter_sdk_example/custom_logger.dart'; -/** - * Logger TODO: - * Check thread safety - * Android logger fix - * Add unit test - */ - void main() { runApp(const MyApp()); } @@ -45,7 +38,7 @@ class _MyAppState extends State { defaultLogLevel: OptimizelyLogLevel.debug, defaultDecideOptions: defaultOptions, logger: customLogger, - ); + ); var response = await flutterSDK.initializeClient(); setState(() { diff --git a/lib/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index 1247f49..f792190 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -100,7 +100,7 @@ class OptimizelyFlutterSdk { if (logger != null) { setLogger(logger); } else { - print("Logger not provided."); + AppLogger.warning("Logger not provided."); } } diff --git a/lib/src/logger/flutter_logger.dart b/lib/src/logger/flutter_logger.dart index 0026dfc..331646a 100644 --- a/lib/src/logger/flutter_logger.dart +++ b/lib/src/logger/flutter_logger.dart @@ -8,6 +8,35 @@ abstract class OptimizelyLogger { class DefaultOptimizelyLogger implements OptimizelyLogger { @override void log(OptimizelyLogLevel level, String message) { - print('[Optimizely ${level.name}] $message'); + print('${level.name} $message'); + } +} + +class AppLogger { + static OptimizelyLogger _instance = DefaultOptimizelyLogger(); + + /// Get the current app logger instance + static OptimizelyLogger get instance => _instance; + + /// Reset to default logger + static void reset() { + _instance = DefaultOptimizelyLogger(); + } + + /// Convenience methods for direct logging + static void error(String message) { + _instance.log(OptimizelyLogLevel.error, message); + } + + static void warning(String message) { + _instance.log(OptimizelyLogLevel.warning, message); + } + + static void info(String message) { + _instance.log(OptimizelyLogLevel.info, message); + } + + static void debug(String message) { + _instance.log(OptimizelyLogLevel.debug, message); } } diff --git a/lib/src/logger/logger_bridge.dart b/lib/src/logger/logger_bridge.dart index 4f801fe..a88eb82 100644 --- a/lib/src/logger/logger_bridge.dart +++ b/lib/src/logger/logger_bridge.dart @@ -11,24 +11,24 @@ class LoggerBridge { /// Initialize the logger bridge to receive calls from native static void initialize(OptimizelyLogger? logger) { - print('[LoggerBridge] Initializing with logger: ${logger != null}'); + AppLogger.info('[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 { - print('[LoggerBridge] Received method call: ${call.method}'); + AppLogger.info('[LoggerBridge] Received method call: ${call.method}'); try { switch (call.method) { case 'log': await _handleLogCall(call); break; default: - print('[LoggerBridge] Unknown method call: ${call.method}'); + AppLogger.warning('[LoggerBridge] Unknown method call: ${call.method}'); } } catch (e) { - print('[LoggerBridge] Error handling method call: $e'); + AppLogger.error('[LoggerBridge] Error handling method call: $e'); } } @@ -41,21 +41,21 @@ class LoggerBridge { final message = args['message'] as String?; if (levelRawValue == null || message == null) { - print('[LoggerBridge] Warning: Missing level or message in log call'); + AppLogger.error('[LoggerBridge] Warning: Missing level or message in log call'); return; } final level = _convertLogLevel(levelRawValue); - print('[LoggerBridge] Processing log: level=$levelRawValue, message=$message'); + AppLogger.info('[LoggerBridge] Processing log: level=$levelRawValue, message=$message'); if (_customLogger != null) { _customLogger!.log(level, message); } else { - print('[Optimizely ${level.name}] $message'); + AppLogger.info('[Optimizely ${level.name}] $message'); } } catch (e) { - print('[LoggerBridge] Error processing log call: $e'); + AppLogger.error('[LoggerBridge] Error processing log call: $e'); } } From 8edc4318e2733ffba5acfa4867a6844a6f49538d Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Sep 2025 20:44:05 +0600 Subject: [PATCH 11/15] style: update comment in sendLogToFlutter method - Remove comparison to Swift's DispatchQueue.main.async --- .../optimizely_flutter_sdk/OptimizelyFlutterLogger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterLogger.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterLogger.java index 4c57f9b..adfa377 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterLogger.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterLogger.java @@ -352,7 +352,7 @@ private void sendLogToFlutter(int level, String message) { return; } - // Ensure we're on the main thread when calling Flutter (similar to Swift's DispatchQueue.main.async) + // Ensure we're on the main thread when calling Flutter Handler mainHandler = new Handler(Looper.getMainLooper()); mainHandler.post(() -> { Map arguments = new HashMap<>(); From 49ed2dc96bd122c26fa449169797b055dccf5cc5 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 25 Sep 2025 16:21:07 +0600 Subject: [PATCH 12/15] 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 --- lib/src/logger/logger_bridge.dart | 1 - test/logger_test.dart | 28 ++++++++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/src/logger/logger_bridge.dart b/lib/src/logger/logger_bridge.dart index a88eb82..1921b3f 100644 --- a/lib/src/logger/logger_bridge.dart +++ b/lib/src/logger/logger_bridge.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'package:flutter/services.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/optimizely_flutter_sdk.dart'; diff --git a/test/logger_test.dart b/test/logger_test.dart index be8441a..0cf4b5b 100644 --- a/test/logger_test.dart +++ b/test/logger_test.dart @@ -43,7 +43,7 @@ void main() { LoggerBridge.initialize(testLogger); // Simulate native log call by directly invoking the method handler - final methodCall = MethodCall('log', { + final methodCall = const MethodCall('log', { 'level': 3, // INFO 'message': 'Test log message from native' }); @@ -85,7 +85,7 @@ void main() { var testLogger = TestLogger(); LoggerBridge.initialize(testLogger); - final methodCall = MethodCall('log', null); + final methodCall = const MethodCall('log', null); await LoggerBridge.handleMethodCallForTesting(methodCall); expect(testLogger.logs.isEmpty, isTrue); @@ -95,7 +95,7 @@ void main() { var testLogger = TestLogger(); LoggerBridge.initialize(testLogger); - final methodCall = MethodCall('log', {}); + final methodCall = const MethodCall('log', {}); await LoggerBridge.handleMethodCallForTesting(methodCall); expect(testLogger.logs.isEmpty, isTrue); @@ -105,7 +105,7 @@ void main() { var testLogger = TestLogger(); LoggerBridge.initialize(testLogger); - final methodCall = MethodCall('log', { + final methodCall = const MethodCall('log', { 'message': 'Test message without level' }); await LoggerBridge.handleMethodCallForTesting(methodCall); @@ -117,7 +117,7 @@ void main() { var testLogger = TestLogger(); LoggerBridge.initialize(testLogger); - final methodCall = MethodCall('log', { + final methodCall = const MethodCall('log', { 'level': 3 }); await LoggerBridge.handleMethodCallForTesting(methodCall); @@ -130,7 +130,7 @@ void main() { LoggerBridge.initialize(testLogger); // Test with string level - var methodCall = MethodCall('log', { + var methodCall = const MethodCall('log', { 'level': 'invalid', 'message': 'Test message' }); @@ -139,7 +139,7 @@ void main() { expect(testLogger.logs.isEmpty, isTrue); // Test with null level - methodCall = MethodCall('log', { + methodCall = const MethodCall('log', { 'level': null, 'message': 'Test message' }); @@ -154,7 +154,7 @@ void main() { var testLogger = TestLogger(); LoggerBridge.initialize(testLogger); - final methodCall = MethodCall('unknownMethod', { + final methodCall = const MethodCall('unknownMethod', { 'level': 3, 'message': 'Test message' }); @@ -368,14 +368,14 @@ void main() { // Perform various operations await LoggerBridge.handleMethodCallForTesting( - MethodCall('log', {'level': 1, 'message': 'First message'}) + const MethodCall('log', {'level': 1, 'message': 'First message'}) ); expect(LoggerBridge.hasLogger(), isTrue); expect(testLogger.logs.length, equals(1)); await LoggerBridge.handleMethodCallForTesting( - MethodCall('log', {'level': 2, 'message': 'Second message'}) + const MethodCall('log', {'level': 2, 'message': 'Second message'}) ); expect(LoggerBridge.hasLogger(), isTrue); @@ -394,7 +394,7 @@ void main() { // Initialize with first logger LoggerBridge.initialize(testLogger1); await LoggerBridge.handleMethodCallForTesting( - MethodCall('log', {'level': 3, 'message': 'Message to logger 1'}) + const MethodCall('log', {'level': 3, 'message': 'Message to logger 1'}) ); expect(testLogger1.logs.length, equals(1)); @@ -403,7 +403,7 @@ void main() { // Replace with second logger LoggerBridge.initialize(testLogger2); await LoggerBridge.handleMethodCallForTesting( - MethodCall('log', {'level': 3, 'message': 'Message to logger 2'}) + const MethodCall('log', {'level': 3, 'message': 'Message to logger 2'}) ); expect(testLogger1.logs.length, equals(1)); // Unchanged @@ -418,7 +418,7 @@ void main() { LoggerBridge.initialize(testLogger); await LoggerBridge.handleMethodCallForTesting( - MethodCall('log', { + const MethodCall('log', { 'level': 3, 'message': '' }) @@ -452,7 +452,7 @@ void main() { // Test with double level - should fail gracefully await LoggerBridge.handleMethodCallForTesting( - MethodCall('log', { + const MethodCall('log', { 'level': 3.0, // Double instead of int 'message': 'Message with double level' }) From 7ae86de683d37e8294328322db05f7821019d204 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 25 Sep 2025 17:15:30 +0600 Subject: [PATCH 13/15] 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 --- example/lib/custom_logger.dart | 2 +- ios/Classes/HelperClasses/Utils.swift | 19 ++++--------------- ios/Classes/OptimizelyFlutterLogger.swift | 13 ++----------- .../SwiftOptimizelyFlutterSdkPlugin.swift | 6 +++++- 4 files changed, 12 insertions(+), 28 deletions(-) diff --git a/example/lib/custom_logger.dart b/example/lib/custom_logger.dart index e8fa4e7..e031fff 100644 --- a/example/lib/custom_logger.dart +++ b/example/lib/custom_logger.dart @@ -5,7 +5,7 @@ class CustomLogger implements OptimizelyLogger { @override void log(OptimizelyLogLevel level, String message) { if (kDebugMode) { - print('[Flutter LOGGER] ${level.name}: $message'); + print('[OPTIMIZELY] ${level.name.toUpperCase()}: $message'); } } } diff --git a/ios/Classes/HelperClasses/Utils.swift b/ios/Classes/HelperClasses/Utils.swift index 9fcaaf9..41b39c1 100644 --- a/ios/Classes/HelperClasses/Utils.swift +++ b/ios/Classes/HelperClasses/Utils.swift @@ -65,10 +65,7 @@ public class Utils: NSObject { "url" : url, "params" : logEvent as Any ] - // Dispatch to main thread for Flutter method channel call. Platform channel messages must be sent on the platform thread to avoid data loss or crashes. - DispatchQueue.main.async { - SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.logEvent)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.logEvent, RequestParameterKey.notificationPayload: listenerDict]) - } + SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.logEvent)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.logEvent, RequestParameterKey.notificationPayload: listenerDict]) } return listener @@ -97,10 +94,7 @@ public class Utils: NSObject { "attributes" : attributes as Any, "variation" : variation ] - // Dispatch to main thread for Flutter method channel call - DispatchQueue.main.async { - SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.activate)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.activate, RequestParameterKey.notificationPayload: listenerDict]) - } + SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.activate)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.activate, RequestParameterKey.notificationPayload: listenerDict]) } return listener } @@ -114,10 +108,7 @@ public class Utils: NSObject { "attributes" : attributes as Any, "decisionInfo": decisionInfo ] - // Dispatch to main thread for Flutter method channel call - DispatchQueue.main.async { - SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.decision)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.decision, RequestParameterKey.notificationPayload: listenerDict]) - } + SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.decision)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.decision, RequestParameterKey.notificationPayload: listenerDict]) } return listener } @@ -132,9 +123,7 @@ public class Utils: NSObject { "userId" : userId, // "event": event as Any, This is causing codec related exceptions on flutter side, need to debug ] - DispatchQueue.main.async { - SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.track)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.track, RequestParameterKey.notificationPayload: listenerDict]) - } + SwiftOptimizelyFlutterSdkPlugin.channel.invokeMethod("\(NotificationType.track)CallbackListener", arguments: [RequestParameterKey.sdkKey: sdkKey, RequestParameterKey.notificationId: id, RequestParameterKey.notificationType: NotificationType.track, RequestParameterKey.notificationPayload: listenerDict]) } return listener } diff --git a/ios/Classes/OptimizelyFlutterLogger.swift b/ios/Classes/OptimizelyFlutterLogger.swift index 5ef559c..7b9217a 100644 --- a/ios/Classes/OptimizelyFlutterLogger.swift +++ b/ios/Classes/OptimizelyFlutterLogger.swift @@ -28,21 +28,12 @@ public class OptimizelyFlutterLogger: NSObject, OPTLogger { return } - // Ensure logging happens on main thread as FlutterMethodChannel requires it - if Thread.isMainThread { - // Already on main thread + // 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 ]) - } else { - // Switch to main thread - DispatchQueue.main.sync { - channel.invokeMethod("log", arguments: [ - "level": level.rawValue, - "message": message - ]) - } } } } diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index 0c7aabb..9dcc1a6 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -45,7 +45,11 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { registrar.addMethodCallDelegate(instance, channel: channel) // Separate logger channel for outgoing log calls - let loggerChannel = FlutterMethodChannel(name: OptimizelyFlutterLogger.LOGGER_CHANNEL, binaryMessenger: registrar.messenger()) + let taskQueue = registrar.messenger().makeBackgroundTaskQueue?() + let loggerChannel = FlutterMethodChannel(name: OptimizelyFlutterLogger.LOGGER_CHANNEL, + binaryMessenger: registrar.messenger(), + codec: FlutterStandardMethodCodec.sharedInstance(), + taskQueue: taskQueue) OptimizelyFlutterLogger.setChannel(loggerChannel) } From e6404aaac3b10b0f0b696dd3f0a9af56c118227d Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 25 Sep 2025 17:51:07 +0600 Subject: [PATCH 14/15] 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 --- lib/optimizely_flutter_sdk.dart | 2 +- lib/src/logger/flutter_logger.dart | 42 +++++++++--------------------- lib/src/logger/logger_bridge.dart | 16 ++++++------ 3 files changed, 22 insertions(+), 38 deletions(-) diff --git a/lib/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index f792190..9a5ed5b 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -100,7 +100,7 @@ class OptimizelyFlutterSdk { if (logger != null) { setLogger(logger); } else { - AppLogger.warning("Logger not provided."); + logWarning("Logger not provided."); } } diff --git a/lib/src/logger/flutter_logger.dart b/lib/src/logger/flutter_logger.dart index 331646a..ad3ec67 100644 --- a/lib/src/logger/flutter_logger.dart +++ b/lib/src/logger/flutter_logger.dart @@ -8,35 +8,19 @@ abstract class OptimizelyLogger { class DefaultOptimizelyLogger implements OptimizelyLogger { @override void log(OptimizelyLogLevel level, String message) { - print('${level.name} $message'); + print('[OPTIMIZELY] [${level.name.toUpperCase()}]: $message'); } } -class AppLogger { - static OptimizelyLogger _instance = DefaultOptimizelyLogger(); - - /// Get the current app logger instance - static OptimizelyLogger get instance => _instance; - - /// Reset to default logger - static void reset() { - _instance = DefaultOptimizelyLogger(); - } - - /// Convenience methods for direct logging - static void error(String message) { - _instance.log(OptimizelyLogLevel.error, message); - } - - static void warning(String message) { - _instance.log(OptimizelyLogLevel.warning, message); - } - - static void info(String message) { - _instance.log(OptimizelyLogLevel.info, message); - } - - static void debug(String message) { - _instance.log(OptimizelyLogLevel.debug, 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 index 1921b3f..9c8dba7 100644 --- a/lib/src/logger/logger_bridge.dart +++ b/lib/src/logger/logger_bridge.dart @@ -10,24 +10,24 @@ class LoggerBridge { /// Initialize the logger bridge to receive calls from native static void initialize(OptimizelyLogger? logger) { - AppLogger.info('[LoggerBridge] Initializing with logger: ${logger != null}'); + 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 { - AppLogger.info('[LoggerBridge] Received method call: ${call.method}'); + logInfo('[LoggerBridge] Received method call: ${call.method}'); try { switch (call.method) { case 'log': await _handleLogCall(call); break; default: - AppLogger.warning('[LoggerBridge] Unknown method call: ${call.method}'); + logWarning('[LoggerBridge] Unknown method call: ${call.method}'); } } catch (e) { - AppLogger.error('[LoggerBridge] Error handling method call: $e'); + logError('[LoggerBridge] Error handling method call: $e'); } } @@ -40,21 +40,21 @@ class LoggerBridge { final message = args['message'] as String?; if (levelRawValue == null || message == null) { - AppLogger.error('[LoggerBridge] Warning: Missing level or message in log call'); + logError('[LoggerBridge] Warning: Missing level or message in log call'); return; } final level = _convertLogLevel(levelRawValue); - AppLogger.info('[LoggerBridge] Processing log: level=$levelRawValue, message=$message'); + logInfo('[LoggerBridge] Processing log: level=$levelRawValue, message=$message'); if (_customLogger != null) { _customLogger!.log(level, message); } else { - AppLogger.info('[Optimizely ${level.name}] $message'); + logInfo('[Optimizely ${level.name}] $message'); } } catch (e) { - AppLogger.error('[LoggerBridge] Error processing log call: $e'); + logError('[LoggerBridge] Error processing log call: $e'); } } From c2f1507a1c6ead8723a553379bd70c4c05b36fdc Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 25 Sep 2025 18:52:06 +0600 Subject: [PATCH 15/15] 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 --- test/logger_test.dart | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/logger_test.dart b/test/logger_test.dart index 0cf4b5b..8c0161e 100644 --- a/test/logger_test.dart +++ b/test/logger_test.dart @@ -244,7 +244,47 @@ void main() { }, 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();