Skip to content

Commit

Permalink
feat: Add support for Google Analytics Custom dimensions
Browse files Browse the repository at this point in the history
Signed-off-by: DavidWells <[email protected]>
  • Loading branch information
DavidWells committed Apr 10, 2020
1 parent ab351a2 commit 50a0bbf
Showing 1 changed file with 194 additions and 26 deletions.
220 changes: 194 additions & 26 deletions packages/analytics-plugin-google-analytics/src/browser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
/* global ga */

const config = {
/* Website's Google Analytics Tracking ID */
trackingId: null,
/* Turn on google analytics debugging */
debug: false,
/* Anonymizing IP addresses sent to Google Analytics https://bit.ly/3c660Rd */
anonymizeIp: false,
/* Custom dimensions allow you to send extra information to Google Analytics. https://bit.ly/3c5de88 */
customDimensions: {},
/* Reset custom dimensions by key on analytics.page calls. Useful for single page apps */
resetCustomDimensionsOnPage: [],
/* Mapped dimensions will be set to the page & sent as properties of all subsequent events on that page.
If false, analytics will only pass custom dimensions as part of individual events
*/
setCustomDimensionsToPage: true,
/* Custom metrics https://bit.ly/3c5de88 */
// TODO customMetrics: { key: 'metric1' }
/* Content groupings https://bit.ly/39Zt3Me */
// TODO contentGroupings: { key: 'contentGroup1' }
}

