Skip to content
This repository was archived by the owner on Dec 16, 2025. It is now read-only.

Commit 57b79d1

Browse files
authored
Merge pull request #842 from googlecodelabs/Add-GA4
Add GA4 analytics support
2 parents 872cbcf + 0df4e0f commit 57b79d1

2 files changed

Lines changed: 165 additions & 4 deletions

File tree

codelab-elements/google-codelab-analytics/google_codelab_analytics.js

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
goog.module('googlecodelabs.CodelabAnalytics');
1919

20+
const Const = goog.require('goog.string.Const');
2021
const EventHandler = goog.require('goog.events.EventHandler');
22+
const TrustedResourceUrl = goog.require('goog.html.TrustedResourceUrl');
23+
const {safeScriptEl} = goog.require('safevalues.dom');
2124

2225
/**
2326
* The general codelab action event fired for trackable interactions.
@@ -38,6 +41,24 @@ const PAGEVIEW_EVENT = 'google-codelab-pageview';
3841
*/
3942
const GAID_ATTR = 'gaid';
4043

44+
/**
45+
* The Google Analytics GA4 ID.
46+
* @const {string}
47+
*/
48+
const GA4ID_ATTR = 'ga4id';
49+
50+
/** @const {string} */
51+
const GTAG = 'gtag';
52+
53+
/**
54+
* Namespaced data layer for use with GA4 properties. Allows for independent
55+
* data layers so that other data layers, like that for GTM, don't receive data
56+
* they don't need.
57+
*
58+
* @const {string}
59+
*/
60+
const CODELAB_DATA_LAYER = 'codelabDataLayer';
61+
4162
/** @const {string} */
4263
const CODELAB_ID_ATTR = 'codelab-id';
4364

@@ -47,6 +68,12 @@ const CODELAB_ID_ATTR = 'codelab-id';
4768
*/
4869
const CODELAB_GAID_ATTR = 'codelab-gaid';
4970

71+
/**
72+
* The GA4ID defined by the current codelab.
73+
* @const {string}
74+
*/
75+
const CODELAB_GA4ID_ATTR = 'codelab-ga4id';
76+
5077
/** @const {string} */
5178
const CODELAB_ENV_ATTR = 'environment';
5279

