From 76b2e8b29e1f6b85acf44b1a3b582b31a9ccdeef Mon Sep 17 00:00:00 2001 From: Robert Long Date: Tue, 1 Feb 2022 15:11:06 -0800 Subject: [PATCH] Add debug log inspector / rageshake --- .env | 3 + package.json | 1 + src/App.jsx | 4 + src/SequenceDiagramViewerPage.jsx | 38 ++++++ src/main.jsx | 8 +- src/room/GroupCallInspector.jsx | 73 ++++++---- src/settings/SettingsModal.jsx | 10 ++ src/settings/useSubmitRageshake.js | 209 +++++++++++++++++++++++++++++ yarn.lock | 2 +- 9 files changed, 320 insertions(+), 28 deletions(-) create mode 100644 src/SequenceDiagramViewerPage.jsx create mode 100644 src/settings/useSubmitRageshake.js diff --git a/.env b/.env index 2785e05..f9e5c88 100644 --- a/.env +++ b/.env @@ -7,6 +7,9 @@ # Used for determining the homeserver to use for short urls etc. # VITE_DEFAULT_HOMESERVER=http://localhost:8008 +# Used for submitting debug logs to an external rageshake server +# VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit + # The Sentry DSN to use for error reporting. Leave undefined to disable. # VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 diff --git a/package.json b/package.json index 0437c93..e78203e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", + "pako": "^2.0.4", "postcss-preset-env": "^6.7.0", "re-resizable": "^6.9.0", "react": "^17.0.0", diff --git a/src/App.jsx b/src/App.jsx index 5dea73e..3c882c5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -25,6 +25,7 @@ import { RoomPage } from "./room/RoomPage"; import { RoomRedirect } from "./room/RoomRedirect"; import { ClientProvider } from "./ClientContext"; import { usePageFocusStyle } from "./usePageFocusStyle"; +import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage"; const SentryRoute = Sentry.withSentryRouting(Route); @@ -48,6 +49,9 @@ export default function App({ history }) { + + + diff --git a/src/SequenceDiagramViewerPage.jsx b/src/SequenceDiagramViewerPage.jsx new file mode 100644 index 0000000..f19b903 --- /dev/null +++ b/src/SequenceDiagramViewerPage.jsx @@ -0,0 +1,38 @@ +import React, { useCallback, useState } from "react"; +import { SequenceDiagramViewer } from "./room/GroupCallInspector"; +import { FieldRow, InputField } from "./input/Input"; + +export function SequenceDiagramViewerPage() { + const [debugLog, setDebugLog] = useState(); + const [selectedUserId, setSelectedUserId] = useState(); + const onChangeDebugLog = useCallback((e) => { + if (e.target.files && e.target.files.length > 0) { + e.target.files[0].text().then((text) => { + setDebugLog(JSON.parse(text)); + }); + } + }, []); + + return ( +
+ + + + {debugLog && ( + + )} +
+ ); +} diff --git a/src/main.jsx b/src/main.jsx index 5db2324..c43db9a 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -22,6 +22,10 @@ import App from "./App"; import * as Sentry from "@sentry/react"; import { Integrations } from "@sentry/tracing"; import { ErrorView } from "./FullScreenView"; +import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake"; +import { InspectorContextProvider } from "./room/GroupCallInspector"; + +rageshake.init(); if (import.meta.env.VITE_CUSTOM_THEME) { const style = document.documentElement.style; @@ -59,7 +63,9 @@ Sentry.init({ ReactDOM.render( - + + + , document.getElementById("root") diff --git a/src/room/GroupCallInspector.jsx b/src/room/GroupCallInspector.jsx index a46db0b..c64f34d 100644 --- a/src/room/GroupCallInspector.jsx +++ b/src/room/GroupCallInspector.jsx @@ -1,5 +1,12 @@ import { Resizable } from "re-resizable"; -import React, { useEffect, useState, useReducer, useRef } from "react"; +import React, { + useEffect, + useState, + useReducer, + useRef, + createContext, + useContext, +} from "react"; import ReactJson from "react-json-view"; import mermaid from "mermaid"; import styles from "./GroupCallInspector.module.css"; @@ -90,7 +97,18 @@ function formatTimestamp(timestamp) { return dateFormatter.format(timestamp); } -function SequenceDiagramViewer({ +export const InspectorContext = createContext(); + +export function InspectorContextProvider({ children }) { + const context = useState({}); + return ( + + {children} + + ); +} + +export function SequenceDiagramViewer({ localUserId, remoteUserIds, selectedUserId, @@ -168,16 +186,16 @@ function reducer(state, action) { const fromId = event.getStateKey(); remoteUserIds = - fromId === state.localUserId || eventsByUserId.has(fromId) + fromId === state.localUserId || eventsByUserId[fromId] ? state.remoteUserIds : [...state.remoteUserIds, fromId]; - eventsByUserId = new Map(state.eventsByUserId); + eventsByUserId = { ...state.eventsByUserId }; if (event.getStateKey() === state.localUserId) { for (const userId in eventsByUserId) { - eventsByUserId.set(userId, [ - ...(eventsByUserId.get(userId) || []), + eventsByUserId[userId] = [ + ...(eventsByUserId[userId] || []), { from: fromId, to: "Room", @@ -186,11 +204,11 @@ function reducer(state, action) { timestamp: event.getTs() || Date.now(), ignored: false, }, - ]); + ]; } } else { - eventsByUserId.set(fromId, [ - ...(eventsByUserId.get(fromId) || []), + eventsByUserId[fromId] = [ + ...(eventsByUserId[fromId] || []), { from: fromId, to: "Room", @@ -199,7 +217,7 @@ function reducer(state, action) { timestamp: event.getTs() || Date.now(), ignored: false, }, - ]); + ]; } } @@ -215,17 +233,17 @@ function reducer(state, action) { } case "receive_to_device_event": { const event = action.event; - const eventsByUserId = new Map(state.eventsByUserId); + const eventsByUserId = { ...state.eventsByUserId }; const fromId = event.getSender(); const toId = state.localUserId; const content = event.getContent(); - const remoteUserIds = eventsByUserId.has(fromId) + const remoteUserIds = eventsByUserId[fromId] ? state.remoteUserIds : [...state.remoteUserIds, fromId]; - eventsByUserId.set(fromId, [ - ...(eventsByUserId.get(fromId) || []), + eventsByUserId[fromId] = [ + ...(eventsByUserId[fromId] || []), { from: fromId, to: toId, @@ -234,22 +252,22 @@ function reducer(state, action) { timestamp: event.getTs() || Date.now(), ignored: state.localSessionId !== content.dest_session_id, }, - ]); + ]; return { ...state, eventsByUserId, remoteUserIds }; } case "send_voip_event": { const event = action.event; - const eventsByUserId = new Map(state.eventsByUserId); + const eventsByUserId = { ...state.eventsByUserId }; const fromId = state.localUserId; const toId = event.userId; - const remoteUserIds = eventsByUserId.has(toId) + const remoteUserIds = eventsByUserId[toId] ? state.remoteUserIds : [...state.remoteUserIds, toId]; - eventsByUserId.set(toId, [ - ...(eventsByUserId.get(toId) || []), + eventsByUserId[toId] = [ + ...(eventsByUserId[toId] || []), { from: fromId, to: toId, @@ -258,7 +276,7 @@ function reducer(state, action) { timestamp: Date.now(), ignored: false, }, - ]); + ]; return { ...state, eventsByUserId, remoteUserIds }; } @@ -271,7 +289,7 @@ function useGroupCallState(client, groupCall, pollCallStats) { const [state, dispatch] = useReducer(reducer, { localUserId: client.getUserId(), localSessionId: client.getSessionId(), - eventsByUserId: new Map(), + eventsByUserId: {}, remoteUserIds: [], callStateEvent: null, memberStateEvents: {}, @@ -399,6 +417,12 @@ export function GroupCallInspector({ client, groupCall, show }) { const [selectedUserId, setSelectedUserId] = useState(); const state = useGroupCallState(client, groupCall, show); + const [_, setState] = useContext(InspectorContext); + + useEffect(() => { + setState({ json: state }); + }, [setState, state]); + if (!show) { return null; } @@ -421,16 +445,13 @@ export function GroupCallInspector({ client, groupCall, show }) { selectedUserId={selectedUserId} onSelectUserId={setSelectedUserId} remoteUserIds={state.remoteUserIds} - events={state.eventsByUserId.get(selectedUserId)} + events={state.eventsByUserId[selectedUserId]} /> )} {currentTab === "inspector" && ( setShowInspector(e.target.checked)} /> + + + + + + diff --git a/src/settings/useSubmitRageshake.js b/src/settings/useSubmitRageshake.js new file mode 100644 index 0000000..2507e90 --- /dev/null +++ b/src/settings/useSubmitRageshake.js @@ -0,0 +1,209 @@ +import { useCallback, useContext } from "react"; +import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake"; +import pako from "pako"; +import { useClient } from "../ClientContext"; +import { InspectorContext } from "../room/GroupCallInspector"; + +export function useSubmitRageshake() { + const { client } = useClient(); + const [{ json, svg }] = useContext(InspectorContext); + + const submitRageshake = useCallback( + async (opts) => { + let userAgent = "UNKNOWN"; + if (window.navigator && window.navigator.userAgent) { + userAgent = window.navigator.userAgent; + } + + let touchInput = "UNKNOWN"; + try { + // MDN claims broad support across browsers + touchInput = String(window.matchMedia("(pointer: coarse)").matches); + } catch (e) {} + + const body = new FormData(); + body.append( + "text", + opts.description || "User did not supply any additional text." + ); + body.append("app", "matrix-video-chat"); + body.append("version", "dev"); + body.append("user_agent", userAgent); + body.append("installed_pwa", false); + body.append("touch_input", touchInput); + + if (client) { + body.append("user_id", client.credentials.userId); + body.append("device_id", client.deviceId); + + if (client.isCryptoEnabled()) { + const keys = [`ed25519:${client.getDeviceEd25519Key()}`]; + if (client.getDeviceCurve25519Key) { + keys.push(`curve25519:${client.getDeviceCurve25519Key()}`); + } + body.append("device_keys", keys.join(", ")); + body.append("cross_signing_key", client.getCrossSigningId()); + + // add cross-signing status information + const crossSigning = client.crypto.crossSigningInfo; + const secretStorage = client.crypto.secretStorage; + + body.append( + "cross_signing_ready", + String(await client.isCrossSigningReady()) + ); + body.append( + "cross_signing_supported_by_hs", + String( + await client.doesServerSupportUnstableFeature( + "org.matrix.e2e_cross_signing" + ) + ) + ); + body.append("cross_signing_key", crossSigning.getId()); + body.append( + "cross_signing_privkey_in_secret_storage", + String( + !!(await crossSigning.isStoredInSecretStorage(secretStorage)) + ) + ); + + const pkCache = client.getCrossSigningCacheCallbacks(); + body.append( + "cross_signing_master_privkey_cached", + String( + !!(pkCache && (await pkCache.getCrossSigningKeyCache("master"))) + ) + ); + body.append( + "cross_signing_self_signing_privkey_cached", + String( + !!( + pkCache && + (await pkCache.getCrossSigningKeyCache("self_signing")) + ) + ) + ); + body.append( + "cross_signing_user_signing_privkey_cached", + String( + !!( + pkCache && + (await pkCache.getCrossSigningKeyCache("user_signing")) + ) + ) + ); + + body.append( + "secret_storage_ready", + String(await client.isSecretStorageReady()) + ); + body.append( + "secret_storage_key_in_account", + String(!!(await secretStorage.hasKey())) + ); + + body.append( + "session_backup_key_in_secret_storage", + String(!!(await client.isKeyBackupKeyStored())) + ); + const sessionBackupKeyFromCache = + await client.crypto.getSessionBackupPrivateKey(); + body.append( + "session_backup_key_cached", + String(!!sessionBackupKeyFromCache) + ); + body.append( + "session_backup_key_well_formed", + String(sessionBackupKeyFromCache instanceof Uint8Array) + ); + } + } + + if (opts.label) { + body.append("label", opts.label); + } + + // add storage persistence/quota information + if (navigator.storage && navigator.storage.persisted) { + try { + body.append( + "storageManager_persisted", + String(await navigator.storage.persisted()) + ); + } catch (e) {} + } else if (document.hasStorageAccess) { + // Safari + try { + body.append( + "storageManager_persisted", + String(await document.hasStorageAccess()) + ); + } catch (e) {} + } + + if (navigator.storage && navigator.storage.estimate) { + try { + const estimate = await navigator.storage.estimate(); + body.append("storageManager_quota", String(estimate.quota)); + body.append("storageManager_usage", String(estimate.usage)); + if (estimate.usageDetails) { + Object.keys(estimate.usageDetails).forEach((k) => { + body.append( + `storageManager_usage_${k}`, + String(estimate.usageDetails[k]) + ); + }); + } + } catch (e) {} + } + + const logs = await rageshake.getLogsForReport(); + + for (const entry of logs) { + // encode as UTF-8 + let buf = new TextEncoder().encode(entry.lines); + + // compress + buf = pako.gzip(buf); + + body.append("compressed-log", new Blob([buf]), entry.id); + } + + if (json) { + body.append( + "file", + new Blob([JSON.stringify(json)], { type: "text/plain" }), + "groupcall.txt" + ); + } + + await fetch( + import.meta.env.VITE_RAGESHAKE_SUBMIT_URL || + "https://element.io/bugreports/submit", + { + method: "POST", + body, + } + ); + }, + [client] + ); + + const downloadDebugLog = useCallback(() => { + const blob = new Blob([JSON.stringify(json)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const el = document.createElement("a"); + el.href = url; + el.download = "groupcall.json"; + el.style.display = "none"; + document.body.appendChild(el); + el.click(); + setTimeout(() => { + URL.revokeObjectURL(url); + el.parentNode.removeChild(el); + }, 0); + }); + + return { submitRageshake, downloadDebugLog }; +} diff --git a/yarn.lock b/yarn.lock index 6a2e98c..8f36309 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9273,7 +9273,7 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -pako@^2.0.3: +pako@^2.0.3, pako@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d" integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==