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
5 changes: 5 additions & 0 deletions .changeset/fresh-error-hooks-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog_flutter': patch
---

Reapply Dart automatic error-capture hooks when setup is called again with changed flags.
7 changes: 6 additions & 1 deletion posthog_flutter/lib/src/posthog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ class Posthog {
/// To listen for feature flag load events, provide an `onFeatureFlags`
/// callback in the [PostHogConfig].
///
/// Repeated setup reapplies Dart-side settings such as the error-tracking
/// hooks. Most native settings still require [close] before setup because
/// the native SDKs ignore repeated setup.
///
/// Returns a [Future] that completes when platform setup has finished.
///
/// **Example:**
Expand Down Expand Up @@ -83,7 +87,8 @@ class Posthog {
}

void _installFlutterIntegrations(PostHogConfig config) {
// Install exception autocapture if enabled
_uninstallFlutterIntegrations();

if (config.errorTrackingConfig.captureFlutterErrors ||
config.errorTrackingConfig.capturePlatformDispatcherErrors ||
config.errorTrackingConfig.captureIsolateErrors) {
Expand Down
209 changes: 209 additions & 0 deletions posthog_flutter/test/repeated_setup_hooks_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:posthog_flutter/posthog_flutter.dart';
import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart';

import 'posthog_flutter_platform_interface_fake.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

late PosthogFlutterPlatformFake platform;
FlutterExceptionHandler? originalFlutterHandler;
ErrorCallback? originalPlatformHandler;

setUp(() async {
platform = PosthogFlutterPlatformFake();
PosthogFlutterPlatformInterface.instance = platform;
await Posthog().close();
originalFlutterHandler = FlutterError.onError;
FlutterError.onError = (_) {};
if (!kIsWeb) {
originalPlatformHandler = PlatformDispatcher.instance.onError;
PlatformDispatcher.instance.onError = (_, __) => true;
}
});

tearDown(() async {
await Posthog().close();
FlutterError.onError = originalFlutterHandler;
if (!kIsWeb) {
PlatformDispatcher.instance.onError = originalPlatformHandler;
}
});

void reportFlutterError({bool silent = false}) {
FlutterError.reportError(FlutterErrorDetails(
exception: StateError('repeated setup repro'),
stack: StackTrace.current,
silent: silent,
));
}

for (final reuseConfig in [false, true]) {
test('disables Flutter hook on repeated setup (reuse=$reuseConfig)',
() async {
final upstream = FlutterError.onError;
final first = PostHogConfig('test-token')
..errorTrackingConfig.captureFlutterErrors = true;
await Posthog().setup(first);
reportFlutterError();
expect(platform.capturedExceptions, hasLength(1));
platform.capturedExceptions.clear();

final second = reuseConfig ? first : PostHogConfig('test-token');
second.errorTrackingConfig.captureFlutterErrors = false;
await Posthog().setup(second);
expect(Posthog().config, same(second));

reportFlutterError();
expect(platform.capturedExceptions, isEmpty);
expect(FlutterError.onError, same(upstream));
});
}

test('disables PlatformDispatcher hook on repeated setup', () async {
final upstream = PlatformDispatcher.instance.onError;
final first = PostHogConfig('test-token')
..errorTrackingConfig.capturePlatformDispatcherErrors = true;
await Posthog().setup(first);
PlatformDispatcher.instance.onError!(
StateError('before'), StackTrace.current);
expect(platform.capturedExceptions, hasLength(1));
platform.capturedExceptions.clear();

await Posthog().setup(PostHogConfig('test-token'));
PlatformDispatcher.instance.onError!(
StateError('after'), StackTrace.current);
expect(platform.capturedExceptions, isEmpty);
expect(PlatformDispatcher.instance.onError, same(upstream));
}, skip: kIsWeb);

test('adds PlatformDispatcher hook while Flutter hook is installed',
Comment thread
marandaneto marked this conversation as resolved.
() async {
final first = PostHogConfig('test-token')
..errorTrackingConfig.captureFlutterErrors = true;
await Posthog().setup(first);

final second = PostHogConfig('test-token')
..errorTrackingConfig.captureFlutterErrors = true
..errorTrackingConfig.capturePlatformDispatcherErrors = true;
await Posthog().setup(second);
expect(Posthog().config, same(second));

PlatformDispatcher.instance.onError!(
StateError('after'), StackTrace.current);
expect(platform.capturedExceptions, hasLength(1));
}, skip: kIsWeb);

test('replaces silent-error policy on repeated setup', () async {
final first = PostHogConfig('test-token')
..errorTrackingConfig.captureFlutterErrors = true;
await Posthog().setup(first);

final second = PostHogConfig('test-token')
..errorTrackingConfig.captureFlutterErrors = true
..errorTrackingConfig.captureSilentFlutterErrors = true;
await Posthog().setup(second);

reportFlutterError(silent: true);
expect(platform.capturedExceptions, hasLength(1));
});

test('mutating the original silent-error policy takes effect', () async {
Comment thread
marandaneto marked this conversation as resolved.
final config = PostHogConfig('test-token')
..errorTrackingConfig.captureFlutterErrors = true;
await Posthog().setup(config);
config.errorTrackingConfig.captureSilentFlutterErrors = true;
await Posthog().setup(config);

reportFlutterError(silent: true);
expect(platform.capturedExceptions, hasLength(1));
});

test('first installation can happen on second setup', () async {
await Posthog().setup(PostHogConfig('test-token'));
await Posthog().setup(PostHogConfig('test-token')
..errorTrackingConfig.captureFlutterErrors = true);

reportFlutterError();
expect(platform.capturedExceptions, hasLength(1));
});

test('identical repeated setup does not duplicate capture', () async {
final config = PostHogConfig('test-token')
..errorTrackingConfig.captureFlutterErrors = true;
await Posthog().setup(config);
await Posthog().setup(config);

reportFlutterError();
expect(platform.capturedExceptions, hasLength(1));
});

for (final platformHandler in [false, true]) {
test(
'repeated setup preserves wrapped handlers (platform=$platformHandler)',
() async {
var upstreamCalls = 0;
var wrapperCalls = 0;
final config = PostHogConfig('test-token')
..errorTrackingConfig.captureFlutterErrors = !platformHandler
..errorTrackingConfig.capturePlatformDispatcherErrors = platformHandler;

if (platformHandler) {
PlatformDispatcher.instance.onError = (_, __) {
upstreamCalls++;
return true;
};
} else {
FlutterError.onError = (_) => upstreamCalls++;
}
await Posthog().setup(config);
if (platformHandler) {
final delegate = PlatformDispatcher.instance.onError!;
PlatformDispatcher.instance.onError = (error, stack) {
wrapperCalls++;
return delegate(error, stack);
};
} else {
final delegate = FlutterError.onError!;
FlutterError.onError = (details) {
wrapperCalls++;
delegate(details);
};
}

for (var i = 0; i < 3; i++) {
await Posthog().setup(config);
}
if (platformHandler) {
expect(
PlatformDispatcher.instance.onError!(
StateError('after'), StackTrace.current),
isTrue,
);
} else {
reportFlutterError();
}
expect(platform.capturedExceptions, hasLength(1));
expect(upstreamCalls, 1);
expect(wrapperCalls, 1);
}, skip: kIsWeb && platformHandler);
}

test('close then setup applies changed hook flags', () async {
await Posthog().setup(PostHogConfig('test-token')
..errorTrackingConfig.captureFlutterErrors = true);
await Posthog().close();
await Posthog().setup(PostHogConfig('test-token')
..errorTrackingConfig.capturePlatformDispatcherErrors = true);

reportFlutterError();
expect(platform.capturedExceptions, isEmpty);
PlatformDispatcher.instance.onError!(
StateError('after'), StackTrace.current);
expect(platform.capturedExceptions, hasLength(1));
}, skip: kIsWeb);
}
59 changes: 59 additions & 0 deletions posthog_flutter/test/repeated_setup_isolate_hooks_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@TestOn('vm')
library;

import 'dart:async';
import 'dart:isolate';

import 'package:flutter_test/flutter_test.dart';
import 'package:posthog_flutter/posthog_flutter.dart';
import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart';

import 'posthog_flutter_platform_interface_fake.dart';

void main() {
test('repeated setup turns the isolate hook off and back on', () async {
final results = ReceivePort();
final isolate = await Isolate.spawn(
_exerciseIsolateHook,
results.sendPort,
errorsAreFatal: false,
onExit: results.sendPort,
);
addTearDown(() {
isolate.kill(priority: Isolate.immediate);
results.close();
});

expect(await results.first, [1, 0, 1]);
});
}

Future<void> _exerciseIsolateHook(SendPort results) async {
final observedErrors = ReceivePort();
final errors = StreamIterator<dynamic>(observedErrors);
Isolate.current.addErrorListener(observedErrors.sendPort);
final platform = PosthogFlutterPlatformFake();
PosthogFlutterPlatformInterface.instance = platform;

try {
final counts = <int>[];
for (final enabled in [true, false, true]) {
await Posthog().setup(PostHogConfig('test-token')
..errorTrackingConfig.captureIsolateErrors = enabled);
platform.capturedExceptions.clear();
Timer.run(() => throw StateError('isolate hook enabled=$enabled'));
await errors.moveNext();
// Error listeners use separate ports; let the SDK's queued callback run.
await Future<void>.delayed(Duration.zero);
counts.add(platform.capturedExceptions.length);
}
results.send(counts);
} catch (error, stack) {
results.send('$error\n$stack');
} finally {
await Posthog().close();
Isolate.current.removeErrorListener(observedErrors.sendPort);
await errors.cancel();
observedErrors.close();
}
}
Loading