Compare commits

...
Author SHA1 Message Date
Shivam Mishra e47d7ae894 refactor: remove METRIC_TO_HIDDEN_KEY 2026-02-04 21:01:29 +05:30
Shivam MishraandGitHub e6df7f7dd7 Merge branch 'develop' into feat/disable-report-metrics 2026-02-04 20:48:33 +05:30
Shivam Mishra 0f5a25bad7 feat: hide report keys 2026-02-04 19:40:25 +05:30
Shivam Mishra 9f80c41e75 refactor: use useAccount composable 2026-02-04 19:20:14 +05:30
Shivam Mishra a4cee4f72d perf(reports): skip computing hidden metrics in summary
Instead of filtering metrics after computation, skip the expensive
database queries entirely for hidden metrics. This prevents timeouts
on accounts with large datasets.

- MetricBuilder now accepts hidden_metrics param and returns nil for
  hidden metrics (compacted from response)
- Reports controller passes hidden_metrics to builder params
2026-02-04 13:44:15 +05:30
Shivam Mishra a34a916b46 feat(reports): hide metrics in frontend reports UI
Add useHiddenMetrics composable to filter hidden metrics from
report keys. Update WootReports.vue to use the composable so
hidden metrics are not fetched or displayed in the reports UI.
2026-02-04 13:20:41 +05:30
Shivam Mishra b09a2c25fa feat(reports): filter hidden metrics from API responses
Apply MetricFilter concern to reports controllers:
- ReportsController: return empty array for hidden timeseries metrics,
  filter summary and bot_summary responses
- SummaryReportsController: filter hidden metrics from array responses
2026-02-04 13:20:28 +05:30
Shivam Mishra 7e65cc752e feat(reports): add MetricFilter concern for hiding metrics
Add Reports::MetricFilter concern that provides methods to filter
hidden metrics from API responses:
- filter_hidden_metrics_from_hash: filters hash responses
- filter_hidden_metrics_from_array: filters array responses
- filter_summary_with_previous: filters summary with nested previous data
- metric_hidden?: checks if a specific metric should be hidden

Includes alias support (avg_reply_time -> reply_time).
2026-02-04 13:20:14 +05:30
Shivam Mishra 0f02c0cd5c feat(account): add report_hidden_metrics setting
Add report_hidden_metrics to account settings schema to allow
hiding specific metrics from reports on a per-account basis.

