Skip to content

feat: add Browser RUM dashboard template#2413

Merged
kodiakhq[bot] merged 12 commits into
mainfrom
teeohhem/hackathon-ideas
Jun 8, 2026
Merged

feat: add Browser RUM dashboard template#2413
kodiakhq[bot] merged 12 commits into
mainfrom
teeohhem/hackathon-ideas

Conversation

@teeohhem

@teeohhem teeohhem commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Browser RUM template to the dashboards gallery (Dashboards → Templates) for browser sessions instrumented with the HyperDX Browser SDK — or any OpenTelemetry browser instrumentation that emits a rum.sessionId resource attribute. It fills a gap: HyperDX ships a browser SDK but had no out-of-the-box RUM dashboard. The template is purely declarative JSON validated by the existing dashboardTemplates schema test; the only code change is registering it in dashboardTemplates/index.ts, plus a changeset.

The dashboard is organized into three sections: Performance Overview (page-view/session/error KPIs, Core Web Vitals LCP/INP/CLS p75, median/p90/p99 page-load percentiles, long tasks), Page Views Breakdown (traffic by URL, browser, country, and device size derived from screen.xy), and a tabbed Errors section (overview, JS exceptions by message and by page, failing API calls). It also defines five dashboard-level filters: Service, Environment, Service Version, Page URL, and Country.

Screenshots or video

image image image
Tab-1780519086079.webm

How to test locally or on Vercel

  1. Dashboards → Templates → Browser RUM → Import, then map each tile/filter to your Traces source (auto-maps if a source named "Traces" exists).
  2. Point a browser app instrumented with @hyperdx/browser at your collector (or seed webvitals / documentLoad / fetch+xhr / exception spans carrying rum.sessionId). Verify the KPIs, Core Web Vitals, breakdown tables, and Errors tabs populate, and that the five filters apply.
  3. Top Browsers / Top Countries tiles and the Browser / Country filters only populate when the collector's useragent and geoip processors are enabled (noted in the tile titles + dashboard description).

References

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-bot

changeset-bot Bot commented Jun 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d742182

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jun 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hyperdx-oss Ignored Ignored Preview Jun 8, 2026 3:02pm
hyperdx-storybook Ignored Ignored Preview Jun 8, 2026 3:02pm

Request Review

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Jun 3, 2026
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.

Why this tier:

  • Large diff: 1026 production lines changed (threshold: 1000)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 2
  • Production lines changed: 1026
  • Branch: teeohhem/hackathon-ideas
  • Author: teeohhem

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

This PR is a declarative dashboard template (browser-rum.json), a one-line registration in index.ts, and a changeset. The existing schema test (dashboardTemplates.test.ts) already covers structural validity and description presence. No P0/P1 issues. One headline finding from the correctness pass — that StatusCode = 'STATUS_CODE_ERROR' never matches stored data — was verified as a false positive and dropped: the stored value is STATUS_CODE_ERROR (per the e2e RUM seed and waterfall.ts), and lucene StatusCode:error renders to ILIKE '%error%', so both forms match the same error rows.

🟡 P2 -- recommended

  • packages/app/src/dashboardTemplates/browser-rum.json:737 -- The "AJAX Errors" time-series (rum-010) counts only StatusCode:error, while the "AJAX Errors" KPI (rum-008, line 703) and "Top Failing API Calls" (rum-013, line 853) also count toUInt16OrZero(SpanAttributes['http.status_code']) >= 400, so a 4xx/5xx fetch whose span status is not error appears in the KPI but is missing from the like-named chart.
    • Fix: Add the http.status_code >= 400 branch to the rum-010 AJAX aggCondition so all three tiles share one AJAX-error definition.
    • correctness, maintainability
  • packages/app/src/__tests__/dashboardTemplates.test.ts:16 -- The template test only runs DashboardTemplateSchema.safeParse, which does not validate tile→container/tab references (that check lives only in the external-API buildDashboardBodySchema), so a typo in a containerId/tabId in this tab-heavy template would pass CI and break only at render.
    • Fix: Add a per-template assertion that every tile.containerId resolves to a declared container and every tabId resolves to a tab within that container.
    • testing
  • .changeset/browser-rum-dashboard-template.md:2 -- The changeset declares a minor bump for @hyperdx/app, but the fixed group in .changeset/config.json (@hyperdx/api, @hyperdx/app, @hyperdx/otel-collector) propagates that minor bump to two packages this PR does not touch.
    • Fix: Use patch for an additive template-only change to keep the fixed-group bump at patch level.
    • project-standards
