diff --git a/live-connect-js-6.0.3-alpha-8f0b28c.0.tgz b/live-connect-js-6.0.3-alpha-8f0b28c.0.tgz new file mode 100644 index 00000000..136fbb99 Binary files /dev/null and b/live-connect-js-6.0.3-alpha-8f0b28c.0.tgz differ diff --git a/package-lock.json b/package-lock.json index a184b42f..b227c479 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "6.0.3-alpha-8f0b28c.0", "license": "Apache-2.0", "dependencies": { - "live-connect-common": "^v3.0.1", + "live-connect-common": "^v3.0.2", "tiny-hashes": "1.0.1" }, "devDependencies": { @@ -14665,9 +14665,9 @@ "dev": true }, "node_modules/live-connect-common": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.1.tgz", - "integrity": "sha512-m8DS4GVonoV9dEbM+vFmDZXWK3PUu0+Ox5qGEnx6ar04MkC33MTb328PhgPNdmMyfz7C+hCgEsDgZmMZSl0+UA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.2.tgz", + "integrity": "sha512-K3LNKd9CpREDJbXGdwKqPojjQaxd4G6c7OAD6Yzp3wsCWTH2hV8xNAbUksSOpOcVyyOT9ilteEFXIJQJrbODxQ==", "engines": { "node": ">=18" } diff --git a/package.json b/package.json index 0e3490dc..ee6d9b35 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "release:ci:major": "release-it major --ci" }, "dependencies": { - "live-connect-common": "^v3.0.1", + "live-connect-common": "^v3.0.2", "tiny-hashes": "1.0.1" }, "devDependencies": { diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 00000000..a58d8200 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,128 @@ +import { strEqualsIgnoreCase, expiresInHours } from 'live-connect-common' +import { WrappedStorageHandler } from './handlers/storage-handler' +import { StorageStrategies, StorageStrategy } from './model/storage-strategy' + +export type CacheRecord = { + data: string + expiresAt?: Date +} + +export interface DurableCache { + get: (key: string) => CacheRecord | null // null is used to signal missing value + set: (key: string, value: string, expiration?: Date) => void +} + +export type StorageHandlerBackedCacheOpts = { + strategy: 'cookie' | 'ls', + storageHandler: WrappedStorageHandler, + domain: string, + defaultExpirationHours?: number +} + +export type MakeCacheOpts = StorageHandlerBackedCacheOpts & { + strategy: StorageStrategy, +} + +export function makeCache(opts: MakeCacheOpts): DurableCache { + if (!strEqualsIgnoreCase(opts.strategy, StorageStrategies.cookie) && strEqualsIgnoreCase(opts.strategy, StorageStrategies.none)) { + return NoOpCache + } else { + // TODO: Remove once we validate config properly + const strategyWithDefault = opts.strategy ?? StorageStrategies.cookie + return new StorageHandlerBackedCache({ ...opts, strategy: strategyWithDefault }) + } +} + +export class StorageHandlerBackedCache implements DurableCache { + private handler + private storageStrategy + private defaultExpirationHours? + private domain + + constructor (opts: StorageHandlerBackedCacheOpts) { + this.handler = opts.storageHandler + this.storageStrategy = opts.strategy + this.defaultExpirationHours = opts.defaultExpirationHours + this.domain = opts.domain + } + + private getCookieRecord(key: string): CacheRecord | null { + let expiresAt: Date | undefined + + const cookieExpirationEntry = this.handler.getCookie(expirationKey(key)) + if (cookieExpirationEntry && cookieExpirationEntry.length > 0) { + expiresAt = new Date(cookieExpirationEntry) + if (expiresAt <= new Date()) { + return null + } + } + + const data = this.handler.getCookie(key) + if (data) { + return { data, expiresAt } + } else { + return null + } + } + + private getLSRecord(key: string): CacheRecord | null { + let expiresAt: Date | undefined + const oldLsExpirationEntry = this.handler.getDataFromLocalStorage(expirationKey(key)) + + if (oldLsExpirationEntry) { + expiresAt = new Date(oldLsExpirationEntry) + if (expiresAt <= new Date()) { + this.handler.removeDataFromLocalStorage(key) + this.handler.removeDataFromLocalStorage(expirationKey(key)) + return null + } + } + + const data = this.handler.getDataFromLocalStorage(key) + if (data) { + return { data, expiresAt } + } else { + return null + } + } + + get(key: string): CacheRecord | null { + if (strEqualsIgnoreCase(this.storageStrategy, StorageStrategies.localStorage) && this.handler.localStorageIsEnabled()) { + return this.getLSRecord(key) + } else { + return this.getCookieRecord(key) + } + } + + set(key: string, value: string, expires?: Date): void { + if (!expires && this.defaultExpirationHours) { + expires = expiresInHours(this.defaultExpirationHours) + } + + if (strEqualsIgnoreCase(this.storageStrategy, StorageStrategies.localStorage) && this.handler.localStorageIsEnabled()) { + this.handler.setDataInLocalStorage(key, value) + if (expires) { + this.handler.setDataInLocalStorage(expirationKey(key), `${expires}`) + } else { + this.handler.removeDataFromLocalStorage(expirationKey(key)) + } + } else { + this.handler.setCookie(key, value, expires, 'Lax', this.domain) + if (expires) { + this.handler.setCookie(expirationKey(key), `${expires}`, expires, 'Lax', this.domain) + } else { + // sentinel value to indicate no expiration + this.handler.setCookie(expirationKey(key), '', undefined, 'Lax', this.domain) + } + } + } +} + +export const NoOpCache: DurableCache = { + get: () => null, + set: () => undefined +} + +function expirationKey(baseKey: string): string { + return `${baseKey}_exp` +} diff --git a/src/enrichers/people-verified.ts b/src/enrichers/people-verified.ts index 80bb3325..6a9f9c23 100644 --- a/src/enrichers/people-verified.ts +++ b/src/enrichers/people-verified.ts @@ -1,10 +1,10 @@ import { PEOPLE_VERIFIED_LS_ENTRY } from '../utils/consts' import { EventBus, State } from '../types' -import { WrappedReadOnlyStorageHandler } from '../handlers/storage-handler' +import { DurableCache } from '../cache' -export function enrich(state: State, storageHandler: WrappedReadOnlyStorageHandler, eventBus: EventBus): State { +export function enrich(state: State, cache: DurableCache, eventBus: EventBus): State { try { - return { peopleVerifiedId: state.peopleVerifiedId || storageHandler.getDataFromLocalStorage(PEOPLE_VERIFIED_LS_ENTRY) || undefined } + return { peopleVerifiedId: state.peopleVerifiedId || cache.get(PEOPLE_VERIFIED_LS_ENTRY)?.data || undefined } } catch (e) { eventBus.emitError('PeopleVerifiedEnrich', e) return {} diff --git a/src/handlers/storage-handler.ts b/src/handlers/storage-handler.ts index 282dfa4a..acdbc182 100644 --- a/src/handlers/storage-handler.ts +++ b/src/handlers/storage-handler.ts @@ -2,11 +2,6 @@ import { StorageStrategies, StorageStrategy } from '../model/storage-strategy' import { EventBus, ReadOnlyStorageHandler, StorageHandler, strEqualsIgnoreCase } from 'live-connect-common' import { WrappingContext } from '../utils/wrapping' -type StorageRecord = { - data: string - expiresAt?: Date -} - const noop = () => undefined function wrapRead(wrapper: WrappingContext, storageStrategy: StorageStrategy, functionName: K) { @@ -72,81 +67,6 @@ export class WrappedStorageHandler extends WrappedReadOnlyStorageHandler impleme return handler } - private getCookieRecord(key: string): StorageRecord | null { - let expiresAt: Date | undefined - - const cookieExpirationEntry = this.getCookie(expirationKey(key)) - if (cookieExpirationEntry && cookieExpirationEntry.length > 0) { - expiresAt = new Date(cookieExpirationEntry) - if (expiresAt <= new Date()) { - return null - } - } - - const data = this.getCookie(key) - if (data) { - return { data, expiresAt } - } else { - return null - } - } - - private getLSRecord(key: string): StorageRecord | null { - if (this.localStorageIsEnabled()) { - let expiresAt: Date | undefined - const oldLsExpirationEntry = this.getDataFromLocalStorage(expirationKey(key)) - - if (oldLsExpirationEntry) { - expiresAt = new Date(oldLsExpirationEntry) - if (expiresAt <= new Date()) { - this.removeDataFromLocalStorage(key) - this.removeDataFromLocalStorage(expirationKey(key)) - return null - } - } - - const data = this.getDataFromLocalStorage(key) - if (data) { - return { data, expiresAt } - } else { - return null - } - } else { - return null - } - } - - get(key: string): StorageRecord | null { - if (strEqualsIgnoreCase(this.storageStrategy, StorageStrategies.none) || strEqualsIgnoreCase(this.storageStrategy, StorageStrategies.disabled)) { - return null - } else if (strEqualsIgnoreCase(this.storageStrategy, StorageStrategies.localStorage)) { - return this.getLSRecord(key) - } else { - return this.getCookieRecord(key) - } - } - - set(key: string, value: string, expires?: Date, domain?: string): void { - if (strEqualsIgnoreCase(this.storageStrategy, StorageStrategies.none) || strEqualsIgnoreCase(this.storageStrategy, StorageStrategies.disabled)) { - // pass - } else if (strEqualsIgnoreCase(this.storageStrategy, StorageStrategies.localStorage) && this.localStorageIsEnabled()) { - this.setDataInLocalStorage(key, value) - if (expires) { - this.setDataInLocalStorage(expirationKey(key), `${expires}`) - } else { - this.removeDataFromLocalStorage(expirationKey(key)) - } - } else { - this.setCookie(key, value, expires, 'Lax', domain) - if (expires) { - this.setCookie(expirationKey(key), `${expires}`, expires, 'Lax', domain) - } else { - // sentinel value to indicate no expiration - this.setCookie(expirationKey(key), '', undefined, 'Lax', domain) - } - } - } - setCookie(key: string, value: string, expires?: Date, sameSite?: string, domain?: string): void { this.functions.setCookie(key, value, expires, sameSite, domain) } @@ -163,7 +83,3 @@ export class WrappedStorageHandler extends WrappedReadOnlyStorageHandler impleme return this.functions.findSimilarCookies(substring) || [] } } - -function expirationKey(baseKey: string): string { - return `${baseKey}_exp` -} diff --git a/src/manager/identifiers.ts b/src/manager/identifiers.ts index 7c01dcb6..bb5d67e8 100644 --- a/src/manager/identifiers.ts +++ b/src/manager/identifiers.ts @@ -5,72 +5,36 @@ import { expiresInDays } from 'live-connect-common' import { PEOPLE_VERIFIED_LS_ENTRY } from '../utils/consts' import { EventBus, State } from '../types' import { WrappedStorageHandler } from '../handlers/storage-handler' +import { DurableCache } from '../cache' const NEXT_GEN_FP_NAME = '_lc2_fpi' -const TLD_CACHE_KEY = '_li_dcdm_c' const DEFAULT_EXPIRATION_DAYS = 730 -export function resolve(state: State, storageHandler: WrappedStorageHandler, eventBus: EventBus): State { - try { - const determineTld = () => { - const cachedDomain = storageHandler.getCookie(TLD_CACHE_KEY) - if (cachedDomain) { - return cachedDomain - } - const domain = loadedDomain() - const arr = domain.split('.') - for (let i = arr.length; i > 0; i--) { - const newD = `.${arr.slice(i - 1, arr.length).join('.')}` - storageHandler.setCookie(TLD_CACHE_KEY, newD, undefined, 'Lax', newD) - if (storageHandler.getCookie(TLD_CACHE_KEY)) { - return newD - } - } - return `.${domain}` - } +export function resolve( + state: { expirationDays?: number, domain: string }, + storageHandler: WrappedStorageHandler, + cache: DurableCache, + eventBus: EventBus +): State { + const expiry = state.expirationDays || DEFAULT_EXPIRATION_DAYS + const oldValue = cache.get(NEXT_GEN_FP_NAME)?.data - const getOrAddWithExpiration = (key: string, value: string) => { - try { - const oldValue = storageHandler.get(key)?.data - const expiry = expiresInDays(storageOptions.expires) - if (oldValue) { - storageHandler.set(key, oldValue, expiry, storageOptions.domain) - } else { - storageHandler.set(key, value, expiry, storageOptions.domain) - } - return storageHandler.get(key)?.data - } catch (e) { - eventBus.emitErrorWithMessage('CookieLsGetOrAdd', 'Failed manipulating cookie jar or ls', e) - return null - } - } + if (oldValue) { + cache.set(NEXT_GEN_FP_NAME, oldValue, expiresInDays(expiry)) + } else { + const newValue = `${domainHash(state.domain)}--${ulid()}` - const generateCookie = (apexDomain: string) => { - const cookie = `${domainHash(apexDomain)}--${ulid()}` - return cookie.toLocaleLowerCase() - } + cache.set(NEXT_GEN_FP_NAME, newValue, expiresInDays(expiry)) + } + + const liveConnectIdentifier = cache.get(NEXT_GEN_FP_NAME)?.data || undefined - const expiry = state.expirationDays || DEFAULT_EXPIRATION_DAYS - const cookieDomain = determineTld() - const storageOptions = { - expires: expiry, - domain: cookieDomain - } - const liveConnectIdentifier = getOrAddWithExpiration( - NEXT_GEN_FP_NAME, - generateCookie(cookieDomain) - ) || undefined + if (liveConnectIdentifier) { + storageHandler.setDataInLocalStorage(PEOPLE_VERIFIED_LS_ENTRY, liveConnectIdentifier) + } - if (liveConnectIdentifier) { - storageHandler.setDataInLocalStorage(PEOPLE_VERIFIED_LS_ENTRY, liveConnectIdentifier) - } - return { - domain: cookieDomain, - liveConnectId: liveConnectIdentifier, - peopleVerifiedId: liveConnectIdentifier - } - } catch (e) { - eventBus.emitErrorWithMessage('IdentifiersResolve', 'Error while managing identifiers', e) - return {} + return { + liveConnectId: liveConnectIdentifier, + peopleVerifiedId: liveConnectIdentifier } } diff --git a/src/pixel/state.ts b/src/pixel/state.ts index 0b6ee90e..a17d668a 100644 --- a/src/pixel/state.ts +++ b/src/pixel/state.ts @@ -4,7 +4,7 @@ import { fiddle, mergeObjects } from './fiddler' import { isObject, trim, isArray, nonNull } from 'live-connect-common' import { asStringParam, asParamOrEmpty, asStringParamWhen, asStringParamTransform } from '../utils/params' import { toParams } from '../utils/url' -import { EventBus, State } from '../types' +import { Enricher, EventBus, State } from '../types' import { collectUrl } from './url-collector' const noOpEvents = ['setemail', 'setemailhash', 'sethashedemail'] @@ -108,6 +108,16 @@ export class StateWrapper { return new StateWrapper(mergeObjects(this.data, newInfo), this.eventBus) } + enrich(enrichers: Enricher[]): void { + enrichers.forEach((enricher) => { + try { + enricher(this.data) + } catch (e) { + this.eventBus.emitErrorWithMessage('StateEnrich', 'Error while enriching state', e) + } + }) + } + sendsPixel() { const source = isObject(this.data.eventSource) ? this.data.eventSource : {} const eventKeys = Object.keys(source) diff --git a/src/standard-live-connect.ts b/src/standard-live-connect.ts index 0309ca1d..fd1bcca2 100644 --- a/src/standard-live-connect.ts +++ b/src/standard-live-connect.ts @@ -18,6 +18,8 @@ import { WrappedCallHandler } from './handlers/call-handler' import { StorageStrategies } from './model/storage-strategy' import { ConfigMismatch, EventBus, ILiveConnect, LiveConnectConfig, State } from './types' import { LocalEventBus, getAvailableBus } from './events/event-bus' +import { determineHighestAccessibleDomain } from './utils/domain' +import { makeCache } from './cache' const hemStore: State = {} function _pushSingleEvent (event: any, pixelClient: PixelSender, enrichedState: StateWrapper, eventBus: EventBus) { @@ -83,12 +85,26 @@ function _getInitializedLiveConnect (liveConnectConfig: LiveConnectConfig): ILiv function _standardInitialization (liveConnectConfig: LiveConnectConfig, externalStorageHandler: StorageHandler, externalCallHandler: CallHandler, eventBus: EventBus): ILiveConnect { try { - const callHandler = new WrappedCallHandler(externalCallHandler, eventBus) + // TODO: proper config validation const validLiveConnectConfig = removeInvalidPairs(liveConnectConfig, eventBus) + + const callHandler = new WrappedCallHandler(externalCallHandler, eventBus) + const configWithPrivacy = mergeObjects(validLiveConnectConfig, privacyConfig(validLiveConnectConfig)) - errorHandler.register(configWithPrivacy, callHandler, eventBus) - const storageStrategy = configWithPrivacy.privacyMode ? StorageStrategies.disabled : configWithPrivacy.storageStrategy + const domain = determineHighestAccessibleDomain(storageHandler) + const configWithDomain = mergeObjects(configWithPrivacy, { domain: domain }) + + errorHandler.register(configWithDomain, callHandler, eventBus) + + const storageStrategy = configWithPrivacy.privacyMode ? StorageStrategies.disabled : (configWithPrivacy.storageStrategy || StorageStrategies.cookie) const storageHandler = WrappedStorageHandler.make(storageStrategy, externalStorageHandler, eventBus) + + const cache = makeCache({ + strategy: storageStrategy, + storageHandler: storageHandler, + domain: domain, + }) + const reducer = (accumulator, func) => accumulator.combineWith(func(accumulator.data, storageHandler, eventBus)) const enrichers = [pageEnrich, identifiersEnrich] @@ -102,7 +118,9 @@ function _standardInitialization (liveConnectConfig: LiveConnectConfig, external const onPixelPreload = () => eventBus.emit(C.PRELOAD_PIXEL, '0') const pixelClient = new PixelSender(configWithPrivacy, callHandler, eventBus, onPixelLoad, onPixelPreload) const resolver = IdentityResolver.make(postManagedState.data, storageHandler, callHandler, eventBus) + const _push = (...args: any[]) => _processArgs(args, pixelClient, postManagedState, eventBus) + return { push: _push, fire: () => _push({}), diff --git a/src/types.ts b/src/types.ts index 3660798f..9a8fcaeb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,8 @@ import { StorageStrategy } from './model/storage-strategy' import { UrlCollectionMode } from './model/url-collection-mode' import { ErrorDetails } from 'live-connect-common' +export type Enricher = (state: State) => void // should mutate state in-place + export interface IdentityResolutionConfig { url?: string expirationHours?: number diff --git a/src/utils/domain.ts b/src/utils/domain.ts new file mode 100644 index 00000000..0e9b5cdd --- /dev/null +++ b/src/utils/domain.ts @@ -0,0 +1,21 @@ +import { WrappedStorageHandler } from "../handlers/storage-handler" +import { loadedDomain } from "./page" + +const TLD_CACHE_KEY = '_li_dcdm_c' + +export function determineHighestAccessibleDomain(storageHandler: WrappedStorageHandler): string { + const cachedDomain = storageHandler.getCookie(TLD_CACHE_KEY) + if (cachedDomain) { + return cachedDomain + } + const domain = loadedDomain() + const arr = domain.split('.') + for (let i = arr.length; i > 0; i--) { + const newD = `.${arr.slice(i - 1, arr.length).join('.')}` + storageHandler.setCookie(TLD_CACHE_KEY, newD, undefined, 'Lax', newD) + if (storageHandler.getCookie(TLD_CACHE_KEY)) { + return newD + } + } + return `.${domain}` +} diff --git a/src/utils/wrapping.ts b/src/utils/wrapping.ts index 720ae633..d8015cf5 100644 --- a/src/utils/wrapping.ts +++ b/src/utils/wrapping.ts @@ -22,8 +22,14 @@ export class WrappingContext { if (isObject(this.obj)) { const member = this.obj[functionName] if (isFunction(member)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return member.bind(this.obj as any) as NonNullable + return ((...args) => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (member as any).call(this.obj, ...args) + } catch (e) { + this.eventBus.emitErrorWithMessage(this.name, `Failed calling ${functionName}`, e) + } + }) } } this.errors.push(functionName) diff --git a/tsconfig.json b/tsconfig.json index 476920af..14b3f468 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "declaration": true, "sourceMap": true, "moduleResolution": "Node", - "module": "ESNext" + "module": "ESNext", + "noEmitOnError": false, }, "include": ["./src/**/*", "./test/**/*"] }