Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6c426a8
feat(core): add RecommendationShelf section with personalization trac…
thiagopereira-vtex Jun 23, 2026
5f3999b
fix(core): add recommendation/personalization GraphQL typeDefs
thiagopereira-vtex Jun 23, 2026
1f92187
test(core): include recommendation operations in schema assertions
thiagopereira-vtex Jun 23, 2026
961112a
fix(api): expose recommendation and personalization operations
thiagopereira-vtex Jun 23, 2026
c0495e5
refactor(core): clean up RecommendationShelf code smells
thiagopereira-vtex Jun 23, 2026
1108aa3
test(core): cover RecommendationShelf and useStartSession
thiagopereira-vtex Jun 23, 2026
4caead7
refactor(core): compare globalThis.window with undefined directly
thiagopereira-vtex Jun 23, 2026
70e3b83
Update packages/core/cms/faststore/components/cms_component__Recommen…
thiagopereira-vtex Jun 23, 2026
ec92aef
fix(api): use private cache scope for personalized recommendations
thiagopereira-vtex Jun 23, 2026
e369181
fix(core): harden RecommendationShelf per review feedback
thiagopereira-vtex Jun 23, 2026
fab2acc
refactor(core): move analytics utils out of RecommendationShelf into SDK
thiagopereira-vtex Jun 23, 2026
91e14d0
refactor(core): reuse SDK GraphQL layer in RecommendationShelf
thiagopereira-vtex Jun 23, 2026
402816e
feat(core): make RecommendationShelf carousel and product card config…
thiagopereira-vtex Jun 24, 2026
4658ec0
refactor(core): remove SendProductViewEvent mutation and related logic
thiagopereira-vtex Jun 24, 2026
814da62
refactor(core): simplify RecommendationShelf per quality review
thiagopereira-vtex Jun 24, 2026
4c934e8
refactor(api): remove SendProductViewEvent mutation and related types
thiagopereira-vtex Jun 26, 2026
f3ef796
refactor(api, core): rename startSession to startRecommendationSessio…
thiagopereira-vtex Jun 26, 2026
1ea6afa
feat(api, core): return StoreProduct from recommendations for consist…
thiagopereira-vtex Jun 26, 2026
3a52956
refactor(api): rename StartSessionResult to StartRecommendationSessio…
thiagopereira-vtex Jun 26, 2026
7b4e2d2
feat(core): enhance RecommendationShelf with configurable items conte…
thiagopereira-vtex Jun 29, 2026
6ea9e5f
Update packages/api/src/platforms/vtex/clients/recommendation/index.ts
thiagopereira-vtex Jul 1, 2026
060d0a0
feat(api, core): implement server-side validation for recommendations…
thiagopereira-vtex Jul 2, 2026
3214947
feat(core): add RecommendationShelf component for personalized produc…
thiagopereira-vtex Jul 2, 2026
acb4bf4
refactor(RecommendationShelf): update prop type and improve empty sta…
thiagopereira-vtex Jul 3, 2026
daa57c5
refactor(api, core): address recommendation shelf review feedback
thiagopereira-vtex Jul 6, 2026
9de413f
test(api, core): cover recommendation resolvers/client and fix Sonar …
thiagopereira-vtex Jul 6, 2026
a43bf23
refactor(core): use Boolean directly as the default retry predicate
thiagopereira-vtex Jul 6, 2026
d89732b
Merge branch 'dev' into feat/recommendation-shelf
thiagopereira-vtex Jul 6, 2026
1852ee1
fix(api): close Recommendation describe block after merge with dev
thiagopereira-vtex Jul 6, 2026
0e36d26
fix(api): add storeCookies option to fetchAPI for recommendations
thiagopereira-vtex Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/api/src/__generated__/schema.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/api/src/platforms/vtex/clients/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type { GraphqlContext } from '..'
import { VtexCommerce } from './commerce'
import { Recommendation } from './recommendation'
import { IntelligentSearch } from './search'

export type Clients = ReturnType<typeof getClients>

