diff --git a/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx b/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx index 1195ce16f1a42a..22ccb1d57d2902 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx @@ -1,6 +1,7 @@ import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; +import {useQuery} from '@tanstack/react-query'; import {Button} from '@sentry/scraps/button'; import {Grid} from '@sentry/scraps/layout'; @@ -20,7 +21,7 @@ import { OrderBy, sortedFlags, } from 'sentry/components/events/featureFlags/utils'; -import {useOrganizationFlagLog} from 'sentry/components/featureFlags/hooks/useOrganizationFlagLog'; +import {organizationFlagLogOptions} from 'sentry/components/featureFlags/hooks/useOrganizationFlagLog'; import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; import {useDrawer} from 'sentry/components/globalDrawer'; import {useLegacyEventSuspectFlags} from 'sentry/components/issues/suspect/useLegacyEventSuspectFlags'; @@ -77,14 +78,16 @@ function BaseEventFeatureFlagList({event, group, project}: EventFeatureFlagSecti const viewAllButtonRef = useRef(null); const eventView = useIssueDetailsEventView({group}); - const {data: rawFlagData} = useOrganizationFlagLog({ - organization, - query: { - start: eventView.start, - end: eventView.end, - statsPeriod: eventView.statsPeriod, - }, - }); + const {data: rawFlagData} = useQuery( + organizationFlagLogOptions({ + organization, + query: { + start: eventView.start, + end: eventView.end, + statsPeriod: eventView.statsPeriod, + }, + }) + ); const location = useLocation(); // issue list params we want to preserve in the search diff --git a/static/app/components/featureFlags/featureFlagsLogTable.tsx b/static/app/components/featureFlags/featureFlagsLogTable.tsx index 149bf005daf3c6..7519f8e0cab71c 100644 --- a/static/app/components/featureFlags/featureFlagsLogTable.tsx +++ b/static/app/components/featureFlags/featureFlagsLogTable.tsx @@ -8,7 +8,6 @@ import {GridEditable, type GridColumnOrder} from 'sentry/components/tables/gridE import {t} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; import {FIELD_FORMATTERS} from 'sentry/utils/discover/fieldRenderers'; -import type {RequestError} from 'sentry/utils/requestError/requestError'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -16,7 +15,7 @@ export type ColumnKey = 'provider' | 'flag' | 'action' | 'createdAt'; interface FeatureFlagsLogTableProps { columns: Array>; - error: RequestError | null; + error: Error | null; flags: RawFlag[]; isPending: boolean; pageLinks: string | null; diff --git a/static/app/components/featureFlags/hooks/useFlagsInEvent.tsx b/static/app/components/featureFlags/hooks/useFlagsInEvent.tsx index d58a367af868b9..e5f0ba66415907 100644 --- a/static/app/components/featureFlags/hooks/useFlagsInEvent.tsx +++ b/static/app/components/featureFlags/hooks/useFlagsInEvent.tsx @@ -1,13 +1,14 @@ +import {skipToken, useQuery} from '@tanstack/react-query'; + import {useFetchGroupAndEvent} from 'sentry/components/featureFlags/hooks/useFetchGroupAndEvent'; import { - useOrganizationFlagLog, + organizationFlagLogOptions, useOrganizationFlagLogInfinite, } from 'sentry/components/featureFlags/hooks/useOrganizationFlagLog'; import type {Event} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; import {defined} from 'sentry/utils'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {useApiQuery} from 'sentry/utils/queryClient'; +import {apiOptions, selectJsonWithHeaders} from 'sentry/utils/api/apiOptions'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useGroup} from 'sentry/views/issueDetails/useGroup'; @@ -48,23 +49,25 @@ export function useFlagsInEventPaginated({ }); const { - data: rawFlagData, - getResponseHeader, + data: rawFlagResp, isPending: isFlagsPending, isError: isFlagsError, error: flagsError, - } = useOrganizationFlagLog({ - organization, - query: { - ...query, - flag: eventFlags, - }, + } = useQuery({ + ...organizationFlagLogOptions({ + organization, + query: { + ...query, + flag: eventFlags, + }, + }), enabled: enabled && Boolean(eventFlags?.length), + select: selectJsonWithHeaders, }); - const pageLinks = getResponseHeader?.('Link') ?? null; + const pageLinks = rawFlagResp?.headers.Link ?? null; return { - flags: rawFlagData?.data ?? [], + flags: rawFlagResp?.json?.data ?? [], event, group, pageLinks, @@ -108,29 +111,27 @@ export function useFlagsInEvent({ const group = groupProp ?? groupData; const projectSlug = group?.project.slug; + const eventEnabled = + enabled && Boolean(eventId && projectSlug && organization.slug) && !eventProp; const { data: eventData, isPending: isEventPending, isError: isEventError, error: eventError, - } = useApiQuery( - [ - getApiUrl( - '/organizations/$organizationIdOrSlug/events/$projectIdOrSlug:$eventId/', - { - path: { - organizationIdOrSlug: organization.slug, - projectIdOrSlug: projectSlug!, - eventId: eventId!, - }, - } - ), - ], - { - staleTime: Infinity, - enabled: - enabled && Boolean(eventId && projectSlug && organization.slug) && !eventProp, - } + } = useQuery( + apiOptions.as()( + '/organizations/$organizationIdOrSlug/events/$projectIdOrSlug:$eventId/', + { + path: eventEnabled + ? { + organizationIdOrSlug: organization.slug, + projectIdOrSlug: projectSlug!, + eventId: eventId!, + } + : skipToken, + staleTime: Infinity, + } + ) ); const event = eventProp ?? eventData; diff --git a/static/app/components/featureFlags/hooks/useOrganizationFlagLog.tsx b/static/app/components/featureFlags/hooks/useOrganizationFlagLog.tsx index 64b772cb45ce40..dced0a64d4dce1 100644 --- a/static/app/components/featureFlags/hooks/useOrganizationFlagLog.tsx +++ b/static/app/components/featureFlags/hooks/useOrganizationFlagLog.tsx @@ -1,37 +1,25 @@ import {useEffect} from 'react'; +import {skipToken, useInfiniteQuery} from '@tanstack/react-query'; import type {RawFlagData} from 'sentry/components/featureFlags/utils'; import type {Organization} from 'sentry/types/organization'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {useApiQuery, useInfiniteApiQuery} from 'sentry/utils/queryClient'; +import {apiOptions} from 'sentry/utils/api/apiOptions'; interface Params { organization: Organization; - query: Record; - enabled?: boolean; + query: Record; } -export function useOrganizationFlagLog({ - organization, - query, - enabled: enabledParam = true, -}: Params) { +export function organizationFlagLogOptions({organization, query}: Params) { // Don't make the request if start = end. The backend returns 400 but we prefer an empty response. - const enabled = - (!query.start || !query.end || query.start !== query.end) && enabledParam; + const enabled = !query.start || !query.end || query.start !== query.end; - return useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/flags/logs/', { - path: { - organizationIdOrSlug: organization.slug, - }, - }), - {query}, - ], + return apiOptions.as()( + '/organizations/$organizationIdOrSlug/flags/logs/', { + path: enabled ? {organizationIdOrSlug: organization.slug} : skipToken, + query, staleTime: 0, - enabled, } ); } @@ -48,39 +36,42 @@ export function useOrganizationFlagLogInfinite({ query, enabled: enabledParam = true, maxPages = 10, -}: InfiniteParams) { +}: InfiniteParams & {enabled?: boolean}) { // Don't make the request if start = end. The backend returns 400 but we prefer an empty response. const enabled = (!query.start || !query.end || query.start !== query.end) && enabledParam; - const apiQuery = useInfiniteApiQuery({ - queryKey: [ - {infinite: true, version: 'v1'}, - getApiUrl('/organizations/$organizationIdOrSlug/flags/logs/', { - path: { - organizationIdOrSlug: organization.slug, - }, - }), - {query}, - ], - staleTime: 0, - enabled, + const { + data: infiniteData, + isFetching, + hasNextPage, + fetchNextPage, + isPending, + isError, + error, + } = useInfiniteQuery({ + ...apiOptions.asInfinite()( + '/organizations/$organizationIdOrSlug/flags/logs/', + { + path: enabled ? {organizationIdOrSlug: organization.slug} : skipToken, + query, + staleTime: 0, + } + ), }); - const currentNumberPages = apiQuery.data?.pages.length ?? 0; + const currentNumberPages = infiniteData?.pages.length ?? 0; useEffect(() => { - if ( - !apiQuery.isFetching && - apiQuery.hasNextPage && - currentNumberPages + 1 < maxPages - ) { - apiQuery.fetchNextPage(); + if (!isFetching && hasNextPage && currentNumberPages + 1 < maxPages) { + fetchNextPage(); } - }, [apiQuery, maxPages, currentNumberPages]); + }, [isFetching, hasNextPage, fetchNextPage, maxPages, currentNumberPages]); return { - ...apiQuery, - data: apiQuery.data?.pages.flatMap(([pageData]) => pageData.data), + data: infiniteData?.pages.flatMap(page => page.json.data), + isPending, + isError, + error, }; } diff --git a/static/app/components/issues/suspect/useLegacyEventSuspectFlags.tsx b/static/app/components/issues/suspect/useLegacyEventSuspectFlags.tsx index f94f5612a36579..701c7b0e4fd352 100644 --- a/static/app/components/issues/suspect/useLegacyEventSuspectFlags.tsx +++ b/static/app/components/issues/suspect/useLegacyEventSuspectFlags.tsx @@ -1,7 +1,9 @@ import {useEffect, useMemo} from 'react'; +import {useQuery} from '@tanstack/react-query'; import intersection from 'lodash/intersection'; import moment from 'moment-timezone'; +import {organizationFlagLogOptions} from 'sentry/components/featureFlags/hooks/useOrganizationFlagLog'; import { hydrateToFlagSeries, type RawFlag, @@ -11,9 +13,6 @@ import type {Event} from 'sentry/types/event'; import type {Organization} from 'sentry/types/organization'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient'; -import type {RequestError} from 'sentry/utils/requestError/requestError'; /** * Legacy suspect flags implementation. @@ -33,7 +32,13 @@ export function useLegacyEventSuspectFlags({ firstSeen: string; organization: Organization; rawFlagData: RawFlagData | undefined; -}): UseApiQueryResult & {suspectFlags: RawFlag[]} { +}): { + data: RawFlagData | undefined; + error: Error | null; + isError: boolean; + isPending: boolean; + suspectFlags: RawFlag[]; +} { const hydratedFlagData = hydrateToFlagSeries(rawFlagData?.data ?? []); // map flag data to arrays of flag names @@ -48,28 +53,19 @@ export function useLegacyEventSuspectFlags({ // get all the audit log flag changes which happened prior to the first seen date const start = moment(firstSeen).subtract(1, 'year').format('YYYY-MM-DD HH:mm:ss'); - const apiQueryResponse = useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/flags/logs/', { - path: {organizationIdOrSlug: organization.slug}, - }), - { - query: { - flag: intersectionFlags, - start, - end: firstSeen, - statsPeriod: undefined, - }, + const {data, isError, isPending, error} = useQuery({ + ...organizationFlagLogOptions({ + organization, + query: { + flag: intersectionFlags, + start, + end: firstSeen, + statsPeriod: undefined, }, - ], - { - staleTime: 0, - // if no intersection, then there are no suspect flags - enabled: enabled && Boolean(intersectionFlags.length), - } - ); - - const {data, isError, isPending} = apiQueryResponse; + }), + // if no intersection, then there are no suspect flags + enabled: enabled && Boolean(intersectionFlags.length), + }); // no flags in common between event evaluations and audit log // only track this analytic if there is at least 1 flag recorded @@ -131,5 +127,5 @@ export function useLegacyEventSuspectFlags({ organization, ]); - return {...apiQueryResponse, suspectFlags}; + return {data, isError, isPending, error, suspectFlags}; } diff --git a/static/app/views/issueDetails/groupFeatureFlags/details/flagDetailsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/details/flagDetailsDrawerContent.tsx index bcf69c130afcad..14681efe10deb7 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/details/flagDetailsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/details/flagDetailsDrawerContent.tsx @@ -1,5 +1,6 @@ import {Fragment, useEffect, useState} from 'react'; import styled from '@emotion/styled'; +import {useQuery} from '@tanstack/react-query'; import {LinkButton} from '@sentry/scraps/button'; import {Stack} from '@sentry/scraps/layout'; @@ -9,7 +10,7 @@ import {DateTime} from 'sentry/components/dateTime'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import {EmptyStateWarning} from 'sentry/components/emptyStateWarning'; import {makeFeatureFlagSearchKey} from 'sentry/components/events/featureFlags/utils'; -import {useOrganizationFlagLog} from 'sentry/components/featureFlags/hooks/useOrganizationFlagLog'; +import {organizationFlagLogOptions} from 'sentry/components/featureFlags/hooks/useOrganizationFlagLog'; import {getFlagActionLabel, type RawFlag} from 'sentry/components/featureFlags/utils'; import {LoadingError} from 'sentry/components/loadingError'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; @@ -18,6 +19,7 @@ import {IconArrow, IconEllipsis} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {Group} from 'sentry/types/group'; import {trackAnalytics} from 'sentry/utils/analytics'; +import {selectJsonWithHeaders} from 'sentry/utils/api/apiOptions'; import {useCopyToClipboard} from 'sentry/utils/useCopyToClipboard'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; @@ -44,40 +46,42 @@ export function FlagDetailsDrawerContent({group}: Props) { data: flagLog, isPending, isError, - getResponseHeader, - } = useOrganizationFlagLog({ - organization, - query: { - flag: tagKey, - per_page: 50, - queryReferrer: 'featureFlagDetailsDrawer', - sort: '-created_at', - cursor: location.query.flagDrawerCursor, - }, + } = useQuery({ + ...organizationFlagLogOptions({ + organization, + query: { + flag: tagKey, + per_page: 50, + queryReferrer: 'featureFlagDetailsDrawer', + sort: '-created_at', + cursor: location.query.flagDrawerCursor, + }, + }), + select: selectJsonWithHeaders, }); - const pageLinks = getResponseHeader?.('Link') ?? null; + const pageLinks = flagLog?.headers.Link ?? null; const analyticsArea = useAnalyticsArea(); useEffect(() => { if (!isPending && !isError) { trackAnalytics('flags.drawer_details_rendered', { organization, - numLogs: flagLog.data.length, + numLogs: flagLog.json.data.length, }); } - }, [organization, flagLog?.data.length, isPending, isError]); + }, [organization, flagLog?.json.data.length, isPending, isError]); if (isPending) { return ; } - if (isError) { + if (isError || !flagLog) { return ( ); } - if (!flagLog.data.length) { + if (!flagLog.json.data.length) { return ( @@ -109,14 +113,14 @@ export function FlagDetailsDrawerContent({group}: Props) { - {flagLog.data.map((flag, i) => { - const prev = flagLog.data[i - 1]; + {flagLog.json.data.map((flag, i) => { + const prev = flagLog.json.data[i - 1]; return ( {group.firstSeen > flag.createdAt && (i === 0 || - (flagLog.data && prev && prev.createdAt > group.firstSeen)) ? ( + (flagLog.json.data && prev && prev.createdAt > group.firstSeen)) ? ( ) : null} diff --git a/static/app/views/settings/featureFlags/changeTracking/organizationFeatureFlagsAuditLogTable.tsx b/static/app/views/settings/featureFlags/changeTracking/organizationFeatureFlagsAuditLogTable.tsx index 465fa83508b49f..8220d60d5c5588 100644 --- a/static/app/views/settings/featureFlags/changeTracking/organizationFeatureFlagsAuditLogTable.tsx +++ b/static/app/views/settings/featureFlags/changeTracking/organizationFeatureFlagsAuditLogTable.tsx @@ -1,12 +1,14 @@ import {Fragment, useCallback, useMemo, useState} from 'react'; +import {useQuery} from '@tanstack/react-query'; import type {ColumnKey} from 'sentry/components/featureFlags/featureFlagsLogTable'; import {FeatureFlagsLogTable} from 'sentry/components/featureFlags/featureFlagsLogTable'; -import {useOrganizationFlagLog} from 'sentry/components/featureFlags/hooks/useOrganizationFlagLog'; +import {organizationFlagLogOptions} from 'sentry/components/featureFlags/hooks/useOrganizationFlagLog'; import type {RawFlag} from 'sentry/components/featureFlags/utils'; import type {GridColumnOrder} from 'sentry/components/tables/gridEditable'; import {useQueryBasedColumnResize} from 'sentry/components/tables/gridEditable/useQueryBasedColumnResize'; import {t} from 'sentry/locale'; +import {selectJsonWithHeaders} from 'sentry/utils/api/apiOptions'; import {decodeScalar} from 'sentry/utils/queryString'; import {useLocationQuery} from 'sentry/utils/url/useLocationQuery'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -48,16 +50,15 @@ export function OrganizationFeatureFlagsAuditLogTable({ }; }, [locationQuery, pageSize]); - const { - data: flags, - isPending, - error, - getResponseHeader, - } = useOrganizationFlagLog({ - organization, - query, + const {data, isPending, error} = useQuery({ + ...organizationFlagLogOptions({ + organization, + query, + }), + select: selectJsonWithHeaders, }); - const pageLinks = getResponseHeader?.('Link') ?? null; + const flags = data?.json; + const pageLinks = data?.headers.Link ?? null; const [activeRowKey, setActiveRowKey] = useState();