blob: 9590a29212a3cdbecc4fa605089497afaa61b0b3 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/attribution_reporting/event_report_windows.h"
#include <stddef.h>
#include <algorithm>
#include <functional>
#include <iterator>
#include <optional>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/flat_set.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "base/values.h"
#include "components/attribution_reporting/constants.h"
#include "components/attribution_reporting/parsing_utils.h"
#include "components/attribution_reporting/source_registration_error.mojom-shared.h"
#include "components/attribution_reporting/source_type.mojom-shared.h"
namespace attribution_reporting {
namespace {
using ::attribution_reporting::mojom::SourceRegistrationError;
using ::attribution_reporting::mojom::SourceType;
bool IsValid(base::TimeDelta start_time,
const base::flat_set<base::TimeDelta>& end_times) {
return !start_time.is_negative() && !end_times.empty() &&
end_times.size() <= kMaxEventLevelReportWindows &&
*end_times.begin() > start_time &&
*end_times.begin() >= kMinReportWindow;
}
bool IsReportWindowValid(base::TimeDelta report_window) {
return report_window >= kMinReportWindow && report_window <= kMaxSourceExpiry;
}
bool IsStrictlyIncreasing(const std::vector<base::TimeDelta>& end_times) {
return std::ranges::adjacent_find(end_times, std::not_fn(std::less{})) ==
end_times.end();
}
base::Time ReportTimeFromDeadline(base::Time source_time,
base::TimeDelta deadline) {
// Valid conversion reports should always have a valid reporting deadline.
CHECK(deadline.is_positive());
return source_time + deadline;
}
} // namespace
// static
std::optional<EventReportWindows> EventReportWindows::FromDefaults(
base::TimeDelta report_window,
SourceType source_type) {
if (!IsReportWindowValid(report_window)) {
return std::nullopt;
}
return EventReportWindows(report_window, source_type);
}
// static
std::optional<EventReportWindows> EventReportWindows::Create(
base::TimeDelta start_time,
std::vector<base::TimeDelta> end_times) {
if (!IsStrictlyIncreasing(end_times)) {
return std::nullopt;
}
base::flat_set<base::TimeDelta> end_times_set(base::sorted_unique,
std::move(end_times));
if (!IsValid(start_time, end_times_set)) {
return std::nullopt;
}
return EventReportWindows(start_time, std::move(end_times_set));
}
EventReportWindows::EventReportWindows(
base::TimeDelta start_time,
base::flat_set<base::TimeDelta> end_times)
: start_time_(start_time), end_times_(std::move(end_times)) {
CHECK(IsValid(start_time_, end_times_));
}
EventReportWindows::EventReportWindows(base::TimeDelta report_window,
SourceType source_type) {
CHECK(IsReportWindowValid(report_window));
std::vector<base::TimeDelta> end_times;
switch (source_type) {
case SourceType::kNavigation:
end_times = {base::Days(2), base::Days(7)};
break;
case SourceType::kEvent:
break;
}
while (!end_times.empty() && report_window <= end_times.back()) {
end_times.pop_back();
}
end_times.push_back(report_window);
end_times_.replace(std::move(end_times));
CHECK(IsValid(start_time_, end_times_));
}
EventReportWindows::EventReportWindows()
: EventReportWindows(/*start_time=*/base::TimeDelta(),
/*end_times=*/{kMaxSourceExpiry}) {}
EventReportWindows::~EventReportWindows() = default;
EventReportWindows::EventReportWindows(const EventReportWindows&) = default;
EventReportWindows& EventReportWindows::operator=(const EventReportWindows&) =
default;
EventReportWindows::EventReportWindows(EventReportWindows&&) = default;
EventReportWindows& EventReportWindows::operator=(EventReportWindows&&) =
default;
// Follows the steps detailed in
// https://wicg.github.io/attribution-reporting-api/#obtain-an-event-level-report-delivery-time
// Starting from step 2.
base::Time EventReportWindows::ComputeReportTime(
base::Time source_time,
base::Time trigger_time) const {
// It is possible for a source to have an assigned time of T and a trigger
// that is attributed to it to have a time of T-X e.g. due to user-initiated
// clock changes.
//
// TODO(crbug.com/40282914): Assume `source_time` is smaller than
// `trigger_time` once attribution time resolution is implemented in storage.
const base::Time trigger_time_floored =
source_time < trigger_time ? trigger_time
: source_time + base::Microseconds(1);
base::TimeDelta reporting_window_to_use = *end_times_.rbegin();
for (base::TimeDelta reporting_window : end_times_) {
if (source_time + reporting_window <= trigger_time_floored) {
continue;
}
reporting_window_to_use = reporting_window;
break;
}
return ReportTimeFromDeadline(source_time, reporting_window_to_use);
}
base::Time EventReportWindows::ReportTimeAtWindow(base::Time source_time,
int window_index) const {
CHECK_GE(window_index, 0);
CHECK_LT(static_cast<size_t>(window_index), end_times_.size());
return ReportTimeFromDeadline(source_time,
*std::next(end_times_.begin(), window_index));
}
base::Time EventReportWindows::StartTimeAtWindow(base::Time source_time,
int window_index) const {
CHECK_GE(window_index, 0);
CHECK_LT(static_cast<size_t>(window_index), end_times_.size());
if (window_index == 0) {
return source_time + start_time_;
}
return source_time + *std::next(end_times_.begin(), window_index - 1);
}
EventReportWindows::WindowResult EventReportWindows::FallsWithin(
base::TimeDelta trigger_moment) const {
// It is possible for a source to have an assigned time of T and a trigger
// that is attributed to it to have a time of T-X e.g. due to user-initiated
// clock changes.
//
// TODO(crbug.com/40283992): Assume trigger moment is not negative once
// attribution time resolution is implemented in storage.
base::TimeDelta bounded_trigger_moment =
trigger_moment.is_negative() ? base::Microseconds(0) : trigger_moment;
if (bounded_trigger_moment < start_time_) {
return WindowResult::kNotStarted;
}
if (bounded_trigger_moment >= *end_times_.rbegin()) {
return WindowResult::kPassed;
}
return WindowResult::kFallsWithin;
}
base::expected<EventReportWindows, SourceRegistrationError>
EventReportWindows::FromJSON(const base::Value::Dict& registration,
base::TimeDelta expiry,
SourceType source_type) {
const base::Value* singular_window = registration.Find(kEventReportWindow);
const base::Value* multiple_windows = registration.Find(kEventReportWindows);
base::UmaHistogramBoolean("Conversions.LegacyEventReportWindow",
!!singular_window);
if (singular_window && multiple_windows) {
return base::unexpected(
SourceRegistrationError::kBothEventReportWindowFieldsFound);
} else if (singular_window) {
ASSIGN_OR_RETURN(
base::TimeDelta report_window,
ParseLegacyDuration(*singular_window,
/*clamp_min=*/kMinReportWindow,
/*clamp_max=*/expiry),
[](ParseError) {
return SourceRegistrationError::kEventReportWindowValueInvalid;
});
return EventReportWindows(report_window, source_type);
} else if (!multiple_windows) {
return EventReportWindows(expiry, source_type);
}
const base::Value::Dict* dict = multiple_windows->GetIfDict();
if (!dict) {
return base::unexpected(
SourceRegistrationError::kEventReportWindowsWrongType);
}
base::TimeDelta start_time = base::Seconds(0);
if (const base::Value* start_time_value = dict->Find(kStartTime)) {
ASSIGN_OR_RETURN(
int int_value, ParseInt(*start_time_value), [](ParseError) {
return SourceRegistrationError::kEventReportWindowsStartTimeInvalid;
});
start_time = base::Seconds(int_value);
if (start_time.is_negative() || start_time > expiry) {
return base::unexpected(
SourceRegistrationError::kEventReportWindowsStartTimeInvalid);
}
}
const base::Value* end_times_value = dict->Find(kEndTimes);
if (!end_times_value) {
return base::unexpected(
SourceRegistrationError::kEventReportWindowsEndTimesMissing);
}
const base::Value::List* end_times_list = end_times_value->GetIfList();
if (!end_times_list || end_times_list->empty() ||
end_times_list->size() > kMaxEventLevelReportWindows) {
return base::unexpected(
SourceRegistrationError::kEventReportWindowsEndTimesListInvalid);
}
std::vector<base::TimeDelta> end_times;
end_times.reserve(end_times_list->size());
base::TimeDelta start_duration = start_time;
for (const auto& item : *end_times_list) {
ASSIGN_OR_RETURN(base::TimeDelta end_time, ParseDuration(item),
[](ParseError) {
return SourceRegistrationError::
kEventReportWindowsEndTimeValueInvalid;
});
if (!end_time.is_positive()) {
return base::unexpected(
SourceRegistrationError::kEventReportWindowsEndTimeValueInvalid);
}
if (end_time > expiry) {
end_time = expiry;
}
if (end_time < kMinReportWindow) {
end_time = kMinReportWindow;
}
if (end_time <= start_duration) {
return base::unexpected(
SourceRegistrationError::kEventReportWindowsEndTimeDurationLTEStart);
}
end_times.push_back(end_time);
start_duration = end_time;
}
return EventReportWindows(start_time, std::move(end_times));
}
void EventReportWindows::Serialize(base::Value::Dict& dict) const {
base::Value::Dict windows_dict;
windows_dict.Set(kStartTime, static_cast<int>(start_time_.InSeconds()));
auto list = base::Value::List::with_capacity(end_times_.size());
for (const auto& end_time : end_times_) {
list.Append(static_cast<int>(end_time.InSeconds()));
}
windows_dict.Set(kEndTimes, std::move(list));
dict.Set(kEventReportWindows, std::move(windows_dict));
}
bool EventReportWindows::IsValidForExpiry(base::TimeDelta expiry) const {
return start_time_ <= expiry && *end_times_.rbegin() <= expiry;
}
} // namespace attribution_reporting