utils.tsx 7.1 KB

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