├── .babelrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── index.js
├── package-lock.json
├── package.json
└── src
├── MobileAnalyticsClient.js
├── MobileAnalyticsSession.js
├── MobileAnalyticsSessionManager.js
├── MobileAnalyticsUtilities.js
├── Storage.js
├── StorageKeys.js
└── __tests__
├── MobileAnalyticsSession.test.js
├── MobileAnalyticsUtilities.test.js
└── Storage.test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-native"]
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | project.xcworkspace
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 |
33 | # node.js
34 | #
35 | node_modules/
36 | npm-debug.log
37 | yarn-error.log
38 |
39 | # BUCK
40 | buck-out/
41 | \.buckd/
42 | *.keystore
43 |
44 | # fastlane
45 | #
46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
47 | # screenshots whenever they are needed.
48 | # For more information about the recommended setup visit:
49 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
50 |
51 | fastlane/report.xml
52 | fastlane/Preview.html
53 | fastlane/screenshots
54 |
55 | /.idea
56 | node_modules/*
57 |
58 | /android/app/innmensa.keystore
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "7"
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 innFactory
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-aws-mobile-analytics
2 |
3 | [](https://www.npmjs.com/package/react-native-aws-mobile-analytics)
4 | [](https://www.npmjs.com/package/react-native-aws-mobile-analytics)
5 | [](https://www.npmjs.com/package/react-native-aws-mobile-analytics)
6 |
7 | A react-native module for using Amazon's AWS Mobile Analytics with the aws-sdk
8 |
9 | Highly inspirated by the javascript version [aws-sdk-mobile-analytics-js](https://github.com/aws/aws-sdk-mobile-analytics-js)
10 |
11 |
12 |
13 | ## Usage
14 | Add react-native-aws-mobile-analytics
15 | ```
16 | npm install --save react-native-aws-mobile-analytics
17 | ```
18 |
19 | Add Permission for Network State to your `AndroidManifest.xml`
20 | ```
21 |
22 | ```
23 |
24 |
25 | Add aws-sdk
26 | ```
27 | npm install --save aws-sdk
28 | ```
29 |
30 | react-native-aws-mobile-analytics needs the [react-native-device-info](https://github.com/rebeccahughes/react-native-device-info) package as dependency for an unique client id. Make sure it is correct linked. You may have to call react-native link:
31 | ```
32 | react-native link
33 | ```
34 |
35 |
36 |
37 | Create file `MobileAnalytics.js` where you can do the configuration:
38 | ```javascript
39 | import AWS from "aws-sdk/dist/aws-sdk-react-native";
40 | import AMA from "react-native-aws-mobile-analytics";
41 | import {
42 | Platform,
43 | } from 'react-native';
44 |
45 | AWS.config.region = 'us-east-1';
46 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({
47 | region: 'us-east-1',
48 | IdentityPoolId: 'us-east-1:4c6d17ff-9eb1-4805-914d-8db8536ab130',
49 | });
50 |
51 |
52 | let appId_PROD = "2e9axxxxxxx742c5a35axxxxxxxxx2f";
53 | let appId_DEV = "xxxx44be23c4xxx9xxxxxxxxxxxxx3fb";
54 |
55 | let options = {
56 | appId: __DEV__?appId_DEV:appId_PROD,
57 | platform : Platform.OS === 'ios' ? 'iPhoneOS' : 'Android',
58 | //logger: console // comment in for debugging
59 | };
60 |
61 | const MobileAnalyticsClient = new AMA.Manager(options);
62 | export default MobileAnalyticsClient;
63 | ```
64 |
65 |
66 |
67 | Before you could send the first event you have to call `MobileAnalyticsClient.initialize(callback)` and wait for the callback. You can handle that in your root component like following:
68 | ```javascript
69 | import React from "react";
70 | import MobileAnalyticsClient from "./MobileAnalytics";
71 | import SomeComponent from "./components/SomeComponent";
72 |
73 | export default class App extends React.Component {
74 | constructor(){
75 | super();
76 |
77 | this.state = {
78 | isMobileAnalyticsLoading: true,
79 | };
80 |
81 | MobileAnalyticsClient.initialize(()=>this.setState({isMobileAnalyticsLoading: false}));
82 | }
83 | render() {
84 | if(this.state.isMobileAnalyticsLoading){
85 | return null;
86 | }
87 | return (
88 |
89 | );
90 | }
91 | };
92 | ```
93 |
94 |
95 |
96 | Now you are able to send event in all your components:
97 | ```javascript
98 | import MobileAnalyticsClient from "../MobileAnalytics";
99 |
100 |
101 | export default class SomeComponent extends Component {
102 | constructor() {
103 | super();
104 | // send a custom event
105 | MobileAnalyticsClient.recordEvent('analyticsDemo', { 'attribute_1': 'main', 'attribute_2': 'page' }, { 'metric_1': 1 });
106 | }
107 | }
108 | ```
109 |
110 |
111 |
112 | #### Checkout the full example:
113 | [react-native-aws-mobile-analytics-demo](https://github.com/innFactory/react-native-aws-mobile-analytics-demo)
114 |
115 |
116 |
117 | ## Contributors
118 |
119 | [Anton Spöck](https://github.com/spoeck)
120 |
121 | Powered by [innFactory](https://innfactory.de/)
122 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @module AMA
3 | * @description The global namespace for Amazon Mobile Analytics
4 | * @see AMA.Manager
5 | * @see AMA.Client
6 | * @see AMA.Session
7 | */
8 | import Client from "./src/MobileAnalyticsClient";
9 | import Util from "./src/MobileAnalyticsUtilities";
10 | import StorageKeys from "./src/StorageKeys";
11 | import Storage from "./src/Storage";
12 | import Session from "./src/MobileAnalyticsSession";
13 | import Manager from "./src/MobileAnalyticsSessionManager";
14 | AMA = {
15 | Client,
16 | Util,
17 | StorageKeys,
18 | Storage,
19 | Session,
20 | Manager
21 | }
22 | export default AMA;
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-aws-mobile-analytics",
3 | "version": "0.1.2",
4 | "description": "A react-native module for using Amazon's AWS Mobile Analytics with the aws-sdk",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/innFactory/react-native-aws-mobile-analytics"
9 | },
10 | "dependencies": {
11 | "aws-sdk": "^2.71.0",
12 | "react-native-device-info": "^0.10.2"
13 | },
14 | "devDependencies": {
15 | "babel-jest": "^19.0.0",
16 | "babel-preset-react-native": "1.9.1",
17 | "jest": "^20.0.4",
18 | "react": "16.0.0-alpha.6",
19 | "react-native": "^0.43.0",
20 | "react-test-renderer": "16.0.0-alpha.6",
21 | "regenerator-runtime": "^0.10.5"
22 | },
23 | "scripts": {
24 | "test": "jest"
25 | },
26 | "jest": {
27 | "preset": "react-native",
28 | "globals": {
29 | "__DEV__": true
30 | }
31 | },
32 | "keywords": [
33 | "react-native",
34 | "aws",
35 | "mobile",
36 | "analytics",
37 | "ama",
38 | "mobile-analytics",
39 | "amazon"
40 | ],
41 | "author": "Anton Spöck (https://github.com/spoeck)",
42 | "license": "MIT"
43 | }
44 |
--------------------------------------------------------------------------------
/src/MobileAnalyticsClient.js:
--------------------------------------------------------------------------------
1 | import AWS from 'aws-sdk/dist/aws-sdk-react-native';
2 | import Storage from './Storage';
3 | import StorageKeys from './StorageKeys.js';
4 | import Util from'./MobileAnalyticsUtilities';
5 | const DeviceInfo = require('react-native-device-info');
6 |
7 | /**
8 | * @typedef AMA.Client.Options
9 | * @property {string} appId - The Application ID from the Amazon Mobile Analytics Console
10 | * @property {string} [apiVersion=2014-06-05] - The version of the Mobile Analytics API to submit to.
11 | * @property {object} [provider=AWS.config.credentials] - Credentials to use for submitting events.
12 | * **Never check in credentials to source
13 | * control.
14 | * @property {boolean} [autoSubmitEvents=true] - Automatically Submit Events, Default: true
15 | * @property {number} [autoSubmitInterval=10000] - Interval to try to submit events in ms,
16 | * Default: 10s
17 | * @property {number} [batchSizeLimit=256000] - Batch Size in Bytes, Default: 256Kb
18 | * @property {AMA.Client.SubmitCallback} [submitCallback=] - Callback function that is executed when events are
19 | * successfully submitted
20 | * @property {AMA.Client.Attributes} [globalAttributes=] - Attribute to be applied to every event, may be
21 | * overwritten with a different value when recording events.
22 | * @property {AMA.Client.Metrics} [globalMetrics=] - Metric to be applied to every event, may be overwritten
23 | * with a different value when recording events.
24 | * @property {string} [clientId=GUID()] - A unique identifier representing this installation instance
25 | * of your app. This will be managed and persisted by the SDK
26 | * by default.
27 | * @property {string} [appTitle=] - The title of your app. For example, My App.
28 | * @property {string} [appVersionName=] - The version of your app. For example, V2.0.
29 | * @property {string} [appVersionCode=] - The version code for your app. For example, 3.
30 | * @property {string} [appPackageName=] - The name of your package. For example, com.example.my_app.
31 | * @property {string} [platform=] - The operating system of the device. For example, iPhoneOS.
32 | * @property {string} [plaformVersion=] - The version of the operating system of the device.
33 | * For example, 4.0.4.
34 | * @property {string} [model=] - The model of the device. For example, Nexus.
35 | * @property {string} [make=] - The manufacturer of the device. For example, Samsung.
36 | * @property {string} [locale=] - The locale of the device. For example, en_US.
37 | * @property {AMA.Client.Logger} [logger=] - Object of logger functions
38 | * @property {AMA.Storage} [storage=] - Storage client to persist events, will create a new AMA.Storage if not provided
39 | * @property {Object} [clientOptions=] - Low level client options to be passed to the AWS.MobileAnalytics low level SDK
40 | */
41 |
42 | /**
43 | * @typedef AMA.Client.Logger
44 | * @description Uses Javascript Style log levels, one function for each level. Basic usage is to pass the console object
45 | * which will output directly to browser developer console.
46 | * @property {Function} [log=] - Logger for client log level messages
47 | * @property {Function} [info=] - Logger for interaction level messages
48 | * @property {Function} [warn=] - Logger for warn level messages
49 | * @property {Function} [error=] - Logger for error level messages
50 | */
51 | /**
52 | * @typedef AMA.Client.Attributes
53 | * @type {object}
54 | * @description A collection of key-value pairs that give additional context to the event. The key-value pairs are
55 | * specified by the developer.
56 | */
57 | /**
58 | * @typedef AMA.Client.Metrics
59 | * @type {object}
60 | * @description A collection of key-value pairs that gives additional measurable context to the event. The pairs
61 | * specified by the developer.
62 | */
63 | /**
64 | * @callback AMA.Client.SubmitCallback
65 | * @param {Error} err
66 | * @param {Null} data
67 | * @param {string} batchId
68 | */
69 | /**
70 | * @typedef AMA.Client.Event
71 | * @type {object}
72 | * @description A JSON object representing an event occurrence in your app and consists of the following:
73 | * @property {string} eventType - A name signifying an event that occurred in your app. This is used for grouping and
74 | * aggregating like events together for reporting purposes.
75 | * @property {string} timestamp - The time the event occurred in ISO 8601 standard date time format.
76 | * For example, 2014-06-30T19:07:47.885Z
77 | * @property {AMA.Client.Attributes} [attributes=] - A collection of key-value pairs that give additional context to
78 | * the event. The key-value pairs are specified by the developer.
79 | * This collection can be empty or the attribute object can be omitted.
80 | * @property {AMA.Client.Metrics} [metrics=] - A collection of key-value pairs that gives additional measurable context
81 | * to the event. The pairs specified by the developer.
82 | * @property {AMA.Session} session - Describes the session. Session information is required on ALL events.
83 | */
84 | /**
85 | * @name AMA.Client
86 | * @namespace AMA.Client
87 | * @constructor
88 | * @param {AMA.Client.Options} options - A configuration map for the AMA.Client
89 | * @returns A new instance of the Mobile Analytics Mid Level Client
90 | */
91 | export default class MobileAnalyticsClient {
92 |
93 | constructor(options, callback) {
94 |
95 | this.options = options || {};
96 | this.options.logger = this.options.logger || {};
97 | this.logger = {
98 | log : this.options.logger.log || Util.NOP,
99 | info : this.options.logger.info || Util.NOP,
100 | warn : this.options.logger.warn || Util.NOP,
101 | error: this.options.logger.error || Util.NOP
102 | };
103 | this.logger.log = this.logger.log.bind(this.options.logger);
104 | this.logger.info = this.logger.info.bind(this.options.logger);
105 | this.logger.warn = this.logger.warn.bind(this.options.logger);
106 | this.logger.error = this.logger.error.bind(this.options.logger);
107 |
108 | this.logger.log('[Function:(AMA)Client Constructor]' +
109 | (options ? '\noptions:' + JSON.stringify(options) : ''));
110 |
111 | this.initOptions(options);
112 |
113 | this.storage = this.options.storage || new Storage(options.appId);
114 | this.storage.reload(()=>{
115 | this.initStorage(callback);
116 | });
117 |
118 |
119 | }
120 |
121 | initStorage(callback) {
122 | this.storage.setLogger(this.logger);
123 |
124 | this.storage.set(
125 | StorageKeys.GLOBAL_ATTRIBUTES,
126 | Util.mergeObjects(this.options.globalAttributes,
127 | this.storage.get(StorageKeys.GLOBAL_ATTRIBUTES) || {})
128 | );
129 | this.storage.set(
130 | StorageKeys.GLOBAL_METRICS,
131 | Util.mergeObjects(this.options.globalMetrics,
132 | this.storage.get(StorageKeys.GLOBAL_METRICS) || {})
133 | );
134 |
135 | this.storage.set(StorageKeys.CLIENT_ID, DeviceInfo.getUniqueID());
136 |
137 | this.StorageKeys = {
138 | 'EVENTS' : 'AWSMobileAnalyticsEventStorage',
139 | 'BATCHES' : 'AWSMobileAnalyticsBatchStorage',
140 | 'BATCH_INDEX': 'AWSMobileAnalyticsBatchIndexStorage'
141 | };
142 |
143 | this.outputs = {};
144 | this.outputs.MobileAnalytics = new AWS.MobileAnalytics(this.options.clientOptions);
145 | this.outputs.timeoutReference = null;
146 | this.outputs.batchesInFlight = {};
147 |
148 | this.outputs.events = this.storage.get(this.StorageKeys.EVENTS) || [];
149 | this.outputs.batches = this.storage.get(this.StorageKeys.BATCHES) || {};
150 | this.outputs.batchIndex = this.storage.get(this.StorageKeys.BATCH_INDEX) || [];
151 |
152 | if (this.options.autoSubmitEvents) {
153 | this.submitEvents();
154 | }
155 |
156 | callback();
157 | }
158 |
159 |
160 | initOptions(options) {
161 |
162 | if (options.appId === undefined) {
163 | this.logger.error('AMA.Client must be initialized with an appId');
164 | return null; //No need to run rest of init since appId is required
165 | }
166 | if (options.platform === undefined) {
167 | this.logger.error('AMA.Client must be initialized with a platform');
168 | }
169 |
170 | this.options.apiVersion = this.options.apiVersion || '2014-06-05';
171 | this.options.provider = this.options.provider || AWS.config.credentials;
172 | this.options.autoSubmitEvents = options.autoSubmitEvents !== false;
173 | this.options.autoSubmitInterval = this.options.autoSubmitInterval || 10000;
174 | this.options.batchSizeLimit = this.options.batchSizeLimit || 256000;
175 | this.options.submitCallback = this.options.submitCallback || Util.NOP;
176 | this.options.globalAttributes = this.options.globalAttributes || {};
177 | this.options.globalMetrics = this.options.globalMetrics || {};
178 | this.options.clientOptions = this.options.clientOptions || {};
179 | this.options.clientOptions.provider = this.options.clientOptions.provider || this.options.provider;
180 | this.options.clientOptions.apiVersion = this.options.clientOptions.apiVersion || this.options.apiVersion;
181 | this.options.clientOptions.correctClockSkew = this.options.clientOptions.correctClockSkew !== false;
182 | this.options.clientOptions.retryDelayOptions = this.options.clientOptions.retryDelayOptions || {};
183 | this.options.clientOptions.retryDelayOptions.base = this.options.clientOptions.retryDelayOptions.base || 3000;
184 |
185 |
186 | this.options.clientContext = this.options.clientContext || {
187 | 'client' : {
188 | 'client_id' : DeviceInfo.getUniqueID(),
189 | 'app_title' : this.options.appTitle,
190 | 'app_version_name': this.options.appVersionName,
191 | 'app_version_code': this.options.appVersionCode,
192 | 'app_package_name': this.options.appPackageName
193 | },
194 | 'env' : {
195 | 'platform' : this.options.platform,
196 | 'platform_version': this.options.platformVersion,
197 | 'model' : this.options.model,
198 | 'make' : this.options.make,
199 | 'locale' : this.options.locale
200 | },
201 | 'services': {
202 | 'mobile_analytics': {
203 | 'app_id' : this.options.appId,
204 | 'sdk_name' : 'aws-sdk-mobile-analytics-js',
205 | 'sdk_version': '0.9.2' + ':' + AWS.VERSION
206 | }
207 | },
208 | 'custom' : {}
209 | };
210 | }
211 |
212 |
213 | validateEvent(event) {
214 | let invalidMetrics = [];
215 |
216 | function customNameErrorFilter(name) {
217 | if (name.length === 0) {
218 | return true;
219 | }
220 | return name.length > 50;
221 | }
222 |
223 | function customAttrValueErrorFilter(name) {
224 | return event.attributes[name] && event.attributes[name].length > 200;
225 | }
226 |
227 | function validationError(errorMsg) {
228 | this.logger.error(errorMsg);
229 | return null;
230 | }
231 |
232 | invalidMetrics = Object.keys(event.metrics).filter(function (metricName) {
233 | return typeof event.metrics[metricName] !== 'number';
234 | });
235 | if (event.version !== 'v2.0') {
236 | return validationError('Event must have version v2.0');
237 | }
238 | if (typeof event.eventType !== 'string') {
239 | return validationError('Event Type must be a string');
240 | }
241 | if (invalidMetrics.length > 0) {
242 | return validationError('Event Metrics must be numeric (' + invalidMetrics[0] + ')');
243 | }
244 | if (Object.keys(event.metrics).length + Object.keys(event.attributes).length > 40) {
245 | return validationError('Event Metric and Attribute Count cannot exceed 40');
246 | }
247 | if (Object.keys(event.attributes).filter(customNameErrorFilter).length) {
248 | return validationError('Event Attribute names must be 1-50 characters');
249 | }
250 | if (Object.keys(event.metrics).filter(customNameErrorFilter).length) {
251 | return validationError('Event Metric names must be 1-50 characters');
252 | }
253 | if (Object.keys(event.attributes).filter(customAttrValueErrorFilter).length) {
254 | return validationError('Event Attribute values cannot be longer than 200 characters');
255 | }
256 | return event;
257 | }
258 |
259 |
260 | /**
261 | * AMA.Client.createEvent
262 | * @param {string} eventType - Custom Event Type to be displayed in Console
263 | * @param {AMA.Session} session - Session Object (required for use within console)
264 | * @param {string} session.id - Identifier for current session
265 | * @param {string} session.startTimestamp - Timestamp that indicates the start of the session
266 | * @param [attributes=] - Custom attributes
267 | * @param [metrics=] - Custom metrics
268 | * @returns {AMA.Event}
269 | */
270 | createEvent(eventType, session, attributes, metrics) {
271 | this.logger.log('[Function:(AMA.Client).createEvent]' +
272 | (eventType ? '\neventType:' + eventType : '') +
273 | (session ? '\nsession:' + session : '') +
274 | (attributes ? '\nattributes:' + JSON.stringify(attributes) : '') +
275 | (metrics ? '\nmetrics:' + JSON.stringify(metrics) : ''));
276 | attributes = attributes || {};
277 | metrics = metrics || {};
278 |
279 | Util.mergeObjects(attributes, this.options.globalAttributes);
280 | Util.mergeObjects(metrics, this.options.globalMetrics);
281 |
282 | Object.keys(attributes).forEach(function (name) {
283 | if (typeof attributes[name] !== 'string') {
284 | try {
285 | attributes[name] = JSON.stringify(attributes[name]);
286 | } catch (e) {
287 | this.logger.warn('Error parsing attribute ' + name);
288 | }
289 | }
290 | });
291 | let event = {
292 | eventType : eventType,
293 | timestamp : new Date().toISOString(),
294 | session : {
295 | id : session.id,
296 | startTimestamp: session.startTimestamp
297 | },
298 | version : 'v2.0',
299 | attributes: attributes,
300 | metrics : metrics
301 | };
302 | if (session.stopTimestamp) {
303 | event.session.stopTimestamp = session.stopTimestamp;
304 | event.session.duration = new Date(event.stopTimestamp).getTime() - new Date(event.startTimestamp).getTime();
305 | }
306 | return this.validateEvent(event);
307 | }
308 |
309 |
310 | /**
311 | * AMA.Client.pushEvent
312 | * @param {AMA.Event} event - event to be pushed onto queue
313 | * @returns {int} Index of event in outputs.events
314 | */
315 | pushEvent(event) {
316 | if (!event) {
317 | return -1;
318 | }
319 | this.logger.log('[Function:(AMA.Client).pushEvent]' +
320 | (event ? '\nevent:' + JSON.stringify(event) : ''));
321 | //Push adds to the end of array and returns the size of the array
322 | let eventIndex = this.outputs.events.push(event);
323 | this.storage.set(this.StorageKeys.EVENTS, this.outputs.events);
324 | return (eventIndex - 1);
325 | }
326 |
327 | /**
328 | * Helper to record events, will automatically submit if the events exceed batchSizeLimit
329 | * @param {string} eventType - Custom event type name
330 | * @param {AMA.Session} session - Session object
331 | * @param {AMA.Client.Attributes} [attributes=] - Custom attributes
332 | * @param {AMA.Client.Metrics} [metrics=] - Custom metrics
333 | * @returns {AMA.Event} The event that was recorded
334 | */
335 | recordEvent(eventType, session, attributes, metrics) {
336 | this.logger.log('[Function:(AMA.Client).recordEvent]' +
337 | (eventType ? '\neventType:' + eventType : '') +
338 | (session ? '\nsession:' + session : '') +
339 | (attributes ? '\nattributes:' + JSON.stringify(attributes) : '') +
340 | (metrics ? '\nmetrics:' + JSON.stringify(metrics) : ''));
341 | let index, event = this.createEvent(eventType, session, attributes, metrics);
342 | if (event) {
343 | index = this.pushEvent(event);
344 | if (Util.getRequestBodySize(this.outputs.events) >= this.options.batchSizeLimit) {
345 | this.submitEvents();
346 | }
347 | return this.outputs.events[index];
348 | }
349 | return null;
350 | }
351 |
352 |
353 | /**
354 | * recordMonetizationEvent
355 | * @param session
356 | * @param {Object} monetizationDetails - Details about Monetization Event
357 | * @param {string} monetizationDetails.currency - ISO Currency of event
358 | * @param {string} monetizationDetails.productId - Product Id of monetization event
359 | * @param {number} monetizationDetails.quantity - Quantity of product in transaction
360 | * @param {string|number} monetizationDetails.price - Price of product either ISO formatted string, or number
361 | * with associated ISO Currency
362 | * @param {AMA.Client.Attributes} [attributes=] - Custom attributes
363 | * @param {AMA.Client.Metrics} [metrics=] - Custom metrics
364 | * @returns {event} The event that was recorded
365 | */
366 | recordMonetizationEvent(session, monetizationDetails, attributes, metrics) {
367 | this.logger.log('[Function:(AMA.Client).recordMonetizationEvent]' +
368 | (session ? '\nsession:' + session : '') +
369 | (monetizationDetails ? '\nmonetizationDetails:' + JSON.stringify(monetizationDetails) : '') +
370 | (attributes ? '\nattributes:' + JSON.stringify(attributes) : '') +
371 | (metrics ? '\nmetrics:' + JSON.stringify(metrics) : ''));
372 |
373 | attributes = attributes || {};
374 | metrics = metrics || {};
375 | attributes._currency = monetizationDetails.currency || attributes._currency;
376 | attributes._product_id = monetizationDetails.productId || attributes._product_id;
377 | metrics._quantity = monetizationDetails.quantity || metrics._quantity;
378 | if (typeof monetizationDetails.price === 'number') {
379 | metrics._item_price = monetizationDetails.price || metrics._item_price;
380 | } else {
381 | attributes._item_price_formatted = monetizationDetails.price || attributes._item_price_formatted;
382 | }
383 | return this.recordEvent('_monetization.purchase', session, attributes, metrics);
384 | }
385 |
386 |
387 | /**
388 | * submitEvents
389 | * @param {Object} [options=] - options for submitting events
390 | * @param {Object} [options.clientContext=this.options.clientContext] - clientContext to submit with defaults
391 | * to options.clientContext
392 | * @param {SubmitCallback} [options.submitCallback=this.options.submitCallback] - Callback function that is executed
393 | * when events are successfully
394 | * submitted
395 | * @returns {Array} Array of batch indices that were submitted
396 | */
397 | submitEvents(options) {
398 | options = options || {};
399 | options.submitCallback = options.submitCallback || this.options.submitCallback;
400 | this.logger.log('[Function:(AMA.Client).submitEvents]' +
401 | (options ? '\noptions:' + JSON.stringify(options) : ''));
402 |
403 |
404 | if (this.options.autoSubmitEvents) {
405 | clearTimeout(this.outputs.timeoutReference);
406 | this.outputs.timeoutReference = setTimeout(this.submitEvents.bind(this), this.options.autoSubmitInterval);
407 | }
408 | let warnMessage;
409 | //Get distribution of retries across clients by introducing a weighted rand.
410 | //Probability will increase over time to an upper limit of 60s
411 | if (this.outputs.isThrottled && this.throttlingSuppressionFunction() < Math.random()) {
412 | warnMessage = 'Prevented submission while throttled';
413 | } else if (Object.keys(this.outputs.batchesInFlight).length > 0) {
414 | warnMessage = 'Prevented submission while batches are in flight';
415 | } else if (this.outputs.batches.length === 0 && this.outputs.events.length === 0) {
416 | warnMessage = 'No batches or events to be submitted';
417 | } else if (this.outputs.lastSubmitTimestamp && Util.timestamp() - this.outputs.lastSubmitTimestamp < 1000) {
418 | warnMessage = 'Prevented multiple submissions in under a second';
419 | }
420 | if (warnMessage) {
421 | this.logger.warn(warnMessage);
422 | return [];
423 | }
424 | this.generateBatches();
425 |
426 | this.outputs.lastSubmitTimestamp = Util.timestamp();
427 | if (this.outputs.isThrottled) {
428 | //Only submit the first batch if throttled
429 | this.logger.warn('Is throttled submitting first batch');
430 | options.batchId = this.outputs.batchIndex[0];
431 | return [this.submitBatchById(options)];
432 | }
433 |
434 | return this.submitAllBatches(options);
435 | }
436 |
437 | throttlingSuppressionFunction(timestamp) {
438 | timestamp = timestamp || Util.timestamp();
439 | return Math.pow(timestamp - this.outputs.lastSubmitTimestamp, 2) / Math.pow(60000, 2);
440 | }
441 |
442 |
443 | generateBatches() {
444 | while (this.outputs.events.length > 0) {
445 | let lastIndex = this.outputs.events.length;
446 | this.logger.log(this.outputs.events.length + ' events to be submitted');
447 | while (lastIndex > 1 &&
448 | Util.getRequestBodySize(this.outputs.events.slice(0, lastIndex)) > this.options.batchSizeLimit) {
449 | this.logger.log('Finding Batch Size (' + this.options.batchSizeLimit + '): ' + lastIndex + '(' +
450 | Util.getRequestBodySize(this.outputs.events.slice(0, lastIndex)) + ')');
451 | lastIndex -= 1;
452 | }
453 | if (this.persistBatch(this.outputs.events.slice(0, lastIndex))) {
454 | //Clear event queue
455 | this.outputs.events.splice(0, lastIndex);
456 | this.storage.set(this.StorageKeys.EVENTS, this.outputs.events);
457 | }
458 | }
459 | }
460 |
461 |
462 | persistBatch(eventBatch) {
463 | this.logger.log(eventBatch.length + ' events in batch');
464 | if (Util.getRequestBodySize(eventBatch) < 512000) {
465 | let batchId = Util.GUID();
466 | //Save batch so data is not lost.
467 | this.outputs.batches[batchId] = eventBatch;
468 | this.storage.set(this.StorageKeys.BATCHES, this.outputs.batches);
469 | this.outputs.batchIndex.push(batchId);
470 | this.storage.set(this.StorageKeys.BATCH_INDEX, this.outputs.batchIndex);
471 | return true;
472 | }
473 | this.logger.error('Events too large');
474 | return false;
475 | }
476 |
477 | submitAllBatches(options) {
478 | options.submitCallback = options.submitCallback || this.options.submitCallback;
479 | this.logger.log('[Function:(AMA.Client).submitAllBatches]' +
480 | (options ? '\noptions:' + JSON.stringify(options) : ''));
481 | let indices = [],
482 | that = this;
483 | this.outputs.batchIndex.forEach(function (batchIndex) {
484 | options.batchId = batchIndex;
485 | options.clientContext = options.clientContext || that.options.clientContext;
486 | if (!that.outputs.batchesInFlight[batchIndex]) {
487 | indices.push(that.submitBatchById(options));
488 | }
489 | });
490 | return indices;
491 | }
492 |
493 |
494 | submitBatchById(options) {
495 | if (typeof(options) !== 'object' || !options.batchId) {
496 | this.logger.error('Invalid Options passed to submitBatchById');
497 | return;
498 | }
499 | options.submitCallback = options.submitCallback || this.options.submitCallback;
500 | this.logger.log('[Function:(AMA.Client).submitBatchById]' +
501 | (options ? '\noptions:' + JSON.stringify(options) : ''));
502 | let eventBatch = {
503 | 'events' : this.outputs.batches[options.batchId],
504 | 'clientContext': JSON.stringify(options.clientContext || this.options.clientContext)
505 | };
506 | this.outputs.batchesInFlight[options.batchId] = Util.timestamp();
507 | this.outputs.MobileAnalytics.putEvents(eventBatch,
508 | this.handlePutEventsResponse(options.batchId, options.submitCallback));
509 | return options.batchId;
510 | }
511 |
512 |
513 | handlePutEventsResponse(batchId, callback) {
514 | const NON_RETRYABLE_EXCEPTIONS = ['BadRequestException', 'SerializationException', 'ValidationException'];
515 | let self = this;
516 | return function (err, data) {
517 | let clearBatch = true,
518 | wasThrottled = self.outputs.isThrottled;
519 | if (err) {
520 | self.logger.error(err, data);
521 | if (err.statusCode === undefined || err.statusCode === 400) {
522 | if (NON_RETRYABLE_EXCEPTIONS.indexOf(err.code) < 0) {
523 | clearBatch = false;
524 | }
525 | self.outputs.isThrottled = err.code === 'ThrottlingException';
526 | if (self.outputs.isThrottled) {
527 | self.logger.warn('Application is currently throttled');
528 | }
529 | }
530 | } else {
531 | self.logger.info('Events Submitted Successfully');
532 | self.outputs.isThrottled = false;
533 | }
534 | if (clearBatch) {
535 | self.clearBatchById(batchId);
536 | }
537 | delete self.outputs.batchesInFlight[batchId];
538 | callback(err, data, batchId);
539 | if (wasThrottled && !self.outputs.isThrottled) {
540 | self.logger.warn('Was throttled flushing remaining batches', callback);
541 | self.submitAllBatches({
542 | submitCallback: callback
543 | });
544 | }
545 | };
546 | }
547 |
548 |
549 | clearBatchById(batchId) {
550 | this.logger.log('[Function:(AMA.Client).clearBatchById]' +
551 | (batchId ? '\nbatchId:' + batchId : ''));
552 | if (this.outputs.batchIndex.indexOf(batchId) !== -1) {
553 | delete this.outputs.batches[batchId];
554 | this.outputs.batchIndex.splice(this.outputs.batchIndex.indexOf(batchId), 1);
555 |
556 | // Persist latest batches / events
557 | this.storage.set(this.StorageKeys.BATCH_INDEX, this.outputs.batchIndex);
558 | this.storage.set(this.StorageKeys.BATCHES, this.outputs.batches);
559 | }
560 | }
561 |
562 |
563 |
564 | }
--------------------------------------------------------------------------------
/src/MobileAnalyticsSession.js:
--------------------------------------------------------------------------------
1 | import StorageKeys from "./StorageKeys.js";
2 | import Util from "./MobileAnalyticsUtilities";
3 |
4 |
5 | /**
6 | * @name AMA.Session
7 | * @namespace AMA.Session
8 | * @constructor
9 | * @param {Object=} [options=] - A configuration map for the Session
10 | * @param {string=} [options.sessionId=Utilities.GUID()]- A sessionId for session.
11 | * @param {string=} [options.appId=new Date().toISOString()] - The start Timestamp (default now).
12 | * @param {number=} [options.sessionLength=600000] - Length of session in Milliseconds (default 10 minutes).
13 | * @param {AMA.Session.ExpirationCallback=} [options.expirationCallback] - Callback Function for when a session expires
14 | * @param {AMA.Client.Logger=} [options.logger=] - Object containing javascript style logger functions (passing console
15 | * will output to browser dev consoles)
16 | */
17 | /**
18 | * @callback AMA.Session.ExpirationCallback
19 | * @param {AMA.Session} session
20 | * @returns {boolean|int} - Returns either true to extend the session by the sessionLength or an int with the number of
21 | * seconds to extend the session. All other values will clear the session from storage.
22 | */
23 | export default class MobileAnalyticsSession {
24 |
25 | constructor(options) {
26 |
27 | this.options = options || {};
28 | this.options.logger = this.options.logger || {};
29 | this.logger = {
30 | log: this.options.logger.log || Util.NOP,
31 | info: this.options.logger.info || Util.NOP,
32 | warn: this.options.logger.warn || Util.NOP,
33 | error: this.options.logger.error || Util.NOP
34 | };
35 | this.logger.log = this.logger.log.bind(this.options.logger);
36 | this.logger.info = this.logger.info.bind(this.options.logger);
37 | this.logger.warn = this.logger.warn.bind(this.options.logger);
38 | this.logger.error = this.logger.error.bind(this.options.logger);
39 | this.logger.log('[Function:(AWS.MobileAnalyticsClient)Session Constructor]' +
40 | (options ? '\noptions:' + JSON.stringify(options) : ''));
41 | this.options.expirationCallback = this.options.expirationCallback || Util.NOP;
42 | this.id = this.options.sessionId || Util.GUID();
43 | this.sessionLength = this.options.sessionLength || 600000; //Default session length is 10 minutes
44 | //Suffix the AMA.Storage Keys with Session Id to ensure proper scope
45 | this.StorageKeys = {
46 | 'SESSION_ID': StorageKeys.SESSION_ID + this.id,
47 | 'SESSION_EXPIRATION': StorageKeys.SESSION_EXPIRATION + this.id,
48 | 'SESSION_START_TIMESTAMP': StorageKeys.SESSION_START_TIMESTAMP + this.id
49 | };
50 | this.startTimestamp = this.options.startTime ||
51 | this.options.storage.get(this.StorageKeys.SESSION_START_TIMESTAMP) ||
52 | new Date().toISOString();
53 | this.expirationDate = parseInt(this.options.storage.get(this.StorageKeys.SESSION_EXPIRATION), 10);
54 | if (isNaN(this.expirationDate)) {
55 | this.expirationDate = (new Date().getTime() + this.sessionLength);
56 | }
57 | this.options.storage.set(this.StorageKeys.SESSION_ID, this.id);
58 | this.options.storage.set(this.StorageKeys.SESSION_EXPIRATION, this.expirationDate);
59 | this.options.storage.set(this.StorageKeys.SESSION_START_TIMESTAMP, this.startTimestamp);
60 | this.sessionTimeoutReference = setTimeout(this.expireSession.bind(this), this.sessionLength);
61 | }
62 |
63 |
64 | /**
65 | * Expire session and clear session
66 | * @param {expirationCallback=} Callback function to call when sessions expire
67 | */
68 | expireSession(expirationCallback) {
69 | this.logger.log('[Function:(Session).expireSession]');
70 | expirationCallback = expirationCallback || this.options.expirationCallback;
71 | let shouldExtend = expirationCallback(this);
72 | if (typeof shouldExtend === 'boolean' && shouldExtend) {
73 | shouldExtend = this.options.sessionLength;
74 | }
75 | if (typeof shouldExtend === 'number') {
76 | this.extendSession(shouldExtend);
77 | } else {
78 | this.clearSession();
79 | }
80 | }
81 |
82 | /**
83 | * Clear session from storage system
84 | */
85 | clearSession() {
86 | this.logger.log('[Function:(Session).clearSession]');
87 | clearTimeout(this.sessionTimeoutReference);
88 | this.options.storage.delete(this.StorageKeys.SESSION_ID);
89 | this.options.storage.delete(this.StorageKeys.SESSION_EXPIRATION);
90 | this.options.storage.delete(this.StorageKeys.SESSION_START_TIMESTAMP);
91 | }
92 |
93 | /**
94 | * Extend session by adding to the expiration timestamp
95 | * @param {int} [sessionExtensionLength=sessionLength] - The number of milliseconds to add to the expiration date
96 | * (session length by default).
97 | */
98 | extendSession(sessionExtensionLength) {
99 | this.logger.log('[Function:(Session).extendSession]' +
100 | (sessionExtensionLength ? '\nsessionExtensionLength:' + sessionExtensionLength : ''));
101 | sessionExtensionLength = sessionExtensionLength || this.sessionLength;
102 | this.setSessionTimeout(this.expirationDate + parseInt(sessionExtensionLength, 10));
103 | }
104 |
105 |
106 | /**
107 | * @param {string} [stopDate=now] - The ISO Date String to set the stopTimestamp to (now for default).
108 | */
109 | stopSession(stopDate) {
110 | this.logger.log('[Function:(Session).stopSession]' + (stopDate ? '\nstopDate:' + stopDate : ''));
111 | this.stopTimestamp = stopDate || new Date().toISOString();
112 | }
113 |
114 |
115 | /**
116 | * Reset session timeout to expire in a given number of seconds
117 | * @param {int} [milliseconds=sessionLength] - The number of milliseconds until the session should expire (from now).
118 | */
119 | resetSessionTimeout(milliseconds) {
120 | this.logger.log('[Function:(Session).resetSessionTimeout]' +
121 | (milliseconds ? '\nmilliseconds:' + milliseconds : ''));
122 | milliseconds = milliseconds || this.sessionLength;
123 | this.setSessionTimeout(new Date().getTime() + milliseconds);
124 | }
125 |
126 | /**
127 | * Setter for the session timeout
128 | * @param {int} timeout - epoch timestamp
129 | */
130 | setSessionTimeout(timeout) {
131 | this.logger.log('[Function:(Session).setSessionTimeout]' + (timeout ? '\ntimeout:' + timeout : ''));
132 | clearTimeout(this.sessionTimeoutReference);
133 | this.expirationDate = timeout;
134 | this.options.storage.set(this.StorageKeys.SESSION_EXPIRATION, this.expirationDate);
135 | this.sessionTimeoutReference = setTimeout(this.expireSession.bind(this),
136 | this.expirationDate - (new Date()).getTime());
137 | }
138 |
139 | }
--------------------------------------------------------------------------------
/src/MobileAnalyticsSessionManager.js:
--------------------------------------------------------------------------------
1 | import Client from './MobileAnalyticsClient';
2 | import Util from'./MobileAnalyticsUtilities';
3 | import StorageKeys from './StorageKeys.js';
4 | import Session from "./MobileAnalyticsSession";
5 | import {NetInfo} from "react-native";
6 |
7 | /**
8 | * @typedef AMA.Manager.Options
9 | * @augments AMA.Client.Options
10 | * @property {AMA.Session.ExpirationCallback} [expirationCallback=] - Callback function to call when sessions expire
11 | */
12 |
13 | /**
14 | * @name AMA.Manager
15 | * @namespace AMA.Manager
16 | * @constructor
17 | * @param {AMA.Client.Options|AMA.Client} options - A configuration map for the AMA.Client or an instantiated AMA.Client
18 | * @see AMA.Client
19 | */
20 | export default class Manager {
21 |
22 |
23 | constructor(options) {
24 | this.options = options;
25 | }
26 |
27 | async initialize(onSuccess, onConnectionFailure) {
28 |
29 | let isConnected = await NetInfo.isConnected.fetch();
30 |
31 | if (isConnected) {
32 | let options = this.options;
33 | if (options instanceof Client) {
34 | this.client = options;
35 | this.initSession(onSuccess);
36 | } else {
37 | options._autoSubmitEvents = options.autoSubmitEvents;
38 | options.autoSubmitEvents = false;
39 | this.client = new Client(options, ()=>{
40 | options.autoSubmitEvents = options._autoSubmitEvents !== false;
41 | delete options._autoSubmitEvents;
42 | this.initSession(onSuccess);
43 | });
44 | }
45 | } else {
46 | console.log('[Function:(AMA.Manager).initialize: Not initializeable (no internet connection)]');
47 | onConnectionFailure ? onConnectionFailure() : onSuccess();
48 | }
49 |
50 | }
51 |
52 | initSession(callback) {
53 |
54 | this.options = this.client.options;
55 | this.outputs = this.client.outputs;
56 | this.options.expirationCallback = this.options.expirationCallback || Util.NOP;
57 |
58 | this.checkForStoredSessions();
59 | if (!this.outputs.session) {
60 | this.startSession();
61 | }
62 | if (this.options.autoSubmitEvents) {
63 | this.client.submitEvents();
64 | }
65 |
66 | callback();
67 | }
68 |
69 |
70 | checkForStoredSessions() {
71 | this.client.storage.each(function (key) {
72 | if (key.indexOf(StorageKeys.SESSION_ID) === 0) {
73 | this.outputs.session = new Session({
74 | storage: this.client.storage,
75 | sessionId: this.client.storage.get(key),
76 | sessionLength: this.options.sessionLength,
77 | expirationCallback: function (session) {
78 | let shouldExtend = this.options.expirationCallback(session);
79 | if (shouldExtend === true || typeof shouldExtend === 'number') {
80 | return shouldExtend;
81 | }
82 | this.stopSession();
83 | }
84 | });
85 | if (new Date().getTime() > this.outputs.session.expirationDate) {
86 | this.outputs.session.expireSession();
87 | delete this.outputs.session;
88 | }
89 | }
90 | });
91 | }
92 |
93 |
94 |
95 | /**
96 | * submitEvents
97 | * @param {Object} [options=] - options for submitting events
98 | * @param {Object} [options.clientContext=this.options.clientContext] - clientContext to submit with defaults to
99 | * options.clientContext
100 | * @returns {Array} Array of batch indices that were submitted
101 | */
102 | submitEvents(options) {
103 | return this.client.submitEvents(options);
104 | };
105 |
106 |
107 | /**
108 | * Function to start a session
109 | * @returns {AMA.Client.Event} The start session event recorded
110 | */
111 | startSession() {
112 | this.client.logger.log('[Function:(AMA.Manager).startSession]');
113 | if (this.outputs.session) {
114 | //Clear Session
115 | this.outputs.session.clearSession();
116 | }
117 | this.outputs.session = new Session({
118 | storage: this.client.storage,
119 | logger: this.client.options.logger,
120 | sessionLength: this.options.sessionLength,
121 | expirationCallback: function (session) {
122 | let shouldExtend = this.options.expirationCallback(session);
123 | if (shouldExtend === true || typeof shouldExtend === 'number') {
124 | return shouldExtend;
125 | }
126 | this.stopSession();
127 | }.bind(this)
128 | });
129 | return this.recordEvent('_session.start');
130 | };
131 |
132 |
133 |
134 | /**
135 | * Function to extend the current session.
136 | * @param {int} [milliseconds=options.sessionLength] - Milliseconds to extend the session by, will default
137 | * to another session length
138 | * @returns {int} The Session expiration (in Milliseconds)
139 | */
140 | extendSession(milliseconds) {
141 | return this.outputs.session.extendSession(milliseconds || this.options.sessionLength);
142 | };
143 |
144 |
145 | /**
146 | * Function to stop the current session
147 | * @returns {AMA.Client.Event} The stop session event recorded
148 | */
149 | stopSession() {
150 | this.client.logger.log('[Function:(AMA.Manager).stopSession]');
151 | this.outputs.session.stopSession();
152 | this.outputs.session.expireSession(Util.NOP);
153 | return this.recordEvent('_session.stop');
154 | };
155 |
156 |
157 | /**
158 | * Function to stop the current session and start a new one
159 | * @returns {AMA.Session} The new Session Object for the SessionManager
160 | */
161 | renewSession() {
162 | this.stopSession();
163 | this.startSession();
164 | return this.outputs.session;
165 | };
166 |
167 |
168 | /**
169 | * Function that constructs a Mobile Analytics Event
170 | * @param {string} eventType - Custom Event Type to be displayed in Console
171 | * @param {AMA.Client.Attributes} [attributes=] - Map of String attributes
172 | * @param {AMA.Client.Metrics} [metrics=] - Map of numeric values
173 | * @returns {AMA.Client.Event}
174 | */
175 | createEvent(eventType, attributes, metrics) {
176 | return this.client.createEvent(eventType, this.outputs.session, attributes, metrics);
177 | };
178 |
179 |
180 | /**
181 | * Function to record a custom event
182 | * @param eventType - Custom event type name
183 | * @param {AMA.Client.Attributes} [attributes=] - Custom attributes
184 | * @param {AMA.Client.Metrics} [metrics=] - Custom metrics
185 | * @returns {AMA.Client.Event} The event that was recorded
186 | */
187 | recordEvent(eventType, attributes, metrics) {
188 | if (this.client != undefined) {
189 | return this.client.recordEvent(eventType, this.outputs.session, attributes, metrics);
190 | } else {
191 | console.log('[Function:(AMA.Manager).recordEvent: Client is not initialized]');
192 | }
193 | };
194 |
195 |
196 | /**
197 | * Function to record a monetization event
198 | * @param {Object} monetizationDetails - Details about Monetization Event
199 | * @param {string} monetizationDetails.currency - ISO Currency of event
200 | * @param {string} monetizationDetails.productId - Product Id of monetization event
201 | * @param {number} monetizationDetails.quantity - Quantity of product in transaction
202 | * @param {string|number} monetizationDetails.price - Price of product either ISO formatted string, or number
203 | * with associated ISO Currency
204 | * @param {AMA.Client.Attributes} [attributes=] - Custom attributes
205 | * @param {AMA.Client.Metrics} [metrics=] - Custom metrics
206 | * @returns {AMA.Client.Event} The event that was recorded
207 | */
208 | recordMonetizationEvent(monetizationDetails, attributes, metrics) {
209 | if (this.client != undefined) {
210 | return this.client.recordMonetizationEvent(this.outputs.session, monetizationDetails, attributes, metrics);
211 | } else {
212 | console.log('[Function:(AMA.Manager).recordMonetizationEvent: Client is not initialized]');
213 | }
214 | };
215 | }
216 |
--------------------------------------------------------------------------------
/src/MobileAnalyticsUtilities.js:
--------------------------------------------------------------------------------
1 | export default class MobileAnalyticsUtilities {
2 |
3 | static s4() {
4 | return Math.floor((1 + Math.random()) * 0x10000)
5 | .toString(16)
6 | .substring(1);
7 | }
8 |
9 |
10 | static utf8ByteLength(str) {
11 | if (typeof str !== 'string') {
12 | str = JSON.stringify(str);
13 | }
14 | let s = str.length, i, code;
15 | for (i = str.length - 1; i >= 0; i -= 1) {
16 | code = str.charCodeAt(i);
17 | if (code > 0x7f && code <= 0x7ff) {
18 | s += 1;
19 | } else if (code > 0x7ff && code <= 0xffff) {
20 | s += 2;
21 | }
22 | if (code >= 0xDC00 && code <= 0xDFFF) { /*trail surrogate*/
23 | i -= 1;
24 | }
25 | }
26 | return s;
27 | }
28 |
29 |
30 | static GUID() {
31 | return this.s4() + this.s4() + '-' + this.s4() + '-' + this.s4() +
32 | '-' + this.s4() + '-' + this.s4() + this.s4() + this.s4();
33 | }
34 |
35 |
36 | static mergeObjects(override, initial) {
37 | Object.keys(initial).forEach(function (key) {
38 | if (initial.hasOwnProperty(key)) {
39 | override[key] = override[key] || initial[key];
40 | }
41 | });
42 | return override;
43 | }
44 |
45 |
46 | static copy(original, extension) {
47 | return this.mergeObjects(JSON.parse(JSON.stringify(original)), extension || {});
48 | }
49 |
50 | static NOP() {
51 | return undefined;
52 | }
53 |
54 |
55 | static timestamp() {
56 | return new Date().getTime();
57 | }
58 |
59 | static getRequestBodySize(str) {
60 | return this.utf8ByteLength(str);
61 | }
62 | }
--------------------------------------------------------------------------------
/src/Storage.js:
--------------------------------------------------------------------------------
1 | import AMA from '../index';
2 | import Util from './MobileAnalyticsUtilities';
3 | import {
4 | AsyncStorage
5 | } from "react-native";
6 |
7 |
8 | export default class Storage {
9 |
10 |
11 | constructor(appId) {
12 | this.storageKey = 'AWSMobileAnalyticsStorage-' + appId;
13 | AMA[this.storageKey] = AMA[this.storageKey] || {};
14 | this.cache = {};
15 | this.cache.id = this.cache.id || Util.GUID();
16 |
17 |
18 | this.logger = {
19 | log: Util.NOP,
20 | info: Util.NOP,
21 | warn: Util.NOP,
22 | error: Util.NOP
23 | };
24 | }
25 |
26 |
27 | get(key) {
28 | return this.cache[key];
29 | };
30 |
31 | set(key, value) {
32 | this.cache[key] = value;
33 | return this.saveToNativeStorage();
34 | };
35 |
36 | delete(key) {
37 | delete this.cache[key];
38 | this.saveToNativeStorage();
39 | };
40 |
41 | each(callback) {
42 | for (let key in this.cache) {
43 | if (this.cache.hasOwnProperty(key)) {
44 | callback(key, this.cache[key]);
45 | }
46 | }
47 | }
48 |
49 | saveToNativeStorage() {
50 | this.logger.log('[Function:(AWS.MobileAnalyticsClient.Storage).saveNativeStorage]');
51 | AsyncStorage.setItem(this.storageKey, JSON.stringify(this.cache), e => {
52 | if (e) {
53 | this.logger.log("Error AMA Storage: " + e);
54 | } else {
55 | this.logger.log("AMA Storage Cache: " + JSON.stringify(this.cache));
56 | }
57 | });
58 | }
59 |
60 |
61 | reload(callback) {
62 | let storedCache;
63 | this.logger.log('[Function:(AWS.MobileAnalyticsClient.Storage).loadNativeStorage]');
64 | storedCache = AsyncStorage.getItem(this.storageKey, (e, r) => {
65 | if (e) {
66 | this.logger.log("Error loading Native Storage: " + JSON.stringify(e))
67 | } else {
68 | //Try to parse, if corrupt delete
69 | try {
70 | this.cache = JSON.parse(storedCache);
71 | callback();
72 | } catch (parseJSONError) {
73 | //Corrupted stored cache, delete it
74 | this.clearNativeStorage(callback);
75 | }
76 | }
77 | });
78 | }
79 |
80 | setLogger(logFunction) {
81 | this.logger = logFunction;
82 | }
83 |
84 | clearNativeStorage(callback) {
85 | this.cache = {};
86 | this.logger.log('[Function:(AWS.MobileAnalyticsClient.Storage).clearNativeStorage]');
87 | AsyncStorage.removeItem(this.storageKey);
88 | callback();
89 |
90 | }
91 |
92 |
93 | }
--------------------------------------------------------------------------------
/src/StorageKeys.js:
--------------------------------------------------------------------------------
1 | const StorageKeys = {
2 | 'CLIENT_ID': 'AWSMobileAnalyticsClientId',
3 | 'GLOBAL_ATTRIBUTES': 'AWSMobileAnalyticsGlobalAttributes',
4 | 'GLOBAL_METRICS': 'AWSMobileAnalyticsGlobalMetrics',
5 | 'SESSION_ID': 'MobileAnalyticsSessionId',
6 | 'SESSION_EXPIRATION': 'MobileAnalyticsSessionExpiration',
7 | 'SESSION_START_TIMESTAMP': 'MobileAnalyticsSessionStartTimeStamp'
8 | };
9 |
10 | export default StorageKeys;
11 |
--------------------------------------------------------------------------------
/src/__tests__/MobileAnalyticsSession.test.js:
--------------------------------------------------------------------------------
1 | import Session from "../MobileAnalyticsSession";
2 | import Storage from "../Storage";
3 |
4 |
5 | describe('Session', () => {
6 | describe('Initialize Session (Default Values)', () => {
7 | const storage = new Storage("1234");
8 | const session = new Session({storage: storage});
9 |
10 | test("should be initialized", () => {
11 | expect(session).toBeDefined();
12 | });
13 |
14 |
15 | test("should have scoped sessionId storage key", () => {
16 | expect(session.StorageKeys.SESSION_ID).not.toBe("MobileAnalyticsSessionId");
17 | expect(session.StorageKeys.SESSION_ID).toEqual(expect.stringContaining("MobileAnalyticsSessionId"));
18 | expect(session.StorageKeys.SESSION_ID).toEqual(expect.stringContaining(session.id));
19 |
20 | });
21 |
22 | test("should have scoped sessionExpiration storage key", () => {
23 | expect(session.StorageKeys.SESSION_EXPIRATION).not.toEqual("MobileAnalyticsSessionExpiration");
24 | expect(session.StorageKeys.SESSION_EXPIRATION).toEqual(expect.stringContaining("MobileAnalyticsSessionExpiration"));
25 | expect(session.StorageKeys.SESSION_EXPIRATION).toEqual(expect.stringContaining(session.id));
26 | });
27 |
28 | test("should persist session id", () => {
29 | expect(storage.get(session.StorageKeys.SESSION_ID)).toBeDefined();
30 | expect(storage.get(session.StorageKeys.SESSION_ID)).toEqual(session.id);
31 | });
32 |
33 | test("should persist expiration", () => {
34 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION)).toBeDefined();
35 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION)).toEqual(session.expirationDate);
36 | });
37 |
38 | test("should have a number expiration", () => {
39 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION)).toEqual(expect.any(Number));
40 | expect(session.expirationDate).toEqual(expect.any(Number));
41 | });
42 |
43 | test("should have an integer expiration", () => {
44 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION) % 1).toEqual(0);
45 | expect(session.expirationDate %1).toEqual(0);
46 | });
47 |
48 | });
49 |
50 | describe('Clear Session', () => {
51 | const storage = new Storage("1234");
52 | const session = new Session({storage: storage});
53 | session.expireSession();
54 |
55 | test("should clear session id", () => {
56 | expect(storage.get(session.StorageKeys.SESSION_ID)).not.toBeDefined();
57 | });
58 |
59 | test("should clear session expiration", () => {
60 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION)).not.toBeDefined();
61 | });
62 | });
63 |
64 |
65 | describe('Extend Session (Default Values)', () => {
66 | const storage = new Storage("1234");
67 | let session = new Session({storage: storage});
68 | let expiration = session.expirationDate;
69 | session.extendSession();
70 |
71 | test("should not be original expiration date", () => {
72 | expect(expiration).not.toEqual(session.expirationDate);
73 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION)).not.toEqual(expiration);
74 | });
75 |
76 | test("should persist new expiration date", () => {
77 | session = new AMA.Session({storage: storage});
78 | expiration = session.expirationDate;
79 | session.extendSession();
80 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION)).toEqual(session.expirationDate);
81 | });
82 |
83 | test("should be 30min later", () => {
84 | expect(session.expirationDate).toEqual(expiration + session.sessionLength);
85 | });
86 | });
87 |
88 | describe('Extend Session (Default Values)', () => {
89 | const storage = new Storage("1234");
90 | let session = new Session({storage: storage});
91 | let expiration = session.expirationDate;
92 | session.extendSession(60000);
93 |
94 | test("should not be original expiration date", () => {
95 | expect(expiration).not.toEqual(session.expirationDate);
96 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION)).not.toEqual(expiration);
97 | });
98 |
99 | test("should persist new expiration date", () => {
100 | session = new AMA.Session({storage: storage});
101 | expiration = session.expirationDate;
102 | session.extendSession(60000);
103 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION)).toEqual(session.expirationDate);
104 | });
105 |
106 | test("should be 60 sec later", () => {
107 | expect(session.expirationDate).toEqual(expiration + 60000);
108 | });
109 | });
110 |
111 | describe('Reset Session Timeout (1 min from now)', () => {
112 | const storage = new Storage("1234");
113 | let session = new Session({storage: storage});
114 | let expiration = session.expirationDate;
115 | session.resetSessionTimeout(60000);
116 |
117 | test("should not be original expiration date", () => {
118 | expect(expiration).not.toEqual(session.expirationDate);
119 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION)).not.toEqual(expiration);
120 | });
121 |
122 | test("should persist new expiration date", () => {
123 | expect(storage.get(session.StorageKeys.SESSION_EXPIRATION)).toEqual(session.expirationDate);
124 | });
125 |
126 | });
127 |
128 | });
--------------------------------------------------------------------------------
/src/__tests__/MobileAnalyticsUtilities.test.js:
--------------------------------------------------------------------------------
1 | import Util from "../MobileAnalyticsUtilities";
2 |
3 |
4 | describe('Util', () => {
5 |
6 | describe('GUID', () => {
7 | test("should not be equal", () => {
8 | expect(Util.GUID()).not.toEqual(Util.GUID());
9 | });
10 | });
11 |
12 | describe('mergeObjects', () => {
13 | let a = {a: 1, b: 2};
14 | let b = {b: 1, c: 3};
15 |
16 | test("should merge with overlapping keys", () => {
17 | expect(Util.mergeObjects(a, b)).toEqual({a:1, b:2, c:3});
18 | });
19 |
20 | test("should mutate original", () => {
21 | expect(Util.mergeObjects(a, b)).toEqual(a);
22 | });
23 | });
24 |
25 |
26 | describe('utf8ByteLength', () => {
27 | test("should test char codes > 127", () => {
28 | expect(Util.getRequestBodySize('©‰')).toEqual(5);
29 | });
30 |
31 | test("should test trail surrogate", () => {
32 | expect(Util.getRequestBodySize('𝌆')).toEqual(4);
33 | });
34 | });
35 |
36 | describe('copy', () => {
37 | let a = {a: 1, b: 2};
38 | let b = {c: 3};
39 |
40 | test("should copy with new keys", () => {
41 | expect(Util.copy(a, b)).toEqual({a:1, b:2, c:3});
42 | });
43 |
44 | test("should not mutate original", () => {
45 | expect(Util.copy(a, b)).not.toEqual(a);
46 | });
47 | });
48 |
49 | });
50 |
--------------------------------------------------------------------------------
/src/__tests__/Storage.test.js:
--------------------------------------------------------------------------------
1 | import Storage from "../Storage";
2 |
3 | describe('Storage', () => {
4 |
5 | test("should initialize storage with appId and cache", () => {
6 | let storage = new Storage("1234");
7 |
8 | // check appId
9 | expect(storage.storageKey).toBe('AWSMobileAnalyticsStorage-' + "1234");
10 | expect(storage.storageKey).not.toBe('AWSMobileAnalyticsStorage-' + "124");
11 |
12 |
13 | // check cache
14 | expect(storage.cache).toMatchObject({});
15 | expect(storage.cache).not.toMatchObject({something: "something"});
16 | });
17 |
18 | test("should set and get correct item of cache", () => {
19 | let storage = new Storage("1234");
20 |
21 | // set
22 | storage.set("someKey", "someValue");
23 | expect(storage.cache).toMatchObject({"someKey": "someValue"});
24 |
25 | // get
26 | expect(storage.get("someKey")).toBe("someValue");
27 |
28 | })
29 | ;
30 |
31 |
32 | test("should set and delete item of cache", () => {
33 | let storage = new Storage("1234");
34 |
35 | // set
36 | storage.set("someKey", "someValue");
37 | storage.set("someKey2", "someValue2");
38 |
39 | // delete
40 | storage.delete("someKey");
41 | expect(storage.get("someKey")).not.toBeDefined();
42 | expect(storage.get("someKey2")).toBeDefined();
43 | });
44 |
45 |
46 | test("should get each key in cache", () => {
47 | let storage = new Storage("1234");
48 |
49 | // keys
50 | let keys = ["someKey1", "someKey2", "someKey3"];
51 | let keysInCache = [];
52 |
53 | // set
54 | storage.set(keys[0], "someValue1");
55 | storage.set(keys[1], "someValue2");
56 | storage.set(keys[2], "someValue3");
57 |
58 |
59 | // each
60 | storage.each((key) => {
61 | keysInCache.push(key);
62 | });
63 |
64 | // compare keys and keysInCache
65 | for (let k in keys.keys()) {
66 | expect(keysInCache).toContain(k);
67 | }
68 | });
69 |
70 | });
--------------------------------------------------------------------------------