• Stars
    star
    408
  • Rank 105,946 (Top 3 %)
  • Language
    CoffeeScript
  • License
    MIT License
  • Created almost 9 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A Node.js library for carefully refactoring critical paths in production

Scientist

npm Build Status Coverage Status

Table of contents

How it works

So you just refactored a swath of code and all tests pass. You feel completely confident that this can go to production. Right? In reality, not so much. Be it poor test coverage or just that the refactored code is very critical, sometimes you need more reassurance.

Scientist lets you run your refactored code alongside the actual code, comparing the outputs and logging when it did not return as expected. It's heavily based on GitHub's Scientist gem. Let's walk through an example. Start with this code:

const sumList = (arr) => {
  let sum = 0;
  for (var i of arr) {
    sum += i;
  }
  return sum;
};

And let's refactor it as so:

const sumList = (arr) => {
  return _.reduce(arr, (sum, i) => sum + i);
};

To do science, all you need to do is replace the original function with a science wrapper that uses both functions:

const sumList = (arr) => {
  return science('sum-list', (experiment) => {
    experiment.use(() => sumListOld(arr));
    experiment.try(() => sumListNew(arr));
  });
};

And that's it. The science function takes a string to identify the experiment by and passes an experiment object to a function that you can use to set up your experiment. We call use to define what our control behavior is -- that's also the value that is returned from the original science call, which makes this a drop-in replacement. The try function can be used to define one or more candidates to compare. So what happens if we do this:

sumList([1, 2, 3]);
// -> 6
// Experiment candidate matched the control

But there's also a bug in our refactored code. Science logs that as appropriate, but still returns the old value that we know works.

sumList([]);
// -> 0
// Experiment candidate did not match the control
//   expected value: 0
//   received value: undefined

You can find this implemented in examples/basic.js.

Getting started

Above we just used a simple science() function to run an experiment. If you're just looking to play around, you can get the same function with require('scientist/console'). If you examine console.js, you'll notice that this is a very simple implementation of the Scientist class, which is exposed through a normal require('scientist') call.

The recommended usage is to create a file specific to your application and export the science method bound to a fully set-up Scientist instance.

const Scientist = require('scientist');

const scientist = new Scientist();

scientist.on('skip', function (experiment) { /* ... */ });
scientist.on('result', function (result) { /* ... */ });
scientist.on('error', function (err) { /* ... */ });

module.exports = scientist.science.bind(scientist);

Then you can rely on your own internal logging and metrics tools to do science.

Errors in behaviors

Scientist has built-in support for handling errors thrown by any of your behaviors.

science('throwing errors', (experiment) => {
  experiment.use(() => {
    throw Error(msg)
  });
  experiment.try("with-new", () => {
    throw new Error(msg)
  });
  experiment.try("as-type-error", () => {
    throw TypeError(msg)
  });
});

error("An error occured!");
// Experiment candidate matched the control
// Experiment candidate did not match the control
//   expected: error: [Error] 'An error occured!'
//   received: error: [TypeError] 'An error occured!'

In this case, the call to science() is actually throwing the same error that the control function threw, but after testing the other functions and readying the logging. The criteria for matching errors is based on the constructor and message.

You can find this full example at examples/errors.js.

Asynchronous behaviors

See docs/async.md.

Customizing your experiment

There are several functions you can use to configure science:

  • context: Record information to give context to results
  • async: Turn async mode on
  • skipWhen: Determine whether the experiment should be skipped
  • map: Change values for more simple comparison and logging
  • ignore: Throw away certain observations
  • compare: Decide whether two observations match
  • clean: Prepare data for logging

Because of the first-class promise support, the compare and clean functions will take values after they are settled. map happens synchronously and may also return a promise, which could be resolved.

If you want to think about the flow of data in a pipeline, it looks like this:

  1. Block is called and the value or error is saved as an observation
  2. map() is applied to the value
  3. Promises are settled if async was set to true
  4. The Result object is instantiated and observations are passed to compare()
  5. The consumer may call inspect() on an observation, which applies clean()

You can see a fairly full example at examples/complex.js

Side effects

So all of these examples were simple because they were either pure functions or functions that produced no observable side-effects. What if we want to test something more complicated? We definitely cannot let our candidate function change the state of the world permanently, such as updating an entry in the database. However, we can still use science to observe functions that change the state of some object.

