Skip to content

Commit c50429d

Browse files
ajhorstAJ Horst
and
AJ Horst
authored
feat: Add error callback (#413)
* Add error callback to log methods * add logic to call error callbacks on event log failures * add error callback to log identify methods * add tests * refactor dual callback calls * prefix private method with underscore Co-authored-by: AJ Horst <[email protected]>
1 parent 876a4b2 commit c50429d

File tree

2 files changed

+334
-88
lines changed

2 files changed

+334
-88
lines changed

src/amplitude-client.js

+155-68
Original file line numberDiff line numberDiff line change
@@ -1032,18 +1032,20 @@ var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(i
10321032
* @param {Identify} identify_obj - the Identify object containing the user property operations to send.
10331033
* @param {Amplitude~eventCallback} opt_callback - (optional) callback function to run when the identify event has been sent.
10341034
* Note: the server response code and response body from the identify event upload are passed to the callback function.
1035+
* @param {Amplitude~eventCallback} opt_error_callback - (optional) a callback function to run after the event logging
1036+
* fails. The failure can be from the request being malformed or from a network failure
1037+
* Note: the server response code and response body from the event upload are passed to the callback function.
10351038
* @example
10361039
* var identify = new amplitude.Identify().set('colors', ['rose', 'gold']).add('karma', 1).setOnce('sign_up_date', '2016-03-31');
10371040
* amplitude.identify(identify);
10381041
*/
1039-
AmplitudeClient.prototype.identify = function (identify_obj, opt_callback) {
1042+
AmplitudeClient.prototype.identify = function (identify_obj, opt_callback, opt_error_callback) {
10401043
if (this._shouldDeferCall()) {
10411044
return this._q.push(['identify'].concat(Array.prototype.slice.call(arguments, 0)));
10421045
}
10431046
if (!this._apiKeySet('identify()')) {
1044-
if (type(opt_callback) === 'function') {
1045-
opt_callback(0, 'No request sent', { reason: 'API key is not set' });
1046-
}
1047+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', { reason: 'API key is not set' });
1048+
10471049
return;
10481050
}
10491051

@@ -1064,42 +1066,49 @@ AmplitudeClient.prototype.identify = function (identify_obj, opt_callback) {
10641066
null,
10651067
null,
10661068
opt_callback,
1069+
opt_error_callback,
10671070
);
10681071
} else {
1069-
if (type(opt_callback) === 'function') {
1070-
opt_callback(0, 'No request sent', { reason: 'No user property operations' });
1071-
}
1072+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
1073+
reason: 'No user property operations',
1074+
});
10721075
}
10731076
} else {
10741077
utils.log.error('Invalid identify input type. Expected Identify object but saw ' + type(identify_obj));
1075-
if (type(opt_callback) === 'function') {
1076-
opt_callback(0, 'No request sent', { reason: 'Invalid identify input type' });
1077-
}
1078+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
1079+
reason: 'Invalid identify input type',
1080+
});
10781081
}
10791082
};
10801083

1081-
AmplitudeClient.prototype.groupIdentify = function (group_type, group_name, identify_obj, opt_callback) {
1084+
AmplitudeClient.prototype.groupIdentify = function (
1085+
group_type,
1086+
group_name,
1087+
identify_obj,
1088+
opt_callback,
1089+
opt_error_callback,
1090+
) {
10821091
if (this._shouldDeferCall()) {
10831092
return this._q.push(['groupIdentify'].concat(Array.prototype.slice.call(arguments, 0)));
10841093
}
10851094
if (!this._apiKeySet('groupIdentify()')) {
1086-
if (type(opt_callback) === 'function') {
1087-
opt_callback(0, 'No request sent', { reason: 'API key is not set' });
1088-
}
1095+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
1096+
reason: 'API key is not set',
1097+
});
10891098
return;
10901099
}
10911100

10921101
if (!utils.validateInput(group_type, 'group_type', 'string') || utils.isEmptyString(group_type)) {
1093-
if (type(opt_callback) === 'function') {
1094-
opt_callback(0, 'No request sent', { reason: 'Invalid group type' });
1095-
}
1102+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
1103+
reason: 'Invalid group type',
1104+
});
10961105
return;
10971106
}
10981107

