@aws-amplify/analytics
Version:
Analytics category of aws-amplify
399 lines (369 loc) • 10.3 kB
text/typescript
// 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;
});
}
}