utils.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import {useMemo} from 'react';
  2. import orderBy from 'lodash/orderBy';
  3. import {
  4. bulkUpdate,
  5. useFetchIssueTag,
  6. useFetchIssueTagValues,
  7. } from 'sentry/actionCreators/group';
  8. import type {Client} from 'sentry/api';
  9. import {t} from 'sentry/locale';
  10. import ConfigStore from 'sentry/stores/configStore';
  11. import GroupStore from 'sentry/stores/groupStore';
  12. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  13. import type {Event} from 'sentry/types/event';
  14. import type {Group, GroupActivity, TagValue} from 'sentry/types/group';
  15. import {defined} from 'sentry/utils';
  16. import type {ApiQueryKey} from 'sentry/utils/queryClient';
  17. import {useLocation} from 'sentry/utils/useLocation';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import {useParams} from 'sentry/utils/useParams';
  20. import {useUser} from 'sentry/utils/useUser';
  21. import {useGroupTagsReadable} from 'sentry/views/issueDetails/groupTags/useGroupTags';
  22. export function markEventSeen(
  23. api: Client,
  24. orgId: string,
  25. projectId: string,
  26. groupId: string
  27. ) {
  28. bulkUpdate(
  29. api,
  30. {
  31. orgId,
  32. projectId,
  33. itemIds: [groupId],
  34. failSilently: true,
  35. data: {hasSeen: true},
  36. },
  37. {}
  38. );
  39. }
  40. export function useDefaultIssueEvent() {
  41. const user = useLegacyStore(ConfigStore).user;
  42. const options = user ? user.options : null;
  43. return options?.defaultIssueEvent ?? 'recommended';
  44. }
  45. /**
  46. * Combines two TagValue arrays and combines TagValue.count upon conflict
  47. */
  48. export function mergeAndSortTagValues(
  49. tagValues1: TagValue[],
  50. tagValues2: TagValue[],
  51. sort: 'count' | 'lastSeen' = 'lastSeen'
  52. ): TagValue[] {
  53. const tagValueCollection = tagValues1.reduce<Record<string, TagValue>>(
  54. (acc, tagValue) => {
  55. acc[tagValue.value] = tagValue;
  56. return acc;
  57. },
  58. {}
  59. );
  60. tagValues2.forEach(tagValue => {
  61. if (tagValueCollection[tagValue.value]) {
  62. tagValueCollection[tagValue.value]!.count += tagValue.count;
  63. if (tagValue.lastSeen > tagValueCollection[tagValue.value]!.lastSeen) {
  64. tagValueCollection[tagValue.value]!.lastSeen = tagValue.lastSeen;
  65. }
  66. } else {
  67. tagValueCollection[tagValue.value] = tagValue;
  68. }
  69. });
  70. const allTagValues: TagValue[] = Object.values(tagValueCollection);
  71. if (sort === 'count') {
  72. allTagValues.sort((a, b) => b.count - a.count);
  73. } else {
  74. allTagValues.sort((a, b) => (b.lastSeen < a.lastSeen ? -1 : 1));
  75. }
  76. return allTagValues;
  77. }
  78. /**
  79. * Returns the environment name for an event or null
  80. *
  81. * @param event
  82. */
  83. export function getEventEnvironment(event: Event) {
  84. const tag = event.tags.find(({key}) => key === 'environment');
  85. return tag ? tag.value : null;
  86. }
  87. const SUBSCRIPTION_REASONS = {
  88. commented: t(
  89. "You're receiving workflow notifications because you have commented on this issue."
  90. ),
  91. assigned: t(
  92. "You're receiving workflow notifications because you were assigned to this issue."
  93. ),
  94. bookmarked: t(
  95. "You're receiving workflow notifications because you have bookmarked this issue."
  96. ),
  97. changed_status: t(
  98. "You're receiving workflow notifications because you have changed the status of this issue."
  99. ),
  100. mentioned: t(
  101. "You're receiving workflow notifications because you have been mentioned in this issue."
  102. ),
  103. };
  104. /**
  105. * @param group
  106. * @param removeLinks add/remove links to subscription reasons text (default: false)
  107. * @returns Reason for subscription
  108. */
  109. export function getSubscriptionReason(group: Group) {
  110. if (group.subscriptionDetails?.disabled) {
  111. return t('You have disabled workflow notifications for this project.');
  112. }
  113. if (!group.isSubscribed) {
  114. return t('Subscribe to workflow notifications for this issue');
  115. }
  116. if (group.subscriptionDetails) {
  117. const {reason} = group.subscriptionDetails;
  118. if (reason === 'unknown') {
  119. return t(
  120. "You're receiving workflow notifications because you are subscribed to this issue."
  121. );
  122. }
  123. if (reason && SUBSCRIPTION_REASONS.hasOwnProperty(reason)) {
  124. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  125. return SUBSCRIPTION_REASONS[reason];
  126. }
  127. }
  128. return t(
  129. "You're receiving updates because you are subscribed to workflow notifications for this project."
  130. );
  131. }
  132. export function getGroupMostRecentActivity(
  133. activities: GroupActivity[] | undefined
  134. ): GroupActivity | undefined {
  135. // Most recent activity
  136. return activities
  137. ? orderBy([...activities], ({dateCreated}) => new Date(dateCreated), ['desc'])[0]
  138. : undefined;
  139. }
  140. export enum ReprocessingStatus {
  141. REPROCESSED_AND_HASNT_EVENT = 'reprocessed_and_hasnt_event',
  142. REPROCESSED_AND_HAS_EVENT = 'reprocessed_and_has_event',
  143. REPROCESSING = 'reprocessing',
  144. NO_STATUS = 'no_status',
  145. }
  146. // Reprocessing Checks
  147. export function getGroupReprocessingStatus(
  148. group: Group,
  149. mostRecentActivity?: GroupActivity
  150. ) {
  151. const {status, count, activity: activities} = group;
  152. const groupCount = Number(count);
  153. switch (status) {
  154. case 'reprocessing':
  155. return ReprocessingStatus.REPROCESSING;
  156. case 'unresolved': {
  157. const groupMostRecentActivity =
  158. mostRecentActivity ?? getGroupMostRecentActivity(activities);
  159. if (groupMostRecentActivity?.type === 'reprocess') {
  160. if (groupCount === 0) {
  161. return ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT;
  162. }
  163. return ReprocessingStatus.REPROCESSED_AND_HAS_EVENT;
  164. }
  165. return ReprocessingStatus.NO_STATUS;
  166. }
  167. default:
  168. return ReprocessingStatus.NO_STATUS;
  169. }
  170. }
  171. export function useEnvironmentsFromUrl(): string[] {
  172. const location = useLocation();
  173. const envs = location.query.environment;
  174. const envsArray = useMemo(() => {
  175. return typeof envs === 'string' ? [envs] : envs ?? [];
  176. }, [envs]);
  177. return envsArray;
  178. }
  179. export function getGroupEventDetailsQueryData({
  180. environments,
  181. query,
  182. start,
  183. end,
  184. statsPeriod,
  185. }: {
  186. query: string | undefined;
  187. end?: string;
  188. environments?: string[];
  189. start?: string;
  190. statsPeriod?: string;
  191. }): Record<string, string | string[]> {
  192. const params: Record<string, string | string[]> = {
  193. collapse: ['fullRelease'],
  194. };
  195. if (query) {
  196. params.query = query;
  197. }
  198. if (environments && environments.length > 0) {
  199. params.environment = environments;
  200. }
  201. if (start) {
  202. params.start = start;
  203. }
  204. if (end) {
  205. params.end = end;
  206. }
  207. if (statsPeriod) {
  208. params.statsPeriod = statsPeriod;
  209. }
  210. return params;
  211. }
  212. export function getGroupEventQueryKey({
  213. orgSlug,
  214. groupId,
  215. eventId,
  216. environments,
  217. query,
  218. start,
  219. end,
  220. statsPeriod,
  221. }: {
  222. environments: string[];
  223. eventId: string;
  224. groupId: string;
  225. orgSlug: string;
  226. end?: string;
  227. query?: string;
  228. start?: string;
  229. statsPeriod?: string;
  230. }): ApiQueryKey {
  231. return [
  232. `/organizations/${orgSlug}/issues/${groupId}/events/${eventId}/`,
  233. {
  234. query: getGroupEventDetailsQueryData({
  235. environments,
  236. query,
  237. start,
  238. end,
  239. statsPeriod,
  240. }),
  241. },
  242. ];
  243. }
  244. export function useHasStreamlinedUI() {
  245. const location = useLocation();
  246. const user = useUser();
  247. const organization = useOrganization();
  248. // Allow query param to override all other settings to set the UI.
  249. if (defined(location.query.streamline)) {
  250. return location.query.streamline === '1';
  251. }
  252. // If the organzation option is set, it determines which interface is used.
  253. if (defined(organization.streamlineOnly)) {
  254. return organization.streamlineOnly;
  255. }
  256. // If the enforce flag is set for the organization, ignore user preferences and enable the UI
  257. if (organization.features.includes('issue-details-streamline-enforce')) {
  258. return true;
  259. }
  260. // Apply the UI based on user preferences
  261. return !!user?.options?.prefersIssueDetailsStreamlinedUI;
  262. }
  263. export function useIsSampleEvent(): boolean {
  264. const params = useParams<{groupId: string}>();
  265. const environments = useEnvironmentsFromUrl();
  266. const groupId = params.groupId;
  267. const group = GroupStore.get(groupId);
  268. const {data} = useGroupTagsReadable(
  269. {
  270. groupId,
  271. environment: environments,
  272. },
  273. // Don't want this query to take precedence over the main requests
  274. {enabled: defined(group)}
  275. );
  276. return data?.some(tag => tag.key === 'sample_event') ?? false;
  277. }
  278. type TagSort = 'date' | 'count';
  279. const DEFAULT_SORT: TagSort = 'count';
  280. export function usePrefetchTagValues(tagKey: string, groupId: string, enabled: boolean) {
  281. const organization = useOrganization();
  282. const location = useLocation();
  283. const sort: TagSort =
  284. (location.query.tagDrawerSort as TagSort | undefined) ?? DEFAULT_SORT;
  285. useFetchIssueTagValues(
  286. {
  287. orgSlug: organization.slug,
  288. groupId,
  289. tagKey,
  290. sort,
  291. cursor: location.query.tagDrawerCursor as string | undefined,
  292. },
  293. {enabled}
  294. );
  295. useFetchIssueTag(
  296. {
  297. orgSlug: organization.slug,
  298. groupId,
  299. tagKey,
  300. },
  301. {enabled}
  302. );
  303. }