├── .eslintignore ├── renovate.json ├── test ├── fixture │ ├── layouts │ │ └── default.vue │ ├── experiments │ │ └── index.js │ ├── pages │ │ └── _any.vue │ └── nuxt.config.js └── module.test.js ├── .gitignore ├── .editorconfig ├── .eslintrc.js ├── .circleci └── config.yml ├── lib ├── module.js └── plugin.js ├── LICENSE ├── package.json ├── CHANGELOG.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/plugin.js 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | *.log* 5 | .nuxt 6 | .vscode 7 | .DS_STORE 8 | coverage 9 | dist -------------------------------------------------------------------------------- /test/fixture/experiments/index.js: -------------------------------------------------------------------------------- 1 | export default [{ 2 | name: 'test1', 3 | experimentID: 'id1', 4 | variants: [ 5 | { weight: 100 }, 6 | { weight: 0 } 7 | ], 8 | maxAge: 120 9 | }] 10 | -------------------------------------------------------------------------------- /test/fixture/pages/_any.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /test/fixture/nuxt.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | module.exports = { 4 | rootDir: resolve(__dirname, '../..'), 5 | srcDir: __dirname, 6 | buildDir: resolve(__dirname, '.nuxt'), 7 | dev: false, 8 | render: { 9 | resourceHints: false 10 | }, 11 | modules: [resolve(__dirname, '../../lib/module')] 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | sourceType: 'module' 5 | }, 6 | env: { 7 | browser: true, 8 | node: true, 9 | jest: true 10 | }, 11 | extends: 'standard', 12 | plugins: [ 13 | 'jest', 14 | 'vue' 15 | ], 16 | rules: { 17 | // Allow paren-less arrow functions 18 | 'arrow-parens': 0, 19 | // Allow async-await 20 | 'generator-star-spacing': 0, 21 | // Allow debugger during development 22 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 23 | // Do not allow console.logs etc... 24 | 'no-console': 2 25 | }, 26 | globals: { 27 | 'jest/globals': true, 28 | jasmine: true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: /usr/src/app 5 | docker: 6 | - image: banian/node 7 | steps: 8 | # Checkout repository 9 | - checkout 10 | 11 | # Restore cache 12 | - restore_cache: 13 | key: yarn-{{ checksum "yarn.lock" }} 14 | 15 | # Install dependencies 16 | - run: 17 | name: Install Dependencies 18 | command: NODE_ENV=dev yarn 19 | 20 | # Keep cache 21 | - save_cache: 22 | key: yarn-{{ checksum "yarn.lock" }} 23 | paths: 24 | - "node_modules" 25 | 26 | # Test 27 | - run: 28 | name: Tests 29 | command: yarn test 30 | 31 | # Coverage 32 | - run: 33 | name: Coverage 34 | command: yarn codecov 35 | -------------------------------------------------------------------------------- /lib/module.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | module.exports = async function module (moduleOptions) { 4 | const options = Object.assign( 5 | { 6 | pushPlugin: true, 7 | experimentsDir: '~/experiments', 8 | maxAge: 60 * 60 * 24 * 7, // 1 Week 9 | plugins: [], 10 | excludeBots: true, 11 | botExpression: /(bot|spider|crawler)/i 12 | }, 13 | 14 | this.options.googleOptimize, 15 | moduleOptions 16 | ) 17 | 18 | const pluginOpts = { 19 | src: resolve(__dirname, 'plugin.js'), 20 | fileName: 'google-optimize.js', 21 | options 22 | } 23 | 24 | if (options.pushPlugin) { 25 | const { dst } = this.addTemplate(pluginOpts) 26 | this.options.plugins.push(resolve(this.options.buildDir, dst)) 27 | } else { 28 | this.addPlugin(pluginOpts) 29 | } 30 | 31 | // Extend with plugins 32 | if (options.plugins) { 33 | options.plugins.forEach(p => this.options.plugins.push(p)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Pooya Parsa - Alibaba Travels Co 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-google-optimize", 3 | "version": "0.5.4", 4 | "description": "SSR friendly Google Optimize module for Nuxt.js", 5 | "license": "MIT", 6 | "contributors": [ 7 | { 8 | "name": "Pooya Parsa " 9 | }, 10 | { 11 | "name": "Farzad Soltani " 12 | }, 13 | { 14 | "name": "Josh Deltener " 15 | } 16 | ], 17 | "main": "lib/module.js", 18 | "repository": "https://github.com/alibaba-aero/nuxt-google-optimize", 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "scripts": { 23 | "dev": "nuxt test/fixture", 24 | "lint": "eslint lib test", 25 | "test": "npm run lint && jest", 26 | "release": "standard-version && git push --follow-tags && npm publish" 27 | }, 28 | "eslintIgnore": [ 29 | "lib/templates/*.*" 30 | ], 31 | "files": [ 32 | "lib" 33 | ], 34 | "jest": { 35 | "testEnvironment": "node", 36 | "collectCoverage": true 37 | }, 38 | "dependencies": { 39 | "cookie": "^0.5.0", 40 | "weighted-random": "^0.1.0" 41 | }, 42 | "devDependencies": { 43 | "@nuxtjs/module-test-utils": "latest", 44 | "codecov": "latest", 45 | "eslint": "latest", 46 | "eslint-config-standard": "latest", 47 | "eslint-plugin-import": "latest", 48 | "eslint-plugin-jest": "latest", 49 | "eslint-plugin-node": "latest", 50 | "eslint-plugin-promise": "latest", 51 | "eslint-plugin-standard": "latest", 52 | "eslint-plugin-vue": "latest", 53 | "jest": "latest", 54 | "nuxt-edge": "latest", 55 | "puppeteer": "latest", 56 | "standard-version": "latest" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/module.test.js: -------------------------------------------------------------------------------- 1 | const { setup, loadConfig, url } = require('@nuxtjs/module-test-utils') 2 | const puppeteer = require('puppeteer') 3 | 4 | describe('defaults', () => { 5 | let nuxt, page, browser 6 | 7 | beforeAll(async () => { 8 | browser = await puppeteer.launch() 9 | page = await browser.newPage(); 10 | 11 | ({ nuxt } = await setup(loadConfig(__dirname))) 12 | }, 60000) 13 | 14 | afterAll(async () => { 15 | await nuxt.close() 16 | await browser.close() 17 | }) 18 | 19 | test('variant-0', async () => { 20 | await page.goto(url('/')) 21 | const $exp = await page.evaluate(() => window.$exp) 22 | 23 | expect($exp.$experimentIndex).toBe(0) 24 | expect($exp.$variantIndexes).toEqual([0]) 25 | expect($exp.$activeVariants).toEqual([{ weight: 100 }]) 26 | expect($exp.$classes).toEqual(['exp-test1-0']) 27 | expect($exp.name).toBe('test1') 28 | expect($exp.experimentID).toBe('id1') 29 | expect($exp.variants).toEqual([{ weight: 100 }, { weight: 0 }]) 30 | expect($exp.maxAge).toBe(120) 31 | }) 32 | 33 | test('client = server', async () => { 34 | const response = await page.goto(url('/')) 35 | const clientExp = await page.evaluate(() => window.$exp) 36 | const html = await response.text() 37 | const result = html.match(/([\s\S]+)<\/code>/im) 38 | const serverExp = JSON.parse(result[1]) 39 | const props = [ 40 | '$experimentIndex', 41 | '$variantIndexes', 42 | '$activeVariants', 43 | '$classes', 44 | 'name', 45 | 'experimentID', 46 | 'variants' 47 | ] 48 | 49 | props.forEach(prop => { 50 | expect(serverExp[prop]).toEqual(clientExp[prop]) 51 | }) 52 | }) 53 | 54 | const blockedUserAgents = [ 55 | 'AdsBot-Google (+http://www.google.com/adsbot.html)', 56 | 'Baiduspider-image', 57 | 'ia_archiver (+http://www.alexa.com/site/help/webmasters; crawler@alexa.com)' 58 | ] 59 | 60 | for (const agent of blockedUserAgents) { 61 | test(`agent: ${agent}`, async () => { 62 | await page.setUserAgent(agent) 63 | await page.goto(url('/')) 64 | 65 | const $exp = await page.evaluate(() => window.$exp) 66 | expect($exp.name).toEqual(undefined) 67 | }) 68 | } 69 | 70 | const unBlockedUserAgents = [ 71 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36', 72 | '' 73 | ] 74 | 75 | for (const agent of unBlockedUserAgents) { 76 | test(`agent: ${agent}`, async () => { 77 | await page.setUserAgent(agent) 78 | await page.goto(url('/')) 79 | 80 | const $exp = await page.evaluate(() => window.$exp) 81 | expect($exp.name).toBe('test1') 82 | }) 83 | } 84 | }) 85 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [0.5.4](https://github.com/alibaba-aero/nuxt-google-optimize/compare/v0.5.3...v0.5.4) (2019-05-25) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **plugin:** do not overwrite existing cookie in res object ([7e096af](https://github.com/alibaba-aero/nuxt-google-optimize/commit/7e096af)) 12 | * **plugin:** fix fetch experiment ID from cookie ([fbcd0e5](https://github.com/alibaba-aero/nuxt-google-optimize/commit/fbcd0e5)) 13 | 14 | 15 | 16 | 17 | ## [0.5.3](https://github.com/alibaba-aero/nuxt-google-optimize/compare/v0.5.2...v0.5.3) (2018-07-14) 18 | 19 | 20 | 21 | 22 | ## [0.5.2](https://github.com/alibaba-aero/nuxt-google-optimize/compare/v0.5.1...v0.5.2) (2018-07-14) 23 | 24 | 25 | 26 | 27 | ## [0.5.1](https://github.com/alibaba-aero/nuxt-google-optimize/compare/v0.5.0...v0.5.1) (2018-07-09) 28 | 29 | 30 | 31 | 32 | # 0.5.0 (2018-07-09) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * **plugin:** currently handle empty experiments ([31caafa](https://github.com/alibaba-aero/nuxt-google-optimize/commit/31caafa)) 38 | * **plugin:** handle dynamically disabled experiments ([c6620d0](https://github.com/alibaba-aero/nuxt-google-optimize/commit/c6620d0)) 39 | 40 | 41 | ### Features 42 | 43 | * **example:** add more details and button to test your luck ([73cc700](https://github.com/alibaba-aero/nuxt-google-optimize/commit/73cc700)) 44 | * **experiment:** accept isEligible function ([7193621](https://github.com/alibaba-aero/nuxt-google-optimize/commit/7193621)) 45 | * **module:** accept plugins array ([4cc7278](https://github.com/alibaba-aero/nuxt-google-optimize/commit/4cc7278)) 46 | * **module:** pushPlugin option ([87ae7e1](https://github.com/alibaba-aero/nuxt-google-optimize/commit/87ae7e1)) 47 | 48 | 49 | 50 | 51 | # [0.4.0](https://github.com/alibaba-aero/nuxt-google-optimize/compare/v0.3.0...v0.4.0) (2018-07-08) 52 | 53 | 54 | ### Features 55 | 56 | * **module:** accept plugins array ([4cc7278](https://github.com/alibaba-aero/nuxt-google-optimize/commit/4cc7278)) 57 | 58 | 59 | 60 | 61 | # [0.3.0](https://github.com/alibaba-aero/nuxt-google-optimize/compare/v0.2.2...v0.3.0) (2018-07-08) 62 | 63 | 64 | ### Features 65 | 66 | * **example:** add more details and button to test your luck ([73cc700](https://github.com/alibaba-aero/nuxt-google-optimize/commit/73cc700)) 67 | 68 | 69 | 70 | 71 | ## [0.2.2](https://github.com/alibaba-aero/nuxt-google-optimize/compare/v0.2.1...v0.2.2) (2018-07-07) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * **plugin:** handle dynamically disabled experiments ([c6620d0](https://github.com/alibaba-aero/nuxt-google-optimize/commit/c6620d0)) 77 | 78 | 79 | 80 | 81 | ## [0.2.1](https://github.com/alibaba-aero/nuxt-google-optimize/compare/v0.2.0...v0.2.1) (2018-07-07) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * **plugin:** currently handle empty experiments ([31caafa](https://github.com/alibaba-aero/nuxt-google-optimize/commit/31caafa)) 87 | 88 | 89 | 90 | 91 | # [0.2.0](https://github.com/alibaba-aero/nuxt-google-optimize/compare/v0.1.0...v0.2.0) (2018-07-07) 92 | 93 | 94 | ### Features 95 | 96 | * **module:** pushPlugin option ([87ae7e1](https://github.com/alibaba-aero/nuxt-google-optimize/commit/87ae7e1)) 97 | 98 | 99 | 100 | 101 | # [0.1.0](https://github.com/alibaba-aero/nuxt-google-optimize/compare/v0.0.1...v0.1.0) (2018-07-07) 102 | 103 | 104 | ### Features 105 | 106 | * **experiment:** accept isEligible function ([7193621](https://github.com/alibaba-aero/nuxt-google-optimize/commit/7193621)) 107 | 108 | 109 | 110 | 111 | ## 0.0.1 (2018-07-07) 112 | -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | import weightedRandom from 'weighted-random' 2 | import { parse as parseCookie, serialize as serializeCookie } from 'cookie' 3 | import experiments from '<%= options.experimentsDir %>' 4 | 5 | export default function (ctx, inject) { 6 | // Assign experiment and variant to user 7 | assignExperiment(ctx) 8 | 9 | // Google optimize integration 10 | googleOptimize(ctx) 11 | 12 | // Inject $exp 13 | inject('exp', ctx.experiment) 14 | } 15 | 16 | // Choose experiment and variant 17 | function assignExperiment(ctx) { 18 | let experimentIndex = -1 19 | let experiment = {} 20 | let variantIndexes = [] 21 | let classes = [] 22 | 23 | // Try to restore from cookie 24 | const cookie = getCookie(ctx, 'exp') || '' // experimentID.var1-var2 25 | const [cookieExp, cookieVars] = cookie.split('.') 26 | 27 | if (cookieExp && cookieVars) { 28 | // Try to find experiment with that id 29 | experimentIndex = experiments.findIndex(exp => exp.experimentID === cookieExp) 30 | experiment = experiments[experimentIndex] 31 | 32 | // Variant indexes 33 | variantIndexes = cookieVars.split('-').map(v => parseInt(v)) 34 | } 35 | 36 | // Choose one experiment 37 | const experimentWeights = experiments.map(exp => exp.weight === undefined ? 1 : exp.weight) 38 | let retries = experiments.length 39 | while (experimentIndex === -1 && retries-- > 0) { 40 | experimentIndex = weightedRandom(experimentWeights) 41 | experiment = experiments[experimentIndex] 42 | 43 | // Check if current user is eligible for experiment 44 | if (typeof experiment.isEligible === 'function') { 45 | if (!experiment.isEligible(ctx)) { 46 | // Try another one 47 | experimentWeights[experimentIndex] = 0 48 | experimentIndex = -1 49 | } 50 | } 51 | } 52 | 53 | if (experimentIndex !== -1 && !skipAssignment(ctx)) { 54 | // Validate variantIndexes against experiment (coming from cookie) 55 | variantIndexes = variantIndexes.filter(index => experiment.variants[index]) 56 | 57 | // Choose enough variants 58 | const variantWeights = experiment.variants.map(variant => variant.weight === undefined ? 1 : variant.weight) 59 | while (variantIndexes.length < (experiment.sections || 1)) { 60 | const index = weightedRandom(variantWeights) 61 | variantWeights[index] = 0 62 | variantIndexes.push(index) 63 | } 64 | 65 | // Write exp cookie if changed 66 | const expCookie = experiment.experimentID + '.' + variantIndexes.join('-') 67 | if (cookie !== expCookie) { 68 | setCookie(ctx, 'exp', expCookie, experiment.maxAge) 69 | } 70 | 71 | // Compute global classes to be injected 72 | classes = variantIndexes.map(index => 'exp-' + experiment.name + '-' + index) 73 | } else { 74 | // No active experiment 75 | experiment = {} 76 | variantIndexes = [] 77 | classes = [] 78 | } 79 | 80 | ctx.experiment = { 81 | $experimentIndex: experimentIndex, 82 | $variantIndexes: variantIndexes, 83 | $activeVariants: variantIndexes.map(index => experiment.variants[index]), 84 | $classes: classes, 85 | ...experiment 86 | } 87 | } 88 | 89 | function getCookie(ctx, name) { 90 | if (process.server && !ctx.req) { 91 | return 92 | } 93 | 94 | // Get and parse cookies 95 | const cookieStr = process.client ? document.cookie : ctx.req.headers.cookie 96 | const cookies = parseCookie(cookieStr || '') || {} 97 | 98 | return cookies[name] 99 | } 100 | 101 | function setCookie(ctx, name, value, maxAge = <%= options.maxAge %>) { 102 | const serializedCookie = serializeCookie(name, value, { 103 | path: '/', 104 | maxAge 105 | }) 106 | 107 | if (process.client) { 108 | // Set in browser 109 | document.cookie = serializedCookie 110 | } else if (process.server && ctx.res) { 111 | // Send Set-Cookie header from server side 112 | const prev = ctx.res.getHeader('Set-Cookie') 113 | let value = serializedCookie 114 | if (prev) { 115 | value = Array.isArray(prev) ? prev.concat(serializedCookie) 116 | : [prev, serializedCookie] 117 | } 118 | ctx.res.setHeader('Set-Cookie', value) 119 | } 120 | } 121 | 122 | // https://developers.google.com/optimize/devguides/experiments 123 | function googleOptimize({ experiment }) { 124 | if (process.server || !window.ga || !experiment || !experiment.experimentID) { 125 | return 126 | } 127 | 128 | const exp = experiment.experimentID + '.' + experiment.$variantIndexes.join('-') 129 | 130 | window.ga('set', 'exp', exp) 131 | } 132 | 133 | // should we skip bots? 134 | function skipAssignment(ctx) { 135 | if (!<%= options.excludeBots %>) { return } 136 | 137 | if (process.server) { 138 | return ctx.req && 139 | ctx.req.headers && 140 | ctx.req.headers['user-agent'] && 141 | ctx.req.headers['user-agent'].match(<%= options.botExpression %>) 142 | } 143 | 144 | return navigator.userAgent.match(<%= options.botExpression %>) 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Google Is Shutting Down Optimize 2 | Google is shutting down Optimize Sept 30, 2023. [Read the annoucement](https://support.google.com/optimize/answer/12979939?hl=en) 3 | 4 | # nuxt-google-optimize 5 | [![npm (scoped with tag)](https://img.shields.io/npm/v/nuxt-google-optimize/latest.svg?style=flat-square)](https://npmjs.com/package/nuxt-google-optimize) 6 | [![npm](https://img.shields.io/npm/dt/nuxt-google-optimize.svg?style=flat-square)](https://npmjs.com/package/nuxt-google-optimize) 7 | [![CircleCI](https://img.shields.io/circleci/project/github/alibaba-aero/nuxt-google-optimize.svg?style=flat-square)](https://circleci.com/gh/alibaba-aero/nuxt-google-optimize) 8 | [![Codecov](https://img.shields.io/codecov/c/github/alibaba-aero/nuxt-google-optimize.svg?style=flat-square)](https://codecov.io/gh/alibaba-aero/nuxt-google-optimize) 9 | [![Dependencies](https://david-dm.org/alibaba-aero/nuxt-google-optimize/status.svg?style=flat-square)](https://david-dm.org/alibaba-aero/nuxt-google-optimize) 10 | [![js-standard-style](https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square)](http://standardjs.com) 11 | 12 | > SSR friendly Google Optimize module for Nuxt.js 13 | 14 | [📖 **Release Notes**](./CHANGELOG.md) 15 | 16 | ## Features 17 | 18 | - Support multiple experiments (AB or MVT[Multi-Variant]) 19 | - Auto assign experiment/variant to users 20 | - SSR support using cookies 21 | - CSS and state injection 22 | - Automatically revoke expired experiments from testers 23 | - Ability to assign experiments based on context conditions (Route, State, etc) 24 | 25 | ## Setup 26 | 27 | - Add `nuxt-google-optimize` dependency using yarn or npm to your project 28 | ```sh 29 | yarn add nuxt-google-optimize 30 | ``` 31 | OR 32 | ```sh 33 | npm install nuxt-google-optimize --save 34 | ``` 35 | 36 | - Add `nuxt-google-optimize` to `modules` section of `nuxt.config.js` 37 | 38 | ```js 39 | { 40 | modules: [ 41 | 'nuxt-google-optimize', 42 | ], 43 | 44 | // Optional options 45 | googleOptimize: { 46 | // experimentsDir: '~/experiments', 47 | // maxAge: 60 * 60 * 24 * 7 // 1 Week 48 | // pushPlugin: true, 49 | // excludeBots: true, 50 | // botExpression: /(bot|spider|crawler)/i 51 | } 52 | } 53 | ``` 54 | 55 | ## Usage 56 | 57 | Create `experiments` directory inside your project. 58 | 59 | Create `experiments/index.js` to define all available experiments: 60 | 61 | ```js 62 | import backgroundColor from './background-color' 63 | 64 | export default [ 65 | backgroundColor 66 | ] 67 | ``` 68 | 69 | ### Creating an experiment 70 | 71 | Each experiment should export an object to define itself. 72 | 73 | `experiments/background-color/index.js`: 74 | 75 | ```js 76 | export default { 77 | // A helper exp-{name}-{var} class will be added to the root element 78 | name: 'background-color', 79 | 80 | // Google optimize experiment id 81 | experimentID: '....', 82 | 83 | // [optional] specify number of sections for MVT experiments 84 | // sections: 1, 85 | 86 | // [optional] maxAge for a user to test this experiment 87 | // maxAge: 60 * 60 * 24, // 24 hours, 88 | 89 | // [optional] Enable/Set experiment on certain conditions 90 | // isEligible: ({ route }) => route.path !== '/foo' 91 | 92 | // Implemented variants and their weights 93 | variants: [ 94 | { weight: 0 }, // <-- This is the default variant 95 | { weight: 2 }, 96 | { weight: 1 } 97 | ], 98 | } 99 | ``` 100 | 101 | ### `$exp` 102 | 103 | Global object `$exp` will be universally injected in the app context to determine the currently active experiment. 104 | 105 | It has the following keys: 106 | 107 | ```json6 108 | { 109 | // Index of currently active experiment 110 | "$experimentIndex": 0, 111 | 112 | // Index of currently active experiment variants 113 | "$variantIndexes": [ 114 | 1 115 | ], 116 | 117 | // Same as $variantIndexes but each item is the real variant object 118 | "$activeVariants": [ 119 | { 120 | /* */ 121 | } 122 | ], 123 | 124 | // Classes to be globally injected (see global style tests section) 125 | "$classes": [ 126 | "exp-background-color-1" // exp-{experiment-name}-{variant-id} 127 | ], 128 | 129 | // All of the keys of currently active experiment are available 130 | "name": "background-color", 131 | "experimentID": "testid", 132 | "sections": 1, 133 | "maxAge": 60, 134 | "variants": [ 135 | /* all variants */ 136 | ] 137 | } 138 | ``` 139 | 140 | **Using inside components:** 141 | 142 | ```html 143 | 152 | ``` 153 | 154 | **Using inside templates:** 155 | 156 | ```html 157 |
158 |