Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { FilterBuilder } from '../../filterBuilder';
import { FeedComposition, Int32Range } from '../../../models/data-contracts';
import { RelevanceModifierBuilder } from '../../relevanceModifierBuilder';

export type FeedCompositionOptions = {
type: 'Product' | 'Content';
count: Int32Range;
name?: string;
};

export class FeedCompositionBuilder {
private composition: FeedComposition;

constructor({ type, count, name }: FeedCompositionOptions) {
this.composition = {
includeEmptyResults: false,
type,
count,
name
};
}

public includeEmptyResults(allow: boolean = true): this {
this.composition.includeEmptyResults = allow;

return this;
}

public rotationLimit(limit: number | null): this {
this.composition.rotationLimit = limit;

return this;
}

public name(name: string | null): this {
this.composition.name = name;

return this;
}

public count(count: Int32Range): this {
this.composition.count = count;

return this;
}

public fill(options: FeedCompositionOptions, fill: (fillBuilder: FeedCompositionBuilder) => void): this {
const builder = new FeedCompositionBuilder(options);
fill(builder);
this.composition.fill = builder.build();

return this;
}

public filters(filterBuilder: (builder: FilterBuilder) => void): this {
const builder = new FilterBuilder();
filterBuilder(builder);

this.composition.filters = builder.build();

return this;
}

public relevanceModifiers(relevanceModifiersBuilder: (builder: RelevanceModifierBuilder) => void): this {
const builder = new RelevanceModifierBuilder();
relevanceModifiersBuilder(builder);

this.composition.relevanceModifiers = builder.build();

return this;
}

public build() {
return this.composition;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Settings } from '../../../builders/settings';
import { Feed, FeedComposition, FeedRecommendationInitializationRequest, FeedSeed, SelectedContentPropertiesSettings, SelectedProductPropertiesSettings, SelectedVariantPropertiesSettings } from '../../../models/data-contracts';
import { RecommendationRequestBuilder } from '../recommendationRequestBuilder';
import { FeedCompositionBuilder, FeedCompositionOptions } from './feedCompositionBuilder';

export class FeedRecommendationInitializationBuilder extends RecommendationRequestBuilder {
private feed: Feed;

constructor(settings: Settings, { minimumPageSize }: { minimumPageSize: number }) {
super(settings);

this.feed = {
minimumPageSize: minimumPageSize,
compositions: [],
allowProductsCurrentlyInCart: false,
recommendVariant: false
};
}

/**
* Useful, for example, when you want to show a feed based on specific product(s) or content, such as a PDP/CDP, a shopping cart, or an order.
* @param seed
*/
public seed(seed: FeedSeed): this {
this.feed.seed = seed;

return this;
}

/**
* Adds a composition to the feed. Defines how the feed will be composed, which types of entities to include, how many of each type, and any filters or relevance modifiers that should apply to each type, etc.
* @param options
* @param builderFn
*/
public addCompostion({ options, settingsBuilder }: { options: FeedCompositionOptions, settingsBuilder?: (feedBuilder: FeedCompositionBuilder) => void }): this {
this.feed.compositions ??= [];

const builder = new FeedCompositionBuilder(options);
if (settingsBuilder) {
settingsBuilder(builder);
}

this.feed.compositions.push(builder.build());

return this;
}

/**
* Select the properties of the product to be returned, by default only the product id is returned.
* @param productProperties
*/
public setSelectedProductProperties(productProperties: Partial<SelectedProductPropertiesSettings> | null): this {
this.feed.selectedProductProperties = productProperties as SelectedProductPropertiesSettings | null;

return this;
}

/**
* Select the properties of the variant to be returned, by default only the variant id is returned.
* @param variantProperties
*/
public setSelectedVariantProperties(variantProperties: Partial<SelectedVariantPropertiesSettings> | null): this {
this.feed.selectedVariantProperties = variantProperties as SelectedVariantPropertiesSettings | null;

return this;
}

/**
* Select the properties of the content to be returned, by default only the content id is returned.
* @param contentProperties
*/
public setSelectedContentProperties(contentProperties: Partial<SelectedContentPropertiesSettings> | null): this {
this.feed.selectedContentProperties = contentProperties as SelectedContentPropertiesSettings | null;

return this;
}

/**
* Defines if variants should be included for returned products
* @param recommend
*/
public recommendVariant(recommend: boolean = true): this {
this.feed.recommendVariant = recommend;

return this;
}

/**
* The minimum number of items to return initially and per every FeedRecommendationNextItemsRequest.
* A higher number of results may be returned if composition configurations dictate so.
* @param size
*/
public minimumPageSize(size: number): this {
this.feed.minimumPageSize = size;

return this;
}

/**
* Defines if products should be excluded if they are currently present in the users Cart
* @param allow
*/
public allowProductsCurrentlyInCart(allow: boolean = true): this {
this.feed.allowProductsCurrentlyInCart = allow;

return this;
}

public build() {
const request: FeedRecommendationInitializationRequest = {
$type: 'Relewise.Client.Requests.Recommendations.Feed.FeedRecommendationInitializationRequest, Relewise.Client',
...this.baseBuild(),
feed: this.feed,
};

return request;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { FeedRecommendationNextItemsRequest } from '../../../models/data-contracts';

export class FeedRecommendationNextItemsBuilder {
private initializedFeedId: string;

constructor({ initializedFeedId }: { initializedFeedId: string }) {
this.initializedFeedId = initializedFeedId;
}

public build() {
const request: FeedRecommendationNextItemsRequest = {
$type: 'Relewise.Client.Requests.Recommendations.Feed.FeedRecommendationNextItemsRequest, Relewise.Client',
initializedFeedId: this.initializedFeedId,
};

return request;
}
}
3 changes: 3 additions & 0 deletions packages/client/src/builders/recommendation/feed/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './feedCompositionBuilder';
export * from './feedRecommendationInitializationBuilder';
export * from './feedRecommendationNextItemsBuilder';
3 changes: 2 additions & 1 deletion packages/client/src/builders/recommendation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './brands';
export * from './product-categories';
export * from './products';
export * from './recommendationRequestBuilder';
export * from './search-terms';
export * from './search-terms';
export * from './feed';
13 changes: 12 additions & 1 deletion packages/client/src/recommender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ import {
ProductsViewedAfterViewingContentRequest,
PopularProductsRequest,
PersonalProductRecommendationRequest,
FeedRecommendationInitializationRequest,
FeedRecommendationResponse,
FeedRecommendationNextItemsRequest,
} from './models/data-contracts';

export class Recommender extends RelewiseClient {
Expand Down Expand Up @@ -161,7 +164,16 @@ export class Recommender extends RelewiseClient {
public async recommendContentsViewedAfterViewingContent(request: ContentsViewedAfterViewingContentRequest, options?: RelewiseRequestOptions): Promise<ContentRecommendationResponse | undefined> {
return this.request<ContentsViewedAfterViewingContentRequest, ContentRecommendationResponse>('ContentsViewedAfterViewingContentRequest', request, options);
}
//#endregion

//#region feed
public async recommendFeedInitialization(request: FeedRecommendationInitializationRequest, options?: RelewiseRequestOptions): Promise<FeedRecommendationResponse | undefined> {
return this.request<FeedRecommendationInitializationRequest, FeedRecommendationResponse>('FeedRecommendationInitializationRequest', request, options);
}

public async recommendFeedNextItems(request: FeedRecommendationNextItemsRequest, options?: RelewiseRequestOptions): Promise<FeedRecommendationResponse | undefined> {
return this.request<FeedRecommendationNextItemsRequest, FeedRecommendationResponse>('FeedRecommendationNextItemsRequest', request, options);
}
//#endregion

//#region Batching
Expand All @@ -180,6 +192,5 @@ export class Recommender extends RelewiseClient {
public async batchProductCategoryRecommendations(request: ProductCategoryRecommendationRequestCollection, options?: RelewiseRequestOptions): Promise<ProductCategoryRecommendationResponseCollection | undefined> {
return this.request<ProductCategoryRecommendationRequestCollection, ProductCategoryRecommendationResponseCollection>('ProductCategoryRecommendationRequestCollection', request, options);
}

//#endregion
}
43 changes: 42 additions & 1 deletion packages/client/src/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
TrackContentEngagementRequest,
TrackProductEngagementRequest,
TrackDisplayAdClickRequest,
TrackFeedDwellRequest,
TrackFeedItemClickRequest,
TrackFeedItemPreviewRequest,
FeedItem,
} from './models/data-contracts';

export class Tracker extends RelewiseClient {
Expand Down Expand Up @@ -198,4 +202,41 @@ export class Tracker extends RelewiseClient {
},
}, options);
}
}

public async trackFeedDwell({ user, feedId, dwellTimeMilliseconds, visibleItems }: { user: User; feedId: string, dwellTimeMilliseconds: number, visibleItems: FeedItem[] }, options?: RelewiseRequestOptions) {
return this.request<TrackFeedDwellRequest, void>('TrackFeedDwellRequest', {
$type: 'Relewise.Client.Requests.Tracking.Feed.TrackFeedDwellRequest, Relewise.Client',
dwell: {
$type: 'Relewise.Client.DataTypes.Feed.FeedDwell, Relewise.Client',
user: user,
feedId: feedId,
dwellTimeMilliseconds: dwellTimeMilliseconds,
visibleItems: visibleItems,
}
}, options);
}

public async trackFeedItemClick({ user, feedId, item }: { user: User; feedId: string; item?: FeedItem }, options?: RelewiseRequestOptions) {
return this.request<TrackFeedItemClickRequest, void>('TrackFeedItemClickRequest', {
$type: 'Relewise.Client.Requests.Tracking.Feed.TrackFeedItemClickRequest, Relewise.Client',
click: {
$type: 'Relewise.Client.DataTypes.Feed.FeedItemClick, Relewise.Client',
user: user,
feedId: feedId,
item: item,
}
}, options);
}

public async trackFeedItemPreview({ user, feedId, item }: { user: User; feedId: string; item?: FeedItem }, options?: RelewiseRequestOptions) {
return this.request<TrackFeedItemPreviewRequest, void>('TrackFeedItemPreviewRequest', {
$type: 'Relewise.Client.Requests.Tracking.Feed.TrackFeedItemPreviewRequest, Relewise.Client',
preview: {
$type: 'Relewise.Client.DataTypes.Feed.FeedItemPreview, Relewise.Client',
user: user,
feedId: feedId,
item: item,
}
}, options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const settings = {
user: UserFactory.anonymous(),
};

test('Batched Content Recommendations', async() => {
test('Batched Content Recommendations', async () => {

const request: ContentRecommendationRequestCollection = new ContentsRecommendationCollectionBuilder()
.addRequest(new PopularContentsBuilder(settings).sinceMinutesAgo(5000).setNumberOfRecommendations(1).build())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FeedRecommendationInitializationBuilder, Recommender, UserFactory } from '../../src';
import { test, expect } from '@jest/globals'

const { npm_config_API_KEY: API_KEY, npm_config_DATASET_ID: DATASET_ID, npm_config_SERVER_URL: SERVER_URL } = process.env;

const recommender = new Recommender(DATASET_ID!, API_KEY!, { serverUrl: SERVER_URL });

const settings = {
language: 'en-US',
currency: 'USD',
displayedAtLocation: 'Integration test',
user: UserFactory.anonymous(),
};

test('Feed Recommendation', async () => {

const request = new FeedRecommendationInitializationBuilder(settings, { minimumPageSize: 10 })
.addCompostion({
options: {
type: 'Product',
count: { lowerBoundInclusive: 1, upperBoundInclusive: 2 }
},
settingsBuilder: (builder) => {
builder.rotationLimit(5)
}
});

const result = await recommender.recommendFeedInitialization(request.build());

expect(result).not.toBe(undefined);
});
Loading