Skip to content

Commit

Permalink
新增 mip 前端小流量机制 (#560)
Browse files Browse the repository at this point in the history
* feat: 新增 mip 前端小流量机制

* 修改注释

* 调整变量名和 cookie 的失效时间
  • Loading branch information
zoumiaojiang authored and yenshih committed Apr 2, 2019
1 parent 1ee786f commit 47db705
Show file tree
Hide file tree
Showing 4 changed files with 347 additions and 0 deletions.
39 changes: 39 additions & 0 deletions packages/mip/src/experiment/config.js
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
// }
// }
}
194 changes: 194 additions & 0 deletions packages/mip/src/experiment/index.js
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
}
3 changes: 3 additions & 0 deletions packages/mip/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ import sleepWakeModule from './sleep-wake-module'
import performance from './performance'
import errorMonitorInstall from './log/error-monitor'
import {OUTER_MESSAGE_PERFORMANCE_UPDATE} from './page/const/index'
import {tryAssertAllAbTests} from './experiment/index'

// Ensure loaded only once
/* istanbul ignore next */
if (typeof window.MIP === 'undefined' || typeof window.MIP.version === 'undefined') {
errorMonitorInstall()
const MIP = getRuntime()
const abTestResult = tryAssertAllAbTests()

util.dom.waitDocumentReady(() => {
// init viewport
Expand Down Expand Up @@ -64,6 +66,7 @@ if (typeof window.MIP === 'undefined' || typeof window.MIP.version === 'undefine
performance.start(window._mipStartTiming)
// send performance data
performance.on('update', timing => {
timing.msids = abTestResult.join(',')
viewer.sendMessage(OUTER_MESSAGE_PERFORMANCE_UPDATE, timing)
})

Expand Down
111 changes: 111 additions & 0 deletions packages/mip/test/specs/experiment/index.spec.js
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)
})
})

0 comments on commit 47db705

Please sign in to comment.