From 04721d441a2f1389663cf690b70b45510a39e9e1 Mon Sep 17 00:00:00 2001 From: Omkar Moghe Date: Wed, 15 May 2024 16:20:34 -0700 Subject: [PATCH] stuff --- Gemfile.lock | 2 +- README.md | 18 +++++++++++++++++- lib/lab_coat/experiment.rb | 29 ++++++++++++++--------------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 05238c6..5ca2558 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - lab_coat (0.1.4) + lab_coat (0.1.5) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index f31647f..4e7b2af 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ See the [`Experiment`](lib/lab_coat/experiment.rb) class for more details. |`ignore?`|Whether or not the result should be ignored. Ignored `Results` are still passed to `#publish!`. Defaults to `false`, i.e. nothing is ignored.| |`publishable_value`|The data to publish for a given `Observation`. This value is only for publishing and **is not** returned by `run!`. Defaults to `Observation#value`.| |`raised`|Callback method that's called when an `Observation` raises.| +|`select_observation`|Override this method to select which observation's `value` should be returned by the `Experiment`. Defaults to the control `Observation`.| > [!TIP] > You should create a shared base class(es) to maintain consistency across experiments within your app. @@ -91,7 +92,7 @@ class ApplicationExperiment < LabCoat::Experiment def publish!(result) payload = result.to_h.merge( user_id: @user.id, # e.g. something from the `Experiment` state - build_number: context.version # e.g. something from the runtime context + build_number: context[:version] # e.g. something from the runtime context ) YourO11yService.track_experiment_result(payload) end @@ -123,6 +124,21 @@ class ApplicationExperiment < LabCoat::Experiment end ``` +You might want to rollout the new code path in certain cases. + +```ruby +# application_experiment.rb +class ApplicationExperiment < LabCoat::Experiment + def select_observation(result) + if result.matched? || YourFeatureFlagService.flag_enabled?(@user.id, @context[:rollout_flag_name]) + candidate + else + super + end + end +end +``` + ### Make some `Observations` via `run!` You don't have to create an `Observation` yourself; that happens automatically when you call `Experiment#run!`. The control and candidate `Observations` are packaged into a `Result` and [passed to `Experiment#publish!`](#publish-the-result). diff --git a/lib/lab_coat/experiment.rb b/lib/lab_coat/experiment.rb index 06e3e49..724c0fc 100644 --- a/lib/lab_coat/experiment.rb +++ b/lib/lab_coat/experiment.rb @@ -57,26 +57,25 @@ def publishable_value(observation) observation.value end - # Override this method to select which observation's `value` should be returned by the `Experiment`. Defaults to - # the control `Observation`. This method is only called if the `Experiment` is enabled. This is useful for rolling - # out new behavior in a controlled way. - # @param control [LabCoat::Observation] The control `Observation`. - # @param candidate [LabCoat::Observation] The candidate `Observation`. - # @return [TrueClass, FalseClass] - def select_observation(control, _candidate) - control - end - # Override this method to publish the `Result`. It's recommended to override this once in an application wide base # class. # @param result [LabCoat::Result] The result of this experiment. # @return [void] def publish!(result); end + # Override this method to select which observation's `value` should be returned by the `Experiment`. Defaults to + # the control `Observation`. This method is only called if the `Experiment` is enabled. This is useful for rolling + # out new behavior in a controlled way. + # @param result [LabCoat::Result] The result of the experiment. + # @return [TrueClass, FalseClass] + def select_observation(result) + result.control + end + # Runs the control and candidate and publishes the result. Always returns the result of `control`. # @param context [Hash] Any data needed at runtime. # @return [Object] An `Observation` value. - def run!(**context) + def run!(**context) # rubocop:disable Metrics/MethodLength # Set the context for this run. @context = context @@ -93,11 +92,11 @@ def run!(**context) result = Result.new(self, control_obs, candidate_obs) publish!(result) - # Reset the context for this run. - @context = {} - # Always return the control. - select_observation(control_obs, candidate_obs).value + select_observation(result).value.tap do + # Reset the context for this run. Done here so that `select_observation` has access to the runtime context. + @context = {} + end end end end