Skip to content

Commit b77fa90

Browse files
authored
Merge pull request #99 from amplitude/device_id_param
Device id param + log event with timestamp
2 parents 310af98 + 819b0a8 commit b77fa90

File tree

8 files changed

+163
-23
lines changed

8 files changed

+163
-23
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## Unreleased
22

3+
* Add `logEventWithTimestamp` to allow logging events with a custom timestamp. The timestamp should a number representing the time in milliseconds since epoch. See [documentation](https://rawgit.com/amplitude/Amplitude-Javascript/master/documentation/AmplitudeClient.html) for more details.
4+
* Add configuration option `deviceIdFromUrlParam`, which when set to `true` will have the SDK parse device IDs from url parameter `amp_device_id` if available. Device IDs defined in the configuration options during init will take priority over device IDs from url parameters.
5+
6+
### 3.3.2 (October 28, 2016)
7+
38
* Updated our [UA-parser-js](https://github.com/amplitude/ua-parser-js) fork to properly parse the version number for Chrome Mobile browsers.
49

510
### 3.3.1 (October 26, 2016)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ amplitude.getInstance().init('YOUR_API_KEY_HERE', null, {
328328
| cookieExpiration | number | The number of days after which the Amplitude cookie will expire | 365\*10 (10 years) |
329329
| cookieName | string | Custom name for the Amplitude cookie | 'amplitude_id' |
330330
| deviceId | string | Custom device ID to set. Note this is not recommended unless you really know what you are doing (like if you have your own system for tracking user devices) | Randomly generated UUID |
331+
| deviceIdFromUrlParam | boolean | If `true`, the SDK will parse device ID values from url parameter `amp_device_id` if available. Device IDs defined in the configuration options during init will take priority over device IDs from url parameters.
331332
| domain | string | Custom cookie domain | The top domain of the current page's url |
332333
| eventUploadPeriodMillis | number | Amount of time in milliseconds that the SDK waits before uploading events if `batchEvents` is `true`. | 30\*1000 (30 sec) |
333334
| eventUploadThreshold | number | Minimum number of events to batch together per request if `batchEvents` is `true`. | 30 |

amplitude.js

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,9 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o
562562

563563
// load deviceId and userId from input, or try to fetch existing value from cookie
564564
this.options.deviceId = (type(opt_config) === 'object' && type(opt_config.deviceId) === 'string' &&
565-
!utils.isEmptyString(opt_config.deviceId) && opt_config.deviceId) || this.options.deviceId || UUID() + 'R';
565+
!utils.isEmptyString(opt_config.deviceId) && opt_config.deviceId) ||
566+
(this.options.deviceIdFromUrlParam && this._getDeviceIdFromUrlParam(this._getUrlParams())) ||
567+
this.options.deviceId || UUID() + 'R';
566568
this.options.userId = (type(opt_userId) === 'string' && !utils.isEmptyString(opt_userId) && opt_userId) ||
567569
this.options.userId || null;
568570

@@ -986,6 +988,14 @@ AmplitudeClient.prototype._saveGclid = function _saveGclid(urlParams) {
986988
_sendParamsReferrerUserProperties(this, gclidProperties);
987989
};
988990

991+
/**
992+
* Try to fetch Amplitude device id from url params.
993+
* @private
994+
*/
995+
AmplitudeClient.prototype._getDeviceIdFromUrlParam = function _getDeviceIdFromUrlParam(urlParams) {
996+
return utils.getQueryParam(Constants.AMP_DEVICE_ID_PARAM, urlParams);
997+
};
998+
989999
/**
9901000
* Parse the domain from referrer info
9911001
* @private
@@ -1092,7 +1102,7 @@ AmplitudeClient.prototype.setGroup = function(groupType, groupName) {
10921102
var groups = {};
10931103
groups[groupType] = groupName;
10941104
var identify = new Identify().set(groupType, groupName);
1095-
this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null);
1105+
this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null, null);
10961106
};
10971107

10981108
/**
@@ -1234,7 +1244,7 @@ AmplitudeClient.prototype.identify = function(identify_obj, opt_callback) {
12341244
// only send if there are operations
12351245
if (Object.keys(identify_obj.userPropertiesOperations).length > 0) {
12361246
return this._logEvent(
1237-
Constants.IDENTIFY_EVENT, null, null, identify_obj.userPropertiesOperations, null, opt_callback
1247+
Constants.IDENTIFY_EVENT, null, null, identify_obj.userPropertiesOperations, null, null, opt_callback
12381248
);
12391249
}
12401250
} else {
@@ -1263,7 +1273,7 @@ AmplitudeClient.prototype.setVersionName = function setVersionName(versionName)
12631273
* Private logEvent method. Keeps apiProperties from being publicly exposed.
12641274
* @private
12651275
*/
1266-
AmplitudeClient.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, callback) {
1276+
AmplitudeClient.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, timestamp, callback) {
12671277
_loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs
12681278
if (!eventType || this.options.optOut) {
12691279
if (type(callback) === 'function') {
@@ -1280,7 +1290,7 @@ AmplitudeClient.prototype._logEvent = function _logEvent(eventType, eventPropert
12801290
eventId = this.nextEventId();
12811291
}
12821292
var sequenceNumber = this.nextSequenceNumber();
1283-
var eventTime = new Date().getTime();
1293+
var eventTime = (type(timestamp) === 'number') ? timestamp : new Date().getTime();
12841294
if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) {
12851295
this._sessionId = eventTime;
12861296
}
@@ -1368,14 +1378,28 @@ AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue
13681378
* @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
13691379
*/
13701380
AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) {
1381+
return this.logEventWithTimestamp(eventType, eventProperties, null, opt_callback);
1382+
};
1383+
1384+
/**
1385+
* Log an event with eventType and eventProperties and a custom timestamp
1386+
* @public
1387+
* @param {string} eventType - name of event
1388+
* @param {object} eventProperties - (optional) an object with string keys and values for the event properties.
1389+
* @param {number} timesatmp - (optional) the custom timestamp as milliseconds since epoch.
1390+
* @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged.
1391+
* Note: the server response code and response body from the event upload are passed to the callback function.
1392+
* @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
1393+
*/
1394+
AmplitudeClient.prototype.logEventWithTimestamp = function logEvent(eventType, eventProperties, timestamp, opt_callback) {
13711395
if (!this._apiKeySet('logEvent()') || !utils.validateInput(eventType, 'eventType', 'string') ||
13721396
utils.isEmptyString(eventType)) {
13731397
if (type(opt_callback) === 'function') {
13741398
opt_callback(0, 'No request sent');
13751399
}
13761400
return -1;
13771401
}
1378-
return this._logEvent(eventType, eventProperties, null, null, null, opt_callback);
1402+
return this._logEvent(eventType, eventProperties, null, null, null, timestamp, opt_callback);
13791403
};
13801404

13811405
/**
@@ -1401,7 +1425,7 @@ AmplitudeClient.prototype.logEventWithGroups = function(eventType, eventProperti
14011425
}
14021426
return -1;
14031427
}
1404-
return this._logEvent(eventType, eventProperties, null, null, groups, opt_callback);
1428+
return this._logEvent(eventType, eventProperties, null, null, groups, null, opt_callback);
14051429
};
14061430

14071431
/**
@@ -1463,7 +1487,7 @@ AmplitudeClient.prototype.logRevenue = function logRevenue(price, quantity, prod
14631487
special: 'revenue_amount',
14641488
quantity: quantity || 1,
14651489
price: price
1466-
}, null, null, null);
1490+
}, null, null, null, null);
14671491
};
14681492

14691493
/**
@@ -1672,7 +1696,9 @@ module.exports = {
16721696
REVENUE_PRODUCT_ID: '$productId',
16731697
REVENUE_QUANTITY: '$quantity',
16741698
REVENUE_PRICE: '$price',
1675-
REVENUE_REVENUE_TYPE: '$revenueType'
1699+
REVENUE_REVENUE_TYPE: '$revenueType',
1700+
1701+
AMP_DEVICE_ID_PARAM: 'amp_device_id' // url param
16761702
};
16771703

16781704
}, {}],
@@ -4977,7 +5003,8 @@ module.exports = {
49775003
eventUploadPeriodMillis: 30 * 1000, // 30s
49785004
forceHttps: false,
49795005
includeGclid: false,
4980-
saveParamsReferrerOncePerSession: true
5006+
saveParamsReferrerOncePerSession: true,
5007+
deviceIdFromUrlParam: false,
49815008
};
49825009

49835010
}, {"./language":29}],

amplitude.min.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/amplitude-client.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o
8181

8282
// load deviceId and userId from input, or try to fetch existing value from cookie
8383
this.options.deviceId = (type(opt_config) === 'object' && type(opt_config.deviceId) === 'string' &&
84-
!utils.isEmptyString(opt_config.deviceId) && opt_config.deviceId) || this.options.deviceId || UUID() + 'R';
84+
!utils.isEmptyString(opt_config.deviceId) && opt_config.deviceId) ||
85+
(this.options.deviceIdFromUrlParam && this._getDeviceIdFromUrlParam(this._getUrlParams())) ||
86+
this.options.deviceId || UUID() + 'R';
8587
this.options.userId = (type(opt_userId) === 'string' && !utils.isEmptyString(opt_userId) && opt_userId) ||
8688
this.options.userId || null;
8789

@@ -505,6 +507,14 @@ AmplitudeClient.prototype._saveGclid = function _saveGclid(urlParams) {
505507
_sendParamsReferrerUserProperties(this, gclidProperties);
506508
};
507509

510+
/**
511+
* Try to fetch Amplitude device id from url params.
512+
* @private
513+
*/
514+
AmplitudeClient.prototype._getDeviceIdFromUrlParam = function _getDeviceIdFromUrlParam(urlParams) {
515+
return utils.getQueryParam(Constants.AMP_DEVICE_ID_PARAM, urlParams);
516+
};
517+
508518
/**
509519
* Parse the domain from referrer info
510520
* @private
@@ -611,7 +621,7 @@ AmplitudeClient.prototype.setGroup = function(groupType, groupName) {
611621
var groups = {};
612622
groups[groupType] = groupName;
613623
var identify = new Identify().set(groupType, groupName);
614-
this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null);
624+
this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null, null);
615625
};
616626

617627
/**
@@ -753,7 +763,7 @@ AmplitudeClient.prototype.identify = function(identify_obj, opt_callback) {
753763
// only send if there are operations
754764
if (Object.keys(identify_obj.userPropertiesOperations).length > 0) {
755765
return this._logEvent(
756-
Constants.IDENTIFY_EVENT, null, null, identify_obj.userPropertiesOperations, null, opt_callback
766+
Constants.IDENTIFY_EVENT, null, null, identify_obj.userPropertiesOperations, null, null, opt_callback
757767
);
758768
}
759769
} else {
@@ -782,7 +792,7 @@ AmplitudeClient.prototype.setVersionName = function setVersionName(versionName)
782792
* Private logEvent method. Keeps apiProperties from being publicly exposed.
783793
* @private
784794
*/
785-
AmplitudeClient.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, callback) {
795+
AmplitudeClient.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, timestamp, callback) {
786796
_loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs
787797
if (!eventType || this.options.optOut) {
788798
if (type(callback) === 'function') {
@@ -799,7 +809,7 @@ AmplitudeClient.prototype._logEvent = function _logEvent(eventType, eventPropert
799809
eventId = this.nextEventId();
800810
}
801811
var sequenceNumber = this.nextSequenceNumber();
802-
var eventTime = new Date().getTime();
812+
var eventTime = (type(timestamp) === 'number') ? timestamp : new Date().getTime();
803813
if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) {
804814
this._sessionId = eventTime;
805815
}
@@ -887,14 +897,28 @@ AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue
887897
* @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
888898
*/
889899
AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) {
900+
return this.logEventWithTimestamp(eventType, eventProperties, null, opt_callback);
901+
};
902+
903+
/**
904+
* Log an event with eventType and eventProperties and a custom timestamp
905+
* @public
906+
* @param {string} eventType - name of event
907+
* @param {object} eventProperties - (optional) an object with string keys and values for the event properties.
908+
* @param {number} timesatmp - (optional) the custom timestamp as milliseconds since epoch.
909+
* @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged.
910+
* Note: the server response code and response body from the event upload are passed to the callback function.
911+
* @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
912+
*/
913+
AmplitudeClient.prototype.logEventWithTimestamp = function logEvent(eventType, eventProperties, timestamp, opt_callback) {
890914
if (!this._apiKeySet('logEvent()') || !utils.validateInput(eventType, 'eventType', 'string') ||
891915
utils.isEmptyString(eventType)) {
892916
if (type(opt_callback) === 'function') {
893917
opt_callback(0, 'No request sent');
894918
}
895919
return -1;
896920
}
897-
return this._logEvent(eventType, eventProperties, null, null, null, opt_callback);
921+
return this._logEvent(eventType, eventProperties, null, null, null, timestamp, opt_callback);
898922
};
899923

900924
/**
@@ -920,7 +944,7 @@ AmplitudeClient.prototype.logEventWithGroups = function(eventType, eventProperti
920944
}
921945
return -1;
922946
}
923-
return this._logEvent(eventType, eventProperties, null, null, groups, opt_callback);
947+
return this._logEvent(eventType, eventProperties, null, null, groups, null, opt_callback);
924948
};
925949

926950
/**
@@ -982,7 +1006,7 @@ AmplitudeClient.prototype.logRevenue = function logRevenue(price, quantity, prod
9821006
special: 'revenue_amount',
9831007
quantity: quantity || 1,
9841008
price: price
985-
}, null, null, null);
1009+
}, null, null, null, null);
9861010
};
9871011

9881012
/**

src/constants.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,7 @@ module.exports = {
2424
REVENUE_PRODUCT_ID: '$productId',
2525
REVENUE_QUANTITY: '$quantity',
2626
REVENUE_PRICE: '$price',
27-
REVENUE_REVENUE_TYPE: '$revenueType'
27+
REVENUE_REVENUE_TYPE: '$revenueType',
28+
29+
AMP_DEVICE_ID_PARAM: 'amp_device_id' // url param
2830
};

src/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ module.exports = {
2222
eventUploadPeriodMillis: 30 * 1000, // 30s
2323
forceHttps: false,
2424
includeGclid: false,
25-
saveParamsReferrerOncePerSession: true
25+
saveParamsReferrerOncePerSession: true,
26+
deviceIdFromUrlParam: false,
2627
};

test/amplitude-client.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,42 @@ describe('AmplitudeClient', function() {
132132
assert.equal(counter, 1);
133133
});
134134

135+
it ('should load the device id from url params if configured', function() {
136+
var deviceId = 'aa_bb_cc_dd';
137+
sinon.stub(amplitude, '_getUrlParams').returns('?utm_source=amplitude&utm_medium=email&gclid=12345&amp_device_id=aa_bb_cc_dd');
138+
amplitude.init(apiKey, userId, {deviceIdFromUrlParam: true});
139+
assert.equal(amplitude.options.deviceId, deviceId);
140+
141+
var cookieData = cookie.get(amplitude.options.cookieName);
142+
assert.equal(cookieData.deviceId, deviceId);
143+
144+
amplitude._getUrlParams.restore();
145+
});
146+
147+
it ('should not load device id from url params if not configured', function() {
148+
var deviceId = 'aa_bb_cc_dd';
149+
sinon.stub(amplitude, '_getUrlParams').returns('?utm_source=amplitude&utm_medium=email&gclid=12345&amp_device_id=aa_bb_cc_dd');
150+
amplitude.init(apiKey, userId, {deviceIdFromUrlParam: false});
151+
assert.notEqual(amplitude.options.deviceId, deviceId);
152+
153+
var cookieData = cookie.get(amplitude.options.cookieName);
154+
assert.notEqual(cookieData.deviceId, deviceId);
155+
156+
amplitude._getUrlParams.restore();
157+
});
158+
159+
it ('should prefer the device id in the config over the url params', function() {
160+
var deviceId = 'dd_cc_bb_aa';
161+
sinon.stub(amplitude, '_getUrlParams').returns('?utm_source=amplitude&utm_medium=email&gclid=12345&amp_device_id=aa_bb_cc_dd');
162+
amplitude.init(apiKey, userId, {deviceId: deviceId, deviceIdFromUrlParam: true});
163+
assert.equal(amplitude.options.deviceId, deviceId);
164+
165+
var cookieData = cookie.get(amplitude.options.cookieName);
166+
assert.equal(cookieData.deviceId, deviceId);
167+
168+
amplitude._getUrlParams.restore();
169+
});
170+
135171
it ('should migrate deviceId, userId, optOut from localStorage to cookie on default instance', function() {
136172
var deviceId = 'test_device_id';
137173
var userId = 'test_user_id';
@@ -2049,6 +2085,50 @@ describe('setVersionName', function() {
20492085
assert.equal(events[0].event_type, 'testEvent');
20502086
assert.isTrue(events[0].user_agent.indexOf(phantomJSUA) > -1);
20512087
});
2088+
2089+
it('should allow logging event with custom timestamp', function() {
2090+
var timestamp = 2000;
2091+
amplitude.logEventWithTimestamp('test', null, timestamp, null);
2092+
assert.lengthOf(server.requests, 1);
2093+
var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e);
2094+
assert.lengthOf(events, 1);
2095+
2096+
// verify the event is correct
2097+
var event = events[0];
2098+
assert.equal(event.event_type, 'test');
2099+
assert.equal(event.event_id, 1);
2100+
assert.equal(event.timestamp, timestamp);
2101+
});
2102+
2103+
it('should use current time if timestamp is null', function() {
2104+
var timestamp = 5000;
2105+
clock.tick(timestamp);
2106+
amplitude.logEventWithTimestamp('test', null, null, null);
2107+
assert.lengthOf(server.requests, 1);
2108+
var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e);
2109+
assert.lengthOf(events, 1);
2110+
2111+
// verify the event is correct
2112+
var event = events[0];
2113+
assert.equal(event.event_type, 'test');
2114+
assert.equal(event.event_id, 1);
2115+
assert.isTrue(event.timestamp >= timestamp);
2116+
});
2117+
2118+
it('should use current time if timestamp is not valid form', function() {
2119+
var timestamp = 6000;
2120+
clock.tick(timestamp);
2121+
amplitude.logEventWithTimestamp('test', null, 'invalid', null);
2122+
assert.lengthOf(server.requests, 1);
2123+
var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e);
2124+
assert.lengthOf(events, 1);
2125+
2126+
// verify the event is correct
2127+
var event = events[0];
2128+
assert.equal(event.event_type, 'test');
2129+
assert.equal(event.event_id, 1);
2130+
assert.isTrue(event.timestamp >= timestamp);
2131+
});
20522132
});
20532133

20542134
describe('optOut', function() {

0 commit comments

Comments
 (0)