@@ -103,6 +130,9 @@ class CodelabAnalytics extends HTMLElement {
103130
/** @private {?string} */
104131
this.gaid_;
105132

133+
/** @private {?string} */
134+
this.ga4Id_;
135+
106136
/** @private {?string} */
107137
this.codelabId_;
108138

@@ -125,8 +155,9 @@ class CodelabAnalytics extends HTMLElement {
125155
*/
126156
connectedCallback() {
127157
this.gaid_ = this.getAttribute(GAID_ATTR) || '';
158+
this.ga4Id_ = this.getAttribute(GA4ID_ATTR) || '';
128159

129-
if (this.hasSetup_ || !this.gaid_) {
160+
if (this.hasSetup_ || (!this.gaid_ && !this.ga4Id_)) {
130161
return;
131162
}
132163

@@ -139,6 +170,14 @@ class CodelabAnalytics extends HTMLElement {
139170
} else {
140171
this.init_();
141172
}
173+
174+
if (this.ga4Id_) {
175+
this.initializeGa4_();
176+
}
177+
178+
if (this.ga4Id_ && !this.gaid_) {
179+
this.addEventListeners_();
180+
}
142181
}
143182

144183
/** @private */
@@ -153,7 +192,7 @@ class CodelabAnalytics extends HTMLElement {
153192
addEventListeners_() {
154193
this.eventHandler_.listen(document.body, ACTION_EVENT,
155194
(e) => {
156-
const detail = /** @type {AnalyticsTrackingEvent} */ (
195+
const detail = /** @type {!AnalyticsTrackingEvent} */ (
157196
e.getBrowserEvent().detail);
158197
// Add tracking...
159198
this.trackEvent_(
@@ -162,7 +201,7 @@ class CodelabAnalytics extends HTMLElement {
162201

163202
this.eventHandler_.listen(document.body, PAGEVIEW_EVENT,
164203
(e) => {
165-
const detail = /** @type {AnalyticsPageview} */ (
204+
const detail = /** @type {!AnalyticsPageview} */ (
166205
e.getBrowserEvent().detail);
167206
this.trackPageview_(detail['page'], detail['title']);
168207
});
@@ -216,6 +255,7 @@ class CodelabAnalytics extends HTMLElement {
216255
* @private
217256
*/
218257
trackEvent_(category, opt_action, opt_label) {
258+
// UA related section.
219259
const params = {
220260
// Always event for trackEvent_ method
221261
'hitType': 'event',
@@ -227,6 +267,30 @@ class CodelabAnalytics extends HTMLElement {
227267
'eventLabel': opt_label || '',
228268
};
229269
this.gaSend_(params);
270+
271+
// GA4 related section.
272+
if (!this.getGa4Ids_().length) {
273+
return;
274+
}
275+
276+
window[CODELAB_DATA_LAYER] = window[CODELAB_DATA_LAYER] || [];
277+
window[GTAG] = window[GTAG] || function() {
278+
window[CODELAB_DATA_LAYER].push(arguments);
279+
};
280+
281+
for (const ga4Id of this.getGa4Ids_()) {
282+
window[GTAG]('event', category, {
283+
// Snakecase naming convention is followed for all built-in GA4 event
284+
// properties.
285+
'send_to': ga4Id,
286+
// Camelcase naming convention is followed for all custom dimensions
287+
// constructed in the custom element.
288+
'eventAction': opt_action || '',
289+
'eventLabel': opt_label || '',
290+
'codelabEnv': this.codelabEnv_ || '',
291+
'codelabId': this.codelabId_ || '',
292+
});
293+
}
230294
}
231295

232296
/**
@@ -235,6 +299,7 @@ class CodelabAnalytics extends HTMLElement {
235299
* @private
236300
*/
237301
trackPageview_(opt_page, opt_title) {
302+
// UA related section.
238303
const params = {
239304
'hitType': 'pageview',
240305
'dimension1': this.codelabEnv_,
@@ -244,6 +309,33 @@ class CodelabAnalytics extends HTMLElement {
244309
'title': opt_title || ''
245310
};
246311
this.gaSend_(params);
312+
313+
// GA4 related section.
314+
if (!this.getGa4Ids_().length) {
315+
return;
316+
}
317+
318+
window[CODELAB_DATA_LAYER] = window[CODELAB_DATA_LAYER] || [];
319+
window[GTAG] = window[GTAG] || function() {
320+
window[CODELAB_DATA_LAYER].push(arguments);
321+
};
322+
323+
for (const ga4Id of this.getGa4Ids_()) {
324+
window[GTAG]('event', 'page_view', {
325+
// Snakecase naming convention is followed for all built-in GA4 event
326+
// properties.
327+
'send_to': ga4Id,
328+
'page_location':
329+
`${document.location.origin}${document.location.pathname}`,
330+
'page_path': opt_page || '',
331+
'page_title': opt_title || '',
332+
// Camelcase naming convention is followed for all custom dimensions
333+
// constructed in the custom element.
334+
'codelabCategory': this.codelabCategory_ || '',
335+
'codelabEnv': this.codelabEnv_ || '',
336+
'codelabId': this.codelabId_ || '',
337+
});
338+
}
247339
}
248340

249341
/**
@@ -385,6 +477,68 @@ class CodelabAnalytics extends HTMLElement {
385477
}
386478
return isCreated;
387479
}
480+
481+
/**
482+
* Gets all GA4 IDs for the current page.
483+
* @return {!Array<string>}
484+
* @private
485+
*/
486+
getGa4Ids_() {
487+
if (!this.ga4Id_) {
488+
return [];
489+
}
490+
const ga4Ids = [];
491+
ga4Ids.push(this.ga4Id_);
492+
const codelabGa4Id = this.getAttribute(CODELAB_GA4ID_ATTR);
493+
if (codelabGa4Id) {
494+
ga4Ids.push(codelabGa4Id);
495+
}
496+
if (ga4Ids.length) {
497+
return ga4Ids;
498+
}
499+
return [];
500+
}
501+
502+
/**
503+
* Initialize the gtag script element and namespaced data layer based on the
504+
* codelabs primary GA4 ID.
505+
* @private
506+
*/
507+
initializeGa4_() {
508+
if (!this.ga4Id_) {
509+
return;
510+
}
511+
512+
// First, set the GTAG data layer before pushing anything to it.
513+
window[CODELAB_DATA_LAYER] = window[CODELAB_DATA_LAYER] || [];
514+
515+
const firstScriptElement = document.querySelector('script');
516+
const gtagScriptElement = /** @type {!HTMLScriptElement} */ (
517+
document.createElement('script'));
518+
gtagScriptElement.async = true;
519+
// Key for the formatted params below:
520+
// 'id': the stream id for the GA4 analytics property. The gtag script
521+
// element must only be created once, and only the ID of the primary
522+
// stream is appended when creating the src for that element.
523+
// Additional streams are initialized via the function call
524+
// `window[GTAG]('config', ga4Id...`
525+
// 'l': the namespaced dataLayer used to separate codelabs related GA4
526+
// data from other data layers that may exist on a site or page.
527+
safeScriptEl.setSrc(
528+
gtagScriptElement, TrustedResourceUrl.formatWithParams(
529+
Const.from('//www.googletagmanager.com/gtag/js'),
530+
{}, {'id': this.ga4Id_, 'l': CODELAB_DATA_LAYER}));
531+
firstScriptElement.parentNode.insertBefore(
532+
gtagScriptElement, firstScriptElement);
533+
534+
window[GTAG] = function() {
535+
window[CODELAB_DATA_LAYER].push(arguments);
536+
};
537+
window[GTAG]('js', new Date(Date.now()));
538+
539+
// Set send_page_view to false. We send pageviews manually.
540+
window[GTAG]('config', this.ga4Id_, {send_page_view: false});
541+
}
388542
}
389543

390544
exports = CodelabAnalytics;

codelab-elements/google-codelab/google_codelab.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ const CATEGORY_ATTR = 'category';
4646
/** @const {string} */
4747
const GAID_ATTR = 'codelab-gaid';
4848

49+
/** @const {string} */
50+
const GA4ID_ATTR = 'codelab-ga4id';
51+
4952
/** @const {string} */
5053
const CODELAB_ID_ATTR = 'codelab-id';
5154

@@ -297,6 +300,10 @@ class Codelab extends HTMLElement {
297300
if (gaid) {
298301
analytics.setAttribute(GAID_ATTR, gaid);
299302
}
303+
const ga4id = this.getAttribute(GA4ID_ATTR);
304+
if (ga4id) {
305+
analytics.setAttribute(GA4ID_ATTR, ga4id);
306+
}
300307
if (this.id_) {
301308
analytics.setAttribute(CODELAB_ID_ATTR, this.id_);
302309
}
@@ -686,7 +693,7 @@ class Codelab extends HTMLElement {
686693
this.currentSelectedStep = selected;
687694
this.firePageViewEvent();
688695

689-
// Set the focus on the new step after the animation is finished becasue it
696+
// Set the focus on the new step after the animation is finished because it
690697
// messes up the animation.
691698
clearTimeout(this.setFocusTimeoutId_);
692699
this.setFocusTimeoutId_ = setTimeout(() => {

0 commit comments

Comments
 (0)