Skip to content
Merged
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
124 changes: 123 additions & 1 deletion platforms/javascript/node/index.pug
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,128 @@ doctype html
html(lang="en")
head
title Optimization Node SDK Dev Development Dashboard

style.
html,
textarea,
input,
button {
font-family:
-apple-system,
BlinkMacSystemFont,
avenir next,
avenir,
segoe ui,
helvetica neue,
Adwaita Sans,
Cantarell,
Ubuntu,
roboto,
noto,
helvetica,
arial,
sans-serif;
}
h1,
main {
max-width: 1040px;
margin-inline: auto;
}
main {
display: grid;
grid-gap: 2rem;
grid-template-rows: auto 1fr;
}
section {
overflow: scroll;

&:first-of-type {
grid-column: 1 / span 2;
}
}
ol {
margin: 0;
}
form {
margin-block: 1rem;
}
button {
cursor: pointer;

&:not([type]) {
appearance: none;
background: transparent;
border: 0;
padding: 0;
font-family: inherit;
font-size: inherit;
text-decoration: underline;

&:hover,
&:active {
color: blue;
}
}
}
fieldset {
display: grid;
grid-template-columns: 1fr auto;
grid-gap: 1rem;
}
summary {
cursor: pointer;
}

body
h1 Optimization Node SDK Dev Development Dashboard
pre= fullProfile

main
section
h2 States

span#consent= consent ? 'Consented' : 'Unconsented'
|
| |
|
button Custom Flags
dialog
pre= JSON.stringify(flags, undefined, 2)
form(method='dialog')
button(type='submit') Close
|
| |
|
button Profile
dialog
pre= JSON.stringify(profile, undefined, 2)
form(method='dialog')
button(type='submit') Close
|
| |
|
button Personalizations
dialog
pre= JSON.stringify(personalizations, undefined, 2)
form(method='dialog')
button(type='submit') Close

section
h2 Entries

ol
each entry in entries
li
button= entry && entry.fields ? entry.fields.internalTitle : 'oops'
dialog
pre= entry
form(method='dialog')
button(type='submit') Close

script.
const triggers = document.querySelectorAll('section > button, li > button')
triggers.forEach((trigger) => {
trigger.addEventListener('click', () => {
trigger.nextElementSibling.showModal()
})
})

