mirror of
https://github.com/vector-im/element-web.git
synced 2026-01-15 13:31:12 +01:00
Add support for storing debug logs locally and allowing local downloads.
This commit is contained in:
parent
04800c15af
commit
07b59f1104
@ -407,6 +407,7 @@ If you run your own rageshake server to collect bug reports, the following optio
|
||||
1. `bug_report_endpoint_url`: URL for where to submit rageshake logs to. Rageshakes include feedback submissions and bug reports. When
|
||||
not present in the config, the app will disable all rageshake functionality. Set to `https://rageshakes.element.io/api/submit` to submit
|
||||
rageshakes to us, or use your own rageshake server.
|
||||
You may also set the value to `"local"` if you wish to only store logs locally, in order to download them for debugging.
|
||||
2. `uisi_autorageshake_app`: If a user has enabled the "automatically send debug logs on decryption errors" flag, this option will be sent
|
||||
alongside the rageshake so the rageshake server can filter them by app name. By default, this will be `element-auto-uisi`
|
||||
(in contrast to other rageshakes submitted by the app, which use `element-web`).
|
||||
|
||||
@ -98,7 +98,10 @@ export interface IConfigOptions {
|
||||
show_labs_settings: boolean;
|
||||
features?: Record<string, boolean>; // <FeatureName, EnabledBool>
|
||||
|
||||
bug_report_endpoint_url?: string; // omission disables bug reporting
|
||||
/**
|
||||
* Bug report endpoint URL. "local" means the logs should not be uploaded.
|
||||
*/
|
||||
bug_report_endpoint_url?: "local" | string; // omission disables bug reporting
|
||||
uisi_autorageshake_app?: string; // defaults to "element-auto-uisi"
|
||||
sentry?: {
|
||||
dsn: string;
|
||||
|
||||
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, type ReactNode } from "react";
|
||||
import { Link } from "@vector-im/compound-web";
|
||||
import { Link, Text } from "@vector-im/compound-web";
|
||||
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import Modal from "../../../Modal";
|
||||
@ -43,6 +43,7 @@ interface IState {
|
||||
progress: string | null;
|
||||
downloadBusy: boolean;
|
||||
downloadProgress: string | null;
|
||||
isLocalOnly: boolean;
|
||||
}
|
||||
|
||||
export default class BugReportDialog extends React.Component<BugReportDialogProps, IState> {
|
||||
@ -61,6 +62,7 @@ export default class BugReportDialog extends React.Component<BugReportDialogProp
|
||||
progress: null,
|
||||
downloadBusy: false,
|
||||
downloadProgress: null,
|
||||
isLocalOnly: SdkConfig.get().bug_report_endpoint_url === "local",
|
||||
};
|
||||
|
||||
this.unmounted = false;
|
||||
@ -142,6 +144,14 @@ export default class BugReportDialog extends React.Component<BugReportDialogProp
|
||||
this.setState({ busy: true, progress: null, err: null });
|
||||
this.sendProgressCallback(_t("bug_reporting|preparing_logs"));
|
||||
|
||||
if (this.state.isLocalOnly) {
|
||||
// Shouldn't reach here, but throw in case we do.
|
||||
this.setState({
|
||||
err: _t("bug_reporting|failed_send_logs_causes|unknown_error"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
|
||||
userText,
|
||||
sendLogs: true,
|
||||
@ -257,26 +267,30 @@ export default class BugReportDialog extends React.Component<BugReportDialogProp
|
||||
>
|
||||
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
||||
{warning}
|
||||
<p>{_t("bug_reporting|description")}</p>
|
||||
<p>
|
||||
<strong>
|
||||
{_t(
|
||||
"bug_reporting|before_submitting",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
<a
|
||||
target="_blank"
|
||||
href={SdkConfig.get().feedback.new_issue_url}
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{sub}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</strong>
|
||||
</p>
|
||||
<Text>{_t("bug_reporting|description")}</Text>
|
||||
<Text>
|
||||
{this.state.isLocalOnly ? (
|
||||
<strong>
|
||||
{_t(
|
||||
"bug_reporting|before_submitting",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
<a
|
||||
target="_blank"
|
||||
href={SdkConfig.get().feedback.new_issue_url}
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{sub}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</strong>
|
||||
) : (
|
||||
_t("bug_reporting|local_only_description")
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<div className="mx_BugReportDialog_download">
|
||||
<AccessibleButton onClick={this.onDownload} kind="link" disabled={this.state.downloadBusy}>
|
||||
@ -293,10 +307,12 @@ export default class BugReportDialog extends React.Component<BugReportDialogProp
|
||||
value={this.state.issueUrl}
|
||||
placeholder="https://github.com/vector-im/element-web/issues/..."
|
||||
ref={this.issueRef}
|
||||
disabled={this.state.isLocalOnly}
|
||||
/>
|
||||
<Field
|
||||
className="mx_BugReportDialog_field_input"
|
||||
element="textarea"
|
||||
disabled={this.state.isLocalOnly}
|
||||
label={_t("bug_reporting|textarea_label")}
|
||||
rows={5}
|
||||
onChange={this.onTextChange}
|
||||
@ -311,7 +327,7 @@ export default class BugReportDialog extends React.Component<BugReportDialogProp
|
||||
onPrimaryButtonClick={this.onSubmit}
|
||||
focus={true}
|
||||
onCancel={this.onCancel}
|
||||
disabled={this.state.busy}
|
||||
disabled={this.state.busy || this.state.isLocalOnly}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
||||
@ -45,6 +45,7 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
|
||||
const onFinished = (sendFeedback: boolean): void => {
|
||||
if (hasFeedback && sendFeedback) {
|
||||
const label = props.feature ? `${props.feature}-feedback` : "feedback";
|
||||
// TODO: Handle rejection.
|
||||
submitFeedback(label, comment, canContact);
|
||||
|
||||
Modal.createDialog(InfoDialog, {
|
||||
|
||||
@ -38,7 +38,7 @@ const GenericFeatureFeedbackDialog: React.FC<IProps> = ({
|
||||
|
||||
const sendFeedback = async (ok: boolean): Promise<void> => {
|
||||
if (!ok) return onFinished(false);
|
||||
|
||||
// TODO: Handle rejection.
|
||||
submitFeedback(rageshakeLabel, comment, canContact, rageshakeData);
|
||||
onFinished(true);
|
||||
|
||||
|
||||
@ -417,6 +417,7 @@
|
||||
"create_new_issue": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
|
||||
"description": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.",
|
||||
"download_logs": "Download logs",
|
||||
"local_only_description": "Your configuration only allows you to download logs locally.",
|
||||
"downloading_logs": "Downloading logs",
|
||||
"error_empty": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.",
|
||||
"failed_download_logs": "Failed to download debug logs: ",
|
||||
|
||||
@ -769,7 +769,7 @@ export class ElementCall extends Call {
|
||||
}
|
||||
|
||||
const rageshakeSubmitUrl = SdkConfig.get("bug_report_endpoint_url");
|
||||
if (rageshakeSubmitUrl) {
|
||||
if (rageshakeSubmitUrl && rageshakeSubmitUrl !== "local") {
|
||||
params.append("rageshakeSubmitUrl", rageshakeSubmitUrl);
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import * as rageshake from "./rageshake";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import { getServerVersionFromFederationApi } from "../components/views/dialogs/devtools/ServerInfo";
|
||||
import type * as Tar from "tar-js";
|
||||
|
||||
interface IOpts {
|
||||
labels?: string[];
|
||||
@ -342,7 +343,7 @@ async function collectLogs(
|
||||
* the server does not respond with an expected body format.
|
||||
*/
|
||||
export default async function sendBugReport(bugReportEndpoint?: string, opts: IOpts = {}): Promise<string> {
|
||||
if (!bugReportEndpoint) {
|
||||
if (!bugReportEndpoint || bugReportEndpoint === "local") {
|
||||
throw new Error("No bug report endpoint has been set.");
|
||||
}
|
||||
|
||||
@ -353,21 +354,7 @@ export default async function sendBugReport(bugReportEndpoint?: string, opts: IO
|
||||
return submitReport(bugReportEndpoint, body, progressCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the files from a bug report. This is the same as sendBugReport,
|
||||
* but instead causes the browser to download the files locally.
|
||||
*
|
||||
* @param {object} opts optional dictionary of options
|
||||
*
|
||||
* @param {string} opts.userText Any additional user input.
|
||||
*
|
||||
* @param {boolean} opts.sendLogs True to send logs
|
||||
*
|
||||
* @param {function(string)} opts.progressCallback Callback to call with progress updates
|
||||
*
|
||||
* @return {Promise} Resolved when the bug report is downloaded (or started).
|
||||
*/
|
||||
export async function downloadBugReport(opts: IOpts = {}): Promise<void> {
|
||||
export async function loadBugReport(opts: IOpts = {}): Promise<Tar> {
|
||||
const Tar = (await import("tar-js")).default;
|
||||
const progressCallback = opts.progressCallback || ((): void => {});
|
||||
const body = await collectBugReport(opts, false);
|
||||
@ -391,7 +378,25 @@ export async function downloadBugReport(opts: IOpts = {}): Promise<void> {
|
||||
}
|
||||
}
|
||||
tape.append("issue.txt", metadata);
|
||||
return tape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the files from a bug report. This is the same as sendBugReport,
|
||||
* but instead causes the browser to download the files locally.
|
||||
*
|
||||
* @param {object} opts optional dictionary of options
|
||||
*
|
||||
* @param {string} opts.userText Any additional user input.
|
||||
*
|
||||
* @param {boolean} opts.sendLogs True to send logs
|
||||
*
|
||||
* @param {function(string)} opts.progressCallback Callback to call with progress updates
|
||||
*
|
||||
* @return {Promise} Resolved when the bug report is downloaded (or started).
|
||||
*/
|
||||
export async function downloadBugReport(opts: IOpts = {}): Promise<void> {
|
||||
const tape = await loadBugReport(opts);
|
||||
// We have to create a new anchor to download if we want a filename. Otherwise we could
|
||||
// just use window.open.
|
||||
const dl = document.createElement("a");
|
||||
@ -417,6 +422,10 @@ export async function submitFeedback(
|
||||
canContact = false,
|
||||
extraData: Record<string, any> = {},
|
||||
): Promise<void> {
|
||||
const bugReportEndpointUrl = SdkConfig.get().bug_report_endpoint_url;
|
||||
if (!bugReportEndpointUrl || bugReportEndpointUrl === "local") {
|
||||
throw Error("Bug report URL is not set or local");
|
||||
}
|
||||
let version: string | undefined;
|
||||
try {
|
||||
version = await PlatformPeg.get()?.getAppVersion();
|
||||
@ -436,11 +445,7 @@ export async function submitFeedback(
|
||||
body.append(k, JSON.stringify(extraData[k]));
|
||||
}
|
||||
|
||||
const bugReportEndpointUrl = SdkConfig.get().bug_report_endpoint_url;
|
||||
|
||||
if (bugReportEndpointUrl) {
|
||||
await submitReport(bugReportEndpointUrl, body, () => {});
|
||||
}
|
||||
await submitReport(bugReportEndpointUrl, body, () => {});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -51,6 +51,7 @@ import MediaPreviewConfigController from "./controllers/MediaPreviewConfigContro
|
||||
import InviteRulesConfigController from "./controllers/InviteRulesConfigController.ts";
|
||||
import { type ComputedInviteConfig } from "../@types/invite-rules.ts";
|
||||
import BlockInvitesConfigController from "./controllers/BlockInvitesConfigController.ts";
|
||||
import { shouldShowFeedback } from "../utils/Feedback.ts";
|
||||
|
||||
export const defaultWatchManager = new WatchManager();
|
||||
|
||||
@ -406,7 +407,7 @@ export const SETTINGS: Settings = {
|
||||
</>
|
||||
),
|
||||
faq: () =>
|
||||
SdkConfig.get().bug_report_endpoint_url && (
|
||||
shouldShowFeedback() && (
|
||||
<>
|
||||
<h4>{_t("labs|video_rooms_faq1_question")}</h4>
|
||||
<p>{_t("labs|video_rooms_faq1_answer")}</p>
|
||||
|
||||
@ -11,5 +11,6 @@ import SettingsStore from "../settings/SettingsStore";
|
||||
import { UIFeature } from "../settings/UIFeature";
|
||||
|
||||
export function shouldShowFeedback(): boolean {
|
||||
return !!SdkConfig.get().bug_report_endpoint_url && SettingsStore.getValue(UIFeature.Feedback);
|
||||
const url = SdkConfig.get().bug_report_endpoint_url;
|
||||
return !!url && url !== "local" && SettingsStore.getValue(UIFeature.Feedback);
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as rageshake from "../rageshake/rageshake";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import sendBugReport from "../rageshake/submit-rageshake";
|
||||
import sendBugReport, { loadBugReport } from "../rageshake/submit-rageshake";
|
||||
|
||||
export function initRageshake(): Promise<void> {
|
||||
// we manually check persistence for rageshakes ourselves
|
||||
@ -54,7 +54,7 @@ export function initRageshakeStore(): Promise<void> {
|
||||
return rageshake.tryInitStorage();
|
||||
}
|
||||
|
||||
window.mxSendRageshake = function (text: string, withLogs?: boolean): void {
|
||||
window.mxSendRageshake = async function (text: string, withLogs?: boolean): Promise<void> {
|
||||
const url = SdkConfig.get().bug_report_endpoint_url;
|
||||
if (!url) {
|
||||
logger.error("Cannot send a rageshake - no bug_report_endpoint_url configured");
|
||||
@ -66,6 +66,17 @@ window.mxSendRageshake = function (text: string, withLogs?: boolean): void {
|
||||
logger.error("Cannot send a rageshake without a message - please tell us what went wrong");
|
||||
return;
|
||||
}
|
||||
if (url === "local") {
|
||||
const tape = await loadBugReport({
|
||||
userText: text,
|
||||
sendLogs: withLogs,
|
||||
progressCallback: logger.log.bind(console),
|
||||
});
|
||||
const blob = new Blob([new Uint8Array(tape.out)], { type: "application/gzip" });
|
||||
const object = URL.createObjectURL(blob);
|
||||
console.log(`Your logs are available at ${object}`);
|
||||
return;
|
||||
}
|
||||
sendBugReport(url, {
|
||||
userText: text,
|
||||
sendLogs: withLogs,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user