UNPKG

9.81 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 this._pluggables.forEach(pluggable => {
103 // for backward compatibility
104 const providerConfig =
105 pluggable.getProviderName() === 'AWSPinpoint' &&
106 !this._config['AWSPinpoint']
107 ? this._config
108 : this._config[pluggable.getProviderName()];
109
110 pluggable.configure({
111 disabled: this._config['disabled'],
112 ...providerConfig,
113 });
114 });
115
116 if (this._pluggables.length === 0) {
117 this.addPluggable(new AWSPinpointProvider());
118 }
119
120 // turn on the autoSessionRecord if not specified
121 if (this._config['autoSessionRecord'] === undefined) {
122 this._config['autoSessionRecord'] = true;
123 }
124
125 dispatchAnalyticsEvent(
126 'configured',
127 null,
128 `The Analytics category has been configured successfully`
129 );
130 logger.debug('current configuration', this._config);
131 return this._config;
132 }
133
134 /**
135 * add plugin into Analytics category
136 * @param {Object} pluggable - an instance of the plugin
137 */
138 public addPluggable(pluggable: AnalyticsProvider) {
139 if (pluggable && pluggable.getCategory() === 'Analytics') {
140 this._pluggables.push(pluggable);
141 // for backward compatibility
142 const providerConfig =
143 pluggable.getProviderName() === 'AWSPinpoint' &&
144 !this._config['AWSPinpoint']
145 ? this._config
146 : this._config[pluggable.getProviderName()];
147 const config = { disabled: this._config['disabled'], ...providerConfig };
148 pluggable.configure(config);
149 return config;
150 }
151 }
152
153 /**
154 * Get the plugin object
155 * @param providerName - the name of the plugin
156 */
157 public getPluggable(providerName) {
158 for (let i = 0; i < this._pluggables.length; i += 1) {
159 const pluggable = this._pluggables[i];
160 if (pluggable.getProviderName() === providerName) {
161 return pluggable;
162 }
163 }
164
165 logger.debug('No plugin found with providerName', providerName);
166 return null;
167 }
168
169 /**
170 * Remove the plugin object
171 * @param providerName - the name of the plugin
172 */
173 public removePluggable(providerName) {
174 let idx = 0;
175 while (idx < this._pluggables.length) {
176 if (this._pluggables[idx].getProviderName() === providerName) {
177 break;
178 }
179 idx += 1;
180 }
181
182 if (idx === this._pluggables.length) {
183 logger.debug('No plugin found with providerName', providerName);
184 return;
185 } else {
186 this._pluggables.splice(idx, idx + 1);
187 return;
188 }
189 }
190
191 /**
192 * stop sending events
193 */
194 public disable() {
195 this._disabled = true;
196 }
197
198 /**
199 * start sending events
200 */
201 public enable() {
202 this._disabled = false;
203 }
204
205 /**
206 * Record Session start
207 * @return - A promise which resolves if buffer doesn't overflow
208 */
209 public async startSession(provider?: string) {
210 const params = { event: { name: '_session.start' }, provider };
211 return this._sendEvent(params);
212 }
213
214 /**
215 * Record Session stop
216 * @return - A promise which resolves if buffer doesn't overflow
217 */
218 public async stopSession(provider?: string) {
219 const params = { event: { name: '_session.stop' }, provider };
220 return this._sendEvent(params);
221 }
222
223 /**
224 * Record one analytic event and send it to Pinpoint
225 * @param {String} name - The name of the event
226 * @param {Object} [attributes] - Attributes of the event
227 * @param {Object} [metrics] - Event metrics
228 * @return - A promise which resolves if buffer doesn't overflow
229 */
230 public async record(
231 event: string | object,
232 provider?,
233 metrics?: EventMetrics
234 ) {
235 let params = null;
236 // this is just for compatibility, going to be deprecated
237 if (typeof event === 'string') {
238 params = {
239 event: {
240 name: event,
241 attributes: provider,
242 metrics,
243 },
244 provider: 'AWSPinpoint',
245 };
246 } else {
247 params = { event, provider };
248 }
249 return this._sendEvent(params);
250 }
251
252 public async updateEndpoint(attrs, provider?) {
253 const event = { ...attrs, name: '_update_endpoint' };
254
255 return this.record(event, provider);
256 }
257
258 private _sendEvent(params) {
259 if (this._disabled) {
260 logger.debug('Analytics has been disabled');
261 return Promise.resolve();
262 }
263
264 const provider = params.provider ? params.provider : 'AWSPinpoint';
265
266 return new Promise((resolve, reject) => {
267 this._pluggables.forEach(pluggable => {
268 if (pluggable.getProviderName() === provider) {
269 pluggable.record(params, { resolve, reject });
270 }
271 });
272 });
273 }
274
275 public autoTrack(trackerType, opts) {
276 if (!trackers[trackerType]) {
277 logger.debug('invalid tracker type');
278 return;
279 }
280
281 // to sync up two different configuration ways of auto session tracking
282 if (trackerType === 'session') {
283 this._config['autoSessionRecord'] = opts['enable'];
284 }
285
286 const tracker = this._trackers[trackerType];
287 if (!tracker) {
288 this._trackers[trackerType] = new trackers[trackerType](
289 this.record,
290 opts
291 );
292 } else {
293 tracker.configure(opts);
294 }
295 }
296}
297
298let endpointUpdated = false;
299let authConfigured = false;
300let analyticsConfigured = false;
301const listener = capsule => {
302 const { channel, payload } = capsule;
303 logger.debug('on hub capsule ' + channel, payload);
304
305 switch (channel) {
306 case 'auth':
307 authEvent(payload);
308 break;
309 case 'storage':
310 storageEvent(payload);
311 break;
312 case 'analytics':
313 analyticsEvent(payload);
314 break;
315 default:
316 break;
317 }
318};
319
320const storageEvent = payload => {
321 const {
322 data: { attrs, metrics },
323 } = payload;
324 if (!attrs) return;
325
326 if (analyticsConfigured) {
327 _instance
328 .record({
329 name: 'Storage',
330 attributes: attrs,
331 metrics,
332 })
333 .catch(e => {
334 logger.debug('Failed to send the storage event automatically', e);
335 });
336 }
337};
338
339const authEvent = payload => {
340 const { event } = payload;
341 if (!event) {
342 return;
343 }
344
345 const recordAuthEvent = async eventName => {
346 if (authConfigured && analyticsConfigured) {
347 try {
348 return await _instance.record({ name: `_userauth.${eventName}` });
349 } catch (err) {
350 logger.debug(
351 `Failed to send the ${eventName} event automatically`,
352 err
353 );
354 }
355 }
356 };
357
358 switch (event) {
359 case 'signIn':
360 return recordAuthEvent('sign_in');
361 case 'signUp':
362 return recordAuthEvent('sign_up');
363 case 'signOut':
364 return recordAuthEvent('sign_out');
365 case 'signIn_failure':
366 return recordAuthEvent('auth_fail');
367 case 'configured':
368 authConfigured = true;
369 if (authConfigured && analyticsConfigured) {
370 sendEvents();
371 }
372 break;
373 }
374};
375
376const analyticsEvent = payload => {
377 const { event } = payload;
378 if (!event) return;
379
380 switch (event) {
381 case 'pinpointProvider_configured':
382 analyticsConfigured = true;
383 if (authConfigured && analyticsConfigured) {
384 sendEvents();
385 }
386 break;
387 }
388};
389
390const sendEvents = () => {
391 const config = _instance.configure();
392 if (!endpointUpdated && config['autoSessionRecord']) {
393 _instance.updateEndpoint({ immediate: true }).catch(e => {
394 logger.debug('Failed to update the endpoint', e);
395 });
396 endpointUpdated = true;
397 }
398 _instance.autoTrack('session', {
399 enable: config['autoSessionRecord'],
400 });
401};
402
403export const Analytics = new AnalyticsClass();
404Amplify.register(Analytics);