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);
}