Skip to content
Closed
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
46 changes: 41 additions & 5 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
AGENT_API_V1_BASE,
AGENT_API_V1_LLM_BASE,
LEGACY_AGENT_API_BASE,
markLegacyApi,
rejectLegacyAgentApi,
} from './middleware/legacyAgentApi';

Expand Down Expand Up @@ -149,8 +150,22 @@ app.get('/debug', (req, res) => {
app.use('/api/sql', sqlRoutes);
app.use('/api/auth', enterpriseAuthRoutes);
app.use('/api/auth', enterpriseApiKeyRoutes);
app.use('/api/traces', simpleTraceRoutes);
app.use(AGENT_API_V1_LLM_BASE, aiChatRoutes);
app.use(
'/api/traces',
markLegacyApi(
'/api/workspaces/:workspaceId/traces',
'Legacy trace API is deprecated. Migrate to workspace-scoped trace APIs',
),
simpleTraceRoutes,
);
app.use(
AGENT_API_V1_LLM_BASE,
markLegacyApi(
'/api/workspaces/:workspaceId/agent/llm',
'Legacy agent LLM API is deprecated. Migrate to workspace-scoped agent APIs',
),
aiChatRoutes,
);
app.use('/api/perfetto', perfettoLocalRoutes);
app.use('/api/auto-analysis', autoAnalysisRoutes);
app.use('/api/sessions', sessionRoutes);
Expand All @@ -160,10 +175,31 @@ app.use('/api/template-analysis', templateAnalysisRoutes);
app.use('/api/skills', skillRoutes);
app.use('/api/admin', skillAdminRoutes);
app.use('/api/admin', strategyAdminRoutes);
app.use('/api/reports', reportRoutes);
app.use(AGENT_API_V1_BASE, agentRoutes);
app.use(
'/api/reports',
markLegacyApi(
'/api/workspaces/:workspaceId/reports',
'Legacy report API is deprecated. Migrate to workspace-scoped report APIs',
),
reportRoutes,
);
app.use(
AGENT_API_V1_BASE,
markLegacyApi(
'/api/workspaces/:workspaceId/agent',
'Legacy agent API is deprecated. Migrate to workspace-scoped agent APIs',
),
agentRoutes,
);
app.use('/api/advanced-ai', advancedAIRoutes);
app.use('/api/v1/providers', providerRoutes);
app.use(
'/api/v1/providers',
markLegacyApi(
'/api/workspaces/:workspaceId/providers',
'Legacy provider API is deprecated. Migrate to workspace-scoped provider APIs',
),
providerRoutes,
);
app.use('/api/flamegraph', flamegraphRoutes);
app.use('/api/critical-path', criticalPathRoutes);
app.use('/api/baselines', baselineRoutes);
Expand Down
46 changes: 46 additions & 0 deletions backend/src/middleware/__tests__/legacyApiCompatibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.

import express from 'express';
import request from 'supertest';
import {
getLegacyApiUsageSnapshot,
resetLegacyApiUsageTelemetryForTests,
} from '../../services/legacyApiTelemetry';
import { LEGACY_AGENT_API_SUNSET, markLegacyApi } from '../legacyAgentApi';

describe('legacy API compatibility headers', () => {
afterEach(() => {
resetLegacyApiUsageTelemetryForTests();
});

test('adds deprecation headers and records telemetry before delegating to current handlers', async () => {
const app = express();
app.get(
'/api/traces',
markLegacyApi(
'/api/workspaces/:workspaceId/traces',
'Legacy trace API is deprecated. Migrate to workspace-scoped trace APIs',
),
(_req, res) => res.json({ success: true }),
);

const res = await request(app)
.get('/api/traces')
.set('Authorization', 'Bearer test-token')
.expect(200);

expect(res.headers.deprecation).toBe('true');
expect(res.headers.sunset).toBe(LEGACY_AGENT_API_SUNSET);
expect(res.headers.link).toBe(
'</api/workspaces/:workspaceId/traces>; rel="successor-version"',
);
expect(res.headers.warning).toContain('Legacy trace API is deprecated');
expect(res.body).toEqual({ success: true });

const telemetry = getLegacyApiUsageSnapshot();
expect(telemetry.totalLegacyRequests).toBe(1);
expect(telemetry.topPaths[0].key).toBe('GET /api/traces');
});
});
11 changes: 11 additions & 0 deletions backend/src/middleware/legacyAgentApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ export const LEGACY_AGENT_API_BASE = '/api/agent';
export const LEGACY_AGENT_API_LLM_BASE = '/api/agent/llm';
export const LEGACY_AGENT_API_SUNSET = 'Wed, 30 Jun 2027 00:00:00 GMT';

export function markLegacyApi(successor: string, message: string) {
return (req: Request, res: Response, next: NextFunction): void => {
recordLegacyApiUsage(req);
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', LEGACY_AGENT_API_SUNSET);
res.setHeader('Link', `<${successor}>; rel="successor-version"`);
res.setHeader('Warning', `299 - "${message}"`);
next();
};
}

export function markLegacyAgentApi(req: Request, res: Response, next: NextFunction): void {
recordLegacyApiUsage(req);
res.setHeader('Deprecation', 'true');
Expand Down
23 changes: 21 additions & 2 deletions backend/src/routes/__tests__/requestContextRouteCoverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { afterEach, describe, expect, it } from '@jest/globals';
import express from 'express';
import request from 'supertest';
import { markLegacyApi } from '../../middleware/legacyAgentApi';
import reportRoutes from '../reportRoutes';
import traceRoutes from '../simpleTraceRoutes';

Expand All @@ -13,8 +14,22 @@ const originalApiKey = process.env.SMARTPERFETTO_API_KEY;
function makeApp(): express.Express {
const app = express();
app.use(express.json());
app.use('/api/traces', traceRoutes);
app.use('/api/reports', reportRoutes);
app.use(
'/api/traces',
markLegacyApi(
'/api/workspaces/:workspaceId/traces',
'Legacy trace API is deprecated. Migrate to workspace-scoped trace APIs',
),
traceRoutes,
);
app.use(
'/api/reports',
markLegacyApi(
'/api/workspaces/:workspaceId/reports',
'Legacy report API is deprecated. Migrate to workspace-scoped report APIs',
),
reportRoutes,
);
return app;
}

Expand Down Expand Up @@ -51,6 +66,8 @@ describe('RequestContext route coverage', () => {
const res = await request(makeApp()).get('/api/traces');

expect(res.status).toBe(401);
expect(res.headers.deprecation).toBe('true');
expect(res.headers.sunset).toBeDefined();
expect(res.body.error).toBe('Unauthorized');
});

Expand All @@ -60,6 +77,8 @@ describe('RequestContext route coverage', () => {
const res = await request(makeApp()).get('/api/reports/missing-report');

expect(res.status).toBe(401);
expect(res.headers.deprecation).toBe('true');
expect(res.headers.sunset).toBeDefined();
expect(res.body.error).toBe('Unauthorized');
});
});
2 changes: 1 addition & 1 deletion docs/features/enterprise-multi-tenant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
- [x] 2.2 OIDC SSO 集成 + Onboarding flow(§15 全流程,含 audit)
- [x] 2.3 API key 管理(创建 / 撤销 / scope / 过期 / 审计)
- [x] 2.4 Membership / Role / RBAC 权限矩阵(§8.2)+ owner guard 全 route 覆盖
- [ ] 2.5 旧 API 兼容 wrapper:返回 `Deprecation: true` + `Sunset` header;统一走 RequestContext
- [x] 2.5 旧 API 兼容 wrapper:返回 `Deprecation: true` + `Sunset` header;统一走 RequestContext
- [ ] 2.6 Resource-oriented API 切到 `/api/workspaces/:workspaceId/*`(§8.3)
- [ ] 2.7 前端 workspace selection UI + workspace/window 上下文持久化分层(§9.2 表格)
- [ ] 2.8 单元测试覆盖:RequestContext 解析 / RBAC / owner guard / 旧路径包装
Expand Down