Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: set server side cookies #1649

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6a681c1
feat: added new load option to set cookies from serverside
MoumitaM Mar 18, 2024
ec4e81e
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM Mar 18, 2024
7b90beb
chore: address review comments
MoumitaM Mar 19, 2024
91789ef
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM Mar 21, 2024
ffa79ee
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM Mar 27, 2024
a59c8d0
chore: modify configuration for getAsyncData fn
MoumitaM Mar 27, 2024
e4717c2
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM Apr 18, 2024
41cd5af
chore: updated cookie setter provider
MoumitaM Apr 22, 2024
0c65220
Merge branch 'feature/sdk-1301-create-a-cookie-setter-provider-for-se…
MoumitaM Apr 22, 2024
0f49281
chore: address review comments
MoumitaM May 6, 2024
5d277b6
chore: resolve merge conflict
MoumitaM May 6, 2024
e4ce0a0
chore: address review comment
MoumitaM May 7, 2024
d2fece5
chore: fix error handling to remove cookies if value is empty
MoumitaM May 7, 2024
a1eaf29
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM May 7, 2024
b1f9bf3
chore: address review comment
MoumitaM May 9, 2024
14f5c23
chore: update cb fn
MoumitaM May 10, 2024
4109f61
chore: add new type for callback
MoumitaM May 10, 2024
54350fb
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM May 10, 2024
e653c05
chore: update test case
MoumitaM May 12, 2024
a90f320
chore: update test case
MoumitaM May 12, 2024
569a8ff
chore: removed unnecessary code
MoumitaM May 12, 2024
e885ef3
chore: more test cases added
MoumitaM May 16, 2024
07cf076
chore: removed code for client side cookie removal and update test cases
MoumitaM May 17, 2024
595c160
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM May 17, 2024
29c848b
chore: use current page url as the base url for cookie request
MoumitaM May 17, 2024
5eaadf1
chore: removed unnecessary only statement
MoumitaM May 18, 2024
2987039
chore: review comment address
MoumitaM May 20, 2024
40523f5
chore: review comment address
MoumitaM May 20, 2024
6797508
chore: use nullish coalescing operator
MoumitaM May 20, 2024
e64fe44
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM May 20, 2024
8f8e48f
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM May 20, 2024
1d1107b
chore: enhance test case
MoumitaM May 20, 2024
598599e
Merge branch 'feature/sdk-1301-create-a-cookie-setter-provider-for-se…
MoumitaM May 20, 2024
b7c464c
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM May 20, 2024
3492300
chore: remove leading slash from provided endpoint
MoumitaM May 21, 2024
3229627
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM May 21, 2024
022d23b
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM May 21, 2024
b0e1484
Merge branch 'develop' into feature/sdk-1301-create-a-cookie-setter-p…
MoumitaM May 21, 2024
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
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;
useServerSideCookies?: boolean;
cookieServerUrl?: string;
};

