Skip to content

Feat(VTEX): Recommendations BFF API#1532

Open
vitoUwu wants to merge 12 commits intodeco-cx:mainfrom
vitoUwu:feat/vtex-recommendation-api
Open

Feat(VTEX): Recommendations BFF API#1532
vitoUwu wants to merge 12 commits intodeco-cx:mainfrom
vitoUwu:feat/vtex-recommendation-api

Conversation

@vitoUwu
Copy link
Copy Markdown
Contributor

@vitoUwu vitoUwu commented Feb 24, 2026

What is this Contribution About?

Implementation of VTEX Recommendations BFF API


Summary by cubic

Adds VTEX Recommendations BFF integration to fetch product recommendations and track events, with session bootstrap and product metadata enrichment. Also adds a cart action to update orderForm custom data.

  • New Features

    • BFF HTTP client with OpenAPI spec for recommendations.
    • Product recommendations loader: supports campaign types, context products, location/sales channel; returns products annotated with correlationId and campaign info.
    • Helpers to get context product IDs from cart and PDP.
    • Event actions: product-view, recommendation-view, recommendation-click (use vtex-rec-user-id cookie or provided userId).
    • Start session action: creates recommendations user and proxies Set-Cookie; no-ops if a recommendations cookie already exists.
    • Cart: update orderForm custom data action to write per-app customData.
    • Config: advancedConfigs.autoStartRecommendationSession to start a session when userId is missing.
    • Typed OrderForm.customData and aligned update custom data action body with VTEX API.
  • Migration

    • Set x-vtex-rec-origin as {account}/{source}/{app} when calling loaders/actions; default is {account}/storefront/deco.recommendations@1.x.
    • If enabling autoStartRecommendationSession, expect one extra call only on the first request without a session (skipped when vtex-rec-user-id exists); requires orderForm cookie.
    • Use loaders/recommendations/productList and pass correlationId to emit recommendation-view and recommendation-click events.

Written for commit c8840f5. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Event tracking for product views, recommendation clicks, and recommendation views.
    • Recommendations: product recommendation feed plus sources from cart and product page.
    • Session: automatic recommendation session support and a start-session action.
    • Cart: ability for apps to update cart custom data.
  • Developer API

    • Added Recommendations BFF OpenAPI spec and new recommendation-related types and utilities.

@github-actions
Copy link
Copy Markdown
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 0.135.2 update
  • 🎉 for Minor 0.136.0 update
  • 🚀 for Major 1.0.0 update

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

Adds event-driven recommendation functionality: new event actions (productView, recommendationClick, recommendationView), a recommendation start-session action, recommendation loaders (productList and sources), OpenAPI spec and BFF client wiring, utilities for origin/cookie handling, type and transform updates, manifest and mod adjustments to register these modules. (34 words)

Changes

