-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: 新增 mip 前端小流量机制 * 修改注释 * 调整变量名和 cookie 的失效时间
- Loading branch information
1 parent
1ee786f
commit 47db705
Showing
4 changed files
with
347 additions
and
0 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/** | ||
* @file MIP 前端实验配置文件 | ||
* @author mj([email protected]) | ||
*/ | ||
|
||
export default { | ||
// site: { | ||
// /** | ||
// * 如果有实验需求就按照如下的格式配置 | ||
// */ | ||
// test1: { | ||
// description: 'Test1 实验的描述', | ||
// startTime: '2019-02-13 00:00:00', | ||
// endTime: '2019-04-10 23:59:59', | ||
// sites: [ | ||
// 'baobao.baidu.com', | ||
// 'muzhi.baidu.com', | ||
// 'm.120ask.com' | ||
// ] | ||
// } | ||
// }, | ||
|
||
// abTest: { | ||
// test1: { | ||
// description: 'abTest1 实验描述', | ||
// startTime: '2019-02-14 00:00:00', | ||
// endTime: '2019-03-25 17:08:00', | ||
// // 所开的流量的百分比 | ||
// ratio: 100 | ||
// }, | ||
// test2: { | ||
// description: 'abTest1 实验描述', | ||
// startTime: '2019-03-21 00:00:00', | ||
// endTime: '2019-03-27 23:59:59', | ||
// // 所开的流量的百分比 | ||
// ratio: 50 | ||
// } | ||
// } | ||
} |
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,194 @@ | ||
/** | ||
* @file 实验 API 提供入口 | ||
* @author mj([email protected]) | ||
*/ | ||
|
||
import defaultExperimentConfig from './config' | ||
|
||
/** | ||
* 默认的 cookie 失效时间 | ||
* | ||
* @type {number} | ||
* @const | ||
*/ | ||
const COOKIE_DEFAULT_EXPIRES = 24 * 60 * 60 * 1000 | ||
|
||
let experimentConfig = defaultExperimentConfig | ||
|
||
/** | ||
* 判断是否是实验生效状态 | ||
* | ||
* @param {Date|stirng|number} start 开始时间 | ||
* @param {Date|stirng|number} end 结束事件 | ||
* @returns {boolean} 当前时间是否生效的结果 | ||
*/ | ||
function isTimeInRange (start, end) { | ||
let expStartTime = (new Date(start || Date.now())).getTime() | ||
let expEndTime = (new Date(end || '2099-01-01')).getTime() | ||
let nowTime = Date.now() | ||
|
||
// 如果实验生效时间配置错误 | ||
if (!expStartTime || !expEndTime || expStartTime > expEndTime) { | ||
return false | ||
} | ||
|
||
return nowTime >= expStartTime && nowTime <= expEndTime | ||
} | ||
|
||
/** | ||
* 设置 cookie | ||
* | ||
* @param {string} key cookie 的 key | ||
* @param {string} value cookie 的 value | ||
* @param {Date} expires cookie 的失效时间 | ||
*/ | ||
function setCookie (key, value, expires) { | ||
var domain = document.domain | ||
let date = new Date() | ||
date.setTime(Date.now() + expires) | ||
document.cookie = `${key}=${escape(value)};path=/;expires=${date.toGMTString()};domain=${domain};` | ||
} | ||
|
||
/** | ||
* 获取 cookie 的内容 | ||
* | ||
* @param {string} key cookie 的名称 | ||
* @returns {string} 获取的 cookie 的内容 | ||
*/ | ||
function getCookie (key) { | ||
let reg = new RegExp('(^| )' + key + '=([^;]*)(;|$)') | ||
let arr = document.cookie.match(reg) | ||
|
||
if (arr && arr[2]) { | ||
return unescape(arr[2]) | ||
} | ||
return '' | ||
} | ||
|
||
/** | ||
* 全局设置实验配置(提供一种 API 设置全局实验配置的机制) | ||
* | ||
* @param {Object} conf 待设置的配置信息 | ||
*/ | ||
export function setConfig (conf) { | ||
experimentConfig = conf | ||
} | ||
|
||
/** | ||
* 验证是否命中 sites 类型的实验 | ||
* | ||
* @param {string} expName 实验名称 | ||
* @returns {boolean} 实验命中情况的结果 | ||
*/ | ||
export function assertSite (expName) { | ||
let siteConf = experimentConfig.site || {} | ||
let expConf = siteConf[expName] | ||
|
||
if (!expConf) { | ||
return false | ||
} | ||
|
||
let currentUrl = window.location.href | ||
let expSites = expConf.sites || [] | ||
|
||
for (let i = 0; i < expSites.length; i++) { | ||
let siteHost = expSites[i] | ||
let siteCacheName = siteHost.replace(/\./g, '-') | ||
let { startTime, endTime } = expConf | ||
|
||
if ((currentUrl.indexOf(siteHost) > -1 || | ||
currentUrl.indexOf(siteCacheName) > -1) && | ||
isTimeInRange(startTime, endTime) | ||
) { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* 验证是否命中了 ABTest 类型实验 | ||
* | ||
* @param {string} expName 实验名称 | ||
* @returns {boolean} 实验命中情况的结果 | ||
*/ | ||
export function assertAbTest (expName) { | ||
return !!getCookie(expName) | ||
} | ||
|
||
/** | ||
* 处理 abTest 小流量命中情况 | ||
* | ||
* @param {string} expName 实验名称 | ||
* @param {?Object} abExpConf abTest 小流量实验配置 | ||
* @returns {boolean} 当前实验是否命中 | ||
*/ | ||
function isShootAbTest (expName, abExpConf = {}) { | ||
let probabilityControlArr = [] | ||
let { | ||
startTime, | ||
endTime, | ||
ratio | ||
} = abExpConf | ||
|
||
setCookie(expName, '', -1) | ||
|
||
if (!ratio || typeof ratio !== 'number' || ratio < 0 || !isTimeInRange(startTime, endTime)) { | ||
// 如果实验不在生效时间中,则直接标识未命中 | ||
return false | ||
} | ||
|
||
for (let i = 0; i < 100; i++) { | ||
probabilityControlArr[i] = i < ratio | ||
} | ||
|
||
let expIndex = parseInt(Math.random() * 100, 10) | ||
|
||
// 如果实验在设定的生效时间中,就可以判断是否命中 | ||
if (probabilityControlArr[expIndex]) { | ||
let expEndTime = (new Date(endTime)).getTime() | ||
let nowTime = Date.now() | ||
let defaultExpires = expEndTime ? expEndTime - nowTime : COOKIE_DEFAULT_EXPIRES | ||
|
||
// 命中实验了,将命中结果存如 cookie,保证当前用户每次都能命中,cookie 默认生效时间 24 小时 | ||
setCookie(expName, JSON.stringify(abExpConf), defaultExpires > 0 ? defaultExpires : COOKIE_DEFAULT_EXPIRES) | ||
return true | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* 获取 abTest 命中的所有实验标识 | ||
* | ||
* @returns {Array<String>} 命中的实验名称标识列表 | ||
*/ | ||
export function tryAssertAllAbTests () { | ||
let abTestNames = [] | ||
|
||
Object.keys(experimentConfig.abTest || {}).forEach(expName => { | ||
let abTestConf = experimentConfig.abTest || {} | ||
let expConf = abTestConf[expName] | ||
let { startTime, endTime } = expConf | ||
|
||
// 如果实验过期,就整体下线 | ||
if (!isTimeInRange(startTime, endTime)) { | ||
setCookie(expName, '', -1) | ||
} | ||
|
||
let cookieResult = getCookie(expName) | ||
|
||
// 如果 cookie 已经记录了命中情况,就不需要重新再走命中判断逻辑 | ||
if (cookieResult) { | ||
// 如果实验配置的信息没有发生变更,直接返回命中情况 | ||
cookieResult === JSON.stringify(expConf) | ||
? abTestNames.push(expName) | ||
: isShootAbTest(expName, expConf) && abTestNames.push(expName) | ||
} else { | ||
isShootAbTest(expName, expConf) && abTestNames.push(expName) | ||
} | ||
}) | ||
|
||
return abTestNames | ||
} |
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,111 @@ | ||
/** | ||
* @file experiment 单测 | ||
* @author mj([email protected]) | ||
*/ | ||
|
||
import { | ||
assertAbTest, | ||
assertSite, | ||
tryAssertAllAbTests, | ||
setConfig | ||
} from 'src/experiment/index' | ||
|
||
/* global describe, it, before, expect */ | ||
|
||
describe('MIP front-end experiment', () => { | ||
before(() => { | ||
setConfig({ | ||
site: { | ||
/** | ||
* 如果有实验需求就按照如下的格式配置 | ||
*/ | ||
test1: { | ||
description: 'Test1 实验的描述', | ||
sites: [ | ||
'localhost' | ||
] | ||
}, | ||
test2: { | ||
description: 'Test2 实验的描述', | ||
sites: [ | ||
'unkonw.host' | ||
] | ||
}, | ||
test3: { | ||
description: 'Test3 实验的描述', | ||
startTime: '2019-02-13 00:00:00', | ||
endTime: '2099-04-10 23:59:59', | ||
sites: [ | ||
'localhost' | ||
] | ||
}, | ||
test4: { | ||
description: 'Test1 实验的描述', | ||
startTime: '2019-02-13 00:00:00', | ||
endTime: '2019-03-10 23:59:59', | ||
sites: [ | ||
'localhost' | ||
] | ||
} | ||
}, | ||
|
||
abTest: { | ||
test1: { | ||
description: 'abTest1 实验描述', | ||
startTime: '2019-02-14 00:00:00', | ||
endTime: '2099-03-25 17:08:00', | ||
// 所开的流量的百分比 | ||
ratio: 100 | ||
}, | ||
test2: { | ||
description: 'abTest2 实验描述', | ||
startTime: '2019-03-21 00:00:00', | ||
endTime: '2019-03-21 23:59:59', | ||
// 所开的流量的百分比 | ||
ratio: 100 | ||
}, | ||
test3: { | ||
description: 'abTest3 实验描述', | ||
startTime: '2019-02-14 00:00:00', | ||
endTime: '2099-03-25 17:08:00', | ||
// 所开的流量的百分比 | ||
ratio: 0 | ||
}, | ||
test4: { | ||
description: 'abTest4 实验描述', | ||
// 所开的流量的百分比 | ||
ratio: 100 | ||
} | ||
} | ||
}) | ||
}) | ||
|
||
it('should run right site experiment', () => { | ||
let site1AssertResult = assertSite('test1') | ||
let site2AssertResult = assertSite('test2') | ||
let site3AssertResult = assertSite('test3') | ||
let site4AssertResult = assertSite('test4') | ||
|
||
expect(site1AssertResult, true) | ||
expect(site2AssertResult, false) | ||
expect(site3AssertResult, true) | ||
expect(site4AssertResult, false) | ||
}) | ||
|
||
it('should get abTest names', () => { | ||
let arr = tryAssertAllAbTests() | ||
expect(arr).to.be.a('array') | ||
}) | ||
|
||
it('should shoot abTest', () => { | ||
let abTest1AssertResult = assertAbTest('test1') | ||
let abTest2AssertResult = assertAbTest('test2') | ||
let abTest3AssertResult = assertAbTest('test3') | ||
let abTest4AssertResult = assertAbTest('test4') | ||
|
||
expect(abTest1AssertResult, true) | ||
expect(abTest2AssertResult, false) | ||
expect(abTest3AssertResult, false) | ||
expect(abTest4AssertResult, true) | ||
}) | ||
}) |