Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add error callback #413

Merged
merged 6 commits into from
Jul 30, 2021
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
223 changes: 155 additions & 68 deletions src/amplitude-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1032,18 +1032,20 @@ var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(i
* @param {Identify} identify_obj - the Identify object containing the user property operations to send.
* @param {Amplitude~eventCallback} opt_callback - (optional) callback function to run when the identify event has been sent.
* Note: the server response code and response body from the identify event upload are passed to the callback function.
* @param {Amplitude~eventCallback} opt_error_callback - (optional) a callback function to run after the event logging
* fails. The failure can be from the request being malformed or from a network failure
* Note: the server response code and response body from the event upload are passed to the callback function.
* @example
* var identify = new amplitude.Identify().set('colors', ['rose', 'gold']).add('karma', 1).setOnce('sign_up_date', '2016-03-31');
* amplitude.identify(identify);
*/
AmplitudeClient.prototype.identify = function (identify_obj, opt_callback) {
AmplitudeClient.prototype.identify = function (identify_obj, opt_callback, opt_error_callback) {
if (this._shouldDeferCall()) {
return this._q.push(['identify'].concat(Array.prototype.slice.call(arguments, 0)));
}
if (!this._apiKeySet('identify()')) {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'API key is not set' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', { reason: 'API key is not set' });

return;
}

Expand All @@ -1064,42 +1066,49 @@ AmplitudeClient.prototype.identify = function (identify_obj, opt_callback) {
null,
null,
opt_callback,
opt_error_callback,
);
} else {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'No user property operations' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
reason: 'No user property operations',
});
}
} else {
utils.log.error('Invalid identify input type. Expected Identify object but saw ' + type(identify_obj));
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'Invalid identify input type' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
reason: 'Invalid identify input type',
});
}
};

AmplitudeClient.prototype.groupIdentify = function (group_type, group_name, identify_obj, opt_callback) {
AmplitudeClient.prototype.groupIdentify = function (
group_type,
group_name,
identify_obj,
opt_callback,
opt_error_callback,
) {
if (this._shouldDeferCall()) {
return this._q.push(['groupIdentify'].concat(Array.prototype.slice.call(arguments, 0)));
}
if (!this._apiKeySet('groupIdentify()')) {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'API key is not set' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
reason: 'API key is not set',
});
return;
}

if (!utils.validateInput(group_type, 'group_type', 'string') || utils.isEmptyString(group_type)) {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'Invalid group type' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
reason: 'Invalid group type',
});
return;
}

if (group_name === null || group_name === undefined) {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'Invalid group name' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
reason: 'Invalid group name',
});
return;
}

Expand All @@ -1120,17 +1129,18 @@ AmplitudeClient.prototype.groupIdentify = function (group_type, group_name, iden
identify_obj.userPropertiesOperations,
null,
opt_callback,
opt_error_callback,
);
} else {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'No group property operations' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
reason: 'No group property operations',
});
}
} else {
utils.log.error('Invalid identify input type. Expected Identify object but saw ' + type(identify_obj));
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'Invalid identify input type' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
reason: 'Invalid identify input type',
});
}
};

