0

Add Synthetic Field Trial TS API

Bug: 394958637
Change-Id: I63dba2ef13866721e93b0a55c9eef96dda990184
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6303005
Reviewed-by: Erik Chen <erikchen@chromium.org>
Commit-Queue: Nick Birnie <birnie@google.com>
Cr-Commit-Position: refs/heads/main@{#1427373}
This commit is contained in:
Nick Birnie
2025-03-03 14:04:15 -08:00
committed by Chromium LUCI CQ
parent ea777fea6d
commit 4d9cf6063c
14 changed files with 246 additions and 0 deletions

@ -25,6 +25,7 @@ source_set("glic") {
"glic_pref_names.h",
"glic_profile_manager.h",
"glic_settings_util.h",
"glic_synthetic_trial_manager.h",
"glic_tab_data.h",
"glic_tab_indicator_helper.h",
"glic_ui.h",
@ -47,9 +48,11 @@ source_set("glic") {
"//chrome/browser/ui/browser_window:browser_window",
"//chrome/browser/ui/tabs:tab_strip_model_observer",
"//components/favicon/core:core",
"//components/metrics:metrics",
"//components/optimization_guide/content/browser:browser",
"//components/prefs",
"//components/signin/public/identity_manager:identity_manager",
"//components/variations:variations",
"//content/public/browser",
"//pdf/mojom:mojom",
"//ui/views:views",
@ -96,6 +99,7 @@ source_set("impl") {
"glic_screenshot_capturer.cc",
"glic_screenshot_capturer.h",
"glic_settings_util.cc",
"glic_synthetic_trial_manager.cc",
"glic_tab_data.cc",
"glic_tab_indicator_helper.cc",
"glic_ui.cc",

@ -361,6 +361,13 @@ interface WebClientHandler {
// Scrolls to and (optionally) highlights content specified by `params`.
// Returns an error reason if the scroll fails, or null otherwise.
ScrollTo(ScrollToParams params) => (ScrollToErrorReason? error_reason);
// Enrolls Chrome in the synthetic experiment group specified by
// trial_name.group_name. Enrollment will start when the API is called and end
// when Chrome closes.
SetSyntheticExperimentState(string trial_name,
string group_name);
};
// State of the glic panel.

@ -15,6 +15,7 @@
#include "chrome/browser/glic/glic_enums.h"
#include "chrome/browser/glic/glic_focused_tab_manager.h"
#include "chrome/browser/glic/glic_page_handler.h"
#include "chrome/browser/glic/glic_synthetic_trial_manager.h"
#include "components/keyed_service/core/keyed_service.h"
class BrowserWindowInterface;
@ -95,6 +96,11 @@ class GlicKeyedService : public KeyedService {
void SetContextAccessIndicator(bool show);
void NotifyWindowIntentToShow();
// Accessor for the GlicSyntheticTrialManager singleton. This exists as a
// singleton so that it may be shared across multiple profiles.
raw_ptr<GlicSyntheticTrialManager> synthetic_trial_manager() {
return GlicSyntheticTrialManager::GetInstance();
}
// Callback for changes to focused tab data.
using FocusedTabChangedCallback =
base::RepeatingCallback<void(FocusedTabData)>;

@ -25,6 +25,7 @@
#include "chrome/browser/glic/glic_keyed_service_factory.h"
#include "chrome/browser/glic/glic_metrics.h"
#include "chrome/browser/glic/glic_pref_names.h"
#include "chrome/browser/glic/glic_synthetic_trial_manager.h"
#include "chrome/browser/glic/glic_tab_data.h"
#include "chrome/browser/glic/glic_web_client_access.h"
#include "chrome/browser/glic/glic_window_controller.h"
@ -407,6 +408,12 @@ class GlicWebClientHandler : public glic::mojom::WebClientHandler,
annotation_manager_->ScrollTo(std::move(params), std::move(callback));
}
void SetSyntheticExperimentState(const std::string& trial_name,
const std::string& group_name) override {
glic_service_->synthetic_trial_manager()->SetSyntheticExperimentState(
trial_name, group_name);
}
// GlicWindowController::StateObserver implementation.
void PanelStateChanged(const glic::mojom::PanelState& panel_state,
Browser* attached_browser) override {

@ -0,0 +1,96 @@
// 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/glic/glic_synthetic_trial_manager.h"
#include <algorithm>
#include <cstddef>
#include <ranges>
#include <string_view>
#include <utility>
#include <vector>
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/singleton.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
#include "components/metrics/metrics_service.h"
#include "components/variations/synthetic_trial_registry.h"
namespace glic {
// static
GlicSyntheticTrialManager* GlicSyntheticTrialManager::GetInstance() {
return base::Singleton<GlicSyntheticTrialManager>::get();
}
GlicSyntheticTrialManager::GlicSyntheticTrialManager() {
metrics::MetricsService* metrics_service =
g_browser_process->metrics_service();
if (metrics_service) {
metrics_service->AddLogsObserver(this);
}
}
GlicSyntheticTrialManager::~GlicSyntheticTrialManager() {
metrics::MetricsService* metrics_service =
g_browser_process->metrics_service();
if (metrics_service) {
metrics_service->RemoveLogsObserver(this);
}
}
void GlicSyntheticTrialManager::SetSyntheticExperimentState(
const std::string& trial_name,
const std::string& group_name) {
// If already registered discard the logs if in a different group. This
// avoids combining logs from multiple groups from different profiles.
bool conflicting_group_registered =
synthetic_field_trial_groups_.count(trial_name) > 0 &&
synthetic_field_trial_groups_[trial_name] != group_name;
if (conflicting_group_registered) {
if (synthetic_field_trial_groups_[trial_name] != "MultiProfileDetected") {
base::UmaHistogramBoolean(
"Glic.ChromeClient.MultiProfileSyntheticTrialConflictDetected",
conflicting_group_registered);
}
ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
trial_name, "MultiProfileDetected",
variations::SyntheticTrialAnnotationMode::kCurrentLog);
synthetic_field_trial_groups_[trial_name] = "MultiProfileDetected";
// After contaminated logfile is uploaded we store the the most recently
// requested group to register the next created log file. If TrialA.Group1
// is reported then TrialA.Group2, then TrialA.Group3, then the next log cut
// will register TrialA.Group3.
staged_synthetic_field_trial_groups_[trial_name] = group_name;
} else {
ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
trial_name, group_name,
variations::SyntheticTrialAnnotationMode::kCurrentLog);
synthetic_field_trial_groups_[trial_name] = group_name;
}
}
void GlicSyntheticTrialManager::OnLogEvent(
metrics::MetricsLogsEventManager::LogEvent event,
std::string_view log_hash,
std::string_view message) {
if (event == metrics::MetricsLogsEventManager::LogEvent::kLogCreated &&
!staged_synthetic_field_trial_groups_.empty()) {
// Remove all elements from synthetic_field_trial_groups_ that have
// entries in staged_synthetic_field_trial_groups_ with the same
// trial_name.
for (const auto& [trial, group] : staged_synthetic_field_trial_groups_) {
ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
trial, group, variations::SyntheticTrialAnnotationMode::kCurrentLog);
synthetic_field_trial_groups_[trial] = group;
}
staged_synthetic_field_trial_groups_ = {};
}
}
} // namespace glic

@ -0,0 +1,57 @@
// 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.
#ifndef CHROME_BROWSER_GLIC_GLIC_SYNTHETIC_TRIAL_MANAGER_H_
#define CHROME_BROWSER_GLIC_GLIC_SYNTHETIC_TRIAL_MANAGER_H_
#include <string>
#include <vector>
#include "base/functional/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "components/metrics/metrics_logs_event_manager.h"
#include "components/metrics/metrics_service.h"
#include "components/variations/synthetic_trial_registry.h"
namespace glic {
class GlicSyntheticTrialManager : metrics::MetricsLogsEventManager::Observer {
public:
static GlicSyntheticTrialManager* GetInstance();
GlicSyntheticTrialManager();
GlicSyntheticTrialManager(const GlicSyntheticTrialManager&) = delete;
GlicSyntheticTrialManager& operator=(const GlicSyntheticTrialManager&) =
delete;
~GlicSyntheticTrialManager() override;
// Used by the web client to enroll Chrome in the specified synthetic trial
// group. If a conflicting group is already registered by another profile
// than we instead register into a new group called `MultiProfileDetected` to
// indicate the log file is corrupted. At this point we stage the requested
// group for enrollment when the next log file is created.
void SetSyntheticExperimentState(const std::string& trial_name,
const std::string& group_name);
// metrics::MetricsLogsEventManager::Observer:
void OnLogEvent(metrics::MetricsLogsEventManager::LogEvent event,
std::string_view log_hash,
std::string_view message) override;
void OnLogCreated(
std::string_view log_hash,
std::string_view log_data,
std::string_view log_timestamp,
metrics::MetricsLogsEventManager::CreateReason reason) override {}
private:
std::map<std::string, std::string> synthetic_field_trial_groups_;
std::map<std::string, std::string> staged_synthetic_field_trial_groups_;
base::WeakPtrFactory<GlicSyntheticTrialManager> weak_ptr_factory_{this};
};
} // namespace glic
#endif // CHROME_BROWSER_GLIC_GLIC_SYNTHETIC_TRIAL_MANAGER_H_

@ -12,10 +12,15 @@
#include "base/gtest_prod_util.h"
#include "build/build_config.h"
#include "chrome/browser/metrics/metrics_reporting_state.h"
#include "chrome/common/buildflags.h"
#include "components/metrics/metrics_service_accessor.h"
#include "components/variations/synthetic_trials.h"
#include "ppapi/buildflags/buildflags.h"
#if BUILDFLAG(ENABLE_GLIC)
#include "chrome/browser/glic/glic_synthetic_trial_manager.h"
#endif
#if BUILDFLAG(ENABLE_PPAPI)
#include "chrome/common/ppapi_metrics.mojom.h"
#endif
@ -175,6 +180,9 @@ class ChromeMetricsServiceAccessor : public metrics::MetricsServiceAccessor {
friend class ChromeBrowserMainExtraPartsGpu;
friend class Browser;
friend class BrowserProcessImpl;
#if BUILDFLAG(ENABLE_GLIC)
friend class glic::GlicSyntheticTrialManager;
#endif
friend class OptimizationGuideKeyedService;
friend class WebUITabStripFieldTrial;
friend class feed::FeedServiceDelegateImpl;

@ -334,6 +334,13 @@ export declare interface GlicBrowserHost {
* @throws {ScrollToError} on failure.
*/
scrollTo?(params: ScrollToParams): Promise<void>;
/**
* Enrolls the Chrome client in the synthetic experiment group specified by
* trial_name.group_name. Enrollment will only start when the API is called
* and end when Chrome closes.
*/
setSyntheticExperimentState?(trialName: string, groupName: string): void;
}
/** Holds optional parameters for `GlicBrowserHost#resizeWindow`. */

@ -372,6 +372,11 @@ class GlicBrowserHostImpl implements GlicBrowserHost {
scrollTo(params: ScrollToParams): Promise<void> {
return this.sender.requestWithResponse('glicBrowserScrollTo', {params});
}
setSyntheticExperimentState(trialName: string, groupName: string): void {
this.sender.requestNoResponse(
'glicBrowserSetSyntheticExperimentState', {trialName, groupName});
}
}
class GlicBrowserHostMetricsImpl implements GlicBrowserHostMetrics {

@ -477,6 +477,14 @@ class HostMessageHandler implements HostMessageHandlerInterface {
}
return {};
}
glicBrowserSetSyntheticExperimentState(request: {
trialName: string,
groupName: string,
}) {
return this.handler.setSyntheticExperimentState(
request.trialName, request.groupName);
}
}
export class GlicApiHost implements PostMessageRequestHandler {

@ -142,6 +142,12 @@ export declare interface HostRequestTypes {
glicBrowserScrollTo: {
request: {params: ScrollToParams},
};
glicBrowserSetSyntheticExperimentState: {
request: {
trialName: string,
groupName: string,
},
};
}
// Types of requests to the GlicWebClient.

@ -277,6 +277,15 @@ found in the LICENSE file.
</div>
<button id="scrollToBn" style="margin-top: 12px;">Scroll!</button>
</div>
<div class="section">
<h1>Set Synthetic Experiment</h1>
<label for="trialName">Trial Name:</label>
<input type="text" id="trialName" name="trialName"><br><br>
<label for="groupName">Group Name:</label>
<input type="text" id="groupName" name="groupName"><br><br>
<button id="setExperiment">Set Experiment</button>
<span id="setExperimentStatus"></span>
</div>
<br />
<div id="status"></div>
</div>

@ -70,6 +70,10 @@ interface PageElementTypes {
fileDropList: HTMLDivElement;
showDirectoryPicker: HTMLButtonElement;
failInitializationCheckbox: HTMLInputElement;
setExperiment: HTMLButtonElement;
trialName: HTMLInputElement;
groupName: HTMLInputElement;
setExperimentStatus: HTMLSpanElement;
}
const $: PageElementTypes = new Proxy({}, {
@ -638,6 +642,14 @@ window.addEventListener('load', () => {
$.desktopScreenshotErrorReason!.innerText = `Caught error: ${error}`;
}
});
$.setExperiment.addEventListener('click', async () => {
const trialName = $.trialName.value;
const groupName = $.groupName.value;
$.setExperimentStatus!.innerText +=
`\nSetting experiment: ${trialName} ${groupName}`;
await getBrowser()!.setSyntheticExperimentState!(trialName, groupName);
$.setExperimentStatus!.innerText += '\nExperiment State Set.';
});
});
function readStream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {

@ -32,6 +32,20 @@ chromium-metrics-reviews@google.com.
</summary>
</histogram>
<histogram name="Glic.ChromeClient.MultiProfileSyntheticTrialConflictDetected"
enum="BooleanEnabled" expires_after="2026-01-15">
<owner>birnie@chromium.org</owner>
<owner>carlosk@chromium.org</owner>
<summary>
Recorded when different Chrome profiles using glic attempt to register
conflicting synthetic field trial groups in the same log file. This should
only happen on multi profile installs that use glic on different profiles in
quick succession. When analyzing metrics from a synthetic field trial this
histogram will allow us to create custom queries to filter out users who may
have contaminated logs from multiple profiles.
</summary>
</histogram>
<histogram name="Glic.EntryPoint.Impression" enum="GlicEntryPointImpression"
expires_after="2026-01-15">
<owner>erikchen@chromium.org</owner>