Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {useEffect, useRef} from 'react';
import omit from 'lodash/omit';
import uniq from 'lodash/uniq';
import Prism from 'prismjs';

import KeyValueList from 'sentry/components/events/interfaces/keyValueList';
import {EntryRequestDataGraphQl, Event} from 'sentry/types';
import {loadPrismLanguage} from 'sentry/utils/loadPrismLanguage';

type GraphQlBodyProps = {data: EntryRequestDataGraphQl['data']; event: Event};

type GraphQlErrors = Array<{
locations?: Array<{column: number; line: number}>;
message?: string;
path?: string[];
}>;

function getGraphQlErrorsFromResponseContext(event: Event): GraphQlErrors {
const responseData = event.contexts?.response?.data;

if (
responseData &&
typeof responseData === 'object' &&
'errors' in responseData &&
Array.isArray(responseData.errors) &&
responseData.errors.every(error => typeof error === 'object')
) {
return responseData.errors;
}

return [];
}

function getErrorLineNumbers(errors: GraphQlErrors): number[] {
return uniq(
errors.flatMap(
error =>
error.locations?.map(loc => loc?.line).filter(line => typeof line === 'number') ??
[]
)
);
}

export function GraphQlRequestBody({data, event}: GraphQlBodyProps) {
const ref = useRef<HTMLElement | null>(null);

// https://prismjs.com/plugins/line-highlight/
useEffect(() => {
import('prismjs/plugins/line-highlight/prism-line-highlight');
}, []);

useEffect(() => {
const element = ref.current;
if (!element) {
return;
}

if ('graphql' in Prism.languages) {
Prism.highlightElement(element);
return;
}

loadPrismLanguage('graphql', {onLoad: () => Prism.highlightElement(element)});
}, []);

const errors = getGraphQlErrorsFromResponseContext(event);
const erroredLines = getErrorLineNumbers(errors);

return (
<div>
<pre className="language-graphql" data-line={erroredLines.join(',')}>
<code className="language-graphql" ref={ref}>
{data.query}
</code>
</pre>
<KeyValueList
data={Object.entries(omit(data, 'query')).map(([key, value]) => ({
key,
subject: key,
value,
}))}
isContextData
/>
</div>
);
}
73 changes: 72 additions & 1 deletion static/app/components/events/interfaces/request/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,10 @@ describe('Request entry', function () {
).toBeInTheDocument(); // tooltip description
});

