Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Add A-Element-R labels to rageshakes if rust (#12251)
Browse files Browse the repository at this point in the history
* Add A-Element-R labels to rageshakes if rust

* fix import

* Add tests for rageshake  collect

* add ts-ignore in test

* refactor rageshake to match sonar Cognitive Complexity
  • Loading branch information
BillCarsonFr committed Feb 15, 2024
1 parent 342d0db commit db096b7
Show file tree
Hide file tree
Showing 3 changed files with 804 additions and 116 deletions.
310 changes: 194 additions & 116 deletions src/rageshake/submit-rageshake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.
*/

import { logger } from "matrix-js-sdk/src/logger";
import { Method } from "matrix-js-sdk/src/matrix";
import { Method, MatrixClient, CryptoApi } from "matrix-js-sdk/src/matrix";

import type * as Pako from "pako";
import { MatrixClientPeg } from "../MatrixClientPeg";
Expand All @@ -37,34 +37,70 @@ interface IOpts {
customFields?: Record<string, string>;
}

async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<FormData> {
const progressCallback = opts.progressCallback || ((): void => {});
/**
* Exported only for testing.
* @internal public for test
*/
export async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<FormData> {
const progressCallback = opts.progressCallback;

progressCallback(_t("bug_reporting|collecting_information"));
let version: string | undefined;
try {
version = await PlatformPeg.get()?.getAppVersion();
} catch (err) {} // PlatformPeg already logs this.
progressCallback?.(_t("bug_reporting|collecting_information"));

const userAgent = window.navigator?.userAgent ?? "UNKNOWN";
logger.log("Sending bug report.");

const body = new FormData();

await collectBaseInformation(body, opts);

const client = MatrixClientPeg.get();

if (client) {
await collectClientInfo(client, body);
}

collectLabels(client, opts, body);

collectSettings(body);

await collectStorageStatInfo(body);

collectMissingFeatures(body);

if (opts.sendLogs) {
await collectLogs(body, gzipLogs, progressCallback);
}

let installedPWA = "UNKNOWN";
return body;
}

async function getAppVersion(): Promise<string | undefined> {
try {
// Known to work at least for desktop Chrome
installedPWA = String(window.matchMedia("(display-mode: standalone)").matches);
} catch (e) {}
return await PlatformPeg.get()?.getAppVersion();
} catch (err) {
// this happens if no version is set i.e. in dev
}
}

let touchInput = "UNKNOWN";
function matchesMediaQuery(query: string): string {
try {
// MDN claims broad support across browsers
touchInput = String(window.matchMedia("(pointer: coarse)").matches);
} catch (e) {}
return String(window.matchMedia(query).matches);
} catch (err) {
// if not supported in browser
}
return "UNKNOWN";
}

const client = MatrixClientPeg.get();
/**
* Collects base information about the user and the app to add to the report.
*/
async function collectBaseInformation(body: FormData, opts: IOpts): Promise<void> {
const version = await getAppVersion();

logger.log("Sending bug report.");
const userAgent = window.navigator?.userAgent ?? "UNKNOWN";

const installedPWA = matchesMediaQuery("(display-mode: standalone)");
const touchInput = matchesMediaQuery("(pointer: coarse)");

const body = new FormData();
body.append("text", opts.userText || "User did not supply any additional text.");
body.append("app", opts.customApp || "element-web");
body.append("version", version ?? "UNKNOWN");
Expand All @@ -77,98 +113,129 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
body.append(key, opts.customFields[key]);
}
}
}

if (client) {
body.append("user_id", client.credentials.userId!);
body.append("device_id", client.deviceId!);

const cryptoApi = client.getCrypto();

if (cryptoApi) {
body.append("crypto_version", cryptoApi.getVersion());

const ownDeviceKeys = await cryptoApi.getOwnDeviceKeys();
const keys = [`curve25519:${ownDeviceKeys.curve25519}`, `ed25519:${ownDeviceKeys.ed25519}`];

body.append("device_keys", keys.join(", "));

// add cross-signing status information
const crossSigningStatus = await cryptoApi.getCrossSigningStatus();
const secretStorage = client.secretStorage;

body.append("cross_signing_ready", String(await cryptoApi.isCrossSigningReady()));
body.append("cross_signing_key", (await cryptoApi.getCrossSigningKeyId()) ?? "n/a");
body.append(
"cross_signing_privkey_in_secret_storage",
String(crossSigningStatus.privateKeysInSecretStorage),
);

body.append(
"cross_signing_master_privkey_cached",
String(crossSigningStatus.privateKeysCachedLocally.masterKey),
);
body.append(
"cross_signing_self_signing_privkey_cached",
String(crossSigningStatus.privateKeysCachedLocally.selfSigningKey),
);
body.append(
"cross_signing_user_signing_privkey_cached",
String(crossSigningStatus.privateKeysCachedLocally.userSigningKey),
);

body.append("secret_storage_ready", String(await cryptoApi.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 cryptoApi.getSessionBackupPrivateKey();
body.append("session_backup_key_cached", String(!!sessionBackupKeyFromCache));
body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array));
}
/**
* Collects client and crypto related info.
*/
async function collectClientInfo(client: MatrixClient, body: FormData): Promise<void> {
body.append("user_id", client.credentials.userId!);
body.append("device_id", client.deviceId!);

const cryptoApi = client.getCrypto();

if (cryptoApi) {
await collectCryptoInfo(cryptoApi, body);
await collectRecoveryInfo(client, cryptoApi, body);
}

await collectSynapseSpecific(client, body);
}

/**
* Collects information about the home server.
*/
async function collectSynapseSpecific(client: MatrixClient, body: FormData): Promise<void> {
try {
// XXX: This is synapse-specific but better than nothing until MSC support for a server version endpoint
const data = await client.http.request<Record<string, any>>(
Method.Get,
"/server_version",
undefined,
undefined,
{
prefix: "/_synapse/admin/v1",
},
);
Object.keys(data).forEach((key) => {
body.append(`matrix_hs_${key}`, data[key]);
});
} catch {
try {
// XXX: This is synapse-specific but better than nothing until MSC support for a server version endpoint
const data = await client.http.request<Record<string, any>>(
Method.Get,
"/server_version",
undefined,
undefined,
{
prefix: "/_synapse/admin/v1",
},
);
Object.keys(data).forEach((key) => {
body.append(`matrix_hs_${key}`, data[key]);
});
// XXX: This relies on the federation listener being delegated via well-known
// or at the same place as the client server endpoint
const data = await getServerVersionFromFederationApi(client);
body.append("matrix_hs_name", data.server.name);
body.append("matrix_hs_version", data.server.version);
} catch {
try {
// XXX: This relies on the federation listener being delegated via well-known
// or at the same place as the client server endpoint
const data = await getServerVersionFromFederationApi(client);
body.append("matrix_hs_name", data.server.name);
body.append("matrix_hs_version", data.server.version);
} catch {
try {
// If that fails we'll hit any endpoint and look at the server response header
const res = await window.fetch(client.http.getUrl("/login"), {
method: "GET",
mode: "cors",
});
if (res.headers.has("server")) {
body.append("matrix_hs_server", res.headers.get("server")!);
}
} catch {
// Could not determine server version
// If that fails we'll hit any endpoint and look at the server response header
const res = await window.fetch(client.http.getUrl("/login"), {
method: "GET",
mode: "cors",
});
if (res.headers.has("server")) {
body.append("matrix_hs_server", res.headers.get("server")!);
}
} catch {
// Could not determine server version
}
}
}
}

/**
* Collects crypto related information.
*/
async function collectCryptoInfo(cryptoApi: CryptoApi, body: FormData): Promise<void> {
body.append("crypto_version", cryptoApi.getVersion());

const ownDeviceKeys = await cryptoApi.getOwnDeviceKeys();
const keys = [`curve25519:${ownDeviceKeys.curve25519}`, `ed25519:${ownDeviceKeys.ed25519}`];

body.append("device_keys", keys.join(", "));

// add cross-signing status information
const crossSigningStatus = await cryptoApi.getCrossSigningStatus();

body.append("cross_signing_ready", String(await cryptoApi.isCrossSigningReady()));
body.append("cross_signing_key", (await cryptoApi.getCrossSigningKeyId()) ?? "n/a");
body.append("cross_signing_privkey_in_secret_storage", String(crossSigningStatus.privateKeysInSecretStorage));

body.append("cross_signing_master_privkey_cached", String(crossSigningStatus.privateKeysCachedLocally.masterKey));
body.append(
"cross_signing_self_signing_privkey_cached",
String(crossSigningStatus.privateKeysCachedLocally.selfSigningKey),
);
body.append(
"cross_signing_user_signing_privkey_cached",
String(crossSigningStatus.privateKeysCachedLocally.userSigningKey),
);
}

/**
* Collects information about secret storage and backup.
*/
async function collectRecoveryInfo(client: MatrixClient, cryptoApi: CryptoApi, body: FormData): Promise<void> {
const secretStorage = client.secretStorage;
body.append("secret_storage_ready", String(await cryptoApi.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 cryptoApi.getSessionBackupPrivateKey();
body.append("session_backup_key_cached", String(!!sessionBackupKeyFromCache));
body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array));
}

/**
* Collects labels to add to the report.
*/
export function collectLabels(client: MatrixClient | null, opts: IOpts, body: FormData): void {
if (client?.getCrypto()?.getVersion()?.startsWith(`Rust SDK`)) {
body.append("label", "A-Element-R");
}

if (opts.labels) {
for (const label of opts.labels) {
body.append("label", label);
}
}
}

/**
* Collects some settings (lab flags and more) to add to the report.
*/
export function collectSettings(body: FormData): void {
// add labs options
const enabledLabs = SettingsStore.getFeatureSettingNames().filter((f) => SettingsStore.getValue(f));
if (enabledLabs.length) {
Expand All @@ -179,6 +246,13 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
body.append("lowBandwidth", "enabled");
}

body.append("mx_local_settings", localStorage.getItem("mx_local_settings")!);
}

/**
* Collects storage statistics to add to the report.
*/
async function collectStorageStatInfo(body: FormData): Promise<void> {
// add storage persistence/quota information
if (navigator.storage && navigator.storage.persisted) {
try {
Expand All @@ -202,7 +276,9 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
}
} catch (e) {}
}
}

function collectMissingFeatures(body: FormData): void {
if (window.Modernizr) {
const missingFeatures = (Object.keys(window.Modernizr) as [keyof ModernizrStatic]).filter(
(key: keyof ModernizrStatic) => window.Modernizr[key] === false,
Expand All @@ -211,33 +287,35 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
body.append("modernizr_missing_features", missingFeatures.join(", "));
}
}
}

body.append("mx_local_settings", localStorage.getItem("mx_local_settings")!);
/**
* Collects logs to add to the report if enabled.
*/
async function collectLogs(
body: FormData,
gzipLogs: boolean,
progressCallback: ((s: string) => void) | undefined,
): Promise<void> {
let pako: typeof Pako | undefined;
if (gzipLogs) {
pako = await import("pako");
}

if (opts.sendLogs) {
let pako: typeof Pako | undefined;
progressCallback?.(_t("bug_reporting|collecting_logs"));
const logs = await rageshake.getLogsForReport();
for (const entry of logs) {
// encode as UTF-8
let buf = new TextEncoder().encode(entry.lines);

// compress
if (gzipLogs) {
pako = await import("pako");
buf = pako!.gzip(buf);
}

progressCallback(_t("bug_reporting|collecting_logs"));
const logs = await rageshake.getLogsForReport();
for (const entry of logs) {
// encode as UTF-8
let buf = new TextEncoder().encode(entry.lines);

// compress
if (gzipLogs) {
buf = pako!.gzip(buf);
}

body.append("compressed-log", new Blob([buf]), entry.id);
}
body.append("compressed-log", new Blob([buf]), entry.id);
}

return body;
}

/**
* Send a bug report.
*
Expand Down
Loading

0 comments on commit db096b7

Please sign in to comment.