Verified Commit cea5e488 authored by Hercules Merscher's avatar Hercules Merscher 🌴
Browse files

feat: CoveredExperience.start

parent f3b3e975
Loading
Loading
Loading
Loading
Loading
+10 −2
Original line number Diff line number Diff line
@@ -6,7 +6,11 @@ require 'labkit/covered_experience/null'
require 'labkit/covered_experience/registry'

module Labkit
  # Module for loading and managing Covered Experiences.
  # Labkit::CoveredExperience namespace module.
  #
  # This module is responsible for managing covered experiences, which are
  # specific events or activities within the application that are measured
  # and reported for performance monitoring and analysis.
  module CoveredExperience
    class << self
      def registry
@@ -17,7 +21,7 @@ module Labkit
        @registry = nil
      end

      def [](experience_id)
      def get(experience_id)
        definition = registry[experience_id]

        if definition
@@ -27,6 +31,10 @@ module Labkit
        end
      end

      def start(experience_id, &)
        get(experience_id).start(&)
      end

      private

      def raise_or_null(experience_id)
+87 −1
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@

This module covers the definition for Covered Experiences, as described in the [blueprint](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/covered_experience_slis/#covered-experience-definition).

## Usage
## Configuration

Covered experience definitions will be lazy loaded from the default directory (`config/covered_experiences`).

@@ -32,3 +32,89 @@ https://docs.gitlab.com/development/feature_categorization/#feature-categorizati
| `sync_slow`  | A user is awaiting a synchronous response which needs to be returned before they can continue with their action, but which the user may accept a slower response | Displaying a full-text search response while displaying an amusement animation | 5s    |
| `async_fast` | An async process which may block a user from continuing with their user journey                                                                                  | MR diff update after git push                                                  | 15s   |
| `async_slow` | An async process which will not block a user and will not be immediately noticed as being slow                                                                   | Notification following an assignment                                           | 5m    |

## Usage

The `Labkit::CoveredExperience` module provides a simple API for measuring and tracking covered experiences in your application.


#### Accessing a Covered Experience

```ruby
# Get a covered experience by ID
experience = Labkit::CoveredExperience.get('merge_request_creation')
```

#### Using with a Block (Recommended)

The simplest way to use covered experiences is with a block, which automatically handles starting and completing the experience:

```ruby
Labkit::CoveredExperience.start('merge_request_creation') do |experience|
  # Your code here
  create_merge_request

  # Add checkpoints for important milestones
  experience.checkpoint

  validate_merge_request
  experience.checkpoint

  send_notifications
end
```

#### Manual Control

For more control, you can manually start, checkpoint, and complete experiences:

```ruby
experience = Labkit::CoveredExperience.get('merge_request_creation')
experience.start

# Perform some work
create_merge_request

# Mark important milestones
experience.checkpoint

# Perform more work
validate_merge_request
experience.checkpoint

# Complete the experience
experience.complete
```

### Error Handling

When using the block form, errors are automatically captured:

```ruby
Labkit::CoveredExperience.start('merge_request_creation') do |experience|
  # If this raises an exception, it will be captured automatically
  risky_operation
end
```

For manual control, you can mark errors explicitly:

```ruby
experience = Labkit::CoveredExperience.get('merge_request_creation')
experience.start

begin
  risky_operation
rescue StandardError => e
  experience.error!(e)
  raise
ensure
  experience.complete
end
```

### Error Behavior

- In `development` and `test` environments, accessing a non-existent covered experience will raise a `NotFoundError`
- In other environments, a null object is returned that safely ignores all method calls
- Attempting to checkpoint or complete an unstarted experience will raise an `UnstartedError` in `development` and `test` environments
+33 −5
Original line number Diff line number Diff line
@@ -2,8 +2,9 @@

require 'spec_helper'
require 'labkit/covered_experience'
require 'labkit/rspec/matchers/covered_experience_matchers'

RSpec.describe Labkit::CoveredExperience do
RSpec.describe Labkit::CoveredExperience, :with_metrics_config do
  include StubENV

  describe '.registry' do
@@ -22,21 +23,48 @@ RSpec.describe Labkit::CoveredExperience do
    end
  end

  describe '.[]' do
  describe '.get' do
    it 'retrieves an experience using the experience_id' do
      expect(described_class['testing_sample']).to be_a(Labkit::CoveredExperience::Experience)
      expect(described_class.get('testing_sample')).to be_a(Labkit::CoveredExperience::Experience)
    end

    it 'returns a null object if experience_id is not found' do
      expect(described_class['nonexistent']).to be(Labkit::CoveredExperience::Null.instance)
      expect(described_class.get('nonexistent')).to be(Labkit::CoveredExperience::Null.instance)
    end

    %w[test development].each do |env|
      it "raises error when RAILS_ENV is #{env}" do
        stub_env('RAILS_ENV', env)

        expect { described_class['nonexistent'] }.to raise_error(Labkit::CoveredExperience::NotFoundError, "Covered Experience nonexistent not found in the registry")
        expect { described_class.get('nonexistent') }.to raise_error(Labkit::CoveredExperience::NotFoundError, "Covered Experience nonexistent not found in the registry")
      end
    end
  end

  describe '.start' do
    context 'when block is given' do
      it 'starts and automatically ends the experience' do
        expect do |block|
          described_class.start('testing_sample', &block)
        end.to yield_with_args(Labkit::CoveredExperience::Experience)
        .and start_covered_experience(:testing_sample)
        .and complete_covered_experience(:testing_sample)
      end

      it 'captures exceptions and marks as error' do
        expect do
          described_class.start('testing_sample') { raise 'Something went wrong' }
        end.to raise_error(RuntimeError, 'Something went wrong')
        .and complete_covered_experience(:testing_sample, error: true)
      end
    end

    context 'when block is not given' do
      subject(:start_experience) { described_class.start('testing_sample') }

      it { expect(start_experience).to be_a(Labkit::CoveredExperience::Experience) }
      it { expect { start_experience }.to start_covered_experience(:testing_sample) }
      it { expect { start_experience }.not_to complete_covered_experience(:testing_sample) }
    end
  end
end