Skip to content

Commit c47b746

Browse files
[FSSDK-11853] add swift logger support (#88)
* build: add Optimizely SDK logger classes - Add FlutterOptimizelyLogger class implementing OPTLogger protocol - Add constants for customLogger and loggerChannel - Implement FlutterMethodChannel for invoking log method - Add logger field to OptimizelyFlutterSdk constructor - Initialize custom logger in OptimizelyClientWrapper - Create LoggerBridge to handle log method calls - Implement OptimizelyLogger interface and DefaultOptimizelyLogger class * fix: resolve logging inconsistencies - Update custom logger initialization in OptimizelyFlutterSdkPlugin - Adjust logger channel setup in onDetachedFromEngine - Refactor LoggerBridge to handle log calls from native Swift/Java code properly - Ensure proper logging when no custom logger is set * refactor: update logger imports - Rename 'OptimizelyLogger.dart' to 'flutter_logger.dart' - Rename 'LoggerBridge.dart' to 'logger_bridge.dart' - Modify imports in 'optimizely_client_wrapper.dart' to reflect changes in logger files * feat: add custom logger implementation - Implement a custom logger class - Define logLevel property with default debug level - Define log method to print log messages with custom format * refactor: rename logger classes in Android and iOS - Rename FlutterOptimizelyLogger to OptimizelyFlutterLogger in Android - Rename FlutterOptimizelyLogger to OptimizelyFlutterLogger in iOS * feat: update logging behavior for Optimizely SDK - Remove unused log level property in CustomLogger - Implement channel setter method in OptimizelyFlutterLogger - Add guard clauses for levels and logger channel availability in log method - Update channel invocation to happen on main thread - Set log level in SwiftOptimizelyFlutterSdkPlugin based on parameters - Simplify DefaultOptimizelyLogger log method - Update log message formatting in logger bridge for consistency * feat: add methods and tests for logger state management - Add methods to expose converting log level, checking if a custom logger is set, retrieving the current logger, and resetting the logger state - Implement a method for simulating method calls - Add tests for maintaining logger state across multiple operations - Include a test for handling logger replacement - Create tests for edge cases including handling empty messages and special characters * feat: add separate logger channel for outgoing log calls - Define LOGGER_CHANNEL constant for OptimizelyFlutterLogger class - Set up separate FlutterMethodChannel for outgoing log calls - Update SwiftOptimizelyFlutterSdkPlugin to use the new logger channel * refactor: improve main thread dispatch for Flutter method channel calls - Add DispatchQueue.main.async for each method call to ensure platform channel messages are sent on the correct thread * chore: clean up logger implementation - Remove unnecessary comments and TODOs - Replace direct print statements with AppLogger methods - Refactor AppLogger to enhance flexibility and ease of use * style: update comment in sendLogToFlutter method - Remove comparison to Swift's DispatchQueue.main.async * chore: remove unused import statement - Remove import statement for 'log_level' that is no longer used - Update import paths for 'flutter_logger' and 'optimizely_flutter_sdk' test: update test cases in logger_test.dart - Update test cases to use 'const MethodCall' for creating method calls - Change null and empty arguments to be created and handled correctly - Fix missing level or message argument handling scenarios - Improve handling of invalid level data types in method calls docs: add comments to improve code clarity in logger_test.dart - Add comments explaining the purpose of each test group and case - Include comments for the different scenarios being tested in each case * chore: update log messages and method channel handling - Update log message format in CustomLogger class - Refactor main thread dispatch logic for method channel calls in Utils.swift - Enhance error handling and main thread dispatch in OptimizelyFlutterLogger.swift - Modify method channel creation and task queue handling in SwiftOptimizelyFlutterSdkPlugin.swift * refactor: enhance logging functionalities - Replace usage of static AppLogger class with separate logging functions - Introduce individual logger functions for error, warning, info, and debug levels - Create a default stand-alone logger instance to handle logging operations * test: add global logging functions test cases - Test calling global logging functions without errors - Test handling empty messages in global functions - Test handling special characters in global functions - Test handling rapid calls to global functions * chore: remove custom logger functionality - Remove code related to setting up and using a custom logger in OptimizelyFlutterClient.java - Delete OptimizelyFlutterLogger.java file as it is no longer needed - Update references to the logger channel in OptimizelyFlutterSdkPlugin.java * refactor: rename customLogger to useCustomLogger - Update 'Constants.swift' to change 'customLogger' to 'useCustomLogger' - Update 'SwiftOptimizelyFlutterSdkPlugin.swift' to change 'customLogger' to 'useCustomLogger' - Update 'optimizely_flutter_sdk.dart' to change 'logger' parameter to 'OptimizelyLogger' - Update 'optimizely_client_wrapper.dart' to change 'customLogger' to 'useCustomLogger' - Update 'constants.dart' to change 'customLogger' to 'useCustomLogger' * clean up
1 parent 34d2a3b commit c47b746

File tree

12 files changed

+749
-18
lines changed

12 files changed

+749
-18
lines changed

android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import com.optimizely.ab.UnknownEventTypeException;
2626
import com.optimizely.ab.android.event_handler.DefaultEventHandler;
2727
import com.optimizely.ab.android.sdk.OptimizelyClient;
28-
2928
import java.util.HashMap;
3029
import java.util.Map;
3130

@@ -187,6 +186,7 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N
187186
if (enableVuid) {
188187
optimizelyManagerBuilder.withVuidEnabled();
189188
}
189+
190190
OptimizelyManager optimizelyManager = optimizelyManagerBuilder.build(context);
191191

192192
optimizelyManager.initialize(context, null, (OptimizelyClient client) -> {

example/lib/custom_logger.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart';
2+
import 'package:flutter/foundation.dart';
3+
4+
class CustomLogger implements OptimizelyLogger {
5+
@override
6+
void log(OptimizelyLogLevel level, String message) {
7+
if (kDebugMode) {
8+
print('[OPTIMIZELY] ${level.name.toUpperCase()}: $message');
9+
}
10+
}
11+
}

example/lib/main.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22
import 'dart:async';
33
import 'dart:math';
44
import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart';
5+
import 'package:optimizely_flutter_sdk_example/custom_logger.dart';
56

67
void main() {
78
runApp(const MyApp());
@@ -28,16 +29,20 @@ class _MyAppState extends State<MyApp> {
2829
OptimizelyDecideOption.includeReasons,
2930
OptimizelyDecideOption.excludeVariables
3031
};
32+
final customLogger = CustomLogger();
33+
3134
var flutterSDK = OptimizelyFlutterSdk("X9mZd2WDywaUL9hZXyh9A",
3235
datafilePeriodicDownloadInterval: 10 * 60,
3336
eventOptions: const EventOptions(
3437
batchSize: 1, timeInterval: 60, maxQueueSize: 10000),
3538
defaultLogLevel: OptimizelyLogLevel.debug,
36-
defaultDecideOptions: defaultOptions);
39+
defaultDecideOptions: defaultOptions,
40+
logger: customLogger,
41+
);
3742
var response = await flutterSDK.initializeClient();
3843

3944
setState(() {
40-
uiResponse = "Optimizely Client initialized: ${response.success} ";
45+
uiResponse = "[Optimizely] Client initialized: ${response.success} ";
4146
});
4247

4348
var rng = Random();

ios/Classes/HelperClasses/Constants.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ struct RequestParameterKey {
9191
static let reasons = "reasons"
9292
static let decideOptions = "optimizelyDecideOption"
9393
static let defaultLogLevel = "defaultLogLevel"
94+
static let useCustomLogger = "useCustomLogger"
9495
static let eventBatchSize = "eventBatchSize"
9596
static let eventTimeInterval = "eventTimeInterval"
9697
static let eventMaxQueueSize = "eventMaxQueueSize"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Flutter
2+
import Optimizely
3+
4+
public class OptimizelyFlutterLogger: NSObject, OPTLogger {
5+
static var LOGGER_CHANNEL: String = "optimizely_flutter_sdk_logger";
6+
7+
public static var logLevel: OptimizelyLogLevel = .info
8+
9+
private static var loggerChannel: FlutterMethodChannel?
10+
11+
public required override init() {
12+
super.init()
13+
}
14+
15+
public static func setChannel(_ channel: FlutterMethodChannel) {
16+
loggerChannel = channel
17+
}
18+
19+
public func log(level: OptimizelyLogLevel, message: String) {
20+
// Early return if level check fails
21+
guard level.rawValue <= OptimizelyFlutterLogger.logLevel.rawValue else {
22+
return
23+
}
24+
25+
// Ensure we have a valid channel
26+
guard let channel = Self.loggerChannel else {
27+
print("[OptimizelyFlutterLogger] ERROR: No logger channel available!")
28+
return
29+
}
30+
31+
// https://docs.flutter.dev/platform-integration/platform-channels#jumping-to-the-main-thread-in-ios
32+
DispatchQueue.main.async {
33+
channel.invokeMethod("log", arguments: [
34+
"level": level.rawValue,
35+
"message": message
36+
])
37+
}
38+
}
39+
}

ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin {
4141
channel = FlutterMethodChannel(name: "optimizely_flutter_sdk", binaryMessenger: registrar.messenger())
4242
let instance = SwiftOptimizelyFlutterSdkPlugin()
4343
registrar.addMethodCallDelegate(instance, channel: channel)
44+
45+
// Separate logger channel for outgoing log calls
46+
let taskQueue = registrar.messenger().makeBackgroundTaskQueue?()
47+
let loggerChannel = FlutterMethodChannel(name: OptimizelyFlutterLogger.LOGGER_CHANNEL,
48+
binaryMessenger: registrar.messenger(),
49+
codec: FlutterStandardMethodCodec.sharedInstance(),
50+
taskQueue: taskQueue)
51+
OptimizelyFlutterLogger.setChannel(loggerChannel)
4452
}
4553

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

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

175+
// Check if custom logger is requested
176+
var logger: OPTLogger?
177+
if let useCustomLogger = parameters[RequestParameterKey.useCustomLogger] as? Bool, useCustomLogger {
178+
// OptimizelyFlutterLogger bridges iOS logs to Flutter via Method Channel
179+
// When useCustomLogger = true:
180+
// iOS SDK log → OptimizelyFlutterLogger → Flutter Method Channel → Flutter console
181+
logger = OptimizelyFlutterLogger()
182+
}
183+
166184
// Creating new instance
167185
let optimizelyInstance = OptimizelyClient(
168186
sdkKey:sdkKey,
187+
logger:logger,
169188
eventDispatcher: eventDispatcher,
170189
datafileHandler: datafileHandler,
171190
periodicDownloadInterval: datafilePeriodicDownloadInterval,

lib/optimizely_flutter_sdk.dart

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_respon
2828
import 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart';
2929
import 'package:optimizely_flutter_sdk/src/user_context/optimizely_user_context.dart';
3030
import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart';
31+
import 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart';
32+
import 'package:optimizely_flutter_sdk/src/logger/logger_bridge.dart';
3133

3234
export 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart'
3335
show ClientPlatform, ListenerType;
@@ -53,6 +55,8 @@ export 'package:optimizely_flutter_sdk/src/data_objects/datafile_options.dart'
5355
show DatafileHostOptions;
5456
export 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart'
5557
show OptimizelyLogLevel;
58+
export 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart'
59+
show OptimizelyLogger;
5660

5761
/// The main client class for the Optimizely Flutter SDK.
5862
///
@@ -68,20 +72,37 @@ class OptimizelyFlutterSdk {
6872
final Set<OptimizelyDecideOption> _defaultDecideOptions;
6973
final OptimizelyLogLevel _defaultLogLevel;
7074
final SDKSettings _sdkSettings;
75+
static OptimizelyLogger? _customLogger;
76+
/// Set a custom logger for the SDK
77+
static void setLogger(OptimizelyLogger logger) {
78+
_customLogger = logger;
79+
LoggerBridge.initialize(logger);
80+
}
81+
/// Get the current logger
82+
static OptimizelyLogger? get logger {
83+
return _customLogger;
84+
}
7185
OptimizelyFlutterSdk(this._sdkKey,
72-
{EventOptions eventOptions = const EventOptions(),
73-
int datafilePeriodicDownloadInterval =
74-
10 * 60, // Default time interval in seconds
75-
Map<ClientPlatform, DatafileHostOptions> datafileHostOptions = const {},
76-
Set<OptimizelyDecideOption> defaultDecideOptions = const {},
77-
OptimizelyLogLevel defaultLogLevel = OptimizelyLogLevel.info,
78-
SDKSettings sdkSettings = const SDKSettings()})
79-
: _eventOptions = eventOptions,
80-
_datafilePeriodicDownloadInterval = datafilePeriodicDownloadInterval,
81-
_datafileHostOptions = datafileHostOptions,
82-
_defaultDecideOptions = defaultDecideOptions,
83-
_defaultLogLevel = defaultLogLevel,
84-
_sdkSettings = sdkSettings;
86+
{EventOptions eventOptions = const EventOptions(),
87+
int datafilePeriodicDownloadInterval = 10 * 60,
88+
Map<ClientPlatform, DatafileHostOptions> datafileHostOptions = const {},
89+
Set<OptimizelyDecideOption> defaultDecideOptions = const {},
90+
OptimizelyLogLevel defaultLogLevel = OptimizelyLogLevel.info,
91+
SDKSettings sdkSettings = const SDKSettings(),
92+
OptimizelyLogger? logger})
93+
: _eventOptions = eventOptions,
94+
_datafilePeriodicDownloadInterval = datafilePeriodicDownloadInterval,
95+
_datafileHostOptions = datafileHostOptions,
96+
_defaultDecideOptions = defaultDecideOptions,
97+
_defaultLogLevel = defaultLogLevel,
98+
_sdkSettings = sdkSettings {
99+
// Set the logger if provided
100+
if (logger != null) {
101+
setLogger(logger);
102+
} else {
103+
logWarning("Logger not provided.");
104+
}
105+
}
85106

86107
/// Starts Optimizely SDK (Synchronous) with provided sdkKey.
87108
Future<BaseResponse> initializeClient() async {
@@ -92,7 +113,9 @@ class OptimizelyFlutterSdk {
92113
_datafileHostOptions,
93114
_defaultDecideOptions,
94115
_defaultLogLevel,
95-
_sdkSettings);
116+
_sdkSettings,
117+
_customLogger
118+
);
96119
}
97120

98121
/// Use the activate method to start an experiment.

lib/src/logger/flutter_logger.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart';
2+
3+
abstract class OptimizelyLogger {
4+
/// Log a message at a certain level
5+
void log(OptimizelyLogLevel level, String message);
6+
}
7+
8+
class DefaultOptimizelyLogger implements OptimizelyLogger {
9+
@override
10+
void log(OptimizelyLogLevel level, String message) {
11+
print('[OPTIMIZELY] [${level.name.toUpperCase()}]: $message');
12+
}
13+
}
14+
15+
/// App logger instance
16+
final _appLogger = DefaultOptimizelyLogger();
17+
18+
/// App logging functions
19+
void logError(String message) =>
20+
_appLogger.log(OptimizelyLogLevel.error, message);
21+
void logWarning(String message) =>
22+
_appLogger.log(OptimizelyLogLevel.warning, message);
23+
void logInfo(String message) =>
24+
_appLogger.log(OptimizelyLogLevel.info, message);
25+
void logDebug(String message) =>
26+
_appLogger.log(OptimizelyLogLevel.debug, message);

lib/src/logger/logger_bridge.dart

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'dart:async';
2+
import 'package:flutter/services.dart';
3+
import 'package:optimizely_flutter_sdk/src/logger/flutter_logger.dart';
4+
import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart';
5+
6+
class LoggerBridge {
7+
static const MethodChannel _loggerChannel =
8+
MethodChannel('optimizely_flutter_sdk_logger');
9+
static OptimizelyLogger? _customLogger;
10+
11+
/// Initialize the logger bridge to receive calls from native
12+
static void initialize(OptimizelyLogger? logger) {
13+
logInfo('[LoggerBridge] Initializing with logger: ${logger != null}');
14+
_customLogger = logger;
15+
_loggerChannel.setMethodCallHandler(_handleMethodCall);
16+
}
17+
18+
/// Handle incoming method calls from native Swift/Java code
19+
static Future<void> _handleMethodCall(MethodCall call) async {
20+
try {
21+
switch (call.method) {
22+
case 'log':
23+
await _handleLogCall(call);
24+
break;
25+
default:
26+
logWarning('[LoggerBridge] Unknown method call: ${call.method}');
27+
}
28+
} catch (e) {
29+
logError('[LoggerBridge] Error handling method call: $e');
30+
}
31+
}
32+
33+
/// Process the log call from Swift/Java
34+
static Future<void> _handleLogCall(MethodCall call) async {
35+
try {
36+
final args = Map<String, dynamic>.from(call.arguments ?? {});
37+
38+
final levelRawValue = args['level'] as int?;
39+
final message = args['message'] as String?;
40+
41+
if (levelRawValue == null || message == null) {
42+
logError('[LoggerBridge] Warning: Missing level or message in log call');
43+
return;
44+
}
45+
46+
final level = _convertLogLevel(levelRawValue);
47+
48+
if (_customLogger != null) {
49+
_customLogger!.log(level, message);
50+
} else {
51+
logInfo('[Optimizely ${level.name}] $message');
52+
}
53+
} catch (e) {
54+
logError('[LoggerBridge] Error processing log call: $e');
55+
}
56+
}
57+
58+
/// Convert native log level to Flutter enum
59+
static OptimizelyLogLevel _convertLogLevel(int rawValue) {
60+
switch (rawValue) {
61+
case 1:
62+
return OptimizelyLogLevel.error;
63+
case 2:
64+
return OptimizelyLogLevel.warning;
65+
case 3:
66+
return OptimizelyLogLevel.info;
67+
case 4:
68+
return OptimizelyLogLevel.debug;
69+
default:
70+
return OptimizelyLogLevel.info;
71+
}
72+
}
73+
74+
/// Expose convertLogLevel
75+
static OptimizelyLogLevel convertLogLevel(int rawValue) {
76+
return _convertLogLevel(rawValue);
77+
}
78+
79+
/// Check if a custom logger is set
80+
static bool hasLogger() {
81+
return _customLogger != null;
82+
}
83+
84+
/// Get the current logger
85+
static OptimizelyLogger? getCurrentLogger() {
86+
return _customLogger;
87+
}
88+
89+
/// Reset logger state
90+
static void reset() {
91+
_customLogger = null;
92+
}
93+
94+
/// Simulate method calls
95+
static Future<void> handleMethodCallForTesting(MethodCall call) async {
96+
await _handleMethodCall(call);
97+
}
98+
}

lib/src/optimizely_client_wrapper.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ class OptimizelyClientWrapper {
6363
Map<ClientPlatform, DatafileHostOptions> datafileHostOptions,
6464
Set<OptimizelyDecideOption> defaultDecideOptions,
6565
OptimizelyLogLevel defaultLogLevel,
66-
SDKSettings sdkSettings) async {
66+
SDKSettings sdkSettings,
67+
OptimizelyLogger? logger) async {
6768
_channel.setMethodCallHandler(methodCallHandler);
6869
final convertedOptions = Utils.convertDecideOptions(defaultDecideOptions);
6970
final convertedLogLevel = Utils.convertLogLevel(defaultLogLevel);
@@ -79,6 +80,7 @@ class OptimizelyClientWrapper {
7980
Constants.eventBatchSize: eventOptions.batchSize,
8081
Constants.eventTimeInterval: eventOptions.timeInterval,
8182
Constants.eventMaxQueueSize: eventOptions.maxQueueSize,
83+
Constants.useCustomLogger: logger != null,
8284
};
8385

8486
// Odp Request params

0 commit comments

Comments
 (0)