diff --git a/README.md b/README.md index 1c4cc42..17386f2 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,24 @@ Vue.use(VueAB, { Note: The expiry date is refreshed with every page visit. The entry only expires, if the user doesn't come back in the specified time. +### Analytics options + +You can set an ***analytics handler***, on initialization. The only internally supported option currently is mixpanel, that will set a super property on the user like 'AB [test name]' equal to the wining slot name + +``` js +Vue.use(VueAB, { + analytics: 'mixpanel' +}) +``` + +If you want to create your own ***analytics handler*** you can simply pass a function + +``` js +Vue.use(VueAB, { + analytics: (name, winner) => console.log('AB Test', name, winner) +}) +``` + ### Component Name By default `` is the component that wraps a new test. This name can be overwritten on initialization: diff --git a/dist/index.js b/dist/index.js index 8b4ca4e..9034ad4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,2 +1,2 @@ -!function(a,b){typeof exports==='object'&&typeof module!=='undefined'?module.exports=b():typeof define==='function'&&define.amd?define(b):(a.vueSplitter=b())}(this,(function(){'use strict';function c(a,b){return b?c(b,a%b):a}function d(a){return a.reduce(function(a,b){return c(a,b)})}function e(a){var b=[],c=[];typeof a==='undefined'||typeof a==='string'?console.error('VueAB: variations must be Array || Object'):Array.isArray(a)?a.forEach(function(a){if(a.data&&a.data.slot){var d=a.data.attrs?a.data.attrs.chance:NaN,e=parseInt(d)||1;b.push(a.data.slot);c.push(e)}}):a!==null&&typeof a==='object'&&Object.keys(a).forEach(function(d){b.push(d);c.push(parseInt(a[d])||1)});var e=c.length?d(c):1;return b.reduce(function(a,b,d){var f=Math.round(c[d]/e);for(var g=0;gc.getTime()?d:null}function o(a,b,c){b.expiry=new Date().getTime()+j(c);localStorage.setItem(a,JSON.stringify(b))}var p={_store:null,name:'split-test',method:'cookie',expiry:30,_load:function a(){this.method==='cookie'?(this._store=l(this.name)||{}):this.method==='localStorage'?(this._store=n(this.name,this.expiry)||{}):(console.warn("VueA2B WARNING: No or invalid storage method. Data will not persist."),this._store={});return this._store},_save:function a(){this.method==='cookie'?m(this.name,this._store,this.expiry):this.method==='localStorage'&&o(this.name,this._store,this.expiry)},get entry () {return this._store||this._load()},set entry (a){var b=a.name,c=a.winner;this._store[b]=c;this._save()}},q=function(a,b){var c=p.entry[a]||g(b);p.entry={name:a,winner:c};return c},r={abtest:q,randomCandidate:g,install:function c(a,b){b===void 0&&(b={});if(b.storage){var d=b.storage;d.name&&(p.name=d.name);d.method&&(p.method=d.method);d.expiry&&(p.expiry=parseInt(expiry))}a.component(b.component||'split-test',{functional:!0,props:{always:String,name:String},render:function c(a,b){var d=b.props.name||b.parent.$options.name;if(!d){throw 'VueA2B Error: The test name is mandatory!'}var e=b.slots(),f=b.props.always||p.entry[d]||g(b.children);p.entry={name:d,winner:f};return e[f]}});a.mixin({beforeCreate:function a(){this.$abtest=q}})}};return r})) +!function(a,b){typeof exports==='object'&&typeof module!=='undefined'?module.exports=b():typeof define==='function'&&define.amd?define(b):(a.vueSplitter=b())}(this,(function(){'use strict';function c(a,b){return b?c(b,a%b):a}function d(a){return a.reduce(function(a,b){return c(a,b)})}function e(a){var b=[],c=[];typeof a==='undefined'||typeof a==='string'?console.error('VueAB: variations must be Array || Object'):Array.isArray(a)?a.forEach(function(a){if(a.data&&a.data.slot){var d=a.data.attrs?a.data.attrs.chance:NaN,e=parseInt(d)||1;b.push(a.data.slot);c.push(e)}}):a!==null&&typeof a==='object'&&Object.keys(a).forEach(function(d){b.push(d);c.push(parseInt(a[d])||1)});var e=c.length?d(c):1;return b.reduce(function(a,b,d){var f=Math.round(c[d]/e);for(var g=0;gc.getTime()?d:null}function o(a,b,c){b.expiry=new Date().getTime()+j(c);localStorage.setItem(a,JSON.stringify(b))}var p={_store:null,name:'split-test',method:'cookie',expiry:30,_load:function a(){this.method==='cookie'?(this._store=l(this.name)||{}):this.method==='localStorage'?(this._store=n(this.name,this.expiry)||{}):(console.warn("VueA2B WARNING: No or invalid storage method. Data will not persist."),this._store={});return this._store},_save:function a(){this.method==='cookie'?m(this.name,this._store,this.expiry):this.method==='localStorage'&&o(this.name,this._store,this.expiry)},get entry () {return this._store||this._load()},set entry (a){var b=a.name,c=a.winner;this._store[b]=c;this._save()}},q=function(a,b){var c=p.entry[a]||g(b);p.entry={name:a,winner:c};return c};function r(a,b,c){c===void 0&&(c=100);if(window.mixpanel){var d={};d['A/B '+a]=b;window.mixpanel.register_once(d)}else setTimeout(function(){return r(a,b,c-1)},1000)}var s={mixpanel:r},t={abtest:q,randomCandidate:g,install:function c(a,b){b===void 0&&(b={});if(b.storage){var d=b.storage;d.name&&(p.name=d.name);d.method&&(p.method=d.method);d.expiry&&(p.expiry=parseInt(expiry))}a.component(b.component||'split-test',{functional:!0,props:{always:String,name:String},render:function d(a,c){var e=c.props.name||c.parent.$options.name;if(!e){throw 'VueA2B Error: The test name is mandatory!'}var f=c.slots(),h=c.props.always||p.entry[e]||g(c.children);p.entry={name:e,winner:h};s[b.analytics]?s[b.analytics](e,h):typeof b.analytics==="function"&&b.analytics(e,h);return f[h]}});a.mixin({beforeCreate:function a(){this.$abtest=q}})}};return t})) //# sourceMappingURL=index.js.map diff --git a/dist/index.js.map b/dist/index.js.map index ec0d62a..a2594a6 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sources":["../src/toolbox.js","../src/persistence.js","../src/index.js"],"sourcesContent":["// greatest common denominator\nfunction gcd (a, b) {\n return b ? gcd(b, a % b) : a\n}\n\n// gcd for n elements\nfunction gcdOfList (ary) {\n return ary.reduce((acc, v) => gcd(acc, v))\n}\n\n// create a list with test candidate that respects the selection chances\nfunction getCandidates (variations) {\n let names = []\n let chances = []\n\n if (typeof variations === 'undefined' || typeof variations === 'string') {\n console.error('VueAB: variations must be Array || Object')\n }\n else if (Array.isArray(variations)) { // assume list of VNodes\n variations.forEach(v => {\n if (v.data && v.data.slot) {\n const chanceAttr = v.data.attrs ? v.data.attrs.chance : NaN\n const chance = parseInt(chanceAttr) || 1\n names.push(v.data.slot)\n chances.push(chance)\n }\n })\n }\n else if (variations !== null && typeof variations === 'object') {\n // assume name:chance object\n Object.keys(variations).forEach(k => {\n names.push(k)\n chances.push(parseInt(variations[k]) || 1)\n })\n }\n\n const divisor = chances.length ? gcdOfList(chances) : 1\n\n return names.reduce((result, name, i) => {\n const n = Math.round(chances[i] / divisor)\n for (let i = 0; i < n; i++) result.push(name)\n return result\n }, [])\n}\n\n// return random element of array\nexport function pickRandomlyFrom (array) {\n const index = Math.floor(Math.random() * array.length)\n return array[index]\n}\n\n// pick a random candidate respecting the selection chances\nexport function randomCandidate (variations) {\n const candidates = getCandidates(variations)\n return pickRandomlyFrom(candidates)\n}\n\nfunction daysToMicroSeconds (days) {\n return days * 24 * 3600 * 1000\n}\n\nexport function getCookie (name) {\n const entries = document.cookie.split(';')\n const len = name.length\n\n for (const i in entries) {\n const entry = entries[i].trim()\n\n if (entry.substr(0, len) === name) {\n const value = decodeURIComponent( entry.slice(len + 1) )\n return JSON.parse(value)\n }\n }\n}\n\nexport function writeCookie (name, data, expiry) {\n const d = new Date()\n d.setTime(d.getTime() + daysToMicroSeconds(expiry))\n\n const options = `expires=${d.toUTCString()};path=/`\n data = encodeURIComponent(JSON.stringify(data))\n document.cookie = `${name}=${data};${options}`\n}\n\nexport function getLocalStorage (name, expiry) {\n const d = new Date()\n d.setTime(d.getTime() + daysToMicroSeconds(expiry))\n const entry = JSON.parse(localStorage.getItem(name))\n\n return entry && entry.expires > d.getTime() ? entry : null\n}\n\nexport function writeLocalStorage (name, value, expiry) {\n value.expiry = new Date().getTime() + daysToMicroSeconds(expiry)\n localStorage.setItem(name, JSON.stringify(value))\n}\n","import {\n getCookie, writeCookie,\n getLocalStorage, writeLocalStorage,\n randomCandidate\n} from './toolbox'\n\nexport const storage = {\n _store: null,\n name: 'split-test', // name of cookie or localStorage entry\n method: 'cookie', // supported methods are 'cookie' and 'localStorage'\n expiry: 30, // ignore entries that weren't touched this amount of days\n\n _load () {\n if (this.method === 'cookie')\n this._store = getCookie(this.name) || {}\n else if (this.method === 'localStorage')\n this._store = getLocalStorage(this.name, this.expiry) || {}\n else {\n console.warn(\"VueA2B WARNING: No or invalid storage method. Data will not persist.\")\n this._store = {}\n }\n return this._store\n },\n\n _save () {\n if (this.method === 'cookie')\n writeCookie(this.name, this._store, this.expiry)\n else if (this.method === 'localStorage')\n writeLocalStorage(this.name, this._store, this.expiry)\n },\n\n get entry () {\n return this._store || this._load()\n },\n\n set entry ({name, winner}) {\n this._store[name] = winner\n this._save()\n }\n}\n\nexport const selectAB = (name, variants) => {\n const winner = storage.entry[name] || randomCandidate(variants)\n storage.entry = {name, winner}\n return winner\n}\n\n","import { storage, selectAB } from './persistence'\nimport { randomCandidate } from './toolbox'\n\nconst VueAB = {\n abtest: selectAB,\n randomCandidate,\n install (Vue, options = {}) {\n if (options.storage) {\n const cfg = options.storage\n if (cfg.name) storage.name = cfg.name\n if (cfg.method) storage.method = cfg.method\n if (cfg.expiry) storage.expiry = parseInt(expiry)\n }\n\n Vue.component(options.component || 'split-test', {\n functional: true,\n props: {\n always: String,\n name: String\n },\n render (h, ctx) {\n const name = ctx.props.name || ctx.parent.$options.name\n if (!name) throw 'VueA2B Error: The test name is mandatory!'\n\n const variations = ctx.slots()\n const winner = ctx.props.always\n || storage.entry[name]\n || randomCandidate(ctx.children)\n\n storage.entry = {name, winner}\n return variations[winner]\n }\n })\n\n Vue.mixin({\n beforeCreate () {\n this.$abtest = selectAB\n }\n })\n }\n}\n\nexport default VueAB\n"],"names":["gcd","a","b","gcdOfList","ary","acc","v","getCandidates","variations","let","names","chances","const","chanceAttr","chance","k","divisor","result","name","i","n","pickRandomlyFrom","array","index","randomCandidate","candidates","daysToMicroSeconds","days","getCookie","entries","len","entry","value","writeCookie","data","expiry","d","options","getLocalStorage","writeLocalStorage","storage","ref","winner","selectAB","variants","VueAB","Vue","cfg","h","ctx"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sources":["../src/toolbox.js","../src/persistence.js","../src/analytics.js","../src/index.js"],"sourcesContent":["// greatest common denominator\nfunction gcd (a, b) {\n return b ? gcd(b, a % b) : a\n}\n\n// gcd for n elements\nfunction gcdOfList (ary) {\n return ary.reduce((acc, v) => gcd(acc, v))\n}\n\n// create a list with test candidate that respects the selection chances\nfunction getCandidates (variations) {\n let names = []\n let chances = []\n\n if (typeof variations === 'undefined' || typeof variations === 'string') {\n console.error('VueAB: variations must be Array || Object')\n }\n else if (Array.isArray(variations)) { // assume list of VNodes\n variations.forEach(v => {\n if (v.data && v.data.slot) {\n const chanceAttr = v.data.attrs ? v.data.attrs.chance : NaN\n const chance = parseInt(chanceAttr) || 1\n names.push(v.data.slot)\n chances.push(chance)\n }\n })\n }\n else if (variations !== null && typeof variations === 'object') {\n // assume name:chance object\n Object.keys(variations).forEach(k => {\n names.push(k)\n chances.push(parseInt(variations[k]) || 1)\n })\n }\n\n const divisor = chances.length ? gcdOfList(chances) : 1\n\n return names.reduce((result, name, i) => {\n const n = Math.round(chances[i] / divisor)\n for (let i = 0; i < n; i++) result.push(name)\n return result\n }, [])\n}\n\n// return random element of array\nexport function pickRandomlyFrom (array) {\n const index = Math.floor(Math.random() * array.length)\n return array[index]\n}\n\n// pick a random candidate respecting the selection chances\nexport function randomCandidate (variations) {\n const candidates = getCandidates(variations)\n return pickRandomlyFrom(candidates)\n}\n\nfunction daysToMicroSeconds (days) {\n return days * 24 * 3600 * 1000\n}\n\nexport function getCookie (name) {\n const entries = document.cookie.split(';')\n const len = name.length\n\n for (const i in entries) {\n const entry = entries[i].trim()\n\n if (entry.substr(0, len) === name) {\n const value = decodeURIComponent( entry.slice(len + 1) )\n return JSON.parse(value)\n }\n }\n}\n\nexport function writeCookie (name, data, expiry) {\n const d = new Date()\n d.setTime(d.getTime() + daysToMicroSeconds(expiry))\n\n const options = `expires=${d.toUTCString()};path=/`\n data = encodeURIComponent(JSON.stringify(data))\n document.cookie = `${name}=${data};${options}`\n}\n\nexport function getLocalStorage (name, expiry) {\n const d = new Date()\n d.setTime(d.getTime() + daysToMicroSeconds(expiry))\n const entry = JSON.parse(localStorage.getItem(name))\n\n return entry && entry.expires > d.getTime() ? entry : null\n}\n\nexport function writeLocalStorage (name, value, expiry) {\n value.expiry = new Date().getTime() + daysToMicroSeconds(expiry)\n localStorage.setItem(name, JSON.stringify(value))\n}\n","import {\n getCookie, writeCookie,\n getLocalStorage, writeLocalStorage,\n randomCandidate\n} from './toolbox'\n\nexport const storage = {\n _store: null,\n name: 'split-test', // name of cookie or localStorage entry\n method: 'cookie', // supported methods are 'cookie' and 'localStorage'\n expiry: 30, // ignore entries that weren't touched this amount of days\n\n _load () {\n if (this.method === 'cookie')\n this._store = getCookie(this.name) || {}\n else if (this.method === 'localStorage')\n this._store = getLocalStorage(this.name, this.expiry) || {}\n else {\n console.warn(\"VueA2B WARNING: No or invalid storage method. Data will not persist.\")\n this._store = {}\n }\n return this._store\n },\n\n _save () {\n if (this.method === 'cookie')\n writeCookie(this.name, this._store, this.expiry)\n else if (this.method === 'localStorage')\n writeLocalStorage(this.name, this._store, this.expiry)\n },\n\n get entry () {\n return this._store || this._load()\n },\n\n set entry ({name, winner}) {\n this._store[name] = winner\n this._save()\n }\n}\n\nexport const selectAB = (name, variants) => {\n const winner = storage.entry[name] || randomCandidate(variants)\n storage.entry = {name, winner}\n return winner\n}\n\n","\nfunction mixpanel (name, winner, countdown=100) {\n if (window.mixpanel) {\n var super_property = {}\n super_property['A/B ' + name] = winner\n window.mixpanel.register_once(super_property)\n } else {\n // loop until mixpanel is loaded\n setTimeout(() => mixpanel(name, winner, countdown - 1), 1000)\n }\n}\n\nexport default {\n mixpanel\n}\n","import { storage, selectAB } from './persistence'\nimport { randomCandidate } from './toolbox'\nimport analytics from './analytics.js'\nconst VueAB = {\n abtest: selectAB,\n randomCandidate,\n install (Vue, options = {}) {\n if (options.storage) {\n const cfg = options.storage\n if (cfg.name) storage.name = cfg.name\n if (cfg.method) storage.method = cfg.method\n if (cfg.expiry) storage.expiry = parseInt(expiry)\n }\n\n Vue.component(options.component || 'split-test', {\n functional: true,\n props: {\n always: String,\n name: String\n },\n render (h, ctx) {\n const name = ctx.props.name || ctx.parent.$options.name\n if (!name) throw 'VueA2B Error: The test name is mandatory!'\n\n const variations = ctx.slots()\n const winner = ctx.props.always\n || storage.entry[name]\n || randomCandidate(ctx.children)\n\n storage.entry = {name, winner}\n\n if (analytics[options.analytics]) {\n analytics[options.analytics](name, winner)\n } else if (typeof options.analytics === \"function\"){\n options.analytics(name, winner)\n }\n return variations[winner]\n }\n })\n\n Vue.mixin({\n beforeCreate () {\n this.$abtest = selectAB\n }\n })\n }\n}\n\nexport default VueAB\n"],"names":["gcd","a","b","gcdOfList","ary","acc","v","getCandidates","variations","let","names","chances","const","chanceAttr","chance","k","divisor","result","name","i","n","pickRandomlyFrom","array","index","randomCandidate","candidates","daysToMicroSeconds","days","getCookie","entries","len","entry","value","writeCookie","data","expiry","d","options","getLocalStorage","writeLocalStorage","storage","ref","winner","selectAB","variants","mixpanel","countdown","super_property","VueAB","Vue","cfg","h","ctx","analytics"],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json index aa0b2a2..d5149d1 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ "build": "cross-env NODE_ENV=production rollup -c", "dev": "cross-env NODE_ENV=development rollup -cw" }, - "dependencies": { - }, + "dependencies": {}, "devDependencies": { "cross-env": "^5.0.0", "rollup": "^0.41.4", diff --git a/src/analytics.js b/src/analytics.js new file mode 100644 index 0000000..e0ce2c5 --- /dev/null +++ b/src/analytics.js @@ -0,0 +1,15 @@ + +function mixpanel (name, winner, countdown=100) { + if (window.mixpanel) { + var super_property = {} + super_property['A/B ' + name] = winner + window.mixpanel.register_once(super_property) + } else { + // loop until mixpanel is loaded + setTimeout(() => mixpanel(name, winner, countdown - 1), 1000) + } +} + +export default { + mixpanel +} diff --git a/src/index.js b/src/index.js index 713470b..4fa2e1d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import { storage, selectAB } from './persistence' import { randomCandidate } from './toolbox' - +import analytics from './analytics.js' const VueAB = { abtest: selectAB, randomCandidate, @@ -28,6 +28,12 @@ const VueAB = { || randomCandidate(ctx.children) storage.entry = {name, winner} + + if (analytics[options.analytics]) { + analytics[options.analytics](name, winner) + } else if (typeof options.analytics === "function"){ + options.analytics(name, winner) + } return variations[winner] } })