/**
* Google analytics plugin
* @link https://getanalytics.io/plugins/google-analytics/
Expand All @@ -15,15 +36,76 @@
* })
*/
function googleAnalytics(pluginConfig = {}) {
let pageCalledOnce = false
// Allow for userland overides of base methods
return {
NAMESPACE: 'google-analytics',
config: pluginConfig,
config: {
...config,
...pluginConfig
},
initialize: initialize,
// Google Analytics page view
page: ({ payload, config }) => {
page: ({ payload, config, instance }) => {
const { properties } = payload
pageView(properties.path)
const { resetCustomDimensionsOnPage, customDimensions } = config
const campaign = instance.getState('context.campaign')
if (gaNotLoaded()) return

/* If dimensions are specifiied to reset, clear them before page view */
if (resetCustomDimensionsOnPage && resetCustomDimensionsOnPage.length) {
const resetDimensions = resetCustomDimensionsOnPage.reduce((acc, key) => {
if (customDimensions[key]) {
acc[customDimensions[key]] = null // { dimension1: null } etc
}
return acc
}, {})
if (Object.keys(resetDimensions).length) {
// Reset custom dimenions
ga('set', resetDimensions)
}
}

const path = properties.path || document.location.pathname

const pageView = {
page: path,
title: properties.title,
location: properties.url
}

let pageData = {
page: path,
title: properties.title
}
// allow referrer override if referrer was manually set
if (properties.referrer !== document.referrer) {
pageData.referrer = properties.referrer
}

const campaignData = addCampaignData(campaign)

const dimensions = setCustomDimenions(properties, config)

/* Dimensions will only be included in the event if config.setCustomDimensionsToPage is false */
const finalPayload = {
...pageView,
...campaignData,
...dimensions
}

ga('set', pageData)

// Remove location for SPA tracking after initial page view
if (pageCalledOnce) {
delete finalPayload.location
}

/* send page view to GA */
ga('send', 'pageview', finalPayload)

// Set after initial page view
pageCalledOnce = true
},
/**
* Google Analytics track event
Expand All @@ -39,6 +121,7 @@ function googleAnalytics(pluginConfig = {}) {
const { properties, event } = payload
const { label, value, category, nonInteraction } = properties
const campaign = instance.getState('context.campaign')
// TODO inline this trackEvent
trackEvent({
hitType: 'event',
event: event,
Expand All @@ -47,19 +130,10 @@ function googleAnalytics(pluginConfig = {}) {
value: value,
nonInteraction,
campaign: campaign
})
}, config, payload)
},
identify: ({ payload, config }) => {
identifyVisitor(payload.userId)
/* Todo implement custom dimensions
http://bit.ly/2yULdOO & http://bit.ly/2NS5nOE
// user mapping required
ga('set', {
'dimensionX': valueX,
'dimensionY': valueY,
'dimensionZ': valueZ
})
*/
identifyVisitor(payload.userId, payload.traits, config)
},
loaded: () => {
return !!window.gaplugins
Expand All @@ -73,7 +147,8 @@ function gaNotLoaded() {
return typeof ga === 'undefined'
}

export function initialize({ config }) {
export function initialize(pluginApi) {
const { config, instance } = pluginApi
if (!config.trackingId) throw new Error('No google analytics trackingId defined')
if (gaNotLoaded()) {
/* eslint-disable */
Expand All @@ -91,6 +166,18 @@ export function initialize({ config }) {
ga('set', 'sendHitTask', null)
window.ga_debug = { trace: true }
}

if (config.anonymizeIp) {
ga('set', 'anonymizeIp', true)
}

/* set custom dimenions from user traits */
const user = instance.user() || {}
const traits = user.traits || {}
if (Object.keys(traits).length) {
const customDimensions = formatObjectIntoDimensions(traits, config)
ga('set', customDimensions)
}
}
}

Expand All @@ -115,7 +202,7 @@ export function pageView(urlPath) {
* @param {string} [eventData.nonInteraction = false] - nonInteraction https://bit.ly/2CUzeoz
* @return {object} sent data
*/
export function trackEvent(eventData) {
export function trackEvent(eventData, opts = {}, payload) {
if (gaNotLoaded()) return

const data = {
Expand All @@ -137,26 +224,107 @@ export function trackEvent(eventData) {
}

/* Attach campaign data */
const campaignData = eventData.campaign || {}
const { name, source, medium, content, keyword } = campaignData
if (name) data.campaignName = name
if (source) data.campaignSource = source
if (medium) data.campaignMedium = medium
if (content) data.campaignContent = content
if (keyword) data.campaignKeyword = keyword
const campaignData = addCampaignData(eventData)
/* Set Dimensions or return them for payload is config.setCustomDimensionsToPage is false */
const dimensions = setCustomDimenions(payload.properties, opts)

const finalPayload = {
...data,
/* Attach campaign data, if exists */
...campaignData,
/* Dimensions will only be included in the event if config.setCustomDimensionsToPage is false */
...dimensions
}

/* Send data to Google Analytics */
ga('send', 'event', data)
return data
ga('send', 'event', finalPayload)
return finalPayload
}

/**
* Add campaign data to GA payload https://bit.ly/34qFCPn
* @param {Object} [campaignData={}] [description]
* @param {String} [campaignData.campaignName] - Name of campaign
* @param {String} [campaignData.campaignSource] - Source of campaign
* @param {String} [campaignData.campaignMedium] - Medium of campaign
* @param {String} [campaignData.campaignContent] - Content of campaign
* @param {String} [campaignData.campaignKeyword] - Keyword of campaign
*/
function addCampaignData(campaignData = {}) {
let campaign = {}
const { name, source, medium, content, keyword } = campaignData
if (name) campaign.campaignName = name
if (source) campaign.campaignSource = source
if (medium) campaign.campaignMedium = medium
if (content) campaign.campaignContent = content
if (keyword) campaign.campaignKeyword = keyword
return campaign
}

/* Todo add includeSearch options ¯\_(ツ)_/¯
function getPagePath(props, opts = {}) {
if (!props) return
if (opts.includeSearch && props.search) {
return `${props.path}${props.search}`
}
return props.path
}
*/

// properties, data=opts
function formatObjectIntoDimensions(properties, opts = {}) {
const { customDimensions } = opts
// TODO map opts.customMetrics; Object.keys(customMetrics) { key: 'metric1' }
// TODO map opts.contentGroupings; Object.keys(contentGroupings) { key: 'contentGroup1' }

/* Map values from payload to any defined custom dimensions */
return Object.keys(customDimensions).reduce((acc, key) => {
const dimensionKey = customDimensions[key]
var value = get(properties, key) || properties[key]
if (typeof value === 'boolean') {
value = value.toString()
}
if (value || value === 0) {
acc[dimensionKey] = value
return acc
}
return acc
}, {})
}

function get(obj, key, def, p, undef) {
key = key.split ? key.split('.') : key
for (p = 0; p < key.length; p++) {
obj = obj ? obj[key[p]] : undef
}
return obj === undef ? def : obj
}

function setCustomDimenions(props = {}, opts) {
const customDimensions = formatObjectIntoDimensions(props, opts)
if (!Object.keys(customDimensions).length) {
return {}
}
// If setCustomDimensionsToPage false, don't save custom dimenions from event to page
if (!opts.setCustomDimensionsToPage) {
return customDimensions
}
// Set custom dimensions
ga('set', customDimensions)
return {}
}

/**
* Identify a visitor by Id
* @param {string} id - unique visitor ID
*/
export function identifyVisitor(id) {
export function identifyVisitor(id, traits = {}, conf = {}) {
if (gaNotLoaded()) return
if (id) ga('set', 'userId', id)
if (Object.keys(traits).length) {
const custom = formatObjectIntoDimensions(traits, conf)
ga('set', custom)
}
}

function format(value) {
Expand Down

0 comments on commit 50a0bbf

Please sign in to comment.