Cohort / File(s) Summary
Event Tracking Actions
vtex/actions/events/productView.ts, vtex/actions/events/recommendationClick.ts, vtex/actions/events/recommendationView.ts
New server actions/loaders that resolve origin and userId (from props or cookies), validate inputs, and POST event payloads to Recommendations BFF endpoints; return { ok: true }.
Recommendation Session Action
vtex/actions/recommendation/startSession.ts
Action that optionally reads existing recommendation userId, forwards start-session to the BFF, proxies Set-Cookie headers from BFF responses, and returns parsed session JSON.
Recommendation Loaders
vtex/loaders/recommendations/productList.ts, vtex/loaders/recommendations/productListFromCart.ts, vtex/loaders/recommendations/productListFromPage.ts
Loaders that build product ID lists (from cart or page), optionally auto-start session, call recommendations BFF, and map response items into Product[] with campaign metadata; includes error handling and origin/header propagation.
Utilities & Types
vtex/utils/recommendations.ts, vtex/utils/types.ts, vtex/utils/transform.ts
Adds getOrigin and parseCookie helpers, new types (CampaignType, ProductListToId, ProductViewSource, CustomApp, CustomData), makes OrderForm.customData nullable with CustomData, and adds isVariantOfAdditionalProperty to ProductOptions.
OpenAPI / Client Contracts
vtex/utils/openapi/recommendations-bff.openapi.gen.ts, vtex/utils/openapi/recommendations-bff.openapi.json, vtex/utils/client.ts
Adds autogenerated OpenAPI TypeScript mapping and JSON spec for Recommendations BFF (events, recommendations, start-session) and updates client typings (including new PUT checkout customData endpoint).
Manifest & Module Wiring
vtex/manifest.gen.ts, vtex/mod.ts
Manifest reorganized to expose event/recommendation actions/loaders; mod.ts wires a recommendations BFF client into app state and adds autoStartRecommendationSession?: boolean to Props.
Cart Custom Data Action
vtex/actions/cart/updateCustomData.ts
New action to PUT custom data into orderForm.customData via checkout API, reading orderFormId from cookies, proxying Set-Cookie headers, and returning the updated OrderForm.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant App as Deco App
    participant BFF as Recommendations BFF
    participant Checkout as Checkout API

    Client->>App: Request recommendations / send event (props, headers, cookies)
    App->>App: resolve origin, parse cookies, determine userId/segment (maybe auto-start session)
    App->>BFF: POST /recommend-bff/v2/recommendations or POST /events/* (include x-vtex-rec-origin, cookie, user-agent)
    BFF-->>App: recommendations payload or event ack / start-session response (+Set-Cookie)
    alt start-session set cookie
        App->>Client: proxy Set-Cookie header
    end
    App-->>Client: Product[] or { ok: true } or session response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • guitavano
  • mcandeia

Poem

🐰 I hopped through headers, cookies in paw,

I nudged events and whispered "start" with awe,
Recommendations sprouted from BFF’s tune,
Cookies set softly beneath the moon —
I munched a carrot and danced by noon.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat(VTEX): Recommendations BFF API' clearly and concisely summarizes the main contribution—implementing VTEX Recommendations BFF API integration with loaders, event actions, session management, and cart functionality.
Description check ✅ Passed The PR description is well-structured with an auto-generated summary covering new features, migration notes, and implementation details; however, it lacks the required Issue Link and Loom Video sections from the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 14 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="vtex/actions/recommendation/startSession.ts">

<violation number="1" location="vtex/actions/recommendation/startSession.ts:39">
P2: Logging response data and Set-Cookie headers can leak sensitive session/auth information into logs. Remove or sanitize these logs in production.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@vtex/actions/events/productView.ts`:
- Around line 12-14: Update the JSDoc for the "Product ID" prop in the
product-view loader so the description reflects a view event rather than a
click: locate the comment block with `@title` "Product ID" in productView.ts (the
product-view loader) and change the phrase "The ID of the product that was
clicked." to something like "The ID of the product that was viewed." to match
the emitted product-view event.

In `@vtex/actions/recommendation/startSession.ts`:
- Line 39: Remove the sensitive console.log in the startSession action: delete
the line that logs "data" and the result of getSetCookies(response.headers) in
the startSession function, since it exposes session/recommendation identifiers;
if you need runtime visibility, replace it with a non-sensitive log such as
response.status or a sanitized message (e.g., "startSession succeeded") and
never log raw cookies or full response body—avoid calling getSetCookies for
logging or redact its output before any log.

In `@vtex/loaders/recommendations/productList.ts`:
- Around line 106-112: For rec-cross-v2 and rec-similar-v2 ensure a source
product exists before building the products/productIds: check campaignType and
validate props.products?.[0] is present; if missing, bail out (throw, return an
error response, or skip calling the BFF) instead of constructing products =
[props.products?.[0]] which yields undefined and productIds = "". Update the
logic around the products and productIds variables (referencing campaignType,
products, productIds, and props.products) to perform this guard and handle the
missing-product case explicitly.
- Line 123: The headers spread is dropping the segment cookie because
withSegmentCookie(segment) returns a Headers instance which doesn't spread into
a plain object; change the merge to preserve entries by converting the Headers
to a plain object (e.g. Object.fromEntries(withSegmentCookie(segment))) or build
a Headers object and call .set("x-vtex-rec-origin", origin) so the segment
cookie from withSegmentCookie(segment) is retained; update the code that
currently uses headers: { ...withSegmentCookie(segment), "x-vtex-rec-origin":
origin } to one of these approaches (refer to withSegmentCookie and the headers
property in productList.ts).

In `@vtex/utils/openapi/recommendations-bff.openapi.json`:
- Around line 266-269: The documentation for the "userId" query parameter
references the wrong cookie name; update its description to mention the correct
cookie "vtex-rec-user-id" (replace `vtex-recommendations-user` with
`vtex-rec-user-id`) so it matches the session flow and utilities, and scan the
OpenAPI spec for any other occurrences of `vtex-recommendations-user` to replace
them with `vtex-rec-user-id`.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9683a21 and fd5cbcd.

📒 Files selected for processing (14)
  • vtex/actions/events/productView.ts
  • vtex/actions/events/recommendationClick.ts
  • vtex/actions/events/recommendationView.ts
  • vtex/actions/recommendation/startSession.ts
  • vtex/loaders/recommendations/productList.ts
  • vtex/loaders/recommendations/productListFromCart.ts
  • vtex/loaders/recommendations/productListFromPage.ts
  • vtex/manifest.gen.ts
  • vtex/mod.ts
  • vtex/utils/openapi/recommendations-bff.openapi.gen.ts
  • vtex/utils/openapi/recommendations-bff.openapi.json
  • vtex/utils/recommendations.ts
  • vtex/utils/transform.ts
  • vtex/utils/types.ts

Comment on lines +12 to +14
* @title Product ID
* @description The ID of the product that was clicked.
*/
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.

