Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Allow overriding the route filtering using a ctor param routeFilter #95

Merged
merged 7 commits into from
Apr 3, 2024
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
working-directory: ./example
run: flutter pub get

- name: Test
run: flutter test

- name: Build iOS
working-directory: ./example
run: flutter build ios --simulator --no-codesign
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## Next

- chore: Allow overriding the route filtering using a ctor param `routeFilter` ([#95](https://github.com/PostHog/posthog-flutter/pull/95))

```dart
bool myRouteFilter(Route<dynamic>? route) =>
route is PageRoute || route is OverlayRoute;
final observer = PosthogObserver(routeFilter: myRouteFilter);
```

## 4.3.0

- add PrivacyInfo ([#94](https://github.com/PostHog/posthog-flutter/pull/94))
Expand Down
2 changes: 1 addition & 1 deletion lib/src/posthog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Posthog {
static PosthogFlutterPlatformInterface get _posthog =>
PosthogFlutterPlatformInterface.instance;

static final Posthog _instance = Posthog._internal();
static final _instance = Posthog._internal();

factory Posthog() {
return _instance;
Expand Down
55 changes: 42 additions & 13 deletions lib/src/posthog_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,76 @@ import 'posthog.dart';

typedef ScreenNameExtractor = String? Function(RouteSettings settings);

/// [PostHogRouteFilter] allows to filter out routes that should not be tracked.
///
/// By default, only [PageRoute]s are tracked.
typedef PostHogRouteFilter = bool Function(Route<dynamic>? route);

String? defaultNameExtractor(RouteSettings settings) => settings.name;

class PosthogObserver extends RouteObserver<PageRoute<dynamic>> {
PosthogObserver({ScreenNameExtractor nameExtractor = defaultNameExtractor})
: _nameExtractor = nameExtractor;
bool defaultPostHogRouteFilter(Route<dynamic>? route) => route is PageRoute;

class PosthogObserver extends RouteObserver<ModalRoute<dynamic>> {
PosthogObserver(
{ScreenNameExtractor nameExtractor = defaultNameExtractor,
PostHogRouteFilter routeFilter = defaultPostHogRouteFilter})
: _nameExtractor = nameExtractor,
_routeFilter = routeFilter;

final ScreenNameExtractor _nameExtractor;

void _sendScreenView(PageRoute<dynamic> route) {
String? screenName = _nameExtractor(route.settings);
if (screenName != null) {
final PostHogRouteFilter _routeFilter;

bool _isTrackeableRoute(String? name) {
return name != null && name.trim().isNotEmpty;
}

void _sendScreenView(Route<dynamic>? route) {
if (route == null) {
return;
}

var screenName = _nameExtractor(route.settings);
if (_isTrackeableRoute(screenName)) {
// if the screen name is the root route, we send it as root ("/") instead of only "/"
if (screenName == '/') {
screenName = 'root (\'/\')';
}

Posthog().screen(screenName: screenName);
Posthog().screen(screenName: screenName!);
}
}

@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
if (route is PageRoute) {
_sendScreenView(route);

if (!_routeFilter(route)) {
return;
}

_sendScreenView(route);
}

@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
if (newRoute is PageRoute) {
_sendScreenView(newRoute);

if (!_routeFilter(newRoute)) {
return;
}

_sendScreenView(newRoute);
}

@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPop(route, previousRoute);
if (previousRoute is PageRoute && route is PageRoute) {
_sendScreenView(previousRoute);

if (!_routeFilter(previousRoute)) {
return;
}

_sendScreenView(previousRoute);
}
}
4 changes: 3 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ dependencies:
plugin_platform_interface: ^2.0.2

dev_dependencies:
flutter_lints: ^2.0.0
flutter_lints: ^3.0.0
flutter_test:
sdk: flutter

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
Expand Down
13 changes: 13 additions & 0 deletions test/posthog_flutter_platform_interface_fake.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart';

class PosthogFlutterPlatformFake extends PosthogFlutterPlatformInterface {
String? screenName;

@override
Future<void> screen({
required String screenName,
Map<String, Object>? properties,
}) async {
this.screenName = screenName;
}
}
115 changes: 115 additions & 0 deletions test/posthog_observer_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:posthog_flutter/src/posthog_flutter_io.dart';
import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart';
import 'package:posthog_flutter/src/posthog_observer.dart';

import 'posthog_flutter_platform_interface_fake.dart';

void main() {
PageRoute<dynamic> route(RouteSettings? settings) => PageRouteBuilder<void>(
pageBuilder: (_, __, ___) => Container(),
settings: settings,
);

final fake = PosthogFlutterPlatformFake();

setUp(() {
TestWidgetsFlutterBinding.ensureInitialized();
PosthogFlutterPlatformInterface.instance = fake;
});

tearDown(() {
fake.screenName = null;
PosthogFlutterPlatformInterface.instance = PosthogFlutterIO();
});

PosthogObserver getSut(
{ScreenNameExtractor nameExtractor = defaultNameExtractor,
PostHogRouteFilter routeFilter = defaultPostHogRouteFilter}) {
return PosthogObserver(
nameExtractor: nameExtractor, routeFilter: routeFilter);
}

test('returns current route name', () {
final currentRoute = route(const RouteSettings(name: 'Current Route'));

final sut = getSut();
sut.didPush(currentRoute, null);

expect(fake.screenName, 'Current Route');
});

test('returns overriden route name', () {
final currentRoute = route(const RouteSettings(name: 'Current Route'));

String? nameExtractor(RouteSettings settings) => 'overriden';

final sut = getSut(nameExtractor: nameExtractor);
sut.didPush(currentRoute, null);

expect(fake.screenName, 'overriden');
});

test('returns overriden root route name', () {
final currentRoute = route(const RouteSettings(name: '/'));

final sut = getSut();
sut.didPush(currentRoute, null);

expect(fake.screenName, 'root (\'/\')');
});

test('does not capture not named routes', () {
final currentRoute = route(const RouteSettings(name: null));

final sut = getSut();
sut.didPush(currentRoute, null);

expect(fake.screenName, null);
});

test('does not capture blank routes', () {
final currentRoute = route(const RouteSettings(name: ' '));

final sut = getSut();
sut.didPush(currentRoute, null);

expect(fake.screenName, null);
});

test('does not capture filtered routes', () {
// CustomOverlawRoute isn't a PageRoute
final overlayRoute = CustomOverlawRoute(
settings: const RouteSettings(name: 'Overlay Route'),
);

final sut = getSut();
sut.didPush(overlayRoute, null);

expect(fake.screenName, null);
});

test('allows overriding the route filter', () {
final overlayRoute = CustomOverlawRoute(
settings: const RouteSettings(name: 'Overlay Route'),
);

bool defaultPostHogRouteFilter(Route<dynamic>? route) =>
route is PageRoute || route is OverlayRoute;

final sut = getSut(routeFilter: defaultPostHogRouteFilter);
sut.didPush(overlayRoute, null);

expect(fake.screenName, 'Overlay Route');
});
}

class CustomOverlawRoute extends OverlayRoute {
CustomOverlawRoute({super.settings});

@override
Iterable<OverlayEntry> createOverlayEntries() {
return [];
}
}