Skip to content

Commit 8c8b184

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

File tree

3 files changed

+138
-5
lines changed

3 files changed

+138
-5
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,8 @@
264264
"eslint-plugin-no-relative-import-paths": "^1.6.1",
265265
"eslint-plugin-react": "7.37.5",
266266
"eslint-plugin-react-hooks": "6.1.0",
267-
"eslint-plugin-regexp": "^3.0.0",
268267
"eslint-plugin-react-you-might-not-need-an-effect": "0.5.3",
268+
"eslint-plugin-regexp": "^3.0.0",
269269
"eslint-plugin-sentry": "^2.10.0",
270270
"eslint-plugin-testing-library": "^7.16.0",
271271
"eslint-plugin-typescript-sort-keys": "^3.3.0",

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: 45 additions & 4 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,
@@ -32,11 +34,12 @@ export function AttributesTreeValue<RendererExtra extends RenderFunctionBaggage>
3234
// Check if we have a custom renderer for this attribute
3335
const attributeKey = originalAttribute.original_attribute_key;
3436
const renderer = renderers[attributeKey];
37+
const value = String(content.value);
3538

36-
const defaultValue = <span>{String(content.value)}</span>;
39+
const defaultValue = <span>{value}</span>;
3740

3841
if (config?.disableRichValue) {
39-
return String(content.value);
42+
return value;
4043
}
4144

4245
if (renderer) {
@@ -57,12 +60,24 @@ export function AttributesTreeValue<RendererExtra extends RenderFunctionBaggage>
5760
);
5861
}
5962
}
60-
return isUrl(String(content.value)) ? (
63+
const parsedJson = tryParseJson(content.value);
64+
if (typeof parsedJson === 'object') {
65+
return (
66+
<AttributeStructuredData
67+
data={parsedJson}
68+
maxDefaultDepth={2}
69+
withAnnotatedText={false}
70+
className={value.length <= 48 ? 'compact' : undefined}
71+
/>
72+
);
73+
}
74+
75+
return isUrl(value) ? (
6176
<AttributeLinkText>
6277
<ExternalLink
6378
onClick={e => {
6479
e.preventDefault();
65-
openNavigateToExternalLinkModal({linkText: String(content.value)});
80+
openNavigateToExternalLinkModal({linkText: value});
6681
}}
6782
>
6883
{defaultValue}
@@ -86,3 +101,29 @@ const AttributeLinkText = styled('span')`
86101
white-space: normal;
87102
}
88103
`;
104+
105+
const AttributeStructuredData = styled(StructuredEventData)`
106+
margin: 0;
107+
padding: 0;
108+
background: transparent;
109+
white-space: pre-wrap;
110+
word-break: break-word;
111+
112+
&.compact {
113+
display: inline;
114+
115+
span[data-base-with-toggle='true'] {
116+
display: inline;
117+
padding-left: 0;
118+
}
119+
120+
button {
121+
display: none;
122+
}
123+
124+
div {
125+
display: inline;
126+
padding-left: 0;
127+
}
128+
}
129+
`;

0 commit comments

Comments
 (0)