blob: d477c902e346541e5dcc0a033347befd09e3db97 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/actor/aggregated_journal.h"
#include "base/memory/safe_ref.h"
#include "base/rand_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/actor/actor_logging.h"
#include "chrome/common/actor/journal_details_builder.h"
#include "chrome/common/chrome_render_frame.mojom.h"
#include "content/public/browser/document_user_data.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_frame_host_receiver_set.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_contents_user_data.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
namespace actor {
namespace {
class NonTerminatedJournalEntries
: public content::DocumentUserData<NonTerminatedJournalEntries> {
public:
~NonTerminatedJournalEntries() override {
// Terminate any entries that have not been terminated indicating
// they were ended because of disconnection.
for (auto& pending_entry : entries_) {
pending_entry.second->EndEntry(
JournalDetailsBuilder()
.Add("end_reason", "Connection Disconnected")
.Build());
}
}
void TrackEntries(base::PassKey<AggregatedJournal> pass_key,
base::SafeRef<AggregatedJournal> journal,
const std::vector<mojom::JournalEntryPtr>& entries) {
for (auto& renderer_entry : entries) {
if (renderer_entry->type == mojom::JournalEntryType::kBegin) {
entries_[renderer_entry->event] =
std::make_unique<AggregatedJournal::PendingAsyncEntry>(
pass_key, journal, renderer_entry->task_id,
renderer_entry->event, renderer_entry->track_uuid);
} else if (renderer_entry->type == mojom::JournalEntryType::kEnd) {
auto it = entries_.find(renderer_entry->event);
if (it != entries_.end()) {
it->second->mark_as_terminated();
entries_.erase(it);
}
}
}
}
private:
explicit NonTerminatedJournalEntries(content::RenderFrameHost* rfh)
: DocumentUserData(rfh) {}
friend DocumentUserData;
DOCUMENT_USER_DATA_KEY_DECL();
std::map<std::string, std::unique_ptr<AggregatedJournal::PendingAsyncEntry>>
entries_;
};
DOCUMENT_USER_DATA_KEY_IMPL(NonTerminatedJournalEntries);
class JournalObserver : public mojom::JournalClient,
public content::WebContentsUserData<JournalObserver> {
public:
JournalObserver(const JournalObserver&) = delete;
JournalObserver& operator=(const JournalObserver&) = delete;
~JournalObserver() override {}
void EnsureJournalBound(content::RenderFrameHost& render_frame_host) {
if (journal_host_receivers_.IsBound(&render_frame_host)) {
return;
}
mojo::PendingAssociatedRemote<actor::mojom::JournalClient> client;
journal_host_receivers_.Bind(&render_frame_host,
client.InitWithNewEndpointAndPassReceiver());
mojo::AssociatedRemote<chrome::mojom::ChromeRenderFrame> renderer;
render_frame_host.GetRemoteAssociatedInterfaces()->GetInterface(&renderer);
renderer->StartActorJournal(std::move(client));
}
private:
friend class content::WebContentsUserData<JournalObserver>;
explicit JournalObserver(content::WebContents* web_contents,
base::PassKey<AggregatedJournal> pass_key,
base::SafeRef<AggregatedJournal> journal)
: content::WebContentsUserData<JournalObserver>(*web_contents),
journal_host_receivers_(web_contents, this),
pass_key_(pass_key),
journal_(journal) {}
// actor::mojom::JournalClient methods.
void AddEntriesToJournal(
std::vector<mojom::JournalEntryPtr> entries) override {
NonTerminatedJournalEntries::GetOrCreateForCurrentDocument(
journal_host_receivers_.GetCurrentTargetFrame())
->TrackEntries(pass_key_, journal_, entries);
journal_->AppendJournalEntries(
*journal_host_receivers_.GetCurrentTargetFrame(), std::move(entries));
}
content::RenderFrameHostReceiverSet<mojom::JournalClient>
journal_host_receivers_;
base::PassKey<AggregatedJournal> pass_key_;
base::SafeRef<AggregatedJournal> journal_;
WEB_CONTENTS_USER_DATA_KEY_DECL();
};
WEB_CONTENTS_USER_DATA_KEY_IMPL(JournalObserver);
} // namespace
AggregatedJournal::Entry::Entry(const std::string& location,
mojom::JournalEntryPtr data_arg)
: url(location), data(std::move(data_arg)) {}
AggregatedJournal::Entry::~Entry() = default;
AggregatedJournal::AggregatedJournal() = default;
AggregatedJournal::~AggregatedJournal() = default;
AggregatedJournal::PendingAsyncEntry::PendingAsyncEntry(
base::PassKey<AggregatedJournal> pass_key,
base::SafeRef<AggregatedJournal> journal,
TaskId task_id,
std::string_view event_name,
uint64_t track_uuid)
: pass_key_(pass_key),
journal_(journal),
task_id_(task_id),
event_name_(event_name),
begin_time_(base::TimeTicks::Now()),
track_uuid_(track_uuid) {}
AggregatedJournal::PendingAsyncEntry::~PendingAsyncEntry() {
if (!terminated_) {
EndEntry({});
}
}
void AggregatedJournal::PendingAsyncEntry::EndEntry(
std::vector<mojom::JournalDetailsPtr> details) {
CHECK(!terminated_);
terminated_ = true;
ACTOR_LOG() << "End " << event_name_ << ": " << details;
journal_->AddEndEvent(pass_key_, task_id_, event_name_, track_uuid_,
std::move(details));
}
AggregatedJournal& AggregatedJournal::PendingAsyncEntry::GetJournal() {
return *journal_;
}
TaskId AggregatedJournal::PendingAsyncEntry::GetTaskId() {
return task_id_;
}
base::SafeRef<AggregatedJournal> AggregatedJournal::GetSafeRef() {
return weak_ptr_factory_.GetSafeRef();
}
uint64_t AggregatedJournal::AllocateDynamicTrackUUID() {
static uint64_t next_track_id = 1000;
return ++next_track_id;
}
std::unique_ptr<AggregatedJournal::PendingAsyncEntry>
AggregatedJournal::CreatePendingAsyncEntry(
const GURL& url,
TaskId task_id,
uint64_t track_uuid,
std::string_view event_name,
std::vector<mojom::JournalDetailsPtr> details) {
ACTOR_LOG() << "Begin " << event_name << ": " << details;
AddEntry(std::make_unique<Entry>(
url.possibly_invalid_spec(),
mojom::JournalEntry::New(mojom::JournalEntryType::kBegin, task_id,
base::Time::Now(), std::string(event_name),
track_uuid, std::move(details))));
return base::WrapUnique(new PendingAsyncEntry(
base::PassKey<AggregatedJournal>(), weak_ptr_factory_.GetSafeRef(),
task_id, event_name, track_uuid));
}
void AggregatedJournal::Log(const GURL& url,
TaskId task_id,
std::string_view event_name,
std::vector<mojom::JournalDetailsPtr> details) {
Log(url, task_id, MakeBrowserTrackUUID(task_id), event_name,
std::move(details));
}
void AggregatedJournal::Log(const GURL& url,
TaskId task_id,
uint64_t track_uuid,
std::string_view event_name,
std::vector<mojom::JournalDetailsPtr> details) {
ACTOR_LOG() << event_name << ": " << details;
AddEntry(std::make_unique<Entry>(
url.possibly_invalid_spec(),
mojom::JournalEntry::New(mojom::JournalEntryType::kInstant, task_id,
base::Time::Now(), std::string(event_name),
track_uuid, std::move(details))));
}
void AggregatedJournal::EnsureJournalBound(content::RenderFrameHost& rfh) {
auto* web_contents = content::WebContents::FromRenderFrameHost(&rfh);
CHECK(web_contents);
auto* journal_observer = JournalObserver::FromWebContents(web_contents);
if (!journal_observer) {
JournalObserver::CreateForWebContents(web_contents,
base::PassKey<AggregatedJournal>(),
weak_ptr_factory_.GetSafeRef());
journal_observer = JournalObserver::FromWebContents(web_contents);
}
journal_observer->EnsureJournalBound(rfh);
}
void AggregatedJournal::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void AggregatedJournal::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void AggregatedJournal::AppendJournalEntries(
content::RenderFrameHost& rfh,
std::vector<mojom::JournalEntryPtr> entries) {
std::string location = rfh.GetLastCommittedURL().possibly_invalid_spec();
for (auto& renderer_entry : entries) {
AddEntry(std::make_unique<Entry>(location, std::move(renderer_entry)));
}
}
void AggregatedJournal::AddEndEvent(
base::PassKey<AggregatedJournal> pass_key,
TaskId task_id,
const std::string& event_name,
uint64_t track_uuid,
std::vector<mojom::JournalDetailsPtr> details) {
AddEntry(std::make_unique<Entry>(
std::string(),
mojom::JournalEntry::New(mojom::JournalEntryType::kEnd, task_id,
base::Time::Now(), event_name, track_uuid,
std::move(details))));
}
void AggregatedJournal::LogScreenshot(const GURL& url,
TaskId task_id,
std::string_view mime_type,
base::span<const uint8_t> data) {
auto entry = std::make_unique<Entry>(
url.possibly_invalid_spec(),
mojom::JournalEntry::New(
mojom::JournalEntryType::kInstant, task_id, base::Time::Now(),
"Screenshot", MakeBrowserTrackUUID(task_id),
/*details=*/std::vector<mojom::JournalDetailsPtr>()));
entry->screenshot.emplace(data.begin(), data.end());
AddEntry(std::move(entry));
}
void AggregatedJournal::LogAnnotatedPageContent(
const GURL& url,
TaskId task_id,
base::span<const uint8_t> data) {
auto entry = std::make_unique<Entry>(
url.possibly_invalid_spec(),
mojom::JournalEntry::New(
mojom::JournalEntryType::kInstant, task_id, base::Time::Now(),
"PageContext", MakeBrowserTrackUUID(task_id),
/*details=*/std::vector<mojom::JournalDetailsPtr>()));
entry->annotated_page_content.emplace(data.begin(), data.end());
AddEntry(std::move(entry));
}
void AggregatedJournal::AddEntry(std::unique_ptr<Entry> new_entry) {
for (auto& observer : observers_) {
observer.WillAddJournalEntry(*new_entry);
}
entries_.SaveToBuffer(std::move(new_entry));
}
} // namespace actor