From 3132d36d70c7862ef067e06f06da867c7335e7d9 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki <44422760+DominikB2014@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:12:49 -0400 Subject: [PATCH] tests(insights): add cache landing tests (#72130) --- .../cache/cacheLandingPage.spec.tsx | 402 ++++++++++++++++++ .../performance/cache/cacheLandingPage.tsx | 18 +- 2 files changed, 411 insertions(+), 9 deletions(-) create mode 100644 static/app/views/performance/cache/cacheLandingPage.spec.tsx diff --git a/static/app/views/performance/cache/cacheLandingPage.spec.tsx b/static/app/views/performance/cache/cacheLandingPage.spec.tsx new file mode 100644 index 00000000000000..15f8b8d8fcd0c0 --- /dev/null +++ b/static/app/views/performance/cache/cacheLandingPage.spec.tsx @@ -0,0 +1,402 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ProjectFixture} from 'sentry-fixture/project'; + +import {render, screen, waitForElementToBeRemoved} from 'sentry-test/reactTestingLibrary'; + +import type {Organization} from 'sentry/types/organization'; +import {useLocation} from 'sentry/utils/useLocation'; +import usePageFilters from 'sentry/utils/usePageFilters'; +import useProjects from 'sentry/utils/useProjects'; +import {CacheLandingPage} from 'sentry/views/performance/cache/cacheLandingPage'; + +jest.mock('sentry/utils/useLocation'); +jest.mock('sentry/utils/usePageFilters'); +jest.mock('sentry/utils/useProjects'); + +const requestMocks = { + onboardingDataCheck: jest.fn(), + missRateChart: jest.fn(), + cacheSamplesMissRateChart: jest.fn(), + throughputChart: jest.fn(), + spanTransactionList: jest.fn(), + transactionDurations: jest.fn(), + spanFields: jest.fn(), +}; + +describe('CacheLandingPage', function () { + const organization = OrganizationFixture({features: ['performance-insights']}); + + jest.mocked(usePageFilters).mockReturnValue({ + isReady: true, + desyncedFilters: new Set(), + pinnedFilters: new Set(), + shouldPersist: true, + selection: { + datetime: { + period: '10d', + start: null, + end: null, + utc: false, + }, + environments: [], + projects: [], + }, + }); + + jest.mocked(useLocation).mockReturnValue({ + pathname: '', + search: '', + query: {statsPeriod: '10d', project: '1'}, + hash: '', + state: undefined, + action: 'PUSH', + key: '', + }); + + jest.mocked(useProjects).mockReturnValue({ + projects: [ + ProjectFixture({ + id: '1', + name: 'Backend', + slug: 'backend', + firstTransactionEvent: true, + platform: 'javascript', + }), + ], + onSearch: jest.fn(), + placeholders: [], + fetching: false, + hasMore: null, + fetchError: null, + initiallyLoaded: false, + }); + + beforeEach(function () { + jest.clearAllMocks(); + setRequestMocks(organization); + }); + + afterAll(function () { + jest.resetAllMocks(); + }); + + it('fetches module data', async function () { + render(, {organization}); + + await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator')); + + expect(requestMocks.missRateChart).toHaveBeenCalledWith( + `/organizations/${organization.slug}/events-stats/`, + expect.objectContaining({ + method: 'GET', + query: { + cursor: undefined, + dataset: 'spansMetrics', + environment: [], + excludeOther: 0, + field: [], + interval: '30m', + orderby: undefined, + partial: 1, + per_page: 50, + project: [], + query: 'span.op:[cache.get_item,cache.get] project.id:1', + referrer: 'api.performance.cache.samples-cache-hit-miss-chart', + statsPeriod: '10d', + topEvents: undefined, + yAxis: 'cache_miss_rate()', + }, + }) + ); + expect(requestMocks.throughputChart).toHaveBeenCalledWith( + `/organizations/${organization.slug}/events-stats/`, + expect.objectContaining({ + method: 'GET', + query: { + cursor: undefined, + dataset: 'spansMetrics', + environment: [], + excludeOther: 0, + field: [], + interval: '30m', + orderby: undefined, + partial: 1, + per_page: 50, + project: [], + query: 'span.op:[cache.get_item,cache.get]', + referrer: 'api.performance.cache.landing-cache-throughput-chart', + statsPeriod: '10d', + topEvents: undefined, + yAxis: 'spm()', + }, + }) + ); + expect(requestMocks.spanTransactionList).toHaveBeenCalledWith( + `/organizations/${organization.slug}/events/`, + expect.objectContaining({ + method: 'GET', + query: { + dataset: 'spansMetrics', + environment: [], + field: [ + 'project', + 'project.id', + 'transaction', + 'spm()', + 'cache_miss_rate()', + 'sum(span.self_time)', + 'time_spent_percentage()', + 'avg(cache.item_size)', + ], + per_page: 20, + project: [], + query: 'span.op:[cache.get_item,cache.get]', + referrer: 'api.performance.cache.landing-cache-transaction-list', + sort: '-time_spent_percentage()', + statsPeriod: '10d', + }, + }) + ); + expect(requestMocks.transactionDurations).toHaveBeenCalledWith( + `/organizations/${organization.slug}/events/`, + expect.objectContaining({ + method: 'GET', + query: { + dataset: 'metrics', + environment: [], + field: ['avg(transaction.duration)', 'transaction'], + per_page: 50, + project: [], + query: 'transaction:["my-transaction"]', + referrer: 'api.performance.cache.landing-cache-transaction-duration', + statsPeriod: '10d', + }, + }) + ); + }); + + it('renders a list of transactions', async function () { + render(, {organization}); + await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator')); + expect(screen.getByRole('columnheader', {name: 'Transaction'})).toBeInTheDocument(); + expect(screen.getByRole('cell', {name: 'my-transaction'})).toBeInTheDocument(); + expect(screen.getByRole('link', {name: 'my-transaction'})).toHaveAttribute( + 'href', + '/organizations/org-slug/insights/caches/?project=123&statsPeriod=10d&transaction=my-transaction' + ); + + expect(screen.getByRole('columnheader', {name: 'Project'})).toBeInTheDocument(); + expect(screen.getByRole('cell', {name: 'backend'})).toBeInTheDocument(); + expect(screen.getByRole('link', {name: 'backend'})).toHaveAttribute( + 'href', + '/organizations/org-slug/projects/backend/?project=1' + ); + + expect( + screen.getByRole('columnheader', {name: 'Avg Value Size'}) + ).toBeInTheDocument(); + expect(screen.getByRole('cell', {name: '123.0 B'})).toBeInTheDocument(); + + expect( + screen.getByRole('columnheader', {name: 'Requests Per Minute'}) + ).toBeInTheDocument(); + expect(screen.getByRole('cell', {name: '123/s'})).toBeInTheDocument(); + + expect( + screen.getByRole('columnheader', {name: 'Avg Transaction Duration'}) + ).toBeInTheDocument(); + const avgTxnCell = screen + .getAllByRole('cell') + .find(cell => cell?.textContent?.includes('456.00ms')); + expect(avgTxnCell).toBeInTheDocument(); + + expect(screen.getByRole('columnheader', {name: 'Miss Rate'})).toBeInTheDocument(); + expect(screen.getByRole('cell', {name: '12.3%'})).toBeInTheDocument(); + + expect(screen.getByRole('columnheader', {name: 'Time Spent'})).toBeInTheDocument(); + const timeSpentCell = screen + .getAllByRole('cell') + .find(cell => cell?.textContent?.includes('123.00ms')); + expect(timeSpentCell).toBeInTheDocument(); + }); + + it('shows module onboarding', async function () { + requestMocks.onboardingDataCheck = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + method: 'GET', + match: [ + MockApiClient.matchQuery({ + referrer: 'api.performance.cache.landing-cache-onboarding', + }), + ], + body: { + data: [{'count()': 0}], + meta: {fields: {'count()': 'integer'}}, + }, + }); + render(, {organization}); + + await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator')); + + expect( + screen.getByText('Start collecting Insights about your Caches!') + ).toBeInTheDocument(); + }); +}); + +const setRequestMocks = (organization: Organization) => { + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/projects/`, + body: [ProjectFixture({name: 'backend'})], + }); + + requestMocks.onboardingDataCheck = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + method: 'GET', + match: [ + MockApiClient.matchQuery({ + referrer: 'api.performance.cache.landing-cache-onboarding', + }), + ], + body: { + data: [{'count()': 43374}], + meta: { + fields: {'count()': 'integer'}, + }, + }, + }); + + requestMocks.missRateChart = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events-stats/`, + method: 'GET', + match: [ + MockApiClient.matchQuery({ + referrer: 'api.performance.cache.landing-cache-hit-miss-chart', + }), + ], + body: { + data: [ + [1716379200, [{count: 0.5}]], + [1716393600, [{count: 0.75}]], + ], + meta: { + fields: { + time: 'date', + cache_miss_rate: 'percentage', + }, + }, + }, + }); + + requestMocks.missRateChart = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events-stats/`, + method: 'GET', + match: [ + MockApiClient.matchQuery({ + referrer: 'api.performance.cache.samples-cache-hit-miss-chart', + }), + ], + body: { + data: [ + [1716379200, [{count: 0.5}]], + [1716393600, [{count: 0.75}]], + ], + meta: { + fields: { + time: 'date', + cache_miss_rate: 'percentage', + }, + }, + }, + }); + + requestMocks.throughputChart = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events-stats/`, + method: 'GET', + match: [ + MockApiClient.matchQuery({ + referrer: 'api.performance.cache.landing-cache-throughput-chart', + }), + ], + body: { + data: [ + [1716379200, [{count: 100}]], + [1716393600, [{count: 200}]], + ], + meta: { + fields: { + time: 'date', + spm_14400: 'rate', + }, + }, + }, + }); + + requestMocks.spanTransactionList = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + method: 'GET', + match: [ + MockApiClient.matchQuery({ + referrer: 'api.performance.cache.landing-cache-transaction-list', + }), + ], + body: { + data: [ + { + transaction: 'my-transaction', + project: 'backend', + 'project.id': 123, + 'avg(cache.item_size)': 123, + 'spm()': 123, + 'sum(span.self_time)': 123, + 'cache_miss_rate()': 0.123, + 'time_spent_percentage()': 0.123, + }, + ], + meta: { + fields: { + transaction: 'string', + project: 'string', + 'project.id': 'integer', + 'avg(cache.item_size)': 'number', + 'spm()': 'rate', + 'sum(span.self_time)': 'duration', + 'cache_miss_rate()': 'percentage', + 'time_spent_percentage()': 'percentage', + }, + units: {}, + }, + }, + }); + + requestMocks.transactionDurations = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + method: 'GET', + match: [ + MockApiClient.matchQuery({ + referrer: 'api.performance.cache.landing-cache-transaction-duration', + }), + ], + body: { + data: [ + { + transaction: 'my-transaction', + 'avg(transaction.duration)': 456, + }, + ], + meta: { + fields: { + transaction: 'string', + 'avg(transaction.duration)': 'duration', + }, + units: {}, + }, + }, + }); + + requestMocks.spanFields = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/spans/fields/`, + method: 'GET', + body: [], + }); +}; diff --git a/static/app/views/performance/cache/cacheLandingPage.tsx b/static/app/views/performance/cache/cacheLandingPage.tsx index bae8f19d704e34..4471b8fe75c66f 100644 --- a/static/app/views/performance/cache/cacheLandingPage.tsx +++ b/static/app/views/performance/cache/cacheLandingPage.tsx @@ -58,7 +58,7 @@ const {CACHE_ITEM_SIZE} = SpanMetricsField; const SDK_UPDATE_ALERT = ( {t( - `If you're noticing missing cache data, try updating to the latest SDK or ensure spans are manually instrumented with the right attributes. ` + `If you're noticing missing cache data, try updating to the latest SDK or ensure spans are manually instrumented with the right attributes. To learn more, ` )} {t('Read the Docs')} @@ -78,9 +78,9 @@ export function CacheLandingPage() { const cursor = decodeScalar(location.query?.[QueryParameterNames.TRANSACTIONS_CURSOR]); const { - isLoading: isCacheHitRateLoading, - data: cacheHitRateData, - error: cacheHitRateError, + isLoading: isCacheMissRateLoading, + data: cacheMissRateData, + error: cacheMissRateError, } = useSpanMetricsSeries( { yAxis: [`${CACHE_MISS_RATE}()`], @@ -149,7 +149,7 @@ export function CacheLandingPage() { useEffect(() => { const hasMissingDataError = - cacheHitRateError?.message === CACHE_ERROR_MESSAGE || + cacheMissRateError?.message === CACHE_ERROR_MESSAGE || transactionsListError?.message === CACHE_ERROR_MESSAGE; if (onboardingProject || isHasDataLoading || !hasData) { @@ -162,7 +162,7 @@ export function CacheLandingPage() { } } }, [ - cacheHitRateError?.message, + cacheMissRateError?.message, transactionsListError?.message, setPageInfo, hasData, @@ -228,10 +228,10 @@ export function CacheLandingPage() {