UNPKG

@elbwalker/utils

Version:

Shared utils for walkerOS packages

1 lines 86.9 kB
{"version":3,"sources":["../src/core/anonymizeIP.ts","../src/core/assign.ts","../src/core/constants.ts","../src/core/is.ts","../src/core/byPath.ts","../src/core/castValue.ts","../src/core/clone.ts","../src/core/getId.ts","../src/core/property.ts","../src/core/tryCatch.ts","../src/core/mapping.ts","../src/core/useHooks.ts","../src/core/invocations.ts","../src/core/handle.ts","../src/core/destination.ts","../src/core/on.ts","../src/core/consent.ts","../src/core/eventGenerator.ts","../src/core/getMarketingParameters.ts","../src/core/onLog.ts","../src/core/request.ts","../src/core/send.ts","../src/core/throwError.ts","../src/core/trim.ts","../src/core/userAgent.ts","../src/core/validate.ts"],"sourcesContent":["export function anonymizeIP(ip: string): string {\n const ipv4Pattern = /^(?:\\d{1,3}\\.){3}\\d{1,3}$/;\n\n if (!ipv4Pattern.test(ip)) return '';\n\n return ip.replace(/\\.\\d+$/, '.0'); // Set the last octet to 0\n}\n","interface Assign {\n merge?: boolean; // Merge array properties (default) instead of overriding them\n shallow?: boolean; // Create a shallow copy (default) instead of updating the target object\n extend?: boolean; // Extend the target with new properties (default) instead of only updating existing ones\n}\n\nconst defaultOptions: Assign = {\n merge: true,\n shallow: true,\n extend: true,\n};\n\nexport function assign<T extends object, U extends object>(\n target: T,\n obj: U = {} as U,\n options: Assign = {},\n): T & U {\n options = { ...defaultOptions, ...options };\n\n const finalObj = Object.entries(obj).reduce((acc, [key, sourceProp]) => {\n const targetProp = target[key as keyof typeof target];\n\n // Only merge arrays\n if (\n options.merge &&\n Array.isArray(targetProp) &&\n Array.isArray(sourceProp)\n ) {\n acc[key as keyof typeof acc] = sourceProp.reduce(\n (acc, item) => {\n // Remove duplicates\n return acc.includes(item) ? acc : [...acc, item];\n },\n [...targetProp],\n );\n } else if (options.extend || key in target) {\n // Extend the target with new properties or update existing ones\n acc[key as keyof typeof acc] = sourceProp;\n }\n\n return acc;\n }, {} as U);\n\n // Handle shallow or deep copy based on options\n if (options.shallow) {\n return { ...target, ...finalObj };\n } else {\n Object.assign(target, finalObj);\n return target as T & U;\n }\n}\n","import type { WalkerOS } from '@elbwalker/types';\n\nexport type CommandTypes =\n | 'Action'\n | 'Config'\n | 'Consent'\n | 'Context'\n | 'Custom'\n | 'Destination'\n | 'Elb'\n | 'Globals'\n | 'Hook'\n | 'Init'\n | 'Link'\n | 'On'\n | 'Prefix'\n | 'Ready'\n | 'Run'\n | 'Session'\n | 'User'\n | 'Walker';\n\n// Define Commands with keys as CommandTypes\nexport const Commands: Record<CommandTypes, WalkerOS.Commands> = {\n Action: 'action',\n Config: 'config',\n Consent: 'consent',\n Context: 'context',\n Custom: 'custom',\n Destination: 'destination',\n Elb: 'elb',\n Globals: 'globals',\n Hook: 'hook',\n Init: 'init',\n Link: 'link',\n On: 'on',\n Prefix: 'data-elb',\n Ready: 'ready',\n Run: 'run',\n Session: 'session',\n User: 'user',\n Walker: 'walker',\n} as const;\n\nexport type StorageType = 'cookie' | 'local' | 'session';\n\nconst UtilsStorage: { [key: string]: StorageType } = {\n Cookie: 'cookie',\n Local: 'local',\n Session: 'session',\n} as const;\n\nconst Utils = {\n Storage: UtilsStorage,\n};\n\nexport const Const = {\n Commands,\n Utils,\n};\n\nexport default Const;\n","import type { WalkerOS } from '@elbwalker/types';\nimport { Const } from './constants';\n\nexport function isArguments(value: unknown): value is IArguments {\n return Object.prototype.toString.call(value) === '[object Arguments]';\n}\n\nexport function isArray<T>(value: unknown): value is T[] {\n return Array.isArray(value);\n}\n\nexport function isBoolean(value: unknown): value is boolean {\n return typeof value === 'boolean';\n}\n\nexport function isCommand(entity: string) {\n return entity === Const.Commands.Walker;\n}\n\nexport function isDefined<T>(val: T | undefined): val is T {\n return typeof val !== 'undefined';\n}\n\nexport function isElementOrDocument(elem: unknown): elem is Element {\n return elem === document || elem instanceof Element;\n}\n\nexport function isNumber(value: unknown): value is number {\n return typeof value === 'number' && !Number.isNaN(value);\n}\n\nexport function isObject(value: unknown): value is WalkerOS.AnyObject {\n return (\n typeof value === 'object' &&\n value !== null &&\n !isArray(value) &&\n Object.prototype.toString.call(value) === '[object Object]'\n );\n}\n\nexport function isSameType<T>(\n variable: unknown,\n type: T,\n): variable is typeof type {\n return typeof variable === typeof type;\n}\n\nexport function isString(value: unknown): value is string {\n return typeof value === 'string';\n}\n","import { WalkerOS } from '@elbwalker/types';\nimport { isArray, isDefined } from './is';\n\nexport function getByPath(\n event: unknown,\n key: string = '',\n defaultValue?: unknown,\n i: unknown = 0,\n): unknown {\n // String dot notation for object (\"data.id\" -> { data: { id: 1 } })\n const keys = key.split('.');\n let values: unknown = event;\n\n for (let index = 0; index < keys.length; index++) {\n const k = keys[index];\n\n if (k === '*' && isArray(values)) {\n const remainingKeys = keys.slice(index + 1).join('.');\n const result: unknown[] = [];\n\n for (const item of values) {\n const value = getByPath(item, remainingKeys, defaultValue, i);\n result.push(value);\n }\n\n return result;\n }\n\n values =\n values instanceof Object ? values[k as keyof typeof values] : undefined;\n\n if (!values) break;\n }\n\n return isDefined(values) ? values : defaultValue;\n}\n\nexport function setByPath(\n event: WalkerOS.Event,\n key: string,\n value: unknown,\n): WalkerOS.Event {\n const keys = key.split('.');\n let current: WalkerOS.AnyObject | WalkerOS.Event = event;\n\n for (let i = 0; i < keys.length; i++) {\n const k = keys[i] as keyof typeof current;\n\n // Set the value if it's the last key\n if (i === keys.length - 1) {\n current[k] = value;\n } else {\n // Traverse to the next level\n if (\n !(k in current) ||\n typeof current[k] !== 'object' ||\n current[k] === null\n ) {\n current[k] = {};\n }\n\n // Move deeper into the object\n current = current[k] as WalkerOS.AnyObject;\n }\n }\n\n return event;\n}\n","import type { WalkerOS } from '@elbwalker/types';\n\nexport function castValue(value: unknown): WalkerOS.PropertyType {\n if (value === 'true') return true;\n if (value === 'false') return false;\n\n const number = Number(value); // Converts \"\" to 0\n if (value == number && value !== '') return number;\n\n return String(value);\n}\n","export function clone<T>(\n org: T,\n visited: WeakMap<object, unknown> = new WeakMap(),\n): T {\n // Handle primitive values and functions directly\n if (typeof org !== 'object' || org === null) return org;\n\n // Check for circular references\n if (visited.has(org)) return visited.get(org) as T;\n\n // Allow list of clonable types\n const type = Object.prototype.toString.call(org);\n if (type === '[object Object]') {\n const clonedObj = {} as Record<string | symbol, unknown>;\n visited.set(org as object, clonedObj); // Remember the reference\n\n for (const key in org as Record<string | symbol, unknown>) {\n if (Object.prototype.hasOwnProperty.call(org, key)) {\n clonedObj[key] = clone(\n (org as Record<string | symbol, unknown>)[key],\n visited,\n );\n }\n }\n return clonedObj as T;\n }\n\n if (type === '[object Array]') {\n const clonedArray = [] as unknown[];\n visited.set(org as object, clonedArray); // Remember the reference\n\n (org as unknown[]).forEach((item) => {\n clonedArray.push(clone(item, visited));\n });\n\n return clonedArray as T;\n }\n\n if (type === '[object Date]') {\n return new Date((org as unknown as Date).getTime()) as T;\n }\n\n if (type === '[object RegExp]') {\n const reg = org as unknown as RegExp;\n return new RegExp(reg.source, reg.flags) as T;\n }\n\n // Skip cloning for unsupported types and return reference\n return org;\n}\n","export function getId(length = 6): string {\n let str = '';\n for (let l = 36; str.length < length; )\n str += ((Math.random() * l) | 0).toString(l);\n return str;\n}\n","import type { WalkerOS } from '@elbwalker/types';\nimport {\n isArguments,\n isArray,\n isBoolean,\n isDefined,\n isNumber,\n isObject,\n isString,\n} from './is';\n\nexport function isPropertyType(value: unknown): value is WalkerOS.PropertyType {\n return (\n isBoolean(value) ||\n isString(value) ||\n isNumber(value) ||\n !isDefined(value) ||\n (isArray(value) && value.every(isPropertyType)) ||\n (isObject(value) && Object.values(value).every(isPropertyType))\n );\n}\n\nexport function filterValues(value: unknown): WalkerOS.Property | undefined {\n if (isBoolean(value) || isString(value) || isNumber(value)) return value;\n\n if (isArguments(value)) return filterValues(Array.from(value));\n\n if (isArray(value)) {\n return value\n .map((item) => filterValues(item))\n .filter((item): item is WalkerOS.PropertyType => item !== undefined);\n }\n\n if (isObject(value)) {\n return Object.entries(value).reduce<Record<string, WalkerOS.Property>>(\n (acc, [key, val]) => {\n const filteredValue = filterValues(val);\n if (filteredValue !== undefined) acc[key] = filteredValue;\n return acc;\n },\n {},\n );\n }\n\n return;\n}\n\nexport function castToProperty(value: unknown): WalkerOS.Property | undefined {\n return isPropertyType(value) ? value : undefined;\n}\n","// Use function overload to support different return type depending on onError\n// Types\nexport function tryCatch<P extends unknown[], R, S>(\n fn: (...args: P) => R | undefined,\n onError: (err: unknown) => S,\n onFinally?: () => void,\n): (...args: P) => R | S;\nexport function tryCatch<P extends unknown[], R>(\n fn: (...args: P) => R | undefined,\n onError?: undefined,\n onFinally?: () => void,\n): (...args: P) => R | undefined;\n// Implementation\nexport function tryCatch<P extends unknown[], R, S>(\n fn: (...args: P) => R | undefined,\n onError?: (err: unknown) => S,\n onFinally?: () => void,\n): (...args: P) => R | S | undefined {\n return function (...args: P): R | S | undefined {\n try {\n return fn(...args);\n } catch (err) {\n if (!onError) return;\n return onError(err);\n } finally {\n onFinally?.();\n }\n };\n}\n\n// Use function overload to support different return type depending on onError\n// Types\nexport function tryCatchAsync<P extends unknown[], R, S>(\n fn: (...args: P) => R,\n onError: (err: unknown) => S,\n onFinally?: () => void | Promise<void>,\n): (...args: P) => Promise<R | S>;\nexport function tryCatchAsync<P extends unknown[], R>(\n fn: (...args: P) => R,\n onError?: undefined,\n onFinally?: () => void | Promise<void>,\n): (...args: P) => Promise<R | undefined>;\n// Implementation\nexport function tryCatchAsync<P extends unknown[], R, S>(\n fn: (...args: P) => R,\n onError?: (err: unknown) => S,\n onFinally?: () => void | Promise<void>,\n): (...args: P) => Promise<R | S | undefined> {\n return async function (...args: P): Promise<R | S | undefined> {\n try {\n return await fn(...args);\n } catch (err) {\n if (!onError) return;\n return await onError(err);\n } finally {\n await onFinally?.();\n }\n };\n}\n","import type { Mapping, WalkerOS } from '@elbwalker/types';\nimport { getGrantedConsent } from './consent';\nimport { getByPath } from './byPath';\nimport { isArray, isDefined, isString, isObject } from './is';\nimport { castToProperty } from './property';\nimport { tryCatchAsync } from './tryCatch';\n\nexport async function getMappingEvent(\n event: WalkerOS.PartialEvent,\n mapping?: Mapping.Config<unknown>,\n): Promise<Mapping.EventMapping> {\n const [entity, action] = (event.event || '').split(' ');\n if (!mapping || !entity || !action) return {};\n\n let eventMapping: Mapping.EventConfig | undefined;\n let mappingKey = '';\n let entityKey = entity;\n let actionKey = action;\n\n const resolveEventMapping = (\n eventMapping?:\n | Mapping.EventConfig<unknown>\n | Mapping.EventConfig<unknown>[],\n ) => {\n if (!eventMapping) return;\n eventMapping = isArray(eventMapping) ? eventMapping : [eventMapping];\n\n return eventMapping.find(\n (eventMapping) =>\n !eventMapping.condition || eventMapping.condition(event),\n );\n };\n\n if (!mapping[entityKey]) entityKey = '*';\n const entityMapping = mapping[entityKey];\n\n if (entityMapping) {\n if (!entityMapping[actionKey]) actionKey = '*';\n eventMapping = resolveEventMapping(entityMapping[actionKey]);\n }\n\n // Fallback to * *\n if (!eventMapping) {\n entityKey = '*';\n actionKey = '*';\n eventMapping = resolveEventMapping(mapping[entityKey]?.[actionKey]);\n }\n\n if (eventMapping) mappingKey = `${entityKey} ${actionKey}`;\n\n return { eventMapping, mappingKey };\n}\n\nexport async function getMappingValue(\n value: WalkerOS.DeepPartialEvent | unknown | undefined,\n data: Mapping.Data = {},\n options: Mapping.Options = {},\n): Promise<WalkerOS.Property | undefined> {\n if (!isDefined(value)) return;\n\n // Get consent state in priority order: value.consent > options.consent > instance?.consent\n const consentState =\n ((isObject(value) && value.consent) as WalkerOS.Consent) ||\n options.consent ||\n options.instance?.consent;\n\n const mappings = isArray(data) ? data : [data];\n\n for (const mapping of mappings) {\n const result = await tryCatchAsync(processMappingValue)(value, mapping, {\n ...options,\n consent: consentState,\n });\n if (isDefined(result)) return result;\n }\n}\n\nasync function processMappingValue(\n value: WalkerOS.DeepPartialEvent | unknown,\n mapping: Mapping.Value,\n options: Mapping.Options = {},\n): Promise<WalkerOS.Property | undefined> {\n const { instance, consent: consentState } = options;\n\n // Ensure mapping is an array for uniform processing\n const mappings = isArray(mapping) ? mapping : [mapping];\n\n // Loop over each mapping and return the first valid result\n return mappings.reduce(async (accPromise, mappingItem) => {\n const acc = await accPromise;\n if (acc) return acc; // A valid result was already found\n\n const mapping = isString(mappingItem) ? { key: mappingItem } : mappingItem;\n\n if (!Object.keys(mapping).length) return;\n\n const {\n condition,\n consent,\n fn,\n key,\n loop,\n map,\n set,\n validate,\n value: staticValue,\n } = mapping;\n\n // Check if this mapping should be used\n if (\n condition &&\n !(await tryCatchAsync(condition)(value, mappingItem, instance))\n )\n return;\n\n // Check if consent is required and granted\n if (consent && !getGrantedConsent(consent, consentState))\n return staticValue;\n\n let mappingValue: unknown = isDefined(staticValue) ? staticValue : value;\n\n if (fn) {\n // Use a custom function to get the value\n mappingValue = await tryCatchAsync(fn)(value, mappingItem, options);\n }\n\n if (key) {\n // Get dynamic value from the event\n mappingValue = getByPath(value, key, staticValue);\n }\n\n if (loop) {\n const [scope, itemMapping] = loop;\n\n const data =\n scope === 'this'\n ? [value]\n : await getMappingValue(value, scope, options);\n\n if (isArray(data)) {\n mappingValue = (\n await Promise.all(\n data.map((item) => getMappingValue(item, itemMapping, options)),\n )\n ).filter(isDefined);\n }\n } else if (map) {\n mappingValue = await Object.entries(map).reduce(\n async (mappedObjPromise, [mapKey, mapValue]) => {\n const mappedObj = await mappedObjPromise;\n const result = await getMappingValue(value, mapValue, options);\n if (isDefined(result)) mappedObj[mapKey] = result;\n return mappedObj;\n },\n Promise.resolve({} as WalkerOS.AnyObject),\n );\n } else if (set) {\n mappingValue = await Promise.all(\n set.map((item) => processMappingValue(value, item, options)),\n );\n }\n\n // Validate the value\n if (validate && !(await tryCatchAsync(validate)(mappingValue)))\n mappingValue = undefined;\n\n const property = castToProperty(mappingValue);\n\n // Finally, check and convert the type\n return isDefined(property) ? property : castToProperty(staticValue); // Always use value as a fallback\n }, Promise.resolve(undefined as WalkerOS.Property | undefined));\n}\n","import type { Hooks } from '@elbwalker/types';\n\nexport function useHooks<P extends unknown[], R>(\n fn: (...args: P) => R,\n name: string,\n hooks: Hooks.Functions,\n): (...args: P) => R {\n return function (...args: P): R {\n let result: R;\n const preHook = ('pre' + name) as keyof Hooks.Functions;\n const postHook = ('post' + name) as keyof Hooks.Functions;\n const preHookFn = hooks[preHook] as unknown as Hooks.HookFn<typeof fn>;\n const postHookFn = hooks[postHook] as unknown as Hooks.HookFn<typeof fn>;\n\n if (preHookFn) {\n // Call the original function within the preHook\n result = preHookFn({ fn }, ...args);\n } else {\n // Regular function call\n result = fn(...args);\n }\n\n if (postHookFn) {\n // Call the post-hook function with fn, result, and the original args\n result = postHookFn({ fn, result }, ...args);\n }\n\n return result;\n };\n}\n","export function debounce<P extends unknown[], R>(\n fn: (...args: P) => R,\n wait = 1000,\n immediate = false,\n) {\n let timer: number | NodeJS.Timeout | null = null;\n let result: R;\n let hasCalledImmediately = false;\n\n return (...args: P): Promise<R> => {\n // Return value as promise\n return new Promise((resolve) => {\n const callNow = immediate && !hasCalledImmediately;\n\n // abort previous invocation\n if (timer) clearTimeout(timer);\n\n timer = setTimeout(() => {\n timer = null;\n if (!immediate || hasCalledImmediately) {\n result = fn(...args);\n resolve(result);\n }\n }, wait);\n\n if (callNow) {\n hasCalledImmediately = true;\n result = fn(...args);\n resolve(result);\n }\n });\n };\n}\n\ntype Timeout = ReturnType<typeof setTimeout>;\nexport function throttle<P extends unknown[], R>(\n fn: (...args: P) => R | undefined,\n delay = 1000,\n): (...args: P) => R | undefined {\n let isBlocked: Timeout | null = null;\n\n return function (...args: P): R | undefined {\n // Skip since function is still blocked by previous call\n if (isBlocked !== null) return;\n\n // Set a blocking timeout\n isBlocked = setTimeout(() => {\n // Unblock function\n isBlocked = null;\n }, delay) as Timeout;\n\n // Call the function\n return fn(...args);\n };\n}\n","import type { WalkerOS } from '@elbwalker/types';\nimport type { Destination } from '@elbwalker/types';\nimport type { Elb } from '@elbwalker/types';\nimport { Commands, Const } from './constants';\nimport { addDestination } from './destination';\nimport { assign } from './assign';\nimport { isObject, isSameType } from './is';\nimport { setConsent } from './consent';\n\nexport async function commonHandleCommand(\n instance: WalkerOS.Instance,\n action: string,\n data?: unknown,\n options?: unknown,\n): Promise<Elb.PushResult | undefined> {\n let result: Elb.PushResult | undefined;\n\n switch (action) {\n case Const.Commands.Consent:\n if (isObject(data)) {\n result = await setConsent(instance, data as WalkerOS.Consent);\n }\n break;\n\n case Const.Commands.Custom:\n if (isObject(data)) {\n instance.custom = assign(instance.custom, data as WalkerOS.Properties);\n }\n break;\n\n case Const.Commands.Destination:\n if (isObject(data) && typeof data.push === 'function') {\n result = await addDestination(\n instance,\n data as Destination.DestinationInit,\n options as Destination.Config,\n );\n }\n break;\n\n case Const.Commands.Globals:\n if (isObject(data)) {\n instance.globals = assign(\n instance.globals,\n data as WalkerOS.Properties,\n );\n }\n break;\n\n case Const.Commands.User:\n if (isObject(data)) {\n assign(instance.user, data as WalkerOS.User, { shallow: false });\n }\n break;\n }\n\n return result;\n}\n\nexport function createEventOrCommand(\n instance: WalkerOS.Instance,\n nameOrEvent: unknown,\n defaults: WalkerOS.PartialEvent = {},\n): { event?: WalkerOS.Event; command?: string } {\n // Determine the partial event\n const partialEvent: WalkerOS.PartialEvent = isSameType(\n nameOrEvent,\n '' as string,\n )\n ? { event: nameOrEvent, ...defaults }\n : { ...defaults, ...(nameOrEvent || {}) };\n\n if (!partialEvent.event) throw new Error('Event name is required');\n\n // Check for valid entity and action event format\n const [entityValue, actionValue] = partialEvent.event.split(' ');\n if (!entityValue || !actionValue) throw new Error('Event name is invalid');\n\n // It's a walker command\n if (entityValue === Commands.Walker) return { command: actionValue };\n\n // Regular event\n ++instance.count;\n\n // Values that are eventually used by other properties\n const {\n timestamp = Date.now(),\n group = instance.group,\n count = instance.count,\n } = partialEvent;\n\n // Extract properties with default fallbacks\n const {\n event = `${entityValue} ${actionValue}`,\n data = {},\n context = {},\n globals = instance.globals,\n custom = {},\n user = instance.user,\n nested = [],\n consent = instance.consent,\n id = `${timestamp}-${group}-${count}`,\n trigger = '',\n entity = entityValue,\n action = actionValue,\n timing = 0,\n version = {\n source: instance.version,\n tagging: instance.config.tagging || 0,\n },\n source = { type: '', id: '', previous_id: '' },\n } = partialEvent;\n\n const fullEvent: WalkerOS.Event = {\n event,\n data,\n context,\n globals,\n custom,\n user,\n nested,\n consent,\n id,\n trigger,\n entity,\n action,\n timestamp,\n timing,\n group,\n count,\n version,\n source,\n };\n\n return { event: fullEvent };\n}\n","import type {\n Destination as WalkerOSDestination,\n Mapping,\n WalkerOS,\n Elb,\n} from '@elbwalker/types';\nimport { getId } from './getId';\nimport { setByPath } from './byPath';\nimport { getMappingEvent, getMappingValue } from './mapping';\nimport { getGrantedConsent } from './consent';\nimport { tryCatchAsync } from './tryCatch';\nimport { assign } from './assign';\nimport { useHooks } from './useHooks';\nimport { isDefined, isObject } from './is';\nimport { debounce } from './invocations';\nimport { clone } from './clone';\nimport { createEventOrCommand } from './handle';\n\nexport type HandleCommandFn<T extends WalkerOS.Instance> = (\n instance: T,\n action: string,\n data?: Elb.PushData,\n options?: Elb.PushOptions,\n) => Promise<Elb.PushResult>;\n\nexport function createPush<T extends WalkerOS.Instance, F extends Elb.Fn>(\n instance: T,\n handleCommand: HandleCommandFn<T>,\n prepareEvent: Elb.Fn<WalkerOS.PartialEvent>,\n): F {\n return useHooks(\n async (...args) => {\n return await tryCatchAsync(\n async (...args: Parameters<Elb.Arguments>): Promise<Elb.PushResult> => {\n const [nameOrEvent, pushData, options] = args;\n const partialEvent = prepareEvent(...args);\n\n const { event, command } = createEventOrCommand(\n instance,\n nameOrEvent,\n partialEvent,\n );\n\n const result = command\n ? await handleCommand(instance, command, pushData, options)\n : await pushToDestinations(instance, event);\n\n return result;\n },\n (error) => {\n // Call custom error handling\n if (instance.config.onError) instance.config.onError(error, instance);\n\n return createPushResult({ ok: false });\n },\n )(...args);\n },\n 'Push',\n instance.hooks,\n ) as unknown as F;\n}\n\nexport async function addDestination(\n instance: WalkerOS.Instance,\n data: WalkerOSDestination.DestinationInit,\n options?: WalkerOSDestination.Config,\n) {\n // Prefer explicit given config over default config\n const config = options || data.config || { init: false };\n // @TODO might not be the best solution to use options || data.config\n\n const destination: WalkerOSDestination.Destination = {\n ...data,\n config,\n };\n\n let id = config.id; // Use given id\n if (!id) {\n // Generate a new id if none was given\n do {\n id = getId(4);\n } while (instance.destinations[id]);\n }\n\n // Add the destination\n instance.destinations[id] = destination;\n\n // Process previous events if not disabled\n if (config.queue !== false) destination.queue = [...instance.queue];\n\n return pushToDestinations(instance, undefined, { [id]: destination });\n}\n\nexport async function pushToDestinations(\n instance: WalkerOS.Instance,\n event?: WalkerOS.Event,\n destinations?: WalkerOS.Destinations,\n): Promise<Elb.PushResult> {\n const { allowed, consent, globals, user } = instance;\n\n // Check if instance is allowed to push\n if (!allowed) return createPushResult({ ok: false });\n\n // Add event to the instance queue\n if (event) instance.queue.push(event);\n\n // Use given destinations or use internal destinations\n if (!destinations) destinations = instance.destinations;\n\n const results = await Promise.all(\n // Process all destinations in parallel\n Object.entries(destinations).map(async ([id, destination]) => {\n // Create a queue of events to be processed\n let currentQueue = (destination.queue || []).map((event) => ({\n ...event,\n consent,\n }));\n\n // Reset original queue while processing to enable async processing\n destination.queue = [];\n\n // Add event to queue stack\n if (event) {\n // Clone the event to avoid mutating the original event\n let currentEvent = clone(event);\n\n // Policy check\n await Promise.all(\n Object.entries(destination.config.policy || []).map(\n async ([key, mapping]) => {\n const value = await getMappingValue(event, mapping, { instance });\n currentEvent = setByPath(currentEvent, key, value);\n },\n ),\n );\n\n // Add event to queue stack\n currentQueue.push(currentEvent);\n }\n\n // Nothing to do here if the queue is empty\n if (!currentQueue.length) return { id, destination, skipped: true };\n\n const allowedEvents: WalkerOS.Events = [];\n const skippedEvents = currentQueue.filter((queuedEvent) => {\n const grantedConsent = getGrantedConsent(\n destination.config.consent, // Required\n consent, // Current instance state\n queuedEvent.consent, // Individual event state\n );\n\n if (grantedConsent) {\n queuedEvent.consent = grantedConsent; // Save granted consent states only\n\n allowedEvents.push(queuedEvent); // Add to allowed queue\n return false; // Remove from destination queue\n }\n\n return true; // Keep denied events in the queue\n });\n\n // Add skipped events back to the queue\n destination.queue.concat(skippedEvents);\n\n // Execution shall not pass if no events are allowed\n if (!allowedEvents.length) {\n return { id, destination, queue: currentQueue }; // Don't push if not allowed\n }\n\n // Initialize the destination if needed\n const isInitialized = await tryCatchAsync(destinationInit)(\n instance,\n destination,\n );\n\n if (!isInitialized) return { id, destination, queue: currentQueue };\n\n // Process the destinations event queue\n let error = false;\n if (!destination.dlq) destination.dlq = [];\n\n // Process allowed events and store failed ones in the dead letter queue (DLQ)\n await Promise.all(\n allowedEvents.map(async (event) => {\n // Merge event with instance state, prioritizing event properties\n event.globals = assign(globals, event.globals);\n event.user = assign(user, event.user);\n\n await tryCatchAsync(destinationPush, (err) => {\n // Call custom error handling if available\n if (instance.config.onError) instance.config.onError(err, instance);\n error = true; // oh no\n\n // Add failed event to destinations DLQ\n destination.dlq!.push([event, err]);\n\n return false;\n })(instance, destination, event);\n\n return event;\n }),\n );\n\n return { id, destination, error };\n }),\n );\n\n const successful = [];\n const queued = [];\n const failed = [];\n\n for (const result of results) {\n if (result.skipped) continue;\n\n const destination = result.destination;\n\n const ref = { id: result.id, destination };\n\n if (result.error) {\n failed.push(ref);\n } else if (result.queue && result.queue.length) {\n // Merge queue with existing queue\n destination.queue = (destination.queue || []).concat(result.queue);\n queued.push(ref);\n } else {\n successful.push(ref);\n }\n }\n\n return createPushResult({\n ok: !failed.length,\n event,\n successful,\n queued,\n failed,\n });\n}\n\nexport async function destinationInit<\n Destination extends WalkerOSDestination.Destination,\n>(instance: WalkerOS.Instance, destination: Destination): Promise<boolean> {\n // Check if the destination was initialized properly or try to do so\n if (destination.init && !destination.config.init) {\n const configResult = await useHooks(\n destination.init,\n 'DestinationInit',\n instance.hooks,\n )(destination.config, instance);\n\n // Actively check for errors (when false)\n if (configResult === false) return configResult; // don't push if init is false\n\n // Update the destination config if it was returned\n destination.config = {\n ...(configResult || destination.config),\n init: true, // Remember that the destination was initialized\n };\n }\n\n return true; // Destination is ready to push\n}\n\nexport async function destinationPush<\n Destination extends WalkerOSDestination.Destination,\n>(\n instance: WalkerOS.Instance,\n destination: Destination,\n event: WalkerOS.Event,\n): Promise<boolean> {\n const { config } = destination;\n const { eventMapping, mappingKey } = await getMappingEvent(\n event,\n config.mapping,\n );\n\n let data =\n config.data && (await getMappingValue(event, config.data, { instance }));\n\n if (eventMapping) {\n // Check if event should be processed or ignored\n if (eventMapping.ignore) return false;\n\n // Check to use specific event names\n if (eventMapping.name) event.event = eventMapping.name;\n\n // Transform event to a custom data\n if (eventMapping.data) {\n const dataEvent =\n eventMapping.data &&\n (await getMappingValue(event, eventMapping.data, { instance }));\n data =\n isObject(data) && isObject(dataEvent) // Only merge objects\n ? assign(data, dataEvent)\n : dataEvent;\n }\n }\n\n const options = { data, instance };\n\n if (eventMapping?.batch && destination.pushBatch) {\n const batched = eventMapping.batched || {\n key: mappingKey || '',\n events: [],\n data: [],\n };\n batched.events.push(event);\n if (isDefined(data)) batched.data.push(data);\n\n eventMapping.batchFn =\n eventMapping.batchFn ||\n debounce((destination, instance) => {\n useHooks(\n destination.pushBatch!,\n 'DestinationPushBatch',\n instance.hooks,\n )(batched, config, options);\n\n // Reset the batched queues\n batched.events = [];\n batched.data = [];\n }, eventMapping.batch);\n\n eventMapping.batched = batched;\n eventMapping.batchFn(destination, instance);\n } else {\n // It's time to go to the destination's side now\n await useHooks(destination.push, 'DestinationPush', instance.hooks)(\n event,\n config,\n eventMapping,\n options,\n );\n }\n\n return true;\n}\n\nexport function createPushResult(\n partialResult?: Partial<Elb.PushResult>,\n): Elb.PushResult {\n return assign(\n {\n ok: !partialResult?.failed?.length,\n successful: [],\n queued: [],\n failed: [],\n },\n partialResult,\n );\n}\n","import type { On, WalkerOS } from '@elbwalker/types';\nimport { isArray } from './is';\nimport { Const } from './constants';\nimport { tryCatch } from './tryCatch';\n\nexport function on(\n instance: WalkerOS.Instance,\n type: On.Types,\n option: WalkerOS.SingleOrArray<On.Options>,\n) {\n const on = instance.on;\n const onType: Array<On.Options> = on[type] || [];\n const options = isArray(option) ? option : [option];\n\n options.forEach((option) => {\n onType.push(option);\n });\n\n // Update instance on state\n (on[type] as typeof onType) = onType;\n\n // Execute the on function directly\n onApply(instance, type, options);\n}\n\nexport function onApply(\n instance: WalkerOS.Instance,\n type: On.Types,\n options?: Array<On.Options>,\n config?: WalkerOS.Consent,\n) {\n // Use the optionally provided options\n let onConfig = options || [];\n\n if (!options) {\n // Get the instance on events\n onConfig = instance.on[type] || [];\n\n // Add all available on events from the destinations\n Object.values(instance.destinations).forEach((destination) => {\n const onTypeConfig = destination.config.on?.[type];\n if (onTypeConfig) onConfig = onConfig.concat(onTypeConfig);\n });\n }\n\n if (!onConfig.length) return; // No on-events registered, nothing to do\n\n switch (type) {\n case Const.Commands.Consent:\n onConsent(instance, onConfig as Array<On.ConsentConfig>, config);\n break;\n case Const.Commands.Ready:\n onReady(instance, onConfig as Array<On.ReadyConfig>);\n break;\n case Const.Commands.Run:\n onRun(instance, onConfig as Array<On.RunConfig>);\n break;\n case Const.Commands.Session:\n onSession(instance, onConfig as Array<On.SessionConfig>);\n break;\n default:\n break;\n }\n}\n\nfunction onConsent(\n instance: WalkerOS.Instance,\n onConfig: Array<On.ConsentConfig>,\n currentConsent?: WalkerOS.Consent,\n): void {\n const consentState = currentConsent || instance.consent;\n\n onConfig.forEach((consentConfig) => {\n // Collect functions whose consent keys match the rule keys directly\n // Directly execute functions whose consent keys match the rule keys\n Object.keys(consentState) // consent keys\n .filter((consent) => consent in consentConfig) // check for matching rule keys\n .forEach((consent) => {\n // Execute the function\n tryCatch(consentConfig[consent])(instance, consentState);\n });\n });\n}\n\nfunction onReady(\n instance: WalkerOS.Instance,\n onConfig: Array<On.ReadyConfig>,\n): void {\n if (instance.allowed)\n onConfig.forEach((func) => {\n tryCatch(func)(instance);\n });\n}\n\nfunction onRun(\n instance: WalkerOS.Instance,\n onConfig: Array<On.RunConfig>,\n): void {\n if (instance.allowed)\n onConfig.forEach((func) => {\n tryCatch(func)(instance);\n });\n}\n\nfunction onSession(\n instance: WalkerOS.Instance,\n onConfig: Array<On.SessionConfig>,\n): void {\n if (!instance.config.session) return; // Session handling is disabled\n\n onConfig.forEach((func) => {\n tryCatch(func)(instance, instance.session);\n });\n}\n","import type { WalkerOS, Elb } from '@elbwalker/types';\nimport { assign } from './assign';\nimport { pushToDestinations, createPushResult } from './destination';\nimport { onApply } from './on';\n\nexport function getGrantedConsent(\n required: WalkerOS.Consent | undefined,\n state: WalkerOS.Consent = {},\n individual: WalkerOS.Consent = {},\n): false | WalkerOS.Consent {\n // Merge state and individual, prioritizing individual states\n const states: WalkerOS.Consent = { ...state, ...individual };\n\n const grantedStates: WalkerOS.Consent = {};\n let hasRequiredConsent = required === undefined;\n\n Object.keys(states).forEach((name) => {\n if (states[name]) {\n // consent granted\n grantedStates[name] = true;\n\n // Check if it's required and granted consent\n if (required && required[name]) hasRequiredConsent = true;\n }\n });\n\n return hasRequiredConsent ? grantedStates : false;\n}\n\nexport async function setConsent(\n instance: WalkerOS.Instance,\n data: WalkerOS.Consent,\n): Promise<Elb.PushResult> {\n const { consent } = instance;\n\n let runQueue = false;\n const update: WalkerOS.Consent = {};\n Object.entries(data).forEach(([name, granted]) => {\n const state = !!granted;\n\n update[name] = state;\n\n // Only run queue if state was set to true\n runQueue = runQueue || state;\n });\n\n // Update consent state\n instance.consent = assign(consent, update);\n\n // Run on consent events\n onApply(instance, 'consent', undefined, update);\n\n // Process previous events if not disabled\n return runQueue\n ? pushToDestinations(instance)\n : createPushResult({ ok: true });\n}\n","import type { WalkerOS } from '@elbwalker/types';\nimport { assign } from './assign';\n\nexport function createEvent(\n props: WalkerOS.DeepPartialEvent = {},\n): WalkerOS.Event {\n const timestamp = props.timestamp || new Date().setHours(0, 13, 37, 0);\n const group = props.group || 'gr0up';\n const count = props.count || 1;\n const id = `${timestamp}-${group}-${count}`;\n\n const defaultEvent: WalkerOS.Event = {\n event: 'entity action',\n data: {\n string: 'foo',\n number: 1,\n boolean: true,\n array: [0, 'text', false],\n not: undefined,\n },\n context: { dev: ['test', 1] },\n globals: { lang: 'elb' },\n custom: { completely: 'random' },\n user: { id: 'us3r', device: 'c00k13', session: 's3ss10n' },\n nested: [\n {\n type: 'child',\n data: { is: 'subordinated' },\n nested: [],\n context: { element: ['child', 0] },\n },\n ],\n consent: { functional: true },\n id,\n trigger: 'test',\n entity: 'entity',\n action: 'action',\n timestamp,\n timing: 3.14,\n group,\n count,\n version: {\n source: '0.0.7',\n tagging: 1,\n },\n source: {\n type: 'web',\n id: 'https://localhost:80',\n previous_id: 'http://remotehost:9001',\n },\n };\n\n // Note: Always prefer the props over the defaults\n\n // Merge properties\n const event = assign(defaultEvent, props, { merge: false });\n\n // Update conditions\n\n // Entity and action from event\n if (props.event) {\n const [entity, action] = props.event.split(' ') ?? [];\n\n if (entity && action) {\n event.entity = entity;\n event.action = action;\n }\n }\n\n return event;\n}\n\nexport function getEvent(\n name: string = 'entity action',\n props: WalkerOS.DeepPartialEvent = {},\n): WalkerOS.Event {\n const timestamp = props.timestamp || new Date().setHours(0, 13, 37, 0);\n\n const quantity = 2;\n const product1 = {\n data: {\n id: 'ers',\n name: 'Everyday Ruck Snack',\n color: 'black',\n size: 'l',\n price: 420,\n },\n };\n const product2 = {\n data: {\n id: 'cc',\n name: 'Cool Cap',\n size: 'one size',\n price: 42,\n },\n };\n\n const defaultEvents: Record<string, WalkerOS.PartialEvent> = {\n 'cart view': {\n data: {\n currency: 'EUR',\n value: product1.data.price * quantity,\n },\n context: { shopping: ['cart', 0] },\n globals: { pagegroup: 'shop' },\n nested: [\n {\n type: 'product',\n data: { ...product1.data, quantity },\n context: { shopping: ['cart', 0] },\n nested: [],\n },\n ],\n trigger: 'load',\n },\n 'checkout view': {\n data: {\n step: 'payment',\n currency: 'EUR',\n value: product1.data.price + product2.data.price,\n },\n context: { shopping: ['checkout', 0] },\n globals: { pagegroup: 'shop' },\n nested: [\n {\n type: 'product',\n ...product1,\n context: { shopping: ['checkout', 0] },\n nested: [],\n },\n {\n type: 'product',\n ...product2,\n context: { shopping: ['checkout', 0] },\n nested: [],\n },\n ],\n trigger: 'load',\n },\n 'order complete': {\n data: {\n id: '0rd3r1d',\n currency: 'EUR',\n shipping: 5.22,\n taxes: 73.76,\n total: 555,\n },\n context: { shopping: ['complete', 0] },\n globals: { pagegroup: 'shop' },\n nested: [\n {\n type: 'product',\n ...product1,\n context: { shopping: ['complete', 0] },\n nested: [],\n },\n {\n type: 'product',\n ...product2,\n context: { shopping: ['complete', 0] },\n nested: [],\n },\n {\n type: 'gift',\n data: {\n name: 'Surprise',\n },\n context: { shopping: ['complete', 0] },\n nested: [],\n },\n ],\n trigger: 'load',\n },\n 'page view': {\n data: {\n domain: 'www.example.com',\n title: 'walkerOS documentation',\n referrer: 'https://www.elbwalker.com/',\n search: '?foo=bar',\n hash: '#hash',\n id: '/docs/',\n },\n globals: { pagegroup: 'docs' },\n trigger: 'load',\n },\n 'product add': {\n ...product1,\n context: { shopping: ['intent', 0] },\n globals: { pagegroup: 'shop' },\n nested: [],\n trigger: 'click',\n },\n 'product view': {\n ...product1,\n context: { shopping: ['detail', 0] },\n globals: { pagegroup: 'shop' },\n nested: [],\n trigger: 'load',\n },\n 'product visible': {\n data: { ...product1.data, position: 3, promo: true },\n context: { shopping: ['discover', 0] },\n globals: { pagegroup: 'shop' },\n nested: [],\n trigger: 'load',\n },\n 'promotion visible': {\n data: {\n name: 'Setting up tracking easily',\n position: 'hero',\n },\n context: { ab_test: ['engagement', 0] },\n globals: { pagegroup: 'homepage' },\n trigger: 'visible',\n },\n 'session start': {\n data: {\n id: 's3ss10n',\n start: timestamp,\n isNew: true,\n count: 1,\n runs: 1,\n isStart: true,\n storage: true,\n referrer: '',\n device: 'c00k13',\n },\n user: {\n id: 'us3r',\n device: 'c00k13',\n session: 's3ss10n',\n hash: 'h4sh',\n address: 'street number',\n email: 'user@example.com',\n phone: '+49 123 456 789',\n userAgent: 'Mozilla...',\n browser: 'Chrome',\n browserVersion: '90',\n deviceType: 'desktop',\n language: 'de-DE',\n country: 'DE',\n region: 'HH',\n city: 'Hamburg',\n zip: '20354',\n timezone: 'Berlin',\n os: 'walkerOS',\n osVersion: '1.0',\n screenSize: '1337x420',\n ip: '127.0.0.0',\n internal: true,\n custom: 'value',\n },\n },\n };\n\n return createEvent({ ...defaultEvents[name], ...props, event: name });\n}\n","import type { WalkerOS } from '@elbwalker/types';\nimport { assign } from './assign';\n\nexport interface MarketingParameters {\n [key: string]: string;\n}\n\nexport function getMarketingParameters(\n url: URL,\n custom: MarketingParameters = {},\n): WalkerOS.Properties {\n const clickId = 'clickId';\n const data: WalkerOS.Properties = {};\n const parameters: MarketingParameters = {\n utm_campaign: 'campaign',\n utm_content: 'content',\n utm_medium: 'medium',\n utm_source: 'source',\n utm_term: 'term',\n dclid: clickId,\n fbclid: clickId,\n gclid: clickId,\n msclkid: clickId,\n ttclid: clickId,\n twclid: clickId,\n igshid: clickId,\n sclid: clickId,\n };\n\n Object.entries(assign(parameters, custom)).forEach(([key, name]) => {\n const param = url.searchParams.get(key); // Search for the parameter in the URL\n if (param) {\n if (name === clickId) {\n name = key;\n data[clickId] = key; // Reference the clickId parameter\n }\n\n data[name] = param;\n }\n });\n\n return data;\n}\n","export function onLog(message: unknown, verbose = false): void {\n // eslint-disable-next-line no-console\n if (verbose) console.dir(message, { depth: 4 });\n}\n","import type { WalkerOS } from '@elbwalker/types';\nimport { tryCatch } from './tryCatch';\nimport { castValue } from './castValue';\nimport { isArray, isObject } from './is';\n\nexport function requestToData(\n parameter: unknown,\n): WalkerOS.AnyObject | undefined {\n const str = String(parameter);\n const queryString = str.split('?')[1] || str;\n\n return tryCatch(() => {\n const params = new URLSearchParams(queryString);\n const result: WalkerOS.AnyObject = {};\n\n params.forEach((value, key) => {\n const keys = key.split(/[[\\]]+/).filter(Boolean);\n let current: unknown = result;\n\n keys.forEach((k, i) => {\n const isLast = i === keys.length - 1;\n\n if (isArray(current)) {\n const index = parseInt(k, 10);\n if (isLast) {\n (current as Array<unknown>)[index] = castValue(value);\n } else {\n (current as Array<unknown>)[index] =\n (current as Array<unknown>)[index] ||\n (isNaN(parseInt(keys[i + 1], 10)) ? {} : []);\n current = (current as Array<unknown>)[index];\n }\n } else if (isObject(current)) {\n if (isLast) {\n (current as WalkerOS.AnyObject)[k] = castValue(value);\n } else {\n (current as WalkerOS.AnyObject)[k] =\n (current as WalkerOS.AnyObject)[k] ||\n (isNaN(parseInt(keys[i + 1], 10)) ? {} : []);\n current = (current as WalkerOS.AnyObject)[k];\n }\n }\n });\n });\n\n return result;\n })();\n}\n\nexport function requestToParameter(\n data: WalkerOS.AnyObject | WalkerOS.PropertyType,\n): string {\n if (!data) return '';\n\n const params: string[] = [];\n const encode = encodeURIComponent;\n\n function addParam(key: string, value: unknown) {\n if (value === undefined || value === null) return;\n\n if (isArray(value)) {\n value.forEach((item, index) => addParam(`${key}[${index}]`, item));\n } else if (isObject(value)) {\n Object.entries(value).forEach(([subKey, subValue]) =>\n addParam(`${key}[${subKey}]`, subValue),\n );\n } else {\n params.push(`${encode(key)}=${encode(String(value))}`);\n }\n }\n\n if (typeof data === 'object') {\n Object.entries(data).forEach(([key, value]) => addParam(key, value));\n } else {\n return encode(data);\n }\n\n return params.join('&');\n}\n","import type { WalkerOS } from '@elbwalker/types';\nimport { isSameType } from './is';\nimport { assign } from './assign';\n\nexport type SendDataValue = WalkerOS.Property | WalkerOS.Properties;\nexport type SendHeaders = { [key: string]: string };\n\nexport interface SendResponse {\n ok: boolean;\n data?: unknown;\n error?: string;\n}\n\nexport function transformData(data?: SendDataValue): string | undefined {\n if (data === undefined) return data;\n\n return isSameType(data, '' as string) ? data : JSON.stringify(data);\n}\n\nexport function getHeaders(headers: SendHeaders = {}): SendHeaders {\n return assign(\n {\n 'Content-Type': 'application/json; charset=utf-8',\n },\n headers,\n );\n}\n","export function throwError(error: unknown): never {\n throw new Error(String(error));\n}\n","export function trim(str: string): string {\n // Remove quotes and whitespaces\n return str ? str.trim().replace(/^'|'$/g, '').trim() : '';\n}\n","import type { WalkerOS } from '@elbwalker/types';\n\nexport function parseUserAgent(userAgent?: string): WalkerOS.User {\n if (!userAgent) return {};\n\n return {\n userAgent,\n browser: getBrowser(userAgent),\n browserVersion: getBrowserVersion(userAgent),\n os: getOS(userAgent),\n osVersion: getOSVersion(userAgent),\n deviceType: getDeviceType(userAgent),\n };\n}\n\nexport function getBrowser(userAgent: string): string | undefined {\n const browsers = [\n { name: 'Edge', substr: 'Edg' },\n { name: