diff --git a/common/changes/@snowplow/browser-tracker-core/issue-1124-client_session_2022-12-23-13-19.json b/common/changes/@snowplow/browser-tracker-core/issue-1124-client_session_2022-12-23-13-19.json new file mode 100644 index 000000000..68088f330 --- /dev/null +++ b/common/changes/@snowplow/browser-tracker-core/issue-1124-client_session_2022-12-23-13-19.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@snowplow/browser-tracker-core", + "comment": "Add client_session context entity if anonymous tracking with session tracking is enabled (#1124)", + "type": "none" + } + ], + "packageName": "@snowplow/browser-tracker-core" +} \ No newline at end of file diff --git a/libraries/browser-tracker-core/src/tracker/id_cookie.ts b/libraries/browser-tracker-core/src/tracker/id_cookie.ts index 6277d8f50..c350f8f5d 100644 --- a/libraries/browser-tracker-core/src/tracker/id_cookie.ts +++ b/libraries/browser-tracker-core/src/tracker/id_cookie.ts @@ -300,14 +300,20 @@ export function serializeIdCookie(idCookie: ParsedIdCookie) { * @param configStateStorageStrategy Cookie storage strategy * @returns Client session context entity */ -export function clientSessionFromIdCookie(idCookie: ParsedIdCookie, configStateStorageStrategy: string) { +export function clientSessionFromIdCookie( + idCookie: ParsedIdCookie, + configStateStorageStrategy: string, + configAnonymousTracking: boolean +) { const firstEventTsInMs = idCookie[firstEventTsInMsIndex]; const clientSession: ClientSession = { - userId: idCookie[domainUserIdIndex], + userId: configAnonymousTracking + ? '00000000-0000-0000-0000-000000000000' // TODO: use uuid.NIL when we upgrade to uuid v8.3 + : idCookie[domainUserIdIndex], sessionId: idCookie[sessionIdIndex], eventIndex: idCookie[eventIndexIndex], sessionIndex: idCookie[visitCountIndex], - previousSessionId: idCookie[previousSessionIdIndex] || null, + previousSessionId: configAnonymousTracking ? null : idCookie[previousSessionIdIndex] || null, storageMechanism: configStateStorageStrategy == 'localStorage' ? 'LOCAL_STORAGE' : 'COOKIE_1', firstEventId: idCookie[firstEventIdIndex] || null, firstEventTimestamp: firstEventTsInMs ? new Date(firstEventTsInMs).toISOString() : null, diff --git a/libraries/browser-tracker-core/src/tracker/index.ts b/libraries/browser-tracker-core/src/tracker/index.ts index b2bcb4362..1952485fd 100755 --- a/libraries/browser-tracker-core/src/tracker/index.ts +++ b/libraries/browser-tracker-core/src/tracker/index.ts @@ -787,8 +787,11 @@ export function Tracker( // Add the page URL last as it may take us over the IE limit (and we don't always need it) payloadBuilder.add('url', purify(configCustomUrl || locationHrefAlias)); - if (configSessionContext && !configAnonymousSessionTracking && !configAnonymousTracking) { - addSessionContextToPayload(payloadBuilder, clientSessionFromIdCookie(idCookie, configStateStorageStrategy)); + if (configSessionContext && (!configAnonymousTracking || configAnonymousSessionTracking)) { + addSessionContextToPayload( + payloadBuilder, + clientSessionFromIdCookie(idCookie, configStateStorageStrategy, configAnonymousTracking) + ); } // Update cookies diff --git a/libraries/browser-tracker-core/test/id_cookie.test.ts b/libraries/browser-tracker-core/test/id_cookie.test.ts index cca0fcd09..aadbea178 100644 --- a/libraries/browser-tracker-core/test/id_cookie.test.ts +++ b/libraries/browser-tracker-core/test/id_cookie.test.ts @@ -262,7 +262,7 @@ describe('serializeIdCookie', () => { describe('clientSessionFromIdCookie', () => { it('Correctly fills out the properties', () => { let idCookie = parseIdCookie('def.1653632272.10.1653632282.1653632262.ses.previous.fid.1653638673483.9', '', '', 0); - let clientSession = clientSessionFromIdCookie(idCookie, 'cookieAndLocalStorage'); + let clientSession = clientSessionFromIdCookie(idCookie, 'cookieAndLocalStorage', false); expect(clientSession.userId).toBe('def'); expect(clientSession.sessionId).toBe('ses'); @@ -273,4 +273,18 @@ describe('clientSessionFromIdCookie', () => { expect(clientSession.firstEventId).toBe('fid'); expect(clientSession.firstEventTimestamp).toBe('2022-05-27T08:04:33.483Z'); }); + + it('Anonymises userId and previousSessionId when anonymous tracking', () => { + let idCookie = parseIdCookie('def.1653632272.10.1653632282.1653632262.ses.previous.fid.1653638673483.9', '', '', 0); + let clientSession = clientSessionFromIdCookie(idCookie, 'cookieAndLocalStorage', true); + + expect(clientSession.userId).toBe('00000000-0000-0000-0000-000000000000'); + expect(clientSession.sessionId).toBe('ses'); + expect(clientSession.previousSessionId).toBeNull; + expect(clientSession.eventIndex).toBe(9); + expect(clientSession.sessionIndex).toBe(10); + expect(clientSession.storageMechanism).toBe('COOKIE_1'); + expect(clientSession.firstEventId).toBe('fid'); + expect(clientSession.firstEventTimestamp).toBe('2022-05-27T08:04:33.483Z'); + }); }); diff --git a/libraries/browser-tracker-core/test/tracker/session_data.test.ts b/libraries/browser-tracker-core/test/tracker/session_data.test.ts index d000b30a2..c64a991fd 100644 --- a/libraries/browser-tracker-core/test/tracker/session_data.test.ts +++ b/libraries/browser-tracker-core/test/tracker/session_data.test.ts @@ -28,6 +28,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import { TrackerConfiguration } from '../../dist/index.module'; import { addTracker, SharedState } from '../../src'; import { createTestIdCookie, createTestSessionIdCookie } from '../helpers'; @@ -54,7 +55,7 @@ describe('Tracker API: ', () => { }); it('Sets initial domain session index on first session', () => { - const tracker = addTracker('sp1', 'sp1', '', '', new SharedState()); + const tracker = createTracker(); expect(tracker?.getDomainSessionIndex()).toEqual(1); }); @@ -62,7 +63,7 @@ describe('Tracker API: ', () => { it('Sets correct domain session index on new session', () => { const initialSessionIndex = 1; document.cookie = createTestIdCookie({ visitCount: initialSessionIndex }); - const tracker = addTracker('sp2', 'sp2', '', '', new SharedState()); + const tracker = createTracker(); expect(tracker?.getDomainSessionIndex()).toEqual(initialSessionIndex + 1); }); @@ -70,7 +71,7 @@ describe('Tracker API: ', () => { it('Sets correct domain session index on existing session', () => { const initialSessionIndex = 2; document.cookie = createTestIdCookie({ visitCount: initialSessionIndex }) + ' ' + createTestSessionIdCookie(); - const tracker = addTracker('sp3', 'sp3', '', '', new SharedState()); + const tracker = createTracker(); expect(tracker?.getDomainSessionIndex()).toEqual(initialSessionIndex); }); @@ -78,7 +79,7 @@ describe('Tracker API: ', () => { it('Sets correct domain session index (1) after clearUserData() on existing session', () => { const initialSessionIndex = 2; document.cookie = createTestIdCookie({ visitCount: initialSessionIndex }) + ' ' + createTestSessionIdCookie(); - const tracker = addTracker('sp4', 'sp4', '', '', new SharedState()); + const tracker = createTracker(); expect(tracker?.getDomainSessionIndex()).toEqual(initialSessionIndex); tracker?.clearUserData(); @@ -86,13 +87,13 @@ describe('Tracker API: ', () => { }); it('Sets correct domain session index anonymous track', () => { - const tracker = addTracker('sp5', 'sp5', '', '', new SharedState(), { anonymousTracking: true }); + const tracker = createTracker({ anonymousTracking: true }); expect(tracker?.getDomainSessionIndex()).toEqual(1); }); it('Retains correct domain session index on opt-out cookie present', () => { const optOutCookieName = 'optOut'; - const tracker = addTracker('sp6', 'sp6', '', '', new SharedState()); + const tracker = createTracker(); tracker?.setOptOutCookie(optOutCookieName); document.cookie = `${optOutCookieName}=1;`; @@ -102,10 +103,73 @@ describe('Tracker API: ', () => { it('Sets correct domain session index after session expiration', () => { // Session timeout is in seconds - const tracker = addTracker('sp7', 'sp7', '', '', new SharedState(), { sessionCookieTimeout: 1 }); + const tracker = createTracker({ sessionCookieTimeout: 1 }); // Advance timer by more than one second jest.advanceTimersByTime(1001); tracker?.trackPageView({ title: 'my page' }); expect(tracker?.getDomainSessionIndex()).toEqual(2); }); + + it('Adds the client session context entity when enabled', (done) => { + const tracker = createTracker({ + contexts: { session: true }, + encodeBase64: false, + plugins: [ + { + afterTrack: (payload) => { + let context = payload.co as string; + expect(context).toContain('client_session'); + done(); + }, + }, + ], + }); + + tracker?.trackPageView(); + }); + + it('Adds the client session context entity when anonymous session tracking', (done) => { + const tracker = createTracker({ + contexts: { session: true }, + encodeBase64: false, + anonymousTracking: { withSessionTracking: true }, + plugins: [ + { + afterTrack: (payload) => { + let context = payload.co as string; + expect(context).toContain('client_session'); + expect(context).toContain('"userId":"00000000-0000-0000-0000-000000000000"'); + expect(context).toContain('"previousSessionId":null'); + done(); + }, + }, + ], + }); + + tracker?.trackPageView(); + }); + + it("Doesn't add the client session context entity when anonymous tracking without session tracking", (done) => { + const tracker = createTracker({ + contexts: { session: true }, + encodeBase64: false, + anonymousTracking: true, + plugins: [ + { + afterTrack: (payload) => { + let context = payload.co as string; + expect(context).not.toContain('client_session'); + done(); + }, + }, + ], + }); + + tracker?.trackPageView(); + }); }); + +function createTracker(configuration?: TrackerConfiguration) { + let id = 'sp-' + Math.random(); + return addTracker(id, id, '', '', new SharedState(), configuration); +}