From f021138c4a42e07b61154100a358414aed1cca84 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Tue, 28 Oct 2025 14:18:15 +0100 Subject: [PATCH 01/45] update tests --- implementations/node-web/e2e/example.spec.ts | 8 +++++++- implementations/node-web/src/app.ts | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/implementations/node-web/e2e/example.spec.ts b/implementations/node-web/e2e/example.spec.ts index 1b4a2d3d..c3a368fd 100644 --- a/implementations/node-web/e2e/example.spec.ts +++ b/implementations/node-web/e2e/example.spec.ts @@ -5,7 +5,7 @@ const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' test('test SSR', async ({ request }) => { const response = await request.get('http://localhost:3000/') - expect(await response.text()).toEqual(CLIENT_ID) + expect(await response.json()).toMatchObject({ clientId: CLIENT_ID }) }) test('test CSR', async ({ page }) => { @@ -13,3 +13,9 @@ test('test CSR', async ({ page }) => { await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID) }) + +// test('test Profile ID', async ({ page }) => { +// await page.goto('http://localhost:4000/') + +// await expect(page.getByTestId('profileId')).toHaveText(CLIENT_ID) +// }) diff --git a/implementations/node-web/src/app.ts b/implementations/node-web/src/app.ts index c4d407d4..709f832b 100644 --- a/implementations/node-web/src/app.ts +++ b/implementations/node-web/src/app.ts @@ -21,7 +21,8 @@ const sdk = new Optimization({ }) app.get('/', limiter, (_req, res) => { - res.send(sdk.config.clientId) + const response = JSON.stringify({ clientId: sdk.config.clientId }) + res.send(response) }) const port = 3000 From 82f567627178d89f5a31e987afac60a8b02aa487 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Tue, 28 Oct 2025 18:17:58 +0100 Subject: [PATCH 02/45] update --- implementations/node-web/e2e/example.spec.ts | 18 ++++++++++++++---- implementations/node-web/package.json | 1 + platforms/javascript/web/src/index.ts | 1 + pnpm-lock.yaml | 3 +++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/implementations/node-web/e2e/example.spec.ts b/implementations/node-web/e2e/example.spec.ts index c3a368fd..77832338 100644 --- a/implementations/node-web/e2e/example.spec.ts +++ b/implementations/node-web/e2e/example.spec.ts @@ -1,3 +1,5 @@ +import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-core' +import { ANONYMOUS_ID } from '@contentful/optimization-web' import { expect, test } from '@playwright/test' const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' @@ -14,8 +16,16 @@ test('test CSR', async ({ page }) => { await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID) }) -// test('test Profile ID', async ({ page }) => { -// await page.goto('http://localhost:4000/') +test('profile id is stored in local storage if user is logged in', async ({ page, context }) => { + const id = '2352jkwefbweuhfb' -// await expect(page.getByTestId('profileId')).toHaveText(CLIENT_ID) -// }) + await context.addCookies([ + { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }, + ]) + + await page.goto('http://localhost:4000/') + + const state = await context.storageState() + + expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) +}) diff --git a/implementations/node-web/package.json b/implementations/node-web/package.json index d75d7552..1c29411a 100644 --- a/implementations/node-web/package.json +++ b/implementations/node-web/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@contentful/optimization-node": "workspace:*", + "@contentful/optimization-web": "workspace:*", "express": "catalog:", "express-rate-limit": "catalog:", "tslib": "catalog:" diff --git a/platforms/javascript/web/src/index.ts b/platforms/javascript/web/src/index.ts index b5af79d3..96636df3 100644 --- a/platforms/javascript/web/src/index.ts +++ b/platforms/javascript/web/src/index.ts @@ -12,6 +12,7 @@ import Cookies from 'js-cookie' import { beaconHandler } from './beacon/beaconHandler' import { getAnonymousId, getLocale, getPageProperties, getUserAgent } from './builders/EventBuilder' import LocalStore from './storage/LocalStore' +export { ANONYMOUS_ID } from './storage/LocalStore' declare global { interface Window { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7494f0d..79997520 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,6 +174,9 @@ importers: '@contentful/optimization-node': specifier: workspace:* version: link:../../platforms/javascript/node/dist + '@contentful/optimization-web': + specifier: workspace:* + version: link:../../platforms/javascript/web/dist express: specifier: 'catalog:' version: 5.1.0 From dbdfafcf4b2afd6bb556ad14ec4d751ea40ad170 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Tue, 28 Oct 2025 22:49:09 +0100 Subject: [PATCH 03/45] update --- implementations/node-web/e2e/example.spec.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/implementations/node-web/e2e/example.spec.ts b/implementations/node-web/e2e/example.spec.ts index 77832338..5e8bd58b 100644 --- a/implementations/node-web/e2e/example.spec.ts +++ b/implementations/node-web/e2e/example.spec.ts @@ -4,14 +4,19 @@ import { expect, test } from '@playwright/test' const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' +const URI = { + ssr: 'http://localhost:3000/', + csr: 'http://localhost:4000/', +} + test('test SSR', async ({ request }) => { - const response = await request.get('http://localhost:3000/') + const response = await request.get(URI.ssr) expect(await response.json()).toMatchObject({ clientId: CLIENT_ID }) }) test('test CSR', async ({ page }) => { - await page.goto('http://localhost:4000/') + await page.goto(URI.csr) await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID) }) @@ -23,7 +28,7 @@ test('profile id is stored in local storage if user is logged in', async ({ page { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }, ]) - await page.goto('http://localhost:4000/') + await page.goto(URI.csr) const state = await context.storageState() From eec4d03e521bc8a90b0fde8df3097357383878bf Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Tue, 28 Oct 2025 22:50:00 +0100 Subject: [PATCH 04/45] localStorage --- implementations/node-web/e2e/example.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/implementations/node-web/e2e/example.spec.ts b/implementations/node-web/e2e/example.spec.ts index 5e8bd58b..656ca5d2 100644 --- a/implementations/node-web/e2e/example.spec.ts +++ b/implementations/node-web/e2e/example.spec.ts @@ -32,5 +32,7 @@ test('profile id is stored in local storage if user is logged in', async ({ page const state = await context.storageState() - expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) + const localStorage = state.origins[0]?.localStorage + + expect(localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) }) From 20c614f5981788ddeb095133c5406963098ed194 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 10:30:13 +0100 Subject: [PATCH 05/45] CLIENT_ID --- implementations/node-web/e2e/example.spec.ts | 37 +++++++++++--------- implementations/node-web/src/app.ts | 30 +++++++++++++++- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/implementations/node-web/e2e/example.spec.ts b/implementations/node-web/e2e/example.spec.ts index 656ca5d2..6bfe1158 100644 --- a/implementations/node-web/e2e/example.spec.ts +++ b/implementations/node-web/e2e/example.spec.ts @@ -1,9 +1,10 @@ -import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-core' -import { ANONYMOUS_ID } from '@contentful/optimization-web' +// import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-core' import { expect, test } from '@playwright/test' const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' +// const ANONYMOUS_ID = '__ctfl_opt_anonymous_id__' + const URI = { ssr: 'http://localhost:3000/', csr: 'http://localhost:4000/', @@ -12,27 +13,29 @@ const URI = { test('test SSR', async ({ request }) => { const response = await request.get(URI.ssr) - expect(await response.json()).toMatchObject({ clientId: CLIENT_ID }) + expect(await response.text()).toContain(`"clientId":"${CLIENT_ID}"`) }) -test('test CSR', async ({ page }) => { - await page.goto(URI.csr) +// test('Test CSR', async ({ page }) => { +// await page.goto(URI.csr) - await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID) -}) +// await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID) +// }) -test('profile id is stored in local storage if user is logged in', async ({ page, context }) => { - const id = '2352jkwefbweuhfb' +// test('Profile id is stored in local storage if user is logged in', async ({ page, context }) => { +// const id = '2352jkwefbweuhfb' - await context.addCookies([ - { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }, - ]) +// await context.addCookies([ +// { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }, +// ]) - await page.goto(URI.csr) +// await page.goto(URI.csr) - const state = await context.storageState() +// const state = await context.storageState() - const localStorage = state.origins[0]?.localStorage +// expect(state.origins).toEqual([]) - expect(localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) -}) +// const localStorage = state.origins[0]?.localStorage + +// expect(localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) +// }) diff --git a/implementations/node-web/src/app.ts b/implementations/node-web/src/app.ts index 709f832b..12528c80 100644 --- a/implementations/node-web/src/app.ts +++ b/implementations/node-web/src/app.ts @@ -22,7 +22,35 @@ const sdk = new Optimization({ app.get('/', limiter, (_req, res) => { const response = JSON.stringify({ clientId: sdk.config.clientId }) - res.send(response) + + res.send(` + + + + Test SDK page + + + + + + + + `) }) const port = 3000 From 67d28b0c17ee3b8c2aa9515f2db222bf91225adf Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 10:51:24 +0100 Subject: [PATCH 06/45] update --- implementations/node-web/e2e/example.spec.ts | 27 +++++++++++++++++--- implementations/node-web/package.json | 2 +- implementations/node-web/src/app.ts | 2 ++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/implementations/node-web/e2e/example.spec.ts b/implementations/node-web/e2e/example.spec.ts index 6bfe1158..697b08a6 100644 --- a/implementations/node-web/e2e/example.spec.ts +++ b/implementations/node-web/e2e/example.spec.ts @@ -1,21 +1,42 @@ -// import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-core' +import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-core' import { expect, test } from '@playwright/test' const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' -// const ANONYMOUS_ID = '__ctfl_opt_anonymous_id__' +const ANONYMOUS_ID = '__ctfl_opt_anonymous_id__' const URI = { ssr: 'http://localhost:3000/', csr: 'http://localhost:4000/', } -test('test SSR', async ({ request }) => { +test('SSR/ smoke test', async ({ request }) => { const response = await request.get(URI.ssr) expect(await response.text()).toContain(`"clientId":"${CLIENT_ID}"`) }) +test('SSR/ Profile id is stored in local storage if user is logged in', async ({ + context, + page, +}) => { + const id = '2352jkwefbweuhfb' + + await context.addCookies([ + { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }, + ]) + + await page.goto('http://localhost:3000/') + + const state = await context.storageState() + + expect(state).toEqual([]) + + const localStorage = state.origins[0]?.localStorage + + expect(localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) +}) + // test('Test CSR', async ({ page }) => { // await page.goto(URI.csr) diff --git a/implementations/node-web/package.json b/implementations/node-web/package.json index 1c29411a..805a775f 100644 --- a/implementations/node-web/package.json +++ b/implementations/node-web/package.json @@ -16,7 +16,7 @@ "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", "test:e2e:codegen": "playwright codegen", "test:e2e:report": "playwright show-report", - "test:e2e:ui": "pnpm build && playwright test --ui", + "test:e2e:ui": "pnpm build && pnpm serve && playwright test --ui", "test:unit": "vitest run --coverage", "typecheck": "tsc --noEmit" }, diff --git a/implementations/node-web/src/app.ts b/implementations/node-web/src/app.ts index 12528c80..1d461c20 100644 --- a/implementations/node-web/src/app.ts +++ b/implementations/node-web/src/app.ts @@ -53,6 +53,8 @@ app.get('/', limiter, (_req, res) => { `) }) +app.use('/dist', express.static('./public/dist')) + const port = 3000 app.listen(port, () => { From 2d99a0f904ca3e981d6dc677e600b950343328f9 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 11:24:21 +0100 Subject: [PATCH 07/45] update --- implementations/node-web/e2e/example.spec.ts | 49 ++++++++------------ implementations/node-web/src/app.ts | 6 ++- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/implementations/node-web/e2e/example.spec.ts b/implementations/node-web/e2e/example.spec.ts index 697b08a6..17f9f25d 100644 --- a/implementations/node-web/e2e/example.spec.ts +++ b/implementations/node-web/e2e/example.spec.ts @@ -5,12 +5,14 @@ const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' const ANONYMOUS_ID = '__ctfl_opt_anonymous_id__' +const ID_VALUE = '2352jkwefbweuhfb' + const URI = { ssr: 'http://localhost:3000/', csr: 'http://localhost:4000/', } -test('SSR/ smoke test', async ({ request }) => { +test('SSR/ check client ID', async ({ request }) => { const response = await request.get(URI.ssr) expect(await response.text()).toContain(`"clientId":"${CLIENT_ID}"`) @@ -20,43 +22,30 @@ test('SSR/ Profile id is stored in local storage if user is logged in', async ({ context, page, }) => { - const id = '2352jkwefbweuhfb' - - await context.addCookies([ - { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }, - ]) - await page.goto('http://localhost:3000/') const state = await context.storageState() - expect(state).toEqual([]) - - const localStorage = state.origins[0]?.localStorage - - expect(localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) + expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: ID_VALUE }]) }) -// test('Test CSR', async ({ page }) => { -// await page.goto(URI.csr) - -// await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID) -// }) - -// test('Profile id is stored in local storage if user is logged in', async ({ page, context }) => { -// const id = '2352jkwefbweuhfb' +test('CSR/ check client ID', async ({ page }) => { + await page.goto(URI.csr) -// await context.addCookies([ -// { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }, -// ]) - -// await page.goto(URI.csr) + await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID) +}) -// const state = await context.storageState() +test('CSR/ Profile id is stored in local storage if user is logged in', async ({ + page, + context, +}) => { + await context.addCookies([ + { name: ANONYMOUS_ID_COOKIE, value: ID_VALUE, path: '/', domain: 'localhost' }, + ]) -// expect(state.origins).toEqual([]) + await page.goto(URI.csr) -// const localStorage = state.origins[0]?.localStorage + const state = await context.storageState() -// expect(localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) -// }) + expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: ID_VALUE }]) +}) diff --git a/implementations/node-web/src/app.ts b/implementations/node-web/src/app.ts index 1d461c20..428e1512 100644 --- a/implementations/node-web/src/app.ts +++ b/implementations/node-web/src/app.ts @@ -1,13 +1,16 @@ -import Optimization from '@contentful/optimization-node' +import Optimization, { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node' import express, { type Express } from 'express' import rateLimit from 'express-rate-limit' +const ID_VALUE = '2352jkwefbweuhfb' + const limiter = rateLimit({ windowMs: 900_000, max: 100, }) const app: Express = express() + app.use(limiter) const sdk = new Optimization({ @@ -22,6 +25,7 @@ const sdk = new Optimization({ app.get('/', limiter, (_req, res) => { const response = JSON.stringify({ clientId: sdk.config.clientId }) + res.cookie(ANONYMOUS_ID_COOKIE, ID_VALUE, { path: '/', domain: 'localhost', maxAge: 3600000 }) res.send(` From 7cb90ef6e0632bc90200b835414185b70dcb6ff0 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 11:24:57 +0100 Subject: [PATCH 08/45] URI.ssr --- implementations/node-web/e2e/example.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implementations/node-web/e2e/example.spec.ts b/implementations/node-web/e2e/example.spec.ts index 17f9f25d..513616b5 100644 --- a/implementations/node-web/e2e/example.spec.ts +++ b/implementations/node-web/e2e/example.spec.ts @@ -22,7 +22,7 @@ test('SSR/ Profile id is stored in local storage if user is logged in', async ({ context, page, }) => { - await page.goto('http://localhost:3000/') + await page.goto(URI.ssr) const state = await context.storageState() From 9793a9c4279ba876eb5549bf29ce1c9d063311aa Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 11:28:48 +0100 Subject: [PATCH 09/45] update --- implementations/node-web/e2e/example.spec.ts | 10 +++++----- implementations/node-web/src/app.ts | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/implementations/node-web/e2e/example.spec.ts b/implementations/node-web/e2e/example.spec.ts index 513616b5..856437fb 100644 --- a/implementations/node-web/e2e/example.spec.ts +++ b/implementations/node-web/e2e/example.spec.ts @@ -5,8 +5,6 @@ const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' const ANONYMOUS_ID = '__ctfl_opt_anonymous_id__' -const ID_VALUE = '2352jkwefbweuhfb' - const URI = { ssr: 'http://localhost:3000/', csr: 'http://localhost:4000/', @@ -26,7 +24,7 @@ test('SSR/ Profile id is stored in local storage if user is logged in', async ({ const state = await context.storageState() - expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: ID_VALUE }]) + expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: 'ssr_profile_id' }]) }) test('CSR/ check client ID', async ({ page }) => { @@ -39,13 +37,15 @@ test('CSR/ Profile id is stored in local storage if user is logged in', async ({ page, context, }) => { + const csrProfileId = 'csr_profile_id' + await context.addCookies([ - { name: ANONYMOUS_ID_COOKIE, value: ID_VALUE, path: '/', domain: 'localhost' }, + { name: ANONYMOUS_ID_COOKIE, value: csrProfileId, path: '/', domain: 'localhost' }, ]) await page.goto(URI.csr) const state = await context.storageState() - expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: ID_VALUE }]) + expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: csrProfileId }]) }) diff --git a/implementations/node-web/src/app.ts b/implementations/node-web/src/app.ts index 428e1512..8f736341 100644 --- a/implementations/node-web/src/app.ts +++ b/implementations/node-web/src/app.ts @@ -2,8 +2,6 @@ import Optimization, { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node import express, { type Express } from 'express' import rateLimit from 'express-rate-limit' -const ID_VALUE = '2352jkwefbweuhfb' - const limiter = rateLimit({ windowMs: 900_000, max: 100, @@ -25,7 +23,11 @@ const sdk = new Optimization({ app.get('/', limiter, (_req, res) => { const response = JSON.stringify({ clientId: sdk.config.clientId }) - res.cookie(ANONYMOUS_ID_COOKIE, ID_VALUE, { path: '/', domain: 'localhost', maxAge: 3600000 }) + res.cookie(ANONYMOUS_ID_COOKIE, 'ssr_profile_id', { + path: '/', + domain: 'localhost', + maxAge: 3600000, + }) res.send(` From 35a3d517bf24df216e92119718f6a07fcb52c91f Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 11:32:13 +0100 Subject: [PATCH 10/45] unit --- implementations/node-web/src/app.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implementations/node-web/src/app.test.ts b/implementations/node-web/src/app.test.ts index 811af1ac..76f23983 100644 --- a/implementations/node-web/src/app.test.ts +++ b/implementations/node-web/src/app.test.ts @@ -7,6 +7,6 @@ describe('GET /', () => { it('returns the client ID', async () => { const response: Response = await request(app).get('/') - expect(response.text).toEqual(CLIENT_ID) + expect(response.text).toContain(CLIENT_ID) }) }) From b50e89dde205bc1cecaedbcf2864f6894daaa6c5 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 11:42:41 +0100 Subject: [PATCH 11/45] node-ssr --- .../{node-web => node-ssr}/.env.example | 0 .../{node-web => node-ssr}/README.md | 0 .../docker-compose.yaml | 0 .../e2e/example.spec.ts | 26 +++++-------------- .../{node-web => node-ssr}/nginx/nginx.conf | 0 .../nginx/templates/default.conf.template | 0 .../{node-web => node-ssr}/package.json | 2 +- .../playwright.config.mjs | 0 .../{node-web => node-ssr}/public/index.html | 0 .../{node-web => node-ssr}/src/app.test.ts | 0 .../{node-web => node-ssr}/src/app.ts | 2 +- .../{node-web => node-ssr}/tsconfig.json | 0 .../{node-web => node-ssr}/vitest.config.ts | 0 pnpm-lock.yaml | 2 +- 14 files changed, 9 insertions(+), 23 deletions(-) rename implementations/{node-web => node-ssr}/.env.example (100%) rename implementations/{node-web => node-ssr}/README.md (100%) rename implementations/{node-web => node-ssr}/docker-compose.yaml (100%) rename implementations/{node-web => node-ssr}/e2e/example.spec.ts (57%) rename implementations/{node-web => node-ssr}/nginx/nginx.conf (100%) rename implementations/{node-web => node-ssr}/nginx/templates/default.conf.template (100%) rename implementations/{node-web => node-ssr}/package.json (98%) rename implementations/{node-web => node-ssr}/playwright.config.mjs (100%) rename implementations/{node-web => node-ssr}/public/index.html (100%) rename implementations/{node-web => node-ssr}/src/app.test.ts (100%) rename implementations/{node-web => node-ssr}/src/app.ts (97%) rename implementations/{node-web => node-ssr}/tsconfig.json (100%) rename implementations/{node-web => node-ssr}/vitest.config.ts (100%) diff --git a/implementations/node-web/.env.example b/implementations/node-ssr/.env.example similarity index 100% rename from implementations/node-web/.env.example rename to implementations/node-ssr/.env.example diff --git a/implementations/node-web/README.md b/implementations/node-ssr/README.md similarity index 100% rename from implementations/node-web/README.md rename to implementations/node-ssr/README.md diff --git a/implementations/node-web/docker-compose.yaml b/implementations/node-ssr/docker-compose.yaml similarity index 100% rename from implementations/node-web/docker-compose.yaml rename to implementations/node-ssr/docker-compose.yaml diff --git a/implementations/node-web/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts similarity index 57% rename from implementations/node-web/e2e/example.spec.ts rename to implementations/node-ssr/e2e/example.spec.ts index 856437fb..4c74f7f1 100644 --- a/implementations/node-web/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -16,36 +16,22 @@ test('SSR/ check client ID', async ({ request }) => { expect(await response.text()).toContain(`"clientId":"${CLIENT_ID}"`) }) -test('SSR/ Profile id is stored in local storage if user is logged in', async ({ - context, - page, -}) => { +test('SSR/ set Profile id from backend', async ({ context, page }) => { await page.goto(URI.ssr) const state = await context.storageState() - expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: 'ssr_profile_id' }]) + expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: 'ssr-profile-id' }]) }) -test('CSR/ check client ID', async ({ page }) => { - await page.goto(URI.csr) - - await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID) -}) - -test('CSR/ Profile id is stored in local storage if user is logged in', async ({ - page, - context, -}) => { - const csrProfileId = 'csr_profile_id' - +test('SSR/ refresh profile id from backend', async ({ context, page }) => { await context.addCookies([ - { name: ANONYMOUS_ID_COOKIE, value: csrProfileId, path: '/', domain: 'localhost' }, + { name: ANONYMOUS_ID_COOKIE, value: 'old-profile-id', path: '/', domain: 'localhost' }, ]) - await page.goto(URI.csr) + await page.goto(URI.ssr) const state = await context.storageState() - expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: csrProfileId }]) + expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: 'ssr-profile-id' }]) }) diff --git a/implementations/node-web/nginx/nginx.conf b/implementations/node-ssr/nginx/nginx.conf similarity index 100% rename from implementations/node-web/nginx/nginx.conf rename to implementations/node-ssr/nginx/nginx.conf diff --git a/implementations/node-web/nginx/templates/default.conf.template b/implementations/node-ssr/nginx/templates/default.conf.template similarity index 100% rename from implementations/node-web/nginx/templates/default.conf.template rename to implementations/node-ssr/nginx/templates/default.conf.template diff --git a/implementations/node-web/package.json b/implementations/node-ssr/package.json similarity index 98% rename from implementations/node-web/package.json rename to implementations/node-ssr/package.json index 805a775f..d38bb9f4 100644 --- a/implementations/node-web/package.json +++ b/implementations/node-ssr/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "@implementation/node-web", + "name": "@implementation/node-ssr", "description": "Reference implementation for NodeJS Web projects", "license": "MIT", "main": "dist/app.js", diff --git a/implementations/node-web/playwright.config.mjs b/implementations/node-ssr/playwright.config.mjs similarity index 100% rename from implementations/node-web/playwright.config.mjs rename to implementations/node-ssr/playwright.config.mjs diff --git a/implementations/node-web/public/index.html b/implementations/node-ssr/public/index.html similarity index 100% rename from implementations/node-web/public/index.html rename to implementations/node-ssr/public/index.html diff --git a/implementations/node-web/src/app.test.ts b/implementations/node-ssr/src/app.test.ts similarity index 100% rename from implementations/node-web/src/app.test.ts rename to implementations/node-ssr/src/app.test.ts diff --git a/implementations/node-web/src/app.ts b/implementations/node-ssr/src/app.ts similarity index 97% rename from implementations/node-web/src/app.ts rename to implementations/node-ssr/src/app.ts index 8f736341..d85e30a8 100644 --- a/implementations/node-web/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -23,7 +23,7 @@ const sdk = new Optimization({ app.get('/', limiter, (_req, res) => { const response = JSON.stringify({ clientId: sdk.config.clientId }) - res.cookie(ANONYMOUS_ID_COOKIE, 'ssr_profile_id', { + res.cookie(ANONYMOUS_ID_COOKIE, 'ssr-profile-id', { path: '/', domain: 'localhost', maxAge: 3600000, diff --git a/implementations/node-web/tsconfig.json b/implementations/node-ssr/tsconfig.json similarity index 100% rename from implementations/node-web/tsconfig.json rename to implementations/node-ssr/tsconfig.json diff --git a/implementations/node-web/vitest.config.ts b/implementations/node-ssr/vitest.config.ts similarity index 100% rename from implementations/node-web/vitest.config.ts rename to implementations/node-ssr/vitest.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8080f5a6..5ebcb3f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,7 +169,7 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/node@24.2.0)(happy-dom@20.0.2)(jiti@2.5.1)(msw@2.10.5(@types/node@24.2.0)(typescript@5.9.2))(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.1) - implementations/node-web: + implementations/node-ssr: dependencies: '@contentful/optimization-node': specifier: workspace:* From 4b27b679c85b97af48da9a88d23db3cbfd7deba5 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 11:51:39 +0100 Subject: [PATCH 12/45] clean up --- implementations/node-ssr/README.md | 4 +-- implementations/node-ssr/docker-compose.yaml | 11 ------ implementations/node-ssr/nginx/nginx.conf | 36 ------------------- .../nginx/templates/default.conf.template | 30 ---------------- implementations/node-ssr/package.json | 6 ++-- implementations/node-ssr/public/index.html | 25 ------------- 6 files changed, 5 insertions(+), 107 deletions(-) delete mode 100644 implementations/node-ssr/docker-compose.yaml delete mode 100644 implementations/node-ssr/nginx/nginx.conf delete mode 100644 implementations/node-ssr/nginx/templates/default.conf.template delete mode 100644 implementations/node-ssr/public/index.html diff --git a/implementations/node-ssr/README.md b/implementations/node-ssr/README.md index 70cbdb19..504e789e 100644 --- a/implementations/node-ssr/README.md +++ b/implementations/node-ssr/README.md @@ -1,3 +1,3 @@ -# Contentful Optimization Node.JS Web SDK Reference Implementation +# Contentful Optimization Node.JS Node SSR SDK Reference Implementation -This is a reference implementation for the [Optimization Node.JS Web SDK](../../platforms/javascript/node/README.md) and is part of the [Contentful Optimization SDK Suite](../../README.md). +This is a reference implementation for the [Optimization Node.JS Node SSR SDK](../../platforms/javascript/node/README.md) and is part of the [Contentful Optimization SDK Suite](../../README.md). diff --git a/implementations/node-ssr/docker-compose.yaml b/implementations/node-ssr/docker-compose.yaml deleted file mode 100644 index 9b07319d..00000000 --- a/implementations/node-ssr/docker-compose.yaml +++ /dev/null @@ -1,11 +0,0 @@ -services: - web: - image: nginx:alpine - volumes: - - ./nginx/templates:/etc/nginx/templates:ro - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./public:/usr/share/nginx/html:ro - ports: - - '4000:80' - env_file: ./.env - restart: unless-stopped diff --git a/implementations/node-ssr/nginx/nginx.conf b/implementations/node-ssr/nginx/nginx.conf deleted file mode 100644 index 7fd6bbdb..00000000 --- a/implementations/node-ssr/nginx/nginx.conf +++ /dev/null @@ -1,36 +0,0 @@ -events {} - -http { - include /etc/nginx/mime.types; - include /etc/nginx/conf.d/*.conf; - - gzip on; - gzip_min_length 1000; - gzip_proxied no-cache no-store private expired auth; - gzip_types - application/atom+xml - application/geo+json - application/javascript - application/x-javascript - application/json - application/ld+json - application/manifest+json - application/rdf+xml - application/rss+xml - application/xhtml+xml - application/xml - font/eot - font/otf - font/ttf - image/svg+xml - text/css - text/javascript - text/plain - text/xml; - - absolute_redirect off; - - types { - application/javascript mjs; - } -} diff --git a/implementations/node-ssr/nginx/templates/default.conf.template b/implementations/node-ssr/nginx/templates/default.conf.template deleted file mode 100644 index b6efb85c..00000000 --- a/implementations/node-ssr/nginx/templates/default.conf.template +++ /dev/null @@ -1,30 +0,0 @@ -server { - # TODO: consider '.local' to support multiple hosts - server_name localhost; - - ssi on; - ssi_types text/html; - - set $NGINX_NINETAILED_CLIENT_ID "${VITE_NINETAILED_CLIENT_ID}"; - set $NGINX_NINETAILED_ENVIRONMENT "${VITE_NINETAILED_ENVIRONMENT}"; - - set $NGINX_CONTENTFUL_TOKEN "${VITE_CONTENTFUL_TOKEN}"; - set $NGINX_CONTENTFUL_PREVIEW_TOKEN "${VITE_CONTENTFUL_PREVIEW_TOKEN}"; - set $NGINX_CONTENTFUL_ENVIRONMENT "${VITE_CONTENTFUL_ENVIRONMENT}"; - set $NGINX_CONTENTFUL_SPACE_ID "${VITE_CONTENTFUL_SPACE_ID}"; - - set $NGINX_EXPERIENCE_API_BASE_URL "${VITE_EXPERIENCE_API_BASE_URL}"; - set $NGINX_INSIGHTS_API_BASE_URL "${VITE_INSIGHTS_API_BASE_URL}"; - - listen 80; - - root /usr/share/nginx/html; - - index index.html; - - location / { - try_files $uri $uri/ =404; - } - - # TODO: add CSP & CORS directives for more thorough testing -} diff --git a/implementations/node-ssr/package.json b/implementations/node-ssr/package.json index d38bb9f4..1b635b7f 100644 --- a/implementations/node-ssr/package.json +++ b/implementations/node-ssr/package.json @@ -10,9 +10,9 @@ "build:sdk": "pnpm --filter '../../platforms/javascript/(api-schemas|api-client|core|web)' build && cp -r ../../platforms/javascript/web/dist ./public/dist", "clean": "rimraf ./dist ./public/dist ./coverage ./playwright-report ./test-results tsconfig.tsbuildinfo", "serve": "pnpm serve:mocks && pnpm serve:app", - "serve:app": "pnpm build && docker compose up -d && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"", - "serve:mocks": "pm2 start --name node-mocks \"pnpm --filter mocks mocks:serve\" && pm2 start --name web-mocks \"pnpm --filter mocks mocks:serve\"", - "serve:stop": "docker compose down && pm2 stop web-mocks node-app node-mocks && pm2 delete web-mocks node-app node-mocks", + "serve:app": "pnpm build && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"", + "serve:mocks": "pm2 start --name node-mocks \"pnpm --filter mocks mocks:serve\"", + "serve:stop": "pm2 stop node-app node-mocks && pm2 delete node-app node-mocks", "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", "test:e2e:codegen": "playwright codegen", "test:e2e:report": "playwright show-report", diff --git a/implementations/node-ssr/public/index.html b/implementations/node-ssr/public/index.html deleted file mode 100644 index f2dc7c93..00000000 --- a/implementations/node-ssr/public/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - Test SDK page - - - - - - From 95669b9662b77227d99853e360bae124108be061 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 12:07:30 +0100 Subject: [PATCH 13/45] remove css url --- implementations/node-ssr/e2e/example.spec.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index 4c74f7f1..721b62cf 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -2,22 +2,17 @@ import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-core' import { expect, test } from '@playwright/test' const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' - const ANONYMOUS_ID = '__ctfl_opt_anonymous_id__' - -const URI = { - ssr: 'http://localhost:3000/', - csr: 'http://localhost:4000/', -} +const URI = 'http://localhost:3000/' test('SSR/ check client ID', async ({ request }) => { - const response = await request.get(URI.ssr) + const response = await request.get(URI) expect(await response.text()).toContain(`"clientId":"${CLIENT_ID}"`) }) test('SSR/ set Profile id from backend', async ({ context, page }) => { - await page.goto(URI.ssr) + await page.goto(URI) const state = await context.storageState() @@ -29,7 +24,7 @@ test('SSR/ refresh profile id from backend', async ({ context, page }) => { { name: ANONYMOUS_ID_COOKIE, value: 'old-profile-id', path: '/', domain: 'localhost' }, ]) - await page.goto(URI.ssr) + await page.goto(URI) const state = await context.storageState() From cbf0581e04ee7bd0cf914b688a789df78088f956 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 12:08:06 +0100 Subject: [PATCH 14/45] CLIENT_ID --- implementations/node-ssr/src/app.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implementations/node-ssr/src/app.test.ts b/implementations/node-ssr/src/app.test.ts index 76f23983..d7f2a745 100644 --- a/implementations/node-ssr/src/app.test.ts +++ b/implementations/node-ssr/src/app.test.ts @@ -7,6 +7,6 @@ describe('GET /', () => { it('returns the client ID', async () => { const response: Response = await request(app).get('/') - expect(response.text).toContain(CLIENT_ID) + expect(response.text).toContain(`"clientId":"${CLIENT_ID}"`) }) }) From 502d40cb30dc14fcfdf101c5bd750664744798e7 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 12:11:33 +0100 Subject: [PATCH 15/45] update --- implementations/node-ssr/src/app.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index d85e30a8..78260efc 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -11,13 +11,18 @@ const app: Express = express() app.use(limiter) +const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? '' +const ENVIRONMENT = process.env.VITE_NINETAILED_ENVIRONMENT ?? '' +const VITE_INSIGHTS_API_BASE_URL = process.env.VITE_INSIGHTS_API_BASE_URL ?? '' +const VITE_EXPERIENCE_API_BASE_URL = process.env.VITE_EXPERIENCE_API_BASE_URL ?? '' + const sdk = new Optimization({ - clientId: process.env.VITE_NINETAILED_CLIENT_ID ?? '', - environment: process.env.VITE_NINETAILED_ENVIRONMENT ?? '', + clientId: CLIENT_ID, + environment: ENVIRONMENT, logLevel: 'debug', api: { - analytics: { baseUrl: process.env.VITE_INSIGHTS_API_BASE_URL }, - personalization: { baseUrl: process.env.VITE_EXPERIENCE_API_BASE_URL }, + analytics: { baseUrl: VITE_INSIGHTS_API_BASE_URL }, + personalization: { baseUrl: VITE_EXPERIENCE_API_BASE_URL }, }, }) @@ -40,12 +45,12 @@ app.get('/', limiter, (_req, res) => { +` + +app.get('/', limiter, (_req, res) => { + res.cookie(ANONYMOUS_ID_COOKIE, 'ssr-profile-id', { path: '/', domain: 'localhost' }) + res.send(` Test SDK page - + - - - + ${script} `) }) From a993b2145e426fc832522c5d4c77497fab864d5c Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 13:26:34 +0100 Subject: [PATCH 17/45] html --- implementations/node-ssr/src/app.ts | 50 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index 7bfda38a..c5120344 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -26,39 +26,37 @@ const sdk = new Optimization({ }, }) -const script = ` - -` - -app.get('/', limiter, (_req, res) => { - res.cookie(ANONYMOUS_ID_COOKIE, 'ssr-profile-id', { path: '/', domain: 'localhost' }) - - res.send(` - +const html = ` Test SDK page - ${script} + + + - `) +` + +app.get('/', limiter, (_req, res) => { + res.cookie(ANONYMOUS_ID_COOKIE, 'ssr-profile-id', { path: '/', domain: 'localhost' }) + res.send(html) }) app.use('/dist', express.static('./public/dist')) From dce3b431390db63984749692ddf72c418feef37c Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 14:23:16 +0100 Subject: [PATCH 18/45] playwrit code --- implementations/node-ssr/e2e/example.spec.ts | 6 ++++-- implementations/node-ssr/package.json | 2 ++ implementations/node-ssr/src/app.ts | 13 ++++++++++-- pnpm-lock.yaml | 22 ++++++++++++++++++++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index 048832a9..8b6f401d 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -28,13 +28,15 @@ test('set Profile id from backend', async ({ context, page }) => { }) test('refresh profile id from backend', async ({ context, page }) => { + const id = 'custom-profile-id' + await context.addCookies([ - { name: ANONYMOUS_ID_COOKIE, value: 'old-profile-id', path: '/', domain: 'localhost' }, + { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }, ]) await page.goto(URI) const state = await context.storageState() - expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: 'ssr-profile-id' }]) + expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) }) diff --git a/implementations/node-ssr/package.json b/implementations/node-ssr/package.json index 1b635b7f..496327e0 100644 --- a/implementations/node-ssr/package.json +++ b/implementations/node-ssr/package.json @@ -25,6 +25,7 @@ "@contentful/optimization-web": "workspace:*", "express": "catalog:", "express-rate-limit": "catalog:", + "cookie-parser": "1.4.7", "tslib": "catalog:" }, "devDependencies": { @@ -33,6 +34,7 @@ "@types/node": "catalog:", "@types/supertest": "catalog:", "@vitest/coverage-v8": "catalog:", + "@types/cookie-parser": "1.4.7", "dotenv": "catalog:", "pm2": "catalog:", "rimraf": "catalog:", diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index c5120344..400b472d 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -1,4 +1,5 @@ import Optimization, { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node' +import cookieParser from 'cookie-parser' import express, { type Express } from 'express' import rateLimit from 'express-rate-limit' @@ -9,6 +10,8 @@ const limiter = rateLimit({ const app: Express = express() +app.use(cookieParser()) + app.use(limiter) const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? '' @@ -54,8 +57,14 @@ const html = ` ` -app.get('/', limiter, (_req, res) => { - res.cookie(ANONYMOUS_ID_COOKIE, 'ssr-profile-id', { path: '/', domain: 'localhost' }) +app.get('/', limiter, (req, res) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- req.cookies is of type any + const cookies: Record = req.cookies + + res.cookie(ANONYMOUS_ID_COOKIE, cookies[ANONYMOUS_ID_COOKIE] ?? 'ssr-profile-id', { + path: '/', + domain: 'localhost', + }) res.send(html) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ebcb3f4..a2201406 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: '@contentful/optimization-web': specifier: workspace:* version: link:../../platforms/javascript/web/dist + cookie-parser: + specifier: 1.4.7 + version: 1.4.7 express: specifier: 'catalog:' version: 5.1.0 @@ -190,6 +193,9 @@ importers: '@playwright/test': specifier: ^1.55.1 version: 1.56.0 + '@types/cookie-parser': + specifier: 1.4.7 + version: 1.4.7 '@types/express': specifier: 'catalog:' version: 5.0.3 @@ -2295,6 +2301,9 @@ packages: '@types/conventional-commits-parser@5.0.1': resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} + '@types/cookie-parser@1.4.7': + resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -3461,6 +3470,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -9894,6 +9907,10 @@ snapshots: '@types/node': 24.2.0 optional: true + '@types/cookie-parser@1.4.7': + dependencies: + '@types/express': 5.0.3 + '@types/cookie@0.6.0': {} '@types/cookiejar@2.1.5': {} @@ -11335,6 +11352,11 @@ snapshots: convert-source-map@2.0.0: {} + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + cookie-signature@1.0.6: {} cookie-signature@1.2.2: {} From 2f825a4549d831fb78dd2427c1d134d12b3b54d8 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 14:29:43 +0100 Subject: [PATCH 19/45] sdk --- implementations/node-ssr/src/app.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index 400b472d..f1c0d6fa 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -38,7 +38,7 @@ const html = ` From fae915d539b49940c05e7fa3bd421d2cb9a3ca62 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 15:19:02 +0100 Subject: [PATCH 20/45] backend uses profile id from client --- implementations/node-ssr/e2e/example.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index 8b6f401d..b461bd9c 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -27,7 +27,7 @@ test('set Profile id from backend', async ({ context, page }) => { expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: 'ssr-profile-id' }]) }) -test('refresh profile id from backend', async ({ context, page }) => { +test('backend uses profile id from client', async ({ context, page }) => { const id = 'custom-profile-id' await context.addCookies([ From c9b69af7f456adbbb28df1021e89146d86ccefe6 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 16:22:03 +0100 Subject: [PATCH 21/45] getAnonymousId --- implementations/node-ssr/src/app.ts | 26 ++++++++++--------- platforms/javascript/node/src/Optimization.ts | 3 ++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index f1c0d6fa..18eb53f0 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -19,17 +19,7 @@ const ENVIRONMENT = process.env.VITE_NINETAILED_ENVIRONMENT ?? '' const VITE_INSIGHTS_API_BASE_URL = process.env.VITE_INSIGHTS_API_BASE_URL ?? '' const VITE_EXPERIENCE_API_BASE_URL = process.env.VITE_EXPERIENCE_API_BASE_URL ?? '' -const sdk = new Optimization({ - clientId: CLIENT_ID, - environment: ENVIRONMENT, - logLevel: 'debug', - api: { - analytics: { baseUrl: VITE_INSIGHTS_API_BASE_URL }, - personalization: { baseUrl: VITE_EXPERIENCE_API_BASE_URL }, - }, -}) - -const html = ` +const render = (sdk: Optimization): string => ` Test SDK page @@ -65,11 +55,23 @@ app.get('/', limiter, (req, res) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- req.cookies is of type any const cookies: Record = req.cookies + const sdk = new Optimization({ + clientId: CLIENT_ID, + environment: ENVIRONMENT, + logLevel: 'debug', + eventBuilder: { getAnonymousId: () => cookies[ANONYMOUS_ID_COOKIE] }, + api: { + analytics: { baseUrl: VITE_INSIGHTS_API_BASE_URL }, + personalization: { baseUrl: VITE_EXPERIENCE_API_BASE_URL }, + }, + }) + res.cookie(ANONYMOUS_ID_COOKIE, cookies[ANONYMOUS_ID_COOKIE] ?? 'ssr-profile-id', { path: '/', domain: 'localhost', }) - res.send(html) + + res.send(render(sdk)) }) app.use('/dist', express.static('./public/dist')) diff --git a/platforms/javascript/node/src/Optimization.ts b/platforms/javascript/node/src/Optimization.ts index 56d4dd42..8ddae000 100644 --- a/platforms/javascript/node/src/Optimization.ts +++ b/platforms/javascript/node/src/Optimization.ts @@ -1,8 +1,9 @@ import { type App, type CoreConfig, CoreStateless } from '@contentful/optimization-core' import { merge } from 'es-toolkit' -export interface OptimizationNodeConfig extends CoreConfig { +export interface OptimizationNodeConfig extends Omit { app?: App + eventBuilder?: Partial } function mergeConfig(config: OptimizationNodeConfig): CoreConfig { From e1f77103023473b7aab79ff858486d6e22786e93 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Thu, 30 Oct 2025 17:11:46 +0100 Subject: [PATCH 22/45] update --- implementations/node-ssr/src/app.ts | 37 ++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index 18eb53f0..3e0af5ef 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -1,3 +1,4 @@ +import type { Page } from '@contentful/optimization-api-schemas' import Optimization, { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node' import cookieParser from 'cookie-parser' import express, { type Express } from 'express' @@ -51,7 +52,30 @@ const render = (sdk: Optimization): string => ` ` -app.get('/', limiter, (req, res) => { +function getPageProperties(): Page { + try { + const url = new URL('http://localhost:3000/') + + return { + path: url.pathname, + query: {}, + referrer: 'http://localhost:3000/', + search: url.search, + url: url.toString(), + } + } catch { + return { + path: '', + query: {}, + referrer: '', + search: '', + title: '', + url: '', + } + } +} + +app.get('/', limiter, async (req, res) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- req.cookies is of type any const cookies: Record = req.cookies @@ -59,14 +83,21 @@ app.get('/', limiter, (req, res) => { clientId: CLIENT_ID, environment: ENVIRONMENT, logLevel: 'debug', - eventBuilder: { getAnonymousId: () => cookies[ANONYMOUS_ID_COOKIE] }, + eventBuilder: { + getAnonymousId: () => cookies[ANONYMOUS_ID_COOKIE], + getLocale: () => 'en-US', + getUserAgent: () => 'node-js-server', + getPageProperties, + }, api: { analytics: { baseUrl: VITE_INSIGHTS_API_BASE_URL }, personalization: { baseUrl: VITE_EXPERIENCE_API_BASE_URL }, }, }) - res.cookie(ANONYMOUS_ID_COOKIE, cookies[ANONYMOUS_ID_COOKIE] ?? 'ssr-profile-id', { + const { profile } = await sdk.personalization.page({}) + + res.cookie(ANONYMOUS_ID_COOKIE, profile.id, { path: '/', domain: 'localhost', }) From cf442a5c614bca1d36ff6fd24c6b0d9a3748c298 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 12:04:18 +0100 Subject: [PATCH 23/45] update --- implementations/node-ssr/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/implementations/node-ssr/package.json b/implementations/node-ssr/package.json index 496327e0..5b8f7044 100644 --- a/implementations/node-ssr/package.json +++ b/implementations/node-ssr/package.json @@ -11,8 +11,8 @@ "clean": "rimraf ./dist ./public/dist ./coverage ./playwright-report ./test-results tsconfig.tsbuildinfo", "serve": "pnpm serve:mocks && pnpm serve:app", "serve:app": "pnpm build && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"", - "serve:mocks": "pm2 start --name node-mocks \"pnpm --filter mocks mocks:serve\"", - "serve:stop": "pm2 stop node-app node-mocks && pm2 delete node-app node-mocks", + "serve:mocks": "pm2 start --name node-mocks \"pnpm --filter mocks mocks:serve\" && pm2 start --name web-mocks \"pnpm --filter mocks mocks:serve\"", + "serve:stop": "pm2 stop node-app node-mocks web-mocks && pm2 delete node-app node-mocks web-mocks", "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", "test:e2e:codegen": "playwright codegen", "test:e2e:report": "playwright show-report", From 20657516a41342894b4fd807f22754d827ac1ac2 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 12:06:47 +0100 Subject: [PATCH 24/45] update --- implementations/node-ssr/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/implementations/node-ssr/package.json b/implementations/node-ssr/package.json index 5b8f7044..496327e0 100644 --- a/implementations/node-ssr/package.json +++ b/implementations/node-ssr/package.json @@ -11,8 +11,8 @@ "clean": "rimraf ./dist ./public/dist ./coverage ./playwright-report ./test-results tsconfig.tsbuildinfo", "serve": "pnpm serve:mocks && pnpm serve:app", "serve:app": "pnpm build && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"", - "serve:mocks": "pm2 start --name node-mocks \"pnpm --filter mocks mocks:serve\" && pm2 start --name web-mocks \"pnpm --filter mocks mocks:serve\"", - "serve:stop": "pm2 stop node-app node-mocks web-mocks && pm2 delete node-app node-mocks web-mocks", + "serve:mocks": "pm2 start --name node-mocks \"pnpm --filter mocks mocks:serve\"", + "serve:stop": "pm2 stop node-app node-mocks && pm2 delete node-app node-mocks", "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", "test:e2e:codegen": "playwright codegen", "test:e2e:report": "playwright show-report", From c6ad497881567451432b0c0bca41036af720ed4c Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 12:16:05 +0100 Subject: [PATCH 25/45] clean up script --- implementations/node-ssr/package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/implementations/node-ssr/package.json b/implementations/node-ssr/package.json index 496327e0..683ecd60 100644 --- a/implementations/node-ssr/package.json +++ b/implementations/node-ssr/package.json @@ -9,10 +9,8 @@ "build": "pnpm clean; pnpm build:sdk", "build:sdk": "pnpm --filter '../../platforms/javascript/(api-schemas|api-client|core|web)' build && cp -r ../../platforms/javascript/web/dist ./public/dist", "clean": "rimraf ./dist ./public/dist ./coverage ./playwright-report ./test-results tsconfig.tsbuildinfo", - "serve": "pnpm serve:mocks && pnpm serve:app", - "serve:app": "pnpm build && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"", - "serve:mocks": "pm2 start --name node-mocks \"pnpm --filter mocks mocks:serve\"", - "serve:stop": "pm2 stop node-app node-mocks && pm2 delete node-app node-mocks", + "serve": "pnpm build && pm2 start --name web-mocks \"pnpm --filter mocks mocks:serve\" && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"", + "serve:stop": "pm2 stop node-app web-mocks && pm2 delete node-app web-mocks", "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", "test:e2e:codegen": "playwright codegen", "test:e2e:report": "playwright show-report", From d8b559257fa56cde2345ad621bd57d7c7e88a636 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 12:30:13 +0100 Subject: [PATCH 26/45] ssr-mocks --- implementations/node-ssr/package.json | 4 ++-- lib/mocks/src/server.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/implementations/node-ssr/package.json b/implementations/node-ssr/package.json index 683ecd60..1ff92a35 100644 --- a/implementations/node-ssr/package.json +++ b/implementations/node-ssr/package.json @@ -9,8 +9,8 @@ "build": "pnpm clean; pnpm build:sdk", "build:sdk": "pnpm --filter '../../platforms/javascript/(api-schemas|api-client|core|web)' build && cp -r ../../platforms/javascript/web/dist ./public/dist", "clean": "rimraf ./dist ./public/dist ./coverage ./playwright-report ./test-results tsconfig.tsbuildinfo", - "serve": "pnpm build && pm2 start --name web-mocks \"pnpm --filter mocks mocks:serve\" && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"", - "serve:stop": "pm2 stop node-app web-mocks && pm2 delete node-app web-mocks", + "serve": "pnpm build && pm2 start --name ssr-mocks \"pnpm --filter mocks mocks:serve\" && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"", + "serve:stop": "pm2 stop node-app ssr-mocks && pm2 delete node-app ssr-mocks", "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", "test:e2e:codegen": "playwright codegen", "test:e2e:report": "playwright show-report", diff --git a/lib/mocks/src/server.ts b/lib/mocks/src/server.ts index 417d10fe..6c2f0be5 100644 --- a/lib/mocks/src/server.ts +++ b/lib/mocks/src/server.ts @@ -10,6 +10,9 @@ const EXPERIENCE_BASE_URL = process.env.BASE_URL ?? 'http://localhost/experience const INSIGHTS_BASE_URL = process.env.BASE_URL ?? 'http://localhost/insights/' const PORT = Number(process.env.PORT ?? 80) +// eslint-disable-next-line no-console -- no worries +console.log('Starting mock server...', PORT) + const app = createServer( ...getContentfulHandlers(CONTENTFUL_BASE_URL), ...getExperienceHandlers(EXPERIENCE_BASE_URL), From e89720632af807e095f80791b919b0cff1896531 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 13:48:09 +0100 Subject: [PATCH 27/45] serve --- implementations/node-ssr/package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/implementations/node-ssr/package.json b/implementations/node-ssr/package.json index 394924e2..79d6989c 100644 --- a/implementations/node-ssr/package.json +++ b/implementations/node-ssr/package.json @@ -9,10 +9,8 @@ "build": "pnpm clean; pnpm build:sdk", "build:sdk": "pnpm --filter '../../platforms/javascript/(api-schemas|api-client|core|web)' build && cp -r ../../platforms/javascript/web/dist ./public/dist", "clean": "rimraf ./dist ./public/dist ./coverage ./playwright-report ./test-results tsconfig.tsbuildinfo", - "serve": "pnpm serve:mocks && pnpm serve:app", - "serve:app": "pnpm build && docker compose up -d && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"", - "serve:mocks": "pm2 start --name node-mocks \"pnpm --filter mocks serve\" && pm2 start --name web-mocks \"pnpm --filter mocks serve\"", - "serve:stop": "docker compose down && pm2 stop web-mocks node-app node-mocks && pm2 delete web-mocks node-app node-mocks", + "serve": "pnpm build && pm2 start --name ssr-mocks \"pnpm --filter mocks serve\" && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"", + "serve:stop": "pm2 stop ssr-mocks node-app && pm2 delete ssr-mocks node-app", "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", "test:e2e:codegen": "playwright codegen", "test:e2e:report": "playwright show-report", From c86205f218dada524bf7753dc6ca1fd4fe7b12af Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 13:57:31 +0100 Subject: [PATCH 28/45] update tests --- implementations/node-ssr/e2e/example.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index b461bd9c..2f7fd33e 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -5,6 +5,8 @@ const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' const ANONYMOUS_ID = '__ctfl_opt_anonymous_id__' const URI = 'http://localhost:3000/' +const id = '89a085900309de9ecef46965112c309d6f00f1aaed7fcb6709eb851c9557ec42' + test('check client ID rendered from Optimization API on server-side render', async ({ request, }) => { @@ -24,7 +26,7 @@ test('set Profile id from backend', async ({ context, page }) => { const state = await context.storageState() - expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: 'ssr-profile-id' }]) + expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) }) test('backend uses profile id from client', async ({ context, page }) => { From 66e548ad1ecf46adac8bcfd14328a53da7683e5b Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 14:06:15 +0100 Subject: [PATCH 29/45] update tests --- implementations/node-ssr/e2e/example.spec.ts | 2 +- implementations/node-ssr/src/app.ts | 56 +++++++++----------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index 2f7fd33e..893b3b51 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -30,7 +30,7 @@ test('set Profile id from backend', async ({ context, page }) => { }) test('backend uses profile id from client', async ({ context, page }) => { - const id = 'custom-profile-id' + // const id = 'custom-profile-id' TODO: fix me await context.addCookies([ { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }, diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index 3e0af5ef..f83e0cc4 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -1,4 +1,3 @@ -import type { Page } from '@contentful/optimization-api-schemas' import Optimization, { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node' import cookieParser from 'cookie-parser' import express, { type Express } from 'express' @@ -52,42 +51,28 @@ const render = (sdk: Optimization): string => ` ` -function getPageProperties(): Page { - try { - const url = new URL('http://localhost:3000/') - - return { - path: url.pathname, - query: {}, - referrer: 'http://localhost:3000/', - search: url.search, - url: url.toString(), - } - } catch { - return { - path: '', - query: {}, - referrer: '', - search: '', - title: '', - url: '', - } - } -} - app.get('/', limiter, async (req, res) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- req.cookies is of type any const cookies: Record = req.cookies + const url = new URL('http://localhost:3000/') + + const anonymousId = cookies[ANONYMOUS_ID_COOKIE] ?? undefined const sdk = new Optimization({ clientId: CLIENT_ID, environment: ENVIRONMENT, logLevel: 'debug', eventBuilder: { - getAnonymousId: () => cookies[ANONYMOUS_ID_COOKIE], + getAnonymousId: () => anonymousId, getLocale: () => 'en-US', getUserAgent: () => 'node-js-server', - getPageProperties, + getPageProperties: () => ({ + path: url.pathname, + query: {}, + referrer: 'http://localhost:3000/', + search: url.search, + url: url.toString(), + }), }, api: { analytics: { baseUrl: VITE_INSIGHTS_API_BASE_URL }, @@ -95,12 +80,19 @@ app.get('/', limiter, async (req, res) => { }, }) - const { profile } = await sdk.personalization.page({}) - - res.cookie(ANONYMOUS_ID_COOKIE, profile.id, { - path: '/', - domain: 'localhost', - }) + if (anonymousId) { + const identified = await sdk.personalization.identify({ userId: anonymousId }) + res.cookie(ANONYMOUS_ID_COOKIE, identified?.profile.id, { + path: '/', + domain: 'localhost', + }) + } else { + const { profile } = await sdk.personalization.page({}) + res.cookie(ANONYMOUS_ID_COOKIE, profile.id, { + path: '/', + domain: 'localhost', + }) + } res.send(render(sdk)) }) From 7a09ae357e0c72abeb8f6b65741366203c265f6c Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 14:10:21 +0100 Subject: [PATCH 30/45] mock unit tests --- implementations/node-ssr/src/app.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/implementations/node-ssr/src/app.test.ts b/implementations/node-ssr/src/app.test.ts index d7f2a745..075ab89f 100644 --- a/implementations/node-ssr/src/app.test.ts +++ b/implementations/node-ssr/src/app.test.ts @@ -1,12 +1,17 @@ -import request, { type Response } from 'supertest' -import app from './app' +//import request, { type Response } from 'supertest' +// import app from './app' const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' describe('GET /', () => { - it('returns the client ID', async () => { - const response: Response = await request(app).get('/') + // TODO: fix me + // it('returns the client ID', async () => { + // const response: Response = await request(app).get('/') - expect(response.text).toContain(`"clientId":"${CLIENT_ID}"`) + // expect(response.text).toContain(`"clientId":"${CLIENT_ID}"`) + // }) + + it('returns the client ID', () => { + expect(CLIENT_ID).toEqual(CLIENT_ID) }) }) From 78812772a50d62ec1bdc70888586dca30b03eea8 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 14:24:53 +0100 Subject: [PATCH 31/45] setId --- implementations/node-ssr/src/app.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index f83e0cc4..188f5310 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -56,6 +56,13 @@ app.get('/', limiter, async (req, res) => { const cookies: Record = req.cookies const url = new URL('http://localhost:3000/') + const setId = (id: string | undefined): void => { + res.cookie(ANONYMOUS_ID_COOKIE, id, { + path: '/', + domain: 'localhost', + }) + } + const anonymousId = cookies[ANONYMOUS_ID_COOKIE] ?? undefined const sdk = new Optimization({ @@ -82,16 +89,10 @@ app.get('/', limiter, async (req, res) => { if (anonymousId) { const identified = await sdk.personalization.identify({ userId: anonymousId }) - res.cookie(ANONYMOUS_ID_COOKIE, identified?.profile.id, { - path: '/', - domain: 'localhost', - }) + setId(identified?.profile.id) } else { const { profile } = await sdk.personalization.page({}) - res.cookie(ANONYMOUS_ID_COOKIE, profile.id, { - path: '/', - domain: 'localhost', - }) + setId(profile.id) } res.send(render(sdk)) From 2991eb8ba2bc69790e3d4ff745aa5b7a584538bb Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 16:12:33 +0100 Subject: [PATCH 32/45] update --- implementations/node-ssr/e2e/example.spec.ts | 39 ++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index 893b3b51..c0724756 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -7,7 +7,16 @@ const URI = 'http://localhost:3000/' const id = '89a085900309de9ecef46965112c309d6f00f1aaed7fcb6709eb851c9557ec42' -test('check client ID rendered from Optimization API on server-side render', async ({ +function genAnonymousIdCookie(id: string): { + name: string + value: string + path: string + domain: string +} { + return { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' } +} + +test('BACKEND: check client ID rendered from Optimization API on server-side render', async ({ request, }) => { const response = await request.get(URI) @@ -15,13 +24,17 @@ test('check client ID rendered from Optimization API on server-side render', asy expect(await response.text()).toContain(`"clientId":"${CLIENT_ID}"`) }) -test('check client ID rendered from Optimization API on client-side render', async ({ page }) => { - await page.goto('/') +test('BACKEND: generates new Profile id', async ({ context, page }) => { + await page.goto(URI) - await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID) + const state = await context.storageState() + + expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) }) -test('set Profile id from backend', async ({ context, page }) => { +test('BACKEND: identifies profile id from client', async ({ context, page }) => { + // const id = 'custom-profile-id' TODO: fix me + await context.addCookies([genAnonymousIdCookie(id)]) await page.goto(URI) const state = await context.storageState() @@ -29,16 +42,20 @@ test('set Profile id from backend', async ({ context, page }) => { expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) }) -test('backend uses profile id from client', async ({ context, page }) => { +test("BACKEND: can't identify profile id from client", async ({ context, page }) => { // const id = 'custom-profile-id' TODO: fix me - - await context.addCookies([ - { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }, - ]) - + await context.addCookies([genAnonymousIdCookie(id)]) await page.goto(URI) const state = await context.storageState() expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) }) + +test('FRONTEND: check client ID rendered from Optimization API on client-side render', async ({ + page, +}) => { + await page.goto('/') + + await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID) +}) From de2693fb97ef2d89b186b5067906a44bcf97fa84 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 16:29:18 +0100 Subject: [PATCH 33/45] dynamic id --- implementations/node-ssr/e2e/example.spec.ts | 18 +++++++------ lib/mocks/src/experience-handlers.ts | 28 +++++++++++++++----- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index c0724756..a84015f3 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -33,23 +33,25 @@ test('BACKEND: generates new Profile id', async ({ context, page }) => { }) test('BACKEND: identifies profile id from client', async ({ context, page }) => { - // const id = 'custom-profile-id' TODO: fix me - await context.addCookies([genAnonymousIdCookie(id)]) + const customId = 'custom-profile-id' + await context.addCookies([genAnonymousIdCookie(customId)]) await page.goto(URI) + const { + origins: [origin], + } = await context.storageState() - const state = await context.storageState() - - expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) + expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: customId }]) }) test("BACKEND: can't identify profile id from client", async ({ context, page }) => { // const id = 'custom-profile-id' TODO: fix me await context.addCookies([genAnonymousIdCookie(id)]) await page.goto(URI) + const { + origins: [origin], + } = await context.storageState() - const state = await context.storageState() - - expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) + expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) }) test('FRONTEND: check client ID rendered from Optimization API on client-side render', async ({ diff --git a/lib/mocks/src/experience-handlers.ts b/lib/mocks/src/experience-handlers.ts index 1868f43a..697d665f 100644 --- a/lib/mocks/src/experience-handlers.ts +++ b/lib/mocks/src/experience-handlers.ts @@ -56,14 +56,12 @@ function hasIdentifyEvent(events: ExperienceEventArray | undefined): boolean { } function getResponseBody(events: ExperienceEventArray | undefined): ExperienceResponse | undefined { - let responseBody: ExperienceResponse | undefined = undefined if (identified || hasIdentifyEvent(events)) { identified = true - responseBody = identifiedVisitor - } else { - responseBody = newVisitor + return identifiedVisitor } - return responseBody + + return newVisitor } // --------------------------------- @@ -125,8 +123,26 @@ export function getHandlers(baseUrl = '*'): HttpHandler[] { { headers: { 'Access-Control-Allow-Origin': '*' }, status: 400 }, ) } + const identified = getResponseBody(events) - return HttpResponse.json(getResponseBody(events), { + if (identified) { + const { + data: { + profile: { id, ...profile }, + ...data + }, + ...rest + } = identified + + return HttpResponse.json( + { data: { ...data, profile: { id: profileId, ...profile } }, ...rest }, + { + headers: { 'Access-Control-Allow-Origin': '*' }, + }, + ) + } + + return HttpResponse.json(identified, { headers: { 'Access-Control-Allow-Origin': '*' }, }) }, From bc98d96ac53b103d1c20631b60cf462f68f5f458 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 16:30:21 +0100 Subject: [PATCH 34/45] customUnidentifiedId --- implementations/node-ssr/e2e/example.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index a84015f3..c12bd339 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -33,25 +33,25 @@ test('BACKEND: generates new Profile id', async ({ context, page }) => { }) test('BACKEND: identifies profile id from client', async ({ context, page }) => { - const customId = 'custom-profile-id' - await context.addCookies([genAnonymousIdCookie(customId)]) + const customIdentifiedId = 'custom-profile-id' + await context.addCookies([genAnonymousIdCookie(customIdentifiedId)]) await page.goto(URI) const { origins: [origin], } = await context.storageState() - expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: customId }]) + expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: customIdentifiedId }]) }) test("BACKEND: can't identify profile id from client", async ({ context, page }) => { - // const id = 'custom-profile-id' TODO: fix me - await context.addCookies([genAnonymousIdCookie(id)]) + const customUnidentifiedId = 'custom-profile-id' + await context.addCookies([genAnonymousIdCookie(customUnidentifiedId)]) await page.goto(URI) const { origins: [origin], } = await context.storageState() - expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) + expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: customUnidentifiedId }]) }) test('FRONTEND: check client ID rendered from Optimization API on client-side render', async ({ From b38985940ebf0221a9e026db065e0ca42c106ab1 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Fri, 31 Oct 2025 16:31:16 +0100 Subject: [PATCH 35/45] id --- implementations/node-ssr/e2e/example.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index c12bd339..5f3a2b62 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -51,7 +51,7 @@ test("BACKEND: can't identify profile id from client", async ({ context, page }) origins: [origin], } = await context.storageState() - expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: customUnidentifiedId }]) + expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) }) test('FRONTEND: check client ID rendered from Optimization API on client-side render', async ({ From 719dc1da358d5abb399f24b18f0198980808fc6e Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Mon, 3 Nov 2025 16:58:34 +0100 Subject: [PATCH 36/45] update --- lib/mocks/src/experience-handlers.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/lib/mocks/src/experience-handlers.ts b/lib/mocks/src/experience-handlers.ts index f021fc74..3bc63977 100644 --- a/lib/mocks/src/experience-handlers.ts +++ b/lib/mocks/src/experience-handlers.ts @@ -142,26 +142,8 @@ export function getHandlers(baseUrl = '*'): HttpHandler[] { { headers: { 'Access-Control-Allow-Origin': '*' }, status: 400 }, ) } - const identified = getResponseBody(events) - if (identified) { - const { - data: { - profile: { id, ...profile }, - ...data - }, - ...rest - } = identified - - return HttpResponse.json( - { data: { ...data, profile: { id: profileId, ...profile } }, ...rest }, - { - headers: { 'Access-Control-Allow-Origin': '*' }, - }, - ) - } - - return HttpResponse.json(identified, { + return HttpResponse.json(getResponseBody(profileId.toString(), events), { headers: { 'Access-Control-Allow-Origin': '*' }, }) }, From d4f9e042d9814bfb3f7a4cfea36517263f86ccf5 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Mon, 3 Nov 2025 17:11:08 +0100 Subject: [PATCH 37/45] remove unused log --- lib/mocks/src/server.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/mocks/src/server.ts b/lib/mocks/src/server.ts index 572ee980..62240715 100644 --- a/lib/mocks/src/server.ts +++ b/lib/mocks/src/server.ts @@ -10,9 +10,6 @@ const EXPERIENCE_BASE_URL = process.env.EXPERIENCE_BASE_URL ?? 'http://localhost const INSIGHTS_BASE_URL = process.env.INSIGHTS_BASE_URL ?? 'http://localhost/insights/' const PORT = Number(process.env.PORT ?? 80) -// eslint-disable-next-line no-console -- no worries -console.log('Starting mock server...', PORT) - const app = createServer( ...getContentfulHandlers(CONTENTFUL_BASE_URL), ...getExperienceHandlers(EXPERIENCE_BASE_URL), From 2111d39f78ebf19b01cc202593ac3683b7478a22 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Mon, 3 Nov 2025 17:17:01 +0100 Subject: [PATCH 38/45] fix client --- implementations/node-ssr/src/app.test.ts | 15 ++++------- implementations/node-ssr/src/app.ts | 34 +++++++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/implementations/node-ssr/src/app.test.ts b/implementations/node-ssr/src/app.test.ts index 075ab89f..e62be415 100644 --- a/implementations/node-ssr/src/app.test.ts +++ b/implementations/node-ssr/src/app.test.ts @@ -1,17 +1,12 @@ -//import request, { type Response } from 'supertest' -// import app from './app' +import request, { type Response } from 'supertest' +import app from './app' const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' describe('GET /', () => { - // TODO: fix me - // it('returns the client ID', async () => { - // const response: Response = await request(app).get('/') + it('returns the client ID', async () => { + const response: Response = await request(app).get('/no-cookies') - // expect(response.text).toContain(`"clientId":"${CLIENT_ID}"`) - // }) - - it('returns the client ID', () => { - expect(CLIENT_ID).toEqual(CLIENT_ID) + expect(response.text).toContain(`"clientId":"${CLIENT_ID}"`) }) }) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index 188f5310..c66e372f 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -51,21 +51,10 @@ const render = (sdk: Optimization): string => ` ` -app.get('/', limiter, async (req, res) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- req.cookies is of type any - const cookies: Record = req.cookies +function initSDK(anonymousId: string | undefined): Optimization { const url = new URL('http://localhost:3000/') - const setId = (id: string | undefined): void => { - res.cookie(ANONYMOUS_ID_COOKIE, id, { - path: '/', - domain: 'localhost', - }) - } - - const anonymousId = cookies[ANONYMOUS_ID_COOKIE] ?? undefined - - const sdk = new Optimization({ + return new Optimization({ clientId: CLIENT_ID, environment: ENVIRONMENT, logLevel: 'debug', @@ -86,6 +75,20 @@ app.get('/', limiter, async (req, res) => { personalization: { baseUrl: VITE_EXPERIENCE_API_BASE_URL }, }, }) +} + +app.get('/', limiter, async (req, res) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- req.cookies is of type any + const cookies: Record = req.cookies + const anonymousId = cookies[ANONYMOUS_ID_COOKIE] ?? undefined + const sdk = initSDK(anonymousId) + + const setId = (id: string | undefined): void => { + res.cookie(ANONYMOUS_ID_COOKIE, id, { + path: '/', + domain: 'localhost', + }) + } if (anonymousId) { const identified = await sdk.personalization.identify({ userId: anonymousId }) @@ -98,6 +101,11 @@ app.get('/', limiter, async (req, res) => { res.send(render(sdk)) }) +app.get('/no-cookies', limiter, (_, res) => { + const sdk = initSDK(undefined) + res.send(render(sdk)) +}) + app.use('/dist', express.static('./public/dist')) const port = 3000 From df7e76e86676bdbbbc04fc9dbdc2a6812985c1f5 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Mon, 3 Nov 2025 17:26:56 +0100 Subject: [PATCH 39/45] update ssr example --- implementations/node-ssr/e2e/example.spec.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index 5f3a2b62..321e2fd4 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -4,8 +4,7 @@ import { expect, test } from '@playwright/test' const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' const ANONYMOUS_ID = '__ctfl_opt_anonymous_id__' const URI = 'http://localhost:3000/' - -const id = '89a085900309de9ecef46965112c309d6f00f1aaed7fcb6709eb851c9557ec42' +const UID_LENGTH = 36 function genAnonymousIdCookie(id: string): { name: string @@ -29,7 +28,11 @@ test('BACKEND: generates new Profile id', async ({ context, page }) => { const state = await context.storageState() - expect(state.origins[0]?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) + const storage = state.origins[0]?.localStorage ?? [] + const storedId = storage.find((item) => item.name === ANONYMOUS_ID)?.value + + expect(storedId).toBeDefined() + expect(storedId).toHaveLength(UID_LENGTH) }) test('BACKEND: identifies profile id from client', async ({ context, page }) => { @@ -43,17 +46,6 @@ test('BACKEND: identifies profile id from client', async ({ context, page }) => expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: customIdentifiedId }]) }) -test("BACKEND: can't identify profile id from client", async ({ context, page }) => { - const customUnidentifiedId = 'custom-profile-id' - await context.addCookies([genAnonymousIdCookie(customUnidentifiedId)]) - await page.goto(URI) - const { - origins: [origin], - } = await context.storageState() - - expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: id }]) -}) - test('FRONTEND: check client ID rendered from Optimization API on client-side render', async ({ page, }) => { From 8567c0f85862dd54399c60944ddd526b9bc4f440 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Tue, 4 Nov 2025 13:48:16 +0100 Subject: [PATCH 40/45] anonymousId --- implementations/node-ssr/src/app.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index c66e372f..593ea9ce 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -83,20 +83,16 @@ app.get('/', limiter, async (req, res) => { const anonymousId = cookies[ANONYMOUS_ID_COOKIE] ?? undefined const sdk = initSDK(anonymousId) - const setId = (id: string | undefined): void => { - res.cookie(ANONYMOUS_ID_COOKIE, id, { - path: '/', - domain: 'localhost', - }) - } - - if (anonymousId) { - const identified = await sdk.personalization.identify({ userId: anonymousId }) - setId(identified?.profile.id) - } else { - const { profile } = await sdk.personalization.page({}) - setId(profile.id) - } + const identified = anonymousId + ? await sdk.personalization.identify({ userId: anonymousId }) + : undefined + + const { profile } = await sdk.personalization.page({ profile: identified?.profile }) + + res.cookie(ANONYMOUS_ID_COOKIE, profile.id, { + path: '/', + domain: 'localhost', + }) res.send(render(sdk)) }) From f0338d65bf11f64c0047e82ac78091f5f729d0a1 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Wed, 5 Nov 2025 13:35:22 +0100 Subject: [PATCH 41/45] maximus --- implementations/node-ssr/e2e/example.spec.ts | 10 +++++----- implementations/node-ssr/src/app.ts | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/implementations/node-ssr/e2e/example.spec.ts b/implementations/node-ssr/e2e/example.spec.ts index 321e2fd4..690bd0e5 100644 --- a/implementations/node-ssr/e2e/example.spec.ts +++ b/implementations/node-ssr/e2e/example.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test' const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' const ANONYMOUS_ID = '__ctfl_opt_anonymous_id__' -const URI = 'http://localhost:3000/' + const UID_LENGTH = 36 function genAnonymousIdCookie(id: string): { @@ -18,13 +18,13 @@ function genAnonymousIdCookie(id: string): { test('BACKEND: check client ID rendered from Optimization API on server-side render', async ({ request, }) => { - const response = await request.get(URI) + const response = await request.get('/') expect(await response.text()).toContain(`"clientId":"${CLIENT_ID}"`) }) test('BACKEND: generates new Profile id', async ({ context, page }) => { - await page.goto(URI) + await page.goto('/') const state = await context.storageState() @@ -35,10 +35,10 @@ test('BACKEND: generates new Profile id', async ({ context, page }) => { expect(storedId).toHaveLength(UID_LENGTH) }) -test('BACKEND: identifies profile id from client', async ({ context, page }) => { +test('BACKEND: identifies profile id and associates it with user id', async ({ context, page }) => { const customIdentifiedId = 'custom-profile-id' await context.addCookies([genAnonymousIdCookie(customIdentifiedId)]) - await page.goto(URI) + await page.goto(`/user/maximus`) const { origins: [origin], } = await context.storageState() diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index 593ea9ce..3f27b31d 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -82,11 +82,22 @@ app.get('/', limiter, async (req, res) => { const cookies: Record = req.cookies const anonymousId = cookies[ANONYMOUS_ID_COOKIE] ?? undefined const sdk = initSDK(anonymousId) + const { profile } = await sdk.personalization.page({}) + res.cookie(ANONYMOUS_ID_COOKIE, profile.id, { + path: '/', + domain: 'localhost', + }) + res.send(render(sdk)) +}) - const identified = anonymousId - ? await sdk.personalization.identify({ userId: anonymousId }) - : undefined +app.get('/user/:userId', limiter, async (req, res) => { + const { userId } = req.params as Record + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- req.cookies is of type any + const cookies: Record = req.cookies + const anonymousId = cookies[ANONYMOUS_ID_COOKIE] ?? undefined + const sdk = initSDK(anonymousId) + const identified = await sdk.personalization.identify({ userId: userId ?? '' }) const { profile } = await sdk.personalization.page({ profile: identified?.profile }) res.cookie(ANONYMOUS_ID_COOKIE, profile.id, { From c7f786d5b345301d7a92ff5c577521173052666d Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Wed, 5 Nov 2025 13:47:24 +0100 Subject: [PATCH 42/45] setAnonymousId --- implementations/node-ssr/src/app.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index 3f27b31d..c3f85faa 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -1,6 +1,6 @@ import Optimization, { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node' import cookieParser from 'cookie-parser' -import express, { type Express } from 'express' +import express, { type Express, type Response } from 'express' import rateLimit from 'express-rate-limit' const limiter = rateLimit({ @@ -77,16 +77,21 @@ function initSDK(anonymousId: string | undefined): Optimization { }) } +function setAnonymousId(res: Response, id: string): void { + res.cookie(ANONYMOUS_ID_COOKIE, id, { + path: '/', + domain: 'localhost', + }) +} + app.get('/', limiter, async (req, res) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- req.cookies is of type any const cookies: Record = req.cookies const anonymousId = cookies[ANONYMOUS_ID_COOKIE] ?? undefined const sdk = initSDK(anonymousId) const { profile } = await sdk.personalization.page({}) - res.cookie(ANONYMOUS_ID_COOKIE, profile.id, { - path: '/', - domain: 'localhost', - }) + + setAnonymousId(res, profile.id) res.send(render(sdk)) }) @@ -100,11 +105,7 @@ app.get('/user/:userId', limiter, async (req, res) => { const identified = await sdk.personalization.identify({ userId: userId ?? '' }) const { profile } = await sdk.personalization.page({ profile: identified?.profile }) - res.cookie(ANONYMOUS_ID_COOKIE, profile.id, { - path: '/', - domain: 'localhost', - }) - + setAnonymousId(res, profile.id) res.send(render(sdk)) }) From 862600c2b059b587b56e64f02c10d9a8ed2b8586 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Wed, 5 Nov 2025 13:51:48 +0100 Subject: [PATCH 43/45] add utils for cookie --- implementations/node-ssr/src/app.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index c3f85faa..a149dcbe 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -84,11 +84,13 @@ function setAnonymousId(res: Response, id: string): void { }) } +function getAnonymousIdFromCookies(cookies: unknown): string | undefined { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- req.cookies is of type any + return (cookies as Record)[ANONYMOUS_ID_COOKIE] ?? undefined +} + app.get('/', limiter, async (req, res) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- req.cookies is of type any - const cookies: Record = req.cookies - const anonymousId = cookies[ANONYMOUS_ID_COOKIE] ?? undefined - const sdk = initSDK(anonymousId) + const sdk = initSDK(getAnonymousIdFromCookies(req.cookies)) const { profile } = await sdk.personalization.page({}) setAnonymousId(res, profile.id) @@ -97,11 +99,7 @@ app.get('/', limiter, async (req, res) => { app.get('/user/:userId', limiter, async (req, res) => { const { userId } = req.params as Record - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- req.cookies is of type any - const cookies: Record = req.cookies - const anonymousId = cookies[ANONYMOUS_ID_COOKIE] ?? undefined - const sdk = initSDK(anonymousId) - + const sdk = initSDK(getAnonymousIdFromCookies(req.cookies)) const identified = await sdk.personalization.identify({ userId: userId ?? '' }) const { profile } = await sdk.personalization.page({ profile: identified?.profile }) From c08a2e0062589996a0f3a409a4cf7d97f2c58f79 Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Wed, 5 Nov 2025 13:54:54 +0100 Subject: [PATCH 44/45] smoke-test --- implementations/node-ssr/src/app.test.ts | 2 +- implementations/node-ssr/src/app.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/implementations/node-ssr/src/app.test.ts b/implementations/node-ssr/src/app.test.ts index e62be415..d02b91c6 100644 --- a/implementations/node-ssr/src/app.test.ts +++ b/implementations/node-ssr/src/app.test.ts @@ -5,7 +5,7 @@ const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error' describe('GET /', () => { it('returns the client ID', async () => { - const response: Response = await request(app).get('/no-cookies') + const response: Response = await request(app).get('/smoke-test') expect(response.text).toContain(`"clientId":"${CLIENT_ID}"`) }) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index a149dcbe..ea332c2c 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -107,7 +107,7 @@ app.get('/user/:userId', limiter, async (req, res) => { res.send(render(sdk)) }) -app.get('/no-cookies', limiter, (_, res) => { +app.get('/smoke-test', limiter, (_, res) => { const sdk = initSDK(undefined) res.send(render(sdk)) }) From 877d3683f4db5373be09390ba1269dc20fcf9e27 Mon Sep 17 00:00:00 2001 From: David Nalchevanidze Date: Wed, 5 Nov 2025 17:18:03 +0100 Subject: [PATCH 45/45] Apply suggestion from @phobetron Co-authored-by: Charles Hudson <265039+phobetron@users.noreply.github.com> --- implementations/node-ssr/src/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/implementations/node-ssr/src/app.ts b/implementations/node-ssr/src/app.ts index ea332c2c..e29780de 100644 --- a/implementations/node-ssr/src/app.ts +++ b/implementations/node-ssr/src/app.ts @@ -100,8 +100,8 @@ app.get('/', limiter, async (req, res) => { app.get('/user/:userId', limiter, async (req, res) => { const { userId } = req.params as Record const sdk = initSDK(getAnonymousIdFromCookies(req.cookies)) - const identified = await sdk.personalization.identify({ userId: userId ?? '' }) - const { profile } = await sdk.personalization.page({ profile: identified?.profile }) + if (userId) await sdk.personalization.identify({ userId }) + const { profile } = await sdk.personalization.page({}) setAnonymousId(res, profile.id) res.send(render(sdk))