Skip to content

Commit

Permalink
Merge pull request #1218 from Sitecore/feature/personalize-performance
Browse files Browse the repository at this point in the history
Performance improvements for personalize service and middleware
  • Loading branch information
art-alexeyenko authored Nov 4, 2022
2 parents 0416c4a + 147fb4a commit ad0d767
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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', '');
Expand Down Expand Up @@ -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;
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -46,6 +47,7 @@ type PersonalizeQueryResult = {

export class GraphQLPersonalizeService {
private graphQLClient: GraphQLClient;
private cache: CacheClient<PersonalizeQueryResult>;
protected get query(): string {
return /* GraphQL */ `
query($siteName: String!, $language: String!, $itemPath: String!) {
Expand All @@ -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();
}

/**
Expand All @@ -87,27 +90,48 @@ export class GraphQLPersonalizeService {
language
);

try {
const data = await this.graphQLClient.request<PersonalizeQueryResult>(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_<id>_<lang>` (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<PersonalizeQueryResult>(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_<id>_<lang>` (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<PersonalizeQueryResult> {
return new MemoryCacheClient<PersonalizeQueryResult>({
cacheEnabled: this.config.cacheEnabled ?? true,
cacheTimeout: this.config.cacheTimeout ?? 10,
});
}

protected getCacheKey(itemPath: string, language: string) {
return `${this.config.siteName}-${itemPath}-${language}`;
}

/**
Expand Down

0 comments on commit ad0d767

Please sign in to comment.