1. はじめに
ChromeやFirefoxで使えるストレージ永続化機能はご存知でしょうか。
https://developers.google.com/web/updates/2016/06/persistent-storage に Persistent Storage
という題で詳しく触れられていますが、通常 LRU で消去されてしまう IndexedDB
や CacheStorage
の内容を永続化してくれる機能だと理解しています。
CacheStorage
と Persistent Storage
の組み合わせは強力で、理論上大容量のアセットを永続化できるようになることから、PWAでのオフライン戦略や通信量削減戦略に対して強力な武器になると考えています。
しかし残念ながら、例によって Safari
はサポートしていません。そのため、WebKit
の利用を強制される iOS では、例え Chrome を使っていたとしてもこの恩恵を受けることはできません。
ただし Android では可能なので、Android だけでもユーザー体験を良くしたいという場合には効果があると考えています。
実際に自分が開発しているサービスでも Android だけ利用可能にしようと開発していましたが、実装・検証していく中で上で紹介した記事には言及されていない挙動をとる場合があることに気づきました。
具体的には、ストレージ永続化をおこなうためには window.navigator.storage.persist()
という API をたたいてストレージ永続化用のパーミッションを取得しなければならず、パーミッション取得のために必要な条件がいくつか存在するのですが、その条件がどうにも記事中のものとは異なる気がしたのです。
そこで今回、検証中に遭遇した謎挙動の理由を探るべく chromium
のソースを追ったり、デバッグ実行しながら詳細な挙動の確認をおこなったことで理由が掴めた気がしたので、忘れないうちにまとめようと思いました。
本記事では、 Persistent Storage
機能を利用するためにクリアしなければいけない条件を整理するために、 Google 側が記事中で提示している条件を挙げたあと、開発・検証中に遭遇した条件にそぐわない挙動を挙げ、chromium
ソースコードの該当箇所を示しながらなぜそういった挙動になったのかを解き明かし、最終的により具体的なパーミッション取得条件を示します。
2. 公式で挙げられているパーミッション取得のための条件
条件を挙げる前に、この機能がサポートされたのが Chrome 55
からであるため、それ以前のバージョンではサポート外にしたほうが良いでしょう。
ですので、まず API を利用できるかどうかの判断として
-
Chrome 55
以上 -
window.navigator.storage.persist
やwindow.navigator.storage.persisted
が存在する
を判定条件とするのが良さそうです。
これをクリアしたブラウザで、次に示す条件をひとつでもクリアするとストレージ永続化が利用できるようになります。
- The site is bookmarked (and the user has 5 or less bookmarks)
- The site has high site engagement
- The site has been added to home screen
- The site has push notifications enabled
※ https://developers.google.com/web/updates/2016/06/persistent-storage から引用
つまり
- 対象サイトがブックマークされていること ( ただしブックマークの数は 5以下 でなければならない )
- 対象サイトが high レベルのエンゲージメントスコアを出していること
- 対象サイトが ホームスクリーン に追加されていること
- 対象サイトがPUSH通知を許可していること
と書かれています。 site engagement
というのは https://www.chromium.org/developers/design-documents/site-engagement で言及されているのですが、 chrome://site-engagement で確認することのできる各サイトのスコア ( 0 ~ 100 の間 ) です。スコアの計算方法は先に挙げた記事中で書かれていますが、サイト上でタッチ操作やスクロール操作をおこなったり、対象サイトをURL直打ちで開いたりすると上がります。ようは その人にとってどれだけ時間を割いて利用しているサイトかどうか というのを定量的に測ろうとした仕組みということで、例えば Google側がもっている対象サイトにおける絶対評価 とかではないというところがポイントになります。
つまり人によって超有名サイトであってもスコアが低くなる可能性はあるし、逆に誰も知らないような個人サイトであってもスコアが高くなる可能性はあります。
さて、この時点ですでに疑問が湧きました。 high って具体的にいくつだろう...?
3. 開発・検証する中で遭遇した謎挙動
3.1 high の定義がわからない
パーミッション取得のための条件は、 先に挙げた条件のどれかひとつでも満たしていれば良い。と書かれています。つまり、ブックマークしておらず、ホームスクリーンにも追加しておらず、PUSH通知の許可もしていないサイトであれば
- 対象サイトが high レベルのエンゲージメントスコアを出していること
を満たさなければならないはずです。スコアは数値であり、 high という閾値がいくつなのかわからないにせよ、
50
でパーミッションが取得でき、 30
で取得できず、 10
で取得できるなんてことにはならないはずです。....ならない...はずなのです。
ですが実際は上記のようなケースが存在したため、 high の定義がなんなのか更にわからなくなりました。
3.2 どれかひとつの条件を満たせばいいという話ではなかったのか
いったんエンゲージメントスコアのことは忘れて、今度はブックマークを試してみました。
もちろんブックマーク数は 5 つ以下におさえた状態であるサイトを登録しました。
window.navigator.storage.persist()
をたたくと true
が返ってきます、成功です。
つづけて別のサイトをブックマークしてみました ( まだ 5 つ以下です )。
... false
が返ってきました。
ブックマークした A というサイトも B というサイトも、他の条件はほぼ同じです。エンゲージメントスコアは共に 3 程度。ブックマーク前の状態で両方のサイトとも window.navigator.storage.persist()
の結果で false
が返ってくることも確認していました。
となると、条件を満たしていたとしてもダメな場合があるとしか想像できません。
何も信用できなくなってきました。
同様の現象は、自分では検証していないのですが Android 端末でホームスクリーンに追加した場合にも起きたようです。
Aという端末ではホームスクリーンに追加したとたん利用可能になったが、Bではホームスクリーンに追加してあるのにダメだったとのことでした。
4. ソースコードから仕様を追う
明らかにドキュメント化されていない仕様がありそうな気配を感じたので、 chromium
のソースを落としてきてビルド、lldbでデバッグしたり printデバッグなどをおこないました。
(余談) 自分は macOS で作業したのですが、当初 GitHub 上の mirror からソースを落としてきてビルドしようと試みて途中でビルドエラーで怒られてしまったので、 https://chromium.googlesource.com/chromium/src/+/master/docs/mac_build_instructions.md にならって素直にその通りやるのがよかったです
4.1. 該当箇所のソースコードを見つける
今回どうやって window.navigator.storage.persist()
の実装箇所を見つけたのか、その過程をできるだけ詳細に残しておこうと思います。ソースコードを追っていく流れをそのまま書いているので、過程に興味がない方はこの章をスキップしていただくと、ピンポイントで関係のあるソースコードの箇所からの説明からになります。
まずはじめに、 window.navigator.storage.persist()
をたたいたときに呼ばれるソースコードを見てみます。これは https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/modules/quota/storage_manager.cc#L82-L104 に書かれていて、中で RequestPermission
を呼んでいるのがわかります。
ソースコードの読み始めのポイントとしては、 JavaScript
側でのAPI呼び出しに対応するC++側のAPIが必ずあるはずなので、その処理を見つけることです。今回であれば persist
という名前で検索すればある程度しぼりこむことができます。
- storage_manager.cc
ScriptPromise StorageManager::persist(ScriptState* script_state) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
ExecutionContext* execution_context = ExecutionContext::From(script_state);
DCHECK(execution_context->IsSecureContext()); // [SecureContext] in IDL
const SecurityOrigin* security_origin =
execution_context->GetSecurityOrigin();
if (security_origin->IsOpaque()) {
resolver->Reject(V8ThrowException::CreateTypeError(
script_state->GetIsolate(), kUniqueOriginErrorMessage));
return promise;
}
Document* doc = To<Document>(execution_context);
GetPermissionService(ExecutionContext::From(script_state))
.RequestPermission(
CreatePermissionDescriptor(PermissionName::DURABLE_STORAGE),
LocalFrame::HasTransientUserActivation(doc->GetFrame()),
WTF::Bind(&StorageManager::PermissionRequestComplete,
WrapPersistent(this), WrapPersistent(resolver)));
return promise;
}
ここでひとつ重要なことがわかりました。 chromium
の中で Persistent Storage
のパーミッションを扱う際のキーワードが DURABLE_STORAGE
だということです。
RequestPermission
の実装は https://github.com/chromium/chromium/blob/master/content/browser/permissions/permission_service_impl.cc#L162-L171 にあり
読み進めていくと
https://github.com/chromium/chromium/blob/master/content/browser/permissions/permission_service_impl.cc#L215-L222 のあたりで
- permission_service_impl.cc
int id =
PermissionControllerImpl::FromBrowserContext(browser_context)
->RequestPermissions(
types, context_->render_frame_host(), origin_.GetURL(),
user_gesture,
base::Bind(&PermissionServiceImpl::OnRequestPermissionsResponse,
weak_factory_.GetWeakPtr(), pending_request_id));
に気づきます。ここで BrowserContext
なるものを使って PermissionController
経由で RequestPermissions
を呼び出しています。 PermissionController::RequestPermissions
の処理は https://github.com/chromium/chromium/blob/master/content/browser/permissions/permission_controller_impl.cc#L199-L229 に書かれていて、 PermissionControllerDelegate
のインスタンスを BrowserContext
の GetPermissionControllerDelegate()
経由で取得し、delegate
の RequestPermissions
を呼んでいます。
- permission_controller_impl.cc
int PermissionControllerImpl::RequestPermissions(
const std::vector<PermissionType>& permissions,
RenderFrameHost* render_frame_host,
const GURL& requesting_origin,
bool user_gesture,
const base::Callback<
void(const std::vector<blink::mojom::PermissionStatus>&)>& callback) {
for (PermissionType permission : permissions)
NotifySchedulerAboutPermissionRequest(render_frame_host, permission);
auto it = devtools_permission_overrides_.find(requesting_origin.GetOrigin());
if (it != devtools_permission_overrides_.end()) {
std::vector<blink::mojom::PermissionStatus> result;
for (auto& permission : permissions)
result.push_back(GetPermissionOverrideStatus(it->second, permission));
callback.Run(result);
return kNoPendingOperation;
}
PermissionControllerDelegate* delegate =
browser_context_->GetPermissionControllerDelegate();
if (!delegate) {
std::vector<blink::mojom::PermissionStatus> result(
permissions.size(), blink::mojom::PermissionStatus::DENIED);
callback.Run(result);
return kNoPendingOperation;
}
return delegate->RequestPermissions(permissions, render_frame_host,
requesting_origin, user_gesture,
callback);
}
この GetPermissionControllerDelegate
は BrowserContext
では pure virtual として宣言されており、継承先で実装するようになっています。継承先はブラウザの種類によって多岐にわたり、 chromecast や headless browser などいくつかの実装があるようです。
今回見たいのはおそらく https://github.com/chromium/chromium/blob/master/chrome/browser/profiles/profile_impl.cc#L1215-L1217 で実装されているところで、中で PermissionManagerFactory
をつかっているのがわかります。
- profile_impl.cc
content::PermissionControllerDelegate*
ProfileImpl::GetPermissionControllerDelegate() {
return PermissionManagerFactory::GetForProfile(this);
}
PermissionManagerFactory::GetForProfile
の実装は https://github.com/chromium/chromium/blob/master/chrome/browser/permissions/permission_manager_factory.cc#L14-L18 に書かれており、 GetServiceForBrowserContext
を通して PermissionManager
インスタンスを作っていることがわかります。 つまり PermissionControllerDelegate
の正体は PermissionManager
クラスで、結局のところこのクラスの RequestPermissions
を呼んでいることがわかりました。
- permission_manager_factory.cc
PermissionManager*
PermissionManagerFactory::GetForProfile(Profile* profile) {
return static_cast<PermissionManager*>(
GetInstance()->GetServiceForBrowserContext(profile, true));
}
PermissionManager::RequestPermissions
の実装は https://github.com/chromium/chromium/blob/master/chrome/browser/permissions/permission_manager.cc#L393-L445 に書かれており、読み進めると
- permission_manager.cc
PermissionContextBase* context = GetPermissionContext(permission);
DCHECK(context);
context->RequestPermission(
web_contents, request, canonical_requesting_origin, user_gesture,
base::BindOnce(
&PermissionResponseCallback::OnPermissionsRequestResponseStatus,
std::move(response_callback)));
で GetPermissionContext
を permission
を引数に ( ここでは DURABLE_STORAGE
) 呼んでいるのがわかります。 GetPermissionContext
の実装は
PermissionContextBase* PermissionManager::GetPermissionContext(
ContentSettingsType type) {
const auto& it = permission_contexts_.find(type);
return it == permission_contexts_.end() ? nullptr : it->second.get();
}
のようになっていて、 ContentSettingsType
が CONTENT_SETTINGS_TYPE_DURABLE_STORAGE
で検索していることがわかります。ではこの permission_contexts_
がどうやって初期化されているかというと、 https://github.com/chromium/chromium/blob/master/chrome/browser/permissions/permission_manager.cc#L280-L334 に初期化処理があり
PermissionManager::PermissionManager(Profile* profile) : profile_(profile) {
... (略) ...
permission_contexts_[CONTENT_SETTINGS_TYPE_DURABLE_STORAGE] =
std::make_unique<DurableStoragePermissionContext>(profile);
... (略) ...
}
正体が DurableStoragePermissionContext
だとわかりました!
では この実装を読みにいきます。
4.2 該当箇所のソースを読む
window.navigator.storage.persist()
を呼んだ時にめぐりめぐって呼ばれるのは DurableStoragePermissionContext::DecidePermission
という部分です。
- durable_storage_permission_context.cc
void DurableStoragePermissionContext::DecidePermission(
content::WebContents* web_contents,
const PermissionRequestID& id,
const GURL& requesting_origin,
const GURL& embedding_origin,
bool user_gesture,
BrowserPermissionCallback callback) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
DCHECK_NE(CONTENT_SETTING_ALLOW,
GetPermissionStatus(nullptr /* render_frame_host */,
requesting_origin, embedding_origin)
.content_setting);
DCHECK_NE(CONTENT_SETTING_BLOCK,
GetPermissionStatus(nullptr /* render_frame_host */,
requesting_origin, embedding_origin)
.content_setting);
// Durable is only allowed to be granted to the top-level origin. Embedding
// origin is the last committed navigation origin to the web contents.
if (requesting_origin != embedding_origin) {
NotifyPermissionSet(id, requesting_origin, embedding_origin,
std::move(callback), false /* persist */,
CONTENT_SETTING_DEFAULT);
return;
}
scoped_refptr<content_settings::CookieSettings> cookie_settings =
CookieSettingsFactory::GetForProfile(profile());
// Don't grant durable for session-only storage, since it won't be persisted
// anyway. Don't grant durable if we can't write cookies.
if (cookie_settings->IsCookieSessionOnly(requesting_origin) ||
!cookie_settings->IsCookieAccessAllowed(requesting_origin,
requesting_origin)) {
NotifyPermissionSet(id, requesting_origin, embedding_origin,
std::move(callback), false /* persist */,
CONTENT_SETTING_DEFAULT);
return;
}
const size_t kMaxImportantResults = 10;
std::vector<ImportantSitesUtil::ImportantDomainInfo> important_sites =
ImportantSitesUtil::GetImportantRegisterableDomains(profile(),
kMaxImportantResults);
std::string registerable_domain =
net::registry_controlled_domains::GetDomainAndRegistry(
requesting_origin,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
if (registerable_domain.empty() && requesting_origin.HostIsIPAddress())
registerable_domain = requesting_origin.host();
for (const auto& important_site : important_sites) {
if (important_site.registerable_domain == registerable_domain) {
NotifyPermissionSet(id, requesting_origin, embedding_origin,
std::move(callback), true /* persist */,
CONTENT_SETTING_ALLOW);
return;
}
}
NotifyPermissionSet(id, requesting_origin, embedding_origin,
std::move(callback), false /* persist */,
CONTENT_SETTING_DEFAULT);
}
このうち、 persist()
の結果で true
を返してくれるのは
const size_t kMaxImportantResults = 10;
std::vector<ImportantSitesUtil::ImportantDomainInfo> important_sites =
ImportantSitesUtil::GetImportantRegisterableDomains(profile(),
kMaxImportantResults);
std::string registerable_domain =
net::registry_controlled_domains::GetDomainAndRegistry(
requesting_origin,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
if (registerable_domain.empty() && requesting_origin.HostIsIPAddress())
registerable_domain = requesting_origin.host();
for (const auto& important_site : important_sites) {
if (important_site.registerable_domain == registerable_domain) {
NotifyPermissionSet(id, requesting_origin, embedding_origin,
std::move(callback), true /* persist */,
CONTENT_SETTING_ALLOW);
return;
}
}
の部分で、 ImportantSitesUtil::GetImportantRegisterableDomains
で返ってきた結果の中に、対象サイトから取得した registerable_domain
が存在していれば true
が返っているとわかります。ここで非常に重要なことは、 ImportantSitesUtil::GetImportantRegisterableDomains
の結果が 最大10件 だということ。もうひとつは、 registerable_domain
の決定方法 です。
registerable_domain
は、ドメイン名によるでしょうが第2・第3レベルまでのドメイン名になるようです。例えば、 https://mail.google.com
であれば google.com
が registerable_domain
になり、 https://www.google.co.jp
の場合は google.co.jp
になります。このルールが先に挙げた謎の挙動のヒントになってきます。
つづいて、ImportantSitesUtil::GetImportantRegisterableDomains
を見にいきます。
実装は https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/important_sites_util.cc#L383-L424 に書かれています
- important_sites_util.cc
std::vector<ImportantDomainInfo>
ImportantSitesUtil::GetImportantRegisterableDomains(Profile* profile,
size_t max_results) {
std::map<std::string, ImportantDomainInfo> important_info;
std::map<GURL, double> engagement_map;
PopulateInfoMapWithEngagement(profile, blink::mojom::EngagementLevel::MEDIUM,
&engagement_map, &important_info);
PopulateInfoMapWithContentTypeAllowed(
profile, CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
ImportantReason::NOTIFICATIONS, &important_info);
PopulateInfoMapWithContentTypeAllowed(
profile, CONTENT_SETTINGS_TYPE_DURABLE_STORAGE, ImportantReason::DURABLE,
&important_info);
PopulateInfoMapWithBookmarks(profile, engagement_map, &important_info);
std::unordered_set<std::string> blacklisted_domains =
GetBlacklistedImportantDomains(profile);
std::vector<std::pair<std::string, ImportantDomainInfo>> items(
important_info.begin(), important_info.end());
std::sort(items.begin(), items.end(), &CompareDescendingImportantInfo);
std::vector<ImportantDomainInfo> final_list;
for (std::pair<std::string, ImportantDomainInfo>& domain_info : items) {
if (final_list.size() >= max_results)
return final_list;
if (blacklisted_domains.find(domain_info.first) !=
blacklisted_domains.end()) {
continue;
}
final_list.push_back(domain_info.second);
RECORD_UMA_FOR_IMPORTANT_REASON(
"Storage.ImportantSites.GeneratedReason",
"Storage.ImportantSites.GeneratedReasonCount",
domain_info.second.reason_bitfield);
}
return final_list;
}
ここではじめに紹介したパーミッション取得のための条件を思い出します。
- 対象サイトがブックマークされていること ( ただしブックマークの数は 5以下 でなければならない )
- 対象サイトが high レベルのエンゲージメントスコアを出していること
- 対象サイトが ホームスクリーン に追加されていること
- 対象サイトがPUSH通知のパーミッションを許可していること
上記の条件がまさにコード中に書かれているのが分かります。
ざっと処理の流れを説明すると
std::map<std::string, ImportantDomainInfo> important_info;
に対してひとつずつ条件を照らし合わせながら該当するものを追加していき、
CompareDescendingImportantInfo
のルールにのっとってこれを sort した後、blacklist にのっているドメインを除きつつ、最大10件になるように final_list
を構築して返却しています。
ここで先ほど 最大10件 というのが重要だと説明しましたが、 important_info
に10件より多くの registerable_domain
が入っていたとしても、
sortの結果によって11件目以降になると、 final_list
の中には現れないということがわかります。
つまり、具体的にどういった条件で important_info
に追加されていくのかという情報と同じかそれ以上に、 sort の条件が気になります。
CompareDescendingImportantInfo
の実装はどうなっているでしょうか。
bool CompareDescendingImportantInfo(
const std::pair<std::string, ImportantDomainInfo>& a,
const std::pair<std::string, ImportantDomainInfo>& b) {
int score_a = GetScoreForReasonsBitfield(a.second.reason_bitfield);
int score_b = GetScoreForReasonsBitfield(b.second.reason_bitfield);
int bitfield_diff = score_a - score_b;
if (bitfield_diff != 0)
return bitfield_diff > 0;
return a.second.engagement_score > b.second.engagement_score;
}
ImportantDomainInfo
に含まれる reason_bitfield
の値によって得たスコアどうしを比較しており、これが大きい方が優先されるとわかります。
もし reason_bitfield
が同じ値の場合は、純粋な engagement_score
( chrome://site-engagement で見れる値 ) の大きい方が優先されるようです。
reason_bitfield
の値は、複数の reason
に分解した後、ひとつずつ以下の GetScoreForReason
を通してスコアに変換して足し合わせています。
int GetScoreForReason(ImportantReason reason) {
switch (reason) {
case ImportantReason::ENGAGEMENT:
return 1 << 0;
case ImportantReason::DURABLE:
return 1 << 1;
case ImportantReason::BOOKMARKS:
return 1 << 2;
case ImportantReason::HOME_SCREEN:
return 1 << 3;
case ImportantReason::NOTIFICATIONS:
return 1 << 4;
case ImportantReason::REASON_BOUNDARY:
return 0;
}
return 0;
}
たとえば、 ImportantReason::ENGAGEMENT
と ImportantReason::DURABLE
を同時に満たすreason_bitfield
のスコアは 3
になります。
ここからわかることは、各条件にはそれぞれ優先度が存在し、例えばブックマークに追加することよりもホームスクリーンに追加したりPUSH通知を許可するほうが優先的に選ばれることになるとわかります。
更に、条件は重ねて適応することができ、例えば同じブックマークに追加したサイトであっても ImportantReason::ENGAGEMENT
を満たしているかでスコアが 1
点 変わることがわかります。
ここまででだいぶ理解は進みましたが、最後に各条件がより具体的にどういったことを求めているかを見ていきたいと思います ( engagement score の high レベルの定義も気になります )。
4.2.1 high site engagement の意味を知る
PopulateInfoMapWithEngagement
の中を見ます (
https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/important_sites_util.cc#L236-L270 )
void PopulateInfoMapWithEngagement(
Profile* profile,
blink::mojom::EngagementLevel minimum_engagement,
std::map<GURL, double>* engagement_map,
std::map<std::string, ImportantDomainInfo>* output) {
SiteEngagementService* service = SiteEngagementService::Get(profile);
std::vector<mojom::SiteEngagementDetails> engagement_details =
service->GetAllDetails();
std::set<GURL> content_origins;
// We can have multiple origins for a single domain, so we record the one
// with the highest engagement score.
for (const auto& detail : engagement_details) {
if (detail.installed_bonus > 0) {
// This origin was recently launched from the home screen.
MaybePopulateImportantInfoForReason(detail.origin, &content_origins,
ImportantReason::HOME_SCREEN, output);
}
(*engagement_map)[detail.origin] = detail.total_score;
if (!service->IsEngagementAtLeast(detail.origin, minimum_engagement))
continue;
std::string registerable_domain =
ImportantSitesUtil::GetRegisterableDomainOrIP(detail.origin);
ImportantDomainInfo& info = (*output)[registerable_domain];
if (detail.total_score > info.engagement_score) {
info.registerable_domain = registerable_domain;
info.engagement_score = detail.total_score;
info.example_origin = detail.origin;
info.reason_bitfield |= 1 << ImportantReason::ENGAGEMENT;
}
}
}
engagement_details
というのは、 chrome://site-engagement で見れるリストが返ってきていると思ってもらっても大丈夫です。このリストを traverse しながら、 installed_bonus
の値が 0 より大きければ ホームスクリーン追加用の reason_bitfield
( ImportantReason::HOME_SCREEN
) を追加しつつ、 minimum_engagement
以上のスコアであるもののみに reason_bitfield
( ImportantReason::ENGAGEMENT
) を追加します。
ここで high
の具体的な条件がわかりました。 minimum_engagement
です。これは引数で与えられたものだったので、呼び出し元に戻ると blink::mojom::EngagementLevel::MEDIUM
を渡していることがわかります。この上で IsEngagementAtLeast
の実装を読んでみます。
https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_service.cc#L248-L270 に書かれており、 blink::mojom::EngagementLevel::MEDIUM
に相当する条件が score >= SiteEngagementScore::GetMediumEngagementBoundary()
だとわかります。
- site_engagement_service.cc
bool SiteEngagementService::IsEngagementAtLeast(
const GURL& url,
blink::mojom::EngagementLevel level) const {
DCHECK_LT(SiteEngagementScore::GetMediumEngagementBoundary(),
SiteEngagementScore::GetHighEngagementBoundary());
double score = GetScore(url);
switch (level) {
case blink::mojom::EngagementLevel::NONE:
return true;
case blink::mojom::EngagementLevel::MINIMAL:
return score > 0;
case blink::mojom::EngagementLevel::LOW:
return score >= 1;
case blink::mojom::EngagementLevel::MEDIUM:
return score >= SiteEngagementScore::GetMediumEngagementBoundary();
case blink::mojom::EngagementLevel::HIGH:
return score >= SiteEngagementScore::GetHighEngagementBoundary();
case blink::mojom::EngagementLevel::MAX:
return score == SiteEngagementScore::kMaxPoints;
}
NOTREACHED();
return false;
}
そこで SiteEngagementScore::GetMediumEngagementBoundary()
を読みにいくと
https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_score.cc#L146-L148 に
- site_engagement_score.cc
double SiteEngagementScore::GetMediumEngagementBoundary() {
return GetParamValues()[MEDIUM_ENGAGEMENT_BOUNDARY].second;
}
から 15
点が閾値だとわかりました!
ここで先ほど軽く説明して終わったホームスクリーンへの追加についても深掘りしたいと思います。
4.2.2 「ホームスクリーンに追加」の具体的な条件を知る
4.2.1 の PopulateInfoMapWithEngagement
の実装の説明の中で、 installed_bonus > 0
であれば ImportantReason::HOME_SCREEN
が reason_bitfield
に足されると説明しましたが、では具体的に installed_bonus
が 0 より大きくなるためにはどうなればいいのでしょうか。
これを知るためには
std::vector<mojom::SiteEngagementDetails> engagement_details =
service->GetAllDetails();
の中を知る必要があります。
この処理は最終的にサイトごとに SiteEngagementScore::GetDetails()
が呼ばれるのですが、 https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_score.cc#L265-L276 のあたりを見ると、どうやって mojom::SiteEngagementDetails
ができているのかがわかります
- site_engagement_score.cc
double SiteEngagementScore::GetTotalScore() const {
return std::min(DecayedScore() + BonusIfShortcutLaunched(), kMaxPoints);
}
mojom::SiteEngagementDetails SiteEngagementScore::GetDetails() const {
mojom::SiteEngagementDetails engagement;
engagement.origin = origin_;
engagement.base_score = DecayedScore();
engagement.installed_bonus = BonusIfShortcutLaunched();
engagement.total_score = GetTotalScore();
return engagement;
}
ここで注目したいのは BonusIfShortcutLaunched()
です。この結果が installed_bonus
の値にもなるし、 total_score
にも反映されます。
double SiteEngagementScore::BonusIfShortcutLaunched() const {
int days_since_shortcut_launch =
(clock_->Now() - last_shortcut_launch_time_).InDays();
if (days_since_shortcut_launch <= kMaxDaysSinceShortcutLaunch)
return GetWebAppInstalledPoints();
return 0;
}
実装は上記のようになっていて、kMaxDaysSinceShortcutLaunch
は https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_score.cc#L34 に書かれていますが 10
です。
つまり最後にホームスクリーン上に追加したアプリから起動したのが10日以内であれば GetWebAppInstalledPoints()
( ちなみに 5
点です https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_score.cc#L81 ) を付与するよということを表しています。
ここから、例えホームスクリーンに追加していたとしても、10日以上起動していなければインストールボーナスはつかない。つまり reason_bitfield
に ImportantReason::HOME_SCREEN
は追加しないということがわかります。
ここまででもう一度 ImportantSitesUtil::GetImportantRegisterableDomains
の実装に戻りましょう。 4.2.1 と 4.2.2 では PopulateInfoMapWithEngagement
の中を具体的にみていきました。
そのあとに書かれている
PopulateInfoMapWithContentTypeAllowed(
profile, CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
ImportantReason::NOTIFICATIONS, &important_info);
の部分は読んでそのままで、 PUSH通知をすでに許可している場合は、 ImportantReason::NOTIFICATIONS
が reason_bitfield
に加えられます。
続けて書いてある
PopulateInfoMapWithContentTypeAllowed(
profile, CONTENT_SETTINGS_TYPE_DURABLE_STORAGE, ImportantReason::DURABLE,
&important_info);
は、 すでにDURABLE_STORAGEの条件をみたしている場合 は ImportantReason::DURABLE
を reason_bitfield
に加えるという意味になります。つまり、一度でも window.navigatgor.storage.persist()
でストレージ永続化できていた場合はここで必ず reason_bitfield
に 2点 加算されます。これも registerable_domain
の優先度を決める上で非常に重要になってきます。
こうなると残りは
PopulateInfoMapWithBookmarks(profile, engagement_map, &important_info);
だけなので、次でブックマークに関する具体的な条件をみていきます
4.2.3 「ブックマークに追加」の具体的な条件を知る
実装は https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/important_sites_util.cc#L307-L352 に書かれており、ブックマークの数が kMaxBookmarks
を超えているかどうかで処理が異なることがわかります。
kMaxBookmarks
は https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/important_sites_util.cc#L61 に書かれていますが 5
です。
つまり、条件になっていた「ブックマーク数が5以下だったら」の数ですね。
void PopulateInfoMapWithBookmarks(
Profile* profile,
const std::map<GURL, double>& engagement_map,
std::map<std::string, ImportantDomainInfo>* output) {
SiteEngagementService* service = SiteEngagementService::Get(profile);
BookmarkModel* model =
BookmarkModelFactory::GetForBrowserContextIfExists(profile);
if (!model)
return;
std::vector<UrlAndTitle> untrimmed_bookmarks;
model->GetBookmarks(&untrimmed_bookmarks);
// Process the bookmarks and optionally trim them if we have too many.
std::vector<UrlAndTitle> result_bookmarks;
if (untrimmed_bookmarks.size() > kMaxBookmarks) {
std::copy_if(untrimmed_bookmarks.begin(), untrimmed_bookmarks.end(),
std::back_inserter(result_bookmarks),
[service](const UrlAndTitle& entry) {
return service->IsEngagementAtLeast(
entry.url.GetOrigin(),
blink::mojom::EngagementLevel::LOW);
});
// TODO(dmurph): Simplify this (and probably much more) once
// SiteEngagementService::GetAllDetails lands (crbug/703848), as that will
// allow us to remove most of these lookups and merging of signals.
std::sort(
result_bookmarks.begin(), result_bookmarks.end(),
[&engagement_map](const UrlAndTitle& a, const UrlAndTitle& b) {
auto a_it = engagement_map.find(a.url.GetOrigin());
auto b_it = engagement_map.find(b.url.GetOrigin());
double a_score = a_it == engagement_map.end() ? 0 : a_it->second;
double b_score = b_it == engagement_map.end() ? 0 : b_it->second;
return a_score > b_score;
});
if (result_bookmarks.size() > kMaxBookmarks)
result_bookmarks.resize(kMaxBookmarks);
} else {
result_bookmarks = std::move(untrimmed_bookmarks);
}
std::set<GURL> content_origins;
for (const UrlAndTitle& bookmark : result_bookmarks) {
MaybePopulateImportantInfoForReason(bookmark.url, &content_origins,
ImportantReason::BOOKMARKS, output);
}
}
ブックマーク数が5以下の場合は、 result_bookmarks
にそのままそれらが入り、 ImportantReason::BOOKMARKS
が reason_bitfield
に追加されます。
そうでない場合は 全ブックマークの中から blink::mojom::EngagementLevel::LOW
( https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_service.cc#L259-L260 から 1
) 以上のスコアのものを集めたあと、 engagement_score
の値によって sort し、上から 5件 を result_bookmarks
に追加するという挙動になるようです。
つまり、5件以下であればそのまま ImportantReason::BOOKMARKS
が追加され、そうでなかったとしても、ブックマークしたサイトのスコアが 1
以上であり、かつスコアの大きい順の上位5つ以内に入っていれば同様に ImportantReason::BOOKMARKS
が reason_bitfield
に足されるとわかります。
5. 謎だった挙動を解明する
4章でソースコードを追いながら詳細な仕様を把握できました。
このことから不可解だった次の挙動にも説明ができるようになります。
- スコアが
50
でパーミッションが取得でき、30
で取得できず、10
で取得できてしまう - ブックマークした A というサイトと B というサイトがあるが A しかパーミッションを取得できない
- ホームスクリーンに追加してもパーミッションを取得できない
まず条件をあらためて整理します。
もともとの条件はこうでした。
- 対象サイトがブックマークされていること ( ただしブックマークの数は 5以下 でなければならない )
- 対象サイトが high レベルのエンゲージメントスコアを出していること
- 対象サイトが ホームスクリーン に追加されていること
- 対象サイトがPUSH通知のパーミッションを許可していること
その上、調べていくと次のことがわかりました
- パーミッションを許可できるのは上位10件のサイトまで
- 別のサイトであっても
registerable_domain
は被る可能性がある - エンゲージメントスコアだけを条件にする場合は15点以上必要
- ホームスクリーンに追加することを条件にするなら、最後に起動してから10日以内でなければならない
- すでにパーミッションを与えているサイトには
reason_bitfield
に2
点が加わる - ブックマークが5件を超える場合は、スコアが1点以上のサイトを対象に上位5件に対して
reason_bitfield
に4
点を加える
これをふまえると
- スコアが
50
でパーミッションが取得でき、30
で取得できず、10
で取得できてしまう
この場合は、 50
でパーミションがとれたのはスコアが 15
点以上だったからなのは良いとして、 10
でも取得できたのはドメイン名が 50
のものと同じだった可能性が高いです( 実際の自分の場合は https://mail.google.com
が上位にきていて、 10
のは別の google.com
ドメインのサイトでした )。逆になぜ 30
でもパーミッションがとれなかったのかは、上位 10件 に漏れてしまっていたからだと考えられます。
罠だったのは、検証のために複数のサイトで window.navigator.storage.persist()
を実行してまわっていったため、一度許可された場合に DURABLE_STORAGE
の条件が満たされ、エンゲージメントスコアが 15点以上のサイトは常に reason_bitfield
が 3
になるという挙動をとる点でした ( ImportantReason::ENGAGEMENT
が 1点、 ImportantReason::DURABLE_STORAGE
が 2点 )。この条件を満たしたものが 10 件を超えていると、新しいサイトでパーミッションを取得しようとした場合に、仮に 15点以上だったとしても、 reason_bitfield
の値が ImportantReason::ENGAGEMENT
だけのため ( 1
点 )、永遠にパーミッションを許可できない状態になっていました。
同様に
- ブックマークした A というサイトと B というサイトがあるが A しかパーミッションを取得できない
この件でも、すでに対象のサイトが10件を超えている場合に問題になります。
もしブックマークの条件が満たされていたとしても、サイトのスコアが 15
点未満の場合は ImportantReason::BOOKMARKS
( 4
点 ) しか適応されていません。
このとき、他にホームスクリーンに追加したアプリが複数あったり、ブックマークとスコア15以上を同時に満たすサイトが10件以上ある状況だと、例えブックマークの登録が5件以内だったとしてもパーミッションを取得できない事態になります。
- ホームスクリーンに追加してもパーミッションを取得できない
この場合は、ホームスクリーンへ追加することの優先度がとても高いことから、他のサイトに優先順位で負けることがあまり考えられないため、アプリを最後に起動したのが10日以上前である可能性が高いです。
6. まとめ
Persistent Storage
のパーミッションを取得するための条件は
- 対象サイトがブックマークされていること ( ただしブックマークの数は 5以下 でなければならない )
- 対象サイトが high レベルのエンゲージメントスコアを出していること
- 対象サイトが ホームスクリーン に追加されていること
- 対象サイトがPUSH通知のパーミッションを許可していること
だけでなく、ソースコードを追うことでさらに細かな条件があることがわかりました。
具体的には
- パーミッションを許可できるのは上位
10
件のregisterable_domain
に合致するサイトだけ - 別のサイトであっても
registerable_domain
は被る可能性がある (a.google.com
とb.google.com
はどちらもgoogle.com
がregisterable_domain
) - エンゲージメントスコアだけを条件にする場合は
15
点以上必要 - ホームスクリーンに追加することを条件にするなら、最後に起動してから
10
日以内でなければならない - ブックマークが5件を超える場合は、エンゲージメントスコアが1点以上のサイトを対象に上位5件が加点対象になる
- エンゲージメントスコアの条件を満たすと
reason_bitfield
に1
点が加わる - すでにパーミッションを与えているサイトには
reason_bitfield
に2
点が加わる - ブックマークに追加しているサイトには
reason_bitfield
に4
点を加える - ホームスクリーンに追加されているサイトには
reason_bitfield
に8
点が加わる - PUSH通知を許可しているサイトには
reason_bitfield
に16
点が加わる -
reason_bitfield
の合計が大きい順に10件を抽出する。reason_bitfield
が同じ場合はエンゲージメントスコアが高い方を優先する
のような条件が裏にあり決められています。
これをふまえると、なかなかサービスの機能としてストレージ永続化を提供するのは難しいなと感じましたが、詳細な条件を知れたことで大分スッキリしましたし、対策も考えやすそうです。
もしすでに謎挙動を観測された経験がある方がいらっしゃれば、
この記事の結論と相違ないかコメントをいただければ幸いです!
7. FAQ ( 追記 )
記事中にわかりにくいなという箇所がいくつかあったので、疑問になりそうなところをあらかじめ解説しておこうと思います。
7.1 異なるサイトで registerable_domain
が一緒だった場合の挙動をもう一度
例えば a.google.com
と b.google.com
があって、それぞれエンゲージメントスコアが 20
と 1
だったとします。
条件から考えると a.google.com
はエンゲージメントスコアが 15
より大きいためパーミッションを取得できますが、 b.google.com
では一見ダメそうです。しかし、これらは registerable_domain
が同じ google.com
なので、 google.com
が 許可すべきドメイン 10 件の中に入っていさえすればパーミッションは取得できます
7.2 得点の説明が reason_bitfield
と エンゲージメントスコアと 2種類あってわかりにくい
エンゲージメントスコアは chrome://site-engagement でみれる 0 ~ 100 の間の点数です。サイトによって異なります。
この値は、 最大10件の registerable_domain
のリストを作るときに利用されます。
また、 reason_bitfield
の値も同様にリストを作るときに利用する値ですが、基本的には reason_bitfield
の値で 降順に並べ、同じ reason_bitfield
のサイトどうしを比較する際にはじめてエンゲージメントスコアを参照する流れです。
エンゲージメントスコアは 15
点以上であるかが基準になっており、 15
以上であれば reason_bitfield
に 1点加算されます。
7.3 一度許可したサイトのパーミッションは消せるのか
ブラウザの再起動等では消えないので、別な削除手段をとる必要がありそうです。一度許可してしまうと、常に reason_bitfield
に 2点 加算されます。