Expand Down Expand Up @@ -1164,19 +1174,20 @@ AmplitudeClient.prototype._logEvent = function _logEvent(
groupProperties,
timestamp,
callback,
errorCallback,
) {
_loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs

if (!eventType) {
if (type(callback) === 'function') {
callback(0, 'No request sent', { reason: 'Missing eventType' });
}
_logErrorsWithCallbacks(callback, errorCallback, 0, 'No request sent', {
reason: 'Missing eventType',
});
return;
}
if (this.options.optOut) {
if (type(callback) === 'function') {
callback(0, 'No request sent', { reason: 'optOut is set to true' });
}
_logErrorsWithCallbacks(callback, errorCallback, 0, 'No request sent', {
reason: 'optOut is set to true',
});
return;
}

Expand Down Expand Up @@ -1233,10 +1244,10 @@ AmplitudeClient.prototype._logEvent = function _logEvent(
};

if (eventType === Constants.IDENTIFY_EVENT || eventType === Constants.GROUP_IDENTIFY_EVENT) {
this._unsentIdentifys.push({ event, callback });
this._unsentIdentifys.push({ event, callback, errorCallback });
this._limitEventsQueued(this._unsentIdentifys);
} else {
this._unsentEvents.push({ event, callback });
this._unsentEvents.push({ event, callback, errorCallback });
this._limitEventsQueued(this._unsentEvents);
}

Expand Down Expand Up @@ -1277,11 +1288,9 @@ AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue
if (queue.length > this.options.savedMaxCount) {
const deletedEvents = queue.splice(0, queue.length - this.options.savedMaxCount);
deletedEvents.forEach((event) => {
if (type(event.callback) === 'function') {
event.callback(0, 'No request sent', {
reason: 'Event dropped because options.savedMaxCount exceeded. User may be offline or have a content blocker',
});
}
_logErrorsWithCallbacks(event.callback, event.errorCallback, 0, 'No request sent', {
reason: 'Event dropped because options.savedMaxCount exceeded. User may be offline or have a content blocker',
});
});
}
};
Expand All @@ -1302,13 +1311,16 @@ AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue
* @param {object} eventProperties - (optional) an object with string keys and values for the event properties.
* @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged.
* Note: the server response code and response body from the event upload are passed to the callback function.
* @param {Amplitude~eventCallback} opt_error_callback - (optional) a callback function to run after the event logging
* fails. The failure can be from the request being malformed or from a network failure
* Note: the server response code and response body from the event upload are passed to the callback function.
* @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
*/
AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) {
AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback, opt_error_callback) {
if (this._shouldDeferCall()) {
return this._q.push(['logEvent'].concat(Array.prototype.slice.call(arguments, 0)));
}
return this.logEventWithTimestamp(eventType, eventProperties, null, opt_callback);
return this.logEventWithTimestamp(eventType, eventProperties, null, opt_callback, opt_error_callback);
};

/**
Expand All @@ -1319,36 +1331,52 @@ AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventPropertie
* @param {number} timestamp - (optional) the custom timestamp as milliseconds since epoch.
* @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged.
* Note: the server response code and response body from the event upload are passed to the callback function.
* @param {Amplitude~eventCallback} opt_error_callback - (optional) a callback function to run after the event logging
* fails. The failure can be from the request being malformed or from a network failure
* Note: the server response code and response body from the event upload are passed to the callback function.
* @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
*/
AmplitudeClient.prototype.logEventWithTimestamp = function logEvent(
eventType,
eventProperties,
timestamp,
opt_callback,
opt_error_callback,
) {
if (this._shouldDeferCall()) {
return this._q.push(['logEventWithTimestamp'].concat(Array.prototype.slice.call(arguments, 0)));
}
if (!this._apiKeySet('logEvent()')) {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'API key not set' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
reason: 'API key not set',
});

return -1;
}
if (!utils.validateInput(eventType, 'eventType', 'string')) {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'Invalid type for eventType' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
reason: 'Invalid type for eventType',
});

return -1;
}
if (utils.isEmptyString(eventType)) {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'Missing eventType' });
}
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
reason: 'Missing eventType',
});
return -1;
}
return this._logEvent(eventType, eventProperties, null, null, null, null, timestamp, opt_callback);
return this._logEvent(
eventType,
eventProperties,
null,
null,
null,
null,
timestamp,
opt_callback,
opt_error_callback,
);
};

