Skip to content
This repository has been archived by the owner on Jan 27, 2025. It is now read-only.

Commit

Permalink
[CM-1105] Refactor how idCookie is stored in the state (#311)
Browse files Browse the repository at this point in the history
* [CM-1105] Do not store resolved idcookie in state when mode is generated

* remove unused params

* adjust config

* adjust docs

* use empty ic param to signal that cookie failed to resolve
  • Loading branch information
mschuwalow authored Jan 22, 2024
1 parent e485130 commit bf1834b
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 107 deletions.
3 changes: 3 additions & 0 deletions COLLECTOR_PARAMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
- the value of the `config.gppString` config parameter
### `gpp_as`
- the value of the `config.gppApplicableSections` config parameter
### `ic`
- md5 hash of the resolved id cookie when configuration `config.idCookie` is used. Empty parameter encodes that an
idcookie was configured but failed to resolve.

## Example of a request to a default collectorUrl:
`https://rp.liadm.com/p?tna=v1.0.16&aid=a-00co&lduid=a-00co--bda8cda1-9000-4632-8c64-06e04fa8d113&duid=df9f30ab37f2--01dwcepmbbbqm0hvj4wytvyss4&pu=https%3A%2F%2Fwww.example.com%2F&se=eyJldmVudCI6InZpZXdIb21lUGFnZSJ9&dtstmp=1577968744235`
40 changes: 27 additions & 13 deletions CONFIGURATION_OPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,24 @@ Example:
}
```

##### `identityResolutionConfig.idCookieMode` [Optional, HasDefault]
Strategy that will be used by live-connect to resolve the `idCookie` identifier.

`generated` (default) : live-connect will use an internally generated value as the idcookie.

`provided`: live-connect will return a user provided identifier as the idCookie. See the [idCookie](#idcookie)
section for how to configure this identifier.


Example:
```javascript
{
identityResolutionConfig: {
idCookieMode: 'generated'
}
}
```

##### `contextSelectors` [Optional]
The context selectors to collect from the current page.
Example:
Expand Down Expand Up @@ -273,25 +291,21 @@ Example:
}
```

##### `idcookie` [Optional]
<h5 id="idcookie">
<code>idCookie</code> [Optional]
</h5>

This parameter allows to configure a custom cookie or localstorage entry as an additional identifier that will
be used for id resolution. It can be resolved by the id resolution module by requesting the attribute 'idcookie'.
This identifier will also be used internally by liveintent to identify a user, so it should be set to a
stable, long-lived and unique identifier. Setting this to an identifier that does not meet these requirements
might negatively affect id resolution.
be used for user identification.

Default configuration uses the liveintent first-party cookie as the idcookie:
```javascript
{
mode: "generated"
}
```
Setting this to a stable, long-lived and unique identifier will improve tracking performance of live-connect which will
in turn improve identity resolution. For these reasons we strongly recommend using this setting if a suitable identifier is available.

Setting this to an identifier that does not meet these requirements might negatively affect id resolution.

