import { Analytics } from '../../core/analytics' import { logger } from '../logger' import { NameValuePair } from './types' export const isValidEmail = (email: string) => { const re = /* eslint-disable-next-line no-useless-escape */ /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ return re.test(email) } /** * @description SHA-1 is a cryptographic hash function which takes an input and produces a 160-bit hash value known as a message digest – typically rendered as a hexadecimal number, 40 digits long. * @param input A string to hash */ export const sha1 = async (input: string): Promise => { const { subtle } = crypto const characters = input.split('') const bytes = new Uint8Array(input.length).map((_, index) => characters[index].charCodeAt(0) ) const algo = atob('U0hBLTE') // base64 of "SHA-1" const hashedInput = Array.from( new Uint8Array(await subtle.digest(algo, bytes)) ) .map((int) => ('00' + int.toString(16)).slice(-2)) .join('') logger.debug(`Input hashed into ${hashedInput}`) return hashedInput } export const getHashOrNull = async ( shouldHash: boolean, value: string ): Promise => { if (shouldHash) { return await sha1(value) } return null } const HAS_IDENTIFIED_KEY = 'dd_has_identified' /** * @description If `email` is found in analytics but no `id`, identify the user with that email. * @param shouldHash Boolean value that controls if a `sha1 digest` of the email is going to be used as an `id` */ export const identifyFromEmail = async ( analytics: Analytics, shouldHash: boolean ) => { const anonymousId = analytics.user().anonymousId() // Skip if we have already identified user with same anonymous id if (localStorage.getItem(HAS_IDENTIFIED_KEY) === anonymousId) { return } const id = analytics.user().id() if (id) { return } const email = analytics.user().traits()?.email const isEmail = email && isValidEmail(email) if (!isEmail) { return } const userId = await getHashOrNull(shouldHash, email) logger.debug('Identifying user from stored email due to missing id', { email, userId, }) // Identify cannot run before `analytics` has been properly initialized // or env enrichment (like setting library version) will not work /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ analytics.ready(() => analytics.identify(userId, { email }).catch((error) => { logger.error('Failed to identify user from existing data') logger.debugError(error) }) ) if (anonymousId) { localStorage.setItem(HAS_IDENTIFIED_KEY, anonymousId) } } export const getEmail = ( elements: NameValuePair[] | HTMLFormControlsCollection ): string | undefined => { const inputs = (Array.isArray(elements) ? elements : Array.from(elements)) as | HTMLInputElement[] | NameValuePair[] const emailInput = inputs.find((element: HTMLInputElement | NameValuePair) => isValidEmail(element.value) ) return emailInput?.value } interface IGetEventNameOptions { eventName?: string useFormIdAsEventName?: boolean useFormNameAsEventName?: boolean } /** * Determines the name to use for tracking form submissions. * * This function selects the track name based on provided options. If `eventName` is specified in the options, * it takes precedence. Otherwise, if `useFormNameAsEventName or useFormIdAsEventName` is true, the function attempts to use the form's `name` or `id` attribute. * Fallbacks to a default name if no name is found or specific conditions are not met. * * @param {HTMLFormElement} $form - The form element from which to derive the tracking name * @param {IAutoIdentifyOptions} options - Configuration options that influence the track name selection. * @returns {string} The determined track name for the form submission event. */ export const getEventName = ( $form: HTMLFormElement, options: IGetEventNameOptions ): string => { const defaultName = 'form-submit' if (options.eventName) { return options.eventName } if (options.useFormIdAsEventName) { const formNameFromIdAttribute = $form.getAttribute('id') if (!formNameFromIdAttribute) { logger.error( `'useFormIdAsEventName' is set to true but the form is missing an 'id' attribute. Defaulting back to name '${defaultName}'` ) return defaultName } return formNameFromIdAttribute } if (options.useFormNameAsEventName) { const formNameFromNameAttribute = $form.getAttribute('name') if (!formNameFromNameAttribute) { logger.error( `'useFormNameAsEventName' is set to true but the form is missing a 'name' attribute. Defaulting back to name '${defaultName}'` ) return defaultName } return formNameFromNameAttribute } return defaultName } export function eventIsTrusted(event: MessageEvent): boolean { return event.isTrusted }