Skip to content

Commit 2737fef

Browse files
authored
chore: exception web (#243)
1 parent ea8e6ad commit 2737fef

18 files changed

+466
-93
lines changed

CHANGELOG.md

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

3+
- feat: flutter error tracking support for web ([#243](https://github.com/PostHog/posthog-flutter/pull/243))
34
- feat: add `userProperties` and `userPropertiesSetOnce` parameters to `capture()` method ([#254](https://github.com/PostHog/posthog-flutter/pull/254))
45

56
# 5.11.1

example/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,19 @@ A few resources to get you started if this is your first Flutter project:
1414
For help getting started with Flutter development, view the
1515
[online documentation](https://docs.flutter.dev/), which offers tutorials,
1616
samples, guidance on mobile development, and a full API reference.
17+
18+
## Running web example
19+
20+
```bash
21+
# release mode
22+
rm -rf build/web
23+
flutter build web --source-maps
24+
posthog-cli sourcemap inject --directory build/web
25+
# check the sourcemaps has chunk_id and release_id injected (*.js.map file)
26+
# check the js file has _posthogChunkIds injected (*.js file)
27+
# check the chunk_id and _posthogChunkIds match
28+
posthog-cli sourcemap upload --directory build/web
29+
cd build/web
30+
# https://pub.dev/packages/dhttpd
31+
dhttpd
32+
```

example/lib/error_example.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import 'package:posthog_flutter/posthog_flutter.dart';
2+
import 'package:posthog_flutter_example/main.dart';
3+
4+
class ErrorExample {
5+
void causeUnhandledDivisionError() {
6+
// This will cause a division by zero error
7+
10 ~/ 0;
8+
}
9+
10+
Future<void> causeHandledDivisionError() async {
11+
try {
12+
// This will cause a division by zero error
13+
10 ~/ 0;
14+
} catch (e, stackTrace) {
15+
await Posthog().captureException(
16+
error: e,
17+
stackTrace: stackTrace,
18+
);
19+
}
20+
}
21+
22+
Future<void> throwWithinDelayed() async {
23+
Future.delayed(Duration.zero, () {
24+
// does not throw on web here, just with runZonedGuarded handler
25+
throw const CustomException('Test throwWithinDelayed',
26+
code: 'PlatformDispatcherTest',
27+
additionalData: {'test_type': 'platform_dispatcher_error'});
28+
});
29+
}
30+
31+
Future<void> throwWithinTimer() async {
32+
Future.delayed(Duration.zero, () {
33+
// does not throw on web here, just with runZonedGuarded handler
34+
throw const CustomException('Test throwWithinTimer',
35+
code: 'PlatformDispatcherTest',
36+
additionalData: {'test_type': 'platform_dispatcher_error'});
37+
});
38+
}
39+
}

example/lib/main.dart

Lines changed: 26 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import 'dart:async';
22

3+
import 'package:flutter/foundation.dart';
34
import 'package:flutter/material.dart';
45
import 'package:posthog_flutter/posthog_flutter.dart';
6+
import 'package:posthog_flutter_example/error_example.dart';
57

68
Future<void> main() async {
7-
// // init WidgetsFlutterBinding if not yet
8-
9-
WidgetsFlutterBinding.ensureInitialized();
109
final config =
1110
PostHogConfig('phc_6lqCaCDCBEWdIGieihq5R2dZpPVbAUFISA75vFZow06');
1211
config.onFeatureFlags = () {
@@ -15,8 +14,8 @@ Future<void> main() async {
1514
config.debug = true;
1615
config.captureApplicationLifecycleEvents = false;
1716
config.host = 'https://us.i.posthog.com';
18-
config.surveys = true;
19-
config.sessionReplay = true;
17+
config.surveys = false;
18+
config.sessionReplay = false;
2019
config.sessionReplayConfig.maskAllTexts = false;
2120
config.sessionReplayConfig.maskAllImages = false;
2221
config.sessionReplayConfig.throttleDelay = const Duration(milliseconds: 1000);
@@ -30,8 +29,20 @@ Future<void> main() async {
3029
config.errorTrackingConfig.captureIsolateErrors =
3130
true; // Capture isolate errors
3231

33-
await Posthog().setup(config);
32+
if (kIsWeb) {
33+
runZonedGuarded(
34+
() async => await _initAndRun(config),
35+
(error, stackTrace) async => await Posthog()
36+
.captureRunZonedGuardedError(error: error, stackTrace: stackTrace),
37+
);
38+
} else {
39+
await _initAndRun(config);
40+
}
41+
}
3442

43+
Future<void> _initAndRun(PostHogConfig config) async {
44+
WidgetsFlutterBinding.ensureInitialized();
45+
await Posthog().setup(config);
3546
runApp(const MyApp());
3647
}
3748

@@ -267,56 +278,10 @@ class InitialScreenState extends State<InitialScreen> {
267278
),
268279
ElevatedButton(
269280
onPressed: () async {
270-
try {
271-
// Simulate an exception in main isolate
272-
// throw 'a custom error string';
273-
// throw 333;
274-
throw CustomException(
275-
'This is a custom exception with additional context',
276-
code: 'DEMO_ERROR_001',
277-
additionalData: {
278-
'user_action': 'button_press',
279-
'timestamp': DateTime.now().millisecondsSinceEpoch,
280-
'feature_enabled': true,
281-
},
282-
);
283-
} catch (e, stack) {
284-
await Posthog().captureException(
285-
error: e,
286-
stackTrace: stack,
287-
properties: {
288-
'test_type': 'main_isolate_exception',
289-
'button_pressed': 'capture_exception_main',
290-
'exception_category': 'custom',
291-
},
292-
);
293-
294-
if (mounted && context.mounted) {
295-
ScaffoldMessenger.of(context).showSnackBar(
296-
const SnackBar(
297-
content: Text(
298-
'Main isolate exception captured successfully! Check PostHog.'),
299-
backgroundColor: Colors.green,
300-
duration: Duration(seconds: 3),
301-
),
302-
);
303-
}
304-
}
281+
await ErrorExample().causeHandledDivisionError();
305282
},
306283
child: const Text("Capture Exception"),
307284
),
308-
ElevatedButton(
309-
style: ElevatedButton.styleFrom(
310-
backgroundColor: Colors.orange,
311-
),
312-
onPressed: () async {
313-
await Posthog().captureException(
314-
error: 'No Stack Trace Error',
315-
properties: {'test_type': 'no_stack_trace'},
316-
);
317-
},
318-
child: const Text("Capture Exception (Missing Stack)"),
319-
),
320285
const Divider(),
321286
const Padding(
322287
padding: EdgeInsets.all(8.0),
@@ -330,7 +295,7 @@ class InitialScreenState extends State<InitialScreen> {
330295
backgroundColor: Colors.red,
331296
foregroundColor: Colors.white,
332297
),
333-
onPressed: () {
298+
onPressed: () async {
334299
if (mounted) {
335300
ScaffoldMessenger.of(context).showSnackBar(
336301
const SnackBar(
@@ -343,10 +308,7 @@ class InitialScreenState extends State<InitialScreen> {
343308
}
344309

345310
// Test Flutter error handler by throwing in widget context
346-
throw const CustomException(
347-
'Test Flutter error for autocapture',
348-
code: 'FlutterErrorTest',
349-
additionalData: {'test_type': 'flutter_error'});
311+
await ErrorExample().causeHandledDivisionError();
350312
},
351313
child: const Text("Test Flutter Error Handler"),
352314
),
@@ -355,18 +317,10 @@ class InitialScreenState extends State<InitialScreen> {
355317
backgroundColor: Colors.blue,
356318
foregroundColor: Colors.white,
357319
),
358-
onPressed: () {
359-
// Test PlatformDispatcher error handler with Future
360-
Future.delayed(Duration.zero, () {
361-
throw const CustomException(
362-
'Test PlatformDispatcher error for autocapture',
363-
code: 'PlatformDispatcherTest',
364-
additionalData: {
365-
'test_type': 'platform_dispatcher_error'
366-
});
367-
});
320+
onPressed: () async {
321+
await ErrorExample().throwWithinDelayed();
368322

369-
if (mounted) {
323+
if (mounted && context.mounted) {
370324
ScaffoldMessenger.of(context).showSnackBar(
371325
const SnackBar(
372326
content: Text(
@@ -384,19 +338,11 @@ class InitialScreenState extends State<InitialScreen> {
384338
backgroundColor: Colors.purple,
385339
foregroundColor: Colors.white,
386340
),
387-
onPressed: () {
341+
onPressed: () async {
388342
// Test isolate error listener by throwing in an async callback
389-
Timer(Duration.zero, () {
390-
throw const CustomException(
391-
'Isolate error for testing',
392-
code: 'IsolateHandlerTest',
393-
additionalData: {
394-
'test_type': 'isolate_error_listener_timer',
395-
},
396-
);
397-
});
343+
await ErrorExample().throwWithinTimer();
398344

399-
if (mounted) {
345+
if (mounted && context.mounted) {
400346
ScaffoldMessenger.of(context).showSnackBar(
401347
const SnackBar(
402348
content:

example/web/index.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,20 @@
3434
<link rel="manifest" href="manifest.json">
3535

3636
<script async>
37-
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
37+
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.full.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
3838
posthog.init(
3939
'phc_6lqCaCDCBEWdIGieihq5R2dZpPVbAUFISA75vFZow06',
4040
{
4141
api_host:'https://us.i.posthog.com',
4242
debug: true,
43+
disable_session_recording: true,
44+
autocapture: false,
45+
disable_surveys: true,
46+
rageclick: false,
47+
enable_heatmaps: false,
48+
capture_dead_clicks: false,
49+
capture_pageview: false,
50+
capture_pageleave: false,
4351
}
4452
)
4553
</script>

lib/posthog_flutter_web.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,25 @@ import 'dart:js_interop';
55

66
import 'package:flutter/services.dart';
77
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
8+
import 'package:posthog_flutter/src/error_tracking/dart_exception_processor.dart';
9+
import 'package:posthog_flutter/src/util/logging.dart';
10+
import 'package:posthog_flutter/src/utils/property_normalizer.dart';
811

912
import 'src/posthog_config.dart';
1013
import 'src/posthog_flutter_platform_interface.dart';
1114
import 'src/posthog_flutter_web_handler.dart';
1215
import 'src/utils/capture_utils.dart';
13-
import 'src/utils/property_normalizer.dart';
1416

1517
/// A web implementation of the PosthogFlutterPlatform of the PosthogFlutter plugin.
1618
class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
1719
/// Constructs a PosthogFlutterWeb
1820
PosthogFlutterWeb();
1921

22+
/// Stored configuration for accessing inAppIncludes and other settings
23+
PostHogConfig? _config;
24+
25+
// TODO: we should change the $lib and $lib_version to be the flutter one when capturing things
26+
2027
static void registerWith(Registrar registrar) {
2128
final channel = MethodChannel(
2229
'posthog_flutter',
@@ -59,6 +66,8 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
5966
// posthog?.callMethod('init'.toJS, config.apiKey.toJS, jsOptions);
6067

6168
final ph = posthog;
69+
_config = config;
70+
6271
if (config.onFeatureFlags != null && ph != null) {
6372
final dartCallback = config.onFeatureFlags!;
6473

@@ -252,6 +261,24 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
252261
StackTrace? stackTrace,
253262
Map<String, Object>? properties,
254263
}) async {
255-
// Not implemented on web
264+
try {
265+
final exceptionData = DartExceptionProcessor.processException(
266+
error: error,
267+
stackTrace: stackTrace,
268+
properties: properties,
269+
inAppIncludes: _config?.errorTrackingConfig.inAppIncludes,
270+
inAppExcludes: _config?.errorTrackingConfig.inAppExcludes,
271+
inAppByDefault: _config?.errorTrackingConfig.inAppByDefault ?? true,
272+
);
273+
274+
final normalizedData =
275+
PropertyNormalizer.normalize(exceptionData.cast<String, Object>());
276+
277+
return handleWebMethodCall(MethodCall('captureException', {
278+
'properties': normalizedData,
279+
}));
280+
} on Exception catch (exception) {
281+
printIfDebug('Exception in captureException: $exception');
282+
}
256283
}
257284
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export 'chunk_ids_io.dart' if (dart.library.js_interop) 'chunk_ids_web.dart';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Map<String, String>? getPosthogChunkIds() {
2+
return null;
3+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import 'dart:js_interop';
2+
import 'dart:js_interop_unsafe';
3+
4+
@JS('globalThis')
5+
external JSObject get globalThis;
6+
7+
Map<String, String>? getPosthogChunkIds() {
8+
final debugIdMapJS = globalThis['_posthogChunkIds'];
9+
final debugIdMap = debugIdMapJS?.dartify() as Map<String, Object>?;
10+
if (debugIdMap == null) {
11+
return null;
12+
}
13+
return debugIdMap.map(
14+
(key, value) => MapEntry(key.toString(), value.toString()),
15+
);
16+
}

0 commit comments

Comments
 (0)