Repository: bigcommerce/catalyst
Pull request #2329: feat: add analytics cookies
Author: login: solofeed
Created: 2025-05-21T22:25:20Z | Merged: 2025-05-22T20:39:58Z

## What/Why?
Add analytics cookies to support native BigCommerce Analytics

## Testing
Added e2e test to verify cookies are set as expected

## Migration
Since this is an add-only change, no migration is needed beside pulling in the new code.


Diff:
diff --git a/.changeset/ready-parks-study.md b/.changeset/ready-parks-study.md
new file mode 100644
index 0000000000..dafaa9c4c3
--- /dev/null
+++ b/.changeset/ready-parks-study.md
@@ -0,0 +1,7 @@
+---
+"@bigcommerce/catalyst-core": minor
+---
+
+Adds analytics cookies needed for native analytics.
+
+This is a add-only change, so migration should be as simple as pulling in the new code.
diff --git a/.gitignore b/.gitignore
index 99e43d16d4..5807197810 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@ bigcommerce.graphql
 bigcommerce-graphql.d.ts
 .DS_Store
 coverage/
+.history
diff --git a/core/auth/index.ts b/core/auth/index.ts
index b10e50474c..24df348d69 100644
--- a/core/auth/index.ts
+++ b/core/auth/index.ts
@@ -1,5 +1,5 @@
 import { decodeJwt } from 'jose';
-import NextAuth, { type DefaultSession, type NextAuthConfig, User } from 'next-auth';
+import NextAuth, { type NextAuthConfig, User } from 'next-auth';
 import 'next-auth/jwt';
 import CredentialsProvider from 'next-auth/providers/credentials';
 import { getTranslations } from 'next-intl/server';
@@ -297,24 +297,3 @@ export const isLoggedIn = async () => {
 
   return Boolean(cat);
 };
-
-declare module 'next-auth' {
-  interface Session {
-    user?: DefaultSession['user'];
-  }
-
-  interface User {
-    name?: string | null;
-    email?: string | null;
-    cartId?: string | null;
-    customerAccessToken?: string;
-    impersonatorId?: string | null;
-  }
-}
-
-declare module 'next-auth/jwt' {
-  interface JWT {
-    id?: string;
-    user?: DefaultSession['user'];
-  }
-}
diff --git a/core/auth/types.ts b/core/auth/types.ts
new file mode 100644
index 0000000000..78ee9103ee
--- /dev/null
+++ b/core/auth/types.ts
@@ -0,0 +1,22 @@
+import { DefaultSession } from 'next-auth';
+
+declare module 'next-auth' {
+  interface Session {
+    user?: DefaultSession['user'];
+  }
+
+  interface User {
+    name?: string | null;
+    email?: string | null;
+    cartId?: string | null;
+    customerAccessToken?: string;
+    impersonatorId?: string | null;
+  }
+}
+
+declare module 'next-auth/jwt' {
+  interface JWT {
+    id?: string;
+    user?: DefaultSession['user'];
+  }
+}
diff --git a/core/lib/analytics/bigcommerce/index.ts b/core/lib/analytics/bigcommerce/index.ts
new file mode 100644
index 0000000000..4ca5eaa84b
--- /dev/null
+++ b/core/lib/analytics/bigcommerce/index.ts
@@ -0,0 +1,40 @@
+import { cookies } from 'next/headers';
+
+const VISITOR_COOKIE_NAME = 'catalyst.visitorId';
+const VISIT_COOKIE_NAME = 'catalyst.visitId';
+const VISITOR_DURATION = 400 * 24 * 60 * 60; // 400 days
+const VISIT_DURATION = 30 * 60; // 30 minutes
+
+export async function getVisitorIdCookie(): Promise<string | undefined> {
+  const cookieStore = await cookies();
+
+  return cookieStore.get(VISITOR_COOKIE_NAME)?.value;
+}
+
+export async function setVisitorIdCookie(visitorId: string): Promise<void> {
+  const cookieStore = await cookies();
+
+  cookieStore.set(VISITOR_COOKIE_NAME, visitorId, {
+    httpOnly: true,
+    secure: true,
+    path: '/',
+    maxAge: VISITOR_DURATION,
+  });
+}
+
+export async function getVisitIdCookie(): Promise<string | undefined> {
+  const cookieStore = await cookies();
+
+  return cookieStore.get(VISIT_COOKIE_NAME)?.value;
+}
+
+export async function setVisitIdCookie(visitId: string): Promise<void> {
+  const cookieStore = await cookies();
+
+  cookieStore.set(VISIT_COOKIE_NAME, visitId, {
+    httpOnly: true,
+    secure: true,
+    path: '/',
+    maxAge: VISIT_DURATION,
+  });
+}
diff --git a/core/middleware.ts b/core/middleware.ts
index c9928bdd70..c068579f37 100644
--- a/core/middleware.ts
+++ b/core/middleware.ts
@@ -1,10 +1,17 @@
 import { composeMiddlewares } from './middlewares/compose-middlewares';
