Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -39,7 +39,12 @@ class ConfidenceFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAwar
override fun onMethodCall(call: MethodCall, result: Result) {
when(call.method) {
"flush" -> {
confidence.flush()
try {
confidence.flush()
result.success(null)
} catch (e: Exception) {
result.error("FLUSH_FAILED", "Failed to flush: ${e.message}", null)
}
}
"setup" -> {
val apiKey = call.argument<String>("apiKey")!!
Expand Down Expand Up @@ -133,7 +138,12 @@ class ConfidenceFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAwar
val eventName = call.argument<String>("eventName")!!
val wrappedData = call.argument<Map<String, Map<String, Any>>>("data")!!
val data: Map<String, ConfidenceValue> = wrappedData.mapValues { (_, value) -> value.convert() }
confidence.track(eventName, data)
try {
confidence.track(eventName, data)
result.success(null)
} catch (e: Exception) {
result.error("TRACK_FAILED", "Failed to track event '$eventName': ${e.message}", null)
}
}
else -> result.notImplemented()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public class ConfidenceFlutterSdkPlugin: NSObject, FlutterPlugin {
return
}
confidence.flush()
// Confidence.flush() is non-throwing, so there is nothing to catch
// here — but the reply is still mandatory: without it the Dart
// future never completes.
result("")
break;
case "readAllFlags":
guard let flags = try? readAllFlags() else {
Expand Down Expand Up @@ -113,12 +117,22 @@ public class ConfidenceFlutterSdkPlugin: NSObject, FlutterPlugin {
break;
case "track":
guard let args = call.arguments as? Dictionary<String, Any> else {
result("")
return
}
let eventName = args["eventName"] as! String
let data = args["data"] as! Dictionary<String, Dictionary<String, Any>>
let convertedData = data.convert()
try? confidence?.track(eventName: eventName, data: convertedData)
do {
try confidence?.track(eventName: eventName, data: convertedData)
result("")
} catch {
NSLog("%@", "Confidence SDK: \(error)")
result(FlutterError(
code: "TRACK_FAILED",
message: "Failed to track event '\(eventName)': \(error)",
details: nil))
}
break;
case "getBool":
let arguments = call.arguments as! Dictionary<String, Any>
Expand Down
21 changes: 18 additions & 3 deletions lib/confidence_flutter_sdk_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,18 @@ class MethodChannelConfidenceFlutterSdk extends ConfidenceFlutterSdkPlatform {
if (kDebugMode) {
print(wrappedData);
}
// track() is intentionally fire-and-forget, so the returned future is not
// awaited. Without this handler a native error reply would surface as an
// unhandled async error in the host app.
methodChannel
.invokeMethod<void>(
'track',
{'eventName': eventName, 'data': wrappedData}
);
).catchError((Object error) {
if (kDebugMode) {
print('Confidence SDK: failed to track "$eventName": $error');
}
});
}

@override
Expand Down Expand Up @@ -129,8 +136,16 @@ class MethodChannelConfidenceFlutterSdk extends ConfidenceFlutterSdkPlatform {

@override
Future<void> flush() async {
await methodChannel
.invokeMethod<void>('flush');
// The platform interface declares flush() as void, so callers discard this
// future. Now that native replies with an error on failure, an unguarded
// rejection would surface as an unhandled async error in the host app.
try {
await methodChannel.invokeMethod<void>('flush');
} catch (error) {
if (kDebugMode) {
print('Confidence SDK: failed to flush: $error');
}
}
}


Expand Down
61 changes: 61 additions & 0 deletions test/confidence_flutter_sdk_method_channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,65 @@ void main() {
'loggingLevel': 'WARN',
});
});

test('track forwards the event name and typed data', () async {
platform.track('my-event', <String, dynamic>{'plan': 'pro', 'seats': 3});

await pumpEventQueue();

expect(methodCalls, hasLength(1));
expect(methodCalls.single.method, 'track');
expect(methodCalls.single.arguments, <String, Object>{
'eventName': 'my-event',
'data': <String, Object>{
'plan': <String, Object>{'type': 'string', 'value': 'pro'},
'seats': <String, Object>{'type': 'int', 'value': 3},
},
});
});

test('flush completes once native replies', () async {
// Native must reply to every flush call. Without a reply the future below
// never completes, so this test would time out rather than fail an
// assertion — which is exactly how the dangling reply went unnoticed.
await platform.flush().timeout(const Duration(seconds: 5));

expect(methodCalls, hasLength(1));
expect(methodCalls.single.method, 'flush');
});

test('flush does not raise an unhandled async error when native fails',
() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(platform.methodChannel, (methodCall) async {
methodCalls.add(methodCall);
throw PlatformException(code: 'FLUSH_FAILED', message: 'boom');
});

// The platform interface types flush() as void, so callers discard this
// future and an unguarded rejection would become an unhandled async error.
// Awaiting it here asserts the guard directly: it must complete, not throw.
await expectLater(platform.flush(), completes);

expect(methodCalls, hasLength(1));
expect(methodCalls.single.method, 'flush');
});

test('track does not raise an unhandled async error when native fails',
() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(platform.methodChannel, (methodCall) async {
methodCalls.add(methodCall);
throw PlatformException(code: 'TRACK_FAILED', message: 'boom');
});

// track() is void, so the future is never awaited by the caller. An
// unguarded rejection would escape the test zone and fail this test.
platform.track('my-event', <String, dynamic>{});

await pumpEventQueue();

expect(methodCalls, hasLength(1));
expect(methodCalls.single.method, 'track');
});
}