UNPKG

@aws-amplify/analytics

Version:

Analytics category of aws-amplify

576 lines • 28.8 kB
"use strict"; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); var cache_1 = require("@aws-amplify/cache"); var core_1 = require("@aws-amplify/core"); var pinpoint_1 = require("@aws-amplify/core/internals/aws-clients/pinpoint"); var uuid_1 = require("uuid"); var UserAgent_1 = require("../utils/UserAgent"); var EventBuffer_1 = tslib_1.__importDefault(require("./EventBuffer")); var AMPLIFY_SYMBOL = (typeof Symbol !== 'undefined' && typeof Symbol.for === 'function' ? Symbol.for('amplify_default') : '@@amplify_default'); var dispatchAnalyticsEvent = function (event, data) { core_1.Hub.dispatch('analytics', { event: event, data: data }, 'Analytics', AMPLIFY_SYMBOL); }; var logger = new core_1.ConsoleLogger('AWSPinpointProvider'); var RETRYABLE_CODES = [429, 500]; var ACCEPTED_CODES = [202]; var FORBIDDEN_CODE = 403; var MOBILE_SERVICE_NAME = 'mobiletargeting'; var EXPIRED_TOKEN_CODE = 'ExpiredTokenException'; var UPDATE_ENDPOINT = '_update_endpoint'; var SESSION_START = '_session.start'; var SESSION_STOP = '_session.stop'; var BEACON_SUPPORTED = typeof navigator !== 'undefined' && navigator && typeof navigator.sendBeacon === 'function'; // events buffer var BUFFER_SIZE = 1000; var FLUSH_SIZE = 100; var FLUSH_INTERVAL = 5 * 1000; // 5s var RESEND_LIMIT = 5; // params: { event: {name: , .... }, timeStamp, config, resendLimits } var AWSPinpointProvider = /** @class */ (function () { function AWSPinpointProvider(config) { this._endpointGenerating = true; this._endpointUpdateInProgress = false; this._buffer = null; this._endpointBuffer = []; this._config = config ? config : {}; this._config.bufferSize = this._config.bufferSize || BUFFER_SIZE; this._config.flushSize = this._config.flushSize || FLUSH_SIZE; this._config.flushInterval = this._config.flushInterval || FLUSH_INTERVAL; this._config.resendLimit = this._config.resendLimit || RESEND_LIMIT; this._clientInfo = core_1.ClientDevice.clientInfo(); } /** * get the category of the plugin */ AWSPinpointProvider.prototype.getCategory = function () { return AWSPinpointProvider.category; }; /** * get provider name of the plugin */ AWSPinpointProvider.prototype.getProviderName = function () { return AWSPinpointProvider.providerName; }; /** * configure the plugin * @param {Object} config - configuration */ AWSPinpointProvider.prototype.configure = function (config) { var _this = this; logger.debug('configure Analytics', config); var conf = config || {}; this._config = Object.assign({}, this._config, conf); // If autoSessionRecord is enabled, we need to wait for the endpoint to be // updated before sending any events. See `sendEvents` in `Analytics.ts` this._endpointGenerating = !!config['autoSessionRecord']; if (this._config.appId && !this._config.disabled) { if (!this._config.endpointId) { var cacheKey = this.getProviderName() + '_' + this._config.appId; this._getEndpointId(cacheKey) .then(function (endpointId) { logger.debug('setting endpoint id from the cache', endpointId); _this._config.endpointId = endpointId; dispatchAnalyticsEvent('pinpointProvider_configured', null); }) .catch(function (err) { logger.debug('Failed to generate endpointId', err); }); } else { dispatchAnalyticsEvent('pinpointProvider_configured', null); } } else { this._flushBuffer(); } return this._config; }; /** * record an event * @param {Object} params - the params of an event */ AWSPinpointProvider.prototype.record = function (params, handlers) { return tslib_1.__awaiter(this, void 0, void 0, function () { var credentials, timestamp; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: logger.debug('_public record', params); return [4 /*yield*/, this._getCredentials()]; case 1: credentials = _a.sent(); if (!credentials || !this._config.appId || !this._config.region) { logger.debug('cannot send events without credentials, applicationId or region'); return [2 /*return*/, handlers.reject(new Error('No credentials, applicationId or region'))]; } this._init(credentials); timestamp = new Date().getTime(); // attach the session and eventId this._generateSession(params); params.event.eventId = uuid_1.v1(); Object.assign(params, { timestamp: timestamp, config: this._config }); if (params.event.immediate) { return [2 /*return*/, this._send(params, handlers)]; } else { this._putToBuffer(params, handlers); } return [2 /*return*/]; } }); }); }; AWSPinpointProvider.prototype._sendEndpointUpdate = function (endpointObject) { return tslib_1.__awaiter(this, void 0, void 0, function () { var next; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: if (this._endpointUpdateInProgress) { this._endpointBuffer.push(endpointObject); return [2 /*return*/]; } this._endpointUpdateInProgress = true; return [4 /*yield*/, this._updateEndpoint(endpointObject)]; case 1: _a.sent(); next = this._endpointBuffer.shift(); this._endpointUpdateInProgress = false; next && this._sendEndpointUpdate(next); return [2 /*return*/]; } }); }); }; /** * @private * @param params - params for event recording * Put events into buffer */ AWSPinpointProvider.prototype._putToBuffer = function (params, handlers) { var _a; if (params.event.name === UPDATE_ENDPOINT) { this._sendEndpointUpdate({ params: params, handlers: handlers }); return; } (_a = this._buffer) === null || _a === void 0 ? void 0 : _a.push({ params: params, handlers: handlers }); }; AWSPinpointProvider.prototype._generateSession = function (params) { this._sessionId = this._sessionId || uuid_1.v1(); var event = params.event; switch (event.name) { case SESSION_START: // refresh the session id and session start time this._sessionStartTimestamp = new Date().getTime(); this._sessionId = uuid_1.v1(); event.session = { Id: this._sessionId, StartTimestamp: new Date(this._sessionStartTimestamp).toISOString(), }; break; case SESSION_STOP: var stopTimestamp = new Date().getTime(); this._sessionStartTimestamp = this._sessionStartTimestamp || new Date().getTime(); this._sessionId = this._sessionId || uuid_1.v1(); event.session = { Id: this._sessionId, Duration: stopTimestamp - this._sessionStartTimestamp, StartTimestamp: new Date(this._sessionStartTimestamp).toISOString(), StopTimestamp: new Date(stopTimestamp).toISOString(), }; this._sessionId = undefined; this._sessionStartTimestamp = undefined; break; default: this._sessionStartTimestamp = this._sessionStartTimestamp || new Date().getTime(); this._sessionId = this._sessionId || uuid_1.v1(); event.session = { Id: this._sessionId, StartTimestamp: new Date(this._sessionStartTimestamp).toISOString(), }; } }; AWSPinpointProvider.prototype._send = function (params, handlers) { return tslib_1.__awaiter(this, void 0, void 0, function () { var event; return tslib_1.__generator(this, function (_a) { event = params.event; switch (event.name) { case UPDATE_ENDPOINT: return [2 /*return*/, this._updateEndpoint({ params: params, handlers: handlers })]; case SESSION_STOP: return [2 /*return*/, this._pinpointSendStopSession(params, handlers)]; default: return [2 /*return*/, this._pinpointPutEvents(params, handlers)]; } return [2 /*return*/]; }); }); }; AWSPinpointProvider.prototype._generateBatchItemContext = function (params) { var _a, _b; var event = params.event, timestamp = params.timestamp, config = params.config; var name = event.name, attributes = event.attributes, metrics = event.metrics, eventId = event.eventId, session = event.session; var appId = config.appId, endpointId = config.endpointId; var endpointContext = {}; return { ApplicationId: appId, EventsRequest: { BatchItem: (_a = {}, _a[endpointId] = { Endpoint: endpointContext, Events: (_b = {}, _b[eventId] = { EventType: name, Timestamp: new Date(timestamp).toISOString(), Attributes: attributes, Metrics: metrics, Session: session, }, _b), }, _a), }, }; }; AWSPinpointProvider.prototype._pinpointPutEvents = function (params, handlers) { var _a, _b, _c, _d, _e; return tslib_1.__awaiter(this, void 0, void 0, function () { var eventId, endpointId, eventParams, _f, credentials, region, data, _g, StatusCode, Message, err_1; return tslib_1.__generator(this, function (_h) { switch (_h.label) { case 0: eventId = params.event.eventId, endpointId = params.config.endpointId; eventParams = this._generateBatchItemContext(params); _h.label = 1; case 1: _h.trys.push([1, 3, , 4]); _f = this._config, credentials = _f.credentials, region = _f.region; return [4 /*yield*/, pinpoint_1.putEvents({ credentials: credentials, region: region, userAgentValue: UserAgent_1.getAnalyticsUserAgentString(core_1.AnalyticsAction.Record), }, eventParams)]; case 2: data = _h.sent(); _g = (_e = (_d = (_c = (_b = (_a = data.EventsResponse) === null || _a === void 0 ? void 0 : _a.Results) === null || _b === void 0 ? void 0 : _b[endpointId]) === null || _c === void 0 ? void 0 : _c.EventsItemResponse) === null || _d === void 0 ? void 0 : _d[eventId]) !== null && _e !== void 0 ? _e : {}, StatusCode = _g.StatusCode, Message = _g.Message; if (StatusCode && ACCEPTED_CODES.includes(StatusCode)) { logger.debug('record event success. ', data); return [2 /*return*/, handlers.resolve(data)]; } else if (StatusCode && RETRYABLE_CODES.includes(StatusCode)) { // TODO: v6 integrate retry to the service handler retryDecider this._retry(params, handlers); } else { logger.error("Event " + eventId + " is not accepted, the error is " + Message); return [2 /*return*/, handlers.reject(data)]; } return [3 /*break*/, 4]; case 3: err_1 = _h.sent(); this._eventError(err_1); return [2 /*return*/, handlers.reject(err_1)]; case 4: return [2 /*return*/]; } }); }); }; AWSPinpointProvider.prototype._pinpointSendStopSession = function (params, handlers) { if (!BEACON_SUPPORTED) { this._pinpointPutEvents(params, handlers); return; } var eventParams = this._generateBatchItemContext(params); var region = this._config.region; var ApplicationId = eventParams.ApplicationId, EventsRequest = eventParams.EventsRequest; var accessInfo = { secret_key: this._config.credentials.secretAccessKey, access_key: this._config.credentials.accessKeyId, session_token: this._config.credentials.sessionToken, }; var url = "https://pinpoint." + region + ".amazonaws.com/v1/apps/" + ApplicationId + "/events/legacy"; var body = JSON.stringify(EventsRequest); var method = 'POST'; var request = { url: url, body: body, method: method, }; var serviceInfo = { region: region, service: MOBILE_SERVICE_NAME }; var requestUrl = core_1.Signer.signUrl(request, accessInfo, serviceInfo); var success = navigator.sendBeacon(requestUrl, body); if (success) { return handlers.resolve('sendBeacon success'); } return handlers.reject('sendBeacon failure'); }; AWSPinpointProvider.prototype._retry = function (params, handlers) { var resendLimit = params.config.resendLimit; // For backward compatibility params.resendLimit = typeof params.resendLimit === 'number' ? params.resendLimit : resendLimit; if (params.resendLimit-- > 0) { logger.debug("resending event " + params.eventName + " with " + params.resendLimit + " retry times left"); this._pinpointPutEvents(params, handlers); } else { logger.debug("retry times used up for event " + params.eventName); } }; AWSPinpointProvider.prototype._updateEndpoint = function (endpointObject) { return tslib_1.__awaiter(this, void 0, void 0, function () { var params, handlers, config, event, appId, endpointId, request, update_params, _a, credentials, region, data, err_2, failureData; return tslib_1.__generator(this, function (_b) { switch (_b.label) { case 0: params = endpointObject.params, handlers = endpointObject.handlers; config = params.config, event = params.event; appId = config.appId, endpointId = config.endpointId; request = this._endpointRequest(config, core_1.transferKeyToLowerCase(event, [], ['attributes', 'userAttributes', 'Attributes', 'UserAttributes'])); update_params = { ApplicationId: appId, EndpointId: endpointId, EndpointRequest: request, }; _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); _a = this._config, credentials = _a.credentials, region = _a.region; return [4 /*yield*/, pinpoint_1.updateEndpoint({ credentials: credentials, region: region, userAgentValue: UserAgent_1.getAnalyticsUserAgentString(core_1.AnalyticsAction.UpdateEndpoint), }, update_params)]; case 2: data = _b.sent(); logger.debug('updateEndpoint success', data); this._endpointGenerating = false; this._resumeBuffer(); handlers.resolve(data); return [2 /*return*/]; case 3: err_2 = _b.sent(); failureData = { err: err_2, update_params: update_params, endpointObject: endpointObject, }; return [2 /*return*/, this._handleEndpointUpdateFailure(failureData)]; case 4: return [2 /*return*/]; } }); }); }; AWSPinpointProvider.prototype._handleEndpointUpdateFailure = function (failureData) { return tslib_1.__awaiter(this, void 0, void 0, function () { var err, endpointObject, statusCode, exponential; return tslib_1.__generator(this, function (_a) { err = failureData.err, endpointObject = failureData.endpointObject; statusCode = err.$metadata && err.$metadata.httpStatusCode; logger.debug('updateEndpoint error', err); switch (statusCode) { case FORBIDDEN_CODE: return [2 /*return*/, this._handleEndpointUpdateForbidden(failureData)]; default: if (RETRYABLE_CODES.includes(statusCode)) { exponential = true; return [2 /*return*/, this._retryEndpointUpdate(endpointObject, exponential)]; } logger.error('updateEndpoint failed', err); endpointObject.handlers.reject(err); } return [2 /*return*/]; }); }); }; AWSPinpointProvider.prototype._handleEndpointUpdateForbidden = function (failureData) { var err = failureData.err, endpointObject = failureData.endpointObject; var code = err.code, retryable = err.retryable; if (code !== EXPIRED_TOKEN_CODE && !retryable) { return endpointObject.handlers.reject(err); } this._retryEndpointUpdate(endpointObject); }; AWSPinpointProvider.prototype._retryEndpointUpdate = function (endpointObject, exponential) { if (exponential === void 0) { exponential = false; } logger.debug('_retryEndpointUpdate', endpointObject); var params = endpointObject.params; // TODO: implement retry with exp back off once exp function is available var resendLimit = params.config.resendLimit; params.resendLimit = typeof params.resendLimit === 'number' ? params.resendLimit : resendLimit; if (params.resendLimit-- > 0) { logger.debug("resending endpoint update " + params.event.eventId + " with " + params.resendLimit + " retry attempts remaining"); // insert at the front of endpointBuffer this._endpointBuffer.length ? this._endpointBuffer.unshift(endpointObject) : this._updateEndpoint(endpointObject); return; } logger.warn("resending endpoint update " + params.event.eventId + " failed after " + params.config.resendLimit + " attempts"); if (this._endpointGenerating) { logger.error('Initial endpoint update failed. '); } }; /** * @private * @param config * Configure credentials and init buffer */ AWSPinpointProvider.prototype._init = function (credentials) { return tslib_1.__awaiter(this, void 0, void 0, function () { var identityId; return tslib_1.__generator(this, function (_a) { logger.debug('init provider'); if (this._config.credentials && this._config.credentials.sessionToken === credentials.sessionToken && this._config.credentials.identityId === credentials.identityId) { logger.debug('no change for aws credentials, directly return from init'); return [2 /*return*/]; } identityId = this._config.credentials ? this._config.credentials.identityId : null; this._config.credentials = credentials; if (!this._bufferExists() || identityId !== credentials.identityId) { // if the identity has changed, flush the buffer and instantiate a new one // this will cause the old buffer to send any remaining events // with the old credentials and then stop looping and shortly thereafter get picked up by GC this._initBuffer(); } return [2 /*return*/]; }); }); }; AWSPinpointProvider.prototype._bufferExists = function () { return this._buffer && this._buffer instanceof EventBuffer_1.default; }; AWSPinpointProvider.prototype._initBuffer = function () { if (this._bufferExists()) { this._flushBuffer(); } this._buffer = new EventBuffer_1.default(this._config); // if the first endpoint update hasn't yet resolved pause the buffer to // prevent race conditions. It will be resumed as soon as that request succeeds if (this._endpointGenerating) { this._buffer.pause(); } }; AWSPinpointProvider.prototype._flushBuffer = function () { var _a; if (this._bufferExists()) { (_a = this._buffer) === null || _a === void 0 ? void 0 : _a.flush(); this._buffer = null; } }; AWSPinpointProvider.prototype._resumeBuffer = function () { var _a; if (this._bufferExists()) { (_a = this._buffer) === null || _a === void 0 ? void 0 : _a.resume(); } }; AWSPinpointProvider.prototype._getEndpointId = function (cacheKey) { return tslib_1.__awaiter(this, void 0, void 0, function () { var endpointId, ttl, expiration; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, cache_1.Cache.getItem(cacheKey)]; case 1: endpointId = _a.sent(); logger.debug('endpointId from cache', endpointId, 'type', typeof endpointId); if (!endpointId) { endpointId = uuid_1.v1(); ttl = 1000 * 60 * 60 * 24 * 365 * 100; expiration = new Date().getTime() + ttl; cache_1.Cache.setItem(cacheKey, endpointId, { expires: expiration, priority: 1, }); } return [2 /*return*/, endpointId]; } }); }); }; /** * EndPoint request * @return {Object} - The request of updating endpoint */ AWSPinpointProvider.prototype._endpointRequest = function (config, event) { var credentials = config.credentials; var clientInfo = this._clientInfo || {}; var clientContext = config.clientContext || {}; // for now we have three different ways for default endpoint configurations // clientInfo // clientContext (deprecated) // config.endpoint var defaultEndpointConfig = config.endpoint || {}; var demographicByClientInfo = { appVersion: clientInfo.appVersion, make: clientInfo.make, model: clientInfo.model, modelVersion: clientInfo.version, platform: clientInfo.platform, }; // for backward compatibility var clientId = clientContext.clientId, appTitle = clientContext.appTitle, appVersionName = clientContext.appVersionName, appVersionCode = clientContext.appVersionCode, appPackageName = clientContext.appPackageName, demographicByClientContext = tslib_1.__rest(clientContext, ["clientId", "appTitle", "appVersionName", "appVersionCode", "appPackageName"]); var channelType = event.address ? clientInfo.platform === 'android' ? 'GCM' : 'APNS' : undefined; var tmp = tslib_1.__assign(tslib_1.__assign(tslib_1.__assign({ channelType: channelType, requestId: uuid_1.v1(), effectiveDate: new Date().toISOString() }, defaultEndpointConfig), event), { attributes: tslib_1.__assign(tslib_1.__assign({}, defaultEndpointConfig.attributes), event.attributes), demographic: tslib_1.__assign(tslib_1.__assign(tslib_1.__assign(tslib_1.__assign({}, demographicByClientInfo), demographicByClientContext), defaultEndpointConfig.demographic), event.demographic), location: tslib_1.__assign(tslib_1.__assign({}, defaultEndpointConfig.location), event.location), metrics: tslib_1.__assign(tslib_1.__assign({}, defaultEndpointConfig.metrics), event.metrics), user: { userId: event.userId || defaultEndpointConfig.userId || credentials.identityId, userAttributes: tslib_1.__assign(tslib_1.__assign({}, defaultEndpointConfig.userAttributes), event.userAttributes), } }); // eliminate unnecessary params var userId = tmp.userId, userAttributes = tmp.userAttributes, name = tmp.name, session = tmp.session, eventId = tmp.eventId, immediate = tmp.immediate, ret = tslib_1.__rest(tmp, ["userId", "userAttributes", "name", "session", "eventId", "immediate"]); return core_1.transferKeyToUpperCase(ret, [], ['metrics', 'userAttributes', 'attributes']); }; AWSPinpointProvider.prototype._eventError = function (err) { logger.error('record event failed.', err); logger.warn("Please ensure you have updated your Pinpoint IAM Policy " + "with the Action: \"mobiletargeting:PutEvents\" " + "in order to record events"); }; AWSPinpointProvider.prototype._getCredentials = function () { return tslib_1.__awaiter(this, void 0, void 0, function () { var credentials, err_3; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, core_1.Credentials.get()]; case 1: credentials = _a.sent(); if (!credentials) return [2 /*return*/, null]; logger.debug('set credentials for analytics', credentials); return [2 /*return*/, core_1.Credentials.shear(credentials)]; case 2: err_3 = _a.sent(); logger.debug('ensure credentials error', err_3); return [2 /*return*/, null]; case 3: return [2 /*return*/]; } }); }); }; AWSPinpointProvider.category = 'Analytics'; AWSPinpointProvider.providerName = 'AWSPinpoint'; return AWSPinpointProvider; }()); exports.AWSPinpointProvider = AWSPinpointProvider; //# sourceMappingURL=AWSPinpointProvider.js.map