Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .changeset/weak-lobsters-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@elbwalker/walker.js': patch
'@elbwalker/utils': patch
---

Support multiple consent states for session detection [#497](https://github.com/elbwalker/walkerOS/issues/497)
7 changes: 7 additions & 0 deletions .changeset/wicked-donkeys-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@elbwalker/walker.js': patch
'@elbwalker/utils': patch
---

Duplicate session starts on multiple consent updates
[#496](https://github.com/elbwalker/walkerOS/issues/496)
66 changes: 64 additions & 2 deletions packages/sources/walkerjs/src/__tests__/session.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Walkerjs } from '..';
import type { SourceWalkerjs } from '..';
import { elb, Walkerjs } from '..';
import { mockDataLayer } from '@elbwalker/jest/web.setup';
import { sessionStart } from '@elbwalker/utils/web';
import type { SourceWalkerjs } from '..';

jest.mock('@elbwalker/utils/web', () => {
const utilsOrg = jest.requireActual('@elbwalker/utils/web');
Expand Down Expand Up @@ -78,4 +78,66 @@ describe('Session', () => {

expect(mockFn).toHaveBeenCalledTimes(2);
});

test('different consent keys', () => {
walkerjs = Walkerjs({
default: true,
session: { consent: ['marketing', 'analytics'], storage: true },
pageview: false,
});

expect(mockDataLayer).toHaveBeenCalledTimes(0);
elb('walker consent', { marketing: false, analytics: true });

expect(mockDataLayer).toHaveBeenCalledTimes(1);
expect(mockDataLayer).toHaveBeenCalledWith(
expect.objectContaining({
event: 'session start',
data: expect.objectContaining({
storage: true, // Prefer granted consent
}),
}),
);
});

test('multiple consent updates', () => {
const originalLocation = window.location;
Object.defineProperty(window, 'location', {
value: new URL('https://www.elbwalker.com/?utm_campaign=foo'),
});

walkerjs = Walkerjs({
default: true,
session: { consent: 'marketing', storage: true },
pageview: false,
});

expect(mockDataLayer).toHaveBeenCalledTimes(0);
elb('walker consent', { marketing: true });
elb('walker consent', { marketing: true });
elb('walker consent', { marketing: true });

expect(mockDataLayer).toHaveBeenCalledTimes(1);
expect(mockDataLayer).toHaveBeenCalledWith(
expect.objectContaining({
event: 'session start',
data: expect.any(Object),
}),
);

elb('walker run');
expect(mockDataLayer).toHaveBeenCalledTimes(2);
expect(mockDataLayer).toHaveBeenCalledWith(
expect.objectContaining({
event: 'session start',
data: expect.objectContaining({
count: 2,
}),
}),
);

Object.defineProperty(window, 'location', {
value: originalLocation,
});
});
});
2 changes: 1 addition & 1 deletion packages/sources/walkerjs/src/lib/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function sessionStart(
instance.session = session;

// Run on session events
onApply(instance as SourceWalkerjs.Instance, 'session');
onApply(instance, 'session');
}

return result;
Expand Down
8 changes: 8 additions & 0 deletions packages/utils/src/__tests__/web/sessionStart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,12 @@ describe('sessionStart', () => {
const session = sessionStart({ data: { isNew: true, isStart: true } });
expect(mockElb).toHaveBeenCalledWith('session start', session);
});

test('multiple consent keys', () => {
sessionStart({ consent: ['foo', 'bar'] });
expect(mockElb).toHaveBeenCalledWith('walker on', 'consent', {
foo: expect.any(Function),
bar: expect.any(Function),
});
});
});
39 changes: 31 additions & 8 deletions packages/utils/src/web/session/sessionStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import type { WalkerOS } from '@elbwalker/types';
import type { SessionStorageConfig } from './';
import { sessionStorage, sessionWindow } from './';
import { elb as elbOrg } from '../elb';
import { isArray, isDefined, isString } from '../../core/is';
import { getGrantedConsent } from '../../core';

