Skip to content

Commit d55ec09

Browse files
Merge pull request #498 from elbwalker/496-duplicate-session-starts-on-multiple-consent-updates
496 duplicate session starts on multiple consent updates
2 parents f0ccee6 + 9642bca commit d55ec09

File tree

8 files changed

+130
-21
lines changed

8 files changed

+130
-21
lines changed

.changeset/weak-lobsters-grab.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@elbwalker/walker.js': patch
3+
'@elbwalker/utils': patch
4+
---
5+
6+
Support multiple consent states for session detection [#497](https://github.com/elbwalker/walkerOS/issues/497)

.changeset/wicked-donkeys-obey.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@elbwalker/walker.js': patch
3+
'@elbwalker/utils': patch
4+
---
5+
6+
Duplicate session starts on multiple consent updates
7+
[#496](https://github.com/elbwalker/walkerOS/issues/496)

packages/sources/walkerjs/src/__tests__/session.test.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Walkerjs } from '..';
1+
import type { SourceWalkerjs } from '..';
2+
import { elb, Walkerjs } from '..';
23
import { mockDataLayer } from '@elbwalker/jest/web.setup';
34
import { sessionStart } from '@elbwalker/utils/web';
4-
import type { SourceWalkerjs } from '..';
55

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

7979
expect(mockFn).toHaveBeenCalledTimes(2);
8080
});
81+
82+
test('different consent keys', () => {
83+
walkerjs = Walkerjs({
84+
default: true,
85+
session: { consent: ['marketing', 'analytics'], storage: true },
86+
pageview: false,
87+
});
88+
89+
expect(mockDataLayer).toHaveBeenCalledTimes(0);
90+
elb('walker consent', { marketing: false, analytics: true });
91+
92+
expect(mockDataLayer).toHaveBeenCalledTimes(1);
93+
expect(mockDataLayer).toHaveBeenCalledWith(
94+
expect.objectContaining({
95+
event: 'session start',
96+
data: expect.objectContaining({
97+
storage: true, // Prefer granted consent
98+
}),
99+
}),
100+
);
101+
});
102+
103+
test('multiple consent updates', () => {
104+
const originalLocation = window.location;
105+
Object.defineProperty(window, 'location', {
106+
value: new URL('https://www.elbwalker.com/?utm_campaign=foo'),
107+
});
108+
109+
walkerjs = Walkerjs({
110+
default: true,
111+
session: { consent: 'marketing', storage: true },
112+
pageview: false,
113+
});
114+
115+
expect(mockDataLayer).toHaveBeenCalledTimes(0);
116+
elb('walker consent', { marketing: true });
117+
elb('walker consent', { marketing: true });
118+
elb('walker consent', { marketing: true });
119+
120+
expect(mockDataLayer).toHaveBeenCalledTimes(1);
121+
expect(mockDataLayer).toHaveBeenCalledWith(
122+
expect.objectContaining({
123+
event: 'session start',
124+
data: expect.any(Object),
125+
}),
126+
);
127+
128+
elb('walker run');
129+
expect(mockDataLayer).toHaveBeenCalledTimes(2);
130+
expect(mockDataLayer).toHaveBeenCalledWith(
131+
expect.objectContaining({
132+
event: 'session start',
133+
data: expect.objectContaining({
134+
count: 2,
135+
}),
136+
}),
137+
);
138+
139+
Object.defineProperty(window, 'location', {
140+
value: originalLocation,
141+
});
142+
});
81143
});

packages/sources/walkerjs/src/lib/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function sessionStart(
3838
instance.session = session;
3939

4040
// Run on session events
41-
onApply(instance as SourceWalkerjs.Instance, 'session');
41+
onApply(instance, 'session');
4242
}
4343

4444
return result;

packages/utils/src/__tests__/web/sessionStart.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,12 @@ describe('sessionStart', () => {
169169
const session = sessionStart({ data: { isNew: true, isStart: true } });
170170
expect(mockElb).toHaveBeenCalledWith('session start', session);
171171
});
172+
173+
test('multiple consent keys', () => {
174+
sessionStart({ consent: ['foo', 'bar'] });
175+
expect(mockElb).toHaveBeenCalledWith('walker on', 'consent', {
176+
foo: expect.any(Function),
177+
bar: expect.any(Function),
178+
});
179+
});
172180
});

