Skip to content

Commit c66ec58

Browse files
alexs-mparticlermi22186
authored andcommitted
refactor: Update Error handling for Identity API Client (#959)
1 parent ec84fc4 commit c66ec58

File tree

5 files changed

+585
-90
lines changed

5 files changed

+585
-90
lines changed

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,7 @@ export const MILLIS_IN_ONE_SEC = 1000;
201201
export const HTTP_OK = 200 as const;
202202
export const HTTP_ACCEPTED = 202 as const;
203203
export const HTTP_BAD_REQUEST = 400 as const;
204+
export const HTTP_UNAUTHORIZED = 401 as const;
204205
export const HTTP_FORBIDDEN = 403 as const;
206+
export const HTTP_NOT_FOUND = 404 as const;
207+
export const HTTP_SERVER_ERROR = 500 as const;

src/identityApiClient.ts

Lines changed: 123 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import Constants, { HTTP_ACCEPTED, HTTP_OK } from './constants';
1+
import Constants, { HTTP_ACCEPTED, HTTP_BAD_REQUEST, HTTP_OK } from './constants';
22
import {
33
AsyncUploader,
44
FetchUploader,
55
XHRUploader,
66
IFetchPayload,
77
} from './uploaders';
88
import { CACHE_HEADER } from './identity-utils';
9-
import { parseNumber } from './utils';
9+
import { parseNumber, valueof } from './utils';
1010
import {
1111
IAliasCallback,
1212
IAliasRequest,
@@ -15,7 +15,6 @@ import {
1515
IIdentityAPIRequestData,
1616
} from './identity.interfaces';
1717
import {
18-
Callback,
1918
IdentityApiData,
2019
MPID,
2120
UserIdentities,
@@ -53,9 +52,8 @@ export interface IIdentityApiClient {
5352
getIdentityResponseFromXHR: (response: XMLHttpRequest) => IIdentityResponse;
5453
}
5554

56-
export interface IAliasResponseBody {
57-
message?: string;
58-
}
55+
// A successfull Alias request will return a 202 with no body
56+
export interface IAliasResponseBody {}
5957

6058
interface IdentityApiRequestPayload extends IFetchPayload {
6159
headers: {
@@ -65,6 +63,23 @@ interface IdentityApiRequestPayload extends IFetchPayload {
6563
};
6664
}
6765

66+
type HTTP_STATUS_CODES = typeof HTTP_OK | typeof HTTP_ACCEPTED;
67+
68+
interface IdentityApiError {
69+
code: string;
70+
message: string;
71+
}
72+
73+
interface IdentityApiErrorResponse {
74+
Errors: IdentityApiError[],
75+
ErrorCode: string,
76+
StatusCode: valueof<HTTP_STATUS_CODES>;
77+
RequestId: string;
78+
}
79+
80+
// All Identity Api Responses have the same structure, except for Alias
81+
interface IAliasErrorResponse extends IdentityApiError {}
82+
6883
export default function IdentityAPIClient(
6984
this: IIdentityApiClient,
7085
mpInstance: MParticleWebSDK
@@ -99,55 +114,72 @@ export default function IdentityAPIClient(
99114
try {
100115
const response: Response = await uploader.upload(uploadPayload);
101116

102-
let message: string;
103117
let aliasResponseBody: IAliasResponseBody;
104-
105-
// FetchUploader returns the response as a JSON object that we have to await
106-
if (response.json) {
107-
// HTTP responses of 202, 200, and 403 do not have a response. response.json will always exist on a fetch, but can only be await-ed when the response is not empty, otherwise it will throw an error.
108-
try {
109-
aliasResponseBody = await response.json();
110-
} catch (e) {
111-
verbose('The request has no response body');
112-
}
113-
} else {
114-
// https://go.mparticle.com/work/SQDSDKS-6568
115-
// XHRUploader returns the response as a string that we need to parse
116-
const xhrResponse = (response as unknown) as XMLHttpRequest;
117-
118-
aliasResponseBody = xhrResponse.responseText
119-
? JSON.parse(xhrResponse.responseText)
120-
: '';
121-
}
122-
118+
let message: string;
123119
let errorMessage: string;
124120

125121
switch (response.status) {
126-
case HTTP_OK:
122+
// A successfull Alias request will return without a body
127123
case HTTP_ACCEPTED:
124+
case HTTP_OK:
128125
// https://go.mparticle.com/work/SQDSDKS-6670
129-
message =
130-
'Successfully sent forwarding stats to mParticle Servers';
126+
message = 'Received Alias Response from server: ' + JSON.stringify(response.status);
131127
break;
132-
default:
133-
// 400 has an error message, but 403 doesn't
134-
if (aliasResponseBody?.message) {
135-
errorMessage = aliasResponseBody.message;
128+
129+
// Our Alias Request API will 400 if there is an issue with the request body (ie timestamps are too far
130+
// in the past or MPIDs don't exist).
131+
// A 400 will return an error in the response body and will go through the happy path to report the error
132+
case HTTP_BAD_REQUEST:
133+
// response.json will always exist on a fetch, but can only be await-ed when the
134+
// response is not empty, otherwise it will throw an error.
135+
if (response.json) {
136+
try {
137+
aliasResponseBody = await response.json();
138+
} catch (e) {
139+
verbose('The request has no response body');
140+
}
141+
} else {
142+
// https://go.mparticle.com/work/SQDSDKS-6568
143+
// XHRUploader returns the response as a string that we need to parse
144+
const xhrResponse = (response as unknown) as XMLHttpRequest;
145+
146+
aliasResponseBody = xhrResponse.responseText
147+
? JSON.parse(xhrResponse.responseText)
148+
: '';
136149
}
150+
151+
const errorResponse: IAliasErrorResponse = aliasResponseBody as unknown as IAliasErrorResponse;
152+
153+
if (errorResponse?.message) {
154+
errorMessage = errorResponse.message;
155+
}
156+
137157
message =
138158
'Issue with sending Alias Request to mParticle Servers, received HTTP Code of ' +
139159
response.status;
160+
161+
if (errorResponse?.code) {
162+
message += ' - ' + errorResponse.code;
163+
}
164+
165+
break;
166+
167+
// Any unhandled errors, such as 500 or 429, will be caught here as well
168+
default: {
169+
throw new Error('Received HTTP Code of ' + response.status);
170+
}
171+
140172
}
141173

142174
verbose(message);
143175
invokeAliasCallback(aliasCallback, response.status, errorMessage);
144176
} catch (e) {
145-
const err = e as Error;
146-
error('Error sending alias request to mParticle servers. ' + err);
177+
const errorMessage = (e as Error).message || e.toString();
178+
error('Error sending alias request to mParticle servers. ' + errorMessage);
147179
invokeAliasCallback(
148180
aliasCallback,
149181
HTTPCodes.noHttpCoverage,
150-
err.message
182+
errorMessage,
151183
);
152184
}
153185
};
@@ -197,33 +229,67 @@ export default function IdentityAPIClient(
197229
},
198230
body: JSON.stringify(identityApiRequest),
199231
};
232+
mpInstance._Store.identityCallInFlight = true;
200233

201234
try {
202-
mpInstance._Store.identityCallInFlight = true;
203235
const response: Response = await uploader.upload(fetchPayload);
204236

205237
let identityResponse: IIdentityResponse;
238+
let message: string;
239+
240+
switch (response.status) {
241+
case HTTP_ACCEPTED:
242+
case HTTP_OK:
243+
244+
// Our Identity API will return a 400 error if there is an issue with the requeest body
245+
// such as if the body is empty or one of the attributes is missing or malformed
246+
// A 400 will return an error in the response body and will go through the happy path to report the error
247+
case HTTP_BAD_REQUEST:
248+
249+
// FetchUploader returns the response as a JSON object that we have to await
250+
if (response.json) {
251+
// https://go.mparticle.com/work/SQDSDKS-6568
252+
// FetchUploader returns the response as a JSON object that we have to await
253+
const responseBody: IdentityResultBody = await response.json();
254+
255+
identityResponse = this.getIdentityResponseFromFetch(
256+
response,
257+
responseBody
258+
);
259+
} else {
260+
identityResponse = this.getIdentityResponseFromXHR(
261+
(response as unknown) as XMLHttpRequest
262+
);
263+
}
206264

207-
if (response.json) {
208-
// https://go.mparticle.com/work/SQDSDKS-6568
209-
// FetchUploader returns the response as a JSON object that we have to await
210-
const responseBody: IdentityResultBody = await response.json();
211-
212-
identityResponse = this.getIdentityResponseFromFetch(
213-
response,
214-
responseBody
215-
);
216-
} else {
217-
identityResponse = this.getIdentityResponseFromXHR(
218-
(response as unknown) as XMLHttpRequest
219-
);
265+
if (identityResponse.status === HTTP_BAD_REQUEST) {
266+
const errorResponse: IdentityApiErrorResponse = identityResponse.responseText as unknown as IdentityApiErrorResponse;
267+
message = 'Issue with sending Identity Request to mParticle Servers, received HTTP Code of ' + identityResponse.status;
268+
269+
if (errorResponse?.Errors) {
270+
const errorMessage = errorResponse.Errors.map((error) => error.message).join(', ');
271+
message += ' - ' + errorMessage;
272+
}
273+
274+
} else {
275+
message = 'Received Identity Response from server: ';
276+
message += JSON.stringify(identityResponse.responseText);
277+
}
278+
279+
break;
280+
281+
// Our Identity API will return:
282+
// - 401 if the `x-mp-key` is incorrect or missing
283+
// - 403 if the there is a permission or account issue related to the `x-mp-key`
284+
// 401 and 403 have no response bodies and should be rejected outright
285+
default: {
286+
throw new Error('Received HTTP Code of ' + response.status);
287+
}
220288
}
221289

222-
verbose(
223-
'Received Identity Response from server: ' +
224-
JSON.stringify(identityResponse.responseText)
225-
);
290+
mpInstance._Store.identityCallInFlight = false;
226291

292+
verbose(message);
227293
parseIdentityResponse(
228294
identityResponse,
229295
previousMPID,
@@ -234,15 +300,16 @@ export default function IdentityAPIClient(
234300
false
235301
);
236302
} catch (err) {
303+
mpInstance._Store.identityCallInFlight = false;
304+
237305
const errorMessage = (err as Error).message || err.toString();
238306

239-
mpInstance._Store.identityCallInFlight = false;
307+
error('Error sending identity request to servers' + ' - ' + errorMessage);
240308
invokeCallback(
241309
callback,
242310
HTTPCodes.noHttpCoverage,
243311
errorMessage,
244312
);
245-
error('Error sending identity request to servers' + ' - ' + err);
246313
}
247314
};
248315

test/src/config/utils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,8 @@ var pluses = /\+/g,
634634
hasIdentifyReturned = () => {
635635
return window.mParticle.Identity.getCurrentUser()?.getMPID() === testMPID;
636636
},
637-
hasIdentityCallInflightReturned = () => !mParticle.getInstance()?._Store?.identityCallInFlight;
637+
hasIdentityCallInflightReturned = () => !mParticle.getInstance()?._Store?.identityCallInFlight,
638+
hasConfigLoaded = () => !!mParticle.getInstance()?._Store?.configurationLoaded
638639

639640
var TestsCore = {
640641
getLocalStorageProducts: getLocalStorageProducts,
@@ -663,6 +664,7 @@ var TestsCore = {
663664
fetchMockSuccess: fetchMockSuccess,
664665
hasIdentifyReturned: hasIdentifyReturned,
665666
hasIdentityCallInflightReturned,
667+
hasConfigLoaded,
666668
};
667669

668670
export default TestsCore;

test/src/tests-core-sdk.js

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ const DefaultConfig = Constants.DefaultConfig,
1212
findEventFromRequest = Utils.findEventFromRequest,
1313
findBatch = Utils.findBatch;
1414

15-
const { waitForCondition, fetchMockSuccess, hasIdentifyReturned, hasIdentityCallInflightReturned } = Utils;
15+
const {
16+
waitForCondition,
17+
fetchMockSuccess,
18+
hasIdentifyReturned,
19+
hasIdentityCallInflightReturned,
20+
hasConfigLoaded,
21+
} = Utils;
1622

1723
describe('core SDK', function() {
1824
beforeEach(function() {
@@ -1126,7 +1132,7 @@ describe('core SDK', function() {
11261132
})
11271133
});
11281134

1129-
it('should initialize and log events even with a failed /config fetch and empty config', function async(done) {
1135+
it('should initialize and log events even with a failed /config fetch and empty config', async () => {
11301136
// this instance occurs when self hosting and the user only passes an object into init
11311137
mParticle._resetForTests(MPConfig);
11321138

@@ -1152,12 +1158,7 @@ describe('core SDK', function() {
11521158

11531159
mParticle.init(apiKey, window.mParticle.config);
11541160

1155-
waitForCondition(() => {
1156-
return (
1157-
mParticle.getInstance()._Store.configurationLoaded === true
1158-
);
1159-
})
1160-
.then(() => {
1161+
await waitForCondition(hasConfigLoaded);
11611162
// fetching the config is async and we need to wait for it to finish
11621163
mParticle.getInstance()._Store.isInitialized.should.equal(true);
11631164

@@ -1170,23 +1171,16 @@ describe('core SDK', function() {
11701171
mParticle.Identity.identify({
11711172
userIdentities: { customerid: 'test' },
11721173
});
1173-
waitForCondition(() => {
1174-
return (
1175-
mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1'
1176-
);
1177-
})
1178-
.then(() => {
1179-
mParticle.logEvent('Test Event');
1180-
const testEvent = findEventFromRequest(
1181-
fetchMock.calls(),
1182-
'Test Event'
1183-
);
11841174

1185-
testEvent.should.be.ok();
1175+
await waitForCondition(() => mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1');
11861176

1187-
done();
1188-
});
1189-
});
1177+
mParticle.logEvent('Test Event');
1178+
const testEvent = findEventFromRequest(
1179+
fetchMock.calls(),
1180+
'Test Event'
1181+
);
1182+
1183+
testEvent.should.be.ok();
11901184
});
11911185

11921186
it('should initialize without a config object passed to init', async function() {

0 commit comments

Comments
 (0)