Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 30 additions & 9 deletions vtex/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { getCookies } from "std/http/cookie.ts";
import { PAGE_CACHE_ALLOWED_KEY, PAGE_DIRTY_KEY } from "@deco/deco/blocks";
import { AppMiddlewareContext } from "./mod.ts";
import {
getISCookiesFromBag,
setISCookiesBag,
} from "./utils/intelligentSearch.ts";
import { getSegmentFromBag, setSegmentBag } from "./utils/segment.ts";
import {
getSegmentFromBag,
isCacheableSegment,
setSegmentBag,
} from "./utils/segment.ts";
import { VTEX_ID_CLIENT_COOKIE } from "./utils/vtexId.ts";

export const middleware = (
_props: unknown,
Expand All @@ -13,17 +19,32 @@ export const middleware = (
) => {
const segment = getSegmentFromBag(ctx);
const isCookies = getISCookiesFromBag(ctx);
const cookies = getCookies(req.headers);

if (!isCookies) {
setISCookiesBag(cookies, ctx);
}

if (!isCookies || !segment) {
const cookies = getCookies(req.headers);
if (!segment) {
setSegmentBag(cookies, req, ctx);
}

if (!isCookies) {
setISCookiesBag(cookies, ctx);
}
const isLoggedIn = Boolean(
cookies[VTEX_ID_CLIENT_COOKIE] ||
cookies[`${VTEX_ID_CLIENT_COOKIE}_${ctx.account}`]
);

const cacheable = isCacheableSegment(ctx) && !isLoggedIn;

// PAGE_DIRTY_KEY: marks page dirty for section-level caching and other consumers
if (!cacheable) {
ctx.bag.set(PAGE_DIRTY_KEY, true);
ctx.response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate");
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Formatting check is currently failing CI.

The pipeline reports deno fmt --check failure; please run formatter and commit the result.

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

In `@vtex/middleware.ts` at line 42, The file vtex/middleware.ts fails deno fmt
--check due to formatting; run the Deno formatter (deno fmt) on the repository
or specifically on vtex/middleware.ts, verify the change around the
ctx.response.headers.set(...) line is applied, and commit the formatted file so
CI passes the formatting check.

}

if (!segment) {
setSegmentBag(cookies, req, ctx);
}
// PAGE_CACHE_ALLOWED_KEY: opts in to CDN page caching (VTEX-only)
if (cacheable) {
ctx.bag.set(PAGE_CACHE_ALLOWED_KEY, true);
}

return ctx.next!();
Expand Down
63 changes: 45 additions & 18 deletions vtex/sections/Analytics/Vtex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,54 @@ import {
SelectItemEvent,
} from "../../../commerce/types.ts";
import { AppContext } from "../../mod.ts";
import { getISCookiesFromBag } from "../../utils/intelligentSearch.ts";
import {
ANONYMOUS_COOKIE,
SESSION_COOKIE,
} from "../../utils/intelligentSearch.ts";
import { SPEvent } from "../../utils/types.ts";
import { type SectionProps } from "@deco/deco";
import { useScriptAsDataURI } from "@deco/deco/hooks";
interface ISCookies {
// deno-lint-ignore no-explicit-any
anonymous: any;
// deno-lint-ignore no-explicit-any
session: any;
}
const snippet = (account: string, agent: string, cookies: ISCookies | null) => {

const ONE_YEAR_SECS = 365 * 24 * 3600;
const THIRTY_MIN_SECS = 30 * 60;

const getCookie = (name: string): string | null => {
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
return match ? match[1] : null;
};

const setCookie = (name: string, value: string, maxAge: number) => {
document.cookie =
`${name}=${value};path=/;max-age=${maxAge};secure;SameSite=Lax`;
};

const getOrCreateISCookies = () => {
let anonymous = getCookie(ANONYMOUS_COOKIE);
if (!anonymous) {
anonymous = crypto.randomUUID();
setCookie(ANONYMOUS_COOKIE, anonymous, ONE_YEAR_SECS);
}

let session = getCookie(SESSION_COOKIE);
if (!session) {
session = crypto.randomUUID();
}
// Always re-set session cookie to simulate sliding expiration
setCookie(SESSION_COOKIE, session, THIRTY_MIN_SECS);

return { anonymous, session };
};

const snippet = (account: string) => {
const cookies = getOrCreateISCookies();

const url = new URL(globalThis.location.href);
const isSearch = url.searchParams.get("q");
const apiUrl = `https://sp.vtex.com/event-api/v1/${account}/event`;
const baseProps = {
agent: navigator.userAgent,
...cookies,
};
const eventFetch = (props: SPEvent) => {
fetch(apiUrl, {
method: "POST",
Expand All @@ -30,10 +64,6 @@ const snippet = (account: string, agent: string, cookies: ISCookies | null) => {
},
});
};
const baseProps = {
agent,
...cookies,
};
// deno-lint-ignore no-explicit-any
function isSelectItemEvent(event: any): event is SelectItemEvent {
return event.name === "select_item";
Expand Down Expand Up @@ -92,21 +122,18 @@ const snippet = (account: string, agent: string, cookies: ISCookies | null) => {
});
};
export default function VtexAnalytics(
{ account, agent, cookies }: SectionProps<typeof loader>,
{ account }: SectionProps<typeof loader>,
) {
return (
<script
type="text/javascript"
defer
src={useScriptAsDataURI(snippet, account, agent, cookies)}
src={useScriptAsDataURI(snippet, account)}
/>
);
}
export const loader = (_props: unknown, req: Request, ctx: AppContext) => {
const cookies = getISCookiesFromBag(ctx);
export const loader = (_props: unknown, _req: Request, ctx: AppContext) => {
return {
account: ctx.account,
agent: req.headers.get("user-agent") || "deco-sites/apps",
cookies,
};
};
19 changes: 0 additions & 19 deletions vtex/utils/intelligentSearch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { setCookie } from "std/http/mod.ts";
import { AppContext } from "../mod.ts";
import { STALE } from "../../utils/fetch.ts";
import type {
Expand Down Expand Up @@ -100,28 +99,10 @@ export const setISCookiesBag = (

if (!anonymous) {
anonymous = crypto.randomUUID();

setCookie(ctx.response.headers, {
value: anonymous,
name: ANONYMOUS_COOKIE,
path: "/",
secure: true,
httpOnly: true,
maxAge: 365 * 24 * 3600,
});
}

if (!session) {
session = crypto.randomUUID();

setCookie(ctx.response.headers, {
value: session,
name: SESSION_COOKIE,
path: "/",
secure: true,
httpOnly: true,
maxAge: 30 * 60,
});
}

ctx?.bag.set(IS_ANONYMOUS, anonymous);
Expand Down
60 changes: 42 additions & 18 deletions vtex/utils/segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ export const isAnonymous = (
!regionId;
};

/**
* Checks if the segment is cacheable for CDN purposes.
* By default, uses isAnonymous (UTMs affect cacheability because prices
* can vary by utm_source). With removeUTMFromCacheKey, UTMs are ignored
* (opt-in for stores that don't vary prices by UTM).
*/
export const isCacheableSegment = (ctx: AppContext) => {
const payload = getSegmentFromBag(ctx)?.payload;
if (payload?.channelPrivacy === "private") return false;

if (ctx.advancedConfigs?.removeUTMFromCacheKey) {
if (!payload) return true;
const { campaigns, channel, priceTables, regionId } = payload;
return !campaigns &&
(!channel || isDefautSalesChannel(ctx, channel)) &&
!priceTables && !regionId;
}
return isAnonymous(ctx);
};

const setSegmentInBag = (ctx: AppContext, data: WrappedSegment) =>
ctx?.bag?.set(SEGMENT, data);

Expand Down Expand Up @@ -223,24 +243,28 @@ export const setSegmentBag = (
const token = serialize(segment);
setSegmentInBag(ctx, { payload: segment, token });

// If the user came from a sales channel in the URL, we set the cookie
if (segmentFromRequest.channel) {
setCookie(ctx.response.headers, {
value: `sc=${segmentFromRequest.channel}`,
name: SALES_CHANNEL_COOKIE,
path: "/",
secure: true,
});
}
// Skip Set-Cookie when the segment only differs by UTMs.
// UTMs don't affect page content, so the response can still be cached.
// Only set cookies when content-affecting fields differ (campaigns,
// non-default sales channel, price tables, region).
if (!isCacheableSegment(ctx)) {
if (segmentFromRequest.channel) {
setCookie(ctx.response.headers, {
value: `sc=${segmentFromRequest.channel}`,
name: SALES_CHANNEL_COOKIE,
path: "/",
secure: true,
});
}

// Avoid setting cookie when segment from request matches the one generated
if (vtex_segment !== token) {
setCookie(ctx.response.headers, {
value: token,
name: SEGMENT_COOKIE_NAME,
path: "/",
secure: true,
httpOnly: true,
});
if (vtex_segment !== token) {
setCookie(ctx.response.headers, {
value: token,
name: SEGMENT_COOKIE_NAME,
path: "/",
secure: true,
httpOnly: true,
});
}
}
Comment on lines +246 to 269
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all TypeScript/JavaScript files that might contain Analytics snippet or client-side cookie handling
fd -t f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) | head -20

Repository: deco-cx/apps

Length of output: 286


🏁 Script executed:

#!/bin/bash
# Search for Analytics-related files
find . -type f -name "*analytics*" -o -name "*Analytics*" | grep -v node_modules | head -20

Repository: deco-cx/apps

Length of output: 687


🏁 Script executed:

#!/bin/bash
# Search for document.cookie usage across the codebase
rg "document\.cookie" -t ts -t tsx -t js -A 2 -B 2 | head -100

Repository: deco-cx/apps

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Search for segment cookie handling (vtex_segment, SEGMENT_COOKIE_NAME, etc.)
rg "vtex_segment|SEGMENT_COOKIE_NAME|SALES_CHANNEL_COOKIE" -t ts -t tsx -t js -A 2 -B 2 | head -100

Repository: deco-cx/apps

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Check the current file to understand the cookie constants being used
cat -n vtex/utils/segment.ts | sed -n '1,50p'

Repository: deco-cx/apps

Length of output: 1586


🏁 Script executed:

#!/bin/bash
# View lines 244-280 of segment.ts to see full context around the reviewed code
cat -n vtex/utils/segment.ts | sed -n '244,280p'

Repository: deco-cx/apps

Length of output: 966


🏁 Script executed:

#!/bin/bash
# Find the isCacheableSegment function definition
rg "isCacheableSegment" vtex/utils/segment.ts -B 5 -A 10

Repository: deco-cx/apps

Length of output: 1280


🏁 Script executed:

#!/bin/bash
# Check the VTEX Analytics section implementation
find ./vtex/sections/Analytics -type f \( -name "*.ts" -o -name "*.js" \) | xargs cat

Repository: deco-cx/apps

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Search for client-side cookie persistence in VTEX-related analytics
rg "document\.cookie" . -g "vtex*" -B 2 -A 2 2>/dev/null | head -80

Repository: deco-cx/apps

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Check the main DecoAnalytics component for client-side cookie handling
cat analytics/components/DecoAnalytics.tsx 2>/dev/null | head -150

Repository: deco-cx/apps

Length of output: 3138


🏁 Script executed:

#!/bin/bash
# Check the analytics loader script
cat analytics/loaders/DecoAnalyticsScript.ts 2>/dev/null

Repository: deco-cx/apps

Length of output: 2977


🏁 Script executed:

#!/bin/bash
# Search for any client-side cookie logic across analytics files
rg "document\.cookie|localStorage|sessionStorage" . -g "analytics*" -g "*Analytics*" -B 2 -A 2 2>/dev/null | head -80

Repository: deco-cx/apps

Length of output: 475


🏁 Script executed:

#!/bin/bash
# Check if there are any TypeScript/JavaScript files in the VTEX Analytics section
find ./vtex/sections/Analytics -type f 2>/dev/null | xargs ls -la

Repository: deco-cx/apps

Length of output: 139


🏁 Script executed:

#!/bin/bash
# Look for how segment token is handled in client-side code
rg "vtex_segment|SEGMENT_COOKIE_NAME" . --type js --type ts -B 3 -A 3 2>/dev/null | head -100

Repository: deco-cx/apps

Length of output: 6261


🏁 Script executed:

#!/bin/bash
# Check the VTEX Analytics implementation for segment cookie handling
cat ./vtex/sections/Analytics/Vtex.tsx

Repository: deco-cx/apps

Length of output: 3902


🏁 Script executed:

#!/bin/bash
# Search for any reference to setting cookies client-side in the entire codebase
rg "setCookie|document\.cookie\s*=" -t ts -t js --max-count=20 2>/dev/null | grep -v node_modules

Repository: deco-cx/apps

Length of output: 6856


🏁 Script executed:

#!/bin/bash
# Check if there's any client-side segment handling at all
rg "vtex_segment|SALES_CHANNEL_COOKIE|SEGMENT_COOKIE_NAME" . -t ts -t js --type-not minified -B 2 -A 2 2>/dev/null | grep -v "utils/segment.ts" | head -80

Repository: deco-cx/apps

Length of output: 3802


Critical: Client-side cookie persistence is not implemented for segment cookies.

The server-side logic correctly skips Set-Cookie headers for cacheable segments (lines 248-267). However, the original review claims that "client-side cookie persistence has moved to the Analytics snippet," but the VTEX Analytics implementation (vtex/sections/Analytics/Vtex.tsx) only handles vtex_is_anonymous and vtex_is_session cookies—not vtex_segment or VTEXSC.

This creates a gap: For cacheable segments (UTM-only or default), cookies are neither set server-side nor persisted client-side. Clarify the intended behavior:

  1. Is missing client-side persistence intentional (cacheable segments are stateless)?
  2. Or does the segment handling need to be added to the Analytics snippet alongside the existing Intelligent Search cookie persistence?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/utils/segment.ts` around lines 244 - 267, The code skips Set-Cookie for
cacheable segments (isCacheableSegment) but there is no client-side persistence
for segment cookies; update the Analytics snippet
(vtex/sections/Analytics/Vtex.tsx) to persist the same segment values the server
would set: when page context exposes vtex_segment (token) or
segmentFromRequest.channel, write client-side cookies with names
SEGMENT_COOKIE_NAME and SALES_CHANNEL_COOKIE respectively, using path="/" and
secure=true (note httpOnly cannot be set client-side), and ensure values mirror
those produced by setCookie in the server logic so cacheable segments still
persist on the client.

};
2 changes: 2 additions & 0 deletions website/matchers/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ const MatchDevice = (
return devices.includes(device);
};
export default MatchDevice;

export const cacheable = true;
2 changes: 2 additions & 0 deletions website/matchers/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ const MatchEnvironment = ({ environment }: Props) => {
};

export default MatchEnvironment;

export const cacheable = true;
2 changes: 2 additions & 0 deletions website/matchers/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ const MatchHost = ({ includes, match }: Props, { request }: MatchContext) => {
return regexMatch && includesFound;
};
export default MatchHost;

export const cacheable = true;
2 changes: 2 additions & 0 deletions website/matchers/pathname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ const MatchPathname = (props: Props, { request }: MatchContext) => {
return props.case.negate ? !result : result;
};
export default MatchPathname;

export const cacheable = true;
2 changes: 2 additions & 0 deletions website/matchers/queryString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,5 @@ const MatchQueryString = (props: Props, { request }: MatchContext) => {
return matches;
};
export default MatchQueryString;

export const cacheable = true;
2 changes: 2 additions & 0 deletions website/matchers/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ const MatchSite = ({ siteId }: Props, { siteId: currSiteId }: MatchContext) => {
return siteId === currSiteId;
};
export default MatchSite;

export const cacheable = true;
Loading