From a644621889617f17caee3633122d6906a852e9f6 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 14 Jun 2022 18:46:02 +0200 Subject: [PATCH 1/7] basic support for sending rageshake in view model --- src/domain/rageshake.ts | 69 +++++++++++++++++++ .../session/settings/SettingsViewModel.js | 16 +++++ src/matrix/net/common.ts | 8 ++- src/platform/web/assets/config.json | 3 +- src/platform/web/dom/request/fetch.js | 16 ++++- 5 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 src/domain/rageshake.ts diff --git a/src/domain/rageshake.ts b/src/domain/rageshake.ts new file mode 100644 index 00000000..e79422cd --- /dev/null +++ b/src/domain/rageshake.ts @@ -0,0 +1,69 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {encodeBody} from "../matrix/net/common"; + +import type {BlobHandle} from "../platform/web/dom/BlobHandle"; +import type {RequestFunction} from "../platform/types/types"; + +// see https://github.com/matrix-org/rageshake#readme +type RageshakeData = { + // A textual description of the problem. Included in the details.log.gz file. + text: string | undefined; + // Application user-agent. Included in the details.log.gz file. + userAgent: string; + // Identifier for the application (eg 'riot-web'). Should correspond to a mapping configured in the configuration file for github issue reporting to work. + app: string; + // Application version. Included in the details.log.gz file. + version: string; + // Label to attach to the github issue, and include in the details file. + label: string | undefined; +}; + +export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: BlobHandle, submitUrl: string, request: RequestFunction): Promise { + const formData = new Map(); + if (data.text) { + formData.set("text", data.text); + } + formData.set("user_agent", data.userAgent); + formData.set("app", data.app); + formData.set("version", data.version); + if (data.label) { + formData.set("label", data.label); + } + formData.set("file", {name: "logs.json", blob: logsBlob}); + const encoded = encodeBody(formData); + const headers: Map = new Map(); + headers.set("Accept", "application/json"); + //headers.set("Content-Type", encoded.mimeType); + const result = request(submitUrl, { + method: "POST", + body: encoded.body, + headers + }); + let response; + try { + response = await result.response(); + } catch (err) { + throw new Error(`Could not submit logs to ${submitUrl}, got error ${err.message}`); + } + const {status, body} = response; + if (status >= 200 && status < 300) { + return body.report_url; + } else { + throw new Error(`Could not submit logs to ${submitUrl}, got status code ${status} with body ${body}`); + } +} diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index d844e21b..b3ca74ca 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -16,6 +16,7 @@ limitations under the License. import {ViewModel} from "../../ViewModel"; import {KeyBackupViewModel} from "./KeyBackupViewModel.js"; +import {submitLogsToRageshakeServer} from "../../../domain/rageshake"; class PushNotificationStatus { constructor() { @@ -152,6 +153,21 @@ export class SettingsViewModel extends ViewModel { this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`); } + async sendLogsToServer() { + const logExport = await this.logger.export(); + await submitLogsToRageshakeServer( + { + app: "hydrogen", + userAgent: "", + version: DEFINE_VERSION, + text: "Submit logs from settings", + }, + logExport.asBlob(), + this.platform.config.bugReportEndpointUrl, + this.platform.request + ); + } + async togglePushNotifications() { this.pushNotifications.updating = true; this.pushNotifications.enabledOnServer = null; diff --git a/src/matrix/net/common.ts b/src/matrix/net/common.ts index 4ba42395..0b35a3e3 100644 --- a/src/matrix/net/common.ts +++ b/src/matrix/net/common.ts @@ -19,7 +19,8 @@ import {BlobHandle} from "../../platform/web/dom/BlobHandle.js"; export type EncodedBody = { mimeType: string; - body: BlobHandle | string; + // the map gets transformed to a FormData object on the web + body: BlobHandle | string | Map; } export function encodeQueryParams(queryParams?: object): string { @@ -41,6 +42,11 @@ export function encodeBody(body: BlobHandle | object): EncodedBody { mimeType: blob.mimeType, body: blob // will be unwrapped in request fn }; + } else if (body instanceof Map) { + return { + mimeType: "multipart/form-data", + body: body + } } else if (typeof body === "object") { const json = JSON.stringify(body); return { diff --git a/src/platform/web/assets/config.json b/src/platform/web/assets/config.json index 703ae1e6..fd46fcbc 100644 --- a/src/platform/web/assets/config.json +++ b/src/platform/web/assets/config.json @@ -4,5 +4,6 @@ "gatewayUrl": "https://matrix.org", "applicationServerKey": "BC-gpSdVHEXhvHSHS0AzzWrQoukv2BE7KzpoPO_FfPacqOo3l1pdqz7rSgmB04pZCWaHPz7XRe6fjLaC-WPDopM" }, - "defaultHomeServer": "matrix.org" + "defaultHomeServer": "matrix.org", + "bugReportEndpointUrl": "https://element.io/bugreports/submit" } diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index adf833ef..1d2ef3c0 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -64,12 +64,26 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { if (requestOptions?.uploadProgress) { return xhrRequest(url, requestOptions); } - let {method, headers, body, timeout, format, cache = false} = requestOptions; + let {method, headers, body, formData, timeout, format, cache = false} = requestOptions; const controller = typeof AbortController === "function" ? new AbortController() : null; // if a BlobHandle, take native blob if (body?.nativeBlob) { body = body.nativeBlob; } + if (body instanceof Map) { + const formData = new FormData(); + for (const [name, value] of body) { + let filename; + // Special case {name: string, blob: BlobHandle} to set a filename. + // This is the format returned by platform.openFile + if (value.blob?.nativeBlob && value.name) { + formData.set(name, value.blob.nativeBlob, value.name); + } else { + formData.set(name, value); + } + } + body = formData; + } let options = {method, body}; if (controller) { options = Object.assign(options, { From 4caabae895948379afc1f9540260401c64457af9 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 10:15:15 +0200 Subject: [PATCH 2/7] extract map -> formdata conversion and also suppor this for xhr --- src/platform/web/dom/request/common.js | 15 +++++++++++++++ src/platform/web/dom/request/fetch.js | 15 ++------------- src/platform/web/dom/request/xhr.js | 5 ++++- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/platform/web/dom/request/common.js b/src/platform/web/dom/request/common.js index d97456aa..c073cf6b 100644 --- a/src/platform/web/dom/request/common.js +++ b/src/platform/web/dom/request/common.js @@ -27,6 +27,21 @@ export function addCacheBuster(urlStr, random = Math.random) { return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`; } +export function mapAsFormData(map) { + const formData = new FormData(); + for (const [name, value] of map) { + let filename; + // Special case {name: string, blob: BlobHandle} to set a filename. + // This is the format returned by platform.openFile + if (value.blob?.nativeBlob && value.name) { + formData.set(name, value.blob.nativeBlob, value.name); + } else { + formData.set(name, value); + } + } + return formData; +} + export function tests() { return { "add cache buster": assert => { diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index 1d2ef3c0..d50f6194 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -20,7 +20,7 @@ import { ConnectionError } from "../../../../matrix/error.js"; import {abortOnTimeout} from "../../../../utils/timeout"; -import {addCacheBuster} from "./common.js"; +import {addCacheBuster, mapAsFormData} from "./common.js"; import {xhrRequest} from "./xhr.js"; class RequestResult { @@ -71,18 +71,7 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { body = body.nativeBlob; } if (body instanceof Map) { - const formData = new FormData(); - for (const [name, value] of body) { - let filename; - // Special case {name: string, blob: BlobHandle} to set a filename. - // This is the format returned by platform.openFile - if (value.blob?.nativeBlob && value.name) { - formData.set(name, value.blob.nativeBlob, value.name); - } else { - formData.set(name, value); - } - } - body = formData; + body = mapAsFormData(body); } let options = {method, body}; if (controller) { diff --git a/src/platform/web/dom/request/xhr.js b/src/platform/web/dom/request/xhr.js index c3ad8ae8..fba7123e 100644 --- a/src/platform/web/dom/request/xhr.js +++ b/src/platform/web/dom/request/xhr.js @@ -18,7 +18,7 @@ import { AbortError, ConnectionError } from "../../../../matrix/error.js"; -import {addCacheBuster} from "./common.js"; +import {addCacheBuster, mapAsFormData} from "./common.js"; class RequestResult { constructor(promise, xhr) { @@ -94,6 +94,9 @@ export function xhrRequest(url, options) { if (body?.nativeBlob) { body = body.nativeBlob; } + if (body instanceof Map) { + body = mapAsFormData(body); + } xhr.send(body || null); return new RequestResult(promise, xhr); From 2129a97588346a87b9bf02a24abab7b2813696cd Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:12:49 +0200 Subject: [PATCH 3/7] remove unused param --- src/domain/rageshake.ts | 4 +--- src/platform/web/dom/request/fetch.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/domain/rageshake.ts b/src/domain/rageshake.ts index e79422cd..b1468bf5 100644 --- a/src/domain/rageshake.ts +++ b/src/domain/rageshake.ts @@ -61,9 +61,7 @@ export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: throw new Error(`Could not submit logs to ${submitUrl}, got error ${err.message}`); } const {status, body} = response; - if (status >= 200 && status < 300) { - return body.report_url; - } else { + if (status < 200 || status >= 300) { throw new Error(`Could not submit logs to ${submitUrl}, got status code ${status} with body ${body}`); } } diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index d50f6194..497ad553 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -64,7 +64,7 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { if (requestOptions?.uploadProgress) { return xhrRequest(url, requestOptions); } - let {method, headers, body, formData, timeout, format, cache = false} = requestOptions; + let {method, headers, body, timeout, format, cache = false} = requestOptions; const controller = typeof AbortController === "function" ? new AbortController() : null; // if a BlobHandle, take native blob if (body?.nativeBlob) { From 69ada73dd4a4234aa375e7d782cf6632ba929120 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:13:05 +0200 Subject: [PATCH 4/7] cleanup rageshake code --- src/domain/rageshake.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/domain/rageshake.ts b/src/domain/rageshake.ts index b1468bf5..a1f3fd39 100644 --- a/src/domain/rageshake.ts +++ b/src/domain/rageshake.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {encodeBody} from "../matrix/net/common"; - import type {BlobHandle} from "../platform/web/dom/BlobHandle"; import type {RequestFunction} from "../platform/types/types"; @@ -45,13 +43,11 @@ export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: formData.set("label", data.label); } formData.set("file", {name: "logs.json", blob: logsBlob}); - const encoded = encodeBody(formData); const headers: Map = new Map(); headers.set("Accept", "application/json"); - //headers.set("Content-Type", encoded.mimeType); const result = request(submitUrl, { method: "POST", - body: encoded.body, + body: formData, headers }); let response; @@ -64,4 +60,6 @@ export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: if (status < 200 || status >= 300) { throw new Error(`Could not submit logs to ${submitUrl}, got status code ${status} with body ${body}`); } + // we don't bother with reading report_url from the body as the rageshake server doesn't always return it + // and would have to have CORS setup properly for us to be able to read it. } From 375d8b066c3fef937a7adf2904d8edceeb6a3f52 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:13:46 +0200 Subject: [PATCH 5/7] complete settings view model for logs ui --- .../session/settings/SettingsViewModel.js | 55 +++++++++++++++---- src/matrix/net/common.ts | 4 +- src/platform/types/types.ts | 4 +- src/platform/web/Platform.js | 4 ++ 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index b3ca74ca..4dcdb111 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -52,6 +52,7 @@ export class SettingsViewModel extends ViewModel { this.maxSentImageSizeLimit = 4000; this.pushNotifications = new PushNotificationStatus(); this._activeTheme = undefined; + this._logsFeedbackMessage = undefined; } get _session() { @@ -153,19 +154,49 @@ export class SettingsViewModel extends ViewModel { this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`); } + get canSendLogsToServer() { + return !!this.platform.config.bugReportEndpointUrl; + } + + get logsServer() { + const {bugReportEndpointUrl} = this.platform.config; + try { + if (bugReportEndpointUrl) { + return new URL(bugReportEndpointUrl).hostname; + } + } catch (e) {} + return ""; + } + async sendLogsToServer() { - const logExport = await this.logger.export(); - await submitLogsToRageshakeServer( - { - app: "hydrogen", - userAgent: "", - version: DEFINE_VERSION, - text: "Submit logs from settings", - }, - logExport.asBlob(), - this.platform.config.bugReportEndpointUrl, - this.platform.request - ); + const {bugReportEndpointUrl} = this.platform.config; + if (bugReportEndpointUrl) { + this._logsFeedbackMessage = this.i18n`Sending logsā€¦`; + this.emitChange(); + try { + const logExport = await this.logger.export(); + await submitLogsToRageshakeServer( + { + app: "hydrogen", + userAgent: this.platform.description, + version: DEFINE_VERSION, + text: `Submit logs from settings for user ${this._session.userId} on device ${this._session.deviceId}`, + }, + logExport.asBlob(), + bugReportEndpointUrl, + this.platform.request + ); + this._logsFeedbackMessage = this.i18n`Logs sent succesfully!`; + this.emitChange(); + } catch (err) { + this._logsFeedbackMessage = err.message; + this.emitChange(); + } + } + } + + get logsFeedbackMessage() { + return this._logsFeedbackMessage; } async togglePushNotifications() { diff --git a/src/matrix/net/common.ts b/src/matrix/net/common.ts index 0b35a3e3..9f0fade4 100644 --- a/src/matrix/net/common.ts +++ b/src/matrix/net/common.ts @@ -17,10 +17,12 @@ limitations under the License. import {BlobHandle} from "../../platform/web/dom/BlobHandle.js"; +export type RequestBody = BlobHandle | string | Map; + export type EncodedBody = { mimeType: string; // the map gets transformed to a FormData object on the web - body: BlobHandle | string | Map; + body: RequestBody } export function encodeQueryParams(queryParams?: object): string { diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index da8ec8e7..1d359a09 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -15,13 +15,13 @@ limitations under the License. */ import type {RequestResult} from "../web/dom/request/fetch.js"; -import type {EncodedBody} from "../../matrix/net/common"; +import type {RequestBody} from "../../matrix/net/common"; import type {ILogItem} from "../../logging/types"; export interface IRequestOptions { uploadProgress?: (loadedBytes: number) => void; timeout?: number; - body?: EncodedBody; + body?: RequestBody; headers?: Map; cache?: boolean; method?: string; diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index dfb26b0f..e2ca2028 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -345,6 +345,10 @@ export class Platform { head.appendChild(styleTag); } + get description() { + return navigator.userAgent ?? ""; + } + dispose() { this._disposables.dispose(); } From 8fe8981ffa914957037c75a41f1714821509e3b3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:14:06 +0200 Subject: [PATCH 6/7] add options to send logs to server in settings ui --- src/platform/web/ui/general/utils.ts | 8 ++++++++ src/platform/web/ui/session/settings/SettingsView.js | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/general/utils.ts b/src/platform/web/ui/general/utils.ts index f8d407e9..b310571f 100644 --- a/src/platform/web/ui/general/utils.ts +++ b/src/platform/web/ui/general/utils.ts @@ -54,3 +54,11 @@ export function insertAt(parentNode: Element, idx: number, childNode: Node): voi export function removeChildren(parentNode: Element): void { parentNode.innerHTML = ''; } + +export function disableTargetCallback(callback: (evt: Event) => Promise): (evt: Event) => Promise { + return async (evt: Event) => { + (evt.target as HTMLElement)?.setAttribute("disabled", "disabled"); + await callback(evt); + (evt.target as HTMLElement)?.removeAttribute("disabled"); + } +} diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js index cf09ddb5..c4405e82 100644 --- a/src/platform/web/ui/session/settings/SettingsView.js +++ b/src/platform/web/ui/session/settings/SettingsView.js @@ -15,6 +15,7 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView"; +import {disableTargetCallback} from "../../general/utils"; import {KeyBackupSettingsView} from "./KeyBackupSettingsView.js" export class SettingsView extends TemplateView { @@ -101,11 +102,17 @@ export class SettingsView extends TemplateView { return row(t, vm.i18n`Use the following theme`, this._themeOptions(t, vm)); }), ); + const logButtons = []; + if (vm.canSendLogsToServer) { + logButtons.push(t.button({onClick: disableTargetCallback(() => vm.sendLogsToServer())}, `Submit logs to ${vm.logsServer}`)); + } + logButtons.push(t.button({onClick: () => vm.exportLogs()}, "Download logs")); settingNodes.push( t.h3("Application"), row(t, vm.i18n`Version`, version), row(t, vm.i18n`Storage usage`, vm => `${vm.storageUsage} / ${vm.storageQuota}`), - row(t, vm.i18n`Debug logs`, t.button({onClick: () => vm.exportLogs()}, "Export")), + row(t, vm.i18n`Debug logs`, logButtons), + t.p({className: {hidden: vm => !vm.logsFeedbackMessage}}, vm => vm.logsFeedbackMessage), t.p(["Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, the usernames of other users and the names of files you send. They do not contain messages. For more information, review our ", t.a({href: "https://element.io/privacy", target: "_blank", rel: "noopener"}, "privacy policy"), "."]), ); From 3b66ed8c17cc26fe6771bd3fb24b01053c5f97a2 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:24:16 +0200 Subject: [PATCH 7/7] fix type --- src/domain/rageshake.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/rageshake.ts b/src/domain/rageshake.ts index a1f3fd39..cb06e638 100644 --- a/src/domain/rageshake.ts +++ b/src/domain/rageshake.ts @@ -31,7 +31,7 @@ type RageshakeData = { label: string | undefined; }; -export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: BlobHandle, submitUrl: string, request: RequestFunction): Promise { +export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: BlobHandle, submitUrl: string, request: RequestFunction): Promise { const formData = new Map(); if (data.text) { formData.set("text", data.text);