mirror of https://github.com/vector-im/riot-web
226 lines
7.8 KiB
TypeScript
226 lines
7.8 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import * as Sentry from "@sentry/browser";
|
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
|
import { type Integration } from "@sentry/types/build/types/integration";
|
|
|
|
import SdkConfig from "./SdkConfig";
|
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
|
import SettingsStore from "./settings/SettingsStore";
|
|
import { IConfigOptions } from "./IConfigOptions";
|
|
|
|
/* eslint-disable camelcase */
|
|
|
|
type StorageContext = {
|
|
storageManager_persisted?: string;
|
|
storageManager_quota?: string;
|
|
storageManager_usage?: string;
|
|
storageManager_usageDetails?: string;
|
|
};
|
|
|
|
type UserContext = {
|
|
username: string;
|
|
enabled_labs: string;
|
|
low_bandwidth: string;
|
|
};
|
|
|
|
type CryptoContext = {
|
|
crypto_version?: string;
|
|
device_keys?: string;
|
|
cross_signing_ready?: string;
|
|
cross_signing_supported_by_hs?: string;
|
|
cross_signing_key?: string;
|
|
cross_signing_privkey_in_secret_storage?: string;
|
|
cross_signing_master_privkey_cached?: string;
|
|
cross_signing_user_signing_privkey_cached?: string;
|
|
secret_storage_ready?: string;
|
|
secret_storage_key_in_account?: string;
|
|
session_backup_key_in_secret_storage?: string;
|
|
session_backup_key_cached?: string;
|
|
session_backup_key_well_formed?: string;
|
|
};
|
|
|
|
type DeviceContext = {
|
|
device_id?: string;
|
|
mx_local_settings: string | null;
|
|
modernizr_missing_features?: string;
|
|
};
|
|
|
|
type Contexts = {
|
|
user: UserContext;
|
|
crypto: CryptoContext;
|
|
device: DeviceContext;
|
|
storage: StorageContext;
|
|
};
|
|
|
|
/* eslint-enable camelcase */
|
|
|
|
async function getStorageContext(): Promise<StorageContext> {
|
|
const result: StorageContext = {};
|
|
|
|
// 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) {
|
|
const usageDetails: string[] = [];
|
|
Object.keys(estimate.usageDetails).forEach((k) => {
|
|
usageDetails.push(`${k}: ${String(estimate.usageDetails![k])}`);
|
|
});
|
|
result[`storageManager_usage`] = usageDetails.join(", ");
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getUserContext(client: MatrixClient): UserContext {
|
|
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(", ");
|
|
}
|
|
return "";
|
|
}
|
|
|
|
async function getCryptoContext(client: MatrixClient): Promise<CryptoContext> {
|
|
const cryptoApi = client.getCrypto();
|
|
if (!cryptoApi) {
|
|
return {};
|
|
}
|
|
|
|
const ownDeviceKeys = await cryptoApi.getOwnDeviceKeys();
|
|
|
|
const keys = [`curve25519:${ownDeviceKeys.curve25519}`, `ed25519:${ownDeviceKeys.ed25519}`];
|
|
|
|
const crossSigningStatus = await cryptoApi.getCrossSigningStatus();
|
|
const secretStorage = client.secretStorage;
|
|
const sessionBackupKeyFromCache = await cryptoApi.getSessionBackupPrivateKey();
|
|
|
|
return {
|
|
crypto_version: cryptoApi.getVersion(),
|
|
device_keys: keys.join(", "),
|
|
cross_signing_ready: String(await cryptoApi.isCrossSigningReady()),
|
|
cross_signing_key: (await cryptoApi.getCrossSigningKeyId()) ?? undefined,
|
|
cross_signing_privkey_in_secret_storage: String(crossSigningStatus.privateKeysInSecretStorage),
|
|
cross_signing_master_privkey_cached: String(crossSigningStatus.privateKeysCachedLocally.masterKey),
|
|
cross_signing_user_signing_privkey_cached: String(crossSigningStatus.privateKeysCachedLocally.userSigningKey),
|
|
secret_storage_ready: String(await cryptoApi.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): DeviceContext {
|
|
const result: DeviceContext = {
|
|
device_id: client?.deviceId ?? undefined,
|
|
mx_local_settings: localStorage.getItem("mx_local_settings"),
|
|
};
|
|
|
|
if (window.Modernizr) {
|
|
const missingFeatures = Object.keys(window.Modernizr).filter(
|
|
(key) => window.Modernizr[key as keyof ModernizrStatic] === false,
|
|
);
|
|
if (missingFeatures.length > 0) {
|
|
result["modernizr_missing_features"] = missingFeatures.join(", ");
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async function getContexts(): Promise<Contexts> {
|
|
const client = MatrixClientPeg.safeGet();
|
|
return {
|
|
user: getUserContext(client),
|
|
crypto: await getCryptoContext(client),
|
|
device: getDeviceContext(client),
|
|
storage: await getStorageContext(),
|
|
};
|
|
}
|
|
|
|
export async function sendSentryReport(userText: string, issueUrl: string, error?: unknown): Promise<void> {
|
|
const sentryConfig = SdkConfig.getObject("sentry");
|
|
if (!sentryConfig) return;
|
|
|
|
const captureContext = {
|
|
contexts: await getContexts(),
|
|
extra: {
|
|
user_text: userText,
|
|
issue_url: issueUrl,
|
|
},
|
|
};
|
|
|
|
// If there's no error and no issueUrl, the report will just produce non-grouped noise in Sentry, so don't
|
|
// upload it
|
|
if (error) {
|
|
Sentry.captureException(error, captureContext);
|
|
} else if (issueUrl) {
|
|
Sentry.captureMessage(`Issue: ${issueUrl}`, captureContext);
|
|
}
|
|
}
|
|
|
|
export function setSentryUser(mxid: string): void {
|
|
if (!SdkConfig.get().sentry || !SettingsStore.getValue("automaticErrorReporting")) return;
|
|
Sentry.setUser({ username: mxid });
|
|
}
|
|
|
|
export async function initSentry(sentryConfig: IConfigOptions["sentry"]): Promise<void> {
|
|
if (!sentryConfig) return;
|
|
// Only enable Integrations.GlobalHandlers, which hooks uncaught exceptions, if automaticErrorReporting is true
|
|
const integrations: Integration[] = [
|
|
Sentry.inboundFiltersIntegration(),
|
|
Sentry.functionToStringIntegration(),
|
|
Sentry.breadcrumbsIntegration(),
|
|
Sentry.httpContextIntegration(),
|
|
Sentry.dedupeIntegration(),
|
|
];
|
|
|
|
if (SettingsStore.getValue("automaticErrorReporting")) {
|
|
integrations.push(Sentry.globalHandlersIntegration({ onerror: false, onunhandledrejection: true }));
|
|
integrations.push(Sentry.browserApiErrorsIntegration());
|
|
}
|
|
|
|
Sentry.init({
|
|
dsn: sentryConfig.dsn,
|
|
release: process.env.VERSION,
|
|
environment: sentryConfig.environment,
|
|
defaultIntegrations: false,
|
|
autoSessionTracking: false,
|
|
integrations,
|
|
// Set to 1.0 which is reasonable if we're only submitting Rageshakes; will need to be set < 1.0
|
|
// if we collect more frequently.
|
|
tracesSampleRate: 1.0,
|
|
});
|
|
}
|
|
|
|
window.mxSendSentryReport = sendSentryReport;
|