Skip to content
Open
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
3 changes: 2 additions & 1 deletion static/app/components/profiling/exportProfileButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export function ExportProfileButton(props: ExportProfileButtonProps) {
const api = useApi();
const organization = useOrganization();

const project = useProjects().projects.find(p => {
const {projects} = useProjects({slugs: props.projectId ? [props.projectId] : []});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this one should just be empty if no projectId is set

const project = projects.find(p => {
return p.slug === props.projectId;
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import {ProjectFixture} from 'sentry-fixture/project';

import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen} from 'sentry-test/reactTestingLibrary';

import {ProfilingBreadcrumbs} from 'sentry/components/profiling/profilingBreadcrumbs';
import ProjectsStore from 'sentry/stores/projectsStore';

describe('Breadcrumb', () => {
beforeEach(() => {
ProjectsStore.loadInitialData([ProjectFixture({slug: 'bar'})]);
});

it('renders the profiling link', () => {
const {organization} = initializeOrg();
render(
Expand Down
9 changes: 8 additions & 1 deletion static/app/components/profiling/profilingBreadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ export interface ProfilingBreadcrumbsProps {
}

function ProfilingBreadcrumbs({organization, trails}: ProfilingBreadcrumbsProps) {
const {projects} = useProjects();
// Extract project slugs from trails that have them
const projectSlugs = trails
.filter(
(trail): trail is ProfileSummaryTrail | FlamegraphTrail =>
trail.type === 'profile summary' || trail.type === 'flamechart'
)
.map(trail => trail.payload.projectSlug);
const {projects} = useProjects({slugs: projectSlugs});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theres gotta be a better way to do breadcrumbs than pulling in projects like this - might need to just look at the payload. if its got slug, it might have name?

const crumbs = useMemo(
() => trails.map(trail => trailToCrumb(trail, {organization, projects})),
[organization, trails, projects]
Expand Down
2 changes: 1 addition & 1 deletion static/app/utils/discover/teamKeyTransactionField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export default function TeamKeyTransactionFieldWrapper({
transactionName,
...props
}: WrapperProps) {
const {projects} = useProjects();
const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to the profiling one, project should be empty if no projectId? thus no projects defined?

const project = projects.find(proj => proj.slug === projectSlug);

// All these fields need to be defined in order to toggle a team key
Expand Down
19 changes: 17 additions & 2 deletions static/app/utils/useProjects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,25 @@ async function fetchProjects(
* This hook also provides a way to select specific project slugs, and search
* (type-ahead) for more projects that may not be in the project store.
*
* NOTE: Currently ALL projects are always loaded, but this hook is designed
* for future-compat in a world where we do _not_ load all projects.
* DEPRECATED USAGE: Calling useProjects() without the `slugs` parameter is
* deprecated. This pattern assumes all projects are loaded in the store,
* which will not be true once we remove the all_projects=1 bootstrap fetch.
*
* RECOMMENDED: Always pass specific slugs you need:
* const {projects} = useProjects({slugs: [group.project.slug]});
*
* This ensures projects are fetched on-demand if not already in the store.
*/
function useProjects({limit, slugs, orgId: propOrgId}: Options = {}) {
if (process.env.NODE_ENV !== 'production' && !slugs) {
// eslint-disable-next-line no-console
console.warn(
'useProjects() called without slugs parameter. ' +
'This usage is deprecated and may break when all_projects=1 is removed. ' +
'Pass specific slugs to ensure projects are fetched on-demand.'
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Deprecation warning fires repeatedly on every render

The console.warn for deprecation is executed directly in the function body rather than inside a useEffect or with a ref guard. This means the warning will fire on every single re-render of any component calling useProjects() without slugs, spamming the console with repeated warnings. The warning needs to be deduplicated, typically by using a ref to track whether it has already warned, or by placing it in a useEffect with empty dependencies.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine


const api = useApi();

const organization = useOrganization({allowNull: true});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function AlertRuleDetails({params, location, router}: AlertRuleDetailsProps) {
const queryClient = useQueryClient();
const organization = useOrganization();
const api = useApi();
const {projects, fetching: projectIsLoading} = useProjects();
const {projects, fetching: projectIsLoading} = useProjects({slugs: [params.projectId]});
const project = projects.find(({slug}) => slug === params.projectId);
const {projectId: projectSlug, ruleId} = params;
const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const STARRED_SEGMENT_TABLE_QUERY_KEY = ['starred-segment-table'];

export function StarredSegmentCell({segmentName, isStarred, projectSlug}: Props) {
const queryClient = useQueryClient();
const {projects} = useProjects();
const {projects} = useProjects({slugs: [projectSlug]});
const project = projects.find(p => p.slug === projectSlug);

const {setStarredSegment, isPending} = useStarredSegment({
Expand Down
4 changes: 2 additions & 2 deletions static/app/views/insights/pages/transactionCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ interface Props {
}

export function TransactionCell({project, transaction, transactionMethod}: Props) {
const projects = useProjects();
const {projects} = useProjects({slugs: project ? [project] : []});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: is project optional here becauase its not always present in the cells? does that mean we only need to look up project when its present similar to the other concerns with props above?

const organization = useOrganization();
const location = useLocation();
const {view} = useDomainViewFilters();

const projectId = projects.projects.find(p => p.slug === project)?.id;
const projectId = projects.find(p => p.slug === project)?.id;

const searchQuery = new MutableSearch('');
if (transactionMethod) {
Expand Down
4 changes: 3 additions & 1 deletion static/app/views/issueDetails/groupDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ function useFetchGroupDetails(): FetchGroupDetailsState {
const navigate = useNavigate();
const defaultIssueEvent = useDefaultIssueEvent();
const hasStreamlinedUI = useHasStreamlinedUI();
const {projects} = useProjects();

const [allProjectChanged, setAllProjectChanged] = useState<boolean>(false);

Expand All @@ -261,6 +260,9 @@ function useFetchGroupDetails(): FetchGroupDetailsState {
refetch: refetchGroupCall,
} = useGroup({groupId});

const groupProjectSlug = groupData?.project?.slug;
const {projects} = useProjects({slugs: groupProjectSlug ? [groupProjectSlug] : []});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if project slug isnt found we should hard error on the group - it means the project's missing/deleted/similar


/**
* TODO(streamline-ui): Remove this whole hook once the legacy UI is removed. The streamlined UI exposes the
* filters on the page so the user is expected to clear it themselves, and the empty state is actually expected.
Expand Down
9 changes: 7 additions & 2 deletions static/app/views/issueDetails/groupDistributionsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ type Props = {
*/
export function GroupDistributionsDrawer({group, includeFeatureFlagsTab}: Props) {
const organization = useOrganization();
const {projects} = useProjects();
const project = projects.find(p => p.slug === group.project.slug)!;
const {projects, fetching} = useProjects({slugs: [group.project.slug]});
const project = projects.find(p => p.slug === group.project.slug);

const {tab, setTab} = useDrawerTab({enabled: includeFeatureFlagsTab});

// Wait for project to load before rendering the drawer content
if (fetching || !project) {
return null;
}

return (
<AnalyticsArea name="distributions_drawer">
<EventDrawerContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export default function FlagDrawerContent({
});

// CTA logic
const {projects} = useProjects();
const project = projects.find(p => p.slug === group.project.slug)!;
const {projects} = useProjects({slugs: [group.project.slug]});
const project = projects.find(p => p.slug === group.project.slug);

const showCTA =
allGroupFlagCount === 0 &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,21 @@ export function MissingInstrumentationNodeDetails({
...props
}: TraceTreeNodeDetailsProps<MissingInstrumentationNode>) {
const {node} = props;
const {projects} = useProjects();
const isEAP = isEAPSpanNode(node.previous);

if (isEAPSpanNode(node.previous)) {
// For EAP spans, the event may not be loaded yet, but we have project_slug on the span itself
const eapProjectSlug = isEAP
? (node.previous as TraceTreeNode<TraceTree.EAPSpan>).value.project_slug
: undefined;
const event = node.previous.event ?? node.next.event ?? null;
const projectSlug = eapProjectSlug ?? event?.projectSlug;
const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []});

if (isEAP) {
return <EAPMissingInstrumentationNodeDetails {...props} projects={projects} />;
}

const event = node.previous.event ?? node.next.event ?? null;
const project = projects.find(proj => proj.slug === event?.projectSlug);
const project = projects.find(proj => proj.slug === projectSlug);
const profileMeta = getProfileMeta(event) || '';
const profileContext = event?.contexts?.profile ?? {};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ export function SpanNodeDetails(
const {node, organization} = props;
const location = useLocation();
const theme = useTheme();
const {projects} = useProjects();
const projectSlug = node.value.project_slug ?? node.event?.projectSlug;
const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard error on missing slug i believe - someone correct me if im wrong

const issues = TraceTree.UniqueIssues(node);

const parentTransaction = isEAPSpanNode(node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function TransactionNodeDetails({
replay,
hideNodeActions,
}: TraceTreeNodeDetailsProps<TraceTreeNode<TraceTree.Transaction>>) {
const {projects} = useProjects();
const {projects} = useProjects({slugs: [node.value.project_slug]});
const issues = useMemo(() => {
return [...node.errors, ...node.occurrences];
}, [node.errors, node.occurrences]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,10 @@ export function UptimeNodeDetails(
const {node} = props;
const location = useLocation();
const theme = useTheme();
const {projects} = useProjects();
const projectSlug = node.value.project_slug ?? node.event?.projectSlug;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard error on missing slug i believe - someone correct me if im wrong

const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []});

const project = projects.find(
proj => proj.slug === (node.value.project_slug ?? node.event?.projectSlug)
);
const project = projects.find(proj => proj.slug === projectSlug);

return (
<UptimeSpanNodeDetails
Expand Down
2 changes: 1 addition & 1 deletion static/app/views/profiling/profilesProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export function ContinuousProfileProvider({
setProfile,
}: ContinuousProfileProviderProps) {
const api = useApi();
const {projects} = useProjects();
const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing fetch state check causes false Sentry errors

The ContinuousProfileProvider now uses useProjects with specific slugs, but doesn't destructure the fetching state. When the project needs to be fetched from the API (not already in store), projects will be empty during the fetch. The useLayoutEffect runs immediately with the empty array, finds no project, and logs "Failed to fetch continuous profile - project not found" to Sentry - even though the project exists and is just being loaded. This causes false positive error reports. The component needs to check fetching before treating a missing project as an error.

Fix in Cursor Fix in Web


useLayoutEffect(() => {
if (!profileMeta) {
Expand Down
2 changes: 1 addition & 1 deletion static/app/views/projectDetail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function ProjectDetailContainer(
'projects' | 'loadingProjects' | 'selection'
>
) {
const {projects} = useProjects();
const {projects} = useProjects({slugs: [props.params.projectId]});
const project = projects.find(p => p.slug === props.params.projectId);

useRouteAnalyticsParams(
Expand Down
10 changes: 5 additions & 5 deletions static/app/views/projectDetail/projectDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ type Props = RouteComponentProps<RouteParams> & {

export default function ProjectDetail({router, location, organization}: Props) {
const api = useApi();
const params = useParams();
const {projects, fetching: loadingProjects} = useProjects();
const params = useParams<RouteParams>();
const {projects, fetching: loadingProjects} = useProjects({slugs: [params.projectId]});
const {selection} = usePageFilters();
const project = projects.find(p => p.slug === params.projectId);
const {query} = location.query;
Expand All @@ -82,7 +82,7 @@ export default function ProjectDetail({router, location, organization}: Props) {
}, [hasTransactions, hasSessions]);

const onRetryProjects = useCallback(() => {
fetchOrganizationDetails(api, params.orgId!);
fetchOrganizationDetails(api, params.orgId);
}, [api, params.orgId]);

const handleSearch = useCallback(
Expand Down Expand Up @@ -263,14 +263,14 @@ export default function ProjectDetail({router, location, organization}: Props) {
<Feature features="incidents" organization={organization}>
<ProjectLatestAlerts
organization={organization}
projectSlug={params.projectId!}
projectSlug={params.projectId}
location={location}
isProjectStabilized={isProjectStabilized}
/>
</Feature>
<ProjectLatestReleases
organization={organization}
projectSlug={params.projectId!}
projectSlug={params.projectId}
location={location}
isProjectStabilized={isProjectStabilized}
project={project}
Expand Down
10 changes: 6 additions & 4 deletions static/app/views/replays/detail/errorList/errorTableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function ErrorTableCell({

const {eventId, groupId, groupShortId, level, projectSlug} = frame.data;
const title = frame.message;
const {projects} = useProjects();
const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []});
const project = useMemo(
() => projects.find(p => p.slug === projectSlug),
[projects, projectSlug]
Expand Down Expand Up @@ -161,9 +161,11 @@ export default function ErrorTableCell({
() => (
<Cell {...columnProps}>
<Text>
<AvatarWrapper>
<ProjectAvatar project={project!} size={16} />
</AvatarWrapper>
{project && (
<AvatarWrapper>
<ProjectAvatar project={project} size={16} />
</AvatarWrapper>
)}
{eventUrl ? (
<Link to={eventUrl}>
<QuickContextHovercard
Expand Down
Loading