HackerReposA FIELD GUIDE TO OPEN SOURCE
ProSponsor the shelf♧ 0
← Back to top all-time
github
scientist
Ruby2014–2023
github / scientist

scientist

:microscope: A Ruby library for carefully refactoring critical paths.

21Distinct people
24Mentions
0Explicit recommenders
7.8KGitHub stars
REPOSITORY AT A GLANCE

What to know before exploring

What it is

:microscope: A Ruby library for carefully refactoring critical paths.

GitHub

Where it is used

No documented product use was verified in the last source check.

What people are saying

No recommendation phrase was found in the indexed comments.

No classified warning is attached to this page. Read the discussions before treating silence as approval.

Also in these discussions

No other repository is repeatedly linked in the same discussions.

This week on Hacker News

Nov 7–Nov 14 UTC · Direct repository links and separately labeled related project stories. Indexed through 11/14/2026, 11:56:56 AM UTC.

No direct repository links were found in the indexed HN discussions over the past seven days. Older discussions are available below.

Mentions over time

HN mentions by year24 dated mentions

Observed mentions: 2014-11-30 21:15:08 UTC to 2023-11-15 12:59:57 UTC.

02462014-01-01 00:00:00 UTC to 2015-01-01 00:00:00 UTC (exclusive): 2 mentions; 1 submissions, 1 comments20142015-01-01 00:00:00 UTC to 2016-01-01 00:00:00 UTC (exclusive): 0 mentions; 0 submissions, 0 comments2016-01-01 00:00:00 UTC to 2017-01-01 00:00:00 UTC (exclusive): 6 mentions; 1 submissions, 5 comments2017-01-01 00:00:00 UTC to 2018-01-01 00:00:00 UTC (exclusive): 3 mentions; 1 submissions, 2 comments2018-01-01 00:00:00 UTC to 2019-01-01 00:00:00 UTC (exclusive): 2 mentions; 0 submissions, 2 comments20182019-01-01 00:00:00 UTC to 2020-01-01 00:00:00 UTC (exclusive): 3 mentions; 2 submissions, 1 comments2020-01-01 00:00:00 UTC to 2021-01-01 00:00:00 UTC (exclusive): 2 mentions; 0 submissions, 2 comments2021-01-01 00:00:00 UTC to 2022-01-01 00:00:00 UTC (exclusive): 1 mentions; 0 submissions, 1 comments2022-01-01 00:00:00 UTC to 2023-01-01 00:00:00 UTC (exclusive): 2 mentions; 1 submissions, 1 comments2023-01-01 00:00:00 UTC to 2024-01-01 00:00:00 UTC (exclusive): 3 mentions; 1 submissions, 2 comments2023

Counts come from the repository’s dated source evidence. Index cutoff: 2026-11-02 08:16:59 UTC.

View mention counts and date ranges
UTC ranges include their start and exclude their end. Select a period to filter the source evidence.
PeriodStart (UTC)End (exclusive, UTC)MentionsSubmissionsComments
20142014-01-01 00:00:00 UTC2015-01-01 00:00:00 UTC211
20152015-01-01 00:00:00 UTC2016-01-01 00:00:00 UTC000
20162016-01-01 00:00:00 UTC2017-01-01 00:00:00 UTC615
20172017-01-01 00:00:00 UTC2018-01-01 00:00:00 UTC312
20182018-01-01 00:00:00 UTC2019-01-01 00:00:00 UTC202
20192019-01-01 00:00:00 UTC2020-01-01 00:00:00 UTC321
20202020-01-01 00:00:00 UTC2021-01-01 00:00:00 UTC202
20212021-01-01 00:00:00 UTC2022-01-01 00:00:00 UTC101
20222022-01-01 00:00:00 UTC2023-01-01 00:00:00 UTC211
20232023-01-01 00:00:00 UTC2024-01-01 00:00:00 UTC312

From the project README

An excerpt from the repository, fetched Nov 13, 2026.