🔵 P3 nitpicks (5)
  • packages/app/src/dashboardTemplates/browser-rum.json:679 -- The "JS Errors" KPI (rum-007) is scoped to ResourceAttributes.rum.sessionId:*, but the "JS Errors" series in rum-010 (line 730) is not, so the two can diverge if non-RUM spans match.
    • Fix: Add ResourceAttributes.rum.sessionId:* to the rum-010 JS aggCondition to match the KPI.
  • packages/app/src/dashboardTemplates/browser-rum.json:99 -- Several page-view/page-load/long-task tiles (rum-006, rum-017, rum-018, rum-020, rum-021, rum-024, rum-014, rum-011) filter only on SpanName, unlike the session-scoped tiles, so non-RUM spans sharing those names would be included.
    • Fix: Prepend ResourceAttributes.rum.sessionId:* to these where clauses for consistency with the rest of the dashboard.
  • packages/app/src/dashboardTemplates/browser-rum.json:86 -- Tile ids use a rum-NNN numbering that is gapped (004, 009, 022, 023, 025 absent) and out of visual order, implying a sequence that does not exist; sibling templates use opaque ids in render order.
    • Fix: Renumber contiguously in render order or switch to opaque/semantic ids.
  • packages/app/src/dashboardTemplates/browser-rum.json:454 -- The URL coalesce(nullif(...)) grouping expression is repeated verbatim across rum-014, rum-011, and rum-015 with no guard against drift if the attribute fallback list changes.
    • Fix: Add a test asserting these grouping expressions stay identical, or accept the duplication as inherent to the format.
  • packages/app/src/dashboardTemplates/browser-rum.json:616 -- "Top Errored Sessions" (rum-016) groups by per-session id and filters errors via a post-aggregation having Errors > 0, aggregating all sessions before trimming.
    • Fix: Pre-filter error rows in the where clause so only errored sessions enter the aggregation.

Reviewers (6): correctness, maintainability, testing, project-standards, performance, learnings-researcher.

Testing gaps:

  • No test verifies that like-named KPI and time-series tiles (Page Views, JS Errors, AJAX Errors) compute equivalent sets — the AJAX divergence above is not caught by CI.
  • No test exercises orphaned containerId/tabId references for templates.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 197 passed • 3 skipped • 1364s

Status Count
✅ Passed 197
❌ Failed 0
⚠️ Flaky 5
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

Trim the dashboard description to a single sentence to match the
length and style of the existing runtime-metrics templates.
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.
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).
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).
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.
@github-actions github-actions Bot removed the review/tier-3 Standard — full human review required label Jun 5, 2026
@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Jun 5, 2026
@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a new Browser RUM dashboard template — a purely declarative JSON file registering 18 tiles across three sections (Performance Overview, Page Views Breakdown, and Errors) plus five dashboard-level filters scoped to rum.sessionId.

  • browser-rum.json: New 970-line template covering Core Web Vitals (LCP/INP/CLS p75), page-load percentiles, session/error KPIs, traffic breakdowns by URL/browser/country/device, and a tabbed Errors section with JS exceptions and API-failure drilldowns.
  • index.ts: Minimal one-line registration alongside existing templates; no logic changes.
  • .changeset: Correctly classified as a minor bump for @hyperdx/app, with a description that now matches the five filters present in the JSON.

Confidence Score: 5/5

Safe to merge — all changes are additive, declarative JSON with no runtime logic.

The change is purely a new JSON template and a one-line import registration. No existing behavior is altered, and the schema validation test already guards against malformed templates. The one inconsistency found (Top Countries onClick not covering the iso_code fallback) is a display-level drilldown edge case that does not affect correctness of the main dashboard tiles.

No files require special attention; the minor drilldown inconsistency in browser-rum.json is self-contained.

Important Files Changed

