diff --git a/static/app/types/hooks.tsx b/static/app/types/hooks.tsx index ca22036f220e2b..ca951e6d25d9dc 100644 --- a/static/app/types/hooks.tsx +++ b/static/app/types/hooks.tsx @@ -8,6 +8,7 @@ import type {ProductSelectionProps} from 'sentry/components/onboarding/productSe import type DateRange from 'sentry/components/timeRangeSelector/dateRange'; import type SelectorItems from 'sentry/components/timeRangeSelector/selectorItems'; import type {SentryRouteObject} from 'sentry/router/types'; +import type {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; import type {WidgetType} from 'sentry/views/dashboards/types'; import type {OrganizationStatsProps} from 'sentry/views/organizationStats'; import type {RouteAnalyticsContext} from 'sentry/views/routeAnalyticsContextProvider'; @@ -331,6 +332,7 @@ type ReactHooks = { dataset: WidgetType; }) => number; 'react-hook:use-get-max-retention-days': () => number | undefined; + 'react-hook:use-max-pickable-days': typeof useMaxPickableDays; 'react-hook:use-metric-detector-limit': () => { detectorCount: number; detectorLimit: number; diff --git a/static/app/utils/useMaxPickableDays.spec.tsx b/static/app/utils/useMaxPickableDays.spec.tsx index 1f6cce4fae04af..915cf9ea1b9784 100644 --- a/static/app/utils/useMaxPickableDays.spec.tsx +++ b/static/app/utils/useMaxPickableDays.spec.tsx @@ -1,6 +1,6 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; -import {renderHook} from 'sentry-test/reactTestingLibrary'; +import {renderHookWithProviders} from 'sentry-test/reactTestingLibrary'; import {DataCategory} from 'sentry/types/core'; @@ -8,10 +8,9 @@ import {useMaxPickableDays} from './useMaxPickableDays'; describe('useMaxPickableDays', () => { it('returns 30/90 for spans without flag', () => { - const {result} = renderHook(() => + const {result} = renderHookWithProviders(() => useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization: OrganizationFixture({features: []}), }) ); @@ -23,11 +22,14 @@ describe('useMaxPickableDays', () => { }); it('returns 90/90 for spans with flag', () => { - const {result} = renderHook(() => - useMaxPickableDays({ - dataCategories: [DataCategory.SPANS], + const {result} = renderHookWithProviders( + () => + useMaxPickableDays({ + dataCategories: [DataCategory.SPANS], + }), + { organization: OrganizationFixture({features: ['visibility-explore-range-high']}), - }) + } ); expect(result.current).toEqual({ @@ -38,10 +40,9 @@ describe('useMaxPickableDays', () => { }); it('returns 30/30 days for tracemetrics', () => { - const {result} = renderHook(() => + const {result} = renderHookWithProviders(() => useMaxPickableDays({ dataCategories: [DataCategory.TRACE_METRICS], - organization: OrganizationFixture(), }) ); @@ -53,10 +54,9 @@ describe('useMaxPickableDays', () => { }); it('returns 30/30 days for logs', () => { - const {result} = renderHook(() => + const {result} = renderHookWithProviders(() => useMaxPickableDays({ dataCategories: [DataCategory.LOG_BYTE, DataCategory.LOG_ITEM], - organization: OrganizationFixture(), }) ); @@ -68,7 +68,7 @@ describe('useMaxPickableDays', () => { }); it('returns 30/90 for many without flag', () => { - const {result} = renderHook(() => + const {result} = renderHookWithProviders(() => useMaxPickableDays({ dataCategories: [ DataCategory.SPANS, @@ -77,7 +77,6 @@ describe('useMaxPickableDays', () => { DataCategory.LOG_BYTE, DataCategory.LOG_ITEM, ], - organization: OrganizationFixture(), }) ); diff --git a/static/app/utils/useMaxPickableDays.tsx b/static/app/utils/useMaxPickableDays.tsx index 08b57e3ffe7568..6753c1ce74db6c 100644 --- a/static/app/utils/useMaxPickableDays.tsx +++ b/static/app/utils/useMaxPickableDays.tsx @@ -3,8 +3,10 @@ import {useMemo, type ReactNode} from 'react'; import HookOrDefault from 'sentry/components/hookOrDefault'; import type {DatePageFilterProps} from 'sentry/components/organizations/datePageFilter'; import {t} from 'sentry/locale'; +import HookStore from 'sentry/stores/hookStore'; import {DataCategory} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; +import useOrganization from 'sentry/utils/useOrganization'; export interface MaxPickableDaysOptions { /** @@ -19,15 +21,20 @@ export interface MaxPickableDaysOptions { upsellFooter?: ReactNode; } -interface UseMaxPickableDaysProps { +export interface UseMaxPickableDaysProps { dataCategories: readonly [DataCategory, ...DataCategory[]]; - organization: Organization; } export function useMaxPickableDays({ dataCategories, - organization, }: UseMaxPickableDaysProps): MaxPickableDaysOptions { + const useMaxPickableDaysHook = + HookStore.get('react-hook:use-max-pickable-days')[0] ?? useMaxPickableDaysImpl; + return useMaxPickableDaysHook({dataCategories}); +} + +function useMaxPickableDaysImpl({dataCategories}: UseMaxPickableDaysProps) { + const organization = useOrganization(); return useMemo(() => { function getMaxPickableDaysFor(dataCategory: DataCategory) { return getMaxPickableDays(dataCategory, organization); @@ -37,7 +44,7 @@ export function useMaxPickableDays({ }, [dataCategories, organization]); } -function getBestMaxPickableDays( +export function getBestMaxPickableDays( dataCategories: readonly [DataCategory, ...DataCategory[]], getMaxPickableDaysFor: (dataCategory: DataCategory) => MaxPickableDaysOptions ) { @@ -69,7 +76,7 @@ function max( const DESCRIPTION = t('To query over longer time ranges, upgrade to Business'); -function getMaxPickableDays( +export function getMaxPickableDays( dataCategory: DataCategory, organization: Organization ): MaxPickableDaysOptions { @@ -84,7 +91,7 @@ function getMaxPickableDays( return { maxPickableDays, maxUpgradableDays: 90, - upsellFooter: , + upsellFooter: SpansUpsellFooter, }; } case DataCategory.TRACE_METRICS: @@ -106,3 +113,7 @@ const UpsellFooterHook = HookOrDefault({ hookName: 'component:header-date-page-filter-upsell-footer', defaultComponent: () => null, }); + +export const SpansUpsellFooter = ( + +); diff --git a/static/app/views/explore/logs/content.tsx b/static/app/views/explore/logs/content.tsx index 1b452b70e0c044..b7d24f2c9468bb 100644 --- a/static/app/views/explore/logs/content.tsx +++ b/static/app/views/explore/logs/content.tsx @@ -62,7 +62,6 @@ export default function LogsContent() { const organization = useOrganization(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.LOG_BYTE], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/app/views/explore/metrics/content.tsx b/static/app/views/explore/metrics/content.tsx index 011f3ed451858b..3e0b224fa6dcb3 100644 --- a/static/app/views/explore/metrics/content.tsx +++ b/static/app/views/explore/metrics/content.tsx @@ -57,7 +57,6 @@ export default function MetricsContent() { const onboardingProject = useOnboardingProject({property: 'hasTraceMetrics'}); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.TRACE_METRICS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); return ( diff --git a/static/app/views/explore/multiQueryMode/content.tsx b/static/app/views/explore/multiQueryMode/content.tsx index 8a7b8896fdc0bf..aa3674a0b806dc 100644 --- a/static/app/views/explore/multiQueryMode/content.tsx +++ b/static/app/views/explore/multiQueryMode/content.tsx @@ -212,10 +212,8 @@ function Content({datePageFilterProps}: ContentProps) { } export function MultiQueryModeContent() { - const organization = useOrganization(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/app/views/explore/spans/content.tsx b/static/app/views/explore/spans/content.tsx index 06a7cd8079f182..9c9af8a7e62765 100644 --- a/static/app/views/explore/spans/content.tsx +++ b/static/app/views/explore/spans/content.tsx @@ -44,7 +44,6 @@ export function ExploreContent() { const onboardingProject = useOnboardingProject(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/app/views/insights/agentModels/views/modelsLandingPage.tsx b/static/app/views/insights/agentModels/views/modelsLandingPage.tsx index 226a59af066d08..7f21b94cc81bcf 100644 --- a/static/app/views/insights/agentModels/views/modelsLandingPage.tsx +++ b/static/app/views/insights/agentModels/views/modelsLandingPage.tsx @@ -10,7 +10,6 @@ import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/c import {DataCategory} from 'sentry/types/core'; import {useDatePageFilterProps} from 'sentry/utils/useDatePageFilterProps'; import {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; -import useOrganization from 'sentry/utils/useOrganization'; import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext'; import {TraceItemDataset} from 'sentry/views/explore/types'; import {InsightsEnvironmentSelector} from 'sentry/views/insights/common/components/enviornmentSelector'; @@ -98,10 +97,8 @@ function AgentModelsLandingPage({datePageFilterProps}: AgentModelsLandingPagePro } function PageWithProviders() { - const organization = useOrganization(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/app/views/insights/agentTools/views/toolsLandingPage.tsx b/static/app/views/insights/agentTools/views/toolsLandingPage.tsx index 2d43612e5e3c2c..9efd0626ecc14b 100644 --- a/static/app/views/insights/agentTools/views/toolsLandingPage.tsx +++ b/static/app/views/insights/agentTools/views/toolsLandingPage.tsx @@ -10,7 +10,6 @@ import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/c import {DataCategory} from 'sentry/types/core'; import {useDatePageFilterProps} from 'sentry/utils/useDatePageFilterProps'; import {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; -import useOrganization from 'sentry/utils/useOrganization'; import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext'; import {TraceItemDataset} from 'sentry/views/explore/types'; import {InsightsEnvironmentSelector} from 'sentry/views/insights/common/components/enviornmentSelector'; @@ -94,10 +93,8 @@ function AgentToolsLandingPage({datePageFilterProps}: AgentToolsLandingPageProps } function PageWithProviders() { - const organization = useOrganization(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/app/views/insights/aiGenerations/views/overview.tsx b/static/app/views/insights/aiGenerations/views/overview.tsx index 81ee5c2f559c04..5931d0ca53aec1 100644 --- a/static/app/views/insights/aiGenerations/views/overview.tsx +++ b/static/app/views/insights/aiGenerations/views/overview.tsx @@ -261,10 +261,8 @@ function AIGenerationsPage({datePageFilterProps}: AIGenerationsPageProps) { } function PageWithProviders() { - const organization = useOrganization(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/app/views/insights/mcp-prompts/views/mcpPromptsLandingPage.tsx b/static/app/views/insights/mcp-prompts/views/mcpPromptsLandingPage.tsx index bf602c0a1df59e..816b9d4b3525c8 100644 --- a/static/app/views/insights/mcp-prompts/views/mcpPromptsLandingPage.tsx +++ b/static/app/views/insights/mcp-prompts/views/mcpPromptsLandingPage.tsx @@ -11,7 +11,6 @@ import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/c import {DataCategory} from 'sentry/types/core'; import {useDatePageFilterProps} from 'sentry/utils/useDatePageFilterProps'; import {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; -import useOrganization from 'sentry/utils/useOrganization'; import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext'; import {TraceItemDataset} from 'sentry/views/explore/types'; import {InsightsEnvironmentSelector} from 'sentry/views/insights/common/components/enviornmentSelector'; @@ -93,10 +92,8 @@ function McpPromptsLandingPage({datePageFilterProps}: McpPromptsLandingPageProps } function PageWithProviders() { - const organization = useOrganization(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/app/views/insights/mcp-resources/views/mcpResourcesLandingPage.tsx b/static/app/views/insights/mcp-resources/views/mcpResourcesLandingPage.tsx index 3b981a5b82041c..1f7c31d2cdceae 100644 --- a/static/app/views/insights/mcp-resources/views/mcpResourcesLandingPage.tsx +++ b/static/app/views/insights/mcp-resources/views/mcpResourcesLandingPage.tsx @@ -11,7 +11,6 @@ import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/c import {DataCategory} from 'sentry/types/core'; import {useDatePageFilterProps} from 'sentry/utils/useDatePageFilterProps'; import {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; -import useOrganization from 'sentry/utils/useOrganization'; import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext'; import {TraceItemDataset} from 'sentry/views/explore/types'; import {InsightsEnvironmentSelector} from 'sentry/views/insights/common/components/enviornmentSelector'; @@ -93,10 +92,8 @@ function McpResourcesLandingPage({datePageFilterProps}: McpResourcesLandingPageP } function PageWithProviders() { - const organization = useOrganization(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/app/views/insights/mcp-tools/views/mcpToolsLandingPage.tsx b/static/app/views/insights/mcp-tools/views/mcpToolsLandingPage.tsx index 0e0db946e954c6..4f7b70f7cf7564 100644 --- a/static/app/views/insights/mcp-tools/views/mcpToolsLandingPage.tsx +++ b/static/app/views/insights/mcp-tools/views/mcpToolsLandingPage.tsx @@ -11,7 +11,6 @@ import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/c import {DataCategory} from 'sentry/types/core'; import {useDatePageFilterProps} from 'sentry/utils/useDatePageFilterProps'; import {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; -import useOrganization from 'sentry/utils/useOrganization'; import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext'; import {TraceItemDataset} from 'sentry/views/explore/types'; import {InsightsEnvironmentSelector} from 'sentry/views/insights/common/components/enviornmentSelector'; @@ -93,10 +92,8 @@ function McpToolsLandingPage({datePageFilterProps}: McpToolsLandingPageProps) { } function PageWithProviders() { - const organization = useOrganization(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/app/views/insights/pages/agents/overview.tsx b/static/app/views/insights/pages/agents/overview.tsx index 1e271c2db6a0d3..56013f10b72552 100644 --- a/static/app/views/insights/pages/agents/overview.tsx +++ b/static/app/views/insights/pages/agents/overview.tsx @@ -172,10 +172,8 @@ function AgentsOverviewPage({datePageFilterProps}: AgentsOverviewPageProps) { } function PageWithProviders() { - const organization = useOrganization(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/app/views/insights/pages/mcp/overview.tsx b/static/app/views/insights/pages/mcp/overview.tsx index 94a9a266bc2316..725432a4ad1df7 100644 --- a/static/app/views/insights/pages/mcp/overview.tsx +++ b/static/app/views/insights/pages/mcp/overview.tsx @@ -124,10 +124,8 @@ function McpOverviewPage({datePageFilterProps}: McpOverviewPageProps) { } function PageWithProviders() { - const organization = useOrganization(); const maxPickableDays = useMaxPickableDays({ dataCategories: [DataCategory.SPANS], - organization, }); const datePageFilterProps = useDatePageFilterProps(maxPickableDays); diff --git a/static/gsApp/hooks/useMaxPickableDays.spec.tsx b/static/gsApp/hooks/useMaxPickableDays.spec.tsx new file mode 100644 index 00000000000000..211b661b6a7121 --- /dev/null +++ b/static/gsApp/hooks/useMaxPickableDays.spec.tsx @@ -0,0 +1,209 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; + +import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription'; +import {renderHookWithProviders} from 'sentry-test/reactTestingLibrary'; + +import {DataCategory} from 'sentry/types/core'; + +import SubscriptionStore from 'getsentry/stores/subscriptionStore'; + +import {useMaxPickableDays} from './useMaxPickableDays'; + +describe('useMaxPickableDays', () => { + describe('without downsampled-date-page-filter', () => { + it('returns 30/90 for spans without flag', () => { + const {result} = renderHookWithProviders(() => + useMaxPickableDays({ + dataCategories: [DataCategory.SPANS], + }) + ); + + expect(result.current).toEqual({ + maxPickableDays: 30, + maxUpgradableDays: 90, + upsellFooter: expect.any(Object), + }); + }); + + it('returns 90/90 for spans with flag', () => { + const {result} = renderHookWithProviders( + () => + useMaxPickableDays({ + dataCategories: [DataCategory.SPANS], + }), + { + organization: OrganizationFixture({ + features: ['visibility-explore-range-high'], + }), + } + ); + + expect(result.current).toEqual({ + maxPickableDays: 90, + maxUpgradableDays: 90, + upsellFooter: expect.any(Object), + }); + }); + + it('returns 30/30 days for tracemetrics', () => { + const {result} = renderHookWithProviders(() => + useMaxPickableDays({ + dataCategories: [DataCategory.TRACE_METRICS], + }) + ); + + expect(result.current).toEqual({ + defaultPeriod: '24h', + maxPickableDays: 30, + maxUpgradableDays: 30, + }); + }); + + it('returns 30/30 days for logs', () => { + const {result} = renderHookWithProviders(() => + useMaxPickableDays({ + dataCategories: [DataCategory.LOG_BYTE, DataCategory.LOG_ITEM], + }) + ); + + expect(result.current).toEqual({ + defaultPeriod: '24h', + maxPickableDays: 30, + maxUpgradableDays: 30, + }); + }); + + it('returns 30/90 for many without flag', () => { + const {result} = renderHookWithProviders(() => + useMaxPickableDays({ + dataCategories: [ + DataCategory.SPANS, + DataCategory.SPANS_INDEXED, + DataCategory.TRACE_METRICS, + DataCategory.LOG_BYTE, + DataCategory.LOG_ITEM, + ], + }) + ); + + expect(result.current).toEqual({ + maxPickableDays: 30, + maxUpgradableDays: 90, + upsellFooter: expect.any(Object), + }); + }); + }); + + describe('with downsampled-date-page-filter', () => { + const organization = OrganizationFixture({ + features: ['downsampled-date-page-filter'], + }); + + const subscription = SubscriptionFixture({ + organization, + effectiveRetentions: { + span: { + standard: 90, + downsampled: 396, + }, + }, + }); + + beforeEach(() => { + SubscriptionStore.set(organization.slug, subscription); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('returns 127/127 for spans on 2025/12/31', () => { + jest.useFakeTimers().setSystemTime(new Date(2025, 11, 31)); + const {result} = renderHookWithProviders( + () => + useMaxPickableDays({ + dataCategories: [DataCategory.SPANS], + }), + {organization} + ); + + expect(result.current).toEqual({ + maxPickableDays: 127, + maxUpgradableDays: 127, + upsellFooter: expect.any(Object), + }); + }); + + it('returns 396/396 for spans on 2027/01/01', () => { + jest.useFakeTimers().setSystemTime(new Date(2027, 0, 1)); + const {result} = renderHookWithProviders( + () => + useMaxPickableDays({ + dataCategories: [DataCategory.SPANS], + }), + {organization} + ); + + expect(result.current).toEqual({ + maxPickableDays: 396, + maxUpgradableDays: 396, + upsellFooter: expect.any(Object), + }); + }); + + it('returns 30/30 days for tracemetrics', () => { + const {result} = renderHookWithProviders( + () => + useMaxPickableDays({ + dataCategories: [DataCategory.TRACE_METRICS], + }), + {organization} + ); + + expect(result.current).toEqual({ + defaultPeriod: '24h', + maxPickableDays: 30, + maxUpgradableDays: 30, + }); + }); + + it('returns 30/30 days for logs', () => { + const {result} = renderHookWithProviders( + () => + useMaxPickableDays({ + dataCategories: [DataCategory.LOG_BYTE, DataCategory.LOG_ITEM], + }), + {organization} + ); + + expect(result.current).toEqual({ + defaultPeriod: '24h', + maxPickableDays: 30, + maxUpgradableDays: 30, + }); + }); + + it('returns 396/396 for many without flag', () => { + jest.useFakeTimers().setSystemTime(new Date(2027, 0, 1)); + const {result} = renderHookWithProviders( + () => + useMaxPickableDays({ + dataCategories: [ + DataCategory.SPANS, + DataCategory.SPANS_INDEXED, + DataCategory.TRACE_METRICS, + DataCategory.LOG_BYTE, + DataCategory.LOG_ITEM, + ], + }), + {organization} + ); + + expect(result.current).toEqual({ + maxPickableDays: 396, + maxUpgradableDays: 396, + upsellFooter: expect.any(Object), + }); + }); + }); +}); diff --git a/static/gsApp/hooks/useMaxPickableDays.tsx b/static/gsApp/hooks/useMaxPickableDays.tsx new file mode 100644 index 00000000000000..754a93fdc1a030 --- /dev/null +++ b/static/gsApp/hooks/useMaxPickableDays.tsx @@ -0,0 +1,111 @@ +import {useMemo} from 'react'; +import moment from 'moment-timezone'; + +import {DataCategory} from 'sentry/types/core'; +import {defined} from 'sentry/utils'; +import { + getBestMaxPickableDays, + getMaxPickableDays, + SpansUpsellFooter, + type MaxPickableDaysOptions, + type UseMaxPickableDaysProps, +} from 'sentry/utils/useMaxPickableDays'; +import useOrganization from 'sentry/utils/useOrganization'; + +import type {Subscription} from 'getsentry/types'; + +import useSubscription from './useSubscription'; + +export function useMaxPickableDays({ + dataCategories, +}: UseMaxPickableDaysProps): MaxPickableDaysOptions { + const organization = useOrganization(); + const subscription = useSubscription(); + + return useMemo(() => { + function getMaxPickableDaysFor(dataCategory: DataCategory) { + if (organization.features.includes('downsampled-date-page-filter')) { + const maxPickableDays = getMaxPickableDaysByScription(dataCategory, subscription); + if (defined(maxPickableDays)) { + return maxPickableDays; + } + } + return getMaxPickableDays(dataCategory, organization); + } + + return getBestMaxPickableDays(dataCategories, getMaxPickableDaysFor); + }, [dataCategories, organization, subscription]); +} + +function getMaxPickableDaysByScription( + dataCategory: DataCategory, + subscription: Subscription | null +): MaxPickableDaysOptions | undefined { + switch (dataCategory) { + case DataCategory.SPANS: + case DataCategory.SPANS_INDEXED: { + // first day we started 13 months downsampled retention + const firstAvailableDate = moment('2025-08-26'); + const now = moment(); + const elapsedDays = Math.max(0, Math.round(now.diff(firstAvailableDate, 'days'))); + + const maxPickableDays = Math.min( + elapsedDays, // only allow back up to the first available day + Math.max( + ...[ + 30, // default 30 days retention + subscription?.effectiveRetentions?.span?.standard, + subscription?.effectiveRetentions?.span?.downsampled, + ].filter(defined) + ) + ); + + return { + maxPickableDays, + maxUpgradableDays: Math.max( + 90, // use 90 days as a placeholder, business plans get 13 months downsampled retention + Math.min(maxPickableDays, elapsedDays) + ), + upsellFooter: SpansUpsellFooter, + }; + } + case DataCategory.PROFILE_CHUNKS: + case DataCategory.PROFILE_CHUNKS_UI: + case DataCategory.PROFILE_DURATION: + case DataCategory.PROFILE_DURATION_UI: { + return { + maxPickableDays: 30, + maxUpgradableDays: 30, + defaultPeriod: '24h', + }; + } + case DataCategory.TRACE_METRICS: { + // TODO: undecided for now, fixed at 30 days + return { + maxPickableDays: 30, + maxUpgradableDays: 30, + defaultPeriod: '24h', + }; + } + case DataCategory.LOG_BYTE: + case DataCategory.LOG_ITEM: { + const maxPickableDays = Math.max( + ...[ + 30, // default 30 day retention + subscription?.effectiveRetentions?.log?.standard, + subscription?.effectiveRetentions?.log?.downsampled, + ].filter(defined) + ); + return { + maxPickableDays, + maxUpgradableDays: Math.max( + 30, // use 30 as a placeholder, all plans get 30 days retention + maxPickableDays + ), + defaultPeriod: '24h', + }; + } + default: + return undefined; + } +} diff --git a/static/gsApp/registerHooks.tsx b/static/gsApp/registerHooks.tsx index 83334b3c1bd919..104457ebcfbdb1 100644 --- a/static/gsApp/registerHooks.tsx +++ b/static/gsApp/registerHooks.tsx @@ -82,6 +82,7 @@ import ReplayOnboardingAlert from './components/replayOnboardingAlert'; import ReplaySettingsAlert from './components/replaySettingsAlert'; import useButtonTracking from './hooks/useButtonTracking'; import useGetMaxRetentionDays from './hooks/useGetMaxRetentionDays'; +import {useMaxPickableDays} from './hooks/useMaxPickableDays'; import useRouteActivatedHook from './hooks/useRouteActivatedHook'; const PartnershipAgreement = lazy(() => import('getsentry/views/partnershipAgreement')); @@ -232,6 +233,7 @@ const GETSENTRY_HOOKS: Partial = { 'component:crons-list-page-header': () => CronsBillingBanner, 'react-hook:route-activated': useRouteActivatedHook, 'react-hook:use-button-tracking': useButtonTracking, + 'react-hook:use-max-pickable-days': useMaxPickableDays, 'react-hook:use-get-max-retention-days': useGetMaxRetentionDays, 'react-hook:use-metric-detector-limit': useMetricDetectorLimit, 'react-hook:use-dashboard-dataset-retention-limit': useDashboardDatasetRetentionLimit, diff --git a/static/gsApp/types/index.tsx b/static/gsApp/types/index.tsx index ac91933379355d..31429dc54e30a5 100644 --- a/static/gsApp/types/index.tsx +++ b/static/gsApp/types/index.tsx @@ -337,6 +337,15 @@ export type Subscription = { dataRetention: string | null; // Event details dateJoined: string; + effectiveRetentions: Partial< + Record< + 'span' | 'log' | 'traceMetric', + { + downsampled: number; + standard: number; + } + > + >; // GDPR Info gdprDetails: GDPRDetails | null; gracePeriodEnd: string | null; diff --git a/tests/js/getsentry-test/fixtures/subscription.ts b/tests/js/getsentry-test/fixtures/subscription.ts index dc3bb92a77af3d..ed7c3b77c24a31 100644 --- a/tests/js/getsentry-test/fixtures/subscription.ts +++ b/tests/js/getsentry-test/fixtures/subscription.ts @@ -255,6 +255,7 @@ export function SubscriptionFixture(props: Props): TSubscription { }), }), }, + effectiveRetentions: {}, ...planData, }; }