{"id":13394881,"url":"https://github.com/github/scientist","last_synced_at":"2025-05-13T10:59:58.106Z","repository":{"id":13851987,"uuid":"16549503","full_name":"github/scientist","owner":"github","description":":microscope: A Ruby library for carefully refactoring critical paths.","archived":false,"fork":false,"pushed_at":"2024-12-16T16:42:01.000Z","size":244,"stargazers_count":7561,"open_issues_count":14,"forks_count":449,"subscribers_count":458,"default_branch":"main","last_synced_at":"2025-05-13T10:59:27.705Z","etag":null,"topics":["refactoring","ruby","rubygem","scientist"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/github.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2014-02-05T15:58:43.000Z","updated_at":"2025-05-12T13:20:55.000Z","dependencies_parsed_at":"2024-05-23T01:30:37.735Z","dependency_job_id":"df9bc022-9abd-4af8-94e6-885b3985efc1","html_url":"https://github.com/github/scientist","commit_stats":{"total_commits":156,"total_committers":60,"mean_commits":2.6,"dds":0.7948717948717949,"last_synced_commit":"0039b7084b848a38de2e960981df25376574adbc"},"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/github%2Fscientist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/github%2Fscientist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/github%2Fscientist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/github%2Fscientist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/github","download_url":"https://codeload.github.com/github/scientist/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253929358,"owners_count":21985802,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["refactoring","ruby","rubygem","scientist"],"created_at":"2024-07-30T17:01:34.975Z","updated_at":"2025-05-13T10:59:58.056Z","avatar_url":"https://github.com/github.png","language":"Ruby","readme":"# Scientist!\n\nA Ruby library for carefully refactoring critical paths. [![Build Status](https://github.com/github/scientist/actions/workflows/ci.yml/badge.svg)](https://github.com/github/scientist/actions/workflows/ci.yml)\n\n## How do I science?\n\nLet's pretend you're changing the way you handle permissions in a large web app. Tests can help guide your refactoring, but you really want to compare the current and refactored behaviors under load.\n\n```ruby\nrequire \"scientist\"\n\nclass MyWidget\n  def allows?(user)\n    experiment = Scientist::Default.new \"widget-permissions\"\n    experiment.use { model.check_user(user).valid? } # old way\n    experiment.try { user.can?(:read, model) } # new way\n\n    experiment.run\n  end\nend\n```\n\nWrap a `use` block around the code's original behavior, and wrap `try` around the new behavior. `experiment.run` will always return whatever the `use` block returns, but it does a bunch of stuff behind the scenes:\n\n* It decides whether or not to run the `try` block,\n* Randomizes the order in which `use` and `try` blocks are run,\n* Measures the wall time and cpu time of all behaviors in seconds,\n* Compares the result of `try` to the result of `use`,\n* Swallow and record exceptions raised in the `try` block when overriding `raised`, and\n* Publishes all this information.\n\nThe `use` block is called the **control**. The `try` block is called the **candidate**.\n\nCreating an experiment is wordy, but when you include the `Scientist` module, the `science` helper will instantiate an experiment and call `run` for you:\n\n```ruby\nrequire \"scientist\"\n\nclass MyWidget\n  include Scientist\n\n  def allows?(user)\n    science \"widget-permissions\" do |experiment|\n      experiment.use { model.check_user(user).valid? } # old way\n      experiment.try { user.can?(:read, model) } # new way\n    end # returns the control value\n  end\nend\n```\n\nIf you don't declare any `try` blocks, none of the Scientist machinery is invoked and the control value is always returned.\n\n## Making science useful\n\nThe examples above will run, but they're not really *doing* anything. The `try` blocks don't run yet and none of the results get published. Replace the default experiment implementation to control execution and reporting:\n\n```ruby\nrequire \"scientist/experiment\"\n\nclass MyExperiment\n  include Scientist::Experiment\n\n  attr_accessor :name\n\n  def initialize(name)\n    @name = name\n  end\n\n  def enabled?\n    # see \"Ramping up experiments\" below\n    true\n  end\n\n  def raised(operation, error)\n    # see \"In a Scientist callback\" below\n    p \"Operation '#{operation}' failed with error '#{error.inspect}'\"\n    super # will re-raise\n  end\n\n  def publish(result)\n    # see \"Publishing results\" below\n    p result\n  end\nend\n```\n\nWhen `Scientist::Experiment` is included in a class, it automatically sets it as the default implementation via `Scientist::Experiment.set_default`. This `set_default` call is skipped if you include `Scientist::Experiment` in a module.\n\nNow calls to the `science` helper will load instances of `MyExperiment`.\n\n### Controlling comparison\n\nScientist compares control and candidate values using `==`. To override this behavior, use `compare` to define how to compare observed values instead:\n\n```ruby\nclass MyWidget\n  include Scientist\n\n  def users\n    science \"users\" do |e|\n      e.use { User.all }         # returns User instances\n      e.try { UserService.list } # returns UserService::User instances\n\n      e.compare do |control, candidate|\n        control.map(\u0026:login) == candidate.map(\u0026:login)\n      end\n    end\n  end\nend\n```\n\nIf either the control block or candidate block raises an error, Scientist compares the two observations' classes and messages using `==`. To override this behavior, use `compare_errors` to define how to compare observed errors instead:\n\n```ruby\nclass MyWidget\n  include Scientist\n\n  def slug_from_login(login)\n    science \"slug_from_login\" do |e|\n      e.use { User.slug_from_login login }         # returns String instance or ArgumentError\n      e.try { UserService.slug_from_login login }  # returns String instance or ArgumentError\n\n      compare_error_message_and_class = -\u003e (control, candidate) do\n        control.class == candidate.class \u0026\u0026\n        control.message == candidate.message\n      end\n\n      compare_argument_errors = -\u003e (control, candidate) do\n        control.class == ArgumentError \u0026\u0026\n        candidate.class == ArgumentError \u0026\u0026\n        control.message.start_with?(\"Input has invalid characters\") \u0026\u0026\n        candidate.message.start_with?(\"Invalid characters in input\")\n      end\n\n      e.compare_errors do |control, candidate|\n        compare_error_message_and_class.call(control, candidate) ||\n        compare_argument_errors.call(control, candidate)\n      end\n    end\n  end\nend\n```\n\n### Adding context\n\nResults aren't very useful without some way to identify them. Use the `context` method to add to or retrieve the context for an experiment:\n\n```ruby\nscience \"widget-permissions\" do |e|\n  e.context :user =\u003e user\n\n  e.use { model.check_user(user).valid? }\n  e.try { user.can?(:read, model) }\nend\n```\n\n`context` takes a Symbol-keyed Hash of extra data. The data is available in `Experiment#publish` via the `context` method. If you're using the `science` helper a lot in a class, you can provide a default context:\n\n```ruby\nclass MyWidget\n  include Scientist\n\n  def allows?(user)\n    science \"widget-permissions\" do |e|\n      e.context :user =\u003e user\n\n      e.use { model.check_user(user).valid? }\n      e.try { user.can?(:read, model) }\n    end\n  end\n\n  def destroy\n    science \"widget-destruction\" do |e|\n      e.use { old_scary_destroy }\n      e.try { new_safe_destroy }\n    end\n  end\n\n  def default_scientist_context\n    { :widget =\u003e self }\n  end\nend\n```\n\nThe `widget-permissions` and `widget-destruction` experiments will both have a `:widget` key in their contexts.\n\n### Expensive setup\n\nIf an experiment requires expensive setup that should only occur when the experiment is going to be run, define it with the `before_run` method:\n\n```ruby\n# Code under test modifies this in-place. We want to copy it for the\n# candidate code, but only when needed:\nvalue_for_original_code = big_object\nvalue_for_new_code      = nil\n\nscience \"expensive-but-worthwhile\" do |e|\n  e.before_run do\n    value_for_new_code = big_object.deep_copy\n  end\n  e.use { original_code(value_for_original_code) }\n  e.try { new_code(value_for_new_code) }\nend\n```\n\n### Keeping it clean\n\nSometimes you don't want to store the full value for later analysis. For example, an experiment may return `User` instances, but when researching a mismatch, all you care about is the logins. You can define how to clean these values in an experiment:\n\n```ruby\nclass MyWidget\n  include Scientist\n\n  def users\n    science \"users\" do |e|\n      e.use { User.all }\n      e.try { UserService.list }\n\n      e.clean do |value|\n        value.map(\u0026:login).sort\n      end\n    end\n  end\nend\n```\n\nAnd this cleaned value is available in observations in the final published result:\n\n```ruby\nclass MyExperiment\n  include Scientist::Experiment\n\n  # ...\n\n  def publish(result)\n    result.control.value         # [\u003cUser alice\u003e, \u003cUser bob\u003e, \u003cUser carol\u003e]\n    result.control.cleaned_value # [\"alice\", \"bob\", \"carol\"]\n  end\nend\n```\n\nNote that the `#clean` method will discard the previous cleaner block if you call it again.  If for some reason you need to access the currently configured cleaner block, `Scientist::Experiment#cleaner` will return the block without further ado.  _(This probably won't come up in normal usage, but comes in handy if you're writing, say, a custom experiment runner that provides default cleaners.)_\n\nThe `#clean` method will not be used for comparison of the results, so in the following example it is not possible to remove the `#compare` method without the experiment failing:\n\n```ruby\ndef user_ids\n  science \"user_ids\" do\n    e.use { [1,2,3] }\n    e.try { [1,3,2] }\n    e.clean { |value| value.sort }\n    e.compare { |a, b| a.sort == b.sort }\n  end\nend\n```\n\n### Ignoring mismatches\n\nDuring the early stages of an experiment, it's possible that some of your code will always generate a mismatch for reasons you know and understand but haven't yet fixed. Instead of these known cases always showing up as mismatches in your metrics or analysis, you can tell an experiment whether or not to ignore a mismatch using the `ignore` method. You may include more than one block if needed:\n\n```ruby\ndef admin?(user)\n  science \"widget-permissions\" do |e|\n    e.use { model.check_user(user).admin? }\n    e.try { user.can?(:admin, model) }\n\n    e.ignore { user.staff? } # user is staff, always an admin in the new system\n    e.ignore do |control, candidate|\n      # new system doesn't handle unconfirmed users yet:\n      control \u0026\u0026 !candidate \u0026\u0026 !user.confirmed_email?\n    end\n  end\nend\n```\n\nThe ignore blocks are only called if the *values* don't match. Unless a `compare_errors` comparator is defined, two cases are considered mismatches: a) one observation raising an exception and the other not, b) observations raising exceptions with different classes or messages.\n\n### Enabling/disabling experiments\n\nSometimes you don't want an experiment to run. Say, disabling a new codepath for anyone who isn't staff. You can disable an experiment by setting a `run_if` block. If this returns `false`, the experiment will merely return the control value. Otherwise, it defers to the experiment's configured `enabled?` method.\n\n```ruby\nclass DashboardController\n  include Scientist\n\n  def dashboard_items\n    science \"dashboard-items\" do |e|\n      # only run this experiment for staff members\n      e.run_if { current_user.staff? }\n      # ...\n  end\nend\n```\n\n### Ramping up experiments\n\nAs a scientist, you know it's always important to be able to turn your experiment off, lest it run amok and result in villagers with pitchforks on your doorstep. In order to control whether or not an experiment is enabled, you must include the `enabled?` method in your `Scientist::Experiment` implementation.\n\n```ruby\nclass MyExperiment\n  include Scientist::Experiment\n\n  attr_accessor :name, :percent_enabled\n\n  def initialize(name)\n    @name = name\n    @percent_enabled = 100\n  end\n\n  def enabled?\n    percent_enabled \u003e 0 \u0026\u0026 rand(100) \u003c percent_enabled\n  end\n\n  # ...\n\nend\n```\n\nThis code will be invoked for every method with an experiment every time, so be sensitive about its performance. For example, you can store an experiment in the database but wrap it in various levels of caching such as memcache or per-request thread-locals.\n\n### Publishing results\n\nWhat good is science if you can't publish your results?\n\nYou must implement the `publish(result)` method, and can publish data however you like. For example, timing data can be sent to graphite, and mismatches can be placed in a capped collection in redis for debugging later.\n\nThe `publish` method is given a `Scientist::Result` instance with its associated `Scientist::Observation`s:\n\n```ruby\nclass MyExperiment\n  include Scientist::Experiment\n\n  # ...\n\n  def publish(result)\n\n    # Wall time\n    # Store the timing for the control value,\n    $statsd.timing \"science.#{name}.control\", result.control.duration\n    # for the candidate (only the first, see \"Breaking the rules\" below,\n    $statsd.timing \"science.#{name}.candidate\", result.candidates.first.duration\n\n    # CPU time\n    # Store the timing for the control value,\n    $statsd.timing \"science.cpu.#{name}.control\", result.control.cpu_time\n    # for the candidate (only the first, see \"Breaking the rules\" below,\n    $statsd.timing \"science.cpu.#{name}.candidate\", result.candidates.first.cpu_time\n\n    # and counts for match/ignore/mismatch:\n    if result.matched?\n      $statsd.increment \"science.#{name}.matched\"\n    elsif result.ignored?\n      $statsd.increment \"science.#{name}.ignored\"\n    else\n      $statsd.increment \"science.#{name}.mismatched\"\n      # Finally, store mismatches in redis so they can be retrieved and examined\n      # later on, for debugging and research.\n      store_mismatch_data(result)\n    end\n  end\n\n  def store_mismatch_data(result)\n    payload = {\n      :name            =\u003e name,\n      :context         =\u003e context,\n      :control         =\u003e observation_payload(result.control),\n      :candidate       =\u003e observation_payload(result.candidates.first),\n      :execution_order =\u003e result.observations.map(\u0026:name)\n    }\n\n    key = \"science.#{name}.mismatch\"\n    $redis.lpush key, payload\n    $redis.ltrim key, 0, 1000\n  end\n\n  def observation_payload(observation)\n    if observation.raised?\n      {\n        :exception =\u003e observation.exception.class,\n        :message   =\u003e observation.exception.message,\n        :backtrace =\u003e observation.exception.backtrace\n      }\n    else\n      {\n        # see \"Keeping it clean\" above\n        :value =\u003e observation.cleaned_value\n      }\n    end\n  end\nend\n```\n\n### Testing\n\nWhen running your test suite, it's helpful to know that the experimental results always match. To help with testing, Scientist defines a `raise_on_mismatches` class attribute when you include `Scientist::Experiment`. Only do this in your test suite!\n\nTo raise on mismatches:\n\n```ruby\nclass MyExperiment\n  include Scientist::Experiment\n  # ... implementation\nend\n\nMyExperiment.raise_on_mismatches = true\n```\n\nScientist will raise a `Scientist::Experiment::MismatchError` exception if any observations don't match.\n\n#### Custom mismatch errors\n\nTo instruct Scientist to raise a custom error instead of the default `Scientist::Experiment::MismatchError`:\n\n```ruby\nclass CustomMismatchError \u003c Scientist::Experiment::MismatchError\n  def to_s\n    message = \"There was a mismatch! Here's the diff:\"\n\n    diffs = result.candidates.map do |candidate|\n      Diff.new(result.control, candidate)\n    end.join(\"\\n\")\n\n    \"#{message}\\n#{diffs}\"\n  end\nend\n```\n\n```ruby\nscience \"widget-permissions\" do |e|\n  e.use { Report.find(id) }\n  e.try { ReportService.new.fetch(id) }\n\n  e.raise_with CustomMismatchError\nend\n```\n\nThis allows for pre-processing on mismatch error exception messages.\n\n### Handling errors\n\n#### In candidate code\n\nScientist rescues and tracks _all_ exceptions raised in a `try` or `use` block, including some where rescuing may cause unexpected behavior (like `SystemExit` or `ScriptError`). To rescue a more restrictive set of exceptions, modify the `RESCUES` list:\n\n```ruby\n# default is [Exception]\nScientist::Observation::RESCUES.replace [StandardError]\n```\n\n**Timeout ⏲️**: If you're introducing a candidate that could possibly timeout, use caution. ⚠️ While Scientist rescues all exceptions that occur in the candidate block, it *does not* protect you from timeouts, as doing so would be complicated. It would likely require running the candidate code in a background job and tracking the time of a request. We feel the cost of this complexity would outweigh the benefit, so make sure that your code doesn't cause timeouts. This risk can be reduced by running the experiment on a low percentage so that users can (most likely) bypass the experiment by refreshing the page if they hit a timeout. See [Ramping up experiments](#ramping-up-experiments) below for how details on how to set the percentage for your experiment.\n\n#### In a Scientist callback\n\nIf an exception is raised within any of Scientist's internal helpers, like `publish`, `compare`, or `clean`, the `raised` method is called with the symbol name of the internal operation that failed and the exception that was raised. The default behavior of `Scientist::Default` is to simply re-raise the exception. Since this halts the experiment entirely, it's often a better idea to handle this error and continue so the experiment as a whole isn't canceled entirely:\n\n```ruby\nclass MyExperiment\n  include Scientist::Experiment\n\n  # ...\n\n  def raised(operation, error)\n    InternalErrorTracker.track! \"science failure in #{name}: #{operation}\", error\n  end\nend\n```\n\nThe operations that may be handled here are:\n\n* `:clean` - an exception is raised in a `clean` block\n* `:compare` - an exception is raised in a `compare` block\n* `:enabled` - an exception is raised in the `enabled?` method\n* `:ignore` - an exception is raised in an `ignore` block\n* `:publish` - an exception is raised in the `publish` method\n* `:run_if` - an exception is raised in a `run_if` block\n\n### Designing an experiment\n\nBecause `enabled?` and `run_if` determine when a candidate runs, it's impossible to guarantee that it will run every time. For this reason, Scientist is only safe for wrapping methods that aren't changing data.\n\nWhen using Scientist, we've found it most useful to modify both the existing and new systems simultaneously anywhere writes happen, and verify the results at read time with `science`. `raise_on_mismatches` has also been useful to ensure that the correct data was written during tests, and reviewing published mismatches has helped us find any situations we overlooked with our production data at runtime. When writing to and reading from two systems, it's also useful to write some data reconciliation scripts to verify and clean up production data alongside any running experiments.\n\n#### Noise and error rates\n\nKeep in mind that Scientist's `try` and `use` blocks run sequentially in random order. As such, any data upon which your code depends may change before the second block is invoked, potentially yielding a mismatch between the candidate and control return values. To calibrate your expectations with respect to [false negatives](https://en.wikipedia.org/wiki/Type_I_and_type_II_errors) arising from systemic conditions external to your proposed changes, consider starting with an experiment in which both the `try` and `use` blocks invoke the control method. Then proceed with introducing a candidate.\n\n### Finishing an experiment\n\nAs your candidate behavior converges on the controls, you'll start thinking about removing an experiment and using the new behavior.\n\n* If there are any ignore blocks, the candidate behavior is *guaranteed* to be different. If this is unacceptable, you'll need to remove the ignore blocks and resolve any ongoing mismatches in behavior until the observations match perfectly every time.\n* When removing a read-behavior experiment, it's a good idea to keep any write-side duplication between an old and new system in place until well after the new behavior has been in production, in case you need to roll back.\n\n## Breaking the rules\n\nSometimes scientists just gotta do weird stuff. We understand.\n\n### Ignoring results entirely\n\nScience is useful even when all you care about is the timing data or even whether or not a new code path blew up. If you have the ability to incrementally control how often an experiment runs via your `enabled?` method, you can use it to silently and carefully test new code paths and ignore the results altogether. You can do this by setting `ignore { true }`, or for greater efficiency, `compare { true }`.\n\nThis will still log mismatches if any exceptions are raised, but will disregard the values entirely.\n\n### Trying more than one thing\n\nIt's not usually a good idea to try more than one alternative simultaneously. Behavior isn't guaranteed to be isolated and reporting + visualization get quite a bit harder. Still, it's sometimes useful.\n\nTo try more than one alternative at once, add names to some `try` blocks:\n\n```ruby\nrequire \"scientist\"\n\nclass MyWidget\n  include Scientist\n\n  def allows?(user)\n    science \"widget-permissions\" do |e|\n      e.use { model.check_user(user).valid? } # old way\n\n      e.try(\"api\") { user.can?(:read, model) } # new service API\n      e.try(\"raw-sql\") { user.can_sql?(:read, model) } # raw query\n    end\n  end\nend\n```\n\nWhen the experiment runs, all candidate behaviors are tested and each candidate observation is compared with the control in turn.\n\n### No control, just candidates\n\nDefine the candidates with named `try` blocks, omit a `use`, and pass a candidate name to `run`:\n\n```ruby\nexperiment = MyExperiment.new(\"various-ways\") do |e|\n  e.try(\"first-way\")  { ... }\n  e.try(\"second-way\") { ... }\nend\n\nexperiment.run(\"second-way\")\n```\n\nThe `science` helper also knows this trick:\n\n```ruby\nscience \"various-ways\", run: \"first-way\" do |e|\n  e.try(\"first-way\")  { ... }\n  e.try(\"second-way\") { ... }\nend\n```\n\n#### Providing fake timing data\n\nIf you're writing tests that depend on specific timing values, you can provide canned durations using the `fabricate_durations_for_testing_purposes` method, and Scientist will report these in `Scientist::Observation#duration` and `Scientist::Observation#cpu_time` instead of the actual execution times.\n\n```ruby\nscience \"absolutely-nothing-suspicious-happening-here\" do |e|\n  e.use { ... } # \"control\"\n  e.try { ... } # \"candidate\"\n  e.fabricate_durations_for_testing_purposes({\n    \"control\" =\u003e { \"duration\" =\u003e 1.0, \"cpu_time\" =\u003e 0.9 },\n    \"candidate\" =\u003e { \"duration\" =\u003e 0.5, \"cpu_time\" =\u003e 0.4 }\n  })\nend\n```\n\n`fabricate_durations_for_testing_purposes` takes a Hash of duration \u0026 cpu_time values, keyed by behavior names.  (By default, Scientist uses `\"control\"` and `\"candidate\"`, but if you override these as shown in [Trying more than one thing](#trying-more-than-one-thing) or [No control, just candidates](#no-control-just-candidates), use matching names here.)  If a name is not provided, the actual execution time will be reported instead.\n\nWe should mention these durations will be used both for the `duration` field and the `cpu_time` field.\n\n_Like `Scientist::Experiment#cleaner`, this probably won't come up in normal usage.  It's here to make it easier to test code that extends Scientist._\n\n### Without including Scientist\n\nIf you need to use Scientist in a place where you aren't able to include the Scientist module, you can call `Scientist.run`:\n\n```ruby\nScientist.run \"widget-permissions\" do |e|\n  e.use { model.check_user(user).valid? }\n  e.try { user.can?(:read, model) }\nend\n```\n\n## Hacking\n\nBe on a Unixy box. Make sure a modern Bundler is available. `script/test` runs the unit tests. All development dependencies are installed automatically. Scientist requires Ruby 2.3 or newer.\n\n## Wrappers\n\n- [RealGeeks/lab_tech](https://github.com/RealGeeks/lab_tech) is a Rails engine for using this library by controlling, storing, and analyzing experiment results with ActiveRecord.\n\n## Alternatives\n\n- [daylerees/scientist](https://github.com/daylerees/scientist) (PHP)\n- [scientistproject/scientist.net](https://github.com/scientistproject/Scientist.net) (.NET)\n- [joealcorn/laboratory](https://github.com/joealcorn/laboratory) (Python)\n- [rawls238/Scientist4J](https://github.com/rawls238/Scientist4J) (Java)\n- [tomiaijo/scientist](https://github.com/tomiaijo/scientist) (C++)\n- [trello/scientist](https://github.com/trello/scientist) (node.js)\n- [ziyasal/scientist.js](https://github.com/ziyasal/scientist.js) (node.js, ES6)\n- [TrueWill/tzientist](https://github.com/TrueWill/tzientist) (node.js, TypeScript)\n- [TrueWill/paleontologist](https://github.com/TrueWill/paleontologist) (Deno, TypeScript)\n- [yeller/laboratory](https://github.com/yeller/laboratory) (Clojure)\n- [lancew/Scientist](https://github.com/lancew/Scientist) (Perl 5)\n- [lancew/ScientistP6](https://github.com/lancew/ScientistP6) (Perl 6)\n- [MadcapJake/Test-Lab](https://github.com/MadcapJake/Test-Lab) (Perl 6)\n- [cwbriones/scientist](https://github.com/cwbriones/scientist) (Elixir)\n- [calavera/go-scientist](https://github.com/calavera/go-scientist) (Go)\n- [jelmersnoeck/experiment](https://github.com/jelmersnoeck/experiment) (Go)\n- [spoptchev/scientist](https://github.com/spoptchev/scientist) (Kotlin / Java)\n- [junkpiano/scientist](https://github.com/junkpiano/scientist) (Swift)\n- [serverless scientist](http://serverlessscientist.com/) (AWS Lambda)\n- [fightmegg/scientist](https://github.com/fightmegg/scientist) (TypeScript, Browser / Node.js)\n- [MisterSpex/misterspex-scientist](https://github.com/MisterSpex/misterspex-scientist) (Java, no dependencies)\n\n## Maintainers\n\n[@jbarnette](https://github.com/jbarnette),\n[@jesseplusplus](https://github.com/jesseplusplus),\n[@rick](https://github.com/rick),\nand [@zerowidth](https://github.com/zerowidth)\n","funding_links":[],"categories":["Ruby","Tools","Ruby Tools","Ruby Tools and Frameworks","Code Analysis and Metrics","Python Frameworks and Tools"],"sub_categories":["Interfaces","E-Books","Mesh networks","JavaScript Libraries for Machine Learning"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgithub%2Fscientist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgithub%2Fscientist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgithub%2Fscientist/lists"}