Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
be95ef5
build: add Optimizely SDK logger classes
muzahidul-opti Sep 19, 2025
9b4c6fe
fix: resolve logging inconsistencies
muzahidul-opti Sep 19, 2025
2f88840
refactor: update logger imports
muzahidul-opti Sep 19, 2025
6336b0c
feat: add custom logger implementation
muzahidul-opti Sep 19, 2025
40cfdaa
refactor: rename logger classes in Android and iOS
muzahidul-opti Sep 19, 2025
8b401ca
feat: update logging behavior for Optimizely SDK
muzahidul-opti Sep 23, 2025
bde2659
feat: add methods and tests for logger state management
muzahidul-opti Sep 24, 2025
6993bfc
feat: add separate logger channel for outgoing log calls
muzahidul-opti Sep 24, 2025
9b26c39
refactor: improve main thread dispatch for Flutter method channel calls
muzahidul-opti Sep 24, 2025
a0a9ca9
chore: clean up logger implementation
muzahidul-opti Sep 24, 2025
8edc431
style: update comment in sendLogToFlutter method
muzahidul-opti Sep 24, 2025
49ed2dc
chore: remove unused import statement
muzahidul-opti Sep 25, 2025
7ae86de
chore: update log messages and method channel handling
muzahidul-opti Sep 25, 2025
e6404aa
refactor: enhance logging functionalities
muzahidul-opti Sep 25, 2025
c2f1507
test: add global logging functions test cases
muzahidul-opti Sep 25, 2025
4b54235
Merge branch 'master' into muzahid/custom-looger
muzahidul-opti Oct 9, 2025
387adc5
chore: remove custom logger functionality
muzahidul-opti Oct 10, 2025
675e3cd
refactor: rename customLogger to useCustomLogger
muzahidul-opti Oct 22, 2025
92256ae
clean up
muzahidul-opti Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import com.optimizely.ab.UnknownEventTypeException;
import com.optimizely.ab.android.event_handler.DefaultEventHandler;
import com.optimizely.ab.android.sdk.OptimizelyClient;

import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -187,6 +186,7 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N
if (enableVuid) {
optimizelyManagerBuilder.withVuidEnabled();
}

OptimizelyManager optimizelyManager = optimizelyManagerBuilder.build(context);

