Verified Commit c72520f2 authored by Hercules Merscher's avatar Hercules Merscher 🌴 Committed by GitLab
Browse files

Merge branch 'feat/backfill-ux-sli-start-time' into 'master'

feat: User Experience SLI observed

See merge request !263

Merged-by: Hercules Merscher's avatarHercules Merscher <hmerscher@gitlab.com>
Approved-by: Bob Van Landuyt's avatarBob Van Landuyt <bob@gitlab.com>
Reviewed-by: Bob Van Landuyt's avatarBob Van Landuyt <bob@gitlab.com>
Reviewed-by: default avatarGitLab Duo <gitlab-duo@gitlab.com>
parents 18414ba6 7f550e5f
Loading
Loading
Loading
Loading
Loading
+0 −9
Original line number Diff line number Diff line
@@ -74,15 +74,6 @@ expect { subject }.to complete_user_experience('rails_request', error: true, suc
expect { subject }.not_to complete_user_experience('rails_request')
```

### Legacy Matchers (Backward Compatibility)

For backward compatibility, the following legacy matchers are still available but deprecated. Please migrate to the new `*_user_experience` matchers above.

- `start_covered_experience`
- `checkpoint_covered_experience`
- `resume_covered_experience`
- `complete_covered_experience`

## Example Usage

### In your spec_helper.rb or rails_helper.rb:
+53 −0
Original line number Diff line number Diff line
@@ -203,6 +203,59 @@ RSpec::Matchers.define :complete_user_experience do |user_experience_id, error:
  end
end

# Matcher for verifying UserExperience observed metrics instrumentation.
#
# Usage:
#   expect { subject }.to observed_user_experience('rails_request')
#
# This matcher verifies that the following metrics are incremented with specific labels:
# - gitlab_user_experience_checkpoint_total (with checkpoint=start)
# - gitlab_user_experience_checkpoint_total (with checkpoint=end)
# - gitlab_user_experience_total (with error=false)
# - gitlab_user_experience_apdex_total (with success=true)
#
# Parameters:
# - user_experience_id: Required. The ID of the user experience (e.g., 'rails_request')
# - error: Optional. The expected error flag for gitlab_user_experience_total (false by default)
# - success: Optional. The expected success flag for gitlab_user_experience_apdex_total (true by default)
RSpec::Matchers.define :observed_user_experience do |user_experience_id, error: false, success: true|
  include Labkit::RSpec::Matchers::UserExperience

  description { "observe user experience '#{user_experience_id}'" }
  supports_block_expectations

  match do |actual|
    labels = attributes(user_experience_id)

    start_before = checkpoint_counter&.get(labels.merge(checkpoint: "start")).to_i
    end_before = checkpoint_counter&.get(labels.merge(checkpoint: "end")).to_i
    total_before = total_counter&.get(labels.merge(error: error)).to_i
    apdex_before = apdex_counter&.get(labels.merge(success: success)).to_i

    actual.call

    start_after = checkpoint_counter&.get(labels.merge(checkpoint: "start")).to_i
    end_after = checkpoint_counter&.get(labels.merge(checkpoint: "end")).to_i
    total_after = total_counter&.get(labels.merge(error: error)).to_i
    apdex_after = apdex_counter&.get(labels.merge(success: success)).to_i

    @start_change = start_after - start_before
    @end_change = end_after - end_before
    @total_change = total_after - total_before
    @apdex_change = apdex_after - apdex_before

    @start_change == 1 && @end_change == 1 && @total_change == 1 && @apdex_change == (error ? 0 : 1)
  end

  failure_message do
    "Failed to observe user experience '#{user_experience_id}':\n" \
      "expected checkpoint='start' counter to increase by 1, but increased by #{@start_change}\n" \
      "expected checkpoint='end' counter to increase by 1, but increased by #{@end_change}\n" \
      "expected total='error: #{error}' counter to increase by 1, but increased by #{@total_change}\n" \
      "expected apdex='success: #{success}' counter to increase by #{error ? 0 : 1}, but increased by #{@apdex_change}"
  end
end

# Backward compatibility matchers for CoveredExperience
RSpec::Matchers.alias_matcher :start_covered_experience, :start_user_experience
RSpec::Matchers.alias_matcher :checkpoint_covered_experience, :checkpoint_user_experience
+15 −3
Original line number Diff line number Diff line
@@ -77,7 +77,7 @@ module Labkit
        reset_configuration
      end

      # Retrieves a covered experience using the experience_id.
      # Retrieves a user experience using the experience_id.
      # It retrieves from the current context when available,
      # otherwise it instantiates a new experience with the definition
      # from the registry.
@@ -88,7 +88,7 @@ module Labkit
        find_current(experience_id) || raise_or_null(experience_id)
      end

      # Starts a covered experience using the experience_id.
      # Starts a user experience using the experience_id.
      #
      # @param experience_id [String, Symbol] The ID of the experience to start.
      # @param extra [Hash] Additional data to include in the log event.
@@ -97,7 +97,19 @@ module Labkit
        get(experience_id).start(**extra, &)
      end

      # Resumes a covered experience using the experience_id.
      # Records a past user experience by its duration atomically.
      #
      # @param experience_id [String, Symbol] The ID of the experience.
      # @param start_time [Time] The time when the experience started.
      # @param extra [Hash] Additional data to include in the log events.
      # @return [Experience, Null] The observed experience or a Null object if not found (in production/staging).
      def observed(experience_id, start_time:, **extra)
        definition = registry[experience_id]
        experience = definition ? Experience.new(definition) : raise_or_null(experience_id)
        experience.observed(start_time: start_time, **extra)
      end

      # Resumes a user experience using the experience_id.
      #
      # @param experience_id [String, Symbol] The ID of the experience to resume.
      # @return [Experience, Null] The started experience or a Null object if not found (in production/staging).
+20 −0
Original line number Diff line number Diff line
@@ -161,6 +161,26 @@ experience.checkpoint
experience.complete
```

#### Observing a Past Experience

When an action already happened and you want to record its duration retroactively, use `observed`. It fires the start and end metrics without registering in the active context:

```ruby
Labkit::UserExperienceSli.observed('merge_request_creation', start_time: start_time_of_past_action)
```

You can also signal that an error occurred during the action:

```ruby
Labkit::UserExperienceSli.observed('merge_request_creation', start_time: start_time_of_past_action, error: true)
```

Extra labels are forwarded to both the start and end log events:

```ruby
Labkit::UserExperienceSli.observed('merge_request_creation', start_time: start_time_of_past_action, worker: 'MyWorker')
```

#### Resuming Experiences

You can resume a user experience SLI that was previously started and stored in the context. This is useful for distributed operations or when work spans multiple processes.
+22 −0
Original line number Diff line number Diff line
@@ -135,6 +135,28 @@ module Labkit
        self
      end

      # Records a past User Experience by its duration.
      #
      # @param start_time [Time] The time when the experience started.
      # @param error [Boolean] Whether the experience ended in an error.
      # @param extra [Hash] Additional data to include in the log events.
      # @return [self]
      def observed(start_time:, error: false, **extra)
        @start_time = start_time.utc
        @end_time = Time.now.utc
        error!("observed_error") if error

        checkpoint_counter.increment(checkpoint: "start", **base_labels)
        log_event("start", **extra)

        checkpoint_counter.increment(checkpoint: "end", **base_labels)
        total_counter.increment(error: has_error?, **base_labels)
        apdex_counter.increment(success: apdex_success?, **base_labels) unless has_error?
        log_event("end", **extra)

        self
      end

      # Marks the experience as failed with an error
      #
      # @param error [StandardError, String] The error that caused the experience to fail.
Loading