UNPKG

9.87 kBPlain TextView Raw
1/*
2 * Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
5 * the License. A copy of the License is located at
6 *
7 * http://aws.amazon.com/apache2.0/
8 *
9 * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10 * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
11 * and limitations under the License.
12 */
13
14import {
15 Amplify,
16 ConsoleLogger as Logger,
17 Hub,
18 Parser,
19} from '@aws-amplify/core';
20import { AWSPinpointProvider } from './Providers/AWSPinpointProvider';
21
22import {
23 AnalyticsProvider,
24 EventAttributes,
25 EventMetrics,
26 pageViewTrackOpts,
27} from './types';
28import { PageViewTracker, EventTracker, SessionTracker } from './trackers';
29
30const logger = new Logger('AnalyticsClass');
31
32const AMPLIFY_SYMBOL = (typeof Symbol !== 'undefined' &&
33typeof Symbol.for === 'function'
34 ? Symbol.for('amplify_default')
35 : '@@amplify_default') as Symbol;
36
37const dispatchAnalyticsEvent = (event: string, data: any, message: string) => {
38 Hub.dispatch(
39 'analytics',
40 { event, data, message },
41 'Analytics',
42 AMPLIFY_SYMBOL
43 );
44};
45
46const trackers = {
47 pageView: PageViewTracker,
48 event: EventTracker,
49 session: SessionTracker,
50};
51
52let _instance = null;
53
54/**
55 * Provide mobile analytics client functions
56 */
57export class AnalyticsClass {
58 private _config;
59 private _pluggables: AnalyticsProvider[];
60 private _disabled;
61 private _trackers;
62
63 /**
64 * Initialize Analtyics
65 * @param config - Configuration of the Analytics
66 */
67 constructor() {
68 this._config = {};
69 this._pluggables = [];
70 this._disabled = false;
71 this._trackers = {};
72 _instance = this;
73
74 this.record = this.record.bind(this);
75 Hub.listen('auth', listener);
76 Hub.listen('storage', listener);
77 Hub.listen('analytics', listener);
78 }
79
80 public getModuleName() {
81 return 'Analytics';
82 }
83 /**
84 * configure Analytics
85 * @param {Object} config - Configuration of the Analytics
86 */
87 public configure(config?) {
88 if (!config) return this._config;
89 logger.debug('configure Analytics', config);
90 const amplifyConfig = Parser.parseMobilehubConfig(config);
91 this._config = Object.assign(
92 {},
93 this._config,
94 amplifyConfig.Analytics,
95 config
96 );
97
98 if (this._config['disabled']) {
99 this._disabled = true;
100 }
101
102 // turn on the autoSessionRecord if not specified
103 if (this._config['autoSessionRecord'] === undefined) {
104 this._config['autoSessionRecord'] = true;
105 }
106
107 this._pluggables.forEach(pluggable => {
108 // for backward compatibility
109 const providerConfig =
110 pluggable.getProviderName() === 'AWSPinpoint' &&
111 !this._config['AWSPinpoint']
112 ? this._config
113 : this._config[pluggable.getProviderName()];
114
115 pluggable.configure({
116 disabled: this._config['disabled'],
117 autoSessionRecord: this._config['autoSessionRecord'],
118 ...providerConfig,
119 });
120 });
121
122 if (this._pluggables.length === 0) {
123 this.addPluggable(new AWSPinpointProvider());
124 }
125
126 dispatchAnalyticsEvent(
127 'configured',
128 null,
129 `The Analytics category has been configured successfully`
130 );
131 logger.debug('current configuration', this._config);
132 return this._config;
133 }
134
135 /**
136 * add plugin into Analytics category
137 * @param {Object} pluggable - an instance of the plugin
138 */
139 public addPluggable(pluggable: AnalyticsProvider) {
140 if (pluggable && pluggable.getCategory() === 'Analytics') {
141 this._pluggables.push(pluggable);
142 // for backward compatibility
143 const providerConfig =
144 pluggable.getProviderName() === 'AWSPinpoint' &&
145 !this._config['AWSPinpoint']
146 ? this._config
147 : this._config[pluggable.getProviderName()];
148 const config = { disabled: this._config['disabled'], ...providerConfig };
149 pluggable.configure(config);
150 return config;
151 }
152 }
153
154 /**
155 * Get the plugin object
156 * @param providerName - the name of the plugin
157 */
158 public getPluggable(providerName) {
159 for (let i = 0; i < this._pluggables.length; i += 1) {
160 const pluggable = this._pluggables[i];
161 if (pluggable.getProviderName() === providerName) {
162 return pluggable;
163 }
164 }
165
166 logger.debug('No plugin found with providerName', providerName);
167 return null;
168 }
169
170 /**
171 * Remove the plugin object
172 * @param providerName - the name of the plugin
173 */
174 public removePluggable(providerName) {
175 let idx = 0;
176 while (idx < this._pluggables.length) {
177 if (this._pluggables[idx].getProviderName() === providerName) {
178 break;
179 }
180 idx += 1;
181 }
182
183 if (idx === this._pluggables.length) {
184 logger.debug('No plugin found with providerName', providerName);
185 return;
186 } else {
187 this._pluggables.splice(idx, idx + 1);
188 return;
189 }
190 }
191
192 /**
193 * stop sending events
194 */
195 public disable() {
196 this._disabled = true;
197 }
198
199 /**
200 * start sending events
201 */
202 public enable() {
203 this._disabled = false;
204 }
205
206 /**
207 * Record Session start
208 * @return - A promise which resolves if buffer doesn't overflow
209 */
210 public async startSession(provider?: string) {
211 const params = { event: { name: '_session.start' }, provider };
212 return this._sendEvent(params);
213 }
214
215 /**
216 * Record Session stop
217 * @return - A promise which resolves if buffer doesn't overflow
218 */
219 public async stopSession(provider?: string) {
220 const params = { event: { name: '_session.stop' }, provider };
221 return this._sendEvent(params);
222 }
223
224 /**
225 * Record one analytic event and send it to Pinpoint
226 * @param {String} name - The name of the event
227 * @param {Object} [attributes] - Attributes of the event
228 * @param {Object} [metrics] - Event metrics
229 * @return - A promise which resolves if buffer doesn't overflow
230 */
231 public async record(
232 event: string | object,
233 provider?,
234 metrics?: EventMetrics
235 ) {
236 let params = null;
237 // this is just for compatibility, going to be deprecated
238 if (typeof event === 'string') {
239 params = {
240 event: {
241 name: event,
242 attributes: provider,
243 metrics,
244 },
245 provider: 'AWSPinpoint',
246 };
247 } else {
248 params = { event, provider };
249 }
250 return this._sendEvent(params);
251 }
252
253 public async updateEndpoint(attrs, provider?) {
254 const event = { ...attrs, name: '_update_endpoint' };
255
256 return this.record(event, provider);
257 }
258
259 private _sendEvent(params) {
260 if (this._disabled) {
261 logger.debug('Analytics has been disabled');
262 return Promise.resolve();
263 }
264
265 const provider = params.provider ? params.provider : 'AWSPinpoint';
266
267 return new Promise((resolve, reject) => {
268 this._pluggables.forEach(pluggable => {
269 if (pluggable.getProviderName() === provider) {
270 pluggable.record(params, { resolve, reject });
271 }
272 });
273 });
274 }
275
276 public autoTrack(trackerType, opts) {
277 if (!trackers[trackerType]) {
278 logger.debug('invalid tracker type');
279 return;
280 }
281
282 // to sync up two different configuration ways of auto session tracking
283 if (trackerType === 'session') {
284 this._config['autoSessionRecord'] = opts['enable'];
285 }
286
287 const tracker = this._trackers[trackerType];
288 if (!tracker) {
289 this._trackers[trackerType] = new trackers[trackerType](
290 this.record,
291 opts
292 );
293 } else {
294 tracker.configure(opts);
295 }
296 }
297}
298
299let endpointUpdated = false;
300let authConfigured = false;
301let analyticsConfigured = false;
302const listener = capsule => {
303 const { channel, payload } = capsule;
304 logger.debug('on hub capsule ' + channel, payload);
305
306 switch (channel) {
307 case 'auth':
308 authEvent(payload);
309 break;
310 case 'storage':
311 storageEvent(payload);
312 break;
313 case 'analytics':
314 analyticsEvent(payload);
315 break;
316 default:
317 break;
318 }
319};
320
321const storageEvent = payload => {
322 const {
323 data: { attrs, metrics },
324 } = payload;
325 if (!attrs) return;
326
327 if (analyticsConfigured) {
328 _instance
329 .record({
330 name: 'Storage',
331 attributes: attrs,
332 metrics,
333 })
334 .catch(e => {
335 logger.debug('Failed to send the storage event automatically', e);
336 });
337 }
338};
339
340const authEvent = payload => {
341 const { event } = payload;
342 if (!event) {
343 return;
344 }
345
346 const recordAuthEvent = async eventName => {
347 if (authConfigured && analyticsConfigured) {
348 try {
349 return await _instance.record({ name: `_userauth.${eventName}` });
350 } catch (err) {
351 logger.debug(
352 `Failed to send the ${eventName} event automatically`,
353 err
354 );
355 }
356 }
357 };
358
359 switch (event) {
360 case 'signIn':
361 return recordAuthEvent('sign_in');
362 case 'signUp':
363 return recordAuthEvent('sign_up');
364 case 'signOut':
365 return recordAuthEvent('sign_out');
366 case 'signIn_failure':
367 return recordAuthEvent('auth_fail');
368 case 'configured':
369 authConfigured = true;
370 if (authConfigured && analyticsConfigured) {
371 sendEvents();
372 }
373 break;
374 }
375};
376
377const analyticsEvent = payload => {
378 const { event } = payload;
379 if (!event) return;
380
381 switch (event) {
382 case 'pinpointProvider_configured':
383 analyticsConfigured = true;
384 if (authConfigured && analyticsConfigured) {
385 sendEvents();
386 }
387 break;
388 }
389};
390
391const sendEvents = () => {
392 const config = _instance.configure();
393 if (!endpointUpdated && config['autoSessionRecord']) {
394 _instance.updateEndpoint({ immediate: true }).catch(e => {
395 logger.debug('Failed to update the endpoint', e);
396 });
397 endpointUpdated = true;
398 }
399 _instance.autoTrack('session', {
400 enable: config['autoSessionRecord'],
401 });
402};
403
404export const Analytics = new AnalyticsClass();
405Amplify.register(Analytics);