export interface SessionConfig extends SessionStorageConfig {
consent?: string;
consent?: string | string[];
storage?: boolean;
cb?: SessionCallback | false;
instance?: WalkerOS.Instance;
Expand All @@ -26,10 +28,15 @@ export function sessionStart(

// Consent
if (consent) {
// require consent
elb('walker on', 'consent', {
[consent]: onConsentFn(config, cb),
});
const consentHandler = onConsentFn(config, cb);

const consentConfig = (
isArray(consent) ? consent : [consent]
).reduce<WalkerOS.AnyObject>(
(acc, key) => ({ ...acc, [key]: consentHandler }),
{},
);
elb('walker on', 'consent', consentConfig);
} else {
// just do it
return callFuncAndCb(sessionFn(config), instance, cb);
Expand All @@ -47,12 +54,28 @@ function callFuncAndCb(
}

function onConsentFn(config: SessionConfig, cb?: SessionCallback | false) {
// Track the last processed group to prevent duplicate processing
let lastProcessedGroup: string | undefined;

const func = (instance: WalkerOS.Instance, consent: WalkerOS.Consent) => {
// Skip if we've already processed this group
if (isDefined(lastProcessedGroup) && lastProcessedGroup === instance?.group)
return;

// Remember this group has been processed
lastProcessedGroup = instance?.group;

let sessionFn: SessionFunction = () => sessionWindow(config); // Window by default

if (config.consent && consent[config.consent])
// Use storage if consent is granted
sessionFn = () => sessionStorage(config);
if (config.consent) {
const consentKeys = (
isArray(config.consent) ? config.consent : [config.consent]
).reduce<WalkerOS.Consent>((acc, key) => ({ ...acc, [key]: true }), {});

if (getGrantedConsent(consentKeys, consent))
// Use storage if consent is granted
sessionFn = () => sessionStorage(config);
}

return callFuncAndCb(sessionFn(), instance, cb);
};
Expand Down
9 changes: 6 additions & 3 deletions website/docs/sources/walkerjs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ const walkerjs = Walkerjs({
consent: 'functional', // Require functional consent
},
elb: 'elb', // Assign the elb function to the window
globals: {
globalsStatic: {
tagged: false, // A static global attribute
},
pageview: true, // Trigger a page view event with each run
run: true, // Automatically start running
session: {
storage: true, // Use the storage for session detection
consent: 'marketing', // Require marketing consent to access storage
consent: ['analytics', 'marketing'], // Required consent states to access storage
length: 60, // Session length in minutes
},
sessionStatic: { id: 's3ss10n', device: 'd3v1c3' }, // Static session data
tagging: 1, // Current version of the tagging
user: { id: '1d', device: 'overruled' }, // Static user data
});
```

Expand All @@ -37,12 +39,13 @@ const walkerjs = Walkerjs({
| default | boolean | Add dataLayer destination and run automatically |
| elb | string | Name of assign the elb function to the window |
| elbLayer | object | Public elbwalker API for async communication |
| globals | object | Static attributes added to each event |
| globalsStatic | object | Static default attributes added to each event |
| instance | string | Name of the walker.js instance to assign to the window |
| pageview | boolean | Trigger a page view event by default |
| prefix | string | Attributes prefix used by walker |
| run | boolean | Automatically start running |
| session | false or<br />SessionConfig | Configuration for session detection |
| sessionStatic | object | Static default session data |
| tagging | number | Current version of the tagging setup |
| user | object | Setting the user ids including id, device, and session |

Expand Down
14 changes: 7 additions & 7 deletions website/docs/utils/session.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ access permissions.

## Session Data

Depending on enabling the `storage` parameter, the `sessionStart` function
Depending on the `storage` parameter `consent`, the `sessionStart` function
returns an object with several properties. If a new session is detected,
`isStart` is set to `true`, otherwise `false`.

Expand All @@ -50,8 +50,8 @@ returns an object with several properties. If a new session is detected,
| marketing | true | If the session was started by a marketing parameter |
| referrer | string | Hostname of the referring site if available |

With `storage: true` and eventually granted `consent`, the returning object will
be extended with the following:
With `storage: true` and granted `consent`, the returning object will be
extended with the following:

| Property | Type | Description |
| -------- | ------- | ------------------------------------------------- |
Expand Down Expand Up @@ -104,7 +104,7 @@ are optional for customization:

| Parameter | Type | Description |
| ------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------- |
| [consent](#consent) | string | The consent state to permit or deny storage access |
| [consent](#consent) | Array&lt;string&gt; | The consent state to permit or deny storage access |
| [storage](#storage) | boolean | If the storage should be used |
| [cb](#callback) | false or<br />function | Callback function that gets called after the detection process. <br />Or to disable default callback |

Expand All @@ -119,8 +119,8 @@ There are additional config parameters [for storage](#sessionstorage) and

Setting a consent state to wait for before detecting a new session is used to
decide if storage access is allowed or not. If set, it registers an
[on consent event](/docs/sources/walkerjs/commands#on) and won't start until
that consent choice is available. If permission was granted, the
[on consent event](/docs/sources/walkerjs/commands#on) and won't start until a
consent choice is available. If at least one permission was granted, the
`sessionStorage` detects a new session; otherwise, the `sessionWindow`.

```js
Expand Down Expand Up @@ -164,7 +164,7 @@ Based on the [storage](#storage) option either the
[sessionStorage](#sessionstorage) or the [sessionWindow](#sessionwindow) is used
to detect a new session. If a [consent](#consent) state is set, the session
detection gets scheduled via an [on-consent](/docs/sources/walkerjs/commands#on)
command.
command. It will only run once per `run`.

```mermaid
---
Expand Down
Loading