10991108
if (group_name === null || group_name === undefined) {
1100-
if (type(opt_callback) === 'function') {
1101-
opt_callback(0, 'No request sent', { reason: 'Invalid group name' });
1102-
}
1109+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
1110+
reason: 'Invalid group name',
1111+
});
11031112
return;
11041113
}
11051114

@@ -1120,17 +1129,18 @@ AmplitudeClient.prototype.groupIdentify = function (group_type, group_name, iden
11201129
identify_obj.userPropertiesOperations,
11211130
null,
11221131
opt_callback,
1132+
opt_error_callback,
11231133
);
11241134
} else {
1125-
if (type(opt_callback) === 'function') {
1126-
opt_callback(0, 'No request sent', { reason: 'No group property operations' });
1127-
}
1135+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
1136+
reason: 'No group property operations',
1137+
});
11281138
}
11291139
} else {
11301140
utils.log.error('Invalid identify input type. Expected Identify object but saw ' + type(identify_obj));
1131-
if (type(opt_callback) === 'function') {
1132-
opt_callback(0, 'No request sent', { reason: 'Invalid identify input type' });
1133-
}
1141+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
1142+
reason: 'Invalid identify input type',
1143+
});
11341144
}
11351145
};
11361146

@@ -1164,19 +1174,20 @@ AmplitudeClient.prototype._logEvent = function _logEvent(
11641174
groupProperties,
11651175
timestamp,
11661176
callback,
1177+
errorCallback,
11671178
) {
11681179
_loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs
11691180

11701181
if (!eventType) {
1171-
if (type(callback) === 'function') {
1172-
callback(0, 'No request sent', { reason: 'Missing eventType' });
1173-
}
1182+
_logErrorsWithCallbacks(callback, errorCallback, 0, 'No request sent', {
1183+
reason: 'Missing eventType',
1184+
});
11741185
return;
11751186
}
11761187
if (this.options.optOut) {
1177-
if (type(callback) === 'function') {
1178-
callback(0, 'No request sent', { reason: 'optOut is set to true' });
1179-
}
1188+
_logErrorsWithCallbacks(callback, errorCallback, 0, 'No request sent', {
1189+
reason: 'optOut is set to true',
1190+
});
11801191
return;
11811192
}
11821193

@@ -1233,10 +1244,10 @@ AmplitudeClient.prototype._logEvent = function _logEvent(
12331244
};
12341245

12351246
if (eventType === Constants.IDENTIFY_EVENT || eventType === Constants.GROUP_IDENTIFY_EVENT) {
1236-
this._unsentIdentifys.push({ event, callback });
1247+
this._unsentIdentifys.push({ event, callback, errorCallback });
12371248
this._limitEventsQueued(this._unsentIdentifys);
12381249
} else {
1239-
this._unsentEvents.push({ event, callback });
1250+
this._unsentEvents.push({ event, callback, errorCallback });
12401251
this._limitEventsQueued(this._unsentEvents);
12411252
}
12421253

@@ -1277,11 +1288,9 @@ AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue
12771288
if (queue.length > this.options.savedMaxCount) {
12781289
const deletedEvents = queue.splice(0, queue.length - this.options.savedMaxCount);
12791290
deletedEvents.forEach((event) => {
1280-
if (type(event.callback) === 'function') {
1281-
event.callback(0, 'No request sent', {
1282-
reason: 'Event dropped because options.savedMaxCount exceeded. User may be offline or have a content blocker',
1283-
});
1284-
}
1291+
_logErrorsWithCallbacks(event.callback, event.errorCallback, 0, 'No request sent', {
1292+
reason: 'Event dropped because options.savedMaxCount exceeded. User may be offline or have a content blocker',
1293+
});
12851294
});
12861295
}
12871296
};
@@ -1302,13 +1311,16 @@ AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue
13021311
* @param {object} eventProperties - (optional) an object with string keys and values for the event properties.
13031312
* @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged.
13041313
* Note: the server response code and response body from the event upload are passed to the callback function.
1314+
* @param {Amplitude~eventCallback} opt_error_callback - (optional) a callback function to run after the event logging
1315+
* fails. The failure can be from the request being malformed or from a network failure
1316+
* Note: the server response code and response body from the event upload are passed to the callback function.
13051317
* @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
13061318
*/
1307-
AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) {
1319+
AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback, opt_error_callback) {
13081320
if (this._shouldDeferCall()) {
13091321
return this._q.push(['logEvent'].concat(Array.prototype.slice.call(arguments, 0)));
13101322
}
1311-
return this.logEventWithTimestamp(eventType, eventProperties, null, opt_callback);
1323+
return this.logEventWithTimestamp(eventType, eventProperties, null, opt_callback, opt_error_callback);
13121324
};
13131325

