UNPKG

@aws-amplify/core

Version:
247 lines (196 loc) • 5.8 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { ConsoleLogger } from '../../../Logger'; import { EventsBatch, PutEventsInput, PutEventsOutput, putEvents, } from '../../../awsClients/pinpoint'; import { BufferedEvent, BufferedEventMap, EventBuffer, PinpointEventBufferConfig, } from '../types/buffer'; import { AuthSession } from '../../../singleton/Auth/types'; import { isAppInForeground } from './isAppInForeground'; const logger = new ConsoleLogger('PinpointEventBuffer'); const RETRYABLE_CODES = [429, 500]; const ACCEPTED_CODES = [202]; export class PinpointEventBuffer { private _config: PinpointEventBufferConfig; private _interval: ReturnType<typeof setInterval> | undefined = undefined; private _buffer: EventBuffer; private _pause = false; private _flush = false; constructor(config: PinpointEventBufferConfig) { this._buffer = []; this._config = config; this._sendBatch = this._sendBatch.bind(this); this._startLoop(); } public push(event: BufferedEvent) { if (this._buffer.length >= this._config.bufferSize) { logger.debug('Exceeded Pinpoint event buffer limits, event dropped.', { eventId: event.eventId, }); return; } this._buffer.push({ [event.eventId]: event }); } public pause() { this._pause = true; } public resume() { this._pause = false; } public flush() { this._flush = true; } public identityHasChanged(identityId: AuthSession['identityId']) { return this._config.identityId !== identityId; } public flushAll() { this._putEvents(this._buffer.splice(0, this._buffer.length)); } private _startLoop() { if (this._interval) { clearInterval(this._interval); } const { flushInterval } = this._config; this._interval = setInterval(this._sendBatch, flushInterval); } private _sendBatch() { const bufferLength = this._buffer.length; if (this._flush && !bufferLength && this._interval) { clearInterval(this._interval); } if (this._pause || !bufferLength || !isAppInForeground()) { return; } const { flushSize } = this._config; const batchSize = Math.min(flushSize, bufferLength); const bufferSubset = this._buffer.splice(0, batchSize); this._putEvents(bufferSubset); } private async _putEvents(buffer: EventBuffer) { const eventMap: BufferedEventMap = this._bufferToMap(buffer); const batchEventParams = this._generateBatchEventParams(eventMap); try { const { credentials, region, userAgentValue } = this._config; const data: PutEventsOutput = await putEvents( { credentials, region, userAgentValue, }, batchEventParams, ); this._processPutEventsSuccessResponse(data, eventMap); } catch (err) { this._handlePutEventsFailure(err, eventMap); } } private _generateBatchEventParams( eventMap: BufferedEventMap, ): PutEventsInput { const batchItem: Record<string, EventsBatch> = {}; Object.values(eventMap).forEach(item => { const { event, timestamp, endpointId, eventId, session } = item; const { name, attributes, metrics } = event; batchItem[endpointId] = { Endpoint: { ...batchItem[endpointId]?.Endpoint, }, Events: { ...batchItem[endpointId]?.Events, [eventId]: { EventType: name, Timestamp: new Date(timestamp).toISOString(), Attributes: attributes, Metrics: metrics, Session: session, }, }, }; }); return { ApplicationId: this._config.appId, EventsRequest: { BatchItem: batchItem, }, }; } private _handlePutEventsFailure(err: any, eventMap: BufferedEventMap) { logger.debug('putEvents call to Pinpoint failed.', err); const statusCode = err.$metadata && err.$metadata.httpStatusCode; if (RETRYABLE_CODES.includes(statusCode)) { const retryableEvents = Object.values(eventMap); this._retry(retryableEvents); } } private _processPutEventsSuccessResponse( data: PutEventsOutput, eventMap: BufferedEventMap, ) { const { Results = {} } = data.EventsResponse ?? {}; const retryableEvents: BufferedEvent[] = []; Object.entries(Results).forEach(([_, endpointValues]) => { const responses = endpointValues.EventsItemResponse ?? {}; Object.entries(responses).forEach(([eventId, eventValues]) => { const eventObject = eventMap[eventId]; if (!eventObject) { return; } const { StatusCode, Message } = eventValues ?? {}; if (StatusCode && ACCEPTED_CODES.includes(StatusCode)) { return; } if (StatusCode && RETRYABLE_CODES.includes(StatusCode)) { retryableEvents.push(eventObject); return; } const { name } = eventObject.event; logger.warn('Pinpoint event failed to send.', { eventId, name, message: Message, }); }); }); if (retryableEvents.length) { this._retry(retryableEvents); } } private _retry(retryableEvents: BufferedEvent[]) { // retryable events that haven't reached the resendLimit const eligibleEvents: EventBuffer = []; retryableEvents.forEach((bufferedEvent: BufferedEvent) => { const { eventId } = bufferedEvent; const { name } = bufferedEvent.event; if (bufferedEvent!.resendLimit!-- > 0) { logger.debug('Resending event.', { eventId, name, remainingAttempts: bufferedEvent.resendLimit, }); eligibleEvents.push({ [eventId!]: bufferedEvent }); return; } logger.debug('No retry attempts remaining for event.', { eventId, name, }); }); // add the events to the front of the buffer this._buffer.unshift(...eligibleEvents); } private _bufferToMap(buffer: EventBuffer) { return buffer.reduce((acc, curVal) => { const [[key, value]] = Object.entries(curVal); acc[key] = value; return acc; }, {}); } }