From 825026d78b536b357eb5eb6489c3e1566119bb3e Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Tue, 14 Jan 2025 10:28:00 +0100 Subject: [PATCH] spot: small refactoring + testing debugger for network capture --- spot/bun.lockb | Bin 253562 -> 255306 bytes spot/entrypoints/background.ts | 142 ++++++++++---------------- spot/entrypoints/content/index.tsx | 52 +++++----- spot/entrypoints/popup/Settings.tsx | 34 +++++- spot/package.json | 4 +- spot/utils/messages.ts | 70 +++++++++++++ spot/utils/networkDebuggerTracking.ts | 110 ++++++++++++++++++++ spot/utils/smallUtils.ts | 10 ++ spot/wxt.config.ts | 1 + 9 files changed, 306 insertions(+), 117 deletions(-) create mode 100644 spot/utils/messages.ts create mode 100644 spot/utils/networkDebuggerTracking.ts create mode 100644 spot/utils/smallUtils.ts diff --git a/spot/bun.lockb b/spot/bun.lockb index f8ea1b3680e5a62d98c121ee1654e5be64c2e5b4..bc6bbe9dfbffa17b67ebf5b570d6955f02adf797 100755 GIT binary patch delta 2931 zcmd^>{Zmxe9mdZ&mj%`Y@dXUHN(3Q-hSddhNfuc`t)o?%h%-TxN<$4bNFXY{5E}-V zaeN7&=#3r-A}B!w*~p9J0*ac{2nfYkAtPE81Qgb2r7ttu8El`kdw1#&?O%{P^W5ir zKj;12bMHO>I`4k)!IgDxqHbmL zkVM3F`e59vEVHaOJ<20q+1@HJMVJpc)JO%M4_)GA##FXucTL{^Z zn40!!>UuxYd)i*011>67!(WB3wh*F&=g6x(UrC7gnh+mEbK#=7?I`65_kquXN7vZn zg>W(KnQ$-o;#xwc!yVK3gF2j@b%aa<|4{Xq`gTvVhwF{buEHtLyxZ$t=RWbHUb64J zIr0bdTVr5CPdM;Xe`#KsYLXiY}shiOu-sO z$ri<=O+YFfC0>#ZF-BS_Cv}-=uUy$>X8u*OwJTElZWSR*P(ULawixM`a?&+3y)IYc z9x0DrGi#q>o}xo%*$`}GuUE?(e~8p3RTHujkjY~UjG6{Sk#b6xN%ME4K9C<>Gig$> zh-++Pxq{S(f@$Bw6258csy5iD{Q}V9tlvy-xvlM5ukV&pqP&eP;eh0<~~3PFT4-1>;b^I0LS_A2LMk6usi@L zt(C^nDkZ^6Z-;2aE9b$-th8RE z!xv1;vmevWnvE#a@N9gPp~N7eu63VsFGdAI;iLO*xKTH6J^9Z z;0v|!gmN~Oo>R7!vt&kZ@_`D3waPy$*d-^e#opRq8kGyx>~rcVJ{QH%F{lJ8g^nxh zzGi0jmUz`KKo_A)(6`Wc&}B&U+zPcr9nc6g3RxlXsSux!3Z<`x{et;6AuZniAEBGj zHK-1{1NB1#{H=P-W>9fE%VtyB#tjXOYip6yiAnYG>kaG-y`^M-!+t}xP4)>~ZsVAM zpW@ocYTTUn0`6h_yJptB_6wvdAQ?IceGct|{*6ogspHY6&;hUP}4|E7R4vDTxAxD)Wqzj<}$Wh(VEC&2uTh+PS+x`0#8YNT-a5zMfA{jp6=na}JI-jlQ6I8EABN>Q zjskdR&Iz;do>@}p)NULWyz!eV%8KaS7W&s=58BLizEUWC#NYOnytR*zFAf(Od^19P z3919c8*^i5oYjZK4 z1^Uo%y@71vmf2`>J8zwh{?rC0`Abf^9GnDv(P`Lcc?Ey_tnQ9|4h!`mq?*t7N1YD7 z+#hvfc?KeNAYQ!2Uz$#Pd56FBOZt#c4vQ64KM2JqrMd~}Y( z*OeJlpBIn1d;PcT2)^nysbPlWloj9mME~p3>H8Qp>J8y~JnNY?+;5H)qH~YV zTM~};Ga8}~)raE2Me|SQNZtz_=ftzmd~W4u9c!GVJ|aVi<0zMLesz}V+dX~u-ox}p zELO`LX@f2XrwaTdvSuFqRqFauc?Y9&ZCe5dA8roB%IxH81EuTLxED1 w5*Q>!Q9gqEgm8mBt8PfyGFO`X8n)4{cO?I8p7su&?2*NiPC0d5`t!^G0TxI7MgRZ+ delta 2805 zcmeH}i&Iop9LLW&*EQB4H6cMG1u1ZmtGf#>h_Xu=nMp63vd=W@VcG9J^Fm9}jkj7C-=6q{8g}SF*@AIrW*=6*82V(F^J<&IC~YgAG)TO? zu{O)$pZeVv#`-dr_eNUAj8`%l%h}49AL5h>#_olmfcJ*aoIC&J)Vb*_)aj0s!TW&E zPM?uJ4Smi>ySoslrAIBCm72j)5Z{g1p@jtW&vkBhN2PG8wFo{4KBS7Vf$$d)hrnMn zFlK~jr_P<5mY%_Us~NiwaX0EwZ%L>Z0RM3tV}s#$?{LQkIL*4NhOt1nUoB$;;GR_s z$a~B)o$)&vy9Wi)9uKZ-4Y2#y1)6>e)qGk;)Ws%$lBCDyJ+C#*jyH57=g^w;c%vEn5{nvlYKh4eNOZOE5lw zDUU@3gO<3!#%F7d$LxHAb`yW^(&CQWjm=mZ4cJpljIxQ}w`+TjCm7=@8JhqoG;X#T z-bIw4H64vN9766TZOrj_LjrbYvTH7@k(*A;Xu__Iat(Db%4VF7Xe1&@;x#5rwN=QFya3jigu zS_pz0#PdU=z;Zv;{<0emc-mjEg*0r*`8*dU*~3^4gJKofy- zX}kj9a|PhlD*&5iBZ1ung0BK>m1$Q2W?cnnA>h=5uJP|Ivi%k>lOulSJ;XJqUVWb^ zE9lj{k&YQ-^_6mIxp2rM<)WU?)AKipFL;2&DWo(Cb?8)vKx<{{7SWFv>X}=_9KpBA zjtYeD=r=0FfgZ+6cW>Y8`u-}hj0em^5A&hdpx2=VP`aK{BkUrPzQ!V`7+M8s&A7}-%4cY(|Ks%tFP@PPy$7&jMzXmac z^R?2vOGx88C^>>f)yUReqJ~%M`MP+K8+W@G;ConN0in82quAk>x)^XV)~>;~K&zl4 zXeqP=x{Z{9@+s(#^gh@!$N~{x?#cg&Je9>jg&y`4>aUQkhsD6$l|aQ%8AL;U40)TZ zMScw=A#ZbUx2wUZn-Ae*p${N$_cVbFm=tV&(iF1SUts#ffu_p$Wn$&CGZf2@dh{U8>s z+`V-F!IlxK+2VaV$#~hMD*3%4hT_9Snq`vXhAR%b)|tKpIo^Mb$z9axd876kof(@j d*eu(dMWAjoD?i+iV-_|^3Cq{BPAOmd{smKWw&wr< diff --git a/spot/entrypoints/background.ts b/spot/entrypoints/background.ts index 5a60ad82a..bb64fdbd1 100644 --- a/spot/entrypoints/background.ts +++ b/spot/entrypoints/background.ts @@ -4,76 +4,22 @@ import { getFinalRequests, stopTrackingNetwork, } from "~/utils/networkTracking"; -import { mergeRequests } from "~/utils/networkTrackingUtils"; +import { mergeRequests, SpotNetworkRequest } from "~/utils/networkTrackingUtils"; +import { safeApiUrl } from '~/utils/smallUtils' +import { + attachDebuggerToTab, + detachDebuggerFromTab, + getRequests as getDebuggerRequests, + resetMap +} from "~/utils/networkDebuggerTracking"; +import { messages } from '~/utils/messages' let checkBusy = false; export default defineBackground(() => { const CHECK_INT = 60 * 1000; const PING_INT = 30 * 1000; - const VER = "1.0.10"; - - const messages = { - popup: { - from: { - updateSettings: "ort:settings", - start: "popup:start", - }, - to: { - micStatus: "popup:mic-status", - stopped: "popup:stopped", - started: "popup:started", - noLogin: "popup:no-login", - }, - stop: "popup:stop", - checkStatus: "popup:check-status", - loginExist: "popup:login", - getAudioPerms: "popup:get-audio-perm", - }, - content: { - from: { - bumpVitals: "ort:bump-vitals", - bumpClicks: "ort:bump-clicks", - bumpLocation: "ort:bump-location", - discard: "ort:discard", - checkLogin: "ort:get-login", - checkRecStatus: "ort:check-status", - checkMicStatus: "ort:getMicStatus", - setLoginToken: "ort:login-token", - invalidateToken: "ort:invalidate-token", - saveSpotData: "ort:save-spot", - saveSpotVidChunk: "ort:save-spot-part", - countEnd: "ort:countend", - contentReady: "ort:content-ready", - checkNewTab: "ort:check-new-tab", - started: "ort:started", - stopped: "ort:stopped", - toStop: "ort:stop", - restart: "ort:restart", - getErrorEvents: "ort:get-error-events", - }, - to: { - setJWT: "content:set-jwt", - micStatus: "content:mic-status", - unmount: "content:unmount", - notification: "notif:display", - updateErrorEvents: "content:error-events", - }, - }, - injected: { - from: { - bumpLogs: "ort:bump-logs", - bumpNetwork: "ort:bump-network", - }, - }, - offscreen: { - to: { - checkRecStatus: "offscr:check-status", - startRecording: "offscr:start-recording", - stopRecording: "offscr:stop-recording", - }, - }, - }; + const VER = "1.0.14"; interface SpotObj { name: string; @@ -116,6 +62,7 @@ export default defineBackground(() => { openInNewTab: true, consoleLogs: true, networkLogs: true, + useDebugger: false, ingestPoint: "https://app.openreplay.com", }; const defaultSpotObj = { @@ -145,12 +92,18 @@ export default defineBackground(() => { let injectNetworkRequests = []; let onStop: (() => void) | null = null; let settings = defaultSettings; - let recordingState = { + type recState = { + activeTabId: number | null; + area: string | null; + recording: string; + audioPerm: number; + } + let recordingState: recState = { activeTabId: null, area: null, recording: REC_STATE.stopped, audioPerm: 0, - } as Record; + } let jwtToken = ""; let refreshInt: any; let pingInt: any; @@ -187,17 +140,6 @@ export default defineBackground(() => { } } - function safeApiUrl(url: string) { - let str = url; - if (str.endsWith("/")) { - str = str.slice(0, -1); - } - if (str.includes("app.openreplay.com")) { - str = str.replace("app.openreplay.com", "api.openreplay.com"); - } - return str; - } - let slackChannels: { name: string; webhookId: number }[] = []; void checkTokenValidity(); @@ -346,7 +288,6 @@ export default defineBackground(() => { recording: REC_STATE.stopped, audioPerm: request.permissions ? (request.mic ? 2 : 1) : 0, }; - startTrackingNetwork(); if (request.area === "tab") { browser.tabs .query({ @@ -358,6 +299,12 @@ export default defineBackground(() => { if (active) { recordingState.activeTabId = active.id; } + if (settings.useDebugger) { + resetMap(); + void attachDebuggerToTab(active.id); + } else { + startTrackingNetwork(); + } void sendToActiveTab({ type: "content:mount", area: request.area, @@ -367,12 +314,22 @@ export default defineBackground(() => { }); }); } else { + if (!settings.useDebugger) { + startTrackingNetwork(); + } void sendToActiveTab({ type: "content:mount", area: request.area, mic: request.mic, audioId: request.selectedAudioDevice, audioPerm: request.permissions ? (request.mic ? 2 : 1) : 0, + }, (tabId) => { + if (settings.useDebugger) { + resetMap(); + void attachDebuggerToTab(tabId); + } else { + startTrackingNetwork(); + } }); } } @@ -675,15 +632,21 @@ export default defineBackground(() => { if (recordingState.recording === REC_STATE.stopped) { return console.error("Calling stopped recording?"); } - const networkRequests = getFinalRequests( - recordingState.activeTabId ?? false, - ); - - stopTrackingNetwork(); - const mappedNetwork = mergeRequests( - networkRequests, - injectNetworkRequests, - ); + let networkRequests; + let mappedNetwork; + if (settings.useDebugger && recordingState.area === "tab") { + void detachDebuggerFromTab(recordingState.activeTabId); + mappedNetwork = getDebuggerRequests(); + } else { + networkRequests = getFinalRequests( + recordingState.activeTabId ?? false, + ); + stopTrackingNetwork(); + mappedNetwork = mergeRequests( + networkRequests, + injectNetworkRequests, + ); + } injectNetworkRequests = []; finalSpotObj.network = mappedNetwork; browser.runtime @@ -1012,7 +975,7 @@ export default defineBackground(() => { data?: any; activeTabId?: number; [key: string]: any; - }) { + }, onSent?: (tabId: number) => void) { let activeTabs = await browser.tabs.query({ active: true, currentWindow: true, @@ -1038,6 +1001,7 @@ export default defineBackground(() => { message, ); await browser.tabs.sendMessage(sendTo, message); + onSent?.(sendTo); } } @@ -1135,6 +1099,7 @@ export default defineBackground(() => { stopTabActivationListening(); } if (tabId !== previousTab) { + detachDebuggerFromTab(previousTab) browser.runtime .sendMessage({ type: messages.offscreen.to.checkRecStatus, @@ -1148,6 +1113,7 @@ export default defineBackground(() => { state: getRecState(), activeTabId: null, }; + attachDebuggerToTab(tabId) void sendToActiveTab(msg); }); if (previousTab) { diff --git a/spot/entrypoints/content/index.tsx b/spot/entrypoints/content/index.tsx index c8401932f..e847fe2a5 100644 --- a/spot/entrypoints/content/index.tsx +++ b/spot/entrypoints/content/index.tsx @@ -6,7 +6,7 @@ import { stopClickRecording, } from "./eventTrackers"; import ControlsBox from "~/entrypoints/content/ControlsBox"; - +import { messages } from '~/utils/messages'; import { convertBlobToBase64, getChromeFullVersion } from "./utils"; import "./style.css"; import "~/assets/main.css"; @@ -55,7 +55,7 @@ export default defineContentScript({ const getMicStatus = async () => { return new Promise((res) => { browser.runtime.sendMessage({ - type: "ort:getMicStatus", + type: messages.content.from.checkMicStatus, }); let int = setInterval(() => { if (micResponse !== null) { @@ -124,7 +124,7 @@ export default defineContentScript({ recState = "stopped"; stopClickRecording(); stopLocationRecording(); - const result = await browser.runtime.sendMessage({ type: "ort:stop" }); + const result = await browser.runtime.sendMessage({ type: messages.content.from.toStop }); if (result.status === "full") { chunksReady = true; data = result; @@ -149,20 +149,20 @@ export default defineContentScript({ const pause = () => { recState = "paused"; - browser.runtime.sendMessage({ type: "ort:pause" }); + browser.runtime.sendMessage({ type: messages.content.from.pause }); }; const resume = () => { recState = "recording"; - browser.runtime.sendMessage({ type: "ort:resume" }); + browser.runtime.sendMessage({ type: messages.content.from.resume }); }; const muteMic = () => { - browser.runtime.sendMessage({ type: "ort:mute-microphone" }); + browser.runtime.sendMessage({ type: messages.content.from.muteMic }); }; const unmuteMic = () => { - browser.runtime.sendMessage({ type: "ort:unmute-microphone" }); + browser.runtime.sendMessage({ type: messages.content.from.unmuteMic }); }; const onClose = async ( @@ -202,14 +202,14 @@ export default defineContentScript({ try { await browser.runtime.sendMessage({ - type: "ort:save-spot", + type: messages.content.from.saveSpotData, spot, }); let index = 0; for (let part of videoData.result) { if (part) { await browser.runtime.sendMessage({ - type: "ort:save-spot-part", + type: messages.content.from.saveSpotVidChunk, part, index, total: videoData.result.length, @@ -238,24 +238,24 @@ export default defineContentScript({ if (event.data.type === "orspot:token") { window.postMessage({ type: "orspot:logged" }, "*"); void browser.runtime.sendMessage({ - type: "ort:login-token", + type: messages.content.from.setLoginToken, token: event.data.token, }); } if (event.data.type === "orspot:invalidate") { void browser.runtime.sendMessage({ - type: "ort:invalidate-token", + type: messages.content.from.invalidateToken, }); } if (event.data.type === "ort:bump-logs") { void chrome.runtime.sendMessage({ - type: "ort:bump-logs", + type: messages.injected.from.bumpLogs, logs: event.data.logs, }); } if (event.data.type === "ort:bump-network") { void chrome.runtime.sendMessage({ - type: "ort:bump-network", + type: messages.injected.from.bumpNetwork, event: event.data.event, }); } @@ -292,7 +292,7 @@ export default defineContentScript({ function onRestart() { chrome.runtime.sendMessage({ - type: "ort:restart", + type: messages.content.from.restart, }); stopClickRecording(); stopLocationRecording(); @@ -316,7 +316,7 @@ export default defineContentScript({ let onEndObj = {}; async function countEnd(): Promise { return browser.runtime - .sendMessage({ ...onEndObj, type: "ort:countend" }) + .sendMessage({ ...onEndObj, type: messages.content.from.countEnd }) .then((r: boolean) => { onEndObj = {}; return r; @@ -324,11 +324,11 @@ export default defineContentScript({ } setInterval(() => { - void browser.runtime.sendMessage({ type: "ort:content-ready" }); + void browser.runtime.sendMessage({ type: messages.content.from.contentReady }); }, 250); // @ts-ignore false positive browser.runtime.onMessage.addListener((message: any, resp) => { - if (message.type === "content:mount") { + if (message.type === messages.content.to.mount) { if (recState === "count") return; recState = "count"; onEndObj = { @@ -339,7 +339,7 @@ export default defineContentScript({ audioPerm = message.audioPerm; ui.mount(); } - if (message.type === "content:start") { + if (message.type === messages.content.to.start) { if (recState === "recording") return; clockStart = message.time; recState = "recording"; @@ -352,13 +352,13 @@ export default defineContentScript({ if (message.withNetwork) { startNetworkTracking(); } - browser.runtime.sendMessage({ type: "ort:started" }); + browser.runtime.sendMessage({ type: messages.content.from.started }); if (message.shouldMount) { ui.mount(); } return "pong"; } - if (message.type === "notif:display") { + if (message.type === messages.content.to.notification) { window.postMessage( { type: "ornotif:display", @@ -367,7 +367,7 @@ export default defineContentScript({ "*", ); } - if (message.type === "content:unmount") { + if (message.type === messages.content.to.unmount) { stopClickRecording(); stopLocationRecording(); stopConsoleTracking(); @@ -376,22 +376,22 @@ export default defineContentScript({ ui.remove(); return "unmounted"; } - if (message.type === "content:video-chunk") { + if (message.type === messages.content.to.videoChunk) { videoChunks[message.index] = message.data; if (message.total === message.index + 1) { chunksReady = true; } } - if (message.type === "content:spot-saved") { + if (message.type === messages.content.to.spotSaved) { window.postMessage({ type: "ornotif:copy", url: message.url }); } - if (message.type === "content:stop") { + if (message.type === messages.content.to.stop) { window.postMessage({ type: "content:trigger-stop" }, "*"); } - if (message.type === "content:mic-status") { + if (message.type === messages.content.to.micStatus) { micResponse = message.micStatus; } - if (message.type === "content:error-events") { + if (message.type === messages.content.to.updateErrorEvents) { errorsReady = true; errorData.push(...message.errorData); } diff --git a/spot/entrypoints/popup/Settings.tsx b/spot/entrypoints/popup/Settings.tsx index edf0bc113..45b73e9af 100644 --- a/spot/entrypoints/popup/Settings.tsx +++ b/spot/entrypoints/popup/Settings.tsx @@ -11,11 +11,11 @@ function Settings({ goBack }: { goBack: () => void }) { const [ingest, setIngest] = createSignal(defaultIngest); const [editIngest, setEditIngest] = createSignal(false); const [tempIngest, setTempIngest] = createSignal(""); + const [useDebugger, setUseDebugger] = createSignal(false); onMount(() => { chrome.storage.local.get("settings", (data: any) => { if (data.settings) { - console.log('update state', data.settings) const ingest = data.settings.ingestPoint || defaultIngest; const devToolsEnabled = @@ -26,6 +26,7 @@ function Settings({ goBack }: { goBack: () => void }) { setTempIngest(ingest); setShowIngest(ingest !== defaultIngest); setEditIngest(!data.settings.ingestPoint); + setUseDebugger(data.settings.useDebugger); } }); }); @@ -94,6 +95,16 @@ function Settings({ goBack }: { goBack: () => void }) { }); }; + const toggleUseDebugger = (e: Event) => { + e.stopPropagation(); + const value = useDebugger(); + setUseDebugger(!value); + chrome.runtime.sendMessage({ + type: "ort:settings", + settings: { useDebugger: !value }, + }); + } + return (
@@ -153,6 +164,27 @@ function Settings({ goBack }: { goBack: () => void }) {

+
+
+

+ Use Debugger +

+
+ +
+
+

+ Enable the chrome debugger to track network requests with more precision. +

+
+

Ingest Point

diff --git a/spot/package.json b/spot/package.json index b1aefd2f4..ed74ba501 100644 --- a/spot/package.json +++ b/spot/package.json @@ -2,7 +2,7 @@ "name": "spot", "description": "manifest.json description", "private": true, - "version": "1.0.14", + "version": "1.0.15", "type": "module", "scripts": { "dev": "wxt", @@ -32,7 +32,7 @@ "@wxt-dev/module-solid": "^1.1.3", "daisyui": "^4.12.10", "typescript": "^5.7.2", - "wxt": "0.19.22" + "wxt": "0.19.24" }, "packageManager": "yarn@4.5.3" } diff --git a/spot/utils/messages.ts b/spot/utils/messages.ts new file mode 100644 index 000000000..ce9366953 --- /dev/null +++ b/spot/utils/messages.ts @@ -0,0 +1,70 @@ +export const messages = { + popup: { + from: { + updateSettings: "ort:settings", + start: "popup:start", + }, + to: { + micStatus: "popup:mic-status", + stopped: "popup:stopped", + started: "popup:started", + noLogin: "popup:no-login", + }, + stop: "popup:stop", + checkStatus: "popup:check-status", + loginExist: "popup:login", + getAudioPerms: "popup:get-audio-perm", + }, + content: { + from: { + bumpVitals: "ort:bump-vitals", + bumpClicks: "ort:bump-clicks", + bumpLocation: "ort:bump-location", + discard: "ort:discard", + checkLogin: "ort:get-login", + checkRecStatus: "ort:check-status", + checkMicStatus: "ort:getMicStatus", + setLoginToken: "ort:login-token", + invalidateToken: "ort:invalidate-token", + saveSpotData: "ort:save-spot", + saveSpotVidChunk: "ort:save-spot-part", + countEnd: "ort:countend", + contentReady: "ort:content-ready", + checkNewTab: "ort:check-new-tab", + started: "ort:started", + stopped: "ort:stopped", + toStop: "ort:stop", + restart: "ort:restart", + getErrorEvents: "ort:get-error-events", + muteMic: "ort:mute-microphone", + unmuteMic: "ort:unmute-microphone", + resume: "ort:resume", + pause: "ort:pause", + }, + to: { + setJWT: "content:set-jwt", + micStatus: "content:mic-status", + unmount: "content:unmount", + mount: "content:mount", + start: "content:start", + notification: "notif:display", + updateErrorEvents: "content:error-events", + videoChunk: "content:video-chunk", + spotSaved: "content:spot-saved", + stop: "content:stop", + }, + }, + injected: { + from: { + bumpLogs: "ort:bump-logs", + bumpNetwork: "ort:bump-network", + }, + }, + offscreen: { + to: { + checkRecStatus: "offscr:check-status", + startRecording: "offscr:start-recording", + stopRecording: "offscr:stop-recording", + }, + }, +}; diff --git a/spot/utils/networkDebuggerTracking.ts b/spot/utils/networkDebuggerTracking.ts new file mode 100644 index 000000000..be066715b --- /dev/null +++ b/spot/utils/networkDebuggerTracking.ts @@ -0,0 +1,110 @@ +let requestMap = {} + +export function resetMap() { + requestMap = {} +} + +export async function attachDebuggerToTab(tabId: string | number) { + await new Promise((resolve, reject) => { + chrome.debugger.attach({ tabId }, "1.3", () => { + if (chrome.runtime.lastError) + return reject(chrome.runtime.lastError.message); + chrome.debugger.sendCommand({ tabId }, "Network.enable", {}, resolve); + }); + chrome.debugger.onEvent.addListener(handleRequestIntercept); + }); +} +export async function detachDebuggerFromTab(tabId: string) { + return new Promise((resolve, reject) => { + chrome.debugger.detach({ tabId }, resolve); + chrome.debugger.onEvent.removeListener(handleRequestIntercept); + }); +} + +const getType = (requestType: string) => { + switch (requestType) { + case "Fetch": + case "XHR": + case "xmlhttprequest": + return 'xmlhttprequest' + default: + return requestType + } +} +function handleRequestIntercept(source, method, params) { + if (!source.tabId) return; // Not our target tab + if (!params.request) return; // No request object + if (params.request.method === "OPTIONS") return; // Ignore preflight requests + + switch (method) { + case "Network.requestWillBeSent": + const reqType = params.type ? getType(params.type) : "resource"; + if (reqType !== "xmlhttprequest") { + console.log(params); + } + + requestMap[params.requestId] = { + encodedBodySize: 0, + responseBodySize: 0, + duration: 0, + method: params.request.method, + type: reqType, + statusCode: 0, + url: params.request.url, + body: params.request.postData || "", + responseBody: "", + fromCache: false, + requestHeaders: params.request.headers || {}, + responseHeaders: {}, + timestamp: Date.now(), + }; + break; + + case "Network.responseReceived": + if (!requestMap[params.requestId]) return; + requestMap[params.requestId].statusCode = params.response.status; + requestMap[params.requestId].responseHeaders = + params.response.headers || {}; + // fromDiskCache or fromServiceWorker if available + if (params.response.fromDiskCache) + requestMap[params.requestId].fromCache = true; + break; + + case "Network.dataReceived": + if (!requestMap[params.requestId]) return; + requestMap[params.requestId].encodedBodySize += params.dataLength; + // There's no direct content-encoding size from debugger + break; + + case "Network.loadingFinished": + if (!requestMap[params.requestId]) return; + requestMap[params.requestId].duration = + Date.now() - requestMap[params.requestId].time; + requestMap[params.requestId].responseBodySize = + requestMap[params.requestId].encodedBodySize; + chrome.debugger.sendCommand( + { tabId: source.tabId }, + "Network.getResponseBody", + { requestId: params.requestId }, + (res) => { + if (!res || res.error) { + requestMap[params.requestId].error = res?.error || "Unknown"; + } else { + requestMap[params.requestId].responseBody = res.base64Encoded + ? atob(res.body) + : res.body; + } + }, + ); + break; + + case "Network.loadingFailed": + if (!requestMap[params.requestId]) return; + requestMap[params.requestId].error = params.errorText || "Unknown"; + break; + } +} + +export const getRequests = () => { + return Object.values(requestMap); +}; diff --git a/spot/utils/smallUtils.ts b/spot/utils/smallUtils.ts new file mode 100644 index 000000000..8b46f0cdb --- /dev/null +++ b/spot/utils/smallUtils.ts @@ -0,0 +1,10 @@ +export function safeApiUrl(url: string) { + let str = url; + if (str.endsWith("/")) { + str = str.slice(0, -1); + } + if (str.includes("app.openreplay.com")) { + str = str.replace("app.openreplay.com", "api.openreplay.com"); + } + return str; +} \ No newline at end of file diff --git a/spot/wxt.config.ts b/spot/wxt.config.ts index 6f7140362..7698adba7 100644 --- a/spot/wxt.config.ts +++ b/spot/wxt.config.ts @@ -28,6 +28,7 @@ export default defineConfig({ "webNavigation", "webRequest", "", + "debugger", ], }, });