Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/crates/adapters/ai-adapters/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::sync::atomic::{AtomicBool, Ordering};

static INCLUDE_SENSITIVE_DIAGNOSTICS: AtomicBool = AtomicBool::new(true);
static INCLUDE_SENSITIVE_DIAGNOSTICS: AtomicBool = AtomicBool::new(false);

pub fn set_include_sensitive_diagnostics(enabled: bool) {
INCLUDE_SENSITIVE_DIAGNOSTICS.store(enabled, Ordering::Relaxed);
Expand Down
8 changes: 4 additions & 4 deletions src/crates/contracts/config-contracts/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ pub struct AppLoggingConfig {
/// Allowed values: trace, debug, info, warn, error, off.
pub level: String,
/// Whether diagnostic logs may include sensitive troubleshooting payloads.
#[serde(default = "default_true")]
#[serde(default)]
pub include_sensitive_diagnostics: bool,
/// Whether the local UI records detailed Flow Chat viewport diagnostics.
#[serde(default)]
Expand Down Expand Up @@ -1946,7 +1946,7 @@ impl Default for AppLoggingConfig {
Self {
// Set to Debug in early development for easier diagnostics
level: "debug".to_string(),
include_sensitive_diagnostics: true,
include_sensitive_diagnostics: false,
flow_chat_diagnostics: false,
model_exchange_tracing: ModelExchangeTracingConfig::default(),
}
Expand Down Expand Up @@ -3743,13 +3743,13 @@ mod tests {
}

#[test]
fn app_logging_defaults_to_sensitive_diagnostics_enabled() {
fn app_logging_defaults_to_sensitive_diagnostics_disabled() {
let config: AppLoggingConfig = serde_json::from_value(serde_json::json!({
"level": "trace"
}))
.expect("logging config without sensitive preference should deserialize");

assert!(config.include_sensitive_diagnostics);
assert!(!config.include_sensitive_diagnostics);
assert!(!config.flow_chat_diagnostics);
assert_eq!(
config.model_exchange_tracing.mode,
Expand Down
9 changes: 9 additions & 0 deletions src/web-ui/src/infrastructure/config/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ Follow `src/web-ui/AGENTS.md` for shared frontend rules.

## Focused verification

For logging preference changes and sensitive-data redaction before startup reads finish:

```bash
pnpm --dir src/web-ui run test:run src/infrastructure/config/services/FrontendLogLevelSync.test.ts src/infrastructure/config/services/FrontendLogLevelSync.privacy.test.ts src/shared/utils/logger.test.ts src/flow_chat/services/EventBatcher.test.ts
```

These fixtures exercise the real frontend logger and event batcher with delayed
configuration reads; they do not establish live remote transport behavior.

For loading states, warm snapshots, asynchronous reads, and device isolation:

```bash
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const configApiMocks = vi.hoisted(() => ({
getConfigs: vi.fn(),
getRuntimeLoggingInfo: vi.fn(),
}));

vi.mock('@/infrastructure/api/service-api/ConfigAPI', () => ({
configAPI: configApiMocks,
}));

vi.mock('@tauri-apps/plugin-log', () => ({
trace: vi.fn(async () => {}),
debug: vi.fn(async () => {}),
info: vi.fn(async () => {}),
warn: vi.fn(async () => {}),
error: vi.fn(async () => {}),
}));

vi.mock('@/infrastructure/diagnostics/flowChatDiagnostics', () => ({
setFlowChatDiagnosticsEnabled: vi.fn(),
}));

describe('FrontendLogLevelSync startup privacy', () => {
beforeEach(() => {
vi.resetModules();
vi.resetAllMocks();
vi.stubGlobal('__OPENBITFUN_BOOTSTRAP_LOG_LEVEL__', 'trace');
vi.stubGlobal('requestAnimationFrame', () => 1);
vi.stubGlobal('cancelAnimationFrame', () => {});
});

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});

it.each([undefined, false, true])(
'redacts startup trace logs until saved preference %s is loaded',
async (savedPreference) => {
const traceOutput = vi.spyOn(console, 'debug').mockImplementation(() => {});
configApiMocks.getConfigs.mockResolvedValue({
'app.logging.level': 'trace',
...(savedPreference === undefined
? {}
: { 'app.logging.include_sensitive_diagnostics': savedPreference }),
});
let finishRuntimeRead!: (value: { effectiveLevel: string }) => void;
configApiMocks.getRuntimeLoggingInfo.mockReturnValue(new Promise((resolve) => {
finishRuntimeRead = resolve;
}));

const { initializeFrontendLogLevelSync } = await import('./FrontendLogLevelSync');
const { EventBatcher } = await import('@/flow_chat/services/EventBatcher');
const initialization = initializeFrontendLogLevelSync();
const batcher = new EventBatcher({ onFlush: vi.fn() });
const flushToolEvent = () => {
batcher.add('tool:params:session:call:none', {
toolEvent: {
event_type: 'ParamsPartial',
params: 'private-source-content',
},
});
batcher.flushNow();
};

try {
flushToolEvent();
expect(traceOutput).toHaveBeenCalled();
expect(JSON.stringify(traceOutput.mock.calls)).not.toContain('private-source-content');

finishRuntimeRead({ effectiveLevel: 'trace' });
await initialization;
traceOutput.mockClear();
flushToolEvent();
expect(traceOutput).toHaveBeenCalled();
const output = JSON.stringify(traceOutput.mock.calls);
if (savedPreference === true) {
expect(output).toContain('private-source-content');
} else {
expect(output).not.toContain('private-source-content');
}
} finally {
finishRuntimeRead({ effectiveLevel: 'trace' });
await initialization;
batcher.destroy();
}
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const configApiMocks = vi.hoisted(() => ({
getRuntimeLoggingInfo: vi.fn(),
}));

const configManagerMocks = vi.hoisted(() => ({
onConfigChange: vi.fn(),
}));

const loggerMocks = vi.hoisted(() => ({
getLevel: vi.fn(),
setLevel: vi.fn(),
Expand All @@ -20,6 +24,10 @@ vi.mock('@/infrastructure/api/service-api/ConfigAPI', () => ({
configAPI: configApiMocks,
}));

vi.mock('./ConfigManager', () => ({
configManager: configManagerMocks,
}));

vi.mock('@/shared/utils/logger', () => ({
LogLevel: {
TRACE: 0,
Expand Down Expand Up @@ -99,4 +107,40 @@ describe('FrontendLogLevelSync startup reads', () => {
expect(loggerMocks.setIncludeSensitiveDiagnostics).toHaveBeenCalledWith(true);
expect(loggerMocks.setFlowChatDiagnosticsEnabled).toHaveBeenCalledWith(false);
});

it('defaults sensitive diagnostics to off when the saved value is missing', async () => {
configApiMocks.getConfigs.mockResolvedValueOnce({
[LOGGING_LEVEL_PATH]: 'debug',
[FLOW_CHAT_DIAGNOSTICS_PATH]: false,
});
configApiMocks.getRuntimeLoggingInfo.mockResolvedValueOnce({ effectiveLevel: 'warn' });

const { initializeFrontendLogLevelSync } = await importSyncModule();
await initializeFrontendLogLevelSync();

expect(loggerMocks.setLevel).toHaveBeenCalledWith(1);
expect(loggerMocks.setIncludeSensitiveDiagnostics).toHaveBeenCalledWith(false);
expect(loggerMocks.setFlowChatDiagnosticsEnabled).toHaveBeenCalledWith(false);
});
});

describe('FrontendLogLevelSync config changes', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it.each([undefined, null, 'true', false, true])(
'requires an explicit boolean opt-in for changed value %s',
async (newValue) => {
const { installFrontendLogLevelConfigWatcher } = await importSyncModule();
await installFrontendLogLevelConfigWatcher();

const onConfigChange = configManagerMocks.onConfigChange.mock.calls[0][0];
onConfigChange(LOGGING_INCLUDE_SENSITIVE_PATH, true, newValue);

expect(loggerMocks.setIncludeSensitiveDiagnostics).toHaveBeenCalledExactlyOnceWith(
newValue === true,
);
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async function resolveInitialLoggingSettings(): Promise<InitialLoggingSettings>
includeSensitiveDiagnostics:
typeof configs[LOGGING_INCLUDE_SENSITIVE_PATH] === 'boolean'
? configs[LOGGING_INCLUDE_SENSITIVE_PATH]
: true,
: false,
flowChatDiagnostics: configs[FLOW_CHAT_DIAGNOSTICS_PATH] === true,
};
}
Expand All @@ -115,7 +115,7 @@ async function resolveInitialLoggingSettings(): Promise<InitialLoggingSettings>
includeSensitiveDiagnostics:
typeof configs[LOGGING_INCLUDE_SENSITIVE_PATH] === 'boolean'
? configs[LOGGING_INCLUDE_SENSITIVE_PATH]
: true,
: false,
flowChatDiagnostics: configs[FLOW_CHAT_DIAGNOSTICS_PATH] === true,
};
}
Expand All @@ -125,7 +125,7 @@ async function resolveInitialLoggingSettings(): Promise<InitialLoggingSettings>
includeSensitiveDiagnostics:
typeof configs[LOGGING_INCLUDE_SENSITIVE_PATH] === 'boolean'
? configs[LOGGING_INCLUDE_SENSITIVE_PATH]
: true,
: false,
flowChatDiagnostics: configs[FLOW_CHAT_DIAGNOSTICS_PATH] === true,
};
}
Expand Down Expand Up @@ -163,7 +163,7 @@ export async function installFrontendLogLevelConfigWatcher(): Promise<void> {
}

if (path === LOGGING_INCLUDE_SENSITIVE_PATH) {
setIncludeSensitiveDiagnostics(typeof newValue === 'boolean' ? newValue : true);
setIncludeSensitiveDiagnostics(newValue === true);
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/shared/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const isTauri = typeof window !== 'undefined' && '__TAURI__' in window;
const isDev = import.meta.env?.DEV ?? process.env.NODE_ENV === 'development';

const CONSOLE_FORWARD_INSTALLED = '__openbitfun_console_forward_installed__';
let includeSensitiveDiagnostics = true;
let includeSensitiveDiagnostics = false;

declare global {
// Injected by the desktop WebView initialization script before the frontend bundle runs.
Expand Down
Loading