커스텀 패널

Sophonz가 직접 만들어 Grafana에 설치하는 6개 패널이 각각 무엇을 그리고, 어떤 데이터와 옵션을 받고, 대시보드 JSON에서 어떻게 설정되는지 정리합니다.

Grafana 기본 패널로는 그릴 수 없는 화면이 있습니다. 세션 하나의 스팬을 시간축에 늘어놓는 일, 트레이스가 지나간 서비스를 노드-엣지로 잇는 일, 화면 사이 이동을 그래프로 만드는 일은 모두 데이터 모양이 정해져 있어야 가능합니다. Sophonz는 그런 화면 6개를 패널 플러그인으로 만들어 Grafana에 설치합니다. 각 패널이 들어간 대시보드와 쿼리 전체는 대시보드 상세에 있습니다.

설치 방식

패널은 CDN에 버전별로 올라가 있고, Grafana가 시작할 때 GF_INSTALL_PLUGINS로 내려받습니다.

https://cdn.sophonz.com/release/grafana/plugins/<패널 id>/<버전>/<패널 id>.zip

버전은 반드시 고정합니다. Grafana 스토리지가 emptyDir이므로 재시작마다 플러그인을 다시 내려받으며, 부동 버전을 사용하면 재시작 한 번으로 화면이 변경될 수 있습니다.

서명하지 않은 플러그인이므로 GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS에도 같은 id를 넣어야 Grafana가 로드합니다.

패널 목록

아래 화면은 모두 공개 데모 조직에 실제로 배포된 패널을 찍은 것입니다. 카드를 누르면 해당 설명으로 이동합니다.

패널id이 문서 기준 버전쓰이는 대시보드
Sophonz KPIsophonz-kpi-panel1.0.0Service
Session Timelinesophonz-session-timeline-panel1.3.0Session, Session Detail
Screen Flowsophonz-session-timeline-graph-panel1.5.1Session, Session Detail
Service Flowsophonz-service-flow-panel1.4.0Session Detail
Request Mapsophonz-requestmap-panel1.0.2Service, Request Map
DynamicXHR Monitorsophonz-dynamicxhr-panel1.0.0Service

공통 규칙

  • 필드 매핑 옵션은 비워 두는 것이 기본입니다. 비우면 컬럼 이름으로 필드를 찾습니다(대소문자 무시). 쿼리에서 AS로 기대하는 이름을 붙여 주는 편이, 옵션에 이름을 적는 것보다 대시보드를 옮길 때 덜 깨집니다.
  • title과 showHeader 는 패널 안쪽 왼쪽 위에 그리는 제목입니다. Grafana 패널 제목과는 별개이고, title이 빈 문자열이면 showHeader가 켜져 있어도 그리지 않습니다. Sophonz 대시보드는 Grafana 제목을 쓰고 이 값은 비워 둡니다.
  • 아래 JSON은 실제 v3 대시보드에서 꺼낸 패널 정의입니다. 쿼리마다 붙어 있는 datasource는 패널의 것과 같아 생략했습니다 — 생략해도 Grafana는 패널 데이터소스를 씁니다.

Sophonz KPI

숫자 하나와 이전 기간 대비 증감, 그리고 스파크라인을 함께 보여주는 카드입니다.

Service (v3) 맨 위의 KPI 카드 6개

쿼리를 3개 받습니다. 기본 stat 패널은 헤드라인 숫자·스파크라인·증감률을 모두 동일한 시계열에서 도출하므로, 숫자가 기간 전체의 값이 아니게 됩니다. 실제 하루치 트래픽으로 측정하면 시간 버킷별 고유 세션을 더한 값은 실제 고유 세션 수보다 31% 많고(버킷 경계를 걸친 세션이 두 번 세어집니다), 버킷 평균의 평균은 실제 평균보다 14% 큽니다.

그래서 이 패널은 값을 따로 묻습니다.

쿼리역할
A현재 기간의 값. 이 숫자가 카드에 그대로 표시됩니다
B직전 동일 기간의 같은 값. 증감의 기준입니다
C스파크라인용 시계열. 그리기만 하고 집계에는 쓰지 않습니다

값을 읽는 방식. 각 쿼리 결과에서 첫 번째 숫자 컬럼을 찾고, 그 컬럼의 마지막 유한값을 씁니다. 그래서 A·B는 SELECT … AS v 한 행이면 충분합니다. 스파크라인은 C의 숫자 컬럼 전체를 순서대로 그리며, 값이 없는 칸은 선을 끊습니다.