1 change: 1 addition & 0 deletions platforms/javascript/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"devDependencies": {
"@types/express": "catalog:",
"@vitest/coverage-v8": "catalog:",
"contentful": "catalog:",
"express": "catalog:",
"express-rate-limit": "catalog:",
"pug": "^3.0.3",
Expand Down
83 changes: 71 additions & 12 deletions platforms/javascript/node/server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import type { SelectedPersonalizationArray } from '@contentful/optimization-api-schemas'
import type { Entry } from 'contentful'
import * as contentful from 'contentful'
import express, { type Express } from 'express'
import rateLimit from 'express-rate-limit'
import path from 'node:path'
import Optimization from './src'

interface ContentEntrySkeleton {
contentTypeId: 'content'
fields: {
text: contentful.EntryFieldTypes.Text
}
}

const limiter = rateLimit({
windowMs: 900_000,
max: 100,
Expand All @@ -14,19 +24,68 @@ app.use(limiter)
app.set('view engine', 'pug') // configure Pug as the view engine
app.set('views', path.join(__dirname, '.')) // define the directory for view templates

const sdk = new Optimization({
clientId: process.env.VITE_NINETAILED_CLIENT_ID ?? '',
environment: process.env.VITE_NINETAILED_ENVIRONMENT ?? '',
logLevel: 'debug',
api: {
analytics: { baseUrl: process.env.VITE_INSIGHTS_API_BASE_URL },
personalization: { baseUrl: process.env.VITE_EXPERIENCE_API_BASE_URL },
},
})

const ctfl = contentful.createClient({
accessToken: process.env.VITE_CONTENTFUL_TOKEN ?? '',
environment: process.env.VITE_CONTENTFUL_ENVIRONMENT ?? '',
space: process.env.VITE_CONTENTFUL_SPACE_ID ?? '',
host: process.env.VITE_CONTENTFUL_CDA_HOST ?? '',
basePath: process.env.VITE_CONTENTFUL_BASE_PATH ?? '',
insecure: Boolean(process.env.VITE_CONTENTFUL_CDA_HOST),
})

async function getEntries(
personalizations: SelectedPersonalizationArray = [],
): Promise<Array<Entry<ContentEntrySkeleton>>> {
const possibleEntries: Array<Entry<ContentEntrySkeleton> | undefined> = await Promise.all(
personalizations.map(async ({ variants }) => {
const baselines = Object.keys(variants)

if (!baselines.length || !baselines[0]) return

try {
return await ctfl.getEntry<ContentEntrySkeleton>(baselines[0], {
include: 10,
})
} catch (_error) {}
}),
)

const entries: Array<Entry<ContentEntrySkeleton>> = possibleEntries.filter(
(entry): entry is Entry<ContentEntrySkeleton> => entry !== undefined,
)

return entries
}

app.get('/', limiter, async (_req, res) => {
const sdk = new Optimization({
clientId: process.env.VITE_NINETAILED_CLIENT_ID ?? '',
environment: process.env.VITE_NINETAILED_ENVIRONMENT ?? '',
logLevel: 'debug',
api: {
analytics: { baseUrl: process.env.VITE_INSIGHTS_API_BASE_URL },
personalization: { baseUrl: process.env.VITE_EXPERIENCE_API_BASE_URL },
},
})

const fullProfile = await sdk.personalization.page({})
res.render('index', { fullProfile: JSON.stringify(fullProfile, undefined, 2) })
const { profile, personalizations, changes } =
(await sdk.personalization.identify({
userId: 'charles',
})) ?? {}

const entries = await getEntries(personalizations)

const pageData = {
consent: false, // Consent is handled manually, server-side
profile,
personalizations,
entries: entries.map((entry) =>
sdk.personalization.personalizedEntryResolver.resolve(entry, personalizations),
),
flags: sdk.personalization.flagsResolver.resolve(changes),
}

res.render('index', { ...pageData })
})

const port = 3000
Expand Down
6 changes: 3 additions & 3 deletions platforms/javascript/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ <h1>Optimization Web SDK Dev Development Dashboard</h1>

<main>
<section>
<h3>States</h3>
<h2>States</h2>

<span id="consent"></span>
|
Expand Down Expand Up @@ -279,7 +279,7 @@ <h3>States</h3>
</section>

<section>
<h3>Entries</h3>
<h2>Entries</h2>

<form id="entry-form">
<fieldset>
Expand All @@ -293,7 +293,7 @@ <h3>Entries</h3>
</section>

<section id="event-stream-panel">
<h3>Event Stream</h3>
<h2>Event Stream</h2>

<ol id="event-stream"></ol>
</section>
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions universal/api-client/src/builders/EventBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const ComponentViewBuilderArgs = z.extend(UniversalEventBuilderArgs, {
export type ComponentViewBuilderArgs = z.infer<typeof ComponentViewBuilderArgs>

const IdentifyBuilderArgs = z.extend(UniversalEventBuilderArgs, {
traits: Traits,
traits: z.optional(Traits),
userId: z.string(),
})
export type IdentifyBuilderArgs = z.infer<typeof IdentifyBuilderArgs>
Expand Down Expand Up @@ -145,7 +145,7 @@ class EventBuilder {
}

buildIdentify(args: IdentifyBuilderArgs): IdentifyEvent {
const { traits, userId, ...universal } = IdentifyBuilderArgs.parse(args)
const { traits = {}, userId, ...universal } = IdentifyBuilderArgs.parse(args)

return {
...this.buildUniversalEventProperties(universal),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { ChangeArray, Flags } from '@contentful/optimization-api-client'

const FlagsResolver = {
resolve(changes: ChangeArray): Flags {
resolve(changes?: ChangeArray): Flags {
if (!changes) return {}

return (
changes
// .filter((change): change is VariableChange => change.type === 'Variable')
Expand Down
Loading