From b6f2166545fbbc2cd97a76e47c39c8ab20c34e63 Mon Sep 17 00:00:00 2001 From: Gia Cajes Date: Mon, 27 Jul 2026 14:49:22 -0600 Subject: [PATCH 1/3] fix(events): show event descriptions for keys with special characters Event descriptions (and custom names / count-sum-dur labels) added via Data Manager did not render on the Events page for events whose key contains a special character (. $ \ & < > " '). The map lookup escaped the key with an ad-hoc unicode scheme (.->., $->$) before looking it up in events.map. But events.map is keyed by the raw event key (the Data Manager write path stores it verbatim), so the escaped lookup key never matched and the description came back blank. Look the key up raw, exactly as it is stored. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 4 ++++ .../core/events/javascripts/countly.details.models.js | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 963b1750c10..e2cb49f90f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 24.05.52 +Fixes: +- [events] Fixed event descriptions (and custom names / count-sum-dur labels) not showing on the Events page for events whose key contains special characters (`.`, `$`, `\`, `&`, `<`, `>`) + ## Version 24.05.51 Enterprise Fixes: - [data-manager] Fixed bug where event and view transformations occasionally failed to apply to incoming data diff --git a/frontend/express/public/core/events/javascripts/countly.details.models.js b/frontend/express/public/core/events/javascripts/countly.details.models.js index 4b22ff19ae2..aefda50a650 100644 --- a/frontend/express/public/core/events/javascripts/countly.details.models.js +++ b/frontend/express/public/core/events/javascripts/countly.details.models.js @@ -236,7 +236,8 @@ return str.replace(/&/g, "&").replace(//g, ">").replace(/<=/g, "≤").replace(/>=/g, "≥"); }, getEventLongName: function(eventKey, eventMap) { - var mapKey = eventKey.replace(/\\/g, "\\\\").replace(/\$/g, "\\u0024").replace(/\./g, "\\u002e"); + //events.map is keyed by the raw event key, exactly as it appears in events.list, so do not escape it here + var mapKey = eventKey; if (eventMap && eventMap[mapKey] && eventMap[mapKey].name) { return eventMap[mapKey].name; } @@ -249,7 +250,8 @@ var allEvents = context.state.allEventsData; var groupData = context.state.groupData.displayMap; var eventMap = allEvents.map; - var mapKey = context.state.selectedEventName.replace(/\\/g, "\\\\").replace(/\$/g, "\\u0024").replace(/\./g, '\\u002e'); + //events.map is keyed by the raw event key, exactly as it appears in events.list, so do not escape it here + var mapKey = context.state.selectedEventName; var countString = (mapKey.startsWith('[CLY]_group') && groupData.c) ? groupData.c : (eventMap && eventMap[mapKey] && eventMap[mapKey].count) ? eventMap[mapKey].count : jQuery.i18n.map["events.table.count"]; var sumString = (mapKey.startsWith('[CLY]_group') && groupData.s) ? groupData.s : (eventMap && eventMap[mapKey] && eventMap[mapKey].sum) ? eventMap[mapKey].sum : jQuery.i18n.map["events.table.sum"]; var durString = (mapKey.startsWith('[CLY]_group') && groupData.d) ? groupData.d : (eventMap && eventMap[mapKey] && eventMap[mapKey].dur) ? eventMap[mapKey].dur : jQuery.i18n.map["events.table.dur"]; From cf226bb12e907db9150c3c823e356c26c0c464a5 Mon Sep 17 00:00:00 2001 From: Gia Cajes Date: Wed, 29 Jul 2026 10:44:55 -0600 Subject: [PATCH 2/3] fix(data-manager): decode HTML-escaped event keys in the events store and edit paths common.returnOutput HTML-escapes every string key and value on the way out (escape_html(str, true), which escapes & < > " '). Two paths did not account for it, which made both bugs look intermittent: keys containing only dots or dashes were unaffected. On the read path, the events store decoded res.list but not the keys of res.map, so every map[selectedEventName] lookup compared a raw key against an escaped map and missed - descriptions came back blank on the Events page for keys containing & < > " '. Both are now normalised in the setAllEventsData mutation, the single point both fetch paths commit through, which also fixes fetchRefreshAllEventsData never decoding list at all. On the write path, the event detail page's "Edit event" button passed API-escaped values straight into the edit drawer, so "a & b" was saved back as "a & b" and forked a second drill_meta / events.map entry with no key and no segments - an undeletable ghost row in the Data Manager events table. The events table's own edit action already decoded; handleEdit and handleChangeVisibility now do the same. handleEdit also deep-copies segments, which were previously passed into the drawer by reference, so decoding in place would have mutated the Vuex store. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 5 +++- .../javascripts/countly.details.models.js | 23 ++++++++++++++++--- .../public/javascripts/countly.views.js | 20 ++++++++++++++-- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2cb49f90f7..36629247202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ## Version 24.05.52 Fixes: -- [events] Fixed event descriptions (and custom names / count-sum-dur labels) not showing on the Events page for events whose key contains special characters (`.`, `$`, `\`, `&`, `<`, `>`) +- [events] Fixed event descriptions (and custom names / count-sum-dur labels) not showing on the Events page for events whose key contains special characters (`.`, `$`, `\`, `&`, `<`, `>`, `"`, `'`) + +Enterprise Fixes: +- [data-manager] Fixed editing an event whose key contains `&` creating undeletable duplicate rows in the events table ## Version 24.05.51 Enterprise Fixes: diff --git a/frontend/express/public/core/events/javascripts/countly.details.models.js b/frontend/express/public/core/events/javascripts/countly.details.models.js index aefda50a650..b2a8bdf7d1c 100644 --- a/frontend/express/public/core/events/javascripts/countly.details.models.js +++ b/frontend/express/public/core/events/javascripts/countly.details.models.js @@ -888,9 +888,7 @@ return countlyAllEvents.service.fetchAllEventsData(context, period) .then(function(res) { if (res) { - if (Array.isArray(res.list)) { - res.list = res.list.map(eventName => countlyCommon.unescapeHtml(eventName)); - } + // setAllEventsData decodes res.list and res.map keys in place context.commit("setAllEventsData", res); if ((!context.state.selectedEventName) || (res.map && res.map[context.state.selectedEventName] && !res.map[context.state.selectedEventName].is_visible) || (res.list && res.list.indexOf(context.state.selectedEventName) === -1)) { var appId = countlyCommon.ACTIVE_APP_ID; @@ -1092,6 +1090,25 @@ var allEventsMutations = { setAllEventsData: function(state, value) { + // The API HTML-escapes every key and value on the way out (common.returnOutput), + // but the rest of this store works with raw event keys - selectedEventName is + // taken from `list`. Normalise `list` and the `map` keys together here, the one + // point both fetch paths commit through, so lookups like map[selectedEventName] + // don't silently miss for keys containing & < > " '. Keys without those chars + // (dots, dashes) were unaffected, which is why only some events lost their + // description on the Events page. + if (value && Array.isArray(value.list)) { + value.list = value.list.map(function(eventName) { + return countlyCommon.unescapeHtml(eventName); + }); + } + if (value && value.map && typeof value.map === "object") { + var decodedMap = {}; + Object.keys(value.map).forEach(function(eventKey) { + decodedMap[countlyCommon.unescapeHtml(eventKey)] = value.map[eventKey]; + }); + value.map = decodedMap; + } state.allEventsData = value; }, setAllEventsList: function(state, value) { diff --git a/plugins/data-manager/frontend/public/javascripts/countly.views.js b/plugins/data-manager/frontend/public/javascripts/countly.views.js index ef5f22390e2..5ee9a5839db 100644 --- a/plugins/data-manager/frontend/public/javascripts/countly.views.js +++ b/plugins/data-manager/frontend/public/javascripts/countly.views.js @@ -696,7 +696,9 @@ var isVisible = command === 'visible'; var events = []; rows.forEach(function(row) { - events.push(row.key); + // row.key is still API-escaped; change_visibility writes events.map[key] + // directly, so submitting it raw would create a ghost map entry + events.push(countlyCommon.unescapeHtml(row.key)); }); this.$store.dispatch('countlyDataManager/changeVisibility', { events: events, isVisible: isVisible }).then(function() { countlyEvent.refreshEvents(); @@ -1324,8 +1326,22 @@ }, handleEdit: function() { var event = JSON.parse(JSON.stringify(this.event)); - event.segments = this.segments; + event.segments = JSON.parse(JSON.stringify(this.segments)); event.isEditMode = true; + // The store holds values exactly as the API returned them, and common.returnOutput + // HTML-escapes every string on the way out. The events table decodes these before + // opening the same drawer (see the 'dm-open-edit-event-drawer' handler) - do the + // same here, otherwise a key like "a & b" is submitted back as "a & b" and + // forks a second drill_meta doc / events.map entry that shows up as a ghost row. + event.key = countlyCommon.unescapeHtml(event.key); + event.e = countlyCommon.unescapeHtml(event.e); + event.name = countlyCommon.unescapeHtml(event.name); + event.description = countlyCommon.unescapeHtml(event.description); + event.categoryName = countlyCommon.unescapeHtml(event.categoryName); + event.segments = event.segments.map(function(seg) { + seg.name = countlyCommon.unescapeHtml(seg.name); + return seg; + }); this.openDrawer("events", event); }, handleEditSegment: function(seg) { From cd06be9ff381c45686215c5e1377e6153759b338 Mon Sep 17 00:00:00 2001 From: Gia Cajes Date: Wed, 29 Jul 2026 12:02:38 -0600 Subject: [PATCH 3/3] fix(events): use a null-prototype object for the decoded event map Event keys are arbitrary user-supplied strings. Assigning a "__proto__" key on a plain object reparents the map rather than adding an own property, which would let unrelated key lookups resolve through the assigned value. Object.create(null) removes that side effect. Co-Authored-By: Claude Opus 5 --- .../core/events/javascripts/countly.details.models.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/express/public/core/events/javascripts/countly.details.models.js b/frontend/express/public/core/events/javascripts/countly.details.models.js index b2a8bdf7d1c..1d67ad7aae8 100644 --- a/frontend/express/public/core/events/javascripts/countly.details.models.js +++ b/frontend/express/public/core/events/javascripts/countly.details.models.js @@ -1103,7 +1103,11 @@ }); } if (value && value.map && typeof value.map === "object") { - var decodedMap = {}; + //null prototype: event keys are arbitrary strings, and assigning a + //"__proto__" key on a plain object reparents the map instead of adding + //an own property, which would let unrelated key lookups resolve through + //the assigned value + var decodedMap = Object.create(null); Object.keys(value.map).forEach(function(eventKey) { decodedMap[countlyCommon.unescapeHtml(eventKey)] = value.map[eventKey]; });