Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[angular-xmcloud][xmcloud-proxy] Personalization support #1964

Merged
merged 31 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e4d232c
export personalize in angular package
art-alexeyenko Oct 21, 2024
d37f9e0
export debug
art-alexeyenko Oct 21, 2024
c01a560
introduce personalize-helper
art-alexeyenko Oct 21, 2024
4109e7b
server-side personalization WIP
art-alexeyenko Oct 23, 2024
f7de292
Merge branch 'dev' of https://github.com/Sitecore/jss into feature/js…
art-alexeyenko Oct 25, 2024
631901b
use released version of cloudsdk
art-alexeyenko Oct 28, 2024
05035ad
clientside personalization WIP
art-alexeyenko Oct 28, 2024
c6cd265
export more layout types
art-alexeyenko Oct 31, 2024
e0e47c8
temp comment
art-alexeyenko Oct 31, 2024
8c32b71
add personalize plugin
art-alexeyenko Oct 31, 2024
beb5286
client navigation personalization works
art-alexeyenko Nov 1, 2024
c64c9ab
move personalize helper into sitecore-jss-proxy package. add personal…
art-alexeyenko Nov 4, 2024
acc48ed
Added unit tests
illiakovalenko Nov 4, 2024
cd05717
missing functionality, refactor, fix 'headers already sent' bug
art-alexeyenko Nov 5, 2024
a8bb8b9
more unit tests
art-alexeyenko Nov 5, 2024
eb8e41f
added excludeRoute unit test
illiakovalenko Nov 5, 2024
c40a8cc
finalize tests and implementation
art-alexeyenko Nov 6, 2024
44c107a
Merge branch 'dev' of https://github.com/Sitecore/jss into feature/js…
art-alexeyenko Nov 6, 2024
c0b9a31
fix extra file change
art-alexeyenko Nov 6, 2024
7d4ac92
changelog
art-alexeyenko Nov 6, 2024
de18c1f
PR comments
art-alexeyenko Nov 6, 2024
2388cdb
Upgrade guide
art-alexeyenko Nov 6, 2024
7b75c6f
only use server bundle env values for personalize config
art-alexeyenko Nov 6, 2024
023cff8
fix unit tests and move query name to /layout module
art-alexeyenko Nov 6, 2024
331c178
ensure personalizeLayout is tested properly
art-alexeyenko Nov 6, 2024
95b5f20
edit upgrade guide
art-alexeyenko Nov 6, 2024
f51bacf
lint
art-alexeyenko Nov 6, 2024
1dd1dc8
ensure personalization is not launched when edge id is missing
art-alexeyenko Nov 6, 2024
59884e7
Pr comments - 3
art-alexeyenko Nov 7, 2024
1e8f518
revert layout personalizer return type changes
art-alexeyenko Nov 7, 2024
b4c767d
fix indent
art-alexeyenko Nov 7, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Our versioning strategy is as follows:
* `proxyAppDestination` arg can be passed into `create-sitecore-jss` command to define path for proxy to be installed in
* `[templates/angular]` `[templates/angular-xmcloud]` `[template/node-xmcloud-proxy]` `[sitecore-jss-proxy]` Introduced /api/editing/config endpoint ([#1903](https://github.com/Sitecore/jss/pull/1903))
* `[templates/angular]` `[templates/angular-xmcloud]` `[template/node-xmcloud-proxy]` `[sitecore-jss-proxy]` Introduced /api/editing/render endpoint ([#1908](https://github.com/Sitecore/jss/pull/1908))
* `[templates/angular-xmcloud]` `[template/node-xmcloud-proxy]` Personalization support ([#1964](https://github.com/Sitecore/jss/pull/1964))
* `[create-sitecore-jss]``[sitecore-jss-angular]``[template/angular-xmcloud]` Angular SXA components
* Angular placeholder now supports SXA components ([#1870](https://github.com/Sitecore/jss/pull/1870))
* Component styles ([#1917](https://github.com/Sitecore/jss/pull/1917))
Expand Down
24 changes: 16 additions & 8 deletions docs/upgrades/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,17 @@
If you plan to use the Angular SDK with XMCloud, you will need to perform next steps:

* On top of existing Angular sample, apply changes from "angular-xmcloud" add-on.
* Update package.json "build:client" script to use explicit "production" configuration:
* Update package.json:
* Update "build:client" script to use explicit "production" configuration:

```shell
"build:client": "cross-env-shell ng build --configuration=production --base-href $npm_package_config_sitecoreDistPath/browser/ --output-path=$npm_package_config_buildArtifactsPath/browser/"
```
```shell
"build:client": "cross-env-shell ng build --configuration=production --base-href $npm_package_config_sitecoreDistPath/browser/ --output-path=$npm_package_config_buildArtifactsPath/browser/"
```
* Add `CloudSDK` dependencies:
```
"@sitecore-cloudsdk/core": "^0.4.0",
"@sitecore-cloudsdk/events": "^0.4.0",
```

* Update /scripts/bootstrap.ts file to generate a metadata for editing integration:
Assuming that you have a `generate-metadata.ts` file pulled from the "angular-xmcloud" add-on:
Expand Down Expand Up @@ -162,7 +168,7 @@ If you plan to use the Angular SDK with XMCloud, you will need to perform next s
```

* Restructure /src/app/lib/client-factory.ts. This is needed in order to separate the GraphQL client factory configuration from the client factory itself, so we have a single source of GraphQL endpoint resolution that can be used in different places. For example node-xmcloud-proxy, scripts/update-graphql-fragment-data.ts, etc.
* Introduce /src/app/lib/graphql-client-factory/config.ts. It should expose the _getGraphQLClientFactoryConfig_ that returns the configuration object for the GraphQL client factory, for example (full code snippet you can find in the "angular-xmcloud" add-on):
* Introduce /src/app/lib/graphql-client-factory/config.ts. It should expose the _getGraphQLClientFactoryConfig_ that returns the configuration object for the GraphQL client factory. You should refer to the full code snippet with Edge endpoint initalization in the the "angular-xmcloud" add-on, but an example initialization for connected and dev setup would look like this:

```ts
import { GraphQLRequestClientFactoryConfig } from '@sitecore-jss/sitecore-jss-angular/cjs';
Expand All @@ -173,8 +179,8 @@ If you plan to use the Angular SDK with XMCloud, you will need to perform next s

if (env.graphQLEndpoint && env.sitecoreApiKey) {
clientConfig = {
endpoint: env.graphQLEndpoint,
apiKey: env.sitecoreApiKey,
endpoint: env.graphQLEndpoint,
apiKey: env.sitecoreApiKey,
};
}

Expand Down Expand Up @@ -224,9 +230,10 @@ If you plan to use the Angular SDK with XMCloud, you will need to perform next s
import { layoutServiceFactory } from './src/app/lib/layout-service-factory';
import { components } from './src/app/components/app-components.module';
import metadata from './src/environments/metadata.json';

...
const defaultLanguage = environment.defaultLanguage;
const sitecoreSiteName = environment.sitecoreSiteName;
const getClientFactoryConfig = getGraphQLClientFactoryConfig;

export {
Expand All @@ -236,6 +243,7 @@ If you plan to use the Angular SDK with XMCloud, you will need to perform next s
dictionaryServiceFactory,
layoutServiceFactory,
defaultLanguage,
sitecoreSiteName,
components,
metadata
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"prepare:proxy-build": "ts-node --project src/tsconfig.webpack-server.json ./scripts/proxy-build.ts"
},
"dependencies": {
"@sitecore-cloudsdk/core": "^0.4.0-rc.0",
"@sitecore-cloudsdk/events": "^0.4.0-rc.0",
"@sitecore-cloudsdk/core": "^0.4.0",
"@sitecore-cloudsdk/events": "^0.4.0",
"font-awesome": "^4.7.0",
"sass": "^1.52.3",
"sass-alias": "^1.0.5"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { layoutServiceFactory } from './src/app/lib/layout-service-factory';
import { environment } from './src/environments/environment';
import { components } from './src/app/components/app-components.module';
import metadata from './src/environments/metadata.json';

/**
* Define the required configuration values to be exported from the server.bundle.ts.
*/

const defaultLanguage = environment.defaultLanguage;
const sitecoreSiteName = environment.sitecoreSiteName;
const getClientFactoryConfig = getGraphQLClientFactoryConfig;

export {
Expand All @@ -19,6 +19,7 @@ export {
dictionaryServiceFactory,
layoutServiceFactory,
defaultLanguage,
sitecoreSiteName,
components,
metadata,
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ export const getGraphQLClientFactoryConfig = () => {
: getEdgeProxyContentUrl(env.sitecoreEdgeContextId, env.proxyHost),
};
} else if (env.graphQLEndpoint && env.sitecoreApiKey) {
const graphQLEndpointPath = new URL(env.graphQLEndpoint).pathname;

// we ignore ssr-proxy and query CM directly in case apiKey is used (i.e. in dev docker deployments)
clientConfig = {
endpoint: isServer ? env.graphQLEndpoint : `${env.proxyHost}${graphQLEndpointPath}`,
endpoint: env.graphQLEndpoint,
apiKey: env.sitecoreApiKey,
};
}
Expand Down
11 changes: 11 additions & 0 deletions packages/create-sitecore-jss/src/templates/node-xmcloud-proxy/.env
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,14 @@ PROXY_BUNDLE_PATH=

# Set the DEBUG environment variable to 'sitecore-jss:*,sitecore-jss:proxy,http-proxy-middleware*' to see all logs:
#DEBUG=sitecore-jss:*,http-proxy-middleware*

# An optional Sitecore Personalize scope identifier.
# This can be used to isolate personalization data when multiple XM Cloud Environments share a Personalize tenant.
# This should match the PAGES_PERSONALIZE_SCOPE environment variable for your connected XM Cloud Environment.
PERSONALIZE_SCOPE=

# Timeout (ms) for Sitecore CDP requests to respond within. Default is 400.
PERSONALIZE_MIDDLEWARE_CDP_TIMEOUT=

# Timeout (ms) for Sitecore Experience Edge requests to respond within. Default is 400.
PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT=
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Config, ServerBundle } from './types';

import { PersonalizeConfig } from '@sitecore-jss/sitecore-jss-proxy';
/**
* The server.bundle.js file from your pre-built SPA app.
*/
Expand All @@ -12,6 +12,11 @@ try {
} catch (error) {
throw new Error(`ERROR: The server.bundle.js error. ${error}`);
}
const graphQLEndpoint = new URL(serverBundle.getClientFactoryConfig().endpoint);
const bundleEdgeEndpoint = `${graphQLEndpoint.protocol}//${graphQLEndpoint.hostname}`;
const bundleEdgeId = graphQLEndpoint.searchParams.get('sitecoreContextId');
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved

const { clientFactory } = serverBundle;

export const config: Config = {
/**
Expand All @@ -23,3 +28,36 @@ export const config: Config = {
*/
port: process.env.PROXY_PORT || 3000,
};

export const personalizeConfig: PersonalizeConfig = {
// Configuration for your Sitecore Experience Edge endpoint
edgeConfig: {
clientFactory,
timeout:
(process.env.PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT &&
parseInt(process.env.PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT)) ||
400,
},
// Configuration for your Sitecore CDP endpoint
// Edge URL and ID can be taken from proxy env, or the base SPA app
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
cdpConfig: {
sitecoreEdgeUrl: bundleEdgeEndpoint,
sitecoreEdgeContextId: bundleEdgeId || '',
timeout:
(process.env.PERSONALIZE_MIDDLEWARE_CDP_TIMEOUT &&
parseInt(process.env.PERSONALIZE_MIDDLEWARE_CDP_TIMEOUT)) ||
400,
},
// Optional Sitecore Personalize scope identifier.
scope: process.env.PERSONALIZE_SCOPE,
// This function determines if the personalization should be turned off.
// IMPORTANT: You should implement based on your cookie consent management solution of choice.
// You may wish to keep it disabled while in development mode.
// Personalization should also be disabled when edge context id is missing
disabled: () => process.env.NODE_ENV === 'development' || !bundleEdgeId,
// This function determines if a route should be excluded from personalization.
excludeRoute: () => false,
sitecoreSiteName: serverBundle.sitecoreSiteName || '',
// defaultLanguage will be used as fallback for personalization, if language cannot be read from layout service data
defaultLanguage: serverBundle.defaultLanguage,
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import 'dotenv/config';
import express, { Response } from 'express';
import compression from 'compression';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware';
import { debug } from '@sitecore-jss/sitecore-jss';
import { editingRouter } from '@sitecore-jss/sitecore-jss-proxy';
import { healthCheck } from '@sitecore-jss/sitecore-jss-proxy';
import { config } from './config';
import { personalizeHelper, personalizePlugin } from './personalize';

const server = express();

Expand Down Expand Up @@ -105,6 +106,8 @@ const handleError = (res: Response, err: unknown) => {

// enable gzip compression for appropriate file types
server.use(compression());
// enable access to req.body
server.use(graphQLEndpoint.path, express.json());

// turn off x-powered-by http header
server.settings['x-powered-by'] = false;
Expand All @@ -125,6 +128,12 @@ server.use(
createProxyMiddleware({
target: graphQLEndpoint.target,
changeOrigin: true,
selfHandleResponse: true,
on: {
proxyReq: fixRequestBody,
illiakovalenko marked this conversation as resolved.
Show resolved Hide resolved
},
// for client-side routing, personalization is performed by modifying layout service response
plugins: [personalizePlugin],
})
);

Expand Down Expand Up @@ -165,11 +174,17 @@ server.use(async (req, res) => {
}

// Language is required. In case it's not specified in the requested URL, fallback to the default language from the app configuration.
const layoutData = await layoutService.fetchLayoutData(
let layoutData = await layoutService.fetchLayoutData(
route,
lang || config.serverBundle.defaultLanguage
);

// for SSR loading routing, personalization is performed by modifying layoutData directly
const personalizedLayoutData = await personalizeHelper.personalizeLayoutData(
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
req,
res,
layoutData
);
layoutData = personalizedLayoutData;
const viewBag = { dictionary: {} };

viewBag.dictionary = await dictionaryService.fetchDictionaryData(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { GRAPHQL_LAYOUT_QUERY_NAME, PersonalizeHelper } from '@sitecore-jss/sitecore-jss-proxy';
import { personalizeConfig } from './config';
import { responseInterceptor } from 'http-proxy-middleware';
import { Plugin } from 'http-proxy-middleware/dist/types';
import { IncomingMessageWithBody } from './types';
export const personalizeHelper = new PersonalizeHelper(personalizeConfig);
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved

// personalize plugin to modify intercepted Layout Service request data
export const personalizePlugin: Plugin = (proxyServer) => {
proxyServer.on(
'proxyRes',
responseInterceptor(async (responseBuffer, _, req, res) => {
let responseText = responseBuffer.toString('utf8');
const payload = JSON.stringify((req as IncomingMessageWithBody).body);
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
if (payload.includes(GRAPHQL_LAYOUT_QUERY_NAME)) {
let layoutDataRaw = JSON.parse(responseText);
if (layoutDataRaw?.data?.layout?.item?.rendered?.sitecore) {
const personalizedLayout = await personalizeHelper.personalizeLayoutData(
req as IncomingMessageWithBody,
res,
layoutDataRaw?.data?.layout?.item?.rendered
);
layoutDataRaw.data.layout.item.rendered = personalizedLayout;
responseText = JSON.stringify(layoutDataRaw);
}
}
return responseText;
})
);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import {
GraphQLRequestClientFactory,
GraphQLRequestClientFactoryConfig,
Expand All @@ -6,6 +7,7 @@ import { DictionaryService } from '@sitecore-jss/sitecore-jss/i18n';
import { Metadata } from '@sitecore-jss/sitecore-jss/utils';
import { LayoutService } from '@sitecore-jss/sitecore-jss/layout';
import { AppRenderer, RouteUrlParser } from '@sitecore-jss/sitecore-jss-proxy';
import { IncomingMessage } from 'http';

export interface ServerBundle {
[key: string]: unknown;
Expand All @@ -14,6 +16,7 @@ export interface ServerBundle {
clientFactory: GraphQLRequestClientFactory;
getClientFactoryConfig: () => GraphQLRequestClientFactoryConfig;
defaultLanguage: string;
sitecoreSiteName: string;
layoutServiceFactory: { create: () => LayoutService };
dictionaryServiceFactory: { create: () => DictionaryService };
components: string[] | Map<string, unknown>;
Expand All @@ -25,3 +28,10 @@ export interface Config {
port: string | number;
serverBundle: ServerBundle;
}

/**
* IncomingMessage type modified with exporess.json() call to include request body
*/
export type IncomingMessageWithBody = IncomingMessage & {
illiakovalenko marked this conversation as resolved.
Show resolved Hide resolved
body: ReadableStream<Uint8Array> | null;
};
12 changes: 11 additions & 1 deletion packages/sitecore-jss-angular/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export {
ComponentParams,
getContentStylesheetLink,
EditMode,
LayoutServiceContext,
} from '@sitecore-jss/sitecore-jss/layout';
export {
RetryStrategy,
Expand All @@ -80,10 +81,20 @@ export {
enableDebug,
ClientError,
HTMLLink,
debug,
CacheClient,
CacheOptions,
MemoryCacheClient,
} from '@sitecore-jss/sitecore-jss';
export {
GraphQLPersonalizeService,
GraphQLPersonalizeServiceConfig,
PersonalizeInfo,
CdpHelper,
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
DEFAULT_VARIANT,
getGroomedVariantIds,
personalizeLayout,
} from '@sitecore-jss/sitecore-jss/personalize';
export { isServer } from '@sitecore-jss/sitecore-jss/utils';
export {
isEditorActive,
Expand All @@ -104,4 +115,3 @@ export {
EventInstance,
PageViewInstance,
} from '@sitecore-jss/sitecore-jss/tracking';
export { CdpHelper } from '@sitecore-jss/sitecore-jss/personalize';
4 changes: 4 additions & 0 deletions packages/sitecore-jss-proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"url": "https://github.com/sitecore/jss/issues"
},
"dependencies": {
"@sitecore-cloudsdk/core": "^0.4.0",
"@sitecore-cloudsdk/personalize": "^0.4.0",
"@sitecore-jss/sitecore-jss": "22.3.0-canary.3",
"http-proxy-middleware": "^2.0.6",
"http-status-codes": "^2.2.0",
Expand All @@ -37,6 +39,7 @@
"@types/express": "^4.17.17",
"@types/mocha": "^10.0.1",
"@types/node": "^20.14.2",
"@types/proxyquire": "^1.3.31",
"@types/set-cookie-parser": "^2.4.2",
"@types/sinon": "^17.0.3",
"@types/supertest": "^6.0.2",
Expand All @@ -46,6 +49,7 @@
"express": "^4.19.2",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"proxyquire": "^2.1.3",
"sinon": "^17.0.1",
"supertest": "^7.0.0",
"ts-node": "^10.9.1",
Expand Down
1 change: 1 addition & 0 deletions packages/sitecore-jss-proxy/personalize.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './types/personalize/index';
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions packages/sitecore-jss-proxy/personalize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/cjs/personalize/index');
2 changes: 2 additions & 0 deletions packages/sitecore-jss-proxy/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './middleware';
export * from './types';
export * from './personalize/PersonalizeHelper';
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
export { GRAPHQL_LAYOUT_QUERY_NAME } from '@sitecore-jss/sitecore-jss/layout';
Loading