증감. (A − B) / |B|입니다. B 쿼리가 없거나, 값이 없거나, 0이면 증감을 표시하지 않습니다 — 0에서 늘어난 비율은 무한대라 표시할 수 있는 숫자가 없습니다. 변화가 0.1% 미만이면 ■로 보합 표시합니다.

옵션

옵션기본값설명
label""카드 이름. 비우면 패널 제목을 사용합니다
comparisonLabel""증감 아래 설명. 예: vs 이전 기간. 비우면 숨깁니다
unit""Grafana 단위 id(short, ms, percent, bytes 등). 비우면 none
decimals0소수 자릿수
color#015AE4카드 배경 그라데이션 색. Sophonz 로고 3색은 #015AE4, #00D0CE, #FE52AF
sentimentneutral상승이 좋은 일인지. higher-is-better면 상승이 초록, lower-is-better면 상승이 빨강, neutral이면 회색입니다. 세션 수처럼 오르내림에 좋고 나쁨이 없는 지표에는 neutral이 정직합니다
showSparklinetrue스파크라인 표시 여부
valueRefIdA현재 값을 돌려주는 쿼리의 refId
previousRefIdB직전 기간 값을 돌려주는 쿼리의 refId. 해당 쿼리가 없으면 증감을 그리지 않습니다
seriesRefIdC스파크라인 시계열 쿼리의 refId
{
  "type": "sophonz-kpi-panel",
  "title": "",
  "datasource": {
    "type": "grafana-clickhouse-datasource",
    "uid": "clickhouse"
  },
  "targets": [
    {
      "refId": "A",
      "format": 1,
      "rawSql": "SELECT uniqCombined64(sessionID) AS v\nFROM sophonz_traces.distributed_sophonz_index_v2\nWHERE $__timeFilter(timestamp)\n  AND $__conditionalAll(serviceNamespace IN (${serviceNamespace:singlequote}), $serviceNamespace)\n  AND $__conditionalAll(serviceName IN (${serviceName:singlequote}), $serviceName)"
    },
    {
      "refId": "B",
      "format": 1,
      "rawSql": "SELECT uniqCombined64(sessionID) AS v\nFROM sophonz_traces.distributed_sophonz_index_v2\nWHERE timestamp >= $__fromTime - ($__toTime - $__fromTime) AND timestamp < $__fromTime\n  AND $__conditionalAll(serviceNamespace IN (${serviceNamespace:singlequote}), $serviceNamespace)\n  AND $__conditionalAll(serviceName IN (${serviceName:singlequote}), $serviceName)"
    },
    {
      "refId": "C",
      "format": 1,
      "rawSql": "SELECT $__timeInterval(timestamp) AS t, uniqCombined64(sessionID) AS v\nFROM sophonz_traces.distributed_sophonz_index_v2\nWHERE $__timeFilter(timestamp)\n  AND $__conditionalAll(serviceNamespace IN (${serviceNamespace:singlequote}), $serviceNamespace)\n  AND $__conditionalAll(serviceName IN (${serviceName:singlequote}), $serviceName)\nGROUP BY t\nORDER BY t"
    }
  ],
  "options": {
    "color": "#015AE4",
    "comparisonLabel": "vs 이전 기간",
    "decimals": 0,
    "label": "세션 수",
    "previousRefId": "B",
    "sentiment": "neutral",
    "seriesRefId": "C",
    "showSparkline": true,
    "unit": "short",
    "valueRefId": "A"
  },
  "gridPos": {
    "h": 5,
    "w": 4,
    "x": 0,
    "y": 10
  }
}

Session Timeline

세션 하나가 만든 스팬 전체를 시간축에 늘어놓습니다. 스팬 타입별로 레인을 나눠서, 사용자가 무엇을 했고 그때 무엇이 느렸는지 한 화면에서 읽습니다. 레인 이름 앞의 숫자는 그 레인의 스팬 수, 맨 위 막대는 세션 전체 길이입니다.

세션 하나의 스팬을 유형별 레인으로 늘어놓은 Session Timeline

Canvas로 그리기 때문에 스팬이 수천 개인 세션도 스크롤이 끊기지 않습니다.

  • ctrl/cmd + 휠 — 마우스 위치를 기준으로 확대·축소
  • space를 누른 채 드래그 — 좌우 이동
  • 스팬 위에 올리면 이름·소요 시간·타입 툴팁
  • 스팬 클릭 — 대시보드 변수 traceId·spanId를 그 스팬으로 채웁니다. 빈 곳을 누르면 둘 다 비웁니다
  • 대시보드에 screenStart·screenEnd(epoch ms)가 들어 있으면 그 구간만 확대해서 그립니다. Screen Flow가 화면을 고를 때 채우는 값입니다