Filename Overview
packages/app/src/dashboardTemplates/browser-rum.json New 970-line declarative RUM dashboard template with 18 tiles across 3 containers. Logic is well-scoped to rum.sessionId throughout. Minor drilldown inconsistency in Top Countries: groupBy falls back to geo.country.iso_code but the onClick filter only targets geo.country.name.
packages/app/src/dashboardTemplates/index.ts Minimal, correct registration of the new browser-rum template alongside existing templates; no logic changes.
.changeset/browser-rum-dashboard-template.md Changeset correctly classifies as minor for @hyperdx/app; description matches the five filters present in the JSON template.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    T["Browser RUM Template"]
    T --> F["5 Filters\n(Service / Env / Version / Page URL / Country)"]
    T --> C1["Performance Overview\n(rum-perf)"]
    T --> C2["Page Views Breakdown\n(rum-breakdown)"]
    T --> C3["Errors\n(rum-errors)"]

    C1 --> P1["Page Views KPI (rum-006)"]
    C1 --> P2["Median Page Load (rum-017)"]
    C1 --> P3["p90 Page Load (rum-018)"]
    C1 --> P4["LCP / INP / CLS p75 (rum-001/002/003)"]
    C1 --> P5["Active Sessions (rum-005)"]
    C1 --> P6["Sessions w/ Errors (rum-019)"]
    C1 --> P7["Page Load time series (rum-020)"]
    C1 --> P8["Page Views & Long Tasks (rum-021/024)"]

    C2 --> B1["Top URLs (rum-014)"]
    C2 --> B2["Top Browsers (rum-026)"]
    C2 --> B3["Top Countries (rum-027)"]
    C2 --> B4["Top Device Sizes (rum-028)"]
    C2 --> B5["Slowest Pages p90 (rum-011)"]
    C2 --> B6["Top Errored Sessions (rum-016)"]

    C3 --> TAB1["Overview Tab\nJS Errors + AJAX Errors KPIs + Errors over Time\n(rum-007/008/010)"]
    C3 --> TAB2["JS Exceptions Tab\nBy message + by page\n(rum-012/015)"]
    C3 --> TAB3["API Failures Tab\nTop Failing API Calls\n(rum-013)"]
Loading

Fix All in Claude Code Fix All in Conductor Fix All in Cursor Fix All in Codex

Reviews (5): Last reviewed commit: "Merge branch 'main' into teeohhem/hackat..." | Re-trigger Greptile

Comment thread packages/app/src/dashboardTemplates/browser-rum.json
Comment thread packages/app/src/dashboardTemplates/browser-rum.json
Comment thread .changeset/browser-rum-dashboard-template.md
…ition

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.

@pulpdrew pulpdrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool stuff, nice to see some newer dashboard features being exercised! Couple of suggestions