export const getClients = (options: Options, ctx: GraphqlContext) => {
const search = IntelligentSearch(options, ctx)
const commerce = VtexCommerce(options, ctx)
const recommendation = Recommendation(options, ctx)

return {
search,
commerce,
recommendation,
}
}
83 changes: 83 additions & 0 deletions packages/api/src/platforms/vtex/clients/recommendation/index.ts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Não existe uma regra específica, mas penso que em vez de ser um novo client para recommendations, poderia ser um namespace dentro do vtex Commerce client. Você enxerga mais operações existentes nesse cliente além das adicionadas até o momento? o que achas?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thiagopereira-vtex essa acho que cabe mais como um namespace dentro do client de commerce que ja existe, ao invés de criar um novo client. O que achas?

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { GraphqlContext } from '../..'
import { getStoreCookie, getWithCookie } from '../../utils/cookies'
import { fetchAPI } from '../fetch'
import type {
RecommendationResult,
StartRecommendationSessionResult,
} from './types/RecommendationResult'

// Identifies the storefront origin to the Recommendations BFF, as required by
// the API (`x-vtex-rec-origin` header).
const REC_ORIGIN_SUFFIX = 'storefront/vtex.recommendation-shelf@2.x'
Comment thread
thiagopereira-vtex marked this conversation as resolved.
Outdated

export interface RecommendationArgs {
campaignVrn: string
userId?: string
products?: string[]
salesChannel?: string
locale?: string
}

export const Recommendation = (
{ account, environment }: Options,
ctx: GraphqlContext
) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether `Options` is declared globally or should be imported from a shared VTEX module.
rg -n --type=ts '\b(interface|type)\s+Options\b|\bdeclare global\b' packages/api/src/platforms/vtex
sed -n '1,40p' packages/api/src/platforms/vtex/clients/recommendation/index.ts
fd -i 'index.ts' packages/api/src/platforms/vtex/clients

Repository: vtex/faststore

Length of output: 1610


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n-- recommendation index --\n'
sed -n '1,220p' packages/api/src/platforms/vtex/clients/recommendation/index.ts

printf '\n-- search index --\n'
sed -n '1,220p' packages/api/src/platforms/vtex/clients/search/index.ts

printf '\n-- search ProductSearchResult types --\n'
sed -n '1,220p' packages/api/src/platforms/vtex/clients/search/types/ProductSearchResult.ts

printf '\n-- all Option type declarations under vtex clients --\n'
rg -n --type=ts '\b(interface|type)\s+Options\b' packages/api/src/platforms/vtex/clients

Repository: vtex/faststore

Length of output: 12176


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n-- all exported Options types in repo --\n'
rg -n --type=ts 'export\s+(interface|type)\s+Options\b' packages

printf '\n-- all imports of Options in vtex clients --\n'
rg -n --type=ts "import type \{[^}]*\bOptions\b[^}]*\}" packages/api/src/platforms/vtex/clients packages/api/src/platforms/vtex

printf '\n-- recommendation client neighbors --\n'
sed -n '1,220p' packages/api/src/platforms/vtex/clients/index.ts
sed -n '1,220p' packages/api/src/platforms/vtex/clients/commerce/index.ts

Repository: vtex/faststore

Length of output: 302


Import the shared VTEX client options type.
Options is referenced here but never imported or declared in this module, so TypeScript will fail to compile this file. Reuse the same client options type used by the other VTEX clients.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/platforms/vtex/clients/recommendation/index.ts` around lines
21 - 24, The Recommendation client is using an Options type that is not declared
in this module, so update the VTEX recommendation client to import and reuse the
shared client options type used by the other VTEX client factories. Locate the
Recommendation function signature and replace the unresolved Options reference
with the shared type import so the module compiles consistently with the other
client implementations.

const base = `https://${account}.${environment}.com.br/api/recommend-bff/v2`
const withCookie = getWithCookie(ctx)
const storeCookies = getStoreCookie(ctx)