레인. render, event, xhr, fetch, error, anr, crash, route, longtask, websocket, webvitals, visibility, connectivity, app_lifecycle, log, user_event 순서입니다. 스팬이 없는 레인은 그리지 않습니다. crash·error·anr은 경고색, event·route·webvitals 같은 순간 이벤트와 10ms 미만 스팬은 점으로 그립니다. 네이티브 스팬(isNative)과 웹 스팬은 색을 달리합니다.

옵션

옵션기본값설명
fullCategoriestrue끄면 render·event·crash·error 레인만 그립니다
timeField""스팬 시작 시각. 비우면 time 타입 필드
spanTypeField""레인을 정하는 스팬 타입
titleField""툴팁에 보이는 스팬 이름
durationField""소요 시간(나노초)
hasErrorField""에러 여부
isNativeField""네이티브 앱 스팬인지(불리언). v3 쿼리는 webVersion = ''로 만듭니다
spanIdField""클릭한 스팬 강조와 var-spanId에 씁니다
traceIdField""클릭 시 var-traceId에 씁니다
{
  "type": "sophonz-session-timeline-panel",
  "title": "Session Timeline",
  "datasource": {
    "type": "grafana-clickhouse-datasource",
    "uid": "clickhouse"
  },
  "targets": [
    {
      "refId": "A",
      "format": 1,
      "editorType": "sql",
      "queryType": "table",
      "rawSql": "SELECT lower(if(appSpanType != '', appSpanType, stringTagMap['span.type'])) AS spanType, timestamp AS startTime, if(httpURL != '', httpURL, if(appScreenName != '', appScreenName, name)) AS title, durationNano, hasError, (webVersion = '') AS isNative, spanID AS spanId, traceID AS traceId, '${spanId}' AS _selspan FROM sophonz_traces.distributed_sophonz_index_v2 WHERE $__timeFilter(timestamp) AND sessionID = '${sessionId}' AND isRootSpan = true AND $__conditionalAll(serviceNamespace IN (${serviceNamespace:singlequote}), $serviceNamespace) ORDER BY timestamp"
    }
  ],
  "options": {
    "durationField": "durationNano",
    "fullCategories": true,
    "hasErrorField": "hasError",
    "isNativeField": "isNative",
    "showHeader": true,
    "spanIdField": "spanId",
    "spanTypeField": "spanType",
    "timeField": "startTime",
    "title": "",
    "titleField": "title",
    "traceIdField": "traceId"
  },
  "gridPos": {
    "h": 10,
    "w": 24,
    "x": 0,
    "y": 0
  }
}

Screen Flow

세션의 화면 이동을 그래프로 만듭니다. 노드가 화면이고 × 뒤의 숫자는 그 화면에서 나온 스팬 수, 엣지가 화면 사이 이동이고, 두 번 이상이면 횟수가 붙습니다. 이동이 잦을수록 엣지가 굵어집니다. 에러가 난 스팬이 하나라도 있는 화면은 빨갛게 표시합니다.

안드로이드 세션의 화면 이동을 그린 Screen Flow

그래프는 세 단계로 펼쳐집니다.

  1. 화면 노드 클릭 — 그 화면을 스팬 타입별 묶음(xhr ×12 같은)으로 펼칩니다. 동시에 그 화면에 머문 구간을 screenStart·screenEnd 변수에 넣어, 같은 대시보드의 Session Timeline이 그 구간으로 확대됩니다. 다시 누르면 접히고 변수를 비웁니다.
  2. 묶음 클릭 — 개별 항목으로 펼칩니다. fetch·xhr은 [200] /api/orders처럼 상태 코드와 URL, 이벤트는 click: 결제하기처럼 이벤트 이름과 요소 이름, 나머지는 스팬 이름으로 표시합니다. 묶음 하나에 항목은 최대 80개까지 그립니다.
  3. 항목 클릭 — traceId·spanId를 채워 트레이스 리스트와 간트 뷰를 그 트레이스로 옮깁니다.

옵션

