blob: f67926bea494b31e7c658a284f037a073a33c6a0 [file] [log] [blame]
// Copyright 2018 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 "chrome/browser/navigation_predictor/navigation_predictor.h"
#include <memory>
#include "base/logging.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/optional.h"
#include "base/system/sys_info.h"
#include "chrome/browser/engagement/site_engagement_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "components/search_engines/template_url_service.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/site_instance.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
#include "url/origin.h"
struct NavigationPredictor::NavigationScore {
NavigationScore(const GURL& url,
size_t area_rank,
double score,
bool contains_image)
: url(url),
area_rank(area_rank),
score(score),
contains_image(contains_image) {}
// URL of the target link.
const GURL url;
// Rank in terms of anchor element area. It starts at 0, a lower rank implies
// a larger area.
const size_t area_rank;
// Calculated navigation score, based on |area_rank| and other metrics.
double score;
// Multiple anchor elements may point to the same |url|. |contains_image| is
// true if at least one of the anchor elements pointing to |url| contains an
// image.
const bool contains_image;
// Rank of the |score| in this document. It starts at 0, a lower rank implies
// a higher |score|.
base::Optional<size_t> score_rank;
};
NavigationPredictor::NavigationPredictor(
content::RenderFrameHost* render_frame_host)
: browser_context_(
render_frame_host->GetSiteInstance()->GetBrowserContext()),
ratio_area_scale_(base::GetFieldTrialParamByFeatureAsInt(
blink::features::kRecordAnchorMetricsVisible,
"ratio_area_scale",
100)),
is_in_iframe_scale_(base::GetFieldTrialParamByFeatureAsInt(
blink::features::kRecordAnchorMetricsVisible,
"is_in_iframe_scale",
0)),
is_same_host_scale_(base::GetFieldTrialParamByFeatureAsInt(
blink::features::kRecordAnchorMetricsVisible,
"is_same_host_scale",
100)),
contains_image_scale_(base::GetFieldTrialParamByFeatureAsInt(
blink::features::kRecordAnchorMetricsVisible,
"contains_image_scale",
50)),
is_url_incremented_scale_(base::GetFieldTrialParamByFeatureAsInt(
blink::features::kRecordAnchorMetricsVisible,
"is_url_incremented_scale",
100)),
source_engagement_score_scale_(base::GetFieldTrialParamByFeatureAsInt(
blink::features::kRecordAnchorMetricsVisible,
"source_engagement_score_scale",
100)),
target_engagement_score_scale_(base::GetFieldTrialParamByFeatureAsInt(
blink::features::kRecordAnchorMetricsVisible,
"target_engagement_score_scale",
100)),
area_rank_scale_(base::GetFieldTrialParamByFeatureAsInt(
blink::features::kRecordAnchorMetricsVisible,
"area_rank_scale",
100)),
sum_scales_(ratio_area_scale_ + is_in_iframe_scale_ +
is_same_host_scale_ + contains_image_scale_ +
is_url_incremented_scale_ + source_engagement_score_scale_ +
target_engagement_score_scale_ + area_rank_scale_),
is_low_end_device_(base::SysInfo::IsLowEndDevice()) {
DCHECK(browser_context_);
DETACH_FROM_SEQUENCE(sequence_checker_);
}
NavigationPredictor::~NavigationPredictor() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void NavigationPredictor::Create(
blink::mojom::AnchorElementMetricsHostRequest request,
content::RenderFrameHost* render_frame_host) {
// Only valid for the main frame.
if (render_frame_host->GetParent())
return;
mojo::MakeStrongBinding(
std::make_unique<NavigationPredictor>(render_frame_host),
std::move(request));
}
bool NavigationPredictor::IsValidMetricFromRenderer(
const blink::mojom::AnchorElementMetrics& metric) const {
return metric.target_url.SchemeIsHTTPOrHTTPS() &&
metric.source_url.SchemeIsHTTPOrHTTPS();
}
void NavigationPredictor::RecordTimingOnClick() {
base::TimeTicks current_timing = base::TimeTicks::Now();
// This is the first click in the document.
// Note that multiple clicks can happen on the same document. For example,
// if the click opens a new tab, then the old document is not necessarily
// destroyed. The user can return to the old document and click.
if (last_click_timing_ == base::TimeTicks()) {
// Document may have not loaded yet when click happens.
UMA_HISTOGRAM_TIMES("AnchorElementMetrics.Clicked.DurationLoadToFirstClick",
document_loaded_timing_ > base::TimeTicks()
? current_timing - document_loaded_timing_
: base::TimeDelta());
} else {
UMA_HISTOGRAM_TIMES("AnchorElementMetrics.Clicked.ClickIntervals",
current_timing - last_click_timing_);
}
last_click_timing_ = current_timing;
}
SiteEngagementService* NavigationPredictor::GetEngagementService() const {
Profile* profile = Profile::FromBrowserContext(browser_context_);
SiteEngagementService* service = SiteEngagementService::Get(profile);
DCHECK(service);
return service;
}
TemplateURLService* NavigationPredictor::GetTemplateURLService() const {
return TemplateURLServiceFactory::GetForProfile(
Profile::FromBrowserContext(browser_context_));
}
void NavigationPredictor::ReportAnchorElementMetricsOnClick(
blink::mojom::AnchorElementMetricsPtr metrics) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (browser_context_->IsOffTheRecord())
return;
if (!IsValidMetricFromRenderer(*metrics)) {
mojo::ReportBadMessage("Bad anchor element metrics: onClick.");
return;
}
source_is_default_search_engine_page_ =
GetTemplateURLService() &&
GetTemplateURLService()->IsSearchResultsPageFromDefaultSearchProvider(
metrics->source_url);
if (!metrics->source_url.SchemeIsCryptographic() ||
!metrics->target_url.SchemeIsCryptographic()) {
return;
}
RecordTimingOnClick();
SiteEngagementService* engagement_service = GetEngagementService();
UMA_HISTOGRAM_COUNTS_100(
"AnchorElementMetrics.Clicked.DocumentEngagementScore",
static_cast<int>(engagement_service->GetScore(metrics->source_url)));
double target_score = engagement_service->GetScore(metrics->target_url);
UMA_HISTOGRAM_COUNTS_100("AnchorElementMetrics.Clicked.HrefEngagementScore2",
static_cast<int>(target_score));
if (target_score > 0) {
UMA_HISTOGRAM_COUNTS_100(
"AnchorElementMetrics.Clicked.HrefEngagementScorePositive",
static_cast<int>(target_score));
}
if (!metrics->is_same_host) {
UMA_HISTOGRAM_COUNTS_100(
"AnchorElementMetrics.Clicked.HrefEngagementScoreExternal",
static_cast<int>(target_score));
}
// Look up the clicked URL in |navigation_scores_map_|. Record if we find it.
auto iter = navigation_scores_map_.find(metrics->target_url.spec());
if (iter == navigation_scores_map_.end())
return;
UMA_HISTOGRAM_COUNTS_100("AnchorElementMetrics.Clicked.AreaRank",
static_cast<int>(iter->second->area_rank));
UMA_HISTOGRAM_COUNTS_100("AnchorElementMetrics.Clicked.NavigationScore",
static_cast<int>(iter->second->score));
UMA_HISTOGRAM_COUNTS_100("AnchorElementMetrics.Clicked.NavigationScoreRank",
static_cast<int>(iter->second->score_rank.value()));
// Guaranteed to be non-zero since we have found the clicked link in
// |navigation_scores_map_|.
int number_of_anchors = static_cast<int>(navigation_scores_map_.size());
if (metrics->is_same_host) {
UMA_HISTOGRAM_PERCENTAGE(
"AnchorElementMetrics.Clicked.RatioSameHost_SameHost",
(number_of_anchors_same_host_ * 100) / number_of_anchors);
} else {
UMA_HISTOGRAM_PERCENTAGE(
"AnchorElementMetrics.Clicked.RatioSameHost_DiffHost",
(number_of_anchors_same_host_ * 100) / number_of_anchors);
}
if (source_is_default_search_engine_page_) {
UMA_HISTOGRAM_BOOLEAN("AnchorElementMetrics.Clicked.OnDSE.SameHost",
metrics->is_same_host);
} else {
UMA_HISTOGRAM_BOOLEAN("AnchorElementMetrics.Clicked.OnNonDSE.SameHost",
metrics->is_same_host);
}
// Check if the clicked anchor element contains image or if any other anchor
// element pointing to the same url contains an image.
if (metrics->contains_image || iter->second->contains_image) {
UMA_HISTOGRAM_PERCENTAGE(
"AnchorElementMetrics.Clicked.RatioContainsImage_ContainsImage",
(number_of_anchors_contains_image_ * 100) / number_of_anchors);
} else {
UMA_HISTOGRAM_PERCENTAGE(
"AnchorElementMetrics.Clicked.RatioContainsImage_NoImage",
(number_of_anchors_contains_image_ * 100) / number_of_anchors);
}
if (metrics->is_in_iframe) {
UMA_HISTOGRAM_PERCENTAGE(
"AnchorElementMetrics.Clicked.RatioInIframe_InIframe",
(number_of_anchors_in_iframe_ * 100) / number_of_anchors);
} else {
UMA_HISTOGRAM_PERCENTAGE(
"AnchorElementMetrics.Clicked.RatioInIframe_NotInIframe",
(number_of_anchors_in_iframe_ * 100) / number_of_anchors);
}
if (metrics->is_url_incremented_by_one) {
UMA_HISTOGRAM_PERCENTAGE(
"AnchorElementMetrics.Clicked.RatioUrlIncremented_UrlIncremented",
(number_of_anchors_url_incremented_ * 100) / number_of_anchors);
} else {
UMA_HISTOGRAM_PERCENTAGE(
"AnchorElementMetrics.Clicked.RatioUrlIncremented_NotIncremented",
(number_of_anchors_url_incremented_ * 100) / number_of_anchors);
}
}
void NavigationPredictor::MergeMetricsSameTargetUrl(
std::vector<blink::mojom::AnchorElementMetricsPtr>* metrics) const {
UMA_HISTOGRAM_COUNTS_100(
"AnchorElementMetrics.Visible.NumberOfAnchorElements", metrics->size());
// Maps from target url (href) to anchor element metrics from renderer.
std::unordered_map<std::string, blink::mojom::AnchorElementMetricsPtr>
metrics_map;
// This size reserve is aggressive since |metrics_map| may contain fewer
// elements than metrics->size() after merge.
metrics_map.reserve(metrics->size());
for (auto& metric : *metrics) {
// Do not include anchor elements that point to the same URL as the URL of
// the current navigation since these are unlikely to be clicked.
if (metric->target_url == metric->source_url)
continue;
if (!metric->target_url.SchemeIsCryptographic())
continue;
// Currently, all predictions are made based on elements that are within the
// main frame since it is unclear if we can pre* the target of the elements
// within iframes.
if (metric->is_in_iframe)
continue;
const std::string& key = metric->target_url.spec();
auto iter = metrics_map.find(key);
if (iter == metrics_map.end()) {
metrics_map[key] = std::move(metric);
} else {
auto& prev_metric = iter->second;
prev_metric->ratio_area += metric->ratio_area;
prev_metric->ratio_visible_area += metric->ratio_visible_area;
// After merging, value of |ratio_area| can go beyond 1.0. This can
// happen, e.g., when there are 2 anchor elements pointing to the same
// target. The first anchor element occupies 90% of the viewport. The
// second one has size 0.8 times the viewport, and only part of it is
// visible in the viewport. In that case, |ratio_area| may be 1.7.
if (prev_metric->ratio_area > 1.0)
prev_metric->ratio_area = 1.0;
DCHECK_LE(0.0, prev_metric->ratio_area);
DCHECK_GE(1.0, prev_metric->ratio_area);
DCHECK_GE(1.0, prev_metric->ratio_visible_area);
// Position related metrics are tricky to merge. Another possible way to
// merge is simply add up the calculated navigation scores.
prev_metric->ratio_distance_root_top =
std::min(prev_metric->ratio_distance_root_top,
metric->ratio_distance_root_top);
prev_metric->ratio_distance_root_bottom =
std::max(prev_metric->ratio_distance_root_bottom,
metric->ratio_distance_root_bottom);
prev_metric->ratio_distance_top_to_visible_top =
std::min(prev_metric->ratio_distance_top_to_visible_top,
metric->ratio_distance_top_to_visible_top);
prev_metric->ratio_distance_center_to_visible_top =
std::min(prev_metric->ratio_distance_center_to_visible_top,
metric->ratio_distance_center_to_visible_top);
// Anchor element is not considered in an iframe as long as at least one
// of them is not in an iframe.
prev_metric->is_in_iframe =
prev_metric->is_in_iframe && metric->is_in_iframe;
prev_metric->contains_image =
prev_metric->contains_image || metric->contains_image;
DCHECK_EQ(prev_metric->is_same_host, metric->is_same_host);
}
}
metrics->clear();
if (metrics_map.empty())
return;
metrics->reserve(metrics_map.size());
for (auto& metric_mapping : metrics_map) {
metrics->push_back(std::move(metric_mapping.second));
}
DCHECK(!metrics->empty());
UMA_HISTOGRAM_COUNTS_100(
"AnchorElementMetrics.Visible.NumberOfAnchorElementsAfterMerge",
metrics->size());
}
void NavigationPredictor::ReportAnchorElementMetricsOnLoad(
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Each document should only report metrics once when page is loaded.
DCHECK(navigation_scores_map_.empty());
if (browser_context_->IsOffTheRecord())
return;
if (metrics.empty()) {
mojo::ReportBadMessage("Bad anchor element metrics: empty.");
return;
}
for (const auto& metric : metrics) {
if (!IsValidMetricFromRenderer(*metric)) {
mojo::ReportBadMessage("Bad anchor element metrics: onLoad.");
return;
}
}
if (!metrics[0]->source_url.SchemeIsCryptographic())
return;
document_loaded_timing_ = base::TimeTicks::Now();
source_is_default_search_engine_page_ =
GetTemplateURLService() &&
GetTemplateURLService()->IsSearchResultsPageFromDefaultSearchProvider(
metrics[0]->source_url);
MergeMetricsSameTargetUrl(&metrics);
if (metrics.empty())
return;
// Count the number of anchors that have specific metrics.
for (const auto& metric : metrics) {
number_of_anchors_same_host_ += static_cast<int>(metric->is_same_host);
number_of_anchors_contains_image_ +=
static_cast<int>(metric->contains_image);
number_of_anchors_in_iframe_ += static_cast<int>(metric->is_in_iframe);
number_of_anchors_url_incremented_ +=
static_cast<int>(metric->is_url_incremented_by_one);
}
// Retrieve site engagement score of the document. |metrics| is guaranteed to
// be non-empty. All |metrics| have the same source_url.
SiteEngagementService* engagement_service = GetEngagementService();
double document_engagement_score =
engagement_service->GetScore(metrics[0]->source_url);
DCHECK(document_engagement_score >= 0 &&
document_engagement_score <= engagement_service->GetMaxPoints());
UMA_HISTOGRAM_COUNTS_100(
"AnchorElementMetrics.Visible.DocumentEngagementScore",
static_cast<int>(document_engagement_score));
// Sort metric by area in descending order to get area rank, which is a
// derived feature to calculate navigation score.
std::sort(metrics.begin(), metrics.end(), [](const auto& a, const auto& b) {
return a->ratio_area > b->ratio_area;
});
// Loop |metrics| to compute navigation scores.
std::vector<std::unique_ptr<NavigationScore>> navigation_scores;
navigation_scores.reserve(metrics.size());
double total_score = 0.0;
for (size_t i = 0; i != metrics.size(); ++i) {
const auto& metric = metrics[i];
RecordMetricsOnLoad(*metric);
const double target_engagement_score =
engagement_service->GetScore(metric->target_url);
DCHECK(target_engagement_score >= 0 &&
target_engagement_score <= engagement_service->GetMaxPoints());
UMA_HISTOGRAM_COUNTS_100(
"AnchorElementMetrics.Visible.HrefEngagementScore2",
static_cast<int>(target_engagement_score));
if (!metric->is_same_host) {
UMA_HISTOGRAM_COUNTS_100(
"AnchorElementMetrics.Visible.HrefEngagementScoreExternal",
static_cast<int>(target_engagement_score));
}
// Anchor elements with the same area are assigned with the same rank.
size_t area_rank = i;
if (i > 0 && metric->ratio_area == metrics[i - 1]->ratio_area)
area_rank = navigation_scores[navigation_scores.size() - 1]->area_rank;
double score = CalculateAnchorNavigationScore(
*metric, document_engagement_score, target_engagement_score, area_rank,
metrics.size());
total_score += score;
navigation_scores.push_back(std::make_unique<NavigationScore>(
metric->target_url, area_rank, score, metric->contains_image));
}
// Normalize |score| to a total sum of 100.0 across all anchor elements
// received.
if (total_score > 0.0) {
for (auto& navigation_score : navigation_scores) {
navigation_score->score = navigation_score->score / total_score * 100.0;
}
}
// Sort scores by the calculated navigation score in descending order. This
// score rank is used by MaybeTakeActionOnLoad, and stored in
// |navigation_scores_map_|.
std::sort(navigation_scores.begin(), navigation_scores.end(),
[](const auto& a, const auto& b) { return a->score > b->score; });
const url::Origin document_origin =
url::Origin::Create(metrics[0]->source_url);
MaybeTakeActionOnLoad(document_origin, navigation_scores);
// Store navigation scores in |navigation_scores_map_| for fast look up upon
// clicks.
navigation_scores_map_.reserve(navigation_scores.size());
for (size_t i = 0; i != navigation_scores.size(); ++i) {
navigation_scores[i]->score_rank = base::make_optional(i);
navigation_scores_map_[navigation_scores[i]->url.spec()] =
std::move(navigation_scores[i]);
}
}
double NavigationPredictor::CalculateAnchorNavigationScore(
const blink::mojom::AnchorElementMetrics& metrics,
double document_engagement_score,
double target_engagement_score,
int area_rank,
int number_of_anchors) const {
DCHECK(!browser_context_->IsOffTheRecord());
if (sum_scales_ == 0)
return 0.0;
double max_engagement_points = GetEngagementService()->GetMaxPoints();
document_engagement_score /= max_engagement_points;
target_engagement_score /= max_engagement_points;
double area_rank_score =
(double)((number_of_anchors - area_rank)) / number_of_anchors;
DCHECK_LE(0, metrics.ratio_visible_area);
DCHECK_GE(1, metrics.ratio_visible_area);
DCHECK_LE(0, metrics.is_in_iframe);
DCHECK_GE(1, metrics.is_in_iframe);
DCHECK_LE(0, metrics.is_same_host);
DCHECK_GE(1, metrics.is_same_host);
DCHECK_LE(0, metrics.contains_image);
DCHECK_GE(1, metrics.contains_image);
DCHECK_LE(0, metrics.is_url_incremented_by_one);
DCHECK_GE(1, metrics.is_url_incremented_by_one);
DCHECK_LE(0, document_engagement_score);
DCHECK_GE(1, document_engagement_score);
DCHECK_LE(0, target_engagement_score);
DCHECK_GE(1, target_engagement_score);
DCHECK_LE(0, area_rank_score);
DCHECK_GE(1, area_rank_score);
double host_score = 0.0;
// On pages from default search engine, give higher weight to target URLs that
// link to a different host. On non-default search engine pages, give higher
// weight to target URLs that link to the same host.
if (!source_is_default_search_engine_page_ && metrics.is_same_host) {
host_score = is_same_host_scale_;
} else if (source_is_default_search_engine_page_ && !metrics.is_same_host) {
host_score = is_same_host_scale_;
}
// TODO(chelu): https://crbug.com/850624/. Experiment with other heuristic
// algorithms for computing the anchor elements score.
double score = ratio_area_scale_ * metrics.ratio_visible_area +
is_in_iframe_scale_ * metrics.is_in_iframe +
contains_image_scale_ * metrics.contains_image + host_score +
is_url_incremented_scale_ * metrics.is_url_incremented_by_one +
source_engagement_score_scale_ * document_engagement_score +
target_engagement_score_scale_ * target_engagement_score +
area_rank_scale_ * (area_rank_score);
// Normalize to 100.
score = score / sum_scales_ * 100.0;
DCHECK_LE(0.0, score);
DCHECK_GE(100.0, score);
return score;
}
void NavigationPredictor::MaybeTakeActionOnLoad(
const url::Origin& document_origin,
const std::vector<std::unique_ptr<NavigationScore>>&
sorted_navigation_scores) {
DCHECK(!browser_context_->IsOffTheRecord());
// |sorted_navigation_scores| are sorted in descending order, the first one
// has the highest navigation score.
UMA_HISTOGRAM_COUNTS_100(
"AnchorElementMetrics.Visible.HighestNavigationScore",
static_cast<int>(sorted_navigation_scores[0]->score));
std::string action_histogram_name =
source_is_default_search_engine_page_
? "NavigationPredictor.OnDSE.ActionTaken"
: "NavigationPredictor.OnNonDSE.ActionTaken";
DCHECK(!prefetch_url_.has_value());
prefetch_url_ = GetUrlToPrefetch(document_origin, sorted_navigation_scores);
if (prefetch_url_.has_value()) {
DCHECK_EQ(document_origin.host(), prefetch_url_->host());
base::UmaHistogramEnumeration(action_histogram_name, Action::kPrefetch);
return;
}
base::UmaHistogramEnumeration(action_histogram_name, Action::kNone);
}
base::Optional<GURL> NavigationPredictor::GetUrlToPrefetch(
const url::Origin& document_origin,
const std::vector<std::unique_ptr<NavigationScore>>&
sorted_navigation_scores) const {
// Currently, prefetch is disabled on low-end devices since prefetch may
// increase memory usage.
if (is_low_end_device_)
return base::nullopt;
// On search engine results page, next navigation is likely to be a different
// origin. Currently, the prefetch is only allowed for same orgins. Hence,
// prefetch is currently disabled on search engine results page.
if (source_is_default_search_engine_page_)
return base::nullopt;
// Return the same-origin URL that has the highest navigation score.
for (const auto& navigation_score : sorted_navigation_scores) {
// Currently, only same origin URLs can be prefetched.
if (url::Origin::Create(navigation_score->url) != document_origin)
continue;
return navigation_score->url;
}
return base::nullopt;
}
void NavigationPredictor::RecordMetricsOnLoad(
const blink::mojom::AnchorElementMetrics& metric) const {
DCHECK(!browser_context_->IsOffTheRecord());
UMA_HISTOGRAM_PERCENTAGE("AnchorElementMetrics.Visible.RatioArea",
static_cast<int>(metric.ratio_area * 100));
UMA_HISTOGRAM_PERCENTAGE("AnchorElementMetrics.Visible.RatioVisibleArea",
static_cast<int>(metric.ratio_visible_area * 100));
UMA_HISTOGRAM_PERCENTAGE(
"AnchorElementMetrics.Visible.RatioDistanceTopToVisibleTop",
static_cast<int>(
std::min(metric.ratio_distance_top_to_visible_top, 1.0f) * 100));
UMA_HISTOGRAM_PERCENTAGE(
"AnchorElementMetrics.Visible.RatioDistanceCenterToVisibleTop",
static_cast<int>(
std::min(metric.ratio_distance_center_to_visible_top, 1.0f) * 100));
UMA_HISTOGRAM_COUNTS_10000(
"AnchorElementMetrics.Visible.RatioDistanceRootTop",
static_cast<int>(std::min(metric.ratio_distance_root_top, 100.0f) * 100));
UMA_HISTOGRAM_COUNTS_10000(
"AnchorElementMetrics.Visible.RatioDistanceRootBottom",
static_cast<int>(std::min(metric.ratio_distance_root_bottom, 100.0f) *
100));
UMA_HISTOGRAM_BOOLEAN("AnchorElementMetrics.Visible.IsInIFrame",
metric.is_in_iframe);
UMA_HISTOGRAM_BOOLEAN("AnchorElementMetrics.Visible.ContainsImage",
metric.contains_image);
UMA_HISTOGRAM_BOOLEAN("AnchorElementMetrics.Visible.IsSameHost",
metric.is_same_host);
UMA_HISTOGRAM_BOOLEAN("AnchorElementMetrics.Visible.IsUrlIncrementedByOne",
metric.is_url_incremented_by_one);
}