export type ConsentOptions = {
Expand Down
2 changes: 1 addition & 1 deletion packages/analytics-js/.size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ module.exports = [
{
name: 'Core - CDN',
path: 'dist/cdn/modern/iife/rsa.min.js',
limit: '23 KiB',
limit: '23.5 KiB',
},
];
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 @@ -1371,16 +1374,99 @@ describe('User session manager', () => {
});

describe('getExternalAnonymousIdByCookieName', () => {
it('Should return null if the cookie value does not exists', () => {
it('should return null if the cookie value does not exists', () => {
const externalAnonymousId =
userSessionManager.getExternalAnonymousIdByCookieName('anonId_cookie');
expect(externalAnonymousId).toEqual(null);
});
it('Should return the cookie value if exists', () => {
it('should return the cookie value if exists', () => {
document.cookie = 'anonId_cookie=sampleAnonymousId12345';
const externalAnonymousId =
userSessionManager.getExternalAnonymousIdByCookieName('anonId_cookie');
expect(externalAnonymousId).toEqual('sampleAnonymousId12345');
});
});

describe('syncValueToStorage', () => {
it('should call setServerSideCookie method in case useServerSideCookie load option is set to true', () => {
state.loadOptions.value.useServerSideCookies = 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', expect.any(Object));
});
});

describe('setServerSideCookie', () => {
beforeAll(() => {
server.listen();
});

afterAll(() => {
server.close();
});
const mockCookieStore = {
encrypt: jest.fn(val => `encrypted_${JSON.parse(val)}`),
set: jest.fn(),
};
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', mockCookieStore);
expect(spy).toHaveBeenCalledWith({
url: `https://dummy.dataplane.host.com/setCookie`,
options: {
method: 'POST',
data: JSON.stringify({
key: 'key',
value: 'encrypted_sample_cookie_value_1234',
options: {
maxage: 10 * 60 * 1000,
path: '/',
domain: 'example.com',
samesite: 'Lax',
},
}),
sendRawData: true,
},
callback: expect.any(Function),
});
});
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', mockCookieStore);
expect(spy).toHaveBeenCalledWith({
url: `https://example.com/setCookie`,
options: {
method: 'POST',
data: JSON.stringify({
key: 'key',
value: 'encrypted_sample_cookie_value_1234',
options: {
maxage: 10 * 60 * 1000,
path: '/',
domain: 'example.com',
samesite: 'Lax',
},
}),
sendRawData: true,
},
callback: expect.any(Function),
});
});
});
});
1 change: 1 addition & 0 deletions packages/analytics-js/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
destSDKBaseURL:
'__DEST_SDK_BASE_URL__' + window.rudderAnalyticsBuildType + '/js-integrations',
pluginsSDKBaseURL: '__PLUGINS_BASE_URL__' + window.rudderAnalyticsBuildType + '/plugins',
// useServerSideCookie:true,
// queueOptions: {
// batch: {
// maxSize: 5 * 1024, // 5KB
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import type { Destination } from '@rudderstack/analytics-js-common/types/Destination';
import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger';
import { CONFIG_MANAGER } from '@rudderstack/analytics-js-common/constants/loggerContexts';
import { isValidSourceConfig, validateLoadArgs } from './util/validate';
import {
isValidSourceConfig,
validateLoadArgs,
validateAndReturnCookieServerUrl,
} from './util/validate';
import {
DATA_PLANE_URL_ERROR,
SOURCE_CONFIG_FETCH_ERROR,
Expand Down Expand Up @@ -91,6 +95,13 @@
lockIntegrationsVersion,
this.logger,
);
if (state.loadOptions.value.cookieServerUrl) {
state.loadOptions.value.cookieServerUrl = validateAndReturnCookieServerUrl(

Check warning on line 99 in packages/analytics-js/src/components/configManager/ConfigManager.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/components/configManager/ConfigManager.ts#L99

Added line #L99 was not covered by tests
state.loadOptions.value.useServerSideCookies,
state.loadOptions.value.cookieServerUrl,
this.logger,
);
}
});

this.getConfig();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
SUPPORTED_STORAGE_TYPES,
type StorageType,
} from '@rudderstack/analytics-js-common/types/Storage';
import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger';
import {
WRITE_KEY_VALIDATION_ERROR,
DATA_PLANE_URL_VALIDATION_ERROR,
COOKIE_SERVER_URL_INVALID_ERROR,
} from '../../../constants/logMessages';
import { isValidUrl } from '../../utilities/url';

Expand Down Expand Up @@ -37,10 +39,25 @@
const isValidStorageType = (storageType?: StorageType): boolean =>
typeof storageType === 'string' && SUPPORTED_STORAGE_TYPES.includes(storageType);

const validateAndReturnCookieServerUrl = (
useServerSideCookies?: boolean,
cookieServerUrl?: string,
logger?: ILogger,
) => {
if (useServerSideCookies && cookieServerUrl) {
if (isValidUrl(cookieServerUrl)) {
return cookieServerUrl;

Check warning on line 49 in packages/analytics-js/src/components/configManager/util/validate.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/components/configManager/util/validate.ts#L49

Added line #L49 was not covered by tests
}
logger?.error(COOKIE_SERVER_URL_INVALID_ERROR('cookieServerUrl'));
}
return 'invalid';

Check warning on line 53 in packages/analytics-js/src/components/configManager/util/validate.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/components/configManager/util/validate.ts#L53

Added line #L53 was not covered by tests
};

