From 75a66217e9ef2e8cbbd53950bd9cd9606e5e1603 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Wed, 3 Jun 2026 16:27:04 -0400 Subject: [PATCH 1/7] feat: add Browser RUM dashboard template Adds a "Browser RUM" template to the dashboards gallery for browser sessions instrumented with the HyperDX Browser SDK (or any OTel browser instrumentation emitting a rum.sessionId resource attribute): - Performance Overview: page-view/session/error KPIs, Core Web Vitals (LCP/INP/CLS) p75, median/p75/p90 page-load percentiles, long tasks - Page Views Breakdown: traffic by URL, browser, country, device size (derived from screen.xy) - Errors section with tabs (overview, JS exceptions by message and by page, failing API calls) - Six dashboard filters: Service, Environment, Service Version, Page URL, Browser, Country Top Browsers / Top Countries tiles and the Browser/Country filters populate when the collector's useragent and geoip processors are on. --- .changeset/browser-rum-dashboard-template.md | 12 + .../src/dashboardTemplates/browser-rum.json | 866 ++++++++++++++++++ packages/app/src/dashboardTemplates/index.ts | 2 + 3 files changed, 880 insertions(+) create mode 100644 .changeset/browser-rum-dashboard-template.md create mode 100644 packages/app/src/dashboardTemplates/browser-rum.json diff --git a/.changeset/browser-rum-dashboard-template.md b/.changeset/browser-rum-dashboard-template.md new file mode 100644 index 0000000000..a61ba48dbe --- /dev/null +++ b/.changeset/browser-rum-dashboard-template.md @@ -0,0 +1,12 @@ +--- +'@hyperdx/app': minor +--- + +feat: add Browser RUM dashboard template + +- New "Browser RUM" template in the dashboards gallery for browser sessions instrumented with the HyperDX Browser SDK (or any OTel browser instrumentation emitting a `rum.sessionId` resource attribute) +- Performance Overview section: page-view/session/error KPIs, Core Web Vitals (LCP/INP/CLS) p75, median/p75/p90 page-load percentiles, and long-task health +- Page Views Breakdown section: traffic grouped by URL, browser, country, and device size (derived from `screen.xy`) +- Errors section with tabs for an overview, JS exceptions (by message and by page), and failing API calls +- Six dashboard-level filters: Service, Environment, Service Version, Page URL, Browser, and Country +- Top Browsers and Top Countries tiles (and the Browser/Country filters) populate when the OTel collector's `useragent` and `geoip` processors are enabled diff --git a/packages/app/src/dashboardTemplates/browser-rum.json b/packages/app/src/dashboardTemplates/browser-rum.json new file mode 100644 index 0000000000..b9efd4f396 --- /dev/null +++ b/packages/app/src/dashboardTemplates/browser-rum.json @@ -0,0 +1,866 @@ +{ + "version": "0.1.0", + "name": "Browser RUM", + "description": "Real-user monitoring overview for browser sessions instrumented with the HyperDX Browser SDK (or any OpenTelemetry browser instrumentation that emits a rum.sessionId resource attribute). Performance Overview shows KPIs, load-time percentiles, and long-task health. Page Views Breakdown groups traffic by URL, browser, country, and device. Errors drills into JS exceptions, failing API calls, and per-page error counts. Top Browsers and Top Countries tiles populate when the OTel collector's useragent and geoip processors are enabled.", + "tags": ["Browser / RUM"], + "filters": [ + { + "id": "rum-filter-service", + "type": "QUERY_EXPRESSION", + "name": "Service", + "expression": "ServiceName", + "source": "Traces", + "where": "ResourceAttributes.rum.sessionId:*", + "whereLanguage": "lucene" + }, + { + "id": "rum-filter-environment", + "type": "QUERY_EXPRESSION", + "name": "Environment", + "expression": "ResourceAttributes['deployment.environment']", + "source": "Traces", + "where": "ResourceAttributes.rum.sessionId:* AND ResourceAttributes.deployment.environment:*", + "whereLanguage": "lucene" + }, + { + "id": "rum-filter-version", + "type": "QUERY_EXPRESSION", + "name": "Service Version", + "expression": "ResourceAttributes['service.version']", + "source": "Traces", + "where": "ResourceAttributes.rum.sessionId:* AND ResourceAttributes.service.version:*", + "whereLanguage": "lucene" + }, + { + "id": "rum-filter-url", + "type": "QUERY_EXPRESSION", + "name": "Page URL", + "expression": "SpanAttributes['location.href']", + "source": "Traces", + "where": "ResourceAttributes.rum.sessionId:* AND SpanAttributes.location.href:*", + "whereLanguage": "lucene" + }, + { + "id": "rum-filter-browser", + "type": "QUERY_EXPRESSION", + "name": "Browser", + "expression": "ResourceAttributes['user_agent.name']", + "source": "Traces", + "where": "ResourceAttributes.rum.sessionId:* AND ResourceAttributes.user_agent.name:*", + "whereLanguage": "lucene" + }, + { + "id": "rum-filter-country", + "type": "QUERY_EXPRESSION", + "name": "Country", + "expression": "ResourceAttributes['geo.country.name']", + "source": "Traces", + "where": "ResourceAttributes.rum.sessionId:* AND ResourceAttributes.geo.country.name:*", + "whereLanguage": "lucene" + } + ], + "containers": [ + { + "id": "rum-perf", + "title": "Performance Overview", + "collapsed": false + }, + { + "id": "rum-breakdown", + "title": "Page Views Breakdown", + "collapsed": false + }, + { + "id": "rum-errors", + "title": "Errors", + "collapsed": false, + "tabs": [ + { "id": "rum-errors-overview", "title": "Overview" }, + { "id": "rum-errors-js", "title": "JS Exceptions" }, + { "id": "rum-errors-api", "title": "API Failures" } + ] + } + ], + "tiles": [ + { + "id": "rum-006", + "containerId": "rum-perf", + "x": 0, + "y": 0, + "w": 6, + "h": 4, + "config": { + "name": "Page Views", + "source": "Traces", + "displayType": "number", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [{ "aggFn": "count", "valueExpression": "" }], + "where": "SpanName:\"documentLoad\" OR SpanName:\"routeChange\" OR SpanAttributes.component:\"page-view\" OR SpanAttributes.component:\"navigation\"", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-017", + "containerId": "rum-perf", + "x": 6, + "y": 0, + "w": 6, + "h": 4, + "config": { + "name": "Median Page Load (ms)", + "source": "Traces", + "displayType": "number", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "quantile", + "level": 0.5, + "valueExpression": "Duration / 1000000", + "aggCondition": "", + "aggConditionLanguage": "lucene" + } + ], + "where": "SpanName:\"documentLoad\"", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-018", + "containerId": "rum-perf", + "x": 12, + "y": 0, + "w": 6, + "h": 4, + "config": { + "name": "p90 Page Load (ms)", + "source": "Traces", + "displayType": "number", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "quantile", + "level": 0.9, + "valueExpression": "Duration / 1000000", + "aggCondition": "", + "aggConditionLanguage": "lucene" + } + ], + "where": "SpanName:\"documentLoad\"", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-001", + "containerId": "rum-perf", + "x": 18, + "y": 0, + "w": 6, + "h": 4, + "config": { + "name": "LCP p75 (ms)", + "source": "Traces", + "displayType": "number", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "quantile", + "level": 0.75, + "valueExpression": "toFloat64OrZero(SpanAttributes['lcp'])", + "aggCondition": "", + "aggConditionLanguage": "lucene" + } + ], + "where": "SpanName:\"webvitals\" AND SpanAttributes.lcp:*", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-002", + "containerId": "rum-perf", + "x": 0, + "y": 4, + "w": 6, + "h": 4, + "config": { + "name": "INP p75 (ms)", + "source": "Traces", + "displayType": "number", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "quantile", + "level": 0.75, + "valueExpression": "toFloat64OrZero(SpanAttributes['inp'])", + "aggCondition": "", + "aggConditionLanguage": "lucene" + } + ], + "where": "SpanName:\"webvitals\" AND SpanAttributes.inp:*", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-003", + "containerId": "rum-perf", + "x": 6, + "y": 4, + "w": 6, + "h": 4, + "config": { + "name": "CLS p75", + "source": "Traces", + "displayType": "number", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "quantile", + "level": 0.75, + "valueExpression": "toFloat64OrZero(SpanAttributes['cls'])", + "aggCondition": "", + "aggConditionLanguage": "lucene" + } + ], + "where": "SpanName:\"webvitals\" AND SpanAttributes.cls:*", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 3, + "thousandSeparated": true + } + } + }, + { + "id": "rum-005", + "containerId": "rum-perf", + "x": 12, + "y": 4, + "w": 6, + "h": 4, + "config": { + "name": "Active Sessions", + "source": "Traces", + "displayType": "number", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count_distinct", + "valueExpression": "ResourceAttributes['rum.sessionId']" + } + ], + "where": "ResourceAttributes.rum.sessionId:*", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-019", + "containerId": "rum-perf", + "x": 18, + "y": 4, + "w": 6, + "h": 4, + "config": { + "name": "Sessions w/ Errors", + "source": "Traces", + "displayType": "number", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count_distinct", + "valueExpression": "ResourceAttributes['rum.sessionId']" + } + ], + "where": "ResourceAttributes.rum.sessionId:* AND StatusCode:error", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-020", + "containerId": "rum-perf", + "x": 0, + "y": 8, + "w": 24, + "h": 7, + "config": { + "name": "Page Load — median, p75 and p90 (ms)", + "source": "Traces", + "displayType": "line", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "quantile", + "level": 0.5, + "valueExpression": "Duration / 1000000", + "aggCondition": "", + "aggConditionLanguage": "lucene", + "alias": "median" + }, + { + "aggFn": "quantile", + "level": 0.75, + "valueExpression": "Duration / 1000000", + "aggCondition": "", + "aggConditionLanguage": "lucene", + "alias": "p75" + }, + { + "aggFn": "quantile", + "level": 0.9, + "valueExpression": "Duration / 1000000", + "aggCondition": "", + "aggConditionLanguage": "lucene", + "alias": "p90" + } + ], + "where": "SpanName:\"documentLoad\"", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-021", + "containerId": "rum-perf", + "x": 0, + "y": 15, + "w": 12, + "h": 6, + "config": { + "name": "Page Views over Time", + "source": "Traces", + "displayType": "line", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "alias": "Page Views" + } + ], + "where": "SpanName:\"documentLoad\" OR SpanName:\"routeChange\" OR SpanAttributes.component:\"page-view\" OR SpanAttributes.component:\"navigation\"", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-024", + "containerId": "rum-perf", + "x": 12, + "y": 15, + "w": 12, + "h": 6, + "config": { + "name": "Long Tasks over Time", + "source": "Traces", + "displayType": "stacked_bar", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "alias": "Long Tasks" + } + ], + "where": "SpanName:\"longtask\"", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-014", + "containerId": "rum-breakdown", + "x": 0, + "y": 0, + "w": 8, + "h": 8, + "config": { + "name": "Top URLs", + "source": "Traces", + "displayType": "table", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "alias": "Views" + }, + { + "aggFn": "count_distinct", + "valueExpression": "ResourceAttributes['rum.sessionId']", + "alias": "Sessions" + } + ], + "where": "SpanName:\"documentLoad\" OR SpanName:\"routeChange\" OR SpanAttributes.component:\"page-view\" OR SpanAttributes.component:\"navigation\"", + "whereLanguage": "lucene", + "groupBy": [ + { + "valueExpression": "coalesce(nullif(SpanAttributes['http.url'], ''), nullif(SpanAttributes['page.url'], ''), nullif(SpanAttributes['location.href'], ''))", + "alias": "URL" + } + ], + "orderBy": "Views DESC", + "limit": { "limit": 20 } + } + }, + { + "id": "rum-026", + "containerId": "rum-breakdown", + "x": 8, + "y": 0, + "w": 8, + "h": 8, + "config": { + "name": "Top Browsers (requires useragent processor)", + "source": "Traces", + "displayType": "table", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count_distinct", + "valueExpression": "ResourceAttributes['rum.sessionId']", + "alias": "Sessions" + }, + { + "aggFn": "count", + "valueExpression": "", + "alias": "Views" + } + ], + "where": "ResourceAttributes.rum.sessionId:*", + "whereLanguage": "lucene", + "groupBy": [ + { + "valueExpression": "coalesce(nullif(ResourceAttributes['user_agent.name'], ''), nullif(SpanAttributes['user_agent.name'], ''), nullif(ResourceAttributes['user_agent.original'], ''), nullif(SpanAttributes['user_agent.original'], ''))", + "alias": "Browser" + } + ], + "orderBy": "Sessions DESC", + "limit": { "limit": 10 } + } + }, + { + "id": "rum-027", + "containerId": "rum-breakdown", + "x": 16, + "y": 0, + "w": 8, + "h": 8, + "config": { + "name": "Top Countries (requires geoip processor)", + "source": "Traces", + "displayType": "table", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count_distinct", + "valueExpression": "ResourceAttributes['rum.sessionId']", + "alias": "Sessions" + }, + { + "aggFn": "count", + "valueExpression": "", + "alias": "Views" + } + ], + "where": "ResourceAttributes.rum.sessionId:*", + "whereLanguage": "lucene", + "groupBy": [ + { + "valueExpression": "coalesce(nullif(ResourceAttributes['geo.country.name'], ''), nullif(SpanAttributes['geo.country.name'], ''), nullif(ResourceAttributes['geo.country.iso_code'], ''), nullif(SpanAttributes['geo.country.iso_code'], ''))", + "alias": "Country" + } + ], + "orderBy": "Sessions DESC", + "limit": { "limit": 10 } + } + }, + { + "id": "rum-028", + "containerId": "rum-breakdown", + "x": 0, + "y": 8, + "w": 12, + "h": 8, + "config": { + "name": "Top Device Sizes", + "source": "Traces", + "displayType": "table", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count_distinct", + "valueExpression": "ResourceAttributes['rum.sessionId']", + "alias": "Sessions" + }, + { + "aggFn": "count", + "valueExpression": "", + "alias": "Views" + } + ], + "where": "SpanName:\"documentLoad\" AND SpanAttributes.screen.xy:*", + "whereLanguage": "lucene", + "groupBy": [ + { + "valueExpression": "multiIf(toUInt32OrZero(splitByChar('x', SpanAttributes['screen.xy'])[1]) < 600, 'Mobile (<600px)', toUInt32OrZero(splitByChar('x', SpanAttributes['screen.xy'])[1]) < 1024, 'Tablet (600–1024px)', toUInt32OrZero(splitByChar('x', SpanAttributes['screen.xy'])[1]) < 1440, 'Laptop (1024–1440px)', 'Desktop (≥1440px)')", + "alias": "Device" + } + ], + "orderBy": "Sessions DESC", + "limit": { "limit": 10 } + } + }, + { + "id": "rum-011", + "containerId": "rum-breakdown", + "x": 12, + "y": 8, + "w": 12, + "h": 8, + "config": { + "name": "Slowest Pages (p75 page load)", + "source": "Traces", + "displayType": "table", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "quantile", + "level": 0.75, + "valueExpression": "Duration / 1000000", + "aggCondition": "", + "aggConditionLanguage": "lucene", + "alias": "Page Load p75 (ms)" + }, + { + "aggFn": "count", + "valueExpression": "", + "alias": "Views" + } + ], + "where": "SpanName:\"documentLoad\"", + "whereLanguage": "lucene", + "groupBy": [ + { + "valueExpression": "coalesce(nullif(SpanAttributes['http.url'], ''), nullif(SpanAttributes['page.url'], ''), nullif(SpanAttributes['location.href'], ''))", + "alias": "URL" + } + ], + "orderBy": "`Page Load p75 (ms)` DESC", + "limit": { "limit": 20 } + } + }, + { + "id": "rum-016", + "containerId": "rum-breakdown", + "x": 0, + "y": 16, + "w": 24, + "h": 8, + "config": { + "name": "Top Errored Sessions", + "source": "Traces", + "displayType": "table", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "aggCondition": "StatusCode:error", + "aggConditionLanguage": "lucene", + "alias": "Errors" + }, + { + "aggFn": "count_distinct", + "valueExpression": "TraceId", + "alias": "Traces" + }, + { + "aggFn": "any", + "valueExpression": "nullif(SpanAttributes['userEmail'], '')", + "alias": "User" + }, + { + "aggFn": "any", + "valueExpression": "ServiceName", + "alias": "Service" + } + ], + "where": "ResourceAttributes.rum.sessionId:*", + "whereLanguage": "lucene", + "groupBy": [ + { + "valueExpression": "ResourceAttributes['rum.sessionId']", + "alias": "Session" + } + ], + "having": "Errors > 0", + "havingLanguage": "sql", + "orderBy": "Errors DESC", + "limit": { "limit": 20 } + } + }, + { + "id": "rum-007", + "containerId": "rum-errors", + "tabId": "rum-errors-overview", + "x": 0, + "y": 0, + "w": 12, + "h": 4, + "config": { + "name": "JS Errors", + "source": "Traces", + "displayType": "number", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [{ "aggFn": "count", "valueExpression": "" }], + "where": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-008", + "containerId": "rum-errors", + "tabId": "rum-errors-overview", + "x": 12, + "y": 0, + "w": 12, + "h": 4, + "config": { + "name": "AJAX Errors", + "source": "Traces", + "displayType": "number", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [{ "aggFn": "count", "valueExpression": "" }], + "where": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "whereLanguage": "sql", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-010", + "containerId": "rum-errors", + "tabId": "rum-errors-overview", + "x": 0, + "y": 4, + "w": 24, + "h": 7, + "config": { + "name": "Errors over Time", + "source": "Traces", + "displayType": "line", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "aggCondition": "StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "aggConditionLanguage": "lucene", + "alias": "JS Errors" + }, + { + "aggFn": "count", + "valueExpression": "", + "aggCondition": "(SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\") AND StatusCode:error", + "aggConditionLanguage": "lucene", + "alias": "AJAX Errors" + } + ], + "where": "ResourceAttributes.rum.sessionId:*", + "whereLanguage": "lucene", + "numberFormat": { + "output": "number", + "mantissa": 0, + "thousandSeparated": true + } + } + }, + { + "id": "rum-012", + "containerId": "rum-errors", + "tabId": "rum-errors-js", + "x": 0, + "y": 0, + "w": 12, + "h": 8, + "config": { + "name": "Top JS Errors (by message)", + "source": "Traces", + "displayType": "table", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "alias": "Occurrences" + }, + { + "aggFn": "count_distinct", + "valueExpression": "ResourceAttributes['rum.sessionId']", + "alias": "Sessions" + } + ], + "where": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "whereLanguage": "lucene", + "groupBy": [ + { + "valueExpression": "coalesce(nullif(SpanAttributes['exception.message'], ''), nullif(SpanAttributes['message'], ''), SpanName)", + "alias": "Error" + } + ], + "orderBy": "Occurrences DESC", + "limit": { "limit": 20 } + } + }, + { + "id": "rum-015", + "containerId": "rum-errors", + "tabId": "rum-errors-js", + "x": 12, + "y": 0, + "w": 12, + "h": 8, + "config": { + "name": "Errors per Page", + "source": "Traces", + "displayType": "table", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "alias": "Errors" + }, + { + "aggFn": "count_distinct", + "valueExpression": "ResourceAttributes['rum.sessionId']", + "alias": "Sessions" + } + ], + "where": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "whereLanguage": "lucene", + "groupBy": [ + { + "valueExpression": "coalesce(nullif(SpanAttributes['http.url'], ''), nullif(SpanAttributes['page.url'], ''), nullif(SpanAttributes['location.href'], ''))", + "alias": "URL" + } + ], + "orderBy": "Errors DESC", + "limit": { "limit": 20 } + } + }, + { + "id": "rum-013", + "containerId": "rum-errors", + "tabId": "rum-errors-api", + "x": 0, + "y": 0, + "w": 24, + "h": 8, + "config": { + "name": "Top Failing API Calls", + "source": "Traces", + "displayType": "table", + "granularity": "auto", + "alignDateRangeToGranularity": true, + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "alias": "Failures" + }, + { + "aggFn": "count_distinct", + "valueExpression": "ResourceAttributes['rum.sessionId']", + "alias": "Sessions" + } + ], + "where": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "whereLanguage": "sql", + "groupBy": [ + { + "valueExpression": "concat(coalesce(nullif(SpanAttributes['http.method'], ''), 'GET'), ' ', coalesce(nullif(SpanAttributes['http.url'], ''), ''), ' → ', coalesce(nullif(SpanAttributes['http.status_code'], ''), 'error'))", + "alias": "Endpoint" + } + ], + "orderBy": "Failures DESC", + "limit": { "limit": 20 } + } + } + ] +} diff --git a/packages/app/src/dashboardTemplates/index.ts b/packages/app/src/dashboardTemplates/index.ts index 609affa295..58e84c5684 100644 --- a/packages/app/src/dashboardTemplates/index.ts +++ b/packages/app/src/dashboardTemplates/index.ts @@ -3,6 +3,7 @@ import { DashboardTemplateSchema, } from '@hyperdx/common-utils/dist/types'; +import browserRum from './browser-rum.json'; import dotnetRuntime from './dotnet-runtime.json'; import goRuntime from './go-runtime.json'; import jvmRuntimeMetrics from './jvm-runtime-metrics.json'; @@ -23,6 +24,7 @@ function parseTemplate( } const templates: Record = { + 'browser-rum': browserRum, 'dotnet-runtime': dotnetRuntime, 'go-runtime': goRuntime, 'jvm-runtime-metrics': jvmRuntimeMetrics, From d1b35c0546d4463f211d0e76c853ea8a21306b37 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Wed, 3 Jun 2026 16:45:16 -0400 Subject: [PATCH 2/7] chore: shorten Browser RUM template description Trim the dashboard description to a single sentence to match the length and style of the existing runtime-metrics templates. --- packages/app/src/dashboardTemplates/browser-rum.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/dashboardTemplates/browser-rum.json b/packages/app/src/dashboardTemplates/browser-rum.json index b9efd4f396..2473e05c7f 100644 --- a/packages/app/src/dashboardTemplates/browser-rum.json +++ b/packages/app/src/dashboardTemplates/browser-rum.json @@ -1,7 +1,7 @@ { "version": "0.1.0", "name": "Browser RUM", - "description": "Real-user monitoring overview for browser sessions instrumented with the HyperDX Browser SDK (or any OpenTelemetry browser instrumentation that emits a rum.sessionId resource attribute). Performance Overview shows KPIs, load-time percentiles, and long-task health. Page Views Breakdown groups traffic by URL, browser, country, and device. Errors drills into JS exceptions, failing API calls, and per-page error counts. Top Browsers and Top Countries tiles populate when the OTel collector's useragent and geoip processors are enabled.", + "description": "Core Web Vitals, page-load percentiles, traffic breakdowns, and error tracking for browser sessions instrumented with the HyperDX Browser SDK (or any OpenTelemetry RUM instrumentation emitting rum.sessionId).", "tags": ["Browser / RUM"], "filters": [ { From 6fbbf49e3ba0a9a30d589ab2a7eecd28fd1fbefb Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Wed, 3 Jun 2026 17:09:37 -0400 Subject: [PATCH 3/7] fix: derive Top Browsers from http.user_agent; drop Browser filter The browser is already captured out of the box: the OTel document-load instrumentation sets http.user_agent (navigator.userAgent) on documentLoad spans. The template was instead grouping on user_agent.name / user_agent.original, which require collector-side enrichment that isn't present by default, so Top Browsers came up empty against real data. - Top Browsers now parses the browser name from SpanAttributes ['http.user_agent'] in SQL (Edge/Opera/Firefox/Chrome/Safari/Other), scoped to spans carrying the UA. Works with no SDK or collector change. - Removed the dashboard-level Browser filter: http.user_agent only exists on documentLoad spans, so a cross-tile filter keyed on it would zero out every non-documentLoad tile. It can return once the UA is promoted to a resource attribute (present on every span). Country tile/filter still depend on the collector geoip processor, since the browser cannot determine the user's country. --- .changeset/browser-rum-dashboard-template.md | 6 +++--- .../app/src/dashboardTemplates/browser-rum.json | 17 ++++------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/.changeset/browser-rum-dashboard-template.md b/.changeset/browser-rum-dashboard-template.md index a61ba48dbe..fd16b607c6 100644 --- a/.changeset/browser-rum-dashboard-template.md +++ b/.changeset/browser-rum-dashboard-template.md @@ -6,7 +6,7 @@ feat: add Browser RUM dashboard template - New "Browser RUM" template in the dashboards gallery for browser sessions instrumented with the HyperDX Browser SDK (or any OTel browser instrumentation emitting a `rum.sessionId` resource attribute) - Performance Overview section: page-view/session/error KPIs, Core Web Vitals (LCP/INP/CLS) p75, median/p75/p90 page-load percentiles, and long-task health -- Page Views Breakdown section: traffic grouped by URL, browser, country, and device size (derived from `screen.xy`) +- Page Views Breakdown section: traffic grouped by URL, browser (parsed from the `http.user_agent` the document-load instrumentation emits), country, and device size (derived from `screen.xy`) - Errors section with tabs for an overview, JS exceptions (by message and by page), and failing API calls -- Six dashboard-level filters: Service, Environment, Service Version, Page URL, Browser, and Country -- Top Browsers and Top Countries tiles (and the Browser/Country filters) populate when the OTel collector's `useragent` and `geoip` processors are enabled +- Five dashboard-level filters: Service, Environment, Service Version, Page URL, and Country +- Top Countries tile and the Country filter populate when the OTel collector's `geoip` processor is enabled (geo can't be derived in the browser) diff --git a/packages/app/src/dashboardTemplates/browser-rum.json b/packages/app/src/dashboardTemplates/browser-rum.json index 2473e05c7f..381f70e253 100644 --- a/packages/app/src/dashboardTemplates/browser-rum.json +++ b/packages/app/src/dashboardTemplates/browser-rum.json @@ -40,15 +40,6 @@ "where": "ResourceAttributes.rum.sessionId:* AND SpanAttributes.location.href:*", "whereLanguage": "lucene" }, - { - "id": "rum-filter-browser", - "type": "QUERY_EXPRESSION", - "name": "Browser", - "expression": "ResourceAttributes['user_agent.name']", - "source": "Traces", - "where": "ResourceAttributes.rum.sessionId:* AND ResourceAttributes.user_agent.name:*", - "whereLanguage": "lucene" - }, { "id": "rum-filter-country", "type": "QUERY_EXPRESSION", @@ -467,7 +458,7 @@ "w": 8, "h": 8, "config": { - "name": "Top Browsers (requires useragent processor)", + "name": "Top Browsers", "source": "Traces", "displayType": "table", "granularity": "auto", @@ -481,14 +472,14 @@ { "aggFn": "count", "valueExpression": "", - "alias": "Views" + "alias": "Page Loads" } ], - "where": "ResourceAttributes.rum.sessionId:*", + "where": "ResourceAttributes.rum.sessionId:* AND SpanAttributes.http.user_agent:*", "whereLanguage": "lucene", "groupBy": [ { - "valueExpression": "coalesce(nullif(ResourceAttributes['user_agent.name'], ''), nullif(SpanAttributes['user_agent.name'], ''), nullif(ResourceAttributes['user_agent.original'], ''), nullif(SpanAttributes['user_agent.original'], ''))", + "valueExpression": "multiIf(positionCaseInsensitive(SpanAttributes['http.user_agent'], 'Edg/') > 0, 'Edge', positionCaseInsensitive(SpanAttributes['http.user_agent'], 'OPR/') > 0, 'Opera', positionCaseInsensitive(SpanAttributes['http.user_agent'], 'Firefox/') > 0, 'Firefox', positionCaseInsensitive(SpanAttributes['http.user_agent'], 'Chrome/') > 0, 'Chrome', positionCaseInsensitive(SpanAttributes['http.user_agent'], 'Safari/') > 0, 'Safari', 'Other')", "alias": "Browser" } ], From 4ac7e789abe31e533138352b4cbd04f9ed9954d5 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 5 Jun 2026 11:10:22 -0400 Subject: [PATCH 4/7] fix: make tile filters visible/editable by moving where to aggCondition The chart builder editor only renders a WHERE input bound to the per-series aggCondition (ChartSeriesEditor); the top-level `where` input renders solely for Search-type tiles (ChartEditorControls.tsx:148 vs :334). So builder tiles that stored their filter in top-level `where` showed an empty WHERE box even though the filter applied correctly in SQL (renderChartConfig reads config.where directly). This affected nearly every tile, not just Page Views; the earlier OR-vs-AND theory was a red herring. Move each tile's filter from top-level `where` into the aggCondition of every select (clearing `where`). renderChartConfig promotes an all-selects aggCondition back into a real WHERE clause (renderChartConfig.ts:944,1019), so for a single shared condition the rendered query is result-identical (count() WHERE c == countIf(c) WHERE c, etc.) while the condition now shows in the editor. Left unchanged: Errors over Time and Top Errored Sessions, which already use per-series aggConditions (their meaningful conditions already display; their top-level where is only the broad rum.sessionId scope). Verified: dashboardTemplates schema test + app ci:lint pass; SQL result-equivalence confirmed by reading renderChartConfig's aggCondition promotion. Live editor click-through deferred (dev stack down). --- .../src/dashboardTemplates/browser-rum.json | 214 ++++++++++++------ 1 file changed, 150 insertions(+), 64 deletions(-) diff --git a/packages/app/src/dashboardTemplates/browser-rum.json b/packages/app/src/dashboardTemplates/browser-rum.json index 381f70e253..a5bb4514f3 100644 --- a/packages/app/src/dashboardTemplates/browser-rum.json +++ b/packages/app/src/dashboardTemplates/browser-rum.json @@ -66,9 +66,18 @@ "title": "Errors", "collapsed": false, "tabs": [ - { "id": "rum-errors-overview", "title": "Overview" }, - { "id": "rum-errors-js", "title": "JS Exceptions" }, - { "id": "rum-errors-api", "title": "API Failures" } + { + "id": "rum-errors-overview", + "title": "Overview" + }, + { + "id": "rum-errors-js", + "title": "JS Exceptions" + }, + { + "id": "rum-errors-api", + "title": "API Failures" + } ] } ], @@ -86,8 +95,15 @@ "displayType": "number", "granularity": "auto", "alignDateRangeToGranularity": true, - "select": [{ "aggFn": "count", "valueExpression": "" }], - "where": "SpanName:\"documentLoad\" OR SpanName:\"routeChange\" OR SpanAttributes.component:\"page-view\" OR SpanAttributes.component:\"navigation\"", + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "aggCondition": "SpanName:\"documentLoad\" OR SpanName:\"routeChange\" OR SpanAttributes.component:\"page-view\" OR SpanAttributes.component:\"navigation\"", + "aggConditionLanguage": "lucene" + } + ], + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -114,11 +130,11 @@ "aggFn": "quantile", "level": 0.5, "valueExpression": "Duration / 1000000", - "aggCondition": "", + "aggCondition": "SpanName:\"documentLoad\"", "aggConditionLanguage": "lucene" } ], - "where": "SpanName:\"documentLoad\"", + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -145,11 +161,11 @@ "aggFn": "quantile", "level": 0.9, "valueExpression": "Duration / 1000000", - "aggCondition": "", + "aggCondition": "SpanName:\"documentLoad\"", "aggConditionLanguage": "lucene" } ], - "where": "SpanName:\"documentLoad\"", + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -176,11 +192,11 @@ "aggFn": "quantile", "level": 0.75, "valueExpression": "toFloat64OrZero(SpanAttributes['lcp'])", - "aggCondition": "", + "aggCondition": "SpanName:\"webvitals\" AND SpanAttributes.lcp:*", "aggConditionLanguage": "lucene" } ], - "where": "SpanName:\"webvitals\" AND SpanAttributes.lcp:*", + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -207,11 +223,11 @@ "aggFn": "quantile", "level": 0.75, "valueExpression": "toFloat64OrZero(SpanAttributes['inp'])", - "aggCondition": "", + "aggCondition": "SpanName:\"webvitals\" AND SpanAttributes.inp:*", "aggConditionLanguage": "lucene" } ], - "where": "SpanName:\"webvitals\" AND SpanAttributes.inp:*", + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -238,11 +254,11 @@ "aggFn": "quantile", "level": 0.75, "valueExpression": "toFloat64OrZero(SpanAttributes['cls'])", - "aggCondition": "", + "aggCondition": "SpanName:\"webvitals\" AND SpanAttributes.cls:*", "aggConditionLanguage": "lucene" } ], - "where": "SpanName:\"webvitals\" AND SpanAttributes.cls:*", + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -267,10 +283,12 @@ "select": [ { "aggFn": "count_distinct", - "valueExpression": "ResourceAttributes['rum.sessionId']" + "valueExpression": "ResourceAttributes['rum.sessionId']", + "aggCondition": "ResourceAttributes.rum.sessionId:*", + "aggConditionLanguage": "lucene" } ], - "where": "ResourceAttributes.rum.sessionId:*", + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -295,10 +313,12 @@ "select": [ { "aggFn": "count_distinct", - "valueExpression": "ResourceAttributes['rum.sessionId']" + "valueExpression": "ResourceAttributes['rum.sessionId']", + "aggCondition": "ResourceAttributes.rum.sessionId:* AND StatusCode:error", + "aggConditionLanguage": "lucene" } ], - "where": "ResourceAttributes.rum.sessionId:* AND StatusCode:error", + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -325,7 +345,7 @@ "aggFn": "quantile", "level": 0.5, "valueExpression": "Duration / 1000000", - "aggCondition": "", + "aggCondition": "SpanName:\"documentLoad\"", "aggConditionLanguage": "lucene", "alias": "median" }, @@ -333,7 +353,7 @@ "aggFn": "quantile", "level": 0.75, "valueExpression": "Duration / 1000000", - "aggCondition": "", + "aggCondition": "SpanName:\"documentLoad\"", "aggConditionLanguage": "lucene", "alias": "p75" }, @@ -341,12 +361,12 @@ "aggFn": "quantile", "level": 0.9, "valueExpression": "Duration / 1000000", - "aggCondition": "", + "aggCondition": "SpanName:\"documentLoad\"", "aggConditionLanguage": "lucene", "alias": "p90" } ], - "where": "SpanName:\"documentLoad\"", + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -372,10 +392,12 @@ { "aggFn": "count", "valueExpression": "", - "alias": "Page Views" + "alias": "Page Views", + "aggCondition": "SpanName:\"documentLoad\" OR SpanName:\"routeChange\" OR SpanAttributes.component:\"page-view\" OR SpanAttributes.component:\"navigation\"", + "aggConditionLanguage": "lucene" } ], - "where": "SpanName:\"documentLoad\" OR SpanName:\"routeChange\" OR SpanAttributes.component:\"page-view\" OR SpanAttributes.component:\"navigation\"", + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -401,10 +423,12 @@ { "aggFn": "count", "valueExpression": "", - "alias": "Long Tasks" + "alias": "Long Tasks", + "aggCondition": "SpanName:\"longtask\"", + "aggConditionLanguage": "lucene" } ], - "where": "SpanName:\"longtask\"", + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -430,15 +454,19 @@ { "aggFn": "count", "valueExpression": "", - "alias": "Views" + "alias": "Views", + "aggCondition": "SpanName:\"documentLoad\" OR SpanName:\"routeChange\" OR SpanAttributes.component:\"page-view\" OR SpanAttributes.component:\"navigation\"", + "aggConditionLanguage": "lucene" }, { "aggFn": "count_distinct", "valueExpression": "ResourceAttributes['rum.sessionId']", - "alias": "Sessions" + "alias": "Sessions", + "aggCondition": "SpanName:\"documentLoad\" OR SpanName:\"routeChange\" OR SpanAttributes.component:\"page-view\" OR SpanAttributes.component:\"navigation\"", + "aggConditionLanguage": "lucene" } ], - "where": "SpanName:\"documentLoad\" OR SpanName:\"routeChange\" OR SpanAttributes.component:\"page-view\" OR SpanAttributes.component:\"navigation\"", + "where": "", "whereLanguage": "lucene", "groupBy": [ { @@ -447,7 +475,9 @@ } ], "orderBy": "Views DESC", - "limit": { "limit": 20 } + "limit": { + "limit": 20 + } } }, { @@ -467,15 +497,19 @@ { "aggFn": "count_distinct", "valueExpression": "ResourceAttributes['rum.sessionId']", - "alias": "Sessions" + "alias": "Sessions", + "aggCondition": "ResourceAttributes.rum.sessionId:* AND SpanAttributes.http.user_agent:*", + "aggConditionLanguage": "lucene" }, { "aggFn": "count", "valueExpression": "", - "alias": "Page Loads" + "alias": "Page Loads", + "aggCondition": "ResourceAttributes.rum.sessionId:* AND SpanAttributes.http.user_agent:*", + "aggConditionLanguage": "lucene" } ], - "where": "ResourceAttributes.rum.sessionId:* AND SpanAttributes.http.user_agent:*", + "where": "", "whereLanguage": "lucene", "groupBy": [ { @@ -484,7 +518,9 @@ } ], "orderBy": "Sessions DESC", - "limit": { "limit": 10 } + "limit": { + "limit": 10 + } } }, { @@ -504,15 +540,19 @@ { "aggFn": "count_distinct", "valueExpression": "ResourceAttributes['rum.sessionId']", - "alias": "Sessions" + "alias": "Sessions", + "aggCondition": "ResourceAttributes.rum.sessionId:*", + "aggConditionLanguage": "lucene" }, { "aggFn": "count", "valueExpression": "", - "alias": "Views" + "alias": "Views", + "aggCondition": "ResourceAttributes.rum.sessionId:*", + "aggConditionLanguage": "lucene" } ], - "where": "ResourceAttributes.rum.sessionId:*", + "where": "", "whereLanguage": "lucene", "groupBy": [ { @@ -521,7 +561,9 @@ } ], "orderBy": "Sessions DESC", - "limit": { "limit": 10 } + "limit": { + "limit": 10 + } } }, { @@ -541,15 +583,19 @@ { "aggFn": "count_distinct", "valueExpression": "ResourceAttributes['rum.sessionId']", - "alias": "Sessions" + "alias": "Sessions", + "aggCondition": "SpanName:\"documentLoad\" AND SpanAttributes.screen.xy:*", + "aggConditionLanguage": "lucene" }, { "aggFn": "count", "valueExpression": "", - "alias": "Views" + "alias": "Views", + "aggCondition": "SpanName:\"documentLoad\" AND SpanAttributes.screen.xy:*", + "aggConditionLanguage": "lucene" } ], - "where": "SpanName:\"documentLoad\" AND SpanAttributes.screen.xy:*", + "where": "", "whereLanguage": "lucene", "groupBy": [ { @@ -558,7 +604,9 @@ } ], "orderBy": "Sessions DESC", - "limit": { "limit": 10 } + "limit": { + "limit": 10 + } } }, { @@ -579,17 +627,19 @@ "aggFn": "quantile", "level": 0.75, "valueExpression": "Duration / 1000000", - "aggCondition": "", + "aggCondition": "SpanName:\"documentLoad\"", "aggConditionLanguage": "lucene", "alias": "Page Load p75 (ms)" }, { "aggFn": "count", "valueExpression": "", - "alias": "Views" + "alias": "Views", + "aggCondition": "SpanName:\"documentLoad\"", + "aggConditionLanguage": "lucene" } ], - "where": "SpanName:\"documentLoad\"", + "where": "", "whereLanguage": "lucene", "groupBy": [ { @@ -598,7 +648,9 @@ } ], "orderBy": "`Page Load p75 (ms)` DESC", - "limit": { "limit": 20 } + "limit": { + "limit": 20 + } } }, { @@ -649,7 +701,9 @@ "having": "Errors > 0", "havingLanguage": "sql", "orderBy": "Errors DESC", - "limit": { "limit": 20 } + "limit": { + "limit": 20 + } } }, { @@ -666,8 +720,15 @@ "displayType": "number", "granularity": "auto", "alignDateRangeToGranularity": true, - "select": [{ "aggFn": "count", "valueExpression": "" }], - "where": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "aggCondition": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "aggConditionLanguage": "lucene" + } + ], + "where": "", "whereLanguage": "lucene", "numberFormat": { "output": "number", @@ -690,8 +751,15 @@ "displayType": "number", "granularity": "auto", "alignDateRangeToGranularity": true, - "select": [{ "aggFn": "count", "valueExpression": "" }], - "where": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "select": [ + { + "aggFn": "count", + "valueExpression": "", + "aggCondition": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "aggConditionLanguage": "sql" + } + ], + "where": "", "whereLanguage": "sql", "numberFormat": { "output": "number", @@ -757,15 +825,19 @@ { "aggFn": "count", "valueExpression": "", - "alias": "Occurrences" + "alias": "Occurrences", + "aggCondition": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "aggConditionLanguage": "lucene" }, { "aggFn": "count_distinct", "valueExpression": "ResourceAttributes['rum.sessionId']", - "alias": "Sessions" + "alias": "Sessions", + "aggCondition": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "aggConditionLanguage": "lucene" } ], - "where": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "where": "", "whereLanguage": "lucene", "groupBy": [ { @@ -774,7 +846,9 @@ } ], "orderBy": "Occurrences DESC", - "limit": { "limit": 20 } + "limit": { + "limit": 20 + } } }, { @@ -795,15 +869,19 @@ { "aggFn": "count", "valueExpression": "", - "alias": "Errors" + "alias": "Errors", + "aggCondition": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "aggConditionLanguage": "lucene" }, { "aggFn": "count_distinct", "valueExpression": "ResourceAttributes['rum.sessionId']", - "alias": "Sessions" + "alias": "Sessions", + "aggCondition": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "aggConditionLanguage": "lucene" } ], - "where": "ResourceAttributes.rum.sessionId:* AND StatusCode:error AND NOT (SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\")", + "where": "", "whereLanguage": "lucene", "groupBy": [ { @@ -812,7 +890,9 @@ } ], "orderBy": "Errors DESC", - "limit": { "limit": 20 } + "limit": { + "limit": 20 + } } }, { @@ -833,15 +913,19 @@ { "aggFn": "count", "valueExpression": "", - "alias": "Failures" + "alias": "Failures", + "aggCondition": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "aggConditionLanguage": "sql" }, { "aggFn": "count_distinct", "valueExpression": "ResourceAttributes['rum.sessionId']", - "alias": "Sessions" + "alias": "Sessions", + "aggCondition": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "aggConditionLanguage": "sql" } ], - "where": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "where": "", "whereLanguage": "sql", "groupBy": [ { @@ -850,7 +934,9 @@ } ], "orderBy": "Failures DESC", - "limit": { "limit": 20 } + "limit": { + "limit": 20 + } } } ] From 252f37d8f54b2c68886ed34cc342863d8c4f5dd3 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 5 Jun 2026 11:30:35 -0400 Subject: [PATCH 5/7] feat: add row-click search drilldown to RUM table tiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire up the table onClick row-action (SavedChartConfig.onClick, type 'search') on the tables whose grouped value reverses cleanly into a search filter: - Top Errored Sessions -> opens the session's spans (rum.sessionId:"{{Session}}") — the client-side tracing drilldown - Top URLs / Slowest Pages -> page views / doc loads for that URL - Errors per Page -> errors for that URL - Top JS Errors -> spans for that exception message Each targets the Traces source by name ({ mode: 'id', id: 'Traces' }); the import flow auto-matches that to the user's mapped source and rewrites it to the concrete ID (DBDashboardImportPage onClick mapping + convertToDashboardDocument), so it stays portable. whereTemplate uses Handlebars row-column variables. Skipped tiles whose group key can't be reversed (Top Failing API Calls concat, Top Browsers/Countries/Device derived buckets). --- .../src/dashboardTemplates/browser-rum.json | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/app/src/dashboardTemplates/browser-rum.json b/packages/app/src/dashboardTemplates/browser-rum.json index a5bb4514f3..e577a79cfa 100644 --- a/packages/app/src/dashboardTemplates/browser-rum.json +++ b/packages/app/src/dashboardTemplates/browser-rum.json @@ -477,6 +477,15 @@ "orderBy": "Views DESC", "limit": { "limit": 20 + }, + "onClick": { + "type": "search", + "target": { + "mode": "id", + "id": "Traces" + }, + "whereTemplate": "(SpanAttributes.http.url:\"{{URL}}\" OR SpanAttributes.page.url:\"{{URL}}\" OR SpanAttributes.location.href:\"{{URL}}\")", + "whereLanguage": "lucene" } } }, @@ -650,6 +659,15 @@ "orderBy": "`Page Load p75 (ms)` DESC", "limit": { "limit": 20 + }, + "onClick": { + "type": "search", + "target": { + "mode": "id", + "id": "Traces" + }, + "whereTemplate": "SpanName:\"documentLoad\" AND (SpanAttributes.http.url:\"{{URL}}\" OR SpanAttributes.page.url:\"{{URL}}\" OR SpanAttributes.location.href:\"{{URL}}\")", + "whereLanguage": "lucene" } } }, @@ -703,6 +721,15 @@ "orderBy": "Errors DESC", "limit": { "limit": 20 + }, + "onClick": { + "type": "search", + "target": { + "mode": "id", + "id": "Traces" + }, + "whereTemplate": "ResourceAttributes.rum.sessionId:\"{{Session}}\"", + "whereLanguage": "lucene" } } }, @@ -848,6 +875,15 @@ "orderBy": "Occurrences DESC", "limit": { "limit": 20 + }, + "onClick": { + "type": "search", + "target": { + "mode": "id", + "id": "Traces" + }, + "whereTemplate": "StatusCode:error AND SpanAttributes.exception.message:\"{{Error}}\"", + "whereLanguage": "lucene" } } }, @@ -892,6 +928,15 @@ "orderBy": "Errors DESC", "limit": { "limit": 20 + }, + "onClick": { + "type": "search", + "target": { + "mode": "id", + "id": "Traces" + }, + "whereTemplate": "StatusCode:error AND (SpanAttributes.http.url:\"{{URL}}\" OR SpanAttributes.page.url:\"{{URL}}\" OR SpanAttributes.location.href:\"{{URL}}\")", + "whereLanguage": "lucene" } } }, From 4491322bd10838d2f28431ecf7dc38810f868567 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 5 Jun 2026 11:49:44 -0400 Subject: [PATCH 6/7] fix: make table row-click drilldowns relevant on every RUM table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builder tables without an onClick fall back to buildTableRowSearchUrl, which derives the drilldown from config.where — now empty (filters moved to aggCondition), so those drilldowns lost their scope. And the derived group keys (browser/device/concat) don't reverse into a filter. There's no template-level way to disable a builder-table row action, so give the remaining tables a correct onClick instead: - Top JS Errors: match the coalesced group value across exception.message / message / SpanName (it previously only matched exception.message, so e.g. an "unhandledrejection" row returned nothing). - Top Browsers: substring-match the parsed name against http.user_agent. - Top Countries: exact geo.country.name match. - Top Failing API Calls: regroup by http.url so the row reverses; drill into fetch/xhr calls to that endpoint. - Top Device Sizes: regroup by raw screen.xy so the row reverses; drill into documentLoad spans at that resolution. Every table now has a working, scoped row action; the scope-less legacy fallback no longer fires. --- .../src/dashboardTemplates/browser-rum.json | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/app/src/dashboardTemplates/browser-rum.json b/packages/app/src/dashboardTemplates/browser-rum.json index e577a79cfa..a7ec03de30 100644 --- a/packages/app/src/dashboardTemplates/browser-rum.json +++ b/packages/app/src/dashboardTemplates/browser-rum.json @@ -529,6 +529,15 @@ "orderBy": "Sessions DESC", "limit": { "limit": 10 + }, + "onClick": { + "type": "search", + "target": { + "mode": "id", + "id": "Traces" + }, + "whereTemplate": "positionCaseInsensitive(SpanAttributes['http.user_agent'], '{{Browser}}') > 0", + "whereLanguage": "sql" } } }, @@ -572,6 +581,15 @@ "orderBy": "Sessions DESC", "limit": { "limit": 10 + }, + "onClick": { + "type": "search", + "target": { + "mode": "id", + "id": "Traces" + }, + "whereTemplate": "ResourceAttributes.geo.country.name:\"{{Country}}\"", + "whereLanguage": "lucene" } } }, @@ -608,13 +626,22 @@ "whereLanguage": "lucene", "groupBy": [ { - "valueExpression": "multiIf(toUInt32OrZero(splitByChar('x', SpanAttributes['screen.xy'])[1]) < 600, 'Mobile (<600px)', toUInt32OrZero(splitByChar('x', SpanAttributes['screen.xy'])[1]) < 1024, 'Tablet (600–1024px)', toUInt32OrZero(splitByChar('x', SpanAttributes['screen.xy'])[1]) < 1440, 'Laptop (1024–1440px)', 'Desktop (≥1440px)')", + "valueExpression": "SpanAttributes['screen.xy']", "alias": "Device" } ], "orderBy": "Sessions DESC", "limit": { "limit": 10 + }, + "onClick": { + "type": "search", + "target": { + "mode": "id", + "id": "Traces" + }, + "whereTemplate": "SpanName:\"documentLoad\" AND SpanAttributes.screen.xy:\"{{Device}}\"", + "whereLanguage": "lucene" } } }, @@ -882,7 +909,7 @@ "mode": "id", "id": "Traces" }, - "whereTemplate": "StatusCode:error AND SpanAttributes.exception.message:\"{{Error}}\"", + "whereTemplate": "StatusCode:error AND (SpanAttributes.exception.message:\"{{Error}}\" OR SpanAttributes.message:\"{{Error}}\" OR SpanName:\"{{Error}}\")", "whereLanguage": "lucene" } } @@ -974,13 +1001,22 @@ "whereLanguage": "sql", "groupBy": [ { - "valueExpression": "concat(coalesce(nullif(SpanAttributes['http.method'], ''), 'GET'), ' ', coalesce(nullif(SpanAttributes['http.url'], ''), ''), ' → ', coalesce(nullif(SpanAttributes['http.status_code'], ''), 'error'))", + "valueExpression": "coalesce(nullif(SpanAttributes['http.url'], ''), '')", "alias": "Endpoint" } ], "orderBy": "Failures DESC", "limit": { "limit": 20 + }, + "onClick": { + "type": "search", + "target": { + "mode": "id", + "id": "Traces" + }, + "whereTemplate": "(SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\") AND SpanAttributes.http.url:\"{{Endpoint}}\"", + "whereLanguage": "lucene" } } } From 0f640f0fd84493538970ed65e7578c67af121e35 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 5 Jun 2026 12:08:09 -0400 Subject: [PATCH 7/7] fix: scope AJAX error tiles to RUM sessions and unify the error definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-review fixes for the Errors section: 1. AJAX Errors KPI (rum-008) and Top Failing API Calls (rum-013) had no rum.sessionId guard, so server-side fetch/xhr spans could inflate the counts relative to the rest of the dashboard. Add the SQL equivalent of the lucene rum.sessionId:* guard the sibling tiles use (ResourceAttributes['rum.sessionId'] != ''). 2. The AJAX Errors KPI counted status>=400 OR error span status, while the "Errors over Time" AJAX series only counted error span status — so a 404 with no error status hit the KPI but not the chart. Align the chart's AJAX series to the same (more complete) definition so the KPI total and the chart line measure the identical event set. --- packages/app/src/dashboardTemplates/browser-rum.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/app/src/dashboardTemplates/browser-rum.json b/packages/app/src/dashboardTemplates/browser-rum.json index a7ec03de30..61d188f498 100644 --- a/packages/app/src/dashboardTemplates/browser-rum.json +++ b/packages/app/src/dashboardTemplates/browser-rum.json @@ -809,7 +809,7 @@ { "aggFn": "count", "valueExpression": "", - "aggCondition": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "aggCondition": "ResourceAttributes['rum.sessionId'] != '' AND (SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", "aggConditionLanguage": "sql" } ], @@ -847,8 +847,8 @@ { "aggFn": "count", "valueExpression": "", - "aggCondition": "(SpanAttributes.component:\"fetch\" OR SpanAttributes.component:\"xml-http-request\") AND StatusCode:error", - "aggConditionLanguage": "lucene", + "aggCondition": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "aggConditionLanguage": "sql", "alias": "AJAX Errors" } ], @@ -986,14 +986,14 @@ "aggFn": "count", "valueExpression": "", "alias": "Failures", - "aggCondition": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "aggCondition": "ResourceAttributes['rum.sessionId'] != '' AND (SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", "aggConditionLanguage": "sql" }, { "aggFn": "count_distinct", "valueExpression": "ResourceAttributes['rum.sessionId']", "alias": "Sessions", - "aggCondition": "(SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", + "aggCondition": "ResourceAttributes['rum.sessionId'] != '' AND (SpanAttributes['component'] = 'fetch' OR SpanAttributes['component'] = 'xml-http-request') AND (toUInt16OrZero(SpanAttributes['http.status_code']) >= 400 OR StatusCode = 'STATUS_CODE_ERROR')", "aggConditionLanguage": "sql" } ],