/**
Expand All @@ -1365,25 +1393,35 @@ AmplitudeClient.prototype.logEventWithTimestamp = function logEvent(
* groupName can be a string or an array of strings.
* @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged.
* Note: the server response code and response body from the event upload are passed to the callback function.
* @param {Amplitude~eventCallback} opt_error_callback - (optional) a callback function to run after the event logging
* fails. The failure can be from the request being malformed or from a network failure
* Note: the server response code and response body from the event upload are passed to the callback function.
* @example amplitudeClient.logEventWithGroups('Clicked Button', null, {'orgId': 24});
*/
AmplitudeClient.prototype.logEventWithGroups = function (eventType, eventProperties, groups, opt_callback) {
AmplitudeClient.prototype.logEventWithGroups = function (
eventType,
eventProperties,
groups,
opt_callback,
opt_error_callback,
) {
if (this._shouldDeferCall()) {
return this._q.push(['logEventWithGroups'].concat(Array.prototype.slice.call(arguments, 0)));
}
if (!this._apiKeySet('logEventWithGroups()')) {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'API key not set' });
}
_logErrorsWithCallbacks(event.callback, event.errorCallback, 0, 'No request sent', {
reason: 'API key not set',
});

return -1;
}
if (!utils.validateInput(eventType, 'eventType', 'string')) {
if (type(opt_callback) === 'function') {
opt_callback(0, 'No request sent', { reason: 'Invalid type for eventType' });
}
_logErrorsWithCallbacks(event.callback, event.errorCallback, 0, 'No request sent', {
reason: 'Invalid type for eventType',
});
return -1;
}
return this._logEvent(eventType, eventProperties, null, null, groups, null, null, opt_callback);
return this._logEvent(eventType, eventProperties, null, null, groups, null, null, opt_callback, opt_error_callback);
};

/**
Expand All @@ -1394,6 +1432,25 @@ var _isNumber = function _isNumber(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};

/**
* Handles errors that are sent to both callbacks
* @private
*/
var _logErrorsWithCallbacks = function _logErrorsWithCallbacks(
opt_callback,
opt_error_callback,
status,
response,
details,
) {
if (type(opt_callback) === 'function') {
opt_callback(status, response, details);
}
if (type(opt_error_callback) === 'function') {
opt_error_callback(status, response, details);
}
};

/**
* Log revenue with Revenue interface. The new revenue interface allows for more revenue fields like
* revenueType and event properties.
Expand Down Expand Up @@ -1468,6 +1525,33 @@ if (BUILD_COMPAT_2_0) {
};
}

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: since logErrorsOnEvents is private, should it be _logErrorsOnEvents for convention?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point, lemme fix that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jooohhn done

* Calls error callback on unsent events
* @private
*/
AmplitudeClient.prototype._logErrorsOnEvents = function _logErrorsOnEvents(
maxEventId,
maxIdentifyId,
status,
response,
) {
const queues = ['_unsentEvents', '_unsentIdentifys'];

for (var j = 0; j < queues.length; j++) {
const queue = queues[j];
const maxId = queue === '_unsentEvents' ? maxEventId : maxIdentifyId;
for (var i = 0; i < this[queue].length || 0; i++) {
const unsentEvent = this[queue][i];

if (unsentEvent.event.event_id <= maxId) {
if (unsentEvent.errorCallback) {
unsentEvent.errorCallback(status, response);
}
}
}
}
};

/**
* Remove events in storage with event ids up to and including maxEventId.
* @private
Expand Down Expand Up @@ -1565,16 +1649,19 @@ AmplitudeClient.prototype.sendEvents = function sendEvents() {
scope._sendEventsIfReady();

// handle payload too large
} else if (status === 413) {
// utils.log('request too large');
// Can't even get this one massive event through. Drop it, even if it is an identify.
if (scope.options.uploadBatchSize === 1) {
scope.removeEvents(maxEventId, maxIdentifyId, status, response);
} else {
scope._logErrorsOnEvents(maxEventId, maxIdentifyId, status, response);
if (status === 413) {
// utils.log('request too large');
// Can't even get this one massive event through. Drop it, even if it is an identify.
if (scope.options.uploadBatchSize === 1) {
scope.removeEvents(maxEventId, maxIdentifyId, status, response);
}

// The server complained about the length of the request. Backoff and try again.
scope.options.uploadBatchSize = Math.ceil(numEvents / 2);
scope.sendEvents();
}

// The server complained about the length of the request. Backoff and try again.
scope.options.uploadBatchSize = Math.ceil(numEvents / 2);
scope.sendEvents();
}
// else {
// all the events are still queued, and will be retried when the next
Expand Down
Loading