⚠️ Potential issue | 🟡 Minor

Fix product-view prop description typo.

Line 13 says the product was “clicked”, but this loader emits a product-view event.

✏️ Suggested edit
-   * `@description` The ID of the product that was clicked.
+   * `@description` The ID of the product that was viewed.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @title Product ID
* @description The ID of the product that was clicked.
*/
* `@title` Product ID
* `@description` The ID of the product that was viewed.
*/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/actions/events/productView.ts` around lines 12 - 14, Update the JSDoc
for the "Product ID" prop in the product-view loader so the description reflects
a view event rather than a click: locate the comment block with `@title` "Product
ID" in productView.ts (the product-view loader) and change the phrase "The ID of
the product that was clicked." to something like "The ID of the product that was
viewed." to match the emitted product-view event.

Comment on lines +106 to +112
const products =
campaignType === "rec-cross-v2" || campaignType === "rec-similar-v2"
? [props.products?.[0]]
: props.products;

const productIds = products?.filter(Boolean).join(",");

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.

⚠️ Potential issue | 🟠 Major

Validate required product context for cross/similar campaigns.

Lines 106-112 allow missing product IDs for rec-cross-v2 / rec-similar-v2, which can send an empty products parameter and cause avoidable BFF failures.

🛡️ Suggested fix
-    const products =
-      campaignType === "rec-cross-v2" || campaignType === "rec-similar-v2"
-        ? [props.products?.[0]]
-        : props.products;
+    const requiresSingleProduct =
+      campaignType === "rec-cross-v2" || campaignType === "rec-similar-v2";
+
+    if (requiresSingleProduct && !props.products?.[0]) {
+      throw new Error(
+        `campaignType "${props.campaignType}" requires one product ID`,
+      );
+    }
+
+    const products = requiresSingleProduct
+      ? [props.products![0]]
+      : props.products;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const products =
campaignType === "rec-cross-v2" || campaignType === "rec-similar-v2"
? [props.products?.[0]]
: props.products;
const productIds = products?.filter(Boolean).join(",");
const requiresSingleProduct =
campaignType === "rec-cross-v2" || campaignType === "rec-similar-v2";
if (requiresSingleProduct && !props.products?.[0]) {
throw new Error(
`campaignType "${props.campaignType}" requires one product ID`,
);
}
const products = requiresSingleProduct
? [props.products![0]]
: props.products;
const productIds = products?.filter(Boolean).join(",");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/loaders/recommendations/productList.ts` around lines 106 - 112, For
rec-cross-v2 and rec-similar-v2 ensure a source product exists before building
the products/productIds: check campaignType and validate props.products?.[0] is
present; if missing, bail out (throw, return an error response, or skip calling
the BFF) instead of constructing products = [props.products?.[0]] which yields
undefined and productIds = "". Update the logic around the products and
productIds variables (referencing campaignType, products, productIds, and
props.products) to perform this guard and handle the missing-product case
explicitly.

