mirror of
https://github.com/growthbook/growthbook.git
synced 2021-08-07 14:23:53 +03:00
Add docs site to monorepo (#3)
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
# build artifacts
|
||||
dist
|
||||
coverage
|
||||
# data definition files
|
||||
**/*.d.ts
|
||||
coverage
|
||||
@@ -14,7 +14,7 @@
|
||||
"react",
|
||||
"@typescript-eslint",
|
||||
"prettier",
|
||||
"@next/eslint-plugin-next",
|
||||
"@next/eslint-plugin-next"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -33,13 +33,10 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: lint, build
|
||||
- name: lint
|
||||
run: |
|
||||
yarn lint
|
||||
# Do a full build of the back-end since it's used later to build the Docker container
|
||||
yarn workspace back-end build
|
||||
# Just do a type-check for the front-end. The build for prod is done in another job.
|
||||
yarn workspace front-end type-check
|
||||
yarn type-check
|
||||
|
||||
- name: test
|
||||
run: |
|
||||
@@ -91,6 +88,9 @@ jobs:
|
||||
ECR_REPOSITORY: api
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
# Build the back-end
|
||||
yarn workspace back-end build
|
||||
|
||||
# Move into the back-end package and add the root yarn.lock
|
||||
cp yarn.lock packages/back-end/yarn.lock
|
||||
cd packages/back-end
|
||||
|
||||
@@ -77,13 +77,14 @@ SITE_MANAGER_EMAIL=admin@example.com
|
||||
|
||||
## Running Growth Book
|
||||
|
||||
This is a monorepo with 2 packages - `back-end` and `front-end`. For ease-of-use, we've added helper scripts at the top level that operate on both simultaneously.
|
||||
This is a monorepo with 3 packages - `back-end`, `front-end`, and `docs`. For ease-of-use, we've added helper scripts at the top level.
|
||||
|
||||
### Development
|
||||
|
||||
- `yarn dev` - Start dev servers with hot reloading
|
||||
- Front-end: http://localhost:3000
|
||||
- Back-end: http://localhost:3100
|
||||
- Docs: http://localhost:3200
|
||||
- `yarn lint` - Run eslint and auto-fix errors when possible
|
||||
- `yarn pretty` - Run prettier across the entire codebase
|
||||
- `yarn type-check` - Check for typescript compile errors
|
||||
@@ -95,10 +96,11 @@ For production, you must first build with Typescript/Webpack and then serve it w
|
||||
|
||||
- `yarn build:front` - Build the front-end and output to `packages/front-end/dist/`
|
||||
- `yarn build:back` - Build the back-end and output to `packages/back-end/dist/`
|
||||
- `yarn build` - Build both production bundles in parallel
|
||||
- `yarn build:docs` - Build the docs and output to `packages/docs/dist/`
|
||||
- `yarn build` - Build everything in parallel
|
||||
- `yarn start:front` - Serve the front-end at http://localhost:3000
|
||||
- `yarn start:back` - Serve the back-end at http://localhost:3100
|
||||
- `yarn start` - Serve both production bundles in parallel
|
||||
- `yarn start:docs` - Serve the docs at http://localhost:3200
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint './**/*.{ts,tsx,js,jsx}' --fix --max-warnings 0",
|
||||
"pretty": "prettier --write ./**/*.{json,css,scss,md}",
|
||||
"pretty": "prettier --write ./**/*.{json,css,scss,md,mdx}",
|
||||
"type-check": "wsrun type-check",
|
||||
"test": "wsrun test",
|
||||
"dev": "wsrun dev",
|
||||
"build": "wsrun build",
|
||||
"build:back": "yarn workspace back-end build",
|
||||
"build:front": "yarn workspace front-end build",
|
||||
"start": "wsrun start",
|
||||
"build:docs": "yarn workspace docs build",
|
||||
"start:back": "yarn workspace back-end start",
|
||||
"start:front": "yarn workspace front-end start",
|
||||
"start:docs": "yarn workspace docs start",
|
||||
"init:dev": "wsrun init:dev"
|
||||
},
|
||||
"workspaces": [
|
||||
|
||||
2
packages/back-end/src/types/jstat.d.ts
vendored
2
packages/back-end/src/types/jstat.d.ts
vendored
@@ -1 +1 @@
|
||||
declare module 'jstat';
|
||||
declare module "jstat";
|
||||
|
||||
5
packages/docs/README.md
Normal file
5
packages/docs/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Growth Book Docs
|
||||
|
||||
How to install, set up, and use Growth Book, the open source A/B testing platform.
|
||||
|
||||
View these hosted docs at https://docs.growthbook.io
|
||||
21
packages/docs/components/TopNav.tsx
Normal file
21
packages/docs/components/TopNav.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function TopNav() {
|
||||
return (
|
||||
<nav className="navbar navbar-expand-lg navbar-light border-bottom bg-light mb-3">
|
||||
<Link href="/">
|
||||
<a className="navbar-brand">
|
||||
<img
|
||||
src="/growth-book-logo.png"
|
||||
alt="Growth Book"
|
||||
style={{ verticalAlign: "middle", marginRight: "10px", height: 40 }}
|
||||
/>
|
||||
<span className="text-muted" style={{ verticalAlign: "bottom" }}>
|
||||
Docs
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
1
packages/docs/mdx.d.ts
vendored
Normal file
1
packages/docs/mdx.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="@mdx-js/loader" />
|
||||
2
packages/docs/next-env.d.ts
vendored
Normal file
2
packages/docs/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
24
packages/docs/next.config.js
Normal file
24
packages/docs/next.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable */
|
||||
const rehypePrism = require("@mapbox/rehype-prism");
|
||||
const addClasses = require("rehype-add-classes");
|
||||
|
||||
// eslint-disable-next-line
|
||||
const withMDX = require("@next/mdx")({
|
||||
extension: /\.mdx?$/,
|
||||
options: {
|
||||
remarkPlugins: [],
|
||||
rehypePlugins: [
|
||||
rehypePrism,
|
||||
[addClasses, {
|
||||
"table": "table table-bordered",
|
||||
"img": "border my-3"
|
||||
}]
|
||||
],
|
||||
},
|
||||
});
|
||||
module.exports = withMDX({
|
||||
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
|
||||
future: {
|
||||
webpack5: true,
|
||||
},
|
||||
});
|
||||
27
packages/docs/package.json
Normal file
27
packages/docs/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "docs",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3200",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3200",
|
||||
"test": "echo 'no tests for docs yet'",
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"init:dev": "touch next.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mapbox/rehype-prism": "^0.6.0",
|
||||
"@mdx-js/loader": "^1.6.22",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@next/mdx": "^10.2.0",
|
||||
"next": "^10.2.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"rehype-add-classes": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.55"
|
||||
}
|
||||
}
|
||||
262
packages/docs/pages/_app.tsx
Normal file
262
packages/docs/pages/_app.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import * as React from "react";
|
||||
import { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import TopNav from "../components/TopNav";
|
||||
import Link from "next/link";
|
||||
|
||||
type ModAppProps = AppProps & {
|
||||
Component: { noOrganization?: boolean; preAuth?: boolean };
|
||||
};
|
||||
|
||||
const navLinks = [
|
||||
{
|
||||
href: "/",
|
||||
name: "Docs Home",
|
||||
},
|
||||
{
|
||||
href: "/app",
|
||||
name: "Growth Book App",
|
||||
links: [
|
||||
{
|
||||
href: "/app/datasources",
|
||||
name: "Data Sources",
|
||||
},
|
||||
{
|
||||
href: "/app/metrics",
|
||||
name: "Metrics",
|
||||
},
|
||||
{
|
||||
href: "/app/experiments",
|
||||
name: "Experiments",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
href: "/lib",
|
||||
name: "Client Libraries",
|
||||
links: [
|
||||
{
|
||||
href: "/lib/js",
|
||||
name: "Javascript",
|
||||
},
|
||||
{
|
||||
href: "/lib/react",
|
||||
name: "React",
|
||||
},
|
||||
{
|
||||
href: "/lib/php",
|
||||
name: "PHP",
|
||||
},
|
||||
{
|
||||
href: "/lib/ruby",
|
||||
name: "Ruby",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
href: "/api-docs",
|
||||
name: "API",
|
||||
},
|
||||
];
|
||||
|
||||
const linksInOrder: { name: string; href: string }[] = [
|
||||
{ name: "Docs Home", href: "/" },
|
||||
];
|
||||
navLinks.forEach((l) => {
|
||||
linksInOrder.push({ name: l.name, href: l.href });
|
||||
if (l.links) {
|
||||
l.links.forEach((l2) => {
|
||||
linksInOrder.push(l2);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function App({
|
||||
Component,
|
||||
pageProps,
|
||||
router,
|
||||
}: ModAppProps): React.ReactElement {
|
||||
let currentIndex = -1;
|
||||
linksInOrder.forEach((l, i) => {
|
||||
if (l.href === router.pathname) {
|
||||
currentIndex = i;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Growth Book Docs</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://cdn.jsdelivr.net/npm/prism-themes@1.7.0/themes/prism-dracula.css"
|
||||
as="style"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://cdn.jsdelivr.net/npm/prism-themes@1.7.0/themes/prism-dracula.css"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
|
||||
as="style"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,400;0,700;1,400&display=swap"
|
||||
as="style"
|
||||
/>{" "}
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,400;0,700;1,400&display=swap"
|
||||
rel="stylesheet"
|
||||
></link>
|
||||
<style>{`
|
||||
body {
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
}
|
||||
pre[class*="language-"] {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
`}</style>
|
||||
<style>{``}</style>
|
||||
</Head>
|
||||
<TopNav />
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-3 col-xl-2">
|
||||
<div className="d-flex d-md-none pb-3 sticky-top border-bottom align-items-center">
|
||||
<div className="mr-2">Jump to:</div>
|
||||
<div className="w-100">
|
||||
<div>
|
||||
<select
|
||||
className="form-control"
|
||||
placeholder="Jump to Section"
|
||||
value={router.pathname}
|
||||
onChange={(e) => {
|
||||
router.push(e.target.value);
|
||||
}}
|
||||
>
|
||||
{navLinks.map((link) => (
|
||||
<React.Fragment key={link.href}>
|
||||
<option value={link.href}>{link.name}</option>
|
||||
{link.links &&
|
||||
link.links.map((sublink) => (
|
||||
<option value={sublink.href} key={sublink.href}>
|
||||
⊢ {sublink.name}
|
||||
</option>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="d-none d-md-block bg-light border rounded sticky-top p-3 mb-3"
|
||||
style={{ maxHeight: "100vh", overflowY: "auto" }}
|
||||
>
|
||||
<h4 className="text-muted">Menu</h4>
|
||||
{navLinks.map((link, i) => {
|
||||
const active = router.pathname === link.href;
|
||||
return (
|
||||
<div key={i}>
|
||||
<div
|
||||
className={`rounded`}
|
||||
style={{
|
||||
backgroundColor: active ? "#ddd" : "",
|
||||
fontWeight: active ? "bold" : "normal",
|
||||
}}
|
||||
>
|
||||
<Link href={link.href}>
|
||||
<a className="p-2 d-block">{link.name}</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{link.links &&
|
||||
link.links.map((sublink, j) => {
|
||||
const active = router.pathname === sublink.href;
|
||||
return (
|
||||
<div
|
||||
className={`rounded ml-3`}
|
||||
key={j}
|
||||
style={{
|
||||
backgroundColor: active ? "#ddd" : "",
|
||||
}}
|
||||
>
|
||||
<Link href={sublink.href}>
|
||||
<a className="p-2 d-block">{sublink.name}</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col pt-2">
|
||||
<div className="d-flex flex-column h-100">
|
||||
<main style={{ flex: 1 }}>
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
{currentIndex >= 0 && (
|
||||
<footer className="mt-4 border-top mb-2">
|
||||
<div className="row p-4">
|
||||
{currentIndex > 0 && (
|
||||
<div className="col-auto">
|
||||
<Link href={linksInOrder[currentIndex - 1].href}>
|
||||
<a>‹ Previous</a>
|
||||
</Link>
|
||||
<span className="d-none text-muted d-md-inline ml-2">
|
||||
({linksInOrder[currentIndex - 1].name})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="col"></div>
|
||||
{currentIndex < linksInOrder.length - 1 && (
|
||||
<div className="col-auto">
|
||||
<span className="d-none text-muted d-md-inline mr-2">
|
||||
({linksInOrder[currentIndex + 1].name})
|
||||
</span>
|
||||
<Link href={linksInOrder[currentIndex + 1].href}>
|
||||
<a>Next ›</a>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center border-top p-4">
|
||||
<a
|
||||
href={`https://github.com/growthbook/growthbook/blob/main/packages/docs/pages${
|
||||
router.pathname
|
||||
}${
|
||||
["/lib", "/app"].includes(router.pathname)
|
||||
? "/index"
|
||||
: ""
|
||||
}${router.pathname === "/" ? "index" : ""}.mdx`}
|
||||
>
|
||||
Edit this page on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
56
packages/docs/pages/api-docs.mdx
Normal file
56
packages/docs/pages/api-docs.mdx
Normal file
@@ -0,0 +1,56 @@
|
||||
# API
|
||||
|
||||
The Growth Book API is pretty easy to integrate into any tech stack.
|
||||
|
||||
To get started, on the Growth Book settings page, generate an API key.
|
||||
|
||||
As of right now, there is only a single public API endpoint: `/config/{apiKey}`. If you are using Managed Growth Book (https://app.growthbook.io), the endpoint is accessible from our global CDN: https://cdn.growthbook.io/config/{apiKey}.
|
||||
|
||||
This returns configuration info for every experiment.
|
||||
It can be passed directly into our client libraries or you can use the info to implement your own variation assignment and targeting.
|
||||
|
||||
Here is an example API response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"overrides": {
|
||||
"experiment-key": {
|
||||
"status": "running",
|
||||
"weights": [0.5, 0.5],
|
||||
"coverage": 1,
|
||||
"targeting": [
|
||||
"age > 18"
|
||||
],
|
||||
"url": "^/post/[0-9]+$"
|
||||
},
|
||||
"another-experiment": {
|
||||
"status": "stopped",
|
||||
"force": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `status` field just mirrors the HTTP status code.
|
||||
|
||||
The `overrides` field has one entry per experiment with overrides that should take precedence over hard-coded values in your code.
|
||||
|
||||
- **status** - Either "draft", "running", or "stopped". Stopped experiments are only included in the response if a non-control variation won.
|
||||
- **weights** - How traffic should be weighted between variations. Will add up to 1.
|
||||
- **coverage** - A float from 0 to 1 (inclusive) which specifies what percent of users to include in the experiment.
|
||||
- **targeting** - An array of targeting rules for who is eligible. There is an implied "AND" between the rules.
|
||||
- **url** - A regex for which URLs the experiment should run on
|
||||
- **force** - Force all users to see the specified variation index (`0` = control, `1` = first variation, etc.).
|
||||
|
||||
|
||||
## Official Client Libraries
|
||||
|
||||
We offer official client libraries that work with these data structures in a few popular languages with more coming soon.
|
||||
|
||||
* [Javascript/Typescript](/lib/js)
|
||||
* [React](/lib/react)
|
||||
* [PHP](/lib/php)
|
||||
* [Ruby](/lib/ruby)
|
||||
* Go - *coming soon*
|
||||
* Python - *coming soon*
|
||||
98
packages/docs/pages/app/datasources.mdx
Normal file
98
packages/docs/pages/app/datasources.mdx
Normal file
@@ -0,0 +1,98 @@
|
||||
# Data Sources
|
||||
|
||||
Data Sources are how Growth Book connects to your analytics tool or data warehouse to automatically pull metrics and experiment results.
|
||||
|
||||
You can use Growth Book without a Data Source, but the user experience is not as smooth since you must enter all data manually.
|
||||
|
||||
Below are the currently supported Data Sources:
|
||||
|
||||
- Redshift
|
||||
- Snowflake
|
||||
- BigQuery
|
||||
- AWS Athena
|
||||
- Postgres
|
||||
- Mixpanel
|
||||
- Google Analytics
|
||||
|
||||
## Configuration Settings
|
||||
|
||||
To effectively use Growth Book, you'll need to tell us a little about the shape of your data.
|
||||
|
||||
### SQL Sources
|
||||
|
||||
The default settings assume you are using Segment to populate your database, but everything is customizeable if that's not the case.
|
||||
|
||||
- Defaults
|
||||
- **timestamp column** - The default timestamp column for all tables
|
||||
- **user id column** - The default column that stores a logged-in user id for all tables
|
||||
- **anonymous id column** - The default column that stores an anonymous id for logged-out users
|
||||
- Users
|
||||
- **table** - Table that contains all logged-in users. Multiple rows per user id are fine since we unique the values before counting.
|
||||
- Anonymous Id Mapping
|
||||
- **table** - Table that maps logged-out anonymous ids to logged-in user ids
|
||||
- Page Views
|
||||
- **table** - Table that contains one row per page view.
|
||||
- **url path column** - The column in this table that stores the URL path (e.g. `/about/us`)
|
||||
- Experiments
|
||||
- **table** - Table that maps a user id to an experiment variation. Duplicate rows are fine since we unique values before counting.
|
||||
- **experiment id column** - The name of the column in this table which stores the experiment id
|
||||
- **variation id column** - The name of the column in this table which stores the variation id
|
||||
- **variation id format** - What format the variation id is stored in.
|
||||
1. Numeric (0 = control, 1 = variation 1, etc.)
|
||||
2. Unique String Keys (e.g. "blue", "random-uuid", etc.)
|
||||
|
||||
### Mixpanel
|
||||
|
||||
We query Mixpanel using JQL. Instead of tables and columns, we need to know about your events and property names.
|
||||
|
||||
- Experiments
|
||||
- **View Experiments Event** - The name of the event you are firing when a user is put into a variation
|
||||
- **Experiment Id Property** - The property name that stores the experiment tracking key
|
||||
- **Variation Id Property** - The property name that stores the variation the user was assigned
|
||||
- **Variation Id Format** - What format the variation id is stored in.
|
||||
1. Numeric (0 = control, 1 = variation 1, etc.)
|
||||
2. Unique String Keys (e.g. "blue", "random-uuid", etc.)
|
||||
- Page Views
|
||||
- **Page Views Event** - the name of the event you are firing for every page view on your site
|
||||
- **URL Path Property** - in the event, the property name that stores the URL path for the pageview
|
||||
|
||||
## Connection Info
|
||||
|
||||
Connection info is encrypted twice - once within the app and again by the database when persisting to disk.
|
||||
|
||||
Growth Book only runs `SELECT` queries (or the equivalent for non-SQL data sources). We still always recommend creating read-only users with as few permissions as possible.
|
||||
|
||||
If you are using the managed Growth Book App (https://app.growthbook.io), make sure to whitelist the ip address `52.70.79.40` if applicable.
|
||||
|
||||
### AWS Athena
|
||||
|
||||
Unlike other database engines with their own user management system, Athena uses IAM for authentication.
|
||||
|
||||
We recommend creating a new IAM user with readonly permissions for Growth Book. The managed [Quick Sight Policy](https://docs.aws.amazon.com/athena/latest/ug/awsquicksightathenaaccess-managed-policy.html) is a good starting point.
|
||||
|
||||
For the S3 results url, we recommend naming your bucket with the prefix `aws-athena-query-results-`
|
||||
|
||||
### BigQuery
|
||||
|
||||
You must first create a Service Account in Google with the proper permissions. To connect Growth Book, we just need the JSON key file for that account.
|
||||
It should contain the project_id, client_email, and private_key.
|
||||
|
||||
### Mixpanel
|
||||
|
||||
You must first create a Service Account in Mixpanel under your [Project Settings](https://mixpanel.com/settings/project#serviceaccounts).
|
||||
|
||||
To add the datasource in Growth Book, you will need:
|
||||
1. The service account username
|
||||
2. The service account secret
|
||||
3. Your project id (found on the Project Settings Overview page)
|
||||
|
||||
### Google Analytics
|
||||
|
||||
Because of Google Analytics tracking limitations, a user can only be in a single experiment at a time. We highly recommend using a more full-featured data source for serious A/B testing.
|
||||
|
||||
We require 3 things to query the Google Analytics API:
|
||||
1. OAuth Authorization
|
||||
2. View ID (found in Admin -> View Settings)
|
||||
3. Custom Dimension Index
|
||||
|
||||
When tracking experiment views, the custom dimension value must be formatted as `experiment-key:variation-index`. For example: `my-test:0` for the control and `my-test:1` for the 1st variation.
|
||||
69
packages/docs/pages/app/experiments.mdx
Normal file
69
packages/docs/pages/app/experiments.mdx
Normal file
@@ -0,0 +1,69 @@
|
||||
# Experiments
|
||||
|
||||
Experiments are the core of Growth Book. This page covers several topics:
|
||||
|
||||
1. Creating
|
||||
2. Targeting
|
||||
3. Implementation
|
||||
4. Starting and Stopping
|
||||
5. Results
|
||||
|
||||
## Creating
|
||||
|
||||
When you create a new experiment, it starts out as a draft and remains fully editable until you start it.
|
||||
|
||||
Experiment drafts are a great place to collaborate between PMs, designers, and engineers.
|
||||
|
||||
PMs can spec out the requirements, designers can upload mockups and screenshots, and engineers can work on implementation.
|
||||
|
||||
Use the built-in discussion thread to add comments or collect feedback.
|
||||
|
||||
## Targeting
|
||||
|
||||
There are a few different ways to limit your experiment to a subset of users:
|
||||
|
||||
1. Specify a pre-defined **Segment**
|
||||
2. Limit to logged-in users only
|
||||
3. Specify a URL regex pattern
|
||||
4. Custom targeting rules (e.g. `age > 18`)
|
||||
|
||||
All of these rules are evaluated locally in your app at runtime and no HTTP requests are made to the GrowthBook servers.
|
||||
|
||||
## Implementation
|
||||
|
||||
We offer official client libraries in a few popular languages with more coming soon:
|
||||
|
||||
* [Javascript/Typescript](/lib/js)
|
||||
* [React](/lib/react)
|
||||
* [PHP](/lib/php)
|
||||
* [Ruby](/lib/ruby)
|
||||
* Go - *coming soon*
|
||||
* Python - *coming soon*
|
||||
|
||||
It's not required to use these libraries. You can do variation assignment yourself or use another library like PlanOut.
|
||||
|
||||
The only requirement is that you track in your datasource when users are put into an experiment and which variation they received.
|
||||
|
||||
## Starting and Stopping
|
||||
|
||||
The client libraries never communicate with the Growth Book servers. So as soon as your deploy the A/B test code to production, people will start getting put into the experiment immediately. And the experiment will continue until you remove the code and do another deploy.
|
||||
|
||||
If you want to be able to control experiments within the Growth Book App, you can integrate with the [API](/api-docs).
|
||||
|
||||
## Results
|
||||
|
||||

|
||||
|
||||
Each row of this table is a different metric.
|
||||
|
||||
**Chance to Beat Control** is a bayesian statistic telling you how likely it is that the variation is better. Typically,
|
||||
you want to wait until this reaches 95%.
|
||||
|
||||
**Percent Change** shows how much better/worse the variation is compared to the control. The numbers show the median and the 95% confidence interval.
|
||||
To generate these numbers, we use statistical bootstrapping and simulate your experiment 10,000 times.
|
||||
|
||||
If you have pre-defined dimensions for your users, you can use the **Dimension** dropdown to drill down into your results.
|
||||
This is very useful for debugging (e.g. if Safari is down, but the other browser are fine, you may have an implementation bug).
|
||||
|
||||
Be careful, the more metrics and dimensions you look at, the more likely you are to see a false positive. If you find something that looks
|
||||
surprising, it's often worth a dedicated follow-up experiment to verify that it's real.
|
||||
98
packages/docs/pages/app/index.mdx
Normal file
98
packages/docs/pages/app/index.mdx
Normal file
@@ -0,0 +1,98 @@
|
||||
# Growth Book App
|
||||
|
||||
The Growth Book App is a web application to manage your A/B tests and analyze results.
|
||||
|
||||
You can host the app yourself with the [Open Source project](https://github.com/growthbook/growthbook). Or, we can host it for you at https://app.growthbook.io.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Get started using the Growth Book experimentation platform
|
||||
in just a few minutes.
|
||||
|
||||
1. Connect to your Data Source
|
||||
2. Add a metric
|
||||
3. Create an Experiment
|
||||
4. View Results
|
||||
|
||||
### Connect to your Data Source
|
||||
|
||||
Unlike other A/B testing platforms like Optimizely or Split.io, you don't send impressions and metrics to Growth Book.
|
||||
|
||||
Instead, you write them to your existing analytics tool or data warehouse and Growth Book queries those data sources to pull results. This has a few huge benefits:
|
||||
|
||||
* The Growth Book App is not in the critical rendering path of your application.
|
||||
* Your data stays with you which simplifies security and data governance issues.
|
||||
* A/B test results can use the exact same data and metrics as your dashboards and other reports.
|
||||
|
||||
For this quick start, we're going to skip this step and just manually enter data instead of querying it.
|
||||
|
||||
View the [data source documentation](/app/datasources) to learn more about data sources.
|
||||
|
||||
### Add a Metric
|
||||
|
||||
Metrics are what you are trying to change with experimentation.
|
||||
For example, newsletter signups, revenue, or unsubscribe rate.
|
||||
|
||||
For this example, we are going to imagine optimizing a pricing page for
|
||||
a SaaS product. The metrics we'll use are `Viewed Checkout` and `Subscribed`.
|
||||
|
||||
To start, click on Metrics in the site menu.
|
||||
|
||||
1. Click the green "`+ Add Metric`" button
|
||||
2. For metric name, enter "`Viewed Checkout`"
|
||||
3. Set the data source to "`Manual`"
|
||||
4. Keep the conversion type set to "`Binomial`" since this metric only has 2 possible values - yes or no
|
||||
5. Leave everything else as the default and click the Save button
|
||||
|
||||
Repeat the above to add a 2nd metric "`Subscribed`", which is also binomial.
|
||||
|
||||
View the [metrics documentation](/app/metrics) to learn more about metrics.
|
||||
|
||||
|
||||
## Create an Experiment
|
||||
|
||||
Experiments are the actual A/B tests you run. In the site menu, click on Experiments and then the New Experiment button in the top right.
|
||||
|
||||
Fill out the following info and click the Create button.
|
||||
|
||||
- **Name**: Pricing Display
|
||||
- **Hypothesis**: Showing the per-month price instead will reduce sticker shock and increase conversions.
|
||||
- **Metrics**: *select both metrics created earlier*
|
||||
- **Control Name**: Annual
|
||||
- **Variation 1 Name**: Monthly
|
||||
|
||||
You'll now have an experiment draft. Click the "Start" button next to the experiment title.
|
||||
|
||||
Experiments go through 1 or more *phases*. For this example, we're just going to have a single *main* phase with
|
||||
100% of traffic split evenly between the variations.
|
||||
|
||||
Accept the default values in the form and click "Start".
|
||||
|
||||
At this point, you would implement the changes on your site using one of our Client Libraries and collect data for a while.
|
||||
For this quick start, we'll just fast forward and assume we already did that.
|
||||
|
||||
View the [experiments documentation](/app/experiments) to learn more about experiments.
|
||||
|
||||
## View Results
|
||||
|
||||
Start by clicking the new Results tab on the experiment page.
|
||||
|
||||
Click the "Update Data" button. Since we haven't set up a data source yet, we'll need to manually enter data:
|
||||
|
||||
| Metric | Annual | Monthly |
|
||||
|------------------|--------|---------|
|
||||
| Users | 5024 | 5067 |
|
||||
| Viewed Checkout | 1032 | 1156 |
|
||||
| Subscribed | 356 | 361 |
|
||||
|
||||
Click Submit to save the data and you should see the statistical analysis.
|
||||
|
||||
Our change got more people to start checking out, but did
|
||||
not result in any increase in subscriptions. Let's stop the experiment and add our notes:
|
||||
|
||||
Click the "Stop Experiment" button and fill in the following info:
|
||||
|
||||
- **Conclusion**: Inconclusive
|
||||
- **Addition Analysis**: Increased checkouts, but not subscriptions. Users may have viewed this as a bait and switch.
|
||||
|
||||
Click the "Stop" button to end the experiment and save your comments.
|
||||
30
packages/docs/pages/app/metrics.mdx
Normal file
30
packages/docs/pages/app/metrics.mdx
Normal file
@@ -0,0 +1,30 @@
|
||||
# Metrics
|
||||
|
||||
Metrics are what your experiments are trying to optimize. Growth Book has a very flexible and powerful way to define metrics.
|
||||
|
||||
## Conversion Types
|
||||
|
||||
Metrics can have different statistical distributions. Below are the ones Growth Book supports or plans to support in the future:
|
||||
|
||||
| Conversion Type | Description | Example |
|
||||
|-----------------|-------------------------------------------|--------------------------|
|
||||
| binomial | A simple yes/no conversion | Created Account |
|
||||
| count | Counts multiple conversions per user | Pages per Visit |
|
||||
| duration | How much time something takes on average | Time on Site |
|
||||
| revenue | The revenue gained/lost on average | Revenue per User |
|
||||
|
||||
Need a metric type we don't support yet? Let us know!
|
||||
|
||||
## User Id and Timestamp Columns
|
||||
|
||||
When setting up your data source, you specified the default column names for User Id and Timestamp.
|
||||
|
||||
When creating a metric, you have the option to override these if you need to. Otherwise, just leave the fields blank to use the default.
|
||||
|
||||
## Conditions
|
||||
|
||||
Conditions are a simple way to filter the underlying data. For example, if you have a SQL table that records every time a user watches a video, you may want
|
||||
to add a metric "Watched Short Comedy Video". You only want to include a subset of the rows in the table, so you would add 2 conditions: `genre = "comedy"` and `length < 60`.
|
||||
|
||||
Sometimes, you need to join multiple tables together for a metric. In the above example, if your data was normalized, the "user watched a video" table probably doesn't have the genre and length directly.
|
||||
You would need to join to the video table to get that info. In these more complex cases, we recommend defining Views directly in your database instead. This makes it easier and more efficient to query, especially if your data source supports Materialized Views.
|
||||
11
packages/docs/pages/index.mdx
Normal file
11
packages/docs/pages/index.mdx
Normal file
@@ -0,0 +1,11 @@
|
||||

|
||||
|
||||
# Growth Book Docs
|
||||
|
||||
Growth Book is split into 2 parts: the **Growth Book App** and **Client Libraries**.
|
||||
|
||||
The [Growth Book App](/app) is where you manage your A/B tests and analyze results.
|
||||
|
||||
The [Client Libraries](/lib) are how you actually implement the A/B tests in your application.
|
||||
|
||||
Use the menu or the Previous/Next links at the bottom to navigate.
|
||||
14
packages/docs/pages/lib/index.mdx
Normal file
14
packages/docs/pages/lib/index.mdx
Normal file
@@ -0,0 +1,14 @@
|
||||
# Client Libraries
|
||||
|
||||
We offer official client libraries in a few popular languages with more coming soon:
|
||||
|
||||
* [Javascript/Typescript](/lib/js)
|
||||
* [React](/lib/react)
|
||||
* [PHP](/lib/php)
|
||||
* [Ruby](/lib/ruby)
|
||||
* Go - *coming soon*
|
||||
* Python - *coming soon*
|
||||
|
||||
It's not required to use these libraries. You can do variation assignment yourself or use another library like PlanOut.
|
||||
|
||||
The only requirement is that you track in your datasource when users are put into an experiment and which variation they received.
|
||||
258
packages/docs/pages/lib/js.mdx
Normal file
258
packages/docs/pages/lib/js.mdx
Normal file
@@ -0,0 +1,258 @@
|
||||
# Javascript
|
||||
|
||||
View the full documentation on [GitHub](https://github.com/growthbook/growthbook-js).
|
||||
|
||||
## Installation
|
||||
|
||||
`yarn add @growthbook/growthbook`
|
||||
|
||||
or
|
||||
|
||||
`npm install --save @growthbook/growthbook`
|
||||
|
||||
or use directly in your HTML without installing first:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import GrowthBookClient from 'https://unpkg.com/@growthbook/growthbook/dist/growthbook.esm.js';
|
||||
//...
|
||||
</script>
|
||||
```
|
||||
|
||||
## Quick Usage
|
||||
|
||||
```ts
|
||||
import GrowthBookClient from '@growthbook/growthbook';
|
||||
|
||||
// Create a client and setup tracking
|
||||
const client = new GrowthBookClient({
|
||||
onExperimentViewed: ({experimentId, variationId}) => {
|
||||
// Use whatever event tracking system you have in place
|
||||
analytics.track("Experiment Viewed", {experimentId, variationId});
|
||||
}
|
||||
});
|
||||
|
||||
// Define the user that you want to run an experiment on
|
||||
const user = client.user({id: "12345"});
|
||||
|
||||
// Put the user in an experiment
|
||||
const {value} = user.experiment({
|
||||
key: "my-experiment",
|
||||
variations: ["A", "B"]
|
||||
});
|
||||
|
||||
console.log(value); // "A" or "B"
|
||||
```
|
||||
|
||||
## Experiments
|
||||
|
||||
As shown above, the simplest experiment you can define has 2 fields: `key` and `variations`.
|
||||
|
||||
There are a lot more configuration options you can specify. Here is the full list of options:
|
||||
|
||||
- **key** (`string`) - The globally unique tracking key for the experiment
|
||||
- **variations** (`any[]`) - The different variations to choose between
|
||||
- **weights** (`number[]`) - How to weight traffic between variations. Must add to 1.
|
||||
- **status** (`string`) - "running" is the default and always active. "draft" is only active during QA and development. "stopped" is only active when forcing a winning variation to 100% of users.
|
||||
- **coverage** (`number`) - What percent of users should be included in the experiment (between 0 and 1, inclusive)
|
||||
- **url** (`string`) - Users can only be included in this experiment if the current URL matches this regex
|
||||
- **targeting** (`string[]`) - Users must pass all of these targeting rules to be included in this experiment (see below for more info)
|
||||
- **force** (`number`) - All users included in the experiment will be forced into the specific variation index
|
||||
- **anon** (`boolean`) - If true, use anonymous id for assigning, otherwise use logged-in user id. Defaults to false.
|
||||
|
||||
## Running Experiments
|
||||
|
||||
Run experiments by calling `user.experiment()` which returns an object with a few useful properties:
|
||||
|
||||
```ts
|
||||
const {inExperiment, variationId, value} = user.experiment({
|
||||
key: "my-experiment",
|
||||
variations: ["A", "B"]
|
||||
});
|
||||
|
||||
// If user is part of the experiment
|
||||
console.log(inExperiment); // true or false
|
||||
|
||||
// The index of the assigned variation
|
||||
console.log(variationId); // 0 or 1
|
||||
|
||||
// The value of the assigned variation
|
||||
console.log(value); // "A" or "B"
|
||||
```
|
||||
|
||||
The `inExperiment` flag can be false if the experiment defines any sort of targeting rules which the user does not pass. In this case, the user is always assigned variation index `0`.
|
||||
|
||||
## Client Configuration
|
||||
|
||||
The GrowthBookClient constructor takes an optional `options` argument.
|
||||
|
||||
Below are all of the available options:
|
||||
|
||||
- **enabled** - Default true. Set to false to completely disable all experiments.
|
||||
- **debug** - Default false. If set to true, console.log info about why experiments are run and why specific variations are chosen.
|
||||
- **onExperimentViewed** - Callback when the user views an experiment.
|
||||
- **url** - The URL for the current request (defaults to `window.location.href` when in a browser)
|
||||
- **enableQueryStringOverride** - Default true. If true, enables forcing variations via the URL. Very useful for QA. https://example.com/?my-experiment=1
|
||||
|
||||
### SPA support
|
||||
|
||||
With a Single Page App (SPA), you need to update the client on navigation in order to target tests based on URL:
|
||||
|
||||
```ts
|
||||
client.config.url = newUrl;
|
||||
```
|
||||
|
||||
Doing this with Next.js for example, will look like this:
|
||||
```tsx
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (newUrl) => client.config.url = newUrl;
|
||||
router.events.on('routeChangeComplete', onChange);
|
||||
return () => router.events.off('routeChangeComplete', onChange);
|
||||
}, [])
|
||||
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
```
|
||||
|
||||
## User Configuration
|
||||
|
||||
The `client.user` method supports both logged-in and anonymous users. To create an anonymous user, specify `anonId` instead of `id`:
|
||||
```js
|
||||
const user = client.user({anonId: "abcdef"});
|
||||
```
|
||||
|
||||
If you have both an anonymous id and a logged-in user id, you can pass both:
|
||||
```js
|
||||
const user = client.user({
|
||||
anonId: "abcdef",
|
||||
userId: "12345"
|
||||
});
|
||||
```
|
||||
|
||||
You can also include attributes about the user. These attributes are never sent across the network and are only used to locally evaluate experiment targeting rules:
|
||||
|
||||
```js
|
||||
const user = client.user({
|
||||
id: "12345",
|
||||
attributes: {
|
||||
// Any attributes about the user or page that you want to use for experiment targeting
|
||||
premium: true,
|
||||
accountAge: 36,
|
||||
source: "google"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
You can update these at any time by calling `user.setAttributes`. By default, this completely overwrites all previous attributes. To do a
|
||||
shallow merge instead, pass `true` as the 2nd argument.
|
||||
|
||||
```js
|
||||
user.setAttributes({
|
||||
premium: false
|
||||
})
|
||||
```
|
||||
|
||||
### Targeting
|
||||
|
||||
Experiments can target on these user attributes with the `targeting` field. Here's an example:
|
||||
|
||||
```ts
|
||||
const {inExperiment, value} = user.experiment({
|
||||
key: "my-targeted-experiment",
|
||||
variations: ["A", "B"],
|
||||
targeting: [
|
||||
"premium = true",
|
||||
"accountAge > 30"
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
If the user does not match the targeting rules, `inExperiment` will be false and they will be assigned variation index `0`.
|
||||
|
||||
## Overriding Weights and Targeting
|
||||
|
||||
It's common practice to adjust experiment settings after a test is live. For example, slowly ramping up traffic, stopping a test automatically if guardrail metrics go down, or rolling out a winning variation to 100% of users.
|
||||
|
||||
Instead of constantly changing your code, you can use client overrides. For example, to roll out a winning variation to 100% of users:
|
||||
```ts
|
||||
client.overrides.set("experiment-key", {
|
||||
status: 'stopped',
|
||||
// Force variation index 1
|
||||
force: 1
|
||||
});
|
||||
```
|
||||
|
||||
The full list of experiment properties you can override is:
|
||||
* status
|
||||
* force
|
||||
* weights
|
||||
* coverage
|
||||
* targeting
|
||||
* url
|
||||
|
||||
This data structure can be easily seralized and stored in a database or returned from an API. There is a small helper function if you have all of your overrides in a single JSON object:
|
||||
|
||||
```ts
|
||||
client.importOverrides({
|
||||
"key1": {...},
|
||||
"key2": {...},
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Event Tracking and Analyzing Results
|
||||
|
||||
This library only handles assigning variations to users. The 2 other parts required for an A/B testing platform are Tracking and Analysis.
|
||||
|
||||
### Tracking
|
||||
|
||||
It's likely you already have some event tracking on your site with the metrics you want to optimize (Google Analytics, Segment, Mixpanel, etc.).
|
||||
|
||||
For A/B tests, you just need to track one additional event - when someone views a variation.
|
||||
|
||||
```ts
|
||||
// Specify a tracking callback when instantiating the client
|
||||
const client = new GrowthBookClient({
|
||||
onExperimentViewed: ({experimentId, variationId}) => {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The object passed to your callback has the following properties:
|
||||
- experimentId (the key of the experiment)
|
||||
- variationId (the array index of the assigned variation)
|
||||
- value (the value of the assigned variation)
|
||||
- experiment (the full experiment object)
|
||||
- userId
|
||||
- anonId
|
||||
- userAttributes
|
||||
|
||||
Below are examples for a few popular event tracking tools:
|
||||
|
||||
#### Google Analytics
|
||||
```ts
|
||||
ga('send', 'event', 'experiment', experimentId, variationId, {
|
||||
// Custom dimension for easier analysis
|
||||
'dimension1': `${experimentId}::${variationId}`
|
||||
});
|
||||
```
|
||||
|
||||
#### Segment
|
||||
```ts
|
||||
analytics.track("Experiment Viewed", {
|
||||
experimentId,
|
||||
variationId
|
||||
});
|
||||
```
|
||||
|
||||
#### Mixpanel
|
||||
```ts
|
||||
mixpanel.track("$experiment_started", {
|
||||
'Experiment name': experimentId,
|
||||
'Variant name': variationId
|
||||
});
|
||||
```
|
||||
277
packages/docs/pages/lib/php.mdx
Normal file
277
packages/docs/pages/lib/php.mdx
Normal file
@@ -0,0 +1,277 @@
|
||||
# PHP
|
||||
|
||||
View the full documentation on [GitHub](https://github.com/growthbook/growthbook-php).
|
||||
|
||||
## Installation
|
||||
|
||||
Growth Book is available on Composer:
|
||||
|
||||
`composer require growthbook/growthbook`
|
||||
|
||||
## Quick Usage
|
||||
|
||||
```php
|
||||
$client = new Growthbook\Client();
|
||||
|
||||
// Define the user that you want to run an experiment on
|
||||
$user = $client->user(["id"=>"12345"]);
|
||||
|
||||
// Define the experiment
|
||||
$experiment = new Growthbook\Experiment("my-experiment", ["A", "B"]);
|
||||
|
||||
// Put the user in the experiment
|
||||
$result = $user->experiment($experiment);
|
||||
|
||||
echo $result->value; // "A" or "B"
|
||||
```
|
||||
|
||||
At the end of the request, you would track all of the viewed experiments in your analytics tool or database (Segment, GA, Mixpanel, etc.).
|
||||
|
||||
```php
|
||||
$impressions = $client->getViewedExperiments();
|
||||
foreach($impressions as $impression) {
|
||||
// Whatever you use for event tracking
|
||||
Segment::track([
|
||||
"userId" => $impression->userId,
|
||||
"event" => "Experiment Viewed",
|
||||
"properties" => [
|
||||
"experimentId" => $impression->experimentId,
|
||||
"variationId" => $impression->variationId
|
||||
]
|
||||
])
|
||||
}
|
||||
```
|
||||
|
||||
## Experiments
|
||||
|
||||
As shown above, the simplest experiment you can define has an id and an array of variations.
|
||||
|
||||
There is an optional 3rd argument, which is an associative array of additional options:
|
||||
|
||||
- **weights** (`float[]`) - How to weight traffic between variations. Must add to 1 and be the same length as the number of variations.
|
||||
- **status** (`string`) - "running" is the default and always active. "draft" is only active during QA and development. "stopped" is only active when forcing a winning variation to 100% of users.
|
||||
- **coverage** (`float`) - What percent of users should be included in the experiment (between 0 and 1, inclusive)
|
||||
- **url** (`string`) - Users can only be included in this experiment if the current URL matches this regex
|
||||
- **targeting** (`string[]`) - Users must pass all of these targeting rules to be included in this experiment (see below for more info)
|
||||
- **force** (`int`) - All users included in the experiment will be forced into the specific variation index
|
||||
- **anon** (`bool`) - If true, use anonymous id for assigning, otherwise use logged-in user id. Defaults to false.
|
||||
|
||||
## Running Experiments
|
||||
|
||||
Run experiments by calling `$user->experiment()` which returns an object with a few useful properties:
|
||||
|
||||
```php
|
||||
$result = $user->experiment(new Growthbook\Experiment(
|
||||
"my-experiment", ["A", "B"]
|
||||
);
|
||||
|
||||
// If user is part of the experiment
|
||||
echo $result->inExperiment; // true or false
|
||||
|
||||
// The index of the assigned variation
|
||||
echo $result->variationId; // 0 or 1
|
||||
|
||||
// The value of the assigned variation
|
||||
echo $result->value; // "A" or "B"
|
||||
```
|
||||
|
||||
The `inExperiment` flag can be false if the experiment defines any sort of targeting rules which the user does not pass. In this case, the user is always assigned variation index `0` and the first variation value.
|
||||
|
||||
## Client Configuration
|
||||
|
||||
The `Growthbook\Client` constructor takes an optional config arugment:
|
||||
|
||||
```php
|
||||
$config = new Growthbook\Config([
|
||||
// options go here
|
||||
]);
|
||||
$client = new Growthbook\Client($config);
|
||||
```
|
||||
|
||||
The `Growthbook\Config` constructor takes an associative array of options. Below are all of the available options currently:
|
||||
|
||||
- **enabled** - Default true. Set to false to completely disable all experiments.
|
||||
- **logger** - An optional psr-3 logger instance
|
||||
- **url** - The url of the page (defaults to `$_SERVER['REQUEST_URL']` if not set)
|
||||
- **enableQueryStringOverride** - Default false. If true, enables forcing variations via the URL. Very useful for QA. https://example.com/?my-experiment=1
|
||||
|
||||
You can change configuration options at any time by setting properties directly:
|
||||
|
||||
```php
|
||||
$client->config->enabled = false;
|
||||
```
|
||||
|
||||
## User Configuration
|
||||
|
||||
The `$client->user` method takes a single associative array argument. There are 3 possible keys you can use:
|
||||
|
||||
- `id` - The logged-in user id
|
||||
- `anonId` - An anonymous identifier for the user (session id, cookie, ip, etc.)
|
||||
- `attributes` - An associative array with user attributes. These are never sent across the network and are only used to locally evaluate experiment targeting rules.
|
||||
|
||||
Although all of these are technically optional, at least 1 type of id must be set or the user will be excluded from all experiments.
|
||||
|
||||
Here is an example that uses all 3 properties:
|
||||
|
||||
```php
|
||||
$user = $client->user([
|
||||
// Logged-in user id
|
||||
"id"=>"12345",
|
||||
|
||||
// Anonymous id
|
||||
"anonId"=>"abcdef",
|
||||
|
||||
// Targeting attributes
|
||||
"attributes"=> [
|
||||
"premium" => true,
|
||||
"accountAge" => 36,
|
||||
"geo" => [
|
||||
"region" => "NY"
|
||||
]
|
||||
]
|
||||
]);
|
||||
```
|
||||
|
||||
You can update attributes at any time by calling `$user->setAttributes`. By default, this completely overwrites all previous attributes. To do a shallow merge instead, pass `true` as the 2nd argument.
|
||||
|
||||
```php
|
||||
// Only overwrite the "premium" key and keep all the others
|
||||
$user->setAttributes([
|
||||
"premium" => false
|
||||
], true);
|
||||
```
|
||||
|
||||
### Targeting
|
||||
|
||||
Experiments can target on these user attributes with the `targeting` field. Here's an example:
|
||||
|
||||
```php
|
||||
$result = $user->experiment(
|
||||
"my-targeted-experiment",
|
||||
["A", "B"],
|
||||
[
|
||||
"targeting" => [
|
||||
"premium = true",
|
||||
"accountAge > 30"
|
||||
]
|
||||
]
|
||||
])
|
||||
```
|
||||
|
||||
If the user does not match the targeting rules, `$result->inExperiment` will be false and they will be assigned variation index `0`.
|
||||
|
||||
## Overriding Weights and Targeting
|
||||
|
||||
It's common practice to adjust experiment settings after a test is live. For example, slowly ramping up traffic, stopping a test automatically if guardrail metrics go down, or rolling out a winning variation to 100% of users.
|
||||
|
||||
Instead of constantly changing your code, you can use client overrides. For example, to roll out a winning variation to 100% of users:
|
||||
|
||||
```php
|
||||
$client->overrides->set("experiment-key", [
|
||||
"status" => 'stopped',
|
||||
// Force variation index 1
|
||||
"force" => 1
|
||||
]);
|
||||
```
|
||||
|
||||
The full list of experiment properties you can override is:
|
||||
* status
|
||||
* force
|
||||
* weights
|
||||
* coverage
|
||||
* targeting
|
||||
* url
|
||||
|
||||
This data structure can be easily seralized and stored in a database or returned from an API. There is a small helper function if you have all of your overrides in a single JSON object:
|
||||
|
||||
```php
|
||||
$json = '{
|
||||
"experiment-key-1": {
|
||||
"status": "stopped"
|
||||
},
|
||||
"experiment-key-2": {
|
||||
"weights": [0.8, 0.2]
|
||||
}
|
||||
}';
|
||||
|
||||
// Convert to associative array
|
||||
$overrides = json_decode($json, true);
|
||||
|
||||
// Import into the client
|
||||
$client->importOverrides($overrides);
|
||||
```
|
||||
|
||||
### Tracking
|
||||
|
||||
It's likely you already have some event tracking on your site with the metrics you want to optimize (Google Analytics, Segment, Mixpanel, etc.).
|
||||
|
||||
For A/B tests, you just need to track one additional event - when someone views a variation.
|
||||
|
||||
You can call `$client->getViewedExperiments()` at the end of a request to forward to your analytics tool of choice.
|
||||
|
||||
```php
|
||||
$impressions = $client->getViewedExperiments();
|
||||
foreach($impressions as $impression) {
|
||||
// Whatever you use for event tracking
|
||||
Segment::track([
|
||||
"userId" => $impression->userId,
|
||||
"event" => "Experiment Viewed",
|
||||
"properties" => [
|
||||
"experimentId" => $impression->experimentId,
|
||||
"variationId" => $impression->variationId
|
||||
]
|
||||
])
|
||||
}
|
||||
```
|
||||
|
||||
Each impression object has the following properties:
|
||||
- experimentId (the key of the experiment)
|
||||
- variationId (the array index of the assigned variation)
|
||||
- value (the value of the assigned variation)
|
||||
- experiment (the full experiment object)
|
||||
- userId
|
||||
- anonId
|
||||
- userAttributes
|
||||
|
||||
Often times you'll want to do the event tracking from the front-end with javascript. To do this, simply add a block to your template (shown here in plain PHP, but similar idea for Twig, Blade, etc.).
|
||||
|
||||
|
||||
```php
|
||||
<script>
|
||||
<?php foreach($client->getViewedExperiments() as $impression): ?>
|
||||
// tracking code goes here
|
||||
<?php endforeach; ?>
|
||||
</script>
|
||||
```
|
||||
|
||||
Below are examples for a few popular front-end tracking libraries:
|
||||
|
||||
#### Google Analytics
|
||||
```php
|
||||
ga('send', 'event', 'experiment',
|
||||
"<?= $impression->experimentId ?>",
|
||||
"<?= $impression->variationId ?>",
|
||||
{
|
||||
// Custom dimension for easier analysis
|
||||
'dimension1': "<?=
|
||||
$impression->experimentId.':'.$impression->variationId
|
||||
?>"
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
#### Segment
|
||||
```php
|
||||
analytics.track("Experiment Viewed", <?=json_encode([
|
||||
"experimentId" => $impression->experimentId,
|
||||
"variationId" => $impression->variationId
|
||||
])?>);
|
||||
```
|
||||
|
||||
#### Mixpanel
|
||||
```php
|
||||
mixpanel.track("Experiment Viewed", <?=json_encode([
|
||||
'Experiment name' => $impression->experimentId,
|
||||
'Variant name' => $impression->variationId
|
||||
])?>);
|
||||
```
|
||||
319
packages/docs/pages/lib/react.mdx
Normal file
319
packages/docs/pages/lib/react.mdx
Normal file
@@ -0,0 +1,319 @@
|
||||
# React
|
||||
|
||||
View the full documentation on [GitHub](https://github.com/growthbook/growthbook-react).
|
||||
|
||||
## Installation
|
||||
|
||||
`yarn add @growthbook/growthbook-react`
|
||||
|
||||
or
|
||||
|
||||
`npm install --save @growthbook/growthbook-react`
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Configure your app
|
||||
```tsx
|
||||
import {
|
||||
GrowthBookClient, GrowthBookProvider
|
||||
} from '@growthbook/growthbook-react'
|
||||
|
||||
// Create a client instance and setup tracking
|
||||
const client = new GrowthBookClient({
|
||||
onExperimentViewed: ({experimentId, variationId}) => {
|
||||
// Mixpanel, Segment, GA, or custom tracking
|
||||
mixpanel.track("Experiment Viewed", {experimentId, variationId})
|
||||
}
|
||||
});
|
||||
|
||||
// Specify the user id you want to experiment on
|
||||
const user = client.user({ id: mixpanel.get_distinct_id() })
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<GrowthBookProvider user={user}>
|
||||
<OtherComponent/>
|
||||
</GrowthBookProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Run an experiment
|
||||
|
||||
```tsx
|
||||
import { useExperiment } from '@growthbook/growthbook-react'
|
||||
|
||||
export default function OtherComponent() {
|
||||
const { value } = useExperiment({
|
||||
key: "new-headline",
|
||||
variations: ["Hello", "Hi", "Good Day"],
|
||||
})
|
||||
|
||||
return <h1>{ value }</h1>
|
||||
}
|
||||
```
|
||||
|
||||
Use class components? We support that too! See below.
|
||||
|
||||
## Dev Mode
|
||||
|
||||
If `process.env.NODE_ENV !== "production"` AND you are in a browser environment, dev mode is enabled by default. You can override this behavior by explicitly passing in the `disableDevMode` prop to `GrowthBookProvider`.
|
||||
|
||||
Dev Mode adds a variation switcher UI that floats on the bottom left of pages. Use this to easily test out all the experiment combinations. It also includes a screenshot tool to download images of all your variations.
|
||||
|
||||
[View Live Demo](https://growthbook.github.io/growthbook-react/)
|
||||
|
||||

|
||||
|
||||
## Experiments
|
||||
|
||||
The simplest experiment you can define has just 2 fields: `key` and `variations`.
|
||||
|
||||
There are a lot more configuration options you can specify. Here is the full typescript definition:
|
||||
|
||||
```ts
|
||||
interface Experiment {
|
||||
// The globally unique tracking key for the experiment
|
||||
key: string;
|
||||
// Array of variations
|
||||
variations: any[];
|
||||
// How to weight traffic between variations. Array of floats that add to 1.
|
||||
weights?: number[];
|
||||
// "running" is always active
|
||||
// "draft" is only active during QA
|
||||
// "stopped" is only active when forcing a winning variation
|
||||
status?: "draft" | "running" | "stopped";
|
||||
// What percent of users should be included in the experiment. Float from 0 to 1.
|
||||
coverage?: number;
|
||||
// Users can only be included in this experiment if the current URL matches this regex
|
||||
url?: string;
|
||||
// Array of strings if the format "{key} {operator} {value}"
|
||||
// Users must pass all of these targeting rules to be included in this experiment
|
||||
targeting?: string[];
|
||||
// All users included in the experiment will be forced into the
|
||||
// specified variation index
|
||||
force?: number;
|
||||
// If true, use anonymous id for assigning, otherwise use logged-in user id
|
||||
anon?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Running Experiments
|
||||
|
||||
The useExperiment hook returns an object with a few useful properties
|
||||
|
||||
```ts
|
||||
const {inExperiment, variationId, value} = useExperiment({
|
||||
key: "my-experiment",
|
||||
variations: ["A", "B"]
|
||||
});
|
||||
|
||||
// If user is part of the experiment
|
||||
console.log(inExperiment); // true or false
|
||||
|
||||
// The index of the assigned variation
|
||||
console.log(variationId); // 0 or 1
|
||||
|
||||
// The value of the assigned variation
|
||||
console.log(value); // "A" or "B"
|
||||
```
|
||||
|
||||
The `inExperiment` flag can be false if the experiment defines any sort of targeting rules which the user does not pass. In this case, the user is always assigned index `0`.
|
||||
|
||||
## Client Configuration
|
||||
|
||||
The GrowthBookClient constructor takes an optional `options` argument.
|
||||
|
||||
Below are all of the available options:
|
||||
|
||||
- **enabled** - Default true. Set to false to completely disable all experiments.
|
||||
- **debug** - Default false. If set to true, console.log info about why experiments are run and why specific variations are chosen.
|
||||
- **onExperimentViewed** - Callback when the user views an experiment. Passed an object with `experiment` and `variation` properties.
|
||||
- **url** - The URL for the current request (defaults to `window.location.href` when in a browser)
|
||||
- **enableQueryStringOverride** - Default true. If true, enables forcing variations via the URL. Very useful for QA. https://example.com/?my-experiment=1
|
||||
|
||||
### SPA support
|
||||
|
||||
With a Single Page App (SPA), you need to update the client on navigation in order to target tests based on URL:
|
||||
|
||||
```ts
|
||||
client.setUrl(newUrl);
|
||||
```
|
||||
|
||||
Doing this with Next.js for example, will look like this:
|
||||
```tsx
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (newUrl) => client.setUrl(newUrl);
|
||||
router.events.on('routeChangeComplete', onChange);
|
||||
return () => router.events.off('routeChangeComplete', onChange);
|
||||
}, [])
|
||||
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
```
|
||||
|
||||
## User Configuration
|
||||
|
||||
The `client.user` method supports both logged-in and anonymous users. To create an anonymous user, specify `anonId` instead of `id`:
|
||||
```js
|
||||
const user = client.user({anonId: "abcdef"});
|
||||
```
|
||||
|
||||
If you have both an anonymous id and a logged-in user id, you can pass both:
|
||||
```js
|
||||
const user = client.user({
|
||||
anonId: "abcdef",
|
||||
userId: "12345"
|
||||
});
|
||||
```
|
||||
|
||||
You can also include attributes about the user. These attributes are never sent across the network and are only used to locally evaluate experiment targeting rules:
|
||||
|
||||
```js
|
||||
const user = client.user({
|
||||
id: "12345",
|
||||
attributes: {
|
||||
// Any attributes about the user or page that you want to use for experiment targeting
|
||||
premium: true,
|
||||
accountAge: 36,
|
||||
source: "google"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
You can update these at any time by calling `user.setAttributes`. By default, this completely overwrites all previous attributes. To do a
|
||||
shallow merge instead, pass `true` as the 2nd argument.
|
||||
|
||||
```js
|
||||
user.setAttributes({
|
||||
premium: false
|
||||
})
|
||||
```
|
||||
|
||||
### Targeting
|
||||
|
||||
Experiments can target on these user attributes with the `targeting` field. Here's an example:
|
||||
|
||||
```ts
|
||||
const {inExperiment, value} = useExperiment({
|
||||
key: "my-targeted-experiment",
|
||||
variations: ["A", "B"],
|
||||
targeting: [
|
||||
"premium = true",
|
||||
"accountAge > 30"
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
If the user does not match the targeting rules, `inExperiment` will be false and they will be assigned variation index `0`.
|
||||
|
||||
## Overriding Weights and Targeting
|
||||
|
||||
It's common practice to adjust experiment settings after a test is live. For example, slowly ramping up traffic, stopping a test automatically if guardrail metrics go down, or rolling out a winning variation to 100% of users.
|
||||
|
||||
Instead of constantly changing your code, you can use client overrides. For example, to roll out a winning variation to 100% of users:
|
||||
```ts
|
||||
client.overrides.set("experiment-key", {
|
||||
status: 'stopped',
|
||||
// Force variation index 1
|
||||
force: 1
|
||||
});
|
||||
```
|
||||
|
||||
The full list of experiment properties you can override is:
|
||||
* status
|
||||
* force
|
||||
* weights
|
||||
* coverage
|
||||
* targeting
|
||||
* url
|
||||
|
||||
This data structure can be easily seralized and stored in a database or returned from an API. There is a small helper function if you have all of your overrides in a single JSON object:
|
||||
|
||||
```ts
|
||||
client.importOverrides({
|
||||
"key1": {...},
|
||||
"key2": {...},
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Event Tracking
|
||||
|
||||
This library only handles assigning variations to users. The 2 other parts required for an A/B testing platform are Tracking and Analysis.
|
||||
|
||||
It's likely you already have some event tracking on your site with the metrics you want to optimize (Google Analytics, Segment, Mixpanel, etc.).
|
||||
|
||||
For A/B tests, you just need to track one additional event - when someone views a variation.
|
||||
|
||||
```ts
|
||||
// Specify a tracking callback when instantiating the client
|
||||
const client = new GrowthBookClient({
|
||||
onExperimentViewed: ({experimentId, variationId}) => {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The object passed to your callback has the following properties:
|
||||
- experimentId (the key of the experiment)
|
||||
- variationId (the array index of the assigned variation)
|
||||
- value (the value of the assigned variation)
|
||||
- experiment (the full experiment object)
|
||||
- userId
|
||||
- anonId
|
||||
- userAttributes
|
||||
|
||||
Below are examples for a few popular event tracking tools:
|
||||
|
||||
### Google Analytics
|
||||
```ts
|
||||
ga('send', 'event', 'experiment', experimentId, variationId, {
|
||||
// Custom dimension for easier analysis
|
||||
'dimension1': `${experimentId}::${variationId}`
|
||||
});
|
||||
```
|
||||
|
||||
### Segment
|
||||
```ts
|
||||
analytics.track("Experiment Viewed", {
|
||||
experimentId,
|
||||
variationId
|
||||
});
|
||||
```
|
||||
|
||||
### Mixpanel
|
||||
```ts
|
||||
mixpanel.track("$experiment_started", {
|
||||
'Experiment name': experimentId,
|
||||
'Variant name': variationId
|
||||
});
|
||||
```
|
||||
|
||||
## React Class Components
|
||||
|
||||
If you aren't using functional components, we offer a `withRunExperiment` Higher Order Component instead.
|
||||
|
||||
**Note:** This library uses hooks internally, so still requires React 16.8 or above.
|
||||
|
||||
```tsx
|
||||
import {withRunExperiment} from '@growthbook/growthbook-react';
|
||||
|
||||
class MyComponent extends Component {
|
||||
render() {
|
||||
// The `runExperiment` prop is identical to the `useExperiment` hook
|
||||
const {value} = this.props.runExperiment({
|
||||
key: "headline-test",
|
||||
variations: ["Hello World", "Hola Mundo"]
|
||||
});
|
||||
|
||||
return <h1>{value}</h1>
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap your component in `withRunExperiment`
|
||||
export default withRunExperiment(MyComponent);
|
||||
```
|
||||
283
packages/docs/pages/lib/ruby.mdx
Normal file
283
packages/docs/pages/lib/ruby.mdx
Normal file
@@ -0,0 +1,283 @@
|
||||
# Ruby
|
||||
|
||||
View the full documentation on [GitHub](https://github.com/growthbook/growthbook-ruby).
|
||||
|
||||
## Installation
|
||||
|
||||
Install the gem
|
||||
|
||||
`gem install growthbook`
|
||||
|
||||
## Quick Usage
|
||||
|
||||
```ruby
|
||||
require 'growthbook'
|
||||
|
||||
# Do this once during app bootup
|
||||
client = Growthbook::Client.new
|
||||
|
||||
# Logged-in id of the user being experimented on
|
||||
# Can also use an anonymous id like session (see below)
|
||||
user = client.user(id: "12345")
|
||||
|
||||
# 2 variations, 50/50 split
|
||||
exp = Growthbook::Experiment.new("experiment-id", 2)
|
||||
|
||||
result = user.experiment(exp)
|
||||
|
||||
case result.variation
|
||||
when 0
|
||||
puts "Control"
|
||||
when 1
|
||||
puts "Variation"
|
||||
else
|
||||
puts "Not in experiment"
|
||||
end
|
||||
```
|
||||
|
||||
## Client Configuration
|
||||
|
||||
The `Growthbook::Client` constructor takes an optional config hash.
|
||||
|
||||
Currently, the only option is `enabled` which defaults to true. When false, all experiments are completely disabled.
|
||||
|
||||
```ruby
|
||||
client = Growthbook::Config.new(enabled: false)
|
||||
|
||||
# All user.experiment calls will now return -1
|
||||
```
|
||||
|
||||
Config options are public instance variables and you can change them any time:
|
||||
|
||||
```ruby
|
||||
client.enabled=true
|
||||
```
|
||||
|
||||
## User Configuration
|
||||
|
||||
The `client.user` method takes a single hash with a few possible keys:
|
||||
|
||||
- `id` - The logged-in user id
|
||||
- `anonId` - An anonymous identifier for the user (session id, cookie, ip, etc.)
|
||||
- `attributes` - A hash with user attributes. These are never sent across the network and are only used to locally evaluate experiment targeting rules.
|
||||
|
||||
Although all of these are technically optional, at least 1 type of id must be set or the user will be excluded from all experiments.
|
||||
|
||||
Here is an example that uses all 3 properties:
|
||||
|
||||
```ruby
|
||||
user = client.user(
|
||||
# Logged-in user id
|
||||
id: "12345",
|
||||
|
||||
# Anonymous id
|
||||
anonId: "abcdef",
|
||||
|
||||
# Targeting attributes
|
||||
attributes: {
|
||||
:premium => true,
|
||||
:accountAge => 36,
|
||||
:geo => [
|
||||
:region => "NY"
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
You can update these at any time:
|
||||
|
||||
```ruby
|
||||
user.id="54321"
|
||||
user.anonId = "fedcba"
|
||||
user.attributes={
|
||||
:premium => false
|
||||
}
|
||||
```
|
||||
|
||||
## Experiment Configuration
|
||||
|
||||
The default test is a 50/50 split with no targeting or customization. There are a few ways to configure this on a test-by-test basis.
|
||||
|
||||
### Option 1: Global Configuration
|
||||
|
||||
With this option, you configure all experiments globally once and then reference them via id throughout the code.
|
||||
|
||||
```ruby
|
||||
client.experiments=[
|
||||
# Default 50/50 2-way test
|
||||
Growthbook::Experiment.new("my-test", 2),
|
||||
|
||||
# 3-way test with reduced coverage and unequal weights
|
||||
Growthbook::Experiment.new(
|
||||
"my-other-test",
|
||||
3,
|
||||
:coverage => 0.4,
|
||||
:weights => [0.5, 0.25, 0.25]
|
||||
)
|
||||
]
|
||||
|
||||
# Later in code, pass the string id instead of the Experiment object
|
||||
result = user.experiment("my-test")
|
||||
```
|
||||
|
||||
There is a helper method that can import a hash of multiple experiments at once. The expected format follows the GrowthBook API response.
|
||||
|
||||
```ruby
|
||||
require 'json'
|
||||
|
||||
# Example response from the GrowthBook API
|
||||
json = '{
|
||||
"status": 200,
|
||||
"experiments": {
|
||||
"my-test": {
|
||||
"variations": 2
|
||||
},
|
||||
"my-other-test": {
|
||||
"variations": 3,
|
||||
"weights": [0.5, 0.25, 0.25]
|
||||
}
|
||||
}
|
||||
}'
|
||||
parsed = JSON.parse(json)
|
||||
|
||||
client.importExperimentsHash(parsed["experiments"])
|
||||
```
|
||||
|
||||
### Option 2: Inline Experiment Configuration
|
||||
|
||||
As shown in the quick start above, you can use a `Growthbook::Experiment` object directly to run an experiment.
|
||||
|
||||
The below example shows all of the possible experiment options you can set:
|
||||
```ruby
|
||||
# 1st argument is the experiment id
|
||||
# 2nd argument is the number of variations
|
||||
experiment = Growthbook::Experiment.new("my-experiment-id", 3,
|
||||
# Percent of eligible traffic to include in the test (from 0 to 1)
|
||||
:coverage => 0.5,
|
||||
|
||||
# How to split traffic between variations (must add to 1)
|
||||
:weights => [0.34, 0.33, 0.33],
|
||||
|
||||
# If false, use the logged-in user id to assign variations
|
||||
# If true, use the anonymous id
|
||||
:anon => false,
|
||||
|
||||
# If set to an integer, force that variation for all users in the experiment
|
||||
:force => 1,
|
||||
|
||||
# Targeting rules
|
||||
# Evaluated against user attributes to determine who is included in the test
|
||||
:targeting => ["source != google"],
|
||||
|
||||
# Add arbitrary data to the variations (see below for more info)
|
||||
:data => {
|
||||
"color" => ["blue","green","red"]
|
||||
}
|
||||
)
|
||||
|
||||
result = user.experiment(experiment)
|
||||
```
|
||||
|
||||
## Running Experiments
|
||||
|
||||
Growth Book supports 3 different implementation approaches:
|
||||
|
||||
1. Branching
|
||||
2. Parameterization
|
||||
3. Config System
|
||||
|
||||
### Approach 1: Branching
|
||||
|
||||
This is the simplest to understand and implement. You add branching via if/else or case statements:
|
||||
|
||||
```ruby
|
||||
result = user.experiment("experiment-id")
|
||||
|
||||
if result.variation == 1
|
||||
# Variation
|
||||
buttonColor = "green"
|
||||
else
|
||||
# Control or Not in experiment
|
||||
buttonColor = "blue"
|
||||
end
|
||||
```
|
||||
|
||||
### Approach 2: Parameterization
|
||||
|
||||
With this approach, you parameterize the variations by associating them with data.
|
||||
|
||||
```ruby
|
||||
experiment = Growthbook::Experiment.new("experiment-id", 2,
|
||||
:data => {
|
||||
"color" => ["blue", "green"]
|
||||
}
|
||||
)
|
||||
|
||||
result = user.experiment(experiment)
|
||||
|
||||
# Will be either "blue" or "green"
|
||||
buttonColor = result.data["color"]
|
||||
|
||||
# If no data is defined for the key, `nil` is returned
|
||||
result.data["unknown"] == nil # true
|
||||
```
|
||||
|
||||
### Approach 3: Configuration System
|
||||
|
||||
If you already have an existing configuration or feature flag system, you can do a deeper integration that
|
||||
avoids `experiment` calls throughout your code base entirely.
|
||||
|
||||
All you need to do is modify your existing config system to get experiment overrides before falling back to your normal lookup process:
|
||||
|
||||
```ruby
|
||||
# Your existing function
|
||||
def getConfig(key)
|
||||
# Look for a valid matching experiment.
|
||||
# If found, choose a variation and return the value for the requested key
|
||||
result = user.lookupByDataKey(key)
|
||||
if result != nil
|
||||
return result.value
|
||||
end
|
||||
|
||||
# Continue with your normal lookup process
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
Instead of generic keys like `color`, you probably want to be more descriptive with this approach (e.g. `homepage.cta.color`).
|
||||
|
||||
With the following experiment data:
|
||||
```ruby
|
||||
{
|
||||
:data => {
|
||||
"homepage.cta.color" => ["blue", "green"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can now do:
|
||||
|
||||
```ruby
|
||||
buttonColor = getConfig("homepage.cta.color")
|
||||
```
|
||||
|
||||
Your code now no longer cares where the value comes from. It could be a hard-coded config value or part of an experiment. This is the cleanest approach of the 3, but it can be difficult to debug if things go wrong.
|
||||
|
||||
## Tracking
|
||||
|
||||
When someone is put into an experiment, you'll typically want to log this event in your analytics system.
|
||||
|
||||
The user object has an array `resultsToTrack` that stores all experiment results that should be tracked.
|
||||
|
||||
For example, if you are using Segment on the front-end, you can add something like this to the bottom of your HTML:
|
||||
|
||||
```erb
|
||||
<% user.resultsToTrack.each do |result| %>
|
||||
<script>
|
||||
analytics.track("Experiment Viewed", {
|
||||
experiment_id: "<%= result.experiment.id %>",
|
||||
variation_id: <%= result.variation_id %>
|
||||
})
|
||||
</script>
|
||||
<% end %>
|
||||
```
|
||||
BIN
packages/docs/public/favicon.ico
Normal file
BIN
packages/docs/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/docs/public/growth-book-logo.png
Normal file
BIN
packages/docs/public/growth-book-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
packages/docs/public/images/hero.png
Normal file
BIN
packages/docs/public/images/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/docs/public/images/results-table.png
Normal file
BIN
packages/docs/public/images/results-table.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
19
packages/docs/tsconfig.json
Normal file
19
packages/docs/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["next-env.d.ts", "mdx.d.ts", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
422
yarn.lock
422
yarn.lock
@@ -22,7 +22,7 @@
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.10.4"
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13":
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13":
|
||||
version "7.12.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
|
||||
integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
|
||||
@@ -34,6 +34,28 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.0.tgz#a901128bce2ad02565df95e6ecbf195cf9465919"
|
||||
integrity sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==
|
||||
|
||||
"@babel/core@7.12.9":
|
||||
version "7.12.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8"
|
||||
integrity sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
"@babel/generator" "^7.12.5"
|
||||
"@babel/helper-module-transforms" "^7.12.1"
|
||||
"@babel/helpers" "^7.12.5"
|
||||
"@babel/parser" "^7.12.7"
|
||||
"@babel/template" "^7.12.7"
|
||||
"@babel/traverse" "^7.12.9"
|
||||
"@babel/types" "^7.12.7"
|
||||
convert-source-map "^1.7.0"
|
||||
debug "^4.1.0"
|
||||
gensync "^1.0.0-beta.1"
|
||||
json5 "^2.1.2"
|
||||
lodash "^4.17.19"
|
||||
resolve "^1.3.2"
|
||||
semver "^5.4.1"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/core@^7.1.0", "@babel/core@^7.7.5":
|
||||
version "7.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.0.tgz#47299ff3ec8d111b493f1a9d04bf88c04e728d88"
|
||||
@@ -55,6 +77,15 @@
|
||||
semver "^6.3.0"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/generator@^7.12.5", "@babel/generator@^7.14.2":
|
||||
version "7.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.2.tgz#d5773e8b557d421fd6ce0d5efa5fd7fc22567c30"
|
||||
integrity sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==
|
||||
dependencies:
|
||||
"@babel/types" "^7.14.2"
|
||||
jsesc "^2.5.1"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/generator@^7.14.0":
|
||||
version "7.14.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.1.tgz#1f99331babd65700183628da186f36f63d615c93"
|
||||
@@ -90,6 +121,15 @@
|
||||
"@babel/template" "^7.12.13"
|
||||
"@babel/types" "^7.12.13"
|
||||
|
||||
"@babel/helper-function-name@^7.14.2":
|
||||
version "7.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz#397688b590760b6ef7725b5f0860c82427ebaac2"
|
||||
integrity sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==
|
||||
dependencies:
|
||||
"@babel/helper-get-function-arity" "^7.12.13"
|
||||
"@babel/template" "^7.12.13"
|
||||
"@babel/types" "^7.14.2"
|
||||
|
||||
"@babel/helper-get-function-arity@^7.12.13":
|
||||
version "7.12.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583"
|
||||
@@ -111,6 +151,20 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.13.12"
|
||||
|
||||
"@babel/helper-module-transforms@^7.12.1":
|
||||
version "7.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz#ac1cc30ee47b945e3e0c4db12fa0c5389509dfe5"
|
||||
integrity sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.13.12"
|
||||
"@babel/helper-replace-supers" "^7.13.12"
|
||||
"@babel/helper-simple-access" "^7.13.12"
|
||||
"@babel/helper-split-export-declaration" "^7.12.13"
|
||||
"@babel/helper-validator-identifier" "^7.14.0"
|
||||
"@babel/template" "^7.12.13"
|
||||
"@babel/traverse" "^7.14.2"
|
||||
"@babel/types" "^7.14.2"
|
||||
|
||||
"@babel/helper-module-transforms@^7.14.0":
|
||||
version "7.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz#8fcf78be220156f22633ee204ea81f73f826a8ad"
|
||||
@@ -132,7 +186,12 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.12.13"
|
||||
|
||||
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.8.0":
|
||||
"@babel/helper-plugin-utils@7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
|
||||
integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
|
||||
|
||||
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.8.0":
|
||||
version "7.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af"
|
||||
integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==
|
||||
@@ -171,7 +230,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831"
|
||||
integrity sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==
|
||||
|
||||
"@babel/helpers@^7.14.0":
|
||||
"@babel/helpers@^7.12.5", "@babel/helpers@^7.14.0":
|
||||
version "7.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.0.tgz#ea9b6be9478a13d6f961dbb5f36bf75e2f3b8f62"
|
||||
integrity sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==
|
||||
@@ -194,6 +253,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.1.tgz#1bd644b5db3f5797c4479d89ec1817fe02b84c47"
|
||||
integrity sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==
|
||||
|
||||
"@babel/parser@^7.12.7", "@babel/parser@^7.14.2":
|
||||
version "7.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.2.tgz#0c1680aa44ad4605b16cbdcc5c341a61bde9c746"
|
||||
integrity sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==
|
||||
|
||||
"@babel/plugin-proposal-object-rest-spread@7.12.1":
|
||||
version "7.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069"
|
||||
integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.10.4"
|
||||
"@babel/plugin-syntax-object-rest-spread" "^7.8.0"
|
||||
"@babel/plugin-transform-parameters" "^7.12.1"
|
||||
|
||||
"@babel/plugin-syntax-async-generators@^7.8.4":
|
||||
version "7.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
|
||||
@@ -229,6 +302,13 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.8.0"
|
||||
|
||||
"@babel/plugin-syntax-jsx@7.12.1":
|
||||
version "7.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926"
|
||||
integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.10.4"
|
||||
|
||||
"@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
|
||||
@@ -250,7 +330,7 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.10.4"
|
||||
|
||||
"@babel/plugin-syntax-object-rest-spread@^7.8.3":
|
||||
"@babel/plugin-syntax-object-rest-spread@7.8.3", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
|
||||
integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
|
||||
@@ -278,6 +358,13 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.12.13"
|
||||
|
||||
"@babel/plugin-transform-parameters@^7.12.1":
|
||||
version "7.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.2.tgz#e4290f72e0e9e831000d066427c4667098decc31"
|
||||
integrity sha512-NxoVmA3APNCC1JdMXkdYXuQS+EMdqy0vIwyDHeKHiJKRxmp1qGSdb0JLEIoPRhkx6H/8Qi3RJ3uqOCYw8giy9A==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.13.0"
|
||||
|
||||
"@babel/runtime@7.12.5":
|
||||
version "7.12.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
|
||||
@@ -304,7 +391,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.14.1.tgz#2c5f6908f03108583eea75bdcc94eb29e720fbac"
|
||||
integrity sha512-HFkwJyIv91mP38447ERwnVgw9yhx2/evs+r0+1hdAXf1Q1fBypPwtY8YOqsPniqoYCEVbBIqYELt0tNrOAg/Iw==
|
||||
|
||||
"@babel/template@^7.12.13", "@babel/template@^7.3.3":
|
||||
"@babel/template@^7.12.13", "@babel/template@^7.12.7", "@babel/template@^7.3.3":
|
||||
version "7.12.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
|
||||
integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==
|
||||
@@ -327,6 +414,20 @@
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/traverse@^7.12.9", "@babel/traverse@^7.14.2":
|
||||
version "7.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.2.tgz#9201a8d912723a831c2679c7ebbf2fe1416d765b"
|
||||
integrity sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.12.13"
|
||||
"@babel/generator" "^7.14.2"
|
||||
"@babel/helper-function-name" "^7.14.2"
|
||||
"@babel/helper-split-export-declaration" "^7.12.13"
|
||||
"@babel/parser" "^7.14.2"
|
||||
"@babel/types" "^7.14.2"
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/types@7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c"
|
||||
@@ -344,6 +445,14 @@
|
||||
"@babel/helper-validator-identifier" "^7.14.0"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.12.7", "@babel/types@^7.14.2":
|
||||
version "7.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.2.tgz#4208ae003107ef8a057ea8333e56eb64d2f6a2c3"
|
||||
integrity sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.14.0"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@bcoe/v8-coverage@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
@@ -688,6 +797,59 @@
|
||||
dependencies:
|
||||
unist-util-visit "^1.3.0"
|
||||
|
||||
"@mapbox/rehype-prism@^0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/rehype-prism/-/rehype-prism-0.6.0.tgz#3d8a860870951d4354257d0ba908d11545bd5ed5"
|
||||
integrity sha512-/0Ev/PB4fXdKPT6VDzVpnAPxGpWFIc4Yz3mf/DzLEMxlpIPZpZlCzaFk4V4NGFofQXPc41+GpEcZtWP3VuFWVA==
|
||||
dependencies:
|
||||
hast-util-to-string "^1.0.4"
|
||||
refractor "^3.3.1"
|
||||
unist-util-visit "^2.0.3"
|
||||
|
||||
"@mdx-js/loader@^1.6.22":
|
||||
version "1.6.22"
|
||||
resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.22.tgz#d9e8fe7f8185ff13c9c8639c048b123e30d322c4"
|
||||
integrity sha512-9CjGwy595NaxAYp0hF9B/A0lH6C8Rms97e2JS9d3jVUtILn6pT5i5IV965ra3lIWc7Rs1GG1tBdVF7dCowYe6Q==
|
||||
dependencies:
|
||||
"@mdx-js/mdx" "1.6.22"
|
||||
"@mdx-js/react" "1.6.22"
|
||||
loader-utils "2.0.0"
|
||||
|
||||
"@mdx-js/mdx@1.6.22":
|
||||
version "1.6.22"
|
||||
resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba"
|
||||
integrity sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA==
|
||||
dependencies:
|
||||
"@babel/core" "7.12.9"
|
||||
"@babel/plugin-syntax-jsx" "7.12.1"
|
||||
"@babel/plugin-syntax-object-rest-spread" "7.8.3"
|
||||
"@mdx-js/util" "1.6.22"
|
||||
babel-plugin-apply-mdx-type-prop "1.6.22"
|
||||
babel-plugin-extract-import-names "1.6.22"
|
||||
camelcase-css "2.0.1"
|
||||
detab "2.0.4"
|
||||
hast-util-raw "6.0.1"
|
||||
lodash.uniq "4.5.0"
|
||||
mdast-util-to-hast "10.0.1"
|
||||
remark-footnotes "2.0.0"
|
||||
remark-mdx "1.6.22"
|
||||
remark-parse "8.0.3"
|
||||
remark-squeeze-paragraphs "4.0.0"
|
||||
style-to-object "0.3.0"
|
||||
unified "9.2.0"
|
||||
unist-builder "2.0.3"
|
||||
unist-util-visit "2.0.3"
|
||||
|
||||
"@mdx-js/react@1.6.22", "@mdx-js/react@^1.6.22":
|
||||
version "1.6.22"
|
||||
resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-1.6.22.tgz#ae09b4744fddc74714ee9f9d6f17a66e77c43573"
|
||||
integrity sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==
|
||||
|
||||
"@mdx-js/util@1.6.22":
|
||||
version "1.6.22"
|
||||
resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b"
|
||||
integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==
|
||||
|
||||
"@medv/finder@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@medv/finder/-/finder-2.0.0.tgz#699b7141393aa815f120b38f54f92ad212225902"
|
||||
@@ -703,6 +865,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-10.2.0.tgz#296faf16a3132b12eceefc3215cb3a7676bcc06d"
|
||||
integrity sha512-FaW/mZrRwbHz10JmWV/JpK+TJpfB3EWTiJulKa1oJgLjOBaWND3PwmuBkQcBwGEE8vEZjsHpMPA8hToQA73dPw==
|
||||
|
||||
"@next/mdx@^10.2.0":
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/mdx/-/mdx-10.2.0.tgz#303bcc4ba4a6902aec8e6d2e677ad0b82dcc0336"
|
||||
integrity sha512-2AwN5Q4Av6McSpVKps2dDGdKEEUdme0cjdmo0iNuBFdBPzMBMeMyIMrT9T/QPCDUtqeB747f6Zkf/Xt2uVwzBg==
|
||||
|
||||
"@next/polyfill-module@10.2.0":
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-10.2.0.tgz#61f41110c4b465cc26d113e2054e205df61c3594"
|
||||
@@ -2021,6 +2188,21 @@ babel-jest@^26.6.3:
|
||||
graceful-fs "^4.2.4"
|
||||
slash "^3.0.0"
|
||||
|
||||
babel-plugin-apply-mdx-type-prop@1.6.22:
|
||||
version "1.6.22"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-apply-mdx-type-prop/-/babel-plugin-apply-mdx-type-prop-1.6.22.tgz#d216e8fd0de91de3f1478ef3231e05446bc8705b"
|
||||
integrity sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "7.10.4"
|
||||
"@mdx-js/util" "1.6.22"
|
||||
|
||||
babel-plugin-extract-import-names@1.6.22:
|
||||
version "1.6.22"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.22.tgz#de5f9a28eb12f3eb2578bf74472204e66d1a13dc"
|
||||
integrity sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "7.10.4"
|
||||
|
||||
babel-plugin-istanbul@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765"
|
||||
@@ -2197,6 +2379,11 @@ body-parser@1.19.0, body-parser@^1.19.0:
|
||||
raw-body "2.4.0"
|
||||
type-is "~1.6.17"
|
||||
|
||||
boolbase@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||
|
||||
bootstrap@^4.5.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7"
|
||||
@@ -2450,6 +2637,11 @@ callsites@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||
|
||||
camelcase-css@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
|
||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||
|
||||
camelcase-keys@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
|
||||
@@ -2463,6 +2655,11 @@ camelcase@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
|
||||
integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
|
||||
|
||||
camelcase@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
|
||||
integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo=
|
||||
|
||||
camelcase@^5.0.0, camelcase@^5.3.1:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||
@@ -2760,7 +2957,7 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
comma-separated-tokens@^1.0.0:
|
||||
comma-separated-tokens@^1.0.0, comma-separated-tokens@^1.0.2:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
|
||||
integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
|
||||
@@ -2993,6 +3190,11 @@ css-color-keywords@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
|
||||
integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=
|
||||
|
||||
css-selector-parser@^1.3.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.4.1.tgz#03f9cb8a81c3e5ab2c51684557d5aaf6d2569759"
|
||||
integrity sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==
|
||||
|
||||
css-to-react-native@^2.2.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-2.3.2.tgz#e75e2f8f7aa385b4c3611c52b074b70a002f2e7d"
|
||||
@@ -3326,6 +3528,13 @@ destroy@~1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
||||
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
|
||||
|
||||
detab@2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/detab/-/detab-2.0.4.tgz#b927892069aff405fbb9a186fe97a44a92a94b43"
|
||||
integrity sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g==
|
||||
dependencies:
|
||||
repeat-string "^1.5.4"
|
||||
|
||||
detect-newline@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
||||
@@ -3512,6 +3721,11 @@ emojis-list@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||
integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
|
||||
|
||||
emojis-list@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||
|
||||
enabled@2.0.x:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
|
||||
@@ -4324,7 +4538,7 @@ gcp-metadata@^4.2.0:
|
||||
gaxios "^4.0.0"
|
||||
json-bigint "^1.0.0"
|
||||
|
||||
gensync@^1.0.0-beta.2:
|
||||
gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2:
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||
@@ -4662,11 +4876,37 @@ hast-util-from-parse5@^6.0.0:
|
||||
vfile-location "^3.2.0"
|
||||
web-namespaces "^1.0.0"
|
||||
|
||||
hast-util-has-property@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-1.0.4.tgz#9f137565fad6082524b382c1e7d7d33ca5059f36"
|
||||
integrity sha512-ghHup2voGfgFoHMGnaLHOjbYFACKrRh9KFttdCzMCbFoBMJXiNi2+XTrPP8+q6cDJM/RSqlCfVWrjp1H201rZg==
|
||||
|
||||
hast-util-is-element@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-1.1.0.tgz#3b3ed5159a2707c6137b48637fbfe068e175a425"
|
||||
integrity sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ==
|
||||
|
||||
hast-util-parse-selector@^2.0.0:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
|
||||
integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==
|
||||
|
||||
hast-util-raw@6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-6.0.1.tgz#973b15930b7529a7b66984c98148b46526885977"
|
||||
integrity sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig==
|
||||
dependencies:
|
||||
"@types/hast" "^2.0.0"
|
||||
hast-util-from-parse5 "^6.0.0"
|
||||
hast-util-to-parse5 "^6.0.0"
|
||||
html-void-elements "^1.0.0"
|
||||
parse5 "^6.0.0"
|
||||
unist-util-position "^3.0.0"
|
||||
vfile "^4.0.0"
|
||||
web-namespaces "^1.0.0"
|
||||
xtend "^4.0.0"
|
||||
zwitch "^1.0.0"
|
||||
|
||||
hast-util-raw@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-6.1.0.tgz#e16a3c2642f65cc7c480c165400a40d604ab75d0"
|
||||
@@ -4684,6 +4924,23 @@ hast-util-raw@^6.1.0:
|
||||
xtend "^4.0.0"
|
||||
zwitch "^1.0.0"
|
||||
|
||||
hast-util-select@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hast-util-select/-/hast-util-select-1.0.1.tgz#9f1591efa62ba0bdf5ea4298b301aaae1dad612d"
|
||||
integrity sha1-nxWR76YroL316kKYswGqrh2tYS0=
|
||||
dependencies:
|
||||
camelcase "^3.0.0"
|
||||
comma-separated-tokens "^1.0.2"
|
||||
css-selector-parser "^1.3.0"
|
||||
hast-util-has-property "^1.0.0"
|
||||
hast-util-is-element "^1.0.0"
|
||||
hast-util-whitespace "^1.0.0"
|
||||
not "^0.1.0"
|
||||
nth-check "^1.0.1"
|
||||
property-information "^3.1.0"
|
||||
space-separated-tokens "^1.1.0"
|
||||
zwitch "^1.0.0"
|
||||
|
||||
hast-util-to-parse5@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz#1ec44650b631d72952066cea9b1445df699f8479"
|
||||
@@ -4695,6 +4952,16 @@ hast-util-to-parse5@^6.0.0:
|
||||
xtend "^4.0.0"
|
||||
zwitch "^1.0.0"
|
||||
|
||||
hast-util-to-string@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-1.0.4.tgz#9b24c114866bdb9478927d7e9c36a485ac728378"
|
||||
integrity sha512-eK0MxRX47AV2eZ+Lyr18DCpQgodvaS3fAQO2+b9Two9F5HEoRPhiUMNzoXArMJfZi2yieFzUBMRl3HNJ3Jus3w==
|
||||
|
||||
hast-util-whitespace@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41"
|
||||
integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A==
|
||||
|
||||
hastscript@^5.0.0:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.1.2.tgz#bde2c2e56d04c62dd24e8c5df288d050a355fb8a"
|
||||
@@ -4991,7 +5258,7 @@ is-accessor-descriptor@^1.0.0:
|
||||
dependencies:
|
||||
kind-of "^6.0.0"
|
||||
|
||||
is-alphabetical@^1.0.0:
|
||||
is-alphabetical@1.0.4, is-alphabetical@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
|
||||
integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==
|
||||
@@ -6144,6 +6411,15 @@ loader-utils@1.2.3:
|
||||
emojis-list "^2.0.0"
|
||||
json5 "^1.0.1"
|
||||
|
||||
loader-utils@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
|
||||
integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
|
||||
dependencies:
|
||||
big.js "^5.2.2"
|
||||
emojis-list "^3.0.0"
|
||||
json5 "^2.1.2"
|
||||
|
||||
locate-path@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
|
||||
@@ -6236,6 +6512,11 @@ lodash.truncate@^4.4.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
|
||||
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
|
||||
|
||||
lodash.uniq@4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||
|
||||
lodash@4.x, lodash@>=4.17.21, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.4:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
@@ -6460,6 +6741,13 @@ mdast-comment-marker@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/mdast-comment-marker/-/mdast-comment-marker-1.1.2.tgz#5ad2e42cfcc41b92a10c1421a98c288d7b447a6d"
|
||||
integrity sha512-vTFXtmbbF3rgnTh3Zl3irso4LtvwUq/jaDvT2D1JqTGAwaipcS7RpTxzi6KjoRqI9n2yuAhzLDAC8xVTF3XYVQ==
|
||||
|
||||
mdast-squeeze-paragraphs@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97"
|
||||
integrity sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ==
|
||||
dependencies:
|
||||
unist-util-remove "^2.0.0"
|
||||
|
||||
mdast-util-assert@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mdast-util-assert/-/mdast-util-assert-3.0.0.tgz#6b9e166962975712274e2cfadb63e5687f96ce56"
|
||||
@@ -6477,6 +6765,27 @@ mdast-util-definitions@^3.0.0:
|
||||
dependencies:
|
||||
unist-util-visit "^2.0.0"
|
||||
|
||||
mdast-util-definitions@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2"
|
||||
integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==
|
||||
dependencies:
|
||||
unist-util-visit "^2.0.0"
|
||||
|
||||
mdast-util-to-hast@10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz#0cfc82089494c52d46eb0e3edb7a4eb2aea021eb"
|
||||
integrity sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA==
|
||||
dependencies:
|
||||
"@types/mdast" "^3.0.0"
|
||||
"@types/unist" "^2.0.0"
|
||||
mdast-util-definitions "^4.0.0"
|
||||
mdurl "^1.0.0"
|
||||
unist-builder "^2.0.0"
|
||||
unist-util-generated "^1.0.0"
|
||||
unist-util-position "^3.0.0"
|
||||
unist-util-visit "^2.0.0"
|
||||
|
||||
mdast-util-to-hast@^9.1.0:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-9.1.2.tgz#10fa5ed9d45bf3755891e5801d0f32e2584a9423"
|
||||
@@ -6986,6 +7295,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
|
||||
not@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/not/-/not-0.1.0.tgz#c9691c1746c55dcfbe54cbd8bd4ff041bc2b519d"
|
||||
integrity sha1-yWkcF0bFXc++VMvYvU/wQbwrUZ0=
|
||||
|
||||
npm-run-path@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||
@@ -7000,6 +7314,13 @@ npm-run-path@^4.0.0:
|
||||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
nth-check@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
|
||||
integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
|
||||
dependencies:
|
||||
boolbase "~1.0.0"
|
||||
|
||||
nunjucks@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/nunjucks/-/nunjucks-3.2.3.tgz#1b33615247290e94e28263b5d855ece765648a31"
|
||||
@@ -7754,6 +8075,11 @@ prop-types@15.7.2, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, p
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
|
||||
property-information@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/property-information/-/property-information-3.2.0.tgz#fd1483c8fbac61808f5fe359e7693a1f48a58331"
|
||||
integrity sha1-/RSDyPusYYCPX+NZ52k6H0ilgzE=
|
||||
|
||||
property-information@^5.0.0, property-information@^5.3.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
|
||||
@@ -8418,6 +8744,13 @@ regexpp@^3.0.0, regexpp@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
|
||||
integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
|
||||
|
||||
rehype-add-classes@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/rehype-add-classes/-/rehype-add-classes-1.0.0.tgz#3d8b37b13ce06e2eb8681b33ccf1c2995611a33b"
|
||||
integrity sha512-Iz8t2KhCNAL+0AHKjxb+kVwsHk/pI3Cy4k0R70ZGzoQiZ7WQm3o8+3odJkMhFRfcNIK1lNShIHEdC90H5LwLdg==
|
||||
dependencies:
|
||||
hast-util-select "^1.0.1"
|
||||
|
||||
rehype-raw@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-5.1.0.tgz#66d5e8d7188ada2d31bc137bc19a1000cf2c6b7e"
|
||||
@@ -8433,7 +8766,26 @@ rehype-react@^6.0.0:
|
||||
"@mapbox/hast-util-table-cell-style" "^0.1.3"
|
||||
hast-to-hyperscript "^9.0.0"
|
||||
|
||||
remark-parse@^8.0.3:
|
||||
remark-footnotes@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/remark-footnotes/-/remark-footnotes-2.0.0.tgz#9001c4c2ffebba55695d2dd80ffb8b82f7e6303f"
|
||||
integrity sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ==
|
||||
|
||||
remark-mdx@1.6.22:
|
||||
version "1.6.22"
|
||||
resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-1.6.22.tgz#06a8dab07dcfdd57f3373af7f86bd0e992108bbd"
|
||||
integrity sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==
|
||||
dependencies:
|
||||
"@babel/core" "7.12.9"
|
||||
"@babel/helper-plugin-utils" "7.10.4"
|
||||
"@babel/plugin-proposal-object-rest-spread" "7.12.1"
|
||||
"@babel/plugin-syntax-jsx" "7.12.1"
|
||||
"@mdx-js/util" "1.6.22"
|
||||
is-alphabetical "1.0.4"
|
||||
remark-parse "8.0.3"
|
||||
unified "9.2.0"
|
||||
|
||||
remark-parse@8.0.3, remark-parse@^8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1"
|
||||
integrity sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==
|
||||
@@ -8462,6 +8814,13 @@ remark-rehype@^7.0.0:
|
||||
dependencies:
|
||||
mdast-util-to-hast "^9.1.0"
|
||||
|
||||
remark-squeeze-paragraphs@4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz#76eb0e085295131c84748c8e43810159c5653ead"
|
||||
integrity sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw==
|
||||
dependencies:
|
||||
mdast-squeeze-paragraphs "^4.0.0"
|
||||
|
||||
remove-accents@0.4.2:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
|
||||
@@ -8587,7 +8946,7 @@ resolve-url@^0.2.1:
|
||||
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
|
||||
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
|
||||
|
||||
resolve@^1.0.0, resolve@^1.10.0, resolve@^1.18.1:
|
||||
resolve@^1.0.0, resolve@^1.10.0, resolve@^1.18.1, resolve@^1.3.2:
|
||||
version "1.20.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
|
||||
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
|
||||
@@ -8807,7 +9166,7 @@ semver-regex@^3.1.2:
|
||||
resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807"
|
||||
integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0:
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
@@ -9121,7 +9480,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
space-separated-tokens@^1.0.0:
|
||||
space-separated-tokens@^1.0.0, space-separated-tokens@^1.1.0:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
|
||||
integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
|
||||
@@ -9521,7 +9880,7 @@ stubs@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b"
|
||||
integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls=
|
||||
|
||||
style-to-object@^0.3.0:
|
||||
style-to-object@0.3.0, style-to-object@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46"
|
||||
integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==
|
||||
@@ -10093,6 +10452,18 @@ unherit@^1.0.4:
|
||||
inherits "^2.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
unified@9.2.0:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8"
|
||||
integrity sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==
|
||||
dependencies:
|
||||
bail "^1.0.0"
|
||||
extend "^3.0.0"
|
||||
is-buffer "^2.0.0"
|
||||
is-plain-obj "^2.0.0"
|
||||
trough "^1.0.0"
|
||||
vfile "^4.0.0"
|
||||
|
||||
unified@^9.0.0:
|
||||
version "9.2.1"
|
||||
resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.1.tgz#ae18d5674c114021bfdbdf73865ca60f410215a3"
|
||||
@@ -10120,7 +10491,7 @@ uniqid@^5.3.0:
|
||||
resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-5.3.0.tgz#4545f8e9c9a5830ef804e512cca6a9331e9761bf"
|
||||
integrity sha512-Jq8EzvAT8/CcLu8tzoSiylnzPkNhJJKpnMT964Dj1jI4pG4sKYP9aFVByNTp8KzMvYlW1Um63PCDqtOoujNzrA==
|
||||
|
||||
unist-builder@^2.0.0:
|
||||
unist-builder@2.0.3, unist-builder@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436"
|
||||
integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==
|
||||
@@ -10160,6 +10531,13 @@ unist-util-remove-position@^2.0.0:
|
||||
dependencies:
|
||||
unist-util-visit "^2.0.0"
|
||||
|
||||
unist-util-remove@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/unist-util-remove/-/unist-util-remove-2.1.0.tgz#b0b4738aa7ee445c402fda9328d604a02d010588"
|
||||
integrity sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q==
|
||||
dependencies:
|
||||
unist-util-is "^4.0.0"
|
||||
|
||||
unist-util-stringify-position@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da"
|
||||
@@ -10182,14 +10560,7 @@ unist-util-visit-parents@^3.0.0:
|
||||
"@types/unist" "^2.0.0"
|
||||
unist-util-is "^4.0.0"
|
||||
|
||||
unist-util-visit@^1.3.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3"
|
||||
integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==
|
||||
dependencies:
|
||||
unist-util-visit-parents "^2.0.0"
|
||||
|
||||
unist-util-visit@^2.0.0, unist-util-visit@^2.0.3:
|
||||
unist-util-visit@2.0.3, unist-util-visit@^2.0.0, unist-util-visit@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c"
|
||||
integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==
|
||||
@@ -10198,6 +10569,13 @@ unist-util-visit@^2.0.0, unist-util-visit@^2.0.3:
|
||||
unist-util-is "^4.0.0"
|
||||
unist-util-visit-parents "^3.0.0"
|
||||
|
||||
unist-util-visit@^1.3.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3"
|
||||
integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==
|
||||
dependencies:
|
||||
unist-util-visit-parents "^2.0.0"
|
||||
|
||||
universalify@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
|
||||
Reference in New Issue
Block a user