대시보드 상세

Sophonz가 제공하는 v3 대시보드 7개를 패널 단위로 정리합니다. 각 패널이 어떤 테이블에서 무엇을 어떻게 계산하는지, 변수와 드릴다운 링크가 대시보드 사이를 어떻게 잇는지 다룹니다.

Sophonz 대시보드는 모두 ClickHouse에 쌓인 스팬·에러·세션·로그를 직접 SQL로 읽습니다. 이 페이지는 개발자가 숫자의 정의를 확인하고, 필요하면 쿼리를 고쳐 쓸 수 있도록 패널 하나하나를 풀어 둔 레퍼런스입니다. 커스텀 패널 플러그인 자체의 동작과 옵션은 커스텀 패널에 따로 있습니다.

한눈에

대시보드uid무엇을 보나패널
Sophonz Service (v3)sophonz-service-v3서비스 전체 상태. 대부분 여기서 시작합니다39
Session (v3)sophonz-session-v3세션 목록과 선택한 세션의 타임라인을 한 화면에5
Sophonz Session Listsophonz-session-list-v3필터가 많은 세션 목록1
Sophonz Session Detailsophonz-session-detail-v3세션 하나를 깊게 — 타임라인·화면 흐름·서비스 흐름·트레이스5
Sophonz Tracessophonz-traces-v3트레이스·스팬 검색과 간트 뷰2
Sophonz Logssophonz-logs-v3OpenTelemetry 로그 검색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같은 대시보드의 간트 뷰 #23traceId
Service #15 느린 트레이스Trace ID같은 대시보드의 간트 뷰 #23traceId
Service #29 세션 목록sessionIdSession (v3)sessionId
Session #1 세션 목록sessionId같은 대시보드sessionId
Session #4 트레이스 리스트title같은 대시보드의 간트 뷰 #5traceId
Session List #2sessionIdSession DetailsessionId
Session Detail #4 트레이스 리스트title같은 대시보드의 트레이스 뷰 #5traceId, spanId
Session Detail #4 트레이스 리스트로그LogstraceId
Traces #2 리스트이름같은 대시보드의 간트 뷰 #3traceId, spanId
Traces #2 리스트로그LogstraceId
Traces #2 리스트sessionIdSession DetailsessionId
Logs #3 로그traceIDTracestraceId
Logs #3 로그sessionIdSession DetailsessionId

대시보드 위쪽 링크(← 서비스 메트릭, 로그 →, 트레이스 →, ← 세션 목록, 선택 해제)도 같은 방식입니다. 선택 해제는 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 버전이 섞여 있어도 같은 기준으로 셉니다. 쿼리를 직접 쓸 때도 이 폴백을 그대로 따르세요.

컬럼태그 맵 폴백
appSpanTypestringTagMap['span.type']
appScreenNamestringTagMap['screen.name']
appScreenTypestringTagMap['screen.type']
httpURLstringTagMap['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 → 추이 → 웹 바이탈 → 트레이스 → 에러 → 화면 → 세션 순서입니다.

맨 위 — 요청 흐름

#패널무엇
25DynamicXHR 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리퀘스트 상위 URLURL별 요청 수와 평균 소요 시간. 평균이 긴 순서로 50개

#7, #21, #22의 렌더링은 render 스팬만 셉니다. 이전 화면들이 쓰던 post-docs 스팬은 포함하지 않으므로, 두 값을 더해 평균을 내던 예전 화면보다 낮게 나올 수 있습니다.

웹 바이탈

webvitals 스팬에서 LCP·INP·CLS·FCP·TTFB를 읽습니다.

#패널무엇
41–45LCP / INP / CLS / FCP / TTFB p75지표별 p75와 표본 수 n
46지표별 등급 분포good · needs-improvement · poor 비율
47웹 바이탈 p75 추이ms 단위 지표 넷(LCP·INP·FCP·TTFB). 오른쪽 축은 표본 수
48CLS p75 추이CLS만 따로. good 0.1 / poor 0.25 임계선
49화면별 로드 바이탈화면별 LCP·FCP·TTFB p75, good 비율, poor 건수
50화면별 상호작용 바이탈화면별 INP·CLS p75, good 비율, poor 건수
51디바이스별 웹 바이탈디바이스별 다섯 지표의 p75

임계값은 Google 기준을 따릅니다.

지표goodpoor
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트레이스 추이버킷별 루트 스팬 수와 에러가 난 루트 스팬 수
13Span 유형 분포유형별 스팬 수(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를 누르면 아래가 그 세션으로 바뀝니다
2Session Timeline세션의 루트 스팬을 유형별 레인으로 시간축에. 패널 설명
6Session Timeline Graph화면 이동을 그래프로. 패널 설명
4트레이스 리스트세션의 루트 스팬 시간순 1000개. 타임라인에서 고른 스팬은 ●
5트레이스 간트 뷰고른 트레이스. 없으면 세션의 가장 최근 트레이스

타임라인에서 스팬을 누르면 spanId·traceId가 채워져 트레이스 리스트에 ●가 찍히고 간트 뷰가 그 트레이스를 로드합니다. 화면 흐름에서 화면을 고르면 그 화면에 머문 구간이 숨은 변수 screenStart·screenEnd에 들어가고, 타임라인이 그 구간으로 확대됩니다.

Sophonz Session List

#2 표 하나에 필터 여덟 개가 붙은 대시보드입니다. 특정 사용자·디바이스·버전의 세션을 찾을 때 씁니다.

필터무엇으로 거르나
status전체 / 에러 / 정상
appTypeclientPlatform — SDK가 보고한 플랫폼 값
appVersionserviceVersion
webVersionwebVersion
userId, deviceId, sessionIdFilter입력한 값

sessionId를 누르면 Session Detail로 넘어갑니다.

Sophonz Session Detail

세션 하나를 깊게 보는 대시보드입니다. Session (v3)과 달리 세션 목록이 없고, 대신 Service Flow와 로그 링크가 있습니다.

#패널무엇
3Session Timeline유형별 레인 타임라인
2화면 흐름 (Screen Flow)화면 이동 그래프
6Service 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일 때 조건이 사라져 쿼리가 가벼워집니다.