From 7a1e7ebed5dab3d0afe1b20d07182fa52749c502 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Fri, 1 Nov 2024 14:20:46 +0100 Subject: [PATCH] [CM-1540] Add support for ip and useragent (#517) * [CM-1540] Add support for ip and useragent * wip * wip * finish adjusting tests * comments * Release 7.1.0-alpha-2b73ebd.0 --------- Co-authored-by: peixunzhang --- package-lock.json | 8 +- package.json | 6 +- src/events/error-pixel.ts | 31 ++- src/handlers/call-handler.ts | 7 +- src/idex.ts | 7 +- src/pixel/fiddler.ts | 70 ++++-- src/pixel/sender.ts | 6 +- src/pixel/state.ts | 48 +++- src/standard-live-connect.ts | 16 +- src/types.ts | 31 ++- test/unit/events/error-pixel.spec.ts | 8 +- test/unit/idex/identity-resolver.spec.ts | 26 +++ test/unit/pixel/fiddler.spec.ts | 60 ++--- test/unit/pixel/sender.spec.ts | 80 +++++-- test/unit/pixel/state.spec.ts | 267 +++++++++++++---------- 15 files changed, 411 insertions(+), 260 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac98ad28..18a2d28d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "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.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", diff --git a/package.json b/package.json index 8b474b59..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, @@ -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..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/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..9e6ac4cc 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 { FiddlerExtraFields } 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): FiddlerExtraFields { + const extraFields: FiddlerExtraFields = { 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): FiddlerExtraFields { + 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..d590ed41 100644 --- a/src/pixel/state.ts +++ b/src/pixel/state.ts @@ -1,34 +1,50 @@ 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, ErrorDetails } from 'live-connect-common' import { QueryBuilder, encodeIdCookie } from '../utils/query.js' -import { EventBus, 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: State - eventBus: EventBus + data: WrappedState - constructor (state: State, eventBus: EventBus) { - this.data = StateWrapper.safeFiddle(state, eventBus) - this.eventBus = 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(newInfo: State, eventBus: EventBus): State { + private static safeFiddle(state: State, eventSource: object, eventBus?: EventBus): State & FiddlerExtraFields { 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) + 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 + } + + getHashedEmail(): string[] { + return this.data.hashedEmail || [] } sendsPixel() { @@ -40,6 +56,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 +104,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..b64a0d5f 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 = StateWrapper.fromEvent(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..b2a1d606 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 = { @@ -44,7 +50,8 @@ export interface LiveConnectConfig { peopleVerifiedId?: string gppString?: string gppApplicableSections?: number[] - idCookie?: IdCookieConfig + idCookie?: IdCookieConfig, + hashedEmail?: string[] } export type ResolutionParams = Record @@ -71,9 +78,7 @@ export interface State extends LiveConnectConfig { hashesFromIdentifiers?: HashedEmail[] decisionIds?: string[] peopleVerifiedId?: string - errorDetails?: ErrorDetails retrievedIdentifiers?: RetrievedIdentifier[] - hashedEmail?: string[] providedHash?: string contextSelectors?: string contextElementsLength?: number @@ -81,9 +86,23 @@ 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 FiddlerExtraFields = { + hashedEmail?: string[] + providedIPV4?: string + providedIPV6?: string + providedUserAgent?: string + eventSource?: Record +} + +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/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)) + }) }) 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..a1711163 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') @@ -58,11 +67,11 @@ 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 }) - 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' }, '{}') }) @@ -92,16 +110,16 @@ 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() } }) 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,18 +158,18 @@ 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}') }) 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 }) - 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,27 +179,55 @@ 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) - expect(pixelRequests[0].uri).to.match(/https:\/\/rp.liadm.com\/p\?dtstmp=\d+&xxx=yyy/) + 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() }) 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() }) + + 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 0bb534ac..868c3cb0 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,31 +15,29 @@ 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 = 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) - expect(event.asQuery().toQueryString()).to.eql('?aid=9898') + 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) - expect(event.asQuery().toQueryString()).to.eql('?aid=9898') + const event = StateWrapper.fromEvent(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', - errorDetails: { testError: 'testError' }, retrievedIdentifiers: [{ name: 'sample_cookie', value: 'sample_value' @@ -61,7 +59,7 @@ describe('EventComposition', () => { gppApplicableSections: [1, 2, 3], cookieDomain: 'test-cookie-domain' } - const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData))) + const event = StateWrapper.fromEvent(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) const expectedPairs = [ 'aid=9898', // appId @@ -69,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 @@ -92,7 +89,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', @@ -110,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))) + const event = StateWrapper.fromEvent(mergeObjects(pixelData, enrichPrivacyMode(pixelData)), { eventName: 'viewContent' }) const expectedPairs = [ 'aid=9898', // appId @@ -142,17 +138,16 @@ describe('EventComposition', () => { appId: '9898', randomField: 2135523 } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql('?aid=9898') + const event = StateWrapper.fromEvent(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 = StateWrapper.fromEvent(pixelData, { eventName: 'viewContent' }) expect(event.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}`) }) @@ -161,8 +156,9 @@ 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 'gpp_as=1%2C2%2C3' // GPP applicable sections ] @@ -173,28 +169,26 @@ describe('EventComposition', () => { const pixelData = { usPrivacyString: '1---' } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql('?us_privacy=1---') + const event = StateWrapper.fromEvent(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 = 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`) }) 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 = 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`) }) @@ -202,20 +196,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 = StateWrapper.fromEvent(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 = 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) - expect(event.asQuery().toQueryString()).to.eql(`?pu=${encodeURIComponent(pageUrl)}`) + const event = StateWrapper.fromEvent(pixelData, {}) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&pu=${encodeURIComponent(pageUrl)}`) }) it('should send the removed parts of the page url', () => { @@ -225,9 +219,9 @@ 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(`?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,69 +231,60 @@ describe('EventComposition', () => { urlCollectionMode: UrlCollectionModes.noPath, queryParametersFilter: '^(foo|bar)$' } - const event = new StateWrapper(pixelData) - expect(event.asQuery().toQueryString()).to.eql(`?pu=${encodeURIComponent(pageUrl)}`) + 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(`?ae=${b64EncodedEventSource}`) + expect(event.asQuery().toQueryString()).to.eql(`?se=e30&ae=${b64EncodedEventSource}`) }) it('should update the data', () => { const pixelData = { - appId: '9898', - eventSource: { eventName: 'viewContent' } + appId: '9898' } - const expectedData = { - appId: '9898', - eventSource: { eventName: 'viewContent' }, - liveConnectId: '213245' - } - const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCJ9' - const event = new StateWrapper(pixelData) + const event = StateWrapper.fromEvent(pixelData, { eventName: 'viewContent' }) - const newEvent = event.combineWith({ liveConnectId: '213245' }) + event.setHashedEmail(['foo']) - expect(newEvent.data).to.eql(expectedData) - expect(newEvent.asQuery().toQueryString()).to.eql(`?aid=9898&se=${b64EncodedEventSource}&duid=213245`) - - 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) - 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', () => { 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) - 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', () => { @@ -315,9 +300,9 @@ describe('EventComposition', () => { retrievedIdentifiers: [cookie1, cookie2] } - const event = new StateWrapper(pixelData) + const event = StateWrapper.fromEvent(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 +320,67 @@ describe('EventComposition', () => { hashesFromIdentifiers: [hashes1, hashes2] } - const event = new StateWrapper(pixelData) + const event = StateWrapper.fromEvent(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 = StateWrapper.fromEvent(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 = StateWrapper.fromEvent(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(StateWrapper.fromEvent({}, event1).sendsPixel()).to.be.false() + + const event2 = { + eventName: 'setEmailHash', + email: ' xxx@yyy.com' + } + + expect(StateWrapper.fromEvent({}, event2).sendsPixel()).to.be.false() + + const event3 = { + eventName: 'setHashedEmail', + email: ' xxx@yyy.com' + } + + expect(StateWrapper.fromEvent({}, event3).sendsPixel()).to.be.false() + + const event4 = { + eventName: 'setContent', + email: ' xxx@yyy.com' + } + + expect(StateWrapper.fromEvent({}, 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 = 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(event.data).to.eql(pixelData) + expect(providedItems).to.eql(providedItemsCopy) }) it('should send distributorId using the short name: did', () => { @@ -405,20 +389,27 @@ describe('EventComposition', () => { distributorId: 'did-9898', liveConnectId: '213245' } - const event = new StateWrapper(pixelData, eventBus) + const event = StateWrapper.fromEvent(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 = StateWrapper.fromEvent(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 +417,45 @@ 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, + eventSource: {} + }) + expect(event.asQuery().toQueryString()).to.eql('?se=e30&ic=') + }) - expect(event.data).to.eql(pixelData) - expect(event.asQuery().toQueryString()).to.eql('?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' + }) }) })