[Feature] RAGOps Dashboard WebSocket 실시간 push - #139
Conversation
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 41 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthrough대시보드 요약 조회 설계를 구체화하고, Changes대시보드 WebSocket 기능
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant WebSocketConfig
participant StompAuthChannelInterceptor
participant DashboardSubscriptionAuthorizationInterceptor
participant DashboardWebSocketController
participant TopicBroker
Client->>WebSocketConfig: /ws SockJS 연결
Client->>StompAuthChannelInterceptor: JWT 포함 CONNECT
StompAuthChannelInterceptor->>Client: Principal 설정 또는 연결 거부
Client->>DashboardSubscriptionAuthorizationInterceptor: /topic/dashboard SUBSCRIBE
DashboardSubscriptionAuthorizationInterceptor->>Client: ROLE_ADMIN 확인 또는 구독 거부
DashboardWebSocketController->>TopicBroker: DashboardSummaryResponse 전송
TopicBroker->>Client: 대시보드 요약 push
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/main/java/com/opensource/docgrid/global/config/SecurityConfig.java (1)
42-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win순차 보안 흐름 주석에 단계 번호를 사용하세요.
src/main/java/com/opensource/docgrid/global/config/SecurityConfig.java#L42-L45: HTTP handshake permitAll, CONNECT 인증, SUBSCRIBE 인가를1.,2.,3.으로 구분하세요.src/main/java/com/opensource/docgrid/global/config/WebSocketConfig.java#L42-L45: CONNECT 인증 후 SUBSCRIBE 인가가 실행되는 순서를 번호 주석으로 표시하세요.As per coding guidelines, "For sequential execution flows, add numbered comments such as
1.,2.,3., and4.at the relevant steps."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/opensource/docgrid/global/config/SecurityConfig.java` around lines 42 - 45, Add numbered sequential-flow comments in SecurityConfig.java lines 42-45: mark HTTP handshake permitAll as 1., STOMP CONNECT authentication as 2., and destination-specific SUBSCRIBE authorization as 3.; also update WebSocketConfig.java lines 42-45 to number the CONNECT authentication and subsequent SUBSCRIBE authorization steps in execution order.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/design/kangcheolung-`#137-ragops-dashboard-websocket-push.md:
- Around line 17-20: Update the success criterion describing non-ADMIN access so
it states that users are rejected when subscribing to /topic/dashboard, rather
than at connection or subscription generally. Keep the existing JWT and
WebSocket connection behavior unchanged.
In
`@src/main/java/com/opensource/docgrid/domain/dashboard/websocket/DashboardSubscriptionAuthorizationInterceptor.java`:
- Around line 37-44: Update
DashboardSubscriptionAuthorizationInterceptor.preSend() to reject
StompCommand.SEND frames targeting DASHBOARD_TOPIC, while preserving the
existing isAdmin() authorization for SUBSCRIBE frames. In
DashboardWebSocketIntegrationTest.java:127-149, add an integration test
verifying a USER send to /topic/dashboard fails and an ADMIN subscriber receives
no payload.
In
`@src/test/java/com/opensource/docgrid/domain/dashboard/websocket/DashboardWebSocketIntegrationTest.java`:
- Line 54: Update DashboardWebSocketIntegrationTest to separate the connection
timeout from the delivery timeout: keep TIMEOUT_SECONDS for connection setup,
add or reuse a distinct 1-second timeout for dashboard receipt assertions near
the delivery verification, and ensure the delivery wait cannot succeed after the
1-second SLA.
- Around line 85-92: Update the subscribeDashboard flow in
DashboardWebSocketIntegrationTest to wait for the session.subscribe(...) receipt
before calling dashboardWebSocketController.sendDashboardUpdate(summary). Expose
or return the Subscription so the test can register a receipt task and trigger
the push only after subscription completion; do not use a fixed sleep.
---
Nitpick comments:
In `@src/main/java/com/opensource/docgrid/global/config/SecurityConfig.java`:
- Around line 42-45: Add numbered sequential-flow comments in
SecurityConfig.java lines 42-45: mark HTTP handshake permitAll as 1., STOMP
CONNECT authentication as 2., and destination-specific SUBSCRIBE authorization
as 3.; also update WebSocketConfig.java lines 42-45 to number the CONNECT
authentication and subsequent SUBSCRIBE authorization steps in execution order.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 36b57662-6636-42b4-9a5d-d7c3d2bff861
📒 Files selected for processing (11)
build.gradledocs/design/kangcheolung-#134-ragops-dashboard-summary.mddocs/design/kangcheolung-#137-ragops-dashboard-websocket-push.mdsrc/main/java/com/opensource/docgrid/domain/auth/jwt/StompAuthChannelInterceptor.javasrc/main/java/com/opensource/docgrid/domain/dashboard/controller/DashboardWebSocketController.javasrc/main/java/com/opensource/docgrid/domain/dashboard/websocket/DashboardSubscriptionAuthorizationInterceptor.javasrc/main/java/com/opensource/docgrid/global/config/CorsConfig.javasrc/main/java/com/opensource/docgrid/global/config/SecurityConfig.javasrc/main/java/com/opensource/docgrid/global/config/WebSocketConfig.javasrc/test/java/com/opensource/docgrid/domain/auth/jwt/StompAuthChannelInterceptorTest.javasrc/test/java/com/opensource/docgrid/domain/dashboard/websocket/DashboardWebSocketIntegrationTest.java
| subscribeDashboard(session, received); | ||
|
|
||
| // When | ||
| DashboardSummaryResponse summary = sampleSummary(); | ||
| dashboardWebSocketController.sendDashboardUpdate(summary); | ||
|
|
||
| // Then | ||
| DashboardSummaryResponse result = received.poll(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Spring Framework StompSession.Subscription에서 SUBSCRIBE receipt를 요청하고 receipt 수신을 대기하는 공식 권장 API를 확인해 주세요.
💡 Result:
Spring Framework에서 StompSession을 통해 구독(SUBSCRIBE) 시 receipt를 요청하고 이를 처리하는 공식적인 방법은 다음과 같습니다 [1][2][3]. 1. Receipt 요청 설정 STOMP 프로토콜의 receipt 기능을 사용하려면 SUBSCRIBE 프레임에 receipt 헤더를 추가해야 합니다 [1][3]. 이를 위해 두 가지 방식을 사용할 수 있습니다. - 자동 설정: StompSession.setAutoReceipt(true)를 호출하면 이후 발생하는 모든 send 및 subscribe 작업에 자동으로 receipt 헤더가 추가됩니다 [2][4][5]. - 수동 설정: subscribe 메서드를 호출할 때 StompHeaders 객체를 직접 생성하여 "receipt" 헤더를 추가할 수 있습니다 [1][2][4]. 2. Receipt 수신 및 처리 (Receiptable API) subscribe 메서드는 StompSession.Subscription 객체를 반환하며, 이는 StompSession.Receiptable 인터페이스를 상속받습니다 [6][2][7]. 이 Receiptable 인터페이스가 제공하는 API를 통해 receipt 수신을 대기하고 처리할 수 있습니다 [7][4]. 주요 API: - addReceiptTask(Runnable task): receipt가 성공적으로 수신되었을 때 실행할 작업을 등록합니다 [7][4]. - addReceiptTask(Consumer task): receipt가 수신되었을 때 해당 RECEIPT 프레임의 헤더 정보를 처리할 수 있는 Consumer를 등록합니다 [7][4]. - addReceiptLostTask(Runnable task): 설정된 제한 시간 내에 receipt가 수신되지 않았을 때(실패 시) 실행할 작업을 등록합니다 [7][4]. 참고 사항: 이 기능을 사용하려면 클라이언트 설정 시 TaskScheduler가 구성되어 있어야 하며, receipt가 만료되기까지의 시간 제한(기본값 15초)을 고려해야 합니다 [1][3][5]. 예시 코드 패턴: StompHeaders headers = new StompHeaders; headers.setDestination("/topic/my-destination"); headers.setReceipt("my-receipt-id"); // 수동 설정 시 StompSession.Subscription subscription = session.subscribe(headers, handler); subscription.addReceiptTask( -> System.out.println("구독 성공!")); subscription.addReceiptLostTask( -> System.err.println("구독 실패 (Receipt 미수신)"));
Citations:
- 1: https://docs.spring.io/spring/reference/web/websocket/stomp/client.html
- 2: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/messaging/simp/stomp/StompSession.html
- 3: https://docs.spring.io/spring-framework/reference/6.2/web/websocket/stomp/client.html
- 4: https://github.com/spring-projects/spring-framework/blob/master/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSession.java
- 5: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/messaging/simp/stomp/DefaultStompSession.html
- 6: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/messaging/simp/stomp/StompSession.Subscription.html
- 7: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/messaging/simp/stomp/StompSession.Receiptable.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the target test file and related websocket controller/config without modifying or running repo code.
target="src/test/java/com/opensource/docgrid/domain/dashboard/websocket/DashboardWebSocketIntegrationTest.java"
printf '--- target file line count ---\n'
wc -l "$target" 2>/dev/null || true
printf '\n--- target outline ---\n'
ast-grep outline "$target" --view compact || true
printf '\n--- relevant target lines 1-130 ---\n'
sed -n '1,130p' "$target" | nl -ba
printf '\n--- websocket-related files ---\n'
git ls-files | rg 'websocket|dashboard|WebSocket|Stomp|Socket' | sed -n '1,120p'Repository: DocGrid/backend
Length of output: 244
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect WebSocket configuration/controller and build files to establish broker ordering and library expectations.
printf '%s\n' '--- dashboard websocket controller candidates ---'
git ls-files | rg 'DashboardWebSocket*|.*WebSocket.*' | sed -n '1,200p'
printf '%s\n' '--- websocket server config examples ---'
for f in $(git ls-files | rg 'websocket|WebSocket|Sock|Stomp' | sed -n '1,80p'); do
echo "### $f"
wc -l "$f"
rg -n "getBroker|setDestination|SimpleBroker|WebSocket|Stomp|messageBroker|channelMapping|INTERCEPT" "$f" || true
echo
done
printf '%s\n' '--- build deps mentioning stomp/websocket ---'
for f in build.gradle* pom.xml gradle.properties settings.gradle*; do
[ -f "$f" ] && { echo "### $f"; sed -n '1,220p' "$f"; }
doneRepository: DocGrid/backend
Length of output: 18877
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the target test file without modifying repository code.
target="src/test/java/com/opensource/docgrid/domain/dashboard/websocket/DashboardWebSocketIntegrationTest.java"
printf '%s\n' '--- target file exists and size ---'
wc -l "$target" 2>/dev/null || (printf 'target file missing\n' && exit 1)
printf '%s\n' '--- target relevant lines 1-140 ---'
sed -n '1,140p' "$target" | nl -ba
printf '%s\n' '--- target relevant subscribe/send lines ---'
rg -n "subscribeDashboard|sendDashboardUpdate|timeout|sleep|receipt|subscribe\(|send\(" "$target" -C 3Repository: DocGrid/backend
Length of output: 372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect WebSocket controller/config to establish timing context without running repository code.
printf '%s\n' '--- websocket-related candidate files ---'
git ls-files | rg 'websocket|WebSocket|Stomp|Sock|dashboard' | sed -n '1,200p'
printf '%s\n' '--- WebSocket config/controller excerpts ---'
for f in $(git ls-files | rg 'websocket|WebSocket|Stomp|Sock|dashboard' | sed -n '1,80p'); do
echo "### $f"
wc -l "$f"
rg -n "SimpleBroker|getBroker|messageBroker|channelMapping|WebSocket|Stomp|sendDashboardUpdate|dashboardUpdate|broadcast|SUBSCRIBE|SEND|INTERCEPT" "$f" || true
echo
doneRepository: DocGrid/backend
Length of output: 16612
🏁 Script executed:
#!/bin/bash
set -euo pipefail
target="src/test/java/com/opensource/docgrid/domain/dashboard/websocket/DashboardWebSocketIntegrationTest.java"
# Use printf -- explicitly after a no-op argument to avoid the "invalid option" shell bug.
printf '%s\n' '--- target file exist and size ---'
wc -l "$target" || { printf 'target missing\n'; exit 1; }
printf '\n%s\n' '--- target lines 70-110 ---'
awk '{printf "%5d %s\n", NR, $0}' "$target" | sed -n '70,110p'
printf '\n%s\n' '--- target lines 170-190 ---'
awk '{printf "%5d %s\n", NR, $0}' "$target" | sed -n '170,190p'
printf '%s\n' '--- controller excerpt ---'
awk '{printf "%5d %s\n", NR, $0}' src/main/java/com/opensource/docgrid/domain/dashboard/controller/DashboardWebSocketController.java | sed -n '25,45p'Repository: DocGrid/backend
Length of output: 3267
구독 완료 후 push를 전송하세요.
subscribeDashboard() 뒤 바로 sendDashboardUpdate(summary)를 호출하면 STOMP 브로커가 SUBSCRIBE 처리를 마치기 전에 전송이 발생할 수 있습니다. session.subscribe(...)의 반환 Subscription에서 receipt 작업을 등록하고, receipt 수신 후 push를 보내는 방식으로 구독 활성状态的 확인을 추가하세요. 고정 sleep은 사용하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/test/java/com/opensource/docgrid/domain/dashboard/websocket/DashboardWebSocketIntegrationTest.java`
around lines 85 - 92, Update the subscribeDashboard flow in
DashboardWebSocketIntegrationTest to wait for the session.subscribe(...) receipt
before calling dashboardWebSocketController.sendDashboardUpdate(summary). Expose
or return the Subscription so the test can register a receipt task and trigger
the push only after subscription completion; do not use a fixed sleep.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
/ws(SockJS) STOMP endpoint와/topic/dashboardMessage Broker 추가websocketTransport가 Upgrade 요청에 커스텀 헤더를 못 싣는 제약 때문에,SecurityConfig는/ws/**를 permitAll로 열고StompAuthChannelInterceptor가 CONNECT 시점에 JWT를 검증/topic/dashboardSUBSCRIBE는DashboardSubscriptionAuthorizationInterceptor가 ROLE_ADMIN을 별도로 재검증 (CONNECT 검증 하나에만 의존하지 않는 이중 방어)DashboardWebSocketController.sendDashboardUpdate()— 순수 전송 계층, 집계는 직접 하지 않고 호출자가 계산해서 넘김 (후속 이슈가 재사용할 지점)설계 변경 사항 (구현 중 발견)
@EnableWebSocketSecurityDSL로 구현하려 했으나, 이 DSL이 STOMP endpoint 등록을 감지하면 세션 기반 CSRF 토큰을 무조건 요구하는CsrfChannelInterceptor를 자동으로 붙임. 이 앱은 stateless JWT라 세션이 없어 CSRF 토큰이 존재할 수 없고, 그 결과 ADMIN 여부와 무관하게 모든 CONNECT가MissingCsrfTokenException으로 거부됨. 원인을 바이트코드까지 확인 후 해당 DSL을 걷어내고 순수ChannelInterceptor로 직접 구현 (spring-security-messaging의존성도 함께 제거)docs/design/kangcheolung-#137-ragops-dashboard-websocket-push.md4장 참고Test plan
StompAuthChannelInterceptorTest단위 테스트 4건 (유효 토큰/헤더 없음/무효 토큰/CONNECT 아닌 프레임)DashboardWebSocketIntegrationTest— 실제 STOMP Client로 서버에 직접 연결하는 통합 테스트 3건 (ADMIN 구독 후 push 수신, 토큰 없이 CONNECT 거부, USER 토큰으로 SUBSCRIBE 거부)./gradlew build전체 회귀 테스트 통과 — 총 733개, failures 0, errors 0상세 설계
docs/design/kangcheolung-#137-ragops-dashboard-websocket-push.mdcloses #137
🤖 Generated with Claude Code
Summary by CodeRabbit
새 기능
문서
테스트