From f2f3a0d786729587d70c59bbfb9419f8f518c54f Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 31 Oct 2024 16:13:25 +0100 Subject: [PATCH 1/6] [CM-1540] Add support for ip and useragent --- package-lock.json | 18 +-- package.json | 4 +- src/events/error-pixel.ts | 2 +- src/handlers/call-handler.ts | 7 +- src/idex.ts | 7 +- src/pixel/fiddler.ts | 70 ++++++---- src/pixel/sender.ts | 6 +- src/pixel/state.ts | 36 ++++-- src/standard-live-connect.ts | 16 ++- src/types.ts | 22 +++- test/unit/pixel/fiddler.spec.ts | 60 +++------ test/unit/pixel/sender.spec.ts | 51 ++++++-- test/unit/pixel/state.spec.ts | 221 ++++++++++++++++---------------- 13 files changed, 294 insertions(+), 226 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39f3a694..cdc33bfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "7.0.0", "license": "Apache-2.0", "dependencies": { - "live-connect-common": "^v4.0.0", + "live-connect-common": "^v4.1.0", "tiny-hashes": "1.0.1" }, "devDependencies": { @@ -57,7 +57,7 @@ "eslint-plugin-wdio": "^9.0.5", "express": "^4.19.2", "global-jsdom": "^25.0.0", - "live-connect-handlers": "^3.0.0", + "live-connect-handlers": "^3.1.0", "mocha": "^10.6.0", "mocha-junit-reporter": "^2.2.1", "release-it": "^17.4.1", @@ -11194,21 +11194,21 @@ } }, "node_modules/live-connect-common": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-4.0.0.tgz", - "integrity": "sha512-Nuv7h+G8txYaQhq58Jmq9PyP1jakQXOsDnUlmKM4PFDvzBfCKKldX4Xm7AIwt0kEtHJAEqNYp65C28tapsT7Ow==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-4.1.0.tgz", + "integrity": "sha512-sRklgbe13377aR+G0qCBiZPayQw5oZZozkuxKEoyipxscLbVzwe9gtA7CPpbmo6UcOdQxdCE6A7J1tI0wTSmqw==", "engines": { "node": ">=20" } }, "node_modules/live-connect-handlers": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/live-connect-handlers/-/live-connect-handlers-3.0.0.tgz", - "integrity": "sha512-T1D0P99h9+uvwpBN2AOcOF/tWybpXdAGUHh5hsjKmPxNziiHNf63/nzMBgmwts1SloxOnr6WSFlwVnJ1XTx1dA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/live-connect-handlers/-/live-connect-handlers-3.1.0.tgz", + "integrity": "sha512-AaMqGBAkr1PnR27Q3kN8BOkWga4SLZoTfOjYogeJTXnBP3UCrvHzuHH2nem4f4VO1PbLJlm82BKjwgue3mqwUA==", "dev": true, "dependencies": { "js-cookie": "^3.0.5", - "live-connect-common": "^4.0.0" + "live-connect-common": "^4.1.0" }, "engines": { "node": ">=20" diff --git a/package.json b/package.json index 8b474b59..4a32ab36 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "release:ci:major": "release-it major --ci" }, "dependencies": { - "live-connect-common": "^v4.0.0", + "live-connect-common": "^v4.1.0", "tiny-hashes": "1.0.1" }, "devDependencies": { @@ -109,7 +109,7 @@ "eslint-plugin-wdio": "^9.0.5", "express": "^4.19.2", "global-jsdom": "^25.0.0", - "live-connect-handlers": "^3.0.0", + "live-connect-handlers": "^3.1.0", "mocha": "^10.6.0", "mocha-junit-reporter": "^2.2.1", "release-it": "^17.4.1", diff --git a/src/events/error-pixel.ts b/src/events/error-pixel.ts index 2cab6320..2abd4c0b 100644 --- a/src/events/error-pixel.ts +++ b/src/events/error-pixel.ts @@ -52,7 +52,7 @@ export function asErrorDetails(e: unknown): { errorDetails: ErrorDetails } { export function register(state: State, sender: PixelSender, eventBus: EventBus): void { try { eventBus.on(ERRORS_CHANNEL, (error) => { - sender.sendPixel(new StateWrapper({ ...state, ...asErrorDetails(error) }, eventBus)) + sender.sendPixel(new StateWrapper({ ...state, ...asErrorDetails(error) }, {}, eventBus)) }) } catch (e) { console.error('handlers.error.register', e) diff --git a/src/handlers/call-handler.ts b/src/handlers/call-handler.ts index 1af32854..44b5c1fc 100644 --- a/src/handlers/call-handler.ts +++ b/src/handlers/call-handler.ts @@ -1,4 +1,4 @@ -import { EventBus, CallHandler } from 'live-connect-common' +import { EventBus, CallHandler, Headers } from 'live-connect-common' import { Wrapped, WrappingContext } from '../utils/wrapping.js' const empty = () => undefined @@ -28,9 +28,10 @@ export class WrappedCallHandler implements CallHandler { url: string, onSuccess: (responseText: string, response: unknown) => void, onError?: (error: unknown) => void, - timeout?: number + timeout?: number, + headers?: Headers ): void { - this.functions.ajaxGet(url, onSuccess, onError, timeout) + this.functions.ajaxGet(url, onSuccess, onError, timeout, headers) } pixelGet( diff --git a/src/idex.ts b/src/idex.ts index babc02f5..3b55f528 100644 --- a/src/idex.ts +++ b/src/idex.ts @@ -1,9 +1,10 @@ import { isFunction, isObject, isString, onNonNull } from 'live-connect-common' import { QueryBuilder, encodeIdCookie } from './utils/query.js' import { DEFAULT_IDEX_AJAX_TIMEOUT, DEFAULT_IDEX_URL, DEFAULT_REQUESTED_ATTRIBUTES } from './utils/consts.js' -import { IdentityResolutionConfig, State, ResolutionParams, EventBus, RetrievedIdentifier } from './types.js' +import { IdentityResolutionConfig, State, ResolutionParams, EventBus, RetrievedIdentifier, ExtraIdexAttributes } from './types.js' import { WrappedCallHandler } from './handlers/call-handler.js' import { stripQueryAndPath } from './pixel/url-collector.js' +import { base64UrlEncode } from './utils/b64.js' const ID_COOKIE_ATTR = 'idCookie' @@ -20,6 +21,7 @@ export class IdentityResolver { publisherId: number | string url: string timeout: number + extraAttributes: ExtraIdexAttributes requestedAttributes: string[] // Be careful, this object is mutable. In cases where temporary changes are needed // - e.g. adding parameters that are only valid for one single call - ensure to copy it @@ -40,6 +42,7 @@ export class IdentityResolver { this.eventBus = eventBus this.calls = calls this.idexConfig = nonNullConfig.identityResolutionConfig || {} + this.extraAttributes = this.idexConfig.extraAttributes || {} this.externalIds = nonNullConfig.retrievedIdentifiers || [] this.source = this.idexConfig.source || 'unknown' this.publisherId = this.idexConfig.publisherId || 'any' @@ -63,6 +66,8 @@ export class IdentityResolver { .addOptional('cd', nonNullConfig.cookieDomain) .addOptional('ic', encodeIdCookie(nonNullConfig.resolvedIdCookie), { stripEmpty: false }) .addOptional('pu', onNonNull(nonNullConfig.pageUrl, stripQueryAndPath)) + .addOptional('pip', onNonNull(this.extraAttributes.ipv4, v => base64UrlEncode(v))) + .addOptional('pip6', onNonNull(this.extraAttributes.ipv6, v => base64UrlEncode(v))) this.externalIds.forEach(retrievedIdentifier => { this.query.add(retrievedIdentifier.name, retrievedIdentifier.value) diff --git a/src/pixel/fiddler.ts b/src/pixel/fiddler.ts index dd87bd14..831feb4e 100644 --- a/src/pixel/fiddler.ts +++ b/src/pixel/fiddler.ts @@ -1,55 +1,77 @@ import { extractEmail } from '../utils/email.js' import { decodeValue } from '../utils/url.js' import { extractHashValue, hashEmail, isHash } from '../utils/hash.js' -import { isArray, isObject, safeToString, trim } from 'live-connect-common' -import { HashedEmail } from '../types.js' +import { isArray, isObject, isRecord, safeToString, trim } from 'live-connect-common' +import { FiddledExtraFields } from '../types.js' + +type AnyRecord = Record const MAX_ITEMS = 10 const LIMITING_KEYS = ['items', 'itemids'] const HASH_BEARERS = ['email', 'emailhash', 'hash', 'hashedemail'] -function provided }>(state: A): A & { hashedEmail?: string[] } { - const eventSource = state.eventSource || {} - const objectKeys = Object.keys(eventSource) - for (const key of objectKeys) { +function extractProvidedAttributes(eventSource: AnyRecord): FiddledExtraFields { + const extraFields: FiddledExtraFields = { eventSource } + + // add provided email hashes. Only consider the first one found. + for (const key of Object.keys(eventSource)) { const lowerCased = key.toLowerCase() if (HASH_BEARERS.indexOf(lowerCased) > -1) { - const value = trim(safeToString(eventSource[key as keyof (typeof eventSource)])) + const value = trim(safeToString(eventSource[key])) const extractedEmail = extractEmail(value) const extractedHash = extractHashValue(value) if (extractedEmail) { const hashes = hashEmail(decodeValue(extractedEmail)) - return mergeObjects({ hashedEmail: [hashes.md5, hashes.sha1, hashes.sha256] }, state) + extraFields.hashedEmail = [hashes.md5, hashes.sha1, hashes.sha256] + break } else if (extractedHash && isHash(extractedHash)) { - return mergeObjects({ hashedEmail: [extractedHash.toLowerCase()] }, state) + extraFields.hashedEmail = [extractedHash.toLowerCase()] + break } } } - return state + + // add provided user agent + if (typeof eventSource.userAgent === 'string') { + extraFields.providedUserAgent = eventSource.userAgent + } + + // add provided ip4 address + if (typeof eventSource.ipv4 === 'string') { + extraFields.providedIPV4 = eventSource.ipv4 + } + + // add provided ip6 address + if (typeof eventSource.ipv6 === 'string') { + extraFields.providedIPV6 = eventSource.ipv6 + } + + return extraFields } -function itemsLimiter(state: { eventSource?: Record }): Record { - const event = state.eventSource || {} +function limitItems(event: AnyRecord): AnyRecord { + const limitedEvent: AnyRecord = {} Object.keys(event).forEach(key => { const lowerCased = key.toLowerCase() - const value = event[key as keyof typeof event] as unknown + const value = event[key] if (LIMITING_KEYS.indexOf(lowerCased) > -1 && isArray(value) && value.length > MAX_ITEMS) { - value.length = MAX_ITEMS + limitedEvent[key] = value.slice(0, MAX_ITEMS) + } else { + limitedEvent[key] = value } }) - return {} + return limitedEvent } -const fiddlers = [provided, itemsLimiter] - -export function fiddle }>(state: A): A & { hashedEmail?: HashedEmail[] } { - function reducer(accumulator: A, func: (current: A) => B): A & B { - return mergeObjects(accumulator, func(accumulator)) - } - if (isObject(state.eventSource)) { - return fiddlers.reduce(reducer, state) +export function fiddle(event: object): FiddledExtraFields { + if (isRecord(event)) { + const extraAttributes = extractProvidedAttributes(event) + return { + ...extraAttributes, + eventSource: limitItems(event) + } } else { - return state + return {} } } diff --git a/src/pixel/sender.ts b/src/pixel/sender.ts index ee2407fa..8cfbfc3c 100644 --- a/src/pixel/sender.ts +++ b/src/pixel/sender.ts @@ -61,6 +61,9 @@ export class PixelSender { sendAjax(state: StateWrapper, opts: { onPreSend?: () => void, onLoad?: () => void } = {}): void { this.sendState(state, 'j', uri => { const go = (remainingRetries: number) => { + // additionally set headers extracted from the state only if the state is not empty + const headers = state.asHeaders() + this.calls.ajaxGet( uri, bakersJson => { @@ -75,7 +78,8 @@ export class PixelSender { go(remainingRetries - 1) } }, - this.timeout + this.timeout, + headers ) } diff --git a/src/pixel/state.ts b/src/pixel/state.ts index 2a557b3c..3769bc68 100644 --- a/src/pixel/state.ts +++ b/src/pixel/state.ts @@ -1,34 +1,38 @@ import { base64UrlEncode } from '../utils/b64.js' import { replacer } from './stringify.js' import { fiddle, mergeObjects } from './fiddler.js' -import { isObject, trim, isArray, nonNull, onNonNull } from 'live-connect-common' +import { isObject, trim, isArray, nonNull, onNonNull, Headers } from 'live-connect-common' import { QueryBuilder, encodeIdCookie } from '../utils/query.js' -import { EventBus, State } from '../types.js' +import { EventBus, FiddledState, State } from '../types.js' import { collectUrl } from './url-collector.js' const noOpEvents = ['setemail', 'setemailhash', 'sethashedemail'] export class StateWrapper { - data: State - eventBus: EventBus + data: FiddledState - constructor (state: State, eventBus: EventBus) { - this.data = StateWrapper.safeFiddle(state, eventBus) - this.eventBus = eventBus + constructor (state: State, eventSource: object, eventBus?: EventBus) { + this.data = StateWrapper.safeFiddle(state, eventSource, eventBus) } - private static safeFiddle(newInfo: State, eventBus: EventBus): State { + private static safeFiddle(state: State, eventSource: object, eventBus?: EventBus): FiddledState { try { - return fiddle(JSON.parse(JSON.stringify(newInfo))) + return mergeObjects(state, fiddle(JSON.parse(JSON.stringify(eventSource)))) } catch (e) { console.error(e) - eventBus.emitErrorWithMessage('StateCombineWith', 'Error while extracting event data', e) + if (eventBus != null) { + eventBus.emitErrorWithMessage('StateCombineWith', 'Error while extracting event data', e) + } return {} } } - combineWith(newInfo: Partial): StateWrapper { - return new StateWrapper(mergeObjects(this.data, newInfo), this.eventBus) + setHashedEmail(hashedEmail: string[]): void { + this.data.hashedEmail = hashedEmail + } + + getHashedEmail(): string[] { + return this.data.hashedEmail || [] } sendsPixel() { @@ -40,6 +44,12 @@ export class StateWrapper { return !eventName || noOpEvents.indexOf(eventName.toLowerCase()) === -1 } + asHeaders(): Headers { + return { + 'X-LI-Provided-User-Agent': this.data.providedUserAgent + } + } + asQuery(): QueryBuilder { const state = this.data @@ -82,6 +92,8 @@ export class StateWrapper { .addOptional('cd', state.cookieDomain) .addOptional('ic', encodeIdCookie(state.resolvedIdCookie), { stripEmpty: false }) .addOptional('c', state.contextElements) + .addOptional('pip', onNonNull(state.providedIPV4, v => base64UrlEncode(v))) + .addOptional('pip6', onNonNull(state.providedIPV6, v => base64UrlEncode(v))) return builder } diff --git a/src/standard-live-connect.ts b/src/standard-live-connect.ts index 77511c51..be3449d6 100644 --- a/src/standard-live-connect.ts +++ b/src/standard-live-connect.ts @@ -1,7 +1,6 @@ import { PixelSender } from './pixel/sender.js' import * as C from './utils/consts.js' import { StateWrapper } from './pixel/state.js' -import { mergeObjects } from './pixel/fiddler.js' import { enrichPage } from './enrichers/page.js' import { enrichIdentifiers } from './enrichers/identifiers.js' import { enrichPrivacyMode } from './enrichers/privacy-config.js' @@ -29,14 +28,19 @@ function pushSingleEvent(event: unknown, pixelClient: PixelSender, enrichedState } else if ('config' in event) { eventBus.emitErrorWithMessage('StrayConfig', 'Received a config after LC has already been initialised', new Error(JSON.stringify(event))) } else { - const wrapper = new StateWrapper(enrichedState, eventBus) - const combined = wrapper.combineWith({ eventSource: event }) - hemStore.hashedEmail = hemStore.hashedEmail || combined.data.hashedEmail - const withHemStore = mergeObjects({ eventSource: event }, hemStore) + // const stateWithEventSource: StateWithEventSource = { eventSource: event, ...enrichedState } + const wrapper = new StateWrapper(enrichedState, event, eventBus) + + // const combined = wrapper.combineWith({ eventSource: event }) + if (wrapper.getHashedEmail().length > 0) { + hemStore.hashedEmail = wrapper.getHashedEmail() + } else if (hemStore.hashedEmail) { + wrapper.setHashedEmail(hemStore.hashedEmail) + } const onPreSend = () => eventBus.emit(C.PRELOAD_PIXEL, '0') const onLoad = () => eventBus.emit(C.PIXEL_SENT_PREFIX, enrichedState) - pixelClient.sendAjax(wrapper.combineWith(withHemStore), { onPreSend, onLoad }) + pixelClient.sendAjax(wrapper, { onPreSend, onLoad }) } } diff --git a/src/types.ts b/src/types.ts index 818d6128..74935afe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,13 +8,19 @@ import { ErrorDetails } from 'live-connect-common' export type Enricher = (state: ActualIn) => ActualIn & Out -export interface IdentityResolutionConfig { +export type ExtraIdexAttributes = { + ipv4?: string + ipv6?: string +} + +export type IdentityResolutionConfig = { url?: string ajaxTimeout?: number source?: string publisherId?: number requestedAttributes?: string[], - idCookieMode?: 'generated' | 'provided' + idCookieMode?: 'generated' | 'provided', + extraAttributes?: ExtraIdexAttributes } export type IdCookieConfig = { @@ -73,7 +79,6 @@ export interface State extends LiveConnectConfig { peopleVerifiedId?: string errorDetails?: ErrorDetails retrievedIdentifiers?: RetrievedIdentifier[] - hashedEmail?: string[] providedHash?: string contextSelectors?: string contextElementsLength?: number @@ -81,9 +86,18 @@ export interface State extends LiveConnectConfig { privacyMode?: boolean referrer?: string cookieDomain?: string - resolvedIdCookie?: string | null // null signals failure to resolve + resolvedIdCookie?: string | null // null signals failure to resolve, +} + +export type FiddledExtraFields = { + hashedEmail?: string[] + providedIPV4?: string + providedIPV6?: string + providedUserAgent?: string, + eventSource?: Record } +export type FiddledState = State & FiddledExtraFields export interface ConfigMismatch { appId: (string | undefined)[] wrapperName: (string | undefined)[] diff --git a/test/unit/pixel/fiddler.spec.ts b/test/unit/pixel/fiddler.spec.ts index 7d480674..3e51b25b 100644 --- a/test/unit/pixel/fiddler.spec.ts +++ b/test/unit/pixel/fiddler.spec.ts @@ -7,35 +7,25 @@ use(dirtyChai) describe('Fiddler', () => { it('should use the providedHash if present', () => { - const pixelData = { - eventSource: { email: '75524519292E51AD6F761BAA82D07D76' } - } + const pixelData = { email: '75524519292E51AD6F761BAA82D07D76' } const result = fiddle(pixelData) expect(result.hashedEmail).to.eql(['75524519292e51ad6f761baa82d07d76']) }) it('should use the providedHash if present, ignoring the case of the key', () => { - const pixelData = { - eventSource: { eMaIl: '75524519292E51AD6F761BAA82D07D76' } - } + const pixelData = { eMaIl: '75524519292E51AD6F761BAA82D07D76' } const result = fiddle(pixelData) expect(result.hashedEmail).to.eql(['75524519292e51ad6f761baa82d07d76']) }) it('should ignore the providedHash if it is not a valid hash', () => { - const pixelData = { - eventSource: { email: '75524519292e51ad6f761baa82d07d76aa' } - } + const pixelData = { email: '75524519292e51ad6f761baa82d07d76aa' } const result = fiddle(pixelData) expect(result.hashedEmail).to.eql(undefined) }) it('should work while ignoring whitespace ', () => { - const pixelData = { - eventSource: { - email: ' 75524519292E51AD6F761BAA82D07D76' - } - } + const pixelData = { email: ' 75524519292E51AD6F761BAA82D07D76' } const result = fiddle(pixelData) expect(result.hashedEmail).to.eql(['75524519292e51ad6f761baa82d07d76']) }) @@ -43,11 +33,9 @@ describe('Fiddler', () => { it('should use the first found hash, even if there are multiple HASH_BEARERS provided', () => { const hashes = hashEmail('mookie@gmail.com') const pixelData = { - eventSource: { - email: hashes.md5, - hash: hashes.sha1, - hashedemail: hashes.sha256 - } + email: hashes.md5, + hash: hashes.sha1, + hashedemail: hashes.sha256 } const result = fiddle(pixelData) expect(result.hashedEmail).to.eql([hashes.md5]) @@ -55,10 +43,8 @@ describe('Fiddler', () => { it('should hash the plain text email if the providedHash if it is not a valid hash', () => { const pixelData = { - eventSource: { - emailHash: '75524519292e51ad6f761baa82d07d76aa', - email: 'mookie@gmail.com' - } + emailHash: '75524519292e51ad6f761baa82d07d76aa', + email: 'mookie@gmail.com' } const result = fiddle(pixelData) const hashes = hashEmail('mookie@gmail.com') @@ -67,9 +53,7 @@ describe('Fiddler', () => { it('should hash the plain text url encoded email as if it contained an @ symbol', () => { const pixelData = { - eventSource: { - email: 'mookie%40gmail.com' - } + email: 'mookie%40gmail.com' } const result = fiddle(pixelData) const hashes = hashEmail('mookie@gmail.com') @@ -78,26 +62,22 @@ describe('Fiddler', () => { it('should hash the plain text url encoded email as if it contained an @ symbol, ignoring the surrounding mess or case', () => { const pixelData = { - eventSource: { - email: '" mOOkie%40gmail.com "' - } + email: '" mOOkie%40gmail.com "' } const result = fiddle(pixelData) const hashes = hashEmail('mookie@gmail.com') expect(result.hashedEmail).to.eql([hashes.md5, hashes.sha1, hashes.sha256]) }) - it('should limit the number of items in the items array, regardless of the case', () => { - expect(fiddle({ - eventSource: { - iTeMs: Array.from(Array(50).keys()) - } - }).eventSource.iTeMs).to.eql(Array.from(Array(10).keys())) + it('should limit the number of items in the items array, regardless of the case - 1', () => { + const event = { iTeMs: Array.from(Array(50).keys()) } + const result = fiddle(event) + expect(result.eventSource!.iTeMs).to.eql(Array.from(Array(10).keys())) + }) - expect(fiddle({ - eventSource: { - iTeMIdS: Array.from(Array(20).keys()) - } - }).eventSource.iTeMIdS).to.eql(Array.from(Array(10).keys())) + it('should limit the number of items in the items array, regardless of the case - 2', () => { + const event = { iTeMIdS: Array.from(Array(20).keys()) } + const result = fiddle(event) + expect(result.eventSource!.iTeMIdS).to.eql(Array.from(Array(10).keys())) }) }) diff --git a/test/unit/pixel/sender.spec.ts b/test/unit/pixel/sender.spec.ts index 162fca0f..81e29c06 100644 --- a/test/unit/pixel/sender.spec.ts +++ b/test/unit/pixel/sender.spec.ts @@ -50,6 +50,15 @@ describe('PixelSender', () => { pixelStub.restore() }) + const stubbedStateWrapper: StateWrapper = { + data: {}, + asQuery: () => new QueryBuilder([['xxx', 'yyy']]), + sendsPixel: () => true, + asHeaders: () => ({}), + setHashedEmail: () => {}, + getHashedEmail: () => [] + } + it('exposes the send and sendPixel functions', () => { const sender = new PixelSender({ collectorUrl: 'http://localhost', callHandler: calls, eventBus }) expect(typeof sender.sendAjax).to.eql('function') @@ -62,7 +71,7 @@ describe('PixelSender', () => { done() } const sender = new PixelSender({ callHandler: calls, eventBus }) - sender.sendAjax({ asQuery: () => new QueryBuilder([['xxx', 'yyy']]), sendsPixel: () => true } as StateWrapper, { onLoad: successCallback }) + sender.sendAjax(stubbedStateWrapper, { onLoad: successCallback }) ajaxRequests[0].respond(200, { 'Content-Type': 'application/json' }, '{}') }) @@ -72,7 +81,7 @@ describe('PixelSender', () => { done() } const sender = new PixelSender({ collectorUrl: 'http://localhost', callHandler: calls, eventBus }) - sender.sendAjax({ asQuery: () => new QueryBuilder([['xxx', 'yyy']]), sendsPixel: () => true } as StateWrapper, { onLoad: successCallback }) + sender.sendAjax(stubbedStateWrapper, { onLoad: successCallback }) ajaxRequests[0].respond(200, { 'Content-Type': 'application/json' }, '{}') }) @@ -82,7 +91,16 @@ describe('PixelSender', () => { done() } const sender = new PixelSender({ collectorUrl: 'http://localhost', callHandler: calls, eventBus }) - sender.sendAjax({ asQuery: () => new QueryBuilder([['xxx', 'yyy'], ['gdpr', '1']]), sendsPixel: () => true } as StateWrapper, { onLoad: successCallback }) + const stubbedStateWrapper1: StateWrapper = { + data: stubbedStateWrapper.data, + asQuery: () => new QueryBuilder([['xxx', 'yyy'], ['gdpr', '1']]), + sendsPixel: stubbedStateWrapper.sendsPixel, + asHeaders: stubbedStateWrapper.asHeaders, + setHashedEmail: stubbedStateWrapper.setHashedEmail, + getHashedEmail: stubbedStateWrapper.getHashedEmail + } + + sender.sendAjax(stubbedStateWrapper1, { onLoad: successCallback }) ajaxRequests[0].respond(200, { 'Content-Type': 'application/json' }, '{}') }) @@ -101,7 +119,7 @@ describe('PixelSender', () => { }) const sender = new PixelSender({ callHandler: calls, eventBus }) - sender.sendAjax({ asQuery: () => new QueryBuilder([['xxx', 'yyy']]), sendsPixel: () => true } as StateWrapper) + sender.sendAjax(stubbedStateWrapper) ajaxRequests[0].respond(200, { 'Content-Type': 'application/json' }, '{ "bakers": ["https://baker1.com/baker", "https://baker2.com/baker"]}') }) @@ -112,7 +130,7 @@ describe('PixelSender', () => { }) const sender = new PixelSender({ callHandler: calls, eventBus }) - sender.sendAjax({ asQuery: () => new QueryBuilder([['xxx', 'yyy']]), sendsPixel: () => true } as StateWrapper) + sender.sendAjax(stubbedStateWrapper) ajaxRequests[0].respond(200, { 'Content-Type': 'application/json' }, '{kaiserschmarrn}') }) @@ -126,7 +144,7 @@ describe('PixelSender', () => { }) const sender = new PixelSender({ callHandler: calls, eventBus, ajaxRetries: 0 }) - sender.sendAjax({ asQuery: () => new QueryBuilder([['xxx', 'yyy']]), sendsPixel: () => true } as StateWrapper, { onLoad: onload }) + sender.sendAjax(stubbedStateWrapper, { onLoad: onload }) ajaxRequests[0].respond(500, { 'Content-Type': 'application/json' }, '{kaiserschmarrn}') }) @@ -140,7 +158,7 @@ describe('PixelSender', () => { }) const sender = new PixelSender({ callHandler: calls, eventBus, ajaxRetries: 1 }) - sender.sendAjax({ asQuery: () => new QueryBuilder([['xxx', 'yyy']]), sendsPixel: () => true } as StateWrapper, { onLoad: onload }) + sender.sendAjax(stubbedStateWrapper, { onLoad: onload }) ajaxRequests[0].respond(500, { 'Content-Type': 'application/json' }, '{kaiserschmarrn}') ajaxRequests[1].respond(500, { 'Content-Type': 'application/json' }, '{kaiserschmarrn}') }) @@ -151,7 +169,7 @@ describe('PixelSender', () => { done() } const sender = new PixelSender({ callHandler: calls, eventBus }) - sender.sendAjax({ asQuery: () => new QueryBuilder([['xxx', 'yyy']]), sendsPixel: () => true } as StateWrapper, { onLoad: successCallback }) + sender.sendAjax(stubbedStateWrapper, { onLoad: successCallback }) ajaxRequests[0].respond(200, { 'Content-Type': 'application/json' }, '{}') }) @@ -161,12 +179,12 @@ describe('PixelSender', () => { done() } const sender = new PixelSender({ callHandler: calls, eventBus }) - sender.sendAjax({ asQuery: () => new QueryBuilder([['xxx', 'yyy']]), sendsPixel: () => true } as StateWrapper, { onPreSend: presend }) + sender.sendAjax(stubbedStateWrapper, { onPreSend: presend }) }) it('defaults to production if none set when sendPixel', () => { const sender = new PixelSender({ callHandler: calls, eventBus }) - sender.sendPixel({ asQuery: () => new QueryBuilder([['xxx', 'yyy']]), sendsPixel: () => true } as StateWrapper) + sender.sendPixel(stubbedStateWrapper) expect(pixelRequests[0].uri).to.match(/https:\/\/rp.liadm.com\/p\?dtstmp=\d+&xxx=yyy/) expect(pixelRequests[0].onload).to.be.undefined() }) @@ -174,14 +192,23 @@ describe('PixelSender', () => { it('sends an image pixel and call onload if request succeeds when sendPixel', () => { const onload = () => 1 const sender = new PixelSender({ collectorUrl: 'http://localhost', callHandler: calls, eventBus }) - sender.sendPixel({ asQuery: () => new QueryBuilder([['xxx', 'yyy']]), sendsPixel: () => true } as StateWrapper, { onLoad: onload }) + sender.sendPixel(stubbedStateWrapper, { onLoad: onload }) expect(pixelRequests[0].uri).to.match(/http:\/\/localhost\/p\?dtstmp=\d+&xxx=yyy/) expect(pixelRequests[0].onload).to.eql(onload) }) it('does not send an image pixel if sendsPixel resolves to false when sendPixel', () => { const sender = new PixelSender({ collectorUrl: 'http://localhost', callHandler: calls, eventBus }) - sender.sendPixel({ asQuery: () => new QueryBuilder([['zzz', 'ccc']]), sendsPixel: () => false } as StateWrapper) + + const stubbedStateWrapper1: StateWrapper = { + data: stubbedStateWrapper.data, + asQuery: () => new QueryBuilder([['zzz', 'ccc']]), + sendsPixel: () => false, + asHeaders: stubbedStateWrapper.asHeaders, + setHashedEmail: stubbedStateWrapper.setHashedEmail, + getHashedEmail: stubbedStateWrapper.getHashedEmail + } + sender.sendPixel(stubbedStateWrapper1) expect(pixelRequests).to.be.empty() }) }) diff --git a/test/unit/pixel/state.spec.ts b/test/unit/pixel/state.spec.ts index 0bb534ac..da3cfe06 100644 --- a/test/unit/pixel/state.spec.ts +++ b/test/unit/pixel/state.spec.ts @@ -15,27 +15,26 @@ const COMMA = encodeURIComponent(',') describe('EventComposition', () => { it('should construct an event out of anything', () => { const pixelData = { appId: '9898' } - const event = new StateWrapper(pixelData) - expect(event.data).to.eql(pixelData) + const event = new StateWrapper(pixelData, {}) + expect(event.data).to.eql({ appId: '9898', eventSource: {} }) }) it('should construct valid params for valid members', () => { const pixelData = { appId: '9898' } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql('?aid=9898') + const event = new StateWrapper(pixelData, {}) + expect(event.asQuery().toQueryString()).to.eql('?aid=9898&se=e30') }) it('should ignore empty fields', () => { const pixelData = { appId: '9898', contextElements: '' } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql('?aid=9898') + const event = new StateWrapper(pixelData, {}) + expect(event.asQuery().toQueryString()).to.eql('?aid=9898&se=e30') }) it('should append c parameter last', () => { const pixelData: State = { contextElements: 'This title is a test', appId: '9898', - eventSource: { eventName: 'viewContent' }, liveConnectId: '213245', trackerVersion: 'test tracker', pageUrl: 'https://wwww.example.com?sss', @@ -61,7 +60,7 @@ describe('EventComposition', () => { gppApplicableSections: [1, 2, 3], cookieDomain: 'test-cookie-domain' } - const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData))) + const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) const expectedPairs = [ 'aid=9898', // appId @@ -92,7 +91,6 @@ describe('EventComposition', () => { const pixelData = { contextElements: 'This title is a test', appId: '9898', - eventSource: { eventName: 'viewContent' }, liveConnectId: '213245', trackerVersion: 'test tracker', pageUrl: 'https://wwww.example.com?sss', @@ -114,7 +112,7 @@ describe('EventComposition', () => { gdprConsent: 'test-consent-string', referrer: 'https://some.test.referrer.com' } - const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData))) + const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) const expectedPairs = [ 'aid=9898', // appId @@ -142,17 +140,16 @@ describe('EventComposition', () => { appId: '9898', randomField: 2135523 } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql('?aid=9898') + const event = new StateWrapper(pixelData, {}) + expect(event.asQuery().toQueryString()).to.eql('?aid=9898&se=e30') }) it('should base64 the source', () => { const pixelData = { - appId: '9898', - eventSource: { eventName: 'viewContent' } + appId: '9898' } const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCJ9' - const event = new StateWrapper(pixelData) + const event = new StateWrapper(pixelData, { eventName: 'viewContent' }) expect(event.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}`) }) @@ -161,8 +158,9 @@ describe('EventComposition', () => { gppString: 'test-gpp-string', gppApplicableSections: [1, 2, 3] } - const event = new StateWrapper(pixelData) + const event = new StateWrapper(pixelData, {}) const expectedPairs = [ + 'se=e30', // eventSource 'gpp_s=test-gpp-string', // GPP string 'gpp_as=1%2C2%2C3' // GPP applicable sections ] @@ -173,28 +171,26 @@ describe('EventComposition', () => { const pixelData = { usPrivacyString: '1---' } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql('?us_privacy=1---') + const event = new StateWrapper(pixelData, {}) + expect(event.asQuery().toQueryString()).to.eql('?se=e30&us_privacy=1---') }) it('should send gdpr as 1 & gdprConsent', () => { const pixelData = { - eventSource: { eventName: 'viewContent' }, gdprApplies: true, gdprConsent: 'some-string' } - const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData))) + const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCJ9' expect(event.asQuery().toQueryString()).to.eql(`?se=${b64EncodedEventSource}&gdpr=1&gdpr_consent=some-string`) }) it('should send gdpr 0 if gdprApplies is false', () => { const pixelData = { - eventSource: { eventName: 'viewContent' }, gdprApplies: false, gdprConsent: 'some-string' } - const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData))) + const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCJ9' expect(event.asQuery().toQueryString()).to.eql(`?se=${b64EncodedEventSource}&gdpr=0&gdpr_consent=some-string`) }) @@ -202,20 +198,20 @@ describe('EventComposition', () => { it('should send the tracker name', () => { const trackerVersion = 'some-name' const pixelData = { trackerVersion } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql(`?tv=${trackerVersion}`) + const event = new StateWrapper(pixelData, {}) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&tv=${trackerVersion}`) }) it('should ignore nullable fields', () => { - const event = new StateWrapper({}) - expect(event.asQuery().toQueryString()).to.eql('') + const event = new StateWrapper({}, {}) + expect(event.asQuery().toQueryString()).to.eql('?se=e30') }) it('should send the page url', () => { const pageUrl = 'https://wwww.example.com?sss' const pixelData = { pageUrl } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql(`?pu=${encodeURIComponent(pageUrl)}`) + const event = new StateWrapper(pixelData, {}) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&pu=${encodeURIComponent(pageUrl)}`) }) it('should send the removed parts of the page url', () => { @@ -225,9 +221,9 @@ describe('EventComposition', () => { urlCollectionMode: UrlCollectionModes.noPath, queryParametersFilter: '^(foo|bar)$' } - const event = new StateWrapper(pixelData) + const event = new StateWrapper(pixelData, {}) const expectedUrl = 'https://www.example.com/?query=v1&id=v4' - expect(event.asQuery().toQueryString()).to.eql(`?pu=${encodeURIComponent(expectedUrl)}&pu_rp=1&pu_rqp=foo${COMMA}bar`) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&pu=${encodeURIComponent(expectedUrl)}&pu_rp=1&pu_rqp=foo${COMMA}bar`) }) it('should not send the removed parts of the page url when nothing was removed', () => { @@ -237,8 +233,8 @@ describe('EventComposition', () => { urlCollectionMode: UrlCollectionModes.noPath, queryParametersFilter: '^(foo|bar)$' } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql(`?pu=${encodeURIComponent(pageUrl)}`) + const event = new StateWrapper(pixelData, {}) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&pu=${encodeURIComponent(pageUrl)}`) }) it('should send the application error', () => { @@ -246,59 +242,53 @@ describe('EventComposition', () => { const pixelData = { errorDetails: applicationError } - const event = new StateWrapper(pixelData) + const event = new StateWrapper(pixelData, {}) const b64EncodedEventSource = 'eyJzb21lS2V5IjoidmFsdWUifQ' - expect(event.asQuery().toQueryString()).to.eql(`?ae=${b64EncodedEventSource}`) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&ae=${b64EncodedEventSource}`) }) it('should update the data', () => { const pixelData = { - appId: '9898', - eventSource: { eventName: 'viewContent' } - } - const expectedData = { - appId: '9898', - eventSource: { eventName: 'viewContent' }, - liveConnectId: '213245' + appId: '9898' } - const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCJ9' - const event = new StateWrapper(pixelData) - - const newEvent = event.combineWith({ liveConnectId: '213245' }) + const event = new StateWrapper(pixelData, { eventName: 'viewContent' }) - expect(newEvent.data).to.eql(expectedData) - expect(newEvent.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}&duid=213245`) + event.setHashedEmail(['foo']) - expect(event.data).to.eql(pixelData) - expect(event.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}`) + expect(event.data).to.eql({ + appId: '9898', + eventSource: { eventName: 'viewContent' }, + hashedEmail: ['foo'] + }) }) it('should send the provided email hash', () => { const pixelData = { - appId: '9898', - eventSource: { - eventName: 'viewContent', - email: ' e168e0eda11f4fbb8fbd7cfe5f750cd0f7e7f4d8649da68e073e927504ec5d72 ' - } + appId: '9898' + } + + const event = { + eventName: 'viewContent', + email: ' e168e0eda11f4fbb8fbd7cfe5f750cd0f7e7f4d8649da68e073e927504ec5d72 ' } const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCIsImVtYWlsIjoiICBlMTY4ZTBlZGExMWY0ZmJiOGZiZDdjZmU1Zjc1MGNkMGY3ZTdmNGQ4NjQ5ZGE2OGUwNzNlOTI3NTA0ZWM1ZDcyICAgICJ9' - const event = new StateWrapper(pixelData) + const event = new StateWrapper(pixelData, event) expect(event.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}&e=e168e0eda11f4fbb8fbd7cfe5f750cd0f7e7f4d8649da68e073e927504ec5d72`) }) it('should never send emails as plain text, and hash the email that is set in the source', () => { const pixelData = { - appId: '9898', - eventSource: { - eventName: 'viewContent', - email: ' xxx@yyy.com' - } + appId: '9898' + } + const event = { + eventName: 'viewContent', + email: ' xxx@yyy.com' } const hashes = hashEmail('xxx@yyy.com') const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCIsImVtYWlsIjoiKioqKioqKioqIn0' - const event = new StateWrapper(pixelData) + const event = new StateWrapper(pixelData, event) expect(event.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}&e=${hashes.md5}%2C${hashes.sha1}%2C${hashes.sha256}`) }) @@ -315,9 +305,9 @@ describe('EventComposition', () => { retrievedIdentifiers: [cookie1, cookie2] } - const event = new StateWrapper(pixelData) + const event = new StateWrapper(pixelData, {}) - expect(event.asQuery().toQueryString()).to.eql(`?ext_${cookie1.name}=${cookie1.value}&ext_${cookie2.name}=${cookie2.value}`) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&ext_${cookie1.name}=${cookie1.value}&ext_${cookie2.name}=${cookie2.value}`) }) it('should send the hashes found in retrieved identifiers', () => { @@ -335,68 +325,67 @@ describe('EventComposition', () => { hashesFromIdentifiers: [hashes1, hashes2] } - const event = new StateWrapper(pixelData) + const event = new StateWrapper(pixelData, {}) - expect(event.asQuery().toQueryString()).to.eql(`?scre=${hashes1.md5}${COMMA}${hashes1.sha1}${COMMA}${hashes1.sha256}&scre=${hashes2.md5}${COMMA}${hashes2.sha1}${COMMA}${hashes2.sha256}`) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&scre=${hashes1.md5}${COMMA}${hashes1.sha1}${COMMA}${hashes1.sha256}&scre=${hashes2.md5}${COMMA}${hashes2.sha1}${COMMA}${hashes2.sha256}`) }) it('should send decisionIds ', () => { const pixelData = { decisionIds: ['1', '2'] } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql(`?li_did=1${COMMA}2`) + const event = new StateWrapper(pixelData, {}) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&li_did=1${COMMA}2`) }) it('should not send decisionIds if array is empty', () => { const pixelData = { decisionIds: [] } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql('') + const event = new StateWrapper(pixelData, {}) + expect(event.asQuery().toQueryString()).to.eql('?se=e30') }) it('should not send an event if the event is just setting a HEM', () => { - expect(new StateWrapper({ - eventSource: { - eventName: 'setEmail', - email: ' xxx@yyy.com' - } - }).sendsPixel()).to.be.false() - - expect(new StateWrapper({ - eventSource: { - eventName: 'setEmailHash', - email: ' xxx@yyy.com' - } - }).sendsPixel()).to.be.false() - - expect(new StateWrapper({ - eventSource: { - eventName: 'setHashedEmail', - email: ' xxx@yyy.com' - } - }).sendsPixel()).to.be.false() - - expect(new StateWrapper({ - eventSource: { - eventName: 'setContent', - email: ' xxx@yyy.com' - } - }).sendsPixel()).to.be.true() + const event1 = { + eventName: 'setEmail', + email: ' xxx@yyy.com' + } + + expect(new StateWrapper({}, event1).sendsPixel()).to.be.false() + + const event2 = { + eventName: 'setEmailHash', + email: ' xxx@yyy.com' + } + + expect(new StateWrapper({}, event2).sendsPixel()).to.be.false() + + const event3 = { + eventName: 'setHashedEmail', + email: ' xxx@yyy.com' + } + + expect(new StateWrapper({}, event3).sendsPixel()).to.be.false() + + const event4 = { + eventName: 'setContent', + email: ' xxx@yyy.com' + } + + expect(new StateWrapper({}, event4).sendsPixel()).to.be.true() }) it('should limit the number of items', () => { const pixelData = { decisionIds: [] } - const event = new StateWrapper(pixelData) - const eventWithItems = event.combineWith({ - eventSource: { items: Array.from(Array(50).keys()) } - }) - expect(eventWithItems.asQuery().toQueryString()).to.eql('?se=eyJpdGVtcyI6WzAsMSwyLDMsNCw1LDYsNyw4LDldfQ') + const providedItems = Array.from(Array(50).keys()) + const providedItemsCopy = [...providedItems] + const event = new StateWrapper(pixelData, { items: providedItems }) + expect(event.asQuery().toQueryString()).to.eql('?se=eyJpdGVtcyI6WzAsMSwyLDMsNCw1LDYsNyw4LDldfQ') // Making sure this works and that we're not changing the object for the customer - expect(event.data).to.eql(pixelData) + expect(providedItems).to.eql(providedItemsCopy) }) it('should send distributorId using the short name: did', () => { @@ -405,20 +394,27 @@ describe('EventComposition', () => { distributorId: 'did-9898', liveConnectId: '213245' } - const event = new StateWrapper(pixelData, eventBus) + const event = new StateWrapper(pixelData, {}, eventBus) - expect(event.data).to.eql(pixelData) - expect(event.asQuery().toQueryString()).to.eql('?did=did-9898&duid=213245') + expect(event.data).to.eql({ + distributorId: 'did-9898', + liveConnectId: '213245', + eventSource: {} + }) + expect(event.asQuery().toQueryString()).to.eql('?did=did-9898&se=e30&duid=213245') }) it('should send ic if idcookie is resolved', () => { const eventBus = LocalEventBus() const resolvedIdCookie = '123' const pixelData = { resolvedIdCookie } - const event = new StateWrapper(pixelData, eventBus) + const event = new StateWrapper(pixelData, {}, eventBus) - expect(event.data).to.eql(pixelData) - expect(event.asQuery().toQueryString()).to.eql(`?ic=${resolvedIdCookie}`) + expect(event.data).to.eql({ + resolvedIdCookie, + eventSource: {} + }) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&ic=${resolvedIdCookie}`) }) it('should send empty ic if idcookie fails to be resolved', () => { @@ -426,9 +422,12 @@ describe('EventComposition', () => { const pixelData = { resolvedIdCookie: null } - const event = new StateWrapper(pixelData, eventBus) + const event = new StateWrapper(pixelData, {}, eventBus) - expect(event.data).to.eql(pixelData) - expect(event.asQuery().toQueryString()).to.eql('?ic=') + expect(event.data).to.eql({ + resolvedIdCookie: null, + eventSource: {} + }) + expect(event.asQuery().toQueryString()).to.eql('?se=e30&ic=') }) }) From ef77c48e54e05353da7b8e2cc270354a86089e08 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 31 Oct 2024 16:31:53 +0100 Subject: [PATCH 2/6] wip --- src/events/error-pixel.ts | 31 +++++------ src/pixel/fiddler.ts | 8 +-- src/pixel/state.ts | 24 ++++++--- src/standard-live-connect.ts | 2 +- src/types.ts | 15 ++++-- test/unit/events/error-pixel.spec.ts | 8 ++- test/unit/pixel/state.spec.ts | 77 +++++++++++++--------------- 7 files changed, 85 insertions(+), 80 deletions(-) diff --git a/src/events/error-pixel.ts b/src/events/error-pixel.ts index 2abd4c0b..2f06ef10 100644 --- a/src/events/error-pixel.ts +++ b/src/events/error-pixel.ts @@ -5,13 +5,6 @@ import { EventBus, State } from '../types.js' const MAX_ERROR_FIELD_LENGTH = 120 -const defaultReturn: { errorDetails: ErrorDetails } = { - errorDetails: { - message: 'Unknown message', - name: 'Unknown name' - } -} - function _asInt(field: unknown): number | undefined { try { const intValue = (field as number) * 1 @@ -32,27 +25,29 @@ function _truncate(value: unknown): string | undefined { } } -export function asErrorDetails(e: unknown): { errorDetails: ErrorDetails } { +export function asErrorDetails(e: unknown): ErrorDetails { if (isRecord(e)) { return { - errorDetails: { - message: _truncate(e.message) || '', - name: _truncate(e.name) || '', - stackTrace: _truncate(e.stack), - lineNumber: _asInt(e.lineNumber), - columnNumber: _asInt(e.columnNumber), - fileName: _truncate(e.fileName) - } + message: _truncate(e.message) || '', + name: _truncate(e.name) || '', + stackTrace: _truncate(e.stack), + lineNumber: _asInt(e.lineNumber), + columnNumber: _asInt(e.columnNumber), + fileName: _truncate(e.fileName) } } else { - return defaultReturn + return { + message: 'Unknown message', + name: 'Unknown name' + } } } export function register(state: State, sender: PixelSender, eventBus: EventBus): void { try { eventBus.on(ERRORS_CHANNEL, (error) => { - sender.sendPixel(new StateWrapper({ ...state, ...asErrorDetails(error) }, {}, eventBus)) + const wrapped = StateWrapper.fromError(state, asErrorDetails(error), eventBus) + sender.sendPixel(wrapped) }) } catch (e) { console.error('handlers.error.register', e) diff --git a/src/pixel/fiddler.ts b/src/pixel/fiddler.ts index 831feb4e..9e6ac4cc 100644 --- a/src/pixel/fiddler.ts +++ b/src/pixel/fiddler.ts @@ -2,7 +2,7 @@ import { extractEmail } from '../utils/email.js' import { decodeValue } from '../utils/url.js' import { extractHashValue, hashEmail, isHash } from '../utils/hash.js' import { isArray, isObject, isRecord, safeToString, trim } from 'live-connect-common' -import { FiddledExtraFields } from '../types.js' +import { FiddlerExtraFields } from '../types.js' type AnyRecord = Record @@ -10,8 +10,8 @@ const MAX_ITEMS = 10 const LIMITING_KEYS = ['items', 'itemids'] const HASH_BEARERS = ['email', 'emailhash', 'hash', 'hashedemail'] -function extractProvidedAttributes(eventSource: AnyRecord): FiddledExtraFields { - const extraFields: FiddledExtraFields = { eventSource } +function extractProvidedAttributes(eventSource: AnyRecord): FiddlerExtraFields { + const extraFields: FiddlerExtraFields = { eventSource } // add provided email hashes. Only consider the first one found. for (const key of Object.keys(eventSource)) { @@ -63,7 +63,7 @@ function limitItems(event: AnyRecord): AnyRecord { return limitedEvent } -export function fiddle(event: object): FiddledExtraFields { +export function fiddle(event: object): FiddlerExtraFields { if (isRecord(event)) { const extraAttributes = extractProvidedAttributes(event) return { diff --git a/src/pixel/state.ts b/src/pixel/state.ts index 3769bc68..d590ed41 100644 --- a/src/pixel/state.ts +++ b/src/pixel/state.ts @@ -1,21 +1,25 @@ import { base64UrlEncode } from '../utils/b64.js' import { replacer } from './stringify.js' import { fiddle, mergeObjects } from './fiddler.js' -import { isObject, trim, isArray, nonNull, onNonNull, Headers } from 'live-connect-common' +import { isObject, trim, isArray, nonNull, onNonNull, Headers, ErrorDetails } from 'live-connect-common' import { QueryBuilder, encodeIdCookie } from '../utils/query.js' -import { EventBus, FiddledState, State } from '../types.js' +import { EventBus, FiddlerExtraFields, State, WrappedState } from '../types.js' import { collectUrl } from './url-collector.js' const noOpEvents = ['setemail', 'setemailhash', 'sethashedemail'] export class StateWrapper { - data: FiddledState + data: WrappedState - constructor (state: State, eventSource: object, eventBus?: EventBus) { - this.data = StateWrapper.safeFiddle(state, eventSource, eventBus) + private constructor (state: State, eventSource: object, error?: ErrorDetails, eventBus?: EventBus) { + const data: WrappedState = StateWrapper.safeFiddle(state, eventSource, eventBus) + if (error) { + data.errorDetails = error + } + this.data = data } - private static safeFiddle(state: State, eventSource: object, eventBus?: EventBus): FiddledState { + private static safeFiddle(state: State, eventSource: object, eventBus?: EventBus): State & FiddlerExtraFields { try { return mergeObjects(state, fiddle(JSON.parse(JSON.stringify(eventSource)))) } catch (e) { @@ -27,6 +31,14 @@ export class StateWrapper { } } + static fromEvent(state: State, event: object, eventBus?: EventBus): StateWrapper { + return new StateWrapper(state, event, undefined, eventBus) + } + + static fromError(state: State, error: ErrorDetails, eventBus?: EventBus): StateWrapper { + return new StateWrapper(state, {}, error, eventBus) + } + setHashedEmail(hashedEmail: string[]): void { this.data.hashedEmail = hashedEmail } diff --git a/src/standard-live-connect.ts b/src/standard-live-connect.ts index be3449d6..b64a0d5f 100644 --- a/src/standard-live-connect.ts +++ b/src/standard-live-connect.ts @@ -29,7 +29,7 @@ function pushSingleEvent(event: unknown, pixelClient: PixelSender, enrichedState eventBus.emitErrorWithMessage('StrayConfig', 'Received a config after LC has already been initialised', new Error(JSON.stringify(event))) } else { // const stateWithEventSource: StateWithEventSource = { eventSource: event, ...enrichedState } - const wrapper = new StateWrapper(enrichedState, event, eventBus) + const wrapper = StateWrapper.fromEvent(enrichedState, event, eventBus) // const combined = wrapper.combineWith({ eventSource: event }) if (wrapper.getHashedEmail().length > 0) { diff --git a/src/types.ts b/src/types.ts index 74935afe..b2a1d606 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,7 +50,8 @@ export interface LiveConnectConfig { peopleVerifiedId?: string gppString?: string gppApplicableSections?: number[] - idCookie?: IdCookieConfig + idCookie?: IdCookieConfig, + hashedEmail?: string[] } export type ResolutionParams = Record @@ -77,7 +78,6 @@ export interface State extends LiveConnectConfig { hashesFromIdentifiers?: HashedEmail[] decisionIds?: string[] peopleVerifiedId?: string - errorDetails?: ErrorDetails retrievedIdentifiers?: RetrievedIdentifier[] providedHash?: string contextSelectors?: string @@ -89,15 +89,20 @@ export interface State extends LiveConnectConfig { resolvedIdCookie?: string | null // null signals failure to resolve, } -export type FiddledExtraFields = { +export type FiddlerExtraFields = { hashedEmail?: string[] providedIPV4?: string providedIPV6?: string - providedUserAgent?: string, + providedUserAgent?: string eventSource?: Record } -export type FiddledState = State & FiddledExtraFields +export type ErrorDetailsExtraFields = { + errorDetails?: ErrorDetails +} + +export type WrappedState = State & FiddlerExtraFields & ErrorDetailsExtraFields + export interface ConfigMismatch { appId: (string | undefined)[] wrapperName: (string | undefined)[] diff --git a/test/unit/events/error-pixel.spec.ts b/test/unit/events/error-pixel.spec.ts index e52a4922..46ddd36f 100644 --- a/test/unit/events/error-pixel.spec.ts +++ b/test/unit/events/error-pixel.spec.ts @@ -61,16 +61,14 @@ describe('ErrorPixel', () => { const longText = 'x'.repeat(200) const error = new Error(longText) const result = errorPixel.asErrorDetails(error) - expect(result.errorDetails.message.length).to.eq(123) + expect(result.message.length).to.eq(123) }) it('should send the default error if none was sent', () => { const result = errorPixel.asErrorDetails(null) expect(result).to.deep.equal({ - errorDetails: { - message: 'Unknown message', - name: 'Unknown name' - } + message: 'Unknown message', + name: 'Unknown name' }) }) }) diff --git a/test/unit/pixel/state.spec.ts b/test/unit/pixel/state.spec.ts index da3cfe06..2ddf62a2 100644 --- a/test/unit/pixel/state.spec.ts +++ b/test/unit/pixel/state.spec.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { expect, use } from 'chai' import { hashEmail } from '../../../src/utils/hash.js' import { enrichPrivacyMode } from '../../../src/enrichers/privacy-config.js' @@ -8,6 +7,7 @@ import dirtyChai from 'dirty-chai' import { LocalEventBus } from '../../../src/events/event-bus.js' import { UrlCollectionModes } from '../../../src/model/url-collection-mode.js' import { State } from '../../../src/types.js' +import { ErrorDetails } from 'live-connect-common' use(dirtyChai) @@ -15,19 +15,19 @@ const COMMA = encodeURIComponent(',') describe('EventComposition', () => { it('should construct an event out of anything', () => { const pixelData = { appId: '9898' } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.data).to.eql({ appId: '9898', eventSource: {} }) }) it('should construct valid params for valid members', () => { const pixelData = { appId: '9898' } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql('?aid=9898&se=e30') }) it('should ignore empty fields', () => { const pixelData = { appId: '9898', contextElements: '' } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql('?aid=9898&se=e30') }) @@ -38,7 +38,6 @@ describe('EventComposition', () => { liveConnectId: '213245', trackerVersion: 'test tracker', pageUrl: 'https://wwww.example.com?sss', - errorDetails: { testError: 'testError' }, retrievedIdentifiers: [{ name: 'sample_cookie', value: 'sample_value' @@ -60,7 +59,7 @@ describe('EventComposition', () => { gppApplicableSections: [1, 2, 3], cookieDomain: 'test-cookie-domain' } - const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) + const event = StateWrapper.fromEvent(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) const expectedPairs = [ 'aid=9898', // appId @@ -68,7 +67,6 @@ describe('EventComposition', () => { 'duid=213245', // liveConnectId 'tv=test%20tracker', // trackerVersion 'pu=https%3A%2F%2Fwwww.example.com%3Fsss', // pageUrl - 'ae=eyJ0ZXN0RXJyb3IiOiJ0ZXN0RXJyb3IifQ', // base64 of errorDetails 'ext_sample_cookie=sample_value', // retrievedIdentifiers 'scre=75524519292e51ad6f761baa82d07d76%2Cec3685d99c376b4ee14a5b985a05fc23e21235cb%2Ce168e0eda11f4fbb8fbd7cfe5f750cd0f7e7f4d8649da68e073e927504ec5d72', // comma-separated hashesFromIdentifiers 'li_did=1%2C2', // decisionIds @@ -108,11 +106,11 @@ describe('EventComposition', () => { hashedEmail: ['eb2684ead8e942b6c4dc7465de66460a'], usPrivacyString: '1---', wrapperName: 'test wrapper name', - gdprApplies: 'a', + gdprApplies: 'a' as unknown as boolean, gdprConsent: 'test-consent-string', referrer: 'https://some.test.referrer.com' } - const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) + const event = StateWrapper.fromEvent(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) const expectedPairs = [ 'aid=9898', // appId @@ -140,7 +138,7 @@ describe('EventComposition', () => { appId: '9898', randomField: 2135523 } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql('?aid=9898&se=e30') }) @@ -149,7 +147,7 @@ describe('EventComposition', () => { appId: '9898' } const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCJ9' - const event = new StateWrapper(pixelData, { eventName: 'viewContent' }) + const event = StateWrapper.fromEvent(pixelData, { eventName: 'viewContent' }) expect(event.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}`) }) @@ -158,7 +156,7 @@ describe('EventComposition', () => { gppString: 'test-gpp-string', gppApplicableSections: [1, 2, 3] } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) const expectedPairs = [ 'se=e30', // eventSource 'gpp_s=test-gpp-string', // GPP string @@ -171,7 +169,7 @@ describe('EventComposition', () => { const pixelData = { usPrivacyString: '1---' } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql('?se=e30&us_privacy=1---') }) @@ -180,7 +178,7 @@ describe('EventComposition', () => { gdprApplies: true, gdprConsent: 'some-string' } - const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) + const event = StateWrapper.fromEvent(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCJ9' expect(event.asQuery().toQueryString()).to.eql(`?se=${b64EncodedEventSource}&gdpr=1&gdpr_consent=some-string`) }) @@ -190,7 +188,7 @@ describe('EventComposition', () => { gdprApplies: false, gdprConsent: 'some-string' } - const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) + const event = StateWrapper.fromEvent(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCJ9' expect(event.asQuery().toQueryString()).to.eql(`?se=${b64EncodedEventSource}&gdpr=0&gdpr_consent=some-string`) }) @@ -198,19 +196,19 @@ describe('EventComposition', () => { it('should send the tracker name', () => { const trackerVersion = 'some-name' const pixelData = { trackerVersion } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql(`?se=e30&tv=${trackerVersion}`) }) it('should ignore nullable fields', () => { - const event = new StateWrapper({}, {}) + const event = StateWrapper.fromEvent({}, {}) expect(event.asQuery().toQueryString()).to.eql('?se=e30') }) it('should send the page url', () => { const pageUrl = 'https://wwww.example.com?sss' const pixelData = { pageUrl } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql(`?se=e30&pu=${encodeURIComponent(pageUrl)}`) }) @@ -221,7 +219,7 @@ describe('EventComposition', () => { urlCollectionMode: UrlCollectionModes.noPath, queryParametersFilter: '^(foo|bar)$' } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) const expectedUrl = 'https://www.example.com/?query=v1&id=v4' expect(event.asQuery().toQueryString()).to.eql(`?se=e30&pu=${encodeURIComponent(expectedUrl)}&pu_rp=1&pu_rqp=foo${COMMA}bar`) }) @@ -233,16 +231,13 @@ describe('EventComposition', () => { urlCollectionMode: UrlCollectionModes.noPath, queryParametersFilter: '^(foo|bar)$' } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql(`?se=e30&pu=${encodeURIComponent(pageUrl)}`) }) it('should send the application error', () => { const applicationError = { someKey: 'value' } - const pixelData = { - errorDetails: applicationError - } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromError({}, applicationError as unknown as ErrorDetails) const b64EncodedEventSource = 'eyJzb21lS2V5IjoidmFsdWUifQ' expect(event.asQuery().toQueryString()).to.eql(`?se=e30&ae=${b64EncodedEventSource}`) }) @@ -251,7 +246,7 @@ describe('EventComposition', () => { const pixelData = { appId: '9898' } - const event = new StateWrapper(pixelData, { eventName: 'viewContent' }) + const event = StateWrapper.fromEvent(pixelData, { eventName: 'viewContent' }) event.setHashedEmail(['foo']) @@ -273,8 +268,8 @@ describe('EventComposition', () => { } const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCIsImVtYWlsIjoiICBlMTY4ZTBlZGExMWY0ZmJiOGZiZDdjZmU1Zjc1MGNkMGY3ZTdmNGQ4NjQ5ZGE2OGUwNzNlOTI3NTA0ZWM1ZDcyICAgICJ9' - const event = new StateWrapper(pixelData, event) - expect(event.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}&e=e168e0eda11f4fbb8fbd7cfe5f750cd0f7e7f4d8649da68e073e927504ec5d72`) + const wrapped = StateWrapper.fromEvent(pixelData, event) + expect(wrapped.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}&e=e168e0eda11f4fbb8fbd7cfe5f750cd0f7e7f4d8649da68e073e927504ec5d72`) }) it('should never send emails as plain text, and hash the email that is set in the source', () => { @@ -288,8 +283,8 @@ describe('EventComposition', () => { const hashes = hashEmail('xxx@yyy.com') const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCIsImVtYWlsIjoiKioqKioqKioqIn0' - const event = new StateWrapper(pixelData, event) - expect(event.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}&e=${hashes.md5}%2C${hashes.sha1}%2C${hashes.sha256}`) + const wrapped = StateWrapper.fromEvent(pixelData, event) + expect(wrapped.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}&e=${hashes.md5}%2C${hashes.sha1}%2C${hashes.sha256}`) }) it('should send the retrieved identifiers', () => { @@ -305,7 +300,7 @@ describe('EventComposition', () => { retrievedIdentifiers: [cookie1, cookie2] } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql(`?se=e30&ext_${cookie1.name}=${cookie1.value}&ext_${cookie2.name}=${cookie2.value}`) }) @@ -325,7 +320,7 @@ describe('EventComposition', () => { hashesFromIdentifiers: [hashes1, hashes2] } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql(`?se=e30&scre=${hashes1.md5}${COMMA}${hashes1.sha1}${COMMA}${hashes1.sha256}&scre=${hashes2.md5}${COMMA}${hashes2.sha1}${COMMA}${hashes2.sha256}`) }) @@ -334,7 +329,7 @@ describe('EventComposition', () => { const pixelData = { decisionIds: ['1', '2'] } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql(`?se=e30&li_did=1${COMMA}2`) }) @@ -342,7 +337,7 @@ describe('EventComposition', () => { const pixelData = { decisionIds: [] } - const event = new StateWrapper(pixelData, {}) + const event = StateWrapper.fromEvent(pixelData, {}) expect(event.asQuery().toQueryString()).to.eql('?se=e30') }) @@ -352,28 +347,28 @@ describe('EventComposition', () => { email: ' xxx@yyy.com' } - expect(new StateWrapper({}, event1).sendsPixel()).to.be.false() + expect(StateWrapper.fromEvent({}, event1).sendsPixel()).to.be.false() const event2 = { eventName: 'setEmailHash', email: ' xxx@yyy.com' } - expect(new StateWrapper({}, event2).sendsPixel()).to.be.false() + expect(StateWrapper.fromEvent({}, event2).sendsPixel()).to.be.false() const event3 = { eventName: 'setHashedEmail', email: ' xxx@yyy.com' } - expect(new StateWrapper({}, event3).sendsPixel()).to.be.false() + expect(StateWrapper.fromEvent({}, event3).sendsPixel()).to.be.false() const event4 = { eventName: 'setContent', email: ' xxx@yyy.com' } - expect(new StateWrapper({}, event4).sendsPixel()).to.be.true() + expect(StateWrapper.fromEvent({}, event4).sendsPixel()).to.be.true() }) it('should limit the number of items', () => { @@ -382,7 +377,7 @@ describe('EventComposition', () => { } const providedItems = Array.from(Array(50).keys()) const providedItemsCopy = [...providedItems] - const event = new StateWrapper(pixelData, { items: providedItems }) + const event = StateWrapper.fromEvent(pixelData, { items: providedItems }) expect(event.asQuery().toQueryString()).to.eql('?se=eyJpdGVtcyI6WzAsMSwyLDMsNCw1LDYsNyw4LDldfQ') // Making sure this works and that we're not changing the object for the customer expect(providedItems).to.eql(providedItemsCopy) @@ -394,7 +389,7 @@ describe('EventComposition', () => { distributorId: 'did-9898', liveConnectId: '213245' } - const event = new StateWrapper(pixelData, {}, eventBus) + const event = StateWrapper.fromEvent(pixelData, {}, eventBus) expect(event.data).to.eql({ distributorId: 'did-9898', @@ -408,7 +403,7 @@ describe('EventComposition', () => { const eventBus = LocalEventBus() const resolvedIdCookie = '123' const pixelData = { resolvedIdCookie } - const event = new StateWrapper(pixelData, {}, eventBus) + const event = StateWrapper.fromEvent(pixelData, {}, eventBus) expect(event.data).to.eql({ resolvedIdCookie, @@ -422,7 +417,7 @@ describe('EventComposition', () => { const pixelData = { resolvedIdCookie: null } - const event = new StateWrapper(pixelData, {}, eventBus) + const event = StateWrapper.fromEvent(pixelData, {}, eventBus) expect(event.data).to.eql({ resolvedIdCookie: null, From 1874ed0f2a2b9637364ef075377fc079a4fcf234 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 31 Oct 2024 16:45:19 +0100 Subject: [PATCH 3/6] wip --- test/unit/pixel/sender.spec.ts | 19 +++++++++++++++++++ test/unit/pixel/state.spec.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/test/unit/pixel/sender.spec.ts b/test/unit/pixel/sender.spec.ts index 81e29c06..37af9ed7 100644 --- a/test/unit/pixel/sender.spec.ts +++ b/test/unit/pixel/sender.spec.ts @@ -211,4 +211,23 @@ describe('PixelSender', () => { sender.sendPixel(stubbedStateWrapper1) expect(pixelRequests).to.be.empty() }) + + it('sends headers', (done) => { + const successCallback = () => { + expect(ajaxRequests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?dtstmp=\d+&xxx=yyy/) + expect(ajaxRequests[0].requestHeaders['X-LI-Provided-User-Agent']).to.eql('Mozilla/5.0') + done() + } + const sender = new PixelSender({ callHandler: calls, eventBus }) + const stubbedStateWrapper1: StateWrapper = { + data: stubbedStateWrapper.data, + asQuery: stubbedStateWrapper.asQuery, + sendsPixel: stubbedStateWrapper.sendsPixel, + asHeaders: () => ({ 'X-LI-Provided-User-Agent': 'Mozilla/5.0' }), + setHashedEmail: stubbedStateWrapper.setHashedEmail, + getHashedEmail: stubbedStateWrapper.getHashedEmail + } + sender.sendAjax(stubbedStateWrapper1, { onLoad: successCallback }) + ajaxRequests[0].respond(200, { 'Content-Type': 'application/json' }, '{}') + }) }) diff --git a/test/unit/pixel/state.spec.ts b/test/unit/pixel/state.spec.ts index 2ddf62a2..868c3cb0 100644 --- a/test/unit/pixel/state.spec.ts +++ b/test/unit/pixel/state.spec.ts @@ -425,4 +425,37 @@ describe('EventComposition', () => { }) expect(event.asQuery().toQueryString()).to.eql('?se=e30&ic=') }) + + it('should extract ipv4', () => { + const eventBus = LocalEventBus() + const eventSource = { + ipv4: '127.0.0.1' + } + const event = StateWrapper.fromEvent({}, eventSource, eventBus) + + expect(event.asQuery().toQueryString()).to.eql('?se=eyJpcHY0IjoiMTI3LjAuMC4xIn0&pip=MTI3LjAuMC4x') + }) + + it('should extract ipv6', () => { + const eventBus = LocalEventBus() + const eventSource = { + ipv6: '4c15:c00b:125f:4c5c:66db:5c16:05bb:0fc5' + } + const event = StateWrapper.fromEvent({}, eventSource, eventBus) + + expect(event.asQuery().toQueryString()).to.eql('?se=eyJpcHY2IjoiNGMxNTpjMDBiOjEyNWY6NGM1Yzo2NmRiOjVjMTY6MDViYjowZmM1In0&pip6=NGMxNTpjMDBiOjEyNWY6NGM1Yzo2NmRiOjVjMTY6MDViYjowZmM1') + }) + + it('should extract userAgent', () => { + const eventBus = LocalEventBus() + const eventSource = { + userAgent: 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0' + } + const event = StateWrapper.fromEvent({}, eventSource, eventBus) + + expect(event.asQuery().toQueryString()).to.eql('?se=eyJ1c2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCA2LjE7IFdpbjY0OyB4NjQ7IHJ2OjQ3LjApIEdlY2tvLzIwMTAwMTAxIEZpcmVmb3gvNDcuMCJ9') + expect(event.asHeaders()).to.eql({ + 'X-LI-Provided-User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0' + }) + }) }) From 3dbfcc361c8d256103f9b90a46fe14431c7adaa4 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 31 Oct 2024 16:49:53 +0100 Subject: [PATCH 4/6] finish adjusting tests --- test/unit/idex/identity-resolver.spec.ts | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/unit/idex/identity-resolver.spec.ts b/test/unit/idex/identity-resolver.spec.ts index 0a39b454..c781542e 100644 --- a/test/unit/idex/identity-resolver.spec.ts +++ b/test/unit/idex/identity-resolver.spec.ts @@ -364,4 +364,30 @@ describe('IdentityResolver without cache', () => { identityResolver.resolve(successCallback) requestToComplete.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({})) }) + + it('should attach provided ipv4', (done) => { + const response = { id: 112233 } + const identityResolver = new IdentityResolver({ peopleVerifiedId: '987', identityResolutionConfig: { extraAttributes: { ipv4: '127.0.0.1' } } }, calls) + const successCallback = (responseAsJson) => { + expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?duid=987&pip=MTI3LjAuMC4x') + expect(errors).to.be.empty() + expect(responseAsJson).to.be.eql(response) + done() + } + identityResolver.resolve(successCallback) + requestToComplete.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(response)) + }) + + it('should attach the duid', (done) => { + const response = { id: 112233 } + const identityResolver = new IdentityResolver({ peopleVerifiedId: '987', identityResolutionConfig: { extraAttributes: { ipv6: '4c15:c00b:125f:4c5c:66db:5c16:05bb:0fc5' } } }, calls) + const successCallback = (responseAsJson) => { + expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?duid=987&pip6=NGMxNTpjMDBiOjEyNWY6NGM1Yzo2NmRiOjVjMTY6MDViYjowZmM1') + expect(errors).to.be.empty() + expect(responseAsJson).to.be.eql(response) + done() + } + identityResolver.resolve(successCallback) + requestToComplete.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(response)) + }) }) From 2b73ebd80bbb66b93d4b5aa2b9cd8dee8f2952f7 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 31 Oct 2024 16:55:57 +0100 Subject: [PATCH 5/6] comments --- test/unit/pixel/sender.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/pixel/sender.spec.ts b/test/unit/pixel/sender.spec.ts index 37af9ed7..a1711163 100644 --- a/test/unit/pixel/sender.spec.ts +++ b/test/unit/pixel/sender.spec.ts @@ -67,7 +67,7 @@ describe('PixelSender', () => { it('defaults to production if none set when sendAjax', (done) => { const successCallback = () => { - expect(ajaxRequests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?dtstmp=\d+&xxx=yyy/) + expect(ajaxRequests[0].url).to.match(/https:\/\/rp\.liadm\.com\/j\?dtstmp=\d+&xxx=yyy/) done() } const sender = new PixelSender({ callHandler: calls, eventBus }) @@ -110,10 +110,10 @@ describe('PixelSender', () => { pixelStub = sandbox.stub(calls, 'pixelGet').callsFake((uri) => { bakersCount++ if (bakersCount === 1) { - expect(uri).to.match(/https:\/\/baker1.com\/baker\?dtstmp=\d+/) + expect(uri).to.match(/https:\/\/baker1\.com\/baker\?dtstmp=\d+/) } if (bakersCount === 2) { - expect(uri).to.match(/https:\/\/baker2.com\/baker\?dtstmp=\d+/) + expect(uri).to.match(/https:\/\/baker2\.com\/baker\?dtstmp=\d+/) done() } }) @@ -165,7 +165,7 @@ describe('PixelSender', () => { it('defaults to production if none set when sendAjax', (done) => { const successCallback = () => { - expect(ajaxRequests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?dtstmp=\d+&xxx=yyy/) + expect(ajaxRequests[0].url).to.match(/https:\/\/rp\.liadm\.com\/j\?dtstmp=\d+&xxx=yyy/) done() } const sender = new PixelSender({ callHandler: calls, eventBus }) @@ -185,7 +185,7 @@ describe('PixelSender', () => { it('defaults to production if none set when sendPixel', () => { const sender = new PixelSender({ callHandler: calls, eventBus }) sender.sendPixel(stubbedStateWrapper) - expect(pixelRequests[0].uri).to.match(/https:\/\/rp.liadm.com\/p\?dtstmp=\d+&xxx=yyy/) + expect(pixelRequests[0].uri).to.match(/https:\/\/rp\.liadm\.com\/p\?dtstmp=\d+&xxx=yyy/) expect(pixelRequests[0].onload).to.be.undefined() }) @@ -214,7 +214,7 @@ describe('PixelSender', () => { it('sends headers', (done) => { const successCallback = () => { - expect(ajaxRequests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?dtstmp=\d+&xxx=yyy/) + expect(ajaxRequests[0].url).to.match(/https:\/\/rp\.liadm\.com\/j\?dtstmp=\d+&xxx=yyy/) expect(ajaxRequests[0].requestHeaders['X-LI-Provided-User-Agent']).to.eql('Mozilla/5.0') done() } From cc01fb2b55078c3985a5a3bee7e7ba568f36f0a2 Mon Sep 17 00:00:00 2001 From: peixunzhang Date: Thu, 31 Oct 2024 17:56:15 +0000 Subject: [PATCH 6/6] Release 7.1.0-alpha-2b73ebd.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cdc33bfa..5001f23b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "live-connect-js", - "version": "7.0.0", + "version": "7.1.0-alpha-2b73ebd.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "live-connect-js", - "version": "7.0.0", + "version": "7.1.0-alpha-2b73ebd.0", "license": "Apache-2.0", "dependencies": { "live-connect-common": "^v4.1.0", diff --git a/package.json b/package.json index 4a32ab36..30ef3e1e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "git+https://github.com/liveintent-berlin/live-connect.git" }, "description": "LiveConnect, The first party identity provider", - "version": "7.0.0", + "version": "7.1.0-alpha-2b73ebd.0", "versionPrefix": "live-connect-v", "license": "Apache-2.0", "private": false,