export {
validateLoadArgs,
isValidSourceConfig,
isValidStorageType,
validateWriteKey,
validateDataPlaneUrl,
validateAndReturnCookieServerUrl,
};
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 @@
} 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 All @@ -39,6 +41,8 @@
import { state } from '../../state';
import { getStorageEngine } from '../../services/StoreManager/storages';
import {
COOKIE_SERVER_REQUEST_FAIL_ERROR,
FAILED_SETTING_COOKIE_FROM_SERVER_FAIL_ERROR,
TIMEOUT_NOT_NUMBER_WARNING,
TIMEOUT_NOT_RECOMMENDED_WARNING,
TIMEOUT_ZERO_WARNING,
Expand All @@ -50,7 +54,7 @@
hasSessionExpired,
isStorageTypeValidForStoringData,
} from './utils';
import { getReferringDomain } from '../utilities/url';
import { getReferringDomain, removeTrailingSlashes } from '../utilities/url';
import { getReferrer } from '../utilities/page';
import { DEFAULT_USER_SESSION_VALUES, USER_SESSION_STORAGE_KEYS } from './constants';
import type { IUserSessionManager, UserSessionStorageKeysType } from './types';
Expand All @@ -59,19 +63,22 @@
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 +269,50 @@
}
}

/**
* 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: ApiObject | string, store?: IStore): void {
const encryptedCookieValue = store?.encrypt(
stringifyWithoutCircular(value, false, [], this.logger),
);
if (encryptedCookieValue) {
let baseUrl = state.lifecycle.activeDataplaneUrl.value;
const { cookieServerUrl } = state.loadOptions.value;
if (cookieServerUrl) {
if (cookieServerUrl === 'invalid') {
return;

Check warning on line 286 in packages/analytics-js/src/components/userSessionManager/UserSessionManager.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/components/userSessionManager/UserSessionManager.ts#L286

Added line #L286 was not covered by tests
}
baseUrl = cookieServerUrl;
}
this.httpClient?.getAsyncData({
url: `${removeTrailingSlashes(baseUrl as string)}/setCookie`,
options: {
method: 'POST',
data: stringifyWithoutCircular({
key,
value: encryptedCookieValue,
options: state.storage.cookie.value,
}) as string,
sendRawData: true,
},
callback: (res, details) => {
if (details?.xhr?.status === 200) {
const cookieValue = store?.get(key);
if (cookieValue !== value) {
this.logger?.error(FAILED_SETTING_COOKIE_FROM_SERVER_FAIL_ERROR(key));
}
} else {
this.logger?.error(COOKIE_SERVER_REQUEST_FAIL_ERROR(details?.xhr?.status));
store?.set(key, value);
}
},
});
}
}

/**
* A function to sync values in storage
* @param sessionKey
Expand All @@ -272,14 +323,20 @@
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,
);
if ((value && isString(value)) || isNonEmptyObject(value)) {
curStore?.set(key, value);
const key = entries[sessionKey]?.key as string;
if (value && (isString(value) || isNonEmptyObject(value))) {
// if useServerSideCookies load option is set to true
// set the cookie from server side
if (state.loadOptions.value.useServerSideCookies && storageType === COOKIE_STORAGE) {
this.setServerSideCookie(key, value, curStore);
} else {
curStore?.set(key, value);
}
} else {
curStore?.remove(key);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@

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

normalizedLoadOpts.useServerSideCookies = normalizedLoadOpts.useServerSideCookies === true;

if (
normalizedLoadOpts.cookieServerUrl &&
typeof normalizedLoadOpts.cookieServerUrl !== 'string'
) {
delete normalizedLoadOpts.cookieServerUrl;

Check warning on line 66 in packages/analytics-js/src/components/utilities/loadOptions.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/components/utilities/loadOptions.ts#L66

Added line #L66 was not covered by tests
}

if (!isObjectLiteralAndNotNull(normalizedLoadOpts.sendAdblockPageOptions)) {
delete normalizedLoadOpts.sendAdblockPageOptions;
}
Expand Down
Loading