diff --git a/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/middleware/plugins/personalize.ts b/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/middleware/plugins/personalize.ts index 0c0b8dea35..2d10841d2c 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/middleware/plugins/personalize.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/middleware/plugins/personalize.ts @@ -43,9 +43,8 @@ class PersonalizePlugin implements MiddlewarePlugin { }, // 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 also wish to disable in development mode (process.env.NODE_ENV === 'development'). - // By default it is always enabled. - disabled: () => false, + // You may wish to keep it disabled while in development mode. + disabled: () => process.env.NODE_ENV === 'development', // This function determines if a route should be excluded from personalization. // Certain paths are ignored by default (e.g. files and Next.js API routes), but you may wish to exclude more. // This is an important performance consideration since Next.js Edge middleware runs on every request. 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 03933b027b..782b6bab05 100644 --- a/packages/sitecore-jss/src/personalize/graphql-personalize-service.test.ts +++ b/packages/sitecore-jss/src/personalize/graphql-personalize-service.test.ts @@ -30,11 +30,7 @@ describe('GraphQLPersonalizeService', () => { }, }; - afterEach(() => { - nock.cleanAll(); - }); - - it('should return personalize info for a route', async () => { + const mockNonEmptyResponse = () => { nock('http://sctest', { reqheaders: { sc_apikey: apiKey, @@ -44,17 +40,9 @@ describe('GraphQLPersonalizeService', () => { .reply(200, { data: personalizeQueryResult, }); + }; - const service = new GraphQLPersonalizeService(config); - const personalizeData = await service.getPersonalizeInfo('/sitecore/content/home', 'en'); - - expect(personalizeData).to.deep.equal({ - contentId: `embedded_${id}_en`.toLowerCase(), - variantIds, - }); - }); - - it('should return undefined if itemPath / language not found', async () => { + const mockEmptyResponse = () => { nock('http://sctest', { reqheaders: { sc_apikey: apiKey, @@ -66,6 +54,26 @@ describe('GraphQLPersonalizeService', () => { layout: {}, }, }); + }; + + afterEach(() => { + nock.cleanAll(); + }); + + it('should return personalize info for a route', async () => { + mockNonEmptyResponse(); + + const service = new GraphQLPersonalizeService(config); + const personalizeData = await service.getPersonalizeInfo('/sitecore/content/home', 'en'); + + expect(personalizeData).to.deep.equal({ + contentId: `embedded_${id}_en`.toLowerCase(), + variantIds, + }); + }); + + it('should return undefined if itemPath / language not found', async () => { + mockEmptyResponse(); const service = new GraphQLPersonalizeService(config); const personalizeData = await service.getPersonalizeInfo('/sitecore/content/home', ''); @@ -133,4 +141,89 @@ describe('GraphQLPersonalizeService', () => { expect(result).to.equal(undefined); }); + + it('should cache service response by default', async () => { + mockNonEmptyResponse(); + + const itemPath = '/sitecore/content/home'; + const lang = 'en'; + + const service = new GraphQLPersonalizeService(config); + const firstResult = await service.getPersonalizeInfo(itemPath, lang); + + expect(firstResult).to.deep.equal({ + contentId: `embedded_${id}_en`.toLowerCase(), + variantIds, + }); + + mockEmptyResponse(); + + const secondResult = await service.getPersonalizeInfo(itemPath, lang); + + expect(secondResult).to.deep.equal(firstResult); + }); + + it('should be possible to disable cache', async () => { + mockNonEmptyResponse(); + + const itemPath = '/sitecore/content/home'; + const lang = 'en'; + + const service = new GraphQLPersonalizeService({ + ...config, + cacheEnabled: false, + }); + const firstResult = await service.getPersonalizeInfo(itemPath, lang); + + expect(firstResult).to.deep.equal({ + contentId: `embedded_${id}_en`.toLowerCase(), + variantIds, + }); + + mockEmptyResponse(); + + const secondResult = await service.getPersonalizeInfo(itemPath, lang); + + expect(secondResult).to.not.deep.equal(firstResult); + }); + + it('cache timeout should be used', async () => { + mockNonEmptyResponse(); + + const itemPath = '/sitecore/content/home'; + const lang = 'en'; + + const service = new GraphQLPersonalizeService({ + ...config, + cacheTimeout: 0.2, + }); + const firstResult = await service.getPersonalizeInfo(itemPath, lang); + + mockEmptyResponse(); + + const cacheNonUpdate = new Promise((resolve) => { + setTimeout( + () => + service.getPersonalizeInfo(itemPath, lang).then((newResult) => { + expect(newResult).to.deep.equal(firstResult); + resolve(undefined); + }), + 100 + ); + }); + + const cacheUpdate = new Promise((resolve) => { + setTimeout( + () => + service.getPersonalizeInfo(itemPath, lang).then((newResult) => { + expect(newResult).to.deep.equal(undefined); + resolve(undefined); + }), + 250 + ); + }); + await cacheNonUpdate; + + await cacheUpdate; + }); }); diff --git a/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts b/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts index c30a16886c..810986c06c 100644 --- a/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts +++ b/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts @@ -2,8 +2,9 @@ import { GraphQLClient, GraphQLRequestClient } 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 = { +export type GraphQLPersonalizeServiceConfig = CacheOptions & { /** * Your Graphql endpoint */ @@ -46,6 +47,7 @@ type PersonalizeQueryResult = { export class GraphQLPersonalizeService { private graphQLClient: GraphQLClient; + private cache: CacheClient; protected get query(): string { return /* GraphQL */ ` query($siteName: String!, $language: String!, $itemPath: String!) { @@ -68,6 +70,7 @@ export class GraphQLPersonalizeService { constructor(protected config: GraphQLPersonalizeServiceConfig) { this.config.timeout = config.timeout || 250; this.graphQLClient = this.getGraphQLClient(); + this.cache = this.getCacheClient(); } /** @@ -87,27 +90,48 @@ export class GraphQLPersonalizeService { language ); - try { - const data = await this.graphQLClient.request(this.query, { - siteName: this.config.siteName, - itemPath, - language, - }); + const cacheKey = this.getCacheKey(itemPath, language); + let data = this.cache.getCacheValue(cacheKey); - return data?.layout?.item - ? { - // CDP expects content id format `embedded__` (lowercase) - contentId: CdpHelper.getContentId(data.layout.item.id, language), - variantIds: data.layout.item.personalization.variantIds, - } - : undefined; - } catch (error) { - if (isTimeoutError(error)) { - return undefined; - } + if (!data) { + try { + data = await this.graphQLClient.request(this.query, { + siteName: this.config.siteName, + itemPath, + language, + }); + this.cache.setCacheValue(cacheKey, data); + } catch (error) { + if (isTimeoutError(error)) { + return undefined; + } - throw error; + throw error; + } } + return data?.layout?.item + ? { + // CDP expects content id format `embedded__` (lowercase) + contentId: CdpHelper.getContentId(data.layout.item.id, language), + variantIds: data.layout.item.personalization.variantIds, + } + : undefined; + } + + /** + * Gets cache client implementation + * Override this method if custom cache needs to be used + * @returns CacheClient instance + */ + protected getCacheClient(): CacheClient { + return new MemoryCacheClient({ + cacheEnabled: this.config.cacheEnabled ?? true, + cacheTimeout: this.config.cacheTimeout ?? 10, + }); + } + + protected getCacheKey(itemPath: string, language: string) { + return `${this.config.siteName}-${itemPath}-${language}`; } /**