track method
Checks consent, the cap and the payload, and queues the event.
A denied category is dropped here, before anything is written
(DV-ANALYTICS-001), so granting consent later does not send it.
Implementation
Future<DVTrackResult> track(DVAnalyticsEvent event) async {
final DVConsentCategory category = event.category;
if (consent.policy.declaration(category) == null) {
throw ArgumentError.value(category.name, 'category',
'of "${event.name}" is not declared in the consent policy');
}
if (!consent.isGranted(category)) {
_diagnose(
'DV-ANALYTICS-001',
'"${event.name}" dropped on the device: "${category.name}" has no '
'consent',
);
return const DVTrackResult._(false, 'DV-ANALYTICS-001', 'no consent');
}
final Map<String, Object?> properties;
try {
properties = _shapeMap(event.properties, direct: true);
} on _SensitiveNamed catch (named) {
_diagnose(
'DV-ANALYTICS-004',
'"${event.name}" names the sensitive field "${named.field}"; the event '
'was not recorded',
);
return const DVTrackResult._(
false, 'DV-ANALYTICS-004', 'names a sensitive field');
} on _Unshaped catch (unshaped) {
final String reason =
'a ${unshaped.type} has no analytics shape (toPublicJson or toJson)';
DVObservability.log('"${event.name}" not recorded: $reason',
level: DVLogLevel.error);
return DVTrackResult._(false, null, reason);
}
final int count = (_counts[event.name] ?? 0) + 1;
_counts[event.name] = count;
if (count > sessionCap) {
if (_capped.add(event.name)) {
_diagnose(
'DV-ANALYTICS-003',
'"${event.name}" reached the per-session cap of $sessionCap; '
'further events of it are dropped this session',
);
}
return const DVTrackResult._(false, 'DV-ANALYTICS-003', 'session cap');
}
final DVAnalyticsRecord record = DVAnalyticsRecord(
id: dvAnalyticsRandomId(),
name: event.name,
category: category.name,
userId: _userId,
anonymousId: _userId == null ? _anonymousId : null,
sessionId: _sessionId,
occurredAt: _clock().toUtc(),
policyVersion: consent.policy.version,
properties: properties,
tenant: const DVTenants().currentTenant,
);
final List<String> sinks = <String>[
storeName,
for (final DVAnalyticsProvider p in providers)
if (p.categories.contains(category)) p.name,
];
await _serial(() async {
final String body = jsonEncode(record.toJson());
for (final String sink in sinks) {
await database.execute(
'INSERT INTO $outboxTable (seq, sink, event_id, category, user_id, '
'anonymous_id, record) VALUES (?, ?, ?, ?, ?, ?, ?)',
<Object?>[
++_seq,
sink,
record.id,
record.category,
record.userId,
record.anonymousId,
body,
],
);
}
});
return const DVTrackResult._(true, null, null);
}