npm package: @kiwicom/darwin
Description: AB testing tool 2.0. :monkey:
Author: oreqizer
Latest version: 0.6.6

# Darwin

Our shiny new AB testing tool! :monkey:

```sh
yarn add @kiwicom/darwin
```

## API

Download the test configuration file. On a request take:

- the `config` object
- request's `userAgent`
- request's language code in _ISO 639-1_ with the country postfix — `xx-XX`
- some salt — usually user's ID

### Config

The configuration object is either a JSON file or a JS object with the following structure:

```ts
// A single AB test
export type Test = {
  name: string; // The test's name
  value: string; // The value string
};

// One of AB test's treatment group configurations
export type Value = {
  value: string; // The treatment group name
  weight: number; // The relative traffic size of the treatment group
};

// Traffic group configuration
export type Traffic = {
  userAgent?: string | null; // Pattern against which to compare the user-agent
  languages?: string[] | null; // List of accpeted languages
  affiliates?: string[] | null; // List of accepted affiliates
};

// Configuration of an AB test
export type TestConfig = {
  name: string; // The AB test name
  values: Value[]; // List of treatment group configurations
};

// Population group with an assigned AB test, or `null` for placeholder slot
export type Group = {
  traffic?: Traffic; // The group's traffic configuration
  tests: TestConfig[]; // The test configurations for this population group
};

// Feature configuration
export type Feature = {
  traffic?: Traffic; // The feature's traffic configuration
  name: string; // The feature's name
  enabled: boolean; // Flag indicating whether the feature is enabled or not
};

// A record of enabled/disabled features
export type Features = Record<string, boolean>;

// The root configuration object
export type Config = {
  features?: Feature[]; // The feature configuration
  groups?: Group[]; // Population group configurations
  winners?: Test[]; // Winning tests and their values
};
```

An example `Traffic` object containing the English and Russian language, and is iPhone only, for
`shrek` and `donkey` affiliates:

```json
{
  "languages": ["en-US", "ru-RU"],
  "userAgent": "iPhone",
  "affiliates": ["shrek", "donkey"]
}
```

