diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 272751a5..e9e3db2d 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -403,6 +403,7 @@ jobs:
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m2-icons-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m3-icons-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m4-icons-e2e.js 2>&1 | tee -a e2e-output.txt
+ CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1657-analytics-channels-group-sprites-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1224-channels-mobile-ux-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1367-channels-chat-app-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js 2>&1 | tee -a e2e-output.txt
diff --git a/public/analytics.js b/public/analytics.js
index 6769da3a..6ee8487a 100644
--- a/public/analytics.js
+++ b/public/analytics.js
@@ -1033,7 +1033,11 @@
if (!rows.length) continue;
parts.push(
'
' +
- esc(sections[si].label) + ' (' + rows.length + ')' +
+ // sections[].label is a hardcoded sprite-bearing string (no user
+ // input) — must be inserted raw so the
'
);
for (var ri = 0; ri < rows.length; ri++) parts.push(channelRowHtml(rows[ri]));
diff --git a/test-issue-1657-analytics-channels-group-sprites-e2e.js b/test-issue-1657-analytics-channels-group-sprites-e2e.js
new file mode 100644
index 00000000..f60cef38
--- /dev/null
+++ b/test-issue-1657-analytics-channels-group-sprites-e2e.js
@@ -0,0 +1,148 @@
+#!/usr/bin/env node
+/* Issue #1657 — Analytics → Channels: group-header rows must render real
+ * Phosphor sprites (My Channels / Network / Encrypted), NOT the
+ * HTML-escaped literal "" source text.
+ *
+ * Bug: public/analytics.js passed the hardcoded group-section label (which
+ * contains the sprite markup) through esc(), HTML-encoding the angle
+ * brackets so the browser displayed the source instead of rendering it.
+ *
+ * This test asserts (in real Chromium against a running server):
+ * (1) The Channel Activity table has at least one group-header row.
+ * (2) Each rendered group-header row contains a real
+ * element (NOT escaped text).
+ * (3) For each expected group (key/radio/lock), if the label text is
+ * present, an actual