"use strict"; const requestLoadedCount = document.getElementById("request-loaded-count"); const requestLatestSeen = document.getElementById("request-latest-seen"); const requestEmpty = document.getElementById("request-empty"); const requestContent = document.getElementById("request-content"); const requestGroups = document.getElementById("request-groups"); const requestDetailSummary = document.getElementById("request-detail-summary"); const requestDetailJson = document.getElementById("request-detail-json"); const requestDetailCorrelation = document.getElementById( "request-detail-correlation" ); const requestDetailFingerprintSource = document.getElementById( "request-detail-fingerprint-source" ); const requestDetailTechnicalFields = document.getElementById( "request-detail-technical-fields" ); let observedRequests = []; let observedRequestGroups = []; let selectedRequestFingerprint = null; document.addEventListener("DOMContentLoaded", async () => { await renderObservedRequests(); }); async function renderObservedRequests() { try { const result = await browser.runtime.sendMessage({ type: "list_recent_observed_requests" }); if (!result?.success) { throw new Error(result?.error ?? "list_recent_observed_requests_failed"); } observedRequests = result.observedRequests ?? []; observedRequestGroups = buildObservedRequestGroups(observedRequests); updateOverview(); if (observedRequests.length === 0) { renderNoObservedRequests(); return; } requestEmpty.hidden = true; requestContent.hidden = false; if (!findObservedRequest(selectedRequestFingerprint)) { selectedRequestFingerprint = observedRequests[0]?.requestFingerprint ?? null; } renderRequestGroups(); renderSelectedRequest(); } catch (error) { observedRequests = []; observedRequestGroups = []; updateOverview(); requestEmpty.hidden = false; requestContent.hidden = true; requestEmpty.textContent = "Beobachtete Requests konnten nicht geladen werden."; console.warn("VG-Observe request list failed", error); } } function updateOverview() { requestLoadedCount.textContent = String(observedRequests.length); requestLatestSeen.textContent = formatNullable( observedRequests[0]?.lastSeenAt ?? null ); } function renderNoObservedRequests() { selectedRequestFingerprint = null; requestGroups.textContent = ""; clearRequestDetails(); requestEmpty.hidden = false; requestContent.hidden = true; requestEmpty.textContent = "Keine beobachteten Requests vorhanden."; } function renderRequestGroups() { requestGroups.textContent = ""; for (const group of observedRequestGroups) { requestGroups.append(buildRequestGroupElement(group)); } } function buildRequestGroupElement(group) { const section = document.createElement("section"); const title = document.createElement("h3"); const context = document.createElement("dl"); const tableWrap = document.createElement("div"); const table = document.createElement("table"); const head = document.createElement("thead"); const headRow = document.createElement("tr"); const body = document.createElement("tbody"); section.className = "request-context-card"; title.textContent = "Technisch beobachteter Vorgang"; context.className = "request-context-summary"; tableWrap.className = "request-list-wrap"; table.className = "request-list"; appendDefinition(context, "Seiten-/Dokumentkontext", group.contextLabel); appendDefinition(context, "Tab / Frame", group.tabFrameLabel); appendDefinition(context, "Zeitraum", group.periodLabel); appendDefinition(context, "Requests", String(group.requests.length)); appendDefinition( context, "Empfaenger-Origins", String(group.recipientOriginCount) ); appendDefinition( context, "Third-Party-Requests", String(group.thirdPartyCount) ); appendDefinition( context, "Requests mit Consent-Parametern", String(group.consentParamCount) ); appendDefinition( context, "Requests mit Consent-Korrelation", String(group.consentCorrelationCount) ); [ "Letzte Beobachtung", "Empfaenger-Origin", "Pfad", "Third-Party", "Consent-Parameter", "Consent-Korrelation", "SeenCount" ].forEach((label) => { const cell = document.createElement("th"); cell.scope = "col"; cell.textContent = label; headRow.append(cell); }); for (const observedRequest of group.requests) { body.append(buildRequestRow(observedRequest)); } head.append(headRow); table.append(head, body); tableWrap.append(table); section.append(title, context, tableWrap); return section; } function appendDefinition(list, label, value) { const item = document.createElement("div"); const term = document.createElement("dt"); const description = document.createElement("dd"); term.textContent = label; description.textContent = value; item.append(term, description); list.append(item); } function buildRequestRow(observedRequest) { const row = document.createElement("tr"); const isSelected = observedRequest?.requestFingerprint === selectedRequestFingerprint; row.className = isSelected ? "is-selected" : ""; row.tabIndex = 0; row.setAttribute("role", "button"); row.setAttribute("aria-pressed", isSelected ? "true" : "false"); row.addEventListener("click", () => { selectObservedRequest(observedRequest?.requestFingerprint ?? null); }); row.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectObservedRequest(observedRequest?.requestFingerprint ?? null); } }); appendListCell(row, formatNullable(observedRequest?.lastSeenAt)); appendListCell( row, formatNullable(observedRequest?.request?.origin), "origin-cell" ); appendListCell( row, formatNullable(observedRequest?.request?.pathname), "path-cell" ); appendListCell(row, formatThirdParty(observedRequest?.request?.thirdParty)); appendListCell(row, formatYesNo(hasConsentParams(observedRequest))); appendListCell(row, formatYesNo(hasConsentCorrelation(observedRequest))); appendListCell(row, formatNullable(observedRequest?.seenCount), "numeric"); return row; } function appendListCell(row, value, className) { const cell = document.createElement("td"); if (className) { cell.className = className; } cell.textContent = value; row.append(cell); } function selectObservedRequest(requestFingerprint) { selectedRequestFingerprint = requestFingerprint; renderRequestGroups(); renderSelectedRequest(); } function renderSelectedRequest() { const observedRequest = findObservedRequest(selectedRequestFingerprint); clearRequestDetails(); if (!observedRequest) { return; } renderSummaryTable([ ["Letzte Beobachtung", formatNullable(observedRequest?.lastSeenAt)], ["Erste Beobachtung", formatNullable(observedRequest?.firstSeenAt)], ["Beobachtet um", formatNullable(getObservedAt(observedRequest))], ["Seiten-/Dokumentkontext", getPageContextLabel(observedRequest)], ["Tab-ID", formatUnknown(getTabId(observedRequest))], ["Frame-ID", formatUnknown(getFrameId(observedRequest))], ["Empfaenger-Origin", formatNullable(observedRequest?.request?.origin)], ["Pfad", formatNullable(observedRequest?.request?.pathname)], ["Methode", formatNullable(observedRequest?.request?.method)], ["Request-Typ", formatNullable(observedRequest?.request?.type)], ["Third-Party", formatThirdParty(observedRequest?.request?.thirdParty)], ["Consent-Parameter vorhanden", formatYesNo(hasConsentParams(observedRequest))], [ "Consent-Korrelation vorhanden", formatYesNo(hasConsentCorrelation(observedRequest)) ], ["SeenCount", formatNullable(observedRequest?.seenCount)], [ "Request-Fingerprint", formatNullable(observedRequest?.requestFingerprint) ] ]); requestDetailJson.textContent = JSON.stringify(observedRequest, null, 2); requestDetailCorrelation.textContent = JSON.stringify( observedRequest?.correlation ?? null, null, 2 ); requestDetailFingerprintSource.textContent = JSON.stringify( observedRequest?.requestFingerprintSource ?? null, null, 2 ); requestDetailTechnicalFields.textContent = JSON.stringify( buildTechnicalFieldExtract(observedRequest), null, 2 ); } function renderSummaryTable(rows) { const table = document.createElement("table"); const body = document.createElement("tbody"); table.className = "inspector-table"; for (const [label, value] of rows) { const row = document.createElement("tr"); const labelCell = document.createElement("th"); const valueCell = document.createElement("td"); labelCell.scope = "row"; labelCell.textContent = label; valueCell.className = "inspector-value"; valueCell.textContent = value; row.append(labelCell, valueCell); body.append(row); } table.append(body); requestDetailSummary.append(table); } function clearRequestDetails() { requestDetailSummary.textContent = ""; requestDetailJson.textContent = ""; requestDetailCorrelation.textContent = ""; requestDetailFingerprintSource.textContent = ""; requestDetailTechnicalFields.textContent = ""; } function findObservedRequest(requestFingerprint) { if (!requestFingerprint) { return null; } return ( observedRequests.find((observedRequest) => { return observedRequest?.requestFingerprint === requestFingerprint; }) ?? null ); } function hasConsentParams(observedRequest) { const consentParams = observedRequest?.consentParams; if (!consentParams || typeof consentParams !== "object") { return false; } return Object.values(consentParams).some((value) => { return value !== null && value !== undefined && value !== ""; }); } function hasConsentCorrelation(observedRequest) { return Boolean(observedRequest?.correlation?.stateFingerprint); } function buildTechnicalFieldExtract(observedRequest) { return { schemaVersion: observedRequest?.schemaVersion ?? null, recordedAt: observedRequest?.recordedAt ?? null, recordingSource: observedRequest?.recordingSource ?? null, observedAt: observedRequest?.observedAt ?? null, firstSeenAt: observedRequest?.firstSeenAt ?? null, lastSeenAt: observedRequest?.lastSeenAt ?? null, seenCount: observedRequest?.seenCount ?? null, requestFingerprint: observedRequest?.requestFingerprint ?? null, requestFingerprintSource: observedRequest?.requestFingerprintSource ?? null, context: observedRequest?.context ?? null, request: observedRequest?.request ?? null, consentParams: observedRequest?.consentParams ?? null, correlation: observedRequest?.correlation ?? null }; } function buildObservedRequestGroups(requests) { const groupsByKey = new Map(); for (const observedRequest of requests) { const key = buildObservedRequestGroupKey(observedRequest); let group = groupsByKey.get(key); if (!group) { group = { key, contextLabel: getPageContextLabel(observedRequest), tabFrameLabel: buildTabFrameLabel(observedRequest), requests: [] }; groupsByKey.set(key, group); } group.requests.push(observedRequest); } return Array.from(groupsByKey.values()) .map(enrichObservedRequestGroup) .sort((left, right) => { return compareTimestampValues(right.lastSeenAt, left.lastSeenAt); }); } function buildObservedRequestGroupKey(observedRequest) { return [ `tab:${formatKeyValue(getTabId(observedRequest))}`, `frame:${formatKeyValue(getFrameId(observedRequest))}`, `page:${getPageContextValue(observedRequest) ?? "unknown"}` ].join("|"); } function enrichObservedRequestGroup(group) { const sortedRequests = [...group.requests].sort((left, right) => { return compareTimestampValues(getObservedAt(left), getObservedAt(right)); }); const recipientOrigins = new Set( sortedRequests .map((observedRequest) => observedRequest?.request?.origin ?? null) .filter((origin) => origin !== null && origin !== undefined && origin !== "") ); let firstSeenAt = null; let lastSeenAt = null; for (const observedRequest of sortedRequests) { firstSeenAt = pickTimestamp(firstSeenAt, observedRequest?.firstSeenAt, "min"); lastSeenAt = pickTimestamp(lastSeenAt, observedRequest?.lastSeenAt, "max"); } return { ...group, requests: sortedRequests, firstSeenAt, lastSeenAt, periodLabel: buildPeriodLabel(firstSeenAt, lastSeenAt), recipientOriginCount: recipientOrigins.size, thirdPartyCount: sortedRequests.filter((observedRequest) => { return observedRequest?.request?.thirdParty === true; }).length, consentParamCount: sortedRequests.filter(hasConsentParams).length, consentCorrelationCount: sortedRequests.filter(hasConsentCorrelation).length }; } function getPageContextLabel(observedRequest) { return formatUnknown(getPageContextValue(observedRequest)); } function getPageContextValue(observedRequest) { return ( getFirstPresentValue( observedRequest, [ "pageUrl", "documentUrl", "initiator", "sourceUrl", "context.pageUrl", "context.documentUrl", "context.initiator", "context.sourceUrl", "request.pageUrl", "request.documentUrl", "request.initiator", "request.sourceUrl" ] ) ?? null ); } function getTabId(observedRequest) { return getFirstPresentValue(observedRequest, ["tabId", "context.tabId"]); } function getFrameId(observedRequest) { return getFirstPresentValue(observedRequest, ["frameId", "context.frameId"]); } function getObservedAt(observedRequest) { return getFirstPresentValue(observedRequest, [ "observedAt", "firstSeenAt", "lastSeenAt" ]); } function getFirstPresentValue(source, paths) { for (const path of paths) { const value = getPathValue(source, path); if (value !== null && value !== undefined && value !== "") { return value; } } return null; } function getPathValue(source, path) { return path.split(".").reduce((value, key) => { if (value === null || value === undefined || typeof value !== "object") { return undefined; } return value[key]; }, source); } function buildTabFrameLabel(observedRequest) { return `Tab ${formatUnknown(getTabId(observedRequest))} / Frame ${formatUnknown( getFrameId(observedRequest) )}`; } function pickTimestamp(current, candidate, direction) { if (!current) { return candidate ?? null; } if (!candidate) { return current; } const comparison = compareTimestampValues(candidate, current); if (direction === "min") { return comparison < 0 ? candidate : current; } return comparison > 0 ? candidate : current; } function compareTimestampValues(left, right) { const leftTime = Date.parse(left ?? ""); const rightTime = Date.parse(right ?? ""); if (Number.isNaN(leftTime) && Number.isNaN(rightTime)) { return 0; } if (Number.isNaN(leftTime)) { return -1; } if (Number.isNaN(rightTime)) { return 1; } return leftTime - rightTime; } function buildPeriodLabel(firstSeenAt, lastSeenAt) { if (!firstSeenAt && !lastSeenAt) { return "unbekannt"; } if (firstSeenAt === lastSeenAt || !lastSeenAt) { return formatUnknown(firstSeenAt); } if (!firstSeenAt) { return formatUnknown(lastSeenAt); } return `${firstSeenAt} bis ${lastSeenAt}`; } function formatKeyValue(value) { return value === null || value === undefined || value === "" ? "unknown" : value; } function formatUnknown(value) { if (value === null || value === undefined || value === "") { return "unbekannt"; } return String(value); } function formatNullable(value) { if (value === null || value === undefined || value === "") { return "unbekannt"; } return String(value); } function formatThirdParty(value) { if (value === true) { return "ja"; } if (value === false) { return "nein"; } return "unbekannt"; } function formatYesNo(value) { return value ? "vorhanden" : "nicht vorhanden"; }