@mparticle/web-sdk
Advanced tools
Comparing version 2.19.1 to 2.19.2
{ | ||
"name": "@mparticle/web-sdk", | ||
"version": "2.19.1", | ||
"version": "2.19.2", | ||
"description": "mParticle core SDK for web applications", | ||
@@ -5,0 +5,0 @@ "license": "Apache-2.0", |
@@ -12,31 +12,13 @@ import { Batch } from '@mparticle/event-models'; | ||
/** | ||
* BatchUploader contains all the logic to upload batches to mParticle. | ||
* It queues events as they come in and at set intervals turns them into batches. | ||
* It then attempts to upload them to mParticle. | ||
* | ||
* These uploads happen on an interval basis using window.fetch or XHR | ||
* requests, depending on what is available in the browser. | ||
* | ||
* Uploads can also be triggered on browser visibility/focus changes via an | ||
* event listener, which then uploads to mPartice via the browser's Beacon API. | ||
*/ | ||
export class BatchUploader { | ||
// We upload JSON, but this content type is required to avoid a CORS preflight request | ||
//we upload JSON, but this content type is required to avoid a CORS preflight request | ||
static readonly CONTENT_TYPE: string = 'text/plain;charset=UTF-8'; | ||
static readonly MINIMUM_INTERVAL_MILLIS: number = 500; | ||
uploadIntervalMillis: number; | ||
eventsQueuedForProcessing: SDKEvent[]; | ||
batchesQueuedForProcessing: Batch[]; | ||
pendingEvents: SDKEvent[]; | ||
pendingUploads: Batch[]; | ||
mpInstance: MParticleWebSDK; | ||
uploadUrl: string; | ||
batchingEnabled: boolean; | ||
private uploader: AsyncUploader; | ||
/** | ||
* Creates an instance of a BatchUploader | ||
* @param {MParticleWebSDK} mpInstance - the mParticle SDK instance | ||
* @param {number} uploadInterval - the desired upload interval in milliseconds | ||
*/ | ||
constructor(mpInstance: MParticleWebSDK, uploadInterval: number) { | ||
@@ -50,4 +32,4 @@ this.mpInstance = mpInstance; | ||
} | ||
this.eventsQueuedForProcessing = []; | ||
this.batchesQueuedForProcessing = []; | ||
this.pendingEvents = []; | ||
this.pendingUploads = []; | ||
@@ -61,16 +43,11 @@ const { SDKConfig, devToken } = this.mpInstance._Store; | ||
this.uploader = window.fetch | ||
? new FetchUploader(this.uploadUrl) | ||
: new XHRUploader(this.uploadUrl); | ||
this.triggerUploadInterval(true, false); | ||
setTimeout(() => { | ||
this.prepareAndUpload(true, false); | ||
}, this.uploadIntervalMillis); | ||
this.addEventListeners(); | ||
} | ||
// Adds listeners to be used trigger Navigator.sendBeacon if the browser | ||
// loses focus for any reason, such as closing browser tab or minimizing window | ||
private addEventListeners() { | ||
const _this = this; | ||
// visibility change is a document property, not window | ||
document.addEventListener('visibilitychange', () => { | ||
@@ -94,19 +71,5 @@ _this.prepareAndUpload(false, _this.isBeaconAvailable()); | ||
// Triggers a setTimeout for prepareAndUpload | ||
private triggerUploadInterval( | ||
triggerFuture: boolean = false, | ||
useBeacon: boolean = false | ||
): void { | ||
setTimeout(() => { | ||
this.prepareAndUpload(triggerFuture, useBeacon); | ||
}, this.uploadIntervalMillis); | ||
} | ||
/** | ||
* This method will queue a single Event which will eventually be processed into a Batch | ||
* @param event event that should be queued | ||
*/ | ||
queueEvent(event: SDKEvent): void { | ||
if (!isEmpty(event)) { | ||
this.eventsQueuedForProcessing.push(event); | ||
this.pendingEvents.push(event); | ||
this.mpInstance.Logger.verbose( | ||
@@ -116,7 +79,5 @@ `Queuing event: ${JSON.stringify(event)}` | ||
this.mpInstance.Logger.verbose( | ||
`Queued event count: ${this.eventsQueuedForProcessing.length}` | ||
`Queued event count: ${this.pendingEvents.length}` | ||
); | ||
// TODO: Remove this check once the v2 code path is removed | ||
// https://go.mparticle.com/work/SQDSDKS-3720 | ||
if ( | ||
@@ -140,3 +101,3 @@ !this.batchingEnabled || | ||
*/ | ||
private static createNewBatches( | ||
private static createNewUploads( | ||
sdkEvents: SDKEvent[], | ||
@@ -189,4 +150,5 @@ defaultUser: MParticleUser, | ||
if (onCreateBatchCallback) { | ||
uploadBatchObject = | ||
onCreateBatchCallback(uploadBatchObject); | ||
uploadBatchObject = onCreateBatchCallback( | ||
uploadBatchObject | ||
); | ||
if (uploadBatchObject) { | ||
@@ -217,12 +179,8 @@ uploadBatchObject.modified = true; | ||
*/ | ||
private async prepareAndUpload( | ||
triggerFuture: boolean, | ||
useBeacon: boolean | ||
): Promise<void> { | ||
private async prepareAndUpload(triggerFuture: boolean, useBeacon: boolean) { | ||
const currentUser = this.mpInstance.Identity.getCurrentUser(); | ||
const currentEvents = this.eventsQueuedForProcessing; | ||
this.eventsQueuedForProcessing = []; | ||
const newBatches = BatchUploader.createNewBatches( | ||
const currentEvents = this.pendingEvents; | ||
this.pendingEvents = []; | ||
const newUploads = BatchUploader.createNewUploads( | ||
currentEvents, | ||
@@ -232,43 +190,21 @@ currentUser, | ||
); | ||
if (!isEmpty(newBatches)) { | ||
this.batchesQueuedForProcessing.push(...newBatches); | ||
if (newUploads && newUploads.length) { | ||
this.pendingUploads.push(...newUploads); | ||
} | ||
const batchesToUpload = this.batchesQueuedForProcessing; | ||
const batchesThatDidNotUpload: Batch[] = []; | ||
this.batchesQueuedForProcessing = []; | ||
// Create an array of promises as we try to upload each batch indvidually | ||
const promises: Promise<Batch>[] = batchesToUpload.map((upload) => { | ||
return this.upload(this.mpInstance.Logger, upload, useBeacon); | ||
}); | ||
// Iterate through fulfilled promises and store any remaining batches | ||
// for future re-transmission attempts | ||
if (!isEmpty(promises)) { | ||
Promise.all(promises) | ||
.then((batchResponses) => { | ||
batchResponses.forEach((batch) => | ||
!isEmpty(batch) | ||
? batchesThatDidNotUpload.push(batch) | ||
: null | ||
); | ||
}) | ||
.catch((error) => { | ||
this.mpInstance.Logger.error( | ||
`Error processing batches during upload attempt: ${error}` | ||
); | ||
}) | ||
.finally(() => { | ||
// Any batches that did not upload should be put back into the queue for processing | ||
if (!isEmpty(batchesThatDidNotUpload)) { | ||
this.batchesQueuedForProcessing.unshift( | ||
...batchesThatDidNotUpload | ||
); | ||
} | ||
}); | ||
const currentUploads = this.pendingUploads; | ||
this.pendingUploads = []; | ||
const remainingUploads: Batch[] = await this.upload( | ||
this.mpInstance.Logger, | ||
currentUploads, | ||
useBeacon | ||
); | ||
if (remainingUploads && remainingUploads.length) { | ||
this.pendingUploads.unshift(...remainingUploads); | ||
} | ||
if (triggerFuture) { | ||
this.triggerUploadInterval(triggerFuture, false); | ||
setTimeout(() => { | ||
this.prepareAndUpload(true, false); | ||
}, this.uploadIntervalMillis); | ||
} | ||
@@ -279,58 +215,74 @@ } | ||
logger: SDKLoggerApi, | ||
batch: Batch, | ||
_uploads: Batch[], | ||
useBeacon: boolean | ||
): Promise<Batch | null> { | ||
if (isEmpty(batch) || isEmpty(batch.events)) { | ||
): Promise<Batch[]> { | ||
let uploader; | ||
// Filter out any batches that don't have events | ||
const uploads = _uploads.filter(upload => !isEmpty(upload.events)); | ||
if (isEmpty(uploads)) { | ||
return null; | ||
} | ||
logger.verbose(`Uploading batches: ${JSON.stringify(batch)}`); | ||
logger.verbose(`Uploading batches: ${JSON.stringify(uploads)}`); | ||
logger.verbose(`Batch count: ${uploads.length}`); | ||
const fetchPayload: fetchPayload = { | ||
method: 'POST', | ||
headers: { | ||
Accept: BatchUploader.CONTENT_TYPE, | ||
'Content-Type': 'text/plain;charset=UTF-8', | ||
}, | ||
body: JSON.stringify(batch), | ||
}; | ||
for (let i = 0; i < uploads.length; i++) { | ||
const fetchPayload: fetchPayload = { | ||
method: 'POST', | ||
headers: { | ||
Accept: BatchUploader.CONTENT_TYPE, | ||
'Content-Type': 'text/plain;charset=UTF-8', | ||
}, | ||
body: JSON.stringify(uploads[i]), | ||
}; | ||
// TODO: Make beacon its own function | ||
// beacon is only used on onbeforeunload onpagehide events | ||
if (useBeacon && this.isBeaconAvailable()) { | ||
let blob = new Blob([fetchPayload.body], { | ||
type: 'text/plain;charset=UTF-8', | ||
}); | ||
navigator.sendBeacon(this.uploadUrl, blob); | ||
} else { | ||
try { | ||
const response = await this.uploader.upload(fetchPayload); | ||
// TODO: Should we make this a switch statement instead? | ||
if (response.status >= 200 && response.status < 300) { | ||
logger.verbose( | ||
`Upload success for request ID: ${batch.source_request_id}` | ||
// beacon is only used on onbeforeunload onpagehide events | ||
if (useBeacon && this.isBeaconAvailable()) { | ||
let blob = new Blob([fetchPayload.body], { | ||
type: 'text/plain;charset=UTF-8', | ||
}); | ||
navigator.sendBeacon(this.uploadUrl, blob); | ||
} else { | ||
if (!uploader) { | ||
if (window.fetch) { | ||
uploader = new FetchUploader(this.uploadUrl, logger); | ||
} else { | ||
uploader = new XHRUploader(this.uploadUrl, logger); | ||
} | ||
} | ||
try { | ||
const response = await uploader.upload( | ||
fetchPayload, | ||
uploads, | ||
i | ||
); | ||
return null; | ||
} else if (response.status >= 500 || response.status === 429) { | ||
if (response.status >= 200 && response.status < 300) { | ||
logger.verbose( | ||
`Upload success for request ID: ${uploads[i].source_request_id}` | ||
); | ||
} else if ( | ||
response.status >= 500 || | ||
response.status === 429 | ||
) { | ||
logger.error( | ||
`HTTP error status ${response.status} received` | ||
); | ||
//server error, add back current events and try again later | ||
return uploads.slice(i, uploads.length); | ||
} else if (response.status >= 401) { | ||
logger.error( | ||
`HTTP error status ${response.status} while uploading - please verify your API key.` | ||
); | ||
//if we're getting a 401, assume we'll keep getting a 401 and clear the uploads. | ||
return null; | ||
} | ||
} catch (e) { | ||
logger.error( | ||
`HTTP error status ${response.status} received` | ||
`Error sending event to mParticle servers. ${e}` | ||
); | ||
// Server error, return current batch and try again later | ||
return batch; | ||
} else if (response.status >= 401) { | ||
logger.error( | ||
`HTTP error status ${response.status} while uploading - please verify your API key.` | ||
); | ||
// if we're getting a 401, assume we'll keep getting a 401 | ||
// so return the upload so it can be stored for later use | ||
return batch; | ||
return uploads.slice(i, uploads.length); | ||
} | ||
} catch (error) { | ||
logger.error( | ||
`Error sending event to mParticle servers. ${error}` | ||
); | ||
return batch; | ||
} | ||
@@ -344,6 +296,7 @@ } | ||
url: string; | ||
public abstract upload(fetchPayload: fetchPayload): Promise<XHRResponse>; | ||
logger: SDKLoggerApi; | ||
constructor(url: string) { | ||
constructor(url: string, logger: SDKLoggerApi) { | ||
this.url = url; | ||
this.logger = logger; | ||
} | ||
@@ -353,3 +306,7 @@ } | ||
class FetchUploader extends AsyncUploader { | ||
public async upload(fetchPayload: fetchPayload): Promise<XHRResponse> { | ||
private async upload( | ||
fetchPayload: fetchPayload, | ||
uploads: Batch[], | ||
i: number | ||
) { | ||
const response: XHRResponse = await fetch(this.url, fetchPayload); | ||
@@ -361,5 +318,10 @@ return response; | ||
class XHRUploader extends AsyncUploader { | ||
public async upload(fetchPayload: fetchPayload): Promise<XHRResponse> { | ||
private async upload( | ||
fetchPayload: fetchPayload, | ||
uploads: Batch[], | ||
i: number | ||
) { | ||
const response: XHRResponse = await this.makeRequest( | ||
this.url, | ||
this.logger, | ||
fetchPayload.body | ||
@@ -372,2 +334,3 @@ ); | ||
url: string, | ||
logger: SDKLoggerApi, | ||
data: string | ||
@@ -374,0 +337,0 @@ ): Promise<XMLHttpRequest> { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1440906
25516