diff --git a/spot/bun.lockb b/spot/bun.lockb index f8ea1b368..bc6bbe9df 100755 Binary files a/spot/bun.lockb and b/spot/bun.lockb differ 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 2cdc65b72..b65140350 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", ], }, });