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

Commit

Permalink
[CM-1540] Add support for ip and useragent (#517)
Browse files Browse the repository at this point in the history
* [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 <[email protected]>
  • Loading branch information
mschuwalow and team-berlin-machine-user authored Nov 1, 2024
1 parent 93eb913 commit 7a1e7eb
Show file tree
Hide file tree
Showing 15 changed files with 411 additions and 260 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

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

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down 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

0 comments on commit 7a1e7eb

Please sign in to comment.