UNPKG

voluptasmollitia

Version:
310 lines (295 loc) 11.3 kB
/** * @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. */ import { DynamicConfig, DataLayer, Gtag, CustomParams, ControlParams, EventParams, MinimalDynamicConfig } from '@firebase/analytics-types'; import { GtagCommand, GTAG_URL } from './constants'; import { logger } from './logger'; /** * Inserts gtag script tag into the page to asynchronously download gtag. * @param dataLayerName Name of datalayer (most often the default, "_dataLayer"). */ export function insertScriptTag( dataLayerName: string, measurementId: string ): void { 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"). */ export function getOrCreateDataLayer(dataLayerName: string): DataLayer { // Check for existing dataLayer and create if needed. let dataLayer: DataLayer = []; if (Array.isArray(window[dataLayerName])) { dataLayer = window[dataLayerName] as DataLayer; } 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: Gtag, initializationPromisesMap: { [appId: string]: Promise<string> }, dynamicConfigPromisesList: Array< Promise<DynamicConfig | MinimalDynamicConfig> >, measurementIdToAppId: { [measurementId: string]: string }, measurementId: string, gtagParams?: ControlParams & EventParams & CustomParams ): Promise<void> { // 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 as string]; 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: Gtag, initializationPromisesMap: { [appId: string]: Promise<string> }, dynamicConfigPromisesList: Array< Promise<DynamicConfig | MinimalDynamicConfig> >, measurementId: string, gtagParams?: ControlParams & EventParams & CustomParams ): Promise<void> { try { let initializationPromisesToWaitFor: Array<Promise<string>> = []; // 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: string | string[] = 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: Gtag, /** * 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: { [appId: string]: Promise<string> }, /** * Wrapped gtag calls sometimes require all dynamic config fetches to have returned * before determining what initialization promises (which include FIDs) to wait for. */ dynamicConfigPromisesList: Array< Promise<DynamicConfig | MinimalDynamicConfig> >, /** * 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: { [measurementId: string]: string } ): Gtag { /** * 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: 'config' | 'set' | 'event', idOrNameOrParams: string | ControlParams, gtagParams?: ControlParams & EventParams & CustomParams ): Promise<void> { 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 as string, gtagParams ); } else if (command === GtagCommand.CONFIG) { // If CONFIG, second arg must be measurementId. await gtagOnConfig( gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, idOrNameOrParams as string, gtagParams ); } else { // If SET, second arg must be params. gtagCore(GtagCommand.SET, idOrNameOrParams as CustomParams); } } 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). */ export function wrapOrCreateGtag( initializationPromisesMap: { [appId: string]: Promise<string> }, dynamicConfigPromisesList: Array< Promise<DynamicConfig | MinimalDynamicConfig> >, measurementIdToAppId: { [measurementId: string]: string }, dataLayerName: string, gtagFunctionName: string ): { gtagCore: Gtag; wrappedGtag: Gtag; } { // Create a basic core gtag function let gtagCore: Gtag = function (..._args: unknown[]) { // Must push IArguments object, not an array. (window[dataLayerName] as DataLayer).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] as Gtag }; } /** * Returns first script tag in DOM matching our gtag url pattern. */ export function findGtagScriptOnPage(): HTMLScriptElement | null { 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; }