The language list is the `639-1`column [here](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
.
Be sure to include the `-XX` country postfix, as Kiwi.com recognizes different countries'
language variants.

For user agents, see [this website](https://developers.whatismybrowser.com/). Pick a chunk of
the user agent you want and put it in the `userAgent` field in the traffic object, like in the
example.

### `getTests`

Feed the `config` object function together with the `userAgent`, language, affiliate and the salt:

```ts
import { Config, Test, getTest } from "@kiwicom/darwin";
import { Cookies } from "@kiwicom/cookies";

const config: Config = {
  /* ... */
};

// Returns the group's name and the `Test` array, or
// `null` group name and empty array for no active test
const { group, tests } = getTests({
  config,
  userAgent: req.headers["User-Agent"], // optional
  language: "en-GB", // optional
  affiliate: "shrek", // optional
  salt: "some-user-id-string", // length at least 10 for optimal results
  savedGroup: req.cookies[Cookies.DARWIN_GROUP], // optional, saved group for the user
});

// optionally, save/remove test name from storage
if (group === null) {
  // remove `group` from e.g. cookies
} else {
  // save `group` to e.g. cookies
}
```

This object has no functions and thus is serializable — send it to the client encoded as JSON.

### `getFeatures`

Feed the `config` object function, optionally together with the `userAgent`, language and affiliate:

```ts
import { Config, Features, getFeatures } from "@kiwicom/darwin";

const config: Config = {
  /* ... */
};

const features: Features = getFeatures({
  config,
  userAgent: req.headers["User-Agent"], // optional
  language: "en-GB", // optional
  affiliate: "shrek", // optional
});
```

This object has no functions and thus is serializable — send it to the client encoded as JSON.

### `DarwinProvider`

**Server:**

Use the `tests` array in your provider:

```tsx
import { DarwinProvider } from "@kiwicom/darwin";

// ...
return ReactDOMServer.renderToString(
  <DarwinProvider tests={tests} features={features} winners={winners}>
    <Root />
  </DarwinProvider>,
);
```

**Client:**

Take the `tests` and `features` sent from the server and feed it to the provider.

> Avoid sending the whole downloaded configuration file to the client to save bundle size and avoid
> leaking test information.

```tsx
import { DarwinProvider } from "@kiwicom/darwin";

import { handleLogDarwin } from "src/logger";

const { tests, features, winners } = window.__DARWIN__;

// ...
ReactDOM.hydrate(
  <DarwinProvider tests={tests} features={features} winners={winners} onTest={handleLogDarwin}>
    <Root />
  </DarwinProvider>,
  document.getElementById("root"),
);
```

### `useTest` hook

Checks if the queried test is running and returns its value, `null` for reference group.

It also saves the queried test to session data if it returned a non-`null` result.

```ts
import { useTest } from "@kiwicom/darwin";

const version = useTest("valdoge");
if (version === "g") {
  // ...
}
```

You can then retrieve the session data with `loadSession`;

### `useFeature` hook

Tells you if a feature is on / off.

```ts
import { useFeature } from "@kiwicom/darwin";

const hasNavbar = useFeature("navbar");
```

### `context`

If you cannot use React's hook API, you can also access the context directly:

```tsx
import { context as contextDarwin } from "@kiwicom/darwin";

class MyComponent extends Component {
  static contextType = contextDarwin;

  handleDarwin() {
    // call these as you'd call the `useTest` and `useFeature` hooks
    const { onTest, onFeature } = this.context;
  }
}
```

### `loadSession`

Loads the session test data, a `Record<string, string>` of active
record names and values.

> Only use on the client!

Useful mainly for logging.

```ts
import { loadSession } from "@kiwicom/darwin";

logger.log("Stuff has happened", {
  abTests: loadSession(),
});
```

## Testing

Use the `@kiwicom/darwin/mock` module for testing purposes.

### `mockFormat`

Appends the URL with specified testing parameters and returns
the formatted URL search:

```ts
import { mockFormat } from "@kiwicom/darwin/mock";

window.location.search = mockFormat([{ name: "yolotest", value: "B" }]);
```

Takes an optional third parameter as the current URL search that defaults
to `window.location.search`.

### `mockRetrieve`

Retrieves mock tests from the URL search.

```ts
import { mockRetrieve } from "@kiwicom/darwin/mock";

const mockTests = mockRetrieve(url.search);
const { tests } = getTests({
  /* ... */
});

const allTests = [...mockTests, ...tests];
```

### `mockFeatureFormat`

Appends the URL with specified features and returns the formatted URL
search:

```ts
import { mockFeatureFormat } from "@kiwicom/darwin/mock";

window.location.search = mockFeatureFormat({ navbar: true, footer: false });
```

Takes an optional second parameter as the current URL search that defaults
to `window.location.search`.

### `mockFeatureRetrieve`

Retrieves the mock features from the URL search. Merge the mock feature
set with features from the config object:

```ts
import { mockFeatureRetrieve } from "@kiwicom/darwin/mock";

const features = {
  ...getFeatures({
    /* ... */
  }),
  ...mockFeatureRetrieve(url.search),
};
```

### `mockWinnersFormat`

Appends the URL with specified test winners and returns the formatted URL
search:

```ts
import { mockWinnersFormat } from "@kiwicom/darwin/mock";

window.location.search = mockWinnersFormat([{ name: "test", value: "off" }]);
```

> Note that the function uses `__` for grouping test name and value. Please
> avoid using double-underscore in test names or values!

Takes an optional second parameter as the current URL search that defaults
to `window.location.search`.

### `mockWinnersRetrieve`

Retrieves the mock test winners from the URL search. Merge the test winners
set with winners from the config object:

```ts
import { mockWinnersRetrieve } from "@kiwicom/darwin/mock";

const winners = [...config.winners, ...mockWinnersRetrieve(url.search)];
```

## Development

Clone and `yarn`.

### Commits

Follow `@commitlint/conventional` with a mandatory **scope** of:

- `dev` for non-production things like CI or tests
- `types` for adjusting `.js.flow` files or type signatures
- `src` for library changes, features, patches...

Examples:

- `docs(dev): document commits`
- `feat(src): add new hook`
- `chore(types): new spread syntax`

### Release

- `yarn release`
- `yarn publish`

## License

MIT