Filter settings exported when rageshaking (#30236)

* Submit filtered settings to rageshakes and sentry.

* Add flag to omit some settings from being exported.

* Hide user timezone

* Hide recent searches and media event ids

* Lint

* use better wording

* lint

* Prevent language from being sent

* Add tests to check keys are prevented from being uploaded.

* don't export invite rules

* Update tests
This commit is contained in:
Will Hunt
2025-07-02 09:03:31 +01:00
committed by GitHub
parent 99f7656d09
commit e1fea71c97
7 changed files with 107 additions and 34 deletions

View File

@@ -171,7 +171,7 @@ async function collectSynapseSpecific(client: MatrixClient, body: FormData): Pro
} catch { } catch {
try { try {
// If that fails we'll hit any endpoint and look at the server response header // If that fails we'll hit any endpoint and look at the server response header
const res = await window.fetch(client.http.getUrl("/login"), { const res = await fetch(client.http.getUrl("/login"), {
method: "GET", method: "GET",
mode: "cors", mode: "cors",
}); });
@@ -257,7 +257,7 @@ export function collectSettings(body: FormData): void {
body.append("lowBandwidth", "enabled"); body.append("lowBandwidth", "enabled");
} }
body.append("mx_local_settings", localStorage.getItem("mx_local_settings")!); body.append("mx_local_settings", SettingsStore.exportForRageshake());
} }
/** /**

View File

@@ -141,7 +141,7 @@ async function getCryptoContext(client: MatrixClient): Promise<CryptoContext> {
function getDeviceContext(client: MatrixClient): DeviceContext { function getDeviceContext(client: MatrixClient): DeviceContext {
const result: DeviceContext = { const result: DeviceContext = {
device_id: client?.deviceId ?? undefined, device_id: client?.deviceId ?? undefined,
mx_local_settings: localStorage.getItem("mx_local_settings"), mx_local_settings: SettingsStore.exportForRageshake(),
}; };
if (window.Modernizr) { if (window.Modernizr) {

View File

@@ -173,6 +173,11 @@ export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
// Whether the setting should have a warning sign in the microcopy // Whether the setting should have a warning sign in the microcopy
shouldWarn?: boolean; shouldWarn?: boolean;
/**
* Whether the setting should be exported in a rageshake report.
*/
shouldExportToRageshake?: boolean;
} }
export interface IFeature extends Omit<IBaseSetting<boolean>, "isFeature"> { export interface IFeature extends Omit<IBaseSetting<boolean>, "isFeature"> {
@@ -441,6 +446,8 @@ export const SETTINGS: Settings = {
controller: new InviteRulesConfigController(), controller: new InviteRulesConfigController(),
supportedLevels: [SettingLevel.ACCOUNT], supportedLevels: [SettingLevel.ACCOUNT],
default: InviteRulesConfigController.default, default: InviteRulesConfigController.default,
// Contains server names
shouldExportToRageshake: false,
}, },
"feature_report_to_moderators": { "feature_report_to_moderators": {
isFeature: true, isFeature: true,
@@ -503,10 +510,14 @@ export const SETTINGS: Settings = {
"mjolnirRooms": { "mjolnirRooms": {
supportedLevels: [SettingLevel.ACCOUNT], supportedLevels: [SettingLevel.ACCOUNT],
default: [], default: [],
// Contains room IDs
shouldExportToRageshake: false,
}, },
"mjolnirPersonalRoom": { "mjolnirPersonalRoom": {
supportedLevels: [SettingLevel.ACCOUNT], supportedLevels: [SettingLevel.ACCOUNT],
default: null, default: null,
// Contains room ID
shouldExportToRageshake: false,
}, },
"feature_html_topic": { "feature_html_topic": {
isFeature: true, isFeature: true,
@@ -797,6 +808,8 @@ export const SETTINGS: Settings = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("settings|preferences|user_timezone"), displayName: _td("settings|preferences|user_timezone"),
default: "", default: "",
// Location leak
shouldExportToRageshake: false,
}, },
"userTimezonePublish": { "userTimezonePublish": {
// This is per-device so you can avoid having devices overwrite each other. // This is per-device so you can avoid having devices overwrite each other.
@@ -913,6 +926,8 @@ export const SETTINGS: Settings = {
"custom_themes": { "custom_themes": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: [], default: [],
// Potential privacy leak via theme origin
shouldExportToRageshake: false,
}, },
"use_system_theme": { "use_system_theme": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
@@ -974,26 +989,36 @@ export const SETTINGS: Settings = {
"language": { "language": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: "en", default: "en",
// For privacy
shouldExportToRageshake: false,
}, },
"breadcrumb_rooms": { "breadcrumb_rooms": {
// not really a setting // not really a setting
supportedLevels: [SettingLevel.ACCOUNT], supportedLevels: [SettingLevel.ACCOUNT],
default: [], default: [],
// Contains joined rooms
shouldExportToRageshake: false,
}, },
"recent_emoji": { "recent_emoji": {
// not really a setting // not really a setting
supportedLevels: [SettingLevel.ACCOUNT], supportedLevels: [SettingLevel.ACCOUNT],
default: [], default: [],
// For privacy
shouldExportToRageshake: false,
}, },
"SpotlightSearch.recentSearches": { "SpotlightSearch.recentSearches": {
// not really a setting // not really a setting
supportedLevels: [SettingLevel.ACCOUNT], supportedLevels: [SettingLevel.ACCOUNT],
default: [], // list of room IDs, most recent first default: [], // list of room IDs, most recent first
// For privacy
shouldExportToRageshake: false,
}, },
"showMediaEventIds": { "showMediaEventIds": {
// not really a setting // not really a setting
supportedLevels: [SettingLevel.DEVICE], supportedLevels: [SettingLevel.DEVICE],
default: {}, // List of events => is visible default: {}, // List of events => is visible
// Exports event IDs
shouldExportToRageshake: false,
}, },
"SpotlightSearch.showNsfwPublicRooms": { "SpotlightSearch.showNsfwPublicRooms": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
@@ -1003,6 +1028,8 @@ export const SETTINGS: Settings = {
"room_directory_servers": { "room_directory_servers": {
supportedLevels: [SettingLevel.ACCOUNT], supportedLevels: [SettingLevel.ACCOUNT],
default: [], default: [],
// Contains connected servers for user
shouldExportToRageshake: false,
}, },
"integrationProvisioning": { "integrationProvisioning": {
supportedLevels: [SettingLevel.ACCOUNT], supportedLevels: [SettingLevel.ACCOUNT],
@@ -1012,6 +1039,7 @@ export const SETTINGS: Settings = {
supportedLevels: [SettingLevel.ROOM_ACCOUNT, SettingLevel.ROOM_DEVICE], supportedLevels: [SettingLevel.ROOM_ACCOUNT, SettingLevel.ROOM_DEVICE],
supportedLevelsAreOrdered: true, supportedLevelsAreOrdered: true,
default: {}, // none allowed default: {}, // none allowed
shouldExportToRageshake: false,
}, },
// Legacy, kept around for transitionary purposes // Legacy, kept around for transitionary purposes
"analyticsOptIn": { "analyticsOptIn": {
@@ -1086,6 +1114,8 @@ export const SETTINGS: Settings = {
"notificationSound": { "notificationSound": {
supportedLevels: LEVELS_ROOM_OR_ACCOUNT, supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
default: false, default: false,
// Contains personal information in file name
shouldExportToRageshake: false,
}, },
"notificationBodyEnabled": { "notificationBodyEnabled": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
@@ -1112,6 +1142,8 @@ export const SETTINGS: Settings = {
allow: [], allow: [],
deny: [], deny: [],
}, },
// Expses widget information
shouldExportToRageshake: false,
}, },
"breadcrumbs": { "breadcrumbs": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
@@ -1201,6 +1233,8 @@ export const SETTINGS: Settings = {
// deprecated // deprecated
supportedLevels: LEVELS_ROOM_OR_ACCOUNT, supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
default: {}, default: {},
// Sensitive information in widget ID
shouldExportToRageshake: false,
}, },
"Widgets.layout": { "Widgets.layout": {
supportedLevels: LEVELS_ROOM_OR_ACCOUNT, supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
@@ -1275,6 +1309,8 @@ export const SETTINGS: Settings = {
"activeCallRoomIds": { "activeCallRoomIds": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: [], default: [],
// Contains room IDs
shouldExportToRageshake: false,
}, },
/** /**
* Enable or disable the release announcement feature * Enable or disable the release announcement feature

View File

@@ -883,6 +883,21 @@ export default class SettingsStore {
logger.log(`--- END DEBUG`); logger.log(`--- END DEBUG`);
} }
/**
* Export all settings as a JSON object, except for settings
* blocked from being exported by `shouldExportToRageshake`.
* @returns Settings as a JSON object string.
*/
public static exportForRageshake(): string {
const settingMap: Record<string, unknown> = {};
for (const settingKey of (Object.keys(SETTINGS) as SettingKey[]).filter(
(s) => SETTINGS[s].shouldExportToRageshake !== false,
)) {
settingMap[settingKey] = SettingsStore.getValue(settingKey);
}
return JSON.stringify(settingMap);
}
private static getHandler(settingName: SettingKey, level: SettingLevel): SettingsHandler | null { private static getHandler(settingName: SettingKey, level: SettingLevel): SettingsHandler | null {
const handlers = SettingsStore.getHandlers(settingName); const handlers = SettingsStore.getHandlers(settingName);
if (!handlers[level]) return null; if (!handlers[level]) return null;

View File

@@ -16,6 +16,7 @@ import BugReportDialog, {
} from "../../../../../src/components/views/dialogs/BugReportDialog"; } from "../../../../../src/components/views/dialogs/BugReportDialog";
import SdkConfig from "../../../../../src/SdkConfig"; import SdkConfig from "../../../../../src/SdkConfig";
import { type ConsoleLogger } from "../../../../../src/rageshake/rageshake"; import { type ConsoleLogger } from "../../../../../src/rageshake/rageshake";
import SettingsStore from "../../../../../src/settings/SettingsStore";
const BUG_REPORT_URL = "https://example.org/submit"; const BUG_REPORT_URL = "https://example.org/submit";
@@ -32,6 +33,16 @@ describe("BugReportDialog", () => {
bug_report_endpoint_url: BUG_REPORT_URL, bug_report_endpoint_url: BUG_REPORT_URL,
}); });
const originalGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, ...args) => {
// These settings rely on a controller that creates an AudioContext in
// order to test whether the setting can be enabled. For the sake of this test, disable that.
if (settingName === "notificationsEnabled" || settingName === "notificationBodyEnabled") {
return true;
}
return originalGetValue(settingName, ...args);
});
const mockConsoleLogger = { const mockConsoleLogger = {
flush: jest.fn(), flush: jest.fn(),
consume: jest.fn(), consume: jest.fn(),
@@ -55,6 +66,7 @@ describe("BugReportDialog", () => {
}); });
afterEach(() => { afterEach(() => {
jest.restoreAllMocks();
SdkConfig.reset(); SdkConfig.reset();
fetchMock.restore(); fetchMock.restore();
}); });

View File

@@ -14,7 +14,7 @@ import SdkConfig from "../../../src/SdkConfig";
import { SettingLevel } from "../../../src/settings/SettingLevel"; import { SettingLevel } from "../../../src/settings/SettingLevel";
import SettingsStore from "../../../src/settings/SettingsStore"; import SettingsStore from "../../../src/settings/SettingsStore";
import { mkStubRoom, mockPlatformPeg, stubClient } from "../../test-utils"; import { mkStubRoom, mockPlatformPeg, stubClient } from "../../test-utils";
import { type SettingKey } from "../../../src/settings/Settings.tsx"; import { SETTINGS, type SettingKey } from "../../../src/settings/Settings.tsx";
import MatrixClientBackedController from "../../../src/settings/controllers/MatrixClientBackedController.ts"; import MatrixClientBackedController from "../../../src/settings/controllers/MatrixClientBackedController.ts";
const TEST_DATA = [ const TEST_DATA = [
@@ -55,6 +55,7 @@ describe("SettingsStore", () => {
beforeEach(() => { beforeEach(() => {
SdkConfig.reset(); SdkConfig.reset();
SettingsStore.reset();
}); });
describe("getValueAt", () => { describe("getValueAt", () => {
@@ -82,6 +83,16 @@ describe("SettingsStore", () => {
}); });
}); });
describe("exportForRageshake", () => {
it("should not export settings marked as non-exportable", async () => {
await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, "Europe/London");
const values = JSON.parse(SettingsStore.exportForRageshake()) as Record<SettingKey, unknown>;
for (const exportedKey of Object.keys(values) as SettingKey[]) {
expect(SETTINGS[exportedKey].shouldExportToRageshake).not.toEqual(false);
}
});
});
describe("runMigrations", () => { describe("runMigrations", () => {
let client: MatrixClient; let client: MatrixClient;
let room: Room; let room: Room;

View File

@@ -22,6 +22,7 @@ import { collectBugReport } from "../../src/rageshake/submit-rageshake";
import SettingsStore from "../../src/settings/SettingsStore"; import SettingsStore from "../../src/settings/SettingsStore";
import { type ConsoleLogger } from "../../src/rageshake/rageshake"; import { type ConsoleLogger } from "../../src/rageshake/rageshake";
import { type FeatureSettingKey, type SettingKey } from "../../src/settings/Settings.tsx"; import { type FeatureSettingKey, type SettingKey } from "../../src/settings/Settings.tsx";
import { SettingLevel } from "../../src/settings/SettingLevel.ts";
describe("Rageshakes", () => { describe("Rageshakes", () => {
const RUST_CRYPTO_VERSION = "Rust SDK 0.7.0 (691ec63), Vodozemac 0.5.0"; const RUST_CRYPTO_VERSION = "Rust SDK 0.7.0 (691ec63), Vodozemac 0.5.0";
@@ -35,6 +36,8 @@ describe("Rageshakes", () => {
onlyData: true, onlyData: true,
}, },
); );
let windowSpy: jest.SpyInstance;
let mockWindow: Mocked<Window>;
beforeEach(() => { beforeEach(() => {
mockClient = getMockClientWithEventEmitter({ mockClient = getMockClientWithEventEmitter({
@@ -50,30 +53,24 @@ describe("Rageshakes", () => {
ed25519: "", ed25519: "",
curve25519: "", curve25519: "",
}); });
mockWindow = {
matchMedia: jest.fn().mockReturnValue({ matches: false }),
navigator: {
userAgent: "",
},
} as unknown as Mocked<Window>;
// @ts-ignore - We just need partial mock
windowSpy = jest.spyOn(global, "window", "get").mockReturnValue(mockWindow);
fetchMock.restore(); fetchMock.restore();
fetchMock.catch(404); fetchMock.catch(404);
}); });
afterEach(() => {
windowSpy.mockRestore();
});
describe("Basic Information", () => { describe("Basic Information", () => {
let mockWindow: Mocked<Window>;
let windowSpy: jest.SpyInstance;
beforeEach(() => {
mockWindow = {
matchMedia: jest.fn().mockReturnValue({ matches: false }),
navigator: {
userAgent: "",
},
} as unknown as Mocked<Window>;
// @ts-ignore - We just need partial mock
windowSpy = jest.spyOn(global, "window", "get").mockReturnValue(mockWindow);
});
afterEach(() => {
windowSpy.mockRestore();
});
it("should include app version", async () => { it("should include app version", async () => {
mockPlatformPeg({ getAppVersion: jest.fn().mockReturnValue("1.11.58") }); mockPlatformPeg({ getAppVersion: jest.fn().mockReturnValue("1.11.58") });
@@ -376,6 +373,10 @@ describe("Rageshakes", () => {
describe("Settings Store", () => { describe("Settings Store", () => {
const mockSettingsStore = mocked(SettingsStore); const mockSettingsStore = mocked(SettingsStore);
afterEach(() => {
jest.restoreAllMocks();
});
it("should collect labs from settings store", async () => { it("should collect labs from settings store", async () => {
const someFeatures = [ const someFeatures = [
"feature_video_rooms", "feature_video_rooms",
@@ -430,6 +431,7 @@ describe("Rageshakes", () => {
afterEach(() => { afterEach(() => {
navigatorSpy.mockRestore(); navigatorSpy.mockRestore();
SettingsStore.reset();
}); });
it("should collect navigator storage persisted", async () => { it("should collect navigator storage persisted", async () => {
@@ -488,6 +490,7 @@ describe("Rageshakes", () => {
}; };
const disabledFeatures = ["cssanimations", "d0", "d1"]; const disabledFeatures = ["cssanimations", "d0", "d1"];
const mockWindow = { const mockWindow = {
matchMedia: jest.fn().mockReturnValue({ matches: false }),
Modernizr: { Modernizr: {
...allFeatures, ...allFeatures,
}, },
@@ -503,20 +506,16 @@ describe("Rageshakes", () => {
}); });
it("should collect localstorage settings", async () => { it("should collect localstorage settings", async () => {
const localSettings = { await SettingsStore.setValue("language", null, SettingLevel.DEVICE, "fr");
language: "fr", await SettingsStore.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
showHiddenEventsInTimeline: true, await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, "Europe/London");
activeCallRoomIds: [], await SettingsStore.setValue("activeCallRoomIds", null, SettingLevel.DEVICE, []);
};
const spy = jest.spyOn(window.localStorage.__proto__, "getItem").mockImplementation((key) => {
return JSON.stringify(localSettings);
});
const formData = await collectBugReport(); const formData = await collectBugReport();
expect(formData.get("mx_local_settings")).toBe(JSON.stringify(localSettings)); const settingDataJSON = formData.get("mx_local_settings");
expect(settingDataJSON).not.toBeNull();
spy.mockRestore(); const settingsData = JSON.parse(settingDataJSON as string);
expect(settingsData.showHiddenEventsInTimeline).toEqual(true);
}); });
it("should collect logs", async () => { it("should collect logs", async () => {