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
24 changes: 12 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ gotchas.
The following software is required or strongly recommended for day-to-day development:

- [Node.js](https://nodejs.org/) (use [`.nvmrc`](./.nvmrc) when possible; the minimum supported
version is `20.19.0`)
version is specified in [`package.json`](./package.json))
- [pnpm](https://pnpm.io/installation) (the pinned version is recorded in the root
[`package.json`](./package.json))
- [`jq`](https://jqlang.org/) for the local `pre-push` hook
Expand All @@ -68,8 +68,8 @@ The following software is required or strongly recommended for day-to-day develo

> [!NOTE]
>
> Browser and implementation-specific E2E flows also require Playwright browser binaries. The
> targeted `pnpm setup:e2e:<implementation>` wrappers install them for you.
> Browser-based E2E flows also require Playwright browser binaries. The targeted
> `pnpm setup:e2e:<implementation>` wrappers install them for browser implementations.

After cloning the repository:

Expand Down Expand Up @@ -220,7 +220,7 @@ pnpm implementation:run -- <implementation> <action> [args...]
```

- `<implementation>` is a folder name under `implementations/` (for example: `web-sdk`,
`web-sdk_react`, `node-sdk`, `node-sdk+web-sdk`, `react-native-sdk`)
`web-sdk_react`, `react-web-sdk`, `node-sdk`, `node-sdk+web-sdk`, `react-native-sdk`)
- `<action>` can be one of these helper actions:
- `implementation:install`
- `implementation:build:run`
Expand Down Expand Up @@ -360,14 +360,14 @@ This is an intentional CI policy:
The path filters do not watch only implementation directories. Shared package and root changes can
also trigger implementation E2E. At a high level:

| E2E job | Also watches shared surfaces |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `e2e_node_ssr_only` | `lib/**`, `packages/node/node-sdk/**`, universal packages, root package/workflow files |
| `e2e_node_ssr_web_vanilla` | `lib/**`, `packages/node/node-sdk/**`, `packages/web/web-sdk/**`, `packages/web/preview-panel/**`, shared root files |
| `e2e_web` | `lib/**`, `packages/web/web-sdk/**`, `packages/web/preview-panel/**`, universal packages, shared root files |
| `e2e_web_react` | `lib/**`, `packages/web/web-sdk/**`, `packages/web/preview-panel/**`, universal packages, shared root files |
| `e2e_react_web_sdk` | `lib/**`, `packages/web/frameworks/react-web-sdk/**`, `packages/web/web-sdk/**`, `packages/web/preview-panel/**`, universal packages, shared root files |
| `e2e_react_native_android` | `lib/**`, `packages/react-native-sdk/**`, universal packages, shared root files |
| Workflow filter -> job(s) | Also watches shared surfaces |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `e2e_node_sdk` -> `e2e-node-sdk` | `lib/**`, `packages/node/node-sdk/**`, universal packages, root package/workflow files |
| `e2e_node_sdk_web_sdk` -> `e2e-node-sdk-web-sdk` | `lib/**`, `packages/node/node-sdk/**`, `packages/web/web-sdk/**`, `packages/web/preview-panel/**`, shared root files |
| `e2e_web_sdk` -> `e2e-web-sdk` | `lib/**`, `packages/web/web-sdk/**`, `packages/web/preview-panel/**`, universal packages, shared root files |
| `e2e_web_sdk_react` -> `e2e-web-sdk_react` | `lib/**`, `packages/web/web-sdk/**`, `packages/web/preview-panel/**`, universal packages, shared root files |
| `e2e_react_web_sdk` -> `e2e-react-web-sdk` | `lib/**`, `packages/web/frameworks/react-web-sdk/**`, `packages/web/web-sdk/**`, `packages/web/preview-panel/**`, universal packages, shared root files |
| `e2e_react_native_android` -> `e2e-react-native-android-build`, `e2e-react-native-android` | `lib/**`, `packages/react-native-sdk/**`, universal packages, shared root files |

See [`.github/workflows/main-pipeline.yaml`](./.github/workflows/main-pipeline.yaml) for the exact
authoritative filter list.
Expand Down
83 changes: 46 additions & 37 deletions documentation/integrating-the-node-sdk-in-a-node-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export const optimization = new ContentfulOptimization({
```

Treat that SDK as a module-level singleton for the current Node process. Do not create a new
`ContentfulOptimization` instance per incoming request. Create a fresh
`optimization.forRequest(...)` scope for each request instead.
`ContentfulOptimization` instance per incoming request. Instead, compute request-scoped Experience
options per request and pass them as the final argument to stateless event methods.

Notes:

Expand Down Expand Up @@ -177,6 +177,9 @@ One common conservative pattern is:
- when consent is revoked, clear the stored anonymous ID and stop sending further optimization
traffic until consent is granted again

If your app stores consent in cookies, register cookie parsing middleware before reading
`req.cookies`. The next section shows the same Express setup for profile persistence.

```ts
import type { Request, Response } from 'express'
import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node/constants'
Expand Down Expand Up @@ -266,22 +269,28 @@ app.get('/', async (req, res) => {
}

const requestContext = getRequestContext(req)
const requestOptimization = optimization.forRequest(getExperienceRequestOptions(req))
const requestOptions = getExperienceRequestOptions(req)
const existingProfile = getProfileFromRequest(req)
const pageResponse: OptimizationData | undefined = await requestOptimization.page({
...requestContext,
profile: existingProfile,
})
const pageResponse: OptimizationData | undefined = await optimization.page(
{
...requestContext,
profile: existingProfile,
},
requestOptions,
)

const userId = getAuthenticatedUserId(req)

const identifyResponse = userId
? await requestOptimization.identify({
...requestContext,
profile: pageResponse?.profile ?? existingProfile,
userId,
traits: { authenticated: true },
})
? await optimization.identify(
{
...requestContext,
profile: pageResponse?.profile ?? existingProfile,
userId,
traits: { authenticated: true },
},
requestOptions,
)
: undefined

if (consented) {
Expand Down Expand Up @@ -358,12 +367,15 @@ async function getArticle(entryId: string): Promise<ArticleEntry> {

app.get('/article/:entryId', async (req, res) => {
const consented = hasOptimizationConsent(req)
const requestOptimization = optimization.forRequest(getExperienceRequestOptions(req))
const requestOptions = getExperienceRequestOptions(req)
const pageResponse = consented
? await requestOptimization.page({
...getRequestContext(req),
profile: getProfileFromRequest(req),
})
? await optimization.page(
{
...getRequestContext(req),
profile: getProfileFromRequest(req),
},
requestOptions,
)
: undefined

const article = await getArticle(req.params.entryId)
Expand Down Expand Up @@ -434,7 +446,7 @@ captured as an Insights event, call `trackFlagView()` explicitly:

```ts
if (pageResponse?.profile) {
await requestOptimization.trackFlagView({
await optimization.trackFlagView({
...getRequestContext(req),
componentId: 'new-navigation',
profile: pageResponse.profile,
Expand Down Expand Up @@ -462,25 +474,28 @@ profile.
Example custom event:

```ts
const requestOptimization = optimization.forRequest(getExperienceRequestOptions(req))
const requestOptions = getExperienceRequestOptions(req)

await requestOptimization.track({
...getRequestContext(req),
profile: pageResponse?.profile,
event: 'quote_requested',
properties: {
plan: 'enterprise',
source: 'pricing-page',
await optimization.track(
{
...getRequestContext(req),
profile: pageResponse?.profile,
event: 'quote_requested',
properties: {
plan: 'enterprise',
source: 'pricing-page',
},
},
})
requestOptions,
)
```

Example rendered-entry view event:

```ts
import { randomUUID } from 'node:crypto'

const requestOptimization = optimization.forRequest(getExperienceRequestOptions(req))
const requestOptions = getExperienceRequestOptions(req)
const viewPayload = {
...getRequestContext(req),
componentId: optimizedArticle.sys.id,
Expand All @@ -491,15 +506,9 @@ const viewPayload = {
}

if (selectedOptimization?.sticky) {
await requestOptimization.trackView({
...viewPayload,
sticky: true,
})
await optimization.trackView({ ...viewPayload, sticky: true }, requestOptions)
} else if (pageResponse?.profile) {
await requestOptimization.trackView({
...viewPayload,
profile: pageResponse.profile,
})
await optimization.trackView({ ...viewPayload, profile: pageResponse.profile }, requestOptions)
}
```

Expand Down
22 changes: 14 additions & 8 deletions implementations/node-sdk+web-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ This is a reference implementation using both the
[Optimization Web SDK](../../packages/web/web-sdk/README.md), and is part of the
[Contentful Optimization SDK Suite](../../README.md).

On the server side, the stateless Node SDK is created once at module load and each request binds its
own request-scoped options through `forRequest(...)`.
On the server side, the stateless Node SDK is created once at module load and each request passes
its request-scoped options directly to stateless event methods.

The goal of this reference implementation is to illustrate the usage of cookie-based communication
in both the Node and Web SDKs, which is an important component of many server-side/client-side
Expand All @@ -47,26 +47,32 @@ All steps should be run from the monorepo root.
pnpm install
```

2. Ensure the required packages can be built:
2. Build the local package tarballs consumed by implementations:

```sh
pnpm --stream build
pnpm build:pkgs
```

3. Configure the environment in a `.env` file in `implementations/node-sdk+web-sdk` based on the
3. Install this implementation so its local `@contentful/*` dependencies resolve from `pkgs/`:

```sh
pnpm implementation:run -- node-sdk+web-sdk implementation:install
```

4. Configure the environment in a `.env` file in `implementations/node-sdk+web-sdk` based on the
`.env.example` included file. The file is pre-populated with values that are valid only against
the mock server implementation. To test the implementation against a live server environment, see
the [mocks package](../../lib/mocks/README.md) for information on how to set up Contentful space
with test data.
4. Start the mock API and application servers:
5. Start the mock API and application servers:

```sh
pnpm --dir implementations/node-sdk+web-sdk --ignore-workspace serve
```

5. The application can be accessed via Web browser at `http://localhost:3000`
6. The application can be accessed via Web browser at `http://localhost:3000`

6. Stop the mock API and application servers:
7. Stop the mock API and application servers:

```sh
pnpm --dir implementations/node-sdk+web-sdk --ignore-workspace serve:stop
Expand Down
32 changes: 19 additions & 13 deletions implementations/node-sdk+web-sdk/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,26 +108,32 @@ async function getProfile(
anonymousId?: string,
): Promise<OptimizationData | undefined> {
const args = getUniversalEventBuilderArgs(req)
const requestOptimization = sdk.forRequest({
const requestOptions = {
locale: args.locale,
})
}
const cookieProfile = anonymousId ? { id: anonymousId } : undefined

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

const identifyResponse = await requestOptimization.identify({
...args,
userId,
profile: cookieProfile,
traits: { identified: true },
})
const identifyResponse = await sdk.identify(
{
...args,
userId,
profile: cookieProfile,
traits: { identified: true },
},
requestOptions,
)

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

app.get('/', limiter, async (req, res) => {
Expand Down
22 changes: 14 additions & 8 deletions implementations/node-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ This is a reference implementation for the
[Optimization Node SDK](../../packages/node/node-sdk/README.md) and is part of the
[Contentful Optimization SDK Suite](../../README.md).

The server creates one stateless Node SDK instance at module load and binds request-specific options
through `forRequest(...)` inside each incoming request handler.
The server creates one stateless Node SDK instance at module load and passes request-specific
options directly to stateless event methods inside each incoming request handler.

> [!WARNING]
>
Expand All @@ -41,26 +41,32 @@ All steps should be run from the monorepo root.
pnpm install
```

2. Ensure the required packages can be built:
2. Build the local package tarballs consumed by implementations:

```sh
pnpm --stream build
pnpm build:pkgs
```

3. Configure the environment in a `.env` file in `implementations/node-sdk` based on the
3. Install this implementation so its local `@contentful/*` dependencies resolve from `pkgs/`:

```sh
pnpm implementation:run -- node-sdk implementation:install
```

4. Configure the environment in a `.env` file in `implementations/node-sdk` based on the
`.env.example` included file. The file is pre-populated with values that are valid only against
the mock server implementation. To test the implementation against a live server environment, see
the [mocks package](../../lib/mocks/README.md) for information on how to set up Contentful space
with test data.
4. Start the mock API and application servers:
5. Start the mock API and application servers:

```sh
pnpm --dir implementations/node-sdk --ignore-workspace serve
```

5. The application can be accessed via Web browser at `http://localhost:3000`
6. The application can be accessed via Web browser at `http://localhost:3000`

6. Stop the mock API and application servers:
7. Stop the mock API and application servers:

```sh
pnpm --dir implementations/node-sdk --ignore-workspace serve:stop
Expand Down
27 changes: 13 additions & 14 deletions implementations/node-sdk/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,27 +149,26 @@ function getUniversalEventBuilderArgs(req: Request): UniversalEventBuilderArgs {

app.get('/', limiter, async (req, res) => {
const universalEventBuilderArgs = getUniversalEventBuilderArgs(req)
const requestOptimization = sdk.forRequest({
const requestOptions = {
locale: universalEventBuilderArgs.locale,
})
}

const userId = isNonEmptyString(req.query.userId) ? req.query.userId.trim() : undefined

const optimizationResponse: OptimizationData = isNonEmptyString(userId)
? await (async (): Promise<OptimizationData> => {
const pageResponse = await requestOptimization.page({
...universalEventBuilderArgs,
})
return await requestOptimization.identify({
...universalEventBuilderArgs,
userId,
traits: { identified: true },
profile: pageResponse.profile,
})
const pageResponse = await sdk.page({ ...universalEventBuilderArgs }, requestOptions)
return await sdk.identify(
{
...universalEventBuilderArgs,
userId,
traits: { identified: true },
profile: pageResponse.profile,
},
requestOptions,
)
})()
: await requestOptimization.page({
...universalEventBuilderArgs,
})
: await sdk.page({ ...universalEventBuilderArgs }, requestOptions)

const { profile, selectedOptimizations } = optimizationResponse

Expand Down
Loading
Loading