-
Notifications
You must be signed in to change notification settings - Fork 5
Ab test rammeverk #440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Ab test rammeverk #440
Changes from all commits
be5756b
e1682cf
d2c9e50
3e5c389
cd77686
ba99f2e
76ecca3
a7c9880
ea5302d
3e1800f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # ndla-abtest | ||
|
|
||
| WIP | ||
|
|
||
| ## Installation | ||
|
|
||
| ```sh | ||
| $ yarn add --save ndla-abtest | ||
| ``` | ||
|
|
||
| ```sh | ||
| $ npm i --save ndla-abtest | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ```js | ||
| import { ExperimentsContext, Experiment, Variant } from '@ndla/abtest'; | ||
|
|
||
| // clean experiments returned from Experiments service | ||
| const cleanExperiments = [ | ||
| { | ||
| id: '6bklbienTOuNQs9JwMrvog', // Experiment 1 ID | ||
| variant: { | ||
| // Use variant with index: 0 | ||
| index: 0, | ||
| name: '', | ||
| }, | ||
| }, | ||
| { | ||
| id: 'gKKvagBlQ5SyhxWP4TqK0g', // Experiment 2 ID | ||
| variant: { | ||
| // Use variant with index: 2 | ||
| index: 2, | ||
| name: '', | ||
| }, | ||
| }, | ||
| ]; | ||
|
|
||
| const experimentId = 'gKKvagBlQ5SyhxWP4TqK0g'; | ||
|
|
||
| <ExperimentsContext.Provider | ||
| value={{ | ||
| experiments: cleanExperiments, | ||
| }}> | ||
| <App> | ||
| <h1>Testing button title in app</h1> | ||
| <ExperimentsContext.Consumer> | ||
| {({ experiments }) => ( | ||
| <Experiment | ||
| id={experimentId} | ||
| experiments={experiments} | ||
| onVariantMount={variantData => { | ||
| console.log('render details', variantData); | ||
| }}> | ||
| <Variant variantIndex={0} original> | ||
| Test 1 | ||
| </Variant> | ||
| <Variant variantIndex={1}>Test 2</Variant> | ||
| <Variant variantIndex={2}>Test 3</Variant> | ||
| </Experiment> | ||
| )} | ||
| </ExperimentsContext.Consumer> | ||
| </App> | ||
| </ExperimentsContext.Provider>; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "name": "@ndla/abtest", | ||
| "version": "0.0.1", | ||
| "description": "AB-test Context", | ||
| "license": "GPL-3.0", | ||
| "main": "lib/index.js", | ||
| "module": "es/index.js", | ||
| "sideEffects": false, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/NDLANO/frontend-packages.git/ndla-abtest/" | ||
| }, | ||
| "keywords": [ | ||
| "ndla", | ||
| "AB-test" | ||
| ], | ||
| "author": "ndla@knowit.no", | ||
| "files": [ | ||
| "lib", | ||
| "es" | ||
| ], | ||
| "publishConfig": { | ||
| "access": "public" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /** | ||
| * Copyright (c) 2019-present, NDLA. | ||
| * | ||
| * This source code is licensed under the GPLv3 license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
|
|
||
| import React from 'react'; | ||
|
|
||
| export const ExperimentsContext = React.createContext({}); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /** | ||
| * Copyright (c) 2019-present, NDLA. | ||
| * | ||
| * This source code is licensed under the GPLv3 license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
|
|
||
| import React from 'react'; | ||
|
|
||
| import { ExperimentsContext } from './Context'; | ||
|
|
||
| export interface VariationsShape { | ||
| index?: number; | ||
| name?: string; | ||
| weight?: number; | ||
| } | ||
|
|
||
| export interface ExperimentShape { | ||
| id: string; | ||
| variant: VariationsShape; | ||
| } | ||
|
|
||
| interface Props { | ||
| experiments: ExperimentShape[]; | ||
| id: string; | ||
| onVariantMount?: void; | ||
| children: React.ReactNode[]; | ||
| } | ||
|
|
||
| export const Experiment: React.FC<Props> = ({ | ||
| experiments, | ||
| id: experimentId, | ||
| onVariantMount, | ||
| children, | ||
| }) => { | ||
| const { Provider } = ExperimentsContext; | ||
| const useVariant = experiments.find( | ||
| experiment => | ||
| experiment.id.localeCompare(experimentId, undefined, { | ||
| sensitivity: 'base', | ||
| }) === 0, | ||
| ); | ||
| return ( | ||
| <Provider | ||
| value={{ | ||
| variant: useVariant ? useVariant.variant : {}, | ||
| experimentId, | ||
| onVariantMount, | ||
| }}> | ||
| {children} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bedre API og sende inn en array med varianter i stedet for renderprops? Trenger man ikke eksponere til ndla-frontend, og man trenger ikke context for man kan bare sende props direkte inn.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Takk for gode tilbakemeldinger. Vi ser på saken 👍 |
||
| </Provider> | ||
| ); | ||
| }; | ||
|
|
||
| interface fetchVariantIndexShape { | ||
| id: string; | ||
| experiments: ExperimentShape[]; | ||
| } | ||
|
|
||
| export const fetchVariantIndex = ({ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kan ikke se at denne blir brukt noe sted? Viss den blir det bør den i en util funksjon |
||
| experiments, | ||
| id: experimentId, | ||
| }: fetchVariantIndexShape) => { | ||
| const useVariant = experiments.find( | ||
| experiment => | ||
| experiment.id.localeCompare(experimentId, undefined, { | ||
| sensitivity: 'base', | ||
| }) === 0, | ||
| ); | ||
| return useVariant ? useVariant.variant : {}; | ||
| }; | ||
|
|
||
| export const isValidExperiment = ({ | ||
| experiments, | ||
| id, | ||
| }: fetchVariantIndexShape) => { | ||
| return experiments && experiments.find(ex => ex.id === id); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /** | ||
| * Copyright (c) 2019-present, NDLA. | ||
| * | ||
| * This source code is licensed under the GPLv3 license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
|
|
||
| import React from 'react'; | ||
| import { ExperimentsContext } from './Context'; | ||
| import { VariationsShape } from './Experiment'; | ||
|
|
||
| interface Props { | ||
| variantIndex: number; | ||
| original?: boolean; | ||
| onVariantMount?: void; | ||
| children: React.ReactNode[]; | ||
| } | ||
|
|
||
| interface ValueShape { | ||
| experimentId?: string; | ||
| variant?: VariationsShape; | ||
| } | ||
|
|
||
| export class Variant extends React.Component<Props> { | ||
| componentDidMount() { | ||
| const { experimentId, variant, onVariantMount } = this.context; | ||
| const isActiveExperiment = this.isActive(this.context); | ||
| if (isActiveExperiment && onVariantMount) { | ||
| onVariantMount({ | ||
| expId: experimentId, | ||
| expVar: variant.index, | ||
| isActiveExperiment, | ||
| }); | ||
| } | ||
| } | ||
| isActive(value: ValueShape) { | ||
| const { variantIndex, original } = this.props; | ||
| return ( | ||
| (!value.variant && original) || | ||
| (value.variant && value.variant.index === variantIndex) | ||
| ); | ||
| } | ||
| render() { | ||
| const { Consumer } = ExperimentsContext; | ||
| return ( | ||
| <Consumer> | ||
| {(value: ValueShape) => | ||
| this.isActive(value) ? this.props.children : null | ||
| } | ||
| </Consumer> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| Variant.contextType = ExperimentsContext; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /** | ||
| * Copyright (c) 2019-present, NDLA. | ||
| * | ||
| * This source code is licensed under the GPLv3 license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
|
|
||
| import { ExperimentShape, VariationsShape } from './Experiment'; | ||
|
|
||
| export interface ExperimentShapeClean { | ||
| id: string; | ||
| variations: VariationsShape[]; | ||
| }; | ||
|
|
||
| export function cleanupExperiments(experiments: ExperimentShapeClean[], cookieExperiments: ExperimentShape[]) { | ||
| return experiments.map(experiment => { | ||
| const { | ||
| id, | ||
| variations, | ||
| } = experiment; | ||
|
|
||
| if (cookieExperiments) { | ||
| const experimentInCookie = cookieExperiments.find((cookieExperiments: ExperimentShape) => cookieExperiments.id === id); | ||
| if (experimentInCookie) { | ||
| return experimentInCookie; | ||
| } | ||
| } | ||
|
|
||
| const pickVariant = Math.random(); | ||
| let variationsWeightCounter = 0; | ||
| const variationsTotal = variations.length - 1; | ||
| const winner = variations.find((variation: VariationsShape, index: number) => { | ||
| if (variationsWeightCounter + (variation.weight || 0) > pickVariant || index === variationsTotal) { | ||
| return true; | ||
| } else { | ||
| variationsWeightCounter += (variation.weight || 0); | ||
| return false; | ||
| } | ||
| }); | ||
|
|
||
| if (typeof winner === 'object') { | ||
| winner.index = variations.findIndex(variant => variant.name === winner.name); | ||
| return { | ||
| id, | ||
| variant: winner, | ||
| } | ||
| } | ||
| return null; | ||
| }).filter(experiment => experiment); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| /** | ||
| * Copyright (c) 2019-present, NDLA. | ||
| * | ||
| * This source code is licensed under the GPLv3 license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
|
|
||
| export { ExperimentsContext } from './Context'; | ||
| export { Experiment, fetchVariantIndex, isValidExperiment } from './Experiment'; | ||
| export { Variant } from './Variant'; | ||
| export { cleanupExperiments } from './cleanupExperiments'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "extends": "../../tsconfig.build.json", | ||
| "compilerOptions": { | ||
| "baseUrl": "./", | ||
| "declarationDir": "./lib", | ||
| "rootDir": "./src" | ||
| }, | ||
| "include": ["./src"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| /** | ||
| * Copyright (c) 2019-present, NDLA. | ||
| * | ||
| * This source code is licensed under the GPLv3 license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
|
|
||
| // N.B! AUTOGENERATED FILE. DO NOT EDIT | ||
| import React from 'react'; | ||
| import Icon from '../Icon'; | ||
|
|
||
| const Hamburger = props => ( | ||
| <Icon | ||
| title="Hamburger" | ||
| viewBox="0 0 24 24" | ||
| data-license="Apache License 2.0" | ||
| data-source="Material Design" | ||
| {...props}> | ||
| <g> | ||
| <path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" /> | ||
| </g> | ||
| </Icon> | ||
| ); | ||
|
|
||
| export default Hamburger; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hvorfor bruke localeCompare? Og hvorfor ikke bruke isValidExperiment funksjonen fra under?