Skip to content
This repository has been archived by the owner on Dec 9, 2024. It is now read-only.

[CM-1540] Add support for ip and useragent #517

Merged
merged 6 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
31 changes: 13 additions & 18 deletions src/events/error-pixel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions src/handlers/call-handler.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand Down
7 changes: 6 additions & 1 deletion src/idex.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
Expand All @@ -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'
Expand All @@ -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)
Expand Down
70 changes: 46 additions & 24 deletions src/pixel/fiddler.ts
Original file line number Diff line number Diff line change
@@ -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<string | symbol | number, unknown>

const MAX_ITEMS = 10
const LIMITING_KEYS = ['items', 'itemids']
const HASH_BEARERS = ['email', 'emailhash', 'hash', 'hashedemail']

function provided<A extends { eventSource?: Record<string, unknown> }>(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<string, unknown> }): Record<string, never> {
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<A extends { eventSource?: Record<string, unknown> }>(state: A): A & { hashedEmail?: HashedEmail[] } {
function reducer<B extends object>(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 {}
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/pixel/sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -75,7 +78,8 @@ export class PixelSender {
go(remainingRetries - 1)
}
},
this.timeout
this.timeout,
headers
)
}

Expand Down
48 changes: 36 additions & 12 deletions src/pixel/state.ts
Original file line number Diff line number Diff line change
@@ -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<State>): 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() {
Expand All @@ -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

Expand Down Expand Up @@ -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
}
Expand Down
Loading
Loading