Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {useOrganization} from 'sentry/utils/useOrganization';
import {useProjectFromId} from 'sentry/utils/useProjectFromId';
import {makeReplaysPathname} from 'sentry/views/explore/replays/pathnames';
import type {ReplayListRecord} from 'sentry/views/explore/replays/types';
import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData';
import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/types';

interface Props {
onClick: () => void;
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/replays/replayBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as events from 'sentry/utils/events';
import {useReplayPrefs} from 'sentry/utils/replays/playback/providers/replayPreferencesContext';
import {useProjectFromId} from 'sentry/utils/useProjectFromId';
import type {ReplayListRecord} from 'sentry/views/explore/replays/types';
import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData';
import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/types';

interface Props {
replay: ReplayListRecord | ReplayListRecordWithTx;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import type {
ReplayListRecord,
ReplayRecordNestedFieldName,
} from 'sentry/views/explore/replays/types';
import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData';
import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/types';

type ListRecord = ReplayListRecord | ReplayListRecordWithTx;

Expand Down
83 changes: 64 additions & 19 deletions static/app/utils/replayCount/useReplayCountForTransaction.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {useMemo} from 'react';

import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {useReplayExists} from 'sentry/utils/replayCount/useReplayExists';
import {useReplays} from 'sentry/utils/replays/hooks/useReplays';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useSpans} from 'sentry/views/insights/common/queries/useDiscover';

Expand All @@ -17,23 +20,54 @@ export function useReplayCountForTransaction({
const {selection} = usePageFilters();
const {replaysExist} = useReplayExists();

const search = new MutableSearch('!replayId:"" is_transaction:true');
search.addFilterValue('transaction', transaction);
// 1. Segment names approach — query replays directly
const replaysQuery = useMemo(() => {
const s = new MutableSearch('');
s.addFilterValue('segment_names', transaction);
return s;
}, [transaction]);

const {data: replaysData, isPending: isReplaysPending} = useReplays({
fields: ['id'],
limit: limit + 1,
projects: selection.projects,
query: replaysQuery,
queryReferrer: 'useReplayCountForTransaction',
sort: '-started_at',
statsPeriod,
});
Comment thread
cursor[bot] marked this conversation as resolved.

const replayIdsFromReplaysSearch = useMemo(() => {
const rows = (replaysData?.data ?? []) as Array<{id: string}>;
const ids = rows.map(r => String(r.id)).filter(Boolean);
return new Set(ids);
}, [replaysData]);

const segmentNamesSufficient =
!isReplaysPending && replayIdsFromReplaysSearch.size > limit;

// 2. Spans-based fallback. Only fires once segment_names has resolved and
// came up short, so a fully-covered transaction never issues this query.
const spansSearch = new MutableSearch('!replayId:"" is_transaction:true');
spansSearch.addFilterValue('transaction', transaction);
if (replayIdsFromReplaysSearch.size > 0) {
spansSearch.addFilterValue(
'!replayId',
`[${[...replayIdsFromReplaysSearch].join(',')}]`,
false
);
}

const {data, isPending} = useSpans(
const {data: spansData, isPending: isSpansPending} = useSpans(
{
search,
// Note that this has to be `replayId` and not `replay.id` - only
// `replayId` holds sampled replays, while `replay.id` currently also
// holds the ID of Replays that were active but not sampled.
// See REPLAY-893.
search: spansSearch,
fields: ['replayId', 'timestamp'],
sorts: [{field: 'timestamp', kind: 'desc'}],
// Over-fetch so we can still distinguish "limit+" from an exact count
// when some candidate IDs don't exist in the replays dataset.
limit: limit * 2,
enabled: !isReplaysPending && !segmentNamesSufficient,
pageFilters: {
...selection,
environments: [],
datetime: {
period: statsPeriod,
start: null,
Expand All @@ -45,20 +79,31 @@ export function useReplayCountForTransaction({
'api.performance.transaction-summary.replay-count'
);

if (isPending) {
if (isReplaysPending) {
return undefined;
}

const candidateIds = Array.from(
new Set(data.map(row => String(row.replayId)).filter(Boolean))
);
if (candidateIds.length === 0) {
return 0;
if (segmentNamesSufficient) {
return replayIdsFromReplaysSearch.size;
}

// Fallback is enabled but hasn't resolved yet.
if (isSpansPending) {
return undefined;
}

const newCandidateIds = [
...new Set(spansData.map(row => String(row.replayId)).filter(Boolean)),
];
if (newCandidateIds.length === 0) {
return replayIdsFromReplaysSearch.size;
}

const existence = replaysExist(candidateIds);
if (Object.keys(existence).length !== candidateIds.length) {
const existence = replaysExist(newCandidateIds);
if (Object.keys(existence).length !== newCandidateIds.length) {
// Existence check is still running.
return undefined;
}
Comment thread
mjq marked this conversation as resolved.
return Object.values(existence).filter(Boolean).length;
const additionalCount = Object.values(existence).filter(Boolean).length;
return Math.min(replayIdsFromReplaysSearch.size + additionalCount, limit + 1);
}
43 changes: 43 additions & 0 deletions static/app/utils/replays/hooks/useReplays.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {useQuery} from '@tanstack/react-query';

import {apiOptions} from 'sentry/utils/api/apiOptions';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useOrganization} from 'sentry/utils/useOrganization';

interface Options {
fields: string[];
limit: number;
projects: number[];
query: MutableSearch;
queryReferrer: string;
sort: string;
statsPeriod: string;
}

export function useReplays({
fields,
limit,
projects,
query,
queryReferrer,
sort,
statsPeriod,
}: Options) {
const organization = useOrganization();

return useQuery(
apiOptions.as<{data: unknown[]}>()('/organizations/$organizationIdOrSlug/replays/', {
path: {organizationIdOrSlug: organization.slug},
query: {
field: fields,
per_page: limit,
project: projects,
sort,
statsPeriod,
query: query.formatString(),
queryReferrer,
},
staleTime: 0,
})
);
}
15 changes: 0 additions & 15 deletions static/app/views/explore/replays/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,21 +185,6 @@ export type ReplayRecordNestedFieldName =
| `os.${keyof ReplayRecord['os']}`
| `user.${keyof ReplayRecord['user']}`;

export type ReplayListLocationQuery = {
cursor?: string;
end?: string;
environment?: string[];
field?: string[];
limit?: string;
offset?: string;
project?: string[];
query?: string;
sort?: string;
start?: string;
statsPeriod?: string;
utc?: 'true' | 'false';
};

export type ReplayListQueryReferrer =
| 'replayList'
| 'issueReplays'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ describe('Performance > Transaction Summary Header', () => {
url: '/organizations/org-slug/replay-count/',
body: {},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/replays/',
body: {data: []},
});
});

it('should render', async () => {
Expand Down
Loading
Loading