import {
createContext,
type Dispatch,
type Reducer,
useCallback,
useContext,
useReducer,
} from 'react';
export const enum SectionKey {
/**
* Trace timeline or linked error
*/
TRACE = 'trace',
USER_FEEDBACK = 'user-feedback',
LLM_MONITORING = 'llm-monitoring',
UPTIME = 'uptime', // Only Uptime issues
DOWNTIME = 'downtime',
CRON_TIMELINE = 'cron-timeline', // Only Cron issues
HIGHLIGHTS = 'highlights',
RESOURCES = 'resources', // Position controlled by flag
EXCEPTION = 'exception',
STACKTRACE = 'stacktrace',
THREADS = 'threads',
SPANS = 'spans',
EVIDENCE = 'evidence',
MESSAGE = 'message',
SUSPECT_ROOT_CAUSE = 'suspect-root-cause',
SPAN_EVIDENCE = 'span-evidence',
HYDRATION_DIFF = 'hydration-diff',
REPLAY = 'replay',
HPKP = 'hpkp',
CSP = 'csp',
EXPECTCT = 'expectct',
EXPECTSTAPLE = 'expectstaple',
TEMPLATE = 'template',
BREADCRUMBS = 'breadcrumbs',
/**
* Also called images loaded
*/
DEBUGMETA = 'debugmeta',
REQUEST = 'request',
TAGS = 'tags',
SCREENSHOT = 'screenshot',
FEATURE_FLAGS = 'feature-flags',
CONTEXTS = 'contexts',
EXTRA = 'extra',
PACKAGES = 'packages',
DEVICE = 'device',
VIEW_HIERARCHY = 'view-hierarchy',
ATTACHMENTS = 'attachments',
SDK = 'sdk',
GROUPING_INFO = 'grouping-info',
PROCESSING_ERROR = 'processing-error',
RRWEB = 'rrweb', // Legacy integration prior to replays
MERGED_ISSUES = 'merged',
SIMILAR_ISSUES = 'similar',
REGRESSION_SUMMARY = 'regression-summary',
REGRESSION_BREAKPOINT_CHART = 'regression-breakpoint-chart',
REGRESSION_FLAMEGRAPH = 'regression-flamegraph',
REGRESSION_PROFILE_COMPARISON = 'regression-profile-comparison',
REGRESSION_EVENT_COMPARISON = 'regression-event-comparison',
REGRESSION_POTENTIAL_CAUSES = 'regression-potential-causes',
REGRESSION_AFFECTED_TRANSACTIONS = 'regression-affected-transactions',
}
/**
* This can be extended to create shared state for each section.
* For example, if we needed to know the number of context cards we're rendering,
* the can update the config for other components to read from.
*/
export interface SectionConfig {
key: SectionKey;
initialCollapse?: boolean;
}
export interface IssueDetailsContextType extends IssueDetailsState {
dispatch: Dispatch;
}
export const IssueDetailsContext = createContext({
sectionData: {},
isSidebarOpen: true,
navScrollMargin: 0,
eventCount: 0,
dispatch: () => {},
});
export function useIssueDetails() {
return useContext(IssueDetailsContext);
}
export interface IssueDetailsState {
/**
* Allows updating the event count based on the date/time/environment filters.
*/
eventCount: number;
/**
* Controls whether the sidebar is open.
*/
isSidebarOpen: boolean;
/**
* The margin to add to the 'Jump To' nav (accounts for the main app sidebar on small screen sizes).
*/
navScrollMargin: number;
/**
* Controls the state of each section.
*/
sectionData: {
[key in SectionKey]?: SectionConfig;
};
}
type UpdateEventSectionAction = {
key: SectionKey;
type: 'UPDATE_EVENT_SECTION';
config?: Partial;
};
type UpdateNavScrollMarginAction = {
margin: number;
type: 'UPDATE_NAV_SCROLL_MARGIN';
};
type UpdateEventCountAction = {
count: number;
type: 'UPDATE_EVENT_COUNT';
};
type UpdateSidebarAction = {
isOpen: boolean;
type: 'UPDATE_SIDEBAR_STATE';
};
export type IssueDetailsActions =
| UpdateEventSectionAction
| UpdateNavScrollMarginAction
| UpdateEventCountAction
| UpdateSidebarAction;
function updateEventSection(
state: IssueDetailsState,
sectionKey: SectionKey,
updatedConfig: Partial
): IssueDetailsState {
const existingConfig = state.sectionData[sectionKey] ?? {key: sectionKey};
const nextState: IssueDetailsState = {
...state,
sectionData: {
...state.sectionData,
[sectionKey]: {...existingConfig, ...updatedConfig},
},
};
return nextState;
}
/**
* If trying to use the current state of the issue/event page, you likely want to use
* `useIssueDetails` instead. This hook is just meant to create state for the provider.
*/
export function useIssueDetailsReducer() {
const initialState: IssueDetailsState = {
sectionData: {},
isSidebarOpen: true,
eventCount: 0,
navScrollMargin: 0,
};
const reducer: Reducer = useCallback(
(state, action): IssueDetailsState => {
switch (action.type) {
case 'UPDATE_SIDEBAR_STATE':
return {...state, isSidebarOpen: action.isOpen};
case 'UPDATE_NAV_SCROLL_MARGIN':
return {...state, navScrollMargin: action.margin};
case 'UPDATE_EVENT_SECTION':
return updateEventSection(state, action.key, action.config ?? {});
case 'UPDATE_EVENT_COUNT':
return {...state, eventCount: action.count};
default:
return state;
}
},
[]
);
const [issueDetails, dispatch] = useReducer(reducer, initialState);
return {
issueDetails,
dispatch,
};
}