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,
};
}