Read the project’s introduction
Scientist! A Ruby library for carefully refactoring critical paths. How do I science? Let'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. require "scientist" class MyWidget def allows?(user) experiment = Scientist::Default.new "widget-permissions" experiment.use { model.check_user(user).valid? } # old way experiment.try { user.can?(:read, model) } # new way experiment.run end end Wrap 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: It decides whether or not to run the try block, Randomizes the order in which use and try blocks are run, Measures the wall time and cpu time of all behaviors in seconds, Compares the result of try to the result of use, Swallow and record exceptions raised in the try block when overriding raised, and Publishes all this information. The use block is called the control. The try block is called the candidate. Creating an experiment is wordy, but when you include the Scientist module, the science helper will instantiate an experiment and call run for you: require "scientist" class MyWidget include Scientist def allows?(user) science "widget-permissions" do |experiment| experiment.use { model.check_user(user).valid? } # old way experiment.try { user.can?(:read, model) } # new way end # returns the control value end end If you don't declare any try blocks, none of the Scientist machinery is invoked and the control value is always returned. Making science useful The 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: require "scientist/experiment" class MyExperiment include Scientist::Experiment attr_accessor :name def initialize(name) @name = name end def enabled? # see "Ramping up experiments" below true end def raised(operation, error) # see "In a Scientist callback" below p "Operation '#{operation}' failed with error '#{error.inspect}'" super # will re-raise end def publish(result) # see "Publishing results" below p result end end When 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. Now calls to the science helper will load instances of MyExperiment. Controlling comparison Scientist compares control and candidate values using ==. To override this behavior, use compare to define how to compare observed values instead: class MyWidget include Scientist def users science "users" do |e| e.use { User.all } # returns User instances e.try { UserService.list } # returns UserService::User instances e.compare do |control, candidate| control.map(&:login) == candidate.map(&:login) end end end end If 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: class MyWidget include Scientist def slug_from_login(login) science "slug_from_login" do |e| e.use { User.slug_from_login login } # returns String instance or ArgumentError e.try { UserService.slug_from_login login } # returns String instance or ArgumentError compare_error_message_and_class = -> (control, candidate) do control.class == candidate.class && control.message == candidate.message end compare_argument_errors = -> (control, candidate) do control.class == ArgumentError && candidate.class == ArgumentError && control.message.start_with?("Input has invalid characters") && candidate.message.start_with?("Invalid characters in input") end e.compare_errors do |control, candidate| compare_error_message_and_class.call(control, candidate) || compare_argument_errors.call(control, candidate) end end end end Adding context Results aren't very useful without some way to identify them. Use the context method to add to or retrieve the context for an experiment: science "widget-permissions" do |e| e.context :user => user e.use { model.check_user(user).valid? } e.try { user.can?(:read, model) } end 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: class MyWidget include Scientist def allows?(user) science "widget-permissions" do |e| e.context :user => user e.use { model.check_user(user).valid? } e.try { user.can?(:read, model) } end end def destroy science "widget-destruction" do |e| e.use { old_scary_destroy } e.try { new_safe_destroy } end end def default_scientist_context { :widget => self } end end The widget-permissions and widget-destruction experiments will both have a :widget key in their contexts. Expensive setup If an experiment requires expensive setup that should only occur when the experiment is going to be run, define it with the before_run method: # Code under test modifies this in-place. We want to copy it for the # candidate code, but only when needed: value_for_original_code = big_object value_for_new_code = nil science "expensive-but-worthwhile" do |e| e.before_run do value_for_new_code = big_object.deep_copy end e.use { original_code(value_for_original_code) } e.try { new_code(
Read the full README on GitHub

What Hacker News says

