utils.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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 GroupStore from 'sentry/stores/groupStore';
  8. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  9. import type {Event} from 'sentry/types/event';
  10. import type {Group, GroupActivity, TagValue} from 'sentry/types/group';
  11. import {defined} from 'sentry/utils';
  12. import {useLocation} from 'sentry/utils/useLocation';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import {useParams} from 'sentry/utils/useParams';
  15. import {useUser} from 'sentry/utils/useUser';
  16. export function markEventSeen(
  17. api: Client,
  18. orgId: string,
  19. projectId: string,
  20. groupId: string
  21. ) {
  22. bulkUpdate(
  23. api,
  24. {
  25. orgId,
  26. projectId,
  27. itemIds: [groupId],
  28. failSilently: true,
  29. data: {hasSeen: true},
  30. },
  31. {}
  32. );
  33. }
  34. export function fetchGroupUserReports(
  35. orgSlug: string,
  36. groupId: string,
  37. query: Record<string, string>
  38. ) {
  39. const api = new Client();
  40. return api.requestPromise(`/organizations/${orgSlug}/issues/${groupId}/user-reports/`, {
  41. includeAllArgs: true,
  42. query,
  43. });
  44. }
  45. export function useDefaultIssueEvent() {
  46. const user = useLegacyStore(ConfigStore).user;
  47. const options = user ? user.options : null;
  48. return options?.defaultIssueEvent ?? 'recommended';
  49. }
  50. /**
  51. * Combines two TagValue arrays and combines TagValue.count upon conflict
  52. */
  53. export function mergeAndSortTagValues(
  54. tagValues1: TagValue[],
  55. tagValues2: TagValue[],
  56. sort: 'count' | 'lastSeen' = 'lastSeen'
  57. ): TagValue[] {
  58. const tagValueCollection = tagValues1.reduce<Record<string, TagValue>>(
  59. (acc, tagValue) => {
  60. acc[tagValue.value] = tagValue;
  61. return acc;
  62. },
  63. {}
  64. );
  65. tagValues2.forEach(tagValue => {
  66. if (tagValueCollection[tagValue.value]) {
  67. tagValueCollection[tagValue.value].count += tagValue.count;
  68. if (tagValue.lastSeen > tagValueCollection[tagValue.value].lastSeen) {
  69. tagValueCollection[tagValue.value].lastSeen = tagValue.lastSeen;
  70. }
  71. } else {
  72. tagValueCollection[tagValue.value] = tagValue;
  73. }
  74. });
  75. const allTagValues: TagValue[] = Object.values(tagValueCollection);
  76. if (sort === 'count') {
  77. allTagValues.sort((a, b) => b.count - a.count);
  78. } else {
  79. allTagValues.sort((a, b) => (b.lastSeen < a.lastSeen ? -1 : 1));
  80. }
  81. return allTagValues;
  82. }
  83. /**
  84. * Returns the environment name for an event or null
  85. *
  86. * @param event
  87. */
  88. export function getEventEnvironment(event: Event) {
  89. const tag = event.tags.find(({key}) => key === 'environment');
  90. return tag ? tag.value : null;
  91. }
  92. const SUBSCRIPTION_REASONS = {
  93. commented: t(
  94. "You're receiving workflow notifications because you have commented on this issue."
  95. ),
  96. assigned: t(
  97. "You're receiving workflow notifications because you were assigned to this issue."
  98. ),
  99. bookmarked: t(
  100. "You're receiving workflow notifications because you have bookmarked this issue."
  101. ),
  102. changed_status: t(
  103. "You're receiving workflow notifications because you have changed the status of this issue."
  104. ),
  105. mentioned: t(
  106. "You're receiving workflow notifications because you have been mentioned in this issue."
  107. ),
  108. };
  109. /**
  110. * @param group
  111. * @param removeLinks add/remove links to subscription reasons text (default: false)
  112. * @returns Reason for subscription
  113. */
  114. export function getSubscriptionReason(group: Group) {
  115. if (group.subscriptionDetails?.disabled) {
  116. return t('You have disabled workflow notifications for this project.');
  117. }
  118. if (!group.isSubscribed) {
  119. return t('Subscribe to workflow notifications for this issue');
  120. }
  121. if (group.subscriptionDetails) {
  122. const {reason} = group.subscriptionDetails;
  123. if (reason === 'unknown') {
  124. return t(
  125. "You're receiving workflow notifications because you are subscribed to this issue."
  126. );
  127. }
  128. if (reason && SUBSCRIPTION_REASONS.hasOwnProperty(reason)) {
  129. return SUBSCRIPTION_REASONS[reason];
  130. }
  131. }
  132. return t(
  133. "You're receiving updates because you are subscribed to workflow notifications for this project."
  134. );
  135. }
  136. export function getGroupMostRecentActivity(
  137. activities: GroupActivity[] | undefined
  138. ): GroupActivity | undefined {
  139. // Most recent activity
  140. return activities
  141. ? orderBy([...activities], ({dateCreated}) => new Date(dateCreated), ['desc'])[0]
  142. : undefined;
  143. }
  144. export enum ReprocessingStatus {
  145. REPROCESSED_AND_HASNT_EVENT = 'reprocessed_and_hasnt_event',
  146. REPROCESSED_AND_HAS_EVENT = 'reprocessed_and_has_event',
  147. REPROCESSING = 'reprocessing',
  148. NO_STATUS = 'no_status',
  149. }
  150. // Reprocessing Checks
  151. export function getGroupReprocessingStatus(
  152. group: Group,
  153. mostRecentActivity?: GroupActivity
  154. ) {
  155. const {status, count, activity: activities} = group;
  156. const groupCount = Number(count);
  157. switch (status) {
  158. case 'reprocessing':
  159. return ReprocessingStatus.REPROCESSING;
  160. case 'unresolved': {
  161. const groupMostRecentActivity =
  162. mostRecentActivity ?? getGroupMostRecentActivity(activities);
  163. if (groupMostRecentActivity?.type === 'reprocess') {
  164. if (groupCount === 0) {
  165. return ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT;
  166. }
  167. return ReprocessingStatus.REPROCESSED_AND_HAS_EVENT;
  168. }
  169. return ReprocessingStatus.NO_STATUS;
  170. }
  171. default:
  172. return ReprocessingStatus.NO_STATUS;
  173. }
  174. }
  175. export const useFetchIssueTagsForDetailsPage = (
  176. {
  177. groupId,
  178. orgSlug,
  179. environment = [],
  180. isStatisticalDetector = false,
  181. statisticalDetectorParameters,
  182. }: {
  183. environment: string[];
  184. orgSlug: string;
  185. groupId?: string;
  186. isStatisticalDetector?: boolean;
  187. statisticalDetectorParameters?: {
  188. durationBaseline: number;
  189. end: string;
  190. start: string;
  191. transaction: string;
  192. };
  193. },
  194. {enabled = true}: {enabled?: boolean} = {}
  195. ) => {
  196. return useFetchIssueTags(
  197. {
  198. groupId,
  199. orgSlug,
  200. environment,
  201. readable: true,
  202. limit: 4,
  203. isStatisticalDetector,
  204. statisticalDetectorParameters,
  205. },
  206. {enabled}
  207. );
  208. };
  209. export function useEnvironmentsFromUrl(): string[] {
  210. const location = useLocation();
  211. const envs = location.query.environment;
  212. const envsArray = useMemo(() => {
  213. return typeof envs === 'string' ? [envs] : envs ?? [];
  214. }, [envs]);
  215. return envsArray;
  216. }
  217. export function getGroupDetailsQueryData({
  218. environments,
  219. }: {
  220. environments?: string[];
  221. } = {}): Record<string, string | string[]> {
  222. // Note, we do not want to include the environment key at all if there are no environments
  223. const query: Record<string, string | string[]> = {
  224. ...(environments && environments.length > 0 ? {environment: environments} : {}),
  225. expand: ['inbox', 'owners'],
  226. collapse: ['release', 'tags'],
  227. };
  228. return query;
  229. }
  230. export function getGroupEventDetailsQueryData({
  231. environments,
  232. query,
  233. stacktraceOnly,
  234. }: {
  235. environments?: string[];
  236. query?: string;
  237. stacktraceOnly?: boolean;
  238. } = {}): Record<string, string | string[]> {
  239. const defaultParams = {
  240. collapse: stacktraceOnly ? ['stacktraceOnly'] : ['fullRelease'],
  241. ...(query ? {query} : {}),
  242. };
  243. if (!environments || environments.length === 0) {
  244. return defaultParams;
  245. }
  246. return {...defaultParams, environment: environments};
  247. }
  248. export function useHasStreamlinedUI() {
  249. const location = useLocation();
  250. const user = useUser();
  251. if (location.query.streamline === '0') {
  252. return false;
  253. }
  254. return (
  255. location.query.streamline === '1' || !!user?.options?.prefersIssueDetailsStreamlinedUI
  256. );
  257. }
  258. export function useIsSampleEvent(): boolean {
  259. const params = useParams();
  260. const organization = useOrganization();
  261. const environments = useEnvironmentsFromUrl();
  262. const groupId = params.groupId;
  263. const group = GroupStore.get(groupId);
  264. const {data} = useFetchIssueTagsForDetailsPage(
  265. {
  266. groupId: groupId,
  267. orgSlug: organization.slug,
  268. environment: environments,
  269. },
  270. // Don't want this query to take precedence over the main requests
  271. {enabled: defined(group)}
  272. );
  273. return data?.some(tag => tag.key === 'sample_event') ?? false;
  274. }