tagStore.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import {createStore} from 'reflux';
  2. import type {Organization, Tag, TagCollection} from 'sentry/types';
  3. import {IssueCategory, IssueType, PriorityLevel} from 'sentry/types';
  4. import {SEMVER_TAGS} from 'sentry/utils/discover/fields';
  5. import {FieldKey, ISSUE_FIELDS} from 'sentry/utils/fields';
  6. import type {CommonStoreDefinition} from './types';
  7. // This list is only used on issues. Events/discover
  8. // have their own field list that exists elsewhere.
  9. // contexts.key and contexts.value omitted on purpose.
  10. const BUILTIN_TAGS = ISSUE_FIELDS.reduce<TagCollection>((acc, tag) => {
  11. acc[tag] = {key: tag, name: tag};
  12. return acc;
  13. }, {});
  14. interface TagStoreDefinition extends CommonStoreDefinition<TagCollection> {
  15. getIssueAttributes(org: Organization): TagCollection;
  16. getIssueTags(org: Organization): TagCollection;
  17. init(): void;
  18. loadTagsSuccess(data: Tag[]): void;
  19. reset(): void;
  20. state: TagCollection;
  21. }
  22. const storeConfig: TagStoreDefinition = {
  23. state: {},
  24. init() {
  25. // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux
  26. // listeners due to their leaky nature in tests.
  27. this.state = {};
  28. },
  29. /**
  30. * Gets only predefined issue attributes
  31. */
  32. getIssueAttributes(org: Organization) {
  33. // TODO(mitsuhiko): what do we do with translations here?
  34. const isSuggestions = [
  35. 'resolved',
  36. 'unresolved',
  37. ...['archived', 'escalating', 'new', 'ongoing', 'regressed'],
  38. 'assigned',
  39. 'unassigned',
  40. 'for_review',
  41. 'linked',
  42. 'unlinked',
  43. ];
  44. const sortedTagKeys = Object.keys(this.state).sort((a, b) => {
  45. return a.toLowerCase().localeCompare(b.toLowerCase());
  46. });
  47. const tagCollection = {
  48. [FieldKey.IS]: {
  49. key: FieldKey.IS,
  50. name: 'Status',
  51. values: isSuggestions,
  52. maxSuggestedValues: isSuggestions.length,
  53. predefined: true,
  54. },
  55. [FieldKey.HAS]: {
  56. key: FieldKey.HAS,
  57. name: 'Has Tag',
  58. values: sortedTagKeys,
  59. predefined: true,
  60. },
  61. [FieldKey.ASSIGNED]: {
  62. key: FieldKey.ASSIGNED,
  63. name: 'Assigned To',
  64. values: [],
  65. predefined: true,
  66. },
  67. [FieldKey.BOOKMARKS]: {
  68. key: FieldKey.BOOKMARKS,
  69. name: 'Bookmarked By',
  70. values: [],
  71. predefined: true,
  72. },
  73. [FieldKey.ISSUE_CATEGORY]: {
  74. key: FieldKey.ISSUE_CATEGORY,
  75. name: 'Issue Category',
  76. values: [
  77. IssueCategory.ERROR,
  78. IssueCategory.PERFORMANCE,
  79. IssueCategory.REPLAY,
  80. ...(org.features.includes('issue-platform') ? [IssueCategory.CRON] : []),
  81. ],
  82. predefined: true,
  83. },
  84. [FieldKey.ISSUE_TYPE]: {
  85. key: FieldKey.ISSUE_TYPE,
  86. name: 'Issue Type',
  87. values: [
  88. IssueType.PERFORMANCE_N_PLUS_ONE_DB_QUERIES,
  89. IssueType.PERFORMANCE_N_PLUS_ONE_API_CALLS,
  90. IssueType.PERFORMANCE_CONSECUTIVE_DB_QUERIES,
  91. IssueType.PERFORMANCE_SLOW_DB_QUERY,
  92. IssueType.PERFORMANCE_RENDER_BLOCKING_ASSET,
  93. IssueType.PERFORMANCE_UNCOMPRESSED_ASSET,
  94. ...(org.features.includes('issue-platform')
  95. ? [
  96. IssueType.PERFORMANCE_ENDPOINT_REGRESSION,
  97. IssueType.PROFILE_FILE_IO_MAIN_THREAD,
  98. IssueType.PROFILE_IMAGE_DECODE_MAIN_THREAD,
  99. IssueType.PROFILE_JSON_DECODE_MAIN_THREAD,
  100. IssueType.PROFILE_REGEX_MAIN_THREAD,
  101. IssueType.PROFILE_FUNCTION_REGRESSION,
  102. ]
  103. : []),
  104. ],
  105. predefined: true,
  106. },
  107. [FieldKey.LAST_SEEN]: {
  108. key: FieldKey.LAST_SEEN,
  109. name: 'Last Seen',
  110. values: [],
  111. predefined: false,
  112. },
  113. [FieldKey.FIRST_SEEN]: {
  114. key: FieldKey.FIRST_SEEN,
  115. name: 'First Seen',
  116. values: [],
  117. predefined: false,
  118. },
  119. [FieldKey.FIRST_RELEASE]: {
  120. key: FieldKey.FIRST_RELEASE,
  121. name: 'First Release',
  122. values: ['latest'],
  123. predefined: true,
  124. },
  125. [FieldKey.EVENT_TIMESTAMP]: {
  126. key: FieldKey.EVENT_TIMESTAMP,
  127. name: 'Event Timestamp',
  128. values: [],
  129. predefined: true,
  130. },
  131. [FieldKey.TIMES_SEEN]: {
  132. key: FieldKey.TIMES_SEEN,
  133. name: 'Times Seen',
  134. isInput: true,
  135. // Below values are required or else SearchBar will attempt to get values
  136. // This is required or else SearchBar will attempt to get values
  137. values: [],
  138. predefined: true,
  139. },
  140. [FieldKey.ASSIGNED_OR_SUGGESTED]: {
  141. key: FieldKey.ASSIGNED_OR_SUGGESTED,
  142. name: 'Assigned or Suggested',
  143. isInput: true,
  144. values: [],
  145. predefined: true,
  146. },
  147. };
  148. if (org.features.includes('issue-priority-ui')) {
  149. tagCollection[FieldKey.ISSUE_PRIORITY] = {
  150. key: FieldKey.ISSUE_PRIORITY,
  151. name: 'Issue Priority',
  152. values: [PriorityLevel.HIGH, PriorityLevel.MEDIUM, PriorityLevel.LOW],
  153. predefined: true,
  154. };
  155. }
  156. // Ony include fields that that are part of the ISSUE_FIELDS. This is
  157. // because we may sometimes have fields that are turned off by removing
  158. // them from ISSUE_FIELDS
  159. const filteredCollection = Object.entries(tagCollection).filter(([key]) =>
  160. ISSUE_FIELDS.includes(key as FieldKey)
  161. );
  162. return Object.fromEntries(filteredCollection);
  163. },
  164. /**
  165. * Get all tags including builtin issue tags and issue attributes
  166. */
  167. getIssueTags(org: Organization) {
  168. const issueTags = {
  169. ...BUILTIN_TAGS,
  170. ...SEMVER_TAGS,
  171. // State tags should overwrite built ins.
  172. ...this.state,
  173. // We want issue attributes to overwrite any built in and state tags
  174. ...this.getIssueAttributes(org),
  175. };
  176. if (!org.features.includes('device-classification')) {
  177. delete issueTags[FieldKey.DEVICE_CLASS];
  178. }
  179. return issueTags;
  180. },
  181. getState() {
  182. return this.state;
  183. },
  184. reset() {
  185. this.state = {};
  186. this.trigger(this.state);
  187. },
  188. loadTagsSuccess(data) {
  189. // Note: We could probably stop cloning the data here and just
  190. // assign to this.state directly, but there is a change someone may
  191. // be relying on referential equality somewhere in the codebase and
  192. // we dont want to risk breaking that.
  193. const newState = {};
  194. for (let i = 0; i < data.length; i++) {
  195. const tag = data[i];
  196. newState[tag.key] = {
  197. values: [],
  198. ...tag,
  199. };
  200. }
  201. // We will iterate through the previous tags in reverse so that previously
  202. // added tags are carried over first. We rely on browser implementation
  203. // of Object.keys() to return keys in insertion order.
  204. const previousTagKeys = Object.keys(this.state);
  205. const MAX_STORE_SIZE = 2000;
  206. // We will carry over the previous tags until we reach the max store size
  207. const toCarryOver = Math.max(0, MAX_STORE_SIZE - data.length);
  208. let carriedOver = 0;
  209. while (previousTagKeys.length > 0 && carriedOver < toCarryOver) {
  210. const tagKey = previousTagKeys.pop();
  211. if (tagKey === undefined) {
  212. // Should be unreachable, but just in case
  213. break;
  214. }
  215. // If the new state already has a previous tag then we will not carry it over
  216. // and use the latest tag in the store instead.
  217. if (newState[tagKey]) {
  218. continue;
  219. }
  220. // Else override the tag with the previous tag
  221. newState[tagKey] = this.state[tagKey];
  222. carriedOver++;
  223. }
  224. this.state = newState;
  225. this.trigger(newState);
  226. },
  227. };
  228. const TagStore = createStore(storeConfig);
  229. export default TagStore;