옵션기본값설명
directionLR레이아웃 방향. LR 가로, TB 세로
timeField""정렬에 쓰는 스팬 시각
screenField""화면 이름. 노드 id가 됩니다
spanTypeField""화면 안 묶음을 나누는 스팬 타입
hasErrorField""에러 여부
titleField""항목 이름
urlField""fetch·xhr 항목의 URL
statusField""fetch·xhr 항목의 HTTP 상태 코드
eventField""event 항목의 요소 이름
spanIdField, traceIdField""항목 클릭 시 채울 값
{
  "type": "sophonz-session-timeline-graph-panel",
  "title": "화면 흐름 (Screen Flow)",
  "datasource": {
    "type": "grafana-clickhouse-datasource",
    "uid": "clickhouse"
  },
  "targets": [
    {
      "refId": "A",
      "format": 1,
      "editorType": "sql",
      "queryType": "table",
      "rawSql": "SELECT timestamp AS time, if(appScreenName != '', appScreenName, stringTagMap['screen.name']) AS screen, lower(if(appSpanType != '', appSpanType, stringTagMap['span.type'])) AS spanType, hasError, if(httpURL != '', httpURL, if(appScreenName != '', appScreenName, name)) AS title, if(stringTagMap['http.url'] != '', stringTagMap['http.url'], httpURL) AS url, responseStatusCode AS statusCode, if(stringTagMap['target_element_text'] != '', stringTagMap['target_element_text'], stringTagMap['target_element_tag']) AS elementName, spanID AS spanId, traceID AS traceId, '${spanId}' AS _selSpan, '${traceId}' AS _selTrace FROM sophonz_traces.distributed_sophonz_index_v2 WHERE $__timeFilter(timestamp) AND sessionID = '${sessionId}' AND isRootSpan = true AND $__conditionalAll(serviceNamespace IN (${serviceNamespace:singlequote}), $serviceNamespace) ORDER BY timestamp"
    }
  ],
  "options": {
    "direction": "LR",
    "eventField": "elementName",
    "hasErrorField": "hasError",
    "screenField": "screen",
    "showHeader": true,
    "spanIdField": "spanId",
    "spanTypeField": "spanType",
    "statusField": "statusCode",
    "timeField": "time",
    "title": "",
    "titleField": "title",
    "traceIdField": "traceId",
    "urlField": "url"
  },
  "gridPos": {
    "h": 13,
    "w": 12,
    "x": 0,
    "y": 10
  }
}

Service Flow

세션(또는 트레이스)이 지나간 서비스를 노드-엣지로 그립니다. 노드는 서비스이고, 클릭하면 그 안의 컴포넌트로 펼쳐집니다. 패널 머리에는 서비스 수·호출 수·트레이스 수가 표시됩니다.

웹 세션이 Express 서버를 거쳐 Prisma와 pg 클라이언트까지 내려가는 Service Flow

컴포넌트 이름. 스팬 타입이 있으면 그것을 쓰고, 없으면 스팬 이름으로 정합니다.

스팬 이름컴포넌트
prisma:로 시작prisma
pg.로 시작pg
middleware로 시작middleware
request handler로 시작handler
Planner, Executor, SeqScan, IndexScan …query plan
서비스 이름이 postgres·mysql·redis·clickhousedatabase
그 밖server (프론트엔드 서비스는 page)

계층. 노드 색을 정합니다.

계층판별 기준
Frontend그 서비스의 스팬 중 하나라도 clientPlatform이 web 또는 browser
Infrastructure서비스 이름이 postgres, mysql, maria, mongo, redis, clickhouse, elastic, kafka, rabbit, memcached, cassandra, dynamodb 중 하나를 포함
Backend나머지

NOTE — 모바일 앱은 Backend로 그려집니다

Frontend 판정은 web·browser만 봅니다. iOS·Android SDK가 보낸 서비스는 서비스 이름이 위 패턴에 걸리지 않는 한 Backend 색으로 표시됩니다.

엣지. 시간 순서가 아니라 부모/자식 스팬 ID로 잇습니다. 브라우저와 서버 사이에는 시계 오차가 있어서, 시간으로 재구성하면 순서가 뒤집힐 수 있습니다. 부모 스팬이 결과에 없으면 엣지를 만들지 않으므로, 쿼리에서 isRootSpan 조건을 걸면 서비스 사이 호출이 사라집니다.

같은 두 노드 사이 호출은 한 엣지로 합칩니다. 한 번이면 12.3ms, 여러 번이면 12.3ms avg · 5×처럼 평균과 횟수를 붙입니다. 서비스 단위 엣지는 서비스가 다를 때만, 컴포넌트 단위 엣지는 컴포넌트가 다를 때만 생깁니다.

