UNPKG

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