products: productIds,
salesChannel: segment?.payload?.channel || ctx.salesChannel,
}, {
headers: { ...withSegmentCookie(segment), "x-vtex-rec-origin": origin },
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check vtex/utils/segment.ts for withSegmentCookie definition and Headers usage
echo "=== Checking vtex/utils/segment.ts ==="
rg -n -C3 'export const withSegmentCookie|new Headers\(|return h' vtex/utils/segment.ts

echo ""
echo "=== Checking vtex/loaders/recommendations/productList.ts line 123 ==="
rg -n -C3 '\.\.\.withSegmentCookie\(segment\)' vtex/loaders/recommendations/productList.ts

Repository: deco-cx/apps

Length of output: 858


Segment cookie is dropped when spreading Headers into a plain object.

Line 123 spreads withSegmentCookie(segment) (which returns a Headers instance) into a plain object literal. A Headers object doesn't preserve its entries when spread into {} because it's a Web API object with custom iteration semantics, not a plain object with enumerable properties. This silently drops the segment cookie and breaks segment-aware recommendations.

Fix
-    const response = await bff["GET /api/recommend-bff/v2/recommendations"]({
+    const response = await bff["GET /api/recommend-bff/v2/recommendations"]({
       an: account,
       campaignVrn,
       pickupPoint: props.pickupPoint,
       regionId: props.regionId,
       zipCode: props.zipCode,
       userId,
       products: productIds,
       salesChannel: segment?.payload?.channel || ctx.salesChannel,
     }, {
-      headers: { ...withSegmentCookie(segment), "x-vtex-rec-origin": origin },
+      headers: withSegmentCookie(
+        segment,
+        new Headers({ "x-vtex-rec-origin": origin }),
+      ),
     }).then((res) => res.json());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/loaders/recommendations/productList.ts` at line 123, The headers spread
is dropping the segment cookie because withSegmentCookie(segment) returns a
Headers instance which doesn't spread into a plain object; change the merge to
preserve entries by converting the Headers to a plain object (e.g.
Object.fromEntries(withSegmentCookie(segment))) or build a Headers object and
call .set("x-vtex-rec-origin", origin) so the segment cookie from
withSegmentCookie(segment) is retained; update the code that currently uses
headers: { ...withSegmentCookie(segment), "x-vtex-rec-origin": origin } to one
of these approaches (refer to withSegmentCookie and the headers property in
productList.ts).

Comment on lines +266 to +269
"name": "userId",
"in": "query",
"description": "User ID, required for personalized recommendations based on user behavior. It can be found in the `vtex-recommendations-user` cookie.",
"required": false,
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.

⚠️ Potential issue | 🟡 Minor

Fix inconsistent cookie name in userId parameter docs.

Line 268 references vtex-recommendations-user, but the session flow and utilities use vtex-rec-user-id. This inconsistency can cause integration mistakes.

🛠️ Proposed fix
-            "description": "User ID, required for personalized recommendations based on user behavior. It can be found in the `vtex-recommendations-user` cookie.",
+            "description": "User ID, required for personalized recommendations based on user behavior. It can be found in the `vtex-rec-user-id` cookie.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"name": "userId",
"in": "query",
"description": "User ID, required for personalized recommendations based on user behavior. It can be found in the `vtex-recommendations-user` cookie.",
"required": false,
"name": "userId",
"in": "query",
"description": "User ID, required for personalized recommendations based on user behavior. It can be found in the `vtex-rec-user-id` cookie.",
"required": false,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/utils/openapi/recommendations-bff.openapi.json` around lines 266 - 269,
The documentation for the "userId" query parameter references the wrong cookie
name; update its description to mention the correct cookie "vtex-rec-user-id"
(replace `vtex-recommendations-user` with `vtex-rec-user-id`) so it matches the
session flow and utilities, and scan the OpenAPI spec for any other occurrences
of `vtex-recommendations-user` to replace them with `vtex-rec-user-id`.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
vtex/actions/cart/updateCustomData.ts (1)

15-18: Update JSDoc to reflect actual functionality.

The title and description reference "Attachment" but this action updates custom data, not attachments.

📝 Proposed fix
 /**
- * `@title` Update Attachment
- * `@description` Update an attachment in the cart
+ * `@title` Update Custom Data
+ * `@description` Update custom data in the order form
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/actions/cart/updateCustomData.ts` around lines 15 - 18, The JSDoc above
the action is misleading: change the `@title` and `@description` that currently say
"Update Attachment" to reflect the actual behavior of this module (updating cart
custom data). Update the block in vtex/actions/cart/updateCustomData.ts (the
JSDoc that documents the updateCustomData action) so the `@title` reads something
like "Update Custom Data" and the `@description` explains that it updates custom
data in the cart (mentioning the specific custom field or namespace if
applicable).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@vtex/actions/cart/updateCustomData.ts`:
- Line 6: Remove the unused import getSegmentFromBag from the top of the file
(the import statement importing getSegmentFromBag from "../../utils/segment.ts")
so the CI error goes away; ensure no other references to getSegmentFromBag exist
in the updateCustomData module (e.g., within updateCustomData) and run lint/type
checks to confirm the unused-import is resolved.

---

Nitpick comments:
In `@vtex/actions/cart/updateCustomData.ts`:
- Around line 15-18: The JSDoc above the action is misleading: change the `@title`
and `@description` that currently say "Update Attachment" to reflect the actual
behavior of this module (updating cart custom data). Update the block in
vtex/actions/cart/updateCustomData.ts (the JSDoc that documents the
updateCustomData action) so the `@title` reads something like "Update Custom Data"
and the `@description` explains that it updates custom data in the cart
(mentioning the specific custom field or namespace if applicable).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29b4428f-2fc4-47bc-abf8-d6123429bbbc

📥 Commits

Reviewing files that changed from the base of the PR and between fd5cbcd and 2811c9d.

📒 Files selected for processing (3)
  • vtex/actions/cart/updateCustomData.ts
  • vtex/manifest.gen.ts
  • vtex/utils/client.ts

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="vtex/utils/types.ts">

<violation number="1" location="vtex/utils/types.ts:53">
P2: `CustomData` requires `customFields`, but VTEX orderForm `customData` is documented/schema-defined around `customApps` and may omit that property, creating an API contract mismatch.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


export interface CustomData {
customApps: CustomApp[];
customFields: unknown[];
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 12, 2026

Choose a reason for hiding this comment

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

P2: CustomData requires customFields, but VTEX orderForm customData is documented/schema-defined around customApps and may omit that property, creating an API contract mismatch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vtex/utils/types.ts, line 53:

<comment>`CustomData` requires `customFields`, but VTEX orderForm `customData` is documented/schema-defined around `customApps` and may omit that property, creating an API contract mismatch.</comment>

<file context>
@@ -42,6 +42,17 @@ export interface OrderForm {
+
+export interface CustomData {
+  customApps: CustomApp[];
+  customFields: unknown[];
+}
+
</file context>
Suggested change
customFields: unknown[];
customFields?: unknown[];
Fix with Cubic

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
vtex/loaders/recommendations/productList.ts (1)

107-112: ⚠️ Potential issue | 🟠 Major

Validate required product context for cross/similar campaigns.

For rec-cross-v2 and rec-similar-v2 campaign types, if props.products is undefined or empty, this creates products = [undefined] which then becomes an empty string after filtering. This sends an empty products parameter to the BFF, likely causing API failures.

🛡️ Suggested fix
-    const products =
-      campaignType === "rec-cross-v2" || campaignType === "rec-similar-v2"
-        ? [props.products?.[0]]
-        : props.products;
+    const requiresSingleProduct =
+      campaignType === "rec-cross-v2" || campaignType === "rec-similar-v2";
+
+    if (requiresSingleProduct && !props.products?.[0]) {
+      console.warn(
+        `campaignType "${props.campaignType}" requires at least one product ID`,
+      );
+      return null;
+    }
+
+    const products = requiresSingleProduct
+      ? [props.products![0]]
+      : props.products;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/loaders/recommendations/productList.ts` around lines 107 - 112, When
campaignType is "rec-cross-v2" or "rec-similar-v2" the current logic sets
products = [props.products?.[0]] which can produce [undefined] when
props.products is missing and later yields an empty productIds; update the logic
in the block that sets products (referencing campaignType, props.products, and
products) to first validate that props.products exists and has at least one
truthy entry before creating the single-item array, and if not, handle the
missing context (e.g., set products = undefined/null or bail out early) so
productIds (computed from products?.filter(Boolean).join(",")) is never an empty
string sent to the BFF.
🧹 Nitpick comments (1)
vtex/loaders/recommendations/productList.ts (1)

169-172: Consider adding context to error logging.

The generic console.error(error) loses context about which campaign or configuration failed. Adding identifiers would help debugging in production.

💡 Optional improvement
   } catch (error) {
-    console.error(error);
+    console.error("Failed to fetch recommendations", {
+      campaignType: props.campaignType,
+      campaignId: props.campaignId,
+      error,
+    });
     return null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/loaders/recommendations/productList.ts` around lines 169 - 172, In the
catch block inside the productList loader (the catch in the productList /
loadProductList function), replace the generic console.error(error) with a
contextual log that includes the relevant identifiers available in scope (for
example campaignId, campaign?.id or campaign?.name, or config?.id) along with
the error object; ensure the log message clearly states "Error loading product
list" and includes those identifiers before returning null so you can trace
which campaign/config failed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@vtex/actions/recommendation/startSession.ts`:
- Around line 12-13: The request is posting a start-session to the BFF even when
parseOrderformCookie(req.headers) returns no orderFormId; update the logic in
startSession.ts to skip the BFF start-session call whenever orderFormId is falsy
(do not post body: { orderFormId } or call the BFF at all), and similarly apply
the same guard in the other branch around the second POST (the block referenced
at lines 34-39) so you only start a session when orderFormId is present; use the
existing variables parseOrderformCookie, parseRecommendationsCookie, orderFormId
and userId to gate the calls.
- Around line 21-25: The code unconditionally overwrites the caller-provided
x-vtex-rec-origin header; change it to respect an incoming header first by
reading the request header (e.g., ctx.request.headers.get('x-vtex-rec-origin')
or equivalent) and only set headers.set('x-vtex-rec-origin',
`${ctx.account}/storefront/deco.recommendations@1.x`) when that incoming value
is missing/empty. Update the logic around the local headers variable in
startSession (where headers is created) to use the existing header value if
present, otherwise fall back to the default constructed from ctx.account and
storefront/deco.recommendations@1.x.

In `@vtex/loaders/recommendations/productList.ts`:
- Around line 132-168: The mapping over response.products can throw if products
is undefined or not an array; update the product list loader to defensively
handle this by checking response.products before mapping (e.g., ensure
Array.isArray(response.products) and fallback to an empty array), then call
.map(...) using the same toProduct(...) call and getFirstItemAvailable helper;
ensure the code still returns an empty array or appropriate default when
products is missing so downstream code doesn't receive undefined.
- Around line 114-128: Replace the raw cookie header when calling the
recommendations BFF so the request includes the VTEX segment token: import
withSegmentCookie from ../../utils/segment.ts, ensure you use the `segment`
value obtained from `getSegmentFromBag(ctx)`, and update the headers passed to
bff["GET /api/recommend-bff/v2/recommendations"] to spread
`withSegmentCookie(segment)` and include "x-vtex-rec-origin": origin instead of
using req.headers.get("cookie") directly; keep the rest of the request payload
(userId, products, salesChannel, etc.) unchanged.

---

Duplicate comments:
In `@vtex/loaders/recommendations/productList.ts`:
- Around line 107-112: When campaignType is "rec-cross-v2" or "rec-similar-v2"
the current logic sets products = [props.products?.[0]] which can produce
[undefined] when props.products is missing and later yields an empty productIds;
update the logic in the block that sets products (referencing campaignType,
props.products, and products) to first validate that props.products exists and
has at least one truthy entry before creating the single-item array, and if not,
handle the missing context (e.g., set products = undefined/null or bail out
early) so productIds (computed from products?.filter(Boolean).join(",")) is
never an empty string sent to the BFF.

---

Nitpick comments:
In `@vtex/loaders/recommendations/productList.ts`:
- Around line 169-172: In the catch block inside the productList loader (the
catch in the productList / loadProductList function), replace the generic
console.error(error) with a contextual log that includes the relevant
identifiers available in scope (for example campaignId, campaign?.id or
campaign?.name, or config?.id) along with the error object; ensure the log
message clearly states "Error loading product list" and includes those
identifiers before returning null so you can trace which campaign/config failed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4ced8bca-1f48-4236-8faf-c4c3d054e2af

📥 Commits

Reviewing files that changed from the base of the PR and between 25a5da7 and c8840f5.

📒 Files selected for processing (4)
  • vtex/actions/events/productView.ts
  • vtex/actions/recommendation/startSession.ts
  • vtex/loaders/recommendations/productList.ts
  • vtex/utils/recommendations.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • vtex/actions/events/productView.ts
  • vtex/utils/recommendations.ts

Comment on lines +12 to +13
const { orderFormId } = parseOrderformCookie(req.headers);
const { userId } = parseRecommendationsCookie(req.headers);
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.

⚠️ Potential issue | 🟠 Major

Skip the BFF call when orderFormId is missing.

parseOrderformCookie() can return no orderFormId, but this branch still posts body: { orderFormId }. vtex/loaders/recommendations/productList.ts:89-95 auto-starts the session whenever userId is absent, so first requests without the orderForm cookie will still make an avoidable start-session call and likely fail.

Also applies to: 34-39

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/actions/recommendation/startSession.ts` around lines 12 - 13, The
request is posting a start-session to the BFF even when
parseOrderformCookie(req.headers) returns no orderFormId; update the logic in
startSession.ts to skip the BFF start-session call whenever orderFormId is falsy
(do not post body: { orderFormId } or call the BFF at all), and similarly apply
the same guard in the other branch around the second POST (the block referenced
at lines 34-39) so you only start a session when orderFormId is present; use the
existing variables parseOrderformCookie, parseRecommendationsCookie, orderFormId
and userId to gate the calls.

Comment on lines +21 to +25
const headers = new Headers();
headers.set(
"x-vtex-rec-origin",
`${ctx.account}/storefront/deco.recommendations@1.x`,
);
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.

⚠️ Potential issue | 🟠 Major

Respect caller-provided x-vtex-rec-origin.

The PR contract says callers can send {account}/{source}/{app}, but Lines 21-25 always overwrite that with the default value. That loses source/app attribution for any non-default integration. Prefer the inbound header and only fall back to the default when it is missing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/actions/recommendation/startSession.ts` around lines 21 - 25, The code
unconditionally overwrites the caller-provided x-vtex-rec-origin header; change
it to respect an incoming header first by reading the request header (e.g.,
ctx.request.headers.get('x-vtex-rec-origin') or equivalent) and only set
headers.set('x-vtex-rec-origin',
`${ctx.account}/storefront/deco.recommendations@1.x`) when that incoming value
is missing/empty. Update the logic around the local headers variable in
startSession (where headers is created) to use the existing header value if
present, otherwise fall back to the default constructed from ctx.account and
storefront/deco.recommendations@1.x.

Comment on lines +114 to +128
const response = await bff["GET /api/recommend-bff/v2/recommendations"]({
an: account,
campaignVrn,
pickupPoint: props.pickupPoint,
regionId: props.regionId,
zipCode: props.zipCode,
userId,
products: productIds,
salesChannel: segment?.payload?.channel || ctx.salesChannel,
}, {
headers: {
cookie: req.headers.get("cookie") ?? "",
"x-vtex-rec-origin": origin,
},
}).then((res) => res.json());
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how other VTEX loaders handle segment cookie
echo "=== How withSegmentCookie is used elsewhere ==="
rg -n "withSegmentCookie" --type=ts -C3 vtex/loaders/

echo ""
echo "=== Check segment utility implementation ==="
cat vtex/utils/segment.ts | head -60

Repository: deco-cx/apps

Length of output: 15921


🏁 Script executed:

cat vtex/utils/segment.ts | sed -n '30,120p'

Repository: deco-cx/apps

Length of output: 2752


🏁 Script executed:

rg -n "withSegmentCookie" vtex/utils/segment.ts -A 20

Repository: deco-cx/apps

Length of output: 509


🏁 Script executed:

sed -n '100,140p' vtex/loaders/recommendations/productList.ts

Repository: deco-cx/apps

Length of output: 1413


🏁 Script executed:

sed -n '1,115p' vtex/loaders/recommendations/productList.ts | tail -40

Repository: deco-cx/apps

Length of output: 1486


🏁 Script executed:

sed -n '1,20p' vtex/loaders/recommendations/productList.ts

Repository: deco-cx/apps

Length of output: 696


Use withSegmentCookie() to pass segment context to the recommendations API.

The segment variable is available from getSegmentFromBag(ctx) but is only used for salesChannel and currencyCode values. The cookie header should use withSegmentCookie(segment) instead of the raw request cookie, consistent with all other VTEX loaders (intelligentSearch, legacy, categories). This ensures the recommendations API receives the proper vtex_segment token for contextualized results.

Import withSegmentCookie from ../../utils/segment.ts and update the headers:

headers: {
  ...withSegmentCookie(segment),
  "x-vtex-rec-origin": origin,
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/loaders/recommendations/productList.ts` around lines 114 - 128, Replace
the raw cookie header when calling the recommendations BFF so the request
includes the VTEX segment token: import withSegmentCookie from
../../utils/segment.ts, ensure you use the `segment` value obtained from
`getSegmentFromBag(ctx)`, and update the headers passed to bff["GET
/api/recommend-bff/v2/recommendations"] to spread `withSegmentCookie(segment)`
and include "x-vtex-rec-origin": origin instead of using
req.headers.get("cookie") directly; keep the rest of the request payload
(userId, products, salesChannel, etc.) unchanged.

Comment on lines +132 to +168
return (response.products as unknown as LegacyProduct[]).map((product) =>
toProduct(
product,
product.items.find(getFirstItemAvailable) ?? product.items[0],
0,
{
baseUrl,
priceCurrency: segment?.payload?.currencyCode ?? "BRL",
isVariantOfAdditionalProperty: [
{
"@type": "PropertyValue",
name: "correlationId",
value: response.correlationId,
valueReference: "RECOMMENDATION",
},
{
"@type": "PropertyValue",
name: "campaignId",
value: response.campaign?.id,
valueReference: "RECOMMENDATION",
},
{
"@type": "PropertyValue",
name: "campaignTitle",
value: response.campaign?.title,
valueReference: "RECOMMENDATION",
},
{
"@type": "PropertyValue",
name: "campaignType",
value: response.campaign?.type,
valueReference: "RECOMMENDATION",
},
],
},
)
);
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.

⚠️ Potential issue | 🟡 Minor

Add defensive check for response.products before mapping.

The code assumes response.products exists and is iterable. If the BFF returns an error response or a malformed payload, calling .map() on undefined/non-array will throw a TypeError.

🛡️ Suggested fix
     const baseUrl = new URL(req.url).origin;

+    if (!Array.isArray(response.products)) {
+      console.warn("BFF returned no products or invalid response", response);
+      return null;
+    }
+
     return (response.products as unknown as LegacyProduct[]).map((product) =>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/loaders/recommendations/productList.ts` around lines 132 - 168, The
mapping over response.products can throw if products is undefined or not an
array; update the product list loader to defensively handle this by checking
response.products before mapping (e.g., ensure Array.isArray(response.products)
and fallback to an empty array), then call .map(...) using the same
toProduct(...) call and getFirstItemAvailable helper; ensure the code still
returns an empty array or appropriate default when products is missing so
downstream code doesn't receive undefined.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant