Skip to content

Commit

Permalink
Add A/B Tests and Feature Flags example (#13629)
Browse files Browse the repository at this point in the history
## Summary
This PR adds a basic example of how [Tesfy](https://tesfy.io/) could be integrated with Next.js. Tesfy is a project that I've working on during quarantine weekends, mainly to learn new stuff and provide **free** and **unlimited** A/B Tests and Feature Flags while keeping a good performance and the library [size](https://bundlephobia.com/[email protected]) as small as possible.

The configuration file could be set up using a [web application](https://app.tesfy.io/) (hosted in Vercel 🎉 ) or by your self.

## Implementation
- Created `with-tesfy` folder
- Added two pages `index.js` and `features.js` to show how experiments and features could be used
- The only thing that must be persisted is the `userId`. Used a cookie to save it.
- Uses `getServerSideProps` to fetch the configuration file and get/create the `userId`.

## Screenshots

There are some screenshots from the web application. Where you can easily configure experiments and audiences per project. Teams and features will soon be added.

![Screenshot 2020-06-01 at 15 40 49](https://user-images.githubusercontent.com/6877967/83414811-60e7ce80-a41e-11ea-9e5c-887c66e80c65.png)
![Screenshot 2020-06-01 at 15 41 02](https://user-images.githubusercontent.com/6877967/83414823-66451900-a41e-11ea-885b-b58e78b042bb.png)
![Screenshot 2020-06-01 at 15 41 11](https://user-images.githubusercontent.com/6877967/83414828-6a713680-a41e-11ea-90a8-8d39a17f19a1.png)

This is my first PR! sorry if I made something wrong 😞 . Any feedback is more than welcome. Also I want to thank you all for the awesome work with Next.js ❤️
  • Loading branch information
andresz1 authored Jun 21, 2020
1 parent ee1cf6f commit 6424e1f
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 3 deletions.
2 changes: 1 addition & 1 deletion examples/cms-agilitycms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Once you have access to [the environment variables you'll need](#step-15-set-up-
Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
npm init next-app --example cms-agilitycms cms-agilitycms-app
npx create-next-app --example cms-agilitycms cms-agilitycms-app
# or
yarn create next-app --example cms-agilitycms cms-agilitycms-app
```
Expand Down
2 changes: 1 addition & 1 deletion examples/cms-cosmic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Once you have access to [the environment variables you'll need](#step-3-set-up-e
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
npm init next-app --example cms-cosmic cms-cosmic-app
npx create-next-app --example cms-cosmic cms-cosmic-app
# or
yarn create next-app --example cms-cosmic cms-cosmic-app
```
Expand Down
2 changes: 1 addition & 1 deletion examples/cms-graphcms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Once you have access to [the environment variables you'll need](#step-3-set-up-e
Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
npm init next-app --example cms-graphcms cms-graphcms-app
npx create-next-app --example cms-graphcms cms-graphcms-app
# or
yarn create next-app --example cms-graphcms cms-graphcms-app
```
Expand Down
50 changes: 50 additions & 0 deletions examples/with-tesfy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Tesfy Example

[Tesfy](https://tesfy.io/) allows you to create **unlimited** A/B Tests and Feature Flags for **free** using a [web app](https://app.tesfy.io/) or by your self.

This example shows how to integrate [react-tesfy](https://github.com/andresz1/react-tesfy) in Next.js.

To use Tesfy there are only two mandatory things needed. A `userId` and a configuration file known as `datafile`. In the `_app.js` you will notice that those are being get.

The `userId` must uniquely identify a user even if not logged in, for that reason a [uuid](https://en.wikipedia.org/wiki/Universally_unique_identifier) is created and stored in a cookie so the next time a page is requested a new `userId` won't be created, instead the cookie one will be used.

The `datafile` is just a `json` that defines the configuration of the experiments and features avaliable. It must be fetched from Tesfy CDN or from your own servers at least everytime a request is performed, later on this configuration could also be fetched if wanted (e.g. during page transitions).

## Deploy your own

Deploy the example using [Vercel](https://vercel.com):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/with-tesfy)

## How to use

### Using `create-next-app`

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
npx create-next-app --example with-tesfy with-tesfy-app
# or
yarn create next-app --example with-tesfy with-tesfy-app
```

### Download manually

Download the example:

```bash
curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-tesfy
cd with-tesfy
```

Install it and run:

```bash
npm install
npm run dev
# or
yarn
yarn dev
```

Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
16 changes: 16 additions & 0 deletions examples/with-tesfy/components/nav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Link from 'next/link'

const Nav = () => {
return (
<nav>
<Link href="/">
<a>Experiments</a>
</Link>{' '}
<Link href="/features">
<a>Features</a>
</Link>
</nav>
)
}

export default Nav
19 changes: 19 additions & 0 deletions examples/with-tesfy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "with-tesfy",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-tesfy": "latest",
"tesfy": "latest",
"cookie": "0.4.1",
"uuid": "8.1.0"
},
"license": "ISC"
}
26 changes: 26 additions & 0 deletions examples/with-tesfy/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'
import { TesfyProvider, createInstance } from 'react-tesfy'
import Nav from '../components/nav'

const App = ({ Component, pageProps }) => {
const { datafile = {}, userId } = pageProps
const engine = createInstance({
datafile,
userId,
})

return (
<TesfyProvider engine={engine}>
<Nav />
<main>
<p>
Your user identifier is <b>{engine.getUserId()}</b>. Delete{' '}
<b>user_id</b> cookie and refresh to get a new one
</p>
<Component {...pageProps} />
</main>
</TesfyProvider>
)
}

export default App
23 changes: 23 additions & 0 deletions examples/with-tesfy/pages/features.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Feature } from 'react-tesfy'
import { getTesfyProps } from '../utils'

export const getServerSideProps = getTesfyProps(async () => {
return { props: {} }
})

const FeaturesPage = () => {
return (
<>
<h1>Features</h1>

<section>
<h2>Feature 1 - Allocation</h2>
<Feature id="feature-1">
{(isEnabled) => (isEnabled ? <p>Enabled</p> : <p>Disabled</p>)}
</Feature>
</section>
</>
)
}

export default FeaturesPage
43 changes: 43 additions & 0 deletions examples/with-tesfy/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Experiment, Variation } from 'react-tesfy'
import { getTesfyProps } from '../utils'

export const getServerSideProps = getTesfyProps(async () => {
return { props: {} }
})

const IndexPage = () => {
return (
<>
<h1>Experiments</h1>

<section>
<h2>Experiment 1 - Allocation</h2>
<Experiment id="experiment-1">
<Variation>
<p style={{ color: 'yellow' }}>Yellow</p>
</Variation>
<Variation id="0">
<p style={{ color: 'blue' }}>Blue</p>
</Variation>
<Variation id="1">
<p style={{ color: 'red' }}>Red</p>
</Variation>
</Experiment>
</section>

<section>
<h2>Experiment 2 - Audience</h2>
<Experiment id="experiment-2" attributes={{ countryCode: 'us' }}>
<Variation>
<p style={{ fontWeight: 'bold' }}>Bold</p>
</Variation>
<Variation id="0">
<p>Normal</p>
</Variation>
</Experiment>
</section>
</>
)
}

export default IndexPage
42 changes: 42 additions & 0 deletions examples/with-tesfy/utils/getDatafile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Fetch your configuration from tesfy cdn or your own server
const getDatafile = () => {
return {
experiments: {
'experiment-1': {
id: 'experiment-1',
percentage: 90,
variations: [
{
id: '0',
percentage: 50,
},
{
id: '1',
percentage: 50,
},
],
},
'experiment-2': {
id: 'experiment-2',
percentage: 100,
variations: [
{
id: '0',
percentage: 100,
},
],
audience: {
'==': [{ var: 'countryCode' }, 'us'],
},
},
},
features: {
'feature-1': {
id: 'feature-1',
percentage: 50,
},
},
}
}

export default getDatafile
13 changes: 13 additions & 0 deletions examples/with-tesfy/utils/getTesfyProps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import getDatafile from './getDatafile'
import getUserId from './getUserId'

const getTesfyProps = (getServerSideProps) => async (ctx) => {
const datafile = getDatafile()
const userId = getUserId(ctx)

const data = (await getServerSideProps(ctx)) || {}

return { ...data, props: { ...data.props, datafile, userId } }
}

export default getTesfyProps
31 changes: 31 additions & 0 deletions examples/with-tesfy/utils/getUserId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import cookie from 'cookie'
import { v4 as uuidv4 } from 'uuid'

const COOKIE_NAME = 'user_id'
const COOKIE_MAX_AGE = 2147483647

const getUserId = (ctx) => {
const { req, res } = ctx

const cookies = cookie.parse(req.headers.cookie ?? '')
let userId = cookies[COOKIE_NAME]

if (!userId) {
userId = uuidv4()
res.setHeader(
'Set-Cookie',
cookie.serialize(COOKIE_NAME, userId, {
maxAge: COOKIE_MAX_AGE,
expires: new Date(Date.now() + COOKIE_MAX_AGE),
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
})
)
}

return userId
}

export default getUserId
3 changes: 3 additions & 0 deletions examples/with-tesfy/utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as getDatafile } from './getDatafile'
export { default as getUserId } from './getUserId'
export { default as getTesfyProps } from './getTesfyProps'

0 comments on commit 6424e1f

Please sign in to comment.