spot: small refactoring + testing debugger for network capture

This commit is contained in:
nick-delirium 2025-01-14 10:28:00 +01:00 committed by Delirium
parent 8be6f63711
commit f59a8c24f4
9 changed files with 306 additions and 117 deletions

Binary file not shown.

View file

@ -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<string, any>;
}
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(
let networkRequests;
let mappedNetwork;
if (settings.useDebugger && recordingState.area === "tab") {
void detachDebuggerFromTab(recordingState.activeTabId);
mappedNetwork = getDebuggerRequests();
} else {
networkRequests = getFinalRequests(
recordingState.activeTabId ?? false,
);
stopTrackingNetwork();
const mappedNetwork = mergeRequests(
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) {

View file

@ -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<boolean> {
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);
}

View file

@ -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 (
<div class={"flex flex-col"}>
<div class={"flex gap-2 items-center justify-between p-4"}>
@ -153,6 +164,27 @@ function Settings({ goBack }: { goBack: () => void }) {
</p>
</div>
<div class="p-4 border-b border-slate-300 hover:bg-indigo-50 cursor-default">
<div class="flex flex-row justify-between items-center">
<p class="font-semibold mb-1 flex items-center">
<span>Use Debugger</span>
</p>
<div>
<label class="cursor-pointer">
<input
type="checkbox"
class="toggle toggle-primary toggle-sm"
checked={useDebugger()}
onChange={toggleUseDebugger}
/>
</label>
</div>
</div>
<p class="text-xs">
Enable the chrome debugger to track network requests with more precision.
</p>
</div>
<div class="p-4 hover:bg-indigo-50 cursor-default">
<div class="flex flex-row justify-between">
<p class="font-semibold mb-1">Ingest Point</p>

View file

@ -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"
}

70
spot/utils/messages.ts Normal file
View file

@ -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",
},
},
};

View file

@ -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);
};

10
spot/utils/smallUtils.ts Normal file
View file

@ -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;
}

View file

@ -28,6 +28,7 @@ export default defineConfig({
"webNavigation",
"webRequest",
"<all_urls>",
"debugger",
],
},
});