diff --git a/packages/shared/src/services/FeatureToggleService.spec.ts b/packages/shared/src/services/FeatureToggleService.spec.ts index 412c5d79f..81dcf1bdb 100644 --- a/packages/shared/src/services/FeatureToggleService.spec.ts +++ b/packages/shared/src/services/FeatureToggleService.spec.ts @@ -28,6 +28,12 @@ jest.mock('../config/featureToggles', () => ({ }), })) +const mockWarnLog = jest.fn() +jest.mock('../utils/general/log', () => ({ + ...jest.requireActual('../utils/general/log'), + warnLog: (...args: unknown[]) => mockWarnLog(...args), +})) + describe('FeatureToggleService', () => { let service: InstanceType< (typeof import('./FeatureToggleService'))['featureToggleService'] extends infer S @@ -43,6 +49,7 @@ describe('FeatureToggleService', () => { delete process.env.FLAGS mockFindUnique.mockReset() mockUpsert.mockReset() + mockWarnLog.mockReset() const mod = await import('./FeatureToggleService') service = mod.featureToggleService as unknown as typeof service @@ -87,7 +94,7 @@ describe('FeatureToggleService', () => { expect(result).toBeNull() }) - it('returns null when db throws', async () => { + it('returns null and logs a warning when db throws', async () => { mockFindUnique.mockRejectedValue(new Error('DB connection failed')) const result = await ( service as unknown as { @@ -95,6 +102,14 @@ describe('FeatureToggleService', () => { } ).getDbGlobalOverride('DOWNLOAD_AUDIO') expect(result).toBeNull() + // #1286 B3: the DB failure must be surfaced, not swallowed silently. + expect(mockWarnLog).toHaveBeenCalledTimes(1) + expect(mockWarnLog).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.any(Error), + data: { name: 'DOWNLOAD_AUDIO' }, + }), + ) }) }) diff --git a/packages/shared/src/services/FeatureToggleService.ts b/packages/shared/src/services/FeatureToggleService.ts index 8988a96bc..383f7b74f 100644 --- a/packages/shared/src/services/FeatureToggleService.ts +++ b/packages/shared/src/services/FeatureToggleService.ts @@ -5,6 +5,7 @@ import type { } from '../types/featureToggle' import { getFeatureToggleConfig } from '../config/featureToggles' import { getPrismaClient } from '../utils/database/prismaClient' +import { warnLog } from '../utils/general/log' /** Manages feature toggles with multi-layer provider support (database, environment). */ class FeatureToggleService { @@ -39,7 +40,16 @@ class FeatureToggleService { select: { enabled: true }, }) return row?.enabled ?? null - } catch { + } catch (error) { + // Fail open to the environment/config fallback, but surface the + // failure: a swallowed-to-null DB error here was indistinguishable + // from "no override set", hiding a persistent outage (#1286 B3). + warnLog({ + message: + 'Failed to read global feature toggle override; falling back to config default', + error, + data: { name }, + }) return null } }