Supported metrics: outgoing_messages_count, incoming_messages_count, reply_time
2026-02-04 13:19:57 +05:30
9 changed files with 185 additions and 52 deletions
@@ -1,14 +1,16 @@
class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::BaseReportBuilder
HIDEABLE_METRICS = %w[incoming_messages_count outgoing_messages_count reply_time].freeze
def summary
{
conversations_count: count('conversations_count'),
incoming_messages_count: count('incoming_messages_count'),
outgoing_messages_count: count('outgoing_messages_count'),
incoming_messages_count: count_unless_hidden('incoming_messages_count'),
outgoing_messages_count: count_unless_hidden('outgoing_messages_count'),
avg_first_response_time: count('avg_first_response_time'),
avg_resolution_time: count('avg_resolution_time'),
resolutions_count: count('resolutions_count'),
reply_time: count('reply_time')
}
reply_time: count_unless_hidden('reply_time')
}.compact
end
def bot_summary
@@ -24,6 +26,18 @@ class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::Ba
builder_class(metric).new(account, builder_params(metric)).aggregate_value
end
def count_unless_hidden(metric)
return nil if metric_hidden?(metric)
count(metric)
end
def metric_hidden?(metric)
return false unless HIDEABLE_METRICS.include?(metric)
(params[:hidden_metrics] || []).include?(metric)
end
def builder_params(metric)
params.merge({ metric: metric })
end
@@ -1,10 +1,14 @@
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
include Api::V2::Accounts::ReportsHelper
include Api::V2::Accounts::HeatmapHelper
include Reports::MetricFilter
before_action :check_authorization
def index
# Return empty timeseries array for hidden metrics (preserves expected response shape)
return render json: [] if metric_hidden?(params[:metric])
builder = V2::Reports::Conversations::ReportBuilder.new(Current.account, report_params)
data = builder.timeseries
render json: data
@@ -112,7 +116,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
common_params.merge({
since: range[:current][:since],
until: range[:current][:until],
timezone_offset: params[:timezone_offset]
timezone_offset: params[:timezone_offset],
hidden_metrics: hidden_metrics
})
end
@@ -120,7 +125,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
common_params.merge({
since: range[:previous][:since],
until: range[:previous][:until],
timezone_offset: params[:timezone_offset]
timezone_offset: params[:timezone_offset],
hidden_metrics: hidden_metrics
})
end
@@ -1,4 +1,6 @@
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
include Reports::MetricFilter
before_action :check_authorization
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label, :channel]
@@ -40,7 +42,7 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
def render_report_with(builder_class)
builder = builder_class.new(account: Current.account, params: @builder_params)
render json: builder.build
render json: filter_hidden_metrics_from_array(builder.build)
end
def permitted_params
@@ -0,0 +1,59 @@
module Reports::MetricFilter
extend ActiveSupport::Concern
HIDEABLE_METRICS = %w[outgoing_messages_count incoming_messages_count reply_time].freeze
# Alias mapping (canonical form)
METRIC_ALIASES = {
'avg_reply_time' => 'reply_time'
}.freeze
private
def filter_hidden_metrics_from_hash(data)
return data if hidden_metrics.blank?
data.reject { |key, _| metric_key_hidden?(key) }
end
def filter_hidden_metrics_from_array(data)
return data if hidden_metrics.blank?
data.map { |item| item.reject { |key, _| metric_key_hidden?(key) } }
end
# Filter summary data including nested 'previous' key (handles both symbol and string keys)
def filter_summary_with_previous(data)
return data if hidden_metrics.blank?
filtered = filter_hidden_metrics_from_hash(data)
previous_data = filtered[:previous] || filtered['previous']
if previous_data.present?
filtered_previous = filter_hidden_metrics_from_hash(previous_data)
# Preserve the original key type
if filtered.key?(:previous)
filtered[:previous] = filtered_previous
else
filtered['previous'] = filtered_previous
end
end
filtered
end
def hidden_metrics
@hidden_metrics ||= (Current.account.report_hidden_metrics || []) & HIDEABLE_METRICS
end
def metric_hidden?(metric_name)
# Normalize to canonical form (e.g., avg_reply_time -> reply_time)
normalized = METRIC_ALIASES[metric_name.to_s] || metric_name.to_s
hidden_metrics.include?(normalized)
end
# Check if a hash key should be hidden (handles both symbol and string keys, plus aliases)
def metric_key_hidden?(key)
key_str = key.to_s
normalized = METRIC_ALIASES[key_str] || key_str
hidden_metrics.include?(normalized)
end
end
@@ -0,0 +1,29 @@
import { computed } from 'vue';
import { useAccount } from './useAccount';
// Alias mapping for consistency (canonical form is reply_time)
const METRIC_ALIASES = {
avg_reply_time: 'reply_time',
};
export function useHiddenMetrics() {
const { currentAccount } = useAccount();
const hiddenMetrics = computed(() => {
return currentAccount.value?.settings?.report_hidden_metrics || [];
});
const isMetricHidden = metricKey => {
const normalized = METRIC_ALIASES[metricKey] || metricKey;
return hiddenMetrics.value.includes(normalized);
};
const filterVisibleReportKeys = reportKeys => {
// Filter out hidden metrics from reportKeys object
return Object.fromEntries(
Object.entries(reportKeys).filter(([, value]) => !isMetricHidden(value))
);
};
return { hiddenMetrics, isMetricHidden, filterVisibleReportKeys };
}
@@ -1,6 +1,7 @@
<script>
import V4Button from 'dashboard/components-next/button/Button.vue';
import { useAlert, useTrack } from 'dashboard/composables';
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
import ReportFilterSelector from './components/FilterSelector.vue';
import { GROUP_BY_FILTER } from './constants';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
@@ -8,7 +9,7 @@ import { generateFileName } from 'dashboard/helper/downloadHelper';
import ReportContainer from './ReportContainer.vue';
import ReportHeader from './components/ReportHeader.vue';
const REPORTS_KEYS = {
const ALL_REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count',
INCOMING_MESSAGES: 'incoming_messages_count',
OUTGOING_MESSAGES: 'outgoing_messages_count',
@@ -26,6 +27,10 @@ export default {
ReportContainer,
V4Button,
},
setup() {
const { filterVisibleReportKeys } = useHiddenMetrics();
return { filterVisibleReportKeys };
},
data() {
return {
from: 0,
@@ -34,6 +39,11 @@ export default {
businessHours: false,
};
},
computed: {
reportKeys() {
return this.filterVisibleReportKeys(ALL_REPORTS_KEYS);
},
},
methods: {
fetchAllData() {
this.fetchAccountSummary();
@@ -47,18 +57,10 @@ export default {
}
},
fetchChartData() {
[
'CONVERSATIONS',
'INCOMING_MESSAGES',
'OUTGOING_MESSAGES',
'FIRST_RESPONSE_TIME',
'RESOLUTION_TIME',
'RESOLUTION_COUNT',
'REPLY_TIME',
].forEach(async key => {
Object.keys(this.reportKeys).forEach(async key => {
try {
await this.$store.dispatch('fetchAccountReport', {
metric: REPORTS_KEYS[key],
metric: this.reportKeys[key],
...this.getRequestPayload(),
});
} catch {
@@ -121,6 +123,6 @@ export default {
show-group-by-filter
@filter-change="onFilterChange"
/>
<ReportContainer :group-by="groupBy" />
<ReportContainer :group-by="groupBy" :report-keys="reportKeys" />
</div>
</template>
@@ -2,6 +2,7 @@
import ReportFilterSelector from './FilterSelector.vue';
import { formatTime } from '@chatwoot/utils';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
import Table from 'dashboard/components/table/Table.vue';
import { generateFileName } from 'dashboard/helper/downloadHelper';
import {
@@ -44,6 +45,7 @@ import SummaryReportLink from './SummaryReportLink.vue';
const rowItems = useMapGetter([props.getterKey]) || [];
const reportMetrics = useMapGetter([props.summaryKey]) || [];
const { isMetricHidden } = useHiddenMetrics();
const getMetrics = id =>
reportMetrics.value.find(metrics => metrics.id === Number(id)) || {};
@@ -59,38 +61,42 @@ const defaulSpanRender = cellProps =>
cellProps.getValue()
);
const columns = computed(() => [
columnHelper.accessor('name', {
header: t(`SUMMARY_REPORTS.${props.type.toUpperCase()}`),
width: 300,
cell: cellProps => h(SummaryReportLink, cellProps),
}),
columnHelper.accessor('conversationsCount', {
header: t('SUMMARY_REPORTS.CONVERSATIONS'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('avgFirstResponseTime', {
header: t('SUMMARY_REPORTS.AVG_FIRST_RESPONSE_TIME'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('avgResolutionTime', {
header: t('SUMMARY_REPORTS.AVG_RESOLUTION_TIME'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('avgReplyTime', {
header: t('SUMMARY_REPORTS.AVG_REPLY_TIME'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('resolutionsCount', {
header: t('SUMMARY_REPORTS.RESOLUTION_COUNT'),
width: 200,
cell: defaulSpanRender,
}),
]);
const columns = computed(() => {
const allColumns = [
columnHelper.accessor('name', {
header: t(`SUMMARY_REPORTS.${props.type.toUpperCase()}`),
width: 300,
cell: cellProps => h(SummaryReportLink, cellProps),
}),
columnHelper.accessor('conversationsCount', {
header: t('SUMMARY_REPORTS.CONVERSATIONS'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('avgFirstResponseTime', {
header: t('SUMMARY_REPORTS.AVG_FIRST_RESPONSE_TIME'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('avgResolutionTime', {
header: t('SUMMARY_REPORTS.AVG_RESOLUTION_TIME'),
width: 200,
cell: defaulSpanRender,
}),
!isMetricHidden('reply_time') &&
columnHelper.accessor('avgReplyTime', {
header: t('SUMMARY_REPORTS.AVG_REPLY_TIME'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('resolutionsCount', {
header: t('SUMMARY_REPORTS.RESOLUTION_COUNT'),
width: 200,
cell: defaulSpanRender,
}),
];
return allColumns.filter(Boolean);
});
const renderAvgTime = value => (value ? formatTime(value) : '--');
@@ -1,6 +1,7 @@
<script>
import V4Button from 'dashboard/components-next/button/Button.vue';
import { useAlert, useTrack } from 'dashboard/composables';
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
import ReportFilters from './ReportFilters.vue';
import ReportContainer from '../ReportContainer.vue';
import { GROUP_BY_FILTER } from '../constants';
@@ -63,6 +64,10 @@ export default {
default: null,
},
},
setup() {
const { filterVisibleReportKeys } = useHiddenMetrics();
return { filterVisibleReportKeys };
},
data() {
return {
from: 0,
@@ -82,7 +87,7 @@ export default {
return this.type === 'agent';
},
reportKeys() {
return {
const allKeys = {
CONVERSATIONS: 'conversations_count',
...(!this.isAgentType && {
INCOMING_MESSAGES: 'incoming_messages_count',
@@ -93,6 +98,8 @@ export default {
RESOLUTION_COUNT: 'resolutions_count',
REPLY_TIME: 'reply_time',
};
// Filter out hidden metrics
return this.filterVisibleReportKeys(allKeys);
},
},
mounted() {
+8
View File
@@ -67,6 +67,13 @@ class Account < ApplicationRecord
'help_center_search': { 'type': %w[boolean null] }
},
'additionalProperties': false
},
'report_hidden_metrics': {
'type': %w[array null],
'items': {
'type': 'string',
'enum': %w[outgoing_messages_count incoming_messages_count reply_time]
}
}
},
'required': [],
@@ -88,6 +95,7 @@ class Account < ApplicationRecord
store_accessor :settings, :audio_transcriptions, :auto_resolve_label
store_accessor :settings, :captain_models, :captain_features
store_accessor :settings, :report_hidden_metrics
has_many :account_users, dependent: :destroy_async
has_many :agent_bot_inboxes, dependent: :destroy_async