feat(reports): delete reports and configure retention (#3338)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-09 01:41:47 +02:00
1 parent 93d1f8af46
commit 82abd03a37
106 files changed
+1457 -170

No files matched your search

+22
View File
@@ -21,6 +21,8 @@ import {
let cachedConfig: MasterConfig | null = null;
const MAX_REPORT_RETENTION_DAYS = 36_500;
const DEFAULT_PASSKEY_ORIGINS = [
'https://fluxer.app',
'https://web.fluxer.app',
@@ -271,6 +273,11 @@ function defaultConfig(): MasterConfig {
attachment_decay_enabled: true,
deletion_grace_period_hours: 336,
inactivity_deletion_threshold_days: 365,
report_retention: {
days: 365,
resolved_days: null,
dry_run: false,
},
};
}
@@ -436,6 +443,20 @@ function validateApiWorkerConfig(config: MasterConfig): void {
}
}
function validateReportRetentionConfig(config: MasterConfig): void {
const retention = config.report_retention;
assertIntegerInRange(retention.days, 'FLUXER_REPORT_RETENTION_DAYS', 1, MAX_REPORT_RETENTION_DAYS);
if (retention.resolved_days !== null) {
assertIntegerInRange(
retention.resolved_days,
'FLUXER_RESOLVED_REPORT_RETENTION_DAYS',
1,
MAX_REPORT_RETENTION_DAYS,
);
}
assertBoolean(retention.dry_run, 'FLUXER_REPORT_RETENTION_DRY_RUN');
}
function validateStorageChangeFeedConfig(config: MasterConfig): void {
const feed = config.services.api?.storage_change_feed;
if (!feed?.enabled) {
@@ -594,6 +615,7 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
validatePostgresConfig(config);
validateApiWorkerConfig(config);
validateStorageChangeFeedConfig(config);
validateReportRetentionConfig(config);
validateCachePurgeConfig(config);
validateStoreBillingConfig(config);
validateReplyToEmail(config.integrations.email.reply_to_email);
+5
View File
@@ -323,4 +323,9 @@ export interface MasterConfig {
attachment_decay_enabled: boolean;
deletion_grace_period_hours: number;
inactivity_deletion_threshold_days: number;
report_retention: {
days: number;
resolved_days: number | null;
dry_run: boolean;
};
}
@@ -978,6 +978,60 @@ describe('ConfigLoader', () => {
});
});
describe('report retention settings', () => {
beforeEach(() => {
resetConfig();
clearFluxerEnv();
});
afterEach(() => {
resetConfig();
vi.unstubAllEnvs();
});
test('deletes reports for real after 365 days with no resolved-report rule by default', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.report_retention).toEqual({days: 365, resolved_days: null, dry_run: false});
});
test('reads both day settings and the dry run switch from the environment', async () => {
stubMinimalEnv({
FLUXER_REPORT_RETENTION_DAYS: '730',
FLUXER_RESOLVED_REPORT_RETENTION_DAYS: ' 90 ',
FLUXER_REPORT_RETENTION_DRY_RUN: 'TRUE',
});
const config = await loadConfig();
expect(config.report_retention).toEqual({days: 730, resolved_days: 90, dry_run: true});
});
test('an empty resolved-report setting leaves the rule off', async () => {
stubMinimalEnv({FLUXER_RESOLVED_REPORT_RETENTION_DAYS: '', FLUXER_REPORT_RETENTION_DRY_RUN: 'false'});
const config = await loadConfig();
expect(config.report_retention).toEqual({days: 365, resolved_days: null, dry_run: false});
});
test.each([
['FLUXER_REPORT_RETENTION_DAYS', '0', 'FLUXER_REPORT_RETENTION_DAYS must be an integer between 1 and 36500'],
['FLUXER_REPORT_RETENTION_DAYS', '36501', 'FLUXER_REPORT_RETENTION_DAYS must be an integer between 1 and 36500'],
['FLUXER_REPORT_RETENTION_DAYS', '1.5', 'FLUXER_REPORT_RETENTION_DAYS must be an integer, got "1.5"'],
[
'FLUXER_RESOLVED_REPORT_RETENTION_DAYS',
'0',
'FLUXER_RESOLVED_REPORT_RETENTION_DAYS must be an integer between 1 and 36500',
],
[
'FLUXER_RESOLVED_REPORT_RETENTION_DAYS',
'-30',
'FLUXER_RESOLVED_REPORT_RETENTION_DAYS must be an integer between 1 and 36500',
],
['FLUXER_REPORT_RETENTION_DRY_RUN', '1', 'FLUXER_REPORT_RETENTION_DRY_RUN must be true or false'],
])('refuses %s=%j', async (name, value, message) => {
stubMinimalEnv({[name]: value});
await expect(loadConfig()).rejects.toThrow(message);
});
});
describe('FLUXER_PUBLIC_ORIGIN', () => {
beforeEach(() => {
resetConfig();
@@ -97,8 +97,11 @@ describe('the shipped compose stack forwards settings from .env', () => {
expect(renamed).toEqual([]);
});
test('the worker is handed the report retention switch and gets no value when .env leaves it out', () => {
const name = 'FLUXER_REPORT_RETENTION_DRY_RUN';
test.each([
'FLUXER_REPORT_RETENTION_DAYS',
'FLUXER_RESOLVED_REPORT_RETENTION_DAYS',
'FLUXER_REPORT_RETENTION_DRY_RUN',
])('the worker is handed %s and gets no value when .env leaves it out', (name) => {
expect(serviceEnvironment('worker')[name]).toBe(`\${${name}:-}`);
});
});
@@ -291,6 +291,9 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_DISCOVERY_ENABLED: {path: ['discovery', 'enabled'], parse: parseBoolean},
FLUXER_DISCOVERY_MIN_MEMBER_COUNT: {path: ['discovery', 'min_member_count'], parse: parseInteger},
FLUXER_DELETION_GRACE_PERIOD_HOURS: {path: ['deletion_grace_period_hours'], parse: parseInteger},
FLUXER_REPORT_RETENTION_DAYS: {path: ['report_retention', 'days'], parse: parseInteger},
FLUXER_RESOLVED_REPORT_RETENTION_DAYS: {path: ['report_retention', 'resolved_days'], parse: parseInteger},
FLUXER_REPORT_RETENTION_DRY_RUN: {path: ['report_retention', 'dry_run'], parse: parseBoolean},
FLUXER_RELAX_REGISTRATION_RATE_LIMITS: {path: ['dev', 'relax_registration_rate_limits'], parse: parseBoolean},
FLUXER_DISABLE_RATE_LIMITS: {path: ['dev', 'disable_rate_limits'], parse: parseBoolean},
FLUXER_TEST_MODE_ENABLED: {path: ['dev', 'test_mode_enabled'], parse: parseBoolean},