Skip to content

Commit 1ffb7f8

Browse files
feat(logs): Add JSON pretty-printing for log attributes
1 parent 4e79ede commit 1ffb7f8

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

static/app/views/explore/components/traceItemAttributes/attributesTreeValue.spec.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,96 @@ describe('AttributesTreeValue', () => {
129129

130130
expect(screen.getByText('null')).toBeInTheDocument();
131131
});
132+
133+
it('renders JSON object values as structured data', () => {
134+
const jsonContent = {
135+
...defaultProps.content,
136+
value: '{"key": "value", "number": 42}',
137+
};
138+
139+
render(<AttributesTreeValue {...defaultProps} content={jsonContent} />);
140+
141+
expect(screen.getByText('key')).toBeInTheDocument();
142+
expect(screen.getByText('value')).toBeInTheDocument();
143+
expect(screen.getByText('42')).toBeInTheDocument();
144+
});
145+
146+
it('renders JSON array values as structured data', () => {
147+
const jsonContent = {
148+
...defaultProps.content,
149+
value: '[1, 2, 3]',
150+
};
151+
152+
render(<AttributesTreeValue {...defaultProps} content={jsonContent} />);
153+
154+
expect(screen.getByText('1')).toBeInTheDocument();
155+
expect(screen.getByText('2')).toBeInTheDocument();
156+
expect(screen.getByText('3')).toBeInTheDocument();
157+
});
158+
159+
it('renders invalid JSON containing braces as plain text', () => {
160+
const invalidJsonContent = {
161+
...defaultProps.content,
162+
value: 'not {json',
163+
};
164+
165+
render(<AttributesTreeValue {...defaultProps} content={invalidJsonContent} />);
166+
167+
expect(screen.getByText('not {json')).toBeInTheDocument();
168+
});
169+
170+
it('renders plain strings without braces as plain text', () => {
171+
const plainContent = {
172+
...defaultProps.content,
173+
value: 'hello world',
174+
};
175+
176+
render(<AttributesTreeValue {...defaultProps} content={plainContent} />);
177+
178+
expect(screen.getByText('hello world')).toBeInTheDocument();
179+
});
180+
181+
it('does not render JSON as structured data when disableRichValue is true', () => {
182+
const jsonContent = {
183+
...defaultProps.content,
184+
value: '{"key": "value"}',
185+
};
186+
187+
render(
188+
<AttributesTreeValue
189+
{...defaultProps}
190+
content={jsonContent}
191+
config={{disableRichValue: true}}
192+
/>
193+
);
194+
195+
expect(screen.getByText('{"key": "value"}')).toBeInTheDocument();
196+
expect(screen.queryByRole('list')).not.toBeInTheDocument();
197+
});
198+
199+
it('renders simple JSON with compact class', () => {
200+
const jsonContent = {
201+
...defaultProps.content,
202+
value: '{"boop": "bop"}',
203+
};
204+
205+
render(<AttributesTreeValue {...defaultProps} content={jsonContent} />);
206+
207+
const pre = screen.getByText('bop').closest('pre');
208+
expect(pre).toHaveClass('compact');
209+
});
210+
211+
it('renders long JSON without compact class', () => {
212+
const jsonContent = {
213+
...defaultProps.content,
214+
value: '{"k": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}',
215+
};
216+
217+
render(<AttributesTreeValue {...defaultProps} content={jsonContent} />);
218+
219+
const pre = screen
220+
.getByText('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
221+
.closest('pre');
222+
expect(pre).not.toHaveClass('compact');
223+
});
132224
});

static/app/views/explore/components/traceItemAttributes/attributesTreeValue.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import styled from '@emotion/styled';
33

44
import {openNavigateToExternalLinkModal} from 'sentry/actionCreators/modal';
55
import {ExternalLink} from 'sentry/components/links/externalLink';
6+
import {StructuredEventData} from 'sentry/components/structuredEventData';
67
import {type RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
78
import {isUrl} from 'sentry/utils/string/isUrl';
89
import {AnnotatedAttributeTooltip} from 'sentry/views/explore/components/annotatedAttributeTooltip';
910
import {getAttributeItem} from 'sentry/views/explore/components/traceItemAttributes/utils';
1011
import {TraceItemMetaInfo} from 'sentry/views/explore/utils';
12+
import {tryParseJson} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/utils';
1113

1214
import type {
1315
AttributesFieldRender,
@@ -57,6 +59,18 @@ export function AttributesTreeValue<RendererExtra extends RenderFunctionBaggage>
5759
);
5860
}
5961
}
62+
const parsedJson = tryParseJson(content.value);
63+
if (typeof parsedJson === 'object' && parsedJson !== null) {
64+
return (
65+
<AttributeStructuredData
66+
data={parsedJson}
67+
maxDefaultDepth={2}
68+
withAnnotatedText={false}
69+
className={String(content.value).length <= 48 ? 'compact' : undefined}
70+
/>
71+
);
72+
}
73+
6074
return isUrl(String(content.value)) ? (
6175
<AttributeLinkText>
6276
<ExternalLink
@@ -86,3 +100,29 @@ const AttributeLinkText = styled('span')`
86100
white-space: normal;
87101
}
88102
`;
103+
104+
const AttributeStructuredData = styled(StructuredEventData)`
105+
margin: 0;
106+
padding: 0;
107+
background: transparent;
108+
white-space: pre-wrap;
109+
word-break: break-word;
110+
111+
&.compact {
112+
display: inline;
113+
114+
span[data-base-with-toggle='true'] {
115+
display: inline;
116+
padding-left: 0;
117+
}
118+
119+
button {
120+
display: none;
121+
}
122+
123+
div {
124+
display: inline;
125+
padding-left: 0;
126+
}
127+
}
128+
`;

0 commit comments

Comments
 (0)