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() {