UNPKG

46.2 kBJavaScriptView Raw
1import { getApp, _getProvider, _registerComponent, registerVersion } from '@firebase/app';
2import { Logger } from '@firebase/logger';
3import { ErrorFactory, calculateBackoffMillis, FirebaseError, isIndexedDBAvailable, validateIndexedDBOpenable, isBrowserExtension, areCookiesEnabled, getModularInstance, deepEqual } from '@firebase/util';
4import { Component } from '@firebase/component';
5import '@firebase/installations';
6
7/**
8 * @license
9 * Copyright 2019 Google LLC
10 *
11 * Licensed under the Apache License, Version 2.0 (the "License");
12 * you may not use this file except in compliance with the License.
13 * You may obtain a copy of the License at
14 *
15 * http://www.apache.org/licenses/LICENSE-2.0
16 *
17 * Unless required by applicable law or agreed to in writing, software
18 * distributed under the License is distributed on an "AS IS" BASIS,
19 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 * See the License for the specific language governing permissions and
21 * limitations under the License.
22 */
23/**
24 * Type constant for Firebase Analytics.
25 */
26const ANALYTICS_TYPE = 'analytics';
27// Key to attach FID to in gtag params.
28const GA_FID_KEY = 'firebase_id';
29const ORIGIN_KEY = 'origin';
30const FETCH_TIMEOUT_MILLIS = 60 * 1000;
31const DYNAMIC_CONFIG_URL = 'https://firebase.googleapis.com/v1alpha/projects/-/apps/{app-id}/webConfig';
32const GTAG_URL = 'https://www.googletagmanager.com/gtag/js';
33
34/**
35 * @license
36 * Copyright 2019 Google LLC
37 *
38 * Licensed under the Apache License, Version 2.0 (the "License");
39 * you may not use this file except in compliance with the License.
40 * You may obtain a copy of the License at
41 *
42 * http://www.apache.org/licenses/LICENSE-2.0
43 *
44 * Unless required by applicable law or agreed to in writing, software
45 * distributed under the License is distributed on an "AS IS" BASIS,
46 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
47 * See the License for the specific language governing permissions and
48 * limitations under the License.
49 */
50const logger = new Logger('@firebase/analytics');
51
52/**
53 * @license
54 * Copyright 2019 Google LLC
55 *
56 * Licensed under the Apache License, Version 2.0 (the "License");
57 * you may not use this file except in compliance with the License.
58 * You may obtain a copy of the License at
59 *
60 * http://www.apache.org/licenses/LICENSE-2.0
61 *
62 * Unless required by applicable law or agreed to in writing, software
63 * distributed under the License is distributed on an "AS IS" BASIS,
64 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
65 * See the License for the specific language governing permissions and
66 * limitations under the License.
67 */
68/**
69 * Makeshift polyfill for Promise.allSettled(). Resolves when all promises
70 * have either resolved or rejected.
71 *
72 * @param promises Array of promises to wait for.
73 */
74function promiseAllSettled(promises) {
75 return Promise.all(promises.map(promise => promise.catch(e => e)));
76}
77/**
78 * Inserts gtag script tag into the page to asynchronously download gtag.
79 * @param dataLayerName Name of datalayer (most often the default, "_dataLayer").
80 */
81function insertScriptTag(dataLayerName, measurementId) {
82 const script = document.createElement('script');
83 // We are not providing an analyticsId in the URL because it would trigger a `page_view`
84 // without fid. We will initialize ga-id using gtag (config) command together with fid.
85 script.src = `${GTAG_URL}?l=${dataLayerName}&id=${measurementId}`;
86 script.async = true;
87 document.head.appendChild(script);
88}
89/**
90 * Get reference to, or create, global datalayer.
91 * @param dataLayerName Name of datalayer (most often the default, "_dataLayer").
92 */
93function getOrCreateDataLayer(dataLayerName) {
94 // Check for existing dataLayer and create if needed.
95 let dataLayer = [];
96 if (Array.isArray(window[dataLayerName])) {
97 dataLayer = window[dataLayerName];
98 }
99 else {
100 window[dataLayerName] = dataLayer;
101 }
102 return dataLayer;
103}
104/**
105 * Wrapped gtag logic when gtag is called with 'config' command.
106 *
107 * @param gtagCore Basic gtag function that just appends to dataLayer.
108 * @param initializationPromisesMap Map of appIds to their initialization promises.
109 * @param dynamicConfigPromisesList Array of dynamic config fetch promises.
110 * @param measurementIdToAppId Map of GA measurementIDs to corresponding Firebase appId.
111 * @param measurementId GA Measurement ID to set config for.
112 * @param gtagParams Gtag config params to set.
113 */
114async function gtagOnConfig(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, measurementId, gtagParams) {
115 // If config is already fetched, we know the appId and can use it to look up what FID promise we
116 /// are waiting for, and wait only on that one.
117 const correspondingAppId = measurementIdToAppId[measurementId];
118 try {
119 if (correspondingAppId) {
120 await initializationPromisesMap[correspondingAppId];
121 }
122 else {
123 // If config is not fetched yet, wait for all configs (we don't know which one we need) and
124 // find the appId (if any) corresponding to this measurementId. If there is one, wait on
125 // that appId's initialization promise. If there is none, promise resolves and gtag
126 // call goes through.
127 const dynamicConfigResults = await promiseAllSettled(dynamicConfigPromisesList);
128 const foundConfig = dynamicConfigResults.find(config => config.measurementId === measurementId);
129 if (foundConfig) {
130 await initializationPromisesMap[foundConfig.appId];
131 }
132 }
133 }
134 catch (e) {
135 logger.error(e);
136 }
137 gtagCore("config" /* CONFIG */, measurementId, gtagParams);
138}
139/**
140 * Wrapped gtag logic when gtag is called with 'event' command.
141 *
142 * @param gtagCore Basic gtag function that just appends to dataLayer.
143 * @param initializationPromisesMap Map of appIds to their initialization promises.
144 * @param dynamicConfigPromisesList Array of dynamic config fetch promises.
145 * @param measurementId GA Measurement ID to log event to.
146 * @param gtagParams Params to log with this event.
147 */
148async function gtagOnEvent(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementId, gtagParams) {
149 try {
150 let initializationPromisesToWaitFor = [];
151 // If there's a 'send_to' param, check if any ID specified matches
152 // an initializeIds() promise we are waiting for.
153 if (gtagParams && gtagParams['send_to']) {
154 let gaSendToList = gtagParams['send_to'];
155 // Make it an array if is isn't, so it can be dealt with the same way.
156 if (!Array.isArray(gaSendToList)) {
157 gaSendToList = [gaSendToList];
158 }
159 // Checking 'send_to' fields requires having all measurement ID results back from
160 // the dynamic config fetch.
161 const dynamicConfigResults = await promiseAllSettled(dynamicConfigPromisesList);
162 for (const sendToId of gaSendToList) {
163 // Any fetched dynamic measurement ID that matches this 'send_to' ID
164 const foundConfig = dynamicConfigResults.find(config => config.measurementId === sendToId);
165 const initializationPromise = foundConfig && initializationPromisesMap[foundConfig.appId];
166 if (initializationPromise) {
167 initializationPromisesToWaitFor.push(initializationPromise);
168 }
169 else {
170 // Found an item in 'send_to' that is not associated
171 // directly with an FID, possibly a group. Empty this array,
172 // exit the loop early, and let it get populated below.
173 initializationPromisesToWaitFor = [];
174 break;
175 }
176 }
177 }
178 // This will be unpopulated if there was no 'send_to' field , or
179 // if not all entries in the 'send_to' field could be mapped to
180 // a FID. In these cases, wait on all pending initialization promises.
181 if (initializationPromisesToWaitFor.length === 0) {
182 initializationPromisesToWaitFor = Object.values(initializationPromisesMap);
183 }
184 // Run core gtag function with args after all relevant initialization
185 // promises have been resolved.
186 await Promise.all(initializationPromisesToWaitFor);
187 // Workaround for http://b/141370449 - third argument cannot be undefined.
188 gtagCore("event" /* EVENT */, measurementId, gtagParams || {});
189 }
190 catch (e) {
191 logger.error(e);
192 }
193}
194/**
195 * Wraps a standard gtag function with extra code to wait for completion of
196 * relevant initialization promises before sending requests.
197 *
198 * @param gtagCore Basic gtag function that just appends to dataLayer.
199 * @param initializationPromisesMap Map of appIds to their initialization promises.
200 * @param dynamicConfigPromisesList Array of dynamic config fetch promises.
201 * @param measurementIdToAppId Map of GA measurementIDs to corresponding Firebase appId.
202 */
203function wrapGtag(gtagCore,
204/**
205 * Allows wrapped gtag calls to wait on whichever intialization promises are required,
206 * depending on the contents of the gtag params' `send_to` field, if any.
207 */
208initializationPromisesMap,
209/**
210 * Wrapped gtag calls sometimes require all dynamic config fetches to have returned
211 * before determining what initialization promises (which include FIDs) to wait for.
212 */
213dynamicConfigPromisesList,
214/**
215 * Wrapped gtag config calls can narrow down which initialization promise (with FID)
216 * to wait for if the measurementId is already fetched, by getting the corresponding appId,
217 * which is the key for the initialization promises map.
218 */
219measurementIdToAppId) {
220 /**
221 * Wrapper around gtag that ensures FID is sent with gtag calls.
222 * @param command Gtag command type.
223 * @param idOrNameOrParams Measurement ID if command is EVENT/CONFIG, params if command is SET.
224 * @param gtagParams Params if event is EVENT/CONFIG.
225 */
226 async function gtagWrapper(command, idOrNameOrParams, gtagParams) {
227 try {
228 // If event, check that relevant initialization promises have completed.
229 if (command === "event" /* EVENT */) {
230 // If EVENT, second arg must be measurementId.
231 await gtagOnEvent(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, idOrNameOrParams, gtagParams);
232 }
233 else if (command === "config" /* CONFIG */) {
234 // If CONFIG, second arg must be measurementId.
235 await gtagOnConfig(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, idOrNameOrParams, gtagParams);
236 }
237 else {
238 // If SET, second arg must be params.
239 gtagCore("set" /* SET */, idOrNameOrParams);
240 }
241 }
242 catch (e) {
243 logger.error(e);
244 }
245 }
246 return gtagWrapper;
247}
248/**
249 * Creates global gtag function or wraps existing one if found.
250 * This wrapped function attaches Firebase instance ID (FID) to gtag 'config' and
251 * 'event' calls that belong to the GAID associated with this Firebase instance.
252 *
253 * @param initializationPromisesMap Map of appIds to their initialization promises.
254 * @param dynamicConfigPromisesList Array of dynamic config fetch promises.
255 * @param measurementIdToAppId Map of GA measurementIDs to corresponding Firebase appId.
256 * @param dataLayerName Name of global GA datalayer array.
257 * @param gtagFunctionName Name of global gtag function ("gtag" if not user-specified).
258 */
259function wrapOrCreateGtag(initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, dataLayerName, gtagFunctionName) {
260 // Create a basic core gtag function
261 let gtagCore = function (..._args) {
262 // Must push IArguments object, not an array.
263 window[dataLayerName].push(arguments);
264 };
265 // Replace it with existing one if found
266 if (window[gtagFunctionName] &&
267 typeof window[gtagFunctionName] === 'function') {
268 // @ts-ignore
269 gtagCore = window[gtagFunctionName];
270 }
271 window[gtagFunctionName] = wrapGtag(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId);
272 return {
273 gtagCore,
274 wrappedGtag: window[gtagFunctionName]
275 };
276}
277/**
278 * Returns first script tag in DOM matching our gtag url pattern.
279 */
280function findGtagScriptOnPage() {
281 const scriptTags = window.document.getElementsByTagName('script');
282 for (const tag of Object.values(scriptTags)) {
283 if (tag.src && tag.src.includes(GTAG_URL)) {
284 return tag;
285 }
286 }
287 return null;
288}
289
290/**
291 * @license
292 * Copyright 2019 Google LLC
293 *
294 * Licensed under the Apache License, Version 2.0 (the "License");
295 * you may not use this file except in compliance with the License.
296 * You may obtain a copy of the License at
297 *
298 * http://www.apache.org/licenses/LICENSE-2.0
299 *
300 * Unless required by applicable law or agreed to in writing, software
301 * distributed under the License is distributed on an "AS IS" BASIS,
302 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
303 * See the License for the specific language governing permissions and
304 * limitations under the License.
305 */
306const ERRORS = {
307 ["already-exists" /* ALREADY_EXISTS */]: 'A Firebase Analytics instance with the appId {$id} ' +
308 ' already exists. ' +
309 'Only one Firebase Analytics instance can be created for each appId.',
310 ["already-initialized" /* ALREADY_INITIALIZED */]: 'initializeAnalytics() cannot be called again with different options than those ' +
311 'it was initially called with. It can be called again with the same options to ' +
312 'return the existing instance, or getAnalytics() can be used ' +
313 'to get a reference to the already-intialized instance.',
314 ["already-initialized-settings" /* ALREADY_INITIALIZED_SETTINGS */]: 'Firebase Analytics has already been initialized.' +
315 'settings() must be called before initializing any Analytics instance' +
316 'or it will have no effect.',
317 ["interop-component-reg-failed" /* INTEROP_COMPONENT_REG_FAILED */]: 'Firebase Analytics Interop Component failed to instantiate: {$reason}',
318 ["invalid-analytics-context" /* INVALID_ANALYTICS_CONTEXT */]: 'Firebase Analytics is not supported in this environment. ' +
319 'Wrap initialization of analytics in analytics.isSupported() ' +
320 'to prevent initialization in unsupported environments. Details: {$errorInfo}',
321 ["indexeddb-unavailable" /* INDEXEDDB_UNAVAILABLE */]: 'IndexedDB unavailable or restricted in this environment. ' +
322 'Wrap initialization of analytics in analytics.isSupported() ' +
323 'to prevent initialization in unsupported environments. Details: {$errorInfo}',
324 ["fetch-throttle" /* FETCH_THROTTLE */]: 'The config fetch request timed out while in an exponential backoff state.' +
325 ' Unix timestamp in milliseconds when fetch request throttling ends: {$throttleEndTimeMillis}.',
326 ["config-fetch-failed" /* CONFIG_FETCH_FAILED */]: 'Dynamic config fetch failed: [{$httpStatus}] {$responseMessage}',
327 ["no-api-key" /* NO_API_KEY */]: 'The "apiKey" field is empty in the local Firebase config. Firebase Analytics requires this field to' +
328 'contain a valid API key.',
329 ["no-app-id" /* NO_APP_ID */]: 'The "appId" field is empty in the local Firebase config. Firebase Analytics requires this field to' +
330 'contain a valid app ID.'
331};
332const ERROR_FACTORY = new ErrorFactory('analytics', 'Analytics', ERRORS);
333
334/**
335 * @license
336 * Copyright 2020 Google LLC
337 *
338 * Licensed under the Apache License, Version 2.0 (the "License");
339 * you may not use this file except in compliance with the License.
340 * You may obtain a copy of the License at
341 *
342 * http://www.apache.org/licenses/LICENSE-2.0
343 *
344 * Unless required by applicable law or agreed to in writing, software
345 * distributed under the License is distributed on an "AS IS" BASIS,
346 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
347 * See the License for the specific language governing permissions and
348 * limitations under the License.
349 */
350/**
351 * Backoff factor for 503 errors, which we want to be conservative about
352 * to avoid overloading servers. Each retry interval will be
353 * BASE_INTERVAL_MILLIS * LONG_RETRY_FACTOR ^ retryCount, so the second one
354 * will be ~30 seconds (with fuzzing).
355 */
356const LONG_RETRY_FACTOR = 30;
357/**
358 * Base wait interval to multiplied by backoffFactor^backoffCount.
359 */
360const BASE_INTERVAL_MILLIS = 1000;
361/**
362 * Stubbable retry data storage class.
363 */
364class RetryData {
365 constructor(throttleMetadata = {}, intervalMillis = BASE_INTERVAL_MILLIS) {
366 this.throttleMetadata = throttleMetadata;
367 this.intervalMillis = intervalMillis;
368 }
369 getThrottleMetadata(appId) {
370 return this.throttleMetadata[appId];
371 }
372 setThrottleMetadata(appId, metadata) {
373 this.throttleMetadata[appId] = metadata;
374 }
375 deleteThrottleMetadata(appId) {
376 delete this.throttleMetadata[appId];
377 }
378}
379const defaultRetryData = new RetryData();
380/**
381 * Set GET request headers.
382 * @param apiKey App API key.
383 */
384function getHeaders(apiKey) {
385 return new Headers({
386 Accept: 'application/json',
387 'x-goog-api-key': apiKey
388 });
389}
390/**
391 * Fetches dynamic config from backend.
392 * @param app Firebase app to fetch config for.
393 */
394async function fetchDynamicConfig(appFields) {
395 var _a;
396 const { appId, apiKey } = appFields;
397 const request = {
398 method: 'GET',
399 headers: getHeaders(apiKey)
400 };
401 const appUrl = DYNAMIC_CONFIG_URL.replace('{app-id}', appId);
402 const response = await fetch(appUrl, request);
403 if (response.status !== 200 && response.status !== 304) {
404 let errorMessage = '';
405 try {
406 // Try to get any error message text from server response.
407 const jsonResponse = (await response.json());
408 if ((_a = jsonResponse.error) === null || _a === void 0 ? void 0 : _a.message) {
409 errorMessage = jsonResponse.error.message;
410 }
411 }
412 catch (_ignored) { }
413 throw ERROR_FACTORY.create("config-fetch-failed" /* CONFIG_FETCH_FAILED */, {
414 httpStatus: response.status,
415 responseMessage: errorMessage
416 });
417 }
418 return response.json();
419}
420/**
421 * Fetches dynamic config from backend, retrying if failed.
422 * @param app Firebase app to fetch config for.
423 */
424async function fetchDynamicConfigWithRetry(app,
425// retryData and timeoutMillis are parameterized to allow passing a different value for testing.
426retryData = defaultRetryData, timeoutMillis) {
427 const { appId, apiKey, measurementId } = app.options;
428 if (!appId) {
429 throw ERROR_FACTORY.create("no-app-id" /* NO_APP_ID */);
430 }
431 if (!apiKey) {
432 if (measurementId) {
433 return {
434 measurementId,
435 appId
436 };
437 }
438 throw ERROR_FACTORY.create("no-api-key" /* NO_API_KEY */);
439 }
440 const throttleMetadata = retryData.getThrottleMetadata(appId) || {
441 backoffCount: 0,
442 throttleEndTimeMillis: Date.now()
443 };
444 const signal = new AnalyticsAbortSignal();
445 setTimeout(async () => {
446 // Note a very low delay, eg < 10ms, can elapse before listeners are initialized.
447 signal.abort();
448 }, timeoutMillis !== undefined ? timeoutMillis : FETCH_TIMEOUT_MILLIS);
449 return attemptFetchDynamicConfigWithRetry({ appId, apiKey, measurementId }, throttleMetadata, signal, retryData);
450}
451/**
452 * Runs one retry attempt.
453 * @param appFields Necessary app config fields.
454 * @param throttleMetadata Ongoing metadata to determine throttling times.
455 * @param signal Abort signal.
456 */
457async function attemptFetchDynamicConfigWithRetry(appFields, { throttleEndTimeMillis, backoffCount }, signal, retryData = defaultRetryData // for testing
458) {
459 const { appId, measurementId } = appFields;
460 // Starts with a (potentially zero) timeout to support resumption from stored state.
461 // Ensures the throttle end time is honored if the last attempt timed out.
462 // Note the SDK will never make a request if the fetch timeout expires at this point.
463 try {
464 await setAbortableTimeout(signal, throttleEndTimeMillis);
465 }
466 catch (e) {
467 if (measurementId) {
468 logger.warn(`Timed out fetching this Firebase app's measurement ID from the server.` +
469 ` Falling back to the measurement ID ${measurementId}` +
470 ` provided in the "measurementId" field in the local Firebase config. [${e.message}]`);
471 return { appId, measurementId };
472 }
473 throw e;
474 }
475 try {
476 const response = await fetchDynamicConfig(appFields);
477 // Note the SDK only clears throttle state if response is success or non-retriable.
478 retryData.deleteThrottleMetadata(appId);
479 return response;
480 }
481 catch (e) {
482 if (!isRetriableError(e)) {
483 retryData.deleteThrottleMetadata(appId);
484 if (measurementId) {
485 logger.warn(`Failed to fetch this Firebase app's measurement ID from the server.` +
486 ` Falling back to the measurement ID ${measurementId}` +
487 ` provided in the "measurementId" field in the local Firebase config. [${e.message}]`);
488 return { appId, measurementId };
489 }
490 else {
491 throw e;
492 }
493 }
494 const backoffMillis = Number(e.customData.httpStatus) === 503
495 ? calculateBackoffMillis(backoffCount, retryData.intervalMillis, LONG_RETRY_FACTOR)
496 : calculateBackoffMillis(backoffCount, retryData.intervalMillis);
497 // Increments backoff state.
498 const throttleMetadata = {
499 throttleEndTimeMillis: Date.now() + backoffMillis,
500 backoffCount: backoffCount + 1
501 };
502 // Persists state.
503 retryData.setThrottleMetadata(appId, throttleMetadata);
504 logger.debug(`Calling attemptFetch again in ${backoffMillis} millis`);
505 return attemptFetchDynamicConfigWithRetry(appFields, throttleMetadata, signal, retryData);
506 }
507}
508/**
509 * Supports waiting on a backoff by:
510 *
511 * <ul>
512 * <li>Promisifying setTimeout, so we can set a timeout in our Promise chain</li>
513 * <li>Listening on a signal bus for abort events, just like the Fetch API</li>
514 * <li>Failing in the same way the Fetch API fails, so timing out a live request and a throttled
515 * request appear the same.</li>
516 * </ul>
517 *
518 * <p>Visible for testing.
519 */
520function setAbortableTimeout(signal, throttleEndTimeMillis) {
521 return new Promise((resolve, reject) => {
522 // Derives backoff from given end time, normalizing negative numbers to zero.
523 const backoffMillis = Math.max(throttleEndTimeMillis - Date.now(), 0);
524 const timeout = setTimeout(resolve, backoffMillis);
525 // Adds listener, rather than sets onabort, because signal is a shared object.
526 signal.addEventListener(() => {
527 clearTimeout(timeout);
528 // If the request completes before this timeout, the rejection has no effect.
529 reject(ERROR_FACTORY.create("fetch-throttle" /* FETCH_THROTTLE */, {
530 throttleEndTimeMillis
531 }));
532 });
533 });
534}
535/**
536 * Returns true if the {@link Error} indicates a fetch request may succeed later.
537 */
538function isRetriableError(e) {
539 if (!(e instanceof FirebaseError) || !e.customData) {
540 return false;
541 }
542 // Uses string index defined by ErrorData, which FirebaseError implements.
543 const httpStatus = Number(e.customData['httpStatus']);
544 return (httpStatus === 429 ||
545 httpStatus === 500 ||
546 httpStatus === 503 ||
547 httpStatus === 504);
548}
549/**
550 * Shims a minimal AbortSignal (copied from Remote Config).
551 *
552 * <p>AbortController's AbortSignal conveniently decouples fetch timeout logic from other aspects
553 * of networking, such as retries. Firebase doesn't use AbortController enough to justify a
554 * polyfill recommendation, like we do with the Fetch API, but this minimal shim can easily be
555 * swapped out if/when we do.
556 */
557class AnalyticsAbortSignal {
558 constructor() {
559 this.listeners = [];
560 }
561 addEventListener(listener) {
562 this.listeners.push(listener);
563 }
564 abort() {
565 this.listeners.forEach(listener => listener());
566 }
567}
568
569/**
570 * @license
571 * Copyright 2020 Google LLC
572 *
573 * Licensed under the Apache License, Version 2.0 (the "License");
574 * you may not use this file except in compliance with the License.
575 * You may obtain a copy of the License at
576 *
577 * http://www.apache.org/licenses/LICENSE-2.0
578 *
579 * Unless required by applicable law or agreed to in writing, software
580 * distributed under the License is distributed on an "AS IS" BASIS,
581 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
582 * See the License for the specific language governing permissions and
583 * limitations under the License.
584 */
585async function validateIndexedDB() {
586 if (!isIndexedDBAvailable()) {
587 logger.warn(ERROR_FACTORY.create("indexeddb-unavailable" /* INDEXEDDB_UNAVAILABLE */, {
588 errorInfo: 'IndexedDB is not available in this environment.'
589 }).message);
590 return false;
591 }
592 else {
593 try {
594 await validateIndexedDBOpenable();
595 }
596 catch (e) {
597 logger.warn(ERROR_FACTORY.create("indexeddb-unavailable" /* INDEXEDDB_UNAVAILABLE */, {
598 errorInfo: e
599 }).message);
600 return false;
601 }
602 }
603 return true;
604}
605/**
606 * Initialize the analytics instance in gtag.js by calling config command with fid.
607 *
608 * NOTE: We combine analytics initialization and setting fid together because we want fid to be
609 * part of the `page_view` event that's sent during the initialization
610 * @param app Firebase app
611 * @param gtagCore The gtag function that's not wrapped.
612 * @param dynamicConfigPromisesList Array of all dynamic config promises.
613 * @param measurementIdToAppId Maps measurementID to appID.
614 * @param installations _FirebaseInstallationsInternal instance.
615 *
616 * @returns Measurement ID.
617 */
618async function _initializeAnalytics(app, dynamicConfigPromisesList, measurementIdToAppId, installations, gtagCore, dataLayerName, options) {
619 var _a;
620 const dynamicConfigPromise = fetchDynamicConfigWithRetry(app);
621 // Once fetched, map measurementIds to appId, for ease of lookup in wrapped gtag function.
622 dynamicConfigPromise
623 .then(config => {
624 measurementIdToAppId[config.measurementId] = config.appId;
625 if (app.options.measurementId &&
626 config.measurementId !== app.options.measurementId) {
627 logger.warn(`The measurement ID in the local Firebase config (${app.options.measurementId})` +
628 ` does not match the measurement ID fetched from the server (${config.measurementId}).` +
629 ` To ensure analytics events are always sent to the correct Analytics property,` +
630 ` update the` +
631 ` measurement ID field in the local config or remove it from the local config.`);
632 }
633 })
634 .catch(e => logger.error(e));
635 // Add to list to track state of all dynamic config promises.
636 dynamicConfigPromisesList.push(dynamicConfigPromise);
637 const fidPromise = validateIndexedDB().then(envIsValid => {
638 if (envIsValid) {
639 return installations.getId();
640 }
641 else {
642 return undefined;
643 }
644 });
645 const [dynamicConfig, fid] = await Promise.all([
646 dynamicConfigPromise,
647 fidPromise
648 ]);
649 // Detect if user has already put the gtag <script> tag on this page.
650 if (!findGtagScriptOnPage()) {
651 insertScriptTag(dataLayerName, dynamicConfig.measurementId);
652 }
653 // This command initializes gtag.js and only needs to be called once for the entire web app,
654 // but since it is idempotent, we can call it multiple times.
655 // We keep it together with other initialization logic for better code structure.
656 // eslint-disable-next-line @typescript-eslint/no-explicit-any
657 gtagCore('js', new Date());
658 // User config added first. We don't want users to accidentally overwrite
659 // base Firebase config properties.
660 const configProperties = (_a = options === null || options === void 0 ? void 0 : options.config) !== null && _a !== void 0 ? _a : {};
661 // guard against developers accidentally setting properties with prefix `firebase_`
662 configProperties[ORIGIN_KEY] = 'firebase';
663 configProperties.update = true;
664 if (fid != null) {
665 configProperties[GA_FID_KEY] = fid;
666 }
667 // It should be the first config command called on this GA-ID
668 // Initialize this GA-ID and set FID on it using the gtag config API.
669 // Note: This will trigger a page_view event unless 'send_page_view' is set to false in
670 // `configProperties`.
671 gtagCore("config" /* CONFIG */, dynamicConfig.measurementId, configProperties);
672 return dynamicConfig.measurementId;
673}
674
675/**
676 * @license
677 * Copyright 2019 Google LLC
678 *
679 * Licensed under the Apache License, Version 2.0 (the "License");
680 * you may not use this file except in compliance with the License.
681 * You may obtain a copy of the License at
682 *
683 * http://www.apache.org/licenses/LICENSE-2.0
684 *
685 * Unless required by applicable law or agreed to in writing, software
686 * distributed under the License is distributed on an "AS IS" BASIS,
687 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
688 * See the License for the specific language governing permissions and
689 * limitations under the License.
690 */
691/**
692 * Analytics Service class.
693 */
694class AnalyticsService {
695 constructor(app) {
696 this.app = app;
697 }
698 _delete() {
699 delete initializationPromisesMap[this.app.options.appId];
700 return Promise.resolve();
701 }
702}
703/**
704 * Maps appId to full initialization promise. Wrapped gtag calls must wait on
705 * all or some of these, depending on the call's `send_to` param and the status
706 * of the dynamic config fetches (see below).
707 */
708let initializationPromisesMap = {};
709/**
710 * List of dynamic config fetch promises. In certain cases, wrapped gtag calls
711 * wait on all these to be complete in order to determine if it can selectively
712 * wait for only certain initialization (FID) promises or if it must wait for all.
713 */
714let dynamicConfigPromisesList = [];
715/**
716 * Maps fetched measurementIds to appId. Populated when the app's dynamic config
717 * fetch completes. If already populated, gtag config calls can use this to
718 * selectively wait for only this app's initialization promise (FID) instead of all
719 * initialization promises.
720 */
721const measurementIdToAppId = {};
722/**
723 * Name for window global data layer array used by GA: defaults to 'dataLayer'.
724 */
725let dataLayerName = 'dataLayer';
726/**
727 * Name for window global gtag function used by GA: defaults to 'gtag'.
728 */
729let gtagName = 'gtag';
730/**
731 * Reproduction of standard gtag function or reference to existing
732 * gtag function on window object.
733 */
734let gtagCoreFunction;
735/**
736 * Wrapper around gtag function that ensures FID is sent with all
737 * relevant event and config calls.
738 */
739let wrappedGtagFunction;
740/**
741 * Flag to ensure page initialization steps (creation or wrapping of
742 * dataLayer and gtag script) are only run once per page load.
743 */
744let globalInitDone = false;
745/**
746 * Configures Firebase Analytics to use custom `gtag` or `dataLayer` names.
747 * Intended to be used if `gtag.js` script has been installed on
748 * this page independently of Firebase Analytics, and is using non-default
749 * names for either the `gtag` function or for `dataLayer`.
750 * Must be called before calling `getAnalytics()` or it won't
751 * have any effect.
752 *
753 * @public
754 *
755 * @param options - Custom gtag and dataLayer names.
756 */
757function settings(options) {
758 if (globalInitDone) {
759 throw ERROR_FACTORY.create("already-initialized" /* ALREADY_INITIALIZED */);
760 }
761 if (options.dataLayerName) {
762 dataLayerName = options.dataLayerName;
763 }
764 if (options.gtagName) {
765 gtagName = options.gtagName;
766 }
767}
768/**
769 * Returns true if no environment mismatch is found.
770 * If environment mismatches are found, throws an INVALID_ANALYTICS_CONTEXT
771 * error that also lists details for each mismatch found.
772 */
773function warnOnBrowserContextMismatch() {
774 const mismatchedEnvMessages = [];
775 if (isBrowserExtension()) {
776 mismatchedEnvMessages.push('This is a browser extension environment.');
777 }
778 if (!areCookiesEnabled()) {
779 mismatchedEnvMessages.push('Cookies are not available.');
780 }
781 if (mismatchedEnvMessages.length > 0) {
782 const details = mismatchedEnvMessages
783 .map((message, index) => `(${index + 1}) ${message}`)
784 .join(' ');
785 const err = ERROR_FACTORY.create("invalid-analytics-context" /* INVALID_ANALYTICS_CONTEXT */, {
786 errorInfo: details
787 });
788 logger.warn(err.message);
789 }
790}
791/**
792 * Analytics instance factory.
793 * @internal
794 */
795function factory(app, installations, options) {
796 warnOnBrowserContextMismatch();
797 const appId = app.options.appId;
798 if (!appId) {
799 throw ERROR_FACTORY.create("no-app-id" /* NO_APP_ID */);
800 }
801 if (!app.options.apiKey) {
802 if (app.options.measurementId) {
803 logger.warn(`The "apiKey" field is empty in the local Firebase config. This is needed to fetch the latest` +
804 ` measurement ID for this Firebase app. Falling back to the measurement ID ${app.options.measurementId}` +
805 ` provided in the "measurementId" field in the local Firebase config.`);
806 }
807 else {
808 throw ERROR_FACTORY.create("no-api-key" /* NO_API_KEY */);
809 }
810 }
811 if (initializationPromisesMap[appId] != null) {
812 throw ERROR_FACTORY.create("already-exists" /* ALREADY_EXISTS */, {
813 id: appId
814 });
815 }
816 if (!globalInitDone) {
817 // Steps here should only be done once per page: creation or wrapping
818 // of dataLayer and global gtag function.
819 getOrCreateDataLayer(dataLayerName);
820 const { wrappedGtag, gtagCore } = wrapOrCreateGtag(initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, dataLayerName, gtagName);
821 wrappedGtagFunction = wrappedGtag;
822 gtagCoreFunction = gtagCore;
823 globalInitDone = true;
824 }
825 // Async but non-blocking.
826 // This map reflects the completion state of all promises for each appId.
827 initializationPromisesMap[appId] = _initializeAnalytics(app, dynamicConfigPromisesList, measurementIdToAppId, installations, gtagCoreFunction, dataLayerName, options);
828 const analyticsInstance = new AnalyticsService(app);
829 return analyticsInstance;
830}
831
832/**
833 * @license
834 * Copyright 2019 Google LLC
835 *
836 * Licensed under the Apache License, Version 2.0 (the "License");
837 * you may not use this file except in compliance with the License.
838 * You may obtain a copy of the License at
839 *
840 * http://www.apache.org/licenses/LICENSE-2.0
841 *
842 * Unless required by applicable law or agreed to in writing, software
843 * distributed under the License is distributed on an "AS IS" BASIS,
844 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
845 * See the License for the specific language governing permissions and
846 * limitations under the License.
847 */
848/**
849 * Logs an analytics event through the Firebase SDK.
850 *
851 * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event
852 * @param eventName Google Analytics event name, choose from standard list or use a custom string.
853 * @param eventParams Analytics event parameters.
854 */
855async function logEvent$1(gtagFunction, initializationPromise, eventName, eventParams, options) {
856 if (options && options.global) {
857 gtagFunction("event" /* EVENT */, eventName, eventParams);
858 return;
859 }
860 else {
861 const measurementId = await initializationPromise;
862 const params = Object.assign(Object.assign({}, eventParams), { 'send_to': measurementId });
863 gtagFunction("event" /* EVENT */, eventName, params);
864 }
865}
866/**
867 * Set screen_name parameter for this Google Analytics ID.
868 *
869 * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event
870 * @param screenName Screen name string to set.
871 */
872async function setCurrentScreen$1(gtagFunction, initializationPromise, screenName, options) {
873 if (options && options.global) {
874 gtagFunction("set" /* SET */, { 'screen_name': screenName });
875 return Promise.resolve();
876 }
877 else {
878 const measurementId = await initializationPromise;
879 gtagFunction("config" /* CONFIG */, measurementId, {
880 update: true,
881 'screen_name': screenName
882 });
883 }
884}
885/**
886 * Set user_id parameter for this Google Analytics ID.
887 *
888 * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event
889 * @param id User ID string to set
890 */
891async function setUserId$1(gtagFunction, initializationPromise, id, options) {
892 if (options && options.global) {
893 gtagFunction("set" /* SET */, { 'user_id': id });
894 return Promise.resolve();
895 }
896 else {
897 const measurementId = await initializationPromise;
898 gtagFunction("config" /* CONFIG */, measurementId, {
899 update: true,
900 'user_id': id
901 });
902 }
903}
904/**
905 * Set all other user properties other than user_id and screen_name.
906 *
907 * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event
908 * @param properties Map of user properties to set
909 */
910async function setUserProperties$1(gtagFunction, initializationPromise, properties, options) {
911 if (options && options.global) {
912 const flatProperties = {};
913 for (const key of Object.keys(properties)) {
914 // use dot notation for merge behavior in gtag.js
915 flatProperties[`user_properties.${key}`] = properties[key];
916 }
917 gtagFunction("set" /* SET */, flatProperties);
918 return Promise.resolve();
919 }
920 else {
921 const measurementId = await initializationPromise;
922 gtagFunction("config" /* CONFIG */, measurementId, {
923 update: true,
924 'user_properties': properties
925 });
926 }
927}
928/**
929 * Set whether collection is enabled for this ID.
930 *
931 * @param enabled If true, collection is enabled for this ID.
932 */
933async function setAnalyticsCollectionEnabled$1(initializationPromise, enabled) {
934 const measurementId = await initializationPromise;
935 window[`ga-disable-${measurementId}`] = !enabled;
936}
937
938/* eslint-disable @typescript-eslint/no-explicit-any */
939/**
940 * Returns an {@link Analytics} instance for the given app.
941 *
942 * @public
943 *
944 * @param app - The {@link @firebase/app#FirebaseApp} to use.
945 */
946function getAnalytics(app = getApp()) {
947 app = getModularInstance(app);
948 // Dependencies
949 const analyticsProvider = _getProvider(app, ANALYTICS_TYPE);
950 if (analyticsProvider.isInitialized()) {
951 return analyticsProvider.getImmediate();
952 }
953 return initializeAnalytics(app);
954}
955/**
956 * Returns an {@link Analytics} instance for the given app.
957 *
958 * @public
959 *
960 * @param app - The {@link @firebase/app#FirebaseApp} to use.
961 */
962function initializeAnalytics(app, options = {}) {
963 // Dependencies
964 const analyticsProvider = _getProvider(app, ANALYTICS_TYPE);
965 if (analyticsProvider.isInitialized()) {
966 const existingInstance = analyticsProvider.getImmediate();
967 if (deepEqual(options, analyticsProvider.getOptions())) {
968 return existingInstance;
969 }
970 else {
971 throw ERROR_FACTORY.create("already-initialized" /* ALREADY_INITIALIZED */);
972 }
973 }
974 const analyticsInstance = analyticsProvider.initialize({ options });
975 return analyticsInstance;
976}
977/**
978 * This is a public static method provided to users that wraps four different checks:
979 *
980 * 1. Check if it's not a browser extension environment.
981 * 2. Check if cookies are enabled in current browser.
982 * 3. Check if IndexedDB is supported by the browser environment.
983 * 4. Check if the current browser context is valid for using `IndexedDB.open()`.
984 *
985 * @public
986 *
987 */
988async function isSupported() {
989 if (isBrowserExtension()) {
990 return false;
991 }
992 if (!areCookiesEnabled()) {
993 return false;
994 }
995 if (!isIndexedDBAvailable()) {
996 return false;
997 }
998 try {
999 const isDBOpenable = await validateIndexedDBOpenable();
1000 return isDBOpenable;
1001 }
1002 catch (error) {
1003 return false;
1004 }
1005}
1006/**
1007 * Use gtag `config` command to set `screen_name`.
1008 *
1009 * @public
1010 *
1011 * @param analyticsInstance - The {@link Analytics} instance.
1012 * @param screenName - Screen name to set.
1013 */
1014function setCurrentScreen(analyticsInstance, screenName, options) {
1015 analyticsInstance = getModularInstance(analyticsInstance);
1016 setCurrentScreen$1(wrappedGtagFunction, initializationPromisesMap[analyticsInstance.app.options.appId], screenName, options).catch(e => logger.error(e));
1017}
1018/**
1019 * Use gtag `config` command to set `user_id`.
1020 *
1021 * @public
1022 *
1023 * @param analyticsInstance - The {@link Analytics} instance.
1024 * @param id - User ID to set.
1025 */
1026function setUserId(analyticsInstance, id, options) {
1027 analyticsInstance = getModularInstance(analyticsInstance);
1028 setUserId$1(wrappedGtagFunction, initializationPromisesMap[analyticsInstance.app.options.appId], id, options).catch(e => logger.error(e));
1029}
1030/**
1031 * Use gtag `config` command to set all params specified.
1032 *
1033 * @public
1034 */
1035function setUserProperties(analyticsInstance, properties, options) {
1036 analyticsInstance = getModularInstance(analyticsInstance);
1037 setUserProperties$1(wrappedGtagFunction, initializationPromisesMap[analyticsInstance.app.options.appId], properties, options).catch(e => logger.error(e));
1038}
1039/**
1040 * Sets whether Google Analytics collection is enabled for this app on this device.
1041 * Sets global `window['ga-disable-analyticsId'] = true;`
1042 *
1043 * @public
1044 *
1045 * @param analyticsInstance - The {@link Analytics} instance.
1046 * @param enabled - If true, enables collection, if false, disables it.
1047 */
1048function setAnalyticsCollectionEnabled(analyticsInstance, enabled) {
1049 analyticsInstance = getModularInstance(analyticsInstance);
1050 setAnalyticsCollectionEnabled$1(initializationPromisesMap[analyticsInstance.app.options.appId], enabled).catch(e => logger.error(e));
1051}
1052/**
1053 * Sends a Google Analytics event with given `eventParams`. This method
1054 * automatically associates this logged event with this Firebase web
1055 * app instance on this device.
1056 * List of official event parameters can be found in the gtag.js
1057 * reference documentation:
1058 * {@link https://developers.google.com/gtagjs/reference/ga4-events
1059 * | the GA4 reference documentation}.
1060 *
1061 * @public
1062 */
1063function logEvent(analyticsInstance, eventName, eventParams, options) {
1064 analyticsInstance = getModularInstance(analyticsInstance);
1065 logEvent$1(wrappedGtagFunction, initializationPromisesMap[analyticsInstance.app.options.appId], eventName, eventParams, options).catch(e => logger.error(e));
1066}
1067
1068const name = "@firebase/analytics";
1069const version = "0.7.5";
1070
1071/**
1072 * Firebase Analytics
1073 *
1074 * @packageDocumentation
1075 */
1076function registerAnalytics() {
1077 _registerComponent(new Component(ANALYTICS_TYPE, (container, { options: analyticsOptions }) => {
1078 // getImmediate for FirebaseApp will always succeed
1079 const app = container.getProvider('app').getImmediate();
1080 const installations = container
1081 .getProvider('installations-internal')
1082 .getImmediate();
1083 return factory(app, installations, analyticsOptions);
1084 }, "PUBLIC" /* PUBLIC */));
1085 _registerComponent(new Component('analytics-internal', internalFactory, "PRIVATE" /* PRIVATE */));
1086 registerVersion(name, version);
1087 // BUILD_TARGET will be replaced by values like esm5, esm2017, cjs5, etc during the compilation
1088 registerVersion(name, version, 'esm2017');
1089 function internalFactory(container) {
1090 try {
1091 const analytics = container.getProvider(ANALYTICS_TYPE).getImmediate();
1092 return {
1093 logEvent: (eventName, eventParams, options) => logEvent(analytics, eventName, eventParams, options)
1094 };
1095 }
1096 catch (e) {
1097 throw ERROR_FACTORY.create("interop-component-reg-failed" /* INTEROP_COMPONENT_REG_FAILED */, {
1098 reason: e
1099 });
1100 }
1101 }
1102}
1103registerAnalytics();
1104
1105export { getAnalytics, initializeAnalytics, isSupported, logEvent, setAnalyticsCollectionEnabled, setCurrentScreen, setUserId, setUserProperties, settings };
1106//# sourceMappingURL=index.esm2017.js.map