Back to Chromium

Critical User Journeys (CUJ) Framework

chrome/browser/metrics/critical_user_journeys/README.md

149.0.7827.217.9 KB
Original Source

Critical User Journeys (CUJ) Framework

Introduction

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:

  1. Observability: To provide clear signals on where users drop off in a multi-step task.
  2. Automation: To automatically log completion, failure, and step-level metrics (e.g., {JourneyName}.StepReached) without requiring repetitive boilerplate.
  3. Lifecycle Management: To handle the registration and tracking of journeys through a unified CriticalUserJourneyService, ensuring proper cleanup and resource management via KeyedService.

System Architecture

The CUJ framework is designed around four primary components that manage the lifecycle of a journey from definition to metric logging.

1. CriticalUserJourneyService (KeyedService)

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:

  • Initialization: Bootstrapping the registry and subscribing to the initial triggers for all registered journeys.
  • Session Management: Instantiating and owning CriticalUserJourneySession objects when a journey's starting trigger is detected.
  • Cleanup: Ensuring active sessions are terminated and resources are freed when the profile is destroyed.

2. CriticalUserJourneyRegistry

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.

3. CriticalUserJourney (The Definition)

A CriticalUserJourney is a static "blueprint" for a specific user task. It is created using a Builder pattern and defines:

  • Feature Binding: Every journey must be associated with a base::Feature. This provides a mandatory kill switch and ensures the journey name is consistently derived from the feature's string name.
  • Steps: The sequence of UI interactions (elements, events, types) that constitute the journey.
  • Metadata: Optional integration points like Happiness Tracking Surveys (HaTS).
  • Branching Logic: Complex journeys can use AddAnyOf to handle multiple valid paths at a given step.

4. CriticalUserJourneySession (The Active Instance)

While a CriticalUserJourney defines the what, a CriticalUserJourneySession represents the now. For every active user task, a session is created to:

  • Gated Execution: The session only runs if the journey's associated feature flag is enabled.
  • Track Progress: It encapsulates a ui::InteractionSequence built from the journey's blueprint.
  • Handle Timeouts: It manages timers to ensure that stale or abandoned journeys do not leak resources or skew metrics.
  • Log Results: Upon completion or failure, the session reports the outcome (Succeeded, Aborted, or Timed Out) back to the service for final metric recording.

Automated Histogram Logging

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}.StepReached

  • Type: Sparse Histogram
  • Description: Logs the metric_id of each step as the user successfully reaches it.
  • Usage: This histogram provides a "funnel" view of the journey, allowing developers to see how many users progress through each stage of the task.

{JourneyName}.StepAborted

  • Type: Sparse Histogram
  • Description: Logs the metric_id of the last reached step when a journey is aborted or times out.
  • Usage: This is critical for identifying exactly where users are dropping off or encountering friction in the journey.

{JourneyName}.Result

  • Type: Enumerated Histogram (CriticalUserJourneyResult enum)
  • Description: Logs the final outcome of the journey session.
  • Values:
    • 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.

Best Practices

UI-Driven Sequences vs. Simple Action Logging

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.

  • Use the CUJ Framework when:
    • The user task involves a sequence of specific UI interactions (e.g., opening a menu, then selecting a specific sub-item, then interacting with the resulting dialog).
    • The task has a clear "happy path" and defined success/failure states.
    • You need to measure the success rate and identifying specific drop-off points in a multi-step process.
    • Each step in the journey can be identified by an ui::ElementIdentifier tied to some action (pressed, activated, shown / hidden, custom events).
  • Use Simple Action Logging (UMA/UserActions) when:
    • You only need to track a single, discrete user action (e.g., "User clicked the 'Settings' button").
    • The events are independent and can occur in any order without a shared task context.
    • There is no requirement to track the temporal or sequential relationship between multiple UI states.

Keep Journeys Concise

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.

UI Element Persistence and Visibility

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).

Troubleshooting

Timeouts

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:

  • User Inactivity: The user starts a task but stops or switches to another application before completing it.
  • Unexpected UI States: If a UI element or event that the journey expects never appears, the session will eventually time out. This often indicates a bug in the feature's UI logic or a mismatch between the journey's definition and the actual implementation.
  • Performance Issues: Significant delays in UI rendering or event processing can cause the journey to exceed its timeout duration even if the user is actively attempting to complete the task.

UI State Mismatches and Aborted Journeys

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:

  • Element Destruction: If a UI component (e.g., a dialog or menu) that contains a tracked ui::ElementIdentifier is closed or destroyed while the journey is in progress, the sequence will abort.
  • Navigation Events: Navigating away from a page or closing a tab that is part of the journey's context will cause any active sessions associated with that context to terminate.
  • Focus Requirements: Some 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.

Step-by-Step Implementation Guide

Follow these steps to instrument a new Critical User Journey in Chromium.

1. Define UI Element Identifiers

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).

2. Define the Journey Feature Flag

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:

cpp
BASE_DECLARE_FEATURE(kMyFeatureJourney);

And in chrome/browser/metrics/critical_user_journeys/features.cc:

cpp
BASE_FEATURE(kMyFeatureJourney, "MyFeatureJourney", base::FEATURE_ENABLED_BY_DEFAULT);

3. Create the Journey Definition

Define your journey using the CriticalUserJourney::Builder. You must pass a pointer to your journey's base::Feature to the constructor.

cpp
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();
}

4. Register the Journey

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.

cpp
void CriticalUserJourneyRegistry::AddJourneys() {
  AddJourney(CreateMyFeatureJourney());
  // ... other registrations
}

5. Trigger the Journey

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.

6. Register Enums / Histograms in XML

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.

xml
<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.

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.

xml
<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.

Examples

Linear Journey

A simple linear journey tracks a fixed sequence of user actions.

cpp
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();
}

Complex Journey with Branching

The AddAnyOf method allows a journey to proceed if any one of several defined paths is taken.

cpp
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();
}

Happiness Tracking Surveys (HaTS) Integration

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.

1. Define the Feature and Trigger String

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).

cpp
BASE_DECLARE_FEATURE(kHappinessTrackingSurveysForMyFeature);
extern const char kHatsSurveyTriggerMyFeature[];

2. Register the Survey Config

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.

cpp
#include "chrome/browser/metrics/critical_user_journeys/features.h"

// In GetAllSurveyConfigs():
survey_configs.emplace_back(
    &metrics::kHappinessTrackingSurveysForMyFeature,
    metrics::kHatsSurveyTriggerMyFeature);

3. Add to the Journey Builder

Use the LaunchHatsSurveyOnCompletion method in the CriticalUserJourney::Builder. Provide a metrics::HatsParams struct containing your trigger string and any optional product-specific data.

cpp
#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();
}