chrome/browser/metrics/critical_user_journeys/README.md
The Critical User Journey (CUJ) framework provides a structured way to measure and monitor multi-step user tasks in Chromium. While traditional UMA histograms are excellent for tracking discrete events, they often struggle to represent the flow and success rate of complex, time-dependent task sequences.
The CUJ framework addresses this by allowing developers to define a sequence of interaction steps that represent a complete user task. It is built strictly on top of the ui::InteractionSequence library, which provides the underlying logic for tracking UI elements (via ui::ElementIdentifier) and observing events. By leveraging ui::InteractionSequence, the framework can reuse the same primitives and patterns used in modern Chromium interactive UI tests (Kombucha), making it easier for developers to instrument their features with high-quality metrics.
The primary goals of this framework are:
{JourneyName}.StepReached) without requiring repetitive boilerplate.CriticalUserJourneyService, ensuring proper cleanup and resource management via KeyedService.The CUJ framework is designed around four primary components that manage the lifecycle of a journey from definition to metric logging.
The CriticalUserJourneyService is the central orchestrator of the framework. As a KeyedService owned by the Profile, it ensures that journey tracking is scoped to the correct user profile and tied to its lifecycle. Its responsibilities include:
CriticalUserJourneySession objects when a journey's starting trigger is detected.The CriticalUserJourneyRegistry serves as the single source of truth for all journey definitions. During service initialization, journeys are registered here. The registry allows the service to efficiently look up which journey should be started when a specific ui::ElementIdentifier or event is observed in the UI.
A CriticalUserJourney is a static "blueprint" for a specific user task. It is created using a Builder pattern and defines:
base::Feature. This provides a mandatory kill switch and ensures the journey name is consistently derived from the feature's string name.AddAnyOf to handle multiple valid paths at a given step.While a CriticalUserJourney defines the what, a CriticalUserJourneySession represents the now. For every active user task, a session is created to:
ui::InteractionSequence built from the journey's blueprint.The CUJ framework automatically generates and logs several UMA histograms for each registered journey. These metrics are prefixed with CriticalUserJourney.{JourneyName}., where {JourneyName} is the string name of the base::Feature associated with the journey.
{JourneyName}.StepReachedmetric_id of each step as the user successfully reaches it.{JourneyName}.StepAbortedmetric_id of the last reached step when a journey is aborted or times out.{JourneyName}.ResultCriticalUserJourneyResult enum)kCompleted (0): The user successfully reached the final step of the journey.kAborted (1): The journey was terminated before completion (e.g., the user closed the relevant UI or navigated away).kTimeout (2): The journey exceeded its defined time_out_duration at a particular step.The CUJ framework is specifically designed for tracking UI-driven sequences that depend on ui::InteractionSequence. It should not be used for simple, disconnected action logging where traditional UMA histograms or UserAction logging are more efficient.
ui::ElementIdentifier tied to some action (pressed, activated, shown / hidden, custom events).To maintain high-quality data signals, journeys should be focused on a single, well-defined user task. Overly complex journeys with excessive branching (AddAnyOf) can become difficult to analyze and may lead to "noisy" metrics. If a journey feels too large, consider whether it can be decomposed into smaller, more focused sub-journeys.
A common pitfall is defining a journey step for an element that may be destroyed or hidden before the ui::InteractionSequence can observe it.
Ensure that the elements you are tracking are persistent enough for the sequence to transition through them. If an interaction causes a UI element to be replaced (e.g., navigating to a new page), ensure the journey can account for this by updating the step to use an element that will become present in the replaced step or by listening for a custom event that you emit that is not tied to a UI element (i.e. a download starts / finishes, page transition, etc).
Every journey defined in the CUJ framework has an associated time_out_duration (either a default or one explicitly set during the journey's construction). If a user does not reach the next step in the sequence within this timeframe, the journey session will automatically transition to a kTimeout state and terminate.
Common causes for timeouts include:
A journey will transition to the kAborted state if the underlying ui::InteractionSequence is terminated before reaching the final step. This typically happens when the UI elements being tracked are destroyed or become hidden unexpectedly.
Common scenarios leading to aborted journeys:
ui::ElementIdentifier is closed or destroyed while the journey is in progress, the sequence will abort.ui::InteractionSequence steps may point to an element that is in a bubble which auto-dismisses on loss of focus / when it is no longer the active window causing the journey to abort early.Follow these steps to instrument a new Critical User Journey in Chromium.
The CUJ framework relies on ui::ElementIdentifier to track UI elements. If your feature's UI components don't already have identifiers, define them in your controller or feature class using DECLARE/DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE() or in chrome/browser/ui/browser_element_identifiers.h if it is a top level UI element.
See ui/base/interaction/element_identifier.h for more information.
Ensure these identifiers are assigned to the actual UI views using views::View::SetProperty(views::kElementIdentifierKey, kYourFeatureMainButtonId).
Every journey requires a dedicated base::Feature to serve as its identity and kill switch. Define this in chrome/browser/metrics/critical_user_journeys/features.h:
BASE_DECLARE_FEATURE(kMyFeatureJourney);
And in chrome/browser/metrics/critical_user_journeys/features.cc:
BASE_FEATURE(kMyFeatureJourney, "MyFeatureJourney", base::FEATURE_ENABLED_BY_DEFAULT);
Define your journey using the CriticalUserJourney::Builder. You must pass a pointer to your journey's base::Feature to the constructor.
std::unique_ptr<metrics::CriticalUserJourney> CreateMyFeatureJourney() {
return metrics::CriticalUserJourney::Builder(&kMyFeatureJourney)
.AddStep(kYourFeatureMainButtonId, ui::InteractionSequence::StepType::kActivated, 1)
.AddStep(kYourFeatureDialogId, ui::InteractionSequence::StepType::kShown, 2)
// Add more steps as needed...
.Build();
}
Register your journey via CriticalUserJourneyRegistry::AddJourneys. Doing so allows all external dependencies to bubble into a single location. The service will automatically skip registration if the journey's feature flag is disabled.
void CriticalUserJourneyRegistry::AddJourneys() {
AddJourney(CreateMyFeatureJourney());
// ... other registrations
}
A journey starts when its first defined step is observed by the framework. Ensure that the initial ui::ElementIdentifier or event is correctly triggered by user interaction. The CriticalUserJourneyService automatically listens for these starting triggers once the journey is registered.
You must define the steps in tools/metrics/histograms/metadata/critical_user_journeys/enums.xml in order to have proper labels when viewing the metrics.
<enum name="MyFeatureJourneySteps">
<int value="1" label="Press the first button"/>
<int value="2" label="Click a different button"/>
<int value="3" label="Wait for dialog to show"/>
</enum>
Finally, you must register the generated histograms in tools/metrics/histograms/metadata/critical_user_journeys/histograms.xml.
<histogram name="CriticalUserJourney.MyFeatureJourney.{StepAction}" enums="MyFeatureJourneySteps" expires_after="2026-03-31">
<owner>[email protected]</owner>
<summary>
The steps reached in the {JourneyName} critical user journey.
</summary>
<token key="StepAction" variants="CriticalUserJourneyStepAction"/>
</histogram>
And then add your journey to the results metric to track the final outcomes of the journeys. Use the <variant> tag to efficiently define the metrics for your journey.
<histogram name="CriticalUserJourney.{JourneyName}.Result" enum="CriticalUserJourneyResult" expires_after="2026-03-31">
<owner>[email protected]</owner>
<summary>
Records the final outcome (Completed, Aborted, or Timed out) of the
{JourneyName} critical user journey.
</summary>
<token key="JourneyName">
<variant name="OtherFeatureJourney"/>
<variant name="AnotherFeatureJourney"/>
<variant name="MyFeatureJourney"/> <!-- Add it here! -->
</token>
</histogram>
Please consider using IfThisThenThat Lint to keep your journey and enum in sync! Doing so prevents misleading information in the metrics during metric analysis.
See documentation for more details.
A simple linear journey tracks a fixed sequence of user actions.
std::unique_ptr<metrics::CriticalUserJourney> CreateSettingsChangeJourney() {
return metrics::CriticalUserJourney::Builder(&kSettingsChangeJourneyFeature)
// Step 1: User opens the main menu.
.AddStep(kMainMenuButtonId, ui::InteractionSequence::StepType::kActivated, 1)
// Step 2: User navigates to the Settings page.
.AddStep(kSettingsMenuEntryId, ui::InteractionSequence::StepType::kActivated, 2)
// Step 3: The Settings dialog is shown to the user.
.AddStep(kSettingsDialogId, ui::InteractionSequence::StepType::kShown, 3)
// Step 4: User clicks "Save" to commit their changes.
.AddStep(kSettingsSaveButtonId, ui::InteractionSequence::StepType::kActivated, 4)
.Build();
}
The AddAnyOf method allows a journey to proceed if any one of several defined paths is taken.
std::unique_ptr<metrics::CriticalUserJourney> CreateMultiOptionTaskJourney() {
return metrics::CriticalUserJourney::Builder(&kMultiOptionTaskJourneyFeature)
// Start by opening the selection interface.
.AddStep(kOpenSelectorButtonId, ui::InteractionSequence::StepType::kActivated, 1)
// The user can choose between two different options to proceed.
.AddAnyOf({
metrics::Branch(kOptionAButtonId, ui::InteractionSequence::StepType::kActivated, 2),
metrics::Branch(kOptionBButtonId, ui::InteractionSequence::StepType::kActivated, 3)
})
// Final confirmation step regardless of which option was chosen.
.AddStep(kConfirmationDialogId, ui::InteractionSequence::StepType::kShown, 4)
.Build();
}
The CUJ framework supports triggering a Happiness Tracking Survey (HaTS) automatically upon the successful completion of a journey. This allows for gathering qualitative user feedback immediately after they have finished a key task.
To enable HaTS integration, you must first define your survey feature and trigger string, register it with the HaTS service, and then use the LaunchHatsSurveyOnCompletion method in your journey builder.
Define a feature flag and a trigger string for your survey. It is recommended to co-locate these with your journey definitions (e.g., in chrome/browser/metrics/critical_user_journeys/features.h and .cc).
BASE_DECLARE_FEATURE(kHappinessTrackingSurveysForMyFeature);
extern const char kHatsSurveyTriggerMyFeature[];
You must register your survey configuration in chrome/browser/ui/hats/survey_config.cc within the GetAllSurveyConfigs() function. Due to HaTS architectural constraints, this must be hardcoded here to ensure HaTS owners can review the configuration.
#include "chrome/browser/metrics/critical_user_journeys/features.h"
// In GetAllSurveyConfigs():
survey_configs.emplace_back(
&metrics::kHappinessTrackingSurveysForMyFeature,
metrics::kHatsSurveyTriggerMyFeature);
Use the LaunchHatsSurveyOnCompletion method in the CriticalUserJourney::Builder. Provide a metrics::HatsParams struct containing your trigger string and any optional product-specific data.
#include "chrome/browser/metrics/critical_user_journeys/critical_user_journey.h"
#include "chrome/browser/metrics/critical_user_journeys/features.h"
std::unique_ptr<metrics::CriticalUserJourney> CreateJourneyWithHats() {
metrics::HatsParams hats_params;
hats_params.trigger = metrics::kHatsSurveyTriggerMyFeature;
// Optional: Provide product-specific data to be sent with the survey response.
hats_params.product_specific_string_data = {{"feature_version", "1.0"}};
return metrics::CriticalUserJourney::Builder(&kFeatureJourneyWithHatsFeature)
.AddStep(kFeatureStartButtonId, ui::InteractionSequence::StepType::kActivated, 1)
.AddStep(kFeatureCompleteDialogId, ui::InteractionSequence::StepType::kShown, 2)
.LaunchHatsSurveyOnCompletion(std::move(hats_params))
.Build();
}