13141326
/**
@@ -1319,36 +1331,52 @@ AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventPropertie
13191331
* @param {number} timestamp - (optional) the custom timestamp as milliseconds since epoch.
13201332
* @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged.
13211333
* Note: the server response code and response body from the event upload are passed to the callback function.
1334+
* @param {Amplitude~eventCallback} opt_error_callback - (optional) a callback function to run after the event logging
1335+
* fails. The failure can be from the request being malformed or from a network failure
1336+
* Note: the server response code and response body from the event upload are passed to the callback function.
13221337
* @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
13231338
*/
13241339
AmplitudeClient.prototype.logEventWithTimestamp = function logEvent(
13251340
eventType,
13261341
eventProperties,
13271342
timestamp,
13281343
opt_callback,
1344+
opt_error_callback,
13291345
) {
13301346
if (this._shouldDeferCall()) {
13311347
return this._q.push(['logEventWithTimestamp'].concat(Array.prototype.slice.call(arguments, 0)));
13321348
}
13331349
if (!this._apiKeySet('logEvent()')) {
1334-
if (type(opt_callback) === 'function') {
1335-
opt_callback(0, 'No request sent', { reason: 'API key not set' });
1336-
}
1350+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
1351+
reason: 'API key not set',
1352+
});
1353+
13371354
return -1;
13381355
}
13391356
if (!utils.validateInput(eventType, 'eventType', 'string')) {
1340-
if (type(opt_callback) === 'function') {
1341-
opt_callback(0, 'No request sent', { reason: 'Invalid type for eventType' });
1342-
}
1357+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
1358+
reason: 'Invalid type for eventType',
1359+
});
1360+
13431361
return -1;
13441362
}
13451363
if (utils.isEmptyString(eventType)) {
1346-
if (type(opt_callback) === 'function') {
1347-
opt_callback(0, 'No request sent', { reason: 'Missing eventType' });
1348-
}
1364+
_logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', {
1365+
reason: 'Missing eventType',
1366+
});
13491367
return -1;
13501368
}
1351-
return this._logEvent(eventType, eventProperties, null, null, null, null, timestamp, opt_callback);
1369+
return this._logEvent(
1370+
eventType,
1371+
eventProperties,
1372+
null,
1373+
null,
1374+
null,
1375+
null,
1376+
timestamp,
1377+
opt_callback,
1378+
opt_error_callback,
1379+
);
13521380
};
13531381

