Skip to content

Commit

Permalink
feat: added new load option to set cookies from serverside
Browse files Browse the repository at this point in the history
  • Loading branch information
MoumitaM committed Mar 18, 2024
1 parent c9abc60 commit 6a681c1
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 7 deletions.
2 changes: 2 additions & 0 deletions packages/analytics-js-common/src/types/LoadOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ export type LoadOptions = {
consentManagement?: ConsentManagementOptions;
sameDomainCookiesOnly?: boolean;
externalAnonymousIdCookieName?: string;
useServerSideCookie?: boolean;
cookieServerUrl?: string;
};

export type ConsentOptions = {
Expand Down
8 changes: 8 additions & 0 deletions packages/analytics-js/__fixtures__/msw.handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -84,6 +86,7 @@ describe('User session manager', () => {
defaultLogger,
defaultPluginsManager,
defaultStoreManager,
defaultHttpClient,
);
});

Expand Down Expand Up @@ -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,
},
});
});
});
});
3 changes: 2 additions & 1 deletion packages/analytics-js/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/analytics-js/src/components/core/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ class Analytics implements IAnalytics {
this.logger,
this.pluginsManager,
this.storeManager,
this.httpClient,
);
this.eventRepository = new EventRepository(
this.pluginsManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
Expand All @@ -272,14 +297,25 @@ class UserSessionManager implements IUserSessionManager {
value: Nullable<ApiObject> | Nullable<string> | 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);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/analytics-js/src/components/utilities/loadOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const normalizeLoadOptions = (

normalizedLoadOpts.sendAdblockPage = normalizedLoadOpts.sendAdblockPage === true;

normalizedLoadOpts.useServerSideCookie = normalizedLoadOpts.useServerSideCookie === true;

if (!isObjectLiteralAndNotNull(normalizedLoadOpts.sendAdblockPageOptions)) {
delete normalizedLoadOpts.sendAdblockPageOptions;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions packages/analytics-js/src/state/slices/loadOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const defaultLoadOptions: LoadOptions = {
migrate: true,
},
sendAdblockPageOptions: {},
useServerSideCookie: false,
};

const loadOptionsState: LoadOptionsState = signal(clone(defaultLoadOptions));
Expand Down

0 comments on commit 6a681c1

Please sign in to comment.