From 6a681c1e06f3753237e0b8182afedc7e2620715d Mon Sep 17 00:00:00 2001 From: MoumitaM Date: Mon, 18 Mar 2024 19:14:22 +0530 Subject: [PATCH] feat: added new load option to set cookies from serverside --- .../src/types/LoadOptions.ts | 2 + .../analytics-js/__fixtures__/msw.handlers.ts | 8 ++ .../UserSessionManager.test.ts | 80 +++++++++++++++++++ packages/analytics-js/public/index.html | 3 +- .../src/components/core/Analytics.ts | 1 + .../userSessionManager/UserSessionManager.ts | 48 +++++++++-- .../src/components/utilities/loadOptions.ts | 2 + .../StoreManager/storages/CookieStorage.ts | 3 + .../src/state/slices/loadOptions.ts | 1 + 9 files changed, 141 insertions(+), 7 deletions(-) diff --git a/packages/analytics-js-common/src/types/LoadOptions.ts b/packages/analytics-js-common/src/types/LoadOptions.ts index 540298706..18d140e57 100644 --- a/packages/analytics-js-common/src/types/LoadOptions.ts +++ b/packages/analytics-js-common/src/types/LoadOptions.ts @@ -147,6 +147,8 @@ export type LoadOptions = { consentManagement?: ConsentManagementOptions; sameDomainCookiesOnly?: boolean; externalAnonymousIdCookieName?: string; + useServerSideCookie?: boolean; + cookieServerUrl?: string; }; export type ConsentOptions = { diff --git a/packages/analytics-js/__fixtures__/msw.handlers.ts b/packages/analytics-js/__fixtures__/msw.handlers.ts index 88dce1fba..bdc21dbbf 100644 --- a/packages/analytics-js/__fixtures__/msw.handlers.ts +++ b/packages/analytics-js/__fixtures__/msw.handlers.ts @@ -77,6 +77,14 @@ const handlers = [ }, }); }), + http.post(`${dummyDataplaneHost}/setCookie`, () => { + return new HttpResponse(null, { + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }); + }), ]; export { handlers }; diff --git a/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts b/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts index b3ad7561e..bef27bc79 100644 --- a/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts +++ b/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts @@ -20,6 +20,8 @@ import { entriesWithOnlyNoStorage, entriesWithStorageOnlyForAnonymousId, } from '../../../__fixtures__/fixtures'; +import { server } from '../../../__fixtures__/msw.server'; +import { defaultHttpClient } from '../../../src/services/HttpClient'; jest.mock('@rudderstack/analytics-js-common/utilities/uuId', () => ({ generateUUID: jest.fn().mockReturnValue('test_uuid'), @@ -84,6 +86,7 @@ describe('User session manager', () => { defaultLogger, defaultPluginsManager, defaultStoreManager, + defaultHttpClient, ); }); @@ -1383,4 +1386,81 @@ describe('User session manager', () => { expect(externalAnonymousId).toEqual('sampleAnonymousId12345'); }); }); + + describe('syncValueToStorage', () => { + it('Should call setServerSideCookie method in case useServerSideCookie load option is set to true', () => { + state.loadOptions.value.useServerSideCookie = true; + state.storage.entries.value = entriesWithOnlyCookieStorage; + const spy = jest.spyOn(userSessionManager, 'setServerSideCookie'); + userSessionManager.syncValueToStorage('anonymousId', 'dummy_anonymousId'); + expect(spy).toHaveBeenCalledWith('rl_anonymous_id', '"dummy_anonymousId"'); + }); + }); + + describe('setServerSideCookie', () => { + beforeAll(() => { + server.listen(); + }); + + afterAll(() => { + server.close(); + }); + it('Should make external request to exposed endpoint', () => { + state.lifecycle.activeDataplaneUrl.value = 'https://dummy.dataplane.host.com'; + state.storage.cookie.value = { + maxage: 10 * 60 * 1000, // 10 min + path: '/', + domain: 'example.com', + samesite: 'Lax', + }; + const spy = jest.spyOn(defaultHttpClient, 'getAsyncData'); + userSessionManager.setServerSideCookie('key', 'sample_cookie_value_1234'); + expect(spy).toHaveBeenCalledWith({ + url: `https://dummy.dataplane.host.com/setCookie`, + options: { + method: 'POST', + data: JSON.stringify({ + key: 'key', + value: 'sample_cookie_value_1234', + options: { + maxage: 10 * 60 * 1000, + path: '/', + domain: 'example.com', + samesite: 'Lax', + }, + }), + sendRawData: true, + }, + }); + }); + it('Should use provided server url to make external request for setting cookie', () => { + state.lifecycle.activeDataplaneUrl.value = 'https://dummy.dataplane.host.com'; + state.loadOptions.value.cookieServerUrl = 'https://example.com'; + state.storage.cookie.value = { + maxage: 10 * 60 * 1000, // 10 min + path: '/', + domain: 'example.com', + samesite: 'Lax', + }; + const spy = jest.spyOn(defaultHttpClient, 'getAsyncData'); + userSessionManager.setServerSideCookie('key', 'sample_cookie_value_1234'); + expect(spy).toHaveBeenCalledWith({ + url: `https://example.com/setCookie`, + options: { + method: 'POST', + data: JSON.stringify({ + key: 'key', + value: 'sample_cookie_value_1234', + options: { + maxage: 10 * 60 * 1000, + path: '/', + domain: 'example.com', + samesite: 'Lax', + }, + }), + sendRawData: true, + }, + }); + }); + }); }); diff --git a/packages/analytics-js/public/index.html b/packages/analytics-js/public/index.html index c8a80740a..cac0837dc 100644 --- a/packages/analytics-js/public/index.html +++ b/packages/analytics-js/public/index.html @@ -79,7 +79,8 @@ configUrl: '__CONFIG_SERVER_HOST__', destSDKBaseURL: '__DEST_SDK_BASE_URL__' + window.rudderAnalyticsBuildType + '/js-integrations', - pluginsSDKBaseURL: '__PLUGINS_BASE_URL__' + window.rudderAnalyticsBuildType + '/plugins', + // pluginsSDKBaseURL: '__PLUGINS_BASE_URL__' + window.rudderAnalyticsBuildType + '/plugins', + // useServerSideCookie:true, // queueOptions: { // batch: { // maxSize: 5 * 1024, // 5KB diff --git a/packages/analytics-js/src/components/core/Analytics.ts b/packages/analytics-js/src/components/core/Analytics.ts index 47321e338..3594a57d0 100644 --- a/packages/analytics-js/src/components/core/Analytics.ts +++ b/packages/analytics-js/src/components/core/Analytics.ts @@ -242,6 +242,7 @@ class Analytics implements IAnalytics { this.logger, this.pluginsManager, this.storeManager, + this.httpClient, ); this.eventRepository = new EventRepository( this.pluginsManager, diff --git a/packages/analytics-js/src/components/userSessionManager/UserSessionManager.ts b/packages/analytics-js/src/components/userSessionManager/UserSessionManager.ts index 4aa6989bc..322a20429 100644 --- a/packages/analytics-js/src/components/userSessionManager/UserSessionManager.ts +++ b/packages/analytics-js/src/components/userSessionManager/UserSessionManager.ts @@ -27,6 +27,8 @@ import { } from '@rudderstack/analytics-js-common/constants/storages'; import type { UserSessionKey } from '@rudderstack/analytics-js-common/types/UserSessionStorage'; import type { StorageEntries } from '@rudderstack/analytics-js-common/types/ApplicationState'; +import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; +import { stringifyWithoutCircular } from '@rudderstack/analytics-js-common/utilities/json'; import { CLIENT_DATA_STORE_COOKIE, CLIENT_DATA_STORE_LS, @@ -59,19 +61,22 @@ import { isPositiveInteger } from '../utilities/number'; class UserSessionManager implements IUserSessionManager { storeManager?: IStoreManager; pluginsManager?: IPluginsManager; - logger?: ILogger; errorHandler?: IErrorHandler; + httpClient?: IHttpClient; + logger?: ILogger; constructor( errorHandler?: IErrorHandler, logger?: ILogger, pluginsManager?: IPluginsManager, storeManager?: IStoreManager, + httpClient?: IHttpClient, ) { this.storeManager = storeManager; this.pluginsManager = pluginsManager; this.logger = logger; this.errorHandler = errorHandler; + this.httpClient = httpClient; this.onError = this.onError.bind(this); } @@ -262,6 +267,26 @@ class UserSessionManager implements IUserSessionManager { } } + /** + * A function to make an external request to set the cookie from server side + * @param key cookie name + * @param value encrypted cookie value + */ + setServerSideCookie(key: string, value: string): void { + let baseUrl = state.lifecycle.activeDataplaneUrl.value; + if (typeof state.loadOptions.value.cookieServerUrl === 'string') { + baseUrl = state.loadOptions.value.cookieServerUrl; + } + this.httpClient?.getAsyncData({ + url: `${baseUrl}/setCookie`, + options: { + method: 'POST', + data: JSON.stringify({ key, value, options: state.storage.cookie.value }), + sendRawData: true, + }, + }); + } + /** * A function to sync values in storage * @param sessionKey @@ -272,14 +297,25 @@ class UserSessionManager implements IUserSessionManager { value: Nullable | Nullable | undefined, ) { const entries = state.storage.entries.value; - const storage = entries[sessionKey]?.type as StorageType; - const key = entries[sessionKey]?.key as string; - if (isStorageTypeValidForStoringData(storage)) { + const storageType = entries[sessionKey]?.type as StorageType; + if (isStorageTypeValidForStoringData(storageType)) { const curStore = this.storeManager?.getStore( - storageClientDataStoreNameMap[storage] as string, + storageClientDataStoreNameMap[storageType] as string, ); + const key = entries[sessionKey]?.key as string; if ((value && isString(value)) || isNonEmptyObject(value)) { - curStore?.set(key, value); + // if useServerSideCookie load option is set to true + // set the cookie from server side + if (state.loadOptions.value.useServerSideCookie && storageType === 'cookieStorage') { + const encryptedCookieValue = curStore?.encrypt( + stringifyWithoutCircular(value, false, [], this.logger), + ); + if (encryptedCookieValue) { + this.setServerSideCookie(key, encryptedCookieValue); + } + } else { + curStore?.set(key, value); + } } else { curStore?.remove(key); } diff --git a/packages/analytics-js/src/components/utilities/loadOptions.ts b/packages/analytics-js/src/components/utilities/loadOptions.ts index b4a973be2..35b5d407d 100644 --- a/packages/analytics-js/src/components/utilities/loadOptions.ts +++ b/packages/analytics-js/src/components/utilities/loadOptions.ts @@ -57,6 +57,8 @@ const normalizeLoadOptions = ( normalizedLoadOpts.sendAdblockPage = normalizedLoadOpts.sendAdblockPage === true; + normalizedLoadOpts.useServerSideCookie = normalizedLoadOpts.useServerSideCookie === true; + if (!isObjectLiteralAndNotNull(normalizedLoadOpts.sendAdblockPageOptions)) { delete normalizedLoadOpts.sendAdblockPageOptions; } diff --git a/packages/analytics-js/src/services/StoreManager/storages/CookieStorage.ts b/packages/analytics-js/src/services/StoreManager/storages/CookieStorage.ts index 2c0fb9c49..729c0da0f 100644 --- a/packages/analytics-js/src/services/StoreManager/storages/CookieStorage.ts +++ b/packages/analytics-js/src/services/StoreManager/storages/CookieStorage.ts @@ -4,6 +4,7 @@ import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable'; import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import { COOKIE_STORAGE } from '@rudderstack/analytics-js-common/constants/storages'; import { mergeDeepRight } from '@rudderstack/analytics-js-common/utilities/object'; +import { state } from '@rudderstack/analytics-js/state'; import { isStorageAvailable } from '../../../components/capabilitiesManager/detection'; import { cookie } from '../component-cookie'; import { getDefaultCookieOptions } from './defaultOptions'; @@ -39,6 +40,8 @@ class CookieStorage implements IStorage { } this.isSupportAvailable = isStorageAvailable(COOKIE_STORAGE, this, this.logger); this.isEnabled = Boolean(this.options.enabled && this.isSupportAvailable); + delete this.options.enabled; + state.storage.cookie.value = this.options; return this.options; } diff --git a/packages/analytics-js/src/state/slices/loadOptions.ts b/packages/analytics-js/src/state/slices/loadOptions.ts index 1184fa107..fc322994d 100644 --- a/packages/analytics-js/src/state/slices/loadOptions.ts +++ b/packages/analytics-js/src/state/slices/loadOptions.ts @@ -37,6 +37,7 @@ const defaultLoadOptions: LoadOptions = { migrate: true, }, sendAdblockPageOptions: {}, + useServerSideCookie: false, }; const loadOptionsState: LoadOptionsState = signal(clone(defaultLoadOptions));