describe('getBodySection', function () {
describe('body section', function () {
it('should return plain-text when given unrecognized inferred Content-Type', function () {
const data: EntryRequest['data'] = {
apiTarget: null,
query: [],
data: 'helloworld',
headers: [],
Expand Down Expand Up @@ -219,6 +220,7 @@ describe('Request entry', function () {

it('should return a KeyValueList element when inferred Content-Type is x-www-form-urlencoded', function () {
const data: EntryRequest['data'] = {
apiTarget: null,
query: [],
data: {foo: ['bar'], bar: ['baz']},
headers: [],
Expand Down Expand Up @@ -253,6 +255,7 @@ describe('Request entry', function () {

it('should return a ContextData element when inferred Content-Type is application/json', function () {
const data: EntryRequest['data'] = {
apiTarget: null,
query: [],
data: {foo: 'bar'},
headers: [],
Expand Down Expand Up @@ -289,6 +292,7 @@ describe('Request entry', function () {
// > decodeURIComponent('a%AFc')
// URIError: URI malformed
const data: EntryRequest['data'] = {
apiTarget: null,
query: 'a%AFc',
data: '',
headers: [],
Expand Down Expand Up @@ -320,6 +324,7 @@ describe('Request entry', function () {

it("should not cause an invariant violation if data.data isn't a string", function () {
const data: EntryRequest['data'] = {
apiTarget: null,
query: [],
data: [{foo: 'bar', baz: 1}],
headers: [],
Expand Down Expand Up @@ -348,5 +353,71 @@ describe('Request entry', function () {
})
).not.toThrow();
});

describe('graphql', function () {
it('should render a graphql query and variables', function () {
const data: EntryRequest['data'] = {
apiTarget: 'graphql',
method: 'POST',
url: '/graphql/',
data: {
query: 'query Test { test }',
variables: {foo: 'bar'},
operationName: 'Test',
},
};

const event = {
...TestStubs.Event(),
entries: [
{
type: EntryType.REQUEST,
data,
},
],
};

render(<Request event={event} data={event.entries[0].data} />);

expect(screen.getByText('query Test { test }')).toBeInTheDocument();
expect(screen.getByRole('row', {name: 'operationName Test'})).toBeInTheDocument();
expect(
screen.getByRole('row', {name: 'variables { foo : bar }'})
).toBeInTheDocument();
});

it('highlights graphql query lines with errors', function () {
const data: EntryRequest['data'] = {
apiTarget: 'graphql',
method: 'POST',
url: '/graphql/',
data: {
query: 'query Test { test }',
variables: {foo: 'bar'},
operationName: 'Test',
},
};

const event = {
...TestStubs.Event(),
entries: [
{
type: EntryType.REQUEST,
data,
},
],
contexts: {response: {data: {errors: [{locations: [{line: 1}]}]}}},
};

const {container} = render(
<Request event={event} data={event.entries[0].data} />
);

expect(container.querySelector('.line-highlight')).toBeInTheDocument();
expect(
container.querySelector('.line-highlight')?.getAttribute('data-start')
).toBe('1');
});
});
});
});
37 changes: 27 additions & 10 deletions static/app/components/events/interfaces/request/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styled from '@emotion/styled';
import ClippedBox from 'sentry/components/clippedBox';
import ErrorBoundary from 'sentry/components/errorBoundary';
import {EventDataSection} from 'sentry/components/events/eventDataSection';
import {GraphQlRequestBody} from 'sentry/components/events/interfaces/request/graphQlRequestBody';
import {getCurlCommand, getFullUrl} from 'sentry/components/events/interfaces/utils';
import ExternalLink from 'sentry/components/links/externalLink';
import {SegmentedControl} from 'sentry/components/segmentedControl';
Expand All @@ -17,14 +18,36 @@ import {defined, isUrl} from 'sentry/utils';
import {RichHttpContentClippedBoxBodySection} from './richHttpContentClippedBoxBodySection';
import {RichHttpContentClippedBoxKeyValueList} from './richHttpContentClippedBoxKeyValueList';

type Props = {
interface RequestProps {
data: EntryRequest['data'];
event: Event;
};
}

interface RequestBodyProps extends RequestProps {
meta: any;
}

type View = 'formatted' | 'curl';

export function Request({data, event}: Props) {
function RequestBodySection({data, event, meta}: RequestBodyProps) {
if (!defined(data.data)) {
return null;
}

if (data.apiTarget === 'graphql' && typeof data.data.query === 'string') {
return <GraphQlRequestBody data={data.data} {...{event, meta}} />;
}

return (
<RichHttpContentClippedBoxBodySection
data={data.data}
inferredContentType={data.inferredContentType}
meta={meta?.data}
/>
);
}

export function Request({data, event}: RequestProps) {
const entryIndex = event.entries.findIndex(entry => entry.type === EntryType.REQUEST);
const meta = event._meta?.entries?.[entryIndex]?.data;

Expand Down Expand Up @@ -106,13 +129,7 @@ export function Request({data, event}: Props) {
</ErrorBoundary>
</ClippedBox>
)}
{defined(data.data) && (
<RichHttpContentClippedBoxBodySection
data={data.data}
inferredContentType={data.inferredContentType}
meta={meta?.data}
/>
)}
<RequestBodySection {...{data, event, meta}} />
{defined(data.cookies) && Object.keys(data.cookies).length > 0 && (
<RichHttpContentClippedBoxKeyValueList
defaultCollapsed
Expand Down
9 changes: 6 additions & 3 deletions static/app/styles/prism.tsx
Copy link
Member Author

@malwilley malwilley Jun 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vuluongj20 was hoping you could review the css changes here

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export const prismStyles = (theme: Theme) => css`
padding: ${space(1)} ${space(2)};
border-radius: ${theme.borderRadius};
box-shadow: none;

code {
background: unset;
vertical-align: middle;
}
}

pre[class*='language-'],
Expand Down Expand Up @@ -106,10 +111,8 @@ export const prismStyles = (theme: Theme) => css`
}
.line-highlight {
position: absolute;
left: 0;
left: -${space(2)};
right: 0;
padding: inherit 0;
margin-top: 1em;
background: var(--prism-highlight-background);
box-shadow: inset 5px 0 0 var(--prism-highlight-accent);
z-index: 0;
Expand Down
47 changes: 33 additions & 14 deletions static/app/types/event.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,22 +332,35 @@ type EntryMessage = {
type: EntryType.MESSAGE;
};

export type EntryRequest = {
export interface EntryRequestDataDefault {
apiTarget: null;
method: string;
url: string;
cookies?: [key: string, value: string][];
data?: string | null | Record<string, any> | [key: string, value: any][];
env?: Record<string, string>;
fragment?: string | null;
headers?: [key: string, value: string][];
inferredContentType?:
| null
| 'application/json'
| 'application/x-www-form-urlencoded'
| 'multipart/form-data';
query?: [key: string, value: string][] | string;
}

export interface EntryRequestDataGraphQl
extends Omit<EntryRequestDataDefault, 'apiTarget' | 'data'> {
apiTarget: 'graphql';
data: {
method: string;
url: string;
cookies?: [key: string, value: string][];
data?: string | null | Record<string, any> | [key: string, value: any][];
env?: Record<string, string>;
fragment?: string | null;
headers?: [key: string, value: string][];
inferredContentType?:
| null
| 'application/json'
| 'application/x-www-form-urlencoded'
| 'multipart/form-data';
query?: [key: string, value: string][] | string;
query: string;
variables: Record<string, string | number | null>;
operationName?: string;
};
}

export type EntryRequest = {
data: EntryRequestDataDefault | EntryRequestDataGraphQl;
type: EntryType.REQUEST;
};

Expand Down Expand Up @@ -616,6 +629,11 @@ export interface BrowserContext {
version: string;
}

export interface ResponseContext {
data: unknown;
type: 'response';
}

type EventContexts = {
'Memory Info'?: MemoryInfoContext;
'ThreadPool Info'?: ThreadPoolInfoContext;
Expand All @@ -630,6 +648,7 @@ type EventContexts = {
// once perf issue data shape is more clear
performance_issue?: any;
replay?: ReplayContext;
response?: ResponseContext;
runtime?: RuntimeContext;
threadpool_info?: ThreadPoolInfoContext;
trace?: TraceContextType;
Expand Down
8 changes: 4 additions & 4 deletions static/app/utils/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ const prismLight = {
'--prism-selected': '#E9E0EB',
'--prism-inline-code': '#D25F7C',
'--prism-inline-code-background': '#F8F9FB',
'--prism-highlight-background': '#E8ECF2',
'--prism-highlight-accent': '#C7CBD1',
'--prism-highlight-background': '#5C78A31C',
'--prism-highlight-accent': '#5C78A344',
'--prism-comment': '#72697C',
'--prism-punctuation': '#70697C',
'--prism-property': '#7A6229',
Expand All @@ -155,8 +155,8 @@ const prismDark = {
'--prism-selected': '#865891',
'--prism-inline-code': '#D25F7C',
'--prism-inline-code-background': '#F8F9FB',
'--prism-highlight-background': '#382F5C',
'--prism-highlight-accent': '#D25F7C',
'--prism-highlight-background': '#A8A2C31C',
'--prism-highlight-accent': '#A8A2C344',
'--prism-comment': '#8B7A9E',
'--prism-punctuation': '#B3ACC1',
'--prism-property': '#EAB944',
Expand Down