Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
A simple experiment library to safely test new code paths. LabCoat
is designed to be highly customizable and play nice with your existing tools/services.
This library is heavily inspired by Scientist, with some key differences:
Experiments
are classes
, not modules
which means they are stateful by default.Result
only supports one comparison at a time, i.e. only 1 candidate
is allowed per run.duration
is measured using Ruby's Benchmark
.Experiment
run can be selected dynamically.Install the gem and add to the application's Gemfile by executing:
bundle add lab_coat
If bundler is not being used to manage dependencies, install the gem by executing:
gem install lab_coat
Experiment
To do some science, i.e. test out a new code path, start by defining an Experiment
. An experiment is any class that inherits from LabCoat::Experiment
and implements the required methods.
# your_experiment.rb
class YourExperiment < LabCoat::Experiment
def control
expensive_query.first
end
def candidate
refactored_version_of_the_query.first
end
def enabled?
true
end
end
The base initializer for an Experiment
requires a name
argument; it's a good idea to name your experiments.
See the Experiment
class for more details.
Method | Description |
---|---|
candidate | The new behavior you want to test. |
control | The existing or default behavior. This will always be returned from #run! . |
enabled? | Returns a Boolean that controls whether or not the experiment runs. |
publish! | This is technically not required, but Experiments are not useful unless you can analyze the results. Override this method to record the Result however you wish. |
[!IMPORTANT] The
#run!
method accepts arbitrary key word arguments and stores them in an instance variable called@context
in case you need to provide data at runtime. You can access the runtime context via@context
orcontext
. The runtime context is reset after each run.
Method | Description |
---|---|
compare | Whether or not the result is a match. This is how you can run complex/custom comparisons. Defaults to control.value == candidate.value . |
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.
You might want to give your experiment some context, or state. You can do this via an initializer or writer methods just like any other Ruby class.
# application_experiment.rb
class ApplicationExperiment < LabCoat::Experiment
def initialize(user)
@user = user
@is_admin = user.admin?
end
end
You might want to publish!
all experiments in a consistent way so that you can analyze the data and make decisions. New Experiment
authors should not have to redo the "plumbing" between your experimentation framework (e.g. LabCoat
) and your observability (o11y) process.
# application_experiment.rb
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
)
YourO11yService.track_experiment_result(payload)
end
end
You might have a common way to enable experiments such as a feature flag system and/or common guards you want to enforce application wide. These might come from a mix of services, the Experiment
's state, or the runtime context
.
# application_experiment.rb
class ApplicationExperiment < LabCoat::Experiment
def enabled?
!@is_admin && YourFeatureFlagService.flag_enabled?(@user.id, name)
end
end
You might want to track any errors thrown from all your experiments and route them to some service, or log them.
# application_experiment.rb
class ApplicationExperiment < LabCoat::Experiment
def raised(observation)
YourErrorService.report_error(
observation.error,
tags: observation.to_h
)
end
end
You might want to rollout the new code path in certain cases.
# 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
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!
.
The run!
method accepts arbitrary keyword arguments, to allow you to set runtime context for the specific run of the experiment. You can access this Hash
via the context
reader method, or directly via the @context
instance variable.
Attribute | Description |
---|---|
duration | The duration of the run represented as a Benchmark::Tms object. |
error | If the code path raised, the thrown exception is stored here. |
experiment | The Experiment instance this Result is for. |
name | Either "control" or "candidate" . |
publishable_value | A publishable representation of the value , as defined by Experiment#publishable_value . |
raised? | Whether or not the code path raised. |
slug | A combination of the Experiment#name and Observation#name , e.g. "experiment_name.control" |
to_h | A hash representation of the Observation . Useful for publishing and/or reporting. |
value | The return value of the observed code path. |
Observation
instances are passed to many of the Experiment
methods that you may override.
# your_experiment.rb
def compare(control, candidate)
return false if control.raised? || candidate.raised?
control.value.some_method == candidate.value.some_method
end
def ignore?(control, candidate)
# You might ignore runs that throw errors and handle them separately via `raised`.
return true if control.raised? || candidate.raised?
# You might ignore runs where the candidate meets some condition.
return true if candidate.value.some_condition?
false
end
def publishable_value(observation)
return nil if observation.raised?
# Let's say your control and candidate blocks return objects that don't serialize nicely.
{
some_attribute: observation.value.some_attribute,
some_other_attribute: observation.value.some_other_attribute,
some_count: observation.value.some_array.count
}
end
# Elsewhere...
YourExperiment.new(...).run!
Result
A Result
represents a single run of an Experiment
.
Attribute | Description |
---|---|
candidate | An Observation instance representing the Experiment#candidate behavior |
control | An Observation instance representing the Experiment#control behavior |
experiment | The Experiment instance this Result is for. |
ignored? | Whether or not the result should be ignored, as defined by Experiment#ignore? |
matched? | Whether or not the control and candidate match, as defined by Experiment#compare |
to_h | A hash representation of the Result . Useful for publishing and/or reporting. |
The Result
is passed to your implementation of #publish!
when an Experiment
is finished running. The to_h
method on a Result is a good place to start and might be sufficient for most experiments. You might want to include additional data such as the runtime context
or other state if you find that relevant for analysis.
# your_experiment.rb
def publish!(result)
return if result.ignored?
puts result.to_h.merge(context:)
end
[!NOTE] All
Results
are passed topublish!
, including ignored ones. It is your responsibility to check theignored?
method and handle those as you wish.
You can always access all of the attributes of the Result
and its Observations
directly to fully customize what your experiment publishing looks like.
# your_experiment.rb
def publish!(result)
if result.ignored?
puts "🙈"
return
end
if result.matched?
puts "😎"
else
control = result.control
candidate = result.candidate
puts <<~MSG
😮
#{control.slug}
Value: #{control.publishable_value}
Duration Real: #{control.duration.real}
Duration System: #{control.duration.stime}
Duration User: #{control.duration.utime}
Error: #{control.error&.message}
#{candidate.slug}
Value: #{candidate.publishable_value}
Duration: #{candidate.duration.real}
Duration System: #{candidate.duration.stime}
Duration User: #{candidate.duration.utime}
Error: #{candidate.error&.message}
MSG
end
end
Running a mismatched experiment with this implementation of publish!
would produce:
😮
my_experiment.control
Value: 420
Duration Real: 12.934
Duration System: 2.134
Duration User: 10.800
Error:
my_experiment.candidate
Value: 69
Duration Real: 9.702
Duration System: 1.002
Duration User: 8.700
Error:
Observations
The Observation
class can be used as a standalone wrapper for any code that you want to experiment with. Instantiating an Observation
automatically:
10.times do |i|
observation = Observation.new("test-#{i}", nil) do
some_code_path
end
puts "#{observation.name} results:"
if observation.raised?
puts "error: #{observation.error.message}"
else
puts <<~MSG
duration: #{observation.duration.real}
succeeded: #{!observation.raised?}
MSG
end
end
[!WARNING] Be careful when using
Observation
instances without anExperiment
set. Some methods like#publishable_value
and#slug
depend on anexperiment
and may raise an error or return unexpected values when called without one.
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/omkarmoghe/lab_coat.
The gem is available as open source under the terms of the MIT License.
FAQs
Unknown package
We found that lab_coat demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.