blob: 8bfd9a19b7b81aec9a3c4debf5d75ce1df3cd655 [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 "third_party/blink/renderer/core/frame/ad_tracker.h"
#include <memory>
#include "base/feature_list.h"
#include "third_party/blink/renderer/bindings/core/v8/source_location.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/core/core_probe_sink.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/platform/bindings/v8_binding.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_initiator_type_names.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/casting.h"
#include "v8/include/v8.h"
namespace blink {
namespace {
bool IsKnownAdExecutionContext(ExecutionContext* execution_context) {
// TODO(jkarlin): Do the same check for worker contexts.
if (auto* window = DynamicTo<LocalDOMWindow>(execution_context)) {
LocalFrame* frame = window->GetFrame();
if (frame && frame->IsAdSubframe())
return true;
}
return false;
}
String GenerateFakeUrlFromScriptId(int script_id) {
// Null string is used to represent scripts with neither a name nor an ID.
if (script_id == v8::Message::kNoScriptIdInfo)
return String();
// The prefix cannot appear in real URLs.
return String::Format("{ id %d }", script_id);
}
} // namespace
namespace features {
// Controls whether the AdTracker will look across async stacks to determine if
// the currently running stack is ad related.
const base::Feature kAsyncStackAdTagging{"AsyncStackAdTagging",
base::FEATURE_ENABLED_BY_DEFAULT};
} // namespace features
// static
AdTracker* AdTracker::FromExecutionContext(
ExecutionContext* execution_context) {
if (!execution_context)
return nullptr;
if (auto* window = DynamicTo<LocalDOMWindow>(execution_context)) {
if (LocalFrame* frame = window->GetFrame()) {
return frame->GetAdTracker();
}
}
return nullptr;
}
// static
bool AdTracker::IsAdScriptExecutingInDocument(Document* document,
StackType stack_type) {
AdTracker* ad_tracker =
document->GetFrame() ? document->GetFrame()->GetAdTracker() : nullptr;
return ad_tracker && ad_tracker->IsAdScriptInStack(stack_type);
}
AdTracker::AdTracker(LocalFrame* local_root)
: local_root_(local_root),
async_stack_enabled_(
base::FeatureList::IsEnabled(features::kAsyncStackAdTagging)) {
local_root_->GetProbeSink()->AddAdTracker(this);
}
AdTracker::~AdTracker() {
DCHECK(!local_root_);
}
void AdTracker::Shutdown() {
if (!local_root_)
return;
local_root_->GetProbeSink()->RemoveAdTracker(this);
local_root_ = nullptr;
}
String AdTracker::ScriptAtTopOfStack() {
// CurrentStackTrace is 10x faster than CaptureStackTrace if all that you need
// is the url of the script at the top of the stack. See crbug.com/1057211 for
// more detail.
v8::Isolate* isolate = v8::Isolate::GetCurrent();
DCHECK(isolate);
v8::Local<v8::StackTrace> stack_trace =
v8::StackTrace::CurrentStackTrace(isolate, /*frame_limit=*/1);
if (stack_trace.IsEmpty() || stack_trace->GetFrameCount() < 1)
return String();
v8::Local<v8::StackFrame> frame = stack_trace->GetFrame(isolate, 0);
v8::Local<v8::String> script_name = frame->GetScriptName();
if (script_name.IsEmpty() || !script_name->Length())
return GenerateFakeUrlFromScriptId(frame->GetScriptId());
return ToCoreString(script_name);
}
ExecutionContext* AdTracker::GetCurrentExecutionContext() {
// Determine the current ExecutionContext.
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
return context.IsEmpty() ? nullptr : ToExecutionContext(context);
}
void AdTracker::WillExecuteScript(ExecutionContext* execution_context,
const String& script_url,
int script_id) {
bool is_ad = false;
// We track scripts with no URL (i.e. dynamically inserted scripts with no
// src) by IDs instead. We also check the stack as they are executed
// immediately and should be tagged based on the script inserting them.
bool should_track_with_id =
script_url.IsEmpty() && script_id != v8::Message::kNoScriptIdInfo;
if (should_track_with_id) {
// This primarily checks if |execution_context| is a known ad context as we
// don't need to keep track of scripts in ad contexts. However, two scripts
// with identical text content can be assigned the same ID.
String fake_url = GenerateFakeUrlFromScriptId(script_id);
if (IsKnownAdScript(execution_context, fake_url)) {
is_ad = true;
} else if (IsAdScriptInStack(StackType::kBottomAndTop)) {
AppendToKnownAdScripts(*execution_context, fake_url);
is_ad = true;
}
}
if (!should_track_with_id)
is_ad = IsKnownAdScript(execution_context, script_url);
stack_frame_is_ad_.push_back(is_ad);
if (is_ad)
num_ads_in_stack_ += 1;
}
void AdTracker::DidExecuteScript() {
if (stack_frame_is_ad_.back()) {
DCHECK_LT(0u, num_ads_in_stack_);
num_ads_in_stack_ -= 1;
}
stack_frame_is_ad_.pop_back();
}
void AdTracker::Will(const probe::ExecuteScript& probe) {
WillExecuteScript(probe.context, probe.script_url, probe.script_id);
}
void AdTracker::Did(const probe::ExecuteScript& probe) {
DidExecuteScript();
}
void AdTracker::Will(const probe::CallFunction& probe) {
// Do not process nested microtasks as that might potentially lead to a
// slowdown of custom element callbacks.
if (probe.depth)
return;
v8::Local<v8::Value> resource_name =
probe.function->GetScriptOrigin().ResourceName();
String script_url;
if (!resource_name.IsEmpty()) {
v8::MaybeLocal<v8::String> resource_name_string =
resource_name->ToString(ToIsolate(local_root_)->GetCurrentContext());
// Rarely, ToString() can return an empty result, even if |resource_name|
// isn't empty (crbug.com/1086832).
if (!resource_name_string.IsEmpty())
script_url = ToCoreString(resource_name_string.ToLocalChecked());
}
WillExecuteScript(probe.context, script_url, probe.function->ScriptId());
}
void AdTracker::Did(const probe::CallFunction& probe) {
if (probe.depth)
return;
DidExecuteScript();
}
bool AdTracker::CalculateIfAdSubresource(
ExecutionContext* execution_context,
const KURL& request_url,
ResourceType resource_type,
const FetchInitiatorInfo& initiator_info,
bool known_ad) {
// Check if the document loading the resource is an ad.
const bool is_ad_execution_context =
IsKnownAdExecutionContext(execution_context);
known_ad = known_ad || is_ad_execution_context;
// We skip script checking for stylesheet-initiated resource requests as the
// stack may represent the cause of a style recalculation rather than the
// actual resources themselves. Instead, the ad bit is set according to the
// CSSParserContext when the request is made. See crbug.com/1051605.
if (initiator_info.name == fetch_initiator_type_names::kCSS ||
initiator_info.name == fetch_initiator_type_names::kUacss) {
return known_ad;
}
// Check if any executing script is an ad.
known_ad = known_ad || IsAdScriptInStack(StackType::kBottomAndTop);
// If it is a script marked as an ad and it's not in an ad context, append it
// to the known ad script set. We don't need to keep track of ad scripts in ad
// contexts, because any script executed inside an ad context is considered an
// ad script by IsKnownAdScript.
if (resource_type == ResourceType::kScript && known_ad &&
!is_ad_execution_context) {
AppendToKnownAdScripts(*execution_context, request_url.GetString());
}
return known_ad;
}
void AdTracker::DidCreateAsyncTask(probe::AsyncTaskId* task) {
DCHECK(task);
if (!async_stack_enabled_)
return;
if (IsAdScriptInStack(StackType::kBottomAndTop))
task->SetAdTask();
}
void AdTracker::DidStartAsyncTask(probe::AsyncTaskId* task) {
DCHECK(task);
if (!async_stack_enabled_)
return;
if (task->IsAdTask())
running_ad_async_tasks_ += 1;
}
void AdTracker::DidFinishAsyncTask(probe::AsyncTaskId* task) {
DCHECK(task);
if (!async_stack_enabled_)
return;
if (task->IsAdTask())
running_ad_async_tasks_ -= 1;
}
bool AdTracker::IsAdScriptInStack(StackType stack_type) {
if (num_ads_in_stack_ > 0 || running_ad_async_tasks_ > 0)
return true;
ExecutionContext* execution_context = GetCurrentExecutionContext();
if (!execution_context)
return false;
// If we're in an ad context, then no matter what the executing script is it's
// considered an ad.
if (IsKnownAdExecutionContext(execution_context))
return true;
if (stack_type == StackType::kBottomOnly)
return false;
// The stack scanned by the AdTracker contains entry points into the stack
// (e.g., when v8 is executed) but not the entire stack. For a small cost we
// can also check the top of the stack (this is much cheaper than getting the
// full stack from v8).
return IsKnownAdScriptForCheckedContext(*execution_context, String());
}
bool AdTracker::IsKnownAdScript(ExecutionContext* execution_context,
const String& url) {
if (!execution_context)
return false;
if (IsKnownAdExecutionContext(execution_context))
return true;
return IsKnownAdScriptForCheckedContext(*execution_context, url);
}
bool AdTracker::IsKnownAdScriptForCheckedContext(
ExecutionContext& execution_context,
const String& url) {
DCHECK(!IsKnownAdExecutionContext(&execution_context));
auto it = known_ad_scripts_.find(&execution_context);
if (it == known_ad_scripts_.end())
return false;
if (it->value.IsEmpty())
return false;
// Delay calling ScriptAtTopOfStack() as much as possible due to its cost.
String script_url = url.IsNull() ? ScriptAtTopOfStack() : url;
if (script_url.IsEmpty())
return false;
return it->value.Contains(script_url);
}
// This is a separate function for testing purposes.
void AdTracker::AppendToKnownAdScripts(ExecutionContext& execution_context,
const String& url) {
DCHECK(!url.IsEmpty());
auto add_result =
known_ad_scripts_.insert(&execution_context, HashSet<String>());
add_result.stored_value->value.insert(url);
}
void AdTracker::Trace(Visitor* visitor) const {
visitor->Trace(local_root_);
visitor->Trace(known_ad_scripts_);
}
} // namespace blink