From 56bf598eebbf1ca1ec4ca97a1bf8fad9177a9503 Mon Sep 17 00:00:00 2001 From: Khaalid Date: Wed, 22 Jul 2026 11:33:08 +0200 Subject: [PATCH] feat: PoC for CF-Entities V3 Experience API paths Patches the ExperienceApiClient to use V3 space-scoped paths: v3/spaces/{spaceId}/environments/{environment}/profiles Adds a minimal React PoC app (implementations/cf-entities-poc/) that: - Initializes the Optimization SDK with spaceId as clientId - Points to the dev Experience API (experience.ninetailed.engineering) - Sends identify/page events to trigger audience evaluation - Displays the raw API response showing experience resolution The V3 path reads config from SPACE_ENV_CONFIGS_KV[spaceId:envId] which is populated by the config pusher in ninetailed-new. --- implementations/cf-entities-poc/index.html | 26 +++ implementations/cf-entities-poc/package.json | 25 +++ .../cf-entities-poc/pnpm-workspace.yaml | 4 + .../cf-entities-poc/rsbuild.config.ts | 25 +++ implementations/cf-entities-poc/src/App.tsx | 174 ++++++++++++++++++ implementations/cf-entities-poc/src/main.tsx | 7 + implementations/cf-entities-poc/tsconfig.json | 15 ++ .../src/experience/ExperienceApiClient.ts | 8 +- 8 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 implementations/cf-entities-poc/index.html create mode 100644 implementations/cf-entities-poc/package.json create mode 100644 implementations/cf-entities-poc/pnpm-workspace.yaml create mode 100644 implementations/cf-entities-poc/rsbuild.config.ts create mode 100644 implementations/cf-entities-poc/src/App.tsx create mode 100644 implementations/cf-entities-poc/src/main.tsx create mode 100644 implementations/cf-entities-poc/tsconfig.json diff --git a/implementations/cf-entities-poc/index.html b/implementations/cf-entities-poc/index.html new file mode 100644 index 000000000..ea6807a02 --- /dev/null +++ b/implementations/cf-entities-poc/index.html @@ -0,0 +1,26 @@ + + + + + + CF-Entities PoC - Optimization SDK + + + +
+ + diff --git a/implementations/cf-entities-poc/package.json b/implementations/cf-entities-poc/package.json new file mode 100644 index 000000000..ed3d31046 --- /dev/null +++ b/implementations/cf-entities-poc/package.json @@ -0,0 +1,25 @@ +{ + "name": "@implementation/cf-entities-poc", + "private": true, + "version": "0.0.0", + "description": "PoC: React app using CF-Entities audiences/experiences via V3 Experience API", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "rsbuild dev", + "build": "rsbuild build", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@contentful/optimization-web": "0.0.0", + "react": "19.2.5", + "react-dom": "19.2.5" + }, + "devDependencies": { + "@rsbuild/core": "1.7.3", + "@rsbuild/plugin-react": "1.4.5", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "typescript": "5.9.3" + } +} diff --git a/implementations/cf-entities-poc/pnpm-workspace.yaml b/implementations/cf-entities-poc/pnpm-workspace.yaml new file mode 100644 index 000000000..f6f7157fa --- /dev/null +++ b/implementations/cf-entities-poc/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - '.' + - '../../packages/web/web-sdk' + - '../../packages/universal/*' diff --git a/implementations/cf-entities-poc/rsbuild.config.ts b/implementations/cf-entities-poc/rsbuild.config.ts new file mode 100644 index 000000000..570d3d807 --- /dev/null +++ b/implementations/cf-entities-poc/rsbuild.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from '@rsbuild/core' +import { pluginReact } from '@rsbuild/plugin-react' + +export default defineConfig({ + plugins: [pluginReact()], + source: { + entry: { index: './src/main.tsx' }, + define: { + 'import.meta.env.PUBLIC_NINETAILED_CLIENT_ID': JSON.stringify( + process.env.PUBLIC_NINETAILED_CLIENT_ID + ), + 'import.meta.env.PUBLIC_NINETAILED_ENVIRONMENT': JSON.stringify( + process.env.PUBLIC_NINETAILED_ENVIRONMENT + ), + 'import.meta.env.PUBLIC_EXPERIENCE_API_BASE_URL': JSON.stringify( + process.env.PUBLIC_EXPERIENCE_API_BASE_URL + ), + 'import.meta.env.PUBLIC_OPTIMIZATION_LOG_LEVEL': JSON.stringify( + process.env.PUBLIC_OPTIMIZATION_LOG_LEVEL + ), + 'import.meta.env.DEV': JSON.stringify(true), + }, + }, + server: { port: 3002 }, +}) diff --git a/implementations/cf-entities-poc/src/App.tsx b/implementations/cf-entities-poc/src/App.tsx new file mode 100644 index 000000000..2b3c655ca --- /dev/null +++ b/implementations/cf-entities-poc/src/App.tsx @@ -0,0 +1,174 @@ +import { useState, useEffect, useCallback } from 'react' +import ContentfulOptimization from '@contentful/optimization-web' + +const CLIENT_ID = import.meta.env.PUBLIC_NINETAILED_CLIENT_ID ?? 'iyqlttzomqet' +const ENVIRONMENT = import.meta.env.PUBLIC_NINETAILED_ENVIRONMENT ?? 'main' +const EXPERIENCE_API_BASE_URL = + import.meta.env.PUBLIC_EXPERIENCE_API_BASE_URL ?? 'https://experience.ninetailed.engineering/' + +type SDKState = { + status: 'loading' | 'ready' | 'error' + sdk?: ContentfulOptimization + profile?: any + experiences?: any[] + error?: string +} + +export function App() { + const [state, setState] = useState({ status: 'loading' }) + const [rawResponse, setRawResponse] = useState('') + + useEffect(() => { + async function init() { + try { + const sdk = new ContentfulOptimization({ + clientId: CLIENT_ID, + environment: ENVIRONMENT, + logLevel: 'debug', + locale: 'en-US', + app: { name: 'CF-Entities PoC', version: '0.1.0' }, + api: { + experienceBaseUrl: EXPERIENCE_API_BASE_URL, + }, + }) + + // Listen for state changes + sdk.onProfileChange((profile: any) => { + setState((prev) => ({ ...prev, profile })) + }) + + sdk.onExperienceChange((experiences: any) => { + setState((prev) => ({ ...prev, experiences })) + }) + + setState({ status: 'ready', sdk }) + } catch (err: any) { + setState({ status: 'error', error: err.message }) + } + } + + init() + }, []) + + const triggerIdentify = useCallback(async () => { + if (!state.sdk) return + + try { + await state.sdk.identify('test-user-001', { + last_email_opened_subject_line: 'Summer Sale', + most_frequent_product_feature_viewed: 'dashboard', + }) + + // After identify, fetch the raw profile response to see the experience resolution + const profileId = state.sdk.profileId + if (profileId) { + const url = `${EXPERIENCE_API_BASE_URL}v3/spaces/${CLIENT_ID}/environments/${ENVIRONMENT}/profiles/${profileId}` + const resp = await fetch(url) + const data = await resp.json() + setRawResponse(JSON.stringify(data, null, 2)) + } + } catch (err: any) { + setRawResponse(`Error: ${err.message}`) + } + }, [state.sdk]) + + const triggerPageView = useCallback(async () => { + if (!state.sdk) return + + try { + await state.sdk.page({ properties: { url: '/pricing' } }) + + const profileId = state.sdk.profileId + if (profileId) { + const url = `${EXPERIENCE_API_BASE_URL}v3/spaces/${CLIENT_ID}/environments/${ENVIRONMENT}/profiles/${profileId}` + const resp = await fetch(url) + const data = await resp.json() + setRawResponse(JSON.stringify(data, null, 2)) + } + } catch (err: any) { + setRawResponse(`Error: ${err.message}`) + } + }, [state.sdk]) + + return ( +
+

CF-Entities PoC

+

+ Optimization SDK calling dev Experience API via V3 paths (space-scoped) +

+ +
+

SDK Status

+

+ + {state.status} + +

+

+ Space: {CLIENT_ID} | Environment: {ENVIRONMENT} +

+

+ Experience API: {EXPERIENCE_API_BASE_URL} +

+ {state.sdk?.profileId && ( +

+ Profile ID: {state.sdk.profileId} +

+ )} +
+ +
+

Trigger Events

+

+ Send events to the Experience API to trigger audience evaluation and experience resolution. +

+ + +
+ + {state.experiences && state.experiences.length > 0 && ( +
+

Resolved Experiences

+ {state.experiences.map((exp: any, i: number) => ( +
+ {exp.name || exp.id} + + ({exp.type}) + + {exp.selectedVariant && ( +
+ Selected variant: {JSON.stringify(exp.selectedVariant)} +
+ )} +
+ ))} +
+ )} + + {state.profile && ( +
+

Profile State

+
{JSON.stringify(state.profile, null, 2)}
+
+ )} + + {rawResponse && ( +
+

Raw API Response

+
{rawResponse}
+
+ )} + + {state.error && ( +
+

Error

+

{state.error}

+
+ )} +
+ ) +} diff --git a/implementations/cf-entities-poc/src/main.tsx b/implementations/cf-entities-poc/src/main.tsx new file mode 100644 index 000000000..a059e67cb --- /dev/null +++ b/implementations/cf-entities-poc/src/main.tsx @@ -0,0 +1,7 @@ +import { createRoot } from 'react-dom/client' +import { App } from './App' + +const root = document.getElementById('root') +if (root) { + createRoot(root).render() +} diff --git a/implementations/cf-entities-poc/tsconfig.json b/implementations/cf-entities-poc/tsconfig.json new file mode 100644 index 000000000..77b77cb84 --- /dev/null +++ b/implementations/cf-entities-poc/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src"] +} diff --git a/packages/universal/api-client/src/experience/ExperienceApiClient.ts b/packages/universal/api-client/src/experience/ExperienceApiClient.ts index 971e9042b..b3d560654 100644 --- a/packages/universal/api-client/src/experience/ExperienceApiClient.ts +++ b/packages/universal/api-client/src/experience/ExperienceApiClient.ts @@ -239,7 +239,7 @@ export default class ExperienceApiClient extends ApiClientBase { try { const response = await this.fetch( this.constructUrl( - `v2/organizations/${this.clientId}/environments/${this.environment}/profiles/${id}`, + `v3/spaces/${this.clientId}/environments/${this.environment}/profiles/${id}`, options, ), { @@ -315,7 +315,7 @@ export default class ExperienceApiClient extends ApiClientBase { try { const response = await this.makeProfileMutationRequest({ - url: `v2/organizations/${this.clientId}/environments/${this.environment}/profiles`, + url: `v3/spaces/${this.clientId}/environments/${this.environment}/profiles`, body, options, }) @@ -370,7 +370,7 @@ export default class ExperienceApiClient extends ApiClientBase { try { const response = await this.makeProfileMutationRequest({ - url: `v2/organizations/${this.clientId}/environments/${this.environment}/profiles/${profileId}`, + url: `v3/spaces/${this.clientId}/environments/${this.environment}/profiles/${profileId}`, body, options, }) @@ -459,7 +459,7 @@ export default class ExperienceApiClient extends ApiClientBase { try { const response = await this.makeProfileMutationRequest({ - url: `v2/organizations/${this.clientId}/environments/${this.environment}/events`, + url: `v3/spaces/${this.clientId}/environments/${this.environment}/events`, body, options: { plainText: false, ...options }, })