UNPKG

@firebase/analytics

Version:

A analytics package for new firebase packages

1,173 lines (1,164 loc) • 54.2 kB
import { _getProvider, getApp, _registerComponent, registerVersion } from '@firebase/app'; import { Logger } from '@firebase/logger'; import { ErrorFactory, calculateBackoffMillis, FirebaseError, isIndexedDBAvailable, validateIndexedDBOpenable, isBrowserExtension, areCookiesEnabled, getModularInstance, deepEqual } from '@firebase/util'; import { Component } from '@firebase/component'; import '@firebase/installations'; /** * @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'; // 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'; /** * @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. */ const ERRORS = { ["already-exists" /* AnalyticsError.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" /* AnalyticsError.ALREADY_INITIALIZED */]: 'initializeAnalytics() cannot be called again with different options than those ' + 'it was initially called with. It can be called again with the same options to ' + 'return the existing instance, or getAnalytics() can be used ' + 'to get a reference to the already-intialized instance.', ["already-initialized-settings" /* AnalyticsError.ALREADY_INITIALIZED_SETTINGS */]: '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" /* AnalyticsError.INTEROP_COMPONENT_REG_FAILED */]: 'Firebase Analytics Interop Component failed to instantiate: {$reason}', ["invalid-analytics-context" /* AnalyticsError.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" /* AnalyticsError.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" /* AnalyticsError.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" /* AnalyticsError.CONFIG_FETCH_FAILED */]: 'Dynamic config fetch failed: [{$httpStatus}] {$responseMessage}', ["no-api-key" /* AnalyticsError.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" /* AnalyticsError.NO_APP_ID */]: 'The "appId" field is empty in the local Firebase config. Firebase Analytics requires this field to' + 'contain a valid app ID.', ["no-client-id" /* AnalyticsError.NO_CLIENT_ID */]: 'The "client_id" field is empty.', ["invalid-gtag-resource" /* AnalyticsError.INVALID_GTAG_RESOURCE */]: 'Trusted Types detected an invalid gtag resource: {$gtagURL}.' }; const ERROR_FACTORY = new ErrorFactory('analytics', 'Analytics', ERRORS); /** * @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. */ /** * Verifies and creates a TrustedScriptURL. */ function createGtagTrustedTypesScriptURL(url) { if (!url.startsWith(GTAG_URL)) { const err = ERROR_FACTORY.create("invalid-gtag-resource" /* AnalyticsError.INVALID_GTAG_RESOURCE */, { gtagURL: url }); logger.warn(err.message); return ''; } return url; } /** * Makeshift polyfill for Promise.allSettled(). Resolves when all promises * have either resolved or rejected. * * @param promises Array of promises to wait for. */ function promiseAllSettled(promises) { return Promise.all(promises.map(promise => promise.catch(e => e))); } /** * Creates a TrustedTypePolicy object that implements the rules passed as policyOptions. * * @param policyName A string containing the name of the policy * @param policyOptions Object containing implementations of instance methods for TrustedTypesPolicy, see {@link https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy#instance_methods * | the TrustedTypePolicy reference documentation}. */ function createTrustedTypesPolicy(policyName, policyOptions) { // Create a TrustedTypes policy that we can use for updating src // properties let trustedTypesPolicy; if (window.trustedTypes) { trustedTypesPolicy = window.trustedTypes.createPolicy(policyName, policyOptions); } return trustedTypesPolicy; } /** * 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 trustedTypesPolicy = createTrustedTypesPolicy('firebase-js-sdk-policy', { createScriptURL: createGtagTrustedTypesScriptURL }); const script = document.createElement('script'); // We are not providing an analyticsId in the URL because it would trigger a `page_view` // without fid. We will initialize ga-id using gtag (config) command together with fid. const gtagScriptURL = `${GTAG_URL}?l=${dataLayerName}&id=${measurementId}`; script.src = trustedTypesPolicy ? trustedTypesPolicy === null || trustedTypesPolicy === void 0 ? void 0 : trustedTypesPolicy.createScriptURL(gtagScriptURL) : gtagScriptURL; 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 promiseAllSettled(dynamicConfigPromisesList); const foundConfig = dynamicConfigResults.find(config => config.measurementId === measurementId); if (foundConfig) { await initializationPromisesMap[foundConfig.appId]; } } } catch (e) { logger.error(e); } gtagCore("config" /* 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 promiseAllSettled(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("event" /* 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, ...args) { try { // If event, check that relevant initialization promises have completed. if (command === "event" /* GtagCommand.EVENT */) { const [measurementId, gtagParams] = args; // If EVENT, second arg must be measurementId. await gtagOnEvent(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementId, gtagParams); } else if (command === "config" /* GtagCommand.CONFIG */) { const [measurementId, gtagParams] = args; // If CONFIG, second arg must be measurementId. await gtagOnConfig(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, measurementId, gtagParams); } else if (command === "consent" /* GtagCommand.CONSENT */) { const [gtagParams] = args; gtagCore("consent" /* GtagCommand.CONSENT */, 'update', gtagParams); } else if (command === "get" /* GtagCommand.GET */) { const [measurementId, fieldName, callback] = args; gtagCore("get" /* GtagCommand.GET */, measurementId, fieldName, callback); } else if (command === "set" /* GtagCommand.SET */) { const [customParams] = args; // If SET, second arg must be params. gtagCore("set" /* GtagCommand.SET */, customParams); } else { gtagCore(command, ...args); } } 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 the script tag in the DOM matching both the gtag url pattern * and the provided data layer name. */ function findGtagScriptOnPage(dataLayerName) { const scriptTags = window.document.getElementsByTagName('script'); for (const tag of Object.values(scriptTags)) { if (tag.src && tag.src.includes(GTAG_URL) && tag.src.includes(dataLayerName)) { return tag; } } return null; } /** * @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" /* AnalyticsError.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" /* AnalyticsError.NO_APP_ID */); } if (!apiKey) { if (measurementId) { return { measurementId, appId }; } throw ERROR_FACTORY.create("no-api-key" /* AnalyticsError.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 ) { var _a; 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 === null || e === void 0 ? void 0 : 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) { const error = e; if (!isRetriableError(error)) { 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. [${error === null || error === void 0 ? void 0 : error.message}]`); return { appId, measurementId }; } else { throw e; } } const backoffMillis = Number((_a = error === null || error === void 0 ? void 0 : error.customData) === null || _a === void 0 ? void 0 : _a.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" /* AnalyticsError.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 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. */ /** * Event parameters to set on 'gtag' during initialization. */ let defaultEventParametersForInit; /** * 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$1(gtagFunction, initializationPromise, eventName, eventParams, options) { if (options && options.global) { gtagFunction("event" /* GtagCommand.EVENT */, eventName, eventParams); return; } else { const measurementId = await initializationPromise; const params = Object.assign(Object.assign({}, eventParams), { 'send_to': measurementId }); gtagFunction("event" /* GtagCommand.EVENT */, eventName, params); } } /** * Set screen_name parameter for this Google Analytics ID. * * @deprecated Use {@link logEvent} with `eventName` as 'screen_view' and add relevant `eventParams`. * See {@link https://firebase.google.com/docs/analytics/screenviews | Track Screenviews}. * * @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$1(gtagFunction, initializationPromise, screenName, options) { if (options && options.global) { gtagFunction("set" /* GtagCommand.SET */, { 'screen_name': screenName }); return Promise.resolve(); } else { const measurementId = await initializationPromise; gtagFunction("config" /* 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$1(gtagFunction, initializationPromise, id, options) { if (options && options.global) { gtagFunction("set" /* GtagCommand.SET */, { 'user_id': id }); return Promise.resolve(); } else { const measurementId = await initializationPromise; gtagFunction("config" /* 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$1(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("set" /* GtagCommand.SET */, flatProperties); return Promise.resolve(); } else { const measurementId = await initializationPromise; gtagFunction("config" /* GtagCommand.CONFIG */, measurementId, { update: true, 'user_properties': properties }); } } /** * Retrieves a unique Google Analytics identifier for the web client. * See {@link https://developers.google.com/analytics/devguides/collection/ga4/reference/config#client_id | client_id}. * * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event */ async function internalGetGoogleAnalyticsClientId(gtagFunction, initializationPromise) { const measurementId = await initializationPromise; return new Promise((resolve, reject) => { gtagFunction("get" /* GtagCommand.GET */, measurementId, 'client_id', (clientId) => { if (!clientId) { reject(ERROR_FACTORY.create("no-client-id" /* AnalyticsError.NO_CLIENT_ID */)); } resolve(clientId); }); }); } /** * Set whether collection is enabled for this ID. * * @param enabled If true, collection is enabled for this ID. */ async function setAnalyticsCollectionEnabled$1(initializationPromise, enabled) { const measurementId = await initializationPromise; window[`ga-disable-${measurementId}`] = !enabled; } /** * Consent parameters to default to during 'gtag' initialization. */ let defaultConsentSettingsForInit; /** * Sets the variable {@link defaultConsentSettingsForInit} for use in the initialization of * analytics. * * @param consentSettings Maps the applicable end user consent state for gtag.js. */ function _setConsentDefaultForInit(consentSettings) { defaultConsentSettingsForInit = consentSettings; } /** * Sets the variable `defaultEventParametersForInit` for use in the initialization of * analytics. * * @param customParams Any custom params the user may pass to gtag.js. */ function _setDefaultEventParametersForInit(customParams) { defaultEventParametersForInit = customParams; } /** * @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" /* AnalyticsError.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" /* AnalyticsError.INDEXEDDB_UNAVAILABLE */, { errorInfo: e === null || e === void 0 ? void 0 : e.toString() }).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 _FirebaseInstallationsInternal instance. * * @returns Measurement ID. */ async function _initializeAnalytics(app, dynamicConfigPromisesList, measurementIdToAppId, installations, gtagCore, dataLayerName, options) { var _a; 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 with the passed in // data layer name. if (!findGtagScriptOnPage(dataLayerName)) { insertScriptTag(dataLayerName, dynamicConfig.measurementId); } // Detects if there are consent settings that need to be configured. if (defaultConsentSettingsForInit) { gtagCore("consent" /* GtagCommand.CONSENT */, 'default', defaultConsentSettingsForInit); _setConsentDefaultForInit(undefined); } // 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()); // User config added first. We don't want users to accidentally overwrite // base Firebase config properties. const configProperties = (_a = options === null || options === void 0 ? void 0 : options.config) !== null && _a !== void 0 ? _a : {}; // guard against developers accidentally setting properties with prefix `firebase_` configProperties[ORIGIN_KEY] = 'firebase'; configProperties.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("config" /* GtagCommand.CONFIG */, dynamicConfig.measurementId, configProperties); // Detects if there is data that will be set on every event logged from the SDK. if (defaultEventParametersForInit) { gtagCore("set" /* GtagCommand.SET */, defaultEventParametersForInit); _setDefaultEventParametersForInit(undefined); } 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. */ /** * Analytics Service class. */ class AnalyticsService { constructor(app) { this.app = app; } _delete() { delete initializationPromisesMap[this.app.options.appId]; return Promise.resolve(); } } /** * 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; /** * Configures Firebase Analytics to use custom `gtag` or `dataLayer` names. * Intended to be used if `gtag.js` script has been installed on * this page independently of Firebase Analytics, and is using non-default * names for either the `gtag` function or for `dataLayer`. * Must be called before calling `getAnalytics()` or it won't * have any effect. * * @public * * @param options - Custom gtag and dataLayer names. */ function settings(options) { if (globalInitDone) { throw ERROR_FACTORY.create("already-initialized" /* AnalyticsError.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" /* AnalyticsError.INVALID_ANALYTICS_CONTEXT */, { errorInfo: details }); logger.warn(err.message); } } /** * Analytics instance factory. * @internal */ function factory(app, installations, options) { warnOnBrowserContextMismatch(); const appId = app.options.appId; if (!appId) { throw ERROR_FACTORY.create("no-app-id" /* AnalyticsError.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" /* AnalyticsError.NO_API_KEY */); } } if (initializationPromisesMap[appId] != null) { throw ERROR_FACTORY.create("already-exists" /* AnalyticsError.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] = _initializeAnalytics(app, dynamicConfigPromisesList, measurementIdToAppId, installations, gtagCoreFunction, dataLayerName, options); const analyticsInstance = new AnalyticsService(app); return analyticsInstance; } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Returns an {@link Analytics} instance for the given app. * * @public * * @param app - The {@link @firebase/app#FirebaseApp} to use. */ function getAnalytics(app = getApp()) { app = getModularInstance(app); // Dependencies const analyticsProvider = _getProvider(app, ANALYTICS_TYPE); if (analyticsProvider.isInitialized()) { return analyticsProvider.getImmediate(); } return initializeAnalytics(app); } /** * Returns an {@link Analytics} instance for the given app. * * @public * * @param app - The {@link @firebase/app#FirebaseApp} to use. */ function initializeAnalytics(app, options = {}) { // Dependencies const analyticsProvider = _getProvider(app, ANALYTICS_TYPE); if (analyticsProvider.isInitialized()) { const existingInstance = analyticsProvider.getImmediate(); if (deepEqual(options, analyticsProvider.getOptions())) { return existingInstance; } else { throw ERROR_FACTORY.create("already-initialized" /* AnalyticsError.ALREADY_INITIALIZED */); } } const analyticsInstance = analyticsProvider.initialize({ options }); return analyticsInstance; } /** * This is a public static method provided to users that wraps four different checks: * * 1. Check if it's not a browser extension environment. * 2. Check if cookies are 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.open()`. * * @public * */ 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; } } /** * Use gtag `config` command to set `screen_name`. * * @public * * @deprecated Use {@link logEvent} with `eventName` as 'screen_view' and add relevant `eventParams`. * See {@link https://firebase.google.com/docs/analytics/screenviews | Track Screenviews}. * * @param analyticsInstance - The {@link Analytics} instance. * @param screenName - Screen name to set. */ function setCurrentScreen(analyticsInstance, screenName, options) { analyticsInstance = getModularInstance(analyticsInstance); setCurrentScreen$1(wrappedGtagFunction, initializationPromisesMap[analyticsInstance.app.options.appId], screenName, options).catch(e => logger.error(e)); } /** * Retrieves a unique Google Analytics identifier for the web client. * See {@link https://developers.google.com/analytics/devguides/collection/ga4/reference/config#client_id | client_id}. * * @public * * @param app - The {@link @firebase/app#FirebaseApp} to use. */ async function getGoogleAnalyticsClientId(analyticsInstance) { analyticsInstance = getModularInstance(analyticsInstance); return internalGetGoogleAnalyticsClientId(wrappedGtagFunction, initializationPromisesMap[analyticsInstance.app.options.appId]); } /** * Use gtag `config` command to set `user_id`. * * @public * * @param analyticsInstance - The {@link Analytics} instance. * @param id - User ID to set. */ function setUserId(analyticsInstance, id, options) { analyticsInstance = getModularInstance(analyticsInstance); setUserId$1(wrappedGtagFunction, initializationPromisesMap[analyticsInstance.app.options.appId], id, options).catch(e => logger.error(e)); } /** * Use gtag `config` command to set all params specified. * * @public */ function setUserProperties(analyticsInstance, properties, options) { analyticsInstance = getModularInstance(analyticsInstance); setUserProperties$1(wrappedGtagFunction, initializationPromisesMap[analyticsInstance.app.options.appId], properties, options).catch(e => logger.error(e)); } /** * Sets whether Google Analytics collection is enabled for this app on this device. * Sets global `window['ga-disable-analyticsId'] = true;` * * @public * * @param analyticsInstance - The {@l