Repository: GoogleChromeLabs/ps-analysis-tool (ts)

Issue:
List cookies based on frame URLs
## Feature Request

**Description:**
List frames used on a page in the vertical menu and get cookies data based on those frame URLs.

**User Story:**
As a developer/website owner/web user I want to be able to filter out cookies based on the frames used on the web page.

**Acceptance Criteria:**
List frame URLs in the vertical menu.
Show relevant cookies data table when clicking on a frame URL



Pull request:
Refactor: Update cookie store and related listeners to add frameIDs
## Description

Cookie Store methods and network listeners have been updated to keep track of frames in which a cookie is being used.

## Relevant Technical Choices

- Update type `CookieData`: Added `frameIdList` property
- Update `CookieStore.update` method: Handles frameIdList merge with previous data. 

## Testing Instructions

<!--
How can the changes in this PR be verified?
Please provide step-by-step instructions to test the changes.
-->

## Additional Information:

`frameId`
Type: number
The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (type is main_frame or sub_frame), frameId indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab.

See [chrome API doc](https://developer.chrome.com/docs/extensions/reference/webRequest/#event-onBeforeSendHeaders)

## Screenshot/Screencast

<!-- Please provide Screenshot/Screencast, if applicable -->

---

<!--
Example:

Fixes #123
Partially addresses #22
See #834
-->

Fixes #72


Patch:
diff --git a/packages/extension/src/localStore/cookieStore.ts b/packages/extension/src/localStore/cookieStore.ts
index daff7984..e379f6e3 100644
--- a/packages/extension/src/localStore/cookieStore.ts
+++ b/packages/extension/src/localStore/cookieStore.ts
@@ -29,20 +29,49 @@ const CookieStore = {
     const newCookies: { [key: string]: CookieData } = {};
 
     for (const cookie of cookies) {
-      if (cookie) {
+      if (!cookie.parsedCookie.name) {
+        continue;
+      }
+
+      if (Object.keys(newCookies).includes(cookie.parsedCookie.name)) {
+        newCookies[cookie.parsedCookie.name] = {
+          ...newCookies[cookie.parsedCookie.name],
+          frameIdList: Array.from(
+            new Set([
+              ...newCookies[cookie.parsedCookie.name].frameIdList,
+              ...cookie.frameIdList,
+            ])
+          ),
+        };
+      } else {
         newCookies[cookie.parsedCookie.name] = cookie;
       }
     }
 
     await updateStorage(tabId, (prevState: TabData) => {
-      const updatedCookies = {
-        ...(prevState?.cookies || {}),
-        ...newCookies,
-      };
+      const _prevCookies = prevState?.cookies || {};
+
+      const _updatedCookies = _prevCookies;
+
+      Object.keys(newCookies).forEach((newCookieKey) => {
+        if (Object.keys(_prevCookies).includes(newCookieKey)) {
+          _updatedCookies[newCookieKey] = {
+            ...newCookies[newCookieKey],
+            frameIdList: Array.from(
+              new Set<number>([
+                ...newCookies[newCookieKey].frameIdList,
+                ..._prevCookies[newCookieKey].frameIdList,
+              ])
+            ),
+          };
+        } else {
+          _updatedCookies[newCookieKey] = newCookies[newCookieKey];
+        }
+      });
 
       return {
         ...prevState,
-        cookies: updatedCookies,
+        cookies: _updatedCookies,
       };
     });
   },
diff --git a/packages/extension/src/localStore/types.ts b/packages/extension/src/localStore/types.ts
index 38fe72b6..386e86a2 100644
--- a/packages/extension/src/localStore/types.ts
+++ b/packages/extension/src/localStore/types.ts
@@ -28,6 +28,7 @@ export type CookieData = {
   analytics: CookieAnalytics | null;
   url: string;
   headerType: 'response' | 'request';
+  frameIdList: number[];
 };
 
 export type TabData = {
diff --git a/packages/extension/src/worker/parseRequestCookieHeader.ts b/packages/extension/src/worker/parseRequestCookieHeader.ts
index c579a771..aa79afdb 100644
--- a/packages/extension/src/worker/parseRequestCookieHeader.ts
+++ b/packages/extension/src/worker/parseRequestCookieHeader.ts
@@ -36,12 +36,14 @@ import findAnalyticsMatch from './findAnalyticsMatch';
  * @param {string} url Cookie URL (URL of the server which is setting/updating cookies).
  * @param {string} value header value
  * @param {CookieDatabase} dictionary Dictionary from open cookie database
+ * @param {number} frameId Id of the frame the cookie is used in.
  * @returns {Promise<CookieData[]>} Parsed cookie object array.
  */
 const parseRequestCookieHeader = async (
   url: string,
   value: string,
-  dictionary: CookieDatabase
+  dictionary: CookieDatabase,
+  frameId: number
 ): Promise<CookieData[]> => {
   try {
     return await Promise.all(
@@ -66,6 +68,7 @@ const parseRequestCookieHeader = async (
           analytics,
           headerType: 'request',
           url,
+          frameIdList: [frameId],
         };
       })
     );
diff --git a/packages/extension/src/worker/parseResponseCookieHeader.ts b/packages/extension/src/worker/parseResponseCookieHeader.ts
index fd08404b..226c3136 100644
--- a/packages/extension/src/worker/parseResponseCookieHeader.ts
+++ b/packages/extension/src/worker/parseResponseCookieHeader.ts
@@ -35,12 +35,14 @@ import { createCookieObject } from './createCookieObject';
  * @param {string} url Cookie URL (URL of the server which is setting/updating cookies).
  * @param {string} value header value
  * @param {CookieDatabase} dictionary Dictionary from open cookie database
+ * @param {number} frameId Id of a frame in which this cookie is used.
  * @returns {Promise<CookieData>} Parsed cookie object.
  */
 const parseResponseCookieHeader = async (
   url: string,
   value: string,
-  dictionary: CookieDatabase
+  dictionary: CookieDatabase,
+  frameId: number
 ): Promise<CookieData> => {
   let parsedCookie: ParsedCookie = cookie.parse(value);
   parsedCookie = await createCookieObject(parsedCookie, url);
@@ -55,6 +57,7 @@ const parseResponseCookieHeader = async (
     analytics,
     url,
     headerType: 'response',
+    frameIdList: [frameId],
   };
 };
 
diff --git a/packages/extension/src/worker/service-worker.ts b/packages/extension/src/worker/service-worker.ts
index b9dab1d6..a87820ad 100644
--- a/packages/extension/src/worker/service-worker.ts
+++ b/packages/extension/src/worker/service-worker.ts
@@ -33,7 +33,7 @@ let cookieDB: CookieDatabase | null = null;
  */
 chrome.webRequest.onResponseStarted.addListener(
   async (details: chrome.webRequest.WebResponseCacheDetails) => {
-    const { tabId, url, responseHeaders } = details;
+    const { tabId, url, responseHeaders, frameId } = details;
 
     const tab = await getTab(tabId);
 
@@ -56,7 +56,8 @@ chrome.webRequest.onResponseStarted.addListener(
           const cookie = await parseResponseCookieHeader(
             url,
             header.value,
-            cookieDB
+            cookieDB,
+            frameId
           );
           return [...(await accumulator), cookie];
         }
@@ -77,7 +78,7 @@ chrome.webRequest.onResponseStarted.addListener(
 );
 
 chrome.webRequest.onBeforeSendHeaders.addListener(
-  ({ url, requestHeaders, tabId }) => {
+  ({ url, requestHeaders, tabId, frameId }) => {
     (async () => {
       const tab = await getTab(tabId);
 
@@ -100,7 +101,8 @@ chrome.webRequest.onBeforeSendHeaders.addListener(
             const cookieList = await parseRequestCookieHeader(
               url,
               header.value,
-              cookieDB
+              cookieDB,
+              frameId
             );
             return [...(await accumulator), ...cookieList];
           }


Test patch:
diff --git a/packages/extension/src/localStore/tests/cookieStore.ts b/packages/extension/src/localStore/tests/cookieStore.ts
index 4d6484fb..6ba403a9 100644
--- a/packages/extension/src/localStore/tests/cookieStore.ts
+++ b/packages/extension/src/localStore/tests/cookieStore.ts
@@ -32,6 +32,7 @@ const cookieArray: CookieData[] = [
       name: 'countryCode1',
       value: 'IN',
     },
+    frameIdList: [1],
     analytics: null,
     url: 'https://example.com',
     headerType: 'response',
@@ -47,6 +48,7 @@ const cookieArray: CookieData[] = [
       name: 'countryCode2',
       value: 'IN',
     },
+    frameIdList: [1],
     analytics: null,
     url: 'https://example.com',
     headerType: 'response',
@@ -119,6 +121,17 @@ describe('local store: CookieStore', () => {
     });
   });
 
+  it('should add/update tab data and merge frame id list', async () => {
+    await CookieStore.update('123', [
+      cookieArray[0],
+      { ...cookieArray[0], frameIdList: [2] },
+      { ...cookieArray[0], frameIdList: [2] },
+    ]);
+    expect(storage['123'].cookies).toStrictEqual({
+      countryCode1: { ...cookieArray[0], frameIdList: [1, 2] },
+    });
+  });
+
   it('should update tab foucusedAt value', async () => {
     await CookieStore.update('123', cookieArray);
     const prevFocusedAt = storage['123'].focusedAt;
diff --git a/packages/extension/src/view/devtools/components/tabContent/cookies/tests/cookieTab.tsx b/packages/extension/src/view/devtools/components/tabContent/cookies/tests/cookieTab.tsx
index aafff035..2be7f24e 100644
--- a/packages/extension/src/view/devtools/components/tabContent/cookies/tests/cookieTab.tsx
+++ b/packages/extension/src/view/devtools/components/tabContent/cookies/tests/cookieTab.tsx
@@ -62,12 +62,14 @@ const mockResponse: {
       analytics: null,
       url: 'https://edition.cnn.com/whatever/api',
       headerType: 'response',
+      frameIdList: [1],
     },
     [uncategorised3pCookie.name]: {
       parsedCookie: uncategorised3pCookie,
       analytics: null,
       url: 'https://api.pubmatic.com/whatever/api',
       headerType: 'response',
+      frameIdList: [1],
     },
     [known1pCookie.name]: {
       parsedCookie: known1pCookie,
@@ -85,6 +87,7 @@ const mockResponse: {
       },
       url: 'https://edition.cnn.com/whatever/api',
       headerType: 'response',
+      frameIdList: [1],
     },
     [known3pCookie.name]: {
       parsedCookie: known3pCookie,
@@ -102,6 +105,7 @@ const mockResponse: {
       },
       url: 'https://api.pubmatic.com/whatever/api',
       headerType: 'response',
+      frameIdList: [1],
     },
   },
   tabUrl: 'https://edition.cnn.com/',
diff --git a/packages/extension/src/worker/tests/parseRequestCookieHeader.ts b/packages/extension/src/worker/tests/parseRequestCookieHeader.ts
index 4930c85d..5372989b 100644
--- a/packages/extension/src/worker/tests/parseRequestCookieHeader.ts
+++ b/packages/extension/src/worker/tests/parseRequestCookieHeader.ts
@@ -38,6 +38,7 @@ const normalCookie1 = {
   analytics: null,
   url: 'https://example.com/public/api/alerts',
   headerType: 'request',
+  frameIdList: [1],
 };
 
 const normalCookie2 = {
@@ -54,6 +55,7 @@ const normalCookie2 = {
   analytics: null,
   url: 'https://example.com/public/api/alerts',
   headerType: 'request',
+  frameIdList: [1],
 };
 
 const specialCookie = {
@@ -70,6 +72,7 @@ const specialCookie = {
   analytics: null,
   url: 'https://example.com/public/api/alerts',
   headerType: 'request',
+  frameIdList: [1],
 };
 
 const wildcardCookie = {
@@ -97,6 +100,7 @@ const wildcardCookie = {
   },
   url: 'https://example.com/public/api/alerts',
   headerType: 'request',
+  frameIdList: [1],
 };
 
 const normalCookie1Header = `${normalCookie1.parsedCookie.name}=${normalCookie1.parsedCookie.value}`;
@@ -114,7 +118,8 @@ describe('parseRequestCookieHeader', () => {
     const parsedCookie = await parseRequestCookieHeader(
       'https://example.com/public/api/alerts',
       header,
-      {}
+      {},
+      1
     );
 
     expect(parsedCookie).toEqual([normalCookie1, normalCookie2, specialCookie]);
@@ -153,7 +158,8 @@ describe('parseRequestCookieHeader', () => {
             wildcard: '1',
           },
         ],
-      }
+      },
+      1
     );
 
     expect(parsedCookie).toEqual([
diff --git a/packages/extension/src/worker/tests/parseResponseCookieHeader.ts b/packages/extension/src/worker/tests/parseResponseCookieHeader.ts
index 909405ce..9d4e4a49 100644
--- a/packages/extension/src/worker/tests/parseResponseCookieHeader.ts
+++ b/packages/extension/src/worker/tests/parseResponseCookieHeader.ts
@@ -33,7 +33,8 @@ describe('parseResponseCookieHeader', () => {
     const parsedCookie = await parseResponseCookieHeader(
       'https://example.com/public/api/alerts',
       'countryCode=IN; Domain=.example.com; Path=/; SameSite=None; Secure',
-      {}
+      {},
+      1
     );
 
     expect(parsedCookie).toEqual({
@@ -50,10 +51,11 @@ describe('parseResponseCookieHeader', () => {
       analytics: null,
       url: 'https://example.com/public/api/alerts',
       headerType: 'response',
+      frameIdList: [1],
     });
   });
 
-  it('Should parse and add add analytics', async () => {
+  it('Should parse and add analytics', async () => {
     const parsedCookie = await parseResponseCookieHeader(
       'https://example.com/public/api/alerts',
       'test_cookie=bla; Domain=.example.com; Path=/; SameSite=None; Secure',
@@ -72,7 +74,8 @@ describe('parseResponseCookieHeader', () => {
             wildcard: '0',
           },
         ],
-      }
+      },
+      1
     );
 
     expect(parsedCookie).toEqual({
@@ -100,10 +103,11 @@ describe('parseResponseCookieHeader', () => {
       },
       url: 'https://example.com/public/api/alerts',
       headerType: 'response',
+      frameIdList: [1],
     });
   });
 
-  it('Should parse and add add analytics for wild card entries', async () => {
+  it('Should parse and add analytics for wild card entries', async () => {
     const parsedCookie = await parseResponseCookieHeader(
       'https://google.com/public/api/alerts',
       '_ga_123=bla; Domain=.google.com; Path=/; SameSite=None; Secure',
@@ -136,7 +140,8 @@ describe('parseResponseCookieHeader', () => {
             wildcard: '1',
           },
         ],
-      }
+      },
+      1
     );
 
     expect(parsedCookie).toEqual({
@@ -164,6 +169,7 @@ describe('parseResponseCookieHeader', () => {
       },
       url: 'https://google.com/public/api/alerts',
       headerType: 'response',
+      frameIdList: [1],
     });
   });
 });