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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@ ALLOWED_ORIGINS=https://craft.app
# Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# See docs/field-encryption.md for key rotation procedure.
FIELD_ENCRYPTION_KEY=your_64_char_hex_key_here

# Analytics Retention
# Number of days to retain deployment_analytics rows (default: 90).
# Set to 0 to disable automatic deletion entirely.
ANALYTICS_RETENTION_DAYS=90
30 changes: 30 additions & 0 deletions apps/backend/src/app/api/cron/purge-analytics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server';
import { analyticsService } from '@/services/analytics.service';

/**
* Cron: purge old deployment_analytics rows
*
* Deletes records from the deployment_analytics table that are older than
* ANALYTICS_RETENTION_DAYS (default: 90). This prevents the table from
* growing unbounded and degrading query performance over time.
*
* Set ANALYTICS_RETENTION_DAYS=0 to disable deletion entirely.
*
* Scheduled daily via vercel.json. Protected by CRON_SECRET.
*/
export async function GET(req: NextRequest) {
const cronSecret = process.env.CRON_SECRET;
if (cronSecret && req.headers.get('authorization') !== `Bearer ${cronSecret}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const retentionDays = parseInt(process.env.ANALYTICS_RETENTION_DAYS ?? '90', 10);

try {
const deleted = await analyticsService.applyRetentionPolicy(retentionDays);
return NextResponse.json({ deleted });
} catch (error: any) {
console.error('Error running analytics retention purge:', error);
return NextResponse.json({ error: error.message || 'Purge failed' }, { status: 500 });
}
}
29 changes: 29 additions & 0 deletions apps/backend/src/services/analytics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,35 @@ export class AnalyticsService {
};
}

/**
* Delete deployment_analytics rows older than `retentionDays` days.
*
* Called by the daily purge-analytics cron job.
* If retentionDays is 0 the policy is considered disabled and nothing is deleted.
*
* @param retentionDays - Number of days to retain records (0 = disabled)
* @returns Number of rows deleted
*/
async applyRetentionPolicy(retentionDays: number): Promise<number> {
if (retentionDays === 0) {
return 0;
}

const supabase = createClient();
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();

const { error, count } = await supabase
.from('deployment_analytics')
.delete()
.lt('recorded_at', cutoff);

if (error) {
throw new Error(`Failed to apply retention policy: ${error.message}`);
}

return count ?? 0;
}

/**
* Export analytics data as CSV
*/
Expand Down
Empty file.
4 changes: 4 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
{
"path": "/api/cron/smoke-test",
"schedule": "0 * * * *"
},
{
"path": "/api/cron/purge-analytics",
"schedule": "0 2 * * *"
}
]
}