optimizelyManager.initialize(context, null, (OptimizelyClient client) -> {
Expand Down
11 changes: 11 additions & 0 deletions example/lib/custom_logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart';
import 'package:flutter/foundation.dart';

class CustomLogger implements OptimizelyLogger {
@override
void log(OptimizelyLogLevel level, String message) {
if (kDebugMode) {
print('[OPTIMIZELY] ${level.name.toUpperCase()}: $message');
}
}
}
9 changes: 7 additions & 2 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:math';
import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart';
import 'package:optimizely_flutter_sdk_example/custom_logger.dart';

void main() {
runApp(const MyApp());
Expand All @@ -28,16 +29,20 @@ class _MyAppState extends State<MyApp> {
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();
Expand Down
1 change: 1 addition & 0 deletions ios/Classes/HelperClasses/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ struct RequestParameterKey {
static let reasons = "reasons"
static let decideOptions = "optimizelyDecideOption"
static let defaultLogLevel = "defaultLogLevel"
static let useCustomLogger = "useCustomLogger"
static let eventBatchSize = "eventBatchSize"
static let eventTimeInterval = "eventTimeInterval"
static let eventMaxQueueSize = "eventMaxQueueSize"
Expand Down
39 changes: 39 additions & 0 deletions ios/Classes/OptimizelyFlutterLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Flutter
import Optimizely

public class OptimizelyFlutterLogger: NSObject, OPTLogger {
static var LOGGER_CHANNEL: String = "optimizely_flutter_sdk_logger";

public static var logLevel: OptimizelyLogLevel = .info

private static var loggerChannel: FlutterMethodChannel?

public required override init() {
super.init()
}

public static func setChannel(_ channel: FlutterMethodChannel) {
loggerChannel = channel
}

public func log(level: OptimizelyLogLevel, message: String) {
// Early return if level check fails
guard level.rawValue <= OptimizelyFlutterLogger.logLevel.rawValue else {
return
}

// Ensure we have a valid channel
guard let channel = Self.loggerChannel else {
print("[OptimizelyFlutterLogger] ERROR: No logger channel available!")
return
}

// https://docs.flutter.dev/platform-integration/platform-channels#jumping-to-the-main-thread-in-ios
DispatchQueue.main.async {
channel.invokeMethod("log", arguments: [
"level": level.rawValue,
"message": message
])
}
}
}
19 changes: 19 additions & 0 deletions ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
channel = FlutterMethodChannel(name: "optimizely_flutter_sdk", binaryMessenger: registrar.messenger())
let instance = SwiftOptimizelyFlutterSdkPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)

// Separate logger channel for outgoing log calls
let taskQueue = registrar.messenger().makeBackgroundTaskQueue?()
let loggerChannel = FlutterMethodChannel(name: OptimizelyFlutterLogger.LOGGER_CHANNEL,
binaryMessenger: registrar.messenger(),
codec: FlutterStandardMethodCodec.sharedInstance(),
taskQueue: taskQueue)
OptimizelyFlutterLogger.setChannel(loggerChannel)
}

/// Part of FlutterPlugin protocol to handle communication with flutter sdk
Expand Down Expand Up @@ -110,6 +118,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
var defaultLogLevel = OptimizelyLogLevel.info
if let logLevel = parameters[RequestParameterKey.defaultLogLevel] as? String {
defaultLogLevel = Utils.getDefaultLogLevel(logLevel)
OptimizelyFlutterLogger.logLevel = defaultLogLevel
}

// SDK Settings Default Values
Expand Down Expand Up @@ -163,9 +172,19 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
notificationIdsTracker.removeValue(forKey: sdkKey)
optimizelyClientsTracker.removeValue(forKey: sdkKey)

// Check if custom logger is requested
var logger: OPTLogger?
if let useCustomLogger = parameters[RequestParameterKey.useCustomLogger] as? Bool, useCustomLogger {
// OptimizelyFlutterLogger bridges iOS logs to Flutter via Method Channel
// When useCustomLogger = true:
// iOS SDK log → OptimizelyFlutterLogger → Flutter Method Channel → Flutter console
logger = OptimizelyFlutterLogger()
}

// Creating new instance
let optimizelyInstance = OptimizelyClient(
sdkKey:sdkKey,
logger:logger,
eventDispatcher: eventDispatcher,
datafileHandler: datafileHandler,
periodicDownloadInterval: datafilePeriodicDownloadInterval,
Expand Down
51 changes: 37 additions & 14 deletions lib/optimizely_flutter_sdk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_respon
import 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart';
import 'package:optimizely_flutter_sdk/src/user_context/optimizely_user_context.dart';
import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart';
import 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart';
import 'package:optimizely_flutter_sdk/src/logger/logger_bridge.dart';

export 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart'
show ClientPlatform, ListenerType;
Expand All @@ -53,6 +55,8 @@ export 'package:optimizely_flutter_sdk/src/data_objects/datafile_options.dart'
show DatafileHostOptions;
export 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'
show OptimizelyLogLevel;
export 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart'
show OptimizelyLogger;

/// The main client class for the Optimizely Flutter SDK.
///
Expand All @@ -68,20 +72,37 @@ class OptimizelyFlutterSdk {
final Set<OptimizelyDecideOption> _defaultDecideOptions;
final OptimizelyLogLevel _defaultLogLevel;
final SDKSettings _sdkSettings;
static OptimizelyLogger? _customLogger;
/// Set a custom logger for the SDK
static void setLogger(OptimizelyLogger logger) {
_customLogger = logger;
LoggerBridge.initialize(logger);
}
/// Get the current logger
static OptimizelyLogger? get logger {
return _customLogger;
}
OptimizelyFlutterSdk(this._sdkKey,
{EventOptions eventOptions = const EventOptions(),
int datafilePeriodicDownloadInterval =
10 * 60, // Default time interval in seconds
Map<ClientPlatform, DatafileHostOptions> datafileHostOptions = const {},
Set<OptimizelyDecideOption> 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<ClientPlatform, DatafileHostOptions> datafileHostOptions = const {},
Set<OptimizelyDecideOption> defaultDecideOptions = const {},
OptimizelyLogLevel defaultLogLevel = OptimizelyLogLevel.info,
SDKSettings sdkSettings = const SDKSettings(),
OptimizelyLogger? logger})
: _eventOptions = eventOptions,
_datafilePeriodicDownloadInterval = datafilePeriodicDownloadInterval,
_datafileHostOptions = datafileHostOptions,
_defaultDecideOptions = defaultDecideOptions,
_defaultLogLevel = defaultLogLevel,
_sdkSettings = sdkSettings {
// Set the logger if provided
if (logger != null) {
setLogger(logger);
} else {
logWarning("Logger not provided.");
}
}

/// Starts Optimizely SDK (Synchronous) with provided sdkKey.
Future<BaseResponse> initializeClient() async {
Expand All @@ -92,7 +113,9 @@ class OptimizelyFlutterSdk {
_datafileHostOptions,
_defaultDecideOptions,
_defaultLogLevel,
_sdkSettings);
_sdkSettings,
_customLogger
);
}

/// Use the activate method to start an experiment.
Expand Down
26 changes: 26 additions & 0 deletions lib/src/logger/flutter_logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart';

abstract class OptimizelyLogger {
/// Log a message at a certain level
void log(OptimizelyLogLevel level, String message);
}

class DefaultOptimizelyLogger implements OptimizelyLogger {
@override
void log(OptimizelyLogLevel level, String message) {
print('[OPTIMIZELY] [${level.name.toUpperCase()}]: $message');
}
}

/// App logger instance
final _appLogger = DefaultOptimizelyLogger();

/// App logging functions
void logError(String message) =>
_appLogger.log(OptimizelyLogLevel.error, message);
void logWarning(String message) =>
_appLogger.log(OptimizelyLogLevel.warning, message);
void logInfo(String message) =>
_appLogger.log(OptimizelyLogLevel.info, message);
void logDebug(String message) =>
_appLogger.log(OptimizelyLogLevel.debug, message);
98 changes: 98 additions & 0 deletions lib/src/logger/logger_bridge.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart';
import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart';

class LoggerBridge {
static const MethodChannel _loggerChannel =
MethodChannel('optimizely_flutter_sdk_logger');
static OptimizelyLogger? _customLogger;

/// Initialize the logger bridge to receive calls from native
static void initialize(OptimizelyLogger? logger) {
logInfo('[LoggerBridge] Initializing with logger: ${logger != null}');
_customLogger = logger;
_loggerChannel.setMethodCallHandler(_handleMethodCall);
}

/// Handle incoming method calls from native Swift/Java code
static Future<void> _handleMethodCall(MethodCall call) async {
try {
switch (call.method) {
case 'log':
await _handleLogCall(call);
break;
default:
logWarning('[LoggerBridge] Unknown method call: ${call.method}');
}
} catch (e) {
logError('[LoggerBridge] Error handling method call: $e');
}
}

/// Process the log call from Swift/Java
static Future<void> _handleLogCall(MethodCall call) async {
try {
final args = Map<String, dynamic>.from(call.arguments ?? {});

final levelRawValue = args['level'] as int?;
final message = args['message'] as String?;

if (levelRawValue == null || message == null) {
logError('[LoggerBridge] Warning: Missing level or message in log call');
return;
}

final level = _convertLogLevel(levelRawValue);

if (_customLogger != null) {
_customLogger!.log(level, message);
} else {
logInfo('[Optimizely ${level.name}] $message');
}
} catch (e) {
logError('[LoggerBridge] Error processing log call: $e');
}
}

/// Convert native log level to Flutter enum
static OptimizelyLogLevel _convertLogLevel(int rawValue) {
switch (rawValue) {
case 1:
return OptimizelyLogLevel.error;
case 2:
return OptimizelyLogLevel.warning;
case 3:
return OptimizelyLogLevel.info;
case 4:
return OptimizelyLogLevel.debug;
default:
return OptimizelyLogLevel.info;
}
}

/// Expose convertLogLevel
static OptimizelyLogLevel convertLogLevel(int rawValue) {
return _convertLogLevel(rawValue);
}

/// Check if a custom logger is set
static bool hasLogger() {
return _customLogger != null;
}

/// Get the current logger
static OptimizelyLogger? getCurrentLogger() {
return _customLogger;
}

/// Reset logger state
static void reset() {
_customLogger = null;
}

/// Simulate method calls
static Future<void> handleMethodCallForTesting(MethodCall call) async {
await _handleMethodCall(call);
}
}
4 changes: 3 additions & 1 deletion lib/src/optimizely_client_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ class OptimizelyClientWrapper {
Map<ClientPlatform, DatafileHostOptions> datafileHostOptions,
Set<OptimizelyDecideOption> defaultDecideOptions,
OptimizelyLogLevel defaultLogLevel,
SDKSettings sdkSettings) async {
SDKSettings sdkSettings,
OptimizelyLogger? logger) async {
_channel.setMethodCallHandler(methodCallHandler);
final convertedOptions = Utils.convertDecideOptions(defaultDecideOptions);
final convertedLogLevel = Utils.convertLogLevel(defaultLogLevel);
Expand All @@ -79,6 +80,7 @@ class OptimizelyClientWrapper {
Constants.eventBatchSize: eventOptions.batchSize,
Constants.eventTimeInterval: eventOptions.timeInterval,
Constants.eventMaxQueueSize: eventOptions.maxQueueSize,
Constants.useCustomLogger: logger != null,
};

// Odp Request params
Expand Down
1 change: 1 addition & 0 deletions lib/src/utils/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class Constants {
static const String optimizelyDecideOption = "optimizelyDecideOption";
static const String optimizelySegmentOption = "optimizelySegmentOption";
static const String optimizelySdkSettings = "optimizelySdkSettings";
static const String useCustomLogger = 'useCustomLogger';
static const String defaultLogLevel = "defaultLogLevel";
static const String payload = "payload";
static const String value = "value";
Expand Down
Loading