import firebase from '@firebase/app'; import '@firebase/installations'; import { Logger } from '@firebase/logger'; import { ErrorFactory, calculateBackoffMillis, FirebaseError, isIndexedDBAvailable, validateIndexedDBOpenable, isBrowserExtension, areCookiesEnabled } from '@firebase/util'; import { Component } from '@firebase/component'; /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Key to attach FID to in gtag params. const GA_FID_KEY = 'firebase_id'; const ORIGIN_KEY = 'origin'; const FETCH_TIMEOUT_MILLIS = 60 * 1000; const DYNAMIC_CONFIG_URL = 'https://firebase.googleapis.com/v1alpha/projects/-/apps/{app-id}/webConfig'; const GTAG_URL = 'https://www.googletagmanager.com/gtag/js'; var GtagCommand; (function (GtagCommand) { GtagCommand["EVENT"] = "event"; GtagCommand["SET"] = "set"; GtagCommand["CONFIG"] = "config"; })(GtagCommand || (GtagCommand = {})); /** * Officially recommended event names for gtag.js * Any other string is also allowed. * * @public */ var EventName; (function (EventName) { EventName["ADD_SHIPPING_INFO"] = "add_shipping_info"; EventName["ADD_PAYMENT_INFO"] = "add_payment_info"; EventName["ADD_TO_CART"] = "add_to_cart"; EventName["ADD_TO_WISHLIST"] = "add_to_wishlist"; EventName["BEGIN_CHECKOUT"] = "begin_checkout"; /** * @deprecated * This event name is deprecated and is unsupported in updated * Enhanced Ecommerce reports. */ EventName["CHECKOUT_PROGRESS"] = "checkout_progress"; EventName["EXCEPTION"] = "exception"; EventName["GENERATE_LEAD"] = "generate_lead"; EventName["LOGIN"] = "login"; EventName["PAGE_VIEW"] = "page_view"; EventName["PURCHASE"] = "purchase"; EventName["REFUND"] = "refund"; EventName["REMOVE_FROM_CART"] = "remove_from_cart"; EventName["SCREEN_VIEW"] = "screen_view"; EventName["SEARCH"] = "search"; EventName["SELECT_CONTENT"] = "select_content"; EventName["SELECT_ITEM"] = "select_item"; EventName["SELECT_PROMOTION"] = "select_promotion"; /** @deprecated */ EventName["SET_CHECKOUT_OPTION"] = "set_checkout_option"; EventName["SHARE"] = "share"; EventName["SIGN_UP"] = "sign_up"; EventName["TIMING_COMPLETE"] = "timing_complete"; EventName["VIEW_CART"] = "view_cart"; EventName["VIEW_ITEM"] = "view_item"; EventName["VIEW_ITEM_LIST"] = "view_item_list"; EventName["VIEW_PROMOTION"] = "view_promotion"; EventName["VIEW_SEARCH_RESULTS"] = "view_search_results"; })(EventName || (EventName = {})); /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Logs an analytics event through the Firebase SDK. * * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event * @param eventName Google Analytics event name, choose from standard list or use a custom string. * @param eventParams Analytics event parameters. */ async function logEvent(gtagFunction, initializationPromise, eventName, eventParams, options) { if (options && options.global) { gtagFunction(GtagCommand.EVENT, eventName, eventParams); return; } else { const measurementId = await initializationPromise; const params = Object.assign(Object.assign({}, eventParams), { 'send_to': measurementId }); gtagFunction(GtagCommand.EVENT, eventName, params); } } /** * Set screen_name parameter for this Google Analytics ID. * * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event * @param screenName Screen name string to set. */ async function setCurrentScreen(gtagFunction, initializationPromise, screenName, options) { if (options && options.global) { gtagFunction(GtagCommand.SET, { 'screen_name': screenName }); return Promise.resolve(); } else { const measurementId = await initializationPromise; gtagFunction(GtagCommand.CONFIG, measurementId, { update: true, 'screen_name': screenName }); } } /** * Set user_id parameter for this Google Analytics ID. * * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event * @param id User ID string to set */ async function setUserId(gtagFunction, initializationPromise, id, options) { if (options && options.global) { gtagFunction(GtagCommand.SET, { 'user_id': id }); return Promise.resolve(); } else { const measurementId = await initializationPromise; gtagFunction(GtagCommand.CONFIG, measurementId, { update: true, 'user_id': id }); } } /** * Set all other user properties other than user_id and screen_name. * * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event * @param properties Map of user properties to set */ async function setUserProperties(gtagFunction, initializationPromise, properties, options) { if (options && options.global) { const flatProperties = {}; for (const key of Object.keys(properties)) { // use dot notation for merge behavior in gtag.js flatProperties[`user_properties.${key}`] = properties[key]; } gtagFunction(GtagCommand.SET, flatProperties); return Promise.resolve(); } else { const measurementId = await initializationPromise; gtagFunction(GtagCommand.CONFIG, measurementId, { update: true, 'user_properties': properties }); } } /** * Set whether collection is enabled for this ID. * * @param enabled If true, collection is enabled for this ID. */ async function setAnalyticsCollectionEnabled(initializationPromise, enabled) { const measurementId = await initializationPromise; window[`ga-disable-${measurementId}`] = !enabled; } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const logger = new Logger('@firebase/analytics'); /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Inserts gtag script tag into the page to asynchronously download gtag. * @param dataLayerName Name of datalayer (most often the default, "_dataLayer"). */ function insertScriptTag(dataLayerName, measurementId) { const script = document.createElement('script'); script.src = `${GTAG_URL}?l=${dataLayerName}&id=${measurementId}`; script.async = true; document.head.appendChild(script); } /** * Get reference to, or create, global datalayer. * @param dataLayerName Name of datalayer (most often the default, "_dataLayer"). */ function getOrCreateDataLayer(dataLayerName) { // Check for existing dataLayer and create if needed. let dataLayer = []; if (Array.isArray(window[dataLayerName])) { dataLayer = window[dataLayerName]; } else { window[dataLayerName] = dataLayer; } return dataLayer; } /** * Wrapped gtag logic when gtag is called with 'config' command. * * @param gtagCore Basic gtag function that just appends to dataLayer. * @param initializationPromisesMap Map of appIds to their initialization promises. * @param dynamicConfigPromisesList Array of dynamic config fetch promises. * @param measurementIdToAppId Map of GA measurementIDs to corresponding Firebase appId. * @param measurementId GA Measurement ID to set config for. * @param gtagParams Gtag config params to set. */ async function gtagOnConfig(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, measurementId, gtagParams) { // If config is already fetched, we know the appId and can use it to look up what FID promise we /// are waiting for, and wait only on that one. const correspondingAppId = measurementIdToAppId[measurementId]; try { if (correspondingAppId) { await initializationPromisesMap[correspondingAppId]; } else { // If config is not fetched yet, wait for all configs (we don't know which one we need) and // find the appId (if any) corresponding to this measurementId. If there is one, wait on // that appId's initialization promise. If there is none, promise resolves and gtag // call goes through. const dynamicConfigResults = await Promise.all(dynamicConfigPromisesList); const foundConfig = dynamicConfigResults.find(config => config.measurementId === measurementId); if (foundConfig) { await initializationPromisesMap[foundConfig.appId]; } } } catch (e) { logger.error(e); } gtagCore(GtagCommand.CONFIG, measurementId, gtagParams); } /** * Wrapped gtag logic when gtag is called with 'event' command. * * @param gtagCore Basic gtag function that just appends to dataLayer. * @param initializationPromisesMap Map of appIds to their initialization promises. * @param dynamicConfigPromisesList Array of dynamic config fetch promises. * @param measurementId GA Measurement ID to log event to. * @param gtagParams Params to log with this event. */ async function gtagOnEvent(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementId, gtagParams) { try { let initializationPromisesToWaitFor = []; // If there's a 'send_to' param, check if any ID specified matches // an initializeIds() promise we are waiting for. if (gtagParams && gtagParams['send_to']) { let gaSendToList = gtagParams['send_to']; // Make it an array if is isn't, so it can be dealt with the same way. if (!Array.isArray(gaSendToList)) { gaSendToList = [gaSendToList]; } // Checking 'send_to' fields requires having all measurement ID results back from // the dynamic config fetch. const dynamicConfigResults = await Promise.all(dynamicConfigPromisesList); for (const sendToId of gaSendToList) { // Any fetched dynamic measurement ID that matches this 'send_to' ID const foundConfig = dynamicConfigResults.find(config => config.measurementId === sendToId); const initializationPromise = foundConfig && initializationPromisesMap[foundConfig.appId]; if (initializationPromise) { initializationPromisesToWaitFor.push(initializationPromise); } else { // Found an item in 'send_to' that is not associated // directly with an FID, possibly a group. Empty this array, // exit the loop early, and let it get populated below. initializationPromisesToWaitFor = []; break; } } } // This will be unpopulated if there was no 'send_to' field , or // if not all entries in the 'send_to' field could be mapped to // a FID. In these cases, wait on all pending initialization promises. if (initializationPromisesToWaitFor.length === 0) { initializationPromisesToWaitFor = Object.values(initializationPromisesMap); } // Run core gtag function with args after all relevant initialization // promises have been resolved. await Promise.all(initializationPromisesToWaitFor); // Workaround for http://b/141370449 - third argument cannot be undefined. gtagCore(GtagCommand.EVENT, measurementId, gtagParams || {}); } catch (e) { logger.error(e); } } /** * Wraps a standard gtag function with extra code to wait for completion of * relevant initialization promises before sending requests. * * @param gtagCore Basic gtag function that just appends to dataLayer. * @param initializationPromisesMap Map of appIds to their initialization promises. * @param dynamicConfigPromisesList Array of dynamic config fetch promises. * @param measurementIdToAppId Map of GA measurementIDs to corresponding Firebase appId. */ function wrapGtag(gtagCore, /** * Allows wrapped gtag calls to wait on whichever intialization promises are required, * depending on the contents of the gtag params' `send_to` field, if any. */ initializationPromisesMap, /** * Wrapped gtag calls sometimes require all dynamic config fetches to have returned * before determining what initialization promises (which include FIDs) to wait for. */ dynamicConfigPromisesList, /** * Wrapped gtag config calls can narrow down which initialization promise (with FID) * to wait for if the measurementId is already fetched, by getting the corresponding appId, * which is the key for the initialization promises map. */ measurementIdToAppId) { /** * Wrapper around gtag that ensures FID is sent with gtag calls. * @param command Gtag command type. * @param idOrNameOrParams Measurement ID if command is EVENT/CONFIG, params if command is SET. * @param gtagParams Params if event is EVENT/CONFIG. */ async function gtagWrapper(command, idOrNameOrParams, gtagParams) { try { // If event, check that relevant initialization promises have completed. if (command === GtagCommand.EVENT) { // If EVENT, second arg must be measurementId. await gtagOnEvent(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, idOrNameOrParams, gtagParams); } else if (command === GtagCommand.CONFIG) { // If CONFIG, second arg must be measurementId. await gtagOnConfig(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, idOrNameOrParams, gtagParams); } else { // If SET, second arg must be params. gtagCore(GtagCommand.SET, idOrNameOrParams); } } catch (e) { logger.error(e); } } return gtagWrapper; } /** * Creates global gtag function or wraps existing one if found. * This wrapped function attaches Firebase instance ID (FID) to gtag 'config' and * 'event' calls that belong to the GAID associated with this Firebase instance. * * @param initializationPromisesMap Map of appIds to their initialization promises. * @param dynamicConfigPromisesList Array of dynamic config fetch promises. * @param measurementIdToAppId Map of GA measurementIDs to corresponding Firebase appId. * @param dataLayerName Name of global GA datalayer array. * @param gtagFunctionName Name of global gtag function ("gtag" if not user-specified). */ function wrapOrCreateGtag(initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, dataLayerName, gtagFunctionName) { // Create a basic core gtag function let gtagCore = function (..._args) { // Must push IArguments object, not an array. window[dataLayerName].push(arguments); }; // Replace it with existing one if found if (window[gtagFunctionName] && typeof window[gtagFunctionName] === 'function') { // @ts-ignore gtagCore = window[gtagFunctionName]; } window[gtagFunctionName] = wrapGtag(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId); return { gtagCore, wrappedGtag: window[gtagFunctionName] }; } /** * Returns first script tag in DOM matching our gtag url pattern. */ function findGtagScriptOnPage() { const scriptTags = window.document.getElementsByTagName('script'); for (const tag of Object.values(scriptTags)) { if (tag.src && tag.src.includes(GTAG_URL)) { return tag; } } return null; } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const ERRORS = { ["already-exists" /* ALREADY_EXISTS */]: 'A Firebase Analytics instance with the appId {$id} ' + ' already exists. ' + 'Only one Firebase Analytics instance can be created for each appId.', ["already-initialized" /* ALREADY_INITIALIZED */]: 'Firebase Analytics has already been initialized.' + 'settings() must be called before initializing any Analytics instance' + 'or it will have no effect.', ["interop-component-reg-failed" /* INTEROP_COMPONENT_REG_FAILED */]: 'Firebase Analytics Interop Component failed to instantiate: {$reason}', ["invalid-analytics-context" /* INVALID_ANALYTICS_CONTEXT */]: 'Firebase Analytics is not supported in this environment. ' + 'Wrap initialization of analytics in analytics.isSupported() ' + 'to prevent initialization in unsupported environments. Details: {$errorInfo}', ["indexeddb-unavailable" /* INDEXEDDB_UNAVAILABLE */]: 'IndexedDB unavailable or restricted in this environment. ' + 'Wrap initialization of analytics in analytics.isSupported() ' + 'to prevent initialization in unsupported environments. Details: {$errorInfo}', ["fetch-throttle" /* FETCH_THROTTLE */]: 'The config fetch request timed out while in an exponential backoff state.' + ' Unix timestamp in milliseconds when fetch request throttling ends: {$throttleEndTimeMillis}.', ["config-fetch-failed" /* CONFIG_FETCH_FAILED */]: 'Dynamic config fetch failed: [{$httpStatus}] {$responseMessage}', ["no-api-key" /* NO_API_KEY */]: 'The "apiKey" field is empty in the local Firebase config. Firebase Analytics requires this field to' + 'contain a valid API key.', ["no-app-id" /* NO_APP_ID */]: 'The "appId" field is empty in the local Firebase config. Firebase Analytics requires this field to' + 'contain a valid app ID.' }; const ERROR_FACTORY = new ErrorFactory('analytics', 'Analytics', ERRORS); /** * @license * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Backoff factor for 503 errors, which we want to be conservative about * to avoid overloading servers. Each retry interval will be * BASE_INTERVAL_MILLIS * LONG_RETRY_FACTOR ^ retryCount, so the second one * will be ~30 seconds (with fuzzing). */ const LONG_RETRY_FACTOR = 30; /** * Base wait interval to multiplied by backoffFactor^backoffCount. */ const BASE_INTERVAL_MILLIS = 1000; /** * Stubbable retry data storage class. */ class RetryData { constructor(throttleMetadata = {}, intervalMillis = BASE_INTERVAL_MILLIS) { this.throttleMetadata = throttleMetadata; this.intervalMillis = intervalMillis; } getThrottleMetadata(appId) { return this.throttleMetadata[appId]; } setThrottleMetadata(appId, metadata) { this.throttleMetadata[appId] = metadata; } deleteThrottleMetadata(appId) { delete this.throttleMetadata[appId]; } } const defaultRetryData = new RetryData(); /** * Set GET request headers. * @param apiKey App API key. */ function getHeaders(apiKey) { return new Headers({ Accept: 'application/json', 'x-goog-api-key': apiKey }); } /** * Fetches dynamic config from backend. * @param app Firebase app to fetch config for. */ async function fetchDynamicConfig(appFields) { var _a; const { appId, apiKey } = appFields; const request = { method: 'GET', headers: getHeaders(apiKey) }; const appUrl = DYNAMIC_CONFIG_URL.replace('{app-id}', appId); const response = await fetch(appUrl, request); if (response.status !== 200 && response.status !== 304) { let errorMessage = ''; try { // Try to get any error message text from server response. const jsonResponse = (await response.json()); if ((_a = jsonResponse.error) === null || _a === void 0 ? void 0 : _a.message) { errorMessage = jsonResponse.error.message; } } catch (_ignored) { } throw ERROR_FACTORY.create("config-fetch-failed" /* CONFIG_FETCH_FAILED */, { httpStatus: response.status, responseMessage: errorMessage }); } return response.json(); } /** * Fetches dynamic config from backend, retrying if failed. * @param app Firebase app to fetch config for. */ async function fetchDynamicConfigWithRetry(app, // retryData and timeoutMillis are parameterized to allow passing a different value for testing. retryData = defaultRetryData, timeoutMillis) { const { appId, apiKey, measurementId } = app.options; if (!appId) { throw ERROR_FACTORY.create("no-app-id" /* NO_APP_ID */); } if (!apiKey) { if (measurementId) { return { measurementId, appId }; } throw ERROR_FACTORY.create("no-api-key" /* NO_API_KEY */); } const throttleMetadata = retryData.getThrottleMetadata(appId) || { backoffCount: 0, throttleEndTimeMillis: Date.now() }; const signal = new AnalyticsAbortSignal(); setTimeout(async () => { // Note a very low delay, eg < 10ms, can elapse before listeners are initialized. signal.abort(); }, timeoutMillis !== undefined ? timeoutMillis : FETCH_TIMEOUT_MILLIS); return attemptFetchDynamicConfigWithRetry({ appId, apiKey, measurementId }, throttleMetadata, signal, retryData); } /** * Runs one retry attempt. * @param appFields Necessary app config fields. * @param throttleMetadata Ongoing metadata to determine throttling times. * @param signal Abort signal. */ async function attemptFetchDynamicConfigWithRetry(appFields, { throttleEndTimeMillis, backoffCount }, signal, retryData = defaultRetryData // for testing ) { const { appId, measurementId } = appFields; // Starts with a (potentially zero) timeout to support resumption from stored state. // Ensures the throttle end time is honored if the last attempt timed out. // Note the SDK will never make a request if the fetch timeout expires at this point. try { await setAbortableTimeout(signal, throttleEndTimeMillis); } catch (e) { if (measurementId) { logger.warn(`Timed out fetching this Firebase app's measurement ID from the server.` + ` Falling back to the measurement ID ${measurementId}` + ` provided in the "measurementId" field in the local Firebase config. [${e.message}]`); return { appId, measurementId }; } throw e; } try { const response = await fetchDynamicConfig(appFields); // Note the SDK only clears throttle state if response is success or non-retriable. retryData.deleteThrottleMetadata(appId); return response; } catch (e) { if (!isRetriableError(e)) { retryData.deleteThrottleMetadata(appId); if (measurementId) { logger.warn(`Failed to fetch this Firebase app's measurement ID from the server.` + ` Falling back to the measurement ID ${measurementId}` + ` provided in the "measurementId" field in the local Firebase config. [${e.message}]`); return { appId, measurementId }; } else { throw e; } } const backoffMillis = Number(e.customData.httpStatus) === 503 ? calculateBackoffMillis(backoffCount, retryData.intervalMillis, LONG_RETRY_FACTOR) : calculateBackoffMillis(backoffCount, retryData.intervalMillis); // Increments backoff state. const throttleMetadata = { throttleEndTimeMillis: Date.now() + backoffMillis, backoffCount: backoffCount + 1 }; // Persists state. retryData.setThrottleMetadata(appId, throttleMetadata); logger.debug(`Calling attemptFetch again in ${backoffMillis} millis`); return attemptFetchDynamicConfigWithRetry(appFields, throttleMetadata, signal, retryData); } } /** * Supports waiting on a backoff by: * * <ul> * <li>Promisifying setTimeout, so we can set a timeout in our Promise chain</li> * <li>Listening on a signal bus for abort events, just like the Fetch API</li> * <li>Failing in the same way the Fetch API fails, so timing out a live request and a throttled * request appear the same.</li> * </ul> * * <p>Visible for testing. */ function setAbortableTimeout(signal, throttleEndTimeMillis) { return new Promise((resolve, reject) => { // Derives backoff from given end time, normalizing negative numbers to zero. const backoffMillis = Math.max(throttleEndTimeMillis - Date.now(), 0); const timeout = setTimeout(resolve, backoffMillis); // Adds listener, rather than sets onabort, because signal is a shared object. signal.addEventListener(() => { clearTimeout(timeout); // If the request completes before this timeout, the rejection has no effect. reject(ERROR_FACTORY.create("fetch-throttle" /* FETCH_THROTTLE */, { throttleEndTimeMillis })); }); }); } /** * Returns true if the {@link Error} indicates a fetch request may succeed later. */ function isRetriableError(e) { if (!(e instanceof FirebaseError) || !e.customData) { return false; } // Uses string index defined by ErrorData, which FirebaseError implements. const httpStatus = Number(e.customData['httpStatus']); return (httpStatus === 429 || httpStatus === 500 || httpStatus === 503 || httpStatus === 504); } /** * Shims a minimal AbortSignal (copied from Remote Config). * * <p>AbortController's AbortSignal conveniently decouples fetch timeout logic from other aspects * of networking, such as retries. Firebase doesn't use AbortController enough to justify a * polyfill recommendation, like we do with the Fetch API, but this minimal shim can easily be * swapped out if/when we do. */ class AnalyticsAbortSignal { constructor() { this.listeners = []; } addEventListener(listener) { this.listeners.push(listener); } abort() { this.listeners.forEach(listener => listener()); } } /** * @license * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ async function validateIndexedDB() { if (!isIndexedDBAvailable()) { logger.warn(ERROR_FACTORY.create("indexeddb-unavailable" /* INDEXEDDB_UNAVAILABLE */, { errorInfo: 'IndexedDB is not available in this environment.' }).message); return false; } else { try { await validateIndexedDBOpenable(); } catch (e) { logger.warn(ERROR_FACTORY.create("indexeddb-unavailable" /* INDEXEDDB_UNAVAILABLE */, { errorInfo: e }).message); return false; } } return true; } /** * Initialize the analytics instance in gtag.js by calling config command with fid. * * NOTE: We combine analytics initialization and setting fid together because we want fid to be * part of the `page_view` event that's sent during the initialization * @param app Firebase app * @param gtagCore The gtag function that's not wrapped. * @param dynamicConfigPromisesList Array of all dynamic config promises. * @param measurementIdToAppId Maps measurementID to appID. * @param installations FirebaseInstallations instance. * * @returns Measurement ID. */ async function initializeIds(app, dynamicConfigPromisesList, measurementIdToAppId, installations, gtagCore, dataLayerName) { const dynamicConfigPromise = fetchDynamicConfigWithRetry(app); // Once fetched, map measurementIds to appId, for ease of lookup in wrapped gtag function. dynamicConfigPromise .then(config => { measurementIdToAppId[config.measurementId] = config.appId; if (app.options.measurementId && config.measurementId !== app.options.measurementId) { logger.warn(`The measurement ID in the local Firebase config (${app.options.measurementId})` + ` does not match the measurement ID fetched from the server (${config.measurementId}).` + ` To ensure analytics events are always sent to the correct Analytics property,` + ` update the` + ` measurement ID field in the local config or remove it from the local config.`); } }) .catch(e => logger.error(e)); // Add to list to track state of all dynamic config promises. dynamicConfigPromisesList.push(dynamicConfigPromise); const fidPromise = validateIndexedDB().then(envIsValid => { if (envIsValid) { return installations.getId(); } else { return undefined; } }); const [dynamicConfig, fid] = await Promise.all([ dynamicConfigPromise, fidPromise ]); // Detect if user has already put the gtag <script> tag on this page. if (!findGtagScriptOnPage()) { insertScriptTag(dataLayerName, dynamicConfig.measurementId); } // This command initializes gtag.js and only needs to be called once for the entire web app, // but since it is idempotent, we can call it multiple times. // We keep it together with other initialization logic for better code structure. // eslint-disable-next-line @typescript-eslint/no-explicit-any gtagCore('js', new Date()); const configProperties = { // guard against developers accidentally setting properties with prefix `firebase_` [ORIGIN_KEY]: 'firebase', update: true }; if (fid != null) { configProperties[GA_FID_KEY] = fid; } // It should be the first config command called on this GA-ID // Initialize this GA-ID and set FID on it using the gtag config API. // Note: This will trigger a page_view event unless 'send_page_view' is set to false in // `configProperties`. gtagCore(GtagCommand.CONFIG, dynamicConfig.measurementId, configProperties); return dynamicConfig.measurementId; } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Maps appId to full initialization promise. Wrapped gtag calls must wait on * all or some of these, depending on the call's `send_to` param and the status * of the dynamic config fetches (see below). */ let initializationPromisesMap = {}; /** * List of dynamic config fetch promises. In certain cases, wrapped gtag calls * wait on all these to be complete in order to determine if it can selectively * wait for only certain initialization (FID) promises or if it must wait for all. */ let dynamicConfigPromisesList = []; /** * Maps fetched measurementIds to appId. Populated when the app's dynamic config * fetch completes. If already populated, gtag config calls can use this to * selectively wait for only this app's initialization promise (FID) instead of all * initialization promises. */ const measurementIdToAppId = {}; /** * Name for window global data layer array used by GA: defaults to 'dataLayer'. */ let dataLayerName = 'dataLayer'; /** * Name for window global gtag function used by GA: defaults to 'gtag'. */ let gtagName = 'gtag'; /** * Reproduction of standard gtag function or reference to existing * gtag function on window object. */ let gtagCoreFunction; /** * Wrapper around gtag function that ensures FID is sent with all * relevant event and config calls. */ let wrappedGtagFunction; /** * Flag to ensure page initialization steps (creation or wrapping of * dataLayer and gtag script) are only run once per page load. */ let globalInitDone = false; /** * For testing */ function resetGlobalVars(newGlobalInitDone = false, newInitializationPromisesMap = {}, newDynamicPromises = []) { globalInitDone = newGlobalInitDone; initializationPromisesMap = newInitializationPromisesMap; dynamicConfigPromisesList = newDynamicPromises; dataLayerName = 'dataLayer'; gtagName = 'gtag'; } /** * For testing */ function getGlobalVars() { return { initializationPromisesMap, dynamicConfigPromisesList }; } /** * This must be run before calling firebase.analytics() or it won't * have any effect. * @param options Custom gtag and dataLayer names. */ function settings(options) { if (globalInitDone) { throw ERROR_FACTORY.create("already-initialized" /* ALREADY_INITIALIZED */); } if (options.dataLayerName) { dataLayerName = options.dataLayerName; } if (options.gtagName) { gtagName = options.gtagName; } } /** * Returns true if no environment mismatch is found. * If environment mismatches are found, throws an INVALID_ANALYTICS_CONTEXT * error that also lists details for each mismatch found. */ function warnOnBrowserContextMismatch() { const mismatchedEnvMessages = []; if (isBrowserExtension()) { mismatchedEnvMessages.push('This is a browser extension environment.'); } if (!areCookiesEnabled()) { mismatchedEnvMessages.push('Cookies are not available.'); } if (mismatchedEnvMessages.length > 0) { const details = mismatchedEnvMessages .map((message, index) => `(${index + 1}) ${message}`) .join(' '); const err = ERROR_FACTORY.create("invalid-analytics-context" /* INVALID_ANALYTICS_CONTEXT */, { errorInfo: details }); logger.warn(err.message); } } function factory(app, installations) { warnOnBrowserContextMismatch(); const appId = app.options.appId; if (!appId) { throw ERROR_FACTORY.create("no-app-id" /* NO_APP_ID */); } if (!app.options.apiKey) { if (app.options.measurementId) { logger.warn(`The "apiKey" field is empty in the local Firebase config. This is needed to fetch the latest` + ` measurement ID for this Firebase app. Falling back to the measurement ID ${app.options.measurementId}` + ` provided in the "measurementId" field in the local Firebase config.`); } else { throw ERROR_FACTORY.create("no-api-key" /* NO_API_KEY */); } } if (initializationPromisesMap[appId] != null) { throw ERROR_FACTORY.create("already-exists" /* ALREADY_EXISTS */, { id: appId }); } if (!globalInitDone) { // Steps here should only be done once per page: creation or wrapping // of dataLayer and global gtag function. getOrCreateDataLayer(dataLayerName); const { wrappedGtag, gtagCore } = wrapOrCreateGtag(initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, dataLayerName, gtagName); wrappedGtagFunction = wrappedGtag; gtagCoreFunction = gtagCore; globalInitDone = true; } // Async but non-blocking. // This map reflects the completion state of all promises for each appId. initializationPromisesMap[appId] = initializeIds(app, dynamicConfigPromisesList, measurementIdToAppId, installations, gtagCoreFunction, dataLayerName); const analyticsInstance = { app, // Public methods return void for API simplicity and to better match gtag, // while internal implementations return promises. logEvent: (eventName, eventParams, options) => { logEvent(wrappedGtagFunction, initializationPromisesMap[appId], eventName, eventParams, options).catch(e => logger.error(e)); }, setCurrentScreen: (screenName, options) => { setCurrentScreen(wrappedGtagFunction, initializationPromisesMap[appId], screenName, options).catch(e => logger.error(e)); }, setUserId: (id, options) => { setUserId(wrappedGtagFunction, initializationPromisesMap[appId], id, options).catch(e => logger.error(e)); }, setUserProperties: (properties, options) => { setUserProperties(wrappedGtagFunction, initializationPromisesMap[appId], properties, options).catch(e => logger.error(e)); }, setAnalyticsCollectionEnabled: enabled => { setAnalyticsCollectionEnabled(initializationPromisesMap[appId], enabled).catch(e => logger.error(e)); }, INTERNAL: { delete: () => { delete initializationPromisesMap[appId]; return Promise.resolve(); } } }; return analyticsInstance; } const name = "@firebase/analytics"; const version = "0.6.17"; /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Type constant for Firebase Analytics. */ const ANALYTICS_TYPE = 'analytics'; function registerAnalytics(instance) { instance.INTERNAL.registerComponent(new Component(ANALYTICS_TYPE, container => { // getImmediate for FirebaseApp will always succeed const app = container.getProvider('app').getImmediate(); const installations = container .getProvider('installations') .getImmediate(); return factory(app, installations); }, "PUBLIC" /* PUBLIC */).setServiceProps({ settings, EventName, isSupported })); instance.INTERNAL.registerComponent(new Component('analytics-internal', internalFactory, "PRIVATE" /* PRIVATE */)); instance.registerVersion(name, version); function internalFactory(container) { try { const analytics = container.getProvider(ANALYTICS_TYPE).getImmediate(); return { logEvent: analytics.logEvent }; } catch (e) { throw ERROR_FACTORY.create("interop-component-reg-failed" /* INTEROP_COMPONENT_REG_FAILED */, { reason: e }); } } } registerAnalytics(firebase); /** * this is a public static method provided to users that wraps four different checks: * * 1. check if it's not a browser extension environment. * 1. check if cookie is enabled in current browser. * 3. check if IndexedDB is supported by the browser environment. * 4. check if the current browser context is valid for using IndexedDB. * */ async function isSupported() { if (isBrowserExtension()) { return false; } if (!areCookiesEnabled()) { return false; } if (!isIndexedDBAvailable()) { return false; } try { const isDBOpenable = await validateIndexedDBOpenable(); return isDBOpenable; } catch (error) { return false; } } export { factory, getGlobalVars, registerAnalytics, resetGlobalVars, settings }; //# sourceMappingURL=index.esm2017.js.map