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