Commit 8ae6a515 authored by Anne Lim's avatar Anne Lim Committed by Commit Bot

[Autofill] New StrikeDatabase class/interface for v2

New StrikeDatabase class for the StrikeDatabase v2. It shares the
same existing ProtoDatabase as legacy_strike_database.h/cc which
is used as persistent storage, as well as an in-memory cache.
Virtual functions are also introduced, which are to be implemented
per project.

Bug: 884817
Change-Id: I48c38b17274ec440afd593dc48223b97c2b2d90e
Reviewed-on: https://chromium-review.googlesource.com/c/1334832Reviewed-by: default avatarSebastien Seguin-Gagnon <sebsg@chromium.org>
Reviewed-by: default avatarIlya Sherman <isherman@chromium.org>
Reviewed-by: default avatarJared Saul <jsaul@google.com>
Commit-Queue: Anne Lim <annelim@google.com>
Cr-Commit-Position: refs/heads/master@{#609887}
parent db286946
......@@ -165,6 +165,8 @@ jumbo_static_library("browser") {
"search_field.h",
"state_names.cc",
"state_names.h",
"strike_database.cc",
"strike_database.h",
"subkey_requester.cc",
"subkey_requester.h",
"suggestion.cc",
......@@ -359,6 +361,8 @@ jumbo_static_library("test_support") {
"test_personal_data_manager.h",
"test_region_data_loader.cc",
"test_region_data_loader.h",
"test_strike_database.cc",
"test_strike_database.h",
"webdata/autofill_sync_bridge_test_util.cc",
"webdata/autofill_sync_bridge_test_util.h",
"webdata/mock_autofill_webdata_backend.cc",
......@@ -503,6 +507,7 @@ source_set("unit_tests") {
"rationalization_util_unittest.cc",
"region_combobox_model_unittest.cc",
"search_field_unittest.cc",
"strike_database_unittest.cc",
"subkey_requester_unittest.cc",
"ui/card_unmask_prompt_controller_impl_unittest.cc",
"validation_unittest.cc",
......
// 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 "components/autofill/core/browser/strike_database.h"
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/post_task.h"
#include "base/time/time.h"
#include "components/autofill/core/browser/proto/strike_data.pb.h"
#include "components/leveldb_proto/proto_database_impl.h"
namespace autofill {
namespace {
const char kDatabaseClientName[] = "StrikeService";
const char kKeyDeliminator[] = "__";
const int kMaxInitAttempts = 3;
} // namespace
StrikeDatabase::StrikeDatabase(const base::FilePath& database_dir)
: db_(std::make_unique<leveldb_proto::ProtoDatabaseImpl<StrikeData>>(
base::CreateSequencedTaskRunnerWithTraits(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}))),
database_dir_(database_dir),
weak_ptr_factory_(this) {
db_->Init(kDatabaseClientName, database_dir,
leveldb_proto::CreateSimpleOptions(),
base::BindRepeating(&StrikeDatabase::OnDatabaseInit,
weak_ptr_factory_.GetWeakPtr()));
}
StrikeDatabase::~StrikeDatabase() {}
int StrikeDatabase::AddStrike(const std::string id) {
std::string key = GetKey(id);
int num_strikes = strike_map_cache_.count(key) // Cache has entry for |key|.
? strike_map_cache_[key].num_strikes() + 1
: 1;
StrikeData data;
data.set_num_strikes(num_strikes);
data.set_last_update_timestamp(
base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds());
UpdateCache(key, data);
SetProtoStrikeData(key, data, base::DoNothing());
return num_strikes;
}
int StrikeDatabase::GetStrikes(const std::string id) {
std::string key = GetKey(id);
return strike_map_cache_.count(key) // Cache contains entry for |key|.
? strike_map_cache_[key].num_strikes()
: 0;
}
void StrikeDatabase::ClearStrikes(const std::string id) {
std::string key = GetKey(id);
strike_map_cache_.erase(key);
ClearAllProtoStrikesForKey(key, base::DoNothing());
}
StrikeDatabase::StrikeDatabase()
: db_(nullptr),
database_dir_(base::FilePath(nullptr)),
weak_ptr_factory_(this) {}
void StrikeDatabase::OnDatabaseInit(bool success) {
database_initialized_ = success;
if (!success) {
base::UmaHistogramCounts100(
"Autofill.StrikeDatabase.StrikeDatabaseInitFailed", num_init_attempts_);
if (num_init_attempts_ < kMaxInitAttempts) {
num_init_attempts_++;
db_->Init(kDatabaseClientName, database_dir_,
leveldb_proto::CreateSimpleOptions(),
base::BindRepeating(&StrikeDatabase::OnDatabaseInit,
weak_ptr_factory_.GetWeakPtr()));
}
return;
}
db_->LoadKeysAndEntries(
base::BindRepeating(&StrikeDatabase::OnDatabaseLoadKeysAndEntries,
weak_ptr_factory_.GetWeakPtr()));
}
void StrikeDatabase::OnDatabaseLoadKeysAndEntries(
bool success,
std::unique_ptr<std::map<std::string, StrikeData>> entries) {
if (!success) {
database_initialized_ = false;
return;
}
strike_map_cache_.insert(entries->begin(), entries->end());
}
std::string StrikeDatabase::GetKey(const std::string id) {
return GetProjectPrefix() + kKeyDeliminator + id;
}
void StrikeDatabase::GetProtoStrikes(const std::string key,
const StrikesCallback& outer_callback) {
if (!database_initialized_) {
outer_callback.Run(false);
return;
}
GetProtoStrikeData(
key,
base::BindRepeating(&StrikeDatabase::OnGetProtoStrikes,
base::Unretained(this), std::move(outer_callback)));
}
void StrikeDatabase::ClearAllProtoStrikes(
const ClearStrikesCallback& outer_callback) {
if (!database_initialized_) {
outer_callback.Run(false);
return;
}
// For deleting all, filter method always returns true.
db_->UpdateEntriesWithRemoveFilter(
std::make_unique<StrikeDataProto::KeyEntryVector>(),
base::BindRepeating([](const std::string& key) { return true; }),
outer_callback);
}
void StrikeDatabase::ClearAllProtoStrikesForKey(
const std::string& key,
const ClearStrikesCallback& outer_callback) {
if (!database_initialized_) {
outer_callback.Run(false);
return;
}
std::unique_ptr<std::vector<std::string>> keys_to_remove(
new std::vector<std::string>());
keys_to_remove->push_back(key);
db_->UpdateEntries(
/*entries_to_save=*/std::make_unique<
leveldb_proto::ProtoDatabase<StrikeData>::KeyEntryVector>(),
/*keys_to_remove=*/std::move(keys_to_remove), outer_callback);
}
void StrikeDatabase::GetProtoStrikeData(const std::string key,
const GetValueCallback& callback) {
if (!database_initialized_) {
callback.Run(false, nullptr);
return;
}
db_->GetEntry(key, callback);
}
void StrikeDatabase::SetProtoStrikeData(const std::string& key,
const StrikeData& data,
const SetValueCallback& callback) {
if (!database_initialized_) {
callback.Run(false);
return;
}
std::unique_ptr<StrikeDataProto::KeyEntryVector> entries(
new StrikeDataProto::KeyEntryVector());
entries->push_back(std::make_pair(key, data));
db_->UpdateEntries(
/*entries_to_save=*/std::move(entries),
/*keys_to_remove=*/std::make_unique<std::vector<std::string>>(),
callback);
}
void StrikeDatabase::OnGetProtoStrikes(
StrikesCallback callback,
bool success,
std::unique_ptr<StrikeData> strike_data) {
if (success && strike_data)
callback.Run(strike_data->num_strikes());
else
callback.Run(0);
}
void StrikeDatabase::LoadKeys(const LoadKeysCallback& callback) {
db_->LoadKeys(callback);
}
void StrikeDatabase::UpdateCache(const std::string& key,
const StrikeData& data) {
strike_map_cache_[key] = data;
}
} // namespace autofill
// 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.
#ifndef COMPONENTS_AUTOFILL_CORE_BROWSER_STRIKE_DATABASE_H_
#define COMPONENTS_AUTOFILL_CORE_BROWSER_STRIKE_DATABASE_H_
#include <map>
#include <memory>
#include <string>
#include <vector>
#include "base/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/leveldb_proto/proto_database.h"
namespace autofill {
class StrikeData;
// Manages data on whether different Autofill opportunities should be offered to
// the user. Projects can earn strikes in a number of ways; for instance, if a
// user ignores or declines a prompt, or if a user accepts a prompt but the task
// fails.
class StrikeDatabase : public KeyedService {
public:
using ClearStrikesCallback = base::RepeatingCallback<void(bool success)>;
using GetValueCallback =
base::RepeatingCallback<void(bool success,
std::unique_ptr<StrikeData> data)>;
using LoadKeysCallback =
base::RepeatingCallback<void(bool success,
std::unique_ptr<std::vector<std::string>>)>;
using SetValueCallback = base::RepeatingCallback<void(bool success)>;
using StrikesCallback = base::RepeatingCallback<void(int num_strikes)>;
using StrikeDataProto = leveldb_proto::ProtoDatabase<StrikeData>;
explicit StrikeDatabase(const base::FilePath& database_dir);
~StrikeDatabase() override;
bool IsMaxStrikesLimitReached();
// Increments in-memory cache and updates underlying ProtoDatabase.
int AddStrike(const std::string id);
// Returns strike count from in-memory cache.
int GetStrikes(const std::string id);
// Removes all database entries from in-memory cache and underlying
// ProtoDatabase.
void ClearStrikes(const std::string id);
protected:
// Constructor for testing that does not initialize a ProtoDatabase.
StrikeDatabase();
// The persistent ProtoDatabase for storing strike information.
std::unique_ptr<leveldb_proto::ProtoDatabase<StrikeData>> db_;
// Cached StrikeDatabase entries.
std::map<std::string, StrikeData> strike_map_cache_;
// Directory where the ProtoDatabase is intialized at.
const base::FilePath database_dir_;
// Whether or not the ProtoDatabase database has been initialized and entries
// have been loaded.
bool database_initialized_ = false;
// Number of attempts at initializing the ProtoDatabase.
int num_init_attempts_ = 0;
private:
FRIEND_TEST_ALL_PREFIXES(ChromeBrowsingDataRemoverDelegateTest,
StrikeDatabaseEmptyOnAutofillRemoveEverything);
friend class StrikeDatabaseTest;
friend class StrikeDatabaseTester;
void OnDatabaseInit(bool success);
void OnDatabaseLoadKeysAndEntries(
bool success,
std::unique_ptr<std::map<std::string, StrikeData>> entries);
// Returns a prefix unique to each project, which will be used to create
// database key.
virtual std::string GetProjectPrefix() = 0;
// Returns the maximum number of strikes after which the project's Autofill
// opportunity stops being offered.
virtual int GetMaxStrikesLimit() = 0;
// Generates key based on project-specific string identifier.
std::string GetKey(const std::string id);
// Passes the number of strikes for |key| to |outer_callback|. In the case
// that the database fails to retrieve the strike update or if no entry is
// found for |key|, 0 is passed.
virtual void GetProtoStrikes(const std::string key,
const StrikesCallback& outer_callback);
// Removes all database entries, which ensures there will be no saved strikes
// the next time the cache is recreated from the underlying ProtoDatabase.
virtual void ClearAllProtoStrikes(const ClearStrikesCallback& outer_callback);
// Removes database entry for |key|, which ensures there will be no saved
// strikes the next time the cache is recreated from the underlying
// ProtoDatabase.
virtual void ClearAllProtoStrikesForKey(
const std::string& key,
const ClearStrikesCallback& outer_callback);
// Passes success status and StrikeData entry for |key| to |inner_callback|.
void GetProtoStrikeData(const std::string key,
const GetValueCallback& inner_callback);
// Sets the entry for |key| to |strike_data|. Success status is passed to the
// callback.
void SetProtoStrikeData(const std::string& key,
const StrikeData& strike_data,
const SetValueCallback& inner_callback);
// Passes number of strikes to |outer_callback|.
void OnGetProtoStrikes(StrikesCallback outer_callback,
bool success,
std::unique_ptr<StrikeData> strike_data);
// Exposed for testing purposes.
void LoadKeys(const LoadKeysCallback& callback);
// Sets the entry for |key| in |strike_map_cache_| to |data|.
void UpdateCache(const std::string& key, const StrikeData& data);
base::WeakPtrFactory<StrikeDatabase> weak_ptr_factory_;
};
} // namespace autofill
#endif // COMPONENTS_AUTOFILL_CORE_BROWSER_STRIKE_DATABASE_H_
// 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 "components/autofill/core/browser/strike_database.h"
#include <utility>
#include <vector>
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_task_environment.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/autofill/core/browser/proto/strike_data.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace autofill {
namespace {
// Note: This class is NOT the same as test_strike_database.h. This is an
// actual implementation of StrikeDatabase, but with helper functions
// added for easier test setup. If you want a TestStrikeDatabase, please use the
// one in test_strike_database.h. This one is purely for this unit test class.
class TestStrikeDatabase : public StrikeDatabase {
public:
TestStrikeDatabase(const base::FilePath& database_dir)
: StrikeDatabase(database_dir) {
database_initialized_ = true;
}
void AddProtoEntries(
std::vector<std::pair<std::string, StrikeData>> entries_to_add,
const SetValueCallback& callback) {
std::unique_ptr<leveldb_proto::ProtoDatabase<StrikeData>::KeyEntryVector>
entries(new leveldb_proto::ProtoDatabase<StrikeData>::KeyEntryVector());
for (std::pair<std::string, StrikeData> entry : entries_to_add) {
entries->push_back(entry);
}
db_->UpdateEntries(
/*entries_to_save=*/std::move(entries),
/*keys_to_remove=*/std::make_unique<std::vector<std::string>>(),
callback);
}
private:
DISALLOW_COPY_AND_ASSIGN(TestStrikeDatabase);
// Do not use. This virtual function needed to be implemented but
// TestStrikeDatabase is not a project class.
std::string GetProjectPrefix() override {
NOTIMPLEMENTED();
return " ";
}
// Do not use. This virtual function needed to be implemented but
// TestStrikeDatabase is not a project class.
int GetMaxStrikesLimit() override {
NOTIMPLEMENTED();
return 0;
}
};
} // anonymous namespace
// Runs tests against the actual StrikeDatabase class, complete with
// ProtoDatabase.
class StrikeDatabaseTest : public ::testing::Test {
public:
StrikeDatabaseTest() : strike_database_(InitFilePath()) {}
void AddProtoEntries(
std::vector<std::pair<std::string, StrikeData>> entries_to_add) {
base::RunLoop run_loop;
strike_database_.AddProtoEntries(
entries_to_add,
base::BindRepeating(&StrikeDatabaseTest::OnAddProtoEntries,
base::Unretained(this), run_loop.QuitClosure()));
run_loop.Run();
}
void OnAddProtoEntries(base::RepeatingClosure run_loop_closure,
bool success) {
run_loop_closure.Run();
}
void OnGetProtoStrikes(base::RepeatingClosure run_loop_closure,
int num_strikes) {
num_strikes_ = num_strikes;
run_loop_closure.Run();
}
int GetProtoStrikes(std::string key) {
base::RunLoop run_loop;
strike_database_.GetProtoStrikes(
key,
base::BindRepeating(&StrikeDatabaseTest::OnGetProtoStrikes,
base::Unretained(this), run_loop.QuitClosure()));
run_loop.Run();
return num_strikes_;
}
void OnClearAllProtoStrikesForKey(base::RepeatingClosure run_loop_closure,
bool success) {
run_loop_closure.Run();
}
void ClearAllProtoStrikesForKey(const std::string key) {
base::RunLoop run_loop;
strike_database_.ClearAllProtoStrikesForKey(
key,
base::BindRepeating(&StrikeDatabaseTest::OnClearAllProtoStrikesForKey,
base::Unretained(this), run_loop.QuitClosure()));
run_loop.Run();
}
void OnClearAllProtoStrikes(base::RepeatingClosure run_loop_closure,
bool success) {
run_loop_closure.Run();
}
void ClearAllProtoStrikes() {
base::RunLoop run_loop;
strike_database_.ClearAllProtoStrikes(
base::BindRepeating(&StrikeDatabaseTest::OnClearAllProtoStrikesForKey,
base::Unretained(this), run_loop.QuitClosure()));
run_loop.Run();
}
protected:
base::HistogramTester* GetHistogramTester() { return &histogram_tester_; }
base::test::ScopedTaskEnvironment scoped_task_environment_;
TestStrikeDatabase strike_database_;
private:
static const base::FilePath InitFilePath() {
base::ScopedTempDir temp_dir_;
EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
const base::FilePath file_path =
temp_dir_.GetPath().AppendASCII("StrikeDatabaseTest");
return file_path;
}
base::HistogramTester histogram_tester_;
int num_strikes_;
std::unique_ptr<StrikeData> strike_data_;
};
TEST_F(StrikeDatabaseTest, GetStrikesForMissingKeyTest) {
const std::string key = "12345";
int strikes = GetProtoStrikes(key);
EXPECT_EQ(0, strikes);
}
TEST_F(StrikeDatabaseTest, GetStrikeForNonZeroStrikesTest) {
// Set up database with 3 pre-existing strikes at |key|.
const std::string key = "12345";
std::vector<std::pair<std::string, StrikeData>> entries;
StrikeData data;
data.set_num_strikes(3);
entries.push_back(std::make_pair(key, data));
AddProtoEntries(entries);
int strikes = GetProtoStrikes(key);
EXPECT_EQ(3, strikes);
}
TEST_F(StrikeDatabaseTest, ClearStrikesForMissingKeyTest) {
const std::string key = "12345";
ClearAllProtoStrikesForKey(key);
int strikes = GetProtoStrikes(key);
EXPECT_EQ(0, strikes);
}
TEST_F(StrikeDatabaseTest, ClearStrikesForNonZeroStrikesTest) {
// Set up database with 3 pre-existing strikes at |key|.
const std::string key = "12345";
std::vector<std::pair<std::string, StrikeData>> entries;
StrikeData data;
data.set_num_strikes(3);
entries.push_back(std::make_pair(key, data));
AddProtoEntries(entries);
int strikes = GetProtoStrikes(key);
EXPECT_EQ(3, strikes);
ClearAllProtoStrikesForKey(key);
strikes = GetProtoStrikes(key);
EXPECT_EQ(0, strikes);
}
TEST_F(StrikeDatabaseTest, ClearStrikesForMultipleNonZeroStrikesEntriesTest) {
// Set up database with 3 pre-existing strikes at |key1|, and 5 pre-existing
// strikes at |key2|.
const std::string key1 = "12345";
const std::string key2 = "13579";
std::vector<std::pair<std::string, StrikeData>> entries;
StrikeData data1;
data1.set_num_strikes(3);
entries.push_back(std::make_pair(key1, data1));
StrikeData data2;
data2.set_num_strikes(5);
entries.push_back(std::make_pair(key2, data2));
AddProtoEntries(entries);
int strikes = GetProtoStrikes(key1);
EXPECT_EQ(3, strikes);
strikes = GetProtoStrikes(key2);
EXPECT_EQ(5, strikes);
ClearAllProtoStrikesForKey(key1);
strikes = GetProtoStrikes(key1);
EXPECT_EQ(0, strikes);
strikes = GetProtoStrikes(key2);
EXPECT_EQ(5, strikes);
}
TEST_F(StrikeDatabaseTest, ClearAllProtoStrikesTest) {
// Set up database with 3 pre-existing strikes at |key1|, and 5 pre-existing
// strikes at |key2|.
const std::string key1 = "12345";
const std::string key2 = "13579";
std::vector<std::pair<std::string, StrikeData>> entries;
StrikeData data1;
data1.set_num_strikes(3);
entries.push_back(std::make_pair(key1, data1));
StrikeData data2;
data2.set_num_strikes(5);
entries.push_back(std::make_pair(key2, data2));
AddProtoEntries(entries);
EXPECT_EQ(3, GetProtoStrikes(key1));
EXPECT_EQ(5, GetProtoStrikes(key2));
ClearAllProtoStrikes();
EXPECT_EQ(0, GetProtoStrikes(key1));
EXPECT_EQ(0, GetProtoStrikes(key2));
}
} // namespace autofill
// 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 "components/autofill/core/browser/test_strike_database.h"
#include "components/autofill/core/browser/proto/strike_data.pb.h"
namespace autofill {
TestStrikeDatabase::TestStrikeDatabase() {}
TestStrikeDatabase::~TestStrikeDatabase() {}
void TestStrikeDatabase::GetProtoStrikes(
const std::string key,
const StrikesCallback& outer_callback) {
outer_callback.Run(GetStrikesForTesting(key));
}
void TestStrikeDatabase::ClearAllProtoStrikes(
const ClearStrikesCallback& outer_callback) {
db_.clear();
outer_callback.Run(/*success=*/true);
}
void TestStrikeDatabase::ClearAllProtoStrikesForKey(
const std::string& key,
const ClearStrikesCallback& outer_callback) {
db_.erase(key);
outer_callback.Run(/*success=*/true);
}
void TestStrikeDatabase::AddEntryWithNumStrikes(const std::string& key,
int num_strikes) {
StrikeData strike_data;
strike_data.set_num_strikes(num_strikes);
strike_data.set_last_update_timestamp(
base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds());
db_[key] = strike_data;
}
int TestStrikeDatabase::GetStrikesForTesting(const std::string& key) {
std::unordered_map<std::string, StrikeData>::iterator it = db_.find(key);
if (it != db_.end())
return it->second.num_strikes();
return 0;
}
} // namespace autofill
// 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.
#ifndef COMPONENTS_AUTOFILL_CORE_BROWSER_TEST_STRIKE_DATABASE_H_
#define COMPONENTS_AUTOFILL_CORE_BROWSER_TEST_STRIKE_DATABASE_H_
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "components/autofill/core/browser/strike_database.h"
namespace autofill {
// An in-memory-only test version of StrikeDatabase.
class TestStrikeDatabase : public StrikeDatabase {
public:
TestStrikeDatabase();
~TestStrikeDatabase() override;
// StrikeDatabase:
void GetProtoStrikes(const std::string key,
const StrikesCallback& outer_callback) override;
void ClearAllProtoStrikes(
const ClearStrikesCallback& outer_callback) override;
void ClearAllProtoStrikesForKey(
const std::string& key,
const ClearStrikesCallback& outer_callback) override;
// TestStrikeDatabase:
void AddEntryWithNumStrikes(const std::string& key, int num_strikes);
int GetStrikesForTesting(const std::string& key);
private:
// In-memory database of StrikeData.
std::unordered_map<std::string, StrikeData> db_;
};
} // namespace autofill
#endif // COMPONENTS_AUTOFILL_CORE_BROWSER_TEST_STRIKE_DATABASE_H_
......@@ -7957,6 +7957,16 @@ uploading your change for review.
</summary>
</histogram>
<histogram name="Autofill.StrikeDatabase.StrikeDatabaseInitFailed"
units="attempts" expires_after="2019-03-18">
<owner>jsaul@google.com</owner>
<owner>annelim@google.com</owner>
<summary>
When a StrikeDatabase fails to initialize, records the number of consecutive
failed initialization attempts.
</summary>
</histogram>
<histogram name="Autofill.StrikeDatabase.StrikesPresentWhenLocalCardSaved"
units="strikes" expires_after="2019-03-18">
<owner>jsaul@google.com</owner>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment