accept method

Future<DVCrashIngestResult> accept(
  1. List<int> body, {
  2. String source = DVClientAddress.unknownSource,
})

Accepts one report sent from source: the client's key as DVClientAddress.sourceOf gives it. A caller that passes none counts every such report in one unknown source, which is limited rather than left unlimited.

Implementation

Future<DVCrashIngestResult> accept(
  List<int> body, {
  String source = DVClientAddress.unknownSource,
}) async {
  if (body.length > maxBytes) {
    return const DVCrashIngestResult(DVCrashIngestOutcome.tooLarge);
  }
  final DVCrashReport report;
  try {
    final Object? json = jsonDecode(utf8.decode(body));
    if (json is! Map<String, Object?>) {
      return const DVCrashIngestResult(DVCrashIngestOutcome.invalid);
    }
    report = DVCrashReport.fromJson(json);
  } on Object {
    return const DVCrashIngestResult(DVCrashIngestOutcome.invalid);
  }
  final String installId = report.context.installId;
  if (!_usable(report.id) ||
      !_usable(installId) ||
      !_usable(report.context.release)) {
    return const DVCrashIngestResult(DVCrashIngestOutcome.invalid);
  }

  final DateTime now = _clock();
  _sweep(now);
  // Looked up, not created: an install id is created only once a report of
  // its is stored, so a client writing a new id per report does not grow
  // this map with ids that were refused.
  final ListQueue<(DateTime, String)> recent =
      _recent[installId] ?? ListQueue<(DateTime, String)>();
  while (recent.isNotEmpty && now.difference(recent.first.$1) >= _window) {
    recent.removeFirst();
  }
  // A resend of one already stored this hour is the same crash, and does
  // not spend the budget.
  if (recent.any(((DateTime, String) entry) => entry.$2 == report.id)) {
    return const DVCrashIngestResult(DVCrashIngestOutcome.duplicate);
  }
  // The install's limit first: one install in a crash loop is answered
  // 202, which is final, rather than 429, which it would retry forever.
  if (recent.length >= perInstallPerHour) {
    _limited[installId] = limited(installId) + 1;
    final DateTime? reported = _limitReported[installId];
    if (reported == null || now.difference(reported) >= _window) {
      _limitReported[installId] = now;
      // Not which install: the id is the report's, and nothing of a report
      // is logged.
      _log('DV-CRASH-004: an install is past $perInstallPerHour crash '
          'reports this hour; further reports from it are counted, not '
          'stored');
    }
    return const DVCrashIngestResult(DVCrashIngestOutcome.limited);
  }

  final ListQueue<DateTime> fromSource =
      _fromSource[source] ?? ListQueue<DateTime>();
  while (fromSource.isNotEmpty &&
      now.difference(fromSource.first) >= _window) {
    fromSource.removeFirst();
  }
  if (fromSource.length >= perSourcePerHour) {
    _sourceLimited[source] = sourceLimited(source) + 1;
    final DateTime? reported = _sourceLimitReported[source];
    if (reported == null || now.difference(reported) >= _window) {
      _sourceLimitReported[source] = now;
      // Not which address: a client address is personal data, and the log
      // says what happened, not to whom.
      _log('DV-CRASH-004: a client address is past $perSourcePerHour crash '
          'reports this hour; further reports from it are refused with 429 '
          'and sent again later');
    }
    return const DVCrashIngestResult(DVCrashIngestOutcome.sourceLimited);
  }

  final bool kept;
  try {
    kept = await repository.put(await _symbolicated(report), receivedAt: now);
  } on Object catch (error) {
    _log('a crash report could not be stored (${error.runtimeType}); the '
        'client keeps it and sends it again');
    return const DVCrashIngestResult(DVCrashIngestOutcome.unavailable);
  }
  if (!kept) {
    return const DVCrashIngestResult(DVCrashIngestOutcome.duplicate);
  }
  // Only a stored report spends either budget: a duplicate, a report
  // counted per install, and a store that failed and will be retried do not.
  _recent[installId] = recent..add((now, report.id));
  _fromSource[source] = fromSource..add(now);
  return const DVCrashIngestResult(DVCrashIngestOutcome.stored);
}