diff --git a/src/components/dashboard/Settings.tsx b/src/components/dashboard/Settings.tsx
index 07d70bea..9aba375c 100644
--- a/src/components/dashboard/Settings.tsx
+++ b/src/components/dashboard/Settings.tsx
@@ -9,6 +9,7 @@ import { ALERT_RULE_TYPE, ALERT_CHANNEL } from "../../lib/alerts";
import PluginRegistryView from "./PluginRegistryView";
import DataExport from "./DataExport";
import LanguageSettings from "./LanguageSettings";
+import { revokeSentryConsent } from "../../utils/monitoring";
const SESSION_API_KEY = 'stellar_custom_api_key';
@@ -404,6 +405,39 @@ export default function Settings() {
+
Extensions
diff --git a/src/utils/__tests__/monitoring.consent.test.ts b/src/utils/__tests__/monitoring.consent.test.ts
new file mode 100644
index 00000000..28df113d
--- /dev/null
+++ b/src/utils/__tests__/monitoring.consent.test.ts
@@ -0,0 +1,74 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import * as Sentry from '@sentry/react';
+import * as preferences from '../preferences';
+
+vi.mock('@sentry/react', () => ({
+ init: vi.fn(),
+ getClient: vi.fn(() => ({
+ close: vi.fn(),
+ })),
+ browserTracingIntegration: vi.fn(),
+ replayIntegration: vi.fn(),
+ breadcrumbsIntegration: vi.fn(),
+ withScope: vi.fn(),
+ captureException: vi.fn(),
+ setUser: vi.fn(),
+ startSpan: vi.fn(),
+ ErrorBoundary: vi.fn(),
+}));
+
+vi.mock('../preferences', () => ({
+ loadPreferences: vi.fn(),
+}));
+
+vi.mock('../logger', () => ({
+ createLogger: vi.fn(() => ({
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
+ })),
+}));
+
+describe('monitoring Sentry consent', () => {
+ beforeEach(() => {
+ vi.resetModules();
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('initializes Sentry when diagnosticsConsent is true', async () => {
+ vi.mocked(preferences.loadPreferences).mockReturnValue({ diagnosticsConsent: true });
+
+ const monitoring = await import('../monitoring');
+ monitoring.initMonitoring({ sentryDsn: 'http://test-dsn@sentry.io/1' });
+
+ expect(Sentry.init).toHaveBeenCalled();
+ const initArgs = vi.mocked(Sentry.init).mock.calls[0][0];
+ expect(initArgs?.dsn).toBe('http://test-dsn@sentry.io/1');
+ });
+
+ it('does not initialize Sentry when diagnosticsConsent is false (defaults to no consent)', async () => {
+ vi.mocked(preferences.loadPreferences).mockReturnValue({ diagnosticsConsent: false });
+
+ const monitoring = await import('../monitoring');
+ monitoring.initMonitoring({ sentryDsn: 'http://test-dsn@sentry.io/1' });
+
+ expect(Sentry.init).not.toHaveBeenCalled();
+ });
+
+ it('closes Sentry client when consent is revoked', async () => {
+ vi.mocked(preferences.loadPreferences).mockReturnValue({ diagnosticsConsent: true });
+
+ const monitoring = await import('../monitoring');
+ monitoring.revokeSentryConsent();
+
+ const getClient = vi.mocked(Sentry.getClient);
+ expect(getClient).toHaveBeenCalled();
+
+ const client = getClient();
+ expect(client?.close).toHaveBeenCalledWith(2000);
+ });
+});
diff --git a/src/utils/monitoring.ts b/src/utils/monitoring.ts
index 570c9b6f..74ac1628 100644
--- a/src/utils/monitoring.ts
+++ b/src/utils/monitoring.ts
@@ -24,6 +24,7 @@ import {
} from '../lib/errorReporting';
import { initPerformanceMonitoring } from '../lib/performance';
import { createLogger } from './logger';
+import { loadPreferences } from './preferences';
export {
collectHealthSnapshot,
@@ -65,6 +66,12 @@ let _initialised = false;
// ─── Sentry init ──────────────────────────────────────────────────────────────
function initialiseSentry(cfg: MonitoringConfig): void {
+ const prefs = loadPreferences();
+ if (prefs.diagnosticsConsent !== true) {
+ logger.info('Sentry initialization skipped: user has not granted diagnostics consent.');
+ return;
+ }
+
if (!cfg.sentryDsn) {
logger.warn('Sentry DSN not set – error tracking disabled.', { env: cfg.environment });
return;
@@ -261,6 +268,15 @@ export function initMonitoring(userConfig: Partial = {}): void
logger.info('Monitoring stack initialised', { env: cfg.environment });
}
+
+export function revokeSentryConsent(): void {
+ logger.info('User revoked diagnostics consent, closing Sentry client.');
+ const client = Sentry.getClient();
+ if (client) {
+ client.close(2000); // 2 second flush then close
+ }
+}
+
// ─── Sentry user context helpers ─────────────────────────────────────────────
/**
@@ -402,4 +418,5 @@ export default {
withSpan,
captureError,
SentryErrorBoundary,
+ revokeSentryConsent,
};
diff --git a/src/utils/preferences.js b/src/utils/preferences.js
index 8d423ef1..d1b5b361 100644
--- a/src/utils/preferences.js
+++ b/src/utils/preferences.js
@@ -5,6 +5,7 @@ export const DEFAULT_PREFERENCES = {
showAdvancedPanels: true,
autoRefreshDashboard: true,
defaultSearchScope: "all",
+ diagnosticsConsent: false,
};
export function loadPreferences() {