13541382
/**
@@ -1365,25 +1393,35 @@ AmplitudeClient.prototype.logEventWithTimestamp = function logEvent(
13651393
* groupName can be a string or an array of strings.
13661394
* @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged.
13671395
* Note: the server response code and response body from the event upload are passed to the callback function.
1396+
* @param {Amplitude~eventCallback} opt_error_callback - (optional) a callback function to run after the event logging
1397+
* fails. The failure can be from the request being malformed or from a network failure
1398+
* Note: the server response code and response body from the event upload are passed to the callback function.
13681399
* @example amplitudeClient.logEventWithGroups('Clicked Button', null, {'orgId': 24});
13691400
*/
1370-
AmplitudeClient.prototype.logEventWithGroups = function (eventType, eventProperties, groups, opt_callback) {
1401+
AmplitudeClient.prototype.logEventWithGroups = function (
1402+
eventType,
1403+
eventProperties,
1404+
groups,
1405+
opt_callback,
1406+
opt_error_callback,
1407+
) {
13711408
if (this._shouldDeferCall()) {
13721409
return this._q.push(['logEventWithGroups'].concat(Array.prototype.slice.call(arguments, 0)));
13731410
}
13741411
if (!this._apiKeySet('logEventWithGroups()')) {
1375-
if (type(opt_callback) === 'function') {
1376-
opt_callback(0, 'No request sent', { reason: 'API key not set' });
1377-
}
1412+
_logErrorsWithCallbacks(event.callback, event.errorCallback, 0, 'No request sent', {
1413+
reason: 'API key not set',
1414+
});
1415+
13781416
return -1;
13791417
}
13801418
if (!utils.validateInput(eventType, 'eventType', 'string')) {
1381-
if (type(opt_callback) === 'function') {
1382-
opt_callback(0, 'No request sent', { reason: 'Invalid type for eventType' });
1383-
}
1419+
_logErrorsWithCallbacks(event.callback, event.errorCallback, 0, 'No request sent', {
1420+
reason: 'Invalid type for eventType',
1421+
});
13841422
return -1;
13851423
}
1386-
return this._logEvent(eventType, eventProperties, null, null, groups, null, null, opt_callback);
1424+
return this._logEvent(eventType, eventProperties, null, null, groups, null, null, opt_callback, opt_error_callback);
13871425
};
13881426

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

1435+
/**
1436+
* Handles errors that are sent to both callbacks
1437+
* @private
1438+
*/
1439+
var _logErrorsWithCallbacks = function _logErrorsWithCallbacks(
1440+
opt_callback,
1441+
opt_error_callback,
1442+
status,
1443+
response,
1444+
details,
1445+
) {
1446+
if (type(opt_callback) === 'function') {
1447+
opt_callback(status, response, details);
1448+
}
1449+
if (type(opt_error_callback) === 'function') {
1450+
opt_error_callback(status, response, details);
1451+
}
1452+
};
1453+
13971454
/**
13981455
* Log revenue with Revenue interface. The new revenue interface allows for more revenue fields like
13991456
* revenueType and event properties.
@@ -1468,6 +1525,33 @@ if (BUILD_COMPAT_2_0) {
14681525
};
14691526
}
14701527

1528+
/**
1529+
* Calls error callback on unsent events
1530+
* @private
1531+
*/
1532+
AmplitudeClient.prototype._logErrorsOnEvents = function _logErrorsOnEvents(
1533+
maxEventId,
1534+
maxIdentifyId,
1535+
status,
1536+
response,
1537+
) {
1538+
const queues = ['_unsentEvents', '_unsentIdentifys'];
1539+
1540+
for (var j = 0; j < queues.length; j++) {
1541+
const queue = queues[j];
1542+
const maxId = queue === '_unsentEvents' ? maxEventId : maxIdentifyId;
1543+
for (var i = 0; i < this[queue].length || 0; i++) {
1544+
const unsentEvent = this[queue][i];
1545+
1546+
if (unsentEvent.event.event_id <= maxId) {
1547+
if (unsentEvent.errorCallback) {
1548+
unsentEvent.errorCallback(status, response);
1549+
}
1550+
}
1551+
}
1552+
}
1553+
};
1554+
14711555
/**
14721556
* Remove events in storage with event ids up to and including maxEventId.
14731557
* @private
@@ -1565,16 +1649,19 @@ AmplitudeClient.prototype.sendEvents = function sendEvents() {
15651649
scope._sendEventsIfReady();
15661650

15671651
// handle payload too large
1568-
} else if (status === 413) {
1569-
// utils.log('request too large');
1570-
// Can't even get this one massive event through. Drop it, even if it is an identify.
1571-
if (scope.options.uploadBatchSize === 1) {
1572-
scope.removeEvents(maxEventId, maxIdentifyId, status, response);
1652+
} else {
1653+
scope._logErrorsOnEvents(maxEventId, maxIdentifyId, status, response);
1654+
if (status === 413) {
1655+
// utils.log('request too large');
1656+
// Can't even get this one massive event through. Drop it, even if it is an identify.
1657+
if (scope.options.uploadBatchSize === 1) {
1658+
scope.removeEvents(maxEventId, maxIdentifyId, status, response);
1659+
}
1660+
1661+
// The server complained about the length of the request. Backoff and try again.
1662+
scope.options.uploadBatchSize = Math.ceil(numEvents / 2);
1663+
scope.sendEvents();
15731664
}
1574-
1575-
// The server complained about the length of the request. Backoff and try again.
1576-
scope.options.uploadBatchSize = Math.ceil(numEvents / 2);
1577-
scope.sendEvents();
15781665
}
15791666
// else {
15801667
// all the events are still queued, and will be retried when the next

0 commit comments

Comments
 (0)