tagStore.tsx 7.1 KB

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