Skip to content
Open
52 changes: 52 additions & 0 deletions vtex/actions/cart/updateCustomData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { AppContext } from "../../mod.ts";
import { proxySetCookie } from "../../utils/cookies.ts";
import { parseCookie } from "../../utils/orderForm.ts";
import type { OrderForm } from "../../utils/types.ts";

export interface Props {
appId: string;
// deno-lint-ignore no-explicit-any
body: any;
}

/**
* @title Update Custom Data
* @description Update the custom data in the cart
*/
const action = async (
props: Props,
req: Request,
ctx: AppContext,
): Promise<OrderForm> => {
const { vcsDeprecated } = ctx;
const {
appId,
body,
} = props;
const { orderFormId } = parseCookie(req.headers);

if (!orderFormId || orderFormId === "") {
throw new Error("Order form ID is required");
}

const cookie = req.headers.get("cookie") ?? "";

const response = await vcsDeprecated
["PUT /api/checkout/pub/orderForm/:orderFormId/customData/:appId"]({
orderFormId,
appId,
}, {
body,
headers: {
accept: "application/json",
"content-type": "application/json",
cookie,
},
});

proxySetCookie(response.headers, ctx.response.headers, req.url);

return response.json();
};

export default action;
62 changes: 62 additions & 0 deletions vtex/actions/events/productView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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.
*/
Comment on lines +12 to +14

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.

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 action(
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") || "",
cookie: req.headers.get("cookie") || "",
},
});

return { ok: true };
}
59 changes: 59 additions & 0 deletions vtex/actions/events/recommendationClick.ts
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 };
}
59 changes: 59 additions & 0 deletions vtex/actions/events/recommendationView.ts
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 };
}
46 changes: 46 additions & 0 deletions vtex/actions/recommendation/startSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { AppContext } from "../../mod.ts";
import { proxySetCookie } from "../../utils/cookies.ts";
import { parseCookie as parseOrderformCookie } from "../../utils/orderForm.ts";
import { parseCookie as parseRecommendationsCookie } from "../../utils/recommendations.ts";

export default async function action(
_: unknown,
req: Request,
ctx: AppContext,
) {
const { bff } = ctx;
const { orderFormId } = parseOrderformCookie(req.headers);
const { userId } = parseRecommendationsCookie(req.headers);
Comment on lines +12 to +13

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.

if (userId) {
return { recommendationsUserId: userId };
}

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

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.

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();

proxySetCookie(response.headers, ctx.response.headers, req.url);

return data;
}
Loading
Loading