utils.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import {useMemo} from 'react';
  2. import orderBy from 'lodash/orderBy';
  3. import {bulkUpdate, useFetchIssueTags} from 'sentry/actionCreators/group';
  4. import {Client} from 'sentry/api';
  5. import {t} from 'sentry/locale';
  6. import ConfigStore from 'sentry/stores/configStore';
  7. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  8. import type {Group, GroupActivity, TagValue} from 'sentry/types';
  9. import type {Event} from 'sentry/types/event';
  10. import {useLocation} from 'sentry/utils/useLocation';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. export function markEventSeen(
  13. api: Client,
  14. orgId: string,
  15. projectId: string,
  16. groupId: string
  17. ) {
  18. bulkUpdate(
  19. api,
  20. {
  21. orgId,
  22. projectId,
  23. itemIds: [groupId],
  24. failSilently: true,
  25. data: {hasSeen: true},
  26. },
  27. {}
  28. );
  29. }
  30. export function fetchGroupUserReports(
  31. orgSlug: string,
  32. groupId: string,
  33. query: Record<string, string>
  34. ) {
  35. const api = new Client();
  36. return api.requestPromise(`/organizations/${orgSlug}/issues/${groupId}/user-reports/`, {
  37. includeAllArgs: true,
  38. query,
  39. });
  40. }
  41. export function useDefaultIssueEvent() {
  42. const user = useLegacyStore(ConfigStore).user;
  43. const options = user ? user.options : null;
  44. return options?.defaultIssueEvent ?? 'recommended';
  45. }
  46. /**
  47. * Combines two TagValue arrays and combines TagValue.count upon conflict
  48. */
  49. export function mergeAndSortTagValues(
  50. tagValues1: TagValue[],
  51. tagValues2: TagValue[],
  52. sort: 'count' | 'lastSeen' = 'lastSeen'
  53. ): TagValue[] {
  54. const tagValueCollection = tagValues1.reduce<Record<string, TagValue>>(
  55. (acc, tagValue) => {
  56. acc[tagValue.value] = tagValue;
  57. return acc;
  58. },
  59. {}
  60. );
  61. tagValues2.forEach(tagValue => {
  62. if (tagValueCollection[tagValue.value]) {
  63. tagValueCollection[tagValue.value].count += tagValue.count;
  64. if (tagValue.lastSeen > tagValueCollection[tagValue.value].lastSeen) {
  65. tagValueCollection[tagValue.value].lastSeen = tagValue.lastSeen;
  66. }
  67. } else {
  68. tagValueCollection[tagValue.value] = tagValue;
  69. }
  70. });
  71. const allTagValues: TagValue[] = Object.values(tagValueCollection);
  72. if (sort === 'count') {
  73. allTagValues.sort((a, b) => b.count - a.count);
  74. } else {
  75. allTagValues.sort((a, b) => (b.lastSeen < a.lastSeen ? -1 : 1));
  76. }
  77. return allTagValues;
  78. }
  79. /**
  80. * Returns the environment name for an event or null
  81. *
  82. * @param event
  83. */
  84. export function getEventEnvironment(event: Event) {
  85. const tag = event.tags.find(({key}) => key === 'environment');
  86. return tag ? tag.value : null;
  87. }
  88. const SUBSCRIPTION_REASONS = {
  89. commented: t(
  90. "You're receiving workflow notifications because you have commented on this issue."
  91. ),
  92. assigned: t(
  93. "You're receiving workflow notifications because you were assigned to this issue."
  94. ),
  95. bookmarked: t(
  96. "You're receiving workflow notifications because you have bookmarked this issue."
  97. ),
  98. changed_status: t(
  99. "You're receiving workflow notifications because you have changed the status of this issue."
  100. ),
  101. mentioned: t(
  102. "You're receiving workflow notifications because you have been mentioned in this issue."
  103. ),
  104. };
  105. /**
  106. * @param group
  107. * @param removeLinks add/remove links to subscription reasons text (default: false)
  108. * @returns Reason for subscription
  109. */
  110. export function getSubscriptionReason(group: Group) {
  111. if (group.subscriptionDetails?.disabled) {
  112. return t('You have disabled workflow notifications for this project.');
  113. }
  114. if (!group.isSubscribed) {
  115. return t('Subscribe to workflow notifications for this issue');
  116. }
  117. if (group.subscriptionDetails) {
  118. const {reason} = group.subscriptionDetails;
  119. if (reason === 'unknown') {
  120. return t(
  121. "You're receiving workflow notifications because you are subscribed to this issue."
  122. );
  123. }
  124. if (reason && SUBSCRIPTION_REASONS.hasOwnProperty(reason)) {
  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 const useFetchIssueTagsForDetailsPage = (
  172. {
  173. groupId,
  174. orgSlug,
  175. environment = [],
  176. isStatisticalDetector = false,
  177. statisticalDetectorParameters,
  178. }: {
  179. environment: string[];
  180. orgSlug: string;
  181. groupId?: string;
  182. isStatisticalDetector?: boolean;
  183. statisticalDetectorParameters?: {
  184. durationBaseline: number;
  185. end: string;
  186. start: string;
  187. transaction: string;
  188. };
  189. },
  190. {enabled = true}: {enabled?: boolean} = {}
  191. ) => {
  192. return useFetchIssueTags(
  193. {
  194. groupId,
  195. orgSlug,
  196. environment,
  197. readable: true,
  198. limit: 4,
  199. isStatisticalDetector,
  200. statisticalDetectorParameters,
  201. },
  202. {enabled}
  203. );
  204. };
  205. export function useEnvironmentsFromUrl(): string[] {
  206. const location = useLocation();
  207. const envs = location.query.environment;
  208. const envsArray = useMemo(() => {
  209. return typeof envs === 'string' ? [envs] : envs ?? [];
  210. }, [envs]);
  211. return envsArray;
  212. }
  213. export function getGroupDetailsQueryData({
  214. environments,
  215. }: {
  216. environments?: string[];
  217. } = {}): Record<string, string | string[]> {
  218. // Note, we do not want to include the environment key at all if there are no environments
  219. const query: Record<string, string | string[]> = {
  220. ...(environments && environments.length > 0 ? {environment: environments} : {}),
  221. expand: ['inbox', 'owners'],
  222. collapse: ['release', 'tags'],
  223. };
  224. return query;
  225. }
  226. export function getGroupEventDetailsQueryData({
  227. environments,
  228. query,
  229. stacktraceOnly,
  230. }: {
  231. environments?: string[];
  232. query?: string;
  233. stacktraceOnly?: boolean;
  234. } = {}): Record<string, string | string[]> {
  235. const defaultParams = {
  236. collapse: stacktraceOnly ? ['stacktraceOnly'] : ['fullRelease'],
  237. ...(query ? {query} : {}),
  238. };
  239. if (!environments || environments.length === 0) {
  240. return defaultParams;
  241. }
  242. return {...defaultParams, environment: environments};
  243. }
  244. export function useHasStreamlinedUI() {
  245. const location = useLocation();
  246. const organization = useOrganization();
  247. if (location.query.streamline === '0') {
  248. return false;
  249. }
  250. return (
  251. location.query.streamline === '1' ||
  252. organization.features.includes('issue-details-streamline')
  253. );
  254. }