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
61 changes: 47 additions & 14 deletions .husky/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ fi
# List workspaces (relative paths)
workspaces=$(pnpm ls -r --depth -1 --json | jq -r ".[].path" | sed "s|^$REPO_ROOT/||")

# List implementations (relative names under implementations/)
implementations=$(find implementations -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)

# Get changed files in this push
changed_files=$(git diff --name-only $range)

Expand All @@ -30,24 +33,54 @@ for workspace in $workspaces; do
fi
done

if [ ${#changed_workspaces[@]} -eq 0 ]; then
echo "No changed workspaces detected for type checking."
# Find changed implementations with tsconfig.json
changed_implementations=()
for implementation in $implementations; do
implementation_path="implementations/$implementation"
if echo "$changed_files" | grep -q "^$implementation_path/"; then
[ -f "$implementation_path/tsconfig.json" ] && changed_implementations+=("$implementation")
fi
done

if [ ${#changed_workspaces[@]} -eq 0 ] && [ ${#changed_implementations[@]} -eq 0 ]; then
echo "No changed workspaces or implementations detected for checks."
exit 0
fi

echo "Type-checking and unit testing changed workspaces:"
for workspace in "${changed_workspaces[@]}"; do
echo " - $workspace"
done
if [ ${#changed_workspaces[@]} -gt 0 ]; then
echo "Type-checking and unit testing changed workspaces:"
for workspace in "${changed_workspaces[@]}"; do
echo " - $workspace"
done

for workspace in "${changed_workspaces[@]}"; do
pnpm --filter "./$workspace" exec tsc --noEmit
done
for workspace in "${changed_workspaces[@]}"; do
pnpm --filter "./$workspace" exec tsc --noEmit
done

echo "✔ Type checking complete."
echo "✔ Workspace type checking complete."

for workspace in "${changed_workspaces[@]}"; do
pnpm --filter "./$workspace" test:unit
done
for workspace in "${changed_workspaces[@]}"; do
pnpm --filter "./$workspace" test:unit
done

echo "✔ Unit testing complete."
echo "✔ Workspace unit testing complete."
fi

if [ ${#changed_implementations[@]} -gt 0 ]; then
echo "Type-checking and unit testing changed implementations:"
for implementation in "${changed_implementations[@]}"; do
echo " - implementations/$implementation"
done

for implementation in "${changed_implementations[@]}"; do
pnpm run implementation:run -- "$implementation" typecheck
done

echo "✔ Implementation type checking complete."

for implementation in "${changed_implementations[@]}"; do
pnpm run implementation:run -- "$implementation" test:unit
done

echo "✔ Implementation unit testing complete."
fi
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ the SDK hierarchy.

## Shared Internal Libraries

Libraries that are shared internally among Optimization SDKs, and are not currently published, are
located within the `/lib` folder in the project root.
Libraries that are shared internally among Optimization SDKs, and are not currently published.

- [Logger](./lib/logger/README.md) is a simple logging abstraction utility
- Logger functionality is provided through `@contentful/optimization-api-client/logger` and passed
through by each SDK at its own `./logger` entry point
- [Mocks](./lib/mocks/README.md) supplies testing fixtures and data, as well as mock server
implementations used in both unit and end to end tests throughout the SDK suite

Expand Down
32 changes: 14 additions & 18 deletions implementations/node-ssr-only/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Optimization, {
import Optimization, { type OptimizationNodeConfig } from '@contentful/optimization-node'
import type { UniversalEventBuilderArgs } from '@contentful/optimization-node/api-client'
import {
isMergeTagEntry,
type OptimizationData,
type OptimizationNodeConfig,
type SelectedPersonalization,
type UniversalEventBuilderArgs,
} from '@contentful/optimization-node'
} from '@contentful/optimization-node/api-schemas'
import { documentToHtmlString } from '@contentful/rich-text-html-renderer'
import { INLINES, type Document } from '@contentful/rich-text-types'
import type { Entry } from 'contentful'
Expand Down Expand Up @@ -146,22 +147,22 @@ app.get('/', limiter, async (req, res) => {
let optimizationResponse: OptimizationData | undefined = undefined

if (isNonEmptyString(userId)) {
const pageResponse = await sdk.personalization.page({
const pageResponse = await sdk.page({
...universalEventBuilderArgs,
})
optimizationResponse = await sdk.personalization.identify({
optimizationResponse = await sdk.identify({
...universalEventBuilderArgs,
userId,
traits: { identified: true },
profile: pageResponse.profile,
profile: pageResponse?.profile,
})
} else {
optimizationResponse = await sdk.personalization.page({
optimizationResponse = await sdk.page({
...universalEventBuilderArgs,
})
}

const { profile, personalizations, changes } = optimizationResponse
const { profile, personalizations, changes } = optimizationResponse ?? {}

const personalizedEntries = new Map<
string,
Expand All @@ -180,10 +181,8 @@ app.get('/', limiter, async (req, res) => {
entry.fields.text = documentToHtmlString(entry.fields.text, {
renderNode: {
[INLINES.EMBEDDED_ENTRY]: (node) => {
if (sdk.personalization.mergeTagValueResolver.isMergeTagEntry(node.data.target)) {
return (
sdk.personalization.mergeTagValueResolver.resolve(node.data.target, profile) ?? ''
)
if (isMergeTagEntry(node.data.target)) {
return sdk.getMergeTagValue(node.data.target, profile) ?? ''
} else {
return ''
}
Expand All @@ -192,15 +191,12 @@ app.get('/', limiter, async (req, res) => {
})
}

const personalizedEntry = sdk.personalization.personalizedEntryResolver.resolve(
entry,
personalizations,
)
const personalizedEntry = sdk.personalizeEntry(entry, personalizations)

personalizedEntries.set(entryId, personalizedEntry)
})

const flags = sdk.personalization.flagsResolver.resolve(changes)
const flags = sdk.getCustomFlags(changes)

const pageData = {
profile,
Expand Down
36 changes: 19 additions & 17 deletions implementations/node-ssr-web-vanilla/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Optimization, {
type OptimizationData,
type UniversalEventBuilderArgs,
} from '@contentful/optimization-node'
import Optimization from '@contentful/optimization-node'
import type { UniversalEventBuilderArgs } from '@contentful/optimization-node/api-client'
import type { OptimizationData } from '@contentful/optimization-node/api-schemas'
import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node/constants'
import cookieParser from 'cookie-parser'
import express, { type Express, type Request, type Response } from 'express'
Expand Down Expand Up @@ -103,42 +102,45 @@ async function getProfile(
req: Request,
userId?: string,
anonymousId?: string,
): Promise<OptimizationData> {
): Promise<OptimizationData | undefined> {
const sdk = new Optimization(config.optimization)
const args = getUniversalEventBuilderArgs(req)
const cookieProfile = anonymousId ? { id: anonymousId } : undefined

if (!userId) {
return await sdk.personalization.page({ ...args, profile: cookieProfile })
return await sdk.page({ ...args, profile: cookieProfile })
}

const { profile } = await sdk.personalization.identify({
...args,
userId,
profile: cookieProfile,
traits: { identified: true },
})
const { profile } =
(await sdk.identify({
...args,
userId,
profile: cookieProfile,
traits: { identified: true },
})) ?? {}

if (!profile) return

return await sdk.personalization.page({
return await sdk.page({
...args,
profile: cookieProfile ?? { id: profile.id },
})
}

app.get('/', limiter, async (req, res) => {
const { profile } = await getProfile(req)
const { profile } = (await getProfile(req)) ?? {}

respond(res, profile.id)
respond(res, profile?.id ?? '')
})
app.get('/smoke-test', limiter, (_, res) => {
res.render('index', { config })
})
app.get('/user/:id', limiter, async (req, res) => {
const anonymousId = getAnonymousIdFromCookies(req.cookies)
const userId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id
const { profile } = await getProfile(req, userId, anonymousId)
const { profile } = (await getProfile(req, userId, anonymousId)) ?? {}

respond(res, profile.id, userId)
respond(res, profile?.id ?? '', userId)
})
app.use('/dist', express.static('./public/dist'))

Expand Down
6 changes: 3 additions & 3 deletions implementations/node-ssr-web-vanilla/src/index.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@
})

// Emit page event
optimization.personalization.page()
optimization.page()

/* --- Utility Functionality (for E2E) --- */

Expand Down Expand Up @@ -328,7 +328,7 @@
const span = document.createElement('span')

// This is how we can inject MergeTag data into Rich Text
span.innerText = optimization.personalization.getMergeTagValue(field.data.target)
span.innerText = optimization.getMergeTagValue(field.data.target)

parent.appendChild(span)
} else {
Expand All @@ -353,7 +353,7 @@

// Render personalized entries
async function renderPersonalizedEntry(rawEntry, element, autoObserve = true) {
const { entry, personalization } = optimization.personalization.personalizeEntry(rawEntry)
const { entry, personalization } = optimization.personalizeEntry(rawEntry)
const renderInline =
element.dataset.e2eEntryInline === 'true' || element.tagName === 'BUTTON'

Expand Down
6 changes: 3 additions & 3 deletions implementations/react-native/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function App(): React.JSX.Element {
return
}

void sdk.personalization.page({ properties: { url: 'app' } })
void sdk.page({ properties: { url: 'app' } })

const subscription = sdk.states.profile.subscribe((profile) => {
if (!profile) {
Expand All @@ -63,15 +63,15 @@ function App(): React.JSX.Element {
const handleIdentify = (): void => {
if (!sdk) return

void sdk.personalization.identify({ userId: 'charles', traits: { identified: true } })
void sdk.identify({ userId: 'charles', traits: { identified: true } })
setIsIdentified(true)
}

const handleReset = (): void => {
if (!sdk) return

sdk.reset()
void sdk.personalization.page({ properties: { url: 'app' } })
void sdk.page({ properties: { url: 'app' } })
setIsIdentified(false)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React from 'react'
import { Text } from 'react-native'

import type Optimization from '@contentful/optimization-react-native'
import type { MergeTagEntry } from '@contentful/optimization-react-native'
import { createScopedLogger } from '@contentful/optimization-react-native'
import type { MergeTagEntry } from '@contentful/optimization-react-native/api-schemas'
import { createScopedLogger } from '@contentful/optimization-react-native/logger'
import type { Entry } from 'contentful'

const logger = createScopedLogger('Demo:RichText')
Expand Down Expand Up @@ -76,9 +76,9 @@ function convertToString(value: unknown): string {
}

function resolveMergeTagValue(includedEntry: MergeTagEntry, sdk: Optimization): string {
const resolvedValue = sdk.personalization.getMergeTagValue(includedEntry)
const resolvedValue = sdk.getMergeTagValue(includedEntry)

if (resolvedValue === undefined || resolvedValue === null) {
if (resolvedValue === undefined) {
logger.error(
`Failed to resolve merge tag: getMergeTagValue returned ${String(resolvedValue)} for merge tag "${includedEntry.fields.nt_name}" (nt_mergetag_id: ${includedEntry.fields.nt_mergetag_id})`,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@ export function LiveUpdatesTestScreen({
}, [])

const handleIdentify = (): void => {
void sdk.personalization.identify({ userId: 'charles', traits: { identified: true } })
void sdk.identify({ userId: 'charles', traits: { identified: true } })
setIsIdentified(true)
}

const handleReset = (): void => {
sdk.reset()
void sdk.personalization.page({ properties: { url: 'live-updates-test' } })
void sdk.page({ properties: { url: 'live-updates-test' } })
setIsIdentified(false)
}

Expand Down
7 changes: 2 additions & 5 deletions implementations/react-native/sections/ContentEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ import React from 'react'
import { Text, View } from 'react-native'

import type Optimization from '@contentful/optimization-react-native'
import {
Analytics,
isPersonalizedEntry,
Personalization,
} from '@contentful/optimization-react-native'
import { Analytics, Personalization } from '@contentful/optimization-react-native'
import { isPersonalizedEntry } from '@contentful/optimization-react-native/api-schemas'
import type { Entry } from 'contentful'

import { getRichTextContent, RichTextRenderer } from '../components/RichTextRenderer'
Expand Down
2 changes: 1 addition & 1 deletion implementations/react-native/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"baseUrl": "./",
"paths": {},
"skipLibCheck": true,
"moduleResolution": "node"
"moduleResolution": "bundler"
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "android", "ios"]
Expand Down
3 changes: 2 additions & 1 deletion implementations/react-native/utils/sdkHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Optimization, { createScopedLogger } from '@contentful/optimization-react-native'
import Optimization from '@contentful/optimization-react-native'
import { createScopedLogger } from '@contentful/optimization-react-native/logger'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient, type Entry } from 'contentful'
import { ENV_CONFIG } from '../env.config'
Expand Down
6 changes: 3 additions & 3 deletions implementations/web-react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Profile } from '@contentful/optimization-web'
import type { Profile } from '@contentful/optimization-web/api-schemas'
import { type JSX, useEffect, useMemo, useState } from 'react'
import { Link, Navigate, Route, Routes, useLocation } from 'react-router-dom'
import { AnalyticsEventDisplay } from './components/AnalyticsEventDisplay'
Expand Down Expand Up @@ -54,7 +54,7 @@ export default function App({
return
}

void sdk.personalization.page({ properties: { url: location.pathname } })
void sdk.page({ properties: { url: location.pathname } })
}, [isReady, location.pathname, sdk])

useEffect(() => {
Expand Down Expand Up @@ -99,7 +99,7 @@ export default function App({
return
}

void sdk.personalization.identify({ userId: 'charles', traits: { identified: true } })
void sdk.identify({ userId: 'charles', traits: { identified: true } })
}

const handleReset = (): void => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CoreStates } from '@contentful/optimization-web'
import type { CoreStates } from '@contentful/optimization-web/core-sdk'
import { useEffect, useState } from 'react'

interface Subscription {
Expand Down
Loading