/** * Finds all the applicable tags based on the true qualification criteria. Method does not return anything * because it uses pass by memory reference. The last 2 paramters (allTagInfo and applicableTagInfo) are * return variables * @param sdkTags * @param sdkProviders * @param validQcList * @param allTagInfo * @param applicableTagInfo */ import type { HydrateTagInfo, MapLike, ReplaceMode, TagInfoItem, } from '../common/app-types'; import type { ClientSdkParamItem, ClientSdkPrItem, ClientSdkTagItem, } from '../models/mp-client-sdk'; import { Logger } from '../common/logger'; import { DataStore } from '../common/data-store'; import { Reporter } from '../common/reporter'; import { Constants } from '../common/constants'; import { Utils } from '../common/utils'; export class TagProcessor { private static findApplicableTags( sdkTags: MapLike, sdkProviders: MapLike, validQcList: string[], allTagInfo: TagInfoItem[], applicableTagInfo: TagInfoItem[] ): void { const applicableTags: string[] = []; const tagKeys = Object.keys(sdkTags); const currentEpochSeconds = Math.round(new Date().getTime() / 1000); Logger.logDbg('Total Tag Length: ', tagKeys.length); for (const tagId of tagKeys) { const tag: ClientSdkTagItem | undefined = sdkTags[tagId]; if (tag) { let isQualified = true; for (const qc of tag.qc) { if (validQcList.indexOf(qc) === -1) { isQualified = false; allTagInfo.push({ id: tagId, nm: tag.nm, status: false, qc, pr: tag.p, prNm: sdkProviders[tag.p]?.nm || '', }); break; } } if (isQualified && applicableTags.indexOf(tagId) === -1) { if (this.checkTagDateValidity(currentEpochSeconds, tag.st, tag.ex)) { applicableTags.push(tagId); allTagInfo.push({ id: tagId, nm: tag.nm, status: true, qc: tag.qc, pr: tag.p, prNm: sdkProviders[tag.p]?.nm || '', }); applicableTagInfo.push({ id: tagId, nm: tag.nm, status: true, qc: tag.qc, pr: tag.p, prNm: sdkProviders[tag.p]?.nm || '', }); } else { allTagInfo.push({ id: tagId, nm: tag.nm, status: false, qc: tag.qc, pr: tag.p, prNm: sdkProviders[tag.p]?.nm || '', }); } } } } } /** * Core function which actually puts the tag on page based on the tag type (JS or Image) * @param eventName * @param evtId */ static async processTags(eventName: string, evtId: string): Promise { try { Logger.logDbg( 'Processing tags for event: ', eventName, ' with id: ', evtId ); const allTagInfo: Array = []; const applicableTagInfo: Array = []; const prStatus = DataStore.getPrivacyCompliance(); const dataElements = DataStore.getDataElements(); const transFunctions = DataStore.getTransFunctions(); const sdkTags = DataStore.getSdkTags(); const sdkProviders = DataStore.getSdkProviders(); const validQcList = DataStore.getValidQcList(); // Find applicable tags this.findApplicableTags( sdkTags, sdkProviders, validQcList, allTagInfo, applicableTagInfo ); // set expected tag count, so that the reporter knows when all tags are finished and can report asynchronously Reporter.setExpectedTagCount(applicableTagInfo.length, evtId); Logger.logDbg('Applicable Tags: ', applicableTagInfo); // process tags one by one for (const tagInfo of applicableTagInfo) { try { Logger.logDbg(`Processing Tag: ${tagInfo.nm} [${tagInfo.id}]`); const tag: ClientSdkTagItem | undefined = sdkTags[tagInfo.id]; if (!tag) { continue; } const provider: ClientSdkPrItem | undefined = sdkProviders[tag.p]; if (!provider) { continue; } const result = this.hydrateTag( tagInfo, tag, provider, prStatus, dataElements, transFunctions ); if (result.isInError) { Logger.logDbg( `Tag ${tagInfo.nm} not processed because of error code: ${result.errCd}` ); if (result.errCd === 'NP') { Reporter.reportItem( { st: Constants.ST_PR_EXC, t: tagInfo.id, p: tagInfo.pr, tNm: tagInfo.nm, req: result.errMsg, }, evtId ); } else if (result.errCd === 'PRB') { Reporter.reportItem( { st: Constants.ST_PR_BL, t: tagInfo.id, p: tagInfo.pr, tNm: tagInfo.nm, req: result.errMsg, }, evtId ); } else if (result.errCd === 'REF') { Reporter.reportItem( { st: Constants.ST_VAL_FAIL, t: tagInfo.id, p: tagInfo.pr, tNm: tagInfo.nm, req: result.errMsg, }, evtId ); } continue; } const finalUrl = result.content; Logger.logDbg('Final Url :: ', finalUrl); if (provider.typ === Constants.PR_TYP_APP) { if (provider.sTyp === Constants.PR_S_TYP_R) { // process and fire app event try { const parsedData = JSON.parse(result.content as string); Utils.triggerEvent( parsedData.event_bus_name, parsedData.event_bus_payload ); Reporter.reportItem( { st: Constants.ST_OK, t: tagInfo.id, p: tagInfo.pr, tNm: tagInfo.nm, req: parsedData, }, evtId ); } catch (err) { Logger.logError(err); Reporter.reportItem( { st: Constants.ST_ERR, t: tagInfo.id, p: tagInfo.pr, tNm: tagInfo.nm, req: result.content, }, evtId ); } } else { Logger.logError('Unsupported provider type: ' + provider.sTyp); } } } catch (err) { Logger.logError( 'Failed loading tag: ', tagInfo.id, ': Name: ', tagInfo.nm, 'with error ', err ); Reporter.reportItem( { st: Constants.ST_ERR, t: tagInfo.id, p: tagInfo.pr, tNm: tagInfo.nm, req: (err as any)?.stack, }, evtId ); } } } catch (err) { Logger.logError('Error processing tags: ', err); // TODO: Report as metric to client } } /** * Takes all the placeholders in a tag and replaces them with actual values from data elements or * actual static values. Method returns an object with errCd and replaced content and a boolean identifier * to denote if this process failed or not * @param tagInfo * @param tag * @param provider * @param prStatus * @param dataElements * @param transFunctions */ private static hydrateTag( tagInfo: TagInfoItem, tag: ClientSdkTagItem, provider: ClientSdkPrItem, prStatus: boolean, dataElements: MapLike, transFunctions: MapLike ): HydrateTagInfo { Logger.logDbg(`Hydrating Tag: ${tagInfo.nm} [${tagInfo.id}]`); if (!provider) { // serious issue. tag is in build sdk but not the appropriate provider Logger.logError( `Not hydrating tag: ${tagInfo.nm} [${tagInfo.id}] because appropriate provider was not found` ); return { isInError: true, content: null, errCd: 'NP', errMsg: `Not hydrating tag: ${tagInfo.nm} [${tagInfo.id}] because appropriate provider was not found`, }; // No Provider } if (provider.pr === 'B' && !prStatus) { Logger.logDbg( `Not hydrating provider: ${provider.nm} [${tag.p}] because it is blacklisted` ); return { isInError: true, content: null, errCd: 'PRB', errMsg: `Not hydrating provider: ${provider.nm} [${tag.p}] because it is blacklisted`, }; // Privacy Blocked } // merge both provider and client tag params into one - tag param overwrites provider params const params: MapLike = Utils.mergeMaps( undefined, provider.rParams, tag.rParams ); if (provider.typ === 'app' && provider.sTyp === 'r') { // this is o app type provider. no need to find place-holders, just convert params to an object and prepare the json object return this.resourceParamsToActualValues( Object.values(params), dataElements, transFunctions ); } else { // make tag url const preReplaceUrl: string = provider.url + (tag.url ? tag.url : ''); const finalUrl: string = provider.url + (tag.url ? tag.url : ''); // find placeholder values const placeHolders: string[] = this.findPlaceHolders(preReplaceUrl); // process tag attributes return this.replacePlaceHolders( finalUrl, placeHolders, dataElements, transFunctions, params, provider.chld || tag.chld, 'rph' ); } } private static findPlaceHolders(content: string): string[] { const placeHolders: string[] = []; let match = Constants.PLACEHOLDER_REGEX.exec(content); while (match !== null) { placeHolders.push(match[2]); match = Constants.PLACEHOLDER_REGEX.exec(content); } return placeHolders; } private static resourceParamsToActualValues( params: ClientSdkParamItem[], deItems: MapLike, transFunctions: MapLike ): HydrateTagInfo { // resolve all param values and convert them into a temporary structure const paramValueArray: MapLike = {}; for (const param of params) { if (param.val_src !== 'CHILDREN') { const deVal = this.parseParamValue(param, deItems, transFunctions); if (param.rqd && typeof deVal === 'undefined') { return { isInError: true, content: '', errCd: 'REF', errMsg: `Tag validation failed. Parameter ${param.nm} is marked as mandatory, but no value was found in source.`, }; } paramValueArray[param.fKey] = deVal; } } const op = Utils.unFlattenObject(paramValueArray); return { isInError: false, content: JSON.stringify(op), errCd: null, errMsg: null, }; } private static parseParamValue( paramItem: ClientSdkParamItem, deItems: MapLike, transFunctions: MapLike ): any { let paramValue = undefined; if (paramItem.sv || paramItem.de || paramItem.tfId) { if (paramItem.sv) { paramValue = paramItem.sv; } else if (paramItem.de) { paramValue = deItems[paramItem.de]; } else if (paramItem.tfId) { paramValue = transFunctions[paramItem.tfId]; } paramValue = Utils.applyTransformationResourceParam( paramValue, paramItem.tf ); return paramValue; } else { return undefined; } } private static replacePlaceHolders( content: string, placeHolders: string[], deItems: MapLike, transFunctions: MapLike, params: MapLike, hasChildren: boolean, replaceMode: ReplaceMode ): HydrateTagInfo { let isInError = false; let errMsg; if (!params) { return { isInError: false, content, errCd: null, errMsg: 'No parameters where found to process in this tag', }; } if (hasChildren) { // convert the flattened parameters to parent - child format. this is inside a condition // because we may not have a lot of JSON type parameters and we can save some processing time by limiting // when this is done params = Utils.unFlattenResourceParams(Object.values(params)); } for (const ph of placeHolders) { try { const tagParam: ClientSdkParamItem = params[ph]; if (tagParam) { if (tagParam.children && tagParam.children.length > 0) { const paramValue: MapLike = {}; for (const childParam of tagParam.children) { paramValue[childParam.nm] = this.parseParamValue( childParam, deItems, transFunctions ); } content = this.replaceValues( ph, content, JSON.stringify(paramValue), replaceMode ); } else { const paramValue = this.parseParamValue( tagParam, deItems, transFunctions ); if (typeof paramValue === 'object') { content = this.replaceValues( ph, content, JSON.stringify(paramValue), replaceMode ); } else { content = this.replaceValues( ph, content, paramValue, replaceMode ); } } } else { // TODO: Report // place holder was found but the corresponding item was not found in tag resource params Logger.logDbg( 'place holder:', ph, ', was found but the corresponding item was not found in tag resource params' ); } } catch (err) { isInError = true; errMsg = (err as any)?.stack; break; } } return { isInError, content, errCd: isInError ? 'REF' : null, errMsg: errMsg, }; } private static checkTagDateValidity( currentEpochSeconds: number, tagStart: number, tagEnd: number ): boolean { return currentEpochSeconds >= tagStart && currentEpochSeconds <= tagEnd; } private static replaceValues( placeHolder: string, content: string, val: any, replaceMode: ReplaceMode ): string { let replaceVal: string; if (replaceMode === 'kph') { replaceVal = val && val !== '' ? val : `{{${placeHolder}}`; } else if (replaceMode === 'rph') { replaceVal = val && val !== '' ? val : ''; } content = content.split(`{{${placeHolder}}}`).join(replaceVal); return content; } }