233 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			233 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
/*
 | 
						|
Copyright 2021 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 * as Sentry from "@sentry/browser";
 | 
						|
import { MatrixClient } from "matrix-js-sdk/src/client";
 | 
						|
 | 
						|
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 = {
 | 
						|
    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> {
 | 
						|
    // TODO: make this work with rust crypto
 | 
						|
    if (!client.isCryptoEnabled() || !client.crypto) {
 | 
						|
        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_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): 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?: Error): 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 = [
 | 
						|
        new Sentry.Integrations.InboundFilters(),
 | 
						|
        new Sentry.Integrations.FunctionToString(),
 | 
						|
        new Sentry.Integrations.Breadcrumbs(),
 | 
						|
        new Sentry.Integrations.HttpContext(),
 | 
						|
        new Sentry.Integrations.Dedupe(),
 | 
						|
    ];
 | 
						|
 | 
						|
    if (SettingsStore.getValue("automaticErrorReporting")) {
 | 
						|
        integrations.push(new Sentry.Integrations.GlobalHandlers({ onerror: false, onunhandledrejection: true }));
 | 
						|
        integrations.push(new Sentry.Integrations.TryCatch());
 | 
						|
    }
 | 
						|
 | 
						|
    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;
 |