A custom cookie can be provided like this:
Example for providing a custom cookie
```javascript
{
mode: "provided",
strategy: "cookie" // or "localStorage"
name: "foobar" // name of the entry
}
Expand Down
20 changes: 8 additions & 12 deletions src/enrichers/idcookie.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import { WrappedReadOnlyStorageHandler } from '../handlers/storage-handler'
import { Enricher, IdCookieConfig } from '../types'
import { Enricher, State } from '../types'

type Input = { idCookie?: IdCookieConfig, peopleVerifiedId?: string }
type Output = { resolvedIdCookie: string | null }
type Input = Pick<State, 'idCookie' | 'peopleVerifiedId'>
type Output = Pick<State, 'resolvedIdCookie'>

export function enrichIdCookie(
storageHandler: WrappedReadOnlyStorageHandler
): Enricher<Input, Output> {
return state => {
let resolvedIdCookie: string | null

if (state.idCookie?.mode === 'provided' && state.idCookie?.strategy === 'cookie' && typeof state.idCookie?.name === 'string') {
resolvedIdCookie = storageHandler.getCookie(state.idCookie.name)
} else if (state.idCookie?.mode === 'provided' && state.idCookie?.strategy === 'localStorage' && typeof state.idCookie?.name === 'string') {
resolvedIdCookie = storageHandler.getDataFromLocalStorage(state.idCookie.name)
if (state.idCookie?.strategy === 'cookie' && typeof state.idCookie?.name === 'string') {
return { ...state, resolvedIdCookie: storageHandler.getCookie(state.idCookie.name) }
} else if (state.idCookie?.strategy === 'localStorage' && typeof state.idCookie?.name === 'string') {
return { ...state, resolvedIdCookie: storageHandler.getDataFromLocalStorage(state.idCookie.name) }
} else {
resolvedIdCookie = state.peopleVerifiedId ?? null
return state
}

return { ...state, resolvedIdCookie }
}
}
43 changes: 27 additions & 16 deletions src/idex.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { toParams } from './utils/url'
import { isFunction, isObject } from 'live-connect-common'
import { asParamOrEmpty, asStringParamWhen, asStringParam, mapAsParams } from './utils/params'
import { asParamOrEmpty, asStringParamWhen, asStringParam, mapAsParams, encodeIdCookieParam } from './utils/params'
import { DEFAULT_IDEX_AJAX_TIMEOUT, DEFAULT_IDEX_URL, DEFAULT_REQUESTED_ATTRIBUTES } from './utils/consts'
import { IdentityResolutionConfig, State, ResolutionParams, EventBus, RetrievedIdentifier } from './types'
import { WrappedCallHandler } from './handlers/call-handler'
import { md5 } from 'tiny-hashes/dist'

const ID_COOKIE_ATTR = 'idCookie'

export type ResolutionMetadata = {
expiresAt?: Date
Expand All @@ -22,8 +23,9 @@ export class IdentityResolver {
requestedAttributes: string[]
tuples: [string, string][]
privacyMode: boolean
resolvedIdCookie: string | null
idCookieMode: 'provided' | 'generated'
resolvedIdCookie?: string | null
generateIdCookie: boolean
peopleVerifiedId?: string

constructor (
config: State,
Expand All @@ -43,7 +45,9 @@ export class IdentityResolver {
this.requestedAttributes = this.idexConfig.requestedAttributes || DEFAULT_REQUESTED_ATTRIBUTES
this.privacyMode = nonNullConfig.privacyMode ?? false
this.resolvedIdCookie = nonNullConfig.resolvedIdCookie
this.idCookieMode = nonNullConfig.idCookie?.mode ?? 'generated'
this.generateIdCookie = this.idexConfig.idCookieMode === 'generated'
this.peopleVerifiedId = nonNullConfig.peopleVerifiedId

this.tuples = []

this.tuples.push(...asStringParam('duid', nonNullConfig.peopleVerifiedId))
Expand All @@ -55,10 +59,7 @@ export class IdentityResolver {
this.tuples.push(...asStringParam('gpp_s', nonNullConfig.gppString))
this.tuples.push(...asStringParam('gpp_as', nonNullConfig.gppApplicableSections?.join(',')))
this.tuples.push(...asStringParam('cd', nonNullConfig.cookieDomain))

if (this.idCookieMode === 'provided' && this.resolvedIdCookie) {
this.tuples.push(...asStringParam('ic', md5(this.resolvedIdCookie)))
}
this.tuples.push(...encodeIdCookieParam(nonNullConfig.resolvedIdCookie))

this.externalIds.forEach(retrievedIdentifier => {
this.tuples.push(...asStringParam(retrievedIdentifier.name, retrievedIdentifier.value))
Expand All @@ -72,7 +73,7 @@ export class IdentityResolver {
private attributeResolutionAllowed(attribute: string): boolean {
if (attribute === 'uid2') {
return !this.privacyMode
} else if (attribute === 'idcookie') {
} else if (attribute === ID_COOKIE_ATTR) {
// cannot be resolved server-side
return false
} else {
Expand All @@ -90,16 +91,22 @@ export class IdentityResolver {
})
}

private enrichExtraIdentifiers<T extends object>(response: T, params: [string, string][]): T & { idcookie?: string } {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private enrichExtraIdentifiers(response: Record<any, any>, params: [string, string][]): Record<any, any> {
const requestedAttributes = params.filter(([key]) => key === 'resolve').map(([, value]) => value)

function requested(attribute: string): boolean {
return requestedAttributes.indexOf(attribute) > -1
}

let result = response
const result = { ...response }

if (requested('idcookie') && this.resolvedIdCookie) {
result = { ...result, idcookie: this.resolvedIdCookie }
if (requested(ID_COOKIE_ATTR)) {
if (this.generateIdCookie && this.peopleVerifiedId) {
result[ID_COOKIE_ATTR] = this.peopleVerifiedId
} else if (this.resolvedIdCookie) {
result[ID_COOKIE_ATTR] = this.resolvedIdCookie
}
}

return result
Expand All @@ -110,10 +117,14 @@ export class IdentityResolver {
params: [string, string][]
): ((responseText: string, response: unknown) => void) {
return (responseText, response) => {
let responseObj = {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let responseObj: Record<any, any> = {}
if (responseText) {
try {
responseObj = JSON.parse(responseText)
const responseJson = JSON.parse(responseText)
if (isObject(responseJson)) {
responseObj = responseJson
}
} catch (ex) {
console.error('Error parsing response', ex)
this.eventBus.emitError('IdentityResolverParser', ex)
Expand Down
11 changes: 3 additions & 8 deletions src/pixel/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { base64UrlEncode } from '../utils/b64'
import { replacer } from './stringify'
import { fiddle, mergeObjects } from './fiddler'
import { isObject, trim, isArray, nonNull } from 'live-connect-common'
import { asStringParam, asParamOrEmpty, asStringParamWhen, asStringParamTransform } from '../utils/params'
import { asStringParam, asParamOrEmpty, asStringParamWhen, asStringParamTransform, encodeIdCookieParam } from '../utils/params'
import { toParams } from '../utils/url'
import { EventBus, State } from '../types'
import { collectUrl } from './url-collector'
import { md5 } from 'tiny-hashes/dist'

type ParamExtractor = (state: State) => [string, string][]

Expand All @@ -23,10 +22,6 @@ function ifDefined<K extends keyof State>(key: K, fun: (value: NonNullable<State
}
}

function ifState(predicate: (state: State) => boolean, extractor: ParamExtractor): ParamExtractor {
return state => predicate(state) ? extractor(state) : []
}

const paramExtractors: ParamExtractor[] = [
ifDefined('appId', aid => asStringParam('aid', aid)),
ifDefined('distributorId', did => asStringParam('did', did)),
Expand Down Expand Up @@ -75,7 +70,7 @@ const paramExtractors: ParamExtractor[] = [
ifDefined('gppString', gppString => asStringParam('gpp_s', gppString)),
ifDefined('gppApplicableSections', gppApplicableSections => asStringParamTransform('gpp_as', gppApplicableSections, (gppAs) => gppAs.join(','))),
ifDefined('cookieDomain', d => asStringParam('cd', d)),
ifState(state => state.idCookie?.mode === 'provided', ifDefined('resolvedIdCookie', p => asStringParam('ic', md5(p))))
(state) => encodeIdCookieParam(state.resolvedIdCookie)
]

export class Query {
Expand Down Expand Up @@ -111,7 +106,7 @@ export class StateWrapper {
} catch (e) {
console.error(e)
eventBus.emitErrorWithMessage('StateCombineWith', 'Error while extracting event data', e)
return { resolvedIdCookie: null }
return {}
}
}

Expand Down
18 changes: 6 additions & 12 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,15 @@ export interface IdentityResolutionConfig {
ajaxTimeout?: number
source?: string
publisherId?: number
requestedAttributes?: string[]
requestedAttributes?: string[],
idCookieMode?: 'generated' | 'provided'
}

export interface ProvidedIdCookieConfig {
mode: 'provided'
strategy: 'cookie' | 'localStorage'
name: string
}

export interface GeneratedIdCookieConfig {
mode: 'generated'
export type IdCookieConfig = {
strategy?: 'cookie' | 'localStorage'
name?: string
}

export type IdCookieConfig = ProvidedIdCookieConfig | GeneratedIdCookieConfig

export interface LiveConnectConfig {
appId?: string
wrapperName?: string
Expand Down Expand Up @@ -87,7 +81,7 @@ export interface State extends LiveConnectConfig {
privacyMode?: boolean
referrer?: string
cookieDomain?: string
resolvedIdCookie: string | null
resolvedIdCookie?: string | null // null signals failure to resolve
}

export interface ConfigMismatch {
Expand Down
11 changes: 11 additions & 0 deletions src/utils/params.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isNonEmpty, isObject, isArray, isFunction } from 'live-connect-common'
import { md5 } from 'tiny-hashes/dist'

export function asParamOrEmpty<A>(param: string, value: A, transform: (a: NonNullable<A>) => string): [string, string][] {
return isNonEmpty(value) ? [[param, transform(value)]] : []
Expand Down Expand Up @@ -34,3 +35,13 @@ export function mapAsParams(paramsMap: Record<string, string | string[]>): [stri
return []
}
}

export function encodeIdCookieParam(value: string | undefined | null): [string, string][] {
if (value !== null && value !== undefined) {
return asStringParam('ic', md5(value))
} else if (value === null) {
return [['ic', '']]
} else {
return []
}
}
2 changes: 1 addition & 1 deletion src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const toParams = (tuples: ([string, string][])) => {
let acc = ''
tuples.forEach((tuple) => {
const operator = acc.length === 0 ? '?' : '&'
if (tuple && tuple.length && tuple.length === 2 && tuple[0] && tuple[1]) {
if (tuple && tuple.length && tuple.length === 2 && tuple[0]) {
acc = `${acc}${operator}${tuple[0]}=${tuple[1]}`
}
})
Expand Down
11 changes: 0 additions & 11 deletions test/unit/enricher/idcookie.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,4 @@ describe('IdentifiersEnricher', () => {

expect(result.resolvedIdCookie).to.eql(value)
})

it('enrich peopleVerifiedId', () => {
const value = 'bar'
const result = enrichIdCookie(storageHandler)({ idCookie: { mode: 'generated' }, peopleVerifiedId: value })
expect(result.resolvedIdCookie).to.eql(value)
})

it('return null when set to generated and peopleVerifiedId missing', () => {
const result = enrichIdCookie(storageHandler)({ idCookie: { mode: 'generated' } })
expect(result.resolvedIdCookie).to.null()
})
})
5 changes: 2 additions & 3 deletions test/unit/events/error-pixel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('ErrorPixel', () => {
it('should register itself on the global bus', () => {
const pixelSender = {} as PixelSender

errorPixel.register({ collectorUrl: 'http://localhost', resolvedIdCookie: null }, pixelSender, eventBus)
errorPixel.register({ collectorUrl: 'http://localhost' }, pixelSender, eventBus)
// @ts-expect-error
const errorHandler = eventBus.data.h
expect(errorHandler).to.have.key(ERRORS_CHANNEL)
Expand All @@ -44,8 +44,7 @@ describe('ErrorPixel', () => {

errorPixel.register({
collectorUrl: 'http://localhost',
pageUrl: 'https://www.example.com/?sad=0&dsad=iou',
resolvedIdCookie: null
pageUrl: 'https://www.example.com/?sad=0&dsad=iou'
}, pixelSender, eventBus)
eventBus.emitErrorWithMessage('Error', 'some other message')
expect(errors.length).to.eql(1)
Expand Down
Loading

0 comments on commit bf1834b

Please sign in to comment.