Skip to content

Commit 26172de

Browse files
fixed casing for parameters
1 parent f73a70d commit 26172de

File tree

10 files changed

+615
-25
lines changed

10 files changed

+615
-25
lines changed

Gruntfile.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ module.exports = function(grunt) {
7474
unitBrowsers: {
7575
options: {
7676
// Run tests on all the browsers on the system.
77-
frameworks: ['jasmine', 'detectBrowsers'],
77+
frameworks: ['jasmine-ajax', 'jasmine', 'detectBrowsers'],
7878
detectBrowsers: {
7979
enabled: true,
8080
// Don't try to load phantomJS, it may not exist.
@@ -87,7 +87,7 @@ module.exports = function(grunt) {
8787
},
8888
integrationBrowsers: {
8989
options: { // Same options as for unitBrowsers.
90-
frameworks: ['jasmine', 'detectBrowsers'],
90+
frameworks: ['jasmine-ajax', 'jasmine', 'detectBrowsers'],
9191
detectBrowsers: {
9292
enabled: true,
9393
usePhantomJS: false,

karma.conf.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
module.exports = function(config) {
55
config.set({
66
basePath: '',
7-
frameworks: ['jasmine'],
7+
frameworks: ['jasmine-ajax', 'jasmine'],
88
// Automatically run tests for files matching these regex.
99
files: [
1010
// ----------------- Third Party Dependencies ----------------------------
@@ -19,16 +19,20 @@ module.exports = function(config) {
1919
// ------------------------ Source Files ---------------------------------
2020
{pattern: 'src/logging.js'},
2121
{pattern: 'src/storage/**.js'},
22-
{pattern: 'src/eventProcessor/**.js'},
22+
{pattern: 'src/eventProcessor/EventProcessorInterface.js'},
23+
{pattern: 'src/eventProcessor/generateUniqueId.js'},
24+
{pattern: 'src/eventProcessor/GoogleAnalyticsEventProcessor.js'},
2325
{pattern: 'src/config/configProcessors.js'},
2426
{pattern: 'src/config/setup.js'},
2527
{pattern: 'src/main.js'},
2628
// ------------------------- Test Files ----------------------------------
29+
'test/unit/mocks.js',
2730
'test/unit/**/*_test.js',
2831
],
2932
preprocessors: {'**/*.js': ['googmodule']},
3033
plugins: [
3134
require('karma-jasmine'),
35+
require('karma-jasmine-ajax'),
3236
require('karma-detect-browsers'),
3337
require('karma-chrome-launcher'),
3438
require('karma-firefox-launcher'),

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"grunt": "~1.2.1",
2121
"grunt-karma": "~4.0.0",
2222
"jasmine": "~3.5.0",
23+
"jasmine-ajax": "~4.0.0",
2324
"jasmine-core": "~3.5.0",
2425
"karma": "~5.1.0",
2526
"karma-chrome-launcher": "~3.1.0",
@@ -28,6 +29,7 @@
2829
"karma-googmodule-preprocessor": "~1.0.1",
2930
"karma-ie-launcher": "~1.0.0",
3031
"karma-jasmine": "~3.3.1",
32+
"karma-jasmine-ajax": "~0.1.13",
3133
"karma-jasmine-html-reporter": "~1.5.4",
3234
"karma-safari-launcher": "~1.0.0",
3335
"karma-spec-reporter": "~0.0.32"

src/eventProcessor/GoogleAnalyticsEventProcessor.js

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
goog.module('measurementLibrary.eventProcessor.GoogleAnalyticsEventProcessor');
22

3+
const uniqueId = goog.require('measurementLibrary.eventProcessor.generateUniqueId');
34
const logging = goog.require('measurementLibrary.logging');
45

56
/**
@@ -8,11 +9,11 @@ const logging = goog.require('measurementLibrary.logging');
89
* @const {!Object<string,boolean>}
910
*/
1011
const TOP_LEVEL_PARAMS = {
11-
'client_id': true,
12-
'user_id': true,
13-
'timestamp_micros': true,
14-
'user_properties': true,
15-
'non_personalized_ads': true,
12+
'client_id': 'clientId',
13+
'user_id': 'userId',
14+
'timestamp_micros': 'timestampMircos',
15+
'user_properties': 'userProperties',
16+
'non_personalized_ads': 'nonPersonalizedAds',
1617
};
1718

1819
/**
@@ -135,16 +136,16 @@ class GoogleAnalyticsEventProcessor {
135136
* @private @const {!Object<string, boolean>}
136137
*/
137138
this.automaticParams_ = {
138-
'page_path': true,
139-
'page_location': true,
140-
'page_title': true,
141-
'user_id': true,
142-
'client_id': true,
139+
'page_path': 'page_path',
140+
'page_location': 'page_location',
141+
'page_title': 'page_title',
142+
'user_id': 'userId',
143+
'client_id': 'clientId',
143144
};
144145

145146
// Add user provided params to automatic param list
146147
for (let i = 0; i < userAutomaticParams.length; ++i) {
147-
this.automaticParams_[userAutomaticParams[i]] = true;
148+
this.automaticParams_[userAutomaticParams[i]] = userAutomaticParams[i];
148149
}
149150

150151
/**
@@ -187,6 +188,47 @@ class GoogleAnalyticsEventProcessor {
187188
return 'googleAnalytics';
188189
}
189190

191+
/**
192+
* Gets value stored in the global model under a given key.
193+
* Only accesses the globel model if the given key is an automatic parameter.
194+
* @param {string} key
195+
* @param {{get:function(string):*, set:function(string, *)}} modelInterface
196+
* An interface to load or save short term page data from the data layer.
197+
* @return {*} value
198+
* @private
199+
*/
200+
getFromGlobalScope_(key, modelInterface) {
201+
if (this.automaticParams_[key]) {
202+
return modelInterface.get(this.automaticParams_[key]);
203+
}
204+
return undefined;
205+
}
206+
207+
/**
208+
* Gets the ID associated with the current client.
209+
* First queries the global model followed by long term storage if not yet
210+
* found for `clientId`. If no previous ID exists, a new one is generated
211+
* and stored for future use in both the global model and long term storage.
212+
* @param {!StorageInterface} storageInterface An interface to an object to
213+
* load or save persistent data with.
214+
* @param {{get:function(string):*, set:function(string, *)}} modelInterface
215+
* An interface to load or save short term page data from the data layer.
216+
* @return {string}
217+
* @private
218+
*/
219+
getClientId_(storageInterface, modelInterface) {
220+
let clientId = this.getFromGlobalScope_('client_id', modelInterface);
221+
if (!clientId || typeof clientId !== 'string') {
222+
clientId = storageInterface.load('clientId');
223+
if (!clientId || typeof clientId !== 'string') {
224+
clientId = uniqueId.generateUniqueId();
225+
storageInterface.save('clientId', clientId, this.clientIdExpires_);
226+
}
227+
modelInterface.set('clientId', clientId);
228+
}
229+
return clientId;
230+
}
231+
190232
/**
191233
* Builds the POST request URL using `measurement_id` and `api_secret`
192234
* query parameters if available
@@ -212,6 +254,27 @@ class GoogleAnalyticsEventProcessor {
212254
return this.measurementUrl_;
213255
}
214256

257+
/**
258+
* Adds a key value pair to JSON POST request if value is defined.
259+
* If the key is a top level parameter, the pair will be added at the topmost
260+
* level of the JSON.
261+
* If not, the pair is interpreted as an event parameter and added to the
262+
* params object within the JSON.
263+
* @param {!Object<string, *>} json
264+
* @param {string} key
265+
* @param {*} value
266+
* @private
267+
*/
268+
addKeyValuePairToJson_(json, key, value) {
269+
if (value !== undefined) {
270+
if (TOP_LEVEL_PARAMS[key]) {
271+
json[TOP_LEVEL_PARAMS[key]] = value;
272+
} else if (!TOP_LEVEL_PARAMS[key]) {
273+
json.events[0].params[key] = value;
274+
}
275+
}
276+
}
277+
215278
/**
216279
* Processes events pushed to the data layer by constructing and sending JSON
217280
* POST requests to Google Analytics.
@@ -226,7 +289,34 @@ class GoogleAnalyticsEventProcessor {
226289
* @export
227290
*/
228291
processEvent(storageInterface, modelInterface, eventName, eventOptions) {
229-
// TODO(kjgalvan):: constructs and sends JSON POST requests to GA
292+
const xhr = new XMLHttpRequest();
293+
xhr.open('POST', this.buildRequestUrl_());
294+
295+
const json = {
296+
events: [{'name': eventName, 'params': {}}],
297+
};
298+
299+
for (const key in this.automaticParams_) {
300+
let value;
301+
if (key === 'client_id') {
302+
value = this.getClientId_(storageInterface, modelInterface);
303+
} else {
304+
value = this.getFromGlobalScope_(key, modelInterface);
305+
}
306+
this.addKeyValuePairToJson_(json, key, value);
307+
}
308+
309+
for (const key in eventOptions) {
310+
this.addKeyValuePairToJson_(json, key, eventOptions[key]);
311+
}
312+
313+
if (logging.DEBUG) {
314+
xhr.onload = () => {
315+
logging.log(xhr.responseText, logging.LogLevel.INFO);
316+
};
317+
}
318+
319+
xhr.send(JSON.stringify(json));
230320
}
231321

232322
/**
@@ -243,7 +333,7 @@ class GoogleAnalyticsEventProcessor {
243333
* @export
244334
*/
245335
persistTime(key, value) {
246-
if (key === 'client_id') {
336+
if (key === 'clientId') {
247337
return this.clientIdExpires_;
248338
} else {
249339
return -1;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
goog.module('measurementLibrary.eventProcessor.generateUniqueId');
2+
3+
/**
4+
* Returns a cryptographically secure random number generator if
5+
* available.
6+
* @return {function(!Uint32Array):!Uint32Array|undefined}
7+
*/
8+
function getCryptoRandomValues() {
9+
let getRandomValues;
10+
if (window.crypto) {
11+
getRandomValues = window.crypto.getRandomValues.bind(crypto);
12+
} else if (window.msCrypto) {
13+
getRandomValues = window.msCrypto.getRandomValues
14+
.bind(window.msCrypto); // IE11
15+
}
16+
return getRandomValues;
17+
}
18+
19+
/**
20+
* Generates a unique ID in the format [uint32].[timestamp]
21+
* @param {(function(!Uint32Array):!Uint32Array|null)=} randomNumberGenerator
22+
* @return {string}
23+
*/
24+
function generateUniqueId(randomNumberGenerator = getCryptoRandomValues()) {
25+
let uint32;
26+
if (randomNumberGenerator) {
27+
uint32 = randomNumberGenerator(new Uint32Array(1))[0];
28+
} else {
29+
uint32 = (Math.random() * (0xFFFFFFFF)) >>> 0;
30+
}
31+
const timestamp = Math.floor(new Date().getTime() / 1000);
32+
return `${uint32}.${timestamp}`;
33+
}
34+
35+
exports = {
36+
generateUniqueId,
37+
};

test/unit/GoogleAnalyticsEventProcessor/constructor_test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
goog.module('measurementLibrary.eventProcessor.testing.GoogleAnalyticsEventProcessor.constructor');
1+
goog.module('measurementLibrary.testing.eventProcessor.GoogleAnalyticsEventProcessor.constructor');
22
goog.setTestOnly();
33

44
const GoogleAnalyticsEventProcessor = goog.require('measurementLibrary.eventProcessor.GoogleAnalyticsEventProcessor');
@@ -12,8 +12,8 @@ describe('The `constructor` of GoogleAnalyticsEventProcessor', () => {
1212
'page_path': true,
1313
'page_location': true,
1414
'page_title': true,
15-
'user_id': true,
16-
'client_id': true,
15+
'userId': true,
16+
'clientId': true,
1717
};
1818

1919
it('does not have a default arguments for ' +
@@ -72,7 +72,7 @@ describe('The `constructor` of GoogleAnalyticsEventProcessor', () => {
7272
automatic_params: ['custom_param'],
7373
});
7474

75-
expect(eventProcessor.automaticParams_['client_id']).toBeTrue();
75+
expect(eventProcessor.automaticParams_['clientId']).toBeTrue();
7676
expect(eventProcessor.automaticParams_['custom_param']).toBeTrue();
7777
});
7878

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
goog.module('measurementLibrary.testing.eventProcessor.GoogleAnalyticsEventProcessor.getClientId');
2+
goog.setTestOnly();
3+
4+
const uniqueId = goog.require('measurementLibrary.eventProcessor.generateUniqueId');
5+
const GoogleAnalyticsEventProcessor = goog.require('measurementLibrary.eventProcessor.GoogleAnalyticsEventProcessor');
6+
const {MockStorage, MockModelInterface} = goog.require('measurementLibrary.testing.mocks');
7+
8+
9+
describe('The `getClientId_` method ' +
10+
'of GoogleAnalyticsEventProcessor', () => {
11+
const mockStoredId = 'stored_clientId';
12+
const mockModelId = 'model_clientId';
13+
const mockGeneratedId = 'generated_clientId';
14+
const eventProcessor = new GoogleAnalyticsEventProcessor();
15+
let mockStorage;
16+
let mockModelInterface;
17+
let generateUniqueId;
18+
19+
// mock the unique ID module
20+
beforeAll(() => {
21+
generateUniqueId = uniqueId.generateUniqueId;
22+
uniqueId.generateUniqueId = () => {
23+
return mockGeneratedId;
24+
};
25+
});
26+
27+
it('generates a client id when no id is present in model or storage ' +
28+
'and adds it to the current model and storage', () => {
29+
mockStorage = new MockStorage({'clientId': undefined});
30+
mockModelInterface =
31+
new MockModelInterface({'clientId': undefined});
32+
33+
expect(eventProcessor.getClientId_(mockStorage, mockModelInterface))
34+
.toBe(mockGeneratedId);
35+
expect(mockModelInterface.get('clientId')).toBe(mockGeneratedId);
36+
expect(mockStorage.load('clientId')).toBe(mockGeneratedId);
37+
});
38+
39+
it('loads client id from storage when no id is present in model ' +
40+
'and adds it to the current model', () => {
41+
mockStorage = new MockStorage({'clientId': mockStoredId});
42+
mockModelInterface =
43+
new MockModelInterface({'clientId': undefined});
44+
45+
expect(eventProcessor.getClientId_(mockStorage, mockModelInterface))
46+
.toBe(mockStoredId);
47+
expect(mockModelInterface.get('clientId')).toBe('stored_clientId');
48+
});
49+
50+
it('loads client id from model if present', () => {
51+
mockStorage = new MockStorage({'clientId': undefined});
52+
mockModelInterface =
53+
new MockModelInterface({'clientId': mockModelId});
54+
55+
expect(eventProcessor.getClientId_(mockStorage, mockModelInterface))
56+
.toBe(mockModelId);
57+
});
58+
59+
it('loads client id from model even if there is a ' +
60+
'different client id in storage', () => {
61+
mockStorage = new MockStorage({'clientId': mockStoredId});
62+
mockModelInterface =
63+
new MockModelInterface({'clientId': mockModelId});
64+
65+
expect(eventProcessor.getClientId_(mockStorage, mockModelInterface))
66+
.toBe(mockModelId);
67+
});
68+
69+
afterAll(() => {
70+
uniqueId.generateUniqueId = generateUniqueId;
71+
});
72+
});

test/unit/GoogleAnalyticsEventProcessor/persistTime_test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@ describe('The `persistTime` method ' +
2121
return expect(eventProcessor.persistTime(key, ''));
2222
}
2323

24-
it('returns default when key is not client_id', () => {
24+
it('returns default when key is not clientId', () => {
2525
expectPersistTime('test').toBe(-1);
2626
});
2727

28-
describe('When key is client_id', () => {
28+
describe('When key is clientId', () => {
2929
it('returns two years when client_id_expires is not set', () => {
30-
expectPersistTime('client_id').toBe(2 * 365 * 24 * 60 * 60);
30+
expectPersistTime('clientId').toBe(2 * 365 * 24 * 60 * 60);
3131
});
3232

3333
it('returns overridden number when client_id_expires is set', () => {
34-
expectPersistTime('client_id', /* clientIdExpires */ 0).toBe(0);
34+
expectPersistTime('clientId', /* clientIdExpires */ 0).toBe(0);
3535
});
3636
});
3737
});

0 commit comments

Comments
 (0)