const headers: HeadersInit = withCookie({
accept: 'application/json',
'content-type': 'application/json',
'x-vtex-rec-origin': `${account}/${REC_ORIGIN_SUFFIX}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it is best to forward all the cookies, otherwise you will need to open the segment and parse all the infos.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — that's already what we do here. The client forwards all cookies through getWithCookie/withCookie: it sets the cookie header to the full normalized ctx.headers.cookie (including vtex_segment, checkout.vtex.com, etc.) merged with any updates from ctx.storage.cookies. So the BFF receives the whole segment and we never open/parse it here.

The salesChannel/locale query params come pre-resolved from ctx.storage.channel/ctx.storage.locale (parsed once upstream by FastStore, not re-parsed in this client), so they're just a convenience on top of the forwarded cookies. Happy to drop them and rely solely on the forwarded vtex_segment if we're confident the recommend-bff reads them from the cookie — let me know and I'll simplify.

})

const recommendations = ({
campaignVrn,
userId,
products = [],
salesChannel,
locale,
}: RecommendationArgs): Promise<RecommendationResult> => {
const params = new URLSearchParams({ an: account, campaignVrn })

if (userId) {
params.append('userId', userId)
}

if (products.length > 0) {
params.append('products', products.join(','))
}

if (salesChannel) {
params.append('salesChannel', salesChannel)
}

if (locale) {
params.append('locale', locale)
}

return fetchAPI(`${base}/recommendations?${params.toString()}`, { headers })
}

// Starts/updates the anonymous personalization session. The BFF resolves the
// orderForm from the forwarded `checkout.vtex.com` cookie and replies with the
// `vtex-rec-user-id`/`vtex-rec-user-start-session` Set-Cookie headers, which we
// forward to the browser through `ctx.storage.cookies`.
const startRecommendationSession = (): Promise<
StartRecommendationSessionResult | undefined
> => {
const params = new URLSearchParams({ an: account })

return fetchAPI(
`${base}/users/start-session?${params.toString()}`,
{ method: 'POST', headers },
{ storeCookies }
)
}

return {
recommendations,
startRecommendationSession,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Product } from '../../search/types/ProductSearchResult'

/**
* Raw response of the VTEX Recommendations BFF
* (`GET /api/recommend-bff/v2/recommendations`).
*
* The BFF already returns the products fully hydrated in the same Intelligent
* Search shape (`Product`) used by the `search` query, so the resolver can map
* them straight to the normalized `StoreProduct` shape via `pickBestSku` +
* `enhanceSku` — no extra round-trip to search is needed.
*/
export interface RecommendationResult {
products: Product[]
correlationId: string
campaign: RecommendationBffCampaign
}

export interface RecommendationBffCampaign {
id: string
title?: string
type: string
}

/** Response of `POST /api/recommend-bff/v2/users/start-session`. */
export interface StartRecommendationSessionResult {
recommendationsUserId: string
}
2 changes: 2 additions & 0 deletions packages/api/src/platforms/vtex/resolvers/mutation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cancelOrder } from './cancelOrder'
import { processOrderAuthorization } from './processOrderAuthorization'
import { startOrderEntryOperation } from './startOrderEntryOperation'
import { startRecommendationSession } from './startRecommendationSession'
import { subscribeToNewsletter } from './subscribeToNewsletter'
import { uploadFileToOrderEntry } from './uploadFileToOrderEntry'
import { validateCart } from './validateCart'
Expand All @@ -14,4 +15,5 @@ export const Mutation = {
processOrderAuthorization,
uploadFileToOrderEntry,
startOrderEntryOperation,
startRecommendationSession,
}
2 changes: 2 additions & 0 deletions packages/api/src/platforms/vtex/resolvers/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
} from '../../../__generated__/schema'
import { getOrderEntryOperation } from './getOrderEntryOperation'
import { getOrderFormItems } from './getOrderFormItems'
import { recommendations } from './recommendations'
import {
BadRequestError,
ForbiddenError,
Expand Down Expand Up @@ -675,4 +676,5 @@ export const Query = {
},
orderEntryOperation: getOrderEntryOperation,
orderFormItems: getOrderFormItems,
recommendations,
}
49 changes: 49 additions & 0 deletions packages/api/src/platforms/vtex/resolvers/recommendations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { QueryRecommendationsArgs } from '../../../__generated__/schema'
import type { GraphqlContext } from '../index'
import { enhanceSku, type EnhancedSku } from '../utils/enhanceSku'
import { pickBestSku } from '../utils/sku'

/**
* Resolves personalized recommendations for a campaign.
*
* The VTEX Recommendations BFF already returns the products fully hydrated in
* the same Intelligent Search shape used by the `search` query. We map them
* straight to the normalized `StoreProduct` shape (`pickBestSku` + `enhanceSku`)
* so recommendation shelves render identical cards to regular shelves, while
* preserving the recommendation order returned by the BFF.
*/
export const recommendations = async (
_: unknown,
{ campaignVrn, userId, products }: QueryRecommendationsArgs,
ctx: GraphqlContext
) => {
const {
clients: { recommendation },
} = ctx

const { salesChannel } = ctx.storage.channel

const response = await recommendation.recommendations({
campaignVrn,
userId: userId ?? undefined,
products: products ?? [],
salesChannel: salesChannel ?? undefined,
locale: ctx.storage.locale,
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const { campaign, correlationId } = response

const orderedProducts = (response.products ?? [])
.map((product) => {
const sku = pickBestSku(product.items)

return sku ? enhanceSku(sku, product) : null
})
.filter((sku): sku is EnhancedSku => Boolean(sku))

return {
products: orderedProducts,
correlationId,
campaign,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { GraphqlContext } from '../index'

/**
* Starts (or updates) the anonymous personalization session for the current
* shopper via the Recommendations BFF.
*
* The BFF replies with the `vtex-rec-user-id`/`vtex-rec-user-start-session`
* Set-Cookie headers, which the client forwards to the browser through
* `ctx.storage.cookies`. Returns `true` once the session has been started.
*/
export const startRecommendationSession = async (
_: unknown,
__: unknown,
ctx: GraphqlContext
) => {
await ctx.clients.recommendation.startRecommendationSession()

return true
}
4 changes: 4 additions & 0 deletions packages/api/src/platforms/vtex/typeDefs/mutation.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ type Mutation {
startOrderEntryOperation(
data: IOrderEntryOperation!
): StoreOrderEntryOperationResult @auth
"""
Starts an anonymous personalization session for the current shopper.
"""
startRecommendationSession: Boolean!
}
9 changes: 9 additions & 0 deletions packages/api/src/platforms/vtex/typeDefs/query.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,15 @@ type Query {
Returns the items in an orderForm by its ID.
"""
orderFormItems(orderFormId: String!): [StoreOrderFormCartItem!]! @auth
"""
Returns personalized product recommendations for a given campaign.
"""
recommendations(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the implementation for this recommendations query?

campaignVrn: String!
userId: String
products: [String!]
): RecommendationResponse!
@cacheControl(scope: "private", sMaxAge: 120, staleWhileRevalidate: 3600)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A recomendação é feita pra cada usuário (userId)? Acho que, nesse caso, faz sentido ser private: ficar no browser do usuário e não "vazar" para cdn ou outra camada compartilhada. Caso contrário, seria melhor public.

}

type ValidateUserData {
Expand Down
11 changes: 11 additions & 0 deletions packages/api/src/platforms/vtex/typeDefs/recommendation.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type RecommendationResponse {
products: [StoreProduct!]!
correlationId: String!
campaign: RecommendationCampaign!
}

type RecommendationCampaign {
id: String!
title: String
type: String!
}
2 changes: 2 additions & 0 deletions packages/api/test/integration/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const QUERIES = [
'pickupPoints',
'orderEntryOperation',
'orderFormItems',
'recommendations',
]

const MUTATIONS = [
Expand All @@ -90,6 +91,7 @@ const MUTATIONS = [
'processOrderAuthorization',
'uploadFileToOrderEntry',
'startOrderEntryOperation',
'startRecommendationSession',
]

let schema: GraphQLSchema
Expand Down
Loading
Loading