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

Commit

Permalink
[CM-1106] Do Not Send Data To Our Backend If GDPR or GPP section 2 ap…
Browse files Browse the repository at this point in the history
…plies (#317)

* [LiveConnect] Do Not Send Data To Our Backend If GDPR or GPP section 2 applies

* fix test

* Delete logs/bstack-wdio-service.log

* remove n3pc

* update doc

* Delete logs/bstack-wdio-service.log

* remove nb tag
  • Loading branch information
peixunzhang authored Jan 31, 2024
1 parent afa277e commit 287db48
Show file tree
Hide file tree
Showing 11 changed files with 67 additions and 71 deletions.
6 changes: 0 additions & 6 deletions COLLECTOR_PARAMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,6 @@
- the value of the `config.usPrivacyString` config parameter.
### `gdpr`
- the value of the `config.gdprApplies` config parameter.
### `n3pc`
- if present, it indicates to not send Set-Cookie headers in the response to set 3rd party cookies; derived from privacy settings (currently, only `config.gdprApplies` is evaluated).
### `n3pct`
- if present, it indicates to not send Set-Cookie headers in the response to update the TTL of existing 3rd party cookies; derived from privacy settings (currently, only `config.gdprApplies` is evaluated).
### `nb`
- if present, it indicates to not send bakers in the response; derived from privacy settings (currently, only `config.gdprApplies` is evaluated).
### `gdpr_consent`
- the value of the `config.gdprConsent` config parameter.
### `dtstmp`
Expand Down
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![dependencies Status](https://img.shields.io/librariesio/release/npm/live-connect)](https://github.com/LiveIntent/live-connect/tree/master)

## Main concepts
The LiveConnect module offers a convenient solution for generating and collecting first-party identifiers based on your preferences, and sending this information to a designated endpoint. With LiveConnect, you gain a straightforward interface that facilitates the collection of identifiers from web pages, as well as capturing user interactions alongside these identifiers.
The LiveConnect module offers a convenient solution for generating and collecting first-party identifiers based on your preferences and sending this information to a designated endpoint. With LiveConnect, you gain a straightforward interface that facilitates the collection of identifiers from web pages, as well as capturing user interactions alongside these identifiers.
If you're interested in reviewing the type of data being sent, please check [what is being sent](#what-is-being-sent) section of this documentation.

## Quick start
Expand All @@ -25,10 +25,10 @@ We welcome ideas, fixes, and improvements from the community. Discover how you c
## Testing
### Running Unit tests
Unit tests are written using [Mocha](http://mochajs.org/) and [Chai](http://chaijs.com/).
Check [Quick start](#quick-start) how to run them.
Check [Quick Start](#quick-start) how to run them.

### Running Browserstack tests
Tests sets the cookies on eTLD+1 domain. For that, execute the following command:
Tests set the cookies on eTLD+1 domain. For that, execute the following command:
```echo "127.0.0.1 bln.test.liveintent.com" | sudo tee -a /etc/hosts && echo "127.0.0.1 test.liveintent.com" | sudo tee -a /etc/hosts && echo "127.0.0.1 me.idex.com" | sudo tee -a /etc/hosts && echo "127.0.0.1 schmoogle.com" | sudo tee -a /etc/hosts && echo "127.0.0.1 framed.test.liveintent.com" | sudo tee -a /etc/hosts && echo "127.0.0.1 double-framed.test.liveintent.com" | sudo tee -a /etc/hosts && echo "127.0.0.1 baked.liveintent.com" | sudo tee -a /etc/hosts```

Add Browserstack keys to your env, where the setup would be as follows:
Expand All @@ -43,24 +43,24 @@ The browsers used in these tests are defined in `test-config/wdio.browserstack.c
___

## Initialization
The initialisation part should be straight forward, considering the snippet:
The initialization part should be straightforward, considering the snippet:
```javascript
import { LiveConnect } from 'live-connect-js'
const lc = LiveConnect(configOptions)
```

The object returned after initialisation (`lc` in the snippet above) is exposing the following functions:
The object returned after initialization (`lc` in the snippet above) is exposing the following functions:
- `push` accepts a custom event one would like to keep track of.
- `fire` just fires a pixel, and can be considered as a simple page view.
- `peopleVerifiedId` returns the most likely first party cookie that can be used for identity resolution.
- `ready` flag, saying that the LC was loaded and ready, can be used when including LiveConnect as a global var on the window object.
- `resolve` function accepts a success callback, an error callback and an additional object with key value pairs. Of course, errors during resolution will be emitted on the EventBus and sent to the collector. The third parameter is `additionalParameters` which is an object, and will be attached to the IdentityResolution request, split into key-value pairs. The purpose of this object is to include key-value pairs in the request, e.g. for identifiers that cannot be found in the cookie jar, or in LocalStorage, or simply there's a requirement for a certain identifier to be represented under a specific key which doesn't match its name in the cookie jar, or LocalStorage key.
- `resolve` function accepts a success callback, an error callback and an additional object with key value pairs. Of course, errors during resolution will be emitted on the EventBus and sent to the collector. The third parameter is `additionalParameters` which is an object and will be attached to the IdentityResolution request, split into key-value pairs. The purpose of this object is to include key-value pairs in the request, e.g. for identifiers that cannot be found in the cookie jar, or in LocalStorage, or simply there's a requirement for a certain identifier to be represented under a specific key which doesn't match its name in the cookie jar, or LocalStorage key.
- `resolutionCallUrl` function returns the URL to be called in order to receive the resolution to a stable identifier.

### Overriding the StorageHandler and CallHandler
LiveConnect is initialized in a way so that it does not manipulate storage and ajax on the device on its own.
LiveConnect is initialized in a way so that it does not manipulate storage and call on the device on its own.

The StorageHandler is an object with functions that adheres to the signature:
The StorageHandler is an object with functions that adhere to the signature:
- `function localStorageIsEnabled ()`
- `function getCookie (key)`
- `function getDataFromLocalStorage (key)`
Expand All @@ -79,7 +79,7 @@ where the `onload` is a `function()`
If one of the functions is not available in the external handler, LiveConnect will fall back to stubs to ensure that the overall functionality isn't being affected. It is recommended to provide full implementations of the interfaces. Default
implementations of the handlers can be found in the `live-connect-handlers` project.

With custom implementations the initialization can look like this:
With custom implementations, the initialization can look like this:
```javascript
import { LiveConnect } from 'live-connect-js'

Expand All @@ -93,15 +93,15 @@ const storageHandler = {
},
...
}
const callsHandler = {
const callHandler = {
ajaxGet: (url, responseHandler, fallback, timeout) => {
//
},
pixelGet: (url, onload) => {
//
}
}
const lc = LiveConnect(configOptions, storageHandler, ajaxHandler)
const lc = LiveConnect(configOptions, storageHandler, callHandler)
```

### Configuration options
Expand All @@ -113,19 +113,19 @@ The code in the `manager` folder is responsible for browser state interaction an
It's sometimes important in which order the managers are invoked, as one might depend on the result of another.

### Decisions manager
`managers/decision.js` is responsible for keeping state in the browser with all the recent `li_did` parameters picked up from urls where LiveConnect was loaded.
`managers/decision.js` is responsible for keeping the state in the browser with all the recent `li_did` parameters picked up from urls where LiveConnect was loaded.

### Identifiers manager
`managers/identifiers.js` takes care of LiveConnect first party identifiers being created (if not present) and picked up so that they can be sent as signal pixels containing that information.
Where the LiveConnect identifiers are stored (Cookie vs LocalStorage) depends on the `config.storageStrategy` option.
How long those identifiers will live is configured in the `config.expirationDays` parameter. In case the `storageStrategy` is set to Cookie, the browser will ensure that the cookie expires.
In case of localStorage, Identifiers Manager and it's underlying `utils/storage.js` helper will ensure that on the next load, the entry is removed from localstorage in case it's obsolete.
In case of localStorage, Identifiers Manager and its underlying `utils/storage.js` helper will ensure that on the next load, the entry is removed from localStorage in case it's obsolete.

### People-verified manager
`managers/people-verified.js` makes sure that either of the selected identifiers is stored as the `_li_duid` key in local storage, as some integrations are using the information stored there.

## Enrichments
The `enrichers` folder contains code responsible for extracting specific information about the page visit when the module is loaded. It makes sure that the extracted data is stored in the state which contains data which is sent as a single pixel.
The `enrichers` folder contains code responsible for extracting specific information about the page visit when the module is loaded. It makes sure that the extracted data is stored in the state which contains data that is sent as a single pixel.

### Page enrichment
`enrichers/page.js` holds the logic which determines the real page url on which we're trying to capture user interactions.
Expand All @@ -134,8 +134,8 @@ The `enrichers` folder contains code responsible for extracting specific informa
`enrichers/identifiers.js` is responsible for reading the `identifiersToResolve` configuration parameter to read any additional identifiers that customers want to share with us.

## Messaging between components via EventBus
LiveConnect exposes an object via the field `eventBus` on the LiveConnect instance which is responsible for communicating various information based on different fields of interests.
For example, there are three topics which anyone can hook to, and receive information about:
LiveConnect exposes an object via the field `eventBus` on the LiveConnect instance which is responsible for communicating various information based on different fields of interest.
For example, there are three topics that anyone can hook to, and receive information about:
- errors, on the `li_errors` topic
- whenever the pixel is sent successfully, the `lips` topic will emit that information
- just before the pixel is sent, `pre_lips` topic will contain the information about it.
Expand All @@ -147,16 +147,16 @@ lc.eventBus.on('lips', lipsLogger)
```
or
```javascript
const lipsLogger = (message) => { console.info('Received a lips message once, i will self destruct now.', message) }
const lipsLogger = (message) => { console.info('Received a lips message once, it will self destruct now.', message) }
lc.eventBus.once('lips', lipsLogger)
```

There are a two ways this can be achieved:
- `once` - will be triggered only once, and the handler which is subscribing to that topic will be automatically removed once the event is received and sent.
There are two ways this can be achieved:
- `on` - will be triggered every time the topic receives a message.
- `once` - will be triggered only once, and the handler that is subscribing to that topic will be automatically removed once the event is received and sent.

The bus isn't a pure event listener, as we also want to cover the case where handlers can be attached even after messages have been emitted.
An example how to hook to the topic will be explained in the next section.
An example of how to hook to the topic will be explained in the next section.

## Error handling
Vital logic is wrapped in try catch blocks, and where it makes sense, the error message is emitted on the `li_errors` topic in the eventBus.
Expand All @@ -167,14 +167,14 @@ lc.eventBus.on('li_errors', logger)
```

## Receiving errors on the collector
LiveConnect has a handler called `handlers/error-pixel.js` which is subscribed on the `li_errors` topic, and wraps the exceptions into the following format:
LiveConnect has a handler called `events/error-pixel.js` which is subscribed on the `li_errors` topic, and wraps the exceptions into the following format:
```javascript
{
message: e.message,
name: e.name,
stackTrace: e.stack,
lineNumber: e.lineNumber,
lineColumn: e.lineColumn,
columnNumber: e.columnNumber,
fileName: e.fileName
}
```
Expand Down
12 changes: 9 additions & 3 deletions src/handlers/call-handler.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { EventBus, CallHandler } from 'live-connect-common'
import { Wrapped, WrappingContext } from '../utils/wrapping'

const empty = () => undefined

function privacyCheck<K extends keyof CallHandler>(wrapper: WrappingContext<CallHandler>, privacyMode: boolean, functionName: K): Wrapped<CallHandler[K]> {
return (privacyMode) ? empty : wrapper.wrap(functionName)
}

export class WrappedCallHandler implements CallHandler {
private functions: {
ajaxGet: Wrapped<CallHandler['ajaxGet']>,
pixelGet: Wrapped<CallHandler['pixelGet']>
}

constructor (externalCallHandler: CallHandler, eventBus: EventBus) {
constructor (externalCallHandler: CallHandler, eventBus: EventBus, privacyMode: boolean) {
const wrapper = new WrappingContext(externalCallHandler, 'CallHandler', eventBus)

this.functions = {
ajaxGet: wrapper.wrap('ajaxGet'),
pixelGet: wrapper.wrap('pixelGet')
ajaxGet: privacyCheck(wrapper, privacyMode, 'ajaxGet'),
pixelGet: privacyCheck(wrapper, privacyMode, 'pixelGet')
}

wrapper.reportErrors()
Expand Down
1 change: 0 additions & 1 deletion src/idex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export class IdentityResolver {
.addOptional('duid', nonNullConfig.peopleVerifiedId)
.addOptional('us_privacy', nonNullConfig.usPrivacyString)
.addOptional('gdpr', onNonNull(nonNullConfig.gdprApplies, v => v ? 1 : 0))
.addOptional('n3pc', nonNullConfig.privacyMode ? 1 : undefined)
.addOptional('gdpr_consent', nonNullConfig.gdprConsent)
.addOptional('did', nonNullConfig.distributorId)
.addOptional('gpp_s', nonNullConfig.gppString)
Expand Down
2 changes: 1 addition & 1 deletion src/minimal-live-connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function _minimalInitialization(liveConnectConfig: LiveConnectConfig, externalSt

const storageHandler = WrappedReadOnlyStorageHandler.make(stateWithStorage.storageStrategy, externalStorageHandler, eventBus)

const callHandler = new WrappedCallHandler(externalCallHandler, eventBus)
const callHandler = new WrappedCallHandler(externalCallHandler, eventBus, enrichPrivacyMode(liveConnectConfig).privacyMode)

const enrichedState =
enrichIdentifiers(storageHandler, eventBus)(
Expand Down
3 changes: 0 additions & 3 deletions src/pixel/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,6 @@ export class StateWrapper {
.addOptional('us_privacy', state.usPrivacyString)
.addOptional('wpn', state.wrapperName)
.addOptional('gdpr', onNonNull(state.gdprApplies, v => v ? '1' : '0'))
.addOptional('n3pc', state.privacyMode ? '1' : undefined)
.addOptional('n3pct', state.privacyMode ? '1' : undefined)
.addOptional('nb', state.privacyMode ? '1' : undefined)
.addOptional('gdpr_consent', state.gdprConsent)
.addOptional('refr', state.referrer)
.addOptional('c', state.contextElements)
Expand Down
2 changes: 1 addition & 1 deletion src/standard-live-connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function standardInitialization(liveConnectConfig: LiveConnectConfig, externalSt
contextElementsLength: liveConnectConfig.contextElementsLength || 0
}

const callHandler = new WrappedCallHandler(externalCallHandler, eventBus)
const callHandler = new WrappedCallHandler(externalCallHandler, eventBus, enrichPrivacyMode(liveConnectConfig).privacyMode)

const stateWithStorage =
enrichPage(enrichStorageStrategy(enrichPrivacyMode(validLiveConnectConfig)))
Expand Down
29 changes: 25 additions & 4 deletions test/unit/handlers/call-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import sinon, { SinonStub } from 'sinon'
import { WrappedCallHandler } from '../../../src/handlers/call-handler'
import dirtyChai from 'dirty-chai'
import { LocalEventBus } from '../../../src/events/event-bus'
import { EventBus } from 'live-connect-common'
import { CallHandler, EventBus } from 'live-connect-common'

use(dirtyChai)

Expand All @@ -13,6 +13,7 @@ type RecordedError = { 'name': string; 'message': string; 'exception': unknown }
describe('CallHandler', () => {
let emitterErrors: RecordedError[] = []
let eventBusStub: SinonStub<[string, string, unknown?], EventBus>

const eventBus = LocalEventBus()
const sandbox = sinon.createSandbox()

Expand Down Expand Up @@ -42,7 +43,7 @@ describe('CallHandler', () => {

const ajaxGet = () => { ajaxCounter += 1 }
const pixelGet = () => { pixelCounter += 1 }
const handler = new WrappedCallHandler({ ajaxGet, pixelGet }, eventBus)
const handler = new WrappedCallHandler({ ajaxGet, pixelGet }, eventBus, false)

handler.ajaxGet('foo', () => undefined)
expect(ajaxCounter).to.be.eql(1)
Expand All @@ -55,7 +56,7 @@ describe('CallHandler', () => {

it('should send an error if an external handler is not provided', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _ = new WrappedCallHandler({}, eventBus)
const _ = new WrappedCallHandler({}, eventBus, false)
expect(emitterErrors.length).to.be.eq(1)
expect(emitterErrors[0].name).to.be.eq('CallHandler')
expect(emitterErrors[0].message).to.be.eq('The functions \'["ajaxGet","pixelGet"]\' were not provided')
Expand All @@ -64,11 +65,31 @@ describe('CallHandler', () => {

it('should send an error if an external handler does not have a get function', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _ = new WrappedCallHandler({}, eventBus)
const _ = new WrappedCallHandler({}, eventBus, false)

expect(emitterErrors.length).to.be.eq(1)
expect(emitterErrors[0].name).to.be.eq('CallHandler')
expect(emitterErrors[0].message).to.be.eq('The functions \'["ajaxGet","pixelGet"]\' were not provided')
expect(emitterErrors[0].exception).to.be.undefined()
})

it('should not do anything when in privacy mode', (done) => {
let requestMade = false
let callBackCalled = false

const underlying: CallHandler = {
ajaxGet: () => { requestMade = true },
pixelGet: () => { requestMade = true }
}

const handler = new WrappedCallHandler(underlying, eventBus, true)
handler.ajaxGet('', () => { callBackCalled = true }, () => { callBackCalled = true })
handler.pixelGet('', () => { callBackCalled = true })

setTimeout(() => {
expect(callBackCalled).to.be.false()
expect(requestMade).to.be.false()
done()
}, 200)
})
})
18 changes: 1 addition & 17 deletions test/unit/idex/identity-resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,22 +197,6 @@ describe('IdentityResolver without cache', () => {
requestToComplete.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(response))
})

it('should attach n3pc when in privacy mode', (done) => {
const response = { id: 112233 }
const identityResolver = new IdentityResolver({
privacyMode: true,
distributorId: 'did-0001'
}, calls)
const successCallback = (responseAsJson: unknown) => {
expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?n3pc=1&did=did-0001')
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 return the default empty response and emit error if response is 500', (done) => {
const identityResolver = new IdentityResolver({}, calls)
const errorCallback = (error) => {
Expand Down Expand Up @@ -261,7 +245,7 @@ describe('IdentityResolver without cache', () => {
expect(callCount).to.be.eql(1)
expect(errors).to.be.empty()
expect(responseAsJson).to.be.eql(response)
expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?n3pc=1&resolve=md5')
expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?resolve=md5')
expect(responseAsJson).to.be.eql(response)
expect(callCount).to.be.eql(1)
done()
Expand Down
Loading

0 comments on commit 287db48

Please sign in to comment.