feat: add media access analytics

This commit is contained in:
Codex
2026-06-29 20:30:00 +08:00
parent e0393126a1
commit 38db867d58
11 changed files with 749 additions and 5 deletions

View File

@@ -3709,6 +3709,59 @@ async function testTenantContentAssetsAndImports() {
'admin preview access event should include watermark trace id',
);
const mediaSummary = await request('/api/tenant-content/media-analytics/summary', {
userId: TENANT_ADMIN_USER_ID,
query: { timeRange: '30d', assetId: localAsset.item.id, traceId: localPreview.watermark.traceId },
});
assert.equal(mediaSummary.assetAccess?.previews >= 1, true, 'media analytics summary should count asset previews');
assert.equal(mediaSummary.assetAccess?.watermarkEvents >= 1, true, 'media analytics summary should count watermark asset events');
assert.equal(mediaSummary.traceMatches?.assets >= 1, true, 'media analytics summary should support trace id asset lookup');
assert.ok(mediaSummary.daily?.some(item => item.assetEvents >= 1), 'media analytics daily series should include asset events');
const mediaAssetEvents = await request('/api/tenant-content/media-analytics/asset-events', {
userId: TENANT_ADMIN_USER_ID,
query: { timeRange: '30d', traceId: localPreview.watermark.traceId, limit: 5 },
});
assert.ok(
mediaAssetEvents.items?.some(item => item.assetId === localAsset.item.id && item.watermarkTraceId === localPreview.watermark.traceId),
'media analytics asset event list should search by watermark trace id',
);
assert.ok(!JSON.stringify(mediaAssetEvents).includes('signature='), 'media analytics asset events should not leak signed URLs');
const mediaVideoSummary = await request('/api/tenant-content/media-analytics/summary', {
userId: TENANT_ADMIN_USER_ID,
query: { timeRange: '30d', videoId: ids.video },
});
assert.equal(mediaVideoSummary.videoPlay?.svipPlays >= 1, true, 'media analytics summary should count SVIP video plays');
assert.ok(mediaVideoSummary.videoTop?.some(item => item.videoId === ids.video), 'media analytics summary should include top videos');
const mediaVideoEventsByVideo = await request('/api/tenant-content/media-analytics/video-events', {
userId: TENANT_ADMIN_USER_ID,
query: { timeRange: '30d', videoId: ids.video, limit: 5 },
});
const videoTraceId = mediaVideoEventsByVideo.items?.find(item => item.videoId === ids.video)?.watermarkTraceId;
assert.ok(videoTraceId, 'media analytics video event list should include watermark trace id');
const mediaVideoEvents = await request('/api/tenant-content/media-analytics/video-events', {
userId: TENANT_ADMIN_USER_ID,
query: { timeRange: '30d', traceId: videoTraceId, limit: 5 },
});
assert.ok(
mediaVideoEvents.items?.some(item => item.videoId === ids.video && item.watermarkTraceId === videoTraceId),
'media analytics video event list should search by watermark trace id',
);
assert.ok(!JSON.stringify(mediaVideoEvents).includes('vp_'), 'media analytics video events should not leak play tokens');
const teacherAnalyticsDenied = await request('/api/tenant-content/media-analytics/summary', {
userId: TENANT_TEACHER_USER_ID,
expectStatus: 403,
});
assert.equal(
teacherAnalyticsDenied.code,
'TENANT_CONTENT_PERMISSION_REQUIRED',
'teacher without analytics permission should not access media audit analytics',
);
const failedAsset = await request('/api/tenant-content/assets', {
userId: TENANT_ADMIN_USER_ID,
method: 'PUT',