Skip to content

fix(dashboard): show chart tooltip labels as text, and decode keys like the api does - #7882

Open
ar2rsawseen wants to merge 1 commit into
masterfrom
fix/chart-tooltip-label-rendering
Open

fix(dashboard): show chart tooltip labels as text, and decode keys like the api does#7882
ar2rsawseen wants to merge 1 commit into
masterfrom
fix/chart-tooltip-label-rendering

Conversation

@ar2rsawseen

Copy link
Copy Markdown
Member

Two related things, both about a value ending up on screen as what it actually is.

sanitizeHtml was a no-op

Every value interpolated into a chart tooltip template goes through sanitizeHtml, which encoded its input and then immediately unescaped it:

value = countlyCommon.encodeHtml(value);
return countlyCommon.unescapeHtml(value);

Those two are inverses, so it returned the input unchanged, and a label containing markup went into the template as markup rather than being shown as text.

That was not the original intent. The call sites escaped correctly until the unescape was added, and git log says why: the escaping made Countly's own key substitutions ($ for a leading $, . for dots) visible to users as literal character references, three days after the escaping first landed. So one goal was traded for the other.

Both hold if the value is normalised first and escaped last, which is what it now does: undo the html escaping the api applies, undo the key substitutions, then escape once. A label reads as its real text and is never treated as markup.

Ordering is the whole point, and it's what the tests pin down.

The two key decoders had drifted

That came out of the first part. There are two implementations of the key substitution pair — the api one that produces every value in the database, and the dashboard one that consumes them:

encode rules decode rules
api 3 ($, ., NUL) 5 (those, plus url encoded $ / .)
dashboard (before) 2 ($, .) 2 ($, .)

So &#9647, $ and . reached callers still encoded. The dashboard decoder's own comment already promised the url encoded forms, so this reads as an incomplete copy rather than a decision.

It went unnoticed because the result is usually interpolated into html, where the browser resolves the leftovers as character references anyway. It shows through anywhere that isn't html.

Worth knowing before reviewing: all 25 callers of the dashboard decoder consume api-produced data, and the dashboard encoder has no callers at all, so the internal symmetry that justified the short rule set was never load-bearing. Both directions are now aligned with the api.

One visible consequence

A value containing NUL now shows the character itself, which is invisible, where it previously showed the placeholder glyph the browser resolved &#9647 into. NUL in a key or segment value is pathological, and the alternative was leaving one of the two decoders knowingly incomplete. Flagging it because it is a behaviour change, not a pure fix.

Tests

  • 13 over the tooltip helper, 4 of which fail without this change. They assert both directions: a label never opens a tag, and dotted keys, dollar-prefixed keys, ampersands and quotes still read normally, since over-escaping here is what caused the original regression.
  • 30 asserting the two substitution pairs agree on behaviour rather than on source text, so reformatting either one does not fail the test but changing what either substitutes does. 6 fail without this change.

Two notes on how they are written. The suite has no DOM, and encodeHtml is implemented with innerText, so it is substituted by its documented effect on element content; the composition order and the decode step are the real source, lifted out of the files. And a key that already looks encoded is not round-trippable — that is a property of the substitution scheme rather than of either implementation, so it is asserted as the shared behaviour it is instead of asserted away.

Full unit suite: 185 passing before, 228 after, same 2 pre-existing failures (Countly Request, network dependent). Baseline captured by reverting and re-running. eslint clean.

Notes

frontend/express/public/javascripts/min/ is gitignored build output, so the stale copy of this code in the concat bundle regenerates and is not touched here.

Going to release.24.05 and countly-platform main next; both carry identical copies of all three functions. countly-platform new-dashboard is also identical, but it currently cannot receive anything from main: the nightly sync stands down while a sync PR is open, and Countly/countly-platform#954 has been open and conflicting since 31 July, so that branch is 158 commits behind. Worth unblocking separately.

🤖 Generated with Claude Code

…ke the api does

Two related things, both about a value ending up on screen as what it actually is.

sanitizeHtml, used for every value interpolated into a chart tooltip template,
encoded its input and then immediately unescaped it. Those two are inverses, so
it returned the input unchanged and a label containing markup went into the
template as markup rather than being shown as text.

That was not the original intent. The call sites escaped correctly until the
unescape was added, to stop Countly's own key substitutions ($ for a leading
$, . for dots) from being shown to the user as literal character references.
Both goals hold if the value is normalised first and escaped last, so that is
what it now does: undo the html escaping the api applies, undo the key
substitutions, then escape once. A label reads as its real text and is never
treated as markup.

The second part came out of the first. There are two implementations of the key
substitution pair, the api one that produces every value in the database and the
dashboard one that consumes them, and they had drifted. The dashboard decoder
undid only $ and . while the api encoder also substitutes NUL and the api decoder
also accepts the url encoded forms, so &#9647, $ and . reached
callers still encoded. Its own comment already promised the url encoded forms, so
this looks like an incomplete copy rather than a decision. It went unnoticed
because the result is usually interpolated into html, where the browser resolves
the leftovers anyway; it shows through anywhere that is not html. All 25 callers
of the dashboard decoder consume api-produced data, and the dashboard encoder has
no callers at all, so the pair is now aligned with the api in both directions.

One visible consequence: a value containing NUL now shows the character itself,
which is invisible, where it previously showed the placeholder glyph the browser
resolved &#9647 into. NUL in a key or segment value is pathological, and the
alternative was leaving one of the two decoders knowingly incomplete.

Tests: 13 over the tooltip helper, 4 of which fail without this change, and 30
asserting the two substitution pairs agree on behaviour rather than on source
text, 6 of which fail without it. The suite has no DOM, so encodeHtml, which is
implemented with innerText, is substituted by its documented effect on element
content; the composition order and the decode step are the real source, lifted
out of the files. A key that already looks encoded is not round-trippable, which
is a property of the scheme rather than of either implementation, so that is
asserted as the shared behaviour it is instead of being asserted away.

Full unit suite: 185 passing before, 228 after, same 2 pre-existing failures
(Countly Request, network dependent). eslint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant