diff --git a/webapp/src/components/retrospective_post.tsx b/webapp/src/components/retrospective_post.tsx index e247b5d1a8..9b9f1710fe 100644 --- a/webapp/src/components/retrospective_post.tsx +++ b/webapp/src/components/retrospective_post.tsx @@ -10,6 +10,13 @@ import {Metric, MetricType} from 'src/types/playbook'; import {RunMetricData} from 'src/types/playbook_run'; +import { + isArrayOf, + isMetric, + isMetricData, + safeJSONParse, +} from 'src/utils'; + import {ClockOutline, DollarSign, PoundSign} from './backstage/playbook_edit/styles'; import {metricToString} from './backstage/playbook_edit/metrics/shared'; @@ -33,8 +40,11 @@ export const RetrospectivePost = (props: Props) => { const mdText = (text: string) => messageHtmlToComponent(formatText(text, markdownOptions), true, messageHtmlToComponentOptions); - const metricsConfigs: Array = JSON.parse(props.post.props.metricsConfigs); - const metricsData: Array = JSON.parse(props.post.props.metricsData); + const parsedMetricsConfigs = safeJSONParse(props.post.props?.metricsConfigs); + const parsedMetricsData = safeJSONParse(props.post.props?.metricsData); + + const metricsConfigs: Array = isArrayOf(parsedMetricsConfigs, isMetric) ? parsedMetricsConfigs : []; + const metricsData: Array = isArrayOf(parsedMetricsData, isMetricData) ? parsedMetricsData : []; return ( <> diff --git a/webapp/src/utils.ts b/webapp/src/utils.ts index bec69ebeb2..d50e06c4e8 100644 --- a/webapp/src/utils.ts +++ b/webapp/src/utils.ts @@ -1,7 +1,7 @@ import {useState} from 'react'; -import {PlaybookRun} from 'src/types/playbook_run'; -import {Checklist} from 'src/types/playbook'; +import {PlaybookRun, RunMetricData} from 'src/types/playbook_run'; +import {Checklist, Metric} from 'src/types/playbook'; let idCounter = 0; @@ -132,3 +132,40 @@ export function runCallsSlashCommand(command: string, channelId: string, teamId: }, }, window.origin); } + +export function safeJSONParse(value: unknown): T | null { + if (!value || typeof value !== 'string') { + return null; + } + + try { + return JSON.parse(value) as T; + } catch { + return null; + } +} + +export function isArrayOf(value: unknown, typeGuard: (value: unknown) => value is T): value is T[] { + return Array.isArray(value) && value.every(typeGuard); +} + +export function isMetric(value: unknown): value is Metric { + if (!value || typeof value !== 'object') { + return false; + } + + const metric = value as Metric; + return typeof metric.id === 'string' && + typeof metric.title === 'string' && + typeof metric.type === 'string'; +} + +export function isMetricData(value: unknown): value is RunMetricData { + if (!value || typeof value !== 'object') { + return false; + } + + const metricData = value as RunMetricData; + return typeof metricData.metric_config_id === 'string' && + (typeof metricData.value === 'string' || typeof metricData.value === 'number'); +}