feat(audit): add kinds param to findRolledUpActivityFeed

Filter is applied at the innermost events CTE to reduce rows
entering the LAG/session CTEs. Existing callers pass ROLLUP_ELIGIBLE
by default so behaviour is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-21 20:59:56 +02:00
parent d700b0a948
commit fe7a8ed9ad
4 changed files with 109 additions and 20 deletions

View File

@@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.OffsetDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@@ -35,8 +36,7 @@ public interface AuditLogQueryRepository extends JpaRepository<AuditLog, UUID> {
ORDER BY a.happened_at
) AS prev_happened_at
FROM audit_log a
WHERE a.kind IN ('TEXT_SAVED','FILE_UPLOADED','ANNOTATION_CREATED',
'BLOCK_REVIEWED','COMMENT_ADDED','MENTION_CREATED')
WHERE a.kind IN (:kinds)
AND a.document_id IS NOT NULL
),
sessions_marked AS (
@@ -108,7 +108,8 @@ public interface AuditLogQueryRepository extends JpaRepository<AuditLog, UUID> {
""", nativeQuery = true)
List<ActivityFeedRow> findRolledUpActivityFeed(
@Param("currentUserId") String currentUserId,
@Param("limit") int limit);
@Param("limit") int limit,
@Param("kinds") Collection<String> kinds);
@Query(value = """
SELECT

View File

@@ -17,7 +17,8 @@ public class AuditLogQueryService {
}
public List<ActivityFeedRow> findActivityFeed(UUID currentUserId, int limit) {
return queryRepository.findRolledUpActivityFeed(currentUserId.toString(), limit);
return queryRepository.findRolledUpActivityFeed(currentUserId.toString(), limit,
AuditKind.ROLLUP_ELIGIBLE.stream().map(Enum::name).toList());
}
public PulseStatsRow getPulseStats(OffsetDateTime weekStart, UUID userId) {

View File

@@ -50,7 +50,8 @@ class AuditLogQueryRepositoryIntegrationTest {
"INSERT INTO audit_log (kind, actor_id, document_id, payload) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '{\"pageNumber\":1}')"
})
void findRolledUpActivityFeed_returnsAnnotationEntry() {
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 10);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 10,
List.of("TEXT_SAVED","FILE_UPLOADED","ANNOTATION_CREATED","BLOCK_REVIEWED","COMMENT_ADDED","MENTION_CREATED"));
assertThat(rows).hasSize(1);
assertThat(rows.get(0).getKind()).isEqualTo("ANNOTATION_CREATED");

View File

@@ -36,6 +36,10 @@ class AuditLogQueryRepositoryRolledUpTest {
static final UUID DOC_ID = UUID.fromString("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee");
static final UUID OTHER_DOC_ID = UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff");
static final List<String> ALL_ELIGIBLE_KINDS = List.of(
"TEXT_SAVED", "FILE_UPLOADED", "ANNOTATION_CREATED",
"BLOCK_REVIEWED", "COMMENT_ADDED", "MENTION_CREATED");
private static final ObjectMapper MAPPER = new ObjectMapper();
@Autowired AuditLogQueryRepository auditLogQueryRepository;
@@ -97,7 +101,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base.plusSeconds(i * 480L));
}
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).hasSize(1);
ActivityFeedRow row = rows.get(0);
@@ -119,7 +123,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", sessionTwoStart);
insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", sessionTwoStart.plusSeconds(300));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).hasSize(2);
assertThat(rows.get(0).getCount()).isEqualTo(2);
@@ -136,7 +140,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(USER_ID, DOC_ID, "ANNOTATION_CREATED", base.plusSeconds(i * 60L * 30L));
}
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).hasSize(1);
assertThat(rows.get(0).getCount()).isEqualTo(30);
@@ -152,7 +156,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", base.plusSeconds(60));
insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", base.plusSeconds(120));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).hasSize(3);
assertThat(rows).allSatisfy(r -> {
@@ -170,7 +174,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(USER_ID, DOC_ID, "METADATA_UPDATED", base.plusSeconds(60));
insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base.plusSeconds(120));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).hasSize(1);
assertThat(rows.get(0).getKind()).isEqualTo("TEXT_SAVED");
@@ -184,7 +188,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", rollupStart.plusSeconds(300));
insertAuditEvent(USER_ID, OTHER_DOC_ID, "FILE_UPLOADED", rollupStart.plusSeconds(900));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).hasSize(2);
assertThat(rows).anySatisfy(r -> {
@@ -209,7 +213,7 @@ class AuditLogQueryRepositoryRolledUpTest {
Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString()));
insertReplyNotification(USER_ID, DOC_ID, commentId);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).anySatisfy(r ->
assertThat(r.isYouParticipated()).isTrue()
@@ -223,7 +227,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED",
Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString()));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).allSatisfy(r ->
assertThat(r.isYouParticipated()).isFalse()
@@ -236,7 +240,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED",
Instant.parse("2026-04-20T10:00:00Z"), Map.of());
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).allSatisfy(r ->
assertThat(r.isYouParticipated()).isFalse()
@@ -251,7 +255,7 @@ class AuditLogQueryRepositoryRolledUpTest {
Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString()));
insertReplyNotification(OTHER_USER_ID, DOC_ID, commentId);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).allSatisfy(r ->
assertThat(r.isYouParticipated()).isFalse()
@@ -264,7 +268,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(OTHER_USER_ID, DOC_ID, "MENTION_CREATED",
Instant.parse("2026-04-20T10:00:00Z"), Map.of("mentionedUserId", USER_ID.toString()));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).anySatisfy(r ->
assertThat(r.isYouMentioned()).isTrue()
@@ -278,7 +282,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED",
Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString()));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).hasSize(1);
assertThat(rows.get(0).getCommentId()).isEqualTo(commentId);
@@ -292,7 +296,7 @@ class AuditLogQueryRepositoryRolledUpTest {
Instant.parse("2026-04-20T10:00:00Z"),
Map.of("commentId", commentId.toString(), "mentionedUserId", USER_ID.toString()));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).hasSize(1);
assertThat(rows.get(0).getCommentId()).isEqualTo(commentId);
@@ -304,7 +308,7 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED",
Instant.parse("2026-04-20T10:00:00Z"), Map.of("blockId", "ccc", "pageNumber", "1"));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).hasSize(1);
assertThat(rows.get(0).getCommentId()).isNull();
@@ -316,10 +320,92 @@ class AuditLogQueryRepositoryRolledUpTest {
insertAuditEvent(USER_ID, DOC_ID, "MENTION_CREATED",
Instant.parse("2026-04-20T10:00:00Z"), Map.of("mentionedUserId", OTHER_USER_ID.toString()));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40, ALL_ELIGIBLE_KINDS);
assertThat(rows).allSatisfy(r ->
assertThat(r.isYouMentioned()).isFalse()
);
}
// ─── kinds filter ─────────────────────────────────────────────────────────
@Test
void rolledUpFeed_with_single_kind_returns_only_that_kind() {
insertUserAndDocs();
Instant base = Instant.parse("2026-04-20T10:00:00Z");
insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base);
insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", base.plusSeconds(60));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(
USER_ID.toString(), 40, List.of("FILE_UPLOADED"));
assertThat(rows).hasSize(1);
assertThat(rows.get(0).getKind()).isEqualTo("FILE_UPLOADED");
}
@Test
void rolledUpFeed_with_multiple_kinds_returns_union() {
insertUserAndDocs();
Instant base = Instant.parse("2026-04-20T10:00:00Z");
insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base);
insertAuditEvent(USER_ID, OTHER_DOC_ID, "FILE_UPLOADED", base.plusSeconds(60));
insertAuditEvent(USER_ID, DOC_ID, "ANNOTATION_CREATED", base.plusSeconds(120));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(
USER_ID.toString(), 40, List.of("TEXT_SAVED", "FILE_UPLOADED"));
assertThat(rows).hasSize(2);
assertThat(rows).extracting(ActivityFeedRow::getKind)
.containsExactlyInAnyOrder("TEXT_SAVED", "FILE_UPLOADED");
}
@Test
void rolledUpFeed_with_default_returns_all_six_eligible_kinds() {
insertUserAndDocs();
Instant base = Instant.parse("2026-04-20T10:00:00Z");
insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base);
insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", base.plusSeconds(60));
insertAuditEvent(USER_ID, DOC_ID, "ANNOTATION_CREATED", base.plusSeconds(120));
insertAuditEvent(USER_ID, DOC_ID, "BLOCK_REVIEWED", base.plusSeconds(7300));
insertAuditEvent(USER_ID, DOC_ID, "COMMENT_ADDED", base.plusSeconds(7360));
insertAuditEvent(USER_ID, DOC_ID, "MENTION_CREATED", base.plusSeconds(7420));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(
USER_ID.toString(), 40,
List.of("TEXT_SAVED", "FILE_UPLOADED", "ANNOTATION_CREATED",
"BLOCK_REVIEWED", "COMMENT_ADDED", "MENTION_CREATED"));
assertThat(rows).hasSize(6);
}
@Test
void rolledUpFeed_excludes_rows_not_in_filter_set() {
insertUserAndDocs();
Instant base = Instant.parse("2026-04-20T10:00:00Z");
insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base);
insertAuditEvent(USER_ID, OTHER_DOC_ID, "FILE_UPLOADED", base.plusSeconds(60));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(
USER_ID.toString(), 40, List.of("TEXT_SAVED"));
assertThat(rows).hasSize(1);
assertThat(rows.get(0).getKind()).isEqualTo("TEXT_SAVED");
}
@Test
void rolledUpFeed_rollup_still_works_when_kind_set_is_filtered_to_single_rollable_kind() {
insertUserAndDocs();
Instant base = Instant.parse("2026-04-20T09:00:00Z");
for (int i = 0; i < 10; i++) {
insertAuditEvent(USER_ID, DOC_ID, "TEXT_SAVED", base.plusSeconds(i * 480L));
}
insertAuditEvent(USER_ID, DOC_ID, "FILE_UPLOADED", base.plusSeconds(20));
List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(
USER_ID.toString(), 40, List.of("TEXT_SAVED"));
assertThat(rows).hasSize(1);
assertThat(rows.get(0).getKind()).isEqualTo("TEXT_SAVED");
assertThat(rows.get(0).getCount()).isEqualTo(10);
}
}