Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions implementations/cf-entities-poc/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CF-Entities PoC - Optimization SDK</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; background: #f9fafb; }
h1 { margin-bottom: 0.5rem; }
.subtitle { color: #6b7280; margin-bottom: 2rem; }
.card { background: white; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card h2 { margin-bottom: 0.5rem; font-size: 1.1rem; }
.status { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85rem; font-weight: 500; }
.status--loading { background: #fef3c7; color: #92400e; }
.status--success { background: #d1fae5; color: #065f46; }
.status--error { background: #fee2e2; color: #991b1b; }
pre { background: #1f2937; color: #e5e7eb; padding: 1rem; border-radius: 6px; overflow-x: auto; font-size: 0.8rem; margin-top: 0.5rem; white-space: pre-wrap; }
.experience { border-left: 4px solid #6366f1; padding-left: 1rem; margin: 0.5rem 0; }
.variant { background: #eef2ff; padding: 0.5rem 1rem; border-radius: 4px; margin-top: 0.5rem; }
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>
25 changes: 25 additions & 0 deletions implementations/cf-entities-poc/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
4 changes: 4 additions & 0 deletions implementations/cf-entities-poc/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
packages:
- '.'
- '../../packages/web/web-sdk'
- '../../packages/universal/*'
25 changes: 25 additions & 0 deletions implementations/cf-entities-poc/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -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 },
})
174 changes: 174 additions & 0 deletions implementations/cf-entities-poc/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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<SDKState>({ status: 'loading' })
const [rawResponse, setRawResponse] = useState<string>('')

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 (
<div>
<h1>CF-Entities PoC</h1>
<p className="subtitle">
Optimization SDK calling dev Experience API via V3 paths (space-scoped)
</p>

<div className="card">
<h2>SDK Status</h2>
<p>
<span className={`status status--${state.status === 'error' ? 'error' : state.status === 'ready' ? 'success' : 'loading'}`}>
{state.status}
</span>
</p>
<p style={{ marginTop: '0.5rem', fontSize: '0.85rem', color: '#6b7280' }}>
Space: <code>{CLIENT_ID}</code> | Environment: <code>{ENVIRONMENT}</code>
</p>
<p style={{ fontSize: '0.85rem', color: '#6b7280' }}>
Experience API: <code>{EXPERIENCE_API_BASE_URL}</code>
</p>
{state.sdk?.profileId && (
<p style={{ fontSize: '0.85rem', color: '#6b7280' }}>
Profile ID: <code>{state.sdk.profileId}</code>
</p>
)}
</div>

<div className="card">
<h2>Trigger Events</h2>
<p style={{ fontSize: '0.85rem', color: '#6b7280', marginBottom: '0.5rem' }}>
Send events to the Experience API to trigger audience evaluation and experience resolution.
</p>
<button onClick={triggerIdentify} style={{ marginRight: '0.5rem', padding: '0.5rem 1rem', borderRadius: '4px', border: '1px solid #d1d5db', cursor: 'pointer' }}>
Identify (Segment User traits)
</button>
<button onClick={triggerPageView} style={{ padding: '0.5rem 1rem', borderRadius: '4px', border: '1px solid #d1d5db', cursor: 'pointer' }}>
Page View (/pricing)
</button>
</div>

{state.experiences && state.experiences.length > 0 && (
<div className="card">
<h2>Resolved Experiences</h2>
{state.experiences.map((exp: any, i: number) => (
<div key={i} className="experience">
<strong>{exp.name || exp.id}</strong>
<span style={{ marginLeft: '0.5rem', fontSize: '0.8rem', color: '#6b7280' }}>
({exp.type})
</span>
{exp.selectedVariant && (
<div className="variant">
Selected variant: <code>{JSON.stringify(exp.selectedVariant)}</code>
</div>
)}
</div>
))}
</div>
)}

{state.profile && (
<div className="card">
<h2>Profile State</h2>
<pre>{JSON.stringify(state.profile, null, 2)}</pre>
</div>
)}

{rawResponse && (
<div className="card">
<h2>Raw API Response</h2>
<pre>{rawResponse}</pre>
</div>
)}

{state.error && (
<div className="card">
<h2>Error</h2>
<p className="status status--error">{state.error}</p>
</div>
)}
</div>
)
}
7 changes: 7 additions & 0 deletions implementations/cf-entities-poc/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createRoot } from 'react-dom/client'
import { App } from './App'

const root = document.getElementById('root')
if (root) {
createRoot(root).render(<App />)
}
15 changes: 15 additions & 0 deletions implementations/cf-entities-poc/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
{
Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -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 },
})
Expand Down
Loading