From bd5ebb36301a9af04678a22d92657a917d3080de Mon Sep 17 00:00:00 2001 From: Adam Brauer <400763+ambrauer@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:20:36 -0400 Subject: [PATCH] [Next.js] Personalize Middleware updates to handle Component A/B Testing (#1848) * updates to personalize middleware and supporting utilities / services for component A/B testing * upgrade to @sitecore-cloudsdk/personalize / events 0.3.1 and add passing of variantIds to personalize call from middleware * update CHANGELOG and upgrade docs * Updates per PR feedback * adjusted test based on PR feedback * remove only --- CHANGELOG.md | 9 +- docs/upgrades/unreleased.md | 13 ++ .../src/templates/nextjs-xmcloud/package.json | 2 +- .../src/lib/middleware/plugins/personalize.ts | 11 +- packages/sitecore-jss-nextjs/package.json | 6 +- .../src/middleware/middleware.test.ts | 31 ++++ .../src/middleware/middleware.ts | 13 ++ .../middleware/personalize-middleware.test.ts | 169 ++++++++++++++++-- .../src/middleware/personalize-middleware.ts | 166 +++++++++++++---- .../services/base-graphql-sitemap-service.ts | 15 +- .../services/graphql-sitemap-service.test.ts | 32 ++++ .../sitemapComponentTestingQueryResult.json | 33 ++++ .../graphql-personalize-service.test.ts | 22 +-- .../graphql-personalize-service.ts | 9 +- .../src/personalize/utils.test.ts | 88 +++++++-- .../sitecore-jss/src/personalize/utils.ts | 33 +++- yarn.lock | 36 ++-- 17 files changed, 546 insertions(+), 142 deletions(-) create mode 100644 packages/sitecore-jss-nextjs/src/test-data/sitemapComponentTestingQueryResult.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 7026880cd6..25366235db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Our versioning strategy is as follows: ### ๐Ÿ› Bug Fixes * `[templates/nextjs]` `[templates/react]` `[templates/vue]` `[templates/angular]` Changed formatting in temp/config to prevent parse issues in Unix systems ([#1787](https://github.com/Sitecore/jss/pull/1787))([#1791](https://github.com/Sitecore/jss/pull/1791)) +* `[sitecore-jss]` `GraphQLRequestClientFactory` type is updated and `config` parameter is now optional. Since it should match `GraphQLRequestClient.createClientFactory` method return type ([#1806](https://github.com/Sitecore/jss/pull/1806)) * `[templates/nextjs-sxa]` The banner variant of image component is fixed with supporting metadata mode. ([#1826](https://github.com/Sitecore/jss/pull/1826)) * `[sitecore-jss]` `[sitecore-jss-react]` DateField empty value is not treated as empty ([#1836](https://github.com/Sitecore/jss/pull/1836)) * `[templates/nextjs-sxa]` Fix styles of title component in metadata mode. ([#1839](https://github.com/Sitecore/jss/pull/1839)) @@ -26,7 +27,7 @@ Our versioning strategy is as follows: * `[sitecore-jss]` _GraphQLRequestClient_ now can accept custom 'headers' in the constructor or via _createClientFactory_ ([#1806](https://github.com/Sitecore/jss/pull/1806)) * `[templates/nextjs]` Removed cors header for API endpoints from _lib/next-config/plugins/cors-header_ plugin since cors is handled by API handlers / middlewares ([#1806](https://github.com/Sitecore/jss/pull/1806)) * `[sitecore-jss-nextjs]` Updates to Next.js editing integration to further support secure hosting scenarios (on XM Cloud & Vercel) ([#1832](https://github.com/Sitecore/jss/pull/1832)) -* `[templates/nextjs-xmcloud]` `[sitecore-jss]` AB testing and componente level personalization support. ([#1844](https://github.com/Sitecore/jss/pull/1844))([1847](https://github.com/Sitecore/jss/pull/1847)) +* `[templates/nextjs-xmcloud]` `[sitecore-jss]` A/B testing and component-level personalization support. ([#1844](https://github.com/Sitecore/jss/pull/1844))([#1847](https://github.com/Sitecore/jss/pull/1847))([#1848](https://github.com/Sitecore/jss/pull/1848)) * `[sitecore-jss]` `[nextjs-xmcloud]` DictionaryService can now use a `site` GraphQL query instead of `search` one to improve performance. This is currently only available for XMCloud deployments and is enabled with `nextjs-xmcloud` add-on by default ([#1804](https://github.com/Sitecore/jss/pull/1804))([#1846](https://github.com/Sitecore/jss/pull/1846))([commit](https://github.com/Sitecore/jss/commit/5813a2df8ad6a9ee63dd74d5f206ed4b4f758753))([commit](https://github.com/Sitecore/jss/commit/d0ea3ac02c78343b5dd60277dbf7403410794a49))([commit](https://github.com/Sitecore/jss/commit/307b905ed60d7fff44b2dc799fd78c0842af6fbd))([commit](https://github.com/Sitecore/jss/commit/66164a42263aac8b55f0c5e47eda4bd4d7a72e87)) * `[templates/nextjs-sxa]` nextjs-sxa components now use the NextImage component instead of the react Image component from JSS lib for image optimization ([#1843](https://github.com/Sitecore/jss/pull/1843)) @@ -52,11 +53,7 @@ Our versioning strategy is as follows: * Updated Angular and core dependencies to ~17.3.11 * Updated Typescript to ~5.2.2 * Updated import statements from zone.js/dist/zone-node to zone.js - -### ๐Ÿ› Bug Fixes - -* `[sitecore-jss]` `GraphQLRequestClientFactory` type is updated and `config` parameter is now optional. Since it should match `GraphQLRequestClient.createClientFactory` method return type ([#1806](https://github.com/Sitecore/jss/pull/1806)) - +* `[sitecore-jss/personalize]` `[sitecore-jss-nextjs]` `CdpHelper.getPersonalizedRewrite` signature changed to accept `variantIds: string[]` as second parameter. `CdpHelper.getContentId` was renamed to `CdpHelper.getPageFriendlyId`. ([#1848](https://github.com/Sitecore/jss/pull/1848)) ### ๐Ÿงน Chores diff --git a/docs/upgrades/unreleased.md b/docs/upgrades/unreleased.md index 245a6b6f6c..6509a62791 100644 --- a/docs/upgrades/unreleased.md +++ b/docs/upgrades/unreleased.md @@ -212,4 +212,17 @@ personalizeData.componentVariantIds ); ``` + +* Update _lib/middleware/plugins/personalize.ts_ `PersonalizeMiddleware` constructor signature, moving `scope` from `cdpConfig` to the root. For now this option will continue working but is marked as deprecated. It will be removed in the next major version release. + + ```ts + this.personalizeMiddleware = new PersonalizeMiddleware({ + ... + cdpConfig: { + ... + scope: process.env.NEXT_PUBLIC_PERSONALIZE_SCOPE, // REMOVE + }, + scope: process.env.NEXT_PUBLIC_PERSONALIZE_SCOPE, // ADD + }); + ``` diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/package.json b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/package.json index 45a6964c7e..c2d7a5e3aa 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/package.json +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/package.json @@ -1,7 +1,7 @@ { "dependencies": { "@sitecore/components": "^1.1.10", - "@sitecore-cloudsdk/events": "^0.3.0", + "@sitecore-cloudsdk/events": "^0.3.1", "@sitecore-feaas/clientside": "^0.5.17" } } diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/middleware/plugins/personalize.ts b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/middleware/plugins/personalize.ts index 6580931843..56df575774 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/middleware/plugins/personalize.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/middleware/plugins/personalize.ts @@ -7,12 +7,12 @@ import { siteResolver } from 'lib/site-resolver'; /** * This is the personalize middleware plugin for Next.js. - * It is used to enable Sitecore personalization of pages in Next.js. + * It is used to enable Sitecore personalization and A/B testing of pages in Next.js. * * The `PersonalizeMiddleware` will - * 1. Make a call to the Sitecore Experience Edge to get the personalization information about the page. - * 2. Based on the response, make a call to the Sitecore CDP (with request/user context) to determine the page variant. - * 3. Rewrite the response to the specific page variant. + * 1. Call Sitecore Experience Edge to get the personalization information about the page. + * 2. Based on the response, call Sitecore Personalize (with request/user context) to determine the page / component variant(s). + * 3. Rewrite the response to the specific page / component variant(s). */ class PersonalizePlugin implements MiddlewarePlugin { private personalizeMiddleware: PersonalizeMiddleware; @@ -30,7 +30,6 @@ class PersonalizePlugin implements MiddlewarePlugin { (process.env.PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT && parseInt(process.env.PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT)) || 400, - scope: process.env.NEXT_PUBLIC_PERSONALIZE_SCOPE, }, // Configuration for your Sitecore CDP endpoint cdpConfig: { @@ -41,6 +40,8 @@ class PersonalizePlugin implements MiddlewarePlugin { parseInt(process.env.PERSONALIZE_MIDDLEWARE_CDP_TIMEOUT)) || 400, }, + // Optional Sitecore Personalize scope identifier. + scope: process.env.NEXT_PUBLIC_PERSONALIZE_SCOPE, // This function determines if the middleware 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. diff --git a/packages/sitecore-jss-nextjs/package.json b/packages/sitecore-jss-nextjs/package.json index 19829d590f..8b0ff7fc36 100644 --- a/packages/sitecore-jss-nextjs/package.json +++ b/packages/sitecore-jss-nextjs/package.json @@ -29,7 +29,7 @@ "url": "https://github.com/sitecore/jss/issues" }, "devDependencies": { - "@sitecore-cloudsdk/personalize": "^0.3.0", + "@sitecore-cloudsdk/personalize": "^0.3.1", "@types/chai": "^4.3.4", "@types/chai-as-promised": "^7.1.5", "@types/chai-string": "^1.4.2", @@ -65,8 +65,8 @@ "typescript": "~4.9.4" }, "peerDependencies": { - "@sitecore-cloudsdk/events": "^0.3.0", - "@sitecore-cloudsdk/personalize": "^0.3.0", + "@sitecore-cloudsdk/events": "^0.3.1", + "@sitecore-cloudsdk/personalize": "^0.3.1", "next": "^14.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/packages/sitecore-jss-nextjs/src/middleware/middleware.test.ts b/packages/sitecore-jss-nextjs/src/middleware/middleware.test.ts index 218e21c537..a78b372b81 100644 --- a/packages/sitecore-jss-nextjs/src/middleware/middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/middleware/middleware.test.ts @@ -109,6 +109,37 @@ describe('MiddlewareBase', () => { }); }); + describe('isPrefetch', () => { + it('should return true when purpose header is prefetch', () => { + const middleware = new SampleMiddleware({ siteResolver: new MockSiteResolver([]) }); + const req = createReq({ + headerValues: { + purpose: 'prefetch', + }, + }); + + expect(middleware['isPrefetch'](req)).to.equal(true); + }); + + it('should return true when Next-Router-Prefetch header is 1', () => { + const middleware = new SampleMiddleware({ siteResolver: new MockSiteResolver([]) }); + const req = createReq({ + headerValues: { + 'Next-Router-Prefetch': '1', + }, + }); + + expect(middleware['isPrefetch'](req)).to.equal(true); + }); + + it('should return false when required header is not provided', () => { + const middleware = new SampleMiddleware({ siteResolver: new MockSiteResolver([]) }); + const req = createReq(); + + expect(middleware['isPrefetch'](req)).to.equal(false); + }); + }); + describe('excludeRoute', () => { it('default', () => { const middleware = new SampleMiddleware({ siteResolver: new MockSiteResolver([]) }); diff --git a/packages/sitecore-jss-nextjs/src/middleware/middleware.ts b/packages/sitecore-jss-nextjs/src/middleware/middleware.ts index dcda603d9d..f3db31f775 100644 --- a/packages/sitecore-jss-nextjs/src/middleware/middleware.ts +++ b/packages/sitecore-jss-nextjs/src/middleware/middleware.ts @@ -47,6 +47,19 @@ export abstract class MiddlewareBase { ); } + /** + * Determines if the request is a Next.js (next/link) prefetch request + * @param {NextRequest} req request + * @returns {boolean} is prefetch + */ + protected isPrefetch(req: NextRequest): boolean { + return ( + // eslint-disable-next-line prettier/prettier + req.headers.get('purpose') === 'prefetch' || // Pages Router + req.headers.get('Next-Router-Prefetch') === '1' // App Router + ); + } + protected excludeRoute(pathname: string) { return ( pathname.startsWith('/api/') || // Ignore Next.js API calls diff --git a/packages/sitecore-jss-nextjs/src/middleware/personalize-middleware.test.ts b/packages/sitecore-jss-nextjs/src/middleware/personalize-middleware.test.ts index 07905e6dbb..79ecaf6950 100644 --- a/packages/sitecore-jss-nextjs/src/middleware/personalize-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/middleware/personalize-middleware.test.ts @@ -7,6 +7,7 @@ import sinon, { spy } from 'sinon'; import nextjs, { NextRequest, NextResponse } from 'next/server'; import { GraphQLRequestClient, debug } from '@sitecore-jss/sitecore-jss'; import { SiteResolver } from '@sitecore-jss/sitecore-jss/site'; +import { CdpHelper } from '@sitecore-jss/sitecore-jss/personalize'; import { PersonalizeMiddleware } from './personalize-middleware'; use(sinonChai); @@ -27,10 +28,8 @@ describe('PersonalizeMiddleware', () => { const hostname = 'foo.net'; const siteName = 'bar'; - const id = 'item-id'; - const version = '1'; + const pageId = 'item-id'; const variantIds = ['variant-1', 'variant-2']; - const contentId = `${id}_en_${version}`.toLowerCase(); const defaultLang = 'en'; const referrer = 'http://localhost:3000'; const createRequest = (props: any = {}) => { @@ -143,12 +142,14 @@ describe('PersonalizeMiddleware', () => { siteResolver?: SiteResolver; edgeConfig?: any; cdpConfig?: any; + scope?: string; variantId?: string; personalizeInfo?: { - contentId: string; + pageId: string; variantIds: string[]; } | null; getPersonalizeInfoStub?: sinon.SinonStub; + personalizeStub?: sinon.SinonStub; handleCookieStub?: sinon.SinonStub; } = {} ) => { @@ -190,11 +191,13 @@ describe('PersonalizeMiddleware', () => { const initPersonalizeServer = (middleware['initPersonalizeServer'] = sinon.stub()); - const personalize = (middleware['personalize'] = sinon.stub().returns( - Promise.resolve({ - variantId: props.variantId, - }) - )); + const personalize = (middleware['personalize'] = + props.personalizeStub || + sinon.stub().returns( + Promise.resolve({ + variantId: props.variantId, + }) + )); const getPersonalizeInfo = (middleware['personalizeService']['getPersonalizeInfo'] = props.getPersonalizeInfoStub || @@ -203,7 +206,7 @@ describe('PersonalizeMiddleware', () => { props.personalizeInfo === null ? undefined : props.personalizeInfo || { - contentId, + pageId, variantIds, } ) @@ -395,7 +398,7 @@ describe('PersonalizeMiddleware', () => { const res = createResponse(); const { middleware, getPersonalizeInfo } = createMiddleware({ personalizeInfo: { - contentId, + pageId, variantIds: [], }, }); @@ -435,20 +438,25 @@ describe('PersonalizeMiddleware', () => { expect(getPersonalizeInfo.calledWith('/styleguide', 'en')).to.be.true; expect(initPersonalizeServer.called).to.be.true; expect(personalize.called).to.be.true; - validateDebugLog('skipped (no variant identified)'); + validateDebugLog('skipped (no variant(s) identified)'); expect(finalRes).to.deep.equal(res); }); it('invalid variant', async () => { const req = createRequest(); const res = createResponse(); const handleCookieStub = sinon.stub().resolves(); + const invalidVariant = 'invalid-variant'; const { middleware, getPersonalizeInfo, initPersonalizeServer, personalize, } = createMiddleware({ - variantId: 'invalid-variant', + personalizeInfo: { + pageId, + variantIds, + }, + variantId: invalidVariant, handleCookieStub, }); const finalRes = await middleware.getHandler()(req, res); @@ -463,14 +471,36 @@ describe('PersonalizeMiddleware', () => { expect(getPersonalizeInfo.calledWith('/styleguide', 'en')).to.be.true; expect(initPersonalizeServer.called).to.be.true; expect(personalize.called).to.be.true; - validateDebugLog('skipped (invalid variant)'); + validateDebugLog('invalid variant %s', invalidVariant); + expect(finalRes).to.deep.equal(res); + }); + + it('prefetch', async () => { + const req = createRequest({ + headerValues: { + purpose: 'prefetch', + }, + }); + const res = createResponse(); + const { middleware } = createMiddleware(); + const finalRes = await middleware.getHandler()(req, res); + + validateDebugLog('personalize middleware start: %o', { + hostname: 'foo.net', + pathname: '/styleguide', + language: 'en', + headers: { + ...req.headers, + }, + }); + validateDebugLog('skipped (prefetch)'); expect(finalRes).to.deep.equal(res); + expect(finalRes.headers['x-middleware-cache']).to.equal('no-cache'); }); }); describe('request passed', () => { it('fallback defaultLocale is used', async () => { - const contentId = `${id}_da-dk_${version}`; const language = 'da-DK'; const req = createRequest({ nextUrl: { @@ -491,7 +521,7 @@ describe('PersonalizeMiddleware', () => { variantId: 'variant-2', personalizeInfo: { variantIds, - contentId, + pageId, }, }); const finalRes = await middleware.getHandler()(req, res); @@ -813,6 +843,113 @@ describe('PersonalizeMiddleware', () => { expect(finalRes).to.deep.equal(res); nextRewriteStub.restore(); }); + + it('configured scope is used', async () => { + const pageId = 'item-id'; + const scope = 'myscope'; + const req = createRequest(); + const res = createResponse(); + const nextRewriteStub = sinon.stub(nextjs.NextResponse, 'rewrite').returns(res); + const personalizeStub = sinon.stub().returns(Promise.resolve({ variantId: undefined })); + const { middleware, getPersonalizeInfo, personalize } = createMiddleware({ + scope, + personalizeInfo: { + pageId, + variantIds: ['variant1'], + }, + personalizeStub, + }); + const finalRes = await middleware.getHandler()(req, res); + + expect(getPersonalizeInfo.calledWith('/styleguide', 'en', siteName)).to.be.true; + expect( + personalize.calledWith( + sinon.match({ friendlyId: CdpHelper.getPageFriendlyId(pageId, 'en', scope) }), + sinon.match.any + ) + ).to.be.true; + expect(finalRes).to.deep.equal(res); + nextRewriteStub.restore(); + }); + + it('component testing is executed', async () => { + const pageId = 'item-id'; + const req = createRequest(); + const res = createResponse(); + const nextRewriteStub = sinon.stub(nextjs.NextResponse, 'rewrite').returns(res); + const personalizeStub = sinon.stub(); + personalizeStub + .withArgs( + sinon.match({ + friendlyId: CdpHelper.getComponentFriendlyId(pageId, 'component1', 'en'), + variantIds: ['component1_default', 'component1_variant1'], + }), + sinon.match.any + ) + .returns(Promise.resolve({ variantId: 'component1_default' })); + personalizeStub + .withArgs( + sinon.match({ + friendlyId: CdpHelper.getComponentFriendlyId(pageId, 'component2', 'en'), + variantIds: ['component2_default', 'component2_variant1', 'component2_variant2'], + }), + sinon.match.any + ) + .returns(Promise.resolve({ variantId: 'component2_variant1' })); + personalizeStub + .withArgs( + sinon.match({ + friendlyId: CdpHelper.getComponentFriendlyId(pageId, 'component3', 'en'), + variantIds: [ + 'component3_default', + 'component3_variant1', + 'component3_variant2', + 'component3_variant3', + ], + }), + sinon.match.any + ) + .returns(Promise.resolve({ variantId: 'component3_variant3' })); + const { middleware, getPersonalizeInfo, initPersonalizeServer } = createMiddleware({ + personalizeInfo: { + pageId, + variantIds: [ + 'component1_variant1', + 'component2_variant1', + 'component2_variant2', + 'component3_variant1', + 'component3_variant2', + 'component3_variant3', + ], + }, + personalizeStub, + }); + const finalRes = await middleware.getHandler()(req); + + expect(getPersonalizeInfo.calledWith('/styleguide', 'en')).to.be.true; + expect(initPersonalizeServer.calledOnce).to.be.true; + expect(personalizeStub.calledThrice).to.be.true; + validateDebugLog('personalize middleware start: %o', { + headers: { + ...req.headers, + }, + hostname: 'foo.net', + pathname: '/styleguide', + language: 'en', + }); + validateEndMessageDebugLog('personalize middleware end in %dms: %o', { + rewritePath: + '/_variantId_component1_default/_variantId_component2_variant1/_variantId_component3_variant3/styleguide', + headers: { + ...res.headers, + 'x-middleware-cache': 'no-cache', + 'x-sc-rewrite': + '/_variantId_component1_default/_variantId_component2_variant1/_variantId_component3_variant3/styleguide', + }, + }); + expect(finalRes).to.deep.equal(res); + nextRewriteStub.restore(); + }); }); describe('error handling', () => { diff --git a/packages/sitecore-jss-nextjs/src/middleware/personalize-middleware.ts b/packages/sitecore-jss-nextjs/src/middleware/personalize-middleware.ts index 5b1944e868..c6977d43e7 100644 --- a/packages/sitecore-jss-nextjs/src/middleware/personalize-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/middleware/personalize-middleware.ts @@ -4,6 +4,8 @@ import { GraphQLPersonalizeServiceConfig, getPersonalizedRewrite, PersonalizeInfo, + CdpHelper, + DEFAULT_VARIANT, } from '@sitecore-jss/sitecore-jss/personalize'; import { debug } from '@sitecore-jss/sitecore-jss'; import { MiddlewareBase, MiddlewareBaseConfig } from './middleware'; @@ -42,6 +44,10 @@ export type PersonalizeMiddlewareConfig = MiddlewareBaseConfig & { * Configuration for your Sitecore CDP endpoint */ cdpConfig: CdpServiceConfig; + /** + * Optional Sitecore Personalize scope identifier allowing you to isolate your personalization data between XM Cloud environments + */ + scope?: string; }; /** @@ -58,6 +64,14 @@ export type ExperienceParams = { }; }; +/** + * Object model of personalize execution data + */ +type PersonalizeExecution = { + friendlyId: string; + variantIds: string[]; +}; + /** * Middleware / handler to support Sitecore Personalize */ @@ -117,26 +131,33 @@ export class PersonalizeMiddleware extends MiddlewareBase { protected async personalize( { params, - personalizeInfo, + friendlyId, language, timeout, + variantIds, }: { - personalizeInfo: PersonalizeInfo; params: ExperienceParams; + friendlyId: string; language: string; timeout?: number; + variantIds?: string[]; }, request: NextRequest ) { - const personalizationData = { - channel: this.config.cdpConfig.channel || 'WEB', - currency: this.config.cdpConfig.currency ?? 'USD', - friendlyId: personalizeInfo.contentId, - params, - language, - }; - - return (await personalize(request, personalizationData, { timeout })) as { + debug.personalize('executing experience for %s %o', friendlyId, params); + + return (await personalize( + request, + { + channel: this.config.cdpConfig.channel || 'WEB', + currency: this.config.cdpConfig.currency ?? 'USD', + friendlyId, + params, + language, + pageVariantIds: variantIds, + }, + { timeout } + )) as { variantId: string; }; } @@ -163,6 +184,62 @@ export class PersonalizeMiddleware extends MiddlewareBase { return pathname.includes('.') || super.excludeRoute(pathname); } + /** + * Aggregates personalize executions based on the provided route personalize information and language + * @param {PersonalizeInfo} personalizeInfo the route personalize information + * @param {string} language the language + * @returns An array of personalize executions + */ + protected getPersonalizeExecutions( + personalizeInfo: PersonalizeInfo, + language: string + ): PersonalizeExecution[] { + if (personalizeInfo.variantIds.length === 0) { + return []; + } + const results: PersonalizeExecution[] = []; + return personalizeInfo.variantIds.reduce((results, variantId) => { + if (variantId.includes('_')) { + // Component-level personalization in format "_" + const componentId = variantId.split('_')[0]; + const friendlyId = CdpHelper.getComponentFriendlyId( + personalizeInfo.pageId, + componentId, + language, + this.config.scope || this.config.edgeConfig.scope + ); + const execution = results.find((x) => x.friendlyId === friendlyId); + if (execution) { + execution.variantIds.push(variantId); + } else { + // The default/control variant (format "_default") is also a valid value returned by the execution + const defaultVariant = `${componentId}${DEFAULT_VARIANT}`; + results.push({ + friendlyId, + variantIds: [defaultVariant, variantId], + }); + } + } else { + // Embedded (page-level) personalization in format "" + const friendlyId = CdpHelper.getPageFriendlyId( + personalizeInfo.pageId, + language, + this.config.scope || this.config.edgeConfig.scope + ); + const execution = results.find((x) => x.friendlyId === friendlyId); + if (execution) { + execution.variantIds.push(variantId); + } else { + results.push({ + friendlyId, + variantIds: [variantId], + }); + } + } + return results; + }, results); + } + private handler = async (req: NextRequest, res?: NextResponse): Promise => { const pathname = req.nextUrl.pathname; const language = this.getLanguage(req); @@ -216,6 +293,16 @@ export class PersonalizeMiddleware extends MiddlewareBase { return response; } + if (this.isPrefetch(req)) { + debug.personalize('skipped (prefetch)'); + // Personalized, but this is a prefetch request. + // In this case, don't execute a personalize request; otherwise, the metrics for component A/B experiments would be inaccurate. + // Disable preflight caching to force revalidation on client-side navigation (personalization WILL be influenced). + // Note the reason we don't move this any earlier in the middleware is that we would then be sacrificing performance for non-personalized pages. + response.headers.set('x-middleware-cache', 'no-cache'); + return response; + } + await this.initPersonalizeServer({ hostname, siteName: site.name, @@ -224,30 +311,35 @@ export class PersonalizeMiddleware extends MiddlewareBase { }); const params = this.getExperienceParams(req); + const executions = this.getPersonalizeExecutions(personalizeInfo, language); + const identifiedVariantIds: string[] = []; + + await Promise.all( + executions.map((execution) => + this.personalize( + { + friendlyId: execution.friendlyId, + variantIds: execution.variantIds, + params, + language, + timeout, + }, + req + ).then((personalization) => { + const variantId = personalization.variantId; + if (variantId) { + if (!execution.variantIds.includes(variantId)) { + debug.personalize('invalid variant %s', variantId); + } else { + identifiedVariantIds.push(variantId); + } + } + }) + ) + ); - debug.personalize('executing experience for %s %o', personalizeInfo.contentId, params); - - let variantId; - - // Execute targeted experience in Personalize SDK - // eslint-disable-next-line no-useless-catch - try { - const personalization = await this.personalize( - { personalizeInfo, params, language, timeout }, - req - ); - variantId = personalization.variantId; - } catch (error) { - throw error; - } - - if (!variantId) { - debug.personalize('skipped (no variant identified)'); - return response; - } - - if (!personalizeInfo.variantIds.includes(variantId)) { - debug.personalize('skipped (invalid variant)'); + if (identifiedVariantIds.length === 0) { + debug.personalize('skipped (no variant(s) identified)'); return response; } @@ -255,11 +347,11 @@ export class PersonalizeMiddleware extends MiddlewareBase { const basePath = res?.headers.get('x-sc-rewrite') || pathname; // Rewrite to persononalized path - const rewritePath = getPersonalizedRewrite(basePath, { variantId }); + const rewritePath = getPersonalizedRewrite(basePath, identifiedVariantIds); response = this.rewrite(rewritePath, req, response); - // Disable preflight caching to force revalidation on client-side navigation (personalization may be influenced) - // See https://github.com/vercel/next.js/issues/32727 + // Disable preflight caching to force revalidation on client-side navigation (personalization MAY be influenced). + // See https://github.com/vercel/next.js/pull/32767 response.headers.set('x-middleware-cache', 'no-cache'); debug.personalize('personalize middleware end in %dms: %o', Date.now() - startTimestamp, { diff --git a/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts index 2080ede237..a0d0faeb7a 100644 --- a/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts @@ -131,8 +131,9 @@ export type RouteListQueryResult = { export interface BaseGraphQLSitemapServiceConfig extends Omit { /** - * A flag for whether to include personalized routes in service output - only works on XM Cloud - * turned off by default + * A flag for whether to include personalized routes in service output. + * Only works on XM Cloud for pages using Embedded Personalization (not Component A/B testing). + * Turned off by default. */ includePersonalizedRoutes?: boolean; /** @@ -258,12 +259,12 @@ export abstract class BaseGraphQLSitemapService { aggregatedPaths.push(formatPath(item.path)); - // check for type safety's sake - personalize may be empty depending on query type - if (item.route?.personalization?.variantIds.length) { + const variantIds = item.route?.personalization?.variantIds?.filter( + (variantId) => !variantId.includes('_') // exclude component A/B test variants + ); + if (variantIds?.length) { aggregatedPaths.push( - ...(item.route?.personalization?.variantIds.map((varId) => - formatPath(getPersonalizedRewrite(item.path, { variantId: varId })) - ) || {}) + ...variantIds.map((varId) => formatPath(getPersonalizedRewrite(item.path, [varId]))) ); } }); diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts index 636dd7f9c0..1c8f223993 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts @@ -8,6 +8,7 @@ import { } from './graphql-sitemap-service'; import sitemapDefaultQueryResult from '../test-data/sitemapDefaultQueryResult.json'; import sitemapPersonalizeQueryResult from '../test-data/sitemapPersonalizeQueryResult.json'; +import sitemapComponentTestingQueryResult from '../test-data/sitemapComponentTestingQueryResult.json'; import sitemapServiceSinglesiteResult from '../test-data/sitemapServiceSinglesiteResult'; import { GraphQLClient, GraphQLRequestClient } from '@sitecore-jss/sitecore-jss/graphql'; @@ -327,6 +328,37 @@ describe('GraphQLSitemapService', () => { return expect(nock.isDone()).to.be.true; }); + it('should not return personalized paths when personalize data is requested and component a/b testing returned', async () => { + const lang = 'ua'; + + nock(endpoint) + .post('/', /PersonalizeSitemapQuery/gi) + .reply(200, sitemapComponentTestingQueryResult); + + const service = new GraphQLSitemapService({ + clientFactory, + siteName, + includePersonalizedRoutes: true, + }); + const sitemap = await service.fetchSSGSitemap([lang]); + + expect(sitemap).to.deep.equal([ + { + params: { + path: [''], + }, + locale: lang, + }, + { + params: { + path: ['y1', 'y2', 'y3', 'y4'], + }, + locale: lang, + }, + ]); + return expect(nock.isDone()).to.be.true; + }); + it('should work when null results are present', async () => { const lang = 'en'; diff --git a/packages/sitecore-jss-nextjs/src/test-data/sitemapComponentTestingQueryResult.json b/packages/sitecore-jss-nextjs/src/test-data/sitemapComponentTestingQueryResult.json new file mode 100644 index 0000000000..c441171161 --- /dev/null +++ b/packages/sitecore-jss-nextjs/src/test-data/sitemapComponentTestingQueryResult.json @@ -0,0 +1,33 @@ +{ + "data": { + "site": { + "siteInfo": { + "routes": { + "total": 6, + "pageInfo": { + "endCursor": "Ng==", + "hasNext": false + }, + "results": [ + { + "path": "/", + "route": { + "personalization": { + "variantIds": ["component1_green", "component1_red"] + } + } + }, + { + "path": "/y1/y2/y3/y4", + "route": { + "personalization": { + "variantIds": ["component1_green", "component1_red", "component2_purple", "component2_yellow"] + } + } + } + ] + } + } + } + } +} diff --git a/packages/sitecore-jss/src/personalize/graphql-personalize-service.test.ts b/packages/sitecore-jss/src/personalize/graphql-personalize-service.test.ts index 4c9acafcce..6e31d0d0bf 100644 --- a/packages/sitecore-jss/src/personalize/graphql-personalize-service.test.ts +++ b/packages/sitecore-jss/src/personalize/graphql-personalize-service.test.ts @@ -74,23 +74,7 @@ describe('GraphQLPersonalizeService', () => { ); expect(personalizeData).to.deep.equal({ - contentId: `embedded_${id}_en`.toLowerCase(), - variantIds, - }); - }); - - it('should return personalize info for a route when scope is provided', async () => { - mockNonEmptyResponse(); - - const service = new GraphQLPersonalizeService({ ...config, scope: 'myscope123' }); - const personalizeData = await service.getPersonalizeInfo( - '/sitecore/content/home', - 'en', - siteName - ); - - expect(personalizeData).to.deep.equal({ - contentId: `embedded_myscope123_${id}_en`.toLowerCase(), + pageId: id, variantIds, }); }); @@ -179,7 +163,7 @@ describe('GraphQLPersonalizeService', () => { const firstResult = await service.getPersonalizeInfo(itemPath, lang, siteName); expect(firstResult).to.deep.equal({ - contentId: `embedded_${id}_en`.toLowerCase(), + pageId: id, variantIds, }); @@ -203,7 +187,7 @@ describe('GraphQLPersonalizeService', () => { const firstResult = await service.getPersonalizeInfo(itemPath, lang, siteName); expect(firstResult).to.deep.equal({ - contentId: `embedded_${id}_en`.toLowerCase(), + pageId: id, variantIds, }); diff --git a/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts b/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts index e77a360389..0c1be8ea05 100644 --- a/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts +++ b/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts @@ -1,7 +1,6 @@ import { GraphQLClient, GraphQLRequestClientFactory } from '../graphql-request-client'; import debug from '../debug'; import { isTimeoutError } from '../utils'; -import { CdpHelper } from './utils'; import { CacheClient, CacheOptions, MemoryCacheClient } from '../cache-client'; export type GraphQLPersonalizeServiceConfig = CacheOptions & { @@ -11,6 +10,7 @@ export type GraphQLPersonalizeServiceConfig = CacheOptions & { timeout?: number; /** * Optional Sitecore Personalize scope identifier allowing you to isolate your personalization data between XM Cloud environments + * @deprecated Will be removed in a future release. */ scope?: string; /** @@ -29,9 +29,9 @@ export type GraphQLPersonalizeServiceConfig = CacheOptions & { */ export type PersonalizeInfo = { /** - * The (CDP-friendly) content id + * The page id */ - contentId: string; + pageId: string; /** * The configured variant ids */ @@ -105,8 +105,7 @@ export class GraphQLPersonalizeService { } return data?.layout?.item ? { - // CDP expects content id format `embedded_[_]_` (lowercase) - contentId: CdpHelper.getContentId(data.layout.item.id, language, this.config.scope), + pageId: data.layout.item.id, variantIds: data.layout.item.personalization.variantIds, } : undefined; diff --git a/packages/sitecore-jss/src/personalize/utils.test.ts b/packages/sitecore-jss/src/personalize/utils.test.ts index 0a8b7da696..bdb6610cdc 100644 --- a/packages/sitecore-jss/src/personalize/utils.test.ts +++ b/packages/sitecore-jss/src/personalize/utils.test.ts @@ -12,26 +12,35 @@ import { describe('utils', () => { describe('getPersonalizedRewrite', () => { - const data = { - variantId: 'variant-id', - }; + const variantIds = ['123', '456_ABC']; it('should return a string', () => { - expect(getPersonalizedRewrite('/pathname', data)).to.be.a('string'); + expect(getPersonalizedRewrite('/pathname', variantIds)).to.be.a('string'); }); - it('should return the path with the variant id when pathname starts with "/"', () => { + it('should return the path with the variant ids when pathname starts with "/"', () => { const pathname = '/some/path'; - const result = getPersonalizedRewrite(pathname, data); - expect(result).to.equal(`/${VARIANT_PREFIX}${data.variantId}/some/path`); + const result = getPersonalizedRewrite(pathname, variantIds); + expect(result).to.equal( + `/${VARIANT_PREFIX}${variantIds[0]}/${VARIANT_PREFIX}${variantIds[1]}/some/path` + ); }); - it('should return the path with the variant id when pathname not starts with "/"', () => { + it('should return the path with the variant ids when pathname not starts with "/"', () => { const pathname = 'some/path'; - const result = getPersonalizedRewrite(pathname, data); - expect(result).to.equal(`/${VARIANT_PREFIX}${data.variantId}/some/path`); + const result = getPersonalizedRewrite(pathname, variantIds); + expect(result).to.equal( + `/${VARIANT_PREFIX}${variantIds[0]}/${VARIANT_PREFIX}${variantIds[1]}/some/path` + ); }); - it('should return the root path with the variant id', () => { + it('should return the root path with the variant ids', () => { const pathname = '/'; - const result = getPersonalizedRewrite(pathname, data); - expect(result).to.equal(`/${VARIANT_PREFIX}${data.variantId}/`); + const result = getPersonalizedRewrite(pathname, variantIds); + expect(result).to.equal( + `/${VARIANT_PREFIX}${variantIds[0]}/${VARIANT_PREFIX}${variantIds[1]}/` + ); + }); + it('should return the path when variant ids are empty', () => { + const pathname = '/some/path'; + const result = getPersonalizedRewrite(pathname, []); + expect(result).to.equal('/some/path'); }); }); @@ -234,42 +243,83 @@ describe('utils', () => { }); }); - describe('getContentId', () => { + describe('getPageFriendlyId', () => { it('should format variant', () => { const pageId = '110d559fdea542ea9c1c8a5df7e70ef9'; const language = 'en'; - const result = CdpHelper.getContentId(pageId, language); + const result = CdpHelper.getPageFriendlyId(pageId, language); expect(result).to.equal(`embedded_${pageId}_${language}`); }); it('should format variant with scope', () => { const pageId = '110d559fdea542ea9c1c8a5df7e70ef9'; const language = 'en'; const scope = 'myscope1'; - const result = CdpHelper.getContentId(pageId, language, scope); + const result = CdpHelper.getPageFriendlyId(pageId, language, scope); expect(result).to.equal(`embedded_${scope}_${pageId}_${language}`); }); it('should use lowercase', () => { const pageId = '3E0A2F20B3255E57881FFF6648D08575'; const language = 'EN'; - const result = CdpHelper.getContentId(pageId, language); + const result = CdpHelper.getPageFriendlyId(pageId, language); expect(result).to.equal(`embedded_${pageId}_${language}`.toLowerCase()); }); it('should convert language dashes to underscores', () => { const pageId = '3E0A2F20B3255E57881FFF6648D08575'; const language = 'da-DK'; - const result = CdpHelper.getContentId(pageId, language); + const result = CdpHelper.getPageFriendlyId(pageId, language); expect(result).to.equal(`embedded_${pageId}_da_DK`.toLowerCase()); }); it('should ensure GUID format N for pageId', () => { const pageId = '{FFCD3AC4-38E3-5286-A0B9-5F7113D5E74A}'; const language = 'en'; - const result = CdpHelper.getContentId(pageId, language); + const result = CdpHelper.getPageFriendlyId(pageId, language); expect(result).to.equal( `embedded_FFCD3AC438E35286A0B95F7113D5E74A_${language}`.toLowerCase() ); }); }); + describe('getComponentFriendlyId', () => { + it('should format', () => { + const pageId = '110d559fdea542ea9c1c8a5df7e70ef9'; + const componentId = '733995bb878042daa6d6c2c5f3f70707'; + const language = 'en'; + const result = CdpHelper.getComponentFriendlyId(pageId, componentId, language); + expect(result).to.equal(`component_${pageId}_${componentId}_${language}*`); + }); + it('should format with scope', () => { + const pageId = '110d559fdea542ea9c1c8a5df7e70ef9'; + const componentId = '733995bb878042daa6d6c2c5f3f70707'; + const language = 'en'; + const scope = 'myscope1'; + const result = CdpHelper.getComponentFriendlyId(pageId, componentId, language, scope); + expect(result).to.equal(`component_${scope}_${pageId}_${componentId}_${language}*`); + }); + it('should use lowercase', () => { + const pageId = '3E0A2F20B3255E57881FFF6648D08575'; + const componentId = '733995BB878042DAA6D6C2C5F3F70707'; + const language = 'EN'; + const result = CdpHelper.getComponentFriendlyId(pageId, componentId, language); + expect(result).to.equal(`component_${pageId}_${componentId}_${language}*`.toLowerCase()); + }); + it('should convert language dashes to underscores', () => { + const pageId = '3E0A2F20B3255E57881FFF6648D08575'; + const componentId = '733995BB878042DAA6D6C2C5F3F70707'; + const language = 'da-DK'; + const result = CdpHelper.getComponentFriendlyId(pageId, componentId, language); + expect(result).to.equal(`component_${pageId}_${componentId}_da_DK*`.toLowerCase()); + }); + it('should ensure GUID format N', () => { + const pageId = '{FFCD3AC4-38E3-5286-A0B9-5F7113D5E74A}'; + const componentId = '{733995BB-8780-42DA-A6D6-C2C5F3F70707}'; + const language = 'en'; + const result = CdpHelper.getComponentFriendlyId(pageId, componentId, language); + expect(result).to.equal( + `component_FFCD3AC438E35286A0B95F7113D5E74A_733995BB878042DAA6D6C2C5F3F70707_${language}*`.toLowerCase() + ); + }); + }); + describe('normalizeScope', () => { it('should return empty string when no scope value is provided', () => { expect(CdpHelper.normalizeScope()).to.equal(''); diff --git a/packages/sitecore-jss/src/personalize/utils.ts b/packages/sitecore-jss/src/personalize/utils.ts index 94c5729142..ef5a0f384d 100644 --- a/packages/sitecore-jss/src/personalize/utils.ts +++ b/packages/sitecore-jss/src/personalize/utils.ts @@ -9,12 +9,12 @@ export type PersonalizedRewriteData = { /** * Get a personalized rewrite path for given pathname * @param {string} pathname the pathname - * @param {PersonalizedRewriteData} data the personalize data to include in the rewrite + * @param {string[]} variantIds the variantIds to include in the rewrite * @returns {string} the rewrite path */ -export function getPersonalizedRewrite(pathname: string, data: PersonalizedRewriteData): string { +export function getPersonalizedRewrite(pathname: string, variantIds: string[]): string { const path = pathname.startsWith('/') ? pathname : '/' + pathname; - return `/${VARIANT_PREFIX}${data.variantId}${path}`; + return `${variantIds.map((variantId) => `/${VARIANT_PREFIX}${variantId}`).join('')}${path}`; } /** @@ -106,19 +106,40 @@ export class CdpHelper { } /** - * Gets the content id for CDP in the required format `embedded_[_]_` + * Gets the friendly id for (page-level) Embedded Personalization in the required format `embedded_[_]_` * @param {string} pageId the page id * @param {string} language the language * @param {string} [scope] the scope value - * @returns {string} the content id + * @returns {string} the friendly id */ - static getContentId(pageId: string, language: string, scope?: string): string { + static getPageFriendlyId(pageId: string, language: string, scope?: string): string { const formattedPageId = pageId.replace(/[{}-]/g, ''); const formattedLanguage = language.replace('-', '_'); const scopeId = scope ? `${this.normalizeScope(scope)}_` : ''; return `embedded_${scopeId}${formattedPageId}_${formattedLanguage}`.toLowerCase(); } + /** + * Gets the friendly id for Component A/B Testing in the required format `component_[_]__*` + * @param {string} pageId the page id + * @param {string} componentId the component id + * @param {string} language the language + * @param {string} [scope] the scope value + * @returns {string} the friendly id + */ + static getComponentFriendlyId( + pageId: string, + componentId: string, + language: string, + scope?: string + ): string { + const formattedPageId = pageId.replace(/[{}-]/g, ''); + const formattedComponentId = componentId.replace(/[{}-]/g, ''); + const formattedLanguage = language.replace('-', '_'); + const scopeId = scope ? `${this.normalizeScope(scope)}_` : ''; + return `component_${scopeId}${formattedPageId}_${formattedComponentId}_${formattedLanguage}*`.toLowerCase(); + } + /** * Normalizes the scope from the given string value * Removes all non-alphanumeric characters diff --git a/yarn.lock b/yarn.lock index 9037194062..fb32fede27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5997,30 +5997,30 @@ __metadata: languageName: node linkType: hard -"@sitecore-cloudsdk/core@npm:^0.3.0": - version: 0.3.0 - resolution: "@sitecore-cloudsdk/core@npm:0.3.0" +"@sitecore-cloudsdk/core@npm:^0.3.1": + version: 0.3.1 + resolution: "@sitecore-cloudsdk/core@npm:0.3.1" dependencies: - "@sitecore-cloudsdk/utils": ^0.3.0 + "@sitecore-cloudsdk/utils": ^0.3.1 debug: ^4.3.4 - checksum: f494bd1d779204cc4ad9c5ae0692a5897ba1b391637a4b40e888363ad00c94feb0a9c5e57c4f67977aacd1905da29edf0fef35d1e8f7e61f7cac7385b08dc029 + checksum: c19d750b1c194f7575d20b3d46739d7b41545087a9fb794ba44ab9b2d812f2b2035caf19c4f4ba51e6ff628c99f2e4a18b0a7a2578b413c7adb04f3d77f392a3 languageName: node linkType: hard -"@sitecore-cloudsdk/personalize@npm:^0.3.0": - version: 0.3.0 - resolution: "@sitecore-cloudsdk/personalize@npm:0.3.0" +"@sitecore-cloudsdk/personalize@npm:^0.3.1": + version: 0.3.1 + resolution: "@sitecore-cloudsdk/personalize@npm:0.3.1" dependencies: - "@sitecore-cloudsdk/core": ^0.3.0 - "@sitecore-cloudsdk/utils": ^0.3.0 - checksum: d7132692a896198b25603db6fb8d3e7813b55389b276c9433e67033f990c06ed818a0979f52a8d0af2492de42d09c1b4fe28c4d4c18a8eca6fa0d82fe7e99d43 + "@sitecore-cloudsdk/core": ^0.3.1 + "@sitecore-cloudsdk/utils": ^0.3.1 + checksum: e48b1429f67be22069bfd8cc6349660ed346877fef5537def5f36c962fd2264969ff02fb2e16333dabc683c4dd0373ae12ce56b97a6850910ab618957b9586d9 languageName: node linkType: hard -"@sitecore-cloudsdk/utils@npm:^0.3.0": - version: 0.3.0 - resolution: "@sitecore-cloudsdk/utils@npm:0.3.0" - checksum: 4ee3224d4623d7a60b3b48178581b61498fceaa9cc6e0396f7d664537e3a37f41faeaaf3c9503d1e4e95fa9862892f407ef84ea0925458d68af94232598ade54 +"@sitecore-cloudsdk/utils@npm:^0.3.1": + version: 0.3.1 + resolution: "@sitecore-cloudsdk/utils@npm:0.3.1" + checksum: c6578ec04823a77aaf7cb6f9cdb842c227698507c38b722db0b9fe1c6b21ee169443cc6b9b8ca77f86c9a4dfc835e1c3b85369079c70ef88e555c8243275d9a6 languageName: node linkType: hard @@ -6213,7 +6213,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-nextjs@workspace:packages/sitecore-jss-nextjs" dependencies: - "@sitecore-cloudsdk/personalize": ^0.3.0 + "@sitecore-cloudsdk/personalize": ^0.3.1 "@sitecore-jss/sitecore-jss": 22.1.0-canary.58 "@sitecore-jss/sitecore-jss-dev-tools": 22.1.0-canary.58 "@sitecore-jss/sitecore-jss-react": 22.1.0-canary.58 @@ -6255,8 +6255,8 @@ __metadata: ts-node: ^10.9.1 typescript: ~4.9.4 peerDependencies: - "@sitecore-cloudsdk/events": ^0.3.0 - "@sitecore-cloudsdk/personalize": ^0.3.0 + "@sitecore-cloudsdk/events": ^0.3.1 + "@sitecore-cloudsdk/personalize": ^0.3.1 next: ^14.1.0 react: ^18.2.0 react-dom: ^18.2.0