Comment on lines +122 to +143
"config": {
"name": "Median Page Load (ms)",
"source": "Traces",
"displayType": "number",
"granularity": "auto",
"alignDateRangeToGranularity": true,
"select": [
{
"aggFn": "quantile",
"level": 0.5,
"valueExpression": "Duration / 1000000",
"aggCondition": "SpanName:\"documentLoad\"",
"aggConditionLanguage": "lucene"
}
],
"where": "",
"whereLanguage": "lucene",
"numberFormat": {
"output": "number",
"mantissa": 0,
"thousandSeparated": true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a change this will be the wrong precision for duration? Same thing elsewhere.

Instead, we could remove the divisor and numberFormat, so that duration format with the correct precision will be inferred:

Suggested change
"config": {
"name": "Median Page Load (ms)",
"source": "Traces",
"displayType": "number",
"granularity": "auto",
"alignDateRangeToGranularity": true,
"select": [
{
"aggFn": "quantile",
"level": 0.5,
"valueExpression": "Duration / 1000000",
"aggCondition": "SpanName:\"documentLoad\"",
"aggConditionLanguage": "lucene"
}
],
"where": "",
"whereLanguage": "lucene",
"numberFormat": {
"output": "number",
"mantissa": 0,
"thousandSeparated": true
}
"config": {
"name": "Median Page Load",
"source": "Traces",
"displayType": "number",
"granularity": "auto",
"alignDateRangeToGranularity": true,
"select": [
{
"aggFn": "quantile",
"level": 0.5,
"valueExpression": "Duration",
"aggCondition": "SpanName:\"documentLoad\"",
"aggConditionLanguage": "lucene"
}
],
"where": "",
"whereLanguage": "lucene"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — done in d3a5b0f. The page-load duration tiles (`rum-017` median, `rum-018` p90, the page-load line `rum-020`, and Slowest Pages `rum-011`) now select the raw `Duration` expression with no divisor and no manual `numberFormat`, so the duration format is inferred at the correct precision.

Comment on lines +184 to +193
"config": {
"name": "LCP p75 (ms)",
"source": "Traces",
"displayType": "number",
"granularity": "auto",
"alignDateRangeToGranularity": true,
"select": [
{
"aggFn": "quantile",
"level": 0.75,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting, we don't support p75 through the app, so this renders as an empty aggFn.

Do we need p75 or could we do p95? If p75, maybe we should try a custom aggregation to populate the dropdown correctly.

Sidenote, we should probably add a validation so we don't accept this during import, or expand support to custom quantile levels.

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched the Core Web Vitals p75 KPIs (LCP/INP/CLS) to a custom aggregation (`aggFn: "none"`, `quantile(0.75)(...)`) in d3a5b0f so they render correctly instead of an empty aggFn. We do want p75 specifically here — it's the standard Core Web Vitals reporting percentile. The page-load duration tiles use the supported p50/p90/p99 levels. Agree that an import-time validation rejecting unsupported quantile levels would be a good app-side follow-up.

Comment on lines +471 to +476
"groupBy": [
{
"valueExpression": "coalesce(nullif(SpanAttributes['http.url'], ''), nullif(SpanAttributes['page.url'], ''), nullif(SpanAttributes['location.href'], ''))",
"alias": "URL"
}
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably another place where we need validation or transformation during import, but this is unrendered because we don't support this groupBy being an array in dashboards (it should be a string)

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converted every table's `groupBy` to string form (` AS ""`) in d3a5b0f, which renders in the builder. Keeping the `AS ""` preserves both the column header and the row-data key that each tile's `onClick.whereTemplate` Handlebars vars depend on. Agreed an import-time transform/validation for array groupBy would be a worthwhile app-side follow-up.

"w": 24,
"h": 8,
"config": {
"name": "Top Errored Sessions",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few of these tables could probably benefit from setting groupByColumnsOnLeft so they read more naturally

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — added `groupByColumnsOnLeft: true` to all the breakdown and error tables in d3a5b0f, so the grouped column reads on the left.

"mode": "id",
"id": "Traces"
},
"whereTemplate": "ResourceAttributes.rum.sessionId:\"{{Session}}\"",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see this getting used!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! 🙏

Comment on lines +662 to +676
{
"aggFn": "quantile",
"level": 0.75,
"valueExpression": "Duration / 1000000",
"aggCondition": "SpanName:\"documentLoad\"",
"aggConditionLanguage": "lucene",
"alias": "Page Load p75 (ms)"
},
{
"aggFn": "count",
"valueExpression": "",
"alias": "Views",
"aggCondition": "SpanName:\"documentLoad\"",
"aggConditionLanguage": "lucene"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add per-series numberFormats here to render the p75 as a duration and the count as a number

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed via the duration-inference change in d3a5b0f — the Slowest Pages table now selects raw `Duration` for the percentile column (auto-formatted as a duration) while the `Views` count stays a plain number, so we get the per-column formatting without a manual `numberFormat`.

@teeohhem teeohhem added review/tier-2 Low risk — AI review + quick human skim and removed review/tier-4 Critical — deep review + domain expert sign-off labels Jun 8, 2026
- Page-load duration tiles (median/p90 KPIs, line, Slowest Pages) now
  select raw Duration with supported percentile levels (p50/p90/p99)
  instead of Duration/1000000 + manual numberFormat, so the builder
  infers human-readable duration formatting and the editor shows real
  percentile options.
- Core Web Vitals p75 KPIs (LCP/INP/CLS) use a custom aggregation
  (quantile(0.75)(...)) so the unsupported 0.75 level renders correctly.
- Table groupBys converted from array to string form (E AS "Alias"),
  preserving column headers and row-click drilldown keys, and grouped
  columns now render on the left (groupByColumnsOnLeft).
@teeohhem

teeohhem commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @pulpdrew! Pushed d3a5b0f3 addressing the template feedback:

  • Page-load duration tiles (rum-017 median, rum-018 p90, page-load line rum-020, Slowest Pages rum-011) now select the raw Duration expression — dropped the / 1000000 divisor and the manual numberFormat so duration formatting is inferred at the correct precision.
  • Core Web Vitals p75 (LCP/INP/CLS) now use a custom aggregation (aggFn: "none", quantile(0.75)(...)) so they render instead of an empty aggFn. p75 is the standard CWV reporting percentile; the page-load tiles use the supported p50/p90/p99 levels.
  • Tables: converted every groupBy from array to string form (<expr> AS "<Alias>") — renders in the builder while preserving the column header and the row-click drilldown keys. Added groupByColumnsOnLeft: true so grouped columns read on the left.
  • Description corrected to five filters (was "six"/"dive"), matching the changeset.

The two greptile P1 AJAX items were already fixed in 0f640f0f (scoped to rum.sessionId, unified error definition).

A couple of suggestions point at app-side follow-ups rather than this template (import-time validation/transformation for unsupported quantile levels and array groupBy) — happy to file those separately.

@pulpdrew pulpdrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of suggestions point at app-side follow-ups rather than this template (import-time validation/transformation for unsupported quantile levels and array groupBy) — happy to file those separately.

Agreed, those make sense as separate improvements

LGTM!

@kodiakhq kodiakhq Bot merged commit 9af8cba into main Jun 8, 2026
19 checks passed
@kodiakhq kodiakhq Bot deleted the teeohhem/hackathon-ideas branch June 8, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants