UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

318 lines (258 loc) • 8.6 kB
import * as R from "ramda"; import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils"; import { ContextKeysManager } from "@applicaster/zapp-react-native-utils/appUtils/contextKeysManager"; import { log_error, log_verbose, log_warning } from "./logger"; export type Params = { [targetParamName: string]: ParamRule; // need to check }; export type ParamRule = { optional?: boolean | null; source?: string | null }; export type EventParams = { [key: string]: string; }; export enum Strategy { allowUnlisted = "allowUnlisted", warnUnlisted = "warnUnlisted", blockUnlisted = "blockUnlisted", } enum sourceType { default = "", // just rename the input param ctx = "ctx", // create new key and and pull value from storages using dot namespace notation analyticsCustomProperties = "analyticsCustomProperties", // create new key for analyticsCustomProperties } // Not sure I get this enum that takes an arg of (prefix) const ValueSourceType = (prefix: string | null = null) => { switch (prefix) { case "ctx": return sourceType.ctx; case "analyticsCustomProperties": return sourceType.analyticsCustomProperties; default: return sourceType.default; } }; class StateHolder { public pluckedParams: Map<string, any>; private _cap: Map<string, any> | null | undefined = undefined; private event: AnalyticsEvent; constructor(event: AnalyticsEvent) { this.event = event; this.pluckedParams = R.clone(event.params); // deep clone, no references } public popValue(key: string, required: boolean): string | null { const value = this.pluckedParams[key]; if (isEmptyOrNil(value)) { if (required) { log_error(`Event ${this.event.name} has missing parameter ${key}`); } return null; } delete this.pluckedParams[key]; return value as string; } private acp(prefix: string, suffix: string): string | null { if (this._cap === undefined) { this._cap = this.pluckedParams[prefix] ? JSON.parse(this.pluckedParams[prefix]) : null; delete this.pluckedParams[prefix]; } return this._cap?.[suffix]; } private async getFromLocalAndSession( key: string, namespace: string | null = null ): Promise<string | null> { const args = namespace ? { key, namespace } : key; const value = await ContextKeysManager.instance.getKey(args); return value?.toString() || null; } private async resolveContext( context: string, compositeKey: string, required: boolean ): Promise<string | null> { if (ValueSourceType(context) !== context) { log_error( `Event ${this.event.name} failed to resolve parameter ${compositeKey}, unsupported source type ${context}` ); return null; } const [namespace, key] = compositeKey.includes(".") ? compositeKey.split(".") : [null, compositeKey]; const contextValue = await this.getFromLocalAndSession(key, namespace); if (required && contextValue === null) { log_error( `Event ${this.event.name} failed to resolve parameter ${compositeKey}` ); return null; } return contextValue; } public async resolveSource( source: string, required: boolean ): Promise<string | null> { const CTX_DELIM = "/"; // maybe use find const ctx = source.indexOf(CTX_DELIM); const prefix = source.substring(0, ctx); const suffix = source.substring(ctx + 1); if (prefix === sourceType.analyticsCustomProperties) { return this.acp(prefix, suffix); } else if (ctx === -1) { return this.popValue(source, required); } else { return await this.resolveContext( source.substring(0, ctx), source.substring(ctx + 1), required ); } } } export type EventRule = { event?: string | null; ignore?: boolean | null; params?: Params | null; rename: string | null; excludeParams?: String[] | null; regex?: string | null; matcher?: RegExp | null; strategy?: string | null; }; export type MapperConfiguration = { rules: EventRule[]; strategy: string; }; export type AnalyticsEvent = { name: string; params: EventParams; }; export class Mapper { private configuration: MapperConfiguration | null = null; setConfiguration(config: MapperConfiguration) { this.configuration = R.clone(config); const rules = this.configuration?.rules; Array.isArray(rules) && rules.forEach((rule) => { rule.matcher = (rule.regex && new RegExp(rule.regex, "gi")) || null; }); } async map(event: AnalyticsEvent): Promise<AnalyticsEvent | null> { const rules = this.configuration?.rules || null; if (isEmptyOrNil(rules)) { log_verbose( "No analytics mapping rules found, skipping.", this.configuration ); return event; } const rule = rules!.find((r) => r.matcher ? r.matcher.test(event.name) : r.event === event.name ); if (rule) { return await this.applyEventRule(rule, event, rule.matcher); } switch (this.configuration!.strategy) { case Strategy.allowUnlisted: return event; case Strategy.warnUnlisted: log_warning(`Unlisted event ${event.name}`, event); return event; case Strategy.blockUnlisted: return null; default: log_error( `Unsupported strategy was provided: ${this.configuration!.strategy}` ); return event; } } private async applyEventRule( rule: EventRule, event: AnalyticsEvent, matcher?: RegExp | null ): Promise<AnalyticsEvent | null> { if (rule.ignore) { return null; } const excludedKeys = rule?.excludeParams ? rule?.excludeParams : []; const state = new StateHolder(event); // remove excluded keys // Omit accepts null values just requires it to be an array state.pluckedParams = R.omit(excludedKeys, state.pluckedParams); // We could also use object since benefits of map not really used here const resultParams = new Map<string, any>(); // perform param mapping rule.params && (await Promise.all( Object.entries(rule.params).map(async (entry) => { const [key, val] = entry; // apply rule const modifiedValue = await this.applyParamRule( key, // outname: string val, // rule: rule value? state // original params: map? ); // assign rule if (!isEmptyOrNil(modifiedValue)) { resultParams.set(key, modifiedValue); } }) )); let params = resultParams.size ? Object.fromEntries(resultParams) : {}; // deal with the remaining params if (!isEmptyOrNil(state.pluckedParams)) { if (rule.strategy) { switch (rule.strategy) { case Strategy.allowUnlisted: params = { ...state.pluckedParams, ...params }; break; case Strategy.warnUnlisted: log_warning( `Event ${event.name} has excess params`, state.pluckedParams ); params = { ...state.pluckedParams, ...params }; break; // block case Strategy.blockUnlisted: break; } } } // rename if needed and return if (isEmptyOrNil(rule.rename)) { return this.AnalyticsEvent(event.name, params); } if (isEmptyOrNil(matcher)) { return this.AnalyticsEvent(rule.rename!, params); } else { // If we have a regex value // Check if event name matches regex pattern // if so return renamed event // If we wanted to use regex groups like this "Screen viewed: (?<screen>.*)" // and utilize the <screen> group we have to use // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges const newName = !isEmptyOrNil(matcher!.test(event.name)) ? event.name.replace(matcher!, rule.rename!) : rule.rename; return this.AnalyticsEvent(newName!, params); } } AnalyticsEvent(name: string, params: EventParams): AnalyticsEvent { return { name, params }; } private async applyParamRule( outName: string, rule: ParamRule, state: StateHolder ): Promise<string | null> { if (isEmptyOrNil(rule.source)) { return state.popValue(outName, rule.optional === false); } else { return await state.resolveSource(rule.source!, rule.optional === false); } } }