-
Notifications
You must be signed in to change notification settings - Fork 29
Feat(VTEX): Recommendations BFF API #1532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
5e5dd88
807adf9
2172269
29beb57
646aaba
ca9d67b
fd5cbcd
2811c9d
25a5da7
9379bd1
30661af
c8840f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import type { AppContext } from "../../mod.ts"; | ||
| import { getOrigin, parseCookie } from "../../utils/recommendations.ts"; | ||
| import type { ProductViewSource } from "../../utils/types.ts"; | ||
|
|
||
| interface Props { | ||
| /** | ||
| * @title User ID | ||
| * @description The ID of the user who viewed the recommendation. | ||
| */ | ||
| userId?: string; | ||
| /** | ||
| * @title Product ID | ||
| * @description The ID of the product that was clicked. | ||
| */ | ||
| productId: string; | ||
| /** | ||
| * @title Source | ||
| * @description The source of the product view event. | ||
| */ | ||
| source: ProductViewSource; | ||
| /** | ||
| * @description The origin of the recommendation request. E.g: apiexamples/storefront/vtex.recommendation-shelf@2.x | ||
| */ | ||
| "x-vtex-rec-origin"?: string; | ||
| } | ||
|
|
||
| export default async function loader( | ||
| props: Props, | ||
| req: Request, | ||
| ctx: AppContext, | ||
| ) { | ||
| const { bff } = ctx; | ||
|
|
||
| const origin = getOrigin(req, ctx.account, props["x-vtex-rec-origin"]); | ||
| if (!origin) { | ||
| throw new Error("x-vtex-rec-origin header is required"); | ||
| } | ||
|
|
||
| const cookies = parseCookie(req.headers); | ||
| const userId = props.userId ?? cookies?.userId; | ||
|
|
||
| if (!userId) { | ||
| throw new Error("userId is required"); | ||
| } | ||
|
|
||
| await bff["POST /api/recommend-bff/v2/events/product-view"]({ | ||
| an: ctx.account, | ||
| }, { | ||
| body: { | ||
| userId, | ||
| product: props.productId, | ||
| source: props.source, | ||
| }, | ||
| headers: { | ||
| "x-vtex-rec-origin": origin, | ||
| "user-agent": req.headers.get("user-agent") || "", | ||
| }, | ||
| }); | ||
|
|
||
| return { ok: true }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { AppContext } from "../../mod.ts"; | ||
| import { getOrigin, parseCookie } from "../../utils/recommendations.ts"; | ||
|
|
||
| interface Props { | ||
| /** | ||
| * @title User ID | ||
| * @description The ID of the user who viewed the recommendation. | ||
| */ | ||
| userId?: string; | ||
| /** | ||
| * @title Product ID | ||
| * @description The ID of the product that was clicked. | ||
| */ | ||
| productId: string; | ||
| /** | ||
| * @title Correlation ID | ||
| * @description The correlation ID of the recommendation request. | ||
| */ | ||
| correlationId: string; | ||
| /** | ||
| * @description The origin of the recommendation request. E.g: apiexamples/storefront/vtex.recommendation-shelf@2.x | ||
| */ | ||
| "x-vtex-rec-origin"?: string; | ||
| } | ||
|
|
||
| export default async function loader( | ||
| props: Props, | ||
| req: Request, | ||
| ctx: AppContext, | ||
| ) { | ||
| const { bff } = ctx; | ||
|
|
||
| const origin = getOrigin(req, ctx.account, props["x-vtex-rec-origin"]); | ||
| if (!origin) { | ||
| throw new Error("x-vtex-rec-origin header is required"); | ||
| } | ||
|
|
||
| const cookies = parseCookie(req.headers); | ||
| const userId = props.userId ?? cookies?.userId; | ||
|
|
||
| if (!userId) { | ||
| throw new Error("userId is required"); | ||
| } | ||
|
|
||
| await bff["POST /api/recommend-bff/v2/events/recommendation-click"]({ | ||
| an: ctx.account, | ||
| }, { | ||
| body: { | ||
| correlationId: props.correlationId, | ||
| userId, | ||
| product: props.productId, | ||
| }, | ||
| headers: { | ||
| "x-vtex-rec-origin": origin, | ||
| }, | ||
| }); | ||
|
|
||
| return { ok: true }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { AppContext } from "../../mod.ts"; | ||
| import { getOrigin, parseCookie } from "../../utils/recommendations.ts"; | ||
|
|
||
| interface Props { | ||
| /** | ||
| * @title User ID | ||
| * @description The ID of the user who viewed the recommendation. | ||
| */ | ||
| userId?: string; | ||
| /** | ||
| * @title Correlation ID | ||
| * @description The correlation ID of the recommendation request. | ||
| */ | ||
| correlationId: string; | ||
| /** | ||
| * @title Products | ||
| * @description The products SKU IDs that were viewed. | ||
| */ | ||
| products: string[]; | ||
| /** | ||
| * @description The origin of the recommendation request. E.g: apiexamples/storefront/vtex.recommendation-shelf@2.x | ||
| */ | ||
| "x-vtex-rec-origin"?: string; | ||
| } | ||
|
|
||
| export default async function loader( | ||
| props: Props, | ||
| req: Request, | ||
| ctx: AppContext, | ||
| ) { | ||
| const { bff } = ctx; | ||
|
|
||
| const origin = getOrigin(req, ctx.account, props["x-vtex-rec-origin"]); | ||
| if (!origin) { | ||
| throw new Error("x-vtex-rec-origin header is required"); | ||
| } | ||
|
|
||
| const cookies = parseCookie(req.headers); | ||
| const userId = props.userId ?? cookies?.userId; | ||
|
|
||
| if (!userId) { | ||
| throw new Error("userId is required"); | ||
| } | ||
|
|
||
| await bff["POST /api/recommend-bff/v2/events/recommendation-view"]({ | ||
| an: ctx.account, | ||
| }, { | ||
| body: { | ||
| correlationId: props.correlationId, | ||
| userId, | ||
| products: props.products, | ||
| }, | ||
| headers: { | ||
| "x-vtex-rec-origin": origin, | ||
| }, | ||
| }); | ||
|
|
||
| return { ok: true }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { getSetCookies } from "std/http/cookie.ts"; | ||
| import type { AppContext } from "../../mod.ts"; | ||
| import { proxySetCookie } from "../../utils/cookies.ts"; | ||
| import { parseCookie } from "../../utils/orderForm.ts"; | ||
|
|
||
| export default async function action( | ||
| _: unknown, | ||
| req: Request, | ||
| ctx: AppContext, | ||
| ) { | ||
| const { bff } = ctx; | ||
| const { orderFormId } = parseCookie(req.headers); | ||
|
|
||
| const url = new URL(req.url); | ||
| const host = url.host; | ||
|
|
||
| const headers = new Headers(); | ||
| headers.set( | ||
| "x-vtex-rec-origin", | ||
| `${ctx.account}/storefront/deco.recommendations@1.x`, | ||
| ); | ||
|
Comment on lines
+21
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Respect caller-provided The PR contract says callers can send 🤖 Prompt for AI Agents |
||
| headers.set("x-forwarded-host", host); | ||
| headers.set("host", `${ctx.account}.vtexcommercestable.com.br`); | ||
|
|
||
| const cookie = req.headers.get("cookie"); | ||
| if (cookie) { | ||
| headers.set("cookie", cookie); | ||
| } | ||
|
|
||
| const response = await bff["POST /api/recommend-bff/v2/users/start-session"]({ | ||
| an: ctx.account, | ||
| }, { | ||
| body: { orderFormId }, | ||
| headers, | ||
| }); | ||
|
|
||
| const data = await response.json(); | ||
|
|
||
| console.log(data, getSetCookies(response.headers)); | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| proxySetCookie(response.headers, ctx.response.headers, req.url); | ||
|
|
||
| return data; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,169 @@ | ||||||||||||||||||||||||||||||||||||||||||
| import { Product } from "../../../commerce/types.ts"; | ||||||||||||||||||||||||||||||||||||||||||
| import { AppContext } from "../../mod.ts"; | ||||||||||||||||||||||||||||||||||||||||||
| import { getOrigin, parseCookie } from "../../utils/recommendations.ts"; | ||||||||||||||||||||||||||||||||||||||||||
| import { getSegmentFromBag, withSegmentCookie } from "../../utils/segment.ts"; | ||||||||||||||||||||||||||||||||||||||||||
| import { toProduct } from "../../utils/transform.ts"; | ||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||
| CampaignType, | ||||||||||||||||||||||||||||||||||||||||||
| LegacyProduct, | ||||||||||||||||||||||||||||||||||||||||||
| ProductListToId, | ||||||||||||||||||||||||||||||||||||||||||
| } from "../../utils/types.ts"; | ||||||||||||||||||||||||||||||||||||||||||
| import { getFirstItemAvailable } from "../legacy/productListingPage.ts"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| type CampaignTypeLabel = | ||||||||||||||||||||||||||||||||||||||||||
| | "Best sellers" | ||||||||||||||||||||||||||||||||||||||||||
| | "Personalized recommendations" | ||||||||||||||||||||||||||||||||||||||||||
| | "Similar items" | ||||||||||||||||||||||||||||||||||||||||||
| | "Cross-sell" | ||||||||||||||||||||||||||||||||||||||||||
| | "Cart-based recommendations" | ||||||||||||||||||||||||||||||||||||||||||
| | "Last seen" | ||||||||||||||||||||||||||||||||||||||||||
| | "Recent interactions" | ||||||||||||||||||||||||||||||||||||||||||
| | "Visual similarity" | ||||||||||||||||||||||||||||||||||||||||||
| | "Search-based recommendations" | ||||||||||||||||||||||||||||||||||||||||||
| | "Next interaction"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const campaignTypeMap: Record<CampaignTypeLabel, CampaignType> = { | ||||||||||||||||||||||||||||||||||||||||||
| "Best sellers": "rec-top-items-v2", | ||||||||||||||||||||||||||||||||||||||||||
| "Personalized recommendations": "rec-persona-v2", | ||||||||||||||||||||||||||||||||||||||||||
| "Similar items": "rec-similar-v2", | ||||||||||||||||||||||||||||||||||||||||||
| "Cross-sell": "rec-cross-v2", | ||||||||||||||||||||||||||||||||||||||||||
| "Cart-based recommendations": "rec-cart-v2", | ||||||||||||||||||||||||||||||||||||||||||
| "Last seen": "rec-last-v2", | ||||||||||||||||||||||||||||||||||||||||||
| "Recent interactions": "rec-interactions-v2", | ||||||||||||||||||||||||||||||||||||||||||
| "Visual similarity": "rec-visual-v2", | ||||||||||||||||||||||||||||||||||||||||||
| "Search-based recommendations": "rec-search-v2", | ||||||||||||||||||||||||||||||||||||||||||
| "Next interaction": "rec-next-v2", | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| interface Props { | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * @title Campaign Type | ||||||||||||||||||||||||||||||||||||||||||
| * @description The type of campaign to fetch recommendations for. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| campaignType: CampaignTypeLabel; | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * @title Campaign ID | ||||||||||||||||||||||||||||||||||||||||||
| * @description Contact https://supporticket.vtex.com/support to get the campaign ID. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| campaignId: string; | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * @title Products | ||||||||||||||||||||||||||||||||||||||||||
| * @description List of product IDs for context-based recommendations. For similar items and cross-sell, send only one product ID. For cart recommendations, send all product IDs in the user's cart. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| products?: ProductListToId; | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * @title Zip Code | ||||||||||||||||||||||||||||||||||||||||||
| * @description Zip code for location-based recommendations. If not provided, it is extracted from the segment cookie. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| zipCode?: string; | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * @title Pickup Point | ||||||||||||||||||||||||||||||||||||||||||
| * @description Pickup point identifier for location-based recommendations. If not provided, it is extracted from the segment cookie. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| pickupPoint?: string; | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * @title Region ID | ||||||||||||||||||||||||||||||||||||||||||
| * @description Region identifier for location-based recommendations. If not provided, it is extracted from the segment cookie. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| regionId?: string; | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * @ignore true | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| "x-vtex-rec-origin"?: string; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * @title VTEX Recommendations Product List | ||||||||||||||||||||||||||||||||||||||||||
| * @description Get a list of products from the VTEX Recommendations API | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export default async function loader( | ||||||||||||||||||||||||||||||||||||||||||
| props: Props, | ||||||||||||||||||||||||||||||||||||||||||
| req: Request, | ||||||||||||||||||||||||||||||||||||||||||
| ctx: AppContext, | ||||||||||||||||||||||||||||||||||||||||||
| ): Promise<Product[] | null> { | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| const { bff, account } = ctx; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const origin = getOrigin(req, ctx.account, props["x-vtex-rec-origin"]); | ||||||||||||||||||||||||||||||||||||||||||
| const segment = getSegmentFromBag(ctx); | ||||||||||||||||||||||||||||||||||||||||||
| const cookies = parseCookie(req.headers); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| let userId = cookies?.userId; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (ctx.advancedConfigs?.autoStartRecommendationSession && !userId) { | ||||||||||||||||||||||||||||||||||||||||||
| const response = await ctx.invoke.vtex.actions.recommendation | ||||||||||||||||||||||||||||||||||||||||||
| .startSession(); | ||||||||||||||||||||||||||||||||||||||||||
| userId = response.recommendationsUserId; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const campaignType = campaignTypeMap[props.campaignType]; | ||||||||||||||||||||||||||||||||||||||||||
| const campaignVrn = | ||||||||||||||||||||||||||||||||||||||||||
| `vrn:recommendations:${account}:${campaignType}:${props.campaignId}`; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // From vtex docs: | ||||||||||||||||||||||||||||||||||||||||||
| // "For similar items and cross-sell, send only one product ID. | ||||||||||||||||||||||||||||||||||||||||||
| // For cart recommendations, send all product IDs in the user's cart." | ||||||||||||||||||||||||||||||||||||||||||
| const products = | ||||||||||||||||||||||||||||||||||||||||||
| campaignType === "rec-cross-v2" || campaignType === "rec-similar-v2" | ||||||||||||||||||||||||||||||||||||||||||
| ? [props.products?.[0]] | ||||||||||||||||||||||||||||||||||||||||||
| : props.products; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const productIds = products?.filter(Boolean).join(","); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+107
to
+113
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate required product context for cross/similar campaigns. Lines 106-112 allow missing product IDs for 🛡️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| 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 }, | ||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.tsRepository: deco-cx/apps Length of output: 858 Segment cookie is dropped when spreading Headers into a plain object. Line 123 spreads 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 |
||||||||||||||||||||||||||||||||||||||||||
| }).then((res) => res.json()); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+114
to
+128
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -60Repository: 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 20Repository: deco-cx/apps Length of output: 509 🏁 Script executed: sed -n '100,140p' vtex/loaders/recommendations/productList.tsRepository: deco-cx/apps Length of output: 1413 🏁 Script executed: sed -n '1,115p' vtex/loaders/recommendations/productList.ts | tail -40Repository: deco-cx/apps Length of output: 1486 🏁 Script executed: sed -n '1,20p' vtex/loaders/recommendations/productList.tsRepository: deco-cx/apps Length of output: 696 Use The Import headers: {
...withSegmentCookie(segment),
"x-vtex-rec-origin": origin,
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const baseUrl = new URL(req.url).origin; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| 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", | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+132
to
+168
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add defensive check for The code assumes 🛡️ 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 |
||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
| console.error(error); | ||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix product-view prop description typo.
Line 13 says the product was “clicked”, but this loader emits a product-view event.
✏️ Suggested edit
📝 Committable suggestion
🤖 Prompt for AI Agents