+import { withAnalyticsCookies } from './middlewares/with-analytics-cookies';
 import { withAuth } from './middlewares/with-auth';
 import { withChannelId } from './middlewares/with-channel-id';
 import { withIntl } from './middlewares/with-intl';
 import { withRoutes } from './middlewares/with-routes';
 
-export const middleware = composeMiddlewares(withAuth, withIntl, withChannelId, withRoutes);
+export const middleware = composeMiddlewares(
+  withAuth,
+  withIntl,
+  withAnalyticsCookies,
+  withChannelId,
+  withRoutes,
+);
 
 export const config = {
   matcher: [
diff --git a/core/middlewares/with-analytics-cookies.ts b/core/middlewares/with-analytics-cookies.ts
new file mode 100644
index 0000000000..c7c1b91edd
--- /dev/null
+++ b/core/middlewares/with-analytics-cookies.ts
@@ -0,0 +1,39 @@
+import { NextRequest } from 'next/server';
+import { validate as isUuid, v4 as uuidv4 } from 'uuid';
+
+import {
+  getVisitIdCookie,
+  getVisitorIdCookie,
+  setVisitIdCookie,
+  setVisitorIdCookie,
+} from '~/lib/analytics/bigcommerce';
+
+import { MiddlewareFactory } from './compose-middlewares';
+
+export const withAnalyticsCookies: MiddlewareFactory = (next) => {
+  return async (request, event) => {
+    let visitorId = await getVisitorIdCookie();
+    let visitId = await getVisitIdCookie();
+
+    if (!visitorId || !isUuid(visitorId)) {
+      visitorId = uuidv4();
+    }
+
+    // Update the visitorId cookie every time
+    await setVisitorIdCookie(visitorId);
+
+    if (!visitId || !isUuid(visitId)) {
+      visitId = uuidv4();
+      await setVisitIdCookie(visitId);
+      event.waitUntil(sendShopperVisitEvent(request, visitorId, visitId));
+    }
+
+    return next(request, event);
+  };
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+async function sendShopperVisitEvent(request: NextRequest, visitorId: string, visitId: string) {
+  // fetch from request user agent, url and referer url
+  // TODO: Send shopper visit event to BigCommerce
+}
diff --git a/core/tests/fixtures/index.ts b/core/tests/fixtures/index.ts
index 000b71846e..45b5ac1bb6 100644
--- a/core/tests/fixtures/index.ts
+++ b/core/tests/fixtures/index.ts
@@ -1,4 +1,5 @@
-import { test as base } from '@playwright/test';
+import { expect as baseExpect, test as baseTest } from '@playwright/test';
+import { validate as isUuid } from 'uuid';
 
 import { AccountFactory } from './utils/account';
 import { OrderFactory } from './utils/order';
@@ -11,7 +12,7 @@ interface Fixtures {
   order: OrderFactory;
 }
 
-export const test = base.extend<Fixtures>({
+export const test = baseTest.extend<Fixtures>({
   account: [
     async ({ page }, use) => {
       const accountFactory = new AccountFactory(page);
@@ -34,4 +35,22 @@ export const test = base.extend<Fixtures>({
   ],
 });
 
-export { expect, type Page } from '@playwright/test';
+export const expect = baseExpect.extend({
+  toBeUuid(received) {
+    const pass = isUuid(received);
+
+    if (pass) {
+      return {
+        message: () => `expected ${received} not to be a uuid`,
+        pass: true,
+      };
+    }
+
+    return {
+      message: () => `expected ${received} to be a uuid`,
+      pass: false,
+    };
+  },
+});
+
+export { type Page } from '@playwright/test';
diff --git a/core/tests/ui/e2e/analytics-session.spec.ts b/core/tests/ui/e2e/analytics-session.spec.ts
new file mode 100644
index 0000000000..4ad25450bd
--- /dev/null
+++ b/core/tests/ui/e2e/analytics-session.spec.ts
@@ -0,0 +1,74 @@
+import { z } from 'zod';
+
+import { expect, test } from '~/tests/fixtures';
+
+const CookieSchema = z.object({
+  name: z.string(),
+  value: z.string(),
+  expires: z.number().optional(),
+});
+
+test.describe('Analytics cookies middleware', () => {
+  test('sets visitorId and visitId cookies on first visit', async ({ page, context }) => {
+    await page.goto('/');
+
+    const cookies = await context.cookies();
+    const visitorId = cookies.find((c) => c.name === 'catalyst.visitorId');
+    const visitId = cookies.find((c) => c.name === 'catalyst.visitId');
+
+    expect(visitorId).toBeDefined();
+    expect(visitorId?.value).toBeUuid();
+    expect(visitId).toBeDefined();
+    expect(visitId?.value).toBeUuid();
+  });
+
+  test('visitId cookie has correct expiry', async ({ page, context }) => {
+    await page.goto('/');
+
+    const cookies = await context.cookies();
+    const visitId = cookies.find((c) => c.name === 'visitId');
+    const parsed = CookieSchema.safeParse(visitId);
+
+    if (parsed.success && parsed.data.expires) {
+      const visitIdExpiry = new Date(parsed.data.expires * 1000);
+      const now = Date.now();
+
+      expect(visitIdExpiry.getTime()).toBeGreaterThan(now);
+      expect(visitIdExpiry.getTime()).toBeLessThan(now + 31 * 60 * 1000); // +1 minute buffer
+    }
+  });
+
+  test('visitorId cookie has correct expiry', async ({ page, context }) => {
+    await page.goto('/');
+
+    const cookies = await context.cookies();
+    const visitorId = cookies.find((c) => c.name === 'visitorId');
+    const parsed = CookieSchema.safeParse(visitorId);
+
+    if (parsed.success && parsed.data.expires) {
+      const visitorIdExpiry = new Date(parsed.data.expires * 1000);
+      const now = Date.now();
+
+      expect(visitorIdExpiry.getTime()).toBeGreaterThan(now);
+      expect(visitorIdExpiry.getTime()).toBeLessThan(now + 401 * 24 * 60 * 60 * 1000); // +1 day buffer
+    }
+  });
+
+  test('creates a new visitId after expiry', async ({ page, context }) => {
+    await page.goto('/');
+
+    let cookies = await context.cookies();
+    const oldVisitId = cookies.find((c) => c.name === 'catalyst.visitId')?.value;
+
+    // Simulate expiry by clearing the visitId cookie
+    await context.clearCookies();
+    await page.reload();
+
+    cookies = await context.cookies();
+
+    const newVisitId = cookies.find((c) => c.name === 'catalyst.visitId')?.value;
+
+    expect(newVisitId).toBeDefined();
+    expect(newVisitId).not.toBe(oldVisitId);
+  });
+});


Reviews:
user: login: chanceaclark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T22:48:24Z
user: login: BC-krasnoshapka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:12:07Z
user: login: solofeed
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:54:19Z
user: login: chanceaclark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T16:28:10Z
user: login: chanceaclark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T16:38:06Z
user: login: jorgemoya
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T20:37:17Z

Review comments:
diff_hunk: @@ -1,10 +1,17 @@
 import { composeMiddlewares } from './middlewares/compose-middlewares';
+import { withAnalyticsCookies } from './middlewares/with-analytics-cookies';
 import { withAuth } from './middlewares/with-auth';
 import { withChannelId } from './middlewares/with-channel-id';
 import { withIntl } from './middlewares/with-intl';
 import { withRoutes } from './middlewares/with-routes';
 
-export const middleware = composeMiddlewares(withAuth, withIntl, withChannelId, withRoutes);
+export const middleware = composeMiddlewares(
+  withAnalyticsCookies,
+  withAuth,
+  withIntl,
+  withChannelId,
+  withRoutes,
+);
path: core/middleware.ts
user: login: chanceaclark
body: Since middleware runs in order and the `withIntl` stuff does some interesting rewrites/redirects depending on the config, can we add this middleware either directly before or after `withChannelId`? It shouldn't matter before or after, just that it's after `withIntl`.
created_at: 2025-05-21T22:34:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,61 @@
+import { cookies } from 'next/headers';
+import { validate as isUuid, v4 as uuidv4 } from 'uuid';
+
+const VISITOR_COOKIE_NAME = 'visitorId';
+const VISIT_COOKIE_NAME = 'visitId';
path: core/lib/analytics/bigcommerce/index.ts
user: login: chanceaclark
body: Something I have been wanting to do is prefix cookies that Catalyst sets for better visibility what sets the cookie? Can we follow auth.js' footsteps here, e.g. `__Secure-authjs.session-token` and do something like `catalyst.visitId`.
created_at: 2025-05-21T22:35:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+import { setVisitIdCookie, setVisitorIdCookie } from '~/lib/analytics/bigcommerce';
+
+import { MiddlewareFactory } from './compose-middlewares';
+
+export const withAnalyticsCookies: MiddlewareFactory = (next) => {
+  return async (request, event) => {
+    await setVisitorIdCookie();
+    await setVisitIdCookie(onNewVisit);
+
path: core/middlewares/with-analytics-cookies.ts
user: login: chanceaclark
body: 🍹 Instead of a callback, can we return a new visit id value and handle that within the middleware request? It'll be easier to use `event.waitUntil` within the middleware body than having to pass it as an argument.

For example, you can trigger an API request without blocking the initial render using `event.waitUntil`:
```ts
async (request, event) => {
  await setVisitorIdCookie();
  
  const newVisitId = await setVisitIdCookie();

  if (newVisitId) {
    // middleware will continue and won't end the process until after the event is triggered
    event.waitUntil(() => {
      await sendShopperVisitEvent(newVisitId);
    });
  }
}
```
created_at: 2025-05-21T22:41:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+import { expect, test } from '@playwright/test';
+import { z } from 'zod';
+
+const uuidMatcher = /[0-9a-fA-F-]{16,}/;
path: core/tests/ui/e2e/analytics-session.spec.ts
user: login: chanceaclark
body: Can we extend expect and use the uuid validate function? Something like this?
```ts
import { validate as isUuid } from 'uuid';

expect.extend({
  toBeUuid(received) {
    const pass = isUuid(received);

    if (pass) {
      return {
        message: () =>`expected ${received} not to be a uuid`,
        pass: true,
      }
    }

    return {
      message: () =>`expected ${received} to be a uuid`,
      pass: false,
    }
  }
})
created_at: 2025-05-21T22:47:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+import { NextRequest } from 'next/server';
+
+import { setVisitIdCookie, setVisitorIdCookie } from '~/lib/analytics/bigcommerce';
+
+import { MiddlewareFactory } from './compose-middlewares';
+
+export const withAnalyticsCookies: MiddlewareFactory = (next) => {
+  return async (request, event) => {
+    const visitorId = await setVisitorIdCookie();
+    const { id: visitId, isNew: isVisitNew } = await setVisitIdCookie();
+
+    if (isVisitNew) {
+      event.waitUntil(sendShopperVisitEvent(request, visitorId, visitId));
+    }
+
+    return next(request, event);
+  };
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+async function sendShopperVisitEvent(request: NextRequest, visitorId: string, visitId: string) {
+  // fetch from request user agent, url and referer url
+  // TODO: Send shopper visit event to BigCommerce
+}
path: core/middlewares/with-analytics-cookies.ts
user: login: BC-krasnoshapka
body: Alternatively can [after](https://nextjs.org/docs/app/api-reference/functions/after) method be used for sending analytic events in non-blocking way, instead of middleware?
created_at: 2025-05-22T12:12:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+import { NextRequest } from 'next/server';
+
+import { setVisitIdCookie, setVisitorIdCookie } from '~/lib/analytics/bigcommerce';
+
+import { MiddlewareFactory } from './compose-middlewares';
+
+export const withAnalyticsCookies: MiddlewareFactory = (next) => {
+  return async (request, event) => {
+    const visitorId = await setVisitorIdCookie();
+    const { id: visitId, isNew: isVisitNew } = await setVisitIdCookie();
+
+    if (isVisitNew) {
+      event.waitUntil(sendShopperVisitEvent(request, visitorId, visitId));
+    }
+
+    return next(request, event);
+  };
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+async function sendShopperVisitEvent(request: NextRequest, visitorId: string, visitId: string) {
+  // fetch from request user agent, url and referer url
+  // TODO: Send shopper visit event to BigCommerce
+}
path: core/middlewares/with-analytics-cookies.ts
user: login: solofeed
body: We will consider this when will be implementing GQL request
created_at: 2025-05-22T12:54:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+import { NextRequest } from 'next/server';
+
+import { setVisitIdCookie, setVisitorIdCookie } from '~/lib/analytics/bigcommerce';
+
+import { MiddlewareFactory } from './compose-middlewares';
+
+export const withAnalyticsCookies: MiddlewareFactory = (next) => {
+  return async (request, event) => {
+    const visitorId = await setVisitorIdCookie();
+    const { id: visitId, isNew: isVisitNew } = await setVisitIdCookie();
+
+    if (isVisitNew) {
+      event.waitUntil(sendShopperVisitEvent(request, visitorId, visitId));
+    }
+
+    return next(request, event);
+  };
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+async function sendShopperVisitEvent(request: NextRequest, visitorId: string, visitId: string) {
+  // fetch from request user agent, url and referer url
+  // TODO: Send shopper visit event to BigCommerce
+}
path: core/middlewares/with-analytics-cookies.ts
user: login: chanceaclark
body: `waitUntil` has more deployment provider support than `after` does, so something to think about here.
created_at: 2025-05-22T16:28:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+import { NextRequest } from 'next/server';
+
+import { setVisitIdCookie, setVisitorIdCookie } from '~/lib/analytics/bigcommerce';
+
+import { MiddlewareFactory } from './compose-middlewares';
+
+export const withAnalyticsCookies: MiddlewareFactory = (next) => {
+  return async (request, event) => {
+    const visitorId = await setVisitorIdCookie();
+    const { id: visitId, isNew: isVisitNew } = await setVisitIdCookie();
+
+    if (isVisitNew) {
+      event.waitUntil(sendShopperVisitEvent(request, visitorId, visitId));
+    }
+
+    return next(request, event);
path: core/middlewares/with-analytics-cookies.ts
user: login: chanceaclark
body: The more I think about this, I think the `set*` functions shouldn't contain side effects 🤔 Can we move the logic into the middleware instead of magically handling it inside the fns?
```suggestion
    let visitorId = await getVisitorIdCookie();
    let visitId = await getVisitIdCookie();
    
    if (!visitorId) {
      visitorId = await setVisitorIdCookie();
    }

    if (!visitId) {
      visitId = await setVisitIdCookie();

      event.waitUntil(sendShopperVisitEvent(request, visitorId, visitId));
    }

    return next(request, event);
```
created_at: 2025-05-22T16:37:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-21T22:25:23Z
body: ###  🦋  Changeset detected

Latest commit: 685879e8732a7d8aa2a87496dc52c9c9b510e7aa

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                       | Type  |
| -------------------------- | ----- |
| @bigcommerce/catalyst-core | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/bigcommerce/catalyst/new/add-analytics-cookies?filename=.changeset/sweet-ways-live.md&value=---%0A%22%40bigcommerce%2Fcatalyst-core%22%3A%20patch%0A---%0A%0Afeat%3A%20add%20analytics%20cookies%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-05-21T22:25:25Z
body: [vc]: #MrP+QBXwJnKWuxWtkISKObbVSD7qey7s8WmqMFdZWYQ=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjYXRhbHlzdC11ayIsInJvb3REaXJlY3RvcnkiOiJjb3JlIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vYmlnY29tbWVyY2UtcGxhdGZvcm0vY2F0YWx5c3QtdWsvOG13OHNDNW52WHVQc1ljck1OYm5xVUpmbnBjOSIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwicHJldmlld1VybCI6ImNhdGFseXN0LXVrLWdpdC1hZGQtYW5hbHl0aWNzLWNvb2tpZXMtYmlnY29tbWVyY2UtcGxhdGZvcm0udmVyY2VsLmFwcCJ9LHsibmFtZSI6ImNhdGFseXN0LWNhbmFyeSIsInJvb3REaXJlY3RvcnkiOiJjb3JlIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2JpZ2NvbW1lcmNlLXBsYXRmb3JtL2NhdGFseXN0LWNhbmFyeS9BbzFNRk10WFlpY055bXpZc2NSMzFyaFR4VTZpIiwicHJldmlld1VybCI6ImNhdGFseXN0LWNhbmFyeS1naXQtYWRkLWFuYWx5dGljcy1jb29raWVzLWJpZ2NvbW1lcmNlLXBsYXRmb3JtLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiY2F0YWx5c3QtY2FuYXJ5LWdpdC1hZGQtYW5hbHl0aWNzLWNvb2tpZXMtYmlnY29tbWVyY2UtcGxhdGZvcm0udmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJjYXRhbHlzdCIsInJvb3REaXJlY3RvcnkiOiJjb3JlIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vYmlnY29tbWVyY2UtcGxhdGZvcm0vY2F0YWx5c3QvREFrZWJpeVZGU1Jlc2J3NUpuZkNCWEJBb0VpTSIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIn0seyJuYW1lIjoiY2F0YWx5c3QtYXUiLCJyb290RGlyZWN0b3J5IjoiY29yZSIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2JpZ2NvbW1lcmNlLXBsYXRmb3JtL2NhdGFseXN0LWF1LzRpekhhWDQzc255WUpReUo1Z2JwRG5lM3FzS3IiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsInByZXZpZXdVcmwiOiJjYXRhbHlzdC1hdS1naXQtYWRkLWFuYWx5dGljcy1jb29raWVzLWJpZ2NvbW1lcmNlLXBsYXRmb3JtLnZlcmNlbC5hcHAifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **catalyst-canary** | ✅ Ready ([Inspect](https://vercel.com/bigcommerce-platform/catalyst-canary/Ao1MFMtXYicNymzYscR31rhTxU6i)) | [Visit Preview](https://catalyst-canary-git-add-analytics-cookies-bigcommerce-platform.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/catalyst-canary-git-add-analytics-cookies-bigcommerce-platform.vercel.app?via=pr-comment-feedback-link) | May 22, 2025 6:42pm |

<details><summary>3 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **catalyst** | ⬜️ Ignored ([Inspect](https://vercel.com/bigcommerce-platform/catalyst/DAkebiyVFSResbw5JnfCBXBAoEiM)) |  |  | May 22, 2025 6:42pm |
| **catalyst-au** | ⬜️ Ignored ([Inspect](https://vercel.com/bigcommerce-platform/catalyst-au/4izHaX43snyYJQyJ5gbpDne3qsKr)) | [Visit Preview](https://catalyst-au-git-add-analytics-cookies-bigcommerce-platform.vercel.app) |  | May 22, 2025 6:42pm |
| **catalyst-uk** | ⬜️ Ignored ([Inspect](https://vercel.com/bigcommerce-platform/catalyst-uk/8mw8sC5nvXuPsYcrMNbnqUJfnpc9)) | [Visit Preview](https://catalyst-uk-git-add-analytics-cookies-bigcommerce-platform.vercel.app) |  | May 22, 2025 6:42pm |
</details>





author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T20:43:50Z
body: # ⚡️🏠 Lighthouse report

*Lighthouse ran against https://catalyst-canary-6oqrjof3l-bigcommerce-platform.vercel.app* 

## 🖥️ Desktop

We ran Lighthouse against the changes on a desktop and produced this [report](https://storage.googleapis.com/lighthouse-infrastructure.appspot.com/reports/1747946565145-6945.report.html). Here's the summary:

| Category | Score |
| -------- | ----- |
| 🟢 Performance | 98 |
| 🟢 Accessibility | 92 |
| 🟠 Best practices | 78 |
| 🟢 SEO | 91 |



## 📱 Mobile

We ran Lighthouse against the changes on a mobile and produced this [report](https://storage.googleapis.com/lighthouse-infrastructure.appspot.com/reports/1747946626193-78232.report.html). Here's the summary:

| Category | Score |
| -------- | ----- |
| 🟠 Performance | 72 |
| 🟢 Accessibility | 92 |
| 🟠 Best practices | 78 |
| 🟢 SEO | 92 |
<!-- Sticky Pull Request Commentlighthouse -->
author_association: CONTRIBUTOR