import { isFormData, isNull, isNullish, hasOwnProperty, isArray, isString, isNumber } from '@posthog/core' import { Properties } from '../types' const breaker: Breaker = {} // eslint-disable-next-line @typescript-eslint/no-empty-object-type export type Breaker = {} export const ArrayProto = Array.prototype export const nativeForEach = ArrayProto.forEach export const nativeIndexOf = ArrayProto.indexOf const win: (Window & typeof globalThis) | undefined = typeof window !== 'undefined' ? window : undefined const global: typeof globalThis | undefined = typeof globalThis !== 'undefined' ? globalThis : win export const navigator = global?.navigator export const document = global?.document export const location = global?.location export const fetch = global?.fetch export const XMLHttpRequest = global?.XMLHttpRequest && 'withCredentials' in new global.XMLHttpRequest() ? global.XMLHttpRequest : undefined export const AbortController = global?.AbortController export const userAgent = navigator?.userAgent export { win as window } export function eachArray( obj: E[] | null | undefined, iterator: (value: E, key: number) => void | Breaker, thisArg?: any ): void { if (isArray(obj)) { if (nativeForEach && obj.forEach === nativeForEach) { obj.forEach(iterator, thisArg) } else if ('length' in obj && obj.length === +obj.length) { for (let i = 0, l = obj.length; i < l; i++) { if (i in obj && iterator.call(thisArg, obj[i], i) === breaker) { return } } } } } /** * @param {*=} obj * @param {function(...*)=} iterator * @param {Object=} thisArg */ export function each(obj: any, iterator: (value: any, key: any) => void | Breaker, thisArg?: any): void { if (isNullish(obj)) { return } if (isArray(obj)) { return eachArray(obj, iterator, thisArg) } if (isFormData(obj)) { for (const pair of obj.entries()) { if (iterator.call(thisArg, pair[1], pair[0]) === breaker) { return } } return } for (const key in obj) { if (hasOwnProperty.call(obj, key)) { if (iterator.call(thisArg, obj[key], key) === breaker) { return } } } } export const extend = function (obj: Record, ...args: Record[]): Record { eachArray(args, function (source) { for (const prop in source) { if (source[prop] !== void 0) { obj[prop] = source[prop] } } }) return obj } export const extendArray = function (obj: T[], ...args: T[][]): T[] { eachArray(args, function (source) { eachArray(source, function (item) { obj.push(item) }) }) return obj } export const include = function ( obj: null | string | Array | Record, target: any ): boolean | Breaker { let found = false if (isNull(obj)) { return found } if (nativeIndexOf && obj.indexOf === nativeIndexOf) { return obj.indexOf(target) != -1 } each(obj, function (value) { if (found || (found = value === target)) { return breaker } return }) return found } /** * Object.entries() polyfill * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries */ export function entries(obj: Record): [string, T][] { const ownProps = Object.keys(obj) let i = ownProps.length const resArray = new Array(i) // preallocate the Array while (i--) { resArray[i] = [ownProps[i], obj[ownProps[i]]] } return resArray } export function addEventListener( element: Window | Document | Element | undefined, event: string, callback: EventListener, options?: AddEventListenerOptions ): void { const { capture = false, passive = true } = options ?? {} // This is the only place where we are allowed to call this function // because the whole idea is that we should be calling this instead of the built-in one // eslint-disable-next-line posthog-js/no-add-event-listener element?.addEventListener(event, callback, { capture, passive }) } export const stripEmptyProperties = function (p: Properties): Properties { const ret: Properties = {} each(p, function (v, k) { if ((isString(v) && v.length > 0) || isNumber(v)) { ret[k] = v } }) return ret } const EXCLUDED_FROM_CROSS_SUBDOMAIN_COOKIE = ['herokuapp.com', 'vercel.app', 'netlify.app'] export function isCrossDomainCookie(documentLocation: Location | undefined) { const hostname = documentLocation?.hostname if (!isString(hostname)) { return false } // split and slice isn't a great way to match arbitrary domains, // but it's good enough for ensuring we only match herokuapp.com when it is the TLD // for the hostname const lastTwoParts = hostname.split('.').slice(-2).join('.') for (const excluded of EXCLUDED_FROM_CROSS_SUBDOMAIN_COOKIE) { if (lastTwoParts === excluded) { return false } } return true } /** * Deep copies an object. * It handles cycles by replacing all references to them with `undefined` * Also supports customizing native values * * @param value * @param customizer * @returns {{}|undefined|*} */ function deepCircularCopy = Record>( value: T, customizer?: (value: T[K], key?: K) => T[K] ): T | undefined { const COPY_IN_PROGRESS_SET = new Set() function internalDeepCircularCopy(value: T, key?: string): T | undefined { if (value !== Object(value)) return customizer ? customizer(value as any, key) : value // primitive value if (COPY_IN_PROGRESS_SET.has(value)) return undefined COPY_IN_PROGRESS_SET.add(value) let result: T if (isArray(value)) { result = [] as any as T eachArray(value, (it) => { result.push(internalDeepCircularCopy(it)) }) } else { result = {} as T each(value, (val, key) => { if (!COPY_IN_PROGRESS_SET.has(val)) { ;(result as any)[key] = internalDeepCircularCopy(val, key) } }) } return result } return internalDeepCircularCopy(value) } export function copyAndTruncateStrings = Record>( object: T, maxStringLength: number | null ): T { return deepCircularCopy(object, (value: any) => { if (isString(value) && !isNull(maxStringLength)) { return (value as string).slice(0, maxStringLength) } return value }) as T }