Skip to content

Commit 88fddae

Browse files
authored
feat: add manual error capture (#212)
* feat: add public interface * feat: add config and exception processor * feat: native plugins * chore: update sample app * feat: add unit tests * feat: handle primitives * chore: update changelog * fix: package and module * fix: format * fix: use Object vs dynamic * chore: rename folder * fix: make stackTrace optional * fix: clean generated stack trace * feat: add unit tests * fix: make function optional * fix: drop primitive check * fix: skip module * fix: make thread_id optional * fix: update config * fix: error type * fix: doc * fix: handle empty stack trace * fix: allow overwriting exception properties * fix: normalize props * chore: bump min dart and flutter version * fix: generate event timestamp flutter side * fix: remove handled from public api * fix: platform call arguments * fix: example app * fix: do not try to parse exception package * fix: normalize props * fix: normalize sets * fix: remove handled from public interface * fix: web handler * fix: replace hof with direct iteration * fix: * feat: add web support * chore: add config comment * feat: add async gap franes * Revert "feat: add web support" This reverts commit 6c0529c.
1 parent b339e95 commit 88fddae

20 files changed

+1245
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
## Next
22

3+
- feat: add manual error capture ([#212](https://github.com/PostHog/posthog-flutter/pull/212))
4+
- **Note**: The following features are not yet supported:
5+
- Automatic exception capture
6+
- De-obfuscating stacktraces from obfuscated builds ([--obfuscate](https://docs.flutter.dev/deployment/obfuscate) and [--split-debug-info](https://docs.flutter.dev/deployment/obfuscate))
7+
- [Source code context](/docs/error-tracking/stack-traces) associated with an exception
8+
- Flutter web support
9+
- **BREAKING**: Minimum Dart SDK version bumped to 3.4.0 and Flutter to 3.22.0 (required for `stack_trace` dependency compatibility)
10+
311
## 5.6.0
412

513
- feat: surveys use the new response question id format ([#210](https://github.com/PostHog/posthog-flutter/pull/210))

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
.PHONY: formatKotlin formatSwift formatDart checkDart installLinters
1+
.PHONY: format formatKotlin formatSwift formatDart checkDart installLinters test
2+
3+
format: formatSwift formatKotlin formatDart
24

35
installLinters:
46
brew install ktlint
@@ -19,3 +21,6 @@ checkFormatDart:
1921

2022
analyzeDart:
2123
dart analyze .
24+
25+
test:
26+
flutter test -r expanded

android/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ android {
5454
dependencies {
5555
testImplementation 'org.jetbrains.kotlin:kotlin-test'
5656
testImplementation 'org.mockito:mockito-core:5.0.0'
57-
// + Version 3.23.0 and the versions up to 4.0.0, not including 4.0.0 and higher
58-
implementation 'com.posthog:posthog-android:[3.23.0,4.0.0]'
57+
// + Version 3.25.0 and the versions up to 4.0.0, not including 4.0.0 and higher
58+
implementation 'com.posthog:posthog-android:[3.25.0,4.0.0]'
5959
}
6060

6161
testOptions {

android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import io.flutter.plugin.common.MethodCall
1919
import io.flutter.plugin.common.MethodChannel
2020
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
2121
import io.flutter.plugin.common.MethodChannel.Result
22+
import java.util.Date
2223

2324
/** PosthogFlutterPlugin */
2425
class PosthogFlutterPlugin :
@@ -156,6 +157,9 @@ class PosthogFlutterPlugin :
156157
"flush" -> {
157158
flush(result)
158159
}
160+
"captureException" -> {
161+
captureException(call, result)
162+
}
159163
"close" -> {
160164
close(result)
161165
}
@@ -532,6 +536,34 @@ class PosthogFlutterPlugin :
532536
}
533537
}
534538

539+
private fun captureException(
540+
call: MethodCall,
541+
result: Result,
542+
) {
543+
try {
544+
val arguments =
545+
call.arguments as? Map<String, Any> ?: run {
546+
result.error("INVALID_ARGUMENTS", "Invalid arguments for captureException", null)
547+
return
548+
}
549+
550+
val properties = arguments["properties"] as? Map<String, Any>
551+
val timestampMs = arguments["timestamp"] as? Long
552+
553+
// Extract timestamp from Flutter
554+
val timestamp: Date? =
555+
timestampMs?.let {
556+
// timestampMs already in UTC milliseconds epoch
557+
Date(timestampMs)
558+
}
559+
560+
PostHog.capture("\$exception", properties = properties, timestamp = timestamp)
561+
result.success(null)
562+
} catch (e: Throwable) {
563+
result.error("CAPTURE_EXCEPTION_ERROR", "Failed to capture exception: ${e.message}", null)
564+
}
565+
}
566+
535567
private fun close(result: Result) {
536568
try {
537569
PostHog.close()

example/lib/main.dart

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,66 @@ class InitialScreenState extends State<InitialScreen> {
240240
child: Text("distinctId"),
241241
)),
242242
const Divider(),
243+
const Padding(
244+
padding: EdgeInsets.all(8.0),
245+
child: Text(
246+
"Error Tracking",
247+
style: TextStyle(fontWeight: FontWeight.bold),
248+
),
249+
),
250+
ElevatedButton(
251+
onPressed: () async {
252+
try {
253+
// Simulate an exception in main isolate
254+
// throw 'a custom error string';
255+
// throw 333;
256+
throw CustomException(
257+
'This is a custom exception with additional context',
258+
code: 'DEMO_ERROR_001',
259+
additionalData: {
260+
'user_action': 'button_press',
261+
'timestamp': DateTime.now().millisecondsSinceEpoch,
262+
'feature_enabled': true,
263+
},
264+
);
265+
} catch (e, stack) {
266+
await Posthog().captureException(
267+
error: e,
268+
stackTrace: stack,
269+
properties: {
270+
'test_type': 'main_isolate_exception',
271+
'button_pressed': 'capture_exception_main',
272+
'exception_category': 'custom',
273+
},
274+
);
275+
276+
if (mounted) {
277+
ScaffoldMessenger.of(context).showSnackBar(
278+
const SnackBar(
279+
content: Text(
280+
'Main isolate exception captured successfully! Check PostHog.'),
281+
backgroundColor: Colors.green,
282+
duration: Duration(seconds: 3),
283+
),
284+
);
285+
}
286+
}
287+
},
288+
child: const Text("Capture Exception"),
289+
),
290+
ElevatedButton(
291+
style: ElevatedButton.styleFrom(
292+
backgroundColor: Colors.orange,
293+
),
294+
onPressed: () async {
295+
await Posthog().captureException(
296+
error: 'No Stack Trace Error',
297+
properties: {'test_type': 'no_stack_trace'},
298+
);
299+
},
300+
child: const Text("Capture Exception (Missing Stack)"),
301+
),
302+
const Divider(),
243303
const Padding(
244304
padding: EdgeInsets.all(8.0),
245305
child: Text(
@@ -391,3 +451,24 @@ class ThirdRoute extends StatelessWidget {
391451
);
392452
}
393453
}
454+
455+
/// Custom exception class for demonstration purposes
456+
class CustomException implements Exception {
457+
final String message;
458+
final String? code;
459+
final Map<String, dynamic>? additionalData;
460+
461+
const CustomException(
462+
this.message, {
463+
this.code,
464+
this.additionalData,
465+
});
466+
467+
@override
468+
String toString() {
469+
if (code != null) {
470+
return 'CustomException($code): $message $additionalData';
471+
}
472+
return 'CustomException: $message $additionalData';
473+
}
474+
}

example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
66
version: 1.0.0+1
77

88
environment:
9-
sdk: '>=2.18.0 <4.0.0'
9+
sdk: '>=3.4.0 <4.0.0'
1010
flutter: '>=3.3.0'
1111

1212
# Dependencies specify other packages that your package needs in order to work.

ios/Classes/PosthogFlutterPlugin.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin {
195195
unregister(call, result: result)
196196
case "flush":
197197
flush(result)
198+
case "captureException":
199+
captureException(call, result: result)
198200
case "close":
199201
close(result)
200202
case "sendMetaEvent":
@@ -677,6 +679,25 @@ extension PosthogFlutterPlugin {
677679
result(nil)
678680
}
679681

682+
private func captureException(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
683+
guard let arguments = call.arguments as? [String: Any] else {
684+
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid arguments for captureException", details: nil))
685+
return
686+
}
687+
688+
let properties = arguments["properties"] as? [String: Any]
689+
690+
// Extract timestamp from Flutter and convert to Date
691+
var timestamp: Date? = nil
692+
if let timestampMs = arguments["timestamp"] as? Int64 {
693+
timestamp = Date(timeIntervalSince1970: TimeInterval(timestampMs) / 1000.0)
694+
}
695+
696+
// Use capture method with timestamp to ensure Flutter timestamp is used
697+
PostHogSDK.shared.capture("$exception", properties: properties, timestamp: timestamp)
698+
result(nil)
699+
}
700+
680701
private func close(_ result: @escaping FlutterResult) {
681702
PostHogSDK.shared.close()
682703
result(nil)

0 commit comments

Comments
 (0)