Skip to content
Open
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 CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

* Add opt-in support for sending the configured visitor ID as Matomo's `cid`
parameter.

## [6.1.1]

* feat: handle optOut in initialization by @TesteurManiak in https://github.com/Floating-Dartists/matomo-tracker/pull/203
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ A fully cross-platform wrap of the Matomo tracking client for Flutter, using the
- [Documentation](#documentation)
- [Supported Matomo Versions](#supported-matomo-versions)
- [Getting Started](#getting-started)
- [Using the Matomo `cid` parameter](#using-the-matomo-cid-parameter)
- [Using userId](#using-userid)
- [Opting Out](#opting-out)
- [Using Dimensions](#using-dimensions)
Expand Down Expand Up @@ -60,6 +61,31 @@ await MatomoTracker.instance.initialize(
```
Note that this Visitor ID should not be confused with the User ID which is explained below!

## Using the Matomo `cid` parameter

By default, the package sends `visitorId` as Matomo's recommended `_id`
parameter. If you need to explicitly control Matomo's visitor matching, you
can opt in to the Tracking API's `cid` parameter:

```dart
await MatomoTracker.instance.initialize(
siteId: siteId,
url: 'https://example.com/matomo.php',
visitorId: '0123456789abcdef',
visitorIdParameter: VisitorIdParameter.cid,
);
```

The `cid` value must be exactly 16 hexadecimal characters and should remain
stable for the visitor. With this option, the package sends `cid` instead of
`_id` on every tracking request. The default remains `_id` for backwards
compatibility.

`cid` is a request parameter, not a value returned by Matomo. It makes the
visitor ID used for new requests explicit; it does not recover or change the
visitor ID of previously tracked requests. For authenticated users, Matomo's
`uid` remains the recommended way to associate activity with a user identity.

## Navigator Observers

The package provides you with two ways to track views:
Expand Down
54 changes: 51 additions & 3 deletions lib/src/matomo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ class MatomoTracker {
Visitor get visitor => _visitor;
late Visitor _visitor;

/// The Matomo request parameter used for [visitor.id].
///
/// This defaults to [VisitorIdParameter.id], which sends the ID as `_id`.
/// Set it to [VisitorIdParameter.cid] during [initialize] only when the
/// application has a stable, valid Matomo visitor ID and needs to explicitly
/// control Matomo's visitor matching for each request.
late final VisitorIdParameter visitorIdParameter;

/// Sets the [User ID](https://matomo.org/guide/reports/user-ids/).
///
/// This should not be confused with the [visitorId] of the [initialize]
Expand Down Expand Up @@ -164,6 +172,10 @@ class MatomoTracker {
_visitor = const Visitor();
} else {
final visitorId = await _getVisitorId();
_validateVisitorIdParameter(
visitorId: visitorId,
visitorIdParameter: visitorIdParameter,
);
Comment on lines +175 to +178
_visitor = Visitor(id: visitorId);
}
}
Expand Down Expand Up @@ -217,9 +229,14 @@ class MatomoTracker {
///
/// The [visitorId] should have a length of 16 characters otherwise an
/// [ArgumentError] will be thrown. This parameter corresponds with the
/// `_id` and should not be confused with the user id `uid`. See the
/// [Visitor] class for additional remarks. It is recommended to leave this
/// to `null` to use an automatically generated id.
/// `_id` by default and should not be confused with the user id `uid`. See
/// the [Visitor] class for additional remarks. It is recommended to leave
/// this to `null` to use an automatically generated id.
///
/// Set [visitorIdParameter] to [VisitorIdParameter.cid] to send the visitor
/// ID as Matomo's `cid` parameter instead. In that mode, the effective
/// visitor ID must be exactly 16 hexadecimal characters. The default is
/// [VisitorIdParameter.id], preserving the package's existing behavior.
///
/// If [cookieless] is set to true, a [CookielessStorage] instance will be
/// used. This means that the first_visit and the user_id will be stored in
Expand All @@ -237,6 +254,7 @@ class MatomoTracker {
required String url,
bool newVisit = true,
String? visitorId,
VisitorIdParameter visitorIdParameter = VisitorIdParameter.id,
String? uid,
String? contentBaseUrl,
DispatchSettings dispatchSettings = const DispatchSettings.nonPersistent(),
Expand Down Expand Up @@ -265,6 +283,11 @@ class MatomoTracker {
);
}

_validateVisitorIdParameter(
visitorId: visitorId,
visitorIdParameter: visitorIdParameter,
);

assertDurationNotNegative(
value: dispatchSettings.dequeueInterval,
name: 'dequeueInterval',
Expand All @@ -286,6 +309,7 @@ class MatomoTracker {
_tokenAuth = tokenAuth;
_newVisit = newVisit;
this.attachLastScreenInfo = attachLastScreenInfo;
this.visitorIdParameter = visitorIdParameter;
_dispatchSettings = dispatchSettings;

_setLocalStorage(localStorage);
Expand All @@ -299,6 +323,10 @@ class MatomoTracker {
: Queue();

final localVisitorId = visitorId ?? await _getVisitorId();
_validateVisitorIdParameter(
visitorId: localVisitorId,
visitorIdParameter: visitorIdParameter,
);
_visitor = Visitor(id: localVisitorId, uid: uid);

// User agent
Expand Down Expand Up @@ -967,6 +995,26 @@ class MatomoTracker {
return localId ?? const Uuid().v4().replaceAll('-', '').substring(0, 16);
}

void _validateVisitorIdParameter({
required String? visitorId,
required VisitorIdParameter visitorIdParameter,
}) {
if (visitorIdParameter != VisitorIdParameter.cid || visitorId == null) {
return;
}

final isHexadecimalVisitorId =
RegExp(r'^[0-9a-fA-F]{16}$').hasMatch(visitorId);
if (!isHexadecimalVisitorId) {
throw ArgumentError.value(
visitorId,
'visitorId',
'The visitorId must be exactly 16 hexadecimal characters when '
'visitorIdParameter is VisitorIdParameter.cid',
);
}
}

@visibleForTesting
void validateDimension(Map<String, String>? dimensions) {
if (dimensions == null) {
Expand Down
4 changes: 3 additions & 1 deletion lib/src/matomo_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ class MatomoAction {
final locale = userLocale ?? PlatformDispatcher.instance.locale;
final country = locale.countryCode?.toLowerCase();
final ping = this.ping ?? false;
final visitorIdParameter =
tracker.visitorIdParameter == VisitorIdParameter.cid ? 'cid' : '_id';

return {
// Required parameters
Expand All @@ -206,7 +208,7 @@ class MatomoAction {
'url': url,
if (campaign?.name case final name?) '_rcn': name,
if (campaign?.keyword case final keyword?) '_rck': keyword,
if (tracker.visitor.id case final id?) '_id': id,
if (tracker.visitor.id case final id?) visitorIdParameter: id,
'rand': '${Random().nextInt(1000000000)}',
'apiv': '1',

Expand Down
14 changes: 13 additions & 1 deletion lib/src/visitor.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
/// The Matomo request parameter used for the configured visitor ID.
enum VisitorIdParameter {
/// Send the visitor ID as Matomo's recommended `_id` parameter.
id,

/// Send the visitor ID as Matomo's explicit `cid` parameter.
///
/// Matomo requires this value to be exactly 16 hexadecimal characters.
cid,
}

class Visitor {
const Visitor({
this.id,
Expand All @@ -9,7 +20,8 @@ class Visitor {

/// The unique visitor ID, must be a 16 characters hexadecimal string.
///
/// Corresponds with `_id`.
/// Corresponds with `_id` by default. See `MatomoTracker.visitorIdParameter`
/// for opting in to `cid`.
///
/// Every unique visitor must be assigned a different ID and this ID must not
/// change after it is assigned. If this value is not set Matomo will still
Expand Down
2 changes: 2 additions & 0 deletions test/ressources/utils/get_initialized_mamoto_tracker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Future<MatomoTracker> getInitializedMatomoTracker({
PlatformInfo? platformInfo,
bool shouldForceCreation = true,
bool cookieless = false,
VisitorIdParameter visitorIdParameter = VisitorIdParameter.id,
}) async {
final matomoTracker =
shouldForceCreation ? MatomoTracker() : MatomoTracker.instance;
Expand All @@ -31,6 +32,7 @@ Future<MatomoTracker> getInitializedMatomoTracker({
localStorage: mockLocalStorage,
packageInfo: mockPackageInfo,
visitorId: visitorId,
visitorIdParameter: visitorIdParameter,
uid: uid,
contentBaseUrl: contentBaseUrl,
tokenAuth: tokenAuth,
Expand Down
3 changes: 3 additions & 0 deletions test/src/dispatch_settings_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:collection/collection.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:matomo_tracker/src/dispatch_settings.dart';
import 'package:matomo_tracker/src/matomo_action.dart';
import 'package:matomo_tracker/src/visitor.dart';
import 'package:mocktail/mocktail.dart';

import '../ressources/mock/data.dart';
Expand Down Expand Up @@ -37,6 +38,8 @@ void main() {
when(() => mockMatomoTracker.contentBase)
.thenReturn(matomoTrackerContentBase);
when(() => mockMatomoTracker.siteId).thenReturn(matomoTrackerSiteId);
when(() => mockMatomoTracker.visitorIdParameter)
.thenReturn(VisitorIdParameter.id);
when(() => mockVisitor.id).thenReturn(visitorId);
when(() => mockVisitor.uid).thenReturn(uid);
when(mockTrackingOrderItem.toArray).thenReturn([]);
Expand Down
20 changes: 20 additions & 0 deletions test/src/matomo_action_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:matomo_tracker/src/content.dart';
import 'package:matomo_tracker/src/event_info.dart';
import 'package:matomo_tracker/src/matomo_action.dart';
import 'package:matomo_tracker/src/performance_info.dart';
import 'package:matomo_tracker/src/visitor.dart';
import 'package:mocktail/mocktail.dart';

import '../ressources/mock/data.dart';
Expand Down Expand Up @@ -208,6 +209,8 @@ void main() {
when(() => mockMatomoTracker.contentBase)
.thenReturn(matomoTrackerContentBase);
when(() => mockMatomoTracker.siteId).thenReturn(matomoTrackerSiteId);
when(() => mockMatomoTracker.visitorIdParameter)
.thenReturn(VisitorIdParameter.id);
when(() => mockVisitor.id).thenReturn(visitorId);
when(() => mockVisitor.uid).thenReturn(uid);
when(mockTrackingOrderItem.toArray).thenReturn([]);
Expand Down Expand Up @@ -266,6 +269,19 @@ void main() {
expect(Uri.encodeQueryComponent(eventMap['url'] ?? ''), expectedUrl);
});
});

test('it should use cid when configured', () {
when(() => mockMatomoTracker.visitorIdParameter)
.thenReturn(VisitorIdParameter.cid);

final eventMap = MatomoAction().toMap(mockMatomoTracker);

expect(eventMap['cid'], visitorId);
expect(eventMap, isNot(contains('_id')));

when(() => mockMatomoTracker.visitorIdParameter)
.thenReturn(VisitorIdParameter.id);
});
});

group('copyWith', () {
Expand All @@ -276,6 +292,8 @@ void main() {
when(() => mockMatomoTracker.contentBase)
.thenReturn(matomoTrackerContentBase);
when(() => mockMatomoTracker.siteId).thenReturn(matomoTrackerSiteId);
when(() => mockMatomoTracker.visitorIdParameter)
.thenReturn(VisitorIdParameter.id);
when(() => mockVisitor.id).thenReturn(visitorId);
when(() => mockVisitor.uid).thenReturn(uid);
when(mockTrackingOrderItem.toArray).thenReturn([]);
Expand Down Expand Up @@ -440,6 +458,8 @@ void main() {
when(() => mockMatomoTracker.contentBase)
.thenReturn(matomoTrackerContentBase);
when(() => mockMatomoTracker.siteId).thenReturn(matomoTrackerSiteId);
when(() => mockMatomoTracker.visitorIdParameter)
.thenReturn(VisitorIdParameter.id);
when(() => mockVisitor.id).thenReturn(visitorId);
when(() => mockVisitor.uid).thenReturn(uid);
when(mockTrackingOrderItem.toArray).thenReturn([]);
Expand Down
33 changes: 33 additions & 0 deletions test/src/matomo_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:matomo_tracker/src/content.dart';
import 'package:matomo_tracker/src/event_info.dart';
import 'package:matomo_tracker/src/exceptions.dart';
import 'package:matomo_tracker/src/matomo.dart';
import 'package:matomo_tracker/src/visitor.dart';
import 'package:mocktail/mocktail.dart';

import '../ressources/mock/data.dart';
Expand Down Expand Up @@ -66,6 +67,38 @@ void main() {
[(tracker, _) => expect(tracker.authToken, matomoTrackerTokenAuth)],
tokenAuth: matomoTrackerTokenAuth,
);

test('it should initialize with the cid visitor ID parameter', () async {
final tracker = await getInitializedMatomoTracker(
visitorId: matomoTrackerVisitorId,
visitorIdParameter: VisitorIdParameter.cid,
);

expect(tracker.visitorIdParameter, VisitorIdParameter.cid);
expect(tracker.visitor.id, matomoTrackerVisitorId);
});

test('it should reject a non-hexadecimal cid visitor ID', () async {
await expectLater(
() => getInitializedMatomoTracker(
visitorId: 'abcdefghijklmnop',
visitorIdParameter: VisitorIdParameter.cid,
),
throwsArgumentError,
);
});

test('it should reject a non-hexadecimal stored cid visitor ID', () async {
when(mockLocalStorage.getVisitorId)
.thenAnswer((_) async => 'abcdefghijklmnop');

await expectLater(
() => getInitializedMatomoTracker(
visitorIdParameter: VisitorIdParameter.cid,
),
throwsArgumentError,
);
});
});

group('OptOut', () {
Expand Down
Loading