옵션

옵션기본값설명
directionLR레이아웃 방향. LR 가로, TB 세로
expandAllfalse처음부터 모든 서비스를 컴포넌트까지 펼칩니다. 트레이스 하나를 볼 때 알맞고, 세션 전체라면 끄는 편이 읽기 좋습니다
spanIdField""비우면 spanID
parentSpanIdField""비우면 parentSpanID
serviceField""비우면 serviceName
nameField""스팬 타입이 없을 때 컴포넌트 이름을 정하는 스팬 이름
durationField""비우면 durationNano. 필드 이름이 ms로 끝나면 밀리초로 읽습니다
hasErrorField""비우면 hasError
spanTypeField""비우면 appSpanType
clientPlatformField""비우면 clientPlatform. Frontend 판정에 씁니다

spanID와 serviceName에 해당하는 필드가 없는 결과 프레임은 건너뜁니다. traceID 컬럼이 있으면 트레이스 수를 셉니다.

{
  "type": "sophonz-service-flow-panel",
  "title": "Service Flow",
  "datasource": {
    "type": "grafana-clickhouse-datasource",
    "uid": "clickhouse"
  },
  "targets": [
    {
      "refId": "A",
      "format": 1,
      "editorType": "sql",
      "rawSql": "SELECT\n  spanID, parentSpanID, serviceName, name, appSpanType, clientPlatform,\n  durationNano, hasError, traceID\nFROM sophonz_traces.distributed_sophonz_index_v2\nWHERE $__timeFilter(timestamp)\n  AND sessionID = '${sessionId}'\n  AND $__conditionalAll(serviceNamespace IN (${serviceNamespace:singlequote}), $serviceNamespace)\nORDER BY timestamp ASC\nLIMIT 20000"
    }
  ],
  "options": {
    "direction": "LR",
    "durationField": "",
    "expandAll": false,
    "hasErrorField": "",
    "nameField": "",
    "parentSpanIdField": "",
    "serviceField": "",
    "showHeader": true,
    "spanIdField": "",
    "spanTypeField": "",
    "title": ""
  },
  "gridPos": {
    "h": 13,
    "w": 12,
    "x": 12,
    "y": 10
  }
}

Request Map

HTTP·XHR 요청의 응답 시간을 시간축 히트맵으로 그립니다. 가로가 시각, 세로가 응답 시간(로그 축, 최대 10초)입니다. 셀 색은 그 칸에서 가장 많았던 상태 코드 계열(2xx/3xx/4xx/5xx), 진하기는 요청 수를 5단계로 나눈 것입니다.

Service (v3)의 리퀘스트 히트맵. 셀에 올리면 구간과 상태별 건수가 나옵니다

셀에 마우스를 올리면 시간 구간, 응답 시간 구간, 총 요청 수, 상태 계열별 건수를 보여줍니다.

셀 크기. 셀은 정사각형입니다. 가로 구간 폭은 2.5초·5초·10초·15초·30초·1분·2분·5분·10분 가운데, 셀이 4px 이상이 되는 가장 작은 값을 고릅니다. 셀 크기는 최대 18px, 세로 칸 수는 8–120개입니다. 상태 코드가 2xx–5xx 밖이면(예: 0) 5xx로 셉니다.

옵션

옵션기본값설명
binMs0가로 구간 폭(ms). 0이면 패널 크기에 맞춰 자동. 지정한 폭으로 셀이 4px보다 작아지면 더 넓은 폭으로 올립니다
timeField""요청 시각. 비우면 time 타입 필드
responseTimeField""응답 시간(ms). 비우면 숫자 컬럼 중 이름에 responsetime, duration, latency, elapsed 등이 들어간 것
statusField""HTTP 상태 코드. 비우면 statuscode, status, code 등
countField""미리 집계한 행의 건수. 비우면 이름에 count, hits, requests, total이 들어간 숫자 컬럼, 그것도 없으면 행 하나를 1건으로

응답 시간과 상태 코드 필드를 둘 다 찾지 못한 결과 프레임은 건너뜁니다. 요청이 많은 서비스라면 원본 행 대신 countField로 미리 집계한 결과를 넘기는 편이 가볍습니다.

