bootstrapRequests.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. // XXX(epurkhiser): Ensure the LatestContextStore is initialized before we set
  2. // the active org. Otherwise we will trigger an action that does nothing
  3. import 'sentry/stores/latestContextStore';
  4. import {useLayoutEffect} from 'react';
  5. import * as Sentry from '@sentry/react';
  6. import {setActiveOrganization} from 'sentry/actionCreators/organizations';
  7. import {type ApiResult, Client} from 'sentry/api';
  8. import OrganizationStore from 'sentry/stores/organizationStore';
  9. import ProjectsStore from 'sentry/stores/projectsStore';
  10. import TeamStore from 'sentry/stores/teamStore';
  11. import type {Organization, Team} from 'sentry/types/organization';
  12. import type {Project} from 'sentry/types/project';
  13. import FeatureFlagOverrides from 'sentry/utils/featureFlagOverrides';
  14. import {
  15. addOrganizationFeaturesHandler,
  16. buildSentryFeaturesHandler,
  17. } from 'sentry/utils/featureFlags';
  18. import parseLinkHeader from 'sentry/utils/parseLinkHeader';
  19. import {queryOptions, skipToken, useQuery} from 'sentry/utils/queryClient';
  20. // 30 second stale time
  21. // Stale time decides if the query should be refetched
  22. const BOOTSTRAP_QUERY_STALE_TIME = 30 * 1000;
  23. // 10 minute gc time
  24. // Warning: We will always have an observer on the organization object
  25. // so it will never be garbage collected from the query cache
  26. const BOOTSTRAP_QUERY_GC_TIME = 10 * 60 * 1000;
  27. export function useBootstrapOrganizationQuery(orgSlug: string | null) {
  28. const organizationQuery = useQuery(getBootstrapOrganizationQueryOptions(orgSlug));
  29. useLayoutEffect(() => {
  30. if (organizationQuery.data) {
  31. // Shallow copy to avoid mutating the original object
  32. const organization = {...organizationQuery.data};
  33. // FeatureFlagOverrides mutates the organization object
  34. FeatureFlagOverrides.singleton().loadOrg(organization);
  35. addOrganizationFeaturesHandler({
  36. organization,
  37. handler: buildSentryFeaturesHandler('feature.organizations:'),
  38. });
  39. OrganizationStore.onUpdate(organization, {replace: true});
  40. setActiveOrganization(organization);
  41. const scope = Sentry.getCurrentScope();
  42. scope.setTag('organization', organization.id);
  43. scope.setTag('organization.slug', organization.slug);
  44. scope.setContext('organization', {
  45. id: organization.id,
  46. slug: organization.slug,
  47. });
  48. }
  49. if (organizationQuery.error) {
  50. OrganizationStore.onFetchOrgError(organizationQuery.error as any);
  51. }
  52. }, [organizationQuery.data, organizationQuery.error]);
  53. return organizationQuery;
  54. }
  55. export function useBootstrapTeamsQuery(orgSlug: string | null) {
  56. const teamsQuery = useQuery(getBoostrapTeamsQueryOptions(orgSlug));
  57. useLayoutEffect(() => {
  58. if (teamsQuery.data) {
  59. TeamStore.loadInitialData(
  60. teamsQuery.data.teams,
  61. teamsQuery.data.hasMore,
  62. teamsQuery.data.cursor
  63. );
  64. }
  65. }, [teamsQuery.data]);
  66. return teamsQuery;
  67. }
  68. export function useBootstrapProjectsQuery(orgSlug: string | null) {
  69. const projectsQuery = useQuery(getBootstrapProjectsQueryOptions(orgSlug));
  70. useLayoutEffect(() => {
  71. if (projectsQuery.data) {
  72. ProjectsStore.loadInitialData(projectsQuery.data);
  73. }
  74. }, [projectsQuery.data]);
  75. return projectsQuery;
  76. }
  77. function getBootstrapOrganizationQueryOptions(orgSlug: string | null) {
  78. return queryOptions({
  79. queryKey: ['bootstrap-organization', orgSlug],
  80. queryFn: orgSlug
  81. ? async (): Promise<Organization> => {
  82. // Get the preloaded data promise
  83. try {
  84. const preloadResponse = await getPreloadedData('organization', orgSlug);
  85. // If the preload request was for a different org or the promise was rejected
  86. if (Array.isArray(preloadResponse) && preloadResponse[0] !== null) {
  87. return preloadResponse[0];
  88. }
  89. } catch {
  90. // Silently try again with non-preloaded data
  91. }
  92. const uncancelableApi = new Client();
  93. const [org] = await uncancelableApi.requestPromise(
  94. `/organizations/${orgSlug}/`,
  95. {
  96. includeAllArgs: true,
  97. query: {detailed: 0, include_feature_flags: 1},
  98. }
  99. );
  100. return org;
  101. }
  102. : skipToken,
  103. staleTime: BOOTSTRAP_QUERY_STALE_TIME,
  104. gcTime: BOOTSTRAP_QUERY_GC_TIME,
  105. retry: false,
  106. });
  107. }
  108. /**
  109. * The TeamsStore expects a cursor, hasMore, and teams
  110. * Since some of this information exists in headers, parse it into something we can serialize
  111. */
  112. function createTeamsObject(response: ApiResult): {
  113. cursor: string | null;
  114. hasMore: boolean;
  115. teams: Team[];
  116. } {
  117. const teams = response[0];
  118. const paginationObject = parseLinkHeader(response[2]!.getResponseHeader('Link'));
  119. const hasMore = paginationObject?.next?.results ?? false;
  120. const cursor = paginationObject.next?.cursor ?? null;
  121. return {teams, hasMore, cursor};
  122. }
  123. function getBoostrapTeamsQueryOptions(orgSlug: string | null) {
  124. return queryOptions({
  125. queryKey: ['bootstrap-teams', orgSlug],
  126. queryFn: orgSlug
  127. ? async (): Promise<{
  128. cursor: string | null;
  129. hasMore: boolean;
  130. teams: Team[];
  131. }> => {
  132. // Get the preloaded data promise
  133. try {
  134. const preloadResponse = await getPreloadedData('teams', orgSlug);
  135. // If the preload request was successful, find the matching team
  136. if (preloadResponse !== null && preloadResponse[0] !== null) {
  137. return createTeamsObject(preloadResponse);
  138. }
  139. } catch {
  140. // Silently try again with non-preloaded data
  141. }
  142. const uncancelableApi = new Client();
  143. const teamsApiResponse = await uncancelableApi.requestPromise(
  144. `/organizations/${orgSlug}/teams/`,
  145. {
  146. includeAllArgs: true,
  147. }
  148. );
  149. return createTeamsObject(teamsApiResponse);
  150. }
  151. : skipToken,
  152. staleTime: BOOTSTRAP_QUERY_STALE_TIME,
  153. gcTime: BOOTSTRAP_QUERY_GC_TIME,
  154. retry: false,
  155. });
  156. }
  157. function getBootstrapProjectsQueryOptions(orgSlug: string | null) {
  158. return queryOptions({
  159. queryKey: ['bootstrap-projects', orgSlug],
  160. queryFn: orgSlug
  161. ? async (): Promise<Project[]> => {
  162. // Get the preloaded data promise
  163. try {
  164. const preloadResponse = await getPreloadedData('projects', orgSlug);
  165. // If the preload request was successful
  166. if (preloadResponse !== null && preloadResponse[0] !== null) {
  167. return preloadResponse[0];
  168. }
  169. } catch {
  170. // Silently try again with non-preloaded data
  171. }
  172. const uncancelableApi = new Client();
  173. const [projects] = await uncancelableApi.requestPromise(
  174. `/organizations/${orgSlug}/projects/`,
  175. {
  176. includeAllArgs: true,
  177. query: {
  178. all_projects: 1,
  179. collapse: ['latestDeploys', 'unusedFeatures'],
  180. },
  181. }
  182. );
  183. return projects;
  184. }
  185. : skipToken,
  186. staleTime: BOOTSTRAP_QUERY_STALE_TIME,
  187. gcTime: BOOTSTRAP_QUERY_GC_TIME,
  188. retry: false,
  189. });
  190. }
  191. /**
  192. * Small helper to access the preload requests in window.__sentry_preload
  193. * See preload-data.html for more details, this request is started before the app is loaded
  194. * saving time on the initial page load.
  195. */
  196. function getPreloadedData(
  197. name: 'organization' | 'projects' | 'teams',
  198. slug: string
  199. ): Promise<ApiResult | null> {
  200. const data = window.__sentry_preload;
  201. if (!data?.[name] || data.orgSlug?.toLowerCase() !== slug.toLowerCase()) {
  202. throw new Error('Prefetch query not found or slug mismatch');
  203. }
  204. const promise = data[name];
  205. // Prevent reusing the promise later
  206. delete data[name];
  207. return promise;
  208. }