packages/utils/src/web/session/sessionStart.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import type { WalkerOS } from '@elbwalker/types';
22
import type { SessionStorageConfig } from './';
33
import { sessionStorage, sessionWindow } from './';
44
import { elb as elbOrg } from '../elb';
5+
import { isArray, isDefined, isString } from '../../core/is';
6+
import { getGrantedConsent } from '../../core';
57

68
export interface SessionConfig extends SessionStorageConfig {
7-
consent?: string;
9+
consent?: string | string[];
810
storage?: boolean;
911
cb?: SessionCallback | false;
1012
instance?: WalkerOS.Instance;
@@ -26,10 +28,15 @@ export function sessionStart(
2628

2729
// Consent
2830
if (consent) {
29-
// require consent
30-
elb('walker on', 'consent', {
31-
[consent]: onConsentFn(config, cb),
32-
});
31+
const consentHandler = onConsentFn(config, cb);
32+
33+
const consentConfig = (
34+
isArray(consent) ? consent : [consent]
35+
).reduce<WalkerOS.AnyObject>(
36+
(acc, key) => ({ ...acc, [key]: consentHandler }),
37+
{},
38+
);
39+
elb('walker on', 'consent', consentConfig);
3340
} else {
3441
// just do it
3542
return callFuncAndCb(sessionFn(config), instance, cb);
@@ -47,12 +54,28 @@ function callFuncAndCb(
4754
}
4855

4956
function onConsentFn(config: SessionConfig, cb?: SessionCallback | false) {
57+
// Track the last processed group to prevent duplicate processing
58+
let lastProcessedGroup: string | undefined;
59+
5060
const func = (instance: WalkerOS.Instance, consent: WalkerOS.Consent) => {
61+
// Skip if we've already processed this group
62+
if (isDefined(lastProcessedGroup) && lastProcessedGroup === instance?.group)
63+
return;
64+
65+
// Remember this group has been processed
66+
lastProcessedGroup = instance?.group;
67+
5168
let sessionFn: SessionFunction = () => sessionWindow(config); // Window by default
5269

53-
if (config.consent && consent[config.consent])
54-
// Use storage if consent is granted
55-
sessionFn = () => sessionStorage(config);
70+
if (config.consent) {
71+
const consentKeys = (
72+
isArray(config.consent) ? config.consent : [config.consent]
73+
).reduce<WalkerOS.Consent>((acc, key) => ({ ...acc, [key]: true }), {});
74+
75+
if (getGrantedConsent(consentKeys, consent))
76+
// Use storage if consent is granted
77+
sessionFn = () => sessionStorage(config);
78+
}
5679

5780
return callFuncAndCb(sessionFn(), instance, cb);
5881
};

website/docs/sources/walkerjs/configuration.mdx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,19 @@ const walkerjs = Walkerjs({
1515
consent: 'functional', // Require functional consent
1616
},
1717
elb: 'elb', // Assign the elb function to the window
18-
globals: {
18+
globalsStatic: {
1919
tagged: false, // A static global attribute
2020
},
2121
pageview: true, // Trigger a page view event with each run
2222
run: true, // Automatically start running
2323
session: {
2424
storage: true, // Use the storage for session detection
25-
consent: 'marketing', // Require marketing consent to access storage
25+
consent: ['analytics', 'marketing'], // Required consent states to access storage
2626
length: 60, // Session length in minutes
2727
},
28+
sessionStatic: { id: 's3ss10n', device: 'd3v1c3' }, // Static session data
2829
tagging: 1, // Current version of the tagging
30+
user: { id: '1d', device: 'overruled' }, // Static user data
2931
});
3032
```
3133

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

website/docs/utils/session.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ access permissions.
3737

3838
## Session Data
3939

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

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

53-
With `storage: true` and eventually granted `consent`, the returning object will
54-
be extended with the following:
53+
With `storage: true` and granted `consent`, the returning object will be
54+
extended with the following:
5555

5656
| Property | Type | Description |
5757
| -------- | ------- | ------------------------------------------------- |
@@ -104,7 +104,7 @@ are optional for customization:
104104

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

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

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

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

169169
```mermaid
170170
---

0 commit comments

Comments
 (0)