UNPKG

@aws-amplify/analytics

Version:

Analytics category of aws-amplify

399 lines (369 loc) • 10.3 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { ConsoleLogger as Logger, Credentials, browserOrNode, AnalyticsAction, } from '@aws-amplify/core'; import { PersonalizeEventsClient, PutEventsCommand, PutEventsCommandInput, } from '@aws-sdk/client-personalize-events'; import { SessionInfo, RequestParams, RecordEventPayload, SessionInfoManager, MediaAutoTrack, } from './AmazonPersonalizeHelper'; import get from 'lodash/get'; import isEmpty from 'lodash/isEmpty'; import isEqual from 'lodash/isEqual'; import { AnalyticsProvider } from '../types'; import { getAnalyticsUserAgent } from '../utils/UserAgent'; const logger = new Logger('AmazonPersonalizeProvider'); // events buffer const FLUSH_SIZE = 5; const FLUSH_SIZE_THRESHHOLD = 10; const FLUSH_INTERVAL = 5 * 1000; // 5s const IDENTIFY_EVENT = 'Identify'; export class AmazonPersonalizeProvider implements AnalyticsProvider { private _config; private _personalize; private _buffer; private _timer; private _sessionInfo: SessionInfo; private _sessionManager; private _isBrowser; constructor(config?) { this._buffer = []; this._config = config ? config : {}; this._config.flushSize = this._config.flushSize > 0 && this._config.flushSize <= FLUSH_SIZE_THRESHHOLD ? this._config.flushSize : FLUSH_SIZE; this._config.flushInterval = this._config.flushInterval || FLUSH_INTERVAL; this._sessionManager = new SessionInfoManager(); if (!isEmpty(this._config.trackingId)) { this._sessionInfo = this._sessionManager.retrieveSessionInfo( this._config.trackingId ); } this._isBrowser = browserOrNode().isBrowser; // flush event buffer this._setupTimer(); } private _setupTimer() { if (this._timer) { clearInterval(this._timer); } const { flushInterval } = this._config; const that = this; this._timer = setInterval(() => { that._sendFromBuffer(); }, flushInterval); } /** * Record event * @param eventType - type of the event action. e.g. "Click" * @param properties - properties of the event * @return Promise */ public async record(params): Promise<boolean> { const credentials = await this._getCredentials(); if (!credentials) return Promise.resolve(false); Object.assign(params, { config: this._config, credentials, sentAt: new Date(), }); const { eventType, properties } = params.event; if (eventType === IDENTIFY_EVENT) { this._sessionManager.updateSessionInfo( properties && properties.userId ? properties.userId : '', this._sessionInfo ); return; } else if (!isEmpty(params.event.userId)) { this._sessionManager.updateSessionInfo( params.event.userId, this._sessionInfo ); } const requestParams: RequestParams = this.generateRequestParams( params, this._sessionInfo ); if (eventType === 'MediaAutoTrack') { if (this._isBrowser) { if ( !isEmpty( get(requestParams, 'eventData.properties.domElementId', null) ) ) { const isLoaded = await this.isElementFullyLoaded( this.loadElement, requestParams.eventData.properties['domElementId'], 500, 5 ); if (isLoaded) { new MediaAutoTrack(requestParams, this); } else { logger.debug('Cannot find the media element.'); } } else { logger.debug( "Missing domElementId field in 'properties' for MediaAutoTrack event type." ); } } else { logger.debug('MediaAutoTrack only for browser'); } return; } return this.putToBuffer(requestParams); } private loadElement(domId): Promise<boolean> { return new Promise((resolve, reject) => { if ( document.getElementById(domId) && document.getElementById(domId).clientHeight ) { return resolve(true); } else { return reject(true); } }); } private isElementFullyLoaded( operation, params, delay, times ): Promise<boolean> { const wait = ms => new Promise(r => setTimeout(r, ms)); return new Promise((resolve, reject) => { return operation(params) .then(resolve) .catch(reason => { if (times - 1 > 0) { return wait(delay) .then( this.isElementFullyLoaded.bind( null, operation, params, delay, times - 1 ) ) .then(resolve) .catch(reject); } return reject(reason); }); }); } /** * get the category of the plugin */ public getCategory(): string { return 'Analytics'; } /** * get provider name of the plugin */ public getProviderName(): string { return 'AmazonPersonalize'; } /** * configure the plugin * @param {Object} config - configuration */ public configure(config): object { logger.debug('configure Analytics', config); const conf = config ? config : {}; this._config = Object.assign({}, this._config, conf); if (!isEmpty(this._config.trackingId)) { this._sessionInfo = this._sessionManager.retrieveSessionInfo( this._config.trackingId ); } this._setupTimer(); return this._config; } /** * Generate the requestParams from customer input params and sessionInfo * @private * @param eventData - customer input for event data * @param api - api name * @return RequestParams - wrapper object with all information required for make request */ private generateRequestParams(params, sessionInfo): RequestParams { const requestParams = <RequestParams>{}; const { eventType, properties } = params.event; requestParams.eventData = { eventType, properties }; requestParams.sessionInfo = sessionInfo; requestParams.sentAt = params.sentAt; requestParams.credentials = params.credentials; requestParams.config = params.config; return requestParams; } /** * record an event * @param {Object} params - the params of an event */ private _sendEvents(group) { const groupLen = group.length; if (groupLen === 0) { logger.debug('events array is empty, directly return'); return; } const { config, credentials, sessionInfo } = group[0]; const initClients = this._init(config, credentials); if (!initClients) return false; if (groupLen > 0) { const events: RecordEventPayload[] = []; for (let i = 0; i < groupLen; i += 1) { const params: RequestParams = group.shift(); const eventPayload: RecordEventPayload = this._generateSingleRecordPayload(params, sessionInfo); events.push(eventPayload); } const payload = <PutEventsCommandInput>{}; payload.trackingId = sessionInfo.trackingId; payload.sessionId = sessionInfo.sessionId; payload.userId = sessionInfo.userId; payload.eventList = []; events.forEach(event => { // @ts-ignore payload.eventList.push(event); }); const command: PutEventsCommand = new PutEventsCommand(payload); this._personalize.send(command, err => { if (err) logger.debug('Failed to call putEvents in Personalize', err); else logger.debug('Put events'); }); } } /** * Put event into buffer * @private * @param params - params for the event recording */ private putToBuffer(params: RequestParams) { if (this._buffer.length < this._config.flushSize) { this._buffer.push(params); } else { this._buffer.push(params); this._sendFromBuffer(); } return Promise.resolve(true); } /** * flush the buffer and batch sending the request * @private * @param eventsParams - the buffer for cache the payload */ private _sendFromBuffer() { const size = this._buffer.length; if (size <= 0) return; const eventsGroups = []; let preCred = null; let group = []; for (let i = 0; i < size; i += 1) { const currRequestParams: RequestParams = this._buffer.shift(); const cred = currRequestParams.credentials; const sessionInfo = currRequestParams.sessionInfo; if (i === 0) { group.push(currRequestParams); preCred = cred; } else { if ( isEqual(sessionInfo, this._sessionInfo) && cred.sessionToken === preCred.sessionToken && cred.identityId === preCred.identityId ) { logger.debug('no change for cred, put event in the same group'); group.push(currRequestParams); } else { eventsGroups.push(group); group = []; group.push(currRequestParams); preCred = cred; this._sessionInfo = sessionInfo; } } } eventsGroups.push(group); eventsGroups.map(group => { this._sendEvents(group); }); } /** * Generate the record payload for single event * @private * @param params - RequestParams */ private _generateSingleRecordPayload( params: RequestParams, sessionInfo ): RecordEventPayload { const { eventData, sentAt } = params; const trackPayload = <RecordEventPayload>{}; trackPayload.sentAt = sentAt; trackPayload.properties = eventData.properties && JSON.stringify(eventData.properties); trackPayload.eventId = this._sessionManager.getTimerKey() + sessionInfo.sessionId; trackPayload.eventType = eventData.eventType; return trackPayload; } /** * Initialize the personalize client * @private * @param params - RequestParams */ private _init(config, credentials) { logger.debug('init clients'); if ( this._personalize && this._config.credentials && this._config.credentials.sessionToken === credentials.sessionToken && this._config.credentials.identityId === credentials.identityId ) { logger.debug('no change for analytics config, directly return from init'); return true; } this._config.credentials = credentials; const { region } = config; logger.debug('initialize personalize with credentials', credentials); this._personalize = new PersonalizeEventsClient({ region, credentials, customUserAgent: getAnalyticsUserAgent(AnalyticsAction.Record), }); return true; } /** * check if current credentials exists * @private */ private _getCredentials() { const that = this; return Credentials.get() .then(credentials => { if (!credentials) return null; logger.debug('set credentials for analytics', that._config.credentials); return Credentials.shear(credentials); }) .catch(err => { logger.debug('ensure credentials error', err); return null; }); } }