Skip to content

Commit

Permalink
Add route observer for auto tracking screen or page view events on na…
Browse files Browse the repository at this point in the history
…vigation (close #9)

PR #10
  • Loading branch information
matus-tomlein committed Jan 31, 2022
1 parent cd4b3d4 commit f957bc2
Show file tree
Hide file tree
Showing 27 changed files with 810 additions and 344 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ It is build on top of Snowplow's native [iOS](https://github.com/snowplow/snowpl
| Feature | Android | iOS | Web |
|---|---|---|---|
| Manual tracking of events: screen views, self-describing, structured, timing, consent granted and withdrawal ||||
| Automatic tracking of views events from Navigator API ||||
| Adding custom context entities to events ||||
| Support for multiple trackers ||||
| Configurable subject properties ||| partly |
Expand Down Expand Up @@ -71,7 +72,7 @@ The endpoint is the URI of the Snowplow collector to send the events to.
There are additional optional arguments to configure the tracker, please refer to the documentation for a complete specification.

```dart
Tracker tracker = await Snowplow.createTracker(
SnowplowTracker tracker = await Snowplow.createTracker(
namespace: 'ns1',
endpoint: 'http://...'
);
Expand Down
2 changes: 1 addition & 1 deletion doc/01-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Tracker namespace identifies the tracker instance, you may create multiple track
The endpoint is the URI of the Snowplow collector to send the events to.

```dart
Tracker tracker = await Snowplow.createTracker(
SnowplowTracker tracker = await Snowplow.createTracker(
namespace: 'ns1',
endpoint: 'http://...'
);
Expand Down
12 changes: 8 additions & 4 deletions doc/02-configuration.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Initialization and configuration

The package provides a single method to initialize and configure a new tracker, the `Snowplow.createTracker` method. It accepts configuration parameters for the tracker and returns a `Tracker` instance.
The package provides a single method to initialize and configure a new tracker, the `Snowplow.createTracker` method. It accepts configuration parameters for the tracker and returns a `SnowplowTracker` instance.

```dart
Tracker tracker = await Snowplow.createTracker(
SnowplowTracker tracker = await Snowplow.createTracker(
namespace: 'ns1',
endpoint: 'http://...',
method: Method.post,
Expand All @@ -13,7 +13,7 @@ Tracker tracker = await Snowplow.createTracker(
);
```

The method returns a `Tracker` instance. This can be later used for tracking events, or accessing tracker properties. However, all methods provided by the `Tracker` instance are also available as static functions in the `Snowplow` class but they require passing the tracker namespace as string.
The method returns a `SnowplowTracker` instance. This can be later used for tracking events, or accessing tracker properties. However, all methods provided by the `SnowplowTracker` instance are also available as static functions in the `Snowplow` class but they require passing the tracker namespace as string.

The only required attributes of the `Snowplow.createTracker` method are `namespace` used to identify the tracker, and the Snowplow collector `endpoint`. Additionally, one can configure the HTTP method to be used when sending events to the collector and provide configuration by instantiating classes for `TrackerConfiguration`, `SubjectConfiguration`, or `GdprConfiguration`. The following arguments are accepted by the `Snowplow.createTracker` method:

Expand All @@ -39,7 +39,11 @@ The only required attributes of the `Snowplow.createTracker` method are `namespa
| `geoLocationContext` | `bool?` | Indicates whether geo-location context should be attached to tracked events. |||| false |
| `sessionContext` | `bool?` | Indicates whether session context should be attached to tracked events. |||| true |
| `webPageContext` | `bool?` | Indicates whether context about current web page should be attached to tracked events. | | || true |
| `activityTrackingConfig` | ActivityTrackingConfiguration?` | Enables activity tracking using page pings on the Web. | | || true |
| `webActivityTracking` | WebActivityTracking?` | Enables activity tracking using page views and pings on the Web. | | || true |

The optional `WebActivityTracking` property configures page tracking on Web. Initializing the configuration will inform `SnowplowObserver` observers (see section on auto-tracking in "Tracking events") to auto track `PageViewEvent` events instead of `ScreenView` events on navigation changes. Further, setting the `minimumVisitLength` and `heartbeatDelay` properties of the `WebActivityTracking` instance will enable activity tracking using 'page ping' events on Web.

Activity tracking monitors whether a user continues to engage with a page over time, and record how he / she digests content on the page over time. That is accomplished using 'page ping' events. If activity tracking is enabled, the web page is monitored to see if a user is engaging with it. (E.g. is the tab in focus, does the mouse move over the page, does the user scroll etc.) If any of these things occur in a set period of time (`minimumVisitLength` seconds from page load and every `heartbeatDelay` seconds after that), a page ping event fires, and records the maximum scroll left / right and up / down in the last ping period. If there is no activity in the page (e.g. because the user is on a different tab in his / her browser), no page ping fires.

## Configuration of subject information: `SubjectConfiguration`

Expand Down
46 changes: 46 additions & 0 deletions doc/03-tracking-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,49 @@ tracker.track(ConsentWithdrawn(
documentDescription: 'description1',
));
```

## Automatically tracking view events using navigator observer

There is also an option to automatically track view events when currently active pages change through the [Navigator API](https://api.flutter.dev/flutter/widgets/Navigator-class.html).

To activate this feature, one has to register a `SnowplowObserver` retrieved from the tracker instance using `SnowplowTracker.getObserver()`. The retrieved observer can be added to `navigatorObservers` in `MaterialApp`:

```dart
MaterialApp(
navigatorObservers: [
tracker.getObserver()
],
...
);
```

If using the `Router` API with the `MaterialApp.router` constructor, add the observer to the `observers` of your `Navigator` instance, e.g.:

```dart
Navigator(
observers: [tracker.getObserver()],
...
);
```

The `SnowplowObserver` automatically tracks `PageViewEvent` and `ScreenView` events when the currently active `ModalRoute` of the navigator changes.

By default, `ScreenView` events are tracked on all platforms. In case `TrackerConfiguration.webActivityTracking` is configured when creating the tracker, `PageViewEvent` events will be tracked on Web instead of `ScreenView` events (`ScreenView` events will still be tracked on other platforms).

The `SnowplowTracker.getObserver()` function takes an optional `nameExtractor` function as argument which is used to extract a name from new routes that is used in tracked `ScreenView` or `PageViewEvent` events.

The following operations will result in tracking a view event:

```dart
Navigator.pushNamed(context, '/contact/123');
Navigator.push<void>(context, MaterialPageRoute(
settings: RouteSettings(name: '/contact/123'),
builder: (_) => ContactDetail(123)));
Navigator.pushReplacement<void>(context, MaterialPageRoute(
settings: RouteSettings(name: '/contact/123'),
builder: (_) => ContactDetail(123)));
Navigator.pop(context);
```
18 changes: 5 additions & 13 deletions example/integration_test/configuration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import 'package:integration_test/integration_test.dart';
import 'package:snowplow_tracker/snowplow_tracker.dart';

import 'helpers.dart';
import 'package:snowplow_tracker_example/main.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand All @@ -25,9 +24,7 @@ void main() {
});

testWidgets("sets and changes user id", (WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

Tracker tracker = await Snowplow.createTracker(
SnowplowTracker tracker = await Snowplow.createTracker(
namespace: 'test',
endpoint: SnowplowTests.microEndpoint,
subjectConfig: const SubjectConfiguration(userId: 'XYZ'));
Expand Down Expand Up @@ -57,14 +54,13 @@ void main() {
if (!kIsWeb) {
return;
}
await tester.pumpWidget(const MyApp(testing: true));

Tracker withoutContext = await Snowplow.createTracker(
SnowplowTracker withoutContext = await Snowplow.createTracker(
namespace: 'withoutContext',
endpoint: SnowplowTests.microEndpoint,
trackerConfig: const TrackerConfiguration(webPageContext: false));

Tracker withContext = await Snowplow.createTracker(
SnowplowTracker withContext = await Snowplow.createTracker(
namespace: 'withContext',
endpoint: SnowplowTests.microEndpoint,
trackerConfig: const TrackerConfiguration(webPageContext: true));
Expand All @@ -91,9 +87,7 @@ void main() {
});

testWidgets("attaches gdpr context to events", (WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

Tracker tracker = await Snowplow.createTracker(
SnowplowTracker tracker = await Snowplow.createTracker(
namespace: 'gdpr',
endpoint: SnowplowTests.microEndpoint,
trackerConfig: const TrackerConfiguration(),
Expand Down Expand Up @@ -124,9 +118,7 @@ void main() {

testWidgets("sets app ID and platform based on configuration",
(WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

Tracker tracker = await Snowplow.createTracker(
SnowplowTracker tracker = await Snowplow.createTracker(
namespace: 'app-platform',
endpoint: SnowplowTests.microEndpoint,
trackerConfig: const TrackerConfiguration(
Expand Down
41 changes: 22 additions & 19 deletions example/integration_test/events_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ void main() {
});

testWidgets("tracks a structured event", (WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

await Snowplow.track(
const Structured(category: 'category', action: 'action'),
tracker: "test");
Expand All @@ -50,8 +48,6 @@ void main() {
});

testWidgets("tracks a self-describing event", (WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

const selfDescribing = SelfDescribing(
schema: 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1',
data: {'targetUrl': 'http://a-target-url.com'},
Expand All @@ -74,7 +70,6 @@ void main() {
});

testWidgets("tracks a screen view event", (WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));
String id = const Uuid().v4();

var screenView = ScreenView(id: id, name: 'name');
Expand All @@ -92,8 +87,6 @@ void main() {
});

testWidgets("tracks a timing event", (WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

var timing =
const Timing(category: 'cat', variable: 'var', timing: 10, label: 'l');
await Snowplow.track(timing, tracker: 'test');
Expand All @@ -109,8 +102,6 @@ void main() {
});

testWidgets("tracks a consent granted event", (WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

final consentGranted = ConsentGranted(
expiry: DateTime.parse('2021-12-30T09:03:51.196Z'),
documentId: '1234',
Expand All @@ -129,8 +120,6 @@ void main() {
});

testWidgets("tracks a consent withdrawn event", (WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

const consentWithdrawn = ConsentWithdrawn(
all: false,
documentId: '1234',
Expand All @@ -150,8 +139,6 @@ void main() {

testWidgets("tracks an event with custom context",
(WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

await Snowplow.track(
const Structured(category: 'category', action: 'action'),
tracker: "test",
Expand Down Expand Up @@ -183,29 +170,45 @@ void main() {
isTrue);
});

testWidgets("tracks a page view event on web", (WidgetTester tester) async {
testWidgets(
"tracks a page view event on web after loading page if web activity tracking",
(WidgetTester tester) async {
if (!kIsWeb) {
return;
}
await tester.pumpWidget(const MyApp(testing: true));

await Snowplow.track(const PageViewEvent(), tracker: 'test');
SnowplowTracker tracker = await Snowplow.createTracker(
namespace: 'web',
endpoint: SnowplowTests.microEndpoint,
trackerConfig: const TrackerConfiguration(
webActivityTracking: WebActivityTracking()));
await tester.pumpWidget(MyApp(tracker: tracker));

expect(
await SnowplowTests.checkMicroGood((events) =>
(events.length == 1) &&
(events[0]['event']['page_title'] == 'Demo App') &&
(events[0]['event']['page_title'] == '/') &&
(events[0]['event']['page_url'] != null) &&
(events[0]['event']['page_referrer'] != null)),
isTrue);
});

testWidgets("tracks a screen view event after loading page",
(WidgetTester tester) async {
await tester.pumpWidget(MyApp(tracker: SnowplowTests.tracker!));

expect(
await SnowplowTests.checkMicroGood((events) =>
(events.length == 1) &&
(events[0]['event']['unstruct_event']['data']['data']['name'] ==
'/')),
isTrue);
});

testWidgets("raises an exception when tracking page view event on mobile",
(WidgetTester tester) async {
if (kIsWeb) {
return;
}
await tester.pumpWidget(const MyApp(testing: true));

try {
await Snowplow.track(const PageViewEvent(), tracker: 'test');
Expand Down
6 changes: 5 additions & 1 deletion example/integration_test/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ import 'package:http/http.dart' as http;

import 'package:flutter_test/flutter_test.dart';
import 'package:snowplow_tracker/snowplow.dart';
import 'package:snowplow_tracker/tracker.dart';
import 'dart:convert';

class SnowplowTests {
static SnowplowTracker? tracker;

static const microEndpoint =
String.fromEnvironment('ENDPOINT', defaultValue: 'http://0.0.0.0:9090');

static Future<void> createTracker() async {
await Snowplow.createTracker(namespace: 'test', endpoint: microEndpoint);
tracker = await Snowplow.createTracker(
namespace: 'test', endpoint: microEndpoint);
}

static Future<void> resetMicro() async {
Expand Down
26 changes: 8 additions & 18 deletions example/integration_test/session_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ import 'package:snowplow_tracker/tracker.dart';
import 'package:integration_test/integration_test.dart';

import 'helpers.dart';
import 'package:snowplow_tracker_example/main.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

setUpAll(() async {
await SnowplowTests.createTracker();
SnowplowTests.createTracker();
});

setUp(() async {
Expand All @@ -32,11 +31,8 @@ void main() {

testWidgets("maintains the same session context",
(WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

await Snowplow.track(
const Structured(category: 'category', action: 'action'),
tracker: "test");
await SnowplowTests.tracker
?.track(const Structured(category: 'category', action: 'action'));

dynamic clientSession1;
expect(
Expand All @@ -60,9 +56,8 @@ void main() {

await SnowplowTests.resetMicro();

await Snowplow.track(
const Structured(category: 'category', action: 'action'),
tracker: "test");
await SnowplowTests.tracker
?.track(const Structured(category: 'category', action: 'action'));

dynamic clientSession2;
expect(
Expand Down Expand Up @@ -90,11 +85,8 @@ void main() {

testWidgets("tracks the same session information as returned from API",
(WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

await Snowplow.track(
const Structured(category: 'category', action: 'action'),
tracker: "test");
await SnowplowTests.tracker
?.track(const Structured(category: 'category', action: 'action'));

dynamic clientSession;
expect(
Expand Down Expand Up @@ -123,9 +115,7 @@ void main() {

testWidgets("doesn't add session context when disabled",
(WidgetTester tester) async {
await tester.pumpWidget(const MyApp(testing: true));

Tracker tracker = await Snowplow.createTracker(
SnowplowTracker tracker = await Snowplow.createTracker(
namespace: 'test-without-session',
endpoint: SnowplowTests.microEndpoint,
trackerConfig: const TrackerConfiguration(sessionContext: false));
Expand Down
Loading

0 comments on commit f957bc2

Please sign in to comment.