Skip to content
Draft
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
48 changes: 2 additions & 46 deletions static/app/components/events/interfaces/frame/context.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import {render, screen} from 'sentry-test/reactTestingLibrary';

import {ProjectsStore} from 'sentry/stores/projectsStore';
import type {Frame} from 'sentry/types/event';
import type {LineCoverage} from 'sentry/types/integrations';
import {CodecovStatusCode, Coverage} from 'sentry/types/integrations';

import {Context, getLineCoverage} from './context';
import {Context} from './context';

describe('Frame - Context', () => {
const org = OrganizationFixture();
Expand All @@ -22,50 +20,8 @@ describe('Frame - Context', () => {
ProjectsStore.loadInitialData([project]);
});

const lines: Array<[number, string]> = [
[231, 'this is line 231'],
[232, 'this is line 232'],
[233, 'this is line 233'],
[234, 'this is line 234'],
];

const lineCoverage: LineCoverage[] = [
[230, Coverage.PARTIAL],
[231, Coverage.PARTIAL],
[232, Coverage.COVERED],
[234, Coverage.NOT_COVERED],
];

it("gets coverage data for the frame's lines", () => {
expect(getLineCoverage(lines, lineCoverage)).toEqual([
[Coverage.PARTIAL, Coverage.COVERED, undefined, Coverage.NOT_COVERED],
true,
]);
});

it("doesn't query stacktrace coverage if the flag is off", () => {
const mock = MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/stacktrace-coverage/`,
body: {status: CodecovStatusCode.NO_COVERAGE_DATA},
});
render(<Context frame={frame} event={event} registers={{}} components={[]} />, {
organization: org,
});

expect(mock).not.toHaveBeenCalled();
});

describe('syntax highlighting', () => {
it('renders code correctly when context lines end in newline characters', () => {
const organization = {
...org,
codecovAccess: true,
};
MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/stacktrace-coverage/`,
body: {status: CodecovStatusCode.NO_COVERAGE_DATA},
});

const testFrame: Frame = {
...frame,
lineNo: 2,
Expand All @@ -85,7 +41,7 @@ describe('Frame - Context', () => {
registers={{}}
components={[]}
/>,
{organization}
{organization: org}
);

expect(screen.getAllByTestId('context-line')).toHaveLength(3);
Expand Down
53 changes: 0 additions & 53 deletions static/app/components/events/interfaces/frame/context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {Fragment, useMemo} from 'react';
import styled from '@emotion/styled';
import keyBy from 'lodash/keyBy';

import {ClippedBox} from 'sentry/components/clippedBox';
import {parseAssembly} from 'sentry/components/events/interfaces/utils';
Expand All @@ -9,16 +8,13 @@ import {IconFlag} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {Event, Frame} from 'sentry/types/event';
import type {
LineCoverage,
SentryAppComponent,
SentryAppSchemaStacktraceLink,
} from 'sentry/types/integrations';
import {CodecovStatusCode, Coverage} from 'sentry/types/integrations';
import type {PlatformKey} from 'sentry/types/project';
import type {StacktraceType} from 'sentry/types/stacktrace';
import {defined} from 'sentry/utils';
import {getFileExtension} from 'sentry/utils/fileExtension';
import {useRouteAnalyticsParams} from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useProjects} from 'sentry/utils/useProjects';

Expand All @@ -28,7 +24,6 @@ import {FrameRegisters} from './frameRegisters';
import {FrameVariables} from './frameVariables';
import {usePrismTokensSourceContext} from './usePrismTokensSourceContext';
import {useSourceContext} from './useSourceContext';
import {useStacktraceCoverage} from './useStacktraceCoverage';
import {hasPotentialSourceContext} from './utils';

type Props = {
Expand All @@ -50,21 +45,6 @@ type Props = {
registersMeta?: Record<any, any>;
};

export function getLineCoverage(
lines: Frame['context'],
lineCov: LineCoverage[]
): [Array<Coverage | undefined>, boolean] {
const keyedCoverage = keyBy(lineCov, 0);
const lineCoverage = lines.map<Coverage | undefined>(
([lineNo]) => keyedCoverage[lineNo]?.[1]
);
const hasCoverage = lineCoverage.some(
coverage => coverage !== Coverage.NOT_APPLICABLE && coverage !== undefined
);

return [lineCoverage, hasCoverage];
}

export function Context({
hasContextVars = false,
hasContextSource = false,
Expand Down Expand Up @@ -117,22 +97,6 @@ export function Context({
const effectiveContext = hasContextSource ? frame?.context : scmContext;
const effectiveHasContextSource = hasContextSource || !!scmContext?.length;

const {data: coverage, isPending: isLoadingCoverage} = useStacktraceCoverage(
{
event,
frame,
orgSlug: organization?.slug || '',
projectSlug: project?.slug,
},
{
enabled:
defined(organization) &&
defined(project) &&
!!organization.codecovAccess &&
isExpanded,
}
);

/**
* frame.lineNo is the highlighted frame in the middle of the context
*/
Expand All @@ -141,22 +105,6 @@ export function Context({
? effectiveContext
: effectiveContext?.filter(l => l[0] === activeLineNumber);

const hasCoverageData =
!isLoadingCoverage && coverage?.status === CodecovStatusCode.COVERAGE_EXISTS;

const [lineCoverage = [], hasCoverage] =
hasCoverageData && coverage?.lineCoverage && !!activeLineNumber! && contextLines
? getLineCoverage(contextLines, coverage.lineCoverage)
: [];

useRouteAnalyticsParams(
hasCoverageData
? {
has_line_coverage: hasCoverage,
}
: {}
);

const fileExtension = getFileExtension(frame.filename || frame.absPath || '') ?? '';
const lines = usePrismTokensSourceContext({
contextLines,
Expand Down Expand Up @@ -211,7 +159,6 @@ export function Context({
<ContextLineNumber
lineNumber={contextLine[0]}
isActive={isActive}
coverage={lineCoverage[i]}
/>
<ContextLineCode>
{line.map((token, key) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,15 @@
import styled from '@emotion/styled';
import classNames from 'classnames';

import {Tooltip} from '@sentry/scraps/tooltip';

import {t} from 'sentry/locale';
import {Coverage} from 'sentry/types/integrations';

interface Props {
isActive: boolean;
lineNumber: number;
children?: React.ReactNode;
coverage?: Coverage;
}

const coverageText: Record<Coverage, string | undefined> = {
[Coverage.NOT_COVERED]: t('Line uncovered by tests'),
[Coverage.COVERED]: t('Line covered by tests'),
[Coverage.PARTIAL]: t('Line partially covered by tests'),
[Coverage.NOT_APPLICABLE]: undefined,
};
const coverageClass: Record<Coverage, string | undefined> = {
[Coverage.NOT_COVERED]: 'uncovered',
[Coverage.COVERED]: 'covered',
[Coverage.PARTIAL]: 'partial',
[Coverage.NOT_APPLICABLE]: undefined,
};

export function ContextLineNumber({
lineNumber,
isActive,
coverage = Coverage.NOT_APPLICABLE,
}: Props) {
export function ContextLineNumber({lineNumber, isActive}: Props) {
return (
<Wrapper className={classNames(coverageClass[coverage], isActive ? 'active' : '')}>
<Tooltip skipWrapper title={coverageText[coverage]} delay={200}>
<div className="line-number">{lineNumber}</div>
</Tooltip>
<Wrapper className={isActive ? 'active' : ''}>
<div className="line-number">{lineNumber}</div>
</Wrapper>
);
}
Expand Down Expand Up @@ -65,38 +39,7 @@ const Wrapper = styled('div')`
user-select: none;
}

&.covered .line-number {
background: ${p => p.theme.colors.green100};
border-right: 3px solid ${p => p.theme.tokens.border.success.vibrant};
}

&.uncovered .line-number {
background: ${p => p.theme.colors.red100};
border-right: 3px solid ${p => p.theme.tokens.border.danger.vibrant};
}

&.partial .line-number {
background: ${p => p.theme.colors.yellow100};
border-right: 3px dashed ${p => p.theme.tokens.border.warning.vibrant};
}

&.active {
background: none;
}

&.active.partial .line-number {
mix-blend-mode: screen;
background: ${p => p.theme.colors.yellow200};
}

&.active.covered .line-number {
mix-blend-mode: screen;
background: ${p => p.theme.colors.green200};
}

&.active.uncovered .line-number {
color: ${p => p.theme.colors.white};
mix-blend-mode: screen;
background: ${p => p.theme.colors.red400};
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ import {ReleaseFixture} from 'sentry-fixture/release';
import {RepositoryFixture} from 'sentry-fixture/repository';
import {RepositoryProjectPathConfigFixture} from 'sentry-fixture/repositoryProjectPathConfig';

import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';

import {HookStore} from 'sentry/stores/hookStore';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import type {Frame} from 'sentry/types/event';
import {CodecovStatusCode} from 'sentry/types/integrations';
import * as analytics from 'sentry/utils/analytics';

import {StacktraceLink} from './stacktraceLink';

Expand All @@ -32,8 +30,6 @@ describe('StacktraceLink', () => {
const frame = {filename: '/sentry/app.py', lineNo: 233, inApp: true} as Frame;
const config = RepositoryProjectPathConfigFixture({project, repo, integration});

const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics');

beforeEach(() => {
jest.clearAllMocks();
MockApiClient.clearMockResponses();
Expand Down Expand Up @@ -123,105 +119,6 @@ describe('StacktraceLink', () => {
expect(await screen.findByText('Set up Code Mapping')).toBeInTheDocument();
});

it('renders the codecov link', async () => {
const organization = {
...org,
codecovAccess: true,
features: ['codecov-integration'],
};
MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
body: {
config,
sourceUrl: 'https://github.com/username/path/to/file.py',
integrations: [integration],
},
});
MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/stacktrace-coverage/`,
body: {
status: CodecovStatusCode.COVERAGE_EXISTS,
lineCoverage: [[233, 0]],
coverageUrl: 'https://app.codecov.io/gh/path/to/file.py',
},
});
render(
<StacktraceLink frame={frame} event={event} line="foo()" disableSetup={false} />,
{
organization,
}
);

const link = await screen.findByRole('button', {name: 'Open in Codecov'});
expect(link).toHaveAttribute(
'href',
'https://app.codecov.io/gh/path/to/file.py#L233'
);

await userEvent.click(link);
expect(analyticsSpy).toHaveBeenCalledWith(
'integrations.stacktrace_codecov_link_clicked',
expect.anything()
);
});

it('renders the missing coverage warning', async () => {
const organization = {
...org,
codecovAccess: true,
features: ['codecov-integration'],
};
MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
body: {
config,
sourceUrl: 'https://github.com/username/path/to/file.py',
integrations: [integration],
},
});
MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/stacktrace-coverage/`,
body: {status: CodecovStatusCode.NO_COVERAGE_DATA},
});
render(
<StacktraceLink frame={frame} event={event} line="foo()" disableSetup={false} />,
{
organization,
}
);
expect(await screen.findByText('Code Coverage not found')).toBeInTheDocument();
});

it('skips codecov when the feature is disabled at org level', async () => {
const organization = {
...org,
codecovAccess: false,
features: ['codecov-integration'],
};
MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
body: {
config,
sourceUrl: 'https://github.com/username/path/to/file.py',
integrations: [integration],
},
});
const stacktraceCoverageMock = MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/stacktrace-coverage/`,
body: {status: CodecovStatusCode.NO_COVERAGE_DATA},
});
render(
<StacktraceLink frame={frame} event={event} line="foo()" disableSetup={false} />,
{
organization,
}
);
expect(
await screen.findByRole('button', {name: 'Open this line in GitHub'})
).toBeInTheDocument();
expect(stacktraceCoverageMock).not.toHaveBeenCalled();
});

it('renders the link using a valid sourceLink for a .NET project', async () => {
const dotnetFrame = {
filename: 'path/to/file.py',
Expand Down
Loading
Loading