From 585aeee48df63d8aee6cab79a55db0d1e6e0ca60 Mon Sep 17 00:00:00 2001 From: Kudaligi Amoghavarsha Date: Wed, 30 Oct 2024 16:32:20 +0530 Subject: [PATCH] Feature: Protected audience analysis in PSAT (#811) * Add PA analysis in service worker. Improve attach CDP function. * Add storage.interestGroupAccessed method to service worker. * Add bid value to the auction events. * Split syncCookieStore into dataStore, CookieStore and PAStore. Change its references. * Export interface from dataStore. * Change some more references. * Add protected audience provider to the devtools. * Convert bid to null. * Add last mile connection to the devtools * Add method to incorporate multiple auction ID. * Handle multiSeller auctions. Console.log data in app.tsx. * Descructure details in the extension. * Refactor service worker. Move service worker PA methods to PAStore. * Fix errors in the refactor. * Fix lint errors * Fix tests. * Move PA types to common package. * Update devtools protocol package to latest version * Move InterestGroup and auctionEvents to common package * Add support to hear globalEvents. * Add comment to explain auctionEvents structure. Refactor PA store to fix some bug when it was made to accomodate global events. * Use globalEvents instead of interestGroupEvents. * compute received Bids and noBids. * Fix noBids in the protected audience provider. * Fix nobids value. Add object diff in state update. Remove console.log. Add adUnit code to the data being provided in the bids and noBids. * Add ads and Bidders in protectedAudience API provider. * Add adUnitCode optional chaining. Fix set spread of bidders. * Fix data being displayed on the panel. * Fix data refresh on tab reload. * Reduce state updates. Refactor code move udeCallback to separate functions. * Fix error of not updating. * Fix undefined error. * Remove logic to reset interest group data on page load or navigation. * Fix failing tests. * Fix merge conflicts. * Fix failing build. * Fix failing tests * Fix initial sync override. * Fix failing tests. * Fix tab reload data refresh. --------- Co-authored-by: sayedtaqui --- package-lock.json | 18 +- packages/cli/src/e2e-tests/index.ts | 4 +- packages/common/package.json | 2 +- packages/common/src/index.ts | 1 + .../common/src/protectedAudience.types.ts | 86 +++ packages/extension/package.json | 2 +- .../extension/src/serviceWorker/attachCDP.ts | 56 +- .../beforeSendHeadersListener.ts | 17 +- .../onCommittedNavigationListener.ts | 24 +- .../chromeListeners/onEnabledListener.ts | 4 +- .../onResponseStartedListener.ts | 17 +- .../runtimeOnInstalledListener.ts | 4 +- .../runtimeOnMessageListener.ts | 75 ++- .../chromeListeners/runtimeStartUpListener.ts | 6 +- .../syncStorageOnChangedListener.ts | 24 +- .../chromeListeners/tabOnCreatedListener.ts | 26 +- .../chromeListeners/tabOnRemovedListener.ts | 6 +- .../tests/beforeSendHeadersListener.ts | 30 +- .../tests/onResponseStartedListener.ts | 30 +- .../tests/tabOnCreatedListener.ts | 28 +- .../chromeListeners/utils/setupIntervals.ts | 12 +- .../utils/updateGlobalVariableAndAttachCDP.ts | 49 +- .../windowsOnRemovedListener.ts | 6 +- packages/extension/src/serviceWorker/index.ts | 250 ++++++--- packages/extension/src/store/PAStore.ts | 285 ++++++++++ packages/extension/src/store/cookieStore.ts | 277 ++++++++++ ...synchnorousCookieStore.ts => dataStore.ts} | 492 +++++++----------- .../src/store/tests/synchnorousCookieStore.ts | 112 ++-- .../extension/src/store/utils/formatTime.ts | 33 ++ .../extension/src/store/utils/networkTime.ts | 41 ++ .../src/utils/getAndParseNetworkCookies.ts | 2 +- .../extension/src/utils/listenToNewTab.ts | 23 +- .../extension/src/view/devtools/index.tsx | 17 +- .../src/view/devtools/stateProviders/index.ts | 1 + .../protectedAudience/context.ts | 50 ++ .../stateProviders/protectedAudience/index.ts | 21 + .../protectedAudienceProvider.tsx | 231 ++++++++ .../protectedAudience/useProtectedAudience.ts | 44 ++ .../utils/computeInterestGroupDetails.ts | 63 +++ .../utils/computeReceivedBidsAndNoBids.ts | 209 ++++++++ .../protectedAudience/utils/index.ts | 17 + .../extension/src/view/devtools/tests/app.tsx | 16 +- 42 files changed, 2047 insertions(+), 664 deletions(-) create mode 100644 packages/common/src/protectedAudience.types.ts create mode 100644 packages/extension/src/store/PAStore.ts create mode 100644 packages/extension/src/store/cookieStore.ts rename packages/extension/src/store/{synchnorousCookieStore.ts => dataStore.ts} (60%) create mode 100644 packages/extension/src/store/utils/formatTime.ts create mode 100644 packages/extension/src/store/utils/networkTime.ts create mode 100644 packages/extension/src/view/devtools/stateProviders/protectedAudience/context.ts create mode 100644 packages/extension/src/view/devtools/stateProviders/protectedAudience/index.ts create mode 100644 packages/extension/src/view/devtools/stateProviders/protectedAudience/protectedAudienceProvider.tsx create mode 100644 packages/extension/src/view/devtools/stateProviders/protectedAudience/useProtectedAudience.ts create mode 100644 packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/computeInterestGroupDetails.ts create mode 100644 packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/computeReceivedBidsAndNoBids.ts create mode 100644 packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/index.ts diff --git a/package-lock.json b/package-lock.json index e16333186..ac04f07fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24153,13 +24153,14 @@ "tldts": "^6.0.14" }, "devDependencies": { - "devtools-protocol": "^0.0.1333880" + "devtools-protocol": "^0.0.1345247" } }, "packages/common/node_modules/devtools-protocol": { - "version": "0.0.1333880", - "dev": true, - "license": "BSD-3-Clause" + "version": "0.0.1345247", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1345247.tgz", + "integrity": "sha512-U6SOWj83Eznob0zaelx2CzURpexV5JwZ3Qsl2Q7cYsToMT3gDBVM2mbDgr0LNxhp9y2RG+7KZHHRRNrSnYozog==", + "dev": true }, "packages/design-system": { "name": "@google-psat/design-system", @@ -24223,14 +24224,15 @@ }, "devDependencies": { "@types/react-copy-to-clipboard": "^5.0.4", - "devtools-protocol": "^0.0.1333880", + "devtools-protocol": "^0.0.1345247", "html-inline-script-webpack-plugin": "^3.2.1" } }, "packages/extension/node_modules/devtools-protocol": { - "version": "0.0.1333880", - "dev": true, - "license": "BSD-3-Clause" + "version": "0.0.1345247", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1345247.tgz", + "integrity": "sha512-U6SOWj83Eznob0zaelx2CzURpexV5JwZ3Qsl2Q7cYsToMT3gDBVM2mbDgr0LNxhp9y2RG+7KZHHRRNrSnYozog==", + "dev": true }, "packages/i18n": { "name": "@google-psat/i18n", diff --git a/packages/cli/src/e2e-tests/index.ts b/packages/cli/src/e2e-tests/index.ts index a1e3268dc..b9aba3af9 100644 --- a/packages/cli/src/e2e-tests/index.ts +++ b/packages/cli/src/e2e-tests/index.ts @@ -20,8 +20,8 @@ describe('CLI E2E Test', () => { it('Should run site analysis', () => { return coffee - .fork(cli, ['-u https://bbc.com', '-w 1000']) - .includes('stdout', '/out/bbc-com/report_') + .fork(cli, ['-u https://domain-aaa.com/', '-w 1000']) + .includes('stdout', '/out/domain-aaa-com/report_') .end(); }, 60000); }); diff --git a/packages/common/package.json b/packages/common/package.json index 6d101f5c0..5a090293a 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -42,6 +42,6 @@ "tldts": "^6.0.14" }, "devDependencies": { - "devtools-protocol": "^0.0.1333880" + "devtools-protocol": "^0.0.1345247" } } diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index f4179154a..64e202888 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -52,3 +52,4 @@ export * from './worker/enums'; export * from './utils/generateReports'; export * from './cookies.types'; export * from './libraryDetection.types'; +export * from './protectedAudience.types'; diff --git a/packages/common/src/protectedAudience.types.ts b/packages/common/src/protectedAudience.types.ts new file mode 100644 index 000000000..034ba642c --- /dev/null +++ b/packages/common/src/protectedAudience.types.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * External dependencies + */ +import type { Protocol } from 'devtools-protocol'; + +export interface singleAuctionEvent { + bidCurrency?: string; + uniqueAuctionId?: Protocol.Storage.InterestGroupAuctionId; + bid?: number; + name?: string; + ownerOrigin?: string; + type: string; + formattedTime: string | Date; + componentSellerOrigin?: string; + time: number; + auctionConfig?: object; + interestGroupConfig?: Protocol.Storage.InterestGroupAccessedEvent; + parentAuctionId?: Protocol.Storage.InterestGroupAuctionId; + eventType: + | 'interestGroupAuctionEventOccurred' + | 'interestGroupAuctionNetworkRequestCompleted' + | 'interestGroupAuctionNetworkRequestCreated' + | 'interestGroupAccessed'; +} + +export interface auctionData { + [uniqueAuctionId: Protocol.Storage.InterestGroupAuctionId]: { + auctionTime: Protocol.Network.TimeSinceEpoch; + auctionConfig?: any; + parentAuctionId?: Protocol.Storage.InterestGroupAuctionId; + }; +} + +export type InterestGroups = singleAuctionEvent & { + details: any; +}; + +export type MultiSellerAuction = { + [parentAuctionId: string]: { + [uniqueAuctionId: string]: singleAuctionEvent[]; + }; +}; + +export type SingleSellerAuction = { + [parentAuctionId: string]: singleAuctionEvent[]; +}; + +export type NoBidsType = { + [auctionId: string]: { + ownerOrigin: string; + name: string; + uniqueAuctionId: string; + adUnitCode?: string; + mediaContainerSize?: number[][]; + }; +}; + +export type AdsAndBiddersType = { + [adUnitCode: string]: { + adUnitCode: string; + bidders: string[]; + mediaContainerSize: number[][]; + }; +}; + +export type ReceivedBids = singleAuctionEvent & { + adUnitCode?: string; + mediaContainerSize?: number[]; +}; + +export type AuctionEventsType = SingleSellerAuction | MultiSellerAuction | null; diff --git a/packages/extension/package.json b/packages/extension/package.json index e55a0d8f7..b2d9c28b0 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -53,7 +53,7 @@ }, "devDependencies": { "@types/react-copy-to-clipboard": "^5.0.4", - "devtools-protocol": "^0.0.1333880", + "devtools-protocol": "^0.0.1345247", "html-inline-script-webpack-plugin": "^3.2.1" } } diff --git a/packages/extension/src/serviceWorker/attachCDP.ts b/packages/extension/src/serviceWorker/attachCDP.ts index 55597a9df..6112d4775 100644 --- a/packages/extension/src/serviceWorker/attachCDP.ts +++ b/packages/extension/src/serviceWorker/attachCDP.ts @@ -17,23 +17,41 @@ * This function will attach the debugger to the given target. * @param {{ [key: string]: number | string }} target The target where debugger needs to be attached. */ -export default async function attachCDP(target: { - [key: string]: number | string; -}) { - try { - await chrome.debugger.attach(target, '1.3'); - await chrome.debugger.sendCommand(target, 'Target.setAutoAttach', { - // If this is set to true, debugger will be attached to every new target that is added to the current target. - autoAttach: true, - waitForDebuggerOnStart: false, - //Enables "flat" access to the session via specifying sessionId attribute in the commands. - // If this is set to true the debugger is also attached to the child targets of that the target it has been attached to. - flatten: true, - }); - await chrome.debugger.sendCommand(target, 'Network.enable'); - await chrome.debugger.sendCommand(target, 'Audits.enable'); - await chrome.debugger.sendCommand(target, 'Page.enable'); - } catch (error) { - //Fail silently - } +export default function attachCDP(target: { [key: string]: number | string }) { + chrome.debugger.attach(target, '1.3', async () => { + if (chrome.runtime.lastError) { + // eslint-disable-next-line no-console + console.warn(chrome.runtime.lastError); + } + try { + await chrome.debugger.sendCommand(target, 'Target.setAutoAttach', { + autoAttach: true, + flatten: false, + waitForDebuggerOnStart: true, + }); + + await chrome.debugger.sendCommand( + target, + 'Storage.setInterestGroupAuctionTracking', + { enable: true } + ); + + await chrome.debugger.sendCommand(target, 'Audits.enable'); + + await chrome.debugger.sendCommand( + target, + 'Storage.setInterestGroupTracking', + { + enable: true, + } + ); + + await chrome.debugger.sendCommand(target, 'Page.enable'); + + await chrome.debugger.sendCommand(target, 'Network.enable'); + } catch (error) { + // eslint-disable-next-line no-console + console.warn(error); + } + }); } diff --git a/packages/extension/src/serviceWorker/chromeListeners/beforeSendHeadersListener.ts b/packages/extension/src/serviceWorker/chromeListeners/beforeSendHeadersListener.ts index 775e7414c..2b5f6b7f6 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/beforeSendHeadersListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/beforeSendHeadersListener.ts @@ -17,7 +17,8 @@ * Internal dependencies */ import parseHeaders from '../../utils/parseHeaders'; -import synchnorousCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; +import cookieStore from '../../store/cookieStore'; import { getTab } from '../../utils/getTab'; export const onBeforeSendHeadersListener = ({ @@ -27,26 +28,26 @@ export const onBeforeSendHeadersListener = ({ frameId, requestId, }: chrome.webRequest.WebRequestHeadersDetails) => { - if (synchnorousCookieStore.globalIsUsingCDP) { + if (dataStore.globalIsUsingCDP) { return; } (async () => { const tab = await getTab(tabId); - let tabUrl = synchnorousCookieStore?.getTabUrl(tabId); + let tabUrl = dataStore?.getTabUrl(tabId); if (tab && tab.pendingUrl) { tabUrl = tab.pendingUrl; } const cookies = await parseHeaders( - synchnorousCookieStore.globalIsUsingCDP, + dataStore.globalIsUsingCDP, 'request', - synchnorousCookieStore.tabToRead, - synchnorousCookieStore.tabMode, + dataStore.tabToRead, + dataStore.tabMode, tabId, url, - synchnorousCookieStore.cookieDB ?? {}, + dataStore.cookieDB ?? {}, tabUrl, frameId.toString(), requestId, @@ -58,6 +59,6 @@ export const onBeforeSendHeadersListener = ({ } // Adds the cookies from the request headers to the cookies object. - synchnorousCookieStore?.update(tabId, cookies); + cookieStore?.update(tabId, cookies); })(); }; diff --git a/packages/extension/src/serviceWorker/chromeListeners/onCommittedNavigationListener.ts b/packages/extension/src/serviceWorker/chromeListeners/onCommittedNavigationListener.ts index 807075a22..6e2463ac9 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/onCommittedNavigationListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/onCommittedNavigationListener.ts @@ -17,7 +17,7 @@ * Internal dependencies */ import { TABID_STORAGE } from '../../constants'; -import synchnorousCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; import getQueryParams from '../../utils/getQueryParams'; import attachCDP from '../attachCDP'; @@ -36,7 +36,7 @@ export const onCommittedNavigationListener = async ({ } const targets = await chrome.debugger.getTargets(); - const mainFrameId = synchnorousCookieStore?.globalIsUsingCDP + const mainFrameId = dataStore?.globalIsUsingCDP ? targets.filter((target) => target.tabId && target.tabId === tabId)[0] ?.id : 0; @@ -50,19 +50,19 @@ export const onCommittedNavigationListener = async ({ isUsingCDP: queryParams.psat_cdp === 'on', }); - synchnorousCookieStore.globalIsUsingCDP = queryParams.psat_cdp === 'on'; - synchnorousCookieStore.tabMode = + dataStore.globalIsUsingCDP = queryParams.psat_cdp === 'on'; + dataStore.tabMode = queryParams.psat_multitab === 'on' ? 'unlimited' : 'single'; } - synchnorousCookieStore?.updateUrl(tabId, url); + dataStore?.updateUrl(tabId, url); if (url && !url.startsWith('chrome://')) { - synchnorousCookieStore?.removeCookieData(tabId); + dataStore?.removeCookieData(tabId); - if (synchnorousCookieStore.globalIsUsingCDP) { - synchnorousCookieStore.deinitialiseVariablesForTab(tabId.toString()); - synchnorousCookieStore.initialiseVariablesForNewTab(tabId.toString()); + if (dataStore.globalIsUsingCDP) { + dataStore.deinitialiseVariablesForTab(tabId.toString()); + dataStore.initialiseVariablesForNewTab(tabId.toString()); await attachCDP({ tabId }); @@ -76,11 +76,7 @@ export const onCommittedNavigationListener = async ({ } ); - synchnorousCookieStore.updateParentChildFrameAssociation( - tabId, - targetId, - '0' - ); + dataStore.updateParentChildFrameAssociation(tabId, targetId, '0'); } } await chrome.tabs.sendMessage(tabId, { diff --git a/packages/extension/src/serviceWorker/chromeListeners/onEnabledListener.ts b/packages/extension/src/serviceWorker/chromeListeners/onEnabledListener.ts index 5c3ce61a0..dd6b0be94 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/onEnabledListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/onEnabledListener.ts @@ -16,7 +16,7 @@ /** * Internal dependencies */ -import synchnorousCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; import { updateGlobalVariableAndAttachCDP, setupIntervals } from './utils'; export const onEnabledListener = async ( @@ -26,7 +26,7 @@ export const onEnabledListener = async ( return; } - synchnorousCookieStore?.clear(); + dataStore?.clear(); setupIntervals(); diff --git a/packages/extension/src/serviceWorker/chromeListeners/onResponseStartedListener.ts b/packages/extension/src/serviceWorker/chromeListeners/onResponseStartedListener.ts index de8c8d9f7..af212a484 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/onResponseStartedListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/onResponseStartedListener.ts @@ -17,8 +17,9 @@ * Internal dependencies */ import parseHeaders from '../../utils/parseHeaders'; -import synchnorousCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; import { getTab } from '../../utils/getTab'; +import cookieStore from '../../store/cookieStore'; export const onResponseStartedListener = ({ tabId, @@ -27,26 +28,26 @@ export const onResponseStartedListener = ({ frameId, requestId, }: chrome.webRequest.WebResponseCacheDetails) => { - if (synchnorousCookieStore.globalIsUsingCDP) { + if (dataStore.globalIsUsingCDP) { return; } (async () => { const tab = await getTab(tabId); - let tabUrl = synchnorousCookieStore?.getTabUrl(tabId); + let tabUrl = dataStore?.getTabUrl(tabId); if (tab && tab.pendingUrl) { tabUrl = tab.pendingUrl; } const cookies = await parseHeaders( - synchnorousCookieStore.globalIsUsingCDP, + dataStore.globalIsUsingCDP, 'response', - synchnorousCookieStore.tabToRead, - synchnorousCookieStore.tabMode, + dataStore.tabToRead, + dataStore.tabMode, tabId, url, - synchnorousCookieStore.cookieDB ?? {}, + dataStore.cookieDB ?? {}, tabUrl, frameId.toString(), requestId, @@ -58,6 +59,6 @@ export const onResponseStartedListener = ({ } // Adds the cookies from the request headers to the cookies object. - synchnorousCookieStore?.update(tabId, cookies); + cookieStore?.update(tabId, cookies); })(); }; diff --git a/packages/extension/src/serviceWorker/chromeListeners/runtimeOnInstalledListener.ts b/packages/extension/src/serviceWorker/chromeListeners/runtimeOnInstalledListener.ts index b92072b59..828dfedb8 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/runtimeOnInstalledListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/runtimeOnInstalledListener.ts @@ -16,13 +16,13 @@ /** * Internal dependencies */ -import synchnorousCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; import { updateGlobalVariableAndAttachCDP, setupIntervals } from './utils'; export const runtimeOnInstalledListener = async ( details: chrome.runtime.InstalledDetails ) => { - synchnorousCookieStore?.clear(); + dataStore?.clear(); setupIntervals(); diff --git a/packages/extension/src/serviceWorker/chromeListeners/runtimeOnMessageListener.ts b/packages/extension/src/serviceWorker/chromeListeners/runtimeOnMessageListener.ts index 2c0140bfa..92f4cb6f6 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/runtimeOnMessageListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/runtimeOnMessageListener.ts @@ -17,7 +17,7 @@ /** * Internal dependencies */ -import synchnorousCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; import { DEVTOOLS_CLOSE, DEVTOOLS_OPEN, @@ -34,6 +34,7 @@ import attachCDP from '../attachCDP'; import reloadCurrentTab from '../../utils/reloadCurrentTab'; import { getTab } from '../../utils/getTab'; import sendMessageWrapper from '../../utils/sendMessageWrapper'; +import cookieStore from '../../store/cookieStore'; // eslint-disable-next-line complexity export const runtimeOnMessageListener = async (request: any) => { @@ -44,14 +45,14 @@ export const runtimeOnMessageListener = async (request: any) => { const incomingMessageType = request.type; if (SET_TAB_TO_READ === incomingMessageType) { - synchnorousCookieStore.tabToRead = request?.payload?.tabId?.toString(); + dataStore.tabToRead = request?.payload?.tabId?.toString(); const newTab = await listenToNewTab(request?.payload?.tabId); // Can't use sendResponse as delay is too long. So using sendMessage instead. await sendMessageWrapper(SET_TAB_TO_READ, { tabId: Number(newTab), }); - if (synchnorousCookieStore.globalIsUsingCDP) { + if (dataStore.globalIsUsingCDP) { await attachCDP({ tabId: Number(newTab) }); } @@ -63,13 +64,13 @@ export const runtimeOnMessageListener = async (request: any) => { const actionsPerformed: { [key: string]: boolean | number } = {}; if (sessionStorage?.allowedNumberOfTabs) { - synchnorousCookieStore.tabMode = sessionStorage.allowedNumberOfTabs; + dataStore.tabMode = sessionStorage.allowedNumberOfTabs; actionsPerformed.allowedNumberOfTabs = sessionStorage.allowedNumberOfTabs === 'unlimited' ? 0 : 1; } if (Object.keys(sessionStorage).includes('isUsingCDP')) { - synchnorousCookieStore.globalIsUsingCDP = sessionStorage.isUsingCDP; + dataStore.globalIsUsingCDP = sessionStorage.isUsingCDP; actionsPerformed.globalIsUsingCDP = true; } @@ -80,8 +81,8 @@ export const runtimeOnMessageListener = async (request: any) => { }); await chrome.storage.sync.set({ - allowedNumberOfTabs: synchnorousCookieStore.tabMode, - isUsingCDP: synchnorousCookieStore.globalIsUsingCDP, + allowedNumberOfTabs: dataStore.tabMode, + isUsingCDP: dataStore.globalIsUsingCDP, }); const tabs = await chrome.tabs.query({}); @@ -93,14 +94,14 @@ export const runtimeOnMessageListener = async (request: any) => { return; } - if (synchnorousCookieStore.tabMode === 'unlimited') { - synchnorousCookieStore.initialiseVariablesForNewTab(id.toString()); + if (dataStore.tabMode === 'unlimited') { + dataStore.initialiseVariablesForNewTab(id.toString()); const currentTab = targets.filter( ({ tabId }) => tabId && id && tabId === id ); - synchnorousCookieStore?.addTabData(id); - synchnorousCookieStore?.updateParentChildFrameAssociation( + dataStore?.addTabData(id); + dataStore?.updateParentChildFrameAssociation( id, currentTab[0].id, '0' @@ -108,7 +109,7 @@ export const runtimeOnMessageListener = async (request: any) => { } try { - if (synchnorousCookieStore.globalIsUsingCDP) { + if (dataStore.globalIsUsingCDP) { await attachCDP({ tabId: id }); } } catch (error) { @@ -132,71 +133,59 @@ export const runtimeOnMessageListener = async (request: any) => { if (DEVTOOLS_OPEN === incomingMessageType) { const dataToSend: { [key: string]: string | boolean } = {}; - dataToSend['tabMode'] = synchnorousCookieStore.tabMode; + dataToSend['tabMode'] = dataStore.tabMode; - if (synchnorousCookieStore.tabMode === 'single') { - dataToSend['tabToRead'] = synchnorousCookieStore.tabToRead; + if (dataStore.tabMode === 'single') { + dataToSend['tabToRead'] = dataStore.tabToRead; } if ( - !synchnorousCookieStore?.tabs[incomingMessageTabId] && - synchnorousCookieStore.tabMode === 'unlimited' + !dataStore?.tabs[incomingMessageTabId] && + dataStore.tabMode === 'unlimited' ) { const currentTab = await getTab(incomingMessageTabId); dataToSend['psatOpenedAfterPageLoad'] = request.payload.doNotReReload ? false : true; - synchnorousCookieStore?.addTabData(incomingMessageTabId); - synchnorousCookieStore?.updateUrl( - incomingMessageTabId, - currentTab?.url || '' - ); + dataStore?.addTabData(incomingMessageTabId); + dataStore?.updateUrl(incomingMessageTabId, currentTab?.url || ''); } await sendMessageWrapper(INITIAL_SYNC, dataToSend); - synchnorousCookieStore?.updateDevToolsState(incomingMessageTabId, true); + dataStore?.updateDevToolsState(incomingMessageTabId, true); - if (synchnorousCookieStore?.tabsData[incomingMessageTabId]) { - synchnorousCookieStore?.sendUpdatedDataToPopupAndDevTools( - incomingMessageTabId, - true - ); + if (dataStore?.tabsData[incomingMessageTabId]) { + dataStore?.sendUpdatedDataToPopupAndDevTools(incomingMessageTabId, true); } } if (POPUP_OPEN === incomingMessageType) { const dataToSend: { [key: string]: string } = {}; - dataToSend['tabMode'] = synchnorousCookieStore.tabMode; + dataToSend['tabMode'] = dataStore.tabMode; - if (synchnorousCookieStore.tabMode === 'single') { - dataToSend['tabToRead'] = synchnorousCookieStore.tabToRead; + if (dataStore.tabMode === 'single') { + dataToSend['tabToRead'] = dataStore.tabToRead; } await sendMessageWrapper(INITIAL_SYNC, dataToSend); - synchnorousCookieStore?.updatePopUpState(incomingMessageTabId, true); + dataStore?.updatePopUpState(incomingMessageTabId, true); - if (synchnorousCookieStore?.tabsData[incomingMessageTabId]) { - synchnorousCookieStore?.sendUpdatedDataToPopupAndDevTools( - incomingMessageTabId, - true - ); + if (dataStore?.tabsData[incomingMessageTabId]) { + dataStore?.sendUpdatedDataToPopupAndDevTools(incomingMessageTabId, true); } } if (DEVTOOLS_CLOSE === incomingMessageType) { - synchnorousCookieStore?.updateDevToolsState(incomingMessageTabId, false); + dataStore?.updateDevToolsState(incomingMessageTabId, false); } if (POPUP_CLOSE === incomingMessageType) { - synchnorousCookieStore?.updatePopUpState(incomingMessageTabId, false); + dataStore?.updatePopUpState(incomingMessageTabId, false); } if (DEVTOOLS_SET_JAVASCSCRIPT_COOKIE === incomingMessageType) { - synchnorousCookieStore?.update( - incomingMessageTabId, - request?.payload?.cookieData - ); + cookieStore?.update(incomingMessageTabId, request?.payload?.cookieData); } }; diff --git a/packages/extension/src/serviceWorker/chromeListeners/runtimeStartUpListener.ts b/packages/extension/src/serviceWorker/chromeListeners/runtimeStartUpListener.ts index a67e3d9eb..9fd6d473f 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/runtimeStartUpListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/runtimeStartUpListener.ts @@ -16,7 +16,7 @@ /** * Internal dependencies */ -import synchnorousCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; import { setupIntervals } from './utils'; export const onStartUpListener = async () => { @@ -24,10 +24,10 @@ export const onStartUpListener = async () => { setupIntervals(); if (storage?.allowedNumberOfTabs) { - synchnorousCookieStore.tabMode = storage.allowedNumberOfTabs; + dataStore.tabMode = storage.allowedNumberOfTabs; } if (Object.keys(storage).includes('isUsingCDP')) { - synchnorousCookieStore.globalIsUsingCDP = storage.isUsingCDP; + dataStore.globalIsUsingCDP = storage.isUsingCDP; } }; diff --git a/packages/extension/src/serviceWorker/chromeListeners/syncStorageOnChangedListener.ts b/packages/extension/src/serviceWorker/chromeListeners/syncStorageOnChangedListener.ts index 395e70822..a7bbbe775 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/syncStorageOnChangedListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/syncStorageOnChangedListener.ts @@ -17,7 +17,7 @@ * Internal dependencies */ import { INITIAL_SYNC } from '../../constants'; -import synchnorousCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; import resetCookieBadgeText from '../../store/utils/resetCookieBadgeText'; import sendMessageWrapper from '../../utils/sendMessageWrapper'; @@ -31,16 +31,16 @@ export const onSyncStorageChangedListenerForMultiTab = async (changes: { ) { return; } - synchnorousCookieStore.tabMode = changes.allowedNumberOfTabs.newValue; + dataStore.tabMode = changes.allowedNumberOfTabs.newValue; const tabs = await chrome.tabs.query({}); await sendMessageWrapper(INITIAL_SYNC, { - tabMode: synchnorousCookieStore.tabMode, - tabToRead: synchnorousCookieStore.tabToRead, + tabMode: dataStore.tabMode, + tabToRead: dataStore.tabToRead, }); if (changes?.allowedNumberOfTabs?.newValue === 'single') { - synchnorousCookieStore.tabToRead = ''; + dataStore.tabToRead = ''; tabs.map((tab) => { if (!tab?.id) { @@ -49,7 +49,7 @@ export const onSyncStorageChangedListenerForMultiTab = async (changes: { resetCookieBadgeText(tab.id); - synchnorousCookieStore?.removeTabData(tab.id); + dataStore?.removeTabData(tab.id); return tab; }); @@ -58,9 +58,9 @@ export const onSyncStorageChangedListenerForMultiTab = async (changes: { if (!tab?.id) { return; } - synchnorousCookieStore?.addTabData(tab.id); - synchnorousCookieStore?.sendUpdatedDataToPopupAndDevTools(tab.id); - synchnorousCookieStore?.updateDevToolsState(tab.id, true); + dataStore?.addTabData(tab.id); + dataStore?.sendUpdatedDataToPopupAndDevTools(tab.id); + dataStore?.updateDevToolsState(tab.id, true); }); } }; @@ -76,12 +76,12 @@ export const onSyncStorageChangedListenerForCDP = async (changes: { return; } - synchnorousCookieStore.globalIsUsingCDP = changes?.isUsingCDP?.newValue; + dataStore.globalIsUsingCDP = changes?.isUsingCDP?.newValue; const tabs = await chrome.tabs.query({}); if (!changes?.isUsingCDP?.newValue) { - if (!synchnorousCookieStore.globalIsUsingCDP) { + if (!dataStore.globalIsUsingCDP) { const targets = await chrome.debugger.getTargets(); await Promise.all( targets.map(async ({ id, attached }) => { @@ -104,7 +104,7 @@ export const onSyncStorageChangedListenerForCDP = async (changes: { return; } - synchnorousCookieStore?.sendUpdatedDataToPopupAndDevTools(id); + dataStore?.sendUpdatedDataToPopupAndDevTools(id); }); } }; diff --git a/packages/extension/src/serviceWorker/chromeListeners/tabOnCreatedListener.ts b/packages/extension/src/serviceWorker/chromeListeners/tabOnCreatedListener.ts index 65e4bd8ff..00fdc2eb0 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/tabOnCreatedListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/tabOnCreatedListener.ts @@ -17,7 +17,7 @@ * Internal dependencies */ import { ALLOWED_NUMBER_OF_TABS } from '../../constants'; -import syncCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; export const onTabCreatedListener = async (tab: chrome.tabs.Tab) => { try { @@ -27,40 +27,40 @@ export const onTabCreatedListener = async (tab: chrome.tabs.Tab) => { const targets = await chrome.debugger.getTargets(); - if (syncCookieStore.tabMode && syncCookieStore.tabMode !== 'unlimited') { - const doesTabExist = syncCookieStore.tabToRead; + if (dataStore.tabMode && dataStore.tabMode !== 'unlimited') { + const doesTabExist = dataStore.tabToRead; if ( - Object.keys(syncCookieStore?.tabsData ?? {}).length >= + Object.keys(dataStore?.tabsData ?? {}).length >= ALLOWED_NUMBER_OF_TABS && doesTabExist ) { return; } - syncCookieStore.tabToRead = tab.id.toString(); - syncCookieStore?.addTabData(tab.id); + dataStore.tabToRead = tab.id.toString(); + dataStore?.addTabData(tab.id); - if (syncCookieStore.globalIsUsingCDP) { + if (dataStore.globalIsUsingCDP) { const currentTab = targets.filter( ({ tabId }) => tabId && tab.id && tabId === tab.id ); - syncCookieStore.initialiseVariablesForNewTab(tab.id.toString()); + dataStore.initialiseVariablesForNewTab(tab.id.toString()); - syncCookieStore.updateParentChildFrameAssociation( + dataStore.updateParentChildFrameAssociation( tab.id, currentTab[0].id, '0' ); } } else { - syncCookieStore?.addTabData(tab.id); + dataStore?.addTabData(tab.id); - if (syncCookieStore.globalIsUsingCDP) { + if (dataStore.globalIsUsingCDP) { const currentTab = targets.filter( ({ tabId }) => tabId && tab.id && tabId === tab.id ); - syncCookieStore.initialiseVariablesForNewTab(tab.id.toString()); + dataStore.initialiseVariablesForNewTab(tab.id.toString()); - syncCookieStore.updateParentChildFrameAssociation( + dataStore.updateParentChildFrameAssociation( tab.id, currentTab[0].id, '0' diff --git a/packages/extension/src/serviceWorker/chromeListeners/tabOnRemovedListener.ts b/packages/extension/src/serviceWorker/chromeListeners/tabOnRemovedListener.ts index 5435fa095..08eefdb0a 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/tabOnRemovedListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/tabOnRemovedListener.ts @@ -16,10 +16,10 @@ /** * Internal dependencies */ -import synchnorousCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; export const onTabRemovedListener = (tabId: number) => { - synchnorousCookieStore.deinitialiseVariablesForTab(tabId.toString()); + dataStore.deinitialiseVariablesForTab(tabId.toString()); - synchnorousCookieStore?.removeTabData(tabId); + dataStore?.removeTabData(tabId); }; diff --git a/packages/extension/src/serviceWorker/chromeListeners/tests/beforeSendHeadersListener.ts b/packages/extension/src/serviceWorker/chromeListeners/tests/beforeSendHeadersListener.ts index 8c5ff0c2a..75161aaec 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/tests/beforeSendHeadersListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/tests/beforeSendHeadersListener.ts @@ -26,7 +26,7 @@ import SinonChrome from 'sinon-chrome'; import OpenCookieDatabase from 'ps-analysis-tool/assets/data/open-cookie-database.json'; import { onBeforeSendHeadersListener } from '../beforeSendHeadersListener'; import { requestHeaders } from '../test-utils/requestHeaders'; -import synchnorousCookieStore from '../../../store/synchnorousCookieStore'; +import dataStore from '../../../store/dataStore'; describe('chrome.webRequest.onBeforeSendHeaders.addListener', () => { beforeAll(() => { @@ -48,14 +48,14 @@ describe('chrome.webRequest.onBeforeSendHeaders.addListener', () => { }); beforeEach(() => { - synchnorousCookieStore.globalIsUsingCDP = false; - synchnorousCookieStore.tabMode = 'single'; - synchnorousCookieStore.addTabData(1141143618); - synchnorousCookieStore.updateUrl(1141143618, 'https://bbc.com'); - synchnorousCookieStore.tabToRead = '1141143618'; + dataStore.globalIsUsingCDP = false; + dataStore.tabMode = 'single'; + dataStore.addTabData(1141143618); + dataStore.updateUrl(1141143618, 'https://bbc.com'); + dataStore.tabToRead = '1141143618'; }); afterEach(() => { - synchnorousCookieStore.removeTabData(1141143618); + dataStore.removeTabData(1141143618); }); test('Should parse request Cookies', async () => { @@ -69,9 +69,7 @@ describe('chrome.webRequest.onBeforeSendHeaders.addListener', () => { await new Promise((r) => setTimeout(r, 2000)); - expect( - Object.keys(synchnorousCookieStore.tabsData[1141143618]).length - ).toEqual(24); + expect(Object.keys(dataStore.tabsData[1141143618]).length).toEqual(24); }); test('Should not parse cookies if no cookie header is found in request header', async () => { @@ -85,13 +83,11 @@ describe('chrome.webRequest.onBeforeSendHeaders.addListener', () => { await new Promise((r) => setTimeout(r, 2000)); - expect( - Object.keys(synchnorousCookieStore.tabsData[1141143618]).length - ).toEqual(0); + expect(Object.keys(dataStore.tabsData[1141143618]).length).toEqual(0); }); test('Should not parse cookies if cdp is on', async () => { - synchnorousCookieStore.globalIsUsingCDP = true; + dataStore.globalIsUsingCDP = true; SinonChrome.webRequest.onBeforeSendHeaders.dispatch({ url: 'https://bbc.com', frameId: 0, @@ -102,9 +98,7 @@ describe('chrome.webRequest.onBeforeSendHeaders.addListener', () => { await new Promise((r) => setTimeout(r, 2000)); - expect( - Object.keys(synchnorousCookieStore.tabsData[1141143618]).length - ).toEqual(0); - synchnorousCookieStore.globalIsUsingCDP = false; + expect(Object.keys(dataStore.tabsData[1141143618]).length).toEqual(0); + dataStore.globalIsUsingCDP = false; }); }); diff --git a/packages/extension/src/serviceWorker/chromeListeners/tests/onResponseStartedListener.ts b/packages/extension/src/serviceWorker/chromeListeners/tests/onResponseStartedListener.ts index 4ce05500c..ba28f979b 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/tests/onResponseStartedListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/tests/onResponseStartedListener.ts @@ -27,7 +27,7 @@ import OpenCookieDatabase from 'ps-analysis-tool/assets/data/open-cookie-databas import { onResponseStartedListener } from '../onResponseStartedListener'; import { responseHeaders } from '../test-utils/requestHeaders'; -import synchnorousCookieStore from '../../../store/synchnorousCookieStore'; +import dataStore from '../../../store/dataStore'; describe('chrome.webRequest.onResponseStarted.addListener', () => { beforeAll(() => { @@ -50,15 +50,15 @@ describe('chrome.webRequest.onResponseStarted.addListener', () => { }); beforeEach(() => { - synchnorousCookieStore.globalIsUsingCDP = false; - synchnorousCookieStore.tabMode = 'single'; - synchnorousCookieStore.addTabData(1141143618); - synchnorousCookieStore.updateUrl(1141143618, 'https://bbc.com'); - synchnorousCookieStore.tabToRead = '1141143618'; + dataStore.globalIsUsingCDP = false; + dataStore.tabMode = 'single'; + dataStore.addTabData(1141143618); + dataStore.updateUrl(1141143618, 'https://bbc.com'); + dataStore.tabToRead = '1141143618'; }); afterEach(() => { - synchnorousCookieStore.removeTabData(1141143618); + dataStore.removeTabData(1141143618); }); test('Should parse response Cookies', async () => { @@ -73,9 +73,7 @@ describe('chrome.webRequest.onResponseStarted.addListener', () => { await new Promise((r) => setTimeout(r, 2000)); - expect( - Object.keys(synchnorousCookieStore.tabsData[1141143618]).length - ).toEqual(2); + expect(Object.keys(dataStore.tabsData[1141143618]).length).toEqual(2); }); test('Should not parse cookies if no cookie header is found in response header', async () => { @@ -91,13 +89,11 @@ describe('chrome.webRequest.onResponseStarted.addListener', () => { await new Promise((r) => setTimeout(r, 2000)); - expect( - Object.keys(synchnorousCookieStore.tabsData[1141143618]).length - ).toEqual(0); + expect(Object.keys(dataStore.tabsData[1141143618]).length).toEqual(0); }); test('Should not parse cookies if cdp is on', async () => { - synchnorousCookieStore.globalIsUsingCDP = true; + dataStore.globalIsUsingCDP = true; SinonChrome.webRequest.onResponseStarted.dispatch({ url: 'https://bbc.com', frameId: 0, @@ -110,9 +106,7 @@ describe('chrome.webRequest.onResponseStarted.addListener', () => { await new Promise((r) => setTimeout(r, 2000)); - expect( - Object.keys(synchnorousCookieStore.tabsData[1141143618]).length - ).toEqual(0); - synchnorousCookieStore.globalIsUsingCDP = false; + expect(Object.keys(dataStore.tabsData[1141143618]).length).toEqual(0); + dataStore.globalIsUsingCDP = false; }); }); diff --git a/packages/extension/src/serviceWorker/chromeListeners/tests/tabOnCreatedListener.ts b/packages/extension/src/serviceWorker/chromeListeners/tests/tabOnCreatedListener.ts index b839c74ee..c1b79446b 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/tests/tabOnCreatedListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/tests/tabOnCreatedListener.ts @@ -25,7 +25,7 @@ import SinonChrome from 'sinon-chrome'; // eslint-disable-next-line import/no-unresolved import OpenCookieDatabase from 'ps-analysis-tool/assets/data/open-cookie-database.json'; import { onTabCreatedListener } from '../tabOnCreatedListener'; -import synchnorousCookieStore from '../../../store/synchnorousCookieStore'; +import dataStore from '../../../store/dataStore'; describe('chrome.tabs.onCreated.addListener', () => { beforeAll(() => { @@ -44,8 +44,8 @@ describe('chrome.tabs.onCreated.addListener', () => { describe('Multitab Mode', () => { beforeAll(() => { - synchnorousCookieStore.globalIsUsingCDP = false; - synchnorousCookieStore.tabMode = 'unlimited'; + dataStore.globalIsUsingCDP = false; + dataStore.tabMode = 'unlimited'; }); test('Openeing new tabs should create an entry for the new tab in synchnorous cookie store.', async () => { @@ -67,20 +67,20 @@ describe('chrome.tabs.onCreated.addListener', () => { await new Promise((r) => setTimeout(r, 2000)); - expect(synchnorousCookieStore.tabs[123456]).toBeTruthy(); + expect(dataStore.tabs[123456]).toBeTruthy(); }); }); describe('Single Tab Mode', () => { beforeEach(() => { - synchnorousCookieStore.globalIsUsingCDP = false; - synchnorousCookieStore.tabMode = 'single'; - synchnorousCookieStore.addTabData(123456); - synchnorousCookieStore.tabToRead = ''; + dataStore.globalIsUsingCDP = false; + dataStore.tabMode = 'single'; + dataStore.addTabData(123456); + dataStore.tabToRead = ''; }); afterEach(() => { - synchnorousCookieStore.tabs = {}; - synchnorousCookieStore.tabsData = {}; + dataStore.tabs = {}; + dataStore.tabsData = {}; }); test('Openeing new tabs should create an entry for the new tab in synchnorous cookie store.', async () => { @@ -102,7 +102,7 @@ describe('chrome.tabs.onCreated.addListener', () => { await new Promise((r) => setTimeout(r, 2000)); - expect(synchnorousCookieStore.tabs[123456]).toBeTruthy(); + expect(dataStore.tabs[123456]).toBeTruthy(); }); test('Openeing more than 1 tab in single tab processing mode should not create an entry in synchrorous cookie store.', async () => { @@ -140,7 +140,7 @@ describe('chrome.tabs.onCreated.addListener', () => { await new Promise((r) => setTimeout(r, 2000)); - expect(synchnorousCookieStore.tabs[24567]).not.toBeTruthy(); + expect(dataStore.tabs[24567]).not.toBeTruthy(); }); }); @@ -162,8 +162,6 @@ describe('chrome.tabs.onCreated.addListener', () => { await new Promise((r) => setTimeout(r, 2000)); - expect(Object.keys(synchnorousCookieStore.tabs).length).toBeLessThanOrEqual( - 1 - ); + expect(Object.keys(dataStore.tabs).length).toBeLessThanOrEqual(1); }); }); diff --git a/packages/extension/src/serviceWorker/chromeListeners/utils/setupIntervals.ts b/packages/extension/src/serviceWorker/chromeListeners/utils/setupIntervals.ts index 74d41c780..b0d87f663 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/utils/setupIntervals.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/utils/setupIntervals.ts @@ -17,7 +17,7 @@ /** * Internal dependencies */ -import synchnorousCookieStore from '../../../store/synchnorousCookieStore'; +import dataStore from '../../../store/dataStore'; import { getAndParseNetworkCookies } from '../../../utils/getAndParseNetworkCookies'; const setupIntervals = () => { @@ -29,22 +29,22 @@ const setupIntervals = () => { // @todo Send tab data of the active tab only, also if sending only the difference would make it any faster. setInterval(() => { - if (Object.keys(synchnorousCookieStore?.tabsData ?? {}).length === 0) { + if (Object.keys(dataStore?.tabsData ?? {}).length === 0) { return; } - Object.keys(synchnorousCookieStore?.tabsData ?? {}).forEach((key) => { - synchnorousCookieStore?.sendUpdatedDataToPopupAndDevTools(Number(key)); + Object.keys(dataStore?.tabsData ?? {}).forEach((key) => { + dataStore?.sendUpdatedDataToPopupAndDevTools(Number(key)); }); }, 1200); // @todo Send tab data of the active tab only, also if sending only the difference would make it any faster. setInterval(() => { - if (Object.keys(synchnorousCookieStore?.tabsData ?? {}).length === 0) { + if (Object.keys(dataStore?.tabsData ?? {}).length === 0) { return; } - Object.keys(synchnorousCookieStore?.tabsData ?? {}).forEach((key) => { + Object.keys(dataStore?.tabsData ?? {}).forEach((key) => { getAndParseNetworkCookies(key); }); }, 5000); diff --git a/packages/extension/src/serviceWorker/chromeListeners/utils/updateGlobalVariableAndAttachCDP.ts b/packages/extension/src/serviceWorker/chromeListeners/utils/updateGlobalVariableAndAttachCDP.ts index 72cc2f0ca..94430bdc1 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/utils/updateGlobalVariableAndAttachCDP.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/utils/updateGlobalVariableAndAttachCDP.ts @@ -16,7 +16,7 @@ /** * Internal dependencies */ -import synchnorousCookieStore from '../../../store/synchnorousCookieStore'; +import dataStore from '../../../store/dataStore'; import attachCDP from '../../attachCDP'; const updateGlobalVariableAndAttachCDP = async () => { @@ -24,40 +24,35 @@ const updateGlobalVariableAndAttachCDP = async () => { const preSetSettings = await chrome.storage.sync.get(); - synchnorousCookieStore.tabMode = - preSetSettings?.allowedNumberOfTabs ?? 'single'; - synchnorousCookieStore.globalIsUsingCDP = preSetSettings?.isUsingCDP ?? false; + dataStore.tabMode = preSetSettings?.allowedNumberOfTabs ?? 'single'; + dataStore.globalIsUsingCDP = preSetSettings?.isUsingCDP ?? false; - if (synchnorousCookieStore.tabMode === 'unlimited') { + if (dataStore.tabMode === 'unlimited') { const allTabs = await chrome.tabs.query({}); const targets = await chrome.debugger.getTargets(); - await Promise.all( - allTabs.map(async (tab) => { - if (!tab.id || tab.url?.startsWith('chrome://')) { - return; - } + allTabs.forEach((tab) => { + if (!tab.id || tab.url?.startsWith('chrome://')) { + return; + } - synchnorousCookieStore?.addTabData(tab.id); + dataStore?.addTabData(tab.id); - if (synchnorousCookieStore.globalIsUsingCDP) { - synchnorousCookieStore.initialiseVariablesForNewTab( - tab.id.toString() - ); + if (dataStore.globalIsUsingCDP) { + dataStore.initialiseVariablesForNewTab(tab.id.toString()); - await attachCDP({ tabId: tab.id }); + attachCDP({ tabId: tab.id }); - const currentTab = targets.filter( - ({ tabId }) => tabId && tab.id && tabId === tab.id - ); - synchnorousCookieStore?.updateParentChildFrameAssociation( - tab.id, - currentTab[0].id, - '0' - ); - } - }) - ); + const currentTab = targets.filter( + ({ tabId }) => tabId && tab.id && tabId === tab.id + ); + dataStore?.updateParentChildFrameAssociation( + tab.id, + currentTab[0].id, + '0' + ); + } + }); } }; diff --git a/packages/extension/src/serviceWorker/chromeListeners/windowsOnRemovedListener.ts b/packages/extension/src/serviceWorker/chromeListeners/windowsOnRemovedListener.ts index 2e4d2fda2..73d810e06 100644 --- a/packages/extension/src/serviceWorker/chromeListeners/windowsOnRemovedListener.ts +++ b/packages/extension/src/serviceWorker/chromeListeners/windowsOnRemovedListener.ts @@ -16,17 +16,17 @@ /** * Internal dependencies */ -import synchnorousCookieStore from '../../store/synchnorousCookieStore'; +import dataStore from '../../store/dataStore'; export const windowsOnRemovedListener = (windowId: number) => { chrome.tabs.query({ windowId }, (tabs) => { tabs.map((tab) => { if (tab.id) { - synchnorousCookieStore.deinitialiseVariablesForTab(tab.id.toString()); + dataStore.deinitialiseVariablesForTab(tab.id.toString()); } return tab; }); }); - synchnorousCookieStore?.removeWindowData(windowId); + dataStore?.removeWindowData(windowId); }; diff --git a/packages/extension/src/serviceWorker/index.ts b/packages/extension/src/serviceWorker/index.ts index 08f5aced2..ade9fc5f9 100644 --- a/packages/extension/src/serviceWorker/index.ts +++ b/packages/extension/src/serviceWorker/index.ts @@ -21,17 +21,24 @@ import { Protocol } from 'devtools-protocol'; /** * Internal dependencies. */ -import syncCookieStore from '../store/synchnorousCookieStore'; import createCookieFromAuditsIssue from '../utils/createCookieFromAuditsIssue'; -import attachCDP from './attachCDP'; + import './chromeListeners'; +import dataStore from '../store/dataStore'; +import cookieStore from '../store/cookieStore'; +import PAStore from '../store/PAStore'; const ALLOWED_EVENTS = [ 'Network.responseReceived', 'Network.requestWillBeSentExtraInfo', 'Network.responseReceivedExtraInfo', + 'Network.loadingFailed', + 'Storage.interestGroupAuctionEventOccurred', + 'Storage.interestGroupAuctionNetworkRequestCreated', + 'Network.loadingFinished', 'Audits.issueAdded', 'Network.requestWillBeSent', + 'Storage.interestGroupAccessed', 'Page.frameAttached', 'Page.frameNavigated', 'Target.attachedToTarget', @@ -62,28 +69,65 @@ chrome.debugger.onEvent.addListener((source, method, params) => { targets = await chrome.debugger.getTargets(); - await Promise.all( - targets.map(async ({ id, url }) => { - if (url.startsWith('http')) { - await attachCDP({ targetId: id }); - } - }) - ); - // This is to get a list of all targets being attached to the main frame. if (method === 'Target.attachedToTarget' && params) { const { targetInfo: { targetId, url }, } = params as Protocol.Target.AttachedToTargetEvent; - await attachCDP({ targetId }); + const childDebuggee = { targetId }; + chrome.debugger.attach(childDebuggee, '1.3', async () => { + if (chrome.runtime.lastError) { + // eslint-disable-next-line no-console + console.warn(chrome.runtime.lastError); + } + try { + await chrome.debugger.sendCommand( + childDebuggee, + 'Storage.setInterestGroupAuctionTracking', + { enable: true } + ); + + await chrome.debugger.sendCommand( + childDebuggee, + 'Network.enable', + {} + ); + + await chrome.debugger.sendCommand( + childDebuggee, + 'Audits.enable', + {} + ); + + await chrome.debugger.sendCommand(childDebuggee, 'Page.enable', {}); + + const message = { + id: 0, + method: 'Runtime.runIfWaitingForDebugger', + params: {}, + }; + + await chrome.debugger.sendCommand( + source, + 'Target.sendMessageToTarget', + { + message: JSON.stringify(message), + targetId: targetId, + } + ); + } catch (error) { + // eslint-disable-next-line no-console + console.warn(error); + } + }); targets = await chrome.debugger.getTargets(); const parentFrameId = targets.filter( (target) => target?.tabId && target.tabId === source.tabId )[0]?.id; - syncCookieStore?.addFrameToTabAndUpdateMetadata( + dataStore?.addFrameToTabAndUpdateMetadata( source.tabId ?? null, source.targetId ?? null, targetId, @@ -98,10 +142,10 @@ chrome.debugger.onEvent.addListener((source, method, params) => { if (source?.tabId) { tabId = source?.tabId?.toString(); } else if (source.targetId) { - const tab = Object.keys(syncCookieStore?.tabs ?? {}).filter( + const tab = Object.keys(dataStore?.tabs ?? {}).filter( (key) => source.targetId && - syncCookieStore?.getFrameIDSet(Number(key))?.has(source.targetId) + dataStore?.getFrameIDSet(Number(key))?.has(source.targetId) ); tabId = tab[0]; } @@ -110,7 +154,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => { const { frameId, parentFrameId } = params as Protocol.Page.FrameAttachedEvent; - await syncCookieStore?.addFrameToTabAndUpdateMetadata( + await dataStore?.addFrameToTabAndUpdateMetadata( source.tabId ?? null, source.targetId ?? null, frameId, @@ -129,7 +173,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => { return; } - await syncCookieStore?.addFrameToTabAndUpdateMetadata( + await dataStore?.addFrameToTabAndUpdateMetadata( source.tabId ?? null, source.targetId ?? null, id, @@ -138,10 +182,58 @@ chrome.debugger.onEvent.addListener((source, method, params) => { ); } + if (dataStore.tabMode !== 'unlimited' && dataStore.tabToRead !== tabId) { + return; + } + + if (method === 'Storage.interestGroupAuctionEventOccurred' && params) { + const interestGroupAuctionEventOccured = + params as Protocol.Storage.InterestGroupAuctionEventOccurredEvent; + + const { uniqueAuctionId, eventTime, auctionConfig, parentAuctionId } = + interestGroupAuctionEventOccured; + + dataStore.auctionDataForTabId[tabId][uniqueAuctionId] = { + ...(dataStore.auctionDataForTabId[tabId]?.[uniqueAuctionId] ?? {}), + auctionConfig, + parentAuctionId, + auctionTime: eventTime, + }; + + PAStore.processInterestGroupAuctionEventOccurred( + interestGroupAuctionEventOccured, + tabId + ); + + return; + } + + if (method === 'Storage.interestGroupAccessed' && params) { + const interestGroupAccessedParams = + params as Protocol.Storage.InterestGroupAccessedEvent; + + PAStore.processInterestGroupEvent(interestGroupAccessedParams, tabId); + } + if ( - syncCookieStore.tabMode !== 'unlimited' && - syncCookieStore.tabToRead !== tabId + method === 'Storage.interestGroupAuctionNetworkRequestCreated' && + params ) { + const interestGroupAuctionNetworkRequestCreatedParams = + params as Protocol.Storage.InterestGroupAuctionNetworkRequestCreatedEvent; + + const { auctions, type, requestId } = + interestGroupAuctionNetworkRequestCreatedParams; + + dataStore.unParsedRequestHeadersForPA[tabId][requestId] = { + auctions, + type, + }; + + if (dataStore.requestIdToCDPURLMapping[tabId][requestId]) { + PAStore.processStartFetchEvents(auctions, tabId, requestId, type); + } + return; } @@ -152,14 +244,12 @@ chrome.debugger.onEvent.addListener((source, method, params) => { requestId, request: { url: requestUrl }, frameId = '', + timestamp, + wallTime, } = params as Protocol.Network.RequestWillBeSentEvent; let finalFrameId = frameId; - if (!finalFrameId) { - return; - } - targets = await chrome.debugger.getTargets(); const setTargets = new Set(); @@ -168,35 +258,49 @@ chrome.debugger.onEvent.addListener((source, method, params) => { return id; }); - finalFrameId = syncCookieStore.addFrameIdAndRequestUrlToResourceMap( + finalFrameId = dataStore.addFrameIdAndRequestUrlToResourceMap( tabId, frameId, setTargets, requestUrl ); - if (!syncCookieStore.requestIdToCDPURLMapping[tabId]) { - syncCookieStore.requestIdToCDPURLMapping[tabId] = { + if (!dataStore.requestIdToCDPURLMapping[tabId]) { + dataStore.requestIdToCDPURLMapping[tabId] = { [requestId]: { finalFrameId, frameId, url: requestUrl, + timeStamp: timestamp, + wallTime, }, }; } else { - syncCookieStore.requestIdToCDPURLMapping[tabId] = { - ...syncCookieStore.requestIdToCDPURLMapping[tabId], + dataStore.requestIdToCDPURLMapping[tabId] = { + ...dataStore.requestIdToCDPURLMapping[tabId], [requestId]: { finalFrameId, frameId, url: requestUrl, + timeStamp: timestamp, + wallTime, }, }; } - if (syncCookieStore.unParsedRequestHeaders[tabId][requestId]) { - syncCookieStore.parseRequestHeaders( - syncCookieStore.unParsedRequestHeaders[tabId][requestId], + if (dataStore.unParsedRequestHeadersForPA[tabId][requestId]) { + const { auctions, type } = + dataStore.unParsedRequestHeadersForPA[tabId][requestId]; + + PAStore.processStartFetchEvents(auctions, tabId, requestId, type); + } + + if ( + dataStore.tabs[Number(tabId)]?.isCookieAnalysisEnabled && + dataStore.unParsedRequestHeadersForCA[tabId][requestId] + ) { + cookieStore.parseRequestHeadersForCA( + dataStore.unParsedRequestHeadersForCA[tabId][requestId], requestId, tabId, Array.from(new Set([finalFrameId, frameId])) @@ -205,28 +309,52 @@ chrome.debugger.onEvent.addListener((source, method, params) => { return; } + if (method === 'Network.loadingFinished' && params) { + const loadingFinishedParams = + params as Protocol.Network.LoadingFinishedEvent; + + PAStore.parseRequestHeadersForPA( + loadingFinishedParams.requestId, + loadingFinishedParams.timestamp, + tabId, + 'Finished Fetch' + ); + } + + if (method === 'Network.loadingFailed' && params) { + const loadingFailedParams = + params as Protocol.Network.LoadingFinishedEvent; + + PAStore.parseRequestHeadersForPA( + loadingFailedParams.requestId, + loadingFailedParams.timestamp, + tabId, + 'Failed Fetch' + ); + } + if (method === 'Network.requestWillBeSentExtraInfo') { const { requestId } = params as Protocol.Network.RequestWillBeSentExtraInfoEvent; - if (syncCookieStore.requestIdToCDPURLMapping[tabId]?.[requestId]) { - syncCookieStore.parseRequestHeaders( + if (dataStore.requestIdToCDPURLMapping[tabId]?.[requestId]) { + cookieStore.parseRequestHeadersForCA( params as Protocol.Network.RequestWillBeSentExtraInfoEvent, requestId, tabId, Array.from( new Set([ - syncCookieStore.requestIdToCDPURLMapping[tabId][requestId] + dataStore.requestIdToCDPURLMapping[tabId][requestId] ?.finalFrameId, - syncCookieStore.requestIdToCDPURLMapping[tabId][requestId] - ?.frameId, + dataStore.requestIdToCDPURLMapping[tabId][requestId]?.frameId, ]) ) ); } else { - syncCookieStore.unParsedRequestHeaders[tabId][requestId] = + dataStore.unParsedRequestHeadersForCA[tabId][requestId] = params as Protocol.Network.RequestWillBeSentExtraInfoEvent; } + return; } @@ -254,25 +382,27 @@ chrome.debugger.onEvent.addListener((source, method, params) => { return id; }); - finalFrameId = syncCookieStore.addFrameIdAndRequestUrlToResourceMap( + finalFrameId = dataStore.addFrameIdAndRequestUrlToResourceMap( tabId, frameId, setTargets, requestUrl ); - if (!syncCookieStore.requestIdToCDPURLMapping[tabId]) { - syncCookieStore.requestIdToCDPURLMapping[tabId] = { + if (!dataStore.requestIdToCDPURLMapping[tabId]) { + dataStore.requestIdToCDPURLMapping[tabId] = { [requestId]: { + ...(dataStore.requestIdToCDPURLMapping[tabId]?.[requestId] ?? {}), finalFrameId, frameId, url: requestUrl, }, }; } else { - syncCookieStore.requestIdToCDPURLMapping[tabId] = { - ...syncCookieStore.requestIdToCDPURLMapping[tabId], + dataStore.requestIdToCDPURLMapping[tabId] = { + ...dataStore.requestIdToCDPURLMapping[tabId], [requestId]: { + ...dataStore.requestIdToCDPURLMapping[tabId][requestId], finalFrameId, frameId, url: requestUrl, @@ -280,25 +410,24 @@ chrome.debugger.onEvent.addListener((source, method, params) => { }; } - if (syncCookieStore.unParsedResponseHeaders[tabId][requestId]) { - syncCookieStore.parseResponseHeaders( - syncCookieStore.unParsedResponseHeaders[tabId][requestId], + if (dataStore.unParsedResponseHeadersForCA[tabId][requestId]) { + cookieStore.parseResponseHeadersForCA( + dataStore.unParsedResponseHeadersForCA[tabId][requestId], requestId, tabId, Array.from(new Set([finalFrameId, frameId])) ); } - if (syncCookieStore.unParsedRequestHeaders[tabId][requestId]) { - syncCookieStore.parseRequestHeaders( - syncCookieStore.unParsedRequestHeaders[tabId][requestId], + if (dataStore.unParsedRequestHeadersForCA[tabId][requestId]) { + cookieStore.parseRequestHeadersForCA( + dataStore.unParsedRequestHeadersForCA[tabId][requestId], requestId, tabId, Array.from(new Set([finalFrameId, frameId])) ); } - delete syncCookieStore.requestIdToCDPURLMapping[tabId][requestId]; return; } @@ -311,33 +440,32 @@ chrome.debugger.onEvent.addListener((source, method, params) => { return; } - if (syncCookieStore.requestIdToCDPURLMapping[tabId][requestId]) { + if (dataStore.requestIdToCDPURLMapping[tabId][requestId]) { const frameIds = Array.from( new Set([ - syncCookieStore.requestIdToCDPURLMapping[tabId][requestId] + dataStore.requestIdToCDPURLMapping[tabId][requestId] ?.finalFrameId, - syncCookieStore.requestIdToCDPURLMapping[tabId][requestId] - ?.frameId, + dataStore.requestIdToCDPURLMapping[tabId][requestId]?.frameId, ]) ); - syncCookieStore.parseResponseHeaders( + cookieStore.parseResponseHeadersForCA( params as Protocol.Network.ResponseReceivedExtraInfoEvent, requestId, tabId, frameIds ); - if (syncCookieStore.unParsedRequestHeaders[tabId][requestId]) { - syncCookieStore.parseRequestHeaders( - syncCookieStore.unParsedRequestHeaders[tabId][requestId], + if (dataStore.unParsedRequestHeadersForCA[tabId][requestId]) { + cookieStore.parseRequestHeadersForCA( + dataStore.unParsedRequestHeadersForCA[tabId][requestId], requestId, tabId, frameIds ); } } else { - syncCookieStore.unParsedResponseHeaders[tabId][requestId] = + dataStore.unParsedResponseHeadersForCA[tabId][requestId] = params as Protocol.Network.ResponseReceivedExtraInfoEvent; } return; @@ -372,14 +500,14 @@ chrome.debugger.onEvent.addListener((source, method, params) => { const cookieObjectToUpdate = createCookieFromAuditsIssue( cookieIssueDetails, - syncCookieStore?.getTabUrl(Number(tabId)) ?? '', + dataStore?.getTabUrl(Number(tabId)) ?? '', [], - syncCookieStore.requestIdToCDPURLMapping[tabId][requestId]?.url, - syncCookieStore.cookieDB ?? {} + dataStore.requestIdToCDPURLMapping[tabId][requestId]?.url, + dataStore.cookieDB ?? {} ); if (cookieObjectToUpdate) { - syncCookieStore?.update(Number(tabId), [cookieObjectToUpdate]); + cookieStore?.update(Number(tabId), [cookieObjectToUpdate]); } return; } diff --git a/packages/extension/src/store/PAStore.ts b/packages/extension/src/store/PAStore.ts new file mode 100644 index 000000000..495d6772e --- /dev/null +++ b/packages/extension/src/store/PAStore.ts @@ -0,0 +1,285 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * External dependencies. + */ +import type { singleAuctionEvent } from '@google-psat/common'; +import type { Protocol } from 'devtools-protocol'; + +/** + * Internal dependencies. + */ +import networkTime from './utils/networkTime'; +import formatTime from './utils/formatTime'; +import dataStore from './dataStore'; + +class PAStore { + /** + * This function parses response headers for Protected Analysis PA. + * @param {string} requestId This is used to get the related data for parsing the request. + * @param { Protocol.Network.MonotonicTime } timestamp Timestamp of the request + * @param {string} tabId The tabId this request is associated to. + * @param {string} method determines which event called the function. + */ + parseRequestHeadersForPA( + requestId: string, + timestamp: Protocol.Network.MonotonicTime, + tabId: string, + method: string + ) { + if (!dataStore.unParsedRequestHeadersForPA[tabId][requestId]?.auctions) { + return; + } + const { auctions, type } = + dataStore.unParsedRequestHeadersForPA[tabId][requestId]; + + const calculatedNetworkTime = networkTime(requestId, timestamp, tabId); + + auctions.forEach((uniqueAuctionId) => { + if (!dataStore.auctionDataForTabId[tabId][uniqueAuctionId]) { + return; + } + const { auctionConfig = {} } = + dataStore.auctionDataForTabId[tabId][uniqueAuctionId]; + + this.getAuctionEventsArray(tabId, uniqueAuctionId).push({ + uniqueAuctionId, + bidCurrency: auctionConfig?.bidCurrency ?? '', + bid: auctionConfig?.bid ?? null, + name: auctionConfig?.name ?? '', + ownerOrigin: auctionConfig?.ownerOrigin ?? '', + type: method + type, + formattedTime: + this.getAuctionEventsArray(tabId, uniqueAuctionId).length === 0 + ? '0 ms' + : formatTime( + dataStore.auctionEvents[tabId][uniqueAuctionId][0].time, + networkTime(requestId, timestamp, tabId) + ), + time: calculatedNetworkTime, + auctionConfig, + parentAuctionId: uniqueAuctionId + ? dataStore.auctionDataForTabId[tabId]?.[uniqueAuctionId] + ?.parentAuctionId + : undefined, + eventType: 'interestGroupAuctionNetworkRequestCompleted', + }); + dataStore.tabs[parseInt(tabId)].newUpdatesPA++; + }); + } + + /** + * Decides if auction is multiSeller or singleSeller + * @param {singleAuctionEvent[]} auctionEvents This is used to get the related data for parsing the request. + * @returns { boolean } True for multiSeller False for singleSeller + */ + isMUltiSellerAuction(auctionEvents: singleAuctionEvent[]): boolean { + const uniqueSellers = new Set(); + + auctionEvents + //@ts-ignore -- Ignoring this for now since we dont have any type of auctionConfig + .filter((event) => Boolean(event.auctionConfig?.seller)) + //@ts-ignore -- Ignoring this for now since we dont have any type of auctionConfig + .forEach(({ auctionConfig }) => uniqueSellers.add(auctionConfig?.seller)); + + return uniqueSellers.size > 1; + } + + /** + * Processes interest group acccess event. + * @param {Protocol.Storage.InterestGroupAccessedEvent} interestGroupAccessedParams The params that were passed when interestGroupAccessed Event was fired + * @param {string} tabId The tabId for which the event has to be added. + */ + processInterestGroupEvent( + interestGroupAccessedParams: Protocol.Storage.InterestGroupAccessedEvent, + tabId: string + ) { + const { uniqueAuctionId, accessTime, ownerOrigin, name, type } = + interestGroupAccessedParams; + + let eventData: singleAuctionEvent = { + uniqueAuctionId, + name, + ownerOrigin, + formattedTime: new Date(accessTime * 1000), + type, + time: accessTime, + eventType: 'interestGroupAccessed' as singleAuctionEvent['eventType'], + }; + + this.getAuctionEventsArray(tabId, 'globalEvents').push(eventData); + + dataStore.tabs[parseInt(tabId)].newUpdatesPA++; + + if (!uniqueAuctionId) { + return; + } + + const { + bid: initialBidValue, + componentSellerOrigin, + bidCurrency: initialBidCurrencyValue, + } = interestGroupAccessedParams; + + let bid; + + if (initialBidValue) { + bid = initialBidValue; + } + + if (uniqueAuctionId && !initialBidValue && type === 'win') { + bid = this.getAuctionEventsArray(tabId, uniqueAuctionId).filter( + ({ + type: storedType, + eventType, + interestGroupConfig: { uniqueAuctionId: eventAuctionId } = {}, + }) => + storedType === 'bid' && + eventType === 'interestGroupAccessed' && + eventAuctionId && + uniqueAuctionId && + eventAuctionId === interestGroupAccessedParams.uniqueAuctionId + )?.[0]?.bid; + } + + eventData = { + uniqueAuctionId, + name, + ownerOrigin, + formattedTime: + uniqueAuctionId && + this.getAuctionEventsArray(tabId, uniqueAuctionId).length === 0 + ? '0 ms' + : formatTime( + dataStore.auctionEvents[tabId][uniqueAuctionId]?.[0].time, + accessTime + ), + type, + time: accessTime, + parentAuctionId: + dataStore.auctionDataForTabId[tabId]?.[uniqueAuctionId] + ?.parentAuctionId ?? undefined, + eventType: 'interestGroupAccessed' as singleAuctionEvent['eventType'], + }; + + if (componentSellerOrigin) { + eventData.componentSellerOrigin = componentSellerOrigin; + } + + if (bid) { + eventData.bid = bid; + } + + if (initialBidCurrencyValue) { + eventData.bidCurrency = initialBidCurrencyValue; + } + + this.getAuctionEventsArray(tabId, uniqueAuctionId).push(eventData); + + dataStore.tabs[parseInt(tabId)].newUpdatesPA++; + } + + /** + * Process StartFetchEvents + * @param {Protocol.Storage.InterestGroupAuctionId[]} auctions Unique auction id's which need to be added to the event data store. + * @param {string} tabId The tabId for which the event has to be added. + * @param {string} requestId The requestId associated with the event. + * @param {string} type The type of JS which was fetched. + */ + processStartFetchEvents( + auctions: Protocol.Storage.InterestGroupAuctionId[], + tabId: string, + requestId: string, + type: string + ) { + const time: number = + networkTime( + requestId, + dataStore.requestIdToCDPURLMapping[tabId][requestId].timeStamp, + tabId + ) ?? new Date().getTime(); + + auctions.forEach((uniqueAuctionId) => { + this.getAuctionEventsArray(tabId, uniqueAuctionId).push({ + uniqueAuctionId, + formattedTime: + dataStore.auctionEvents[tabId][uniqueAuctionId].length === 0 + ? '0 ms' + : formatTime( + dataStore.auctionEvents[tabId][uniqueAuctionId][0].time, + time + ), + type: 'Start fetch ' + type, + time, + parentAuctionId: + dataStore.auctionDataForTabId[tabId]?.[uniqueAuctionId] + ?.parentAuctionId, + eventType: 'interestGroupAuctionNetworkRequestCreated', + }); + + dataStore.tabs[parseInt(tabId)].newUpdatesPA++; + }); + } + + /** + * Process InterestGroupAuctionEventOccurred + * @param {Protocol.Storage.InterestGroupAuctionEventOccurredEvent} interestGroupAuctionEventOccured Event data passed to the InterestGroupAuctionEventOccurred + * @param {string} tabId The tabId for which the event has to be added. + */ + processInterestGroupAuctionEventOccurred( + interestGroupAuctionEventOccured: Protocol.Storage.InterestGroupAuctionEventOccurredEvent, + tabId: string + ) { + const { uniqueAuctionId, eventTime, type, auctionConfig, parentAuctionId } = + interestGroupAuctionEventOccured; + + const eventData = { + uniqueAuctionId, + type, + formattedTime: + this.getAuctionEventsArray(tabId, uniqueAuctionId).length === 0 + ? '0 ms' + : formatTime( + dataStore.auctionEvents[tabId][uniqueAuctionId][0].time, + eventTime + ), + time: eventTime, + auctionConfig, + parentAuctionId, + eventType: + 'interestGroupAuctionEventOccurred' as singleAuctionEvent['eventType'], + }; + + this.getAuctionEventsArray(tabId, uniqueAuctionId).push(eventData); + + dataStore.tabs[parseInt(tabId)].newUpdatesPA++; + } + + /** + * Push into auction events + * @param {string} tabId The ID of the tab auction event is associated with. + * @param {string} uniqueAuctionId The ID of the auction event whose array is to be fetched. + * @returns {object} An object holder reporesenting the event data. + */ + getAuctionEventsArray(tabId: string, uniqueAuctionId: string) { + if (!dataStore.auctionEvents[tabId][uniqueAuctionId]) { + dataStore.auctionEvents[tabId][uniqueAuctionId] = []; + } + return dataStore.auctionEvents[tabId][uniqueAuctionId]; + } +} + +export default new PAStore(); diff --git a/packages/extension/src/store/cookieStore.ts b/packages/extension/src/store/cookieStore.ts new file mode 100644 index 000000000..2bbf303f2 --- /dev/null +++ b/packages/extension/src/store/cookieStore.ts @@ -0,0 +1,277 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * External dependencies. + */ +import { + getCookieKey, + type CookieData, + type BlockedReason, + parseResponseReceivedExtraInfo, + parseRequestWillBeSentExtraInfo, + deriveBlockingStatus, +} from '@google-psat/common'; +import type { Protocol } from 'devtools-protocol'; + +/** + * Internal dependencies. + */ +import updateCookieBadgeText from './utils/updateCookieBadgeText'; +import dataStore from './dataStore'; +import shouldUpdateCounter from '../utils/shouldUpdateCounter'; + +class CookieStore { + /** + * This function parses response headers for Cookie Analysis. + * @param {Protocol.Network.ResponseReceivedExtraInfoEvent} response The response to be parsed. + * @param {string} requestId This is used to get the related data for parsing the response. + * @param {string} tabId The tabId this request is associated to. + * @param {string[]} frameIds This is used to associate the cookies from request to set of frameIds. + */ + parseResponseHeadersForCA( + response: Protocol.Network.ResponseReceivedExtraInfoEvent, + requestId: string, + tabId: string, + frameIds: string[] + ) { + const { headers, blockedCookies, cookiePartitionKey, exemptedCookies } = + response; + + const cookies: CookieData[] = parseResponseReceivedExtraInfo( + headers, + blockedCookies, + exemptedCookies, + cookiePartitionKey, + dataStore.requestIdToCDPURLMapping[tabId][requestId]?.url ?? '', + dataStore.tabs[Number(tabId)].url ?? '', + dataStore.cookieDB ?? {}, + frameIds, + requestId + ); + this.update(Number(tabId), cookies); + + delete dataStore.unParsedResponseHeadersForCA[tabId][requestId]; + } + + /** + * This function parses request headers for Cookie Analysis. + * @param {Protocol.Network.RequestWillBeSentExtraInfoEvent} request The response to be parsed. + * @param {string} requestId This is used to get the related data for parsing the response. + * @param {string} tabId The tabId this request is associated to. + * @param {string[]} frameIds This is used to associate the cookies from request to set of frameIds. + */ + parseRequestHeadersForCA( + request: Protocol.Network.RequestWillBeSentExtraInfoEvent, + requestId: string, + tabId: string, + frameIds: string[] + ) { + const { associatedCookies } = request; + + const cookies: CookieData[] = parseRequestWillBeSentExtraInfo( + associatedCookies, + dataStore.cookieDB ?? {}, + dataStore.requestIdToCDPURLMapping[tabId][requestId]?.url ?? '', + dataStore.tabs[Number(tabId)].url ?? '', + frameIds, + requestId + ); + + delete dataStore.unParsedRequestHeadersForCA[tabId][requestId]; + if (cookies.length === 0) { + return; + } + + this.update(Number(tabId), cookies); + delete dataStore.unParsedRequestHeadersForCA[tabId][requestId]; + } + + /** + * Adds exclusion and warning reasons for a given cookie. + * @param {string} cookieName Name of the cookie. + * @param {string[]} exclusionReasons reasons to be added to the blocked reason array. + * @param {string[]} warningReasons warning reasons to be added to the warning reason array. + * @param {number} tabId tabId where change has to be made. + */ + addCookieExclusionWarningReason( + cookieName: string, + exclusionReasons: BlockedReason[], + warningReasons: Protocol.Audits.CookieWarningReason[], + tabId: number + ) { + if (!dataStore.tabsData[tabId]) { + return; + } + if (dataStore.tabsData[tabId] && dataStore.tabsData[tabId][cookieName]) { + dataStore.tabsData[tabId][cookieName].blockedReasons = [ + ...new Set([ + ...(dataStore.tabsData[tabId][cookieName].blockedReasons ?? []), + ...exclusionReasons, + ]), + ]; + dataStore.tabsData[tabId][cookieName].warningReasons = [ + ...new Set([ + ...(dataStore.tabsData[tabId][cookieName].warningReasons ?? []), + ...warningReasons, + ]), + ]; + + dataStore.tabsData[tabId][cookieName].isBlocked = + exclusionReasons.length > 0 ? true : false; + dataStore.tabs[tabId].newUpdatesCA++; + } else { + dataStore.tabs[tabId].newUpdatesCA++; + // If none of them exists. This case is possible when the cookies hasnt processed and we already have an issue. + dataStore.tabsData[tabId] = { + ...dataStore.tabsData[tabId], + [cookieName]: { + ...(dataStore.tabsData[tabId][cookieName] ?? {}), + blockedReasons: [...exclusionReasons], + warningReasons: [...warningReasons], + isBlocked: exclusionReasons.length > 0 ? true : false, + }, + }; + } + } + + /** + * Update cookie store. + * @param {number} tabId Tab id. + * @param {Array} cookies Cookies data. + */ + // eslint-disable-next-line complexity + update(tabId: number, cookies: CookieData[]) { + try { + if (!dataStore.tabsData[tabId] || !dataStore.tabs[tabId]) { + return; + } + + for (const cookie of cookies) { + const cookieKey = getCookieKey(cookie.parsedCookie); + if (!cookieKey) { + continue; + } + + // Merge in previous blocked reasons. + const blockedReasons: BlockedReason[] = [ + ...new Set([ + ...(cookie?.blockedReasons ?? []), + ...(dataStore.tabsData[tabId]?.[cookieKey]?.blockedReasons ?? []), + ]), + ]; + + const warningReasons = Array.from( + new Set([ + ...(cookie?.warningReasons ?? []), + ...(dataStore.tabsData[tabId]?.[cookieKey]?.warningReasons ?? []), + ]) + ); + + const frameIdList = Array.from( + new Set([ + ...((cookie?.frameIdList ?? []) as number[]), + ...((dataStore.tabsData[tabId]?.[cookieKey]?.frameIdList ?? + []) as number[]), + ]) + ).map((frameId) => frameId.toString()); + + const updateCounterBoolean = shouldUpdateCounter( + dataStore.tabsData[tabId][cookieKey], + cookie + ); + + if (updateCounterBoolean) { + dataStore.tabs[tabId].newUpdatesCA++; + } + + if (dataStore.tabsData[tabId]?.[cookieKey]) { + // Merge in previous warning reasons. + const parsedCookie = { + ...dataStore.tabsData[tabId][cookieKey].parsedCookie, + ...cookie.parsedCookie, + samesite: ( + cookie.parsedCookie.samesite ?? + dataStore.tabsData[tabId][cookieKey].parsedCookie.samesite ?? + 'lax' + ).toLowerCase(), + httponly: + cookie.parsedCookie.httponly ?? + dataStore.tabsData[tabId][cookieKey].parsedCookie.httponly, + priority: + cookie.parsedCookie?.priority ?? + dataStore.tabsData[tabId][cookieKey].parsedCookie?.priority ?? + 'Medium', + partitionKey: '', + }; + if ( + cookie.parsedCookie?.partitionKey || + dataStore.tabsData[tabId][cookieKey].parsedCookie?.partitionKey + ) { + parsedCookie.partitionKey = + cookie.parsedCookie?.partitionKey || + dataStore.tabsData[tabId][cookieKey].parsedCookie?.partitionKey; + } + + const networkEvents: CookieData['networkEvents'] = { + requestEvents: [ + ...(dataStore.tabsData[tabId][cookieKey]?.networkEvents + ?.requestEvents || []), + ...(cookie.networkEvents?.requestEvents || []), + ], + responseEvents: [ + ...(dataStore.tabsData[tabId][cookieKey]?.networkEvents + ?.responseEvents || []), + ...(cookie.networkEvents?.responseEvents || []), + ], + }; + + dataStore.tabsData[tabId][cookieKey] = { + ...dataStore.tabsData[tabId][cookieKey], + ...cookie, + parsedCookie, + isBlocked: blockedReasons.length > 0, + blockedReasons, + networkEvents, + blockingStatus: deriveBlockingStatus(networkEvents), + warningReasons, + url: dataStore.tabsData[tabId][cookieKey].url ?? cookie.url, + headerType: + dataStore.tabsData[tabId][cookieKey].headerType === 'javascript' + ? dataStore.tabsData[tabId][cookieKey].headerType + : cookie.headerType, + frameIdList, + exemptionReason: + cookie?.exemptionReason || + dataStore.tabsData[tabId][cookieKey]?.exemptionReason, + }; + } else { + dataStore.tabsData[tabId][cookieKey] = { + ...cookie, + blockingStatus: deriveBlockingStatus(cookie.networkEvents), + }; + } + } + + updateCookieBadgeText(dataStore.tabsData[tabId], tabId); + } catch (error) { + //Fail silently + // eslint-disable-next-line no-console + console.warn(error); + } + } +} + +export default new CookieStore(); diff --git a/packages/extension/src/store/synchnorousCookieStore.ts b/packages/extension/src/store/dataStore.ts similarity index 60% rename from packages/extension/src/store/synchnorousCookieStore.ts rename to packages/extension/src/store/dataStore.ts index 46afb6b34..f223a3b96 100644 --- a/packages/extension/src/store/synchnorousCookieStore.ts +++ b/packages/extension/src/store/dataStore.ts @@ -1,3 +1,18 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ /* * Copyright 2023 Google LLC * @@ -16,28 +31,24 @@ /** * External dependencies. */ -import { - getCookieKey, - type CookieData, - type BlockedReason, - parseResponseReceivedExtraInfo, - type CookieDatabase, - parseRequestWillBeSentExtraInfo, - deriveBlockingStatus, +import type { + CookieData, + CookieDatabase, + singleAuctionEvent, + auctionData, } from '@google-psat/common'; import type { Protocol } from 'devtools-protocol'; /** * Internal dependencies. */ -import updateCookieBadgeText from './utils/updateCookieBadgeText'; import { NEW_COOKIE_DATA } from '../constants'; import isValidURL from '../utils/isValidURL'; import { doesFrameExist } from '../utils/doesFrameExist'; import { fetchDictionary } from '../utils/fetchCookieDictionary'; -import shouldUpdateCounter from '../utils/shouldUpdateCounter'; +import PAStore from './PAStore'; -class SynchnorousCookieStore { +class DataStore { /** * The cookie data of the tabs. */ @@ -47,6 +58,44 @@ class SynchnorousCookieStore { }; } = {}; + /** + * The auction event of the tabs (Interest group access as well as interest group auction events). + */ + auctionEvents: { + [tabId: string]: { + [uniqueAuctionId: string]: singleAuctionEvent[]; + }; + } = {}; + /** + * For tab 123456 auction events will have interestGroup accessed events as well as the interestGroupAuctionEvents. + * There can be 2 types of interestGroupAccessed events: + * 1) Join and Leave eventsare global and are fired on all the tabs. + * 2) Bid Win topLevelBid are fired where bidding is happening. + * + * To accomodate these 2 events we have 2 different keys in the auctionEvent for each tab: + * 1) The uniqueParentAuctionId, + * a) if its a multi-seller auction then it will have: + * i) a uniqueParentAuctionId where component auction Events are added. + * ii) top level auction in an id whose key is '0', + * b) if its a single sale then it will have a key with uniqueAuctionId where all the auctionEvents are added. + * 2) a 'globalEvents' key where all the global events are added like join leave etc. + * + * Structure may look like this: + * auctionEvents: { + * 123456: { + * 'globalEvents': [], + * '12413hsad23e1nsd': {} + * } + * }; + */ + + /** + * The auction data of the tabs which is added when interestGroupAuctionEvent occurs. + */ + auctionDataForTabId: { + [tabId: string]: auctionData; + } = {}; + /** * The cookie data of the tabs. */ @@ -71,6 +120,8 @@ class SynchnorousCookieStore { frameId: string; url: string; finalFrameId: string; + timeStamp: Protocol.Network.MonotonicTime; + wallTime: Protocol.Network.TimeSinceEpoch; }; }; } = {}; @@ -79,7 +130,7 @@ class SynchnorousCookieStore { * This variable stores the unParsedRequest headers received from Network.requestWillBeSentExtraInfo. * These are the requests whose Network.requestWillBeSent counter part havent yet been fired. */ - unParsedRequestHeaders: { + unParsedRequestHeadersForCA: { [tabId: string]: { [requestId: string]: Protocol.Network.RequestWillBeSentExtraInfoEvent; }; @@ -89,7 +140,20 @@ class SynchnorousCookieStore { * This variable stores the unParsedResonse headers received from Network.responseReceivedExtraInfo. * These are the responses whose Network.responseReceived counter part havent yet been fired. */ - unParsedResponseHeaders: { + unParsedRequestHeadersForPA: { + [tabId: string]: { + [requestId: string]: { + auctions: Protocol.Storage.InterestGroupAuctionId[]; + type: Protocol.Storage.InterestGroupAuctionFetchType; + }; + }; + } = {}; + + /** + * This variable stores the unParsedResonse headers received from Network.responseReceivedExtraInfo. + * These are the responses whose Network.responseReceived counter part havent yet been fired. + */ + unParsedResponseHeadersForCA: { [tabId: string]: { [requestId: string]: Protocol.Network.ResponseReceivedExtraInfoEvent; }; @@ -115,9 +179,12 @@ class SynchnorousCookieStore { url: string; devToolsOpenState: boolean; popupOpenState: boolean; - newUpdates: number; + newUpdatesCA: number; + newUpdatesPA: number; frameIDURLSet: Record; parentChildFrameAssociation: Record; + isCookieAnalysisEnabled: boolean; + isPAAnalysisEnabled: boolean; }; } = {}; @@ -172,71 +239,6 @@ class SynchnorousCookieStore { } } - /** - * This function parses response headers - * @param {Protocol.Network.ResponseReceivedExtraInfoEvent} response The response to be parsed. - * @param {string} requestId This is used to get the related data for parsing the response. - * @param {string} tabId The tabId this request is associated to. - * @param {string[]} frameIds This is used to associate the cookies from request to set of frameIds. - */ - parseResponseHeaders( - response: Protocol.Network.ResponseReceivedExtraInfoEvent, - requestId: string, - tabId: string, - frameIds: string[] - ) { - const { headers, blockedCookies, cookiePartitionKey, exemptedCookies } = - response; - - const cookies: CookieData[] = parseResponseReceivedExtraInfo( - headers, - blockedCookies, - exemptedCookies, - cookiePartitionKey, - this.requestIdToCDPURLMapping[tabId][requestId]?.url ?? '', - this.tabs[Number(tabId)].url ?? '', - this.cookieDB ?? {}, - frameIds, - requestId - ); - this.update(Number(tabId), cookies); - - delete this.unParsedResponseHeaders[tabId][requestId]; - } - - /** - * This function parses request headers - * @param {Protocol.Network.RequestWillBeSentExtraInfoEvent} request The response to be parsed. - * @param {string} requestId This is used to get the related data for parsing the response. - * @param {string} tabId The tabId this request is associated to. - * @param {string[]} frameIds This is used to associate the cookies from request to set of frameIds. - */ - parseRequestHeaders( - request: Protocol.Network.RequestWillBeSentExtraInfoEvent, - requestId: string, - tabId: string, - frameIds: string[] - ) { - const { associatedCookies } = request; - - const cookies: CookieData[] = parseRequestWillBeSentExtraInfo( - associatedCookies, - this.cookieDB ?? {}, - this.requestIdToCDPURLMapping[tabId][requestId]?.url ?? '', - this.tabs[Number(tabId)].url ?? '', - frameIds, - requestId - ); - - delete this.unParsedRequestHeaders[tabId][requestId]; - if (cookies.length === 0) { - return; - } - - this.update(Number(tabId), cookies); - delete this.unParsedRequestHeaders[tabId][requestId]; - } - /** * This function adds frame to the appropriate tab. * @param {number} tabId The tabId of the event if available. @@ -301,53 +303,6 @@ class SynchnorousCookieStore { ); } - /** - * Adds exclusion and warning reasons for a given cookie. - * @param {string} cookieName Name of the cookie. - * @param {string[]} exclusionReasons reasons to be added to the blocked reason array. - * @param {string[]} warningReasons warning reasons to be added to the warning reason array. - * @param {number} tabId tabId where change has to be made. - */ - addCookieExclusionWarningReason( - cookieName: string, - exclusionReasons: BlockedReason[], - warningReasons: Protocol.Audits.CookieWarningReason[], - tabId: number - ) { - if (!this.tabsData[tabId]) { - return; - } - if (this.tabsData[tabId] && this.tabsData[tabId][cookieName]) { - this.tabsData[tabId][cookieName].blockedReasons = [ - ...new Set([ - ...(this.tabsData[tabId][cookieName].blockedReasons ?? []), - ...exclusionReasons, - ]), - ]; - this.tabsData[tabId][cookieName].warningReasons = [ - ...new Set([ - ...(this.tabsData[tabId][cookieName].warningReasons ?? []), - ...warningReasons, - ]), - ]; - - this.tabsData[tabId][cookieName].isBlocked = - exclusionReasons.length > 0 ? true : false; - } else { - this.tabs[tabId].newUpdates++; - // If none of them exists. This case is possible when the cookies hasnt processed and we already have an issue. - this.tabsData[tabId] = { - ...this.tabsData[tabId], - [cookieName]: { - ...(this.tabsData[tabId][cookieName] ?? {}), - blockedReasons: [...exclusionReasons], - warningReasons: [...warningReasons], - isBlocked: exclusionReasons.length > 0 ? true : false, - }, - }; - } - } - /** * Creates an entry for a tab * @param {number} tabId The tab id. @@ -360,17 +315,25 @@ class SynchnorousCookieStore { globalThis.PSAT = { tabsData: this.tabsData, tabs: this.tabs, + auctionEvents: this.auctionEvents, }; this.tabsData[tabId] = {}; + this.auctionEvents[tabId.toString()] = {}; this.tabs[tabId] = { url: '', devToolsOpenState: false, popupOpenState: false, - newUpdates: 0, + newUpdatesCA: 0, + newUpdatesPA: 0, frameIDURLSet: {}, parentChildFrameAssociation: {}, + isCookieAnalysisEnabled: true, + isPAAnalysisEnabled: true, }; + + this.auctionDataForTabId[tabId] = {}; + (async () => { if (!this.cookieDB) { this.cookieDB = await fetchDictionary(); @@ -397,8 +360,8 @@ class SynchnorousCookieStore { * @param {string} tabId The tab whose data has to be deinitialised. */ deinitialiseVariablesForTab(tabId: string) { - delete this.unParsedRequestHeaders[tabId]; - delete this.unParsedResponseHeaders[tabId]; + delete this.unParsedRequestHeadersForCA[tabId]; + delete this.unParsedResponseHeadersForCA[tabId]; delete this.requestIdToCDPURLMapping[tabId]; delete this.frameIdToResourceMap[tabId]; } @@ -466,16 +429,19 @@ class SynchnorousCookieStore { * @param {string} tabId The tab whose data has to be initialised. */ initialiseVariablesForNewTab(tabId: string) { - this.unParsedRequestHeaders[tabId] = {}; - this.unParsedResponseHeaders[tabId] = {}; + this.unParsedRequestHeadersForCA[tabId] = {}; + this.unParsedResponseHeadersForCA[tabId] = {}; this.requestIdToCDPURLMapping[tabId] = {}; this.frameIdToResourceMap[tabId] = {}; + this.unParsedRequestHeadersForPA[tabId] = {}; //@ts-ignore globalThis.PSATAdditionalData = { - unParsedRequestHeaders: this.unParsedRequestHeaders, - unParsedResponseHeaders: this.unParsedResponseHeaders, + unParsedRequestHeadersForCA: this.unParsedRequestHeadersForCA, + unParsedResponseHeadersForCA: this.unParsedResponseHeadersForCA, requestIdToCDPURLMapping: this.requestIdToCDPURLMapping, frameIdToResourceMap: this.frameIdToResourceMap, + auctionDataForTabId: this.auctionDataForTabId, + unParsedRequestHeadersForPA: this.unParsedRequestHeadersForPA, }; } @@ -490,10 +456,17 @@ class SynchnorousCookieStore { delete this.tabsData[tabId]; this.tabsData[tabId] = {}; - this.tabs[tabId].newUpdates = 0; + this.tabs[tabId].newUpdatesCA = 0; + this.tabs[tabId].newUpdatesPA = 0; this.tabs[tabId].frameIDURLSet = {}; this.tabs[tabId].parentChildFrameAssociation = {}; - + Object.keys(this.auctionEvents[tabId.toString()]).forEach((key) => { + if (key === 'globalEvents') { + return; + } + delete this.auctionEvents[tabId.toString()][key]; + }); + this.auctionDataForTabId[tabId] = {}; this.sendUpdatedDataToPopupAndDevTools(tabId, true); } @@ -504,6 +477,8 @@ class SynchnorousCookieStore { removeTabData(tabId: number) { delete this.tabsData[tabId]; delete this.tabs[tabId]; + delete this.auctionDataForTabId[tabId]; + delete this.auctionEvents[tabId]; } /** @@ -533,181 +508,116 @@ class SynchnorousCookieStore { if (!this.tabs[tabId] || !this.tabsData[tabId]) { return; } - let sentMessageAnyWhere = false; try { if ( (this.tabs[tabId].devToolsOpenState || this.tabs[tabId].popupOpenState) && - (overrideForInitialSync || this.tabs[tabId].newUpdates > 0) + (overrideForInitialSync || + this.tabs[tabId].newUpdatesCA > 0 || + this.tabs[tabId].newUpdatesPA > 0) ) { - sentMessageAnyWhere = true; - - const newCookieData: { - [cookieKey: string]: CookieData; - } = {}; - - Object.keys(this.tabsData[tabId]).forEach((key) => { - newCookieData[key] = { - ...this.tabsData[tabId][key], - networkEvents: { - requestEvents: [], - responseEvents: [], - }, - url: '', - headerType: ['request', 'response'].includes( - this.tabsData[tabId][key]?.headerType ?? '' - ) - ? 'http' - : 'javascript', - }; - }); - - await chrome.runtime.sendMessage({ - type: NEW_COOKIE_DATA, - payload: { - tabId, - cookieData: newCookieData, - extraData: { - extraFrameData: this.tabs[tabId].frameIDURLSet, + if (this.tabs[tabId].newUpdatesCA > 0 || overrideForInitialSync) { + const newCookieData: { + [cookieKey: string]: CookieData; + } = {}; + + Object.keys(this.tabsData[tabId]).forEach((key) => { + newCookieData[key] = { + ...this.tabsData[tabId][key], + networkEvents: { + requestEvents: [], + responseEvents: [], + }, + url: '', + headerType: ['request', 'response'].includes( + this.tabsData[tabId][key]?.headerType ?? '' + ) + ? 'http' + : 'javascript', + }; + }); + + await chrome.runtime.sendMessage({ + type: NEW_COOKIE_DATA, + payload: { + tabId, + cookieData: newCookieData, + extraData: { + extraFrameData: this.tabs[tabId].frameIDURLSet, + }, }, - }, - }); - } - - if (sentMessageAnyWhere) { - this.tabs[tabId].newUpdates = 0; - } - } catch (error) { - // eslint-disable-next-line no-console - console.warn(error); - //Fail silently. Ignoring the console.warn here because the only error this will throw is of "Error: Could not establish connection". - } - } - - /** - * Update cookie store. - * @param {number} tabId Tab id. - * @param {Array} cookies Cookies data. - */ - // eslint-disable-next-line complexity - update(tabId: number, cookies: CookieData[]) { - try { - if (!this.tabsData[tabId] || !this.tabs[tabId]) { - return; - } + }); - for (const cookie of cookies) { - const cookieKey = getCookieKey(cookie.parsedCookie); - if (!cookieKey) { - continue; + this.tabs[tabId].newUpdatesCA = 0; } - // Merge in previous blocked reasons. - const blockedReasons: BlockedReason[] = [ - ...new Set([ - ...(cookie?.blockedReasons ?? []), - ...(this.tabsData[tabId]?.[cookieKey]?.blockedReasons ?? []), - ]), - ]; - - const warningReasons = Array.from( - new Set([ - ...(cookie?.warningReasons ?? []), - ...(this.tabsData[tabId]?.[cookieKey]?.warningReasons ?? []), - ]) - ); + const { globalEvents, ...rest } = this.auctionEvents[tabId]; - const frameIdList = Array.from( - new Set([ - ...((cookie?.frameIdList ?? []) as number[]), - ...((this.tabsData[tabId]?.[cookieKey]?.frameIdList ?? - []) as number[]), - ]) - ).map((frameId) => frameId.toString()); - - const updateCounterBoolean = shouldUpdateCounter( - this.tabsData[tabId][cookieKey], - cookie + const isMultiSellerAuction = PAStore.isMUltiSellerAuction( + Object.values(rest).flat() ); - - if (updateCounterBoolean) { - this.tabs[tabId].newUpdates++; - } - - if (this.tabsData[tabId]?.[cookieKey]) { - // Merge in previous warning reasons. - const parsedCookie = { - ...this.tabsData[tabId][cookieKey].parsedCookie, - ...cookie.parsedCookie, - samesite: ( - cookie.parsedCookie.samesite ?? - this.tabsData[tabId][cookieKey].parsedCookie.samesite ?? - 'lax' - ).toLowerCase(), - httponly: - cookie.parsedCookie.httponly ?? - this.tabsData[tabId][cookieKey].parsedCookie.httponly, - priority: - cookie.parsedCookie?.priority ?? - this.tabsData[tabId][cookieKey].parsedCookie?.priority ?? - 'Medium', - partitionKey: '', + const groupedAuctionBids: { + [parentAuctionId: string]: { + 0: singleAuctionEvent[]; + [uniqueAuctionId: string]: singleAuctionEvent[]; }; - if ( - cookie.parsedCookie?.partitionKey || - this.tabsData[tabId][cookieKey].parsedCookie?.partitionKey - ) { - parsedCookie.partitionKey = - cookie.parsedCookie?.partitionKey || - this.tabsData[tabId][cookieKey].parsedCookie?.partitionKey; - } + } = {}; - const networkEvents: CookieData['networkEvents'] = { - requestEvents: [ - ...(this.tabsData[tabId][cookieKey]?.networkEvents - ?.requestEvents || []), - ...(cookie.networkEvents?.requestEvents || []), - ], - responseEvents: [ - ...(this.tabsData[tabId][cookieKey]?.networkEvents - ?.responseEvents || []), - ...(cookie.networkEvents?.responseEvents || []), - ], - }; + const auctionEventsToBeProcessed = Object.values(rest).flat(); + + if (isMultiSellerAuction) { + auctionEventsToBeProcessed.forEach((event) => { + const { parentAuctionId = null, uniqueAuctionId = null } = event; + + if (!parentAuctionId) { + if (uniqueAuctionId) { + if (!groupedAuctionBids[uniqueAuctionId]) { + groupedAuctionBids[uniqueAuctionId] = { + 0: [], + }; + } + groupedAuctionBids[uniqueAuctionId]['0'].push(event); + } + return; + } + + if (!groupedAuctionBids[parentAuctionId]) { + groupedAuctionBids[parentAuctionId] = { + 0: [], + }; + } + + if (!uniqueAuctionId) { + return; + } + + if (!groupedAuctionBids[parentAuctionId][uniqueAuctionId]) { + groupedAuctionBids[parentAuctionId][uniqueAuctionId] = []; + } + + groupedAuctionBids[parentAuctionId][uniqueAuctionId].push(event); + }); + } - this.tabsData[tabId][cookieKey] = { - ...this.tabsData[tabId][cookieKey], - ...cookie, - parsedCookie, - isBlocked: blockedReasons.length > 0, - blockedReasons, - networkEvents, - blockingStatus: deriveBlockingStatus(networkEvents), - warningReasons, - url: this.tabsData[tabId][cookieKey].url ?? cookie.url, - headerType: - this.tabsData[tabId][cookieKey].headerType === 'javascript' - ? this.tabsData[tabId][cookieKey].headerType - : cookie.headerType, - frameIdList, - exemptionReason: - cookie?.exemptionReason || - this.tabsData[tabId][cookieKey]?.exemptionReason, - }; - } else { - this.tabsData[tabId][cookieKey] = { - ...cookie, - blockingStatus: deriveBlockingStatus(cookie.networkEvents), - }; + if (this.tabs[tabId].newUpdatesPA > 0 || overrideForInitialSync) { + await chrome.runtime.sendMessage({ + type: 'AUCTION_EVENTS', + payload: { + refreshTabData: overrideForInitialSync, + tabId, + auctionEvents: isMultiSellerAuction ? groupedAuctionBids : rest, + multiSellerAuction: isMultiSellerAuction, + globalEvents: globalEvents ?? [], + }, + }); + this.tabs[tabId].newUpdatesPA = 0; } } - - updateCookieBadgeText(this.tabsData[tabId], tabId); } catch (error) { - //Fail silently // eslint-disable-next-line no-console console.warn(error); + //Fail silently. Ignoring the console.warn here because the only error this will throw is of "Error: Could not establish connection". } } @@ -814,4 +724,4 @@ class SynchnorousCookieStore { } } -export default new SynchnorousCookieStore(); +export default new DataStore(); diff --git a/packages/extension/src/store/tests/synchnorousCookieStore.ts b/packages/extension/src/store/tests/synchnorousCookieStore.ts index be257e247..b721c3993 100644 --- a/packages/extension/src/store/tests/synchnorousCookieStore.ts +++ b/packages/extension/src/store/tests/synchnorousCookieStore.ts @@ -25,7 +25,8 @@ import SinonChrome from 'sinon-chrome'; // eslint-disable-next-line import/no-unresolved import OpenCookieDatabase from 'ps-analysis-tool/assets/data/open-cookie-database.json'; import data from '../../utils/test-data/cookieMockData'; -import synchnorousCookieStore from '../synchnorousCookieStore'; +import synchnorousCookieStore from '../cookieStore'; +import dataStore from '../dataStore'; describe('SynchnorousCookieStore:', () => { beforeAll(() => { @@ -42,29 +43,25 @@ describe('SynchnorousCookieStore:', () => { }); beforeEach(() => { - synchnorousCookieStore.addTabData(123456); - synchnorousCookieStore.updateUrl(123456, 'https://bbc.com'); + dataStore.addTabData(123456); + dataStore.updateUrl(123456, 'https://bbc.com'); }); afterEach(() => { - synchnorousCookieStore.clear(); + dataStore.clear(); }); it('Should not update storage if there are no new cookies', () => { synchnorousCookieStore.update(123456, []); - expect(Object.keys(synchnorousCookieStore.tabsData[123456]).length).toBe(0); - expect(synchnorousCookieStore.tabs[123456].newUpdates).toBe(0); + expect(Object.keys(dataStore.tabsData[123456]).length).toBe(0); + expect(dataStore.tabs[123456].newUpdatesCA).toBe(0); }); it('Should add new cookie if cookie doesnt exist', () => { - expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/'] - ).toBeUndefined(); + expect(dataStore.tabsData[123456]['_cbcnn.com/']).toBeUndefined(); synchnorousCookieStore.update(123456, [data.tabCookies['_cb']]); - expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/'] - ).not.toBeUndefined(); - expect(synchnorousCookieStore.tabs[123456].newUpdates).toBe(1); + expect(dataStore.tabsData[123456]['_cbcnn.com/']).not.toBeUndefined(); + expect(dataStore.tabs[123456].newUpdatesCA).toBe(1); }); it('Should update cookie with new blocked reason', () => { @@ -72,15 +69,12 @@ describe('SynchnorousCookieStore:', () => { { ...data.tabCookies['_cb'], isBlocked: false, blockedReasons: [] }, ]); - expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/']?.isBlocked - ).toBe(false); + expect(dataStore.tabsData[123456]['_cbcnn.com/']?.isBlocked).toBe(false); expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/']?.blockedReasons - ?.length + dataStore.tabsData[123456]['_cbcnn.com/']?.blockedReasons?.length ).toBe(0); - expect(synchnorousCookieStore.tabs[123456].newUpdates).toBe(1); + expect(dataStore.tabs[123456].newUpdatesCA).toBe(1); synchnorousCookieStore.update(123456, [ { @@ -90,14 +84,12 @@ describe('SynchnorousCookieStore:', () => { }, ]); - expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/']?.isBlocked - ).toBe(true); + expect(dataStore.tabsData[123456]['_cbcnn.com/']?.isBlocked).toBe(true); - expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/'].blockedReasons - ).toContain('InvalidDomain'); - expect(synchnorousCookieStore.tabs[123456].newUpdates).toBe(2); + expect(dataStore.tabsData[123456]['_cbcnn.com/'].blockedReasons).toContain( + 'InvalidDomain' + ); + expect(dataStore.tabs[123456].newUpdatesCA).toBe(2); }); it('Should persist the partition key from the previous set data', () => { @@ -113,27 +105,25 @@ describe('SynchnorousCookieStore:', () => { ]); expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/']?.parsedCookie - ?.partitionKey + dataStore.tabsData[123456]['_cbcnn.com/']?.parsedCookie?.partitionKey ).toBe('https://bbc.com'); - expect(synchnorousCookieStore.tabs[123456].newUpdates).toBe(1); + expect(dataStore.tabs[123456].newUpdatesCA).toBe(1); synchnorousCookieStore.update(123456, [ { ...data.tabCookies['_cb'], parsedCookie: { ...data.tabCookies['_cb'].parsedCookie, - partitionKey: undefined, + partitionKey: '', priority: 'Medium', }, }, ]); expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/']?.parsedCookie - ?.partitionKey + dataStore.tabsData[123456]['_cbcnn.com/']?.parsedCookie?.partitionKey ).toBe('https://bbc.com'); - expect(synchnorousCookieStore.tabs[123456].newUpdates).toBe(2); + expect(dataStore.tabs[123456].newUpdatesCA).toBe(2); }); it('Should not update the data if tabId is not present', () => { @@ -147,28 +137,28 @@ describe('SynchnorousCookieStore:', () => { }, }, ]); - expect(synchnorousCookieStore.tabs[12345]?.newUpdates).toBeUndefined(); + expect(dataStore.tabs[12345]?.newUpdatesCA).toBeUndefined(); }); it('Should clear cookie store when clear is called', () => { - expect(synchnorousCookieStore.tabs[123456]).toBeDefined(); - expect(synchnorousCookieStore.tabsData[123456]).toBeDefined(); + expect(dataStore.tabs[123456]).toBeDefined(); + expect(dataStore.tabsData[123456]).toBeDefined(); - synchnorousCookieStore.clear(); + dataStore.clear(); - expect(synchnorousCookieStore.tabs[123456]).toBeUndefined(); - expect(synchnorousCookieStore.tabsData[123456]).toBeUndefined(); + expect(dataStore.tabs[123456]).toBeUndefined(); + expect(dataStore.tabsData[123456]).toBeUndefined(); }); it('Should return the tab url for tab if exists', () => { - expect(synchnorousCookieStore.getTabUrl(123456)).toBe('https://bbc.com'); - expect(synchnorousCookieStore.getTabUrl(12345)).toBeNull(); + expect(dataStore.getTabUrl(123456)).toBe('https://bbc.com'); + expect(dataStore.getTabUrl(12345)).toBeNull(); }); it('Should update the url of tab if it exists', () => { - expect(synchnorousCookieStore.getTabUrl(123456)).toBe('https://bbc.com'); - synchnorousCookieStore.updateUrl(123456, 'https://cnn.com'); - expect(synchnorousCookieStore.getTabUrl(123456)).toBe('https://cnn.com'); + expect(dataStore.getTabUrl(123456)).toBe('https://bbc.com'); + dataStore.updateUrl(123456, 'https://cnn.com'); + expect(dataStore.getTabUrl(123456)).toBe('https://cnn.com'); }); it('Should add cookie exclusion reason and warning reason', () => { @@ -179,43 +169,37 @@ describe('SynchnorousCookieStore:', () => { 123456 ); - expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/'].warningReasons - ).toContain('WarnSameSiteUnspecifiedCrossSiteContext'); + expect(dataStore.tabsData[123456]['_cbcnn.com/'].warningReasons).toContain( + 'WarnSameSiteUnspecifiedCrossSiteContext' + ); synchnorousCookieStore.addCookieExclusionWarningReason( '_cbcnn.com/', - ['ExcludeSameSiteUnspecifiedTreatedAsLax'], + ['SameSiteUnspecifiedTreatedAsLax'], [], 123456 ); - expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/'].blockedReasons - ).toContain('ExcludeSameSiteUnspecifiedTreatedAsLax'); + expect(dataStore.tabsData[123456]['_cbcnn.com/'].blockedReasons).toContain( + 'SameSiteUnspecifiedTreatedAsLax' + ); }); it('Should remove tabData', () => { - expect(synchnorousCookieStore.tabs[123456]).toBeDefined(); - synchnorousCookieStore.removeTabData(123456); - expect(synchnorousCookieStore.tabs[123456]).toBeUndefined(); + expect(dataStore.tabs[123456]).toBeDefined(); + dataStore.removeTabData(123456); + expect(dataStore.tabs[123456]).toBeUndefined(); }); it('Should remove cookie', () => { - expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/'] - ).toBeUndefined(); + expect(dataStore.tabsData[123456]['_cbcnn.com/']).toBeUndefined(); synchnorousCookieStore.update(123456, [data.tabCookies['_cb']]); - expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/'] - ).toBeDefined(); + expect(dataStore.tabsData[123456]['_cbcnn.com/']).toBeDefined(); - synchnorousCookieStore.removeCookieData(123456); + dataStore.removeCookieData(123456); - expect( - synchnorousCookieStore.tabsData[123456]['_cbcnn.com/'] - ).toBeUndefined(); + expect(dataStore.tabsData[123456]['_cbcnn.com/']).toBeUndefined(); }); }); diff --git a/packages/extension/src/store/utils/formatTime.ts b/packages/extension/src/store/utils/formatTime.ts new file mode 100644 index 000000000..220cab376 --- /dev/null +++ b/packages/extension/src/store/utils/formatTime.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * External dependencies + */ +import type { Protocol } from 'devtools-protocol'; +/** + * This formats the time and returns it in a readable format + * @param { Protocol.Network.TimeSinceEpoch } startTime time at which the event started. + * @param { Protocol.Network.TimeSinceEpoch } eventTime time at which the event occured. + * @returns {Date} the formatted time. + */ +export default function formatTime( + startTime: Protocol.Network.TimeSinceEpoch, + eventTime: Protocol.Network.TimeSinceEpoch +) { + return startTime + ? `${((eventTime - startTime) * 1000).toFixed(2)}ms` + : new Date(eventTime * 1000); +} diff --git a/packages/extension/src/store/utils/networkTime.ts b/packages/extension/src/store/utils/networkTime.ts new file mode 100644 index 000000000..429c249ab --- /dev/null +++ b/packages/extension/src/store/utils/networkTime.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * External dependencies + */ +import type { Protocol } from 'devtools-protocol'; +import dataStore from '../dataStore'; + +/** + * Helps decode timestamps in network-related events, which are only convertible + * to global time in context of corresponding Network.requestWillBeSent + * @param { string } requestId request identifier for which network time needs to be calculated + * @param { Protocol.Network.MonotonicTime } timestamp Timestamp of the request + * @param { string } tabId Tab the request is associated to. + * @returns timestamp + */ +export default function networkTime( + requestId: string, + timestamp: Protocol.Network.MonotonicTime, + tabId: string +) { + const timeInfo = dataStore.requestIdToCDPURLMapping[tabId][requestId]; + // Somehow missed the start event? + if (!timeInfo) { + return new Date().getTime(); + } + return timeInfo.wallTime + (timestamp - timeInfo.timeStamp); +} diff --git a/packages/extension/src/utils/getAndParseNetworkCookies.ts b/packages/extension/src/utils/getAndParseNetworkCookies.ts index 6f1f1b9e0..5d2783c4b 100644 --- a/packages/extension/src/utils/getAndParseNetworkCookies.ts +++ b/packages/extension/src/utils/getAndParseNetworkCookies.ts @@ -21,7 +21,7 @@ import type { CookieData } from '@google-psat/common'; /** * Internal dependencies */ -import synchnorousCookieStore from '../store/synchnorousCookieStore'; +import synchnorousCookieStore from '../store/PAStore'; import parseNetworkCookies from './parseNetworkCookies'; export const getAndParseNetworkCookies = async (tabId: string) => { diff --git a/packages/extension/src/utils/listenToNewTab.ts b/packages/extension/src/utils/listenToNewTab.ts index d0feab56f..b900dfa1f 100644 --- a/packages/extension/src/utils/listenToNewTab.ts +++ b/packages/extension/src/utils/listenToNewTab.ts @@ -16,7 +16,7 @@ /** * Internal dependencies */ -import synchnorousCookieStore from '../store/synchnorousCookieStore'; +import dataStore from '../store/dataStore'; import { getTab } from './getTab'; const listenToNewTab = async (tabId?: number) => { @@ -28,15 +28,12 @@ const listenToNewTab = async (tabId?: number) => { return ''; } - if ( - synchnorousCookieStore.tabMode && - synchnorousCookieStore.tabMode !== 'unlimited' - ) { - const storedTabData = Object.keys(synchnorousCookieStore?.tabsData ?? {}); + if (dataStore.tabMode && dataStore.tabMode !== 'unlimited') { + const storedTabData = Object.keys(dataStore?.tabsData ?? {}); await Promise.all( storedTabData.map(async (tabIdToDelete) => { - synchnorousCookieStore?.removeTabData(Number(tabIdToDelete)); + dataStore?.removeTabData(Number(tabIdToDelete)); try { await chrome.action.setBadgeText({ tabId: Number(tabIdToDelete), @@ -51,17 +48,17 @@ const listenToNewTab = async (tabId?: number) => { ); } - synchnorousCookieStore.tabToRead = newTabId; + dataStore.tabToRead = newTabId; - synchnorousCookieStore.initialiseVariablesForNewTab(newTabId); + dataStore.initialiseVariablesForNewTab(newTabId); - synchnorousCookieStore?.addTabData(Number(newTabId)); - synchnorousCookieStore?.updateDevToolsState(Number(newTabId), true); - synchnorousCookieStore?.updatePopUpState(Number(newTabId), true); + dataStore?.addTabData(Number(newTabId)); + dataStore?.updateDevToolsState(Number(newTabId), true); + dataStore?.updatePopUpState(Number(newTabId), true); const currentTab = await getTab(Number(newTabId)); - synchnorousCookieStore?.updateUrl(Number(newTabId), currentTab?.url ?? ''); + dataStore?.updateUrl(Number(newTabId), currentTab?.url ?? ''); return newTabId; }; diff --git a/packages/extension/src/view/devtools/index.tsx b/packages/extension/src/view/devtools/index.tsx index 54207df66..c6014dabc 100644 --- a/packages/extension/src/view/devtools/index.tsx +++ b/packages/extension/src/view/devtools/index.tsx @@ -33,6 +33,7 @@ import { CookieProvider, SettingsProvider, AllowedListProvider, + ProtectedAudienceContextProvider, } from './stateProviders'; const isDarkMode = chrome.devtools.panels.themeName === 'dark'; @@ -45,13 +46,15 @@ if (root) { - - - - - - - + + + + + + + + + diff --git a/packages/extension/src/view/devtools/stateProviders/index.ts b/packages/extension/src/view/devtools/stateProviders/index.ts index a2d1bc49c..de83565af 100644 --- a/packages/extension/src/view/devtools/stateProviders/index.ts +++ b/packages/extension/src/view/devtools/stateProviders/index.ts @@ -16,3 +16,4 @@ export * from './cookie'; export * from './settings'; export * from './allowedList'; +export * from './protectedAudience'; diff --git a/packages/extension/src/view/devtools/stateProviders/protectedAudience/context.ts b/packages/extension/src/view/devtools/stateProviders/protectedAudience/context.ts new file mode 100644 index 000000000..ccb885e2b --- /dev/null +++ b/packages/extension/src/view/devtools/stateProviders/protectedAudience/context.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * External dependencies. + */ +import { + createContext, + type AuctionEventsType, + type InterestGroups, + type ReceivedBids, + type NoBidsType, + type AdsAndBiddersType, +} from '@google-psat/common'; + +export interface ProtectedAudienceContextType { + state: { + auctionEvents: AuctionEventsType; + interestGroupDetails: InterestGroups[]; + isMultiSellerAuction: boolean; + receivedBids: ReceivedBids[]; + noBids: NoBidsType; + adsAndBidders: AdsAndBiddersType; + }; +} + +const initialState: ProtectedAudienceContextType = { + state: { + auctionEvents: null, + interestGroupDetails: [], + isMultiSellerAuction: false, + receivedBids: [], + noBids: {}, + adsAndBidders: {}, + }, +}; + +export default createContext(initialState); diff --git a/packages/extension/src/view/devtools/stateProviders/protectedAudience/index.ts b/packages/extension/src/view/devtools/stateProviders/protectedAudience/index.ts new file mode 100644 index 000000000..15b163843 --- /dev/null +++ b/packages/extension/src/view/devtools/stateProviders/protectedAudience/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { default as ProtectedAudienceContextProvider } from './protectedAudienceProvider'; +export { + default as ProtectedAudienceContext, + type ProtectedAudienceContextType, +} from './context'; +export { default as useProtectedAudience } from './useProtectedAudience'; diff --git a/packages/extension/src/view/devtools/stateProviders/protectedAudience/protectedAudienceProvider.tsx b/packages/extension/src/view/devtools/stateProviders/protectedAudience/protectedAudienceProvider.tsx new file mode 100644 index 000000000..00da9ae10 --- /dev/null +++ b/packages/extension/src/view/devtools/stateProviders/protectedAudience/protectedAudienceProvider.tsx @@ -0,0 +1,231 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * External dependencies. + */ +import React, { + type PropsWithChildren, + useEffect, + useState, + useCallback, + useMemo, + useRef, +} from 'react'; +import type { + NoBidsType, + singleAuctionEvent, + ReceivedBids, +} from '@google-psat/common'; + +/** + * Internal dependencies. + */ +import Context, { type ProtectedAudienceContextType } from './context'; +import { + computeInterestGroupDetails, + computeReceivedBidsAndNoBids, +} from './utils'; +import { isEqual } from 'lodash-es'; + +const Provider = ({ children }: PropsWithChildren) => { + const [auctionEvents, setAuctionEvents] = + useState(null); + + const [isMultiSellerAuction, setIsMultiSellerAuction] = + useState(false); + + const globalEvents = useRef([]); + + const [interestGroupDetails, setInterestGroupDetails] = useState< + ProtectedAudienceContextType['state']['interestGroupDetails'] + >([]); + + const [receivedBids, setReceivedBids] = useState< + ProtectedAudienceContextType['state']['receivedBids'] + >([]); + + const [noBids, setNoBids] = useState< + ProtectedAudienceContextType['state']['noBids'] + >({}); + + const [adsAndBidders, setAdsAndBidders] = useState< + ProtectedAudienceContextType['state']['adsAndBidders'] + >({}); + + const messagePassingListener = useCallback( + // eslint-disable-next-line complexity + async (message: { + type: string; + payload: { + tabId: number; + auctionEvents: ProtectedAudienceContextType['state']['auctionEvents']; + multiSellerAuction: boolean; + globalEvents: singleAuctionEvent[]; + refreshTabData: boolean; + }; + }) => { + let didAuctionEventsChange = false; + + if (!message.type) { + return; + } + + const tabId = chrome.devtools.inspectedWindow.tabId; + const incomingMessageType = message.type; + + if ( + incomingMessageType === 'AUCTION_EVENTS' && + message.payload.auctionEvents + ) { + if (message.payload.tabId === tabId) { + setIsMultiSellerAuction(message.payload.multiSellerAuction); + + setAuctionEvents((prevState) => { + if (!prevState && message.payload.auctionEvents) { + didAuctionEventsChange = true; + return message.payload.auctionEvents; + } + + if ( + prevState && + message.payload.auctionEvents && + !isEqual(prevState, message.payload.auctionEvents) + ) { + didAuctionEventsChange = true; + return message.payload.auctionEvents; + } + + return prevState; + }); + + if ( + !didAuctionEventsChange && + isEqual(globalEvents.current, message.payload.globalEvents) + ) { + return; + } + + let shapedInterestGroupDetails: ProtectedAudienceContextType['state']['interestGroupDetails'] = + []; + + shapedInterestGroupDetails = await computeInterestGroupDetails( + message.payload.globalEvents + ); + + globalEvents.current = message.payload.globalEvents; + + const computedBids: { + receivedBids: ReceivedBids[]; + noBids: NoBidsType; + } | null = computeReceivedBidsAndNoBids( + message.payload.auctionEvents, + message.payload.multiSellerAuction + ); + + if (computedBids) { + const adUnitCodeToBidders: ProtectedAudienceContextType['state']['adsAndBidders'] = + {}; + + computedBids.receivedBids.forEach( + ({ adUnitCode, ownerOrigin, mediaContainerSize }) => { + if (!adUnitCode) { + return; + } + + adUnitCodeToBidders[adUnitCode] = { + adUnitCode: + adUnitCodeToBidders[adUnitCode]?.adUnitCode ?? adUnitCode, + bidders: [ + ...new Set([ + ...(adUnitCodeToBidders[adUnitCode]?.bidders ?? []), + ...(ownerOrigin ? [ownerOrigin] : []), + ]), + ], + mediaContainerSize: [ + Array.from( + new Set( + ...(adUnitCodeToBidders[adUnitCode] + ?.mediaContainerSize ?? []), + mediaContainerSize + ) + ), + ], + }; + } + ); + + setAdsAndBidders((prevState) => { + return !isEqual(prevState, adUnitCodeToBidders) + ? adUnitCodeToBidders + : prevState; + }); + + setReceivedBids((prevState) => { + return !isEqual(prevState, computedBids.receivedBids) + ? computedBids.receivedBids + : prevState; + }); + + setNoBids((prevState) => { + return !isEqual(prevState, computedBids.noBids) + ? computedBids.noBids + : prevState; + }); + } + + setInterestGroupDetails((prevState) => { + return !isEqual(prevState, shapedInterestGroupDetails) + ? shapedInterestGroupDetails + : prevState; + }); + } + } + }, + [] + ); + + useEffect(() => { + chrome.runtime.onMessage.addListener(messagePassingListener); + + return () => { + chrome.runtime.onMessage.removeListener(messagePassingListener); + }; + }, [messagePassingListener]); + + const memoisedValue: ProtectedAudienceContextType = useMemo(() => { + return { + state: { + auctionEvents, + interestGroupDetails, + isMultiSellerAuction, + receivedBids, + noBids, + adsAndBidders, + }, + }; + }, [ + auctionEvents, + interestGroupDetails, + isMultiSellerAuction, + noBids, + receivedBids, + adsAndBidders, + ]); + + return {children}; +}; + +export default Provider; diff --git a/packages/extension/src/view/devtools/stateProviders/protectedAudience/useProtectedAudience.ts b/packages/extension/src/view/devtools/stateProviders/protectedAudience/useProtectedAudience.ts new file mode 100644 index 000000000..2e1241471 --- /dev/null +++ b/packages/extension/src/view/devtools/stateProviders/protectedAudience/useProtectedAudience.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * External dependencies. + */ +import { useContextSelector } from '@google-psat/common'; + +/** + * Internal dependencies. + */ +import Context, { type ProtectedAudienceContextType } from './context'; + +export function useCookie(): ProtectedAudienceContextType; +export function useCookie( + selector: (state: ProtectedAudienceContextType) => T +): T; + +/** + * Cookie store hook. + * @param selector Selector function to partially select state. + * @returns selected part of the state + */ +export function useCookie( + selector: ( + state: ProtectedAudienceContextType + ) => T | ProtectedAudienceContextType = (state) => state +) { + return useContextSelector(Context, selector); +} + +export default useCookie; diff --git a/packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/computeInterestGroupDetails.ts b/packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/computeInterestGroupDetails.ts new file mode 100644 index 000000000..d4ce30a7b --- /dev/null +++ b/packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/computeInterestGroupDetails.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Internal dependencies + */ +import type { singleAuctionEvent } from '@google-psat/common'; +import type Protocol from 'devtools-protocol'; + +/** + * This function parses the auction events to get interest group details wherever applicable. + * @param auctionEventsToBeParsed The auction events to be parsed to get the Interest Group Details. + * @returns The Interest group details for each event. + */ +function computeInterestGroupDetails( + auctionEventsToBeParsed: singleAuctionEvent[] +) { + if (!auctionEventsToBeParsed) { + return []; + } + + return Promise.all( + auctionEventsToBeParsed + .filter((event) => event.eventType === 'interestGroupAccessed') + .map(async (event) => { + if (!event?.name && !event?.ownerOrigin) { + return { + ...event, + details: {}, + }; + } + + const result = (await chrome.debugger.sendCommand( + { tabId: chrome.devtools.inspectedWindow.tabId }, + 'Storage.getInterestGroupDetails', + { + name: event?.name, + ownerOrigin: event?.ownerOrigin, + } + )) as Protocol.Storage.GetInterestGroupDetailsResponse; + + return { + ...event, + details: { + ...result.details, + }, + }; + }) + ); +} +export default computeInterestGroupDetails; diff --git a/packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/computeReceivedBidsAndNoBids.ts b/packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/computeReceivedBidsAndNoBids.ts new file mode 100644 index 000000000..e87361f63 --- /dev/null +++ b/packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/computeReceivedBidsAndNoBids.ts @@ -0,0 +1,209 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * External dependencies + */ +import type { + MultiSellerAuction, + NoBidsType, + ReceivedBids, + SingleSellerAuction, +} from '@google-psat/common'; + +/** + * Internal dependencies + */ +import type { ProtectedAudienceContextType } from '../context'; + +const BIDDING_TYPES = [ + 'bid', + 'additionalBid', + 'topLevelBid', + 'topLevelAdditionalBid', +]; + +/** + * This function computes the received bids and no bids data to be displayed in the PSAT extension. + * @param _auctionEvents The array of auction events that are associated with the tab. + * @param _isMultiSellerAuction This speicifes if the auction event occuring is multi seller auction or single seller auction. + * @returns null | receivedBids array and noBids object. + */ +function computeReceivedBidsAndNoBids( + _auctionEvents: ProtectedAudienceContextType['state']['auctionEvents'], + _isMultiSellerAuction: boolean +) { + if ( + !_auctionEvents || + (_auctionEvents && Object.keys(_auctionEvents).length === 0) + ) { + return null; + } + + const _receivedBids: ReceivedBids[] = []; + const _noBids: NoBidsType = {}; + + const _interestGroupBuyers = new Set(); + + if (_isMultiSellerAuction) { + const multisellerAuctionEvents = _auctionEvents as MultiSellerAuction; + + Object.keys(multisellerAuctionEvents).forEach((parentAuctionId) => { + Object.keys(multisellerAuctionEvents[parentAuctionId]).forEach( + (uniqueAuctionId) => { + if ('globalEvents' === uniqueAuctionId) { + return; + } + + const { auctionConfig, uniqueAuctionId: _uniqueAuctionId } = + multisellerAuctionEvents[parentAuctionId]?.[ + uniqueAuctionId + ]?.filter(({ type }) => type && type === 'configResolved')?.[0] ?? + {}; + + const { name } = + multisellerAuctionEvents[parentAuctionId]?.[uniqueAuctionId]?.find( + (event) => event?.eventType === 'interestGroupAccessed' + ) ?? {}; + + //@ts-ignore + auctionConfig?.interestGroupBuyers?.forEach((element) => { + _interestGroupBuyers.add(element); + }); + + const filteredEvents = multisellerAuctionEvents[parentAuctionId][ + uniqueAuctionId + ].filter( + (event) => + event.eventType === 'interestGroupAccessed' && + BIDDING_TYPES.includes(event.type) + ); + + _receivedBids.push( + ...filteredEvents.map((event) => { + const sellerSignals = JSON.parse( + //@ts-ignore -- since auction config is of type object but we know what data is being passed in this. + auctionConfig?.sellerSignals?.value ?? '{}' + ); + + return { + ...event, + mediaContainerSize: sellerSignals?.size, + adUnitCode: sellerSignals?.divId, + }; + }) + ); + + if (_interestGroupBuyers.size > 0) { + const buyersWhoBid = new Set(); + + _receivedBids.forEach((event) => { + if (event.ownerOrigin) { + buyersWhoBid.add(event.ownerOrigin); + } + }); + + Array.from(_interestGroupBuyers.difference(buyersWhoBid)).forEach( + (buyer) => { + const auctionId = + uniqueAuctionId === '0' ? _uniqueAuctionId : uniqueAuctionId; + + if (!auctionId) { + return; + } + + const sellerSignals = JSON.parse( + //@ts-ignore -- since auction config is of type object but we know what data is being passed in this. + auctionConfig?.sellerSignals?.value ?? '{}' + ); + + _noBids[auctionId] = { + ownerOrigin: buyer, + name: name ?? '', + uniqueAuctionId: auctionId, + adUnitCode: sellerSignals?.divId, + }; + } + ); + } + } + ); + }); + } else { + const singleSellerAuctionEvents = _auctionEvents as SingleSellerAuction; + + Object.keys(singleSellerAuctionEvents).forEach((uniqueAuctionId) => { + if ('globalEvents' === uniqueAuctionId) { + return; + } + + const filteredEvents = singleSellerAuctionEvents[uniqueAuctionId].filter( + (event) => + event.eventType === 'interestGroupAccessed' && + BIDDING_TYPES.includes(event.type) + ); + + const { auctionConfig } = + singleSellerAuctionEvents?.[uniqueAuctionId]?.filter( + ({ type }) => type && type === 'configResolved' + )?.[0] ?? {}; + + _receivedBids.push( + ...filteredEvents.map((event) => { + const sellerSignals = JSON.parse( + //@ts-ignore -- since auction config is of type object but we know what data is being passed in this. + auctionConfig?.sellerSignals?.value ?? '{}' + ); + + return { + ...event, + mediaContainerSize: sellerSignals.size, + adUnitCode: sellerSignals?.divId, + }; + }) + ); + + const { name } = + singleSellerAuctionEvents[uniqueAuctionId]?.find( + (event) => event?.eventType === 'interestGroupAccessed' + ) ?? {}; + + if (_interestGroupBuyers.size > 0) { + const buyersWhoBid = new Set(); + + _receivedBids.forEach((event) => { + if (event.ownerOrigin) { + buyersWhoBid.add(event.ownerOrigin); + } + }); + + Array.from(_interestGroupBuyers.difference(buyersWhoBid)).forEach( + (buyer) => { + _noBids[uniqueAuctionId] = { + ownerOrigin: buyer, + name: name ?? '', + //@ts-ignore -- since auction config is of type object but we know what data is being passed in this. + adUnitCode: JSON.parse(auctionConfig?.sellerSignals)?.divId, + uniqueAuctionId, + }; + } + ); + } + }); + } + return { receivedBids: _receivedBids, noBids: _noBids }; +} + +export default computeReceivedBidsAndNoBids; diff --git a/packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/index.ts b/packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/index.ts new file mode 100644 index 000000000..1c93598a1 --- /dev/null +++ b/packages/extension/src/view/devtools/stateProviders/protectedAudience/utils/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { default as computeInterestGroupDetails } from './computeInterestGroupDetails'; +export { default as computeReceivedBidsAndNoBids } from './computeReceivedBidsAndNoBids'; diff --git a/packages/extension/src/view/devtools/tests/app.tsx b/packages/extension/src/view/devtools/tests/app.tsx index 2cc1729c3..d177567fa 100644 --- a/packages/extension/src/view/devtools/tests/app.tsx +++ b/packages/extension/src/view/devtools/tests/app.tsx @@ -28,7 +28,11 @@ import { I18n } from '@google-psat/i18n'; * Internal dependencies. */ import App from '../app'; -import { useCookie, useSettings } from '../stateProviders'; +import { + useCookie, + useSettings, + useProtectedAudience, +} from '../stateProviders'; // @ts-ignore // eslint-disable-next-line import/no-unresolved import PSInfo from 'ps-analysis-tool/data/PSInfo.json'; @@ -37,6 +41,7 @@ import data from '../../../utils/test-data/cookieMockData'; jest.mock('../stateProviders', () => ({ useCookie: jest.fn(), useSettings: jest.fn(), + useProtectedAudience: jest.fn(), })); jest.mock( @@ -50,6 +55,7 @@ globalThis.chrome.runtime.getURL = () => ''; const mockUseCookieStore = useCookie as jest.Mock; const mockUseTablePersistentSettingStore = useTablePersistentSettingsStore as jest.Mock; +const mockUseProtectedAudienceStore = useProtectedAudience as jest.Mock; const mockUseSettingsStore = useSettings as jest.Mock; describe('App', () => { @@ -207,6 +213,10 @@ describe('App', () => { isCurrentTabBeingListenedTo: true, tabToRead: '40245632', }); + mockUseProtectedAudienceStore.mockReturnValue({ + auctionEvents: {}, + interestGroupDetails: [], + }); mockUseTablePersistentSettingStore.mockReturnValue({ getPreferences: () => '', setPreferences: noop, @@ -237,6 +247,10 @@ describe('App', () => { tabUrl: data.tabUrl, isCurrentTabBeingListenedTo: true, }); + mockUseProtectedAudienceStore.mockReturnValue({ + auctionEvents: {}, + interestGroupDetails: [], + }); mockUseTablePersistentSettingStore.mockReturnValue({ getPreferences: () => '', setPreferences: noop,