Skip to content

Commit

Permalink
DCN-151 - Add AB Testing with Google Analytics experiments
Browse files Browse the repository at this point in the history
  • Loading branch information
hejtmii committed May 16, 2019
1 parent 4ed6f7d commit 67d48c6
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 6 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -16,6 +16,7 @@
"google-maps-react": "^2.0.2",
"immutable": "^3.8.2",
"kentico-cloud-delivery": "^5.7.2",
"load-script": "^1.0.0",
"lodash": "^4.17.11",
"qs": "^6.5.2",
"react": "^16.4.2",
Expand Down
2 changes: 1 addition & 1 deletion src/Client.js
Expand Up @@ -22,7 +22,7 @@ import { HostedVideo } from './Models/hosted_video';
import { Office } from './Models/office';
import { Tweet } from './Models/tweet';

const projectId = '';
const projectId = '471f9f4c-4f97-009b-a0b8-79db2558e63f';
const previewApiKey = '';

// configure type resolvers
Expand Down
3 changes: 3 additions & 0 deletions src/Models/home.js
Expand Up @@ -44,6 +44,9 @@ export class Home extends ContentItem {
if (fieldName === 'url_pattern') {
return 'urlPattern';
}
if (fieldName === 'a_b_testing') {
return 'abTesting';
}
return fieldName;
},
linkResolver: link => resolveContentLink(link)
Expand Down
82 changes: 77 additions & 5 deletions src/Stores/Home.js
Expand Up @@ -6,6 +6,7 @@ import {
defaultLanguage
} from '../Utilities/LanguageCodes';
import { spinnerService } from '@chevtek/react-spinners';
import { getVariation } from './experiments';

let unsubscribe = new Subject();
let changeListeners = [];
Expand All @@ -20,23 +21,94 @@ let notifyChange = () => {
});
};

let homeLoaded = (homeItem, language) => {
if (language) {
home[language] = homeItem;
} else {
home[defaultLanguage] = homeItem;
}
notifyChange();
};

let fetchHomeVariant = (originalHome, language, experimentId, variantId) => {
let query = Client.items()
.type('home')
.limitParameter(1);

if (language) {
query.languageParameter(language);
}

// Filter by specific experiment and variant
const experimentPrefix = experimentId;
const variantPrefix = variantId ? `/${variantId}` : '';
const startsWith = encodeURIComponent(
`["${experimentPrefix}${variantPrefix}"`
);

query.rangeFilter('elements.a_b_testing', startsWith, startsWith + '~');

// Make sure the home pages in the result are ordered in the order of variants (original first)
query.orderByAscending('elements.a_b_testing');

query
.getObservable()
.pipe(takeUntil(unsubscribe))
.subscribe(response => {
const homeItem = response.items[0] || originalHome;
if (homeItem) {
homeLoaded(homeItem, language);
}
});
};

let ensureHomeVariant = async (homeItem, language) => {
const abTesting = JSON.parse(homeItem.abTesting.value);
const structuredValue = abTesting[1];
const experiment = structuredValue && structuredValue.experiment;
if (experiment && experiment.id && experiment.key) {
const chosenVariation = await getVariation(experiment.id, experiment.key);
const expectedVariantId = `${chosenVariation}`;

const variant = structuredValue.variant;
const variantId = variant && variant.id;
if (variantId === undefined || expectedVariantId === variantId) {
// Correct variant already loaded
homeLoaded(homeItem, language);
} else {
// Fetch another variant
fetchHomeVariant(homeItem, language, experiment.id, expectedVariantId);
}
} else {
// Not in an experiment
homeLoaded(homeItem, language);
}
};

let fetchHome = language => {
let query = Client.items().type('home');
let query = Client.items()
.type('home')
.limitParameter(1);

if (language) {
query.languageParameter(language);
}

// Make sure the home pages in the result are ordered in the order of variants (original first)
query.orderByAscending('elements.a_b_testing');

query
.getObservable()
.pipe(takeUntil(unsubscribe))
.subscribe(response => {
if (language) {
home[language] = response.items[0];
const homeItem = response.items[0];

// If experiment is selected, make sure to load the right variant
if (homeItem.abTesting) {
ensureHomeVariant(homeItem, language);
} else {
home[defaultLanguage] = response.items[0];
homeLoaded(homeItem, language);
}
notifyChange();
});
};

Expand Down
92 changes: 92 additions & 0 deletions src/Stores/experiments.js
@@ -0,0 +1,92 @@
import loadScript from 'load-script';

// We need to do the same what a snippet for Google Experiment does because we are in React which needs to load things dynamically
// GOOGLE ANALYTICS EXPERIMENT SNIPPET -- START
const loadedExperiments = {};

function loadExperimentCode(experimentKey) {
const existingPromise = loadedExperiments[experimentKey];
if (existingPromise) {
return existingPromise;
}

const newPromise = new Promise((resolve, reject) => {
const location = document.location;
if (location.search.indexOf('utm_expid=' + experimentKey) > 0) {
reject();
}

const cookies = document.cookie;

function getCookie(cookieName) {
if (cookies) {
var i = cookies.indexOf(cookieName + '=');
if (i > -1) {
var j = cookies.indexOf(';', i);
return escape(
cookies.substring(
i + cookieName.length + 1,
j < 0 ? cookies.length : j
)
);
}
}
}

const x = getCookie('__utmx');
const xx = getCookie('__utmxx');
const hash = location.hash;

const url = `http${
location.protocol === 'https:' ? 's://ssl' : '://www'
}.google-analytics.com/ga_exp.js?utmxkey=${experimentKey}&utmx=${
x ? x : ''
}&utmxx=${xx ? xx : ''}&utmxtime=${new Date().valueOf()}${
hash ? '&utmxhash=' + escape(hash.substr(1)) : ''
}`;
loadScript(url, function(err) {
if (err) {
reject();
} else {
resolve();
}
});
});

loadedExperiments[experimentKey] = newPromise;
return newPromise;
}
// GOOGLE ANALYTICS EXPERIMENT SNIPPET -- END

const chosenVariations = {};

function loadVariation(experimentId) {
const existingPromise = chosenVariations[experimentId];
if (existingPromise) {
return existingPromise;
}

const newPromise = new Promise(resolve => {
const scriptUrl = `https://www.google-analytics.com/cx/api.js?experiment=${experimentId}`;
loadScript(scriptUrl, function(err) {
if (err) {
resolve(null);
} else {
var chosenVariation = window.cxApi.chooseVariation();
resolve(chosenVariation);
}
});
});

chosenVariations[experimentId] = newPromise;
return newPromise;
}

async function getVariation(experimentId, experimentKey) {
await loadExperimentCode(experimentKey);
const variationId = await loadVariation(experimentId);

return variationId;
}

export { getVariation };
17 changes: 17 additions & 0 deletions src/index.css
Expand Up @@ -6173,3 +6173,20 @@ body {
.application-content > div:first-child {
display: block !important;
}

/* CTA button for links in banner text */
.banner-text a {
padding: 0.3rem 2rem;
cursor: pointer;
color: #fff;
background: #08768c;
border-radius: 0.2em;
display: inline-block;
text-decoration: none;
text-transform: uppercase;
}

.banner-text a:hover {
color: #fff;
background: #2896ac;
}

0 comments on commit 67d48c6

Please sign in to comment.