science('user middleware', (experiment) => {
  experiment.use(() => {
    findUser(req);
    return req;
  });
  experiment.try(() => {
    let clone = _.clone(req);
    findUserById(clone);
    findUserByName(clone);
    return clone;
  });
});

Enabling and skipping

Often you don't want to run science on every single function call. Since we're testing under production load and running the functionality at least twice, you can imagine that some parts may get out of control. Scientist provides a solution to let you sample a test so that you can slowly ramp it up in production and stop when you have a comfortable amount of data. You can configure this with the Scientist#sample() function.

const scienceConfig = require('./science-config.json');
const scientist = new Scientist();

scientist.sample((experimentName) => {
  if (experimentName in scienceConfig) {
    // Configuration maps a name to a percentage
    return Math.random() < scienceConfig[experimentName];
  } else {
    // Default to not running for safety
    return false;
  }
});

Note that the sampling function is provided the experiment name and must be synchronous.

If you want to skip experiments based on more information, you can configure this at the experiment level with skipWhen().

science('parse headers', (experiment) => {
  experiment.skipWhen(() => 'x-internal' in headers);
  // ...
});

Why CoffeeScript?

This project started out internally at Trello and only later was spun off into a separate module. As such, it was written using the language, dependencies, and style of the Trello codebase. The code is hopefully simple enough to grok such that the language choice does not deter contributors.

More Repositories

1

RxLifecycle

Lifecycle handling APIs for Android apps using RxJava
Java
7,720
star
2

victor

Use SVGs as resources in Android
Java
1,007
star
3

navi

Adds listening capabilities to Activities and Fragments
Java
617
star
4

trellisheets

Guidelines, resources, and examples for writing CSS for Trello
HTML
253
star
5

mr-clean

Don't leak sensitive data.
Kotlin
230
star
6

iconathon

An icon task runner that convert Sketch files to mobile and web formats.
CoffeeScript
219
star
7

power-up-template

A static GitHub pages hosted sample Power-Up
JavaScript
123
star
8

trello-ios-assisted-onboarding

This project is a simple iOS App that hosts the Trello iOS Assisted Onboarding screens.
Swift
44
star
9

node-dependencies

Check out-of-date dependencies for your Node.js app
JavaScript
34
star
10

chromello

A sample Chrome extension written for Trello with a few great features.
JavaScript
32
star
11

api-docs

The documentation site for the Build with Trello content
27
star
12

weather-power-up

A small sample Power-Up for Trello that shows weather data on cards
JavaScript
21
star
13

trellicolors

Converts the Trello brand colors to various formats.
CoffeeScript
19
star
14

category-theory

sometimes math is just too much fun
19
star
15

node-coffee-cache

Caches the contents of required CoffeeScript files so that they are not recompiled to help improve startup time
JavaScript
18
star
16

full-name-splitter

Attempts to split a Latinesque fullname into first name and last name components
JavaScript
17
star
17

glitch-trello-power-up

Example Glitch Project Using Many Power-up Capabilities
JavaScript
15
star
18

glitch-power-up-tutorial-part-one

JavaScript
12
star
19

node-coffee-backtrace

Give some context to uncaught exceptions for Node.js projects written in CoffeeScript
JavaScript
7
star
20

diplomat

A Slack bot for making international collaboration and communication more seamless.
JavaScript
6
star
21

support-team-bookmarklets

Some bookmarklets you can run while using Trello
JavaScript
6
star
22

hearsay

A library for observing things
CoffeeScript
6
star
23

staunton

The massive multiplayer Chess game slash ✨ ReactiveCocoa tutorial ✨
Objective-C
6
star
24

magellan

Mapping and routing for REST endpoints
Objective-C
4
star
25

trello.cards

Less
3
star
26

power-up-on-heroku

A simple Trello Power-Up hosted on Heroku.
JavaScript
3
star
27

code-snippets

Trello Code Snippets Power-Up
JavaScript
3
star
28

yeoman-generator-trello

A Yeoman generator for quickly getting started with the Trello API.
JavaScript
2
star
29

url-parse-fix-auth

Fix url.parse to work with percent (%) characters in auth strings
JavaScript
2
star