{
  "type": "sophonz-requestmap-panel",
  "title": "리퀘스트 히트맵 (Request Map)",
  "datasource": {
    "type": "grafana-clickhouse-datasource",
    "uid": "clickhouse"
  },
  "targets": [
    {
      "refId": "A",
      "format": 1,
      "editorType": "sql",
      "queryType": "table",
      "rawSql": "SELECT timestamp AS time, durationNano / 1e6 AS responseTime, responseStatusCode AS statusCode FROM sophonz_traces.distributed_sophonz_index_v2 WHERE $__timeFilter(timestamp) AND lower(if(appSpanType != '', appSpanType, stringTagMap['span.type'])) IN ('xhr','fetch') AND $__conditionalAll(serviceNamespace IN (${serviceNamespace:singlequote}), $serviceNamespace) AND $__conditionalAll(serviceName IN (${serviceName:singlequote}), $serviceName) ORDER BY timestamp LIMIT 100000"
    }
  ],
  "options": {
    "binMs": 0,
    "countField": "",
    "responseTimeField": "responseTime",
    "showHeader": true,
    "statusField": "statusCode",
    "timeField": "time",
    "title": ""
  },
  "gridPos": {
    "h": 10,
    "w": 12,
    "x": 12,
    "y": 0
  }
}

DynamicXHR Monitor

요청 트래픽을 애니메이션으로 보여주는 패널입니다. 요청이 점으로 흘러 들어와 가운데 띠를 지나가고, 띠는 성공(파랑)과 실패(분홍) 비율로 나뉩니다. 비율은 띠 위에, 세션 수는 왼쪽 아래, 성공·에러 수는 오른쪽 아래에 표시됩니다.

현재 시간 범위의 요청과 실패 비율을 보여주는 DynamicXHR Monitor

지표를 읽는 화면이라기보다 지금 트래픽이 흐르고 있는지를 한눈에 확인하는 용도입니다.

값을 읽는 방식. 숫자 세 개(total, error, session)만 받습니다. 시계열이 오면 각 컬럼의 마지막 값을 씁니다. 실패 비율은 round(error / total × 100), 성공은 그 나머지이고, error가 total보다 크면 total로 자릅니다. v3 쿼리는 상태 코드가 0이거나 400 이상인 xhr·fetch를 실패로 셉니다.

옵션

옵션기본값설명
showCountstrue아래쪽 Sessions / OK / Errors 수. 1000 이상은 62.1k처럼 줄입니다
showPercenttrue가운데 성공·실패 비율. 요청이 0건이면 숨깁니다
bounceDirectionhorizontal가운데 띠가 튕기며 움직이는 축. horizontal 또는 vertical
totalField""전체 요청 수. 비우면 이름에 total, request, count 등이 들어간 숫자 컬럼
errorField""실패 수. 비우면 error, fail, 5xx, 4xx 등
sessionField""세션 수. 비우면 session, user, visitor 등

이름으로 못 찾은 값은 남은 숫자 컬럼을 순서대로 total → session → error에 채웁니다. 쿼리에서 AS total, AS error, AS session으로 이름을 붙여 두면 헷갈릴 일이 없습니다.

{
  "type": "sophonz-dynamicxhr-panel",
  "title": "DynamicXHR Monitor",
  "datasource": {
    "type": "grafana-clickhouse-datasource",
    "uid": "clickhouse"
  },
  "targets": [
    {
      "refId": "A",
      "format": 1,
      "editorType": "sql",
      "queryType": "table",
      "rawSql": "SELECT countIf(lower(if(appSpanType != '', appSpanType, stringTagMap['span.type'])) IN ('xhr','fetch')) AS total, countIf(lower(if(appSpanType != '', appSpanType, stringTagMap['span.type'])) IN ('xhr','fetch') AND (responseStatusCode = 0 OR responseStatusCode >= 400)) AS error, uniqCombined64(sessionID) AS session FROM sophonz_traces.distributed_sophonz_index_v2 WHERE $__timeFilter(timestamp) AND $__conditionalAll(serviceNamespace IN (${serviceNamespace:singlequote}), $serviceNamespace) AND $__conditionalAll(serviceName IN (${serviceName:singlequote}), $serviceName)"
    }
  ],
  "options": {
    "bounceDirection": "horizontal",
    "errorField": "error",
    "sessionField": "session",
    "showCounts": true,
    "showHeader": true,
    "showPercent": true,
    "title": "",
    "totalField": "total"
  },
  "gridPos": {
    "h": 10,
    "w": 12,
    "x": 0,
    "y": 0
  }
}