From 0201f9d7ef4149dce64701e20c2aa145240578f7 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 11 Sep 2024 12:43:35 -0600 Subject: [PATCH 1/6] fix(preact/instantiator/recommendation): adding fallback selector to support email recs imager --- .../src/Instantiators/RecommendationInstantiator.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx index e6779a52b..9bcb801ef 100644 --- a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx +++ b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx @@ -127,7 +127,9 @@ export class RecommendationInstantiator { this.targeter = new DomTargeter( [ { - selector: this.config.selector || 'script[type="searchspring/recommend"], script[type="searchspring/personalized-recommendations"]', + selector: `${ + this.config.selector || 'script[type="searchspring/recommend"], script[type="searchspring/personalized-recommendations"]' + }, script[type="searchspring/recommend"][profile="email"]`, autoRetarget: true, clickRetarget: true, inject: { From bb0212a8412ed5337a50be83a95ce3fb24c3bb8d Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 11 Sep 2024 12:59:47 -0600 Subject: [PATCH 2/6] fix(preact-components/recommendation): adjusting logic to prevent rendering title with zero results --- .../src/components/Organisms/Recommendation/Recommendation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snap-preact-components/src/components/Organisms/Recommendation/Recommendation.tsx b/packages/snap-preact-components/src/components/Organisms/Recommendation/Recommendation.tsx index 042fd64cc..fcbdf0abf 100644 --- a/packages/snap-preact-components/src/components/Organisms/Recommendation/Recommendation.tsx +++ b/packages/snap-preact-components/src/components/Organisms/Recommendation/Recommendation.tsx @@ -136,7 +136,7 @@ export const Recommendation = observer((properties: RecommendationProps): JSX.El setIsVisible(true); } - return children || resultsToRender?.length ? ( + return (Array.isArray(children) && children.length) || resultsToRender?.length ? (
{isVisible ? ( From 5b2e4607d3a286bd508c265e782cbc76f92114f7 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 11 Sep 2024 14:33:45 -0600 Subject: [PATCH 3/6] fix(client/recommend): adjusting request batching to ensure undefined entries do not overwrite --- .../src/Client/apis/Recommend.test.ts | 35 +++++++++++++ .../snap-client/src/Client/apis/Recommend.ts | 52 ++++++++----------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/packages/snap-client/src/Client/apis/Recommend.test.ts b/packages/snap-client/src/Client/apis/Recommend.test.ts index acbd9b476..fc0f5d669 100644 --- a/packages/snap-client/src/Client/apis/Recommend.test.ts +++ b/packages/snap-client/src/Client/apis/Recommend.test.ts @@ -259,6 +259,41 @@ describe('Recommend Api', () => { requestMock.mockReset(); }); + it('batchRecommendations uses parameters regardless of order specified in requests', async () => { + const api = new RecommendAPI(new ApiConfiguration({})); + + const requestMock = jest + .spyOn(global.window, 'fetch') + .mockImplementation(() => Promise.resolve({ status: 200, json: () => Promise.resolve(mockData.recommend()) } as Response)); + + api.batchRecommendations({ + tag: 'similar', + products: ['sku1'], + batched: true, + siteId: '8uyt2m', + }); + + api.batchRecommendations({ + tag: 'crossSell', + batched: true, + siteId: '8uyt2m', + }); + + //add delay for paramBatch.timeout + await wait(250); + + const POSTParams = { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: '{"profiles":[{"tag":"similar","limit":20},{"tag":"crossSell","limit":20}],"siteId":"8uyt2m","products":["sku1"]}', + }; + + expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); + requestMock.mockReset(); + }); + it('batchRecommendations handles order prop as expected', async () => { const api = new RecommendAPI(new ApiConfiguration({})); diff --git a/packages/snap-client/src/Client/apis/Recommend.ts b/packages/snap-client/src/Client/apis/Recommend.ts index 593d3c559..d8070bdd4 100644 --- a/packages/snap-client/src/Client/apis/Recommend.ts +++ b/packages/snap-client/src/Client/apis/Recommend.ts @@ -72,12 +72,21 @@ export class RecommendAPI extends API { // delete the batch so a new one can take its place delete this.batches[key]; - //resort batch entries based on order + // resort batch entries based on order batch.entries.sort(sortBatchEntries); - // now that the requests are in proper order, map through them - // and build out the batches + // now that the requests are in proper order, map through them and build out the batches batch.entries.map((entry) => { + // use products request only and combine when needed + if (entry.request.product) { + if (Array.isArray(entry.request.products) && entry.request.products.indexOf(entry.request.product) == -1) { + entry.request.products = entry.request.products.concat(entry.request.product); + } else { + entry.request.products = [entry.request.product]; + } + } + + // parameters used for profile specific const { tag, categories, brands, query, filters, dedupe } = entry.request; let transformedFilters; @@ -85,6 +94,7 @@ export class RecommendAPI extends API { transformedFilters = transformRecommendationFiltersPost(filters) as RecommendPostRequestFiltersModel[]; } + // build profile specific parameters const profile: RecommendPostRequestProfileModel = { tag, categories, @@ -97,28 +107,16 @@ export class RecommendAPI extends API { batch.request.profiles?.push(profile); - batch.request = { - ...batch.request, - siteId: parameters.siteId, - product: parameters.product, - products: parameters.products, - blockedItems: parameters.blockedItems, - test: parameters.test, - cart: parameters.cart, - lastViewed: parameters.lastViewed, - shopper: parameters.shopper, - } as RecommendPostRequestModel; - - // use products request only and combine when needed - if (batch.request.product) { - if (Array.isArray(batch.request.products) && batch.request.products.indexOf(batch.request.product) == -1) { - batch.request.products = batch.request.products.concat(batch.request.product); - } else { - batch.request.products = [batch.request.product]; - } - - delete batch.request.product; - } + // parameters used globally + const { siteId, products, blockedItems, test, cart, lastViewed, shopper } = entry.request; + // only when these parameters are defined should they be added to the list + if (siteId) batch.request.siteId = siteId; + if (products) batch.request.products = products; + if (blockedItems) batch.request.blockedItems = blockedItems; + if (test) batch.request.test = test; + if (cart) batch.request.cart = cart; + if (lastViewed) batch.request.lastViewed = lastViewed; + if (shopper) batch.request.shopper = shopper; }); try { @@ -126,10 +124,6 @@ export class RecommendAPI extends API { batch.request.test = true; } - if (batch.request['product']) { - batch.request['product'] = batch.request['product'].toString(); - } - const response = await this.postRecommendations(batch.request as RecommendPostRequestModel); batch.entries?.forEach((entry, index) => { From 66a65fb21e27b38891e5b3d1a00ee29ae82068d3 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 12 Sep 2024 10:51:17 -0600 Subject: [PATCH 4/6] refactor(client/recommend): refactoring recommend request to better support global and profile param --- .../snap-client/src/Client/Client.test.ts | 6 +- .../src/Client/apis/Recommend.test.ts | 225 +++++++++++++----- .../snap-client/src/Client/apis/Recommend.ts | 98 +++++--- .../transforms/recommendationFiltersPost.ts | 4 +- packages/snap-client/src/index.ts | 9 +- packages/snap-client/src/types.ts | 36 +-- 6 files changed, 273 insertions(+), 105 deletions(-) diff --git a/packages/snap-client/src/Client/Client.test.ts b/packages/snap-client/src/Client/Client.test.ts index 03e6c8c7d..e69983dd4 100644 --- a/packages/snap-client/src/Client/Client.test.ts +++ b/packages/snap-client/src/Client/Client.test.ts @@ -359,7 +359,6 @@ describe('Snap Client', () => { body: { profiles: [ { - limit: 20, tag: 'dress', }, ], @@ -368,7 +367,7 @@ describe('Snap Client', () => { }, }; - const recommendCacheKey = '{"profiles":[{"tag":"dress","limit":20}],"siteId":"8uyt2m","test":true}'; + const recommendCacheKey = '{"profiles":[{"tag":"dress"}],"siteId":"8uyt2m","test":true}'; expect(recommendRequesterSpy).toHaveBeenCalledTimes(2); expect(recommendRequesterSpy.mock.calls).toEqual([ @@ -606,7 +605,6 @@ describe('Snap Client', () => { body: { profiles: [ { - limit: 20, tag: 'dress', }, ], @@ -615,7 +613,7 @@ describe('Snap Client', () => { }, }; - const recommendCacheKey = '{"profiles":[{"tag":"dress","limit":20}],"siteId":"8uyt2m","test":true}'; + const recommendCacheKey = '{"profiles":[{"tag":"dress"}],"siteId":"8uyt2m","test":true}'; expect(recommendRequesterSpy).toHaveBeenCalledTimes(2); expect(recommendRequesterSpy.mock.calls).toEqual([ diff --git a/packages/snap-client/src/Client/apis/Recommend.test.ts b/packages/snap-client/src/Client/apis/Recommend.test.ts index fc0f5d669..32bda6af4 100644 --- a/packages/snap-client/src/Client/apis/Recommend.test.ts +++ b/packages/snap-client/src/Client/apis/Recommend.test.ts @@ -1,5 +1,5 @@ import 'whatwg-fetch'; -import { ApiConfiguration } from './Abstract'; +import { ApiConfiguration, ApiConfigurationParameters } from './Abstract'; import { RecommendAPI } from './Recommend'; import { MockData } from '@searchspring/snap-shared'; @@ -13,9 +13,11 @@ const wait = (time?: number) => { }); }; +const apiConfig: ApiConfigurationParameters = { cache: { enabled: false } }; + describe('Recommend Api', () => { it('has expected default functions', () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); // @ts-ignore - accessing private property expect(api?.batches).toBeDefined(); @@ -28,7 +30,7 @@ describe('Recommend Api', () => { }); it('can call getProfile', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const params = { body: undefined, @@ -53,7 +55,7 @@ describe('Recommend Api', () => { }); it('can call postRecommendations', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const params = { method: 'POST', @@ -86,14 +88,14 @@ describe('Recommend Api', () => { }); it('can call batchRecommendations', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const params = { method: 'POST', headers: { 'Content-Type': 'text/plain', }, - body: `{"profiles":[{"tag":"similar","limit":20}],"siteId":"8uyt2m"}`, + body: `{"profiles":[{"tag":"similar"}],"siteId":"8uyt2m"}`, }; const requestUrl = 'https://8uyt2m.a.searchspring.io/boost/8uyt2m/recommend'; @@ -139,7 +141,7 @@ describe('Recommend Api', () => { const RequestUrl = 'https://8uyt2m.a.searchspring.io/boost/8uyt2m/recommend'; it('batchRecommendations batches as expected', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const requestMock = jest .spyOn(global.window, 'fetch') @@ -175,7 +177,7 @@ describe('Recommend Api', () => { }); it('batchRecommendations handles multiple categories as expected', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const requestMock = jest .spyOn(global.window, 'fetch') @@ -221,7 +223,7 @@ describe('Recommend Api', () => { }); it('batchRecommendations handles multiple brands as expected', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const requestMock = jest .spyOn(global.window, 'fetch') @@ -260,7 +262,7 @@ describe('Recommend Api', () => { }); it('batchRecommendations uses parameters regardless of order specified in requests', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const requestMock = jest .spyOn(global.window, 'fetch') @@ -271,6 +273,7 @@ describe('Recommend Api', () => { products: ['sku1'], batched: true, siteId: '8uyt2m', + limit: 20, }); api.batchRecommendations({ @@ -287,15 +290,158 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"similar","limit":20},{"tag":"crossSell","limit":20}],"siteId":"8uyt2m","products":["sku1"]}', + body: '{"profiles":[{"tag":"similar","limit":20},{"tag":"crossSell"}],"siteId":"8uyt2m","products":["sku1"]}', + }; + + expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); + requestMock.mockReset(); + }); + + it('batchRecommendations unbatches properly', async () => { + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); + + const requestMock = jest + .spyOn(global.window, 'fetch') + .mockImplementation(() => Promise.resolve({ status: 200, json: () => Promise.resolve(mockData.recommend()) } as Response)); + + api.batchRecommendations({ + tag: 'similar', + products: ['sku1'], + batched: false, + siteId: '8uyt2m', + limit: 20, + }); + + api.batchRecommendations({ + tag: 'crossSell', + batched: true, + siteId: '8uyt2m', + }); + + api.batchRecommendations({ + tag: 'static', + batched: true, + siteId: '8uyt2m', + }); + + //add delay for paramBatch.timeout + await wait(250); + + const firstBatchPOSTParams = { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: '{"profiles":[{"tag":"similar","limit":20}],"siteId":"8uyt2m","products":["sku1"]}', + }; + const secondBatchPOSTParams = { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: '{"profiles":[{"tag":"crossSell"},{"tag":"static"}],"siteId":"8uyt2m"}', + }; + + expect(requestMock).toHaveBeenCalledTimes(2); + expect(requestMock).toHaveBeenNthCalledWith(1, 'https://8uyt2m.a.searchspring.io/boost/8uyt2m/recommend', firstBatchPOSTParams); + expect(requestMock).toHaveBeenNthCalledWith(2, 'https://8uyt2m.a.searchspring.io/boost/8uyt2m/recommend', secondBatchPOSTParams); + requestMock.mockReset(); + }); + + it('batchRecommendations can be passed a profile', async () => { + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); + + const requestMock = jest + .spyOn(global.window, 'fetch') + .mockImplementation(() => Promise.resolve({ status: 200, json: () => Promise.resolve(mockData.recommend()) } as Response)); + + api.batchRecommendations({ + tag: 'similar', + siteId: '8uyt2m', + products: ['sku1'], + batched: true, + profile: { + limit: 20, + }, + }); + + api.batchRecommendations({ + tag: 'crossSell', + siteId: '8uyt2m', + batched: true, + profile: { + limit: 10, + }, + }); + + //add delay for paramBatch.timeout + await wait(250); + + const POSTParams = { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: '{"profiles":[{"tag":"similar","limit":20},{"tag":"crossSell","limit":10}],"siteId":"8uyt2m","products":["sku1"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); requestMock.mockReset(); }); + it('batchRecommendations unbatches when different siteIds are provided', async () => { + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); + + const requestMock = jest + .spyOn(global.window, 'fetch') + .mockImplementation(() => Promise.resolve({ status: 200, json: () => Promise.resolve(mockData.recommend()) } as Response)); + + api.batchRecommendations({ + tag: 'similar', + products: ['sku1'], + batched: true, + siteId: '8uyt2m', + profile: { + limit: 20, + }, + }); + + api.batchRecommendations({ + tag: 'crossSell', + batched: true, + siteId: '8uyt2m', + profile: { + siteId: '123abc', + limit: 5, + }, + }); + + //add delay for paramBatch.timeout + await wait(250); + + const POSTParams8uyt2m = { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: '{"profiles":[{"tag":"similar","limit":20}],"siteId":"8uyt2m","products":["sku1"]}', + }; + const POSTParams123abc = { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: '{"profiles":[{"tag":"crossSell","limit":5}],"siteId":"123abc"}', + }; + + expect(requestMock).toHaveBeenCalledTimes(2); + expect(requestMock).toHaveBeenNthCalledWith(1, 'https://8uyt2m.a.searchspring.io/boost/8uyt2m/recommend', POSTParams8uyt2m); + expect(requestMock).toHaveBeenNthCalledWith(2, 'https://123abc.a.searchspring.io/boost/123abc/recommend', POSTParams123abc); + requestMock.mockReset(); + }); + it('batchRecommendations handles order prop as expected', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const requestMock = jest .spyOn(global.window, 'fetch') @@ -351,7 +497,7 @@ describe('Recommend Api', () => { }); it('batchRecommendations resolves in right order with order prop', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const response = mockData.file('recommend/results/8uyt2m/ordered.json'); const requestMock = jest @@ -398,7 +544,7 @@ describe('Recommend Api', () => { }); it('batchRecommendations handles filters expected', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const requestMock = jest .spyOn(global.window, 'fetch') @@ -424,35 +570,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"crossSell","limit":10,"filters":[{"field":"color","type":"=","values":["red"]}]}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', - }; - - expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); - requestMock.mockReset(); - }); - - it('batchRecommendations handles undefined limit', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); - - const requestMock = jest - .spyOn(global.window, 'fetch') - .mockImplementation(() => Promise.resolve({ status: 200, json: () => Promise.resolve(mockData.recommend()) } as Response)); - - //now consts try with no limit - api.batchRecommendations({ - tag: 'crossSell', - ...batchParams, - limit: undefined, - }); - - //add delay for paramBatch.timeout - await wait(250); - const POSTParams = { - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - }, - body: '{"profiles":[{"tag":"crossSell","limit":20}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', + body: '{"profiles":[{"tag":"crossSell","limit":10}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"filters":[{"field":"color","type":"=","values":["red"]}],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); @@ -460,7 +578,7 @@ describe('Recommend Api', () => { }); it('batchRecommendations will combine `product` and `products` params into `products` when used together', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const requestMock = jest .spyOn(global.window, 'fetch') @@ -476,7 +594,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"crossSell","limit":20}],"siteId":"8uyt2m","products":["some_sku","some_sku2","marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', + body: '{"profiles":[{"tag":"crossSell"}],"siteId":"8uyt2m","products":["some_sku","some_sku2","marnie-runner-2-7x10"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); @@ -484,7 +602,7 @@ describe('Recommend Api', () => { }); it('batchRecommendations will utilize the `blockedItems` parameter when provided', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); const requestMock = jest .spyOn(global.window, 'fetch') @@ -500,7 +618,7 @@ describe('Recommend Api', () => { headers: { 'Content-Type': 'text/plain', }, - body: '{"profiles":[{"tag":"crossSell","limit":20}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"blockedItems":["blocked_sku1","blocked_sku2"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', + body: '{"profiles":[{"tag":"crossSell"}],"siteId":"8uyt2m","products":["marnie-runner-2-7x10"],"blockedItems":["blocked_sku1","blocked_sku2"],"lastViewed":["marnie-runner-2-7x10","ruby-runner-2-7x10","abbie-runner-2-7x10","riley-4x6","joely-5x8","helena-4x6","kwame-4x6","sadie-4x6","candice-runner-2-7x10","esmeray-4x6","camilla-230x160","candice-4x6","sahara-4x6","dayna-4x6","moema-4x6"]}', }; expect(requestMock).toHaveBeenCalledWith(RequestUrl, POSTParams); @@ -508,7 +626,7 @@ describe('Recommend Api', () => { }); it('batchRecommendations handles POST requests', async () => { - const api = new RecommendAPI(new ApiConfiguration({})); + const api = new RecommendAPI(new ApiConfiguration(apiConfig)); //now consts try a post const POSTParams = { @@ -521,16 +639,15 @@ describe('Recommend Api', () => { profiles: Array.from({ length: 100 }, (item, index) => { return { tag: index.toString(), - limit: 20, - filters: [ - { field: 'color', type: '=', values: ['blue'] }, - { field: 'price', type: '>=', values: [0] }, - { field: 'price', type: '<=', values: [20] }, - ], }; }), siteId: '8uyt2m', products: ['marnie-runner-2-7x10'], + filters: [ + { field: 'color', type: '=', values: ['blue'] }, + { field: 'price', type: '>=', values: [0] }, + { field: 'price', type: '<=', values: [20] }, + ], lastViewed: [ 'marnie-runner-2-7x10', 'ruby-runner-2-7x10', diff --git a/packages/snap-client/src/Client/apis/Recommend.ts b/packages/snap-client/src/Client/apis/Recommend.ts index d8070bdd4..475e4298b 100644 --- a/packages/snap-client/src/Client/apis/Recommend.ts +++ b/packages/snap-client/src/Client/apis/Recommend.ts @@ -1,5 +1,5 @@ import { API, ApiConfiguration } from './Abstract'; -import { HTTPHeaders, RecommendPostRequestFiltersModel, RecommendPostRequestProfileModel } from '../../types'; +import { HTTPHeaders, RecommendPostRequestProfileModel } from '../../types'; import { AppMode } from '@searchspring/snap-toolbox'; import { transformRecommendationFiltersPost } from '../transforms'; import { ProfileRequestModel, ProfileResponseModel, RecommendResponseModel, RecommendRequestModel, RecommendPostRequestModel } from '../../types'; @@ -57,7 +57,7 @@ export class RecommendAPI extends API { const batchId = parameters.batchId || 1; // set up batch key and deferred promises - const key = parameters.batched ? `${parameters.siteId}:${batchId}` : `${Math.random()}:${batchId}`; + const key = parameters.batched ? `${parameters.profile?.siteId || parameters.siteId}:${batchId}` : `${Math.random()}:${batchId}`; const batch = (this.batches[key] = this.batches[key] || { timeout: null, request: { profiles: [] }, entries: [] }); const deferred = new Deferred(); @@ -86,37 +86,59 @@ export class RecommendAPI extends API { } } - // parameters used for profile specific - const { tag, categories, brands, query, filters, dedupe } = entry.request; - - let transformedFilters; - if (filters) { - transformedFilters = transformRecommendationFiltersPost(filters) as RecommendPostRequestFiltersModel[]; - } - // build profile specific parameters - const profile: RecommendPostRequestProfileModel = { - tag, - categories, - brands, - limit: entry.request.limit || 20, - searchTerm: query, - filters: transformedFilters, - dedupe, - }; - - batch.request.profiles?.push(profile); + if (entry.request.profile) { + const { + tag, + profile: { categories, brands, blockedItems, limit, query, filters, dedupe }, + } = entry.request; + + const profile: RecommendPostRequestProfileModel = { + tag, + ...defined({ + categories, + brands, + blockedItems, + limit: limit, + searchTerm: query, + filters: transformRecommendationFiltersPost(filters), + dedupe, + }), + }; + + batch.request.profiles?.push(profile); + } else { + const { tag, categories, brands, limit, query, dedupe } = entry.request; + + const profile: RecommendPostRequestProfileModel = { + tag, + ...defined({ + categories, + brands, + limit: limit, + searchTerm: query, + dedupe, + }), + }; + + batch.request.profiles?.push(profile); + } // parameters used globally - const { siteId, products, blockedItems, test, cart, lastViewed, shopper } = entry.request; - // only when these parameters are defined should they be added to the list - if (siteId) batch.request.siteId = siteId; - if (products) batch.request.products = products; - if (blockedItems) batch.request.blockedItems = blockedItems; - if (test) batch.request.test = test; - if (cart) batch.request.cart = cart; - if (lastViewed) batch.request.lastViewed = lastViewed; - if (shopper) batch.request.shopper = shopper; + const { products, blockedItems, filters, test, cart, lastViewed, shopper } = entry.request; + batch.request = { + ...batch.request, + ...defined({ + siteId: entry.request.profile?.siteId || entry.request.siteId, + products, + blockedItems, + filters: transformRecommendationFiltersPost(filters), + test, + cart, + lastViewed, + shopper, + }), + }; }); try { @@ -182,3 +204,19 @@ function sortBatchEntries(a: BatchEntry, b: BatchEntry) { } return 0; } + +type DefinedProps = { + [key: string]: any; +}; + +export function defined(properties: Record): DefinedProps { + const definedProps: DefinedProps = {}; + + Object.keys(properties).map((key) => { + if (properties[key] !== undefined) { + definedProps[key] = properties[key]; + } + }); + + return definedProps; +} diff --git a/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts b/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts index 3b164690a..e00c77084 100644 --- a/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts +++ b/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts @@ -1,6 +1,8 @@ import { RecommendationRequestFilterModel, RecommendPostRequestFiltersModel } from '../../types'; -export const transformRecommendationFiltersPost = (filters: RecommendationRequestFilterModel[]) => { +export const transformRecommendationFiltersPost = (filters?: RecommendationRequestFilterModel[]) => { + if (!filters) return; + const filterArray: RecommendPostRequestFiltersModel[] = []; filters.map((filter) => { if (filter.type == 'value') { diff --git a/packages/snap-client/src/index.ts b/packages/snap-client/src/index.ts index 9837cffd0..3b0b47d9b 100644 --- a/packages/snap-client/src/index.ts +++ b/packages/snap-client/src/index.ts @@ -1,3 +1,10 @@ export * from './Client/Client'; -export { ClientGlobals, ClientConfig, TrendingResponseModel, RecommendRequestModel, RecommendCombinedResponseModel } from './types'; +export { + ClientGlobals, + ClientConfig, + TrendingResponseModel, + RecommendRequestModel, + RecommendationRequestFilterModel, + RecommendCombinedResponseModel, +} from './types'; diff --git a/packages/snap-client/src/types.ts b/packages/snap-client/src/types.ts index 5c1d50cae..9f399a223 100644 --- a/packages/snap-client/src/types.ts +++ b/packages/snap-client/src/types.ts @@ -102,24 +102,34 @@ export type TrendingResponseModel = { }; }; -export type RecommendRequestModel = { - tag: string; - siteId?: string; +export type RecommendRequestModel = RecommendRequestGlobalsModel & + RecommendRequestOptionsModel & { + tag: string; + profile?: RecommendRequestOptionsModel; + }; + +export type RecommendRequestGlobalsModel = { product?: string; products?: string[]; - shopper?: string; - categories?: string[]; - brands?: string[]; cart?: string[]; lastViewed?: string[]; + shopper?: string; + filters?: RecommendationRequestFilterModel[]; + blockedItems?: string[]; + batchId?: number; test?: boolean; +}; + +export type RecommendRequestOptionsModel = { + siteId?: string; + categories?: string[]; + brands?: string[]; branch?: string; filters?: RecommendationRequestFilterModel[]; blockedItems?: string[]; batched?: boolean; limit?: number; order?: number; - batchId?: number; query?: string; dedupe?: boolean; }; @@ -139,19 +149,15 @@ export type RecommendPostRequestModel = { filters?: RecommendPostRequestFiltersModel[]; }; -export type RecommendPostRequestProfileModel = Omit & { - filters?: RecommendPostRequestFiltersModel[]; - searchTerm?: string; -}; - -export type RecommendRequestProfileModel = { +export type RecommendPostRequestProfileModel = { tag: string; categories?: string[]; brands?: string[]; + blockedItems?: string[]; limit?: number; dedupe?: boolean; - query?: string; - filters?: RecommendationRequestFilterModel[]; + searchTerm?: string; + filters?: RecommendPostRequestFiltersModel[]; }; export type RecommendPostRequestFiltersModel = { From 70e2ef432ae9b9e0545a3b7ec1616c1450caecef Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 12 Sep 2024 10:53:16 -0600 Subject: [PATCH 5/6] refactor(preact/instantiator): updating instantiator with recommend changes --- .../RecommendationController.ts | 1 - packages/snap-preact-demo/public/product.html | 2 +- .../public/recommendations.html | 2 +- .../e2e/recommendation/recommendation.cy.js | 2 +- .../RecommendationInstantiator.test.tsx | 194 +++++++++++++----- .../RecommendationInstantiator.tsx | 106 ++++++---- 6 files changed, 211 insertions(+), 96 deletions(-) diff --git a/packages/snap-controller/src/Recommendation/RecommendationController.ts b/packages/snap-controller/src/Recommendation/RecommendationController.ts index db79a51e4..d57449223 100644 --- a/packages/snap-controller/src/Recommendation/RecommendationController.ts +++ b/packages/snap-controller/src/Recommendation/RecommendationController.ts @@ -399,7 +399,6 @@ export class RecommendationController extends AbstractController { tag: this.config.tag, batched: this.config.batched, branch: this.config.branch || 'production', - order: this.context?.options?.order, batchId: this.config.batchId, ...this.config.globals, }; diff --git a/packages/snap-preact-demo/public/product.html b/packages/snap-preact-demo/public/product.html index 82281aadc..bf1ee166f 100644 --- a/packages/snap-preact-demo/public/product.html +++ b/packages/snap-preact-demo/public/product.html @@ -180,7 +180,7 @@

Stripe Out Off-The-Shoulder Dress

profiles = [ { profile: 'similar', - target: '.ss__recs__similar' + selector: '.ss__recs__similar' }, ] diff --git a/packages/snap-preact-demo/public/recommendations.html b/packages/snap-preact-demo/public/recommendations.html index fcfc8ebd9..37c9985f9 100644 --- a/packages/snap-preact-demo/public/recommendations.html +++ b/packages/snap-preact-demo/public/recommendations.html @@ -180,7 +180,7 @@

Stripe Out Off-The-Shoulder Dress

profiles = [ { profile: 'similar', - target: '.ss__recs__similar' + selector: '.ss__recs__similar' }, ] diff --git a/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js b/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js index 1a5ce82f9..e5f1f60e6 100644 --- a/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js +++ b/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js @@ -66,7 +66,7 @@ describe('Recommendations', () => { describe('Tests Recommendations', () => { it('has a controller', function () { cy.snapController(integration?.selectors?.recommendation.controller).then(({ store }) => { - expect(store.config.globals.limit).equals(store.results.length); + expect(store.results.length).equals(20); // max limit when no limit specified expect((store.config.globals.product || store.config.globals.products).length).to.be.greaterThan(0); }); }); diff --git a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx index a4afa9d04..05228a2fe 100644 --- a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx +++ b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx @@ -342,28 +342,107 @@ describe('RecommendationInstantiator', () => { expect(clientSpy).toHaveBeenCalledTimes(1); expect(clientSpy).toHaveBeenCalledWith({ - batched: true, + tag: 'trending', + products: ['sku1'], + shopper: 'snapdev', branch: 'testing', - categories: ['cats', 'dogs'], + batched: true, + siteId: baseConfig.client?.globals.siteId, + profile: { + siteId: 'abc123', + branch: 'testing', + categories: ['cats', 'dogs'], + filters: [ + { + type: 'value', + field: 'color', + value: 'blue', + }, + { + type: 'range', + field: 'price', + value: { low: 0, high: 20 }, + }, + ], + brands: ['nike', 'h&m'], + limit: 5, + }, + }); + }); + + it('uses the globals from the config in the request', async () => { + document.body.innerHTML = ``; + + const client = new MockClient(baseConfig.client!.globals, {}); + const clientSpy = jest.spyOn(client, 'recommend'); + + const globalConfig = { + ...baseConfig, + client: { + globals: { + siteId: '8uyt2m', + filters: [ + { + type: 'value', + field: 'color', + value: 'red', + }, + ], + }, + }, + }; + + const recommendationInstantiator = new RecommendationInstantiator(globalConfig, { client }); + await wait(); + expect(Object.keys(recommendationInstantiator.controller).length).toBe(1); + Object.keys(recommendationInstantiator.controller).forEach((controllerId) => { + const controller = recommendationInstantiator.controller[controllerId]; + expect(controller.context).toStrictEqual({ + profile: 'trending', + options: { + filters: [ + { + type: 'value', + field: 'color', + value: 'blue', + }, + ], + }, + }); + }); + + expect(clientSpy).toHaveBeenCalledTimes(1); + expect(clientSpy).toHaveBeenCalledWith({ + tag: 'trending', + branch: 'production', + batched: true, + siteId: baseConfig.client?.globals.siteId, filters: [ { type: 'value', field: 'color', - value: 'blue', - }, - { - type: 'range', - field: 'price', - value: { low: 0, high: 20 }, + value: 'red', }, ], - batchId: 1, - brands: ['nike', 'h&m'], - limit: 5, - products: ['sku1'], - shopper: 'snapdev', - siteId: 'abc123', - tag: 'trending', + profile: { + filters: [ + { + type: 'value', + field: 'color', + value: 'blue', + }, + ], + }, }); }); @@ -371,10 +450,10 @@ describe('RecommendationInstantiator', () => { const profileContextArray = [ { profile: 'trending', - target: '#tout1', + selector: '#tout1', custom: { some: 'thing1' }, options: { - siteId: '8uyt2m', + siteId: 'abc123', limit: 1, categories: ['1234'], brands: ['12345'], @@ -392,7 +471,7 @@ describe('RecommendationInstantiator', () => { }, { profile: 'similar', - target: '#tout2', + selector: '#tout2', custom: { some: 'thing2' }, options: { limit: 2, @@ -427,10 +506,10 @@ describe('RecommendationInstantiator', () => { profiles = [ { profile: 'trending', - target: '#tout1', + selector: '#tout1', custom: { some: 'thing1' }, options: { - siteId: '8uyt2m', + siteId: 'abc123', limit: 1, categories: ["1234"], brands: ["12345"], @@ -446,7 +525,7 @@ describe('RecommendationInstantiator', () => { }, { profile: 'similar', - target: '#tout2', + selector: '#tout2', custom: { some: 'thing2' }, options: { limit: 2, @@ -486,50 +565,55 @@ describe('RecommendationInstantiator', () => { expect(clientSpy).toHaveBeenCalledTimes(2); expect(clientSpy).toHaveBeenNthCalledWith(1, { - batched: true, - blockedItems: ['1234', '5678'], - branch: 'production', - brands: ['12345'], - cart: ['5678'], - categories: ['1234'], - limit: 1, - filters: [ - { - field: 'price', - type: 'range', - value: { - low: 20, - high: 40, - }, - }, - ], + tag: 'trending', products: ['C-AD-W1-1869P'], + cart: ['5678'], + blockedItems: ['1234', '5678'], shopper: 'snapdev', batchId, - siteId: '8uyt2m', - tag: 'trending', + siteId: baseConfig.client?.globals.siteId, + branch: 'production', + batched: true, + profile: { + brands: ['12345'], + categories: ['1234'], + limit: 1, + siteId: 'abc123', + filters: [ + { + field: 'price', + type: 'range', + value: { + low: 20, + high: 40, + }, + }, + ], + }, }); expect(clientSpy).toHaveBeenNthCalledWith(2, { + tag: 'similar', + products: ['C-AD-W1-1869P'], + shopper: 'snapdev', + batchId, + siteId: baseConfig.client?.globals.siteId, batched: true, blockedItems: ['1234', '5678'], branch: 'production', - brands: ['65432'], cart: ['5678'], - categories: ['5678'], - limit: 2, - filters: [ - { - field: 'color', - type: 'value', - value: 'blue', - }, - ], - products: ['C-AD-W1-1869P'], - shopper: 'snapdev', - batchId, - siteId: undefined, - tag: 'similar', + profile: { + limit: 2, + brands: ['65432'], + categories: ['5678'], + filters: [ + { + field: 'color', + type: 'value', + value: 'blue', + }, + ], + }, }); }); diff --git a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx index 9bcb801ef..606576c8c 100644 --- a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx +++ b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx @@ -6,15 +6,9 @@ import { Client } from '@searchspring/snap-client'; import { Logger } from '@searchspring/snap-logger'; import { Tracker } from '@searchspring/snap-tracker'; -import type { ClientConfig, ClientGlobals, RecommendRequestModel } from '@searchspring/snap-client'; +import type { ClientConfig, ClientGlobals, RecommendRequestModel, RecommendationRequestFilterModel } from '@searchspring/snap-client'; import type { UrlTranslatorConfig } from '@searchspring/snap-url-manager'; -import type { - AbstractController, - RecommendationController, - Attachments, - ContextVariables, - RecommendationControllerConfig, -} from '@searchspring/snap-controller'; +import type { AbstractController, RecommendationController, Attachments, ContextVariables } from '@searchspring/snap-controller'; import type { VariantConfig } from '@searchspring/snap-store-mobx'; import type { Middleware } from '@searchspring/snap-event-manager'; import type { Target } from '@searchspring/snap-toolbox'; @@ -56,15 +50,15 @@ type ProfileSpecificProfile = { realtime?: boolean; }; profile: string; - target: string; + selector: string; }; type ProfileSpecificGlobals = { + filters?: RecommendationRequestFilterModel[]; blockedItems: string[]; cart?: string[] | (() => string[]); products?: string[]; shopper?: { id?: string }; - siteId?: string; }; type ExtendedRecommendaitonProfileTarget = Target & { @@ -152,12 +146,26 @@ export class RecommendationInstantiator { ], async (target: Target, elem: Element | undefined, originalElem: Element | undefined) => { const elemContext = getContext( - ['shopperId', 'shopper', 'product', 'products', 'seed', 'cart', 'options', 'profile', 'custom', 'profiles', 'globals'], + [ + 'shopperId', + 'shopper', + 'product', + 'products', + 'seed', + 'cart', + 'filters', + 'blockedItems', + 'options', + 'profile', + 'custom', + 'profiles', + 'globals', + ], (originalElem || elem) as HTMLScriptElement ); if (elemContext.profiles && elemContext.profiles.length) { - // using the new script integration structure + // using the "grouped block" integration structure // type the new profile specific integration context variables const scriptContextProfiles = elemContext.profiles as ProfileSpecificProfile[]; @@ -165,21 +173,23 @@ export class RecommendationInstantiator { // grab from globals const requestGlobals: Partial = { - blockedItems: scriptContextGlobals.blockedItems, - cart: scriptContextGlobals.cart && getArrayFunc(scriptContextGlobals.cart), - products: scriptContextGlobals.products, - shopper: scriptContextGlobals.shopper?.id, - siteId: scriptContextGlobals.siteId, - batchId: Math.random(), + ...defined({ + blockedItems: scriptContextGlobals.blockedItems, + filters: scriptContextGlobals.filters, + cart: scriptContextGlobals.cart && getArrayFunc(scriptContextGlobals.cart), + products: scriptContextGlobals.products, + shopper: scriptContextGlobals.shopper?.id, + batchId: Math.random(), + }), }; const targetsArr: ExtendedRecommendaitonProfileTarget[] = []; // build out the targets array for each profile scriptContextProfiles.forEach((profile) => { - if (profile.target) { + if (profile.selector) { const targetObj = { - selector: profile.target, + selector: profile.selector, autoRetarget: true, clickRetarget: true, profile, @@ -193,7 +203,11 @@ export class RecommendationInstantiator { targetsArr, async (target: ExtendedRecommendaitonProfileTarget, elem: Element | undefined, originalElem: Element | undefined) => { if (target.profile?.profile) { - const profileRequestGlobals: RecommendRequestModel = { ...requestGlobals, ...target.profile?.options, tag: target.profile.profile }; + const profileRequestGlobals: RecommendRequestModel = { + ...requestGlobals, + profile: target.profile?.options, + tag: target.profile.profile, + }; const profileContext: ContextVariables = deepmerge(this.context, { globals: scriptContextGlobals, profile: target.profile }); if (elemContext.custom) { profileContext.custom = elemContext.custom; @@ -204,17 +218,19 @@ export class RecommendationInstantiator { } ); } else { - // using the "legacy" method - const { profile, products, product, seed, options, batched, shopper, shopperId } = elemContext; + // using the "legacy" integration structure + const { profile, products, product, seed, filters, blockedItems, options, shopper, shopperId } = elemContext; const profileRequestGlobals: Partial = { tag: profile, - batched: batched ?? true, - batchId: 1, - products: products || (product && [product]) || (seed && [seed]), - cart: elemContext.cart && getArrayFunc(elemContext.cart), - shopper: shopper?.id || shopperId, - ...options, + ...defined({ + products: products || (product && [product]) || (seed && [seed]), + cart: elemContext.cart && getArrayFunc(elemContext.cart), + shopper: shopper?.id || shopperId, + filters, + blockedItems, + profile: options, + }), }; readyTheController(this, elem, elemContext, profileCount, originalElem, profileRequestGlobals); @@ -242,9 +258,10 @@ async function readyTheController( context: ContextVariables, profileCount: RecommendationProfileCounts, elem: Element | undefined, - controllerGlobals: Partial + controllerGlobals: Partial ) { - const { batched, batchId, realtime, cart, tag } = controllerGlobals; + const { profile, batchId, cart, tag } = controllerGlobals; + const batched = (profile?.batched || controllerGlobals.batched) ?? true; if (!tag) { // FEEDBACK: change message depending on script integration type (profile vs. legacy) @@ -258,12 +275,7 @@ async function readyTheController( profileCount[tag] = profileCount[tag] + 1 || 1; - const defaultGlobals: Partial = { - limit: 20, - }; - const globals: Partial = deepmerge.all([ - defaultGlobals, instance.config.client?.globals || {}, instance.config.config?.globals || {}, controllerGlobals, @@ -273,12 +285,16 @@ async function readyTheController( id: `recommend_${tag}_${profileCount[tag] - 1}`, tag, batched: batched ?? true, - realtime: Boolean(realtime), + realtime: Boolean(context.options?.realtime ?? context.profile?.options?.realtime), batchId: batchId, ...instance.config.config, globals, }; + if (profile?.branch) { + controllerConfig.branch = profile?.branch; + } + // try to find an existing controller by similar configuration let controller = Object.keys(instance.controller) .map((id) => instance.controller[id]) @@ -390,3 +406,19 @@ function getArrayFunc(arrayOrFunc: string[] | (() => string[])): string[] { return []; } + +type DefinedProps = { + [key: string]: any; +}; + +export function defined(properties: Record): DefinedProps { + const definedProps: DefinedProps = {}; + + Object.keys(properties).map((key) => { + if (properties[key] !== undefined) { + definedProps[key] = properties[key]; + } + }); + + return definedProps; +} From df0dc3259e36dbfb7929b223ff909b550f4f3ab9 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 12 Sep 2024 10:53:43 -0600 Subject: [PATCH 6/6] docs(docs): updating documentation for recommendation integration changes --- docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md | 74 +++++++++++++++++----- docs/INTEGRATION_RECOMMENDATIONS.md | 23 +++---- 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md b/docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md index a546d171f..37cf46271 100644 --- a/docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md +++ b/docs/INTEGRATION_LEGACY_RECOMMENDATIONS.md @@ -11,17 +11,20 @@ It is recommended to utilize the [`RecommendationInstantiator`](https://github.c ``` -The `RecommendationInstantiator` will look for these elements on the page and attempt to inject components based on the `profile` specified. In the example above, the profile specified is the `recently-viewed` profile, and would typically be setup to display the last products viewed by the shopper. These profiles must be setup in the Searchspring Management Console (SMC) and have associated Snap templates selected. +The `RecommendationInstantiator` will look for these elements on the page and attempt to inject components based on the `profile` specified in the script attribute. In the example above, the profile specified is the `recently-viewed` profile, and would typically be setup to display the last products viewed by the shopper. These profiles must be setup in the Searchspring Management Console (SMC). ## Recommendation Context Variables -Context variables may be applied to individual recommendation profiles similar to how they are done on the integration script tag. Variables here may be required depending on the profile type utilized, and can be used to alter the results displayed by our recommendations. +Profile configurations are applied to recommendation via script context variables. The variables here may be required depending on the profile type utilized, and can be used to alter the results displayed by our recommendations. When multiple recommendation integration script blocks are found, a batch will be created by default, and any profile configurations NOT in the `options` variable are applied globally to all profiles. | Option | Value | Page | Description | |---|---|:---:|---| -| products | array of SKU strings | product detail page | SKU value(s) to identify the current product(s) being viewed | -| cart | array (or function that returns an array) of current cart skus | all | optional method of setting cart contents | -| options.siteId | global siteId overwrite | all | optional global siteId overwrite | +| products | array of SKU strings | product detail page | SKU value(s) to identify the current product(s) being viewed (global) | +| cart | array (or function that returns an array) of current cart skus | all | optional method of setting cart contents (global) | +| blockedItems | array of strings | all | SKU values to identify which products to exclude from the response (global) | +| filters | array of filters | all | optional recommendation filters (global) | +| shopper.id | logged in user unique identifier | all | required for personalization functionallity if not provided to the bundle context (global) | +| options.siteId | siteId overwrite | all | optional siteId overwrite (will force a new batch) | | options.categories | array of category path strings | all | optional category identifiers used in category trending recommendation profiles | | options.brands | array of brand strings | all | optional brand identifiers used in brand trending recommendation profiles | | options.branch | template branch overwrite | all | optional branch overwrite for recommendations template (advanced usage) | @@ -30,16 +33,16 @@ Context variables may be applied to individual recommendation profiles similar t | options.realtime | boolean | all | optional update recommendations if cart contents change (requires [cart attribute tracking](https://github.com/searchspring/snap/blob/main/docs/INTEGRATION_TRACKING.md)) | | options.blockedItems | array of strings | all | SKU values to identify which products to exclude from the response | | options.batched | boolean (default: `true`)| all | only applies to recommendation context, optional disable profile from being batched in a single request, can also be set globally [via config](https://github.com/searchspring/snap/tree/main/packages/snap-controller/src/Recommendation) | +| options.dedupe | boolean (default: `true`) | all | specify wether or not the profile should deduplicate products when in a batch | | options.order | number | all | optional order number for recommendation params to be added to the batched request. Profiles that do not specify an order will be placed at the end, in the occurrence they appear in the DOM. | options.limit | number (default: 20, max: 20) | all | optional maximum number of results to display, can also be set globally [via config globals](https://github.com/searchspring/snap/tree/main/packages/snap-controller/src/Recommendation) | -| shopper.id | logged in user unique identifier | all | required for personalization functionallity if not provided to the bundle (global) context | ## Batching and Ordering -By default, recommendation profile results are fetched in the same API request (batch), this is done in an effort to prevent the display of duplicate products across multiple profiles. The order of the profiles in the DOM determines the priority of results for de-duplication (best recommendations). If you wish to change the order, an `order` value can be provided (lowest value has highest priority). For some profiles (like product bundles) it is important that they receive the best suggested products prior to de-duplication, for these, the `order` would be set manually so that de-duplication does not occur. +By default, recommendation profile results are fetched in the same API request (batch), this is done in an effort to prevent the display of duplicate products across multiple profiles. The order of the profiles in the DOM determines the priority of results for de-duplication (best recommendations). If you wish to change the order, an `order` value can be provided (lowest value has highest priority). For some profiles (like product bundles) it is important that they receive the best suggested products prior to de-duplication, for these, the `order` should be set manually so that de-duplication does not occur. -In most cases batching is the best practice, however for profiles like a mini cart (side cart) de-duplication may not be desired. Batching can be turned off per profile with a `batched: false` value. +In most cases batching is the best practice, however for profiles like a mini cart (side cart) de-duplication may not be desired. Using `dedupe` would allow for opting out of deduplication for that profile in the batch. -The example below shows how to manually specify the order and batching of specific profiles. +The example below shows how to manually specify the order of the profiles and how to dedupe them. In the example the 'bundle' profile in the batch receives the best suggestions because it has the lowest order, and the 'quick-cart' profile is not deduplicating products at all. ```html + +``` + +Alternatively, a profile can be placed in it's own batch via the `batched: false` value. The example below shows how to place the 'quick-cart' profile into it's own batch. + +```html + + ``` @@ -87,14 +105,14 @@ If tracking scripts are not in place, "also bought" profiles may require the car ```html ``` If the shopper identifier is not beeing captured by the `bundle.js` context, it must be provided for proper personalization. ```html - +``` + +The next example shows a global filter being used, this will filter all of the profiles in the batch for products matching the field `onSale` with a value `true`; the 'similar' profile will additionally apply a filter using the field `price` with a range from `0` to `20`. + +```html + + + ``` \ No newline at end of file diff --git a/docs/INTEGRATION_RECOMMENDATIONS.md b/docs/INTEGRATION_RECOMMENDATIONS.md index b2ead6db2..0c3d5668c 100644 --- a/docs/INTEGRATION_RECOMMENDATIONS.md +++ b/docs/INTEGRATION_RECOMMENDATIONS.md @@ -14,7 +14,7 @@ It is recommended to utilize the [`RecommendationInstantiator`](https://github.c profiles = [ { profile: 'recently-viewed', - target: '.ss__recs__recently-viewed', + selector: '.ss__recs__recently-viewed', options: { limit: 5 } @@ -23,7 +23,7 @@ It is recommended to utilize the [`RecommendationInstantiator`](https://github.c ``` -The `RecommendationInstantiator` will look for these elements on the page and attempt to inject components based on the `profiles` specified. In the example above, the profile specified is the `recently-viewed` profile, and is set to render inside the target `.ss__recs__recently-viewed`, this profile would typically be setup to display the last products viewed by the shopper. These profiles must be setup in the Searchspring Management Console (SMC) and have associated Snap templates selected. +The `RecommendationInstantiator` will look for these elements on the page and attempt to inject components based on the `profiles` specified. In the example above, the profile specified is the `recently-viewed` profile, and is set to render inside the selector `.ss__recs__recently-viewed`, this profile would typically be setup to display the last products viewed by the shopper. These profiles must be setup in the Searchspring Management Console (SMC) and have associated Snap templates selected. ## Recommendation Context Variables @@ -34,6 +34,7 @@ Context variables are applied to individual recommendation profiles similar to h |---|---|:---:|---|:---:| | products | array of SKU strings | product detail page | SKU value(s) to identify the current product(s) being viewed | ✔️ | | blockedItems | array of strings | all | SKU values to identify which products to exclude from the response | | +| filters | array of filters | all | optional recommendation filters to apply to ALL profiles in the batch | | | cart | array (or function that returns an array) of current cart skus | all | optional method of setting cart contents | | | shopper.id | logged in user unique identifier | all | required for personalization functionallity if not provided to the bundle (global) context | | @@ -42,7 +43,7 @@ Context variables are applied to individual recommendation profiles similar to h | Option | Value | Placement | Description | Required |---|---|:---:|---|:---:| | profile | string | all | profile name to use | ✔️ | -| target | string | all | CSS selector to render component inside | ✔️ | +| selector | string | all | CSS selector to render component inside | ✔️ | | options.siteId | global siteId overwrite | all | optional global siteId overwrite | | | options.categories | array of category path strings | all | optional category identifiers used in category trending recommendation profiles | | | options.brands | array of brand strings | all | optional brand identifiers used in brand trending recommendation profiles | | @@ -55,7 +56,7 @@ Context variables are applied to individual recommendation profiles similar to h ## Batching and Ordering -Each "searchspring/recommendations" script block groups multiple recommendation profiles into a single API request, known as a batch. By default, the script tag fetches recommendations for all profiles with a matching target in one batched request. The order of profiles in the array determines their priority within the batch. +Each "searchspring/recommendations" script block groups multiple recommendation profiles into a single API request, known as a batch. By default, the script tag fetches recommendations for all profiles with a matching selector in one batched request. The order of profiles in the array determines their priority within the batch. While batching all profiles together is generally the most efficient approach, there may be cases where separate batching is preferred. For instance, recommendations for a mini cart (side cart) might not require de-duplication with other recommendations. You can disable de-duplication for a specific profile by setting `dedupe: false` in its options, or create a separate batch by using an additional script tag. @@ -83,19 +84,19 @@ Here's an example that demonstrates deduping: profiles = [ { profile: 'customers-also-bought', - target: '.ss__recs__crosssell', + selector: '.ss__recs__crosssell', options: { limit: 5 } }, { profile: 'customers-also-viewed', - target: '.ss__recs__similar' + selector: '.ss__recs__similar' }, // same batch, but dedupe false { profile: 'customers-also-like', - target: '.ss__recs__alsoliked', + selector: '.ss__recs__alsoliked', options: { dedupe: false } @@ -118,7 +119,7 @@ A typical "similar" profile that would display products similar to the product p profiles = [ { profile: 'customers-also-viewed', - target: '.ss__recs__similar' + selector: '.ss__recs__similar' } ]; @@ -134,7 +135,7 @@ If tracking scripts are not in place, "crosssell" profiles may require the cart profiles = [ { profile: 'customers-also-bought', - target: '.ss__recs__crosssell' + selector: '.ss__recs__crosssell' } ]; @@ -152,7 +153,7 @@ If the shopper identifier is not beeing captured by the `bundle.js` context, it profiles = [ { profile: 'view-cart', - target: '.ss__recs__cart' + selector: '.ss__recs__cart' } ]; @@ -166,7 +167,7 @@ The example shown below will filter the recommendations for products matching fi profiles = [ { profile: 'customers-also-bought', - target: '.ss__recs__crosssell', + selector: '.ss__recs__crosssell', options: { filters: [ {