24 sources
Something I've learned in Ruby land (prob standard in other places, forgive my ignorance) that seems a bit different than what the article advocates for (fake services): - Write your service wrapper (eg your logic to interact with Twilio) - Call the service and record API outputs, save those responses as fixtures that will be returned as response body in your tests without hitting the real thing (eg VCR, WebMock) - You can now run your tests against old responses (this runs your logic except for getting a real response from the 3rd party; this approach leaves you exposed to API changes and edge cases your logic did not handle) For the last part, two approaches to overcome this: - Wrap any new logic in try/catch and report to Sentry: you avoid breaking prod and get info on new edge cases you didn't cover (this may not be feasible if the pa
Read the rest
th where you're inserting new logic into does not work at all without the new feature; address this with thoughtful design/rollout of new features) - Run new logic side by side to see what happens to the new logic when running in production (https://github.com/github/scientist) I use the first approach bc small startup.
In Test Against Reality
There’s a difference between these two senses of “automatic”, though. An “automatic” update of a language-ecosystem build-time dependency package, is still curated by your (not usually fully-automatic) release-management process, like any other codebase change. With full CI/CD automation, some bot (e.g. https://dependabot.com/) notices that a dep has updated, and “proposes” a PR that updates your project’s lockfile; your CI buildbot then notices the new PR, and tries building that branch to see whether it compiles and all the tests pass; and, if everything looks good, it merges the PR into the main development branch. Even then, it’s now just merged into the ‘develop’ branch, and is not necessarily going to be a part of a cut release yet. A CD bot may put it into a QA environment from there; divert a percentage of traffic to it (ala https
Read the rest
://github.com/github/scientist); and then maybe eventually make the call that it’s not hurting your metrics, and so switch the traffic fully over to it. But more often, there are usually humans in charge of that final cut-over switch, whoget to make a final call, before this kind of “automatic” update fully hits prod. Whereas, with OS packages, there’s no CI/CD pipeline — the version number on the package (coupled with your auto-update config) is the “last line of defense” standing between the package and your production system. You can set up a mirroring QA environment that gets updates first; but this is effectively just “smoke testing” — you don’t really get to “run all the tests” for your entire OS and application layer over again, in light of e.g. a new libc, to “prove”—at least in some minimal sense—that things will be fine in prod. You just deploy the configuration, and observe that it’s “working.” Maybe you will have metrics on OS log volume or something similar; but OS package updates often introduce new spurious logs that you don’t care about, so this is a bad metric to track. (Of course, you can do the QA traffic-splitting thing for your OS-package updates as well... but this means slowing the cadence of OS updates / bundling updates together so that you can gather enough data to say something about whether each new update-bundle is “working.” And if you bundle enough updates together, you’re effectively taking the reins of the OS from its maintainers, with the OS becoming just another part of your release-management process, i.e. with you effectively cutting whole-VM-image “releases.” Which somewhat removes the key advantage of automatic updates — especially automatic security updates, and especially especially automatic kernel security updates [ala https://ubuntu.com/livepatch]. You make yourself vulnerable to these discovered attacks in the interrim, in the name of stability.) When you think about it, as long as you’re operating in “full paranoia” mode in both cases (as described above), then for the “build-time dependency” packages, all the stuff the CI/CD pipeline does kind of obviates versioning. An incompatible new version of a dep that breaks your software, will just be caught during the regular CI process. You may as well use the sort of timestamp+gitsha versioning that the OP proposes; nothing would really change. It’s only in the OS package case, where semver is getting you a benefit you wouldn’t otherwise get just from good release hygiene.
In Stop Abusing Semver
An example of when you might use this is when you're cutting over to a new service, and you want to validate that the new service behaves the same way as the old one before transitioning all the traffic to the new service. In order to build confidence in the new service, you can assert that the expected output is the same, and raise alerts when there is an inconsistency. This incremental rewrite approach allows you to slowly replace pieces of an application with a new service and helps prevent regressions in behaviour. Or consider when you're performing a database migration (on large databases) with 0 downtime. Typically this involves something like: 1. Dual Writing: Create 2 tables and write to both and keep them in sync (by duplicating new data, and back-filling old data) 2. Update read paths: Change all code to read from the new table,
Read the rest
and validate that the data being read is consistent with the old table. You can use a library like Scientist [0] to validate that the reads are the same. 3. Update write paths: Change all code to write to the new table (and raise alerts if the old path is exercised) 4. Deleting old data: Remove code and data that relies on the old data model [0] https://github.com/github/scientist
In Never edit a method, always rewrite it?

Showing the 15 most recent of 24 sources. Enable JavaScript to search and browse every indexed mention.

MAKE YOURSELF AT HOME

A few shortcuts

/
Search the collection
?
Open this guide
Esc
Close dialogs and navigation

Your saved repositories and interests stay in this browser.