@mparticle/web-sdk
Advanced tools
Comparing version 2.19.2 to 2.19.3
{ | ||
"name": "@mparticle/web-sdk", | ||
"version": "2.19.2", | ||
"version": "2.19.3", | ||
"description": "mParticle core SDK for web applications", | ||
@@ -31,2 +31,3 @@ "license": "Apache-2.0", | ||
"build:snippet": "uglifyjs snippet.js -nm -o snippet.min.js", | ||
"build:test-bundle": "cross-env TESTTYPE=main ENVIRONMENT=prod rollup --config rollup.test.config.js", | ||
"build:browserify:cjs": "browserify test/integrations/cjs/browserify/index.js -o test/integrations/cjs/dist/browserify-output.js && npm run test:karma:browserify:cjs", | ||
@@ -38,3 +39,3 @@ "build:rollup:cjs": "rollup --config test/integrations/cjs/rollup/rollup.config.js && npm run test:karma:rollup:cjs", | ||
"build:ts": "tsc -p .", | ||
"test": "npm run build && cross-env TESTTYPE=main ENVIRONMENT=prod rollup --config rollup.test.config.js && cross-env DEBUG=false karma start test/karma.config.js", | ||
"test": "npm run build && npm run build:test-bundle && cross-env DEBUG=false karma start test/karma.config.js", | ||
"test:debug": "cross-env DEBUG=true karma start test/karma.config.js", | ||
@@ -41,0 +42,0 @@ "test:stub": "cross-env TESTTYPE=stub ENVIRONMENT=prod rollup --config rollup.test.config.js && karma start test/stub/karma.stub.config.js", |
@@ -12,13 +12,31 @@ 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; | ||
pendingEvents: SDKEvent[]; | ||
pendingUploads: Batch[]; | ||
eventsQueuedForProcessing: SDKEvent[]; | ||
batchesQueuedForProcessing: 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) { | ||
@@ -32,4 +50,4 @@ this.mpInstance = mpInstance; | ||
} | ||
this.pendingEvents = []; | ||
this.pendingUploads = []; | ||
this.eventsQueuedForProcessing = []; | ||
this.batchesQueuedForProcessing = []; | ||
@@ -43,11 +61,16 @@ const { SDKConfig, devToken } = this.mpInstance._Store; | ||
setTimeout(() => { | ||
this.prepareAndUpload(true, false); | ||
}, this.uploadIntervalMillis); | ||
this.uploader = window.fetch | ||
? new FetchUploader(this.uploadUrl) | ||
: new XHRUploader(this.uploadUrl); | ||
this.triggerUploadInterval(true, false); | ||
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', () => { | ||
@@ -71,5 +94,19 @@ _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.pendingEvents.push(event); | ||
this.eventsQueuedForProcessing.push(event); | ||
this.mpInstance.Logger.verbose( | ||
@@ -79,5 +116,7 @@ `Queuing event: ${JSON.stringify(event)}` | ||
this.mpInstance.Logger.verbose( | ||
`Queued event count: ${this.pendingEvents.length}` | ||
`Queued event count: ${this.eventsQueuedForProcessing.length}` | ||
); | ||
// TODO: Remove this check once the v2 code path is removed | ||
// https://go.mparticle.com/work/SQDSDKS-3720 | ||
if ( | ||
@@ -101,3 +140,3 @@ !this.batchingEnabled || | ||
*/ | ||
private static createNewUploads( | ||
private static createNewBatches( | ||
sdkEvents: SDKEvent[], | ||
@@ -150,5 +189,4 @@ defaultUser: MParticleUser, | ||
if (onCreateBatchCallback) { | ||
uploadBatchObject = onCreateBatchCallback( | ||
uploadBatchObject | ||
); | ||
uploadBatchObject = | ||
onCreateBatchCallback(uploadBatchObject); | ||
if (uploadBatchObject) { | ||
@@ -179,8 +217,13 @@ uploadBatchObject.modified = true; | ||
*/ | ||
private async prepareAndUpload(triggerFuture: boolean, useBeacon: boolean) { | ||
private async prepareAndUpload( | ||
triggerFuture: boolean, | ||
useBeacon: boolean | ||
): Promise<void> { | ||
// Fetch current user so that events can be grouped by MPID | ||
const currentUser = this.mpInstance.Identity.getCurrentUser(); | ||
const currentEvents = this.pendingEvents; | ||
this.pendingEvents = []; | ||
const newUploads = BatchUploader.createNewUploads( | ||
const currentEvents = this.eventsQueuedForProcessing; | ||
this.eventsQueuedForProcessing = []; | ||
const newBatches = BatchUploader.createNewBatches( | ||
currentEvents, | ||
@@ -190,33 +233,39 @@ currentUser, | ||
); | ||
if (newUploads && newUploads.length) { | ||
this.pendingUploads.push(...newUploads); | ||
if (!isEmpty(newBatches)) { | ||
this.batchesQueuedForProcessing.push(...newBatches); | ||
} | ||
const currentUploads = this.pendingUploads; | ||
this.pendingUploads = []; | ||
const remainingUploads: Batch[] = await this.upload( | ||
// Clear out pending batches to avoid any potential duplication | ||
const batchesToUpload = this.batchesQueuedForProcessing; | ||
this.batchesQueuedForProcessing = []; | ||
const batchesThatDidNotUpload = await this.uploadBatches( | ||
this.mpInstance.Logger, | ||
currentUploads, | ||
batchesToUpload, | ||
useBeacon | ||
); | ||
if (remainingUploads && remainingUploads.length) { | ||
this.pendingUploads.unshift(...remainingUploads); | ||
// Batches that do not successfully upload are added back to the process queue | ||
// in the order they were created so that we can attempt re-transmission in | ||
// the same sequence. This is to prevent any potential data corruption. | ||
if (!isEmpty(batchesThatDidNotUpload)) { | ||
// TODO: https://go.mparticle.com/work/SQDSDKS-5165 | ||
this.batchesQueuedForProcessing.unshift(...batchesThatDidNotUpload); | ||
} | ||
if (triggerFuture) { | ||
setTimeout(() => { | ||
this.prepareAndUpload(true, false); | ||
}, this.uploadIntervalMillis); | ||
this.triggerUploadInterval(triggerFuture, false); | ||
} | ||
} | ||
private async upload( | ||
// TODO: Refactor to use logger as a class method | ||
// https://go.mparticle.com/work/SQDSDKS-5167 | ||
private async uploadBatches( | ||
logger: SDKLoggerApi, | ||
_uploads: Batch[], | ||
batches: Batch[], | ||
useBeacon: boolean | ||
): Promise<Batch[]> { | ||
let uploader; | ||
): Promise<Batch[] | null> { | ||
// Filter out any batches that don't have events | ||
const uploads = _uploads.filter(upload => !isEmpty(upload.events)); | ||
const uploads = batches.filter((batch) => !isEmpty(batch.events)); | ||
@@ -247,15 +296,4 @@ if (isEmpty(uploads)) { | ||
} 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 | ||
); | ||
const response = await this.uploader.upload(fetchPayload); | ||
@@ -273,3 +311,3 @@ if (response.status >= 200 && response.status < 300) { | ||
); | ||
//server error, add back current events and try again later | ||
// Server error, add back current batches and try again later | ||
return uploads.slice(i, uploads.length); | ||
@@ -282,2 +320,12 @@ } else if (response.status >= 401) { | ||
return null; | ||
} else { | ||
// In case there is an HTTP error we did not anticipate. | ||
console.error( | ||
`HTTP error status ${response.status} while uploading events.`, | ||
response | ||
); | ||
throw new Error( | ||
`Uncaught HTTP Error ${response.status}. Batch upload will be re-attempted.` | ||
); | ||
} | ||
@@ -298,7 +346,6 @@ } catch (e) { | ||
url: string; | ||
logger: SDKLoggerApi; | ||
public abstract upload(fetchPayload: fetchPayload): Promise<XHRResponse>; | ||
constructor(url: string, logger: SDKLoggerApi) { | ||
constructor(url: string) { | ||
this.url = url; | ||
this.logger = logger; | ||
} | ||
@@ -308,7 +355,3 @@ } | ||
class FetchUploader extends AsyncUploader { | ||
private async upload( | ||
fetchPayload: fetchPayload, | ||
uploads: Batch[], | ||
i: number | ||
) { | ||
public async upload(fetchPayload: fetchPayload): Promise<XHRResponse> { | ||
const response: XHRResponse = await fetch(this.url, fetchPayload); | ||
@@ -320,10 +363,5 @@ return response; | ||
class XHRUploader extends AsyncUploader { | ||
private async upload( | ||
fetchPayload: fetchPayload, | ||
uploads: Batch[], | ||
i: number | ||
) { | ||
public async upload(fetchPayload: fetchPayload): Promise<XHRResponse> { | ||
const response: XHRResponse = await this.makeRequest( | ||
this.url, | ||
this.logger, | ||
fetchPayload.body | ||
@@ -336,3 +374,2 @@ ); | ||
url: string, | ||
logger: SDKLoggerApi, | ||
data: string | ||
@@ -339,0 +376,0 @@ ): Promise<XMLHttpRequest> { |
@@ -6,2 +6,8 @@ import Types from './types'; | ||
var self = this; | ||
const UserAttributeActionTypes = { | ||
setUserAttribute: 'setUserAttribute', | ||
removeUserAttribute: 'removeUserAttribute', | ||
}; | ||
this.initForwarders = function(userIdentities, forwardingStatsCallback) { | ||
@@ -391,29 +397,43 @@ var user = mpInstance.Identity.getCurrentUser(); | ||
this.callSetUserAttributeOnForwarders = function(key, value) { | ||
if (kitBlocker && kitBlocker.isAttributeKeyBlocked(key)) { | ||
this.handleForwarderUserAttributes = function(functionNameKey, key, value) { | ||
if ( | ||
(kitBlocker && kitBlocker.isAttributeKeyBlocked(key)) || | ||
!mpInstance._Store.activeForwarders.length | ||
) { | ||
return; | ||
} | ||
if (mpInstance._Store.activeForwarders.length) { | ||
mpInstance._Store.activeForwarders.forEach(function(forwarder) { | ||
mpInstance._Store.activeForwarders.forEach(function(forwarder) { | ||
const forwarderFunction = forwarder[functionNameKey]; | ||
if ( | ||
!forwarderFunction || | ||
mpInstance._Helpers.isFilteredUserAttribute( | ||
key, | ||
forwarder.userAttributeFilters | ||
) | ||
) { | ||
return; | ||
} | ||
try { | ||
let result; | ||
if ( | ||
forwarder.setUserAttribute && | ||
forwarder.userAttributeFilters && | ||
!mpInstance._Helpers.inArray( | ||
forwarder.userAttributeFilters, | ||
mpInstance._Helpers.generateHash(key) | ||
) | ||
functionNameKey === | ||
UserAttributeActionTypes.setUserAttribute | ||
) { | ||
try { | ||
var result = forwarder.setUserAttribute(key, value); | ||
result = forwarder.setUserAttribute(key, value); | ||
} else if ( | ||
functionNameKey === | ||
UserAttributeActionTypes.removeUserAttribute | ||
) { | ||
result = forwarder.removeUserAttribute(key); | ||
} | ||
if (result) { | ||
mpInstance.Logger.verbose(result); | ||
} | ||
} catch (e) { | ||
mpInstance.Logger.error(e); | ||
} | ||
if (result) { | ||
mpInstance.Logger.verbose(result); | ||
} | ||
}); | ||
} | ||
} catch (e) { | ||
mpInstance.Logger.error(e); | ||
} | ||
}); | ||
}; | ||
@@ -420,0 +440,0 @@ |
@@ -390,2 +390,7 @@ import Types from './types'; | ||
this.isFilteredUserAttribute = function(userAttributeKey, filterList) { | ||
const hashedUserAttribute = self.generateHash(userAttributeKey); | ||
return filterList && self.inArray(filterList, hashedUserAttribute); | ||
}; | ||
this.isEventType = function(type) { | ||
@@ -392,0 +397,0 @@ for (var prop in Types.EventType) { |
@@ -9,8 +9,6 @@ import { Logger } from '@mparticle/web-sdk'; | ||
export default class Vault<StorableItem extends Dictionary> { | ||
public contents: Dictionary<StorableItem>; | ||
export default class Vault<StorableItem> { | ||
public contents: StorableItem; | ||
private readonly _storageKey: string; | ||
private readonly _itemKey: keyof StorableItem; | ||
private logger?: Logger; | ||
private offlineStorageEnabled: boolean = false; | ||
@@ -23,13 +21,6 @@ /** | ||
*/ | ||
constructor( | ||
storageKey: string, | ||
itemKey: keyof StorableItem, | ||
options?: IVaultOptions | ||
) { | ||
constructor(storageKey: string, options?: IVaultOptions) { | ||
this._storageKey = storageKey; | ||
this._itemKey = itemKey; | ||
this.contents = this.getItems() || {}; | ||
this.contents = this.getFromLocalStorage(); | ||
this.offlineStorageEnabled = options?.offlineStorageEnabled; | ||
// Add a fake logger in case one is not provided or needed | ||
@@ -44,50 +35,15 @@ this.logger = options?.logger || { | ||
/** | ||
* Stores a single Item using `itemId` as an index | ||
* Stores a StorableItem to Local Storage | ||
* @method storeItem | ||
* @param item {StorableItem} a Dictonary with key to store | ||
* @param item {StorableItem} | ||
*/ | ||
public storeItem(item: StorableItem): void { | ||
this.contents = this.getItems(); | ||
public store(item: StorableItem): void { | ||
this.contents = item; | ||
if (item[this._itemKey]) { | ||
this.contents[item[this._itemKey]] = item; | ||
this.logger.verbose( | ||
`Saved items to vault with key: ${item[this._itemKey]}` | ||
); | ||
} | ||
this.logger.verbose(`Saved to local storage: ${item}`); | ||
this.saveItems(this.contents); | ||
this.saveToLocalStorage(this.contents); | ||
} | ||
/** | ||
* Stores Items using `itemId` as an index | ||
* @method storeItems | ||
* @param {StorableItem[]} an Array of StorableItems | ||
*/ | ||
public storeItems(items: StorableItem[]): void { | ||
items.forEach((item) => this.storeItem(item)); | ||
} | ||
/** | ||
* Removes a single StorableItem based on `indexId` | ||
* @method removeItem | ||
* @param {String} indexId | ||
*/ | ||
public removeItem(indexId: string): void { | ||
this.logger.verbose(`Removing from vault: ${indexId}`); | ||
this.contents = this.getItems() || {}; | ||
try { | ||
delete this.contents[indexId]; | ||
this.saveItems(this.contents); | ||
} catch (error) { | ||
this.logger.error( | ||
`Unable to remove item without a matching ID ${indexId}` | ||
); | ||
this.logger.error(error as string); | ||
} | ||
} | ||
/** | ||
* Retrieves all StorableItems from local storage as an array | ||
@@ -97,6 +53,6 @@ * @method retrieveItems | ||
*/ | ||
public retrieveItems(): StorableItem[] { | ||
this.contents = this.getItems(); | ||
public retrieve(): StorableItem { | ||
this.contents = this.getFromLocalStorage(); | ||
return Object.keys(this.contents).map((item) => this.contents[item]); | ||
return this.contents; | ||
} | ||
@@ -109,10 +65,7 @@ | ||
public purge(): void { | ||
this.contents = {}; | ||
this.removeItems(); | ||
this.contents = null; | ||
this.removeFromLocalStorage(); | ||
} | ||
private saveItems(items: Dictionary<StorableItem>): void { | ||
if (!this.offlineStorageEnabled) { | ||
return; | ||
} | ||
private saveToLocalStorage(items: StorableItem): void { | ||
try { | ||
@@ -129,19 +82,13 @@ window.localStorage.setItem( | ||
private getItems(): Dictionary<StorableItem> { | ||
if (!this.offlineStorageEnabled) { | ||
return this.contents; | ||
} | ||
private getFromLocalStorage(): StorableItem | null { | ||
// TODO: Handle cases where Local Storage is unavailable | ||
// https://go.mparticle.com/work/SQDSDKS-5022 | ||
const itemString = window.localStorage.getItem(this._storageKey); | ||
const item: string = window.localStorage.getItem(this._storageKey); | ||
return itemString ? JSON.parse(itemString) : {}; | ||
return item ? JSON.parse(item) : null; | ||
} | ||
private removeItems(): void { | ||
if (!this.offlineStorageEnabled) { | ||
return; | ||
} | ||
private removeFromLocalStorage(): void { | ||
window.localStorage.removeItem(this._storageKey); | ||
} | ||
} | ||
} |
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
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
1448422
25606