From 1641fbb19e969adabb321bad650ee1317edd54e3 Mon Sep 17 00:00:00 2001 From: nicholas-codecov Date: Wed, 30 Oct 2024 08:08:01 -0300 Subject: [PATCH 01/10] chore: Remove virtual diff feature flag from commits indirect changes diff (#3448) --- .../CommitFileDiff/CommitFileDiff.jsx | 166 ------------- ...eDiff.test.jsx => CommitFileDiff.test.tsx} | 234 +++++++----------- .../CommitFileDiff/CommitFileDiff.tsx | 185 ++++++++++++++ .../CommitFileDiff/{index.js => index.ts} | 0 4 files changed, 269 insertions(+), 316 deletions(-) delete mode 100644 src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.jsx rename src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/{CommitFileDiff.test.jsx => CommitFileDiff.test.tsx} (62%) create mode 100644 src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.tsx rename src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/{index.js => index.ts} (100%) diff --git a/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.jsx b/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.jsx deleted file mode 100644 index 778d640e80..0000000000 --- a/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.jsx +++ /dev/null @@ -1,166 +0,0 @@ -import without from 'lodash/without' -import PropTypes from 'prop-types' -import { Fragment } from 'react' -import { useParams } from 'react-router-dom' - -import { useIgnoredIds } from 'pages/CommitDetailPage/hooks/useIgnoredIds' -import { useComparisonForCommitAndParent } from 'services/comparison/useComparisonForCommitAndParent' -import { transformImpactedFileToDiff } from 'services/comparison/utils' -import { useNavLinks } from 'services/navigation' -import { useRepoOverview } from 'services/repo' -import { useFlags } from 'shared/featureFlags' -import { - CODE_RENDERER_TYPE, - STICKY_PADDING_SIZES, -} from 'shared/utils/fileviewer' -import A from 'ui/A' -import CodeRenderer from 'ui/CodeRenderer' -import CodeRendererInfoRow from 'ui/CodeRenderer/CodeRendererInfoRow' -import CriticalFileLabel from 'ui/CodeRenderer/CriticalFileLabel' -import DiffLine from 'ui/CodeRenderer/DiffLine' -import { VirtualDiffRenderer } from 'ui/VirtualRenderers' - -function ErrorDisplayMessage() { - return ( -

- There was a problem getting the source code from your provider. Unable to - show line by line coverage. -
- - If you continue to experience this issue, please try{' '} - - logging in - {' '} - again to refresh your credentials. - -

- ) -} - -function CommitFileDiff({ path }) { - const { commitFileDiff } = useNavLinks() - const { owner, repo, provider, commit } = useParams() - const { data: overview } = useRepoOverview({ provider, owner, repo }) - const { virtualDiffRenderer } = useFlags({ virtualDiffRenderer: false }) - const { data: ignoredUploadIds } = useIgnoredIds() - - const { data: comparisonData } = useComparisonForCommitAndParent({ - provider, - owner, - repo, - commitid: commit, - path, - filters: { - hasUnintendedChanges: true, - }, - }) - - if (!comparisonData || !comparisonData?.impactedFile) { - return - } - - const { fileLabel, headName, isCriticalFile, segments } = - transformImpactedFileToDiff(comparisonData?.impactedFile) - - let stickyPadding = undefined - let fullFilePath = commitFileDiff.path({ commit, tree: path }) - if (overview?.coverageEnabled && overview?.bundleAnalysisEnabled) { - stickyPadding = STICKY_PADDING_SIZES.DIFF_LINE_DROPDOWN_PADDING - fullFilePath = `${fullFilePath}?dropdown=coverage` - } - - return ( - <> - {isCriticalFile && } - {segments?.map((segment, segmentIndex) => { - const content = segment.lines.map((line) => line.content).join('\n') - - let newDiffContent = '' - const lineData = [] - if (virtualDiffRenderer) { - segment.lines.forEach((line, lineIndex) => { - newDiffContent += line.content - - if (lineIndex < segment.lines.length - 1) { - newDiffContent += '\n' - } - - lineData.push({ - headNumber: line?.headNumber, - baseNumber: line?.baseNumber, - headCoverage: line?.headCoverage, - baseCoverage: line?.baseCoverage, - hitCount: without( - line?.coverageInfo?.hitUploadIds, - ...ignoredUploadIds - ).length, - }) - }) - } - - return ( - - -
-
- {segment?.header} - {fileLabel && ( - {fileLabel} - )} -
- - View full file - -
-
- {virtualDiffRenderer ? ( - - ) : ( - ( - = segment.lines.length - 3} - path={comparisonData?.hashedPath} - hitCount={ - without( - segment?.lines?.[i]?.coverageInfo?.hitUploadIds, - ...ignoredUploadIds - ).length - } - stickyPadding={stickyPadding} - {...props} - {...segment.lines[i]} - /> - )} - /> - )} -
- ) - })} - - ) -} - -CommitFileDiff.propTypes = { - path: PropTypes.string, -} - -export default CommitFileDiff diff --git a/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.test.jsx b/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.test.tsx similarity index 62% rename from src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.test.jsx rename to src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.test.tsx index bec1d93308..2d208900f4 100644 --- a/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.test.jsx +++ b/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.test.tsx @@ -4,13 +4,15 @@ import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { Suspense } from 'react' import { MemoryRouter, Route } from 'react-router-dom' +import { type MockInstance } from 'vitest' + +import { ImpactedFileType } from 'services/commit' import CommitFileDiff from './CommitFileDiff' const mocks = vi.hoisted(() => ({ - useFlags: vi.fn(), useScrollToLine: vi.fn(), - withProfiler: (component) => component, + withProfiler: (component: any) => component, captureMessage: vi.fn(), })) @@ -22,10 +24,6 @@ vi.mock('ui/CodeRenderer/hooks', () => { } }) -vi.mock('shared/featureFlags', () => ({ - useFlags: mocks.useFlags, -})) - vi.mock('@sentry/react', () => { const originalModule = vi.importActual('@sentry/react') return { @@ -46,9 +44,9 @@ window.scrollTo = scrollToMock window.scrollY = 100 class ResizeObserverMock { - callback = (x) => null + callback = (x: any) => null - constructor(callback) { + constructor(callback: any) { this.callback = callback } @@ -72,7 +70,7 @@ class ResizeObserverMock { } global.window.ResizeObserver = ResizeObserverMock -const baseMock = (impactedFile) => { +const baseMock = (impactedFile: ImpactedFileType | null) => { if (!impactedFile) { return { owner: null } } @@ -179,7 +177,7 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, suspense: true } }, }) -const wrapper = ({ children }) => ( +const wrapper: React.FC = ({ children }) => ( ( ) -beforeAll(() => server.listen()) +beforeAll(() => { + server.listen() +}) + afterEach(() => { queryClient.clear() server.resetHandlers() }) -afterAll(() => server.close()) + +afterAll(() => { + server.close() +}) + +interface SetupArgs { + impactedFile?: ImpactedFileType | null + bundleAnalysisEnabled?: boolean +} describe('CommitFileDiff', () => { function setup( { impactedFile = mockImpactedFile, bundleAnalysisEnabled = false, - featureFlag = false, - } = { + }: SetupArgs = { impactedFile: mockImpactedFile, bundleAnalysisEnabled: false, - featureFlag: false, } ) { mocks.useScrollToLine.mockImplementation(() => ({ @@ -218,10 +225,6 @@ describe('CommitFileDiff', () => { targeted: false, })) - mocks.useFlags.mockImplementation(() => ({ - virtualDiffRenderer: featureFlag, - })) - server.use( graphql.query('ImpactedFileComparedWithParent', (info) => { return HttpResponse.json({ data: baseMock(impactedFile) }) @@ -336,7 +339,7 @@ describe('CommitFileDiff', () => { }) describe('when there is no data', () => { - let consoleSpy + let consoleSpy: MockInstance beforeAll(() => { consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -367,161 +370,92 @@ describe('CommitFileDiff', () => { }) describe('code renderer', () => { - describe('feature flag is true', () => { - it('renders the text area', async () => { - setup({ featureFlag: true }) - render(, { wrapper }) - - const textArea = await screen.findByTestId( - 'virtual-file-renderer-text-area' - ) - expect(textArea).toBeInTheDocument() - - const calculator = await within(textArea).findByText(/Calculator/) - expect(calculator).toBeInTheDocument() - - const value = await within(textArea).findByText(/value/) - expect(value).toBeInTheDocument() - - const calcMode = await within(textArea).findByText(/calcMode/) - expect(calcMode).toBeInTheDocument() - }) - - it('renders the lines of a segment', async () => { - setup({ featureFlag: true }) - render(, { wrapper }) - - const codeDisplayOverlay = await screen.findByTestId( - 'virtual-file-renderer-overlay' - ) + it('renders the text area', async () => { + setup({}) + render(, { wrapper }) - const calculator = - await within(codeDisplayOverlay).findByText(/Calculator/) - expect(calculator).toBeInTheDocument() + const textArea = await screen.findByTestId( + 'virtual-file-renderer-text-area' + ) + expect(textArea).toBeInTheDocument() - const value = await within(codeDisplayOverlay).findByText(/value/) - expect(value).toBeInTheDocument() + const calculator = await within(textArea).findByText(/Calculator/) + expect(calculator).toBeInTheDocument() - const calcMode = await within(codeDisplayOverlay).findByText(/calcMode/) - expect(calcMode).toBeInTheDocument() - }) + const value = await within(textArea).findByText(/value/) + expect(value).toBeInTheDocument() - describe('rendering hit icon', () => { - describe('there are no ignored ids', () => { - it('renders hit count icon', async () => { - setup({ featureFlag: true }) - render(, { wrapper }) + const calcMode = await within(textArea).findByText(/calcMode/) + expect(calcMode).toBeInTheDocument() + }) - const hitCount = await screen.findByText('5') - expect(hitCount).toBeInTheDocument() - }) - }) + it('renders the lines of a segment', async () => { + setup({}) + render(, { wrapper }) - describe('there are ignored ids', () => { - beforeEach(() => { - queryClient.setQueryData(['IgnoredUploadIds'], [0]) - }) + const codeDisplayOverlay = await screen.findByTestId( + 'virtual-file-renderer-overlay' + ) - it('renders hit count icon', async () => { - setup({ featureFlag: true }) - render(, { wrapper }) + const calculator = + await within(codeDisplayOverlay).findByText(/Calculator/) + expect(calculator).toBeInTheDocument() - const hitCount = await screen.findByText('4') - expect(hitCount).toBeInTheDocument() - }) - }) - }) + const value = await within(codeDisplayOverlay).findByText(/value/) + expect(value).toBeInTheDocument() - describe('when segment is an empty array', () => { - const impactedFile = { - ...mockImpactedFile, - isCriticalFile: false, - headName: 'flag1/file.js', - segments: { - results: [], - }, - } + const calcMode = await within(codeDisplayOverlay).findByText(/calcMode/) + expect(calcMode).toBeInTheDocument() + }) - it('does not render information on the code renderer', async () => { - setup({ impactedFile, featureFlag: true }) + describe('rendering hit icon', () => { + describe('there are no ignored ids', () => { + it('renders hit count icon', async () => { + setup({}) render(, { wrapper }) - await waitFor(() => queryClient.isFetching) - await waitFor(() => !queryClient.isFetching) - - const unexpectedChange = screen.queryByText(/Unexpected Changes/i) - expect(unexpectedChange).not.toBeInTheDocument() - - const diffLine = screen.queryByText('fv-diff-line') - expect(diffLine).not.toBeInTheDocument() + const hitCount = await screen.findByText('5') + expect(hitCount).toBeInTheDocument() }) }) - }) - - describe('feature flag is false', () => { - it('renders the lines of a segment', async () => { - setup() - render(, { wrapper }) - const calculator = await screen.findByText(/Calculator/) - expect(calculator).toBeInTheDocument() - - const value = await screen.findByText(/value/) - expect(value).toBeInTheDocument() - - const calcMode = await screen.findByText(/calcMode/) - expect(calcMode).toBeInTheDocument() - }) - - describe('rendering hit icon', () => { - describe('there are no ignored ids', () => { - it('renders hit count icon', async () => { - setup() - render(, { wrapper }) - - const hitCount = await screen.findByText('5') - expect(hitCount).toBeInTheDocument() - }) + describe('there are ignored ids', () => { + beforeEach(() => { + queryClient.setQueryData(['IgnoredUploadIds'], [0]) }) - describe('there are ignored ids', () => { - beforeEach(() => { - queryClient.setQueryData(['IgnoredUploadIds'], [0]) - }) - - it('renders hit count icon', async () => { - setup() - render(, { wrapper }) + it('renders hit count icon', async () => { + setup({}) + render(, { wrapper }) - const hitCount = await screen.findByText('4') - expect(hitCount).toBeInTheDocument() - }) + const hitCount = await screen.findByText('4') + expect(hitCount).toBeInTheDocument() }) }) + }) - describe('when segment is an empty array', () => { - const impactedFile = { - ...mockImpactedFile, - isCriticalFile: false, - headName: 'flag1/file.js', - segments: { - results: [], - }, - } + describe('when segment is an empty array', () => { + const impactedFile = { + ...mockImpactedFile, + isCriticalFile: false, + headName: 'flag1/file.js', + segments: { + results: [], + }, + } - it('does not render information on the code renderer', async () => { - setup({ impactedFile }) - render(, { wrapper }) + it('does not render information on the code renderer', async () => { + setup({ impactedFile }) + render(, { wrapper }) - await waitFor(() => queryClient.isFetching) - await waitFor(() => !queryClient.isFetching) + await waitFor(() => queryClient.isFetching) + await waitFor(() => !queryClient.isFetching) - const unexpectedChange = screen.queryByText(/Unexpected Changes/i) - expect(unexpectedChange).not.toBeInTheDocument() + const unexpectedChange = screen.queryByText(/Unexpected Changes/i) + expect(unexpectedChange).not.toBeInTheDocument() - const diffLine = screen.queryByText('fv-diff-line') - expect(diffLine).not.toBeInTheDocument() - }) + const diffLine = screen.queryByText('fv-diff-line') + expect(diffLine).not.toBeInTheDocument() }) }) }) diff --git a/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.tsx b/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.tsx new file mode 100644 index 0000000000..13a7252740 --- /dev/null +++ b/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/CommitFileDiff.tsx @@ -0,0 +1,185 @@ +import PropTypes from 'prop-types' +import { Fragment, useMemo } from 'react' +import { useParams } from 'react-router-dom' + +import { useIgnoredIds } from 'pages/CommitDetailPage/hooks/useIgnoredIds' +import { + type ImpactedFileType, + useComparisonForCommitAndParent, +} from 'services/comparison/useComparisonForCommitAndParent' +import { transformImpactedFileToDiff } from 'services/comparison/utils' +import { useNavLinks } from 'services/navigation' +import { useRepoOverview } from 'services/repo' +import A from 'ui/A' +import CodeRendererInfoRow from 'ui/CodeRenderer/CodeRendererInfoRow' +import CriticalFileLabel from 'ui/CodeRenderer/CriticalFileLabel' +import { + type CoverageValue, + type LineData, + VirtualDiffRenderer, +} from 'ui/VirtualRenderers/VirtualDiffRenderer' + +function transformSegmentsToLineData( + segments: ImpactedFileType['segments']['results'] | undefined, + ignoredUploadIds: number[] +) { + if (!segments) { + return [] + } + + const ignoredUploadIdsSet = new Set(ignoredUploadIds) + + return segments.map((segment) => { + // we need to create a string of the diff content for the virtual diff renderer text area + let newDiffContent = '' + const lineData: LineData[] = [] + + segment.lines.forEach((line, lineIndex) => { + newDiffContent += line.content + + // only add a newline if it's not the last line + if (lineIndex !== segment.lines.length - 1) { + newDiffContent += '\n' + } + + lineData.push({ + headNumber: line?.headNumber, + baseNumber: line?.baseNumber, + headCoverage: line?.headCoverage as CoverageValue, + baseCoverage: line?.baseCoverage as CoverageValue, + hitCount: line?.coverageInfo?.hitUploadIds?.reduce( + (count, value) => + ignoredUploadIdsSet.has(value) ? count : count + 1, + 0 + ), + }) + }) + + return { ...segment, lineData, newDiffContent } + }) +} + +function DiffRenderer({ + impactedFile, + path, +}: { + impactedFile: ImpactedFileType + path: string +}) { + const { commitFileDiff } = useNavLinks() + const { owner, repo, provider, commit } = useParams() + const { data: overview } = useRepoOverview({ provider, owner, repo }) + const { data: ignoredUploadIds } = useIgnoredIds() + + const fileDiff = useMemo(() => { + const transformedData = transformImpactedFileToDiff(impactedFile) + + const modifiedSegments = transformSegmentsToLineData( + transformedData?.segments, + ignoredUploadIds + ) + + return { ...transformedData, segments: modifiedSegments } + }, [ignoredUploadIds, impactedFile]) + + let fullFilePath = commitFileDiff.path({ commit, tree: path }) + if (overview?.coverageEnabled && overview?.bundleAnalysisEnabled) { + fullFilePath = `${fullFilePath}?dropdown=coverage` + } + + return ( + <> + {fileDiff.isCriticalFile && } + {fileDiff.segments?.map((segment, segmentIndex) => { + return ( + + +
+
+ {segment?.header} + {fileDiff.fileLabel && ( + + {fileDiff.fileLabel} + + )} +
+ {/* @ts-expect-error TODO: Anchor tag */} + + View full file + +
+
+ +
+ ) + })} + + ) +} + +function ErrorDisplayMessage() { + return ( +

+ There was a problem getting the source code from your provider. Unable to + show line by line coverage. +
+ + If you continue to experience this issue, please try{' '} + + logging in + {' '} + again to refresh your credentials. + +

+ ) +} + +interface URLParams { + provider: string + owner: string + repo: string + commit: string +} + +interface CommitFileDiffProps { + path: string | null | undefined +} + +function CommitFileDiff({ path }: CommitFileDiffProps) { + const { owner, repo, provider, commit } = useParams() + + const { data: comparisonData } = useComparisonForCommitAndParent({ + provider, + owner, + repo, + commitid: commit, + path: path ?? '', + filters: { + hasUnintendedChanges: true, + }, + }) + + if (!comparisonData || !comparisonData?.impactedFile || !path) { + return + } + + return +} + +CommitFileDiff.propTypes = { + path: PropTypes.string, +} + +export default CommitFileDiff diff --git a/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/index.js b/src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/index.ts similarity index 100% rename from src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/index.js rename to src/pages/CommitDetailPage/CommitCoverage/routes/IndirectChangesTab/IndirectChangesTable/CommitFileDiff/index.ts From 3303c04bcbbf01265a4a4733f1dc0202c321e036 Mon Sep 17 00:00:00 2001 From: nicholas-codecov Date: Wed, 30 Oct 2024 08:08:09 -0300 Subject: [PATCH 02/10] chore: Remove virtual renderer flag pulls files changed diff (#3442) --- .../FilesChanged/FileDiff/FileDiff.jsx | 137 ---------- .../FilesChanged/FileDiff/index.js | 1 - .../FilesChangedTable.test.tsx | 6 +- .../FilesChangedTable/FilesChangedTable.tsx | 2 +- .../PullFileDiff.test.tsx} | 235 ++++++++++-------- .../PullFileDiff/PullFileDiff.tsx | 163 ++++++++++++ .../FilesChanged/PullFileDiff/index.ts | 1 + .../FilesChanged/TableTeam/TableTeam.test.tsx | 6 +- .../FilesChanged/TableTeam/TableTeam.tsx | 2 +- src/services/pull/fragments.ts | 82 +++--- .../pull/usePrefetchSingleFileComp.tsx | 4 +- ...useSingularImpactedFileComparison.test.tsx | 27 +- .../useSingularImpactedFileComparison.tsx | 45 ++-- src/services/pull/utils/index.ts | 2 +- .../pull/utils/transformImpactedFileToDiff.js | 18 -- .../utils/transformImpactedFileToDiff.test.js | 19 -- .../transformImpactedPullFileToDiff.test.ts | 63 +++++ .../utils/transformImpactedPullFileToDiff.ts | 42 ++++ 18 files changed, 496 insertions(+), 359 deletions(-) delete mode 100644 src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/FileDiff.jsx delete mode 100644 src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/index.js rename src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/{FileDiff/FileDiff.test.jsx => PullFileDiff/PullFileDiff.test.tsx} (56%) create mode 100644 src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/PullFileDiff.tsx create mode 100644 src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/index.ts delete mode 100644 src/services/pull/utils/transformImpactedFileToDiff.js delete mode 100644 src/services/pull/utils/transformImpactedFileToDiff.test.js create mode 100644 src/services/pull/utils/transformImpactedPullFileToDiff.test.ts create mode 100644 src/services/pull/utils/transformImpactedPullFileToDiff.ts diff --git a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/FileDiff.jsx b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/FileDiff.jsx deleted file mode 100644 index 9d53d07308..0000000000 --- a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/FileDiff.jsx +++ /dev/null @@ -1,137 +0,0 @@ -import uniqueId from 'lodash/uniqueId' -import PropTypes from 'prop-types' -import { Fragment } from 'react' -import { useParams } from 'react-router-dom' - -import { useNavLinks } from 'services/navigation' -import { useSingularImpactedFileComparison } from 'services/pull' -import { useRepoOverview } from 'services/repo' -import { useFlags } from 'shared/featureFlags' -import { - CODE_RENDERER_TYPE, - STICKY_PADDING_SIZES, -} from 'shared/utils/fileviewer' -import A from 'ui/A' -import CodeRenderer from 'ui/CodeRenderer' -import CodeRendererInfoRow from 'ui/CodeRenderer/CodeRendererInfoRow' -import CriticalFileLabel from 'ui/CodeRenderer/CriticalFileLabel' -import DiffLine from 'ui/CodeRenderer/DiffLine' -import Spinner from 'ui/Spinner' -import { VirtualDiffRenderer } from 'ui/VirtualRenderers' - -const Loader = () => ( -
- -
-) - -function FileDiff({ path }) { - const { pullFileView } = useNavLinks() - const { provider, owner, repo, pullId } = useParams() - const { data: overview } = useRepoOverview({ provider, owner, repo }) - const { virtualDiffRenderer } = useFlags({ virtualDiffRenderer: false }) - const { data, isLoading } = useSingularImpactedFileComparison({ - provider, - owner, - repo, - pullId, - path, - filters: { hasUnintendedChanges: false }, - }) - - if (isLoading) { - return - } - - const { fileLabel, headName, isCriticalFile, segments } = data - - let stickyPadding = undefined - let fullFilePath = pullFileView.path({ - pullId, - tree: path, - }) - if (overview?.coverageEnabled && overview?.bundleAnalysisEnabled) { - stickyPadding = STICKY_PADDING_SIZES.DIFF_LINE_DROPDOWN_PADDING - fullFilePath = `${fullFilePath}?dropdown=coverage` - } - - return ( - <> - {isCriticalFile && } - {segments?.map((segment, segmentIndex) => { - const content = segment.lines.map((line) => line.content).join('\n') - - let newDiffContent = '' - const lineData = [] - if (virtualDiffRenderer) { - segment.lines.forEach((line, lineIndex) => { - newDiffContent += line.content - - if (lineIndex < segment.lines.length - 1) { - newDiffContent += '\n' - } - - lineData.push({ - headNumber: line?.headNumber, - baseNumber: line?.baseNumber, - headCoverage: line?.headCoverage, - baseCoverage: line?.baseCoverage, - hitCount: undefined, - }) - }) - } - - return ( - - -
-
- {segment?.header} - {fileLabel && ( - {fileLabel} - )} -
- - View full file - -
-
- {virtualDiffRenderer ? ( - - ) : ( - ( - = segment.lines.length - 3} - path={data?.hashedPath} - stickyPadding={stickyPadding} - {...props} - {...segment.lines[i]} - /> - )} - /> - )} -
- ) - })} - - ) -} - -FileDiff.propTypes = { - path: PropTypes.string, -} - -export default FileDiff diff --git a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/index.js b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/index.js deleted file mode 100644 index 39ed304c8d..0000000000 --- a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './FileDiff' diff --git a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FilesChangedTable/FilesChangedTable.test.tsx b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FilesChangedTable/FilesChangedTable.test.tsx index 80485752a6..70039f6340 100644 --- a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FilesChangedTable/FilesChangedTable.test.tsx +++ b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FilesChangedTable/FilesChangedTable.test.tsx @@ -11,7 +11,7 @@ import { UploadTypeEnum } from 'shared/utils/commit' import FilesChangedTable, { getFilter } from './FilesChangedTable' -vi.mock('../FileDiff', () => ({ default: () => 'FileDiff' })) +vi.mock('../PullFileDiff', () => ({ default: () => 'PullFileDiff' })) const mockImpactedFiles = [ { @@ -410,7 +410,7 @@ describe('FilesChangedTable', () => { expect(expander).toBeInTheDocument() await user.click(expander) - const pullFileDiff = await screen.findByText('FileDiff') + const pullFileDiff = await screen.findByText('PullFileDiff') expect(pullFileDiff).toBeInTheDocument() }) @@ -422,7 +422,7 @@ describe('FilesChangedTable', () => { ]), }) - const pullFileDiff = await screen.findByText('FileDiff') + const pullFileDiff = await screen.findByText('PullFileDiff') expect(pullFileDiff).toBeInTheDocument() }) }) diff --git a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FilesChangedTable/FilesChangedTable.tsx b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FilesChangedTable/FilesChangedTable.tsx index 3fd0f79cf0..226243c62d 100644 --- a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FilesChangedTable/FilesChangedTable.tsx +++ b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FilesChangedTable/FilesChangedTable.tsx @@ -27,7 +27,7 @@ import Icon from 'ui/Icon' import Spinner from 'ui/Spinner' import TotalsNumber from 'ui/TotalsNumber' -const PullFileDiff = lazy(() => import('../FileDiff')) +const PullFileDiff = lazy(() => import('../PullFileDiff')) const columnHelper = createColumnHelper() diff --git a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/FileDiff.test.jsx b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/PullFileDiff.test.tsx similarity index 56% rename from src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/FileDiff.test.jsx rename to src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/PullFileDiff.test.tsx index a2b674ba0b..1584820c3f 100644 --- a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/FileDiff/FileDiff.test.jsx +++ b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/PullFileDiff.test.tsx @@ -2,14 +2,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, within } from '@testing-library/react' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' +import { Suspense } from 'react' import { MemoryRouter, Route } from 'react-router-dom' -import FileDiff from './FileDiff' +import FileDiff from './PullFileDiff' const mocks = vi.hoisted(() => ({ - useFlags: vi.fn(), useScrollToLine: vi.fn(), - withProfiler: (component) => component, + withProfiler: (component: any) => component, captureMessage: vi.fn(), })) @@ -21,10 +21,6 @@ vi.mock('ui/CodeRenderer/hooks/useScrollToLine', async () => { } }) -vi.mock('shared/featureFlags', () => ({ - useFlags: mocks.useFlags, -})) - vi.mock('@sentry/react', () => { const originalModule = vi.importActual('@sentry/react') return { @@ -45,9 +41,9 @@ window.scrollTo = scrollToMock window.scrollY = 100 class ResizeObserverMock { - callback = (x) => null + callback = (x: any) => null - constructor(callback) { + constructor(callback: any) { this.callback = callback } @@ -71,7 +67,17 @@ class ResizeObserverMock { } global.window.ResizeObserver = ResizeObserverMock -const baseMock = (impactedFile) => ({ +const baseMock = ({ + isNewFile, + isRenamedFile, + isDeletedFile, + isCriticalFile, +}: { + isNewFile?: boolean + isRenamedFile?: boolean + isDeletedFile?: boolean + isCriticalFile?: boolean +}) => ({ owner: { repository: { __typename: 'Repository', @@ -79,8 +85,34 @@ const baseMock = (impactedFile) => ({ compareWithBase: { __typename: 'Comparison', impactedFile: { - ...mockImpactedFile, - ...impactedFile, + headName: 'flag1/file.js', + hashedPath: 'hashedFilePath', + isRenamedFile, + isDeletedFile, + isCriticalFile, + isNewFile, + baseCoverage: null, + headCoverage: null, + patchCoverage: null, + changeCoverage: null, + segments: { + __typename: 'SegmentComparisons', + results: [ + { + header: '-0,0 +1,45', + hasUnintendedChanges: false, + lines: [ + { + baseNumber: '1', + headNumber: '1', + content: 'const Calculator = ({ value, calcMode }) => {', + baseCoverage: 'M', + headCoverage: 'H', + }, + ], + }, + ], + }, }, }, }, @@ -88,41 +120,6 @@ const baseMock = (impactedFile) => ({ }, }) -const mockImpactedFile = { - headName: 'flag1/file.js', - hashedPath: 'hashedFilePath', - isRenamedFile: false, - isDeletedFile: false, - isCriticalFile: false, - isNewFile: false, - baseCoverage: null, - headCoverage: null, - patchCoverage: null, - changeCoverage: null, - segments: { - __typename: 'SegmentComparisons', - results: [ - { - header: '-0,0 +1,45', - hasUnintendedChanges: false, - lines: [ - { - baseNumber: '1', - headNumber: '1', - content: 'const Calculator = ({ value, calcMode }) => {', - baseCoverage: 'M', - headCoverage: 'H', - coverageInfo: { - hitCount: 18, - hitUploadIds: null, - }, - }, - ], - }, - ], - }, -} - const mockOverview = (bundleAnalysisEnabled = false) => { return { owner: { @@ -143,34 +140,54 @@ const mockOverview = (bundleAnalysisEnabled = false) => { const server = setupServer() const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, + defaultOptions: { queries: { retry: false, suspense: true } }, }) -const wrapper = ({ children }) => ( +const wrapper: React.FC = ({ children }) => ( - {children} + + Loading...}>{children} + ) -beforeAll(() => server.listen()) +beforeAll(() => { + server.listen() +}) + afterEach(() => { queryClient.clear() server.resetHandlers() }) -afterAll(() => server.close()) + +afterAll(() => { + server.close() +}) + +interface SetupArgs { + bundleAnalysisEnabled?: boolean + isNewFile?: boolean + isRenamedFile?: boolean + isDeletedFile?: boolean + isCriticalFile?: boolean +} describe('FileDiff', () => { function setup( { - impactedFile = mockImpactedFile, bundleAnalysisEnabled = false, - featureFlag = false, - } = { - impactedFile: mockImpactedFile, + isNewFile = false, + isRenamedFile = false, + isDeletedFile = false, + isCriticalFile = false, + }: SetupArgs = { + isNewFile: false, + isRenamedFile: false, + isDeletedFile: false, + isCriticalFile: false, bundleAnalysisEnabled: false, - featureFlag: false, } ) { mocks.useScrollToLine.mockImplementation(() => ({ @@ -179,13 +196,16 @@ describe('FileDiff', () => { targeted: false, })) - mocks.useFlags.mockImplementation(() => ({ - virtualDiffRenderer: featureFlag, - })) - server.use( graphql.query('ImpactedFileComparison', (info) => { - return HttpResponse.json({ data: baseMock(impactedFile) }) + return HttpResponse.json({ + data: baseMock({ + isNewFile, + isRenamedFile, + isDeletedFile, + isCriticalFile, + }), + }) }), graphql.query('GetRepoOverview', (info) => { return HttpResponse.json({ data: mockOverview(bundleAnalysisEnabled) }) @@ -233,7 +253,7 @@ describe('FileDiff', () => { describe('a new file', () => { beforeEach(() => { - setup({ impactedFile: { isNewFile: true } }) + setup({ isNewFile: true }) }) it('renders a new file label', async () => { @@ -246,7 +266,7 @@ describe('FileDiff', () => { describe('a renamed file', () => { beforeEach(() => { - setup({ impactedFile: { isRenamedFile: true } }) + setup({ isRenamedFile: true }) }) it('renders a renamed file label', async () => { render(, { wrapper }) @@ -258,7 +278,7 @@ describe('FileDiff', () => { describe('a deleted file', () => { beforeEach(() => { - setup({ impactedFile: { isDeletedFile: true } }) + setup({ isDeletedFile: true }) }) it('renders a deleted file label', async () => { render(, { wrapper }) @@ -270,7 +290,7 @@ describe('FileDiff', () => { describe('a critical file', () => { beforeEach(() => { - setup({ impactedFile: { isCriticalFile: true } }) + setup({ isCriticalFile: true }) }) it('renders a critical file label', async () => { render(, { wrapper }) @@ -281,60 +301,63 @@ describe('FileDiff', () => { }) describe('code renderer', () => { - describe('flag is true', () => { - it('renders the text area', async () => { - setup({ featureFlag: true }) - render(, { wrapper }) + it('renders the text area', async () => { + setup({}) + render(, { wrapper }) - const textArea = await screen.findByTestId( - 'virtual-file-renderer-text-area' - ) - expect(textArea).toBeInTheDocument() + const textArea = await screen.findByTestId( + 'virtual-file-renderer-text-area' + ) + expect(textArea).toBeInTheDocument() - const calculator = await within(textArea).findByText(/Calculator/) - expect(calculator).toBeInTheDocument() + const calculator = await within(textArea).findByText(/Calculator/) + expect(calculator).toBeInTheDocument() - const value = await within(textArea).findByText(/value/) - expect(value).toBeInTheDocument() + const value = await within(textArea).findByText(/value/) + expect(value).toBeInTheDocument() - const calcMode = await within(textArea).findByText(/calcMode/) - expect(calcMode).toBeInTheDocument() - }) + const calcMode = await within(textArea).findByText(/calcMode/) + expect(calcMode).toBeInTheDocument() + }) - it('renders the lines of a segment', async () => { - setup({ featureFlag: true }) - render(, { wrapper }) + it('renders the lines of a segment', async () => { + setup({}) + render(, { wrapper }) - const codeDisplayOverlay = await screen.findByTestId( - 'virtual-file-renderer-overlay' - ) + const codeDisplayOverlay = await screen.findByTestId( + 'virtual-file-renderer-overlay' + ) - const calculator = - await within(codeDisplayOverlay).findByText(/Calculator/) - expect(calculator).toBeInTheDocument() + const calculator = + await within(codeDisplayOverlay).findByText(/Calculator/) + expect(calculator).toBeInTheDocument() - const value = await within(codeDisplayOverlay).findByText(/value/) - expect(value).toBeInTheDocument() + const value = await within(codeDisplayOverlay).findByText(/value/) + expect(value).toBeInTheDocument() - const calcMode = await within(codeDisplayOverlay).findByText(/calcMode/) - expect(calcMode).toBeInTheDocument() - }) + const calcMode = await within(codeDisplayOverlay).findByText(/calcMode/) + expect(calcMode).toBeInTheDocument() }) + }) - describe('flag is false', () => { - it('renders the lines of a segment', async () => { - setup({ featureFlag: false }) - render(, { wrapper }) + describe('when path is undefined', () => { + it('renders an error message', async () => { + setup({}) + render(, { wrapper }) - const calculator = await screen.findByText(/Calculator/) - expect(calculator).toBeInTheDocument() + const errorMessage = await screen.findByText( + /There was a problem getting the source code from your provider./ + ) + expect(errorMessage).toBeInTheDocument() + }) - const value = await screen.findByText(/value/) - expect(value).toBeInTheDocument() + it('renders a login link', async () => { + setup({}) + render(, { wrapper }) - const calcMode = await screen.findByText(/calcMode/) - expect(calcMode).toBeInTheDocument() - }) + const loginLink = await screen.findByRole('link', { name: /logging in/ }) + expect(loginLink).toBeInTheDocument() + expect(loginLink).toHaveAttribute('href', '/login') }) }) }) diff --git a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/PullFileDiff.tsx b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/PullFileDiff.tsx new file mode 100644 index 0000000000..7ab2f712ba --- /dev/null +++ b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/PullFileDiff.tsx @@ -0,0 +1,163 @@ +import { Fragment, useMemo } from 'react' +import { useParams } from 'react-router-dom' + +import { useNavLinks } from 'services/navigation' +import { + type PullImpactedFile, + useSingularImpactedFileComparison, +} from 'services/pull/useSingularImpactedFileComparison' +import { useRepoOverview } from 'services/repo' +import A from 'ui/A' +import CodeRendererInfoRow from 'ui/CodeRenderer/CodeRendererInfoRow' +import CriticalFileLabel from 'ui/CodeRenderer/CriticalFileLabel' +import { VirtualDiffRenderer } from 'ui/VirtualRenderers' +import { CoverageValue } from 'ui/VirtualRenderers/types' +import { LineData } from 'ui/VirtualRenderers/VirtualDiffRenderer' + +function transformSegmentsToLineData( + segments: PullImpactedFile['segments']['results'] | undefined +) { + if (!segments) { + return [] + } + + return segments.map((segment) => { + // we need to create a string of the diff content for the virtual diff renderer text area + let newDiffContent = '' + const lineData: LineData[] = [] + + segment.lines.forEach((line, lineIndex) => { + newDiffContent += line.content + + // only add a newline if it's not the last line + if (lineIndex !== segment.lines.length - 1) { + newDiffContent += '\n' + } + + lineData.push({ + headNumber: line?.headNumber, + baseNumber: line?.baseNumber, + headCoverage: line?.headCoverage as CoverageValue, + baseCoverage: line?.baseCoverage as CoverageValue, + hitCount: undefined, + }) + }) + + return { ...segment, lineData, newDiffContent } + }) +} + +interface URLParams { + provider: string + owner: string + repo: string + pullId: string +} + +function DiffRenderer({ + impactedFile, + path, +}: { + impactedFile: ReturnType['data'] + path: string +}) { + const { pullFileView } = useNavLinks() + const { provider, owner, repo, pullId } = useParams() + const { data: overview } = useRepoOverview({ provider, owner, repo }) + + const fileDiff = useMemo(() => { + return transformSegmentsToLineData(impactedFile?.segments) + }, [impactedFile]) + + let fullFilePath = pullFileView.path({ + pullId, + tree: path, + }) + if (overview?.coverageEnabled && overview?.bundleAnalysisEnabled) { + fullFilePath = `${fullFilePath}?dropdown=coverage` + } + + return ( + <> + {impactedFile?.isCriticalFile && ( + + )} + {fileDiff?.map((segment, segmentIndex) => { + return ( + + +
+
+ {segment?.header} + {impactedFile?.fileLabel && ( + + {impactedFile.fileLabel} + + )} +
+ {/* @ts-expect-error TODO: Anchor tag */} + + View full file + +
+
+ +
+ ) + })} + + ) +} + +function ErrorDisplayMessage() { + return ( +

+ There was a problem getting the source code from your provider. Unable to + show line by line coverage. +
+ + If you continue to experience this issue, please try{' '} + + logging in + {' '} + again to refresh your credentials. + +

+ ) +} + +interface PullFileDiffProps { + path: string | null | undefined +} + +function PullFileDiff({ path }: PullFileDiffProps) { + const { provider, owner, repo, pullId } = useParams() + const { data } = useSingularImpactedFileComparison({ + provider, + owner, + repo, + pullId, + path: path ?? '', + filters: { hasUnintendedChanges: false }, + }) + + if (!data || typeof path !== 'string') { + return + } + + return +} + +export default PullFileDiff diff --git a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/index.ts b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/index.ts new file mode 100644 index 0000000000..8cb9e31223 --- /dev/null +++ b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/PullFileDiff/index.ts @@ -0,0 +1 @@ +export { default } from './PullFileDiff' diff --git a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/TableTeam/TableTeam.test.tsx b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/TableTeam/TableTeam.test.tsx index 6828295b36..8502d98baa 100644 --- a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/TableTeam/TableTeam.test.tsx +++ b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/TableTeam/TableTeam.test.tsx @@ -9,7 +9,7 @@ import { OrderingDirection, OrderingParameter } from 'services/pull/usePullTeam' import TableTeam, { getFilter } from './TableTeam' -vi.mock('../FileDiff', () => ({ default: () => 'FileDiff' })) +vi.mock('../PullFileDiff', () => ({ default: () => 'PullFileDiff' })) const mockComparisonTeamData = { owner: { @@ -412,7 +412,7 @@ describe('TableTeam', () => { expect(expander).toBeInTheDocument() await user.click(expander) - const pullFileDiff = await screen.findByText('FileDiff') + const pullFileDiff = await screen.findByText('PullFileDiff') expect(pullFileDiff).toBeInTheDocument() }) @@ -424,7 +424,7 @@ describe('TableTeam', () => { ]), }) - const pullFileDiff = await screen.findByText('FileDiff') + const pullFileDiff = await screen.findByText('PullFileDiff') expect(pullFileDiff).toBeInTheDocument() }) }) diff --git a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/TableTeam/TableTeam.tsx b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/TableTeam/TableTeam.tsx index 562e8d1922..89bc9e5287 100644 --- a/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/TableTeam/TableTeam.tsx +++ b/src/pages/PullRequestPage/PullCoverage/routes/FilesChangedTab/FilesChanged/TableTeam/TableTeam.tsx @@ -25,7 +25,7 @@ import A from 'ui/A' import Icon from 'ui/Icon' import Spinner from 'ui/Spinner' -const PullFileDiff = lazy(() => import('../FileDiff')) +const PullFileDiff = lazy(() => import('../PullFileDiff')) const columnHelper = createColumnHelper() diff --git a/src/services/pull/fragments.ts b/src/services/pull/fragments.ts index fd2ad8bbc5..eca87d5e68 100644 --- a/src/services/pull/fragments.ts +++ b/src/services/pull/fragments.ts @@ -256,51 +256,51 @@ fragment FileComparisonWithBase on Pull { const CoverageLineSchema = z.enum(['H', 'M', 'P']) -export const ComparisonSchema = z.object({ - __typename: z.literal('Comparison'), - impactedFile: z +export const ImpactedFileSchema = z.object({ + headName: z.string().nullable(), + hashedPath: z.string(), + isNewFile: z.boolean(), + isRenamedFile: z.boolean(), + isDeletedFile: z.boolean(), + isCriticalFile: z.boolean(), + changeCoverage: z.number().nullable(), + baseCoverage: z + .object({ + percentCovered: z.number().nullable(), + }) + .nullable(), + headCoverage: z + .object({ + percentCovered: z.number().nullable(), + }) + .nullable(), + patchCoverage: z .object({ - headName: z.string().nullable(), - hashedPath: z.string(), - isNewFile: z.boolean(), - isRenamedFile: z.boolean(), - isDeletedFile: z.boolean(), - isCriticalFile: z.boolean(), - changeCoverage: z.number().nullable(), - baseCoverage: z - .object({ - percentCovered: z.number().nullable(), - }) - .nullable(), - headCoverage: z - .object({ - percentCovered: z.number().nullable(), - }) - .nullable(), - patchCoverage: z - .object({ - percentCovered: z.number().nullable(), - }) - .nullable(), - segments: z.object({ - results: z.array( + percentCovered: z.number().nullable(), + }) + .nullable(), + segments: z.object({ + results: z.array( + z.object({ + header: z.string(), + hasUnintendedChanges: z.boolean(), + lines: z.array( z.object({ - header: z.string(), - hasUnintendedChanges: z.boolean(), - lines: z.array( - z.object({ - baseNumber: z.string().nullable(), - headNumber: z.string().nullable(), - baseCoverage: CoverageLineSchema.nullable(), - headCoverage: CoverageLineSchema.nullable(), - content: z.string().nullable(), - }) - ), + baseNumber: z.string().nullable(), + headNumber: z.string().nullable(), + baseCoverage: CoverageLineSchema.nullable(), + headCoverage: CoverageLineSchema.nullable(), + content: z.string().nullable(), }) ), - }), - }) - .nullable(), + }) + ), + }), +}) + +export const ComparisonSchema = z.object({ + __typename: z.literal('Comparison'), + impactedFile: ImpactedFileSchema.nullable(), }) export const PullCompareWithBaseFragment = ` diff --git a/src/services/pull/usePrefetchSingleFileComp.tsx b/src/services/pull/usePrefetchSingleFileComp.tsx index 612e3fab72..de30115a46 100644 --- a/src/services/pull/usePrefetchSingleFileComp.tsx +++ b/src/services/pull/usePrefetchSingleFileComp.tsx @@ -18,7 +18,7 @@ import { NetworkErrorObject } from 'shared/api/helpers' import A from 'ui/A' import { ComparisonSchema, FileComparisonWithBase } from './fragments' -import { transformImpactedFileToDiff } from './utils' +import { transformImpactedPullFileToDiff } from './utils' const FileComparisonWithBaseSchema = z.object({ compareWithBase: z @@ -157,7 +157,7 @@ export function usePrefetchSingleFileComp({ data.owner?.repository.pull?.compareWithBase?.__typename === 'Comparison' ) { - return transformImpactedFileToDiff( + return transformImpactedPullFileToDiff( data?.owner?.repository?.pull?.compareWithBase?.impactedFile ) } diff --git a/src/services/pull/useSingularImpactedFileComparison.test.tsx b/src/services/pull/useSingularImpactedFileComparison.test.tsx index cd651a51ed..e930ff0654 100644 --- a/src/services/pull/useSingularImpactedFileComparison.test.tsx +++ b/src/services/pull/useSingularImpactedFileComparison.test.tsx @@ -4,7 +4,6 @@ import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { useSingularImpactedFileComparison } from './useSingularImpactedFileComparison' -import { transformImpactedFileToDiff } from './utils' console.error = () => {} @@ -164,11 +163,27 @@ describe('useSingularImpactedFileComparison', () => { await waitFor(() => !result.current.isLoading) await waitFor(() => - expect(result.current.data).toEqual( - transformImpactedFileToDiff( - mockResponse.owner.repository.pull.compareWithBase.impactedFile - ) - ) + expect(result.current.data).toEqual({ + fileLabel: null, + hashedPath: 'hashedPath', + headName: 'headName', + isCriticalFile: false, + segments: [ + { + hasUnintendedChanges: false, + header: 'header', + lines: [ + { + baseCoverage: 'M', + baseNumber: '1', + content: 'content', + headCoverage: 'H', + headNumber: '1', + }, + ], + }, + ], + }) ) }) }) diff --git a/src/services/pull/useSingularImpactedFileComparison.tsx b/src/services/pull/useSingularImpactedFileComparison.tsx index 1cdaedecd0..0f3456cc6b 100644 --- a/src/services/pull/useSingularImpactedFileComparison.tsx +++ b/src/services/pull/useSingularImpactedFileComparison.tsx @@ -17,29 +17,35 @@ import Api from 'shared/api' import { NetworkErrorObject } from 'shared/api/helpers' import A from 'ui/A' -import { ComparisonSchema, FileComparisonWithBase } from './fragments' -import { transformImpactedFileToDiff } from './utils' +import { + ComparisonSchema, + FileComparisonWithBase, + ImpactedFileSchema, +} from './fragments' +import { transformImpactedPullFileToDiff } from './utils' + +export type PullImpactedFile = z.infer const query = ` - query ImpactedFileComparison($owner: String!, $repo: String!, $pullId: Int!, $path: String!, $filters: SegmentsFilters) { - owner(username: $owner) { - repository(name: $repo) { - __typename - ... on Repository { - pull(id: $pullId) { - ...FileComparisonWithBase - } - } - ... on NotFoundError { - message - } - ... on OwnerNotActivatedError { - message - } +query ImpactedFileComparison($owner: String!, $repo: String!, $pullId: Int!, $path: String!, $filters: SegmentsFilters) { + owner(username: $owner) { + repository(name: $repo) { + __typename + ... on Repository { + pull(id: $pullId) { + ...FileComparisonWithBase } } + ... on NotFoundError { + message + } + ... on OwnerNotActivatedError { + message + } } - ${FileComparisonWithBase} + } +} +${FileComparisonWithBase} ` const RepositorySchema = z.object({ @@ -157,7 +163,7 @@ export function useSingularImpactedFileComparison({ data?.owner?.repository?.pull?.compareWithBase?.__typename === 'Comparison' ) { - return transformImpactedFileToDiff( + return transformImpactedPullFileToDiff( data?.owner?.repository?.pull?.compareWithBase?.impactedFile ) } @@ -168,6 +174,5 @@ export function useSingularImpactedFileComparison({ dev: 'useSingularImpactedFileComparison - 404 missing data', } satisfies NetworkErrorObject) }), - suspense: false, }) } diff --git a/src/services/pull/utils/index.ts b/src/services/pull/utils/index.ts index ed668719fb..4d656f785d 100644 --- a/src/services/pull/utils/index.ts +++ b/src/services/pull/utils/index.ts @@ -1,2 +1,2 @@ export * from './setFileLabel' -export * from './transformImpactedFileToDiff' +export * from './transformImpactedPullFileToDiff' diff --git a/src/services/pull/utils/transformImpactedFileToDiff.js b/src/services/pull/utils/transformImpactedFileToDiff.js deleted file mode 100644 index 4a164e75d4..0000000000 --- a/src/services/pull/utils/transformImpactedFileToDiff.js +++ /dev/null @@ -1,18 +0,0 @@ -import { setFileLabel } from './setFileLabel' - -export function transformImpactedFileToDiff(impactedFile) { - const fileLabel = setFileLabel({ - isNewFile: impactedFile?.isNewFile, - isRenamedFile: impactedFile?.isRenamedFile, - isDeletedFile: impactedFile?.isDeletedFile, - }) - const hashedPath = impactedFile?.hashedPath - - return { - fileLabel, - headName: impactedFile?.headName, - isCriticalFile: impactedFile?.isCriticalFile, - segments: impactedFile?.segments?.results, - ...(hashedPath && { hashedPath }), - } -} diff --git a/src/services/pull/utils/transformImpactedFileToDiff.test.js b/src/services/pull/utils/transformImpactedFileToDiff.test.js deleted file mode 100644 index bf4675d853..0000000000 --- a/src/services/pull/utils/transformImpactedFileToDiff.test.js +++ /dev/null @@ -1,19 +0,0 @@ -import { transformImpactedFileToDiff } from './transformImpactedFileToDiff' - -describe('transformImpactedFileToDiff', () => { - it('returns file information', () => { - const data = transformImpactedFileToDiff({ - isNewFile: true, - headName: 'name', - isCriticalFile: false, - segments: { results: [{ segment: true }] }, - }) - - expect(data).toStrictEqual({ - fileLabel: 'New', - headName: 'name', - isCriticalFile: false, - segments: [{ segment: true }], - }) - }) -}) diff --git a/src/services/pull/utils/transformImpactedPullFileToDiff.test.ts b/src/services/pull/utils/transformImpactedPullFileToDiff.test.ts new file mode 100644 index 0000000000..b590d11eab --- /dev/null +++ b/src/services/pull/utils/transformImpactedPullFileToDiff.test.ts @@ -0,0 +1,63 @@ +import { transformImpactedPullFileToDiff } from './transformImpactedPullFileToDiff' + +describe('transformImpactedPullFileToDiff', () => { + it('returns file information', () => { + const data = transformImpactedPullFileToDiff({ + hashedPath: 'hashedPath', + headName: 'headName', + isRenamedFile: false, + isNewFile: false, + isDeletedFile: false, + isCriticalFile: false, + baseCoverage: { + percentCovered: 23, + }, + headCoverage: { + percentCovered: 24, + }, + patchCoverage: { + percentCovered: 25, + }, + changeCoverage: 0, + segments: { + results: [ + { + header: 'header', + hasUnintendedChanges: false, + lines: [ + { + baseNumber: '1', + headNumber: '1', + baseCoverage: 'M', + headCoverage: 'H', + content: 'content', + }, + ], + }, + ], + }, + }) + + expect(data).toStrictEqual({ + fileLabel: null, + hashedPath: 'hashedPath', + headName: 'headName', + isCriticalFile: false, + segments: [ + { + hasUnintendedChanges: false, + header: 'header', + lines: [ + { + baseCoverage: 'M', + baseNumber: '1', + content: 'content', + headCoverage: 'H', + headNumber: '1', + }, + ], + }, + ], + }) + }) +}) diff --git a/src/services/pull/utils/transformImpactedPullFileToDiff.ts b/src/services/pull/utils/transformImpactedPullFileToDiff.ts new file mode 100644 index 0000000000..91636d23de --- /dev/null +++ b/src/services/pull/utils/transformImpactedPullFileToDiff.ts @@ -0,0 +1,42 @@ +import isEmpty from 'lodash/isEmpty' +import { type z } from 'zod' + +import { ComparisonSchema } from '../fragments' + +function _setFileLabel({ + isNewFile, + isRenamedFile, + isDeletedFile, +}: { + isNewFile: boolean + isRenamedFile: boolean + isDeletedFile: boolean +}): string | null { + if (isNewFile) return 'New' + if (isRenamedFile) return 'Renamed' + if (isDeletedFile) return 'Deleted' + return null +} + +export function transformImpactedPullFileToDiff( + impactedFile: z.infer['impactedFile'] +) { + if (isEmpty(impactedFile)) { + return null + } + + const fileLabel = _setFileLabel({ + isNewFile: impactedFile?.isNewFile, + isRenamedFile: impactedFile?.isRenamedFile, + isDeletedFile: impactedFile?.isDeletedFile, + }) + const hashedPath = impactedFile?.hashedPath + + return { + fileLabel, + headName: impactedFile?.headName, + isCriticalFile: impactedFile?.isCriticalFile, + segments: impactedFile?.segments?.results, + ...(!!hashedPath && { hashedPath }), + } +} From 60b7286a99e8d77f824311bacad34092763b3c98 Mon Sep 17 00:00:00 2001 From: nicholas-codecov Date: Wed, 30 Oct 2024 14:42:16 -0300 Subject: [PATCH 03/10] chore: Remove virtual diff feature flag from pulls indirect tab file diff (#3454) --- .../PullCoverage/PullCoverage.test.jsx | 3 + .../IndirectChangesTab/FileDiff/FileDiff.jsx | 137 ---------- .../IndirectChangesTab/FileDiff/index.js | 1 - .../IndirectChangedFiles.test.tsx | 11 +- .../IndirectChangedFiles.tsx | 4 +- .../PullFileDiff.test.tsx} | 252 +++++++++--------- .../PullFileDiff/PullFileDiff.tsx | 157 +++++++++++ .../IndirectChangesTab/PullFileDiff/index.ts | 1 + 8 files changed, 305 insertions(+), 261 deletions(-) delete mode 100644 src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/FileDiff.jsx delete mode 100644 src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/index.js rename src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/{FileDiff/FileDiff.test.jsx => PullFileDiff/PullFileDiff.test.tsx} (52%) create mode 100644 src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/PullFileDiff.tsx create mode 100644 src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/index.ts diff --git a/src/pages/PullRequestPage/PullCoverage/PullCoverage.test.jsx b/src/pages/PullRequestPage/PullCoverage/PullCoverage.test.jsx index 0cffee492d..c88be88850 100644 --- a/src/pages/PullRequestPage/PullCoverage/PullCoverage.test.jsx +++ b/src/pages/PullRequestPage/PullCoverage/PullCoverage.test.jsx @@ -83,6 +83,9 @@ const mockPullData = (resultType) => { bundleAnalysisEnabled: true, pull: { pullId: 1, + commits: { + totalCount: 11, + }, head: { commitid: '123', bundleAnalysis: { diff --git a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/FileDiff.jsx b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/FileDiff.jsx deleted file mode 100644 index 41993e2c31..0000000000 --- a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/FileDiff.jsx +++ /dev/null @@ -1,137 +0,0 @@ -import uniqueId from 'lodash/uniqueId' -import PropTypes from 'prop-types' -import { Fragment } from 'react' -import { useParams } from 'react-router-dom' - -import { useNavLinks } from 'services/navigation' -import { useSingularImpactedFileComparison } from 'services/pull' -import { useRepoOverview } from 'services/repo' -import { useFlags } from 'shared/featureFlags' -import { - CODE_RENDERER_TYPE, - STICKY_PADDING_SIZES, -} from 'shared/utils/fileviewer' -import A from 'ui/A' -import CodeRenderer from 'ui/CodeRenderer' -import CodeRendererInfoRow from 'ui/CodeRenderer/CodeRendererInfoRow' -import CriticalFileLabel from 'ui/CodeRenderer/CriticalFileLabel' -import DiffLine from 'ui/CodeRenderer/DiffLine' -import Spinner from 'ui/Spinner' -import { VirtualDiffRenderer } from 'ui/VirtualRenderers' - -const Loader = () => ( -
- -
-) - -function FileDiff({ path }) { - const { pullFileView } = useNavLinks() - const { provider, owner, repo, pullId } = useParams() - const { data: overview } = useRepoOverview({ provider, owner, repo }) - const { virtualDiffRenderer } = useFlags({ virtualDiffRenderer: false }) - const { data, isLoading } = useSingularImpactedFileComparison({ - provider, - owner, - repo, - pullId, - path, - filters: { hasUnintendedChanges: true }, - }) - - if (isLoading) { - return - } - - const { fileLabel, headName, isCriticalFile, segments } = data - - let stickyPadding = undefined - let fullFilePath = pullFileView.path({ - pullId, - tree: path, - }) - if (overview?.coverageEnabled && overview?.bundleAnalysisEnabled) { - stickyPadding = STICKY_PADDING_SIZES.DIFF_LINE_DROPDOWN_PADDING - fullFilePath = `${fullFilePath}?dropdown=coverage` - } - - return ( - <> - {isCriticalFile && } - {segments?.map((segment, segmentIndex) => { - const content = segment.lines.map((line) => line.content).join('\n') - - let newDiffContent = '' - const lineData = [] - if (virtualDiffRenderer) { - segment.lines.forEach((line, lineIndex) => { - newDiffContent += line.content - - if (lineIndex < segment.lines.length - 1) { - newDiffContent += '\n' - } - - lineData.push({ - headNumber: line?.headNumber, - baseNumber: line?.baseNumber, - headCoverage: line?.headCoverage, - baseCoverage: line?.baseCoverage, - hitCount: undefined, - }) - }) - } - - return ( - - -
-
- {segment?.header} - {fileLabel && ( - {fileLabel} - )} -
- - View full file - -
-
- {virtualDiffRenderer ? ( - - ) : ( - ( - = segment.lines.length - 3} - path={data?.hashedPath} - stickyPadding={stickyPadding} - {...props} - {...segment.lines[i]} - /> - )} - /> - )} -
- ) - })} - - ) -} - -FileDiff.propTypes = { - path: PropTypes.string, -} - -export default FileDiff diff --git a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/index.js b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/index.js deleted file mode 100644 index 39ed304c8d..0000000000 --- a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './FileDiff' diff --git a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/IndirectChangedFiles/IndirectChangedFiles.test.tsx b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/IndirectChangedFiles/IndirectChangedFiles.test.tsx index d3a01e9e7e..f61a13d8ee 100644 --- a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/IndirectChangedFiles/IndirectChangedFiles.test.tsx +++ b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/IndirectChangedFiles/IndirectChangedFiles.test.tsx @@ -10,7 +10,7 @@ import { UploadTypeEnum } from 'shared/utils/commit' import IndirectChangedFiles from './IndirectChangedFiles' -vi.mock('../FileDiff', () => ({ default: () => 'FileDiff Component' })) +vi.mock('../PullFileDiff', () => ({ default: () => 'FileDiff Component' })) const mockImpactedFiles = [ { @@ -222,6 +222,15 @@ describe('IndirectChangedFiles', () => { ), graphql.query('GetRepoOverview', (info) => { return HttpResponse.json({ data: mockOverview }) + }), + graphql.query('PullComponentsSelector', (info) => { + return HttpResponse.json({ data: { owner: null } }) + }), + graphql.query('BackfillFlagMemberships', (info) => { + return HttpResponse.json({ data: { owner: null } }) + }), + graphql.query('OwnerTier', (info) => { + return HttpResponse.json({ data: { owner: null } }) }) ) diff --git a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/IndirectChangedFiles/IndirectChangedFiles.tsx b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/IndirectChangedFiles/IndirectChangedFiles.tsx index 3756f902a1..6327456adc 100644 --- a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/IndirectChangedFiles/IndirectChangedFiles.tsx +++ b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/IndirectChangedFiles/IndirectChangedFiles.tsx @@ -20,7 +20,7 @@ import TotalsNumber from 'ui/TotalsNumber' import { useIndirectChangedFilesTable } from './hooks' import NameColumn from './NameColumn/NameColumn' -import FileDiff from '../FileDiff' +import PullFileDiff from '../PullFileDiff' interface ImpactedFile { missesCount: number | undefined @@ -117,7 +117,7 @@ function RenderSubComponent({ row }: { row: Row }) { const nameColumn = row.original?.headName return ( }> - + ) } diff --git a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/FileDiff.test.jsx b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/PullFileDiff.test.tsx similarity index 52% rename from src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/FileDiff.test.jsx rename to src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/PullFileDiff.test.tsx index e5be3875ad..559ac955d0 100644 --- a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/FileDiff/FileDiff.test.jsx +++ b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/PullFileDiff.test.tsx @@ -4,12 +4,11 @@ import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { MemoryRouter, Route } from 'react-router-dom' -import FileDiff from './FileDiff' +import PullFileDiff from './PullFileDiff' const mocks = vi.hoisted(() => ({ - useFlags: vi.fn(), useScrollToLine: vi.fn(), - withProfiler: (component) => component, + withProfiler: (component: any) => component, captureMessage: vi.fn(), })) @@ -21,10 +20,6 @@ vi.mock('ui/CodeRenderer/hooks/useScrollToLine', async () => { } }) -vi.mock('shared/featureFlags', () => ({ - useFlags: mocks.useFlags, -})) - vi.mock('@sentry/react', () => { const originalModule = vi.importActual('@sentry/react') return { @@ -45,9 +40,9 @@ window.scrollTo = scrollToMock window.scrollY = 100 class ResizeObserverMock { - callback = (x) => null + callback = (x: any) => null - constructor(callback) { + constructor(callback: any) { this.callback = callback } @@ -71,7 +66,17 @@ class ResizeObserverMock { } global.window.ResizeObserver = ResizeObserverMock -const baseMock = (impactedFile) => ({ +const baseMock = ({ + isNewFile, + isRenamedFile, + isDeletedFile, + isCriticalFile, +}: { + isNewFile?: boolean + isRenamedFile?: boolean + isDeletedFile?: boolean + isCriticalFile?: boolean +}) => ({ owner: { repository: { __typename: 'Repository', @@ -79,8 +84,34 @@ const baseMock = (impactedFile) => ({ compareWithBase: { __typename: 'Comparison', impactedFile: { - ...mockImpactedFile, - ...impactedFile, + headName: 'flag1/file.js', + hashedPath: 'hashedFilePath', + isRenamedFile, + isDeletedFile, + isCriticalFile, + isNewFile, + baseCoverage: null, + headCoverage: null, + patchCoverage: null, + changeCoverage: null, + segments: { + __typename: 'SegmentComparisons', + results: [ + { + header: '-0,0 +1,45', + hasUnintendedChanges: false, + lines: [ + { + baseNumber: '1', + headNumber: '1', + content: 'const Calculator = ({ value, calcMode }) => {', + baseCoverage: 'M', + headCoverage: 'H', + }, + ], + }, + ], + }, }, }, }, @@ -88,41 +119,6 @@ const baseMock = (impactedFile) => ({ }, }) -const mockImpactedFile = { - headName: 'flag1/file.js', - hashedPath: 'hashedFilePath', - isRenamedFile: false, - isDeletedFile: false, - isCriticalFile: false, - isNewFile: false, - baseCoverage: null, - headCoverage: null, - patchCoverage: null, - changeCoverage: null, - segments: { - __typename: 'SegmentComparisons', - results: [ - { - header: '-0,0 +1,45', - hasUnintendedChanges: false, - lines: [ - { - baseNumber: '1', - headNumber: '1', - content: 'const Calculator = ({ value, calcMode }) => {', - baseCoverage: 'M', - headCoverage: 'H', - coverageInfo: { - hitCount: 18, - hitUploadIds: null, - }, - }, - ], - }, - ], - }, -} - const mockOverview = (bundleAnalysisEnabled = false) => { return { owner: { @@ -143,10 +139,10 @@ const mockOverview = (bundleAnalysisEnabled = false) => { const server = setupServer() const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, + defaultOptions: { queries: { retry: false, suspense: true } }, }) -const wrapper = ({ children }) => ( +const wrapper: React.FC = ({ children }) => ( {children} @@ -154,38 +150,51 @@ const wrapper = ({ children }) => ( ) -beforeAll(() => server.listen()) +beforeAll(() => { + server.listen() +}) + afterEach(() => { queryClient.clear() server.resetHandlers() }) -afterAll(() => server.close()) + +afterAll(() => { + server.close() +}) + +interface SetupArgs { + bundleAnalysisEnabled?: boolean + isNewFile?: boolean + isRenamedFile?: boolean + isDeletedFile?: boolean + isCriticalFile?: boolean +} describe('FileDiff', () => { - function setup( - { - impactedFile = mockImpactedFile, - bundleAnalysisEnabled = false, - featureFlag = false, - } = { - impactedFile: mockImpactedFile, - bundleAnalysisEnabled: false, - featureFlag: false, - } - ) { + function setup({ + bundleAnalysisEnabled = false, + isNewFile = false, + isRenamedFile = false, + isDeletedFile = false, + isCriticalFile = false, + }: SetupArgs) { mocks.useScrollToLine.mockImplementation(() => ({ lineRef: () => {}, handleClick: vi.fn(), targeted: false, })) - mocks.useFlags.mockImplementation(() => ({ - virtualDiffRenderer: featureFlag, - })) - server.use( graphql.query('ImpactedFileComparison', (info) => { - return HttpResponse.json({ data: baseMock(impactedFile) }) + return HttpResponse.json({ + data: baseMock({ + isNewFile, + isRenamedFile, + isDeletedFile, + isCriticalFile, + }), + }) }), graphql.query('GetRepoOverview', (info) => { return HttpResponse.json({ data: mockOverview(bundleAnalysisEnabled) }) @@ -195,8 +204,8 @@ describe('FileDiff', () => { describe('when rendered', () => { it('renders the line changes header', async () => { - setup() - render(, { wrapper }) + setup({}) + render(, { wrapper }) const changeHeader = await screen.findByText('-0,0 +1,45') expect(changeHeader).toBeInTheDocument() @@ -204,8 +213,8 @@ describe('FileDiff', () => { describe('when only coverage is enabled', () => { it('renders the commit redirect url', async () => { - setup() - render(, { wrapper }) + setup({}) + render(, { wrapper }) const viewFullFileText = await screen.findByText(/View full file/) expect(viewFullFileText).toBeInTheDocument() @@ -219,7 +228,7 @@ describe('FileDiff', () => { describe('when both coverage and bundle products are enabled', () => { it('renders the commit redirect url', async () => { setup({ bundleAnalysisEnabled: true }) - render(, { wrapper }) + render(, { wrapper }) const viewFullFileText = await screen.findByText(/View full file/) expect(viewFullFileText).toBeInTheDocument() @@ -233,11 +242,11 @@ describe('FileDiff', () => { describe('a new file', () => { beforeEach(() => { - setup({ impactedFile: { isNewFile: true } }) + setup({ isNewFile: true }) }) it('renders a new file label', async () => { - render(, { wrapper }) + render(, { wrapper }) const newText = await screen.findByText(/New/i) expect(newText).toBeInTheDocument() @@ -246,10 +255,10 @@ describe('FileDiff', () => { describe('a renamed file', () => { beforeEach(() => { - setup({ impactedFile: { isRenamedFile: true } }) + setup({ isRenamedFile: true }) }) it('renders a renamed file label', async () => { - render(, { wrapper }) + render(, { wrapper }) const renamed = await screen.findByText(/Renamed/i) expect(renamed).toBeInTheDocument() @@ -258,10 +267,10 @@ describe('FileDiff', () => { describe('a deleted file', () => { beforeEach(() => { - setup({ impactedFile: { isDeletedFile: true } }) + setup({ isDeletedFile: true }) }) it('renders a deleted file label', async () => { - render(, { wrapper }) + render(, { wrapper }) const deleted = await screen.findByText(/Deleted/i) expect(deleted).toBeInTheDocument() @@ -270,10 +279,10 @@ describe('FileDiff', () => { describe('a critical file', () => { beforeEach(() => { - setup({ impactedFile: { isCriticalFile: true } }) + setup({ isCriticalFile: true }) }) it('renders a critical file label', async () => { - render(, { wrapper }) + render(, { wrapper }) const criticalFile = await screen.findByText(/Critical File/i) expect(criticalFile).toBeInTheDocument() @@ -281,60 +290,63 @@ describe('FileDiff', () => { }) describe('code renderer', () => { - describe('feature flag is true', () => { - it('renders the textarea', async () => { - setup({ featureFlag: true }) - render(, { wrapper }) + it('renders the textarea', async () => { + setup({}) + render(, { wrapper }) - const textArea = await screen.findByTestId( - 'virtual-file-renderer-text-area' - ) - expect(textArea).toBeInTheDocument() + const textArea = await screen.findByTestId( + 'virtual-file-renderer-text-area' + ) + expect(textArea).toBeInTheDocument() - const calculator = await within(textArea).findByText(/Calculator/) - expect(calculator).toBeInTheDocument() + const calculator = await within(textArea).findByText(/Calculator/) + expect(calculator).toBeInTheDocument() - const value = await within(textArea).findByText(/value/) - expect(value).toBeInTheDocument() + const value = await within(textArea).findByText(/value/) + expect(value).toBeInTheDocument() - const calcMode = await within(textArea).findByText(/calcMode/) - expect(calcMode).toBeInTheDocument() - }) + const calcMode = await within(textArea).findByText(/calcMode/) + expect(calcMode).toBeInTheDocument() + }) - it('renders the lines of a segment', async () => { - setup({ featureFlag: true }) - render(, { wrapper }) + it('renders the lines of a segment', async () => { + setup({}) + render(, { wrapper }) - const codeDisplayOverlay = await screen.findByTestId( - 'virtual-file-renderer-overlay' - ) + const codeDisplayOverlay = await screen.findByTestId( + 'virtual-file-renderer-overlay' + ) - const calculator = - await within(codeDisplayOverlay).findByText(/Calculator/) - expect(calculator).toBeInTheDocument() + const calculator = + await within(codeDisplayOverlay).findByText(/Calculator/) + expect(calculator).toBeInTheDocument() - const value = await within(codeDisplayOverlay).findByText(/value/) - expect(value).toBeInTheDocument() + const value = await within(codeDisplayOverlay).findByText(/value/) + expect(value).toBeInTheDocument() - const calcMode = await within(codeDisplayOverlay).findByText(/calcMode/) - expect(calcMode).toBeInTheDocument() - }) + const calcMode = await within(codeDisplayOverlay).findByText(/calcMode/) + expect(calcMode).toBeInTheDocument() }) + }) - describe('feature flag is false', () => { - it('renders the lines of a segment', async () => { - setup({ featureFlag: false }) - render(, { wrapper }) + describe('when path is undefined', () => { + it('renders an error message', async () => { + setup({}) + render(, { wrapper }) - const calculator = await screen.findByText(/Calculator/) - expect(calculator).toBeInTheDocument() + const errorMessage = await screen.findByText( + /There was a problem getting the source code from your provider./ + ) + expect(errorMessage).toBeInTheDocument() + }) - const value = await screen.findByText(/value/) - expect(value).toBeInTheDocument() + it('renders a login link', async () => { + setup({}) + render(, { wrapper }) - const calcMode = await screen.findByText(/calcMode/) - expect(calcMode).toBeInTheDocument() - }) + const loginLink = await screen.findByRole('link', { name: /logging in/ }) + expect(loginLink).toBeInTheDocument() + expect(loginLink).toHaveAttribute('href', '/login') }) }) }) diff --git a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/PullFileDiff.tsx b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/PullFileDiff.tsx new file mode 100644 index 0000000000..9b222fba2f --- /dev/null +++ b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/PullFileDiff.tsx @@ -0,0 +1,157 @@ +import { Fragment, useMemo } from 'react' +import { useParams } from 'react-router-dom' + +import { useNavLinks } from 'services/navigation' +import { + PullImpactedFile, + useSingularImpactedFileComparison, +} from 'services/pull' +import { useRepoOverview } from 'services/repo' +import A from 'ui/A' +import CodeRendererInfoRow from 'ui/CodeRenderer/CodeRendererInfoRow' +import CriticalFileLabel from 'ui/CodeRenderer/CriticalFileLabel' +import { + type CoverageValue, + type LineData, + VirtualDiffRenderer, +} from 'ui/VirtualRenderers/VirtualDiffRenderer' + +function transformSegmentsToLineData( + segments: PullImpactedFile['segments']['results'] | undefined +) { + if (!segments) { + return [] + } + return segments.map((segment) => { + // we need to create a string of the diff content for the virtual diff renderer text area + let newDiffContent = '' + const lineData: LineData[] = [] + segment.lines.forEach((line, lineIndex) => { + newDiffContent += line.content + // only add a newline if it's not the last line + if (lineIndex !== segment.lines.length - 1) { + newDiffContent += '\n' + } + lineData.push({ + headNumber: line?.headNumber, + baseNumber: line?.baseNumber, + headCoverage: line?.headCoverage as CoverageValue, + baseCoverage: line?.baseCoverage as CoverageValue, + hitCount: undefined, + }) + }) + return { ...segment, lineData, newDiffContent } + }) +} + +interface URLParams { + provider: string + owner: string + repo: string + pullId: string +} + +function DiffRenderer({ + impactedFile, + path, +}: { + impactedFile: ReturnType['data'] + path: string +}) { + const { pullFileView } = useNavLinks() + const { provider, owner, repo, pullId } = useParams() + const { data: overview } = useRepoOverview({ provider, owner, repo }) + const fileDiff = useMemo(() => { + return transformSegmentsToLineData(impactedFile?.segments) + }, [impactedFile]) + let fullFilePath = pullFileView.path({ + pullId, + tree: path, + }) + if (overview?.coverageEnabled && overview?.bundleAnalysisEnabled) { + fullFilePath = `${fullFilePath}?dropdown=coverage` + } + return ( + <> + {impactedFile?.isCriticalFile && ( + + )} + {fileDiff?.map((segment, segmentIndex) => { + return ( + + +
+
+ {segment?.header} + {impactedFile?.fileLabel && ( + + {impactedFile.fileLabel} + + )} +
+ {/* @ts-expect-error TODO: Anchor tag */} + + View full file + +
+
+ +
+ ) + })} + + ) +} + +function ErrorDisplayMessage() { + return ( +

+ There was a problem getting the source code from your provider. Unable to + show line by line coverage. +
+ + If you continue to experience this issue, please try{' '} + + logging in + {' '} + again to refresh your credentials. + +

+ ) +} + +interface PullFileDiffProps { + path: string | null | undefined +} + +function PullFileDiff({ path }: PullFileDiffProps) { + const { provider, owner, repo, pullId } = useParams() + const { data } = useSingularImpactedFileComparison({ + provider, + owner, + repo, + pullId, + path: path ?? '', + filters: { hasUnintendedChanges: true }, + }) + + if (!data || typeof path !== 'string') { + return + } + + return +} + +export default PullFileDiff diff --git a/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/index.ts b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/index.ts new file mode 100644 index 0000000000..8cb9e31223 --- /dev/null +++ b/src/pages/PullRequestPage/PullCoverage/routes/IndirectChangesTab/PullFileDiff/index.ts @@ -0,0 +1 @@ +export { default } from './PullFileDiff' From 85e5c2764c0ca72297fc120c9aa2d03db390d56d Mon Sep 17 00:00:00 2001 From: calvin-codecov Date: Wed, 30 Oct 2024 11:30:00 -0700 Subject: [PATCH 04/10] chore: upgrade sentry/react version (#3450) --- package.json | 2 +- yarn.lock | 130 +++++++++++++++++++++++++-------------------------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/package.json b/package.json index 0fafe66d90..cada444064 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.2", - "@sentry/react": "^8.32.0", + "@sentry/react": "^8.35.0", "@stripe/react-stripe-js": "^2.7.1", "@stripe/stripe-js": "^3.4.0", "@tanstack/react-query": "^4.29.5", diff --git a/yarn.lock b/yarn.lock index 73c6919fad..96a574226e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5577,49 +5577,49 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/browser-utils@npm:8.32.0": - version: 8.32.0 - resolution: "@sentry-internal/browser-utils@npm:8.32.0" +"@sentry-internal/browser-utils@npm:8.35.0": + version: 8.35.0 + resolution: "@sentry-internal/browser-utils@npm:8.35.0" dependencies: - "@sentry/core": "npm:8.32.0" - "@sentry/types": "npm:8.32.0" - "@sentry/utils": "npm:8.32.0" - checksum: 10c0/accaf5af1c44761e1bcceedd4b91c1707fcc082081378c987a2b375407d175c9542d255bb29613d87bc3b288be2d967ff2d690193f610bec79bbcc708b51b911 + "@sentry/core": "npm:8.35.0" + "@sentry/types": "npm:8.35.0" + "@sentry/utils": "npm:8.35.0" + checksum: 10c0/949873577e3e1654935a5dd9e259f3f6e79dff253806b68811634129815a489318f1051cb7d82db4000e3f5bff03c776456cb5fcaeca1beec5d45916d11c4037 languageName: node linkType: hard -"@sentry-internal/feedback@npm:8.32.0": - version: 8.32.0 - resolution: "@sentry-internal/feedback@npm:8.32.0" +"@sentry-internal/feedback@npm:8.35.0": + version: 8.35.0 + resolution: "@sentry-internal/feedback@npm:8.35.0" dependencies: - "@sentry/core": "npm:8.32.0" - "@sentry/types": "npm:8.32.0" - "@sentry/utils": "npm:8.32.0" - checksum: 10c0/8b2191230d8bb2ff42696ef4e1266e76c70966b0cd74802cc754623c970a5c7b03458fa4ef0a758435236061a61566c4ff1e65a06afdb173887948278eb6f383 + "@sentry/core": "npm:8.35.0" + "@sentry/types": "npm:8.35.0" + "@sentry/utils": "npm:8.35.0" + checksum: 10c0/861ba92c85e19fb3096018928a28f6d8a1deb5f1ae163fe8c881a5db5dd14d44be0461c8d5133905ee0def6d388da7b54f1ea2c3b2a611dab096e47b0ef835ba languageName: node linkType: hard -"@sentry-internal/replay-canvas@npm:8.32.0": - version: 8.32.0 - resolution: "@sentry-internal/replay-canvas@npm:8.32.0" +"@sentry-internal/replay-canvas@npm:8.35.0": + version: 8.35.0 + resolution: "@sentry-internal/replay-canvas@npm:8.35.0" dependencies: - "@sentry-internal/replay": "npm:8.32.0" - "@sentry/core": "npm:8.32.0" - "@sentry/types": "npm:8.32.0" - "@sentry/utils": "npm:8.32.0" - checksum: 10c0/2974fa21fcd8947c7cedb265419273eec9bf31b331c0309c4eccdc43563882b6f098e93481e21908fea5d712138a6115aff93bf900f4a27c3f4b512c880a4964 + "@sentry-internal/replay": "npm:8.35.0" + "@sentry/core": "npm:8.35.0" + "@sentry/types": "npm:8.35.0" + "@sentry/utils": "npm:8.35.0" + checksum: 10c0/e4a4e466cc04294174b9b304c7b43e683904c5ffdb408972d95cca999c32de6134f36f7d438d334cf0e5c4ba2f986c0753693b2a8e5e9ebf81e574a433f2f32e languageName: node linkType: hard -"@sentry-internal/replay@npm:8.32.0": - version: 8.32.0 - resolution: "@sentry-internal/replay@npm:8.32.0" +"@sentry-internal/replay@npm:8.35.0": + version: 8.35.0 + resolution: "@sentry-internal/replay@npm:8.35.0" dependencies: - "@sentry-internal/browser-utils": "npm:8.32.0" - "@sentry/core": "npm:8.32.0" - "@sentry/types": "npm:8.32.0" - "@sentry/utils": "npm:8.32.0" - checksum: 10c0/2278920e42b939588ce2d4b70c1a14f85ab3e03ba3e141acd89060df02c5794c560b132cad0d47f8d1a3ac2e62e6b14a7d7deacc4d5c038ac2851cc7b2346849 + "@sentry-internal/browser-utils": "npm:8.35.0" + "@sentry/core": "npm:8.35.0" + "@sentry/types": "npm:8.35.0" + "@sentry/utils": "npm:8.35.0" + checksum: 10c0/8b3cbfa7c9aa8bedc8c9bf41460aadb2baddacd5ac7d9d6b2b42a833a51f73c6f061352142e67e1b2cfd21c80e6dbe43bea812f66acff44db3a3a8cf31829b25 languageName: node linkType: hard @@ -5630,18 +5630,18 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:8.32.0": - version: 8.32.0 - resolution: "@sentry/browser@npm:8.32.0" +"@sentry/browser@npm:8.35.0": + version: 8.35.0 + resolution: "@sentry/browser@npm:8.35.0" dependencies: - "@sentry-internal/browser-utils": "npm:8.32.0" - "@sentry-internal/feedback": "npm:8.32.0" - "@sentry-internal/replay": "npm:8.32.0" - "@sentry-internal/replay-canvas": "npm:8.32.0" - "@sentry/core": "npm:8.32.0" - "@sentry/types": "npm:8.32.0" - "@sentry/utils": "npm:8.32.0" - checksum: 10c0/21990eb0857dd77ab76ea162dbf532af611ff8b16a6ae7dd9d51ecb31092a0a6d385bcb237d727f5ca7f913a9763f1ca83cbf3b0f224f7e518cb6a053e9a46af + "@sentry-internal/browser-utils": "npm:8.35.0" + "@sentry-internal/feedback": "npm:8.35.0" + "@sentry-internal/replay": "npm:8.35.0" + "@sentry-internal/replay-canvas": "npm:8.35.0" + "@sentry/core": "npm:8.35.0" + "@sentry/types": "npm:8.35.0" + "@sentry/utils": "npm:8.35.0" + checksum: 10c0/91fc8c2f70e09a76cb25d013b47b1d6819b200af421bc9f0d49eae722e7526831d25fac3289bd2fbcface0f45bedb9616769daf28019b57061d300e8fa9ac5d2 languageName: node linkType: hard @@ -5747,44 +5747,44 @@ __metadata: languageName: node linkType: hard -"@sentry/core@npm:8.32.0": - version: 8.32.0 - resolution: "@sentry/core@npm:8.32.0" +"@sentry/core@npm:8.35.0": + version: 8.35.0 + resolution: "@sentry/core@npm:8.35.0" dependencies: - "@sentry/types": "npm:8.32.0" - "@sentry/utils": "npm:8.32.0" - checksum: 10c0/11de3a4810c9f33548577356afda93081daf925663dd01c89778f82f5e74a7c46dc4b84f2983291c91ba8d040b01ad0b881b627ddcff2fc5e5354dfe66ff0cdb + "@sentry/types": "npm:8.35.0" + "@sentry/utils": "npm:8.35.0" + checksum: 10c0/93f2dd5a08484712d895b110f7f4c364e7cf43aef770a05e089584ec4decf95fac5a6fe255714aa216f1f2ac65b38306138307f9a84235a3090659fa53e4c074 languageName: node linkType: hard -"@sentry/react@npm:^8.32.0": - version: 8.32.0 - resolution: "@sentry/react@npm:8.32.0" +"@sentry/react@npm:^8.35.0": + version: 8.35.0 + resolution: "@sentry/react@npm:8.35.0" dependencies: - "@sentry/browser": "npm:8.32.0" - "@sentry/core": "npm:8.32.0" - "@sentry/types": "npm:8.32.0" - "@sentry/utils": "npm:8.32.0" + "@sentry/browser": "npm:8.35.0" + "@sentry/core": "npm:8.35.0" + "@sentry/types": "npm:8.35.0" + "@sentry/utils": "npm:8.35.0" hoist-non-react-statics: "npm:^3.3.2" peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - checksum: 10c0/7e4a1e7693a12bb2b85fce59f5665bd276f15adfe543f144b7f5fc54c73a6046e7069427c3b00530042e9d4f385de9e53524be0b18fa92456a133cd89245de51 + checksum: 10c0/af429303efab9304a0ff740c3e900839e260fd578f0db6e3abfd0e10ce6bcf15ebb185fd7729ac03c4edee57736c1e0dd65e6ffb39f17ad5f8a36e8c6c26bc28 languageName: node linkType: hard -"@sentry/types@npm:8.32.0": - version: 8.32.0 - resolution: "@sentry/types@npm:8.32.0" - checksum: 10c0/386f5c8fa126c5ce8adf7740b1203b9833a78dcc23ebbe43a5e48a76093779596d35c4e58564d32b3a358a81e696f31706c870fa7ce43d3616018bb6b25c872a +"@sentry/types@npm:8.35.0": + version: 8.35.0 + resolution: "@sentry/types@npm:8.35.0" + checksum: 10c0/b28d87ef26d1b889cf7c951a697caf70d9092d84dd1ae777700a0a009da832a8a5c298632dba62fbcb1a3252d011353068c64419e8e952b676f20deca8d03d64 languageName: node linkType: hard -"@sentry/utils@npm:8.32.0": - version: 8.32.0 - resolution: "@sentry/utils@npm:8.32.0" +"@sentry/utils@npm:8.35.0": + version: 8.35.0 + resolution: "@sentry/utils@npm:8.35.0" dependencies: - "@sentry/types": "npm:8.32.0" - checksum: 10c0/cfff34eaa411dd913861d1734f02111aaebbf8d7078a0e1931a3c3ed06f332abbcbe4ec0022505c625ee296b28692f4ae5cee1c66d592ddf87fd798784d5382a + "@sentry/types": "npm:8.35.0" + checksum: 10c0/5c1178f0000165d6436d98902bc4107fcdae9aa84c23458b4c6b1a89b4734185292fb776427d6ec8e174e7e6c1c32b7c72585bb3ddd3ddd893dd8e5884c50d67 languageName: node linkType: hard @@ -12714,7 +12714,7 @@ __metadata: "@radix-ui/react-popover": "npm:^1.0.6" "@radix-ui/react-radio-group": "npm:^1.1.3" "@radix-ui/react-tooltip": "npm:^1.1.2" - "@sentry/react": "npm:^8.32.0" + "@sentry/react": "npm:^8.35.0" "@sentry/vite-plugin": "npm:^2.22.4" "@storybook/addon-a11y": "npm:^8.3.4" "@storybook/addon-actions": "npm:^8.3.4" From 36e556010b8e1319fee74d0206fe1a01b6735eb6 Mon Sep 17 00:00:00 2001 From: nicholas-codecov Date: Thu, 31 Oct 2024 13:53:46 -0300 Subject: [PATCH 05/10] chore: Remove Sentry metrics (#3457) --- .../ThemeToggle/ThemeToggle.test.tsx | 15 -- .../components/ThemeToggle/ThemeToggle.tsx | 7 - .../CommitBundleAnalysis.test.tsx | 31 +-- .../CommitBundleAnalysis.tsx | 13 +- .../CommitCoverage/CommitCoverage.jsx | 11 +- .../CommitCoverage/CommitCoverage.test.jsx | 41 --- src/pages/PlanPage/PlanMetrics/planMetrics.ts | 54 ---- .../UpdateButton/UpdateButton.test.tsx | 164 +----------- .../UpgradeForm/UpdateButton/UpdateButton.tsx | 21 +- .../PullBundleAnalysis.test.tsx | 34 --- .../PullBundleAnalysis/PullBundleAnalysis.tsx | 13 +- .../PullCoverage/PullCoverage.jsx | 18 +- .../PullCoverage/PullCoverage.test.jsx | 45 +--- .../BundleContent/BundleContent.test.tsx | 16 +- .../BundleContent/BundleContent.tsx | 7 +- .../NuxtOnboarding/NuxtOnboarding.test.tsx | 230 +--------------- .../NuxtOnboarding/NuxtOnboarding.tsx | 81 +----- .../RemixOnboarding/RemixOnboarding.test.tsx | 230 +--------------- .../RemixOnboarding/RemixOnboarding.tsx | 76 +----- .../RollupOnboarding.test.tsx | 230 +--------------- .../RollupOnboarding/RollupOnboarding.tsx | 70 +---- .../SolidStartOnboarding.test.tsx | 246 +----------------- .../SolidStartOnboarding.tsx | 56 +--- .../SvelteKitOnboarding.test.tsx | 246 +----------------- .../SvelteKitOnboarding.tsx | 61 +---- .../ViteOnboarding/ViteOnboarding.test.tsx | 230 +--------------- .../ViteOnboarding/ViteOnboarding.tsx | 80 +----- .../WebpackOnboarding.test.tsx | 236 +---------------- .../WebpackOnboarding/WebpackOnboarding.tsx | 71 +---- .../BundleOnboarding/metricHelpers.ts | 60 ----- .../RequestInstallBanner.test.tsx | 29 --- .../RequestInstallBanner.tsx | 2 - .../{index.js => index.ts} | 0 src/shared/utils/metrics.test.ts | 49 ---- src/shared/utils/metrics.ts | 133 ---------- 35 files changed, 65 insertions(+), 2841 deletions(-) delete mode 100644 src/pages/PlanPage/PlanMetrics/planMetrics.ts delete mode 100644 src/pages/RepoPage/BundlesTab/BundleOnboarding/metricHelpers.ts rename src/shared/GlobalTopBanners/RequestInstallBanner/{index.js => index.ts} (100%) delete mode 100644 src/shared/utils/metrics.test.ts delete mode 100644 src/shared/utils/metrics.ts diff --git a/src/layouts/Header/components/ThemeToggle/ThemeToggle.test.tsx b/src/layouts/Header/components/ThemeToggle/ThemeToggle.test.tsx index 51d84b4390..976acd0214 100644 --- a/src/layouts/Header/components/ThemeToggle/ThemeToggle.test.tsx +++ b/src/layouts/Header/components/ThemeToggle/ThemeToggle.test.tsx @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/react' import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -61,12 +60,6 @@ describe('ThemeToggle', () => { expect(sunIcon).toHaveAttribute('data-icon', 'sun') }) - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'button_clicked.theme.dark', - 1, - undefined - ) - // toggle back to light mode await userEvent.click(button) await waitFor(() => { @@ -76,14 +69,6 @@ describe('ThemeToggle', () => { await waitFor(() => { expect(icon).toHaveAttribute('data-icon', 'moon') }) - - await waitFor(() => { - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'button_clicked.theme.light', - 1, - undefined - ) - }) }) it('assumes light mode when there is no theme in local storage', () => { diff --git a/src/layouts/Header/components/ThemeToggle/ThemeToggle.tsx b/src/layouts/Header/components/ThemeToggle/ThemeToggle.tsx index 92308bb638..e2240f7ee0 100644 --- a/src/layouts/Header/components/ThemeToggle/ThemeToggle.tsx +++ b/src/layouts/Header/components/ThemeToggle/ThemeToggle.tsx @@ -1,5 +1,4 @@ import { Theme, useThemeContext } from 'shared/ThemeContext' -import { metrics } from 'shared/utils/metrics' import Icon from 'ui/Icon' const ThemeToggle = () => { @@ -9,12 +8,6 @@ const ThemeToggle = () => { const newTheme = theme === Theme.LIGHT ? Theme.DARK : Theme.LIGHT setTheme(newTheme) - - if (newTheme === Theme.DARK) { - metrics.increment('button_clicked.theme.dark', 1) - } else if (newTheme === Theme.LIGHT) { - metrics.increment('button_clicked.theme.light', 1) - } } return ( diff --git a/src/pages/CommitDetailPage/CommitBundleAnalysis/CommitBundleAnalysis.test.tsx b/src/pages/CommitDetailPage/CommitBundleAnalysis/CommitBundleAnalysis.test.tsx index d1b693e2b6..8b7d54f192 100644 --- a/src/pages/CommitDetailPage/CommitBundleAnalysis/CommitBundleAnalysis.test.tsx +++ b/src/pages/CommitDetailPage/CommitBundleAnalysis/CommitBundleAnalysis.test.tsx @@ -1,6 +1,5 @@ -import * as Sentry from '@sentry/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { Suspense } from 'react' @@ -229,20 +228,6 @@ describe('CommitBundleAnalysis', () => { expect(commitBundleAnalysisTable).toBeInTheDocument() }) - it('sends bundle dropdown metric to sentry', async () => { - setup({ coverageEnabled: true, bundleAnalysisEnabled: true }) - render(, { wrapper }) - - await waitFor(() => expect(Sentry.metrics.increment).toHaveBeenCalled()) - await waitFor(() => - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'commit_detail_page.bundle_dropdown.opened', - 1, - undefined - ) - ) - }) - describe('there is no data', () => { it('renders unknown error message', async () => { setup({ @@ -526,19 +511,5 @@ describe('CommitBundleAnalysis', () => { ) expect(commitBundleAnalysisTable).toBeInTheDocument() }) - - it('sends bundle dropdown metric to sentry', async () => { - setup({ coverageEnabled: false, bundleAnalysisEnabled: true }) - render(, { wrapper }) - - await waitFor(() => expect(Sentry.metrics.increment).toHaveBeenCalled()) - await waitFor(() => - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'commit_detail_page.bundle_page.visited_page', - 1, - undefined - ) - ) - }) }) }) diff --git a/src/pages/CommitDetailPage/CommitBundleAnalysis/CommitBundleAnalysis.tsx b/src/pages/CommitDetailPage/CommitBundleAnalysis/CommitBundleAnalysis.tsx index 511460dd3b..deeb3c89e8 100644 --- a/src/pages/CommitDetailPage/CommitBundleAnalysis/CommitBundleAnalysis.tsx +++ b/src/pages/CommitDetailPage/CommitBundleAnalysis/CommitBundleAnalysis.tsx @@ -1,10 +1,8 @@ -import { lazy, Suspense, useEffect } from 'react' +import { lazy, Suspense } from 'react' import { useParams } from 'react-router-dom' -import { useRepoOverview } from 'services/repo' import ComparisonErrorBanner from 'shared/ComparisonErrorBanner' import { ReportUploadType } from 'shared/utils/comparison' -import { metrics } from 'shared/utils/metrics' import Spinner from 'ui/Spinner' import BundleMessage from './BundleMessage' @@ -65,7 +63,6 @@ const BundleContent: React.FC = ({ bundleCompareType }) => { const CommitBundleAnalysis: React.FC = () => { const { provider, owner, repo, commit: commitSha } = useParams() - const { data: overview } = useRepoOverview({ provider, owner, repo }) const { data: commitPageData } = useCommitPageData({ provider, owner, @@ -73,14 +70,6 @@ const CommitBundleAnalysis: React.FC = () => { commitId: commitSha, }) - useEffect(() => { - if (overview?.bundleAnalysisEnabled && overview?.coverageEnabled) { - metrics.increment('commit_detail_page.bundle_dropdown.opened', 1) - } else if (overview?.bundleAnalysisEnabled) { - metrics.increment('commit_detail_page.bundle_page.visited_page', 1) - } - }, [overview?.bundleAnalysisEnabled, overview?.coverageEnabled]) - const bundleCompareType = commitPageData?.commit?.bundleAnalysis?.bundleAnalysisCompareWithParent ?.__typename diff --git a/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.jsx b/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.jsx index 3f8af7141d..b72484dfeb 100644 --- a/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.jsx +++ b/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.jsx @@ -1,5 +1,5 @@ import isEmpty from 'lodash/isEmpty' -import { lazy, Suspense, useEffect } from 'react' +import { lazy, Suspense } from 'react' import { Redirect, Switch, useParams } from 'react-router-dom' import { SentryRoute } from 'sentry' @@ -13,7 +13,6 @@ import ComparisonErrorBanner from 'shared/ComparisonErrorBanner' import GitHubRateLimitExceededBanner from 'shared/GlobalBanners/GitHubRateLimitExceeded/GitHubRateLimitExceededBanner' import { ReportUploadType } from 'shared/utils/comparison' import { extractUploads } from 'shared/utils/extractUploads' -import { metrics } from 'shared/utils/metrics' import Spinner from 'ui/Spinner' import BotErrorBanner from './BotErrorBanner' @@ -182,14 +181,6 @@ function CommitCoverage() { commitId: commitSha, }) - useEffect(() => { - if (overview.bundleAnalysisEnabled && overview.coverageEnabled) { - metrics.increment('commit_detail_page.coverage_dropdown.opened', 1) - } else if (overview.coverageEnabled) { - metrics.increment('commit_detail_page.coverage_page.visited_page', 1) - } - }, [overview.bundleAnalysisEnabled, overview.coverageEnabled]) - const showCommitSummary = !(overview.private && tierName === TierNames.TEAM) const showFirstPullBanner = commitPageData?.commit?.compareWithParent?.__typename === 'FirstPullRequest' diff --git a/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.test.jsx b/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.test.jsx index dbf42bcec6..2fce9879b6 100644 --- a/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.test.jsx +++ b/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.test.jsx @@ -1,9 +1,7 @@ -import * as Sentry from '@sentry/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, - waitFor, waitForElementToBeRemoved, } from '@testing-library/react' import { graphql, HttpResponse } from 'msw' @@ -710,45 +708,6 @@ describe('CommitCoverage', () => { }) }) - describe('sending metrics', () => { - describe('when only coverage is enabled', () => { - it('sends correct metrics', async () => { - const { queryClient } = setup({ - coverageEnabled: true, - bundleAnalysisEnabled: false, - }) - render(, { wrapper: wrapper({ queryClient }) }) - - await waitFor(() => expect(Sentry.metrics.increment).toHaveBeenCalled()) - await waitFor(() => - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'commit_detail_page.coverage_page.visited_page', - 1, - undefined - ) - ) - }) - }) - - describe('when coverage and bundle analysis are enabled', () => { - it('sends correct metrics', async () => { - const { queryClient } = setup({ - coverageEnabled: true, - bundleAnalysisEnabled: true, - }) - render(, { wrapper: wrapper({ queryClient }) }) - - await waitFor(() => expect(Sentry.metrics.increment).toHaveBeenCalled()) - await waitFor(() => - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'commit_detail_page.coverage_dropdown.opened', - 1, - undefined - ) - ) - }) - }) - }) describe('github rate limit messaging', () => { it('renders banner when github is rate limited', async () => { const { queryClient } = setup({ diff --git a/src/pages/PlanPage/PlanMetrics/planMetrics.ts b/src/pages/PlanPage/PlanMetrics/planMetrics.ts deleted file mode 100644 index 4c1e49c64c..0000000000 --- a/src/pages/PlanPage/PlanMetrics/planMetrics.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { isProPlan, isTeamPlan } from 'shared/utils/billing' -import { metrics } from 'shared/utils/metrics' - -// Updates metrics on the checkout page, which consists of plan + seat selection -export const updateBillingMetrics = ( - isSamePlan: boolean, - seats: number, - currentPlanValue: string, - newPlan: String, - currentPlanQuantity: number -) => { - const seatDelta = seats - currentPlanQuantity - if (isTeamPlan(currentPlanValue) && isProPlan(newPlan)) { - metrics.gauge( - 'billing_change.user.seats_change', - currentPlanQuantity * -1, - { tags: { plan: 'team' } } - ) - - metrics.gauge('billing_change.user.seats_change', seats, { - tags: { plan: 'pro' }, - }) - } - - if (isProPlan(currentPlanValue) && isTeamPlan(newPlan)) { - metrics.gauge( - 'billing_change.user.seats_change', - currentPlanQuantity * -1, - { tags: { plan: 'pro' } } - ) - - metrics.gauge('billing_change.user.seats_change', seats, { - tags: { plan: 'team' }, - }) - } - - if (isSamePlan && isTeamPlan(newPlan)) { - metrics.gauge('billing_change.user.seats_change', seatDelta, { - tags: { plan: 'team' }, - }) - } - - if (isSamePlan && isProPlan(newPlan)) { - metrics.gauge('billing_change.user.seats_change', seatDelta, { - tags: { plan: 'pro' }, - }) - } - - metrics.increment('billing_change.user.checkout_from_page') -} - -export const incrementBillingPageVisitCounter = () => { - metrics.increment('bundles_tab.bundle_details.visited_page') -} diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.test.tsx index df39d03f29..8e332d7df6 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.test.tsx @@ -10,23 +10,6 @@ import { Plans } from 'shared/utils/billing' import UpdateButton from './UpdateButton' -const mocks = vi.hoisted(() => ({ - increment: vi.fn(), - gauge: vi.fn(), -})) - -vi.mock('@sentry/react', async () => { - const originalModule = await vi.importActual('@sentry/react') - return { - ...originalModule, - metrics: { - ...originalModule.metrics!, - increment: mocks.increment, - gauge: mocks.gauge, - }, - } -}) - const server = setupServer() const queryClient = new QueryClient({ defaultOptions: { queries: { suspense: true } }, @@ -45,10 +28,12 @@ const wrapper: React.FC = ({ children }) => ( beforeAll(() => { server.listen() }) + afterEach(() => { queryClient.clear() server.resetHandlers() }) + afterAll(() => { server.close() }) @@ -182,150 +167,5 @@ describe('UpdateButton', () => { expect(button).toBeDisabled() }) }) - - describe('sends metrics to sentry', () => { - it('updates counter on load and checkout', async () => { - const { user } = setup({ planValue: Plans.USERS_TEAMM }) - - const props = { - isValid: true, - newPlan: Plans.USERS_PR_INAPPM, - seats: 4, - } - - render(, { - wrapper, - }) - - const button = await screen.findByText('Update') - expect(button).toBeInTheDocument() - - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.bundle_details.visited_page', - undefined, - undefined - ) - await user.click(button) - expect(mocks.increment).toHaveBeenCalledWith( - 'billing_change.user.checkout_from_page', - undefined, - undefined - ) - }) - - it('updates gauge on team to pro', async () => { - const { user } = setup({ planValue: Plans.USERS_TEAMM }) - - const props = { - isValid: true, - newPlan: Plans.USERS_PR_INAPPM, - seats: 4, - } - - render(, { - wrapper, - }) - - const button = await screen.findByText('Update') - expect(button).toBeInTheDocument() - await user.click(button) - expect(mocks.gauge).toHaveBeenCalledWith( - 'billing_change.user.seats_change', - -3, - { - tags: { plan: 'team' }, - } - ) - expect(mocks.gauge).toHaveBeenCalledWith( - 'billing_change.user.seats_change', - 4, - { - tags: { plan: 'pro' }, - } - ) - }) - - it('updates gauge on pro to team', async () => { - const { user } = setup({ planValue: Plans.USERS_PR_INAPPM }) - - const props = { - isValid: true, - newPlan: Plans.USERS_TEAMM, - seats: 2, - } - - render(, { - wrapper, - }) - - const button = await screen.findByText('Update') - expect(button).toBeInTheDocument() - await user.click(button) - expect(mocks.gauge).toHaveBeenCalledWith( - 'billing_change.user.seats_change', - 2, - { - tags: { plan: 'team' }, - } - ) - expect(mocks.gauge).toHaveBeenCalledWith( - 'billing_change.user.seats_change', - -4, - { - tags: { plan: 'pro' }, - } - ) - }) - - it('updates seat count on a team plan change', async () => { - const { user } = setup({ planValue: Plans.USERS_TEAMM }) - - const props = { - isValid: true, - newPlan: Plans.USERS_TEAMM, - seats: 5, - } - - render(, { - wrapper, - }) - - const button = await screen.findByText('Update') - expect(button).toBeInTheDocument() - await user.click(button) - expect(mocks.gauge).toHaveBeenCalledWith( - 'billing_change.user.seats_change', - 2, - { - tags: { plan: 'team' }, - } - ) - }) - - it('updates seat count on a pro plan change', async () => { - const { user } = setup({ planValue: Plans.USERS_PR_INAPPM }) - - const props = { - isValid: true, - newPlan: Plans.USERS_PR_INAPPM, - seats: 1, - } - - render(, { - wrapper, - }) - - const button = await screen.findByText('Update') - expect(button).toBeInTheDocument() - await user.click(button) - expect(mocks.gauge).toHaveBeenCalledWith( - 'billing_change.user.seats_change', - -3, - { - tags: { plan: 'pro' }, - } - ) - }) - }) }) }) diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.tsx index 487c899dd9..8795c664b4 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.tsx @@ -1,10 +1,6 @@ -import React, { useEffect } from 'react' +import React from 'react' import { useParams } from 'react-router-dom' -import { - incrementBillingPageVisitCounter, - updateBillingMetrics, -} from 'pages/PlanPage/PlanMetrics/planMetrics' import { useAccountDetails } from 'services/account' import { isFreePlan } from 'shared/utils/billing' import Button from 'ui/Button' @@ -32,20 +28,6 @@ const UpdateButton: React.FC = ({ const noChangeInSeats = seats === currentPlanQuantity const disabled = !isValid || (isSamePlan && noChangeInSeats) - useEffect(() => { - incrementBillingPageVisitCounter() - }, []) - - const sendBillingMetricsToSentry = () => { - updateBillingMetrics( - isSamePlan, - seats, - currentPlanValue, - newPlan, - currentPlanQuantity - ) - } - return (
diff --git a/src/pages/PullRequestPage/PullBundleAnalysis/PullBundleAnalysis.test.tsx b/src/pages/PullRequestPage/PullBundleAnalysis/PullBundleAnalysis.test.tsx index 1040276a97..1f9c710e34 100644 --- a/src/pages/PullRequestPage/PullBundleAnalysis/PullBundleAnalysis.test.tsx +++ b/src/pages/PullRequestPage/PullBundleAnalysis/PullBundleAnalysis.test.tsx @@ -1,9 +1,7 @@ -import * as Sentry from '@sentry/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, - waitFor, waitForElementToBeRemoved, } from '@testing-library/react' import { graphql, HttpResponse } from 'msw' @@ -187,19 +185,6 @@ describe('PullBundleAnalysis', () => { const table = await screen.findByText('PullBundleComparisonTable') expect(table).toBeInTheDocument() }) - - it('sends bundle dropdown metrics', async () => { - setup({ coverageEnabled: true, bundleAnalysisEnabled: true }) - render(, { wrapper }) - - await waitFor(() => - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'pull_request_page.bundle_dropdown.opened', - 1, - undefined - ) - ) - }) }) describe('return first pull request typename', () => { @@ -211,9 +196,6 @@ describe('PullBundleAnalysis', () => { }) render(, { wrapper }) - const loader = await screen.findByText('Loading') - await waitForElementToBeRemoved(loader) - const message = screen.queryByText(/Bundle report:/) expect(message).not.toBeInTheDocument() }) @@ -254,9 +236,6 @@ describe('PullBundleAnalysis', () => { }) render(, { wrapper }) - const loader = await screen.findByText('Loading') - await waitForElementToBeRemoved(loader) - const message = screen.queryByText(/Bundle report:/) expect(message).not.toBeInTheDocument() }) @@ -321,19 +300,6 @@ describe('PullBundleAnalysis', () => { const table = await screen.findByText('PullBundleComparisonTable') expect(table).toBeInTheDocument() }) - - it('sends bundle dropdown metrics', async () => { - setup({ coverageEnabled: false, bundleAnalysisEnabled: true }) - render(, { wrapper }) - - await waitFor(() => - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'pull_request_page.bundle_page.visited_page', - 1, - undefined - ) - ) - }) }) describe('return first pull request typename', () => { diff --git a/src/pages/PullRequestPage/PullBundleAnalysis/PullBundleAnalysis.tsx b/src/pages/PullRequestPage/PullBundleAnalysis/PullBundleAnalysis.tsx index 125b445823..923472229d 100644 --- a/src/pages/PullRequestPage/PullBundleAnalysis/PullBundleAnalysis.tsx +++ b/src/pages/PullRequestPage/PullBundleAnalysis/PullBundleAnalysis.tsx @@ -1,10 +1,8 @@ -import { lazy, Suspense, useEffect } from 'react' +import { lazy, Suspense } from 'react' import { useParams } from 'react-router-dom' -import { useRepoOverview } from 'services/repo' import ComparisonErrorBanner from 'shared/ComparisonErrorBanner' import { ReportUploadType } from 'shared/utils/comparison' -import { metrics } from 'shared/utils/metrics' import Spinner from 'ui/Spinner' import BundleMessage from './BundleMessage' @@ -82,7 +80,6 @@ const BundleContent: React.FC = ({ const PullBundleAnalysis: React.FC = () => { const { provider, owner, repo, pullId } = useParams() - const { data: overview } = useRepoOverview({ provider, owner, repo }) // we can set team plan true here because we don't care about the fields it will skip - tho we should really stop doing this and just return null on the API if they're on a team plan so we can save on requests made const { data } = usePullPageData({ @@ -93,14 +90,6 @@ const PullBundleAnalysis: React.FC = () => { isTeamPlan: true, }) - useEffect(() => { - if (overview?.bundleAnalysisEnabled && overview?.coverageEnabled) { - metrics.increment('pull_request_page.bundle_dropdown.opened', 1) - } else if (overview?.bundleAnalysisEnabled) { - metrics.increment('pull_request_page.bundle_page.visited_page', 1) - } - }, [overview?.bundleAnalysisEnabled, overview?.coverageEnabled]) - const bundleCompareType = data?.pull?.bundleAnalysisCompareWithBase?.__typename const headHasBundle = diff --git a/src/pages/PullRequestPage/PullCoverage/PullCoverage.jsx b/src/pages/PullRequestPage/PullCoverage/PullCoverage.jsx index 740f10a7f6..05331d9b22 100644 --- a/src/pages/PullRequestPage/PullCoverage/PullCoverage.jsx +++ b/src/pages/PullRequestPage/PullCoverage/PullCoverage.jsx @@ -1,4 +1,4 @@ -import { lazy, Suspense, useEffect } from 'react' +import { lazy, Suspense } from 'react' import { Redirect, Switch, useParams } from 'react-router-dom' import { SentryRoute } from 'sentry' @@ -9,7 +9,6 @@ import { TierNames, useTier } from 'services/tier' import ComparisonErrorBanner from 'shared/ComparisonErrorBanner' import GitHubRateLimitExceededBanner from 'shared/GlobalBanners/GitHubRateLimitExceeded/GitHubRateLimitExceededBanner' import { ComparisonReturnType, ReportUploadType } from 'shared/utils/comparison' -import { metrics } from 'shared/utils/metrics' import Spinner from 'ui/Spinner' import PullCoverageTabs from './PullCoverageTabs' @@ -39,14 +38,6 @@ function PullCoverageContent() { const { data: overview } = useRepoOverview({ provider, owner, repo }) const { data: tierData } = useTier({ provider, owner }) - useEffect(() => { - if (overview?.bundleAnalysisEnabled && overview?.coverageEnabled) { - metrics.increment('pull_request_page.coverage_dropdown.opened', 1) - } else if (overview?.coverageEnabled) { - metrics.increment('pull_request_page.coverage_page.visited_page', 1) - } - }, [overview?.bundleAnalysisEnabled, overview?.coverageEnabled]) - const isTeamPlan = tierData === TierNames.TEAM && overview?.private const { data } = usePullPageData({ @@ -135,13 +126,6 @@ function PullCoverage() { const { data: overview } = useRepoOverview({ provider, owner, repo }) const { data: tierData } = useTier({ provider, owner }) const { data: rateLimit } = useRepoRateLimitStatus({ provider, owner, repo }) - useEffect(() => { - if (overview?.bundleAnalysisEnabled && overview?.coverageEnabled) { - metrics.increment('pull_request_page.coverage_dropdown.opened', 1) - } else if (overview?.coverageEnabled) { - metrics.increment('pull_request_page.coverage_page.visited_page', 1) - } - }, [overview?.bundleAnalysisEnabled, overview?.coverageEnabled]) const isTeamPlan = tierData === TierNames.TEAM && overview?.private diff --git a/src/pages/PullRequestPage/PullCoverage/PullCoverage.test.jsx b/src/pages/PullRequestPage/PullCoverage/PullCoverage.test.jsx index c88be88850..ada6d7d7af 100644 --- a/src/pages/PullRequestPage/PullCoverage/PullCoverage.test.jsx +++ b/src/pages/PullRequestPage/PullCoverage/PullCoverage.test.jsx @@ -1,6 +1,5 @@ -import * as Sentry from '@sentry/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { MemoryRouter, Route } from 'react-router-dom' @@ -411,48 +410,6 @@ describe('PullRequestPageContent', () => { }) }) - describe('user lands on page', () => { - describe('coverage and bundle analysis is enabled', () => { - it('sends dropdown metric to sentry', async () => { - setup({ - coverageEnabled: true, - bundleAnalysisEnabled: true, - }) - render(, { - wrapper: wrapper(), - }) - - await waitFor(() => - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'pull_request_page.coverage_dropdown.opened', - 1, - undefined - ) - ) - }) - }) - - describe('bundle analysis is disabled', () => { - it('sends coverage page metric to sentry', async () => { - setup({ - coverageEnabled: true, - bundleAnalysisEnabled: false, - }) - render(, { - wrapper: wrapper(), - }) - - await waitFor(() => - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'pull_request_page.coverage_page.visited_page', - 1, - undefined - ) - ) - }) - }) - }) - describe('github rate limit messaging', () => { it('renders banner when github is rate limited', async () => { setup({ diff --git a/src/pages/RepoPage/BundlesTab/BundleContent/BundleContent.test.tsx b/src/pages/RepoPage/BundlesTab/BundleContent/BundleContent.test.tsx index 2b4188bdbb..c465ca3189 100644 --- a/src/pages/RepoPage/BundlesTab/BundleContent/BundleContent.test.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleContent/BundleContent.test.tsx @@ -1,6 +1,5 @@ -import * as Sentry from '@sentry/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { Suspense } from 'react' @@ -323,19 +322,6 @@ describe('BundleContent', () => { ) } - it('sends bundle tab metric to sentry', async () => { - setup({}) - render(, { wrapper: wrapper() }) - - await waitFor(() => - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'bundles_tab.bundle_details.visited_page', - 1, - undefined - ) - ) - }) - describe('rendering select section', () => { it('renders the bundle summary', async () => { setup({}) diff --git a/src/pages/RepoPage/BundlesTab/BundleContent/BundleContent.tsx b/src/pages/RepoPage/BundlesTab/BundleContent/BundleContent.tsx index b3ef3e6989..2fa150ed58 100644 --- a/src/pages/RepoPage/BundlesTab/BundleContent/BundleContent.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleContent/BundleContent.tsx @@ -1,10 +1,9 @@ -import { lazy, Suspense, useEffect } from 'react' +import { lazy, Suspense } from 'react' import { Switch, useParams } from 'react-router-dom' import { SentryRoute } from 'sentry' import { useBranchBundleSummary } from 'services/bundleAnalysis' -import { metrics } from 'shared/utils/metrics' import Spinner from 'ui/Spinner' import { ToggleElement } from 'ui/ToggleElement' @@ -35,10 +34,6 @@ const Loader = () => ( const BundleContent: React.FC = () => { const { provider, owner, repo, branch, bundle } = useParams() - useEffect(() => { - metrics.increment('bundles_tab.bundle_details.visited_page', 1) - }, []) - const { data } = useBranchBundleSummary({ provider, owner, repo, branch }) const bundleType = diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/NuxtOnboarding/NuxtOnboarding.test.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/NuxtOnboarding/NuxtOnboarding.test.tsx index 7c0eeae4bd..8a7d17d0de 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/NuxtOnboarding/NuxtOnboarding.test.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/NuxtOnboarding/NuxtOnboarding.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor, within } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' @@ -8,21 +8,6 @@ import { MemoryRouter, Route } from 'react-router-dom' import NuxtOnboarding from './NuxtOnboarding' -const mocks = vi.hoisted(() => ({ - increment: vi.fn(), -})) - -vi.mock('@sentry/react', async () => { - const originalModule = await vi.importActual('@sentry/react') - - return { - ...originalModule, - metrics: { - increment: mocks.increment, - }, - } -}) - const mockGetRepo = { owner: { isCurrentUserPartOfOrg: true, @@ -104,21 +89,6 @@ describe('NuxtOnboarding', () => { return { user } } - describe('rendering onboarding', () => { - it('sends nuxt onboarding metric', async () => { - setup(null) - render(, { wrapper }) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.visited_page', - 1, - { tags: { bundler: 'nuxt' } } - ) - ) - }) - }) - describe('step 1', () => { it('renders header', async () => { setup(null) @@ -155,28 +125,6 @@ describe('NuxtOnboarding', () => { ) expect(npmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const npmInstall = await screen.findByTestId('nuxt-npm-install') - const npmInstallCopy = await within(npmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(npmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'npm', bundler: 'nuxt' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -189,28 +137,6 @@ describe('NuxtOnboarding', () => { ) expect(yarnInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const yarnInstall = await screen.findByTestId('nuxt-yarn-install') - const yarnInstallCopy = await within(yarnInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(yarnInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'nuxt' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -223,28 +149,6 @@ describe('NuxtOnboarding', () => { ) expect(pnpmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const pnpmInstall = await screen.findByTestId('nuxt-pnpm-install') - const pnpmInstallCopy = await within(pnpmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pnpmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'nuxt' } } - ) - ) - }) - }) }) }) }) @@ -294,28 +198,6 @@ describe('NuxtOnboarding', () => { expect(token).toBeInTheDocument() }) }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const uploadToken = await screen.findByTestId('nuxt-upload-token') - const uploadTokenCopy = await within(uploadToken).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(uploadTokenCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.token', - 1, - { tags: { bundler: 'nuxt' } } - ) - ) - }) - }) }) describe('step 3', () => { @@ -350,28 +232,6 @@ describe('NuxtOnboarding', () => { const pluginText = await screen.findByText(/\/\/ nuxt.config.js/) expect(pluginText).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const pluginConfig = await screen.findByTestId('nuxt-plugin-config') - const pluginConfigCopy = await within(pluginConfig).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pluginConfigCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.config', - 1, - { tags: { bundler: 'nuxt' } } - ) - ) - }) - }) }) describe('step 4', () => { @@ -407,28 +267,6 @@ describe('NuxtOnboarding', () => { ) expect(gitCommit).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const commitCommand = await screen.findByTestId('nuxt-commit-command') - const commitCommandCopy = await within(commitCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(commitCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.commit', - 1, - { tags: { bundler: 'nuxt' } } - ) - ) - }) - }) }) describe('step 5', () => { @@ -462,28 +300,6 @@ describe('NuxtOnboarding', () => { const npmBuild = await screen.findByText('npm run build') expect(npmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('nuxt-npm-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'npm', bundler: 'nuxt' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -494,28 +310,6 @@ describe('NuxtOnboarding', () => { const yarnBuild = await screen.findByText('yarn run build') expect(yarnBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('nuxt-yarn-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'nuxt' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -526,28 +320,6 @@ describe('NuxtOnboarding', () => { const pnpmBuild = await screen.findByText('pnpm run build') expect(pnpmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('nuxt-pnpm-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'nuxt' } } - ) - ) - }) - }) }) }) }) diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/NuxtOnboarding/NuxtOnboarding.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/NuxtOnboarding/NuxtOnboarding.tsx index 324d123bed..b2b80e9c6d 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/NuxtOnboarding/NuxtOnboarding.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/NuxtOnboarding/NuxtOnboarding.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React from 'react' import { useParams } from 'react-router-dom' import { useOrgUploadToken } from 'services/orgUploadToken' @@ -8,14 +8,6 @@ import { Card } from 'ui/Card' import { CodeSnippet } from 'ui/CodeSnippet' import LearnMoreBlurb from '../LearnMoreBlurb' -import { - copiedBuildCommandMetric, - copiedCommitMetric, - copiedConfigMetric, - copiedInstallCommandMetric, - copiedTokenMetric, - visitedOnboardingMetric, -} from '../metricHelpers' const npmInstall = `npm install @codecov/nuxt-plugin --save-dev` const yarnInstall = `yarn add @codecov/nuxt-plugin --dev` @@ -61,31 +53,13 @@ const StepOne: React.FC = () => { {' '} to your project, use one of the following commands.

- { - copiedInstallCommandMetric('npm', 'nuxt') - }} - data-testid="nuxt-npm-install" - > + {npmInstall} - { - copiedInstallCommandMetric('yarn', 'nuxt') - }} - data-testid="nuxt-yarn-install" - > + {yarnInstall} - { - copiedInstallCommandMetric('pnpm', 'nuxt') - }} - data-testid="nuxt-pnpm-install" - > + {pnpmInstall} @@ -111,9 +85,6 @@ const StepTwo: React.FC<{ uploadToken: string }> = ({ uploadToken }) => { { - copiedTokenMetric('nuxt') - }} data-testid="nuxt-upload-token" > {uploadToken} @@ -140,13 +111,7 @@ const StepThree: React.FC = () => { {' '} file, and pass your configuration.

- { - copiedConfigMetric('nuxt') - }} - data-testid="nuxt-plugin-config" - > + {pluginConfig} @@ -167,13 +132,7 @@ const StepFour: React.FC = () => { The plugin requires at least one commit to be made to properly upload bundle analysis information to Codecov.

- { - copiedCommitMetric('nuxt') - }} - data-testid="nuxt-commit-command" - > + {commitString} @@ -192,31 +151,13 @@ const StepFive: React.FC = () => { When building your application the plugin will automatically upload the stats information to Codecov.

- { - copiedBuildCommandMetric('npm', 'nuxt') - }} - data-testid="nuxt-npm-build" - > + {npmBuild} - { - copiedBuildCommandMetric('yarn', 'nuxt') - }} - data-testid="nuxt-yarn-build" - > + {yarnBuild} - { - copiedBuildCommandMetric('pnpm', 'nuxt') - }} - data-testid="nuxt-pnpm-build" - > + {pnpmBuild} @@ -257,10 +198,6 @@ const NuxtOnboarding: React.FC = () => { const uploadToken = orgUploadToken ?? repoData?.repository?.uploadToken ?? '' - useEffect(() => { - visitedOnboardingMetric('nuxt') - }, []) - return (
diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/RemixOnboarding/RemixOnboarding.test.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/RemixOnboarding/RemixOnboarding.test.tsx index ad6ae12fdf..1d6801d231 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/RemixOnboarding/RemixOnboarding.test.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/RemixOnboarding/RemixOnboarding.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor, within } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' @@ -8,21 +8,6 @@ import { MemoryRouter, Route } from 'react-router-dom' import RemixOnboarding from './RemixOnboarding' -const mocks = vi.hoisted(() => ({ - increment: vi.fn(), -})) - -vi.mock('@sentry/react', async () => { - const originalModule = await vi.importActual('@sentry/react') - - return { - ...originalModule, - metrics: { - increment: mocks.increment, - }, - } -}) - const mockGetRepo = { owner: { isCurrentUserPartOfOrg: true, @@ -104,21 +89,6 @@ describe('RemixOnboarding', () => { return { user } } - describe('rendering onboarding', () => { - it('sends remix onboarding metric', async () => { - setup(null) - render(, { wrapper }) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.visited_page', - 1, - { tags: { bundler: 'remix' } } - ) - ) - }) - }) - describe('step 1', () => { it('renders header', async () => { setup(null) @@ -155,28 +125,6 @@ describe('RemixOnboarding', () => { ) expect(npmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const npmInstall = await screen.findByTestId('remix-npm-install') - const npmInstallCopy = await within(npmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(npmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'npm', bundler: 'remix' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -189,28 +137,6 @@ describe('RemixOnboarding', () => { ) expect(yarnInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const yarnInstall = await screen.findByTestId('remix-yarn-install') - const yarnInstallCopy = await within(yarnInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(yarnInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'remix' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -223,28 +149,6 @@ describe('RemixOnboarding', () => { ) expect(pnpmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const pnpmInstall = await screen.findByTestId('remix-pnpm-install') - const pnpmInstallCopy = await within(pnpmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pnpmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'remix' } } - ) - ) - }) - }) }) }) }) @@ -294,28 +198,6 @@ describe('RemixOnboarding', () => { expect(token).toBeInTheDocument() }) }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const uploadToken = await screen.findByTestId('remix-upload-token') - const uploadTokenCopy = await within(uploadToken).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(uploadTokenCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.token', - 1, - { tags: { bundler: 'remix' } } - ) - ) - }) - }) }) describe('step 3', () => { @@ -350,28 +232,6 @@ describe('RemixOnboarding', () => { const pluginText = await screen.findByText(/\/\/ vite.config.ts/) expect(pluginText).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const pluginConfig = await screen.findByTestId('remix-plugin-config') - const pluginConfigCopy = await within(pluginConfig).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pluginConfigCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.config', - 1, - { tags: { bundler: 'remix' } } - ) - ) - }) - }) }) describe('step 4', () => { @@ -407,28 +267,6 @@ describe('RemixOnboarding', () => { ) expect(gitCommit).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const commitCommand = await screen.findByTestId('remix-commit-command') - const commitCommandCopy = await within(commitCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(commitCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.commit', - 1, - { tags: { bundler: 'remix' } } - ) - ) - }) - }) }) describe('step 5', () => { @@ -462,28 +300,6 @@ describe('RemixOnboarding', () => { const npmBuild = await screen.findByText('npm run build') expect(npmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('remix-npm-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'npm', bundler: 'remix' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -494,28 +310,6 @@ describe('RemixOnboarding', () => { const yarnBuild = await screen.findByText('yarn run build') expect(yarnBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('remix-yarn-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'remix' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -526,28 +320,6 @@ describe('RemixOnboarding', () => { const pnpmBuild = await screen.findByText('pnpm run build') expect(pnpmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('remix-pnpm-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'remix' } } - ) - ) - }) - }) }) }) }) diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/RemixOnboarding/RemixOnboarding.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/RemixOnboarding/RemixOnboarding.tsx index 43401d3509..84ec8d96b5 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/RemixOnboarding/RemixOnboarding.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/RemixOnboarding/RemixOnboarding.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React from 'react' import { useParams } from 'react-router-dom' import { useOrgUploadToken } from 'services/orgUploadToken' @@ -8,14 +8,6 @@ import { Card } from 'ui/Card' import { CodeSnippet } from 'ui/CodeSnippet' import LearnMoreBlurb from '../LearnMoreBlurb' -import { - copiedBuildCommandMetric, - copiedCommitMetric, - copiedConfigMetric, - copiedInstallCommandMetric, - copiedTokenMetric, - visitedOnboardingMetric, -} from '../metricHelpers' const npmInstall = `npm install @codecov/remix-vite-plugin --save-dev` const yarnInstall = `yarn add @codecov/remix-vite-plugin --dev` @@ -62,31 +54,13 @@ const StepOne: React.FC = () => { {' '} to your project, use one of the following commands.

- { - copiedInstallCommandMetric('npm', 'remix') - }} - data-testid="remix-npm-install" - > + {npmInstall} - { - copiedInstallCommandMetric('yarn', 'remix') - }} - data-testid="remix-yarn-install" - > + {yarnInstall} - { - copiedInstallCommandMetric('pnpm', 'remix') - }} - data-testid="remix-pnpm-install" - > + {pnpmInstall} @@ -112,9 +86,6 @@ const StepTwo: React.FC<{ uploadToken: string }> = ({ uploadToken }) => { { - copiedTokenMetric('remix') - }} data-testid="remix-upload-token" > {uploadToken} @@ -141,13 +112,7 @@ const StepThree: React.FC = () => { {' '} file, and pass your configuration.

- { - copiedConfigMetric('remix') - }} - data-testid="remix-plugin-config" - > + {pluginConfig} @@ -170,9 +135,6 @@ const StepFour: React.FC = () => {

{ - copiedCommitMetric('remix') - }} data-testid="remix-commit-command" > {commitString} @@ -193,31 +155,13 @@ const StepFive: React.FC = () => { When building your application the plugin will automatically upload the stats information to Codecov.

- { - copiedBuildCommandMetric('npm', 'remix') - }} - data-testid="remix-npm-build" - > + {npmBuild} - { - copiedBuildCommandMetric('yarn', 'remix') - }} - data-testid="remix-yarn-build" - > + {yarnBuild} - { - copiedBuildCommandMetric('pnpm', 'remix') - }} - data-testid="remix-pnpm-build" - > + {pnpmBuild} @@ -258,10 +202,6 @@ const RemixOnboarding: React.FC = () => { const uploadToken = orgUploadToken ?? repoData?.repository?.uploadToken ?? '' - useEffect(() => { - visitedOnboardingMetric('remix') - }, []) - return (
diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/RollupOnboarding/RollupOnboarding.test.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/RollupOnboarding/RollupOnboarding.test.tsx index cb30eaaecd..a1ec0a521e 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/RollupOnboarding/RollupOnboarding.test.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/RollupOnboarding/RollupOnboarding.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor, within } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' @@ -8,21 +8,6 @@ import { MemoryRouter, Route } from 'react-router-dom' import RollupOnboarding from './RollupOnboarding' -const mocks = vi.hoisted(() => ({ - increment: vi.fn(), -})) - -vi.mock('@sentry/react', async () => { - const originalModule = await vi.importActual('@sentry/react') - - return { - ...originalModule, - metrics: { - increment: mocks.increment, - }, - } -}) - const mockGetRepo = { owner: { isCurrentUserPartOfOrg: true, @@ -104,21 +89,6 @@ describe('RollupOnboarding', () => { return { user } } - describe('rendering onboarding', () => { - it('sends rollup onboarding metric', async () => { - setup(null) - render(, { wrapper }) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.visited_page', - 1, - { tags: { bundler: 'rollup' } } - ) - ) - }) - }) - describe('step 1', () => { it('renders header', async () => { setup(null) @@ -155,28 +125,6 @@ describe('RollupOnboarding', () => { ) expect(npmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const npmInstall = await screen.findByTestId('rollup-npm-install') - const npmInstallCopy = await within(npmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(npmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'npm', bundler: 'rollup' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -189,28 +137,6 @@ describe('RollupOnboarding', () => { ) expect(yarnInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const yarnInstall = await screen.findByTestId('rollup-yarn-install') - const yarnInstallCopy = await within(yarnInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(yarnInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'rollup' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -223,28 +149,6 @@ describe('RollupOnboarding', () => { ) expect(pnpmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const pnpmInstall = await screen.findByTestId('rollup-pnpm-install') - const pnpmInstallCopy = await within(pnpmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pnpmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'rollup' } } - ) - ) - }) - }) }) }) }) @@ -294,28 +198,6 @@ describe('RollupOnboarding', () => { expect(token).toBeInTheDocument() }) }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const uploadToken = await screen.findByTestId('rollup-upload-token') - const uploadTokenCopy = await within(uploadToken).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(uploadTokenCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.token', - 1, - { tags: { bundler: 'rollup' } } - ) - ) - }) - }) }) describe('step 3', () => { @@ -350,28 +232,6 @@ describe('RollupOnboarding', () => { const pluginText = await screen.findByText(/\/\/ rollup.config.js/) expect(pluginText).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const pluginConfig = await screen.findByTestId('rollup-plugin-config') - const pluginConfigCopy = await within(pluginConfig).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pluginConfigCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.config', - 1, - { tags: { bundler: 'rollup' } } - ) - ) - }) - }) }) describe('step 4', () => { @@ -407,28 +267,6 @@ describe('RollupOnboarding', () => { ) expect(gitCommit).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const commitCommand = await screen.findByTestId('rollup-commit-command') - const commitCommandCopy = await within(commitCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(commitCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.commit', - 1, - { tags: { bundler: 'rollup' } } - ) - ) - }) - }) }) describe('step 5', () => { @@ -462,28 +300,6 @@ describe('RollupOnboarding', () => { const npmBuild = await screen.findByText('npm run build') expect(npmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('rollup-npm-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'npm', bundler: 'rollup' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -494,28 +310,6 @@ describe('RollupOnboarding', () => { const yarnBuild = await screen.findByText('yarn run build') expect(yarnBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('rollup-yarn-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'rollup' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -526,28 +320,6 @@ describe('RollupOnboarding', () => { const pnpmBuild = await screen.findByText('pnpm run build') expect(pnpmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('rollup-pnpm-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'rollup' } } - ) - ) - }) - }) }) }) }) diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/RollupOnboarding/RollupOnboarding.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/RollupOnboarding/RollupOnboarding.tsx index 1394ff9f66..8cf97ea331 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/RollupOnboarding/RollupOnboarding.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/RollupOnboarding/RollupOnboarding.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react' import { useParams } from 'react-router-dom' import { useOrgUploadToken } from 'services/orgUploadToken' @@ -8,14 +7,6 @@ import { Card } from 'ui/Card' import { CodeSnippet } from 'ui/CodeSnippet' import LearnMoreBlurb from '../LearnMoreBlurb' -import { - copiedBuildCommandMetric, - copiedCommitMetric, - copiedConfigMetric, - copiedInstallCommandMetric, - copiedTokenMetric, - visitedOnboardingMetric, -} from '../metricHelpers' const npmInstall = `npm install @codecov/rollup-plugin --save-dev` const yarnInstall = `yarn add @codecov/rollup-plugin --dev` @@ -58,31 +49,13 @@ const StepOne: React.FC = () => { {' '} to your project, use one of the following commands.

- { - copiedInstallCommandMetric('npm', 'rollup') - }} - data-testid="rollup-npm-install" - > + {npmInstall} - { - copiedInstallCommandMetric('yarn', 'rollup') - }} - data-testid="rollup-yarn-install" - > + {yarnInstall} - { - copiedInstallCommandMetric('pnpm', 'rollup') - }} - data-testid="rollup-pnpm-install" - > + {pnpmInstall} @@ -108,9 +81,6 @@ const StepTwo: React.FC<{ uploadToken: string }> = ({ uploadToken }) => { { - copiedTokenMetric('rollup') - }} data-testid="rollup-upload-token" > {uploadToken} @@ -140,9 +110,6 @@ const StepThree: React.FC = () => {

{ - copiedConfigMetric('rollup') - }} data-testid="rollup-plugin-config" > {pluginConfig} @@ -167,9 +134,6 @@ const StepFour: React.FC = () => {

{ - copiedCommitMetric('rollup') - }} data-testid="rollup-commit-command" > {commitString} @@ -190,31 +154,13 @@ const StepFive: React.FC = () => { When building your application the plugin will automatically upload the stats information to Codecov.

- { - copiedBuildCommandMetric('npm', 'rollup') - }} - data-testid="rollup-npm-build" - > + {npmBuild} - { - copiedBuildCommandMetric('yarn', 'rollup') - }} - data-testid="rollup-yarn-build" - > + {yarnBuild} - { - copiedBuildCommandMetric('pnpm', 'rollup') - }} - data-testid="rollup-pnpm-build" - > + {pnpmBuild} @@ -255,10 +201,6 @@ const RollupOnboarding: React.FC = () => { const uploadToken = orgUploadToken ?? repoData?.repository?.uploadToken ?? '' - useEffect(() => { - visitedOnboardingMetric('rollup') - }, []) - return (
diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/SolidStartOnboarding/SolidStartOnboarding.test.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/SolidStartOnboarding/SolidStartOnboarding.test.tsx index 2dbce2bff2..67facef59c 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/SolidStartOnboarding/SolidStartOnboarding.test.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/SolidStartOnboarding/SolidStartOnboarding.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor, within } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' @@ -8,21 +8,6 @@ import { MemoryRouter, Route } from 'react-router-dom' import SolidStartOnboarding from './SolidStartOnboarding' -const mocks = vi.hoisted(() => ({ - increment: vi.fn(), -})) - -vi.mock('@sentry/react', async () => { - const originalModule = await vi.importActual('@sentry/react') - - return { - ...originalModule, - metrics: { - increment: mocks.increment, - }, - } -}) - const mockGetRepo = { owner: { isCurrentUserPartOfOrg: true, @@ -104,21 +89,6 @@ describe('SolidStartOnboarding', () => { return { user } } - describe('rendering onboarding', () => { - it('sends solidstart onboarding metric', async () => { - setup(null) - render(, { wrapper }) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.visited_page', - 1, - { tags: { bundler: 'solidstart' } } - ) - ) - }) - }) - describe('step 1', () => { it('renders header', async () => { setup(null) @@ -155,30 +125,6 @@ describe('SolidStartOnboarding', () => { ) expect(npmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const npmInstall = await screen.findByTestId( - 'solidstart-npm-install' - ) - const npmInstallCopy = await within(npmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(npmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'npm', bundler: 'solidstart' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -191,30 +137,6 @@ describe('SolidStartOnboarding', () => { ) expect(yarnInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const yarnInstall = await screen.findByTestId( - 'solidstart-yarn-install' - ) - const yarnInstallCopy = await within(yarnInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(yarnInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'solidstart' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -227,30 +149,6 @@ describe('SolidStartOnboarding', () => { ) expect(pnpmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const pnpmInstall = await screen.findByTestId( - 'solidstart-pnpm-install' - ) - const pnpmInstallCopy = await within(pnpmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pnpmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'solidstart' } } - ) - ) - }) - }) }) }) }) @@ -300,28 +198,6 @@ describe('SolidStartOnboarding', () => { expect(token).toBeInTheDocument() }) }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const uploadToken = await screen.findByTestId('solidstart-upload-token') - const uploadTokenCopy = await within(uploadToken).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(uploadTokenCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.token', - 1, - { tags: { bundler: 'solidstart' } } - ) - ) - }) - }) }) describe('step 3', () => { @@ -356,30 +232,6 @@ describe('SolidStartOnboarding', () => { const pluginText = await screen.findByText(/\/\/ app.config.ts/) expect(pluginText).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const pluginConfig = await screen.findByTestId( - 'solidstart-plugin-config' - ) - const pluginConfigCopy = await within(pluginConfig).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pluginConfigCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.config', - 1, - { tags: { bundler: 'solidstart' } } - ) - ) - }) - }) }) describe('step 4', () => { @@ -415,30 +267,6 @@ describe('SolidStartOnboarding', () => { ) expect(gitCommit).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const commitCommand = await screen.findByTestId( - 'solidstart-commit-command' - ) - const commitCommandCopy = await within(commitCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(commitCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.commit', - 1, - { tags: { bundler: 'solidstart' } } - ) - ) - }) - }) }) describe('step 5', () => { @@ -472,30 +300,6 @@ describe('SolidStartOnboarding', () => { const npmBuild = await screen.findByText('npm run build') expect(npmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId( - 'solidstart-npm-build' - ) - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'npm', bundler: 'solidstart' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -506,30 +310,6 @@ describe('SolidStartOnboarding', () => { const yarnBuild = await screen.findByText('yarn run build') expect(yarnBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId( - 'solidstart-yarn-build' - ) - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'solidstart' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -540,30 +320,6 @@ describe('SolidStartOnboarding', () => { const pnpmBuild = await screen.findByText('pnpm run build') expect(pnpmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId( - 'solidstart-pnpm-build' - ) - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'solidstart' } } - ) - ) - }) - }) }) }) }) diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/SolidStartOnboarding/SolidStartOnboarding.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/SolidStartOnboarding/SolidStartOnboarding.tsx index 9a05293bbf..0457d2fb28 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/SolidStartOnboarding/SolidStartOnboarding.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/SolidStartOnboarding/SolidStartOnboarding.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React from 'react' import { useParams } from 'react-router-dom' import { useOrgUploadToken } from 'services/orgUploadToken' @@ -8,14 +8,6 @@ import { Card } from 'ui/Card' import { CodeSnippet } from 'ui/CodeSnippet' import LearnMoreBlurb from '../LearnMoreBlurb' -import { - copiedBuildCommandMetric, - copiedCommitMetric, - copiedConfigMetric, - copiedInstallCommandMetric, - copiedTokenMetric, - visitedOnboardingMetric, -} from '../metricHelpers' const npmInstall = `npm install @codecov/solidstart-plugin --save-dev` const yarnInstall = `yarn add @codecov/solidstart-plugin --dev` @@ -64,27 +56,18 @@ const StepOne: React.FC = () => {

{ - copiedInstallCommandMetric('npm', 'solidstart') - }} data-testid="solidstart-npm-install" > {npmInstall} { - copiedInstallCommandMetric('yarn', 'solidstart') - }} data-testid="solidstart-yarn-install" > {yarnInstall} { - copiedInstallCommandMetric('pnpm', 'solidstart') - }} data-testid="solidstart-pnpm-install" > {pnpmInstall} @@ -112,9 +95,6 @@ const StepTwo: React.FC<{ uploadToken: string }> = ({ uploadToken }) => { { - copiedTokenMetric('solidstart') - }} data-testid="solidstart-upload-token" > {uploadToken} @@ -143,9 +123,6 @@ const StepThree: React.FC = () => {

{ - copiedConfigMetric('solidstart') - }} data-testid="solidstart-plugin-config" > {pluginConfig} @@ -170,9 +147,6 @@ const StepFour: React.FC = () => {

{ - copiedCommitMetric('solidstart') - }} data-testid="solidstart-commit-command" > {commitString} @@ -193,31 +167,13 @@ const StepFive: React.FC = () => { When building your application the plugin will automatically upload the stats information to Codecov.

- { - copiedBuildCommandMetric('npm', 'solidstart') - }} - data-testid="solidstart-npm-build" - > + {npmBuild} - { - copiedBuildCommandMetric('yarn', 'solidstart') - }} - data-testid="solidstart-yarn-build" - > + {yarnBuild} - { - copiedBuildCommandMetric('pnpm', 'solidstart') - }} - data-testid="solidstart-pnpm-build" - > + {pnpmBuild} @@ -258,10 +214,6 @@ const SolidStartOnboarding: React.FC = () => { const uploadToken = orgUploadToken ?? repoData?.repository?.uploadToken ?? '' - useEffect(() => { - visitedOnboardingMetric('solidstart') - }, []) - return (
diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/SvelteKitOnboarding/SvelteKitOnboarding.test.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/SvelteKitOnboarding/SvelteKitOnboarding.test.tsx index 3a4aaa9dd5..9c70d0ed13 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/SvelteKitOnboarding/SvelteKitOnboarding.test.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/SvelteKitOnboarding/SvelteKitOnboarding.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor, within } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' @@ -8,21 +8,6 @@ import { MemoryRouter, Route } from 'react-router-dom' import SvelteKitOnboarding from './SvelteKitOnboarding' -const mocks = vi.hoisted(() => ({ - increment: vi.fn(), -})) - -vi.mock('@sentry/react', async () => { - const originalModule = await vi.importActual('@sentry/react') - - return { - ...originalModule, - metrics: { - increment: mocks.increment, - }, - } -}) - const mockGetRepo = { owner: { isCurrentUserPartOfOrg: true, @@ -104,21 +89,6 @@ describe('SvelteKitOnboarding', () => { return { user } } - describe('rendering onboarding', () => { - it('sends sveltekit onboarding metric', async () => { - setup(null) - render(, { wrapper }) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.visited_page', - 1, - { tags: { bundler: 'sveltekit' } } - ) - ) - }) - }) - describe('step 1', () => { it('renders header', async () => { setup(null) @@ -155,30 +125,6 @@ describe('SvelteKitOnboarding', () => { ) expect(npmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const npmInstall = await screen.findByTestId( - 'sveltekit-npm-install' - ) - const npmInstallCopy = await within(npmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(npmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'npm', bundler: 'sveltekit' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -191,30 +137,6 @@ describe('SvelteKitOnboarding', () => { ) expect(yarnInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const yarnInstall = await screen.findByTestId( - 'sveltekit-yarn-install' - ) - const yarnInstallCopy = await within(yarnInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(yarnInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'sveltekit' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -227,30 +149,6 @@ describe('SvelteKitOnboarding', () => { ) expect(pnpmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const pnpmInstall = await screen.findByTestId( - 'sveltekit-pnpm-install' - ) - const pnpmInstallCopy = await within(pnpmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pnpmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'sveltekit' } } - ) - ) - }) - }) }) }) }) @@ -300,28 +198,6 @@ describe('SvelteKitOnboarding', () => { expect(token).toBeInTheDocument() }) }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const uploadToken = await screen.findByTestId('sveltekit-upload-token') - const uploadTokenCopy = await within(uploadToken).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(uploadTokenCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.token', - 1, - { tags: { bundler: 'sveltekit' } } - ) - ) - }) - }) }) describe('step 3', () => { @@ -356,30 +232,6 @@ describe('SvelteKitOnboarding', () => { const pluginText = await screen.findByText(/\/\/ vite.config.ts/) expect(pluginText).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const pluginConfig = await screen.findByTestId( - 'sveltekit-plugin-config' - ) - const pluginConfigCopy = await within(pluginConfig).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pluginConfigCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.config', - 1, - { tags: { bundler: 'sveltekit' } } - ) - ) - }) - }) }) describe('step 4', () => { @@ -415,30 +267,6 @@ describe('SvelteKitOnboarding', () => { ) expect(gitCommit).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const commitCommand = await screen.findByTestId( - 'sveltekit-commit-command' - ) - const commitCommandCopy = await within(commitCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(commitCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.commit', - 1, - { tags: { bundler: 'sveltekit' } } - ) - ) - }) - }) }) describe('step 5', () => { @@ -472,30 +300,6 @@ describe('SvelteKitOnboarding', () => { const npmBuild = await screen.findByText('npm run build') expect(npmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId( - 'sveltekit-npm-build' - ) - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'npm', bundler: 'sveltekit' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -506,30 +310,6 @@ describe('SvelteKitOnboarding', () => { const yarnBuild = await screen.findByText('yarn run build') expect(yarnBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId( - 'sveltekit-yarn-build' - ) - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'sveltekit' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -540,30 +320,6 @@ describe('SvelteKitOnboarding', () => { const pnpmBuild = await screen.findByText('pnpm run build') expect(pnpmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId( - 'sveltekit-pnpm-build' - ) - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'sveltekit' } } - ) - ) - }) - }) }) }) }) diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/SvelteKitOnboarding/SvelteKitOnboarding.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/SvelteKitOnboarding/SvelteKitOnboarding.tsx index b75033db34..c614901dcc 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/SvelteKitOnboarding/SvelteKitOnboarding.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/SvelteKitOnboarding/SvelteKitOnboarding.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React from 'react' import { useParams } from 'react-router-dom' import { useOrgUploadToken } from 'services/orgUploadToken' @@ -8,14 +8,6 @@ import { Card } from 'ui/Card' import { CodeSnippet } from 'ui/CodeSnippet' import LearnMoreBlurb from '../LearnMoreBlurb' -import { - copiedBuildCommandMetric, - copiedCommitMetric, - copiedConfigMetric, - copiedInstallCommandMetric, - copiedTokenMetric, - visitedOnboardingMetric, -} from '../metricHelpers' const npmInstall = `npm install @codecov/sveltekit-plugin --save-dev` const yarnInstall = `yarn add @codecov/sveltekit-plugin --dev` @@ -60,29 +52,17 @@ const StepOne: React.FC = () => { {' '} to your project, use one of the following commands.

- { - copiedInstallCommandMetric('npm', 'sveltekit') - }} - data-testid="sveltekit-npm-install" - > + {npmInstall} { - copiedInstallCommandMetric('yarn', 'sveltekit') - }} data-testid="sveltekit-yarn-install" > {yarnInstall} { - copiedInstallCommandMetric('pnpm', 'sveltekit') - }} data-testid="sveltekit-pnpm-install" > {pnpmInstall} @@ -110,9 +90,6 @@ const StepTwo: React.FC<{ uploadToken: string }> = ({ uploadToken }) => { { - copiedTokenMetric('sveltekit') - }} data-testid="sveltekit-upload-token" > {uploadToken} @@ -141,9 +118,6 @@ const StepThree: React.FC = () => {

{ - copiedConfigMetric('sveltekit') - }} data-testid="sveltekit-plugin-config" > {pluginConfig} @@ -168,9 +142,6 @@ const StepFour: React.FC = () => {

{ - copiedCommitMetric('sveltekit') - }} data-testid="sveltekit-commit-command" > {commitString} @@ -191,31 +162,13 @@ const StepFive: React.FC = () => { When building your application the plugin will automatically upload the stats information to Codecov.

- { - copiedBuildCommandMetric('npm', 'sveltekit') - }} - data-testid="sveltekit-npm-build" - > + {npmBuild} - { - copiedBuildCommandMetric('yarn', 'sveltekit') - }} - data-testid="sveltekit-yarn-build" - > + {yarnBuild} - { - copiedBuildCommandMetric('pnpm', 'sveltekit') - }} - data-testid="sveltekit-pnpm-build" - > + {pnpmBuild} @@ -256,10 +209,6 @@ const SvelteKitOnboarding: React.FC = () => { const uploadToken = orgUploadToken ?? repoData?.repository?.uploadToken ?? '' - useEffect(() => { - visitedOnboardingMetric('sveltekit') - }, []) - return (
diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/ViteOnboarding/ViteOnboarding.test.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/ViteOnboarding/ViteOnboarding.test.tsx index 5d1759ae40..7533df7ad3 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/ViteOnboarding/ViteOnboarding.test.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/ViteOnboarding/ViteOnboarding.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor, within } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' @@ -8,21 +8,6 @@ import { MemoryRouter, Route } from 'react-router-dom' import ViteOnboarding from './ViteOnboarding' -const mocks = vi.hoisted(() => ({ - increment: vi.fn(), -})) - -vi.mock('@sentry/react', async () => { - const originalModule = await vi.importActual('@sentry/react') - - return { - ...originalModule, - metrics: { - increment: mocks.increment, - }, - } -}) - const mockGetRepo = { owner: { isCurrentUserPartOfOrg: true, @@ -105,21 +90,6 @@ describe('ViteOnboarding', () => { return { user } } - describe('rendering onboarding', () => { - it('sends vite onboarding metric', async () => { - setup(null) - render(, { wrapper }) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.visited_page', - 1, - { tags: { bundler: 'vite' } } - ) - ) - }) - }) - describe('step 1', () => { it('renders header', async () => { setup(null) @@ -156,28 +126,6 @@ describe('ViteOnboarding', () => { ) expect(npmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const npmInstall = await screen.findByTestId('vite-npm-install') - const npmInstallCopy = await within(npmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(npmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'npm', bundler: 'vite' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -190,28 +138,6 @@ describe('ViteOnboarding', () => { ) expect(yarnInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const yarnInstall = await screen.findByTestId('vite-yarn-install') - const yarnInstallCopy = await within(yarnInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(yarnInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'vite' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -224,28 +150,6 @@ describe('ViteOnboarding', () => { ) expect(pnpmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const pnpmInstall = await screen.findByTestId('vite-pnpm-install') - const pnpmInstallCopy = await within(pnpmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pnpmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'vite' } } - ) - ) - }) - }) }) }) }) @@ -295,28 +199,6 @@ describe('ViteOnboarding', () => { expect(token).toBeInTheDocument() }) }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const uploadToken = await screen.findByTestId('vite-upload-token') - const uploadTokenCopy = await within(uploadToken).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(uploadTokenCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.token', - 1, - { tags: { bundler: 'vite' } } - ) - ) - }) - }) }) describe('step 3', () => { @@ -351,28 +233,6 @@ describe('ViteOnboarding', () => { const pluginText = await screen.findByText(/\/\/ vite.config.js/) expect(pluginText).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const pluginConfig = await screen.findByTestId('vite-plugin-config') - const pluginConfigCopy = await within(pluginConfig).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pluginConfigCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.config', - 1, - { tags: { bundler: 'vite' } } - ) - ) - }) - }) }) describe('step 4', () => { @@ -408,28 +268,6 @@ describe('ViteOnboarding', () => { ) expect(gitCommit).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const commitCommand = await screen.findByTestId('vite-commit-command') - const commitCommandCopy = await within(commitCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(commitCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.commit', - 1, - { tags: { bundler: 'vite' } } - ) - ) - }) - }) }) describe('step 5', () => { @@ -463,28 +301,6 @@ describe('ViteOnboarding', () => { const npmBuild = await screen.findByText('npm run build') expect(npmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('vite-npm-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'npm', bundler: 'vite' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -495,28 +311,6 @@ describe('ViteOnboarding', () => { const yarnBuild = await screen.findByText('yarn run build') expect(yarnBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('vite-yarn-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'vite' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -527,28 +321,6 @@ describe('ViteOnboarding', () => { const pnpmBuild = await screen.findByText('pnpm run build') expect(pnpmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('vite-pnpm-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'vite' } } - ) - ) - }) - }) }) }) }) diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/ViteOnboarding/ViteOnboarding.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/ViteOnboarding/ViteOnboarding.tsx index 5fb77b81e2..93663f0de3 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/ViteOnboarding/ViteOnboarding.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/ViteOnboarding/ViteOnboarding.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react' import { useParams } from 'react-router-dom' import { useOrgUploadToken } from 'services/orgUploadToken' @@ -8,14 +7,6 @@ import { Card } from 'ui/Card' import { CodeSnippet } from 'ui/CodeSnippet' import LearnMoreBlurb from '../LearnMoreBlurb' -import { - copiedBuildCommandMetric, - copiedCommitMetric, - copiedConfigMetric, - copiedInstallCommandMetric, - copiedTokenMetric, - visitedOnboardingMetric, -} from '../metricHelpers' const npmInstall = `npm install @codecov/vite-plugin --save-dev` const yarnInstall = `yarn add @codecov/vite-plugin --dev` @@ -58,31 +49,13 @@ const StepOne: React.FC = () => { {' '} to your project, use one of the following commands.

- { - copiedInstallCommandMetric('npm', 'vite') - }} - data-testid="vite-npm-install" - > + {npmInstall} - { - copiedInstallCommandMetric('yarn', 'vite') - }} - data-testid="vite-yarn-install" - > + {yarnInstall} - { - copiedInstallCommandMetric('pnpm', 'vite') - }} - data-testid="vite-pnpm-install" - > + {pnpmInstall} @@ -108,9 +81,6 @@ const StepTwo: React.FC<{ uploadToken: string }> = ({ uploadToken }) => { { - copiedTokenMetric('vite') - }} data-testid="vite-upload-token" > {uploadToken} @@ -138,13 +108,7 @@ const StepThree: React.FC = () => { {' '} file.

- { - copiedConfigMetric('vite') - }} - data-testid="vite-plugin-config" - > + {pluginConfig} @@ -165,13 +129,7 @@ const StepFour: React.FC = () => { The plugin requires at least one commit to be made to properly upload bundle analysis information to Codecov.

- { - copiedCommitMetric('vite') - }} - data-testid="vite-commit-command" - > + {commitString} @@ -191,31 +149,13 @@ const StepFive: React.FC = () => { the stats information to Codecov.

- { - copiedBuildCommandMetric('npm', 'vite') - }} - data-testid="vite-npm-build" - > + {npmBuild} - { - copiedBuildCommandMetric('yarn', 'vite') - }} - data-testid="vite-yarn-build" - > + {yarnBuild} - { - copiedBuildCommandMetric('pnpm', 'vite') - }} - data-testid="vite-pnpm-build" - > + {pnpmBuild}
@@ -257,10 +197,6 @@ const ViteOnboarding: React.FC = () => { const uploadToken = orgUploadToken ?? repoData?.repository?.uploadToken ?? '' - useEffect(() => { - visitedOnboardingMetric('vite') - }, []) - return (
diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/WebpackOnboarding/WebpackOnboarding.test.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/WebpackOnboarding/WebpackOnboarding.test.tsx index d345f7f705..c1e087557a 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/WebpackOnboarding/WebpackOnboarding.test.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/WebpackOnboarding/WebpackOnboarding.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor, within } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' @@ -8,21 +8,6 @@ import { MemoryRouter, Route } from 'react-router-dom' import WebpackOnboarding from './WebpackOnboarding' -const mocks = vi.hoisted(() => ({ - increment: vi.fn(), -})) - -vi.mock('@sentry/react', async () => { - const originalModule = await vi.importActual('@sentry/react') - - return { - ...originalModule, - metrics: { - increment: mocks.increment, - }, - } -}) - const mockGetRepo = { owner: { isCurrentUserPartOfOrg: true, @@ -104,21 +89,6 @@ describe('WebpackOnboarding', () => { return { user } } - describe('rendering onboarding', () => { - it('sends webpack onboarding metric', async () => { - setup(null) - render(, { wrapper }) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.visited_page', - 1, - { tags: { bundler: 'webpack' } } - ) - ) - }) - }) - describe('step 1', () => { it('renders header', async () => { setup(null) @@ -155,28 +125,6 @@ describe('WebpackOnboarding', () => { ) expect(npmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const npmInstall = await screen.findByTestId('webpack-npm-install') - const npmInstallCopy = await within(npmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(npmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'npm', bundler: 'webpack' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -189,30 +137,6 @@ describe('WebpackOnboarding', () => { ) expect(yarnInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const yarnInstall = await screen.findByTestId( - 'webpack-yarn-install' - ) - const yarnInstallCopy = await within(yarnInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(yarnInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'webpack' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -225,30 +149,6 @@ describe('WebpackOnboarding', () => { ) expect(pnpmInstallCommand).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(null) - render(, { wrapper }) - - const pnpmInstall = await screen.findByTestId( - 'webpack-pnpm-install' - ) - const pnpmInstallCopy = await within(pnpmInstall).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pnpmInstallCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.install_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'webpack' } } - ) - ) - }) - }) }) }) }) @@ -298,28 +198,6 @@ describe('WebpackOnboarding', () => { expect(token).toBeInTheDocument() }) }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const uploadToken = await screen.findByTestId('webpack-upload-token') - const uploadTokenCopy = await within(uploadToken).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(uploadTokenCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.token', - 1, - { tags: { bundler: 'webpack' } } - ) - ) - }) - }) }) describe('step 3', () => { @@ -354,28 +232,6 @@ describe('WebpackOnboarding', () => { const pluginText = await screen.findByText(/\/\/ webpack.config.js/) expect(pluginText).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const pluginConfig = await screen.findByTestId('webpack-plugin-config') - const pluginConfigCopy = await within(pluginConfig).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(pluginConfigCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.config', - 1, - { tags: { bundler: 'webpack' } } - ) - ) - }) - }) }) describe('step 4', () => { @@ -411,30 +267,6 @@ describe('WebpackOnboarding', () => { ) expect(gitCommit).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const commitCommand = await screen.findByTestId( - 'webpack-commit-command' - ) - const commitCommandCopy = await within(commitCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(commitCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.commit', - 1, - { tags: { bundler: 'webpack' } } - ) - ) - }) - }) }) describe('step 5', () => { @@ -468,28 +300,6 @@ describe('WebpackOnboarding', () => { const npmBuild = await screen.findByText('npm run build') expect(npmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('webpack-npm-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'npm', bundler: 'webpack' } } - ) - ) - }) - }) }) describe('yarn', () => { @@ -500,28 +310,6 @@ describe('WebpackOnboarding', () => { const yarnBuild = await screen.findByText('yarn run build') expect(yarnBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('webpack-yarn-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'yarn', bundler: 'webpack' } } - ) - ) - }) - }) }) describe('pnpm', () => { @@ -532,28 +320,6 @@ describe('WebpackOnboarding', () => { const pnpmBuild = await screen.findByText('pnpm run build') expect(pnpmBuild).toBeInTheDocument() }) - - describe('user clicks copy button', () => { - it('sends metric to sentry', async () => { - const { user } = setup(true) - render(, { wrapper }) - - const buildCommand = await screen.findByTestId('webpack-pnpm-build') - const buildCommandCopy = await within(buildCommand).findByTestId( - 'clipboard-code-snippet' - ) - - await user.click(buildCommandCopy) - - await waitFor(() => - expect(mocks.increment).toHaveBeenCalledWith( - 'bundles_tab.onboarding.copied.build_command', - 1, - { tags: { package_manager: 'pnpm', bundler: 'webpack' } } - ) - ) - }) - }) }) }) }) diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/WebpackOnboarding/WebpackOnboarding.tsx b/src/pages/RepoPage/BundlesTab/BundleOnboarding/WebpackOnboarding/WebpackOnboarding.tsx index c699d3e532..8376907515 100644 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/WebpackOnboarding/WebpackOnboarding.tsx +++ b/src/pages/RepoPage/BundlesTab/BundleOnboarding/WebpackOnboarding/WebpackOnboarding.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React from 'react' import { useParams } from 'react-router-dom' import { useOrgUploadToken } from 'services/orgUploadToken' @@ -8,14 +8,6 @@ import { Card } from 'ui/Card' import { CodeSnippet } from 'ui/CodeSnippet' import LearnMoreBlurb from '../LearnMoreBlurb' -import { - copiedBuildCommandMetric, - copiedCommitMetric, - copiedConfigMetric, - copiedInstallCommandMetric, - copiedTokenMetric, - visitedOnboardingMetric, -} from '../metricHelpers' const npmInstall = `npm install @codecov/webpack-plugin --save-dev` const yarnInstall = `yarn add @codecov/webpack-plugin --dev` @@ -59,31 +51,13 @@ const StepOne: React.FC = () => { {' '} to your project, use one of the following commands.

- { - copiedInstallCommandMetric('npm', 'webpack') - }} - data-testid="webpack-npm-install" - > + {npmInstall} - { - copiedInstallCommandMetric('yarn', 'webpack') - }} - data-testid="webpack-yarn-install" - > + {yarnInstall} - { - copiedInstallCommandMetric('pnpm', 'webpack') - }} - data-testid="webpack-pnpm-install" - > + {pnpmInstall} @@ -109,9 +83,6 @@ const StepTwo: React.FC<{ uploadToken: string }> = ({ uploadToken }) => { { - copiedTokenMetric('webpack') - }} data-testid="webpack-upload-token" > {uploadToken} @@ -156,9 +127,6 @@ const StepThree: React.FC = () => {

{ - copiedConfigMetric('webpack') - }} data-testid="webpack-plugin-config" > {pluginConfig} @@ -183,9 +151,6 @@ const StepFour: React.FC = () => {

{ - copiedCommitMetric('webpack') - }} data-testid="webpack-commit-command" > {commitString} @@ -206,31 +171,13 @@ const StepFive: React.FC = () => { When building your application the plugin will automatically upload the stats information to Codecov.

- { - copiedBuildCommandMetric('npm', 'webpack') - }} - data-testid="webpack-npm-build" - > + {npmBuild} - { - copiedBuildCommandMetric('yarn', 'webpack') - }} - data-testid="webpack-yarn-build" - > + {yarnBuild} - { - copiedBuildCommandMetric('pnpm', 'webpack') - }} - data-testid="webpack-pnpm-build" - > + {pnpmBuild} @@ -271,10 +218,6 @@ const WebpackOnboarding: React.FC = () => { const uploadToken = orgUploadToken ?? repoData?.repository?.uploadToken ?? '' - useEffect(() => { - visitedOnboardingMetric('webpack') - }, []) - return (
diff --git a/src/pages/RepoPage/BundlesTab/BundleOnboarding/metricHelpers.ts b/src/pages/RepoPage/BundlesTab/BundleOnboarding/metricHelpers.ts deleted file mode 100644 index cc6cc0cd3e..0000000000 --- a/src/pages/RepoPage/BundlesTab/BundleOnboarding/metricHelpers.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-disable camelcase */ -import { metrics } from 'shared/utils/metrics' - -type Bundlers = - | 'rollup' - | 'vite' - | 'webpack' - | 'remix' - | 'nuxt' - | 'sveltekit' - | 'solidstart' -type PackageManagers = 'npm' | 'yarn' | 'pnpm' - -export const copiedInstallCommandMetric = ( - packageManager: PackageManagers, - bundler: Bundlers -) => { - metrics.increment('bundles_tab.onboarding.copied.install_command', 1, { - tags: { - package_manager: packageManager, - bundler, - }, - }) -} - -export const copiedBuildCommandMetric = ( - packageManager: PackageManagers, - bundler: Bundlers -) => { - metrics.increment('bundles_tab.onboarding.copied.build_command', 1, { - tags: { - package_manager: packageManager, - bundler, - }, - }) -} - -export const copiedTokenMetric = (bundler: Bundlers) => { - metrics.increment('bundles_tab.onboarding.copied.token', 1, { - tags: { bundler }, - }) -} - -export const copiedConfigMetric = (bundler: Bundlers) => { - metrics.increment('bundles_tab.onboarding.copied.config', 1, { - tags: { bundler }, - }) -} - -export const copiedCommitMetric = (bundler: Bundlers) => { - metrics.increment('bundles_tab.onboarding.copied.commit', 1, { - tags: { bundler }, - }) -} - -export const visitedOnboardingMetric = (bundler: Bundlers) => { - metrics.increment('bundles_tab.onboarding.visited_page', 1, { - tags: { bundler }, - }) -} diff --git a/src/shared/GlobalTopBanners/RequestInstallBanner/RequestInstallBanner.test.tsx b/src/shared/GlobalTopBanners/RequestInstallBanner/RequestInstallBanner.test.tsx index f439d1a32f..5b362bbd49 100644 --- a/src/shared/GlobalTopBanners/RequestInstallBanner/RequestInstallBanner.test.tsx +++ b/src/shared/GlobalTopBanners/RequestInstallBanner/RequestInstallBanner.test.tsx @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/react' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import React from 'react' @@ -30,17 +29,6 @@ vi.mock('shared/featureFlags', async () => { useFlags: mocks.useFlags, } }) -vi.mock('@sentry/react', async () => { - const originalModule = await vi.importActual('@sentry/react') - return { - ...originalModule, - metrics: { - // @ts-expect-error - ...originalModule.metrics, - increment: mocks.captureMessage, - }, - } -}) console.error = () => {} @@ -118,23 +106,6 @@ describe('RequestInstallBanner', () => { ) expect(modalText).toBeInTheDocument() }) - - it('should capture the user shared request metric', async () => { - const { user, mockGetItem } = setup({}) - render(, { wrapper: wrapper('/gh/codecov') }) - - mockGetItem.mockReturnValue(null) - - const btn = screen.getByRole('button', { name: /Share Request/ }) - expect(btn).toBeInTheDocument() - await user.click(btn) - - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'request_install.user.shared.request', - undefined, - undefined - ) - }) }) describe('user closes modal without clicking Done', () => { diff --git a/src/shared/GlobalTopBanners/RequestInstallBanner/RequestInstallBanner.tsx b/src/shared/GlobalTopBanners/RequestInstallBanner/RequestInstallBanner.tsx index 5d5e416176..81efca5f72 100644 --- a/src/shared/GlobalTopBanners/RequestInstallBanner/RequestInstallBanner.tsx +++ b/src/shared/GlobalTopBanners/RequestInstallBanner/RequestInstallBanner.tsx @@ -6,7 +6,6 @@ import config from 'config' import { useLocationParams } from 'services/navigation' import AppInstallModal from 'shared/AppInstallModal' import { providerToName } from 'shared/utils' -import { metrics } from 'shared/utils/metrics' import Button from 'ui/Button' import Icon from 'ui/Icon' import TopBanner, { saveToLocalStorage } from 'ui/TopBanner' @@ -76,7 +75,6 @@ const RequestInstallBanner = () => { onClick={() => { // this has the side effect of hiding the banner setShowAppInstallModal(true) - metrics.increment('request_install.user.shared.request') }} > Share Request diff --git a/src/shared/GlobalTopBanners/RequestInstallBanner/index.js b/src/shared/GlobalTopBanners/RequestInstallBanner/index.ts similarity index 100% rename from src/shared/GlobalTopBanners/RequestInstallBanner/index.js rename to src/shared/GlobalTopBanners/RequestInstallBanner/index.ts diff --git a/src/shared/utils/metrics.test.ts b/src/shared/utils/metrics.test.ts deleted file mode 100644 index fb7a685719..0000000000 --- a/src/shared/utils/metrics.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as Sentry from '@sentry/react' - -import { metrics } from './metrics' - -describe('metrics wrapper', () => { - describe('distribution', () => { - it('calls sentry distribution', () => { - metrics.distribution('testKey', 1) - - expect(Sentry.metrics.distribution).toHaveBeenCalledWith( - 'testKey', - 1, - undefined - ) - }) - }) - - describe('gauge', () => { - it('calls sentry gauge', () => { - metrics.gauge('billing_change.user.seats_change', 1) - - expect(Sentry.metrics.gauge).toHaveBeenCalledWith( - 'billing_change.user.seats_change', - 1, - undefined - ) - }) - }) - - describe('increment', () => { - it('calls sentry increment', () => { - metrics.increment('coverage_tab.visited_page', 1) - - expect(Sentry.metrics.increment).toHaveBeenCalledWith( - 'coverage_tab.visited_page', - 1, - undefined - ) - }) - }) - - describe('set', () => { - it('calls sentry set', () => { - metrics.set('testKey', 1) - - expect(Sentry.metrics.set).toHaveBeenCalledWith('testKey', 1, undefined) - }) - }) -}) diff --git a/src/shared/utils/metrics.ts b/src/shared/utils/metrics.ts deleted file mode 100644 index e0ffcd3ccd..0000000000 --- a/src/shared/utils/metrics.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* eslint-disable camelcase */ -import { metrics as sentryMetrics } from '@sentry/react' - -type MetricKeyNameUnion = { - [P in keyof T]: T[P] extends object - ? MetricKeyNameUnion - : `${K}${K extends '' ? '' : '.'}${string & P}` -}[keyof T] - -type DistributionKeys = { - // TODO: Remove this key when adding in non-test keys - testKey: string -} - -type GaugeKeys = { - billing_change: { - user: { - seats_change: string - } - } -} - -type IncrementKeys = { - coverage_tab: { - visited_page: string - } - commit_detail_page: { - coverage_page: { - visited_page: string - } - coverage_dropdown: { - opened: string - } - bundle_page: { - visited_page: string - } - bundle_dropdown: { - opened: string - } - } - pull_request_page: { - coverage_page: { - visited_page: string - } - coverage_dropdown: { - opened: string - } - bundle_page: { - visited_page: string - } - bundle_dropdown: { - opened: string - } - } - bundles_tab: { - bundle_details: { - visited_page: string - } - onboarding: { - visited_page: string - copied: { - token: string - config: string - commit: string - install_command: string - build_command: string - } - } - } - request_install: { - user: { - shared: { - request: string - } - } - } - billing_change: { - user: { - visited_page: string - checkout_from_page: string - } - } - button_clicked: { - theme: { - light: string - dark: string - } - } -} - -type SetKeys = { - // TODO: Remove this key when adding in non-test keys - testKey: string -} - -type DistributionValue = Parameters['1'] -type DistributionData = Parameters['2'] - -type GaugeValue = Parameters['1'] -type GaugeData = Parameters['2'] - -type IncrementValue = Parameters['1'] -type IncrementData = Parameters['2'] - -type SetValue = Parameters['1'] -type SetData = Parameters['2'] - -export const metrics = { - distribution: ( - name: MetricKeyNameUnion, - value: DistributionValue, - data?: DistributionData - ) => { - sentryMetrics.distribution(name, value, data) - }, - gauge: ( - name: MetricKeyNameUnion, - value: GaugeValue, - data?: GaugeData - ) => { - sentryMetrics.gauge(name, value, data) - }, - increment: ( - name: MetricKeyNameUnion, - value?: IncrementValue, - data?: IncrementData - ) => { - sentryMetrics.increment(name, value, data) - }, - set: (name: MetricKeyNameUnion, value: SetValue, data?: SetData) => { - sentryMetrics.set(name, value, data) - }, -} as const From b92c5389b771b3ce3d690eceb85be5e7df695220 Mon Sep 17 00:00:00 2001 From: ajay-sentry <159853603+ajay-sentry@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:53:26 -0700 Subject: [PATCH 06/10] feat: Default color mode to user's current system theme (#3456) --- src/globals.css | 8 ++++---- src/shared/ThemeContext/ThemeContext.test.tsx | 1 + src/shared/ThemeContext/ThemeContext.tsx | 11 +++++++++-- src/vitest.setup.ts | 2 ++ 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/globals.css b/src/globals.css index 4161be393a..0022964a77 100644 --- a/src/globals.css +++ b/src/globals.css @@ -253,6 +253,8 @@ --color-code-operator: 219 178, 89; } + /* Also used for prefers-color-scheme: light */ + /* explicitly define it below if we want to change behavior */ body { @apply lightMode; } @@ -261,7 +263,7 @@ @apply darkMode; } - /* new body theme can be applied here + /* new body theme can be applied here */ @media (prefers-color-scheme: dark) { body { @@ -271,9 +273,7 @@ body.light { @apply lightMode; } - - new body theme added above should be applied here as well to cover specificity - } */ + } } #sentry-feedback { diff --git a/src/shared/ThemeContext/ThemeContext.test.tsx b/src/shared/ThemeContext/ThemeContext.test.tsx index b4137d28d5..176b3ff622 100644 --- a/src/shared/ThemeContext/ThemeContext.test.tsx +++ b/src/shared/ThemeContext/ThemeContext.test.tsx @@ -27,6 +27,7 @@ describe('Theme context', () => { function setup() { window.localStorage.__proto__.setItem = vi.fn() window.localStorage.__proto__.getItem = vi.fn() + window.matchMedia = vi.fn().mockResolvedValue({ matches: false }) const user = userEvent.setup() diff --git a/src/shared/ThemeContext/ThemeContext.tsx b/src/shared/ThemeContext/ThemeContext.tsx index 9f69d135dd..eb2d134876 100644 --- a/src/shared/ThemeContext/ThemeContext.tsx +++ b/src/shared/ThemeContext/ThemeContext.tsx @@ -29,8 +29,15 @@ interface ThemeContextProviderProps { export const ThemeContextProvider: FC = ({ children, }) => { - const currentTheme = (localStorage.getItem('theme') as Theme) || Theme.LIGHT - const [theme, setTheme] = useState(currentTheme) + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + + let systemTheme = Theme.LIGHT + if (prefersDark) { + systemTheme = Theme.DARK + } + + const currentTheme = localStorage.getItem('theme') as Theme + const [theme, setTheme] = useState(currentTheme ?? systemTheme) const initialRender = useRef(true) if (initialRender.current) { diff --git a/src/vitest.setup.ts b/src/vitest.setup.ts index e20d3ff5b7..761ec2ff06 100644 --- a/src/vitest.setup.ts +++ b/src/vitest.setup.ts @@ -28,6 +28,8 @@ vi.mock('@sentry/react', async () => { } }) +window.matchMedia = vi.fn().mockResolvedValue({ matches: false }) + beforeAll(() => { globalThis.jest = { ...globalThis.jest, From f235dce5bc8a36567fdf1f69be8f51bf85ffe6bd Mon Sep 17 00:00:00 2001 From: Rohit Vinnakota <148245014+rohitvinnakota-codecov@users.noreply.github.com> Date: Fri, 1 Nov 2024 10:41:08 -0400 Subject: [PATCH 07/10] Update codecov AI tab with screenshot and remove copilot features (#3455) --- .../codecovAI/pr-review-example-dark-mode.png | Bin 0 -> 122959 bytes .../pr-review-example-light-mode.png | Bin 0 -> 123000 bytes src/config.js | 2 +- src/globals.css | 2 ++ .../CodecovAICommands/CodecovAICommands.tsx | 33 +++++++----------- .../CodecovAIPage/CodecovAIPage.test.tsx | 32 ++++++++++------- src/pages/CodecovAIPage/CodecovAIPage.tsx | 5 ++- .../ConfiguredRepositories.tsx | 2 +- .../InstallCodecovAI/InstallCodecovAI.tsx | 5 ++- .../LearnMoreBlurb/LearnMoreBlurb.tsx | 5 +-- .../useNavLinks/useStaticNavLinks.js | 9 ++++- src/ui/Button/Button.jsx | 3 +- tailwind.config.mjs | 1 + 13 files changed, 55 insertions(+), 44 deletions(-) create mode 100644 src/assets/codecovAI/pr-review-example-dark-mode.png create mode 100644 src/assets/codecovAI/pr-review-example-light-mode.png diff --git a/src/assets/codecovAI/pr-review-example-dark-mode.png b/src/assets/codecovAI/pr-review-example-dark-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..535d9866a9c7307efa7e9e6489ff617d30744b75 GIT binary patch literal 122959 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 literal 0 HcmV?d00001 diff --git a/src/assets/codecovAI/pr-review-example-light-mode.png b/src/assets/codecovAI/pr-review-example-light-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..ee45b62cc72950aed1d0309b3f4ed8ffe6f3a327 GIT binary patch literal 123000 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 S!;R5UUt0I{)ywWY3jZG$xZ92Z literal 0 HcmV?d00001 diff --git a/src/config.js b/src/config.js index fb0f2ef40a..8e253129a8 100644 --- a/src/config.js +++ b/src/config.js @@ -9,7 +9,7 @@ const defaultConfig = { SENTRY_SESSION_SAMPLE_RATE: 0.1, SENTRY_ERROR_SAMPLE_RATE: 1.0, GH_APP: 'codecov', - GH_APP_AI: 'codecov', // TODO: Update to proper GH app name once it is live + GH_APP_AI: 'codecov-ai', } export function removeReactAppPrefix(obj) { diff --git a/src/globals.css b/src/globals.css index 0022964a77..d25ca6c229 100644 --- a/src/globals.css +++ b/src/globals.css @@ -199,6 +199,8 @@ --color-github: 255, 255, 255; --color-github-text: 14, 27, 41; + --color-github-hover-bg: 70, 75, 85; + --color-gitlab: 242, 98, 42; --color-bitbucket: 2, 75, 186; --color-okta: 255, 255, 255; diff --git a/src/pages/CodecovAIPage/CodecovAICommands/CodecovAICommands.tsx b/src/pages/CodecovAIPage/CodecovAICommands/CodecovAICommands.tsx index d22d4d4601..bbf960a44b 100644 --- a/src/pages/CodecovAIPage/CodecovAICommands/CodecovAICommands.tsx +++ b/src/pages/CodecovAIPage/CodecovAICommands/CodecovAICommands.tsx @@ -1,7 +1,13 @@ +import darkModeImage from 'assets/codecovAI/pr-review-example-dark-mode.png' +import lightModeImage from 'assets/codecovAI/pr-review-example-light-mode.png' +import { Theme, useThemeContext } from 'shared/ThemeContext' import { Card } from 'ui/Card' import { ExpandableSection } from 'ui/ExpandableSection' const CodecovAICommands: React.FC = () => { + const { theme } = useThemeContext() + const prReviewExampleSource = + theme === Theme.DARK ? darkModeImage : lightModeImage return (
@@ -9,14 +15,8 @@ const CodecovAICommands: React.FC = () => { Codecov AI Commands - After installing the app, use these commands in your PR comments: + After installing the app, use this command in your PR comments:
    -
  • - - @codecov-ai-reviewer test - - -- the assistant will generate tests for the PR. -
  • @codecov-ai-reviewer review @@ -33,19 +33,12 @@ const CodecovAICommands: React.FC = () => { generation may take time.

    - - Screenshot goes here - - - - -

    - Here is an example of Codecov AI Test Generator in PR comments. - Comment generation may take time. -

    -
    - - Screenshot goes here + + codecov pr review example
diff --git a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx index 09c943cafd..b6a5917f6c 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx @@ -1,7 +1,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { cleanup, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' +import { Suspense } from 'react' import { MemoryRouter, Route } from 'react-router-dom' import { ThemeContextProvider } from 'shared/ThemeContext' @@ -35,7 +37,9 @@ const wrapper: React.FC = ({ children }) => ( - {children} + + {children} + @@ -136,14 +140,9 @@ describe('CodecovAIPage', () => { expect(commandText).toBeInTheDocument() const commandOneText = await screen.findByText( - / the assistant will generate tests/ - ) - expect(commandOneText).toBeInTheDocument() - - const commandTwoText = await screen.findByText( / the assistant will review the PR/ ) - expect(commandTwoText).toBeInTheDocument() + expect(commandOneText).toBeInTheDocument() }) it('renders examples', async () => { @@ -153,14 +152,23 @@ describe('CodecovAIPage', () => { /Here is an example of Codecov AI Reviewer in PR comments/ ) expect(reviewExample).toBeInTheDocument() + }) - const testGenerator = await screen.findByText( - /Here is an example of Codecov AI Test Generator/ + it('renders screenshot', async () => { + render(, { wrapper }) + const user = userEvent.setup() + const trigger = await screen.findByText((content) => + content.startsWith('Here is an example') ) - expect(testGenerator).toBeInTheDocument() - }) + expect(trigger).toBeInTheDocument() + + await user.click(trigger) - //TODO: Once we have screenshots, test that they are visible + const screenshot = await screen.findByRole('img', { + name: /codecov pr review example/, + }) + expect(screenshot).toBeInTheDocument() + }) it('renders a link to the docs', async () => { render(, { wrapper }) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.tsx b/src/pages/CodecovAIPage/CodecovAIPage.tsx index 0f9cb42a0c..1cd95adf31 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.tsx @@ -36,9 +36,8 @@ const CodecovAIPage: React.FC = () => {

Codecov AI is a generative AI assistant developed by Codecov at - Sentry. It helps you with generating new tests for uncovered code and - reviews your code changes, offering suggestions for improvement before - merging pull requests. + Sentry. It helps you review your code changes, offering suggestions + for improvement before merging pull requests.

diff --git a/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx b/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx index c39ff7b882..9d178479ab 100644 --- a/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx +++ b/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx @@ -4,7 +4,7 @@ import { getCoreRowModel, useReactTable, } from '@tanstack/react-table' -import React, { useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { useParams } from 'react-router-dom' import { useCodecovAIInstalledRepos } from 'services/codecovAI/useCodecovAIInstalledRepos' diff --git a/src/pages/CodecovAIPage/InstallCodecovAI/InstallCodecovAI.tsx b/src/pages/CodecovAIPage/InstallCodecovAI/InstallCodecovAI.tsx index b7510c3795..9d8bff127f 100644 --- a/src/pages/CodecovAIPage/InstallCodecovAI/InstallCodecovAI.tsx +++ b/src/pages/CodecovAIPage/InstallCodecovAI/InstallCodecovAI.tsx @@ -23,9 +23,8 @@ const InstallCodecovAI: React.FC = () => { To enable the Codecov AI assistant in your GitHub organization, or on specific repositories, you need to install the Codecov AI GitHub App - Integration. This will allow the assistant to analyze pull requests, - provide insights, and generate new tests to help increase your code - coverage. + Integration. This will allow the assistant to analyze pull requests + and provide insights.
diff --git a/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.jsx b/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.jsx deleted file mode 100644 index a8b677be8f..0000000000 --- a/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import Banner from 'ui/Banner' -import BannerContent from 'ui/Banner/BannerContent' -import BannerHeading from 'ui/Banner/BannerHeading' - -function YamlErrorBanner() { - return ( - - -

Commit YAML is invalid

-
- - Coverage data is unable to be displayed, as the commit YAML appears to - be invalid. - -
- ) -} - -export default YamlErrorBanner diff --git a/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.test.jsx b/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.test.jsx deleted file mode 100644 index 1e561ab4ad..0000000000 --- a/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.test.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import { render, screen } from '@testing-library/react' -import { MemoryRouter, Route } from 'react-router-dom' - -import YamlErrorBanner from './YamlErrorBanner' - -describe('YamlErrorBanner', () => { - function setup() { - render( - - - - - - ) - } - - describe('when rendered', () => { - beforeEach(() => { - setup() - }) - - it('renders heading of banner', () => { - expect(screen.getByText(/Commit YAML/)).toBeInTheDocument() - }) - - it('renders content', () => { - expect( - screen.getByText( - /Coverage data is unable to be displayed, as the commit YAML appears to be invalid./ - ) - ).toBeInTheDocument() - }) - }) -}) diff --git a/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.test.tsx b/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.test.tsx new file mode 100644 index 0000000000..534100c51b --- /dev/null +++ b/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.test.tsx @@ -0,0 +1,68 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MemoryRouter, Route } from 'react-router-dom' + +import YamlErrorBanner from './YamlErrorBanner' + +vi.mock('../YamlModal', () => ({ default: () => 'YamlModalComponent' })) + +interface SetupArgs { + shouldLinkToModal?: boolean +} + +describe('YamlErrorBanner', () => { + function setup({ shouldLinkToModal }: SetupArgs) { + render( + + + + + + ) + } + + describe('when rendered', () => { + beforeEach(() => { + setup({}) + }) + + it('renders heading of banner', () => { + expect(screen.getByText(/YAML is invalid/)).toBeInTheDocument() + }) + + it('renders content', () => { + expect( + screen.getByText( + /Coverage data is unable to be displayed, as the yaml appears to be invalid/ + ) + ).toBeInTheDocument() + }) + }) + + describe('handle shouldLinkToModal', () => { + it('links to modal when true', async () => { + setup({ shouldLinkToModal: true }) + const yamlModal = screen.queryByText(/YamlModalComponent/) + expect(yamlModal).toBeInTheDocument() + + const yamlModalLink = await screen.findByTestId('open yaml modal') + expect(yamlModalLink).toBeInTheDocument() + + const user = userEvent.setup() + user.click(yamlModalLink) + }) + + it('does not link to modal when false', async () => { + setup({}) + const yamlModal = screen.queryByText(/YamlModalComponent/) + expect(yamlModal).not.toBeInTheDocument() + + const yamlIsInvalid = await screen.findByText('YAML is invalid') + expect(yamlIsInvalid).toBeInTheDocument() + expect(yamlIsInvalid).not.toHaveAttribute('href') + + const yamlModalLink = screen.queryByTestId('open yaml modal') + expect(yamlModalLink).not.toBeInTheDocument() + }) + }) +}) diff --git a/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.tsx b/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.tsx new file mode 100644 index 0000000000..6cb9b59e85 --- /dev/null +++ b/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/YamlErrorBanner.tsx @@ -0,0 +1,54 @@ +import { useState } from 'react' + +import YamlModal from 'pages/CommitDetailPage/CommitCoverage/YamlModal' +import A from 'ui/A' +import { Alert } from 'ui/Alert' + +function YamlErrorBanner({ + shouldLinkToModal = false, +}: { + shouldLinkToModal?: boolean +}) { + const [showYamlModal, setShowYamlModal] = useState(false) + + return ( + <> + + +
+ {shouldLinkToModal ? ( + + {/* @ts-ignore ignore until we convert A to ts */} + setShowYamlModal(true)} + hook="open yaml modal" + isExternal={true} + > + YAML + +   is invalid + + ) : ( + YAML is invalid + )} +
+
+ + Coverage data is unable to be displayed, as the yaml appears to be + invalid. The  + {/* @ts-ignore ignore until we convert A to ts */} + yaml validator can help + determine its validation. + +
+ {shouldLinkToModal ? ( + + ) : null} + + ) +} + +export default YamlErrorBanner diff --git a/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/index.js b/src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/index.ts similarity index 100% rename from src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/index.js rename to src/pages/CommitDetailPage/CommitCoverage/YamlErrorBanner/index.ts diff --git a/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.jsx b/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.jsx index 31545a67be..4c81668f6b 100644 --- a/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.jsx +++ b/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.jsx @@ -1,13 +1,12 @@ import PropTypes from 'prop-types' import { lazy, Suspense } from 'react' +import YamlErrorBanner from 'pages/CommitDetailPage/CommitCoverage/YamlErrorBanner' import { useCommitErrors } from 'services/commitErrors' import A from 'ui/A' import Modal from 'ui/Modal' import Spinner from 'ui/Spinner' -import YamlModalErrorBanner from './YamlModalErrorBanner' - const YAMLViewer = lazy(() => import('./YAMLViewer')) function YamlModal({ showYAMLModal, setShowYAMLModal }) { @@ -31,7 +30,7 @@ function YamlModal({ showYAMLModal, setShowYAMLModal }) { } >
- {invalidYaml && } + {invalidYaml && }
diff --git a/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.test.jsx b/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.test.jsx index 6b75824b3a..7149fa661d 100644 --- a/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.test.jsx +++ b/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.test.jsx @@ -115,7 +115,7 @@ describe('YamlModal', () => { wrapper, }) - const bannerHeader = await screen.findByText('Commit YAML is invalid') + const bannerHeader = await screen.findByText('YAML is invalid') expect(bannerHeader).toBeInTheDocument() }) }) @@ -130,7 +130,7 @@ describe('YamlModal', () => { await waitFor(() => queryClient.isFetching) await waitFor(() => !queryClient.isFetching) - const bannerHeader = screen.queryByText('Commit YAML is invalid') + const bannerHeader = screen.queryByText('YAML is invalid') expect(bannerHeader).not.toBeInTheDocument() }) }) diff --git a/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModalErrorBanner.jsx b/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModalErrorBanner.jsx deleted file mode 100644 index 1e0943c5e3..0000000000 --- a/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModalErrorBanner.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import A from 'ui/A' -import Banner from 'ui/Banner' -import BannerContent from 'ui/Banner/BannerContent' -import BannerHeading from 'ui/Banner/BannerHeading' - -function YamlModalErrorBanner() { - return ( - - -
Commit YAML is invalid
-
- - When the commit-level YAML is invalid, we use the last valid repo YAML. - To determine if your YAML is valid, please follow the steps{' '} - here. - -
- ) -} - -export default YamlModalErrorBanner diff --git a/src/pages/CommitDetailPage/Dropdowns/CommitCoverageDropdown.test.tsx b/src/pages/CommitDetailPage/Dropdowns/CommitCoverageDropdown.test.tsx index 1a552c5a83..0fd8a760cf 100644 --- a/src/pages/CommitDetailPage/Dropdowns/CommitCoverageDropdown.test.tsx +++ b/src/pages/CommitDetailPage/Dropdowns/CommitCoverageDropdown.test.tsx @@ -5,7 +5,9 @@ import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { Suspense } from 'react' import { MemoryRouter, Route } from 'react-router-dom' +import { z } from 'zod' +import { RequestSchema } from 'services/commit/useCommitCoverageDropdownSummary' import SummaryDropdown from 'ui/SummaryDropdown' import CommitCoverageDropdown from './CommitCoverageDropdown' @@ -42,7 +44,7 @@ const mockSummaryData = ( }, }, }, - } + } as z.infer } const mockNoData = { owner: null } @@ -75,6 +77,23 @@ const mockComparisonError = { }, } +const mockYamlError = { + owner: { + repository: { + __typename: 'Repository', + commit: { + compareWithParent: { + __typename: 'FirstPullRequest', + message: 'First pull request', + }, + yamlErrors: { + edges: [{ node: { errorCode: 'invalid_yaml' } }], + }, + }, + }, + }, +} + const server = setupServer() const queryClient = new QueryClient({ defaultOptions: { @@ -124,6 +143,7 @@ interface SetupArgs { uploadState?: 'COMPLETE' | 'ERROR' multipleUploads?: boolean firstPullRequest?: boolean + hasYamlError?: boolean } describe('CommitCoverageDropdown', () => { @@ -137,6 +157,7 @@ describe('CommitCoverageDropdown', () => { uploadState = 'COMPLETE', multipleUploads = false, firstPullRequest = false, + hasYamlError = false, }: SetupArgs = {}) { const user = userEvent.setup() @@ -148,6 +169,8 @@ describe('CommitCoverageDropdown', () => { return HttpResponse.json({ data: mockComparisonError }) } else if (firstPullRequest) { return HttpResponse.json({ data: mockFirstPullRequest }) + } else if (hasYamlError) { + return HttpResponse.json({ data: mockYamlError }) } return HttpResponse.json({ @@ -365,6 +388,23 @@ describe('CommitCoverageDropdown', () => { }) }) + describe('there is a yaml error', () => { + it('renders the yaml error message', async () => { + setup({ hasYamlError: true }) + render( + +

Passed child

+
, + { wrapper } + ) + + const errorMsg = await screen.findByText( + /data unavailable due to invalid yaml/ + ) + expect(errorMsg).toBeInTheDocument() + }) + }) + describe('expanding the dropdown', () => { it('renders the passed children', async () => { const { user } = setup({ diff --git a/src/pages/CommitDetailPage/Dropdowns/CommitCoverageDropdown.tsx b/src/pages/CommitDetailPage/Dropdowns/CommitCoverageDropdown.tsx index af16c280ce..c15a3e7e83 100644 --- a/src/pages/CommitDetailPage/Dropdowns/CommitCoverageDropdown.tsx +++ b/src/pages/CommitDetailPage/Dropdowns/CommitCoverageDropdown.tsx @@ -20,6 +20,13 @@ const CoverageMessage: React.FC = () => { }) const comparison = data?.commit?.compareWithParent const uploadErrorCount = data?.uploadErrorCount + const invalidYamlError = data?.yamlErrors?.find( + (err) => err?.errorCode === 'invalid_yaml' + ) + + if (invalidYamlError) { + return <>data unavailable due to invalid yaml ⚠️ + } if (!!uploadErrorCount) { if (uploadErrorCount === 1) { diff --git a/src/services/commit/useCommitCoverageDropdownSummary.test.tsx b/src/services/commit/useCommitCoverageDropdownSummary.test.tsx index ad2a0586d3..81635252f9 100644 --- a/src/services/commit/useCommitCoverageDropdownSummary.test.tsx +++ b/src/services/commit/useCommitCoverageDropdownSummary.test.tsx @@ -138,6 +138,7 @@ describe('useCommitCoverageDropdownSummary', () => { }, }, }, + yamlErrors: [], } await waitFor(() => @@ -164,6 +165,7 @@ describe('useCommitCoverageDropdownSummary', () => { expect(result.current.data).toStrictEqual({ uploadErrorCount: 0, commit: null, + yamlErrors: [], }) ) }) diff --git a/src/services/commit/useCommitCoverageDropdownSummary.tsx b/src/services/commit/useCommitCoverageDropdownSummary.tsx index bf42d879a7..067fe0a0bd 100644 --- a/src/services/commit/useCommitCoverageDropdownSummary.tsx +++ b/src/services/commit/useCommitCoverageDropdownSummary.tsx @@ -44,6 +44,10 @@ const NodeSchema = z.object({ node: z.object({ state: z.nativeEnum(UploadStateEnum) }), }) +export const YamlErrorNodeSchema = z.object({ + node: z.object({ errorCode: z.string() }), +}) + const RepositorySchema = z.object({ __typename: z.literal('Repository'), commit: z @@ -54,11 +58,16 @@ const RepositorySchema = z.object({ edges: z.array(NodeSchema.nullable()), }) .nullish(), + yamlErrors: z + .object({ + edges: z.array(YamlErrorNodeSchema.nullable()), + }) + .nullish(), }) .nullable(), }) -const RequestSchema = z.object({ +export const RequestSchema = z.object({ owner: z .object({ repository: z.discriminatedUnion('__typename', [ @@ -115,6 +124,13 @@ query CommitDropdownSummary( message } } + yamlErrors: errors(errorType: YAML_ERROR){ + edges { + node { + errorCode + } + } + } } } ... on NotFoundError { @@ -202,9 +218,13 @@ export function useCommitCoverageDropdownSummary({ delete commit.uploads } + const yamlErrors = + mapEdges(data?.owner?.repository?.commit?.yamlErrors) || [] + return { uploadErrorCount, commit, + yamlErrors, } }), }) diff --git a/src/services/navigation/useNavLinks/useStaticNavLinks.js b/src/services/navigation/useNavLinks/useStaticNavLinks.js index bde98655fa..17f7d8e9fd 100644 --- a/src/services/navigation/useNavLinks/useStaticNavLinks.js +++ b/src/services/navigation/useNavLinks/useStaticNavLinks.js @@ -520,5 +520,12 @@ export function useStaticNavLinks() { isExternalLink: true, openNewTab: true, }, + yamlValidatorDocs: { + text: 'YAML validator', + path: () => + 'https://docs.codecov.com/docs/codecov-yaml#validate-your-repository-yaml', + isExternalLink: true, + openNewTab: true, + }, } } diff --git a/tailwind.config.mjs b/tailwind.config.mjs index 98c34205b3..24cf5ae413 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -88,6 +88,7 @@ const config = { }, }, codecov: { + red: withOpacity('--color-codecov-red'), orange: withOpacity('--color-codecov-orange'), footer: withOpacity('--color-codecov-footer'), code: withOpacity('--color-codecov-code'), From 8f4669e992a86b7076de44f818b780a7daa5e3d8 Mon Sep 17 00:00:00 2001 From: calvin-codecov Date: Mon, 4 Nov 2024 13:45:57 -0500 Subject: [PATCH 09/10] feat: Remove token from Other CI onboarding step 3 (#3452) --- src/pages/RepoPage/CoverageOnboarding/OtherCI/OtherCI.test.tsx | 2 +- src/pages/RepoPage/CoverageOnboarding/OtherCI/OtherCI.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/RepoPage/CoverageOnboarding/OtherCI/OtherCI.test.tsx b/src/pages/RepoPage/CoverageOnboarding/OtherCI/OtherCI.test.tsx index ebda26b116..8e06acb1c3 100644 --- a/src/pages/RepoPage/CoverageOnboarding/OtherCI/OtherCI.test.tsx +++ b/src/pages/RepoPage/CoverageOnboarding/OtherCI/OtherCI.test.tsx @@ -146,7 +146,7 @@ describe('OtherCI', () => { expect(codecovToken).toBeInTheDocument() const tokenValue = await screen.findAllByText(/repo-token-jkl;-7890/) - expect(tokenValue).toHaveLength(2) + expect(tokenValue).toHaveLength(1) }) }) }) diff --git a/src/pages/RepoPage/CoverageOnboarding/OtherCI/OtherCI.tsx b/src/pages/RepoPage/CoverageOnboarding/OtherCI/OtherCI.tsx index 2f826fa0a2..f80c16a66c 100644 --- a/src/pages/RepoPage/CoverageOnboarding/OtherCI/OtherCI.tsx +++ b/src/pages/RepoPage/CoverageOnboarding/OtherCI/OtherCI.tsx @@ -36,7 +36,7 @@ function OtherCI() { const tokenCopy = orgUploadToken ? 'global' : 'repository' const apiUrlCopy = config.IS_SELF_HOSTED ? ` -u ${config.API_URL}` : '' - const uploadCommand = `./codecov${apiUrlCopy} upload-process -t ${uploadToken}${ + const uploadCommand = `./codecov${apiUrlCopy} upload-process${ orgUploadToken ? ` -r ${repo}` : '' }` From 81806aef3887686bed4913668aa0fd2db0d7a643 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Mon, 4 Nov 2024 16:03:44 -0500 Subject: [PATCH 10/10] Release 24.11.1 (#3462) --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 46cc01256f..f677377056 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -24.10.1 \ No newline at end of file +24.11.1 \ No newline at end of file