|
1 | | -Feature toggles and A/B tests are wonderful, but can easily introduce debt in |
2 | | -your codebase. |
| 1 | +# Opticks Monorepo |
3 | 2 |
|
4 | | -This project allows you to experiment and automatically clean up your JavaScript |
5 | | -experimentation code after the experiment concludes using JSCodeShift codemods. |
| 3 | +Welcome to the Opticks monorepo! This repository includes three main packages: |
6 | 4 |
|
7 | | -The library consists of two related concepts: |
| 5 | +- **lib:** (https://www.npmjs.com/package/opticks) |
| 6 | +- **cli:** (https://www.npmjs.com/package/opticks-cli) |
| 7 | +- **eslint-plugin:** (https://www.npmjs.com/package/eslint-plugin-opticks) |
8 | 8 |
|
9 | | -- The Toggle library, a simple API for introducing toggles and A/B tests in your |
10 | | - codebase |
11 | | -- Codemods for cleaning up toggle code |
| 9 | +Each package has its own directory and corresponding README file with specific instructions and documentation. |
12 | 10 |
|
13 | | -# Toggle Library |
| 11 | +## Table of Contents |
14 | 12 |
|
15 | | -At the heart of our experimentation framework is the `toggle` function. |
| 13 | +- [Opticks Monorepo](#opticks-monorepo) |
| 14 | + - [Table of Contents](#table-of-contents) |
| 15 | + - [Setup](#setup) |
| 16 | + - [Building and Testing](#building-and-testing) |
| 17 | + - [Publishing Packages](#publishing-packages) |
16 | 18 |
|
17 | | -A toggle allows you to switch between multiple experiment variants (a/b/c/...) |
18 | | -and also turn functionality on or off (feature flags) |
| 19 | +## Setup |
19 | 20 |
|
20 | | -It can be used in a variety of ways: |
| 21 | +To get started with the Opticks monorepo, ensure you have the following prerequisites installed: |
21 | 22 |
|
22 | | -1. Reading the value of the toggle (boolean, or a/b/c for multi toggles ) |
23 | | -1. Execute code or for a variant of a multi toggle |
24 | | -1. Execute code when a boolean toggle is on |
| 23 | +- Node.js (version 20.8.0) |
| 24 | +- Yarn |
25 | 25 |
|
26 | | -We use React at vio.com and some of the code examples use JSX, but the code |
27 | | -and concept is compatible with any front-end framework or architecture. |
| 26 | +Clone the repository and install dependencies: |
28 | 27 |
|
29 | | -### Opticks vs other experimentation frameworks |
30 | | - |
31 | | -The main reason for using the Opticks library is to be able to clean your code |
32 | | -afterwards by providing a predictable experimentation API. |
33 | | - |
34 | | -We don't intend to reinvent the wheel and aim to keep it easy to integrate |
35 | | -existing frameworks and services such as Optimizely, LaunchDarkly and |
36 | | -Conductrics behind a simple facade. |
37 | | - |
38 | | -## Usage and Integrations |
39 | | - |
40 | | -Currently Opticks has two 'integrations' or adapters, a simple one based on a |
41 | | -local key/value store, and one wrapping the Optimizely Full Stack SDK. Once |
42 | | -initialized, using/consuming the toggle decisions is the same. |
43 | | - |
44 | | -In the future the integrations will be packaged separately so you can include |
45 | | -the one you need, for now the "simple" is the default and the Optimizely adapter |
46 | | -can be included directly via: |
47 | | -`import Opticks from 'opticks/lib/optimizely'` |
48 | | - |
49 | | -## Integrations |
50 | | - |
51 | | -### Simple |
52 | | - |
53 | | -See the [Simple integration documentation](docs/simple-integration.md). |
54 | | - |
55 | | -### Optimizely |
56 | | - |
57 | | -See the [Optimizely integration documentation](docs/optimizely-integration.md). |
58 | | - |
59 | | -## Toggles |
60 | | - |
61 | | -Toggles can be used to implement a/b/c style MVT testing and on/off feature flags as well. |
62 | | -We specify multiple variants of which only one is active at any time. |
63 | | -By convention the variants are named `a` (control), `b`, `c` etc. |
64 | | - |
65 | | -### Reading values |
66 | | - |
67 | | -While it's recommended to use the strategy described in |
68 | | -[executing code for variants](#executing-code-for-variants), the following shows |
69 | | -how the variant executing and code clean up works under the hood. |
70 | | - |
71 | | -The simplest signature is as follows, to read the toggle value directly: |
72 | | - |
73 | | -``` |
74 | | -toggle(experimentId: string) => { variant: 'a' | 'b' | 'c' | 'd' | ... } |
75 | | -``` |
76 | | - |
77 | | -For example when the user is assigned to the `b` side: |
78 | | - |
79 | | -``` |
80 | | -const fooResult = toggle('foo') |
81 | | -if (fooResult === 'b') console.log('b side of the foo experiment') |
82 | | -``` |
83 | | - |
84 | | -This works fine, but will result in code that will be hard to clean up |
85 | | -automatically. Considering the codemods replace the toggle with the 'winning' |
86 | | -value (`b` in this case): |
87 | | - |
88 | | -``` |
89 | | -const fooResult = 'b' |
90 | | -if (fooResult === 'b') console.log('b variant of the foo experiment') |
91 | | -``` |
92 | | - |
93 | | -This would leave your code more messy than necessary. You could skip the |
94 | | -intermediate value but it will still result in awkward leftover code: |
95 | | - |
96 | | -``` |
97 | | -if (toggle('foo') === 'b') ... |
98 | | -
|
99 | | -// becomes |
100 | | -if ('b' === 'b') ... |
101 | | -// or |
102 | | -if ('a' === 'b') ... |
| 28 | +```bash |
| 29 | +git clone https://github.com/viodotcom/opticks.git |
| 30 | +cd opticks |
| 31 | +yarn install |
103 | 32 | ``` |
104 | 33 |
|
105 | | -Rather than reading the return value directly, you can map your variant to |
106 | | -arguments to the toggle function, like so: |
| 34 | +## Building and Testing |
107 | 35 |
|
108 | | -``` |
109 | | -// Defines the result if that toggle or experiment wins, either a function that |
110 | | -// will be executed, or any other value |
111 | | -type ToggleResultType = function | any |
112 | | -
|
113 | | -// do something for a/b/c variant: |
114 | | -toggle( |
115 | | - experimentId, |
116 | | - variantA: ToggleResultType, |
117 | | - variantB: ToggleResultType, |
118 | | - ?variantC: ToggleResultType, |
119 | | - ... |
120 | | -) |
121 | | -``` |
| 36 | +To build and test all packages, you can use the following Yarn commands: |
122 | 37 |
|
123 | | -The signature might look more complicated, but it allows you to define what the |
124 | | -results for your variants a, b, c etc map to. The first value is the |
125 | | -experimentId, then the values for `a`, `b`, etc. |
126 | | - |
127 | | -For instance: |
| 38 | +```bash |
| 39 | +# Build all packages |
| 40 | +yarn workspaces foreach -A run build |
128 | 41 |
|
| 42 | +# Test all packages |
| 43 | +yarn workspaces foreach -A run test |
129 | 44 | ``` |
130 | | -// simple boolean switch |
131 | | -const shouldDoSomething = toggle('foo', false, true) |
132 | 45 |
|
133 | | -// multiple variants as strings |
134 | | -// 'black' is the default, red and green are variants to experiment with |
135 | | -const buttonColor = toggle('foo', 'black', 'green', 'red') |
136 | | -``` |
| 46 | +## Publishing Packages |
137 | 47 |
|
138 | | -The benefit is that after concluding your experiment, you can integrate the |
139 | | -winning variation's code directly without awkward references to `a` or `b` etc. |
| 48 | +To publish a package after making changes, follow these steps: |
140 | 49 |
|
141 | | -After you run the codemods to declare `b` the winner, the corresponding raw |
142 | | -value is kept: |
| 50 | +1. **Generate a Changeset:** |
| 51 | + After making changes to a package, run the following command to generate a changeset: |
143 | 52 |
|
144 | | -``` |
145 | | -const shouldDoSomething = toggle('foo', false, true) |
146 | | -const buttonColor = toggle('foo', 'black', 'green', 'red') |
| 53 | + ```bash |
| 54 | + npx changeset |
| 55 | + ``` |
147 | 56 |
|
148 | | -// becomes: |
149 | | -const shouldDoSomething = true |
150 | | -const buttonColor = 'green' |
151 | | -``` |
152 | | - |
153 | | -Much better already, but there is more room for improvement, especially if you |
154 | | -want to do things conditionally for a variant. |
155 | | - |
156 | | -Consider the following set up: |
157 | | - |
158 | | -``` |
159 | | -const shouldShowWarning = toggle('shouldShowWarning', false, true) |
160 | | -if (shouldShowWarning) showWarning() |
161 | | -
|
162 | | -// or directly: |
163 | | -if (toggle('shouldShowWarning', false, true)) showWarning() |
164 | | -``` |
165 | | - |
166 | | -This would end up with an orphaned conditional after the codemods did their |
167 | | -cleaning: |
168 | | - |
169 | | -``` |
170 | | -const shouldShowWarning = true |
171 | | -if (shouldShowWarning) showWarning() |
172 | | -
|
173 | | -// or directly |
174 | | -if (true) showWarning() |
175 | | -``` |
176 | | - |
177 | | -The next section explains a more useful concept for this type of conditional |
178 | | -branching. |
179 | | - |
180 | | -### Executing code for variants |
181 | | - |
182 | | -A better approach that allows you to clean the code easier would be to |
183 | | -encapsulate variant logic by executing code from the toggle: |
184 | | - |
185 | | -``` |
186 | | -const price = 100 |
187 | | -const savings = 20 |
188 | | -
|
189 | | -const CTA = toggle( |
190 | | - 'CTAWording', |
191 | | - () => `Buy now for just ${price}` // variant a (default) |
192 | | - () => `Buy now for ${price} and save ${savings}`, // variant b |
193 | | - () => `From ${price + savings} to ${price}` // variant c |
194 | | -) |
195 | | -``` |
196 | | - |
197 | | -Then after running the cleaning codemods when the `b` variant wins: |
198 | | - |
199 | | -``` |
200 | | -const price = 100 |
201 | | -const savings = 20 |
202 | | -
|
203 | | -const CTA = `Buy now for ${price} and save ${savings}` |
204 | | -``` |
205 | | - |
206 | | -Or for an example of conditionally calling code: |
207 | | - |
208 | | -``` |
209 | | -alwaysDoSomething() |
210 | | -toggle('shouldShowWarning', null, () => shouldShowWarning()) |
211 | | -``` |
212 | | - |
213 | | -This shows two special concepts of the codemods, passing a `null` and the use of |
214 | | -arrow functions. |
215 | | -Passing `null` allows for full clean up when that branch loses. |
216 | | -For winners, the _body_ of the arrow function is kept as-is. |
217 | | - |
218 | | -After running the codemods with the winning `b` variant: |
219 | | - |
220 | | -``` |
221 | | -alwaysDoSomething() |
222 | | -shouldShowWarning() // no trace this was the result of a winning experiment |
223 | | -``` |
224 | | - |
225 | | -But if `a` would have won: |
226 | | - |
227 | | -``` |
228 | | -alwaysDoSomething() |
229 | | -// no trace that something was experimented with but lost |
230 | | -``` |
| 57 | + Follow the prompts to describe your changes. This will create a changeset file in the `.changeset` directory. |
231 | 58 |
|
232 | | -## Removal of dead code |
| 59 | +2. **Push Your Changes:** |
| 60 | + 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. |
233 | 61 |
|
234 | | -Above are just a few examples of how the codemods operate on the code. |
235 | | -For instructions and more recipes, see |
236 | | -[Removal of dead code](docs/dead-code-removal.md). |
| 62 | +3. **Merge the Release Pull Request:** |
| 63 | + When this automatically created pull request is merged, the relevant package will be published to the npm registry. |
0 commit comments