organization.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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 * as Sentry from '@sentry/react';
  5. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  6. import {setActiveOrganization} from 'sentry/actionCreators/organizations';
  7. import type {ResponseMeta} from 'sentry/api';
  8. import {Client} from 'sentry/api';
  9. import OrganizationStore from 'sentry/stores/organizationStore';
  10. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  11. import ProjectsStore from 'sentry/stores/projectsStore';
  12. import TeamStore from 'sentry/stores/teamStore';
  13. import type {Organization, Team} from 'sentry/types/organization';
  14. import type {Project} from 'sentry/types/project';
  15. import FeatureFlagOverrides from 'sentry/utils/featureFlagOverrides';
  16. import {
  17. addOrganizationFeaturesHandler,
  18. buildSentryFeaturesHandler,
  19. } from 'sentry/utils/featureFlags';
  20. import {getPreloadedDataPromise} from 'sentry/utils/getPreloadedData';
  21. import parseLinkHeader from 'sentry/utils/parseLinkHeader';
  22. async function fetchOrg(
  23. api: Client,
  24. slug: string,
  25. usePreload?: boolean
  26. ): Promise<Organization> {
  27. const [org] = await getPreloadedDataPromise(
  28. 'organization',
  29. slug,
  30. () =>
  31. // This data should get preloaded in static/sentry/index.ejs
  32. // If this url changes make sure to update the preload
  33. api.requestPromise(`/organizations/${slug}/`, {
  34. includeAllArgs: true,
  35. query: {detailed: 0, include_feature_flags: 1},
  36. }),
  37. usePreload
  38. );
  39. if (!org) {
  40. throw new Error('retrieved organization is falsey');
  41. }
  42. FeatureFlagOverrides.singleton().loadOrg(org);
  43. addOrganizationFeaturesHandler({
  44. organization: org,
  45. handler: buildSentryFeaturesHandler('feature.organizations:'),
  46. });
  47. OrganizationStore.onUpdate(org, {replace: true});
  48. setActiveOrganization(org);
  49. const scope = Sentry.getCurrentScope();
  50. // XXX(dcramer): this is duplicated in sdk.py on the backend
  51. scope.setTag('organization', org.id);
  52. scope.setTag('organization.slug', org.slug);
  53. scope.setContext('organization', {id: org.id, slug: org.slug});
  54. return org;
  55. }
  56. async function fetchProjectsAndTeams(
  57. slug: string,
  58. usePreload?: boolean
  59. ): Promise<
  60. [
  61. [Project[], string | undefined, XMLHttpRequest | ResponseMeta | undefined],
  62. [Team[], string | undefined, XMLHttpRequest | ResponseMeta | undefined],
  63. ]
  64. > {
  65. // Create a new client so the request is not cancelled
  66. const uncancelableApi = new Client();
  67. const projectsPromise = getPreloadedDataPromise(
  68. 'projects',
  69. slug,
  70. () =>
  71. // This data should get preloaded in static/sentry/index.ejs
  72. // If this url changes make sure to update the preload
  73. uncancelableApi.requestPromise(`/organizations/${slug}/projects/`, {
  74. includeAllArgs: true,
  75. query: {
  76. all_projects: 1,
  77. collapse: ['latestDeploys', 'unusedFeatures'],
  78. },
  79. }),
  80. usePreload
  81. );
  82. const teamsPromise = getPreloadedDataPromise(
  83. 'teams',
  84. slug,
  85. // This data should get preloaded in static/sentry/index.ejs
  86. // If this url changes make sure to update the preload
  87. () =>
  88. uncancelableApi.requestPromise(`/organizations/${slug}/teams/`, {
  89. includeAllArgs: true,
  90. }),
  91. usePreload
  92. );
  93. try {
  94. return await Promise.all([projectsPromise, teamsPromise]);
  95. } catch (err) {
  96. // It's possible these requests fail with a 403 if the user has a role with
  97. // insufficient access to projects and teams, but *can* access org details
  98. // (e.g. billing). An example of this is in org settings.
  99. //
  100. // Ignore 403s and bubble up other API errors
  101. if (err.status !== 403) {
  102. throw err;
  103. }
  104. }
  105. return [
  106. [[], undefined, undefined],
  107. [[], undefined, undefined],
  108. ];
  109. }
  110. /**
  111. * Fetches an organization's details
  112. *
  113. * @param api A reference to the api client
  114. * @param slug The organization slug
  115. * @param silent Should we silently update the organization (do not clear the
  116. * current organization in the store)
  117. * @param usePreload Should the preloaded data be used if available?
  118. */
  119. export function fetchOrganizationDetails(
  120. api: Client,
  121. slug: string,
  122. silent: boolean,
  123. usePreload?: boolean
  124. ) {
  125. if (!silent) {
  126. OrganizationStore.reset();
  127. ProjectsStore.reset();
  128. TeamStore.reset();
  129. PageFiltersStore.onReset();
  130. }
  131. const getErrorMessage = err => {
  132. if (typeof err.responseJSON?.detail === 'string') {
  133. return err.responseJSON?.detail;
  134. }
  135. if (typeof err.responseJSON?.detail?.message === 'string') {
  136. return err.responseJSON?.detail.message;
  137. }
  138. return null;
  139. };
  140. const loadOrganization = () => {
  141. return new Promise(async (resolve, reject) => {
  142. let org: Organization | undefined = undefined;
  143. try {
  144. org = await fetchOrg(api, slug, usePreload);
  145. } catch (err) {
  146. if (!err) {
  147. reject(err);
  148. return;
  149. }
  150. OrganizationStore.onFetchOrgError(err);
  151. if (err.status === 403 || err.status === 401) {
  152. const errMessage = getErrorMessage(err);
  153. if (errMessage) {
  154. addErrorMessage(errMessage);
  155. reject(errMessage);
  156. }
  157. return;
  158. }
  159. Sentry.captureException(err);
  160. }
  161. resolve(org);
  162. });
  163. };
  164. const loadTeamsAndProjects = () => {
  165. return new Promise(async resolve => {
  166. const [[projects], [teams, , resp]] = await fetchProjectsAndTeams(slug, usePreload);
  167. ProjectsStore.loadInitialData(projects ?? []);
  168. const teamPageLinks = resp?.getResponseHeader('Link');
  169. if (teamPageLinks) {
  170. const paginationObject = parseLinkHeader(teamPageLinks);
  171. const hasMore = paginationObject?.next?.results ?? false;
  172. const cursor = paginationObject.next?.cursor;
  173. TeamStore.loadInitialData(teams, hasMore, cursor);
  174. } else {
  175. TeamStore.loadInitialData(teams);
  176. }
  177. resolve([projects, teams]);
  178. });
  179. };
  180. return Promise.all([loadOrganization(), loadTeamsAndProjects()]);
  181. }