Skip to content

Instantly share code, notes, and snippets.

@CGeohagan
Created October 11, 2023 16:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save CGeohagan/1d81e230143c5885e52711d6f1f09876 to your computer and use it in GitHub Desktop.
Save CGeohagan/1d81e230143c5885e52711d6f1f09876 to your computer and use it in GitHub Desktop.
GA4 Implementation
import VideoEvents from '@src/metrics/video_events';
import logger from '@src/shared/logger';
/**
* Custom Dimensions Map that is required for UA events with gtag
* This mapping is not required for GA4
*/
const concertCustomDimensionMap = {
'dimension1': 'video_id',
'dimension2': 'video_length',
'dimension3': 'ad_format',
'dimension4': 'test_variant',
'dimension11': 'site',
'dimension18': 'advertiser',
'dimension19': 'campaign',
'dimension21': 'line_item_id',
'dimension22': 'creative_id',
'dimension23': 'concert_request_id',
'dimension24': 'concert_creative_control'
};
/**
* Setting up data layer
*/
window.dataLayer = window.dataLayer || [];
class GoogleAnalytics {
constructor(adConfig, gaId, video = null) {
this.adData = adConfig;
this.GAConfig = {};
this.video = video;
this.videoEvents = new VideoEvents(video);
this.log = logger.init('metrics/providers/google_analytics.js');
this.setConfiguration(gaId);
this.customDimensionParams = {};
this.updateCustomDimensionParams();
this.loadGA();
if(video) {
this.trackVideo();
}
}
/**
* Log event to google analytics
* @param {string} category
* @param {string} action
* @param {Object} options
*/
track(category, action, options = {}) {
if (!this.video || this.videoEvents.videoReadyToBeTracked()) {
this.trackUA(category, action, options);
this.trackGA4(category, action, options);
} else {
this.log(`Queueing video event for ga: ${category}, ${action}, ${JSON.stringify(options)}`);
this.videoEvents.queue(category, action, options);
}
}
/**
* For UA we are using the ad action as the event name per this example (https://support.google.com/analytics/answer/11091026?hl=en#gtag-dual-tagging&zippy=%2Cin-this-article)
* and sending the
* event_action, event_category, event_label, and already mapped custom dimensions as parameters
* @param {string} category
* @param {string} action
* @param {object} options
*/
trackUA(category, action, options = {}) {
gtag('event', `ad: + ${action}`, {
send_to: this.GAConfig.googleAnalyticsID,
event_action: 'ad:' + action,
event_category: category,
event_label: this.generateLabel(),
value: (typeof options.value === 'number') ? parseInt(options.value) : undefined,
non_interaction: !!options.nonInteraction,
...this.customDimensionParams,
...options
});
this.log(`Logging to GA(UA): category:${category}, action:${action}, options:${JSON.stringify(options)}`);
}
/**
* For GA4 we are using the category as the event name and sending the
* action, ad_name, and custom dimensions as parameters.
* GA4 does not require custom dimensions to be mapped. They will be sent
* as parameter names.
* @param {string} category
* @param {string} action
* @param {object} options
*/
trackGA4(category, action, options = {}) {
gtag('event', category, {
send_to: this.GAConfig.ga4ID,
action: 'ad:' + action,
ad_name: this.generateLabel(),
event_value: (typeof options.value === 'number') ? parseInt(options.value) : undefined, // do we want this
non_interaction: !!options.nonInteraction,
...this.customDimensionParams,
...options
});
this.log(`Logging to GA4: category:${category}, action:${action}, options:${JSON.stringify(options)}`);
}
/**
* Set up ga config values from ad data
* @private
* @param {string} gaId
*/
setConfiguration(gaId) {
this.GAConfig = {
adId: this.adData.id,
adName: this.adData.adName,
variantId: this.adData.variantID,
adSlug: this.adData.slug,
clientName: this.adData.brand,
campaignName: this.adData.campaignName,
adFormat: this.adData.designTemplate,
googleAnalyticsID: gaId || 'UA-96398693-1',
ga4ID: 'G-5MBLG9L0C6',
lineItemId: this.adData.dfpConfig.lineItemId,
creativeId: this.adData.dfpConfig.creativeId,
siteName: this.adData.dfpConfig.network,
concertId: this.adData.dfpConfig.concertId,
concertControl: this.adData.dfpConfig.concertControl
};
if (this.video) {
this.GAConfig.videoID = this.video.getVideoId();
}
}
/**
* Combines ad name and id to generate label for event name
* @private
*/
generateLabel() {
const adName = this.GAConfig.adName;
const adId = this.GAConfig.adId;
return [adName, adId].filter((l) => l.length > 0).join(' | ');
}
/**
* Record standard video events
* @private
*/
trackVideo() {
let currentSecondsElapsedIndex = 0;
const secondsElapsed = [3, 6, 10, 15, 20, 25, 30];
const category = 'video';
if (this.video && typeof(this.video.onPlay) === 'function' && !this.videoEvents.videoAlreadyTracked(this.video)) {
this.video.onInitialPlay( () => this.track(category, 'start') );
this.video.onPlay( () => this.track(category, 'play') );
this.video.onQuartile( (quartile) => this.track(category, `quartile-${quartile}` ) );
this.video.onPause( () => this.track(category, 'pause') );
this.video.onMute( () => this.track(category, 'mute') );
this.video.onUnmute( () => this.track(category, 'unmute') );
this.video.onMaxAutoPlay( () => this.track(category, 'max-auto-play') );
this.video.onSetSource((videoID) => {
this.updateCustomDimensionParams({ video_id: videoID });
// Reset the time on new video
currentSecondsElapsedIndex = 0;
});
this.video.onTimeUpdate( (time) => {
if (time >= secondsElapsed[currentSecondsElapsedIndex]) {
const ev = secondsElapsed[currentSecondsElapsedIndex];
this.track(category, `time-${ev}sec`);
currentSecondsElapsedIndex++;
}
});
this.video.onDurationChange(() => {
const duration = this.video.getDuration();
if (!isNaN(duration)) {
this.updateCustomDimensionParams({ video_length: Math.floor(duration).toString() });
}
this.processQueuedEvents();
});
this.video.onComplete( () => this.track(category, 'end') );
}
}
/**
* Process queued video events by sending them to Google Analytics
* @private
*/
processQueuedEvents() {
this.videoEvents.trackingEnabled = true;
this.videoEvents.events().forEach(event => {
this.track(...event);
});
this.videoEvents.clearQueue();
}
/**
* Loads GA script on page
* @private
*/
loadGA() {
if (!window.hymnalGALoaded) {
this.log('Loading GA script');
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${this.GAConfig.googleAnalyticsID}`;
document.head.appendChild(script);
gtag('js', new Date());
this.loadUA();
this.loadGA4();
window.hymnalGALoaded = true;
}
}
loadUA() {
gtag('config', this.GAConfig.googleAnalyticsID, {
send_page_view: false,
custom_map: concertCustomDimensionMap,
page_location: this.getDocumentLocation() // check that this works
});
}
loadGA4() {
gtag('config', this.GAConfig.ga4ID, {
send_page_view: false,
page_location: this.getDocumentLocation() // check that this works
});
}
/**
* Fetch the document location, based on availability.
* @private
*/
getDocumentLocation() {
const SAFEFRAME_URL_REGEX = /^https:\/\/tpc\.googlesyndication\.com\/safeframe\//;
try {
if (SAFEFRAME_URL_REGEX.test(window.location.href)) {
return document.referrer;
} else {
return window.location.href;
}
} catch (e) {
return document.referrer;
}
}
/**
* Creates the custom dimension params that get sent with each event
* @param {object} updatedParams
*/
updateCustomDimensionParams(updatedParams) {
const data = {
ad_format: this.GAConfig.adFormat,
advertiser: this.GAConfig.clientName,
campaign: this.GAConfig.campaignName,
site: this.GAConfig.siteName,
line_item_id: this.GAConfig.lineItemId,
creative_id: this.GAConfig.creativeId,
concert_request_id: this.GAConfig.concertId,
concert_creative_control: this.GAConfig.concertControl,
test_variant: this.GAConfig.variantId
};
if (this.video) {
data.video_id = this.GAConfig.videoID;
}
for (const key in updatedParams) {
data[key] = updatedParams[key];
}
this.customDimensionParams = data;
}
}
export default GoogleAnalytics;
function gtag() {
window.dataLayer.push(arguments);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment