Skip to content

Commit

Permalink
Merge branch 'release/0.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
matus-tomlein committed Jan 31, 2022
2 parents cd4b3d4 + 7e600c4 commit c0fe3e3
Show file tree
Hide file tree
Showing 32 changed files with 821 additions and 351 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 0.1.0

* Add route observer for auto tracking screen or page view events on navigation (#9)

# 0.1.0-alpha.2

* Add page view events and activity tracking on Web (#6)
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
package com.snowplowanalytics.snowplow_tracker

object TrackerVersion {
val TRACKER_VERSION = "flutter-0.1.0-alpha.2"
val TRACKER_VERSION = "flutter-0.1.0"
}
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 c0fe3e3

Please sign in to comment.