From 921f98154fcb6a95840d1d478c266d0050157d49 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 5 Mar 2024 15:36:07 +0000 Subject: [PATCH] feat: add support for sharing gcm This adds support for sharing google consent mode with other apps such as Luxury Checkout through a cookie set for the same domain. --- packages/analytics/src/utils/getters.ts | 4 + packages/react/src/analytics/analytics.ts | 9 +- packages/react/src/analytics/constants.ts | 9 + .../GoogleConsentMode/GoogleConsentMode.ts | 224 +++++++++++------- .../__tests__/GoogleConsentMode.test.ts | 154 +++++++++--- .../GoogleConsentMode.test.ts.snap | 65 ++++- .../shared/GoogleConsentMode/cookieUtils.ts | 24 ++ 7 files changed, 366 insertions(+), 123 deletions(-) create mode 100644 packages/react/src/analytics/constants.ts create mode 100644 packages/react/src/analytics/integrations/shared/GoogleConsentMode/cookieUtils.ts diff --git a/packages/analytics/src/utils/getters.ts b/packages/analytics/src/utils/getters.ts index a48a69483..eb170b87b 100644 --- a/packages/analytics/src/utils/getters.ts +++ b/packages/analytics/src/utils/getters.ts @@ -134,6 +134,10 @@ export const getLocation = ( * @returns The cookie value. */ export const getCookie = (name: string): string | void => { + if (typeof document === 'undefined') { + return; + } + const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); diff --git a/packages/react/src/analytics/analytics.ts b/packages/react/src/analytics/analytics.ts index 4433670a8..f3b45e93a 100644 --- a/packages/react/src/analytics/analytics.ts +++ b/packages/react/src/analytics/analytics.ts @@ -1,4 +1,5 @@ import { get } from 'lodash-es'; +import { PACKAGE_NAME, PACKAGE_NAME_VERSION } from './constants.js'; import Analytics, { type EventContextData, type EventData, @@ -11,12 +12,6 @@ import Analytics, { import webContext, { type WebContext } from './context.js'; import WebContextStateManager from './WebContextStateManager.js'; -const { - name: PACKAGE_NAME, - version: PACKAGE_VERSION, - // eslint-disable-next-line @typescript-eslint/no-var-requires -} = require('../../package.json'); - /** * Analytics facade for web applications. Refer to \@farfetch/blackout-analytics * documentation to know the inherited methods from Analytics. @@ -85,7 +80,7 @@ class AnalyticsWeb extends Analytics { if (context) { context.library = { name: PACKAGE_NAME, - version: `${context.library.name}@${context.library.version};${PACKAGE_NAME}@${PACKAGE_VERSION};`, + version: `${context.library.name}@${context.library.version};${PACKAGE_NAME_VERSION};`, }; const webContextStateSnapshot = this.webContextStateManager.getSnapshot(); diff --git a/packages/react/src/analytics/constants.ts b/packages/react/src/analytics/constants.ts new file mode 100644 index 000000000..fc3e8de14 --- /dev/null +++ b/packages/react/src/analytics/constants.ts @@ -0,0 +1,9 @@ +// We use a require here to avoid typescript complaining of `package.json` is not +// under rootDir that we would get if we used an import. Typescript apparently ignores +// requires. +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { name, version } = require('../../package.json'); + +export const PACKAGE_VERSION = version as string; +export const PACKAGE_NAME = name as string; +export const PACKAGE_NAME_VERSION = `${PACKAGE_NAME}@${PACKAGE_VERSION}`; diff --git a/packages/react/src/analytics/integrations/shared/GoogleConsentMode/GoogleConsentMode.ts b/packages/react/src/analytics/integrations/shared/GoogleConsentMode/GoogleConsentMode.ts index 4e14b5c75..bd32d3f93 100644 --- a/packages/react/src/analytics/integrations/shared/GoogleConsentMode/GoogleConsentMode.ts +++ b/packages/react/src/analytics/integrations/shared/GoogleConsentMode/GoogleConsentMode.ts @@ -1,25 +1,28 @@ -import { type ConsentData } from '@farfetch/blackout-analytics'; +import { type ConsentData, utils } from '@farfetch/blackout-analytics'; +import { GCM_SHARED_COOKIE_NAME, setCookie } from './cookieUtils.js'; import { type GoogleConsentCategoryConfig, type GoogleConsentModeConfig, GoogleConsentType, } from './types.js'; -import { isEqual, omit } from 'lodash-es'; +import { omit } from 'lodash-es'; /** * GoogleConsentMode handles with Google Consent Mode v2. */ export class GoogleConsentMode { private dataLayer!: string; // Stores different data layer names - private config?: GoogleConsentModeConfig; // Stores default or customized consent category mappings - private configExcludingModeRegionsAndWaitForUpdate!: Record< - string, - GoogleConsentCategoryConfig - >; // exclude not consent properties from config - private lastConsent?: Record< - string, - Array | string | number | undefined - >; + private configWithConsentOnly!: Record; // exclude not consent properties from config + private consentDataLayerCommands: Array< + [ + 'consent', + 'default' | 'update', + Record | string | number | undefined> | undefined, + ] + > = []; + private waitForUpdate?: number; + private regions?: GoogleConsentModeConfig['regions']; + private hasConfig: boolean; constructor( dataLayer: string, @@ -27,96 +30,160 @@ export class GoogleConsentMode { config?: GoogleConsentModeConfig, ) { this.dataLayer = dataLayer; - this.config = config; + + this.waitForUpdate = config?.waitForUpdate; + this.regions = config?.regions; // select only the Google Consent Elements - this.configExcludingModeRegionsAndWaitForUpdate = omit(this.config || {}, [ + this.configWithConsentOnly = omit(config || {}, [ 'waitForUpdate', 'regions', 'mode', ]); - this.loadDefaults(initConsent); + this.hasConfig = Object.keys(this.configWithConsentOnly).length > 0; + + this.initialize(initConsent); } + /** - * Write google consent default values to dataLayer. - * - * @param initConsent - The init consent data to be set. + * Tries to load shared consent from cookies if available + * and writes it to the dataLayer. + * This method is only supposed to be called if no google + * consent config was passed. */ - private loadDefaults(initConsent: ConsentData | null) { - if (this.config) { - const initialValue: Record = {}; + private loadSharedConsentFromCookies() { + const consentModeCookieValue = utils.getCookie(GCM_SHARED_COOKIE_NAME); - if (this.config.waitForUpdate) { - initialValue['wait_for_update'] = this.config.waitForUpdate; + if (consentModeCookieValue) { + try { + const values = JSON.parse(consentModeCookieValue); + + if (Array.isArray(values)) { + values.forEach(value => { + const [consentCommand, command, consent] = value; + + this.write(consentCommand, command, consent); + }); + } + } catch { + // Do nothing... } + } + } - // Obtain default google consent registry - const consentRegistry = Object.keys( - this.configExcludingModeRegionsAndWaitForUpdate, - ).reduce( - (result, consentKey) => ({ - ...result, - [consentKey]: - this.configExcludingModeRegionsAndWaitForUpdate[consentKey] - ?.default || GoogleConsentType.Denied, - }), - initialValue, - ); + /** + * Loads default values from the configuration and + * writes them in a cookie for sharing. + * + * @param initConsent - The consent data available, which can be null if the user has not yet given consent. + */ + private loadDefaultsFromConfig(initConsent: ConsentData | null) { + const initialValue: Record = {}; - // Write default consent to data layer - this.write('consent', 'default', consentRegistry); + if (this.waitForUpdate) { + initialValue['wait_for_update'] = this.waitForUpdate; + } - // write regions to data layer if they exists - this.config.regions?.forEach(region => { + // Obtain default google consent registry + const consentRegistry = Object.keys(this.configWithConsentOnly).reduce( + (result, consentKey) => ({ + ...result, + [consentKey]: + this.configWithConsentOnly[consentKey]?.default || + GoogleConsentType.Denied, + }), + initialValue, + ); + + // Write default consent to data layer + this.write('consent', 'default', consentRegistry); + + // write regions to data layer if they exist + const regions = this.regions; + + if (regions) { + regions.forEach(region => { this.write('consent', 'default', region); }); + } + + this.updateConsent(initConsent); - this.updateConsent(initConsent); + this.saveConsent(); + } + + /** + * Try to set consent types with dataLayer. If a valid + * config was passed, default values for the consent + * types are used. Else, try to load the commands + * set from the cookie if it is available. + * + * @param initConsent - The consent data available, which can be null if the user has not yet given consent. + */ + private initialize(initConsent: ConsentData | null) { + if (this.hasConfig) { + this.loadDefaultsFromConfig(initConsent); + } else { + this.loadSharedConsentFromCookies(); } } /** - * Update consent. + * Writes consent updates to the dataLayer + * by applying the configuration (if any) to + * the passed consent data. * - * @param consentData - The consent data to be set. + * @param consentData - Consent data obtained from the user or null if not available. */ updateConsent(consentData: ConsentData | null) { - if (this.config) { - // Dealing with null or undefined consent values - const safeConsent = consentData || {}; - + if (this.hasConfig && consentData) { // Fill consent value into consent element, using analytics consent categories - const consentRegistry = Object.keys( - this.configExcludingModeRegionsAndWaitForUpdate, - ).reduce((result, consentKey) => { - let consentValue = GoogleConsentType.Denied; - const consent = - this.configExcludingModeRegionsAndWaitForUpdate[consentKey]; - - if (consent) { - // has consent config key - - if (consent.getConsentValue) { - // give priority to custom function - consentValue = consent.getConsentValue(safeConsent); - } else if ( - consent?.categories !== undefined && - consent.categories.every(consent => safeConsent[consent]) - ) { - // The second option to assign value is by categories list - consentValue = GoogleConsentType.Granted; + const consentRegistry = Object.keys(this.configWithConsentOnly).reduce( + (result, consentKey) => { + let consentValue = GoogleConsentType.Denied; + const consent = this.configWithConsentOnly[consentKey]; + + if (consent) { + // has consent config key + if (consent.getConsentValue) { + // give priority to custom function + consentValue = consent.getConsentValue(consentData); + } else if ( + consent?.categories !== undefined && + consent.categories.every(consent => consentData[consent]) + ) { + // The second option to assign value is by categories list + consentValue = GoogleConsentType.Granted; + } } - } - return { - ...result, - [consentKey]: consentValue, - }; - }, {}); + return { + ...result, + [consentKey]: consentValue, + }; + }, + {}, + ); // Write consent to data layer this.write('consent', 'update', consentRegistry); + + this.saveConsent(); + } + } + + /** + * Saves calculated google consent mode to a cookie + * for sharing consent between apps in same + * domain. + */ + saveConsent() { + if (this.consentDataLayerCommands.length > 0) { + setCookie( + GCM_SHARED_COOKIE_NAME, + JSON.stringify(this.consentDataLayerCommands), + ); } } @@ -128,11 +195,8 @@ export class GoogleConsentMode { * @param consentParams - The consent arguments. */ private write( - // eslint-disable-next-line @typescript-eslint/no-unused-vars consentCommand: 'consent', - // eslint-disable-next-line @typescript-eslint/no-unused-vars command: 'default' | 'update', - // eslint-disable-next-line @typescript-eslint/no-unused-vars consentParams: | Record | string | number | undefined> | undefined, @@ -141,19 +205,19 @@ export class GoogleConsentMode { // that was written to the datalayer, so the parameters added to the function signature are only to // avoid mistakes when calling the function. - if ( - this.config && - typeof window !== 'undefined' && - consentParams && - !isEqual(this.lastConsent, consentParams) - ) { + if (typeof window !== 'undefined' && consentParams) { // @ts-ignore window[this.dataLayer] = window[this.dataLayer] || []; // @ts-ignore // eslint-disable-next-line prefer-rest-params window[this.dataLayer].push(arguments); - this.lastConsent = consentParams; + + this.consentDataLayerCommands.push([ + consentCommand, + command, + consentParams, + ]); } } } diff --git a/packages/react/src/analytics/integrations/shared/GoogleConsentMode/__tests__/GoogleConsentMode.test.ts b/packages/react/src/analytics/integrations/shared/GoogleConsentMode/__tests__/GoogleConsentMode.test.ts index 2dab0d59e..44e3e53c1 100644 --- a/packages/react/src/analytics/integrations/shared/GoogleConsentMode/__tests__/GoogleConsentMode.test.ts +++ b/packages/react/src/analytics/integrations/shared/GoogleConsentMode/__tests__/GoogleConsentMode.test.ts @@ -5,6 +5,18 @@ import { } from '../index.js'; import type { ConsentData } from '@farfetch/blackout-analytics'; +function deleteAllCookies() { + const cookies = document.cookie.split(';'); + + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i]; + const eqPos = cookie!.indexOf('='); + const name = eqPos > -1 ? cookie!.substr(0, eqPos) : cookie; + + document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT'; + } +} + describe('GoogleConsentMode', () => { const dataLayerName = 'dataLayer'; const mockConsent: ConsentData = { @@ -34,6 +46,8 @@ describe('GoogleConsentMode', () => { beforeEach(() => { window[dataLayerName] = []; + + deleteAllCookies(); }); describe('Basic Google Consent Mode Configuration', () => { @@ -46,8 +60,8 @@ describe('GoogleConsentMode', () => { expect(googleConsent).toBeInstanceOf(GoogleConsentMode); - expect(window.dataLayer).toHaveLength(2); - expect(window.dataLayer).toMatchSnapshot(); + expect(window[dataLayerName]).toHaveLength(2); + expect(window[dataLayerName]).toMatchSnapshot(); }); it('should update dataLayer consent when "updateConsent" is called', () => { @@ -57,34 +71,22 @@ describe('GoogleConsentMode', () => { defaultConsentConfig, ); - expect(window.dataLayer).toMatchSnapshot(); + expect(window[dataLayerName]).toMatchSnapshot(); + expect(window[dataLayerName]).toHaveLength(2); + + jest.clearAllMocks(); + + window[dataLayerName] = []; googleConsent.updateConsent(mockConsent); - expect(window.dataLayer).toMatchSnapshot(); - expect(window.dataLayer).toHaveLength(2); + expect(window[dataLayerName]).toMatchSnapshot(); }); it('should not write to datalayer if no configuration set', () => { new GoogleConsentMode(dataLayerName, mockConsent); - expect(window.dataLayer).toEqual([]); - }); - - it('should not update twice dataLayer consent when "updateConsent" is called with same values', () => { - const googleConsent = new GoogleConsentMode( - dataLayerName, - mockConsent, - defaultConsentConfig, - ); - - expect(googleConsent).toBeInstanceOf(GoogleConsentMode); - - expect(window.dataLayer).toHaveLength(2); - expect(window.dataLayer).toMatchSnapshot(); - - googleConsent.updateConsent(mockConsent); - expect(window.dataLayer).toHaveLength(2); + expect(window[dataLayerName]).toEqual([]); }); }); @@ -116,7 +118,7 @@ describe('GoogleConsentMode', () => { // update consent with grant conditions googleConsent.updateConsent({ ...mockConsent, consent_x: true }); - expect(window.dataLayer).toMatchSnapshot(); + expect(window[dataLayerName]).toMatchSnapshot(); }); it('should deal with no categories or getConsentValue function set', () => { @@ -131,7 +133,7 @@ describe('GoogleConsentMode', () => { googleConsent.updateConsent(mockConsent); - expect(window.dataLayer).toMatchSnapshot(); + expect(window[dataLayerName]).toMatchSnapshot(); }); }); @@ -155,8 +157,8 @@ describe('GoogleConsentMode', () => { expect(googleConsent).toBeInstanceOf(GoogleConsentMode); - expect(window.dataLayer).toHaveLength(3); - expect(window.dataLayer).toMatchSnapshot(); + expect(window[dataLayerName]).toHaveLength(3); + expect(window[dataLayerName]).toMatchSnapshot(); }); it('should update dataLayer consent when "updateConsent" is called', () => { @@ -166,12 +168,12 @@ describe('GoogleConsentMode', () => { defaultRegionConsentConfig, ); - expect(window.dataLayer).toMatchSnapshot(); + expect(window[dataLayerName]).toMatchSnapshot(); googleConsent.updateConsent(mockConsent); - expect(window.dataLayer).toMatchSnapshot(); - expect(window.dataLayer).toHaveLength(3); + expect(window[dataLayerName]).toMatchSnapshot(); + expect(window[dataLayerName]).toHaveLength(4); }); it('should update dataLayer consent when "updateConsent" is called with `wait_for_update` property', () => { @@ -184,13 +186,99 @@ describe('GoogleConsentMode', () => { }, ); - expect(window.dataLayer).toMatchSnapshot(); - expect(window.dataLayer).toHaveLength(3); + expect(window[dataLayerName]).toMatchSnapshot(); + expect(window[dataLayerName]).toHaveLength(3); googleConsent.updateConsent(mockConsent); - expect(window.dataLayer).toHaveLength(4); - expect(window.dataLayer).toMatchSnapshot(); + expect(window[dataLayerName]).toHaveLength(4); + expect(window[dataLayerName]).toMatchSnapshot(); + }); + }); + + describe('sharing cookie with consent', () => { + describe('when consent configuration is provided', () => { + it('should save a cookie when consent is written to dataLayer', () => { + const googleConsent = new GoogleConsentMode( + dataLayerName, + null, + defaultConsentConfig, + ); + + expect(document.cookie).toMatchSnapshot(); + + googleConsent.updateConsent(mockConsent); + + expect(document.cookie).toMatchSnapshot(); + }); + }); + + describe('when configuration is not provided', () => { + it('should load and write consent from the cookie if available', () => { + // Write cookie with consent values + new GoogleConsentMode( + dataLayerName, + mockConsent, + defaultConsentConfig, + ); + + expect(document.cookie).not.toBe(''); + + window[dataLayerName] = []; + + // Create new instance without configuration to test + new GoogleConsentMode(dataLayerName, null); + + expect(window[dataLayerName]).toMatchSnapshot(); + }); + + it('should not write consent if cookie is not available', () => { + // Create new instance without configuration to test + new GoogleConsentMode(dataLayerName, mockConsent); + + expect(window[dataLayerName]).toEqual([]); + }); + }); + + describe('when partial configuration is provided which does not include consent configuration', () => { + it('should write consent to dataLayer when cookie is available', () => { + // Write cookie with consent values + new GoogleConsentMode( + dataLayerName, + mockConsent, + defaultConsentConfig, + ); + + expect(document.cookie).not.toBe(''); + + window[dataLayerName] = []; + + // @ts-expect-error Force partial configuration + new GoogleConsentMode(dataLayerName, mockConsent, { + mode: 'Advanced', + waitForUpdate: 100, + }); + + expect(window[dataLayerName]).toMatchSnapshot(); + }); + + it('should not write consent to dataLayer when cookie is not available', () => { + const googleConsent = new GoogleConsentMode( + dataLayerName, + mockConsent, + // @ts-expect-error Force partial configuration + { + mode: 'Advanced', + waitForUpdate: 100, + }, + ); + + expect(window[dataLayerName]).toEqual([]); + + googleConsent.updateConsent(mockConsent); + + expect(window[dataLayerName]).toEqual([]); + }); }); }); }); diff --git a/packages/react/src/analytics/integrations/shared/GoogleConsentMode/__tests__/__snapshots__/GoogleConsentMode.test.ts.snap b/packages/react/src/analytics/integrations/shared/GoogleConsentMode/__tests__/__snapshots__/GoogleConsentMode.test.ts.snap index 46e828cf7..1ea4ae944 100644 --- a/packages/react/src/analytics/integrations/shared/GoogleConsentMode/__tests__/__snapshots__/GoogleConsentMode.test.ts.snap +++ b/packages/react/src/analytics/integrations/shared/GoogleConsentMode/__tests__/__snapshots__/GoogleConsentMode.test.ts.snap @@ -25,7 +25,7 @@ Array [ ] `; -exports[`GoogleConsentMode Basic Google Consent Mode Configuration should not update twice dataLayer consent when "updateConsent" is called with same values 1`] = ` +exports[`GoogleConsentMode Basic Google Consent Mode Configuration should update dataLayer consent when "updateConsent" is called 1`] = ` Array [ Arguments [ "consent", @@ -50,7 +50,22 @@ Array [ ] `; -exports[`GoogleConsentMode Basic Google Consent Mode Configuration should update dataLayer consent when "updateConsent" is called 1`] = ` +exports[`GoogleConsentMode Basic Google Consent Mode Configuration should update dataLayer consent when "updateConsent" is called 2`] = ` +Array [ + Arguments [ + "consent", + "update", + Object { + "ad_personalization": "granted", + "ad_storage": "granted", + "ad_user_data": "granted", + "analytics_storage": "granted", + }, + ], +] +`; + +exports[`GoogleConsentMode Extended Google Consent Mode Configuration sharing cookie with consent when configuration is not provided should load and write consent from the cookie if available 1`] = ` Array [ Arguments [ "consent", @@ -75,7 +90,11 @@ Array [ ] `; -exports[`GoogleConsentMode Basic Google Consent Mode Configuration should update dataLayer consent when "updateConsent" is called 2`] = ` +exports[`GoogleConsentMode Extended Google Consent Mode Configuration sharing cookie with consent when consent configuration is provided should save a cookie when consent is written to dataLayer 1`] = `"@farfetch/blackout-react__gcm_shared_consent_mode=[[\\"consent\\",\\"default\\",{\\"ad_storage\\":\\"denied\\",\\"ad_user_data\\":\\"denied\\",\\"ad_personalization\\":\\"denied\\",\\"analytics_storage\\":\\"denied\\"}]]"`; + +exports[`GoogleConsentMode Extended Google Consent Mode Configuration sharing cookie with consent when consent configuration is provided should save a cookie when consent is written to dataLayer 2`] = `"@farfetch/blackout-react__gcm_shared_consent_mode=[[\\"consent\\",\\"default\\",{\\"ad_storage\\":\\"denied\\",\\"ad_user_data\\":\\"denied\\",\\"ad_personalization\\":\\"denied\\",\\"analytics_storage\\":\\"denied\\"}],[\\"consent\\",\\"update\\",{\\"ad_storage\\":\\"granted\\",\\"ad_user_data\\":\\"granted\\",\\"ad_personalization\\":\\"granted\\",\\"analytics_storage\\":\\"granted\\"}]]"`; + +exports[`GoogleConsentMode Extended Google Consent Mode Configuration sharing cookie with consent when partial configuration is provided which does not include consent configuration should write consent to dataLayer when cookie is available 1`] = ` Array [ Arguments [ "consent", @@ -205,6 +224,16 @@ Array [ "analytics_storage": "granted", }, ], + Arguments [ + "consent", + "update", + Object { + "ad_personalization": "granted", + "ad_storage": "granted", + "ad_user_data": "granted", + "analytics_storage": "granted", + }, + ], ] `; @@ -314,6 +343,26 @@ Array [ "analytics_storage": "granted", }, ], + Arguments [ + "consent", + "update", + Object { + "ad_personalization": "denied", + "ad_storage": "granted", + "ad_user_data": "granted", + "analytics_storage": "granted", + }, + ], + Arguments [ + "consent", + "update", + Object { + "ad_personalization": "denied", + "ad_storage": "granted", + "ad_user_data": "granted", + "analytics_storage": "granted", + }, + ], Arguments [ "consent", "update", @@ -349,5 +398,15 @@ Array [ "analytics_storage": "granted", }, ], + Arguments [ + "consent", + "update", + Object { + "ad_personalization": "denied", + "ad_storage": "granted", + "ad_user_data": "granted", + "analytics_storage": "granted", + }, + ], ] `; diff --git a/packages/react/src/analytics/integrations/shared/GoogleConsentMode/cookieUtils.ts b/packages/react/src/analytics/integrations/shared/GoogleConsentMode/cookieUtils.ts new file mode 100644 index 000000000..86855b7e4 --- /dev/null +++ b/packages/react/src/analytics/integrations/shared/GoogleConsentMode/cookieUtils.ts @@ -0,0 +1,24 @@ +import { PACKAGE_NAME } from '../../../constants.js'; + +export const GCM_SHARED_COOKIE_NAME = `${PACKAGE_NAME}__gcm_shared_consent_mode`; + +function getDomain() { + const fullDomain = window.location.hostname; + + const domainParts = fullDomain.split('.'); + + // Check if there is a subdomain + if (domainParts.length > 1) { + return ( + domainParts[domainParts.length - 2] + + '.' + + domainParts[domainParts.length - 1] + ); + } + + return fullDomain; +} + +export function setCookie(name: string, value: string, domain = getDomain()) { + document.cookie = name + '=' + (value || '') + '; path=/ ; domain=' + domain; +}