context.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import {
  2. createContext,
  3. type Dispatch,
  4. type Reducer,
  5. useCallback,
  6. useContext,
  7. useReducer,
  8. } from 'react';
  9. export const enum SectionKey {
  10. /**
  11. * Trace timeline or linked error
  12. */
  13. TRACE = 'trace',
  14. USER_FEEDBACK = 'user-feedback',
  15. LLM_MONITORING = 'llm-monitoring',
  16. UPTIME = 'uptime', // Only Uptime issues
  17. DOWNTIME = 'downtime',
  18. CRON_TIMELINE = 'cron-timeline', // Only Cron issues
  19. HIGHLIGHTS = 'highlights',
  20. RESOURCES = 'resources', // Position controlled by flag
  21. EXCEPTION = 'exception',
  22. STACKTRACE = 'stacktrace',
  23. THREADS = 'threads',
  24. SPANS = 'spans',
  25. EVIDENCE = 'evidence',
  26. MESSAGE = 'message',
  27. SUSPECT_ROOT_CAUSE = 'suspect-root-cause',
  28. SPAN_EVIDENCE = 'span-evidence',
  29. HYDRATION_DIFF = 'hydration-diff',
  30. REPLAY = 'replay',
  31. HPKP = 'hpkp',
  32. CSP = 'csp',
  33. EXPECTCT = 'expectct',
  34. EXPECTSTAPLE = 'expectstaple',
  35. TEMPLATE = 'template',
  36. BREADCRUMBS = 'breadcrumbs',
  37. /**
  38. * Also called images loaded
  39. */
  40. DEBUGMETA = 'debugmeta',
  41. REQUEST = 'request',
  42. TAGS = 'tags',
  43. SCREENSHOT = 'screenshot',
  44. FEATURE_FLAGS = 'feature-flags',
  45. CONTEXTS = 'contexts',
  46. EXTRA = 'extra',
  47. PACKAGES = 'packages',
  48. DEVICE = 'device',
  49. VIEW_HIERARCHY = 'view-hierarchy',
  50. ATTACHMENTS = 'attachments',
  51. SDK = 'sdk',
  52. GROUPING_INFO = 'grouping-info',
  53. PROCESSING_ERROR = 'processing-error',
  54. RRWEB = 'rrweb', // Legacy integration prior to replays
  55. MERGED_ISSUES = 'merged',
  56. SIMILAR_ISSUES = 'similar',
  57. REGRESSION_SUMMARY = 'regression-summary',
  58. REGRESSION_BREAKPOINT_CHART = 'regression-breakpoint-chart',
  59. REGRESSION_FLAMEGRAPH = 'regression-flamegraph',
  60. REGRESSION_PROFILE_COMPARISON = 'regression-profile-comparison',
  61. REGRESSION_EVENT_COMPARISON = 'regression-event-comparison',
  62. REGRESSION_POTENTIAL_CAUSES = 'regression-potential-causes',
  63. REGRESSION_AFFECTED_TRANSACTIONS = 'regression-affected-transactions',
  64. }
  65. /**
  66. * This can be extended to create shared state for each section.
  67. * For example, if we needed to know the number of context cards we're rendering,
  68. * the <ContextDataSection /> can update the config for other components to read from.
  69. */
  70. export interface SectionConfig {
  71. key: SectionKey;
  72. initialCollapse?: boolean;
  73. }
  74. export interface IssueDetailsContextType extends IssueDetailsState {
  75. dispatch: Dispatch<IssueDetailsActions>;
  76. }
  77. export const IssueDetailsContext = createContext<IssueDetailsContextType>({
  78. sectionData: {},
  79. isSidebarOpen: true,
  80. navScrollMargin: 0,
  81. eventCount: 0,
  82. dispatch: () => {},
  83. });
  84. export function useIssueDetails() {
  85. return useContext(IssueDetailsContext);
  86. }
  87. export interface IssueDetailsState {
  88. /**
  89. * Allows updating the event count based on the date/time/environment filters.
  90. */
  91. eventCount: number;
  92. /**
  93. * Controls whether the sidebar is open.
  94. */
  95. isSidebarOpen: boolean;
  96. /**
  97. * The margin to add to the 'Jump To' nav (accounts for the main app sidebar on small screen sizes).
  98. */
  99. navScrollMargin: number;
  100. /**
  101. * Controls the state of each section.
  102. */
  103. sectionData: {
  104. [key in SectionKey]?: SectionConfig;
  105. };
  106. }
  107. type UpdateEventSectionAction = {
  108. key: SectionKey;
  109. type: 'UPDATE_EVENT_SECTION';
  110. config?: Partial<SectionConfig>;
  111. };
  112. type UpdateNavScrollMarginAction = {
  113. margin: number;
  114. type: 'UPDATE_NAV_SCROLL_MARGIN';
  115. };
  116. type UpdateEventCountAction = {
  117. count: number;
  118. type: 'UPDATE_EVENT_COUNT';
  119. };
  120. type UpdateSidebarAction = {
  121. isOpen: boolean;
  122. type: 'UPDATE_SIDEBAR_STATE';
  123. };
  124. export type IssueDetailsActions =
  125. | UpdateEventSectionAction
  126. | UpdateNavScrollMarginAction
  127. | UpdateEventCountAction
  128. | UpdateSidebarAction;
  129. function updateEventSection(
  130. state: IssueDetailsState,
  131. sectionKey: SectionKey,
  132. updatedConfig: Partial<SectionConfig>
  133. ): IssueDetailsState {
  134. const existingConfig = state.sectionData[sectionKey] ?? {key: sectionKey};
  135. const nextState: IssueDetailsState = {
  136. ...state,
  137. sectionData: {
  138. ...state.sectionData,
  139. [sectionKey]: {...existingConfig, ...updatedConfig},
  140. },
  141. };
  142. return nextState;
  143. }
  144. /**
  145. * If trying to use the current state of the issue/event page, you likely want to use
  146. * `useIssueDetails` instead. This hook is just meant to create state for the provider.
  147. */
  148. export function useIssueDetailsReducer() {
  149. const initialState: IssueDetailsState = {
  150. sectionData: {},
  151. isSidebarOpen: true,
  152. eventCount: 0,
  153. navScrollMargin: 0,
  154. };
  155. const reducer: Reducer<IssueDetailsState, IssueDetailsActions> = useCallback(
  156. (state, action): IssueDetailsState => {
  157. switch (action.type) {
  158. case 'UPDATE_SIDEBAR_STATE':
  159. return {...state, isSidebarOpen: action.isOpen};
  160. case 'UPDATE_NAV_SCROLL_MARGIN':
  161. return {...state, navScrollMargin: action.margin};
  162. case 'UPDATE_EVENT_SECTION':
  163. return updateEventSection(state, action.key, action.config ?? {});
  164. case 'UPDATE_EVENT_COUNT':
  165. return {...state, eventCount: action.count};
  166. default:
  167. return state;
  168. }
  169. },
  170. []
  171. );
  172. const [issueDetails, dispatch] = useReducer(reducer, initialState);
  173. return {
  174. issueDetails,
  175. dispatch,
  176. };
  177. }