├── .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 |
2 |
3 |
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 |
2 |
3 |
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 | [](https://npmjs.com/package/nuxt-google-optimize)
6 | [](https://npmjs.com/package/nuxt-google-optimize)
7 | [](https://circleci.com/gh/alibaba-aero/nuxt-google-optimize)
8 | [](https://codecov.io/gh/alibaba-aero/nuxt-google-optimize)
9 | [](https://david-dm.org/alibaba-aero/nuxt-google-optimize)
10 | [](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 |