Skip to content

Commit

Permalink
Move opticks readme and create root readme (#89)
Browse files Browse the repository at this point in the history
* move readme

* add monorepo readme

* add changeset
  • Loading branch information
dale-french authored Jul 25, 2024
1 parent 2cbd454 commit 7fb5dee
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 213 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-phones-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'opticks': patch
---

Add opticks readme
253 changes: 40 additions & 213 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,236 +1,63 @@
Feature toggles and A/B tests are wonderful, but can easily introduce debt in
your codebase.
# Opticks Monorepo

This project allows you to experiment and automatically clean up your JavaScript
experimentation code after the experiment concludes using JSCodeShift codemods.
Welcome to the Opticks monorepo! This repository includes three main packages:

The library consists of two related concepts:
- **lib:** (https://www.npmjs.com/package/opticks)
- **cli:** (https://www.npmjs.com/package/opticks-cli)
- **eslint-plugin:** (https://www.npmjs.com/package/eslint-plugin-opticks)

- The Toggle library, a simple API for introducing toggles and A/B tests in your
codebase
- Codemods for cleaning up toggle code
Each package has its own directory and corresponding README file with specific instructions and documentation.

# Toggle Library
## Table of Contents

At the heart of our experimentation framework is the `toggle` function.
- [Opticks Monorepo](#opticks-monorepo)
- [Table of Contents](#table-of-contents)
- [Setup](#setup)
- [Building and Testing](#building-and-testing)
- [Publishing Packages](#publishing-packages)

A toggle allows you to switch between multiple experiment variants (a/b/c/...)
and also turn functionality on or off (feature flags)
## Setup

It can be used in a variety of ways:
To get started with the Opticks monorepo, ensure you have the following prerequisites installed:

1. Reading the value of the toggle (boolean, or a/b/c for multi toggles )
1. Execute code or for a variant of a multi toggle
1. Execute code when a boolean toggle is on
- Node.js (version 20.8.0)
- Yarn

We use React at vio.com and some of the code examples use JSX, but the code
and concept is compatible with any front-end framework or architecture.
Clone the repository and install dependencies:

### Opticks vs other experimentation frameworks

The main reason for using the Opticks library is to be able to clean your code
afterwards by providing a predictable experimentation API.

We don't intend to reinvent the wheel and aim to keep it easy to integrate
existing frameworks and services such as Optimizely, LaunchDarkly and
Conductrics behind a simple facade.

## Usage and Integrations

Currently Opticks has two 'integrations' or adapters, a simple one based on a
local key/value store, and one wrapping the Optimizely Full Stack SDK. Once
initialized, using/consuming the toggle decisions is the same.

In the future the integrations will be packaged separately so you can include
the one you need, for now the "simple" is the default and the Optimizely adapter
can be included directly via:
`import Opticks from 'opticks/lib/optimizely'`

## Integrations

### Simple

See the [Simple integration documentation](docs/simple-integration.md).

### Optimizely

See the [Optimizely integration documentation](docs/optimizely-integration.md).

## Toggles

Toggles can be used to implement a/b/c style MVT testing and on/off feature flags as well.
We specify multiple variants of which only one is active at any time.
By convention the variants are named `a` (control), `b`, `c` etc.

### Reading values

While it's recommended to use the strategy described in
[executing code for variants](#executing-code-for-variants), the following shows
how the variant executing and code clean up works under the hood.

The simplest signature is as follows, to read the toggle value directly:

```
toggle(experimentId: string) => { variant: 'a' | 'b' | 'c' | 'd' | ... }
```

For example when the user is assigned to the `b` side:

```
const fooResult = toggle('foo')
if (fooResult === 'b') console.log('b side of the foo experiment')
```

This works fine, but will result in code that will be hard to clean up
automatically. Considering the codemods replace the toggle with the 'winning'
value (`b` in this case):

```
const fooResult = 'b'
if (fooResult === 'b') console.log('b variant of the foo experiment')
```

This would leave your code more messy than necessary. You could skip the
intermediate value but it will still result in awkward leftover code:

```
if (toggle('foo') === 'b') ...
// becomes
if ('b' === 'b') ...
// or
if ('a' === 'b') ...
```bash
git clone https://github.com/viodotcom/opticks.git
cd opticks
yarn install
```

Rather than reading the return value directly, you can map your variant to
arguments to the toggle function, like so:
## Building and Testing

```
// Defines the result if that toggle or experiment wins, either a function that
// will be executed, or any other value
type ToggleResultType = function | any
// do something for a/b/c variant:
toggle(
experimentId,
variantA: ToggleResultType,
variantB: ToggleResultType,
?variantC: ToggleResultType,
...
)
```
To build and test all packages, you can use the following Yarn commands:

The signature might look more complicated, but it allows you to define what the
results for your variants a, b, c etc map to. The first value is the
experimentId, then the values for `a`, `b`, etc.

For instance:
```bash
# Build all packages
yarn workspaces foreach -A run build

# Test all packages
yarn workspaces foreach -A run test
```
// simple boolean switch
const shouldDoSomething = toggle('foo', false, true)

// multiple variants as strings
// 'black' is the default, red and green are variants to experiment with
const buttonColor = toggle('foo', 'black', 'green', 'red')
```
## Publishing Packages

The benefit is that after concluding your experiment, you can integrate the
winning variation's code directly without awkward references to `a` or `b` etc.
To publish a package after making changes, follow these steps:

After you run the codemods to declare `b` the winner, the corresponding raw
value is kept:
1. **Generate a Changeset:**
After making changes to a package, run the following command to generate a changeset:

```
const shouldDoSomething = toggle('foo', false, true)
const buttonColor = toggle('foo', 'black', 'green', 'red')
```bash
npx changeset
```

// becomes:
const shouldDoSomething = true
const buttonColor = 'green'
```

Much better already, but there is more room for improvement, especially if you
want to do things conditionally for a variant.

Consider the following set up:

```
const shouldShowWarning = toggle('shouldShowWarning', false, true)
if (shouldShowWarning) showWarning()
// or directly:
if (toggle('shouldShowWarning', false, true)) showWarning()
```

This would end up with an orphaned conditional after the codemods did their
cleaning:

```
const shouldShowWarning = true
if (shouldShowWarning) showWarning()
// or directly
if (true) showWarning()
```

The next section explains a more useful concept for this type of conditional
branching.

### Executing code for variants

A better approach that allows you to clean the code easier would be to
encapsulate variant logic by executing code from the toggle:

```
const price = 100
const savings = 20
const CTA = toggle(
'CTAWording',
() => `Buy now for just ${price}` // variant a (default)
() => `Buy now for ${price} and save ${savings}`, // variant b
() => `From ${price + savings} to ${price}` // variant c
)
```

Then after running the cleaning codemods when the `b` variant wins:

```
const price = 100
const savings = 20
const CTA = `Buy now for ${price} and save ${savings}`
```

Or for an example of conditionally calling code:

```
alwaysDoSomething()
toggle('shouldShowWarning', null, () => shouldShowWarning())
```

This shows two special concepts of the codemods, passing a `null` and the use of
arrow functions.
Passing `null` allows for full clean up when that branch loses.
For winners, the _body_ of the arrow function is kept as-is.

After running the codemods with the winning `b` variant:

```
alwaysDoSomething()
shouldShowWarning() // no trace this was the result of a winning experiment
```

But if `a` would have won:

```
alwaysDoSomething()
// no trace that something was experimented with but lost
```
Follow the prompts to describe your changes. This will create a changeset file in the `.changeset` directory.

## Removal of dead code
2. **Push Your Changes:**
Push your changes and create a pull request. Once the pull request is merged, a new pull request will be automatically created. This new pull request will remove the changeset file and update the changelog.

Above are just a few examples of how the codemods operate on the code.
For instructions and more recipes, see
[Removal of dead code](docs/dead-code-removal.md).
3. **Merge the Release Pull Request:**
When this automatically created pull request is merged, the relevant package will be published to the npm registry.
Loading

0 comments on commit 7fb5dee

Please sign in to comment.