diff --git a/src/sentry.ts b/src/sentry.ts index 925aa48251..bccfdaad79 100644 --- a/src/sentry.ts +++ b/src/sentry.ts @@ -2,14 +2,127 @@ import * as Sentry from "@sentry/browser"; import { Integrations } from "@sentry/tracing"; import PlatformPeg from "./PlatformPeg"; import SdkConfig from "./SdkConfig"; +import {MatrixClientPeg} from "./MatrixClientPeg"; +import SettingsStore from "./settings/SettingsStore"; +import {MatrixClient} from "../../matrix-js-sdk"; -export function sendSentryReport(userText: string, label: string, error: Error): void { +async function getStorageOptions(): Record { + const result = {} + + // add storage persistence/quota information + if (navigator.storage && navigator.storage.persisted) { + try { + result["storageManager_persisted"] = String(await navigator.storage.persisted()); + } catch (e) {} + } else if (document.hasStorageAccess) { // Safari + try { + result["storageManager_persisted"] = String(await document.hasStorageAccess()); + } catch (e) {} + } + if (navigator.storage && navigator.storage.estimate) { + try { + const estimate = await navigator.storage.estimate(); + result["storageManager_quota"] = String(estimate.quota); + result["storageManager_usage"] = String(estimate.usage); + if (estimate.usageDetails) { + Object.keys(estimate.usageDetails).forEach(k => { + result[`storageManager_usage_${k}`] = String(estimate.usageDetails[k]); + }); + } + } catch (e) {} + } + + return result; +} + +function getUserContext(client: MatrixClient): Record { + return { + "username": client.credentials.userId, + "enabled_labs": getEnabledLabs(), + "low_bandwidth": SettingsStore.getValue("lowBandwidth") ? "enabled" : "disabled", + }; +} + +function getEnabledLabs(): string { + const enabledLabs = SettingsStore.getFeatureSettingNames().filter(f => SettingsStore.getValue(f)); + if (enabledLabs.length) { + return enabledLabs.join(", "); + } +} + +async function getCryptoContext(client: MatrixClient): Record { + if (!client.isCryptoEnabled()) { + return {}; + } + const keys = [`ed25519:${client.getDeviceEd25519Key()}`]; + if (client.getDeviceCurve25519Key) { + keys.push(`curve25519:${client.getDeviceCurve25519Key()}`); + } + const crossSigning = client.crypto.crossSigningInfo; + const secretStorage = client.crypto.secretStorage; + const pkCache = client.getCrossSigningCacheCallbacks(); + const sessionBackupKeyFromCache = await client.crypto.getSessionBackupPrivateKey(); + + return { + "device_keys": keys.join(', '), + "cross_signing_ready": String(await client.isCrossSigningReady()), + "cross_signing_supported_by_hs": + String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")), + "cross_signing_key": crossSigning.getId(), + "cross_signing_privkey_in_secret_storage": String( + !!(await crossSigning.isStoredInSecretStorage(secretStorage))), + "cross_signing_master_privkey_cached": String( + !!(pkCache && await pkCache.getCrossSigningKeyCache("master"))), + "cross_signing_user_signing_privkey_cached": String( + !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"))), + "secret_storage_ready": String(await client.isSecretStorageReady()), + "secret_storage_key_in_account": String(!!(await secretStorage.hasKey())), + "session_backup_key_in_secret_storage": String(!!(await client.isKeyBackupKeyStored())), + "session_backup_key_cached": String(!!sessionBackupKeyFromCache), + "session_backup_key_well_formed": String(sessionBackupKeyFromCache instanceof Uint8Array), + }; +} + +function getDeviceContext(client: MatrixClient): Record { + const result = { + "device_id": client?.deviceId, + "mx_local_settings": localStorage.getItem('mx_local_settings'), + }; + + if (window.Modernizr) { + const missingFeatures = Object.keys(window.Modernizr).filter(key => window.Modernizr[key] === false); + if (missingFeatures.length > 0) { + result["modernizr_missing_features"] = missingFeatures.join(", "); + } + } + + return result; +} + +async function getContext() { + const client = MatrixClientPeg.get(); + return { + "contexts": { + "user": getUserContext(client), + "crypto": await getCryptoContext(client), + "device": getDeviceContext(client), + "storage": await getStorageOptions() + }, + "extra": { + + }, + }; +} + +export async function sendSentryReport(userText: string, label: string, error: Error): void { if (!SdkConfig.get()["sentry"]) return; // Ignore reports without errors, as they're not useful in sentry and can't easily be aggregated if (error) { - Sentry.captureException(error); + Sentry.captureException(error, await getContext()); } + + // TODO: use https://docs.sentry.io/api/projects/submit-user-feedback/ to submit userText } interface ISentryConfig {