대시보드 상세
Sophonz가 제공하는 v3 대시보드 7개를 패널 단위로 정리합니다. 각 패널이 어떤 테이블에서 무엇을 어떻게 계산하는지, 변수와 드릴다운 링크가 대시보드 사이를 어떻게 잇는지 다룹니다.
Sophonz 대시보드는 모두 ClickHouse에 쌓인 스팬·에러·세션·로그를 직접 SQL로 읽습니다. 이 페이지는 개발자가 숫자의 정의를 확인하고, 필요하면 쿼리를 고쳐 쓸 수 있도록 패널 하나하나를 풀어 둔 레퍼런스입니다. 커스텀 패널 플러그인 자체의 동작과 옵션은 커스텀 패널에 따로 있습니다.
한눈에
| 대시보드 | uid | 무엇을 보나 | 패널 |
|---|---|---|---|
| Sophonz Service (v3) | sophonz-service-v3 | 서비스 전체 상태. 대부분 여기서 시작합니다 | 39 |
| Session (v3) | sophonz-session-v3 | 세션 목록과 선택한 세션의 타임라인을 한 화면에 | 5 |
| Sophonz Session List | sophonz-session-list-v3 | 필터가 많은 세션 목록 | 1 |
| Sophonz Session Detail | sophonz-session-detail-v3 | 세션 하나를 깊게 — 타임라인·화면 흐름·서비스 흐름·트레이스 | 5 |
| Sophonz Traces | sophonz-traces-v3 | 트레이스·스팬 검색과 간트 뷰 | 2 |
| Sophonz Logs | sophonz-logs-v3 | OpenTelemetry 로그 검색 | 2 |
| Request Map (v3) | sophonz-requestmap-v3 | 요청 응답 시간 히트맵 한 장 | 1 |
Service 대시보드의 기본 시간 범위는 최근 6시간, 새로고침은 1분입니다.
아래 표의 #은 패널 id입니다. 패널 하나만 크게 열려면 주소에 viewPanel을 붙입니다.
/d/sophonz-service-v3/?viewPanel=26대시보드 사이 이동
표의 링크를 누르면 필요한 변수를 채워 다른 대시보드로 넘어갑니다. 모든 링크가 ${__url_time_range}를 함께 넘기므로 보고 있던 시간 범위가 그대로 따라갑니다.
| 출발 | 누르는 곳 | 도착 | 넘기는 변수 |
|---|---|---|---|
Service #14 최근 트레이스 | Trace ID | 같은 대시보드의 간트 뷰 #23 | traceId |
Service #15 느린 트레이스 | Trace ID | 같은 대시보드의 간트 뷰 #23 | traceId |
Service #29 세션 목록 | sessionId | Session (v3) | sessionId |
Session #1 세션 목록 | sessionId | 같은 대시보드 | sessionId |
Session #4 트레이스 리스트 | title | 같은 대시보드의 간트 뷰 #5 | traceId |
Session List #2 | sessionId | Session Detail | sessionId |
Session Detail #4 트레이스 리스트 | title | 같은 대시보드의 트레이스 뷰 #5 | traceId, spanId |
Session Detail #4 트레이스 리스트 | 로그 | Logs | traceId |
Traces #2 리스트 | 이름 | 같은 대시보드의 간트 뷰 #3 | traceId, spanId |
Traces #2 리스트 | 로그 | Logs | traceId |
Traces #2 리스트 | sessionId | Session Detail | sessionId |
Logs #3 로그 | traceID | Traces | traceId |
Logs #3 로그 | sessionId | Session Detail | sessionId |
대시보드 위쪽 링크(← 서비스 메트릭, 로그 →, 트레이스 →, ← 세션 목록, 선택 해제)도 같은 방식입니다. 선택 해제는 traceId·spanId만 비워 같은 대시보드를 다시 엽니다.
데이터가 오는 곳
모든 패널의 데이터소스는 grafana-clickhouse-datasource입니다.
| 테이블 | 한 행이 | 쓰는 곳 |
|---|---|---|
sophonz_traces.distributed_sophonz_index_v2 | 스팬 하나 | 거의 모든 패널 |
sophonz_traces.distributed_sophonz_error_index_v2 | 에러 하나. 예외 유형·메시지·groupID | 에러 KPI, 에러 그루핑 |
sophonz_traces.stored_analysis_session | 세션 집계 상태. …State 컬럼을 groupUniqArrayMerge로 풉니다 | 세션 목록 |
sophonz_logs.distributed_logs_v2 | 로그 레코드 | Logs |
승격 컬럼과 태그 맵
거의 모든 쿼리에 이런 모양이 반복됩니다.
lower(if(appSpanType != '', appSpanType, stringTagMap['span.type']))SDK가 붙이는 속성 중 자주 쓰는 것은 수집할 때 전용 컬럼(appSpanType, appScreenName, appScreenType, httpURL)으로 옮겨 둡니다. 이전 SDK 버전이 보낸 데이터에는 이 컬럼이 비어 있고 값이 stringTagMap에만 있습니다. 컬럼을 먼저 읽고 비었으면 태그 맵으로 떨어지므로, 한 프로젝트에 SDK 버전이 섞여 있어도 같은 기준으로 셉니다. 쿼리를 직접 쓸 때도 이 폴백을 그대로 따르세요.
| 컬럼 | 태그 맵 폴백 |
|---|---|
appSpanType | stringTagMap['span.type'] |
appScreenName | stringTagMap['screen.name'] |
appScreenType | stringTagMap['screen.type'] |
httpURL | stringTagMap['http.url'], 그다음 stringTagMap['url.full'] |
공통 매크로
| 매크로 | 역할 |
|---|---|
$__timeFilter(timestamp) | 대시보드 시간 범위로 거릅니다 |
$__timeInterval(timestamp) | 시계열 버킷으로 내림합니다 |
$__fromTime, $__toTime | 범위의 시작·끝. KPI의 직전 동일 기간 비교에 씁니다 |
$__conditionalAll(조건, $변수) | 변수가 All이면 조건을 통째로 뺍니다. 값 수천 개짜리 IN 목록을 만들지 않습니다 |
공통 변수
| 변수 | 종류 | 값 | 대시보드 |
|---|---|---|---|
serviceNamespace | 쿼리 | 프로젝트(service.namespace) | 전부 |
serviceName | 쿼리 | 앱(service.name). serviceNamespace에 종속 | Service, Request Map |
sessionId | 쿼리 | 최근 세션부터 | Session, Session Detail |
traceId, spanId | 텍스트 | 드릴다운 링크가 채웁니다. 비우면 최근 트레이스 | Service, Session, Session Detail, Traces, Logs |
screenStart, screenEnd | 텍스트(숨김) | 화면 흐름에서 고른 구간 | Session, Session Detail |
status, appType, appVersion, webVersion, userId, deviceId, sessionIdFilter | 섞임 | 세션 목록 필터 | Session List |
view, spanType, errorOnly, userId, q | 섞임 | 트레이스 검색 | Traces |
severity, q | 섞임 | 로그 검색 | Logs |
Sophonz Service (v3)
서비스 하나의 상태를 위에서 아래로 훑습니다. 흐름 → KPI → 추이 → 웹 바이탈 → 트레이스 → 에러 → 화면 → 세션 순서입니다.
맨 위 — 요청 흐름
| # | 패널 | 무엇 |
|---|---|---|
| 25 | DynamicXHR Monitor | 현재 범위의 xhr·fetch 요청 수, 실패 수, 세션 수. 상태 코드가 0이거나 400 이상이면 실패로 셉니다. 패널 설명 |
| 26 | 리퀘스트 히트맵 | xhr·fetch 요청을 시간(가로)·응답 시간(세로, 로그 축)에 뿌립니다. 최대 10만 건. 패널 설명 |
KPI 카드
| # | 이름 | 값 | 좋은 방향 |
|---|---|---|---|
| 1 | 세션 수 | uniqCombined64(sessionID) | 중립 |
| 3 | 평균 렌더링 | render 스팬의 평균 소요 시간(ms) | 낮을수록 |
| 4 | 평균 리퀘스트 | xhr·fetch 스팬의 평균 소요 시간(ms) | 낮을수록 |
| 2 | 느린 리퀘스트 | 1초를 넘긴 xhr·fetch 비율(%) | 낮을수록 |
| 52 | 에러 세션 | 한 번이라도 에러를 만난 세션 비율(%) | 낮을수록 |
| 5 | 에러 수 | 에러 인덱스의 행 수 | 낮을수록 |
카드마다 쿼리가 세 개입니다 — 현재 기간 값(A), 직전 동일 기간 값(B), 스파크라인(C). 왜 세 개로 나누는지는 커스텀 패널의 KPI 설명에 있습니다. 에러 세션 카드는 이렇게 생겼습니다.
-- A: 지금 범위
SELECT ROUND(100 * uniqCombined64If(sessionID, hasError)
/ nullIf(uniqCombined64(sessionID), 0), 1) AS v
FROM sophonz_traces.distributed_sophonz_index_v2
WHERE $__timeFilter(timestamp)
AND $__conditionalAll(serviceNamespace IN (${serviceNamespace:singlequote}), $serviceNamespace)
AND $__conditionalAll(serviceName IN (${serviceName:singlequote}), $serviceName)
-- B: 같은 길이만큼 바로 앞
WHERE timestamp >= $__fromTime - ($__toTime - $__fromTime) AND timestamp < $__fromTime
-- C: 스파크라인
SELECT $__timeInterval(timestamp) AS t, … GROUP BY t ORDER BY t에러 수 대신 에러 세션 비율을 둔 이유는 트래픽에 좌우되지 않아서입니다. 방문이 두 배가 되면 에러 수도 두 배가 되지만 비율은 그대로입니다. 느린 리퀘스트도 같은 발상입니다 — 평균은 소수의 매우 느린 요청을 가리지만, 1초 초과 비율은 그 꼬리를 드러냅니다.
uniqCombined64는 근사 고유값 함수입니다. 세션이 많을 때 1% 안팎의 오차가 있습니다.
추이와 상위 URL
| # | 패널 | 무엇 |
|---|---|---|
| 6 | 세션 추이 | 버킷별 고유 세션 수 |
| 7 | 평균 렌더링/리퀘스트 시간 추이 | 버킷별 render 평균과 xhr·fetch 평균 |
| 9 | 리퀘스트 상위 URL | URL별 요청 수와 평균 소요 시간. 평균이 긴 순서로 50개 |
#7, #21, #22의 렌더링은 render 스팬만 셉니다. 이전 화면들이 쓰던 post-docs 스팬은 포함하지 않으므로, 두 값을 더해 평균을 내던 예전 화면보다 낮게 나올 수 있습니다.
웹 바이탈
webvitals 스팬에서 LCP·INP·CLS·FCP·TTFB를 읽습니다.
| # | 패널 | 무엇 |
|---|---|---|
| 41–45 | LCP / INP / CLS / FCP / TTFB p75 | 지표별 p75와 표본 수 n |
| 46 | 지표별 등급 분포 | good · needs-improvement · poor 비율 |
| 47 | 웹 바이탈 p75 추이 | ms 단위 지표 넷(LCP·INP·FCP·TTFB). 오른쪽 축은 표본 수 |
| 48 | CLS p75 추이 | CLS만 따로. good 0.1 / poor 0.25 임계선 |
| 49 | 화면별 로드 바이탈 | 화면별 LCP·FCP·TTFB p75, good 비율, poor 건수 |
| 50 | 화면별 상호작용 바이탈 | 화면별 INP·CLS p75, good 비율, poor 건수 |
| 51 | 디바이스별 웹 바이탈 | 디바이스별 다섯 지표의 p75 |
임계값은 Google 기준을 따릅니다.
| 지표 | good | poor |
|---|---|---|
| LCP | ≤ 2500 ms | > 4000 ms |
| INP | ≤ 200 ms | > 500 ms |
| CLS | ≤ 0.1 | > 0.25 |
| FCP | ≤ 1800 ms | > 3000 ms |
| TTFB | ≤ 800 ms | > 1800 ms |
같은 측정을 한 번만 셉니다. 웹 바이탈은 값이 확정되기 전까지 여러 번 보고됩니다 — CLS는 레이아웃이 밀릴 때마다 커지고, INP는 더 느린 상호작용이 나오면 바뀝니다. 그래서 (sessionID, 측정 id) 단위로 묶어 가장 마지막 값만 씁니다. 측정 id가 없는 이전 SDK는 spanID로 대신합니다.
SELECT vital,
argMax(value, timestamp) AS value,
argMax(rating, timestamp) AS rating,
argMax(screen, timestamp) AS screen,
argMax(device, timestamp) AS device
FROM (
SELECT timestamp, sessionID,
if(stringTagMap['browser.web_vital.id'] != '',
stringTagMap['browser.web_vital.id'], spanID) AS vid,
stringTagMap['browser.web_vital.name'] AS vital,
numberTagMap['browser.web_vital.value'] AS value,
stringTagMap['browser.web_vital.rating'] AS rating,
appScreenName AS screen,
resourceTagsMap['sophonz.browser.device'] AS device
FROM sophonz_traces.distributed_sophonz_index_v2
WHERE $__timeFilter(timestamp) AND appSpanType = 'webvitals'
)
GROUP BY vital, sessionID, vid실제 쿼리는 이전 속성 이름(web-vital.name, web-vital.value, web-vital.rating)으로도 폴백하고, 디바이스 태그가 없으면 unknown으로 둡니다.
그 밖에 쿼리가 지키는 규칙이 몇 가지 있습니다.
- p75는 원본 행에서 직접 계산합니다. 백분위는 합성할 수 없어서, 화면별 p75를 평균 내면 전체 p75가 아닙니다.
quantileExact(0.75)를 중복 제거한 행 전체에 겁니다. - 비율은 어떤 단위로 묶어도 안전합니다. 그래서 표본이 적은 화면에서도
#46의 등급 분포는 의미가 유지됩니다. - 표본이 30 미만이면 p75는 노이즈입니다.
#49–#51은n을 빨갛게 칠할 뿐 작은 그룹을 숨기지 않습니다. - 정렬은 p75가 아니라 poor 건수입니다. 하루 세 명 오는 화면의 최악 p75보다, 실제로 나쁜 경험을 겪은 사용자가 많은 화면이 먼저 고칠 곳입니다.
- 로드 바이탈과 상호작용 바이탈은 다른 표입니다. LCP·FCP·TTFB는 하드 내비게이션마다 한 번 발생해 진입 화면에 귀속되지만, INP·CLS는 SPA 라우트 전환을 넘어 누적되므로 마지막으로 보고된 화면에 귀속됩니다.
- 디바이스는
resourceTagsMap['sophonz.browser.device']기준입니다. Google이 모바일과 데스크톱을 따로 보고하는 것과 맞춥니다.
트레이스
| # | 패널 | 무엇 |
|---|---|---|
| 12 | 트레이스 추이 | 버킷별 루트 스팬 수와 에러가 난 루트 스팬 수 |
| 13 | Span 유형 분포 | 유형별 스팬 수(render, xhr, longtask, anr …) |
| 14 | 최근 트레이스 | 루트 스팬 최근 200개. Trace ID를 누르면 #23에 로드 |
| 15 | 느린 트레이스 Top | 루트 스팬을 소요 시간 긴 순으로 50개 |
| 23 | 트레이스 간트 뷰 | Grafana 기본 traces 시각화. traceId가 비면 범위 안의 가장 최근 트레이스 |
트레이스 하나는 isRootSpan = true인 행 하나로 셉니다. 간트 뷰는 stringTagMap과 resourceTagsMap을 {key, value} 배열로 펼쳐 스팬 속성·리소스 속성 탭에 넣고, 최대 1000개 스팬을 그립니다.
에러 그루핑
| # | 패널 | 무엇 |
|---|---|---|
| 17 | 에러 그룹 | groupID 단위로 예외 유형·메시지·발생 수·영향 세션·영향 디바이스·마지막 발생. 발생 수 순 100개 |
| 18 | 에러 유형별 추이 | 버킷별·예외 유형별 건수 |
| 10 | 에러 Top | 예외 유형·메시지·serviceVersion·webVersion 조합별 건수 |
에러 인덱스가 부여한 groupID로 묶습니다. 발생 수만 보면 한 사용자가 반복해서 낸 에러가 위로 올라오므로 영향 세션 수를 같이 봅니다. #10은 버전 열이 있어 특정 배포에서 새로 생긴 에러를 찾을 때 씁니다.
화면 분석
| # | 패널 | 무엇 |
|---|---|---|
| 20 | 화면별 방문수 Top 20 | 화면별 render·route 스팬 수 |
| 21 | 화면별 평균 렌더링 Top 20 | 화면별 render 평균 |
| 22 | 화면별 상세 | 유형·화면·방문수·렌더링·리퀘스트·에러수·에러율 |
#22의 유형은 appScreenType입니다. 웹은 page, 안드로이드는 activity, iOS는 view, Jetpack Compose는 composable로 들어오므로, 플랫폼이 섞인 프로젝트도 한 표에서 화면 단위로 나란히 비교합니다.
세션
| # | 패널 | 무엇 |
|---|---|---|
| 29 | 세션 목록 | 세션별 상태(에러/정상)·사용자·디바이스·앱 유형·웹 버전·시작·길이(초)·트레이스 수. sessionId를 누르면 Session (v3)로 |
세션 목록은 스팬 인덱스가 아니라 stored_analysis_session에서 읽습니다. 세션마다 미리 집계된 상태를 합치기만 하므로 기간이 길어도 가볍습니다.
Session (v3)
세션을 고르고 그 세션을 바로 아래에서 보는, 한 화면짜리 대시보드입니다.
| # | 패널 | 무엇 |
|---|---|---|
| 1 | 세션 목록 | Service #29와 같은 목록. sessionId를 누르면 아래가 그 세션으로 바뀝니다 |
| 2 | Session Timeline | 세션의 루트 스팬을 유형별 레인으로 시간축에. 패널 설명 |
| 6 | Session Timeline Graph | 화면 이동을 그래프로. 패널 설명 |
| 4 | 트레이스 리스트 | 세션의 루트 스팬 시간순 1000개. 타임라인에서 고른 스팬은 ● |
| 5 | 트레이스 간트 뷰 | 고른 트레이스. 없으면 세션의 가장 최근 트레이스 |
타임라인에서 스팬을 누르면 spanId·traceId가 채워져 트레이스 리스트에 ●가 찍히고 간트 뷰가 그 트레이스를 로드합니다. 화면 흐름에서 화면을 고르면 그 화면에 머문 구간이 숨은 변수 screenStart·screenEnd에 들어가고, 타임라인이 그 구간으로 확대됩니다.
Sophonz Session List
#2 표 하나에 필터 여덟 개가 붙은 대시보드입니다. 특정 사용자·디바이스·버전의 세션을 찾을 때 씁니다.
| 필터 | 무엇으로 거르나 |
|---|---|
status | 전체 / 에러 / 정상 |
appType | clientPlatform — SDK가 보고한 플랫폼 값 |
appVersion | serviceVersion |
webVersion | webVersion |
userId, deviceId, sessionIdFilter | 입력한 값 |
sessionId를 누르면 Session Detail로 넘어갑니다.
Sophonz Session Detail
세션 하나를 깊게 보는 대시보드입니다. Session (v3)과 달리 세션 목록이 없고, 대신 Service Flow와 로그 링크가 있습니다.
| # | 패널 | 무엇 |
|---|---|---|
| 3 | Session Timeline | 유형별 레인 타임라인 |
| 2 | 화면 흐름 (Screen Flow) | 화면 이동 그래프 |
| 6 | Service Flow | 세션의 모든 스팬(최대 2만 개)을 서비스 노드와 호출 엣지로. 패널 설명 |
| 4 | 트레이스 리스트 | 시간순 루트 스팬. 로그 열을 누르면 그 트레이스의 로그로 |
| 5 | 트레이스 뷰 | 고른 트레이스의 간트 |
타임라인과 화면 흐름은 루트 스팬만 읽지만, Service Flow는 서비스 사이 호출을 이어야 하므로 isRootSpan 조건 없이 세션의 스팬을 전부 읽습니다.
Sophonz Traces
트레이스와 스팬을 검색합니다.
| # | 패널 | 무엇 |
|---|---|---|
| 2 | 트레이스 / 스팬 리스트 | view가 trace면 루트 스팬만(트레이스당 한 행), span이면 모든 스팬 |
| 3 | 트레이스 간트 뷰 | 리스트에서 이름을 누른 트레이스. 없으면 범위 안의 최근 트레이스 |
리스트 열은 시각·사용자·유형·화면·이름·루트 여부(●)·상태·소요 시간·HTTP 상태 코드·로그·세션입니다. spanType, errorOnly, userId, q(이름 검색)로 거릅니다.
Sophonz Logs
OpenTelemetry 로그를 검색합니다.
| # | 패널 | 무엇 |
|---|---|---|
| 2 | 로그 볼륨 | 심각도별 누적 건수. 심각도가 비어 있으면 UNSET |
| 3 | 로그 | 시각·심각도·본문·traceID·spanID·세션·서비스. 최근 2000건 |
로그에는 세션 id가 없습니다. #3은 같은 시간 범위의 스팬 인덱스를 traceID로 LEFT JOIN해 세션을 찾아 붙입니다. 그래서 트레이스에 연결되지 않은 로그는 세션 칸이 비어 있습니다.
severity(TRACE·DEBUG·INFO·WARN·ERROR·FATAL), q(본문 부분 일치, 대소문자 무시), traceId, spanId로 거릅니다. WARN과 WARNING은 같은 것으로 셉니다.
Request Map (v3)
Service #26의 히트맵을 대시보드 하나로 크게 띄운 것입니다. serviceNamespace·serviceName으로 거릅니다.
쿼리를 직접 고칠 때
- 고유 세션 수를 버킷 합으로 만들지 마세요. 버킷 경계를 걸친 세션이 두 번 세어집니다. 실제 하루치 트래픽에서 31% 많게 나왔습니다.
- 백분위를 평균 내지 마세요. p75는 원본 행에서 다시 계산해야 합니다.
- 비율과 건수는 합쳐도 됩니다. 등급 분포처럼 쪼갠 뒤 묶어도 값이 유지됩니다.
- 승격 컬럼 → 태그 맵 폴백을 그대로 따르세요. 빠뜨리면 이전 SDK 데이터가 조용히 빠집니다.
- 필터 변수는
$__conditionalAll로 감싸세요. All일 때 조건이 사라져 쿼리가 가벼워집니다.