Skip to content

Commit f3acb12

Browse files
authored
Google Analytics 4 (#570)
* Initial GA4 Commit * Add tests, Fix bugs * Update config and set calls * Remove commented language var * Remove reject from group handler * Fix comments and add complex setting example * Update setting keys * Iterate on feedback * Remove stray comment * Update version to 0.0.1
1 parent 20f7641 commit f3acb12

File tree

5 files changed

+678
-0
lines changed

5 files changed

+678
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('../../karma.conf-ci.js');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('../../karma.conf');
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
'use strict';
2+
3+
/**
4+
* Module dependencies.
5+
*/
6+
var integration = require('@segment/analytics.js-integration');
7+
var reject = require('reject');
8+
9+
/**
10+
* GA4
11+
*/
12+
var GA4 = (module.exports = integration('Google Analytics 4')
13+
.global('gtag')
14+
.global('ga4DataLayer')
15+
.option('measurementIds', [])
16+
.option('cookieDomainName', 'auto')
17+
.option('cookiePrefix', '_ga')
18+
.option('cookieExpiration', 63072000)
19+
.option('cookieUpdate', true)
20+
.option('cookieFlags', '')
21+
.option('sendAutomaticPageViewEvent', false)
22+
.option('allowAllAdvertisingFeatures', false)
23+
.option('allowAdvertisingPersonalization', false)
24+
.option('disableGoogleAnalytics', false)
25+
.option('googleReportingIdentity', 'device')
26+
.option('userProperties', {})
27+
/**
28+
* Custom Events and Parameters setting. This setting is used by the track
29+
* handler to map Segment events and fields to Google analytics events and parameters.
30+
*
31+
* Example:
32+
* [
33+
* {
34+
* "googleEvent": "new_episode",
35+
* "parameters": [
36+
* {
37+
* "key": "properties.title",
38+
* "value": "title"
39+
* },
40+
* {
41+
* "key": "properties.genre",
42+
* "value": "genre"
43+
* }
44+
* ],
45+
* "segmentEvent": "Started Episode"
46+
* }
47+
* ]
48+
*/
49+
.option('customEventsAndParameters', [])
50+
.tag(
51+
'<script src="//www.googletagmanager.com/gtag/js?id={{ measurementId }}&l=ga4DataLayer">'
52+
));
53+
54+
/**
55+
* Initialize.
56+
*
57+
* https://developers.google.com/analytics/devguides/collection/ga4
58+
*
59+
* @api public
60+
*/
61+
GA4.prototype.initialize = function() {
62+
window.ga4DataLayer = window.ga4DataLayer || [];
63+
window.gtag = function() {
64+
window.ga4DataLayer.push(arguments);
65+
};
66+
67+
/**
68+
* This line is in all of the gtag examples but is not well documented. Research
69+
* says that it is is related to deduplication.
70+
* https://stackoverflow.com/questions/59256532/what-is-the-js-gtags-js-command
71+
*/
72+
window.gtag('js', new Date());
73+
74+
var opts = this.options;
75+
var measurementIds = opts.measurementIds;
76+
77+
/**
78+
* Avoid loading and configuring gtag.js if any are true:
79+
* - Disable Google Analytics setting is enabled
80+
* - No measurement IDs are configured
81+
*/
82+
if (!measurementIds.length || opts.disableGoogleAnalytics) {
83+
return;
84+
}
85+
86+
var config = {
87+
/**
88+
* Disable Google's Automatic Page View Measurement
89+
* https://developers.google.com/analytics/devguides/collection/ga4/disable-page-view
90+
*/
91+
send_page_view: opts.sendAutomaticPageViewEvent,
92+
93+
/**
94+
* Cookie Update
95+
* https://developers.google.com/analytics/devguides/collection/ga4/cookies-user-id#cookie_update_parameter
96+
*/
97+
cookie_update: opts.cookieUpdate,
98+
99+
/**
100+
* Cookie Domain Name
101+
* https://developers.google.com/analytics/devguides/collection/ga4/cookies-user-id#cookie_domain_configuration
102+
*/
103+
cookie_domain: opts.cookieDomainName,
104+
105+
/**
106+
* Cookie Prefix
107+
* https://developers.google.com/analytics/devguides/collection/ga4/cookies-user-id#cookie_prefix
108+
*/
109+
cookie_prefix: opts.cookiePrefix,
110+
111+
/**
112+
* Cookie Expiration
113+
* https://developers.google.com/analytics/devguides/collection/ga4/cookies-user-id#cookie_expiration
114+
*/
115+
cookie_expires: opts.cookieExpiration,
116+
};
117+
118+
var sets = [
119+
/**
120+
* Cookie Flags
121+
* https://developers.google.com/analytics/devguides/collection/ga4/cookies-user-id#cookie_flags
122+
*/
123+
[{ cookie_flags: opts.cookieFlags }],
124+
125+
/**
126+
* Disable All Advertising
127+
* https://developers.google.com/analytics/devguides/collection/ga4/display-features#disable_all_advertising_features
128+
*/
129+
['allow_google_signals', opts.allowAllAdvertisingFeatures],
130+
131+
/**
132+
* Disable Advertising Personalization
133+
* https://developers.google.com/analytics/devguides/collection/ga4/display-features#disable_advertising_personalization
134+
*/
135+
['allow_ad_personalization_signals', opts.allowAdvertisingPersonalization]
136+
];
137+
138+
// Load gtag.js using the first measurement ID, then configure using the `config` commands built above.
139+
var self = this;
140+
this.load({ measurementId: measurementIds[0] }, function() {
141+
/**
142+
* Measurement IDs.
143+
* The same configuration information is shared across all measurement IDs.
144+
* https://developers.google.com/analytics/devguides/collection/ga4#add_an_additional_google_analytics_property_to_an_existing_tag
145+
*/
146+
for (var i = 0; i < measurementIds.length; i++) {
147+
window.gtag('config', measurementIds[i], config)
148+
149+
}
150+
151+
/**
152+
* Set persistent values shared across all gtag.js usage.
153+
* https://developers.google.com/gtagjs/reference/api#set
154+
*/
155+
for (var i = 0; i < sets.length; i++) {
156+
window.gtag.apply(null, sets[i]);
157+
}
158+
159+
self.ready();
160+
});
161+
};
162+
163+
/**
164+
* Loaded?
165+
*
166+
* @api private
167+
* @return {boolean}
168+
*/
169+
GA4.prototype.loaded = function() {
170+
return !!(
171+
window.ga4DataLayer && Array.prototype.push !== window.ga4DataLayer.push
172+
);
173+
};
174+
175+
/**
176+
* Identify.
177+
*
178+
* @api public
179+
* @param {Facade.Identify} event
180+
*/
181+
GA4.prototype.identify = function(identify) {
182+
var opts = this.options;
183+
var userPropertyMappings = opts.userProperties;
184+
185+
var userProperties = {};
186+
187+
// Map all customer-defined user property mappings.
188+
for (var eventField in userPropertyMappings) {
189+
if (!userPropertyMappings.hasOwnProperty(eventField)) {
190+
continue;
191+
}
192+
193+
var userProp = userPropertyMappings[eventField];
194+
var value = identify.proxy(eventField);
195+
196+
userProperties[userProp] = value;
197+
}
198+
199+
/**
200+
* Map the user_id property if the Google Reporting Identity is set one of:
201+
* - By User ID, Google signals, then device (userIdSignalsAndDevice)
202+
* - By User ID and Devicea (userIdAndDevice)
203+
*
204+
* Google's Reporting Identity: https://support.google.com/analytics/answer/9213390?hl=en
205+
*
206+
* Note that the user ID can be appended as part of the user_properties
207+
* object instead of being configured by an explicit command.
208+
* https://developers.google.com/analytics/devguides/collection/ga4/cookies-user-id#set_user_id
209+
*/
210+
var userId = identify.userId();
211+
var validReportingIdentity = opts.googleReportingIdentity === 'userIdSignalsAndDevice' || opts.googleReportingIdentity === 'userIdAndDevice'
212+
if (userId && validReportingIdentity) {
213+
userProperties.user_id = userId;
214+
}
215+
216+
if (Object.keys(userProperties).length) {
217+
window.gtag('set', 'user_properties', userProperties);
218+
}
219+
};
220+
221+
/**
222+
* Group
223+
*
224+
* @api public
225+
* @param {Facade.Group} group
226+
*/
227+
GA4.prototype.group = function(group) {
228+
window.gtag('event', 'join_group', {
229+
group_id: group.groupId()
230+
});
231+
};
232+
233+
/**
234+
* Page
235+
*
236+
* @api public
237+
* @param {Facade.Page} page
238+
*/
239+
GA4.prototype.page = function(page) {
240+
// If the Send Google's Automatic Page View Measurement setting is set to true then
241+
// don't handle page calls to avoid duplicate page_view events.
242+
if (this.options.sendAutomaticPageViewEvent) {
243+
return;
244+
}
245+
246+
var props = page.properties();
247+
var name = page.fullName();
248+
249+
var pageLocation = props.url;
250+
var pageReferrer = page.referrer();
251+
var pageTitle = name || props.title;
252+
253+
window.gtag('event', 'page_view', {
254+
page_location: pageLocation,
255+
page_referrer: pageReferrer,
256+
page_title: pageTitle
257+
});
258+
};
259+
260+
/**
261+
* Track
262+
*
263+
* @api public
264+
* @param {Track} track
265+
*/
266+
267+
GA4.prototype.track = function(track) {
268+
269+
var mappings = this.options.customEventsAndParameters;
270+
271+
for (var i = 0; i < mappings.length; i++) {
272+
var mapping = mappings[i];
273+
if (typeof mapping !== 'object') {
274+
continue;
275+
}
276+
277+
var segmentEvent = mapping.segmentEvent;
278+
var googleEvent = mapping.googleEvent;
279+
280+
if (!segmentEvent || !googleEvent || segmentEvent !== track.event()) {
281+
continue;
282+
}
283+
284+
var parameterMappings = mapping.parameters || [];
285+
var parameters = {};
286+
287+
if (!(parameterMappings instanceof Array)) {
288+
continue;
289+
}
290+
291+
// Map Segment event fields to Google Event Parameters.
292+
// Text map settings that are nested in a mixed settings take on a different shape
293+
// than a top-level text map setting.
294+
// eg; [{ key: 'properties.genre', value: 'primary_genre }]
295+
//
296+
for (var j = 0; j < parameterMappings.length; j++) {
297+
var map = parameterMappings[j] || {};
298+
if (typeof map !== 'object' || !map.key || !map.value) {
299+
continue;
300+
}
301+
302+
var param = map.value;
303+
var value = track.proxy(map.key);
304+
parameters[param] = value;
305+
}
306+
307+
window.gtag('event', googleEvent, parameters);
308+
}
309+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "@segment/analytics.js-integration-google-analytics-4",
3+
"version": "0.0.1",
4+
"description": "",
5+
"main": "lib/index.js",
6+
"directories": {
7+
"lib": "lib",
8+
"test": "test"
9+
},
10+
"scripts": {
11+
"test": "karma start"
12+
},
13+
"author": "Segment <friends@segment.com>",
14+
"license": "SEE LICENSE IN LICENSE",
15+
"homepage": "https://github.com/segmentio/analytics.js-integrations/blob/master/integrations/google-analytics-4#readme",
16+
"bugs": {
17+
"url": "https://github.com/segmentio/analytics.js-integrations/issues"
18+
},
19+
"repository": {
20+
"type": "git",
21+
"url": "git+https://github.com/segmentio/analytics.js-integrations.git"
22+
},
23+
"dependencies": {
24+
"@ndhoule/defaults": "^2.0.1",
25+
"@segment/analytics.js-integration": "^3.1.0",
26+
"component-each": "^0.2.6",
27+
"extend": "^3.0.2",
28+
"global-queue": "^1.0.1",
29+
"is": "^3.1.0",
30+
"lodash": "^4.17.4",
31+
"obj-case": "^0.2.0",
32+
"object-component": "0.0.3",
33+
"reject": "0.0.1",
34+
"segmentio-facade": "^3.2.7",
35+
"use-https": "^0.1.1"
36+
},
37+
"devDependencies": {
38+
"@segment/analytics.js-core": "^3.8.2",
39+
"@segment/analytics.js-integration-tester": "^3.1.1",
40+
"@segment/clear-env": "^2.1.1",
41+
"browserify": "^16.2.3",
42+
"eslint": "^5.16.0",
43+
"karma": "^4.1.0",
44+
"karma-browserify": "^6.0.0",
45+
"karma-chrome-launcher": "^2.2.0",
46+
"karma-mocha": "^1.3.0",
47+
"karma-mocha-reporter": "^2.2.5",
48+
"karma-sauce-launcher": "^2.0.2",
49+
"karma-spec-reporter": "^0.0.32",
50+
"karma-summary-reporter": "^1.6.0",
51+
"mocha": "^6.1.4",
52+
"to-array": "^0.1.4",
53+
"watchify": "^3.7.0"
54+
}
55+
}

0 commit comments

Comments
 (0)