blob: f61ac5e7f7505474cd16b3bbf82f166f94ca0ef9 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/page/scrolling/text_fragment_selector_generator.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/default_tick_clock.h"
#include "components/shared_highlighting/core/common/shared_highlighting_features.h"
#include "components/shared_highlighting/core/common/shared_highlighting_metrics.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/platform/interface_registry.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/finder/find_buffer.h"
#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/page/scrolling/text_fragment_anchor_metrics.h"
#include "third_party/blink/renderer/core/page/scrolling/text_fragment_finder.h"
#include "third_party/blink/renderer/platform/text/text_boundaries.h"
using LinkGenerationError = shared_highlighting::LinkGenerationError;
namespace blink {
namespace {
// Returns true if text from beginning of |node| until |pos_offset| can be
// considered empty. Otherwise, return false.
bool IsFirstVisiblePosition(Node* node, unsigned pos_offset) {
auto range_start = Position::FirstPositionInNode(*node);
auto range_end = Position(node, pos_offset);
return node->getNodeType() == Node::kElementNode || pos_offset == 0 ||
PlainText(EphemeralRange(range_start, range_end))
.StripWhiteSpace()
.IsEmpty();
}
// Returns true if text from |pos_offset| until end of |node| can be considered
// empty. Otherwise, return false.
bool IsLastVisiblePosition(Node* node, unsigned pos_offset) {
auto range_start = Position(node, pos_offset);
auto range_end = Position::LastPositionInNode(*node);
return node->getNodeType() == Node::kElementNode ||
pos_offset == node->textContent().length() ||
PlainText(EphemeralRange(range_start, range_end))
.StripWhiteSpace()
.IsEmpty();
}
struct ForwadDirection {
static Node* Next(const Node& node) { return FlatTreeTraversal::Next(node); }
static Node* Next(const Node& node, const Node* stay_within) {
return FlatTreeTraversal::Next(node, stay_within);
}
static Node* GetVisibleTextNode(Node& start_node) {
return FindBuffer::ForwardVisibleTextNode(start_node);
}
// |IsInSameUninterruptedBlock| is diraction specific because |start| and
// |end| should be in right order.
static bool IsInSameUninterruptedBlock(Node& start, Node& end) {
return FindBuffer::IsInSameUninterruptedBlock(start, end);
}
};
struct BackwardDirection {
static Node* Next(const Node& node) {
return FlatTreeTraversal::Previous(node);
}
static Node* GetVisibleTextNode(Node& start_node) {
return FindBuffer::BackwardVisibleTextNode(start_node);
}
// |IsInSameUninterruptedBlock| is diraction specific because |start| and
// |end| should be in right order.
static bool IsInSameUninterruptedBlock(Node& start, Node& end) {
return FindBuffer::IsInSameUninterruptedBlock(end, start);
}
};
template <class Direction>
Node* NextNonEmptyVisibleTextNode(Node* start_node) {
if (!start_node)
return nullptr;
// Move forward/backward until non empty visible text node is found.
for (Node* node = start_node; node; node = Direction::Next(*node)) {
Node* next_node = Direction::GetVisibleTextNode(*node);
if (!next_node || !PlainText(EphemeralRange::RangeOfContents(*next_node))
.StripWhiteSpace()
.IsEmpty())
return next_node;
node = next_node;
}
return nullptr;
}
// Returns the next/previous visible node to |start_node|.
Node* FirstNonEmptyVisibleTextNode(Node* start_node) {
return NextNonEmptyVisibleTextNode<ForwadDirection>(start_node);
}
Node* BackwardNonEmptyVisibleTextNode(Node* start_node) {
return NextNonEmptyVisibleTextNode<BackwardDirection>(start_node);
}
// Returns the furthest node within same block as |start_node| without crossing
// block boundaries.
// Returns the next/previous visible node to |start_node|.
template <class Direction>
Node* FurthestVisibleTextNodeWithinBlock(Node* start_node) {
// Move forward/backward until no next/previous node is available within same
// |block_ancestor|.
Node* last_node = nullptr;
for (Node* node = start_node; node; node = Direction::Next(*node)) {
node = Direction::GetVisibleTextNode(*node);
if (node && !node->GetLayoutObject())
continue;
// Stop, if crossed block boundaries.
if (!node || !Direction::IsInSameUninterruptedBlock(*start_node, *node))
break;
last_node = node;
}
return last_node;
}
Node* FirstVisibleTextNodeWithinBlock(Node* start_node) {
return FurthestVisibleTextNodeWithinBlock<ForwadDirection>(start_node);
}
Node* LastVisibleTextNodeWithinBlock(Node* start_node) {
return FurthestVisibleTextNodeWithinBlock<BackwardDirection>(start_node);
}
// Returns whitespace-trimmed substring of |text| containing the final
// |word_num| words.
String GetWordsFromEnd(String text, int word_num) {
if (text.IsEmpty())
return "";
int pos = text.length() - 1;
text.Ensure16Bit();
while (word_num-- > 0)
pos = FindNextWordBackward(text.Characters16(), text.length(), pos);
return text.Substring(pos, text.length()).StripWhiteSpace();
}
// Returns whitespace-trimmed substring of |text| containing the first
// |word_num| words.
String GetWordsFromStart(String text, int word_num) {
if (text.IsEmpty())
return "";
int pos = 0;
text.Ensure16Bit();
while (word_num-- > 0)
pos = FindNextWordForward(text.Characters16(), text.length(), pos);
return text.Substring(0, pos).StripWhiteSpace();
}
// For Element-based Position returns the node that its pointing to, otherwise
// returns the container node.
Node* ResolvePositionToNode(const PositionInFlatTree& position) {
Node* node = position.ComputeContainerNode();
int offset = position.ComputeOffsetInContainerNode();
if (node->getNodeType() == Node::kElementNode && node->hasChildren() &&
node->childNodes()->item(offset)) {
return node->childNodes()->item(offset);
}
return node;
}
} // namespace
constexpr int kExactTextMaxChars = 300;
constexpr int kNoContextMinChars = 20;
constexpr int kMaxContextWords = 10;
constexpr int kMaxRangeWords = 10;
constexpr int kMaxIterationCountToRecord = 10;
constexpr int kMinWordCount_ = 3;
void TextFragmentSelectorGenerator::UpdateSelection(
LocalFrame* selection_frame,
const EphemeralRangeInFlatTree& selection_range) {
DCHECK(selection_frame);
// Scroll-to-text doesn't support iframes.
DCHECK(selection_frame->IsMainFrame());
selection_frame_ = selection_frame;
selection_range_ = MakeGarbageCollected<Range>(
selection_range.GetDocument(),
ToPositionInDOMTree(selection_range.StartPosition()),
ToPositionInDOMTree(selection_range.EndPosition()));
if (base::FeatureList::IsEnabled(
shared_highlighting::kPreemptiveLinkToTextGeneration)) {
Reset();
GenerateSelector();
}
}
void TextFragmentSelectorGenerator::BindTextFragmentSelectorProducer(
mojo::PendingReceiver<mojom::blink::TextFragmentSelectorProducer>
producer) {
DCHECK(selection_frame_);
selector_producer_.reset();
selector_producer_.Bind(
std::move(producer),
selection_frame_->GetTaskRunner(blink::TaskType::kInternalDefault));
}
void TextFragmentSelectorGenerator::AdjustSelection() {
if (!selection_range_)
return;
EphemeralRangeInFlatTree ephemeral_range(selection_range_);
Node* start_container =
ephemeral_range.StartPosition().ComputeContainerNode();
Node* end_container = ephemeral_range.EndPosition().ComputeContainerNode();
Node* corrected_start =
ResolvePositionToNode(ephemeral_range.StartPosition());
int corrected_start_offset =
(corrected_start->isSameNode(start_container))
? ephemeral_range.StartPosition().ComputeOffsetInContainerNode()
: 0;
Node* corrected_end = ResolvePositionToNode(ephemeral_range.EndPosition());
int corrected_end_offset =
(corrected_end->isSameNode(end_container))
? ephemeral_range.EndPosition().ComputeOffsetInContainerNode()
: 0;
// If start node has no text or given start position point to the last visible
// text in its containiner node, use the following visible node for selection
// start. This has to happen before generation, so that selection is correctly
// classified as same block or not.
if (IsLastVisiblePosition(corrected_start, corrected_start_offset)) {
corrected_start = FirstNonEmptyVisibleTextNode(
FlatTreeTraversal::NextSkippingChildren(*corrected_start));
corrected_start_offset = 0;
} else {
// if node change was not necessary move start and end positions to
// contain full words. This is not necessary when node change happened
// because block limits are also word limits.
String start_text = corrected_start->textContent();
start_text.Ensure16Bit();
corrected_start_offset = FindWordStartBoundary(
start_text.Characters16(), start_text.length(), corrected_start_offset);
}
// If end node has no text or given end position point to the first visible
// text in its containiner node, use the previous visible node for selection
// end. This has to happen before generation, so that selection is correctly
// classified as same block or not.
if (IsFirstVisiblePosition(corrected_end, corrected_end_offset)) {
// Here, |Previous()| already skips the children of the given node,
// because we're doing pre-order traversal.
corrected_end = BackwardNonEmptyVisibleTextNode(
FlatTreeTraversal::Previous(*corrected_end));
if (corrected_end)
corrected_end_offset = corrected_end->textContent().length();
} else {
// if node change was not necessary move start and end positions to
// contain full words. This is not necessary when node change happened
// because block limits are also word limits.
String end_text = corrected_end->textContent();
end_text.Ensure16Bit();
// If |selection_end_pos| is at the beginning of a new word then don't
// search for the word end as it will be the end of the next word, which was
// not included in the selection.
if (corrected_end_offset != FindWordStartBoundary(end_text.Characters16(),
end_text.length(),
corrected_end_offset)) {
corrected_end_offset = FindWordEndBoundary(
end_text.Characters16(), end_text.length(), corrected_end_offset);
}
}
if (corrected_start != start_container ||
corrected_start_offset !=
ephemeral_range.StartPosition().ComputeOffsetInContainerNode() ||
corrected_end != end_container ||
corrected_end_offset !=
ephemeral_range.EndPosition().ComputeOffsetInContainerNode()) {
selection_range_ = MakeGarbageCollected<Range>(
selection_range_->OwnerDocument(),
Position(corrected_start, corrected_start_offset),
Position(corrected_end, corrected_end_offset));
}
}
void TextFragmentSelectorGenerator::Cancel() {
Reset();
}
void TextFragmentSelectorGenerator::RequestSelector(
RequestSelectorCallback callback) {
DCHECK(callback);
if (!base::FeatureList::IsEnabled(
shared_highlighting::kPreemptiveLinkToTextGeneration)) {
Reset();
pending_generate_selector_callback_ = std::move(callback);
GenerateSelector();
} else {
pending_generate_selector_callback_ = std::move(callback);
DCHECK_NE(state_, kNotStarted);
if (state_ == kFailure || state_ == kSuccess) {
selector_requested_before_ready_ = false;
if (state_ == kFailure) {
NotifyClientSelectorReady(
TextFragmentSelector(TextFragmentSelector::SelectorType::kInvalid));
} else {
NotifyClientSelectorReady(*selector_);
}
return;
}
selector_requested_before_ready_ = true;
}
}
void TextFragmentSelectorGenerator::GenerateSelector() {
DCHECK(selection_range_);
selection_range_->OwnerDocument().UpdateStyleAndLayout(
DocumentUpdateReason::kFindInPage);
// Shouldn't continue is selection is empty.
EphemeralRangeInFlatTree ephemeral_range(selection_range_);
String selected_text = PlainText(ephemeral_range).StripWhiteSpace();
if (selected_text.IsEmpty()) {
state_ = kFailure;
error_ = LinkGenerationError::kEmptySelection;
ResolveSelectorState();
return;
}
AdjustSelection();
UMA_HISTOGRAM_COUNTS_1000(
"SharedHighlights.LinkGenerated.SelectionLength",
PlainText(EphemeralRange(selection_range_)).length());
state_ = kNeedsNewCandidate;
GenerateSelectorCandidate();
}
void TextFragmentSelectorGenerator::GenerateSelectorCandidate() {
DCHECK_EQ(kNeedsNewCandidate, state_);
if (step_ == kExact)
GenerateExactSelector();
if (step_ == kRange)
ExtendRangeSelector();
if (step_ == kContext)
ExtendContext();
ResolveSelectorState();
}
void TextFragmentSelectorGenerator::ResolveSelectorState() {
switch (state_) {
case kTestCandidate:
RunTextFinder();
break;
case kNotStarted:
case kNeedsNewCandidate:
NOTREACHED();
ABSL_FALLTHROUGH_INTENDED;
case kFailure:
OnSelectorReady(
TextFragmentSelector(TextFragmentSelector::SelectorType::kInvalid));
break;
case kSuccess:
OnSelectorReady(*selector_);
break;
}
}
void TextFragmentSelectorGenerator::RunTextFinder() {
DCHECK(selector_);
iteration_++;
// |FindMatch| will call |DidFindMatch| indicating if the match was unique.
finder_ = MakeGarbageCollected<TextFragmentFinder>(
*this, *selector_, selection_frame_->GetDocument(),
TextFragmentFinder::FindBufferRunnerType::kAsynchronous);
finder_->FindMatch();
}
void TextFragmentSelectorGenerator::DidFindMatch(
const EphemeralRangeInFlatTree& match,
const TextFragmentAnchorMetrics::Match match_metrics,
bool is_unique) {
if (is_unique && PlainText(match).StripWhiteSpace().length() ==
PlainText(EphemeralRangeInFlatTree(selection_range_))
.StripWhiteSpace()
.length()) {
state_ = kSuccess;
ResolveSelectorState();
} else {
state_ = kNeedsNewCandidate;
// If already tried exact selector then should continue by adding context.
if (step_ == kExact)
step_ = kContext;
GenerateSelectorCandidate();
}
}
void TextFragmentSelectorGenerator::NoMatchFound() {
state_ = kFailure;
error_ = LinkGenerationError::kIncorrectSelector;
ResolveSelectorState();
}
void TextFragmentSelectorGenerator::OnSelectorReady(
const TextFragmentSelector& selector) {
RecordAllMetrics(selector);
if (pending_generate_selector_callback_) {
NotifyClientSelectorReady(selector);
}
}
void TextFragmentSelectorGenerator::NotifyClientSelectorReady(
const TextFragmentSelector& selector) {
DCHECK(pending_generate_selector_callback_);
if (base::FeatureList::IsEnabled(
shared_highlighting::kPreemptiveLinkToTextGeneration))
RecordPreemptiveGenerationMetrics(selector);
std::move(pending_generate_selector_callback_).Run(selector.ToString());
}
void TextFragmentSelectorGenerator::ClearSelection() {
if (selection_range_) {
selection_range_->Dispose();
selection_range_ = nullptr;
selection_frame_ = nullptr;
}
}
void TextFragmentSelectorGenerator::Trace(Visitor* visitor) const {
visitor->Trace(selection_frame_);
visitor->Trace(selection_range_);
visitor->Trace(selector_producer_);
visitor->Trace(finder_);
}
void TextFragmentSelectorGenerator::GenerateExactSelector() {
DCHECK_EQ(kExact, step_);
DCHECK_EQ(kNeedsNewCandidate, state_);
EphemeralRangeInFlatTree ephemeral_range(selection_range_);
// If not in same block, should use ranges.
if (!TextFragmentFinder::IsInSameUninterruptedBlock(
ephemeral_range.StartPosition(), ephemeral_range.EndPosition())) {
step_ = kRange;
return;
}
String selected_text = PlainText(ephemeral_range).StripWhiteSpace();
// If too long should use ranges.
if (selected_text.length() > kExactTextMaxChars) {
step_ = kRange;
return;
}
selector_ = std::make_unique<TextFragmentSelector>(
TextFragmentSelector::SelectorType::kExact, selected_text, "", "", "");
// If too short should use exact selector, but should add context.
if (selected_text.length() < kNoContextMinChars) {
step_ = kContext;
return;
}
state_ = kTestCandidate;
}
void TextFragmentSelectorGenerator::ExtendRangeSelector() {
DCHECK_EQ(kRange, step_);
DCHECK_EQ(kNeedsNewCandidate, state_);
// Give up if range is already too long.
if (num_range_words_ > kMaxRangeWords) {
step_ = kContext;
return;
}
// Initialize range start/end and word min count, if needed.
if (max_available_range_start_.IsEmpty() &&
max_available_range_end_.IsEmpty()) {
EphemeralRangeInFlatTree ephemeral_range(selection_range_);
// If selection starts and ends in the same block, then split selected text
// roughly in the middle.
if (TextFragmentFinder::IsInSameUninterruptedBlock(
ephemeral_range.StartPosition(), ephemeral_range.EndPosition())) {
String selection_text = PlainText(ephemeral_range);
selection_text.Ensure16Bit();
int selection_length = selection_text.length();
int mid_point =
FindNextWordForward(selection_text.Characters16(), selection_length,
selection_length / 2);
max_available_range_start_ = selection_text.Left(mid_point);
// If from middle till end of selection there is no word break, then we
// cannot use it for range end.
if (mid_point == selection_length) {
state_ = kFailure;
error_ = LinkGenerationError::kNoRange;
return;
}
max_available_range_end_ =
selection_text.Right(selection_text.length() - mid_point - 1);
} else {
// If not the same node, then we use first and last block of the selection
// range.
max_available_range_start_ =
GetNextTextBlock(selection_range_->StartPosition());
max_available_range_end_ =
GetPreviousTextBlock(selection_range_->EndPosition());
}
// Use at least 3 words from both sides for more robust link to text.
num_range_words_ = kMinWordCount_;
}
String start =
GetWordsFromStart(max_available_range_start_, num_range_words_);
String end = GetWordsFromEnd(max_available_range_end_, num_range_words_);
num_range_words_++;
// If the start and end didn't change, it means we exhausted the selected
// text and should try adding context.
if (selector_ && start == selector_->Start() && end == selector_->End()) {
step_ = kContext;
return;
}
selector_ = std::make_unique<TextFragmentSelector>(
TextFragmentSelector::SelectorType::kRange, start, end, "", "");
state_ = kTestCandidate;
}
void TextFragmentSelectorGenerator::ExtendContext() {
DCHECK_EQ(kContext, step_);
DCHECK_EQ(kNeedsNewCandidate, state_);
DCHECK(selector_);
// Give up if context is already too long.
if (num_context_words_ == kMaxContextWords) {
state_ = kFailure;
error_ = LinkGenerationError::kContextLimitReached;
return;
}
// Try initiating properties necessary for calculating prefix and suffix.
if (max_available_prefix_.IsEmpty() && max_available_suffix_.IsEmpty()) {
max_available_prefix_ =
GetPreviousTextBlock(selection_range_->StartPosition());
max_available_suffix_ = GetNextTextBlock(selection_range_->EndPosition());
// Use at least 3 words from both sides for more robust link to text.
num_context_words_ = kMinWordCount_;
}
if (max_available_prefix_.IsEmpty() && max_available_suffix_.IsEmpty()) {
state_ = kFailure;
error_ = LinkGenerationError::kNoContext;
return;
}
String prefix = GetWordsFromEnd(max_available_prefix_, num_context_words_);
String suffix = GetWordsFromStart(max_available_suffix_, num_context_words_);
num_context_words_++;
// Give up if we were unable to get new prefix and suffix.
if (prefix == selector_->Prefix() && suffix == selector_->Suffix()) {
state_ = kFailure;
error_ = LinkGenerationError::kContextExhausted;
return;
}
selector_ = std::make_unique<TextFragmentSelector>(
selector_->Type(), selector_->Start(), selector_->End(), prefix, suffix);
state_ = kTestCandidate;
}
String TextFragmentSelectorGenerator::GetPreviousTextBlock(
const Position& prefix_end_position) {
Node* prefix_end = prefix_end_position.ComputeContainerNode();
unsigned prefix_end_offset =
prefix_end_position.ComputeOffsetInContainerNode();
// If given position point to the first visible text in its containiner node,
// use the preceding visible node for the suffix.
if (IsFirstVisiblePosition(prefix_end, prefix_end_offset)) {
prefix_end = BackwardNonEmptyVisibleTextNode(
FlatTreeTraversal::Previous(*prefix_end));
if (!prefix_end)
return "";
prefix_end_offset = prefix_end->textContent().length();
}
// The furthest node within same block without crossing block boundaries would
// be the suffix end.
Node* prefix_start = LastVisibleTextNodeWithinBlock(prefix_end);
if (!prefix_start)
return "";
auto range_start = Position(prefix_start, 0);
auto range_end = Position(prefix_end, prefix_end_offset);
return PlainText(EphemeralRange(range_start, range_end)).StripWhiteSpace();
}
String TextFragmentSelectorGenerator::GetNextTextBlock(
const Position& suffix_start_position) {
Node* suffix_start = suffix_start_position.ComputeContainerNode();
unsigned suffix_start_offset =
suffix_start_position.ComputeOffsetInContainerNode();
// If given position point to the last visible text in its containiner node,
// use the following visible node for the suffix.
if (IsLastVisiblePosition(suffix_start, suffix_start_offset)) {
suffix_start = FirstNonEmptyVisibleTextNode(
FlatTreeTraversal::NextSkippingChildren(*suffix_start));
suffix_start_offset = 0;
}
if (!suffix_start)
return "";
// The furthest node within same block without crossing block boundaries would
// be the suffix end.
Node* suffix_end = FirstVisibleTextNodeWithinBlock(suffix_start);
if (!suffix_end)
return "";
auto range_start = Position(suffix_start, suffix_start_offset);
auto range_end = Position(suffix_end, suffix_end->textContent().length());
return PlainText(EphemeralRange(range_start, range_end)).StripWhiteSpace();
}
void TextFragmentSelectorGenerator::Reset() {
if (finder_)
finder_->Cancel();
generation_start_time_ = base::DefaultTickClock::GetInstance()->NowTicks();
state_ = kNotStarted;
error_.reset();
step_ = kExact;
max_available_prefix_ = "";
max_available_suffix_ = "";
max_available_range_start_ = "";
max_available_range_end_ = "";
num_context_words_ = 0;
num_range_words_ = 0;
iteration_ = 0;
selector_ = nullptr;
selector_requested_before_ready_.reset();
pending_generate_selector_callback_.Reset();
}
void TextFragmentSelectorGenerator::RecordAllMetrics(
const TextFragmentSelector& selector) {
UMA_HISTOGRAM_BOOLEAN(
"SharedHighlights.LinkGenerated",
selector.Type() != TextFragmentSelector::SelectorType::kInvalid);
ukm::UkmRecorder* recorder = selection_frame_->GetDocument()->UkmRecorder();
ukm::SourceId source_id = selection_frame_->GetDocument()->UkmSourceID();
if (selector.Type() != TextFragmentSelector::SelectorType::kInvalid) {
UMA_HISTOGRAM_COUNTS_1000("SharedHighlights.LinkGenerated.ParamLength",
selector.ToString().length());
UMA_HISTOGRAM_EXACT_LINEAR("SharedHighlights.LinkGenerated.Iterations",
iteration_, kMaxIterationCountToRecord);
UMA_HISTOGRAM_TIMES("SharedHighlights.LinkGenerated.TimeToGenerate",
base::DefaultTickClock::GetInstance()->NowTicks() -
generation_start_time_);
UMA_HISTOGRAM_ENUMERATION(
"SharedHighlights.LinkGenerated.SelectorParameters",
TextFragmentAnchorMetrics::GetParametersForSelector(selector));
shared_highlighting::LogLinkGeneratedSuccessUkmEvent(recorder, source_id);
} else {
UMA_HISTOGRAM_EXACT_LINEAR(
"SharedHighlights.LinkGenerated.Error.Iterations", iteration_,
kMaxIterationCountToRecord);
UMA_HISTOGRAM_TIMES("SharedHighlights.LinkGenerated.Error.TimeToGenerate",
base::DefaultTickClock::GetInstance()->NowTicks() -
generation_start_time_);
LinkGenerationError error =
error_.has_value() ? error_.value() : LinkGenerationError::kUnknown;
shared_highlighting::LogLinkGenerationErrorReason(error);
shared_highlighting::LogLinkGeneratedErrorUkmEvent(recorder, source_id,
error);
}
}
void TextFragmentSelectorGenerator::RecordPreemptiveGenerationMetrics(
const TextFragmentSelector& selector) {
DCHECK(selector_requested_before_ready_.has_value());
bool success =
selector.Type() != TextFragmentSelector::SelectorType::kInvalid;
std::string uma_prefix = "SharedHighlights.LinkGenerated";
if (selector_requested_before_ready_.value()) {
uma_prefix = base::StrCat({uma_prefix, ".RequestedBeforeReady"});
} else {
uma_prefix = base::StrCat({uma_prefix, ".RequestedAfterReady"});
}
base::UmaHistogramBoolean(uma_prefix, success);
if (!success) {
LinkGenerationError error =
error_.has_value() ? error_.value() : LinkGenerationError::kUnknown;
base::UmaHistogramEnumeration(
"SharedHighlights.LinkGenerated.Error.Requested", error);
}
}
} // namespace blink