Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Unreleased

* Add option to track GCLID (Google Click ID) as a user property (set `includeGclid` to `true` in the SDK configuration).
* Add option to track new UTM parameters, referrer, and GCLID values during the same session. By default the SDK only saves the values once at the start of the session. You can remove this restriction by setting `saveParamsReferrerOncePerSession` to `false` in the SDK configuration. See the [Readme](https://github.com/amplitude/Amplitude-Javascript#tracking-utm-parameters-referrer-and-gclid) for more information.

### 3.2.0 (October 7, 2016)

* Block event property and user property dictionaries that have more than 1000 items. This is to block properties that are set unintentionally (for example in a loop). A single call to `logEvent` should not have more than 1000 event properties. Similarly a single call to `setUserProperties` should not have more than 1000 user properties.
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,19 +330,32 @@ amplitude.getInstance().init('YOUR_API_KEY_HERE', null, {
| eventUploadPeriodMillis | number | Amount of time in milliseconds that the SDK waits before uploading events if `batchEvents` is `true`. | 30\*1000 (30 sec) |
| eventUploadThreshold | number | Minimum number of events to batch together per request if `batchEvents` is `true`. | 30 |
| forceHttps | boolean | If `true`, the events will always be uploaded to HTTPS endpoint. Otherwise it will use the embedding site's protocol. | `false` |
| includeGclid | boolean | If `true`, captures the `gclid` url parameter as well as the user's `initial_gclid` via a set once operation. | `false` |
| includeReferrer | boolean | If `true`, captures the `referrer` and `referring_domain` for each session, as well as the user's `initial_referrer` and `initial_referring_domain` via a set once operation. | `false` |
| includeUtm | boolean | If `true`, finds utm parameters in the query string or the __utmz cookie, parses, and includes them as user propeties on all events uploaded. Also captures initial utm parameters for each session via a set once operation. | `false` |
| language | string | Custom language to set | Language determined by browser |
| optOut | boolean | Whether to disable tracking for the current user | `false` |
| platform | string | Custom platform to set | 'Web' |
| saveEvents | boolean | If `true`, saves events to localStorage and removes them upon successful upload.<br><i>NOTE:</i> Without saving events, events may be lost if the user navigates to another page before events are uploaded. | `true` |
| savedMaxCount | number | Maximum number of events to save in localStorage. If more events are logged while offline, old events are removed. | 1000 |
| saveParamsReferrerOncePerSession | boolean | If `true` then `includeGclid`, `includeReferrer`, and `includeUtm` will only track their respective properties once per session. New values that come in during the middle of the user's session will be ignored. Set to `false` to always capture new values. | `true` |
| sessionTimeout | number | Time between logged events before a new session starts in milliseconds | 30\*60\*1000 (30 min) |
| uploadBatchSize | number | Maximum number of events to send to the server per request. | 100 |

# Advanced #
This SDK automatically grabs useful data about the browser, including browser type and operating system version.

### Tracking UTM Parameters, Referrer, and GCLID ###

Amplitude supports automatically tracking:
* Standard UTM parameters from the user's cookie or URL parameters, just set configuration option `includeUtm` to `true` during initialization.
* The referring URL, just set configuration option `includeReferrer` to `true` during initialization.
* GCLID (Google Click ID) from URL params, just set configuration option `includeGclid` to `true` during initialization.

If tracking is enabled, then the SDK will set the values as user properties, for example `referrer` or `utm_source`, once per session (this is last touch). The SDK will also save the initial values using a `setOnce` operation, for example `initial_referrer` or `initial_utm_source`, and once set that value will never change (this is first touch).

**Note:** By default the SDK will only save the values at the start of the session. For example if a user lands on your site with an initial set of UTM parameters, triggers some flow that causes them to land on your site again with a different set of UTM parameters within the same Amplitude session, that second set will not be saved. You can set configuration option `saveParamsReferrerOncePerSession` to `false` to remove that restriction, so the SDK will always capture any new values from the user.

### Setting Groups ###

Amplitude supports assigning users to groups, and performing queries such as Count by Distinct on those groups. An example would be if you want to group your users based on what organization they are in by using an orgId. You can designate Joe to be in orgId 10, while Sue is in orgId 15. When performing an event segmentation query, you can then select Count by Distinct orgIds to query the number of different orgIds that have performed a specific event. As long as at least one member of that group has performed the specific event, that group will be included in the count. See our help article on [Count By Distinct](https://amplitude.zendesk.com/hc/en-us/articles/218824237) for more information.
Expand Down
97 changes: 62 additions & 35 deletions amplitude.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,13 +572,15 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o
this._sessionId = now;

// only capture UTM params and referrer if new session
if (this.options.includeUtm) {
this._initUtmData();
}
if (this.options.includeReferrer) {
this._saveReferrer(this._getReferrer());
if (this.options.saveParamsReferrerOncePerSession) {
this._trackParamsAndReferrer();
}
}

if (!this.options.saveParamsReferrerOncePerSession) {
this._trackParamsAndReferrer();
}

this._lastEventTime = now;
_saveCookieData(this);

Expand Down Expand Up @@ -613,6 +615,21 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o
}
};

/**
* @private
*/
AmplitudeClient.prototype._trackParamsAndReferrer = function _trackParamsAndReferrer() {
if (this.options.includeUtm) {
this._initUtmData();
}
if (this.options.includeReferrer) {
this._saveReferrer(this._getReferrer());
}
if (this.options.includeGclid) {
this._saveGclid(this._getUrlParams());
}
};

/**
* Parse and validate user specified config values and overwrite existing option value
* DEFAULT_OPTIONS provides list of all config keys that are modifiable, as well as expected types for values
Expand Down Expand Up @@ -913,17 +930,18 @@ var _saveCookieData = function _saveCookieData(scope) {
* @private
*/
AmplitudeClient.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) {
queryParams = queryParams || location.search;
queryParams = queryParams || this._getUrlParams();
cookieParams = cookieParams || this.cookieStorage.get('__utmz');
var utmProperties = getUtmData(cookieParams, queryParams);
_sendUserPropertiesOncePerSession(this, Constants.UTM_PROPERTIES, utmProperties);
_sendParamsReferrerUserProperties(this, utmProperties);
};

/**
* Since user properties are propagated on server, only send once per session, don't need to send with every event
* The calling function should determine when it is appropriate to send these user properties. This function
* will no longer contain any session storage checking logic.
* @private
*/
var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSession(scope, storageKey, userProperties) {
var _sendParamsReferrerUserProperties = function _sendParamsReferrerUserProperties(scope, userProperties) {
if (type(userProperties) !== 'object' || Object.keys(userProperties).length === 0) {
return;
}
Expand All @@ -933,20 +951,7 @@ var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSessi
for (var key in userProperties) {
if (userProperties.hasOwnProperty(key)) {
identify.setOnce('initial_' + key, userProperties[key]);
}
}

// only save userProperties if not already in sessionStorage under key or if storage disabled
var hasSessionStorage = utils.sessionStorageEnabled();
if ((hasSessionStorage && !(scope._getFromStorage(sessionStorage, storageKey))) || !hasSessionStorage) {
for (var property in userProperties) {
if (userProperties.hasOwnProperty(property)) {
identify.set(property, userProperties[property]);
}
}

if (hasSessionStorage) {
scope._setInStorage(sessionStorage, storageKey, JSON.stringify(userProperties));
identify.set(key, userProperties[key]);
}
}

Expand All @@ -960,6 +965,26 @@ AmplitudeClient.prototype._getReferrer = function _getReferrer() {
return document.referrer;
};

/**
* @private
*/
AmplitudeClient.prototype._getUrlParams = function _getUrlParams() {
return location.search;
};

/**
* Try to fetch Google Gclid from url params.
* @private
*/
AmplitudeClient.prototype._saveGclid = function _saveGclid(urlParams) {
var gclid = utils.getQueryParam('gclid', urlParams);
if (utils.isEmptyString(gclid)) {
return;
}
var gclidProperties = {'gclid': gclid};
_sendParamsReferrerUserProperties(this, gclidProperties);
};

/**
* Parse the domain from referrer info
* @private
Expand Down Expand Up @@ -988,7 +1013,7 @@ AmplitudeClient.prototype._saveReferrer = function _saveReferrer(referrer) {
'referrer': referrer,
'referring_domain': this._getReferringDomain(referrer)
};
_sendUserPropertiesOncePerSession(this, Constants.REFERRER, referrerInfo);
_sendParamsReferrerUserProperties(this, referrerInfo);
};

/**
Expand Down Expand Up @@ -1632,9 +1657,7 @@ module.exports = {
LAST_EVENT_TIME: 'amplitude_lastEventTime',
LAST_IDENTIFY_ID: 'amplitude_lastIdentifyId',
LAST_SEQUENCE_NUMBER: 'amplitude_lastSequenceNumber',
REFERRER: 'amplitude_referrer',
SESSION_ID: 'amplitude_sessionId',
UTM_PROPERTIES: 'amplitude_utm_properties',

// Used in cookie as well
DEVICE_ID: 'amplitude_deviceId',
Expand Down Expand Up @@ -2864,9 +2887,18 @@ var validateGroupName = function validateGroupName(key, groupName) {
'. Please use strings or array of strings for groupName');
};

// parses the value of a url param (for example ?gclid=1234&...)
var getQueryParam = function getQueryParam(name, query) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
var results = regex.exec(query);
return results === null ? undefined : decodeURIComponent(results[1].replace(/\+/g, " "));
};

module.exports = {
log: log,
isEmptyString: isEmptyString,
getQueryParam: getQueryParam,
sessionStorageEnabled: sessionStorageEnabled,
truncate: truncate,
validateGroups: validateGroups,
Expand Down Expand Up @@ -3029,20 +3061,13 @@ module.exports = localStorage;
13: [function(require, module, exports) {
var utils = require('./utils');

var getUtmParam = function getUtmParam(name, query) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
var results = regex.exec(query);
return results === null ? undefined : decodeURIComponent(results[1].replace(/\+/g, " "));
};

var getUtmData = function getUtmData(rawCookie, query) {
// Translate the utmz cookie format into url query string format.
var cookie = rawCookie ? '?' + rawCookie.split('.').slice(-1)[0].replace(/\|/g, '&') : '';

var fetchParam = function fetchParam(queryName, query, cookieName, cookie) {
return getUtmParam(queryName, query) ||
getUtmParam(cookieName, cookie);
return utils.getQueryParam(queryName, query) ||
utils.getQueryParam(cookieName, cookie);
};

var utmSource = fetchParam('utm_source', query, 'utmcsr', cookie);
Expand Down Expand Up @@ -4950,6 +4975,8 @@ module.exports = {
eventUploadThreshold: 30,
eventUploadPeriodMillis: 30 * 1000, // 30s
forceHttps: false,
includeGclid: false,
saveParamsReferrerOncePerSession: true
};

}, {"./language":29}],
Expand Down
6 changes: 3 additions & 3 deletions amplitude.min.js

Large diffs are not rendered by default.

73 changes: 49 additions & 24 deletions src/amplitude-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,15 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o
this._sessionId = now;

// only capture UTM params and referrer if new session
if (this.options.includeUtm) {
this._initUtmData();
}
if (this.options.includeReferrer) {
this._saveReferrer(this._getReferrer());
if (this.options.saveParamsReferrerOncePerSession) {
this._trackParamsAndReferrer();
}
}

if (!this.options.saveParamsReferrerOncePerSession) {
this._trackParamsAndReferrer();
}

this._lastEventTime = now;
_saveCookieData(this);

Expand Down Expand Up @@ -132,6 +134,21 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o
}
};

/**
* @private
*/
AmplitudeClient.prototype._trackParamsAndReferrer = function _trackParamsAndReferrer() {
if (this.options.includeUtm) {
this._initUtmData();
}
if (this.options.includeReferrer) {
this._saveReferrer(this._getReferrer());
}
if (this.options.includeGclid) {
this._saveGclid(this._getUrlParams());
}
};

/**
* Parse and validate user specified config values and overwrite existing option value
* DEFAULT_OPTIONS provides list of all config keys that are modifiable, as well as expected types for values
Expand Down Expand Up @@ -432,17 +449,18 @@ var _saveCookieData = function _saveCookieData(scope) {
* @private
*/
AmplitudeClient.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) {
queryParams = queryParams || location.search;
queryParams = queryParams || this._getUrlParams();
cookieParams = cookieParams || this.cookieStorage.get('__utmz');
var utmProperties = getUtmData(cookieParams, queryParams);
_sendUserPropertiesOncePerSession(this, Constants.UTM_PROPERTIES, utmProperties);
_sendParamsReferrerUserProperties(this, utmProperties);
};

/**
* Since user properties are propagated on server, only send once per session, don't need to send with every event
* The calling function should determine when it is appropriate to send these user properties. This function
* will no longer contain any session storage checking logic.
* @private
*/
var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSession(scope, storageKey, userProperties) {
var _sendParamsReferrerUserProperties = function _sendParamsReferrerUserProperties(scope, userProperties) {
if (type(userProperties) !== 'object' || Object.keys(userProperties).length === 0) {
return;
}
Expand All @@ -452,20 +470,7 @@ var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSessi
for (var key in userProperties) {
if (userProperties.hasOwnProperty(key)) {
identify.setOnce('initial_' + key, userProperties[key]);
}
}

// only save userProperties if not already in sessionStorage under key or if storage disabled
var hasSessionStorage = utils.sessionStorageEnabled();
if ((hasSessionStorage && !(scope._getFromStorage(sessionStorage, storageKey))) || !hasSessionStorage) {
for (var property in userProperties) {
if (userProperties.hasOwnProperty(property)) {
identify.set(property, userProperties[property]);
}
}

if (hasSessionStorage) {
scope._setInStorage(sessionStorage, storageKey, JSON.stringify(userProperties));
identify.set(key, userProperties[key]);
}
}

Expand All @@ -479,6 +484,26 @@ AmplitudeClient.prototype._getReferrer = function _getReferrer() {
return document.referrer;
};

/**
* @private
*/
AmplitudeClient.prototype._getUrlParams = function _getUrlParams() {
return location.search;
};

/**
* Try to fetch Google Gclid from url params.
* @private
*/
AmplitudeClient.prototype._saveGclid = function _saveGclid(urlParams) {
var gclid = utils.getQueryParam('gclid', urlParams);
if (utils.isEmptyString(gclid)) {
return;
}
var gclidProperties = {'gclid': gclid};
_sendParamsReferrerUserProperties(this, gclidProperties);
};

/**
* Parse the domain from referrer info
* @private
Expand Down Expand Up @@ -507,7 +532,7 @@ AmplitudeClient.prototype._saveReferrer = function _saveReferrer(referrer) {
'referrer': referrer,
'referring_domain': this._getReferringDomain(referrer)
};
_sendUserPropertiesOncePerSession(this, Constants.REFERRER, referrerInfo);
_sendParamsReferrerUserProperties(this, referrerInfo);
};

/**
Expand Down
2 changes: 0 additions & 2 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ module.exports = {
LAST_EVENT_TIME: 'amplitude_lastEventTime',
LAST_IDENTIFY_ID: 'amplitude_lastIdentifyId',
LAST_SEQUENCE_NUMBER: 'amplitude_lastSequenceNumber',
REFERRER: 'amplitude_referrer',
SESSION_ID: 'amplitude_sessionId',
UTM_PROPERTIES: 'amplitude_utm_properties',

// Used in cookie as well
DEVICE_ID: 'amplitude_deviceId',
Expand Down
2 changes: 2 additions & 0 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ module.exports = {
eventUploadThreshold: 30,
eventUploadPeriodMillis: 30 * 1000, // 30s
forceHttps: false,
includeGclid: false,
saveParamsReferrerOncePerSession: true
};
9 changes: 9 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,18 @@ var validateGroupName = function validateGroupName(key, groupName) {
'. Please use strings or array of strings for groupName');
};

// parses the value of a url param (for example ?gclid=1234&...)
var getQueryParam = function getQueryParam(name, query) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this line doesn't get used anymore?

var regex = new RegExp("[\\?&]" + name + "=([^&#]*)");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems ok but I always get a bit nervous with regex parsing. Is it worth taking into account the possibility of repeated params?

var results = regex.exec(query);
return results === null ? undefined : decodeURIComponent(results[1].replace(/\+/g, " "));
};

module.exports = {
log: log,
isEmptyString: isEmptyString,
getQueryParam: getQueryParam,
sessionStorageEnabled: sessionStorageEnabled,
truncate: truncate,
validateGroups: validateGroups,
Expand Down
Loading