import rfc3339 from './rfc3339' import { getEventObject, tracktorConfig, httpRequest } from './helpers' import { Event, TracktorConfig, UserConfig, UNKNOWN } from './interfaces' type Batch = { events?: Array retryId?: number retryCount?: number pending?: boolean } type retryQueue = { [id: number]: Batch } export default class Manager { config: TracktorConfig timeoutRetry: number | NodeJS.Timer retryQueues: retryQueue retryQueueInProcess = false retryCount = 0 batchRetryId = 0 constructor(config?: TracktorConfig) { this.config = { ...tracktorConfig, ...config } } public async send(batch: Batch, userConfig: UserConfig) { batch.pending = true const { sendUrl, appName } = this.config const { ip, sessionStamp, deviceId, userId, visitorId, locale, userAgent, osVersion, osName, deviceBrand, deviceModel } = userConfig const headers = {} if (ip) { headers['X-Forwarded-For'] = ip } let screen_size if (window) { screen_size = window.innerWidth } const requestOptions = { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', method: 'push', params: { session_stamp: sessionStamp, device_id: deviceId, user_id: userId, visitor_id: visitorId, user_agent: `${userAgent} tklib/1.0.0 ${appName}/1.0.0`, loc: locale, events: batch.events, retry: this.getBatchRetryCount(batch), sent_at_client_local: rfc3339(), screen_size, platform_name: appName, os_version: osVersion || UNKNOWN, os_name: osName || UNKNOWN, device_brand: deviceBrand || UNKNOWN, device_model: deviceModel || UNKNOWN, }, }), } try { const response = await httpRequest(sendUrl, requestOptions) if (!response.ok) { this.handleSendError(batch, userConfig) } } catch (error) { // According to MDN: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch // // "A fetch() promise will reject with a TypeError when a network error is encountered // or CORS is misconfigured on the server side, although this usually means permission // issues or similar" } // Remove the batch from the queue if necessary this.deleteBatchRetry(batch) batch.pending = false } public sendFailureEvent(message: string, userConfig: UserConfig): void { this.config.onSendFailure(new Error(message)) this.send({ events: [getEventObject('tk_failure', 1, { message })] }, userConfig) } private handleSendError({ retryId, events }: Batch, userConfig: UserConfig): void { if (!retryId) { this.batchRetryId += 1 } const id = retryId || this.batchRetryId if (this.retryQueues[id]) { this.retryQueues[id].retryCount += 1 } else { this.retryQueues[id] = { events, retryId: id, retryCount: 1, pending: false, } } const batch = this.retryQueues[id] if (batch.retryCount > this.config.retries_max) { this.sendFailureEvent(JSON.stringify(batch.events), userConfig) this.deleteBatchRetry(batch) } this.periodicRetryQueue(userConfig) } private periodicRetryQueue(userConfig: UserConfig) { if ( Object.values(this.retryQueues).filter(batch => !batch.pending).length && !this.timeoutRetry ) { this.timeoutRetry = setTimeout(() => { this.retry(userConfig) this.timeoutRetry = null this.periodicRetryQueue(userConfig) }, this.config.retries_frequency) } } private getBatchRetryCount({ retryId }: Batch): number { if (retryId && retryId in this.retryQueues) { return this.retryQueues[retryId].retryCount } return 0 } private deleteBatchRetry({ retryId }: Batch) { if (retryId && retryId in this.retryQueues) { delete this.retryQueues[retryId] } } private retry(userConfig: UserConfig) { Object.values(this.retryQueues).forEach(batch => { this.send(batch, userConfig) }) } }