-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
230cc52
commit 247607a
Showing
9 changed files
with
343 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Local Storage | ||
|
||
Enhanced local storage API | ||
|
||
## Install | ||
|
||
``` | ||
npm install -D @rambler-tech/local-storage | ||
``` | ||
|
||
or | ||
|
||
``` | ||
yarn add -D @rambler-tech/local-storage | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import {getItem, setItem} from '@rambler-tech/cookie-storage' | ||
import {Splits, SplitsVariations} from '.' | ||
|
||
let splits: Splits | ||
let initialVariations: SplitsVariations | ||
|
||
setItem('foo', 'bar') | ||
|
||
test('initial split', () => { | ||
const tests = [ | ||
{ | ||
name: 'login', | ||
variations: [ | ||
{ | ||
name: 'compact', | ||
probability: [0, 0.5] | ||
} | ||
] | ||
}, | ||
{ | ||
name: 'phone', | ||
cookie: 'login', | ||
variations: [ | ||
{ | ||
name: 'without', | ||
probability: [0.5, 1] | ||
} | ||
] | ||
} | ||
] | ||
|
||
const options = { | ||
prefix: 'x_' | ||
} | ||
|
||
splits = new Splits(tests, options) | ||
initialVariations = splits.getVariations() | ||
|
||
expect(Object.keys(initialVariations)).toEqual(tests.map((ts) => ts.name)) | ||
expect(initialVariations.login).not.toBe(initialVariations.phone) | ||
expect(getItem('foo')).toBe('bar') | ||
expect(getItem('x_login')).toBeTruthy() | ||
}) | ||
|
||
test('get variations', () => { | ||
const variations = splits.getVariations() | ||
|
||
expect(variations).toEqual(initialVariations) | ||
expect(variations.login).not.toBe(variations.phone) | ||
expect(getItem('foo')).toBe('bar') | ||
expect(getItem('x_login')).toBeTruthy() | ||
}) | ||
|
||
test('drop irrelevant cookies', () => { | ||
const tests = [ | ||
{ | ||
name: 'phone', | ||
variations: [ | ||
{ | ||
name: 'without', | ||
probability: [0.5, 1] | ||
} | ||
] | ||
} | ||
] | ||
|
||
const options = { | ||
prefix: 'x_' | ||
} | ||
|
||
const nextSplits = new Splits(tests, options) | ||
|
||
nextSplits.getVariations() | ||
|
||
expect(getItem('foo')).toBe('bar') | ||
expect(getItem('x_phone')).toBeTruthy() | ||
expect(getItem('x_login')).toBeFalsy() | ||
}) | ||
|
||
test('wrong settings', () => { | ||
expect(() => new Splits({} as any)).toThrow( | ||
'expected `settings.tests` is not empty array' | ||
) | ||
expect( | ||
() => | ||
new Splits([ | ||
{ | ||
name: 'phone number' | ||
} | ||
] as any) | ||
).toThrow('expected `phone number` contains `A-Za-z0-9_-` symbols only') | ||
expect( | ||
() => | ||
new Splits([ | ||
{ | ||
name: 'phone', | ||
variations: null | ||
} | ||
] as any) | ||
).toThrow('expected `variations` of `phone` is not empty array') | ||
expect( | ||
() => | ||
new Splits([ | ||
{ | ||
name: 'phone', | ||
variations: [ | ||
{ | ||
name: 'short', | ||
probability: null | ||
} | ||
] | ||
} | ||
] as any) | ||
).toThrow('expected `probability` of `phone.short` is array of numbers') | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
/* eslint-disable import/no-unused-modules */ | ||
import { | ||
getAll, | ||
getItem, | ||
setItem, | ||
removeItem, | ||
type SetCookieItemOptions | ||
} from '@rambler-tech/cookie-storage' | ||
|
||
const DEFAULT_OPTIONS: SplitsOptions = { | ||
path: '/', | ||
expires: 182, | ||
domain: `.${window.location.hostname}`, | ||
samesite: 'none', | ||
secure: true | ||
} | ||
|
||
/** Split test declaration */ | ||
export interface SplitTest { | ||
/** Test name */ | ||
name: string | ||
/** Custom cookie name */ | ||
cookie?: string | ||
/** Test variations */ | ||
variations: SplitTestVariation[] | ||
} | ||
|
||
/** Split test variation declaration */ | ||
export interface SplitTestVariation { | ||
/** Variation name */ | ||
name: string | ||
/** Variation probability */ | ||
probability: number[] | ||
} | ||
|
||
/** Split options */ | ||
export interface SplitsOptions extends Omit<SetCookieItemOptions, 'raw'> { | ||
/** Cookie prefix */ | ||
prefix?: string | ||
} | ||
|
||
/** Split calculation result */ | ||
export interface SplitsVariations { | ||
[key: string]: string | ||
} | ||
|
||
/** | ||
* Splits calculator | ||
* | ||
* ```ts | ||
* const tests = [ | ||
* { | ||
* name: 'loginType', | ||
* variations: [ | ||
* { | ||
* name: 'compact', | ||
* probability: [0, 0.5] | ||
* }, | ||
* { | ||
* name: 'full', | ||
* probability: [0.5, 1] | ||
* } | ||
* ] | ||
* } | ||
* ] | ||
* | ||
* const splits = new Splits(tests, {prefix: 'x_'}) | ||
* const variations = splits.getVariations() | ||
* | ||
* // {loginType: 'compact'} | ||
* ``` | ||
*/ | ||
export class Splits { | ||
private tests: SplitTest[] | ||
private options: SplitsOptions | ||
|
||
public constructor(tests: SplitTest[], options: SplitsOptions = {}) { | ||
if (!Array.isArray(tests) || !tests.length) { | ||
throw new Error('expected `settings.tests` is not empty array') | ||
} | ||
|
||
tests.forEach(({name, variations}) => { | ||
if (!/^[A-Za-z0-9_-]+$/.test(name)) { | ||
throw new Error( | ||
`expected \`${name}\` contains \`A-Za-z0-9_-\` symbols only` | ||
) | ||
} | ||
|
||
if (!Array.isArray(variations) || !variations.length) { | ||
throw new Error( | ||
`expected \`variations\` of \`${name}\` is not empty array` | ||
) | ||
} | ||
|
||
variations.forEach(({name: varName, probability}) => { | ||
if ( | ||
!Array.isArray(probability) || | ||
typeof probability[0] !== 'number' || | ||
typeof probability[1] !== 'number' | ||
) { | ||
throw new Error( | ||
`expected \`probability\` of \`${name}.${varName}\` is array of numbers` | ||
) | ||
} | ||
}) | ||
}) | ||
|
||
this.tests = tests | ||
this.options = {...DEFAULT_OPTIONS, ...options} | ||
} | ||
|
||
/** Get calculated variations */ | ||
public getVariations(): SplitsVariations { | ||
const variations = this.tests.reduce<SplitsVariations>( | ||
(accumulator, {name, cookie, variations}) => { | ||
accumulator[name] = this.chooseVariation( | ||
variations, | ||
this.getProbability(cookie || name, this.options) | ||
) | ||
|
||
return accumulator | ||
}, | ||
{} | ||
) | ||
|
||
this.dropIrrelevantProbabilities() | ||
|
||
return variations | ||
} | ||
|
||
private dropIrrelevantProbabilities(): void { | ||
const items = getAll() | ||
|
||
const { | ||
tests, | ||
options: {prefix = '', ...options} | ||
} = this | ||
|
||
if (items) { | ||
const removal = Object.keys(items).filter( | ||
(cookieName) => | ||
cookieName.indexOf(prefix) === 0 && | ||
!tests.filter( | ||
({name, cookie}) => cookieName === `${prefix}${cookie || name}` | ||
).length | ||
) | ||
|
||
removal.forEach((cookieName) => removeItem(cookieName, options)) | ||
} | ||
} | ||
|
||
private getProbability(name: string, {prefix = '', ...options}): number { | ||
const cookieName = `${prefix}${name}` | ||
let probability = getItem<number>(cookieName) | ||
|
||
if (!probability || typeof probability !== 'number') { | ||
probability = Math.random() | ||
} | ||
|
||
setItem<number>(cookieName, probability, options) | ||
|
||
return probability | ||
} | ||
|
||
private chooseVariation( | ||
variations: SplitTestVariation[], | ||
probability: number | ||
): string { | ||
return variations.reduce<string>( | ||
(accumulator, {name, probability: [start, end]}) => | ||
probability >= start && probability < end ? name : accumulator, | ||
'default' | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"name": "@rambler-tech/splits", | ||
"version": "0.0.0", | ||
"main": "dist", | ||
"module": "dist", | ||
"types": "dist/index.d.ts", | ||
"license": "MIT", | ||
"sideEffects": false, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"dependencies": { | ||
"@rambler-tech/cookie-storage": "*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"module": "commonjs", | ||
"baseUrl": ".", | ||
"outDir": "dist" | ||
}, | ||
"include": [".", "../../types"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"extends": "@rambler-tech/typedoc-config", | ||
"readme": "../../README.md", | ||
"entryPoints": ["./index.ts"] | ||
} |