Repository: PostHog/posthog
Pull request #30019: fix(err): ui refresh
Author: login: hpouillot
Created: 2025-03-17T14:49:40Z | Merged: 2025-03-19T11:21:01Z

## Changes

- Bulk edition for all issue status and filters out accessible status based on selection
- Fix cache on issue update. When updating an issue (status or assignee) new data is not fetched properly because of cache
- Introduce color code for issue status
- UI Refresh on issue detail page :
	- Move issue data to widgets
	- Move session recording to modal
	- Use tabs to switch between overview and events list
	- Add toggle to reorder stacktrace

## Screenshots

![image](https://github.com/user-attachments/assets/2d1d3869-4e1a-42ae-ad72-fb516a02f524)
![image](https://github.com/user-attachments/assets/c884596b-faa1-413b-bea5-0d48164cbce7)




Diff:
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index f15312aed2d1..d92c698527e1 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -2142,12 +2142,13 @@ const api = {
             return await new ApiRequest().errorTrackingAssignIssue(id).update({ data: { assignee } })
         },
 
-        async bulkResolve(ids: ErrorTrackingIssue['id'][]): Promise<{ content: string }> {
-            return await new ApiRequest().errorTrackingIssueBulk().create({ data: { action: 'resolve', ids } })
-        },
-
-        async bulkSuppress(ids: ErrorTrackingIssue['id'][]): Promise<{ content: string }> {
-            return await new ApiRequest().errorTrackingIssueBulk().create({ data: { action: 'suppress', ids } })
+        async bulkMarkStatus(
+            ids: ErrorTrackingIssue['id'][],
+            status: ErrorTrackingIssue['status']
+        ): Promise<{ content: string }> {
+            return await new ApiRequest()
+                .errorTrackingIssueBulk()
+                .create({ data: { action: 'set_status', ids, status: status } })
         },
 
         async bulkAssign(
diff --git a/frontend/src/lib/components/Errors/StackTraces.tsx b/frontend/src/lib/components/Errors/StackTraces.tsx
index 168d2a38ef62..ecf1c8e2547f 100644
--- a/frontend/src/lib/components/Errors/StackTraces.tsx
+++ b/frontend/src/lib/components/Errors/StackTraces.tsx
@@ -38,7 +38,7 @@ export function ChainedStackTraces({
     }, [exceptionList, loadFromRawIds])
 
     return (
-        <>
+        <div className="flex flex-col gap-y-2">
             {exceptionList.map(({ stacktrace, value, type }, index) => {
                 if (stacktrace && stacktrace.type === 'resolved') {
                     const { frames } = stacktrace
@@ -53,8 +53,13 @@ export function ChainedStackTraces({
                             className={clsx('StackTrace flex flex-col gap-y-2', embedded && 'StackTrace--embedded')}
                         >
                             <div className="flex flex-col gap-0.5">
-                                <h3 className="StackTrace__type mb-0">{type}</h3>
-                                <div className="StackTrace__value line-clamp-2 text-secondary italic text-xs">
+                                <h3 className="StackTrace__type mb-0" title={type}>
+                                    {type}
+                                </h3>
+                                <div
+                                    className="StackTrace__value line-clamp-2 text-secondary italic text-xs"
+                                    title={value}
+                                >
                                     {value}
                                 </div>
                             </div>
@@ -63,7 +68,7 @@ export function ChainedStackTraces({
                     )
                 }
             })}
-        </>
+        </div>
     )
 }
 
diff --git a/frontend/src/lib/components/Errors/stackFrameLogic.ts b/frontend/src/lib/components/Errors/stackFrameLogic.ts
index ae4668410b74..de23c4b09729 100644
--- a/frontend/src/lib/components/Errors/stackFrameLogic.ts
+++ b/frontend/src/lib/components/Errors/stackFrameLogic.ts
@@ -21,6 +21,7 @@ export const stackFrameLogic = kea<stackFrameLogicType>([
         loadFromRawIds: (rawIds: ErrorTrackingStackFrame['raw_id'][]) => ({ rawIds }),
         loadForSymbolSet: (symbolSetId: ErrorTrackingSymbolSet['id']) => ({ symbolSetId }),
         setShowAllFrames: (showAllFrames: boolean) => ({ showAllFrames }),
+        reverseFrameOrder: (reverseOrder: boolean) => ({ reverseOrder }),
     }),
 
     reducers(() => ({
@@ -31,6 +32,13 @@ export const stackFrameLogic = kea<stackFrameLogicType>([
                 setShowAllFrames: (_, { showAllFrames }) => showAllFrames,
             },
         ],
+        frameOrderReversed: [
+            false,
+            { persist: true },
+            {
+                reverseFrameOrder: (_, { reverseOrder }) => reverseOrder,
+            },
+        ],
     })),
 
     loaders(({ values }) => ({
diff --git a/frontend/src/scenes/error-tracking/AssigneeSelect.tsx b/frontend/src/scenes/error-tracking/AssigneeSelect.tsx
index 6520959ee407..a1c6543901c5 100644
--- a/frontend/src/scenes/error-tracking/AssigneeSelect.tsx
+++ b/frontend/src/scenes/error-tracking/AssigneeSelect.tsx
@@ -35,7 +35,7 @@ export const AssigneeSelect = ({
 
     useEffect(() => {
         ensureAssigneeTypesLoaded()
-    }, [])
+    }, [ensureAssigneeTypesLoaded])
 
     return (
         <LemonDropdown
diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx
index f62243afd340..665b4937f888 100644
--- a/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx
+++ b/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx
@@ -15,7 +15,7 @@ const errorTrackingDateOptions = dateMapping.filter((dm) => dm.key != 'Yesterday
 
 export const ErrorTrackingFilters = (): JSX.Element => {
     return (
-        <div className="deprecated-space-y-1">
+        <div className="space-y-1">
             <div className="flex gap-2 items-center">
                 <DateRange />
                 <FilterGroup />
@@ -31,19 +31,21 @@ const FilterGroup = (): JSX.Element => {
     const { setFilterGroup } = useActions(errorTrackingLogic)
 
     return (
-        <UniversalFilters
-            rootKey="error-tracking"
-            group={filterGroup}
-            // TODO: Probably makes sense to create a new taxonomic group for exception-specific event property filters only, keep it clean.
-            taxonomicGroupTypes={[
-                TaxonomicFilterGroupType.EventProperties,
-                TaxonomicFilterGroupType.PersonProperties,
-                TaxonomicFilterGroupType.Cohorts,
-            ]}
-            onChange={setFilterGroup}
-        >
-            <RecordingsUniversalFilterGroup />
-        </UniversalFilters>
+        <span className="bg-surface-primary rounded">
+            <UniversalFilters
+                rootKey="error-tracking"
+                group={filterGroup}
+                // TODO: Probably makes sense to create a new taxonomic group for exception-specific event property filters only, keep it clean.
+                taxonomicGroupTypes={[
+                    TaxonomicFilterGroupType.EventProperties,
+                    TaxonomicFilterGroupType.PersonProperties,
+                    TaxonomicFilterGroupType.Cohorts,
+                ]}
+                onChange={setFilterGroup}
+            >
+                <RecordingsUniversalFilterGroup />
+            </UniversalFilters>
+        </span>
     )
 }
 
@@ -84,15 +86,17 @@ const DateRange = (): JSX.Element => {
     const { setDateRange } = useActions(errorTrackingLogic)
 
     return (
-        <DateFilter
-            size="small"
-            dateFrom={dateRange.date_from}
-            dateTo={dateRange.date_to}
-            dateOptions={errorTrackingDateOptions}
-            onChange={(changedDateFrom, changedDateTo) =>
-                setDateRange({ date_from: changedDateFrom, date_to: changedDateTo })
-            }
-        />
+        <span className="bg-surface-primary rounded">
+            <DateFilter
+                size="small"
+                dateFrom={dateRange.date_from}
+                dateTo={dateRange.date_to}
+                dateOptions={errorTrackingDateOptions}
+                onChange={(changedDateFrom, changedDateTo) =>
+                    setDateRange({ date_from: changedDateFrom, date_to: changedDateTo })
+                }
+            />
+        </span>
     )
 }
 
diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx
index 6a57df94f964..b30bfc6bbb4b 100644
--- a/frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx
+++ b/frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx
@@ -1,20 +1,23 @@
 import './ErrorTracking.scss'
 
-import { LemonButton, LemonDivider, Spinner } from '@posthog/lemon-ui'
+import { LemonTabs, Spinner } from '@posthog/lemon-ui'
 import { useActions, useValues } from 'kea'
 import { PageHeader } from 'lib/components/PageHeader'
-import PanelLayout from 'lib/components/PanelLayout/PanelLayout'
-import { useEffect } from 'react'
+import { useEffect, useState } from 'react'
 import { SceneExport } from 'scenes/sceneTypes'
 
 import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
 
 import { AssigneeSelect } from './AssigneeSelect'
+import { ErrorTrackingFilters } from './ErrorTrackingFilters'
 import { errorTrackingIssueSceneLogic } from './errorTrackingIssueSceneLogic'
 import { ErrorTrackingSetupPrompt } from './ErrorTrackingSetupPrompt'
-import { Events } from './issue/Events'
+import { GenericSelect } from './issue/GenericSelect'
+import { IssueStatus, StatusIndicator } from './issue/Indicator'
 import { Metadata } from './issue/Metadata'
-import { SparklinePanel } from './issue/Sparkline'
+import { EventsTab } from './issue/tabs/EventsTab'
+import { DetailsWidget } from './issue/widgets/DetailsWidget'
+import { StacktraceWidget } from './issue/widgets/StacktraceWidget'
 
 export const scene: SceneExport = {
     component: ErrorTrackingIssueScene,
@@ -36,72 +39,76 @@ export const STATUS_LABEL: Record<ErrorTrackingIssue['status'], string> = {
 export function ErrorTrackingIssueScene(): JSX.Element {
     const { issue } = useValues(errorTrackingIssueSceneLogic)
     const { updateIssue, initIssue, assignIssue } = useActions(errorTrackingIssueSceneLogic)
+    const [activeTab, setActiveTab] = useState('stacktrace')
 
     useEffect(() => {
         initIssue()
-    }, [])
+    }, [initIssue])
 
     return (
         <ErrorTrackingSetupPrompt>
             <>
                 <PageHeader
                     buttons={
-                        issue ? (
-                            issue.status === 'active' ? (
-                                <div className="flex divide-x gap-x-2">
-                                    <AssigneeSelect
-                                        assignee={issue.assignee}
-                                        onChange={assignIssue}
-                                        type="secondary"
-                                        showName
-                                    />
-                                    <div className="flex pl-2 gap-x-2">
-                                        <LemonButton
-                                            type="secondary"
-                                            onClick={() => updateIssue({ status: 'archived' })}
-                                        >
-                                            Archive
-                                        </LemonButton>
-                                        <LemonButton type="primary" onClick={() => updateIssue({ status: 'resolved' })}>
-                                            Resolve
-                                        </LemonButton>
-                                        <LemonButton
-                                            type="secondary"
-                                            status="danger"
-                                            tooltip="Stop capturing these errors"
-                                            onClick={() => updateIssue({ status: 'suppressed' })}
-                                        >
-                                            Suppress
-                                        </LemonButton>
-                                    </div>
-                                </div>
-                            ) : (
-                                <LemonButton
+                        <div className="flex gap-x-2">
+                            {issue && issue.status == 'active' && (
+                                <AssigneeSelect
+                                    assignee={issue.assignee}
+                                    onChange={assignIssue}
                                     type="secondary"
-                                    className="upcasefirst-letter:uppercase"
-                                    onClick={() => updateIssue({ status: 'active' })}
-                                    tooltip="Mark as active"
-                                >
-                                    {STATUS_LABEL[issue.status]}
-                                </LemonButton>
-                            )
-                        ) : (
-                            false
-                        )
+                                    showName
+                                />
+                            )}
+                            {issue && (
+                                <GenericSelect
+                                    size="small"
+                                    current={issue.status}
+                                    values={['active', 'resolved', 'suppressed']}
+                                    placeholder="Mark as"
+                                    renderValue={(value) => (
+                                        <StatusIndicator
+                                            status={value as IssueStatus}
+                                            size="small"
+                                            withTooltip={true}
+                                        />
+                                    )}
+                                    onChange={(value) => updateIssue({ status: value })}
+                                />
+                            )}
+                        </div>
                     }
                 />
                 {issue ? (
                     <div className="ErrorTrackingIssue">
                         <Metadata />
-                        <LemonDivider className="my-4" />
-                        <PanelLayout>
-                            <PanelLayout.Container column primary>
-                                <SparklinePanel />
-                                <PanelLayout.Panel primary>
-                                    <Events />
-                                </PanelLayout.Panel>
-                            </PanelLayout.Container>
-                        </PanelLayout>
+                        <LemonTabs
+                            activeKey={activeTab}
+                            onChange={(key) => setActiveTab(key)}
+                            tabs={[
+                                {
+                                    key: 'stacktrace',
+                                    label: 'Overview',
+                                    content: (
+                                        <div className="space-y-2">
+                                            <DetailsWidget />
+                                            <StacktraceWidget />
+                                        </div>
+                                    ),
+                                },
+                                {
+                                    key: 'events',
+                                    label: 'Events',
+                                    content: (
+                                        <div className="space-y-2">
+                                            <ErrorTrackingFilters />
+                                            <div className="border-1 overflow-hidden border-accent-primary border-primary rounded bg-surface-primary relative">
+                                                <EventsTab />
+                                            </div>
+                                        </div>
+                                    ),
+                                },
+                            ]}
+                        />
                     </div>
                 ) : (
                     <Spinner />
diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingListOptions.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingListOptions.tsx
index e03f54a3cac2..2a0375f9b25d 100644
--- a/frontend/src/scenes/error-tracking/ErrorTrackingListOptions.tsx
+++ b/frontend/src/scenes/error-tracking/ErrorTrackingListOptions.tsx
@@ -1,115 +1,46 @@
-import { LemonButton, LemonSelect } from '@posthog/lemon-ui'
+import { LemonSelect } from '@posthog/lemon-ui'
 import { useActions, useValues } from 'kea'
 
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
 import { AssigneeSelect } from './AssigneeSelect'
-import { errorTrackingDataNodeLogic } from './errorTrackingDataNodeLogic'
 import { errorTrackingLogic } from './errorTrackingLogic'
 import { errorTrackingSceneLogic } from './errorTrackingSceneLogic'
+import { BulkActions } from './issue/BulkActions'
+import { GenericSelect } from './issue/GenericSelect'
+import { LabelIndicator, StatusIndicator } from './issue/Indicator'
 
 export const ErrorTrackingListOptions = (): JSX.Element => {
     const { assignee } = useValues(errorTrackingLogic)
     const { setAssignee } = useActions(errorTrackingLogic)
     const { orderBy, status, orderDirection } = useValues(errorTrackingSceneLogic)
     const { setOrderBy, setStatus, setOrderDirection } = useActions(errorTrackingSceneLogic)
-    const { results } = useValues(errorTrackingDataNodeLogic)
-
     const { selectedIssueIds } = useValues(errorTrackingSceneLogic)
-    const { setSelectedIssueIds } = useActions(errorTrackingSceneLogic)
-    const { mergeIssues, assignIssues, resolveIssues, suppressIssues } = useActions(errorTrackingDataNodeLogic)
 
     return (
         <>
             <div className="sticky top-[var(--breadcrumbs-height-compact)] z-20 py-2 bg-primary flex justify-between">
                 <div className="flex deprecated-space-x-2">
-                    {selectedIssueIds.length > 0 ? (
-                        <>
-                            <LemonButton type="secondary" size="small" onClick={() => setSelectedIssueIds([])}>
-                                Unselect all
-                            </LemonButton>
-                            <LemonButton
-                                disabledReason={
-                                    selectedIssueIds.length < 2 ? 'Select at least two issues to merge' : null
-                                }
-                                type="secondary"
-                                size="small"
-                                onClick={() => {
-                                    mergeIssues(selectedIssueIds)
-                                    setSelectedIssueIds([])
-                                }}
-                            >
-                                Merge
-                            </LemonButton>
-                            <LemonButton
-                                type="secondary"
-                                size="small"
-                                onClick={() => {
-                                    resolveIssues(selectedIssueIds)
-                                    setSelectedIssueIds([])
-                                }}
-                            >
-                                Resolve
-                            </LemonButton>
-                            <LemonButton
-                                type="secondary"
-                                size="small"
-                                status="danger"
-                                tooltip="Stop capturing these errors"
-                                onClick={() => {
-                                    suppressIssues(selectedIssueIds)
-                                    setSelectedIssueIds([])
-                                }}
-                            >
-                                Suppress
-                            </LemonButton>
-                            <AssigneeSelect
-                                type="secondary"
-                                size="small"
-                                showName
-                                showIcon={false}
-                                unassignedLabel="Assign"
-                                assignee={null}
-                                onChange={(assignee) => assignIssues(selectedIssueIds, assignee)}
-                            />
-                        </>
-                    ) : (
-                        <LemonButton
-                            type="secondary"
-                            size="small"
-                            onClick={() => setSelectedIssueIds(results.map((issue) => issue.id))}
-                        >
-                            Select all
-                        </LemonButton>
-                    )}
+                    <BulkActions />
                 </div>
                 {selectedIssueIds.length < 1 && (
                     <span className="flex deprecated-space-x-2">
-                        <div className="flex items-center gap-1">
-                            <span>Status:</span>
-                            <LemonSelect
-                                onSelect={setStatus}
-                                onChange={setStatus}
-                                value={status}
-                                options={[
-                                    {
-                                        value: 'all',
-                                        label: 'All',
-                                    },
-                                    {
-                                        value: 'active',
-                                        label: 'Active',
-                                    },
-                                    {
-                                        value: 'resolved',
-                                        label: 'Resolved',
-                                    },
-                                    {
-                                        value: 'suppressed',
-                                        label: 'Suppressed',
-                                    },
-                                ]}
-                                size="small"
-                            />
-                        </div>
+                        <GenericSelect<ErrorTrackingIssue['status'] | 'all' | null>
+                            values={['all', 'active', 'resolved', 'suppressed']}
+                            current={status || null}
+                            renderValue={(key) => {
+                                switch (key) {
+                                    case 'all':
+                                    case null:
+                                        return <LabelIndicator intent="muted" label="All" size="small" />
+                                    default:
+                                        return <StatusIndicator status={key} size="small" />
+                                }
+                            }}
+                            placeholder="Select status"
+                            onChange={(value) => setStatus(value || undefined)}
+                            size="small"
+                        />
                         <div className="flex items-center gap-1">
                             <span>Sort by:</span>
                             <LemonSelect
diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx
index 1354cfa5a615..a62f9cfe9271 100644
--- a/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx
+++ b/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx
@@ -6,7 +6,6 @@ import {
     LemonDivider,
     LemonSegmentedButton,
     LemonSkeleton,
-    LemonTag,
     Link,
     Tooltip,
 } from '@posthog/lemon-ui'
@@ -15,9 +14,10 @@ import { FeedbackNotice } from 'lib/components/FeedbackNotice'
 import { PageHeader } from 'lib/components/PageHeader'
 import { Sparkline } from 'lib/components/Sparkline'
 import { TZLabel } from 'lib/components/TZLabel'
-import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink'
+import { FloatingContainerContext } from 'lib/hooks/useFloatingContainerContext'
 import { humanFriendlyLargeNumber } from 'lib/utils'
 import { posthog } from 'posthog-js'
+import { useRef } from 'react'
 import { SceneExport } from 'scenes/sceneTypes'
 import { urls } from 'scenes/urls'
 import { userLogic } from 'scenes/userLogic'
@@ -31,12 +31,12 @@ import { InsightLogicProps } from '~/types'
 import { AssigneeSelect } from './AssigneeSelect'
 import { errorTrackingDataNodeLogic } from './errorTrackingDataNodeLogic'
 import { ErrorTrackingFilters } from './ErrorTrackingFilters'
-import { STATUS_LABEL } from './ErrorTrackingIssueScene'
 import { errorTrackingIssueSceneLogic } from './errorTrackingIssueSceneLogic'
 import { ErrorTrackingListOptions } from './ErrorTrackingListOptions'
 import { errorTrackingLogic } from './errorTrackingLogic'
 import { errorTrackingSceneLogic } from './errorTrackingSceneLogic'
 import { ErrorTrackingSetupPrompt } from './ErrorTrackingSetupPrompt'
+import { StatusIndicator } from './issue/Indicator'
 import { sparklineLabels, sparklineLabelsDay, sparklineLabelsMonth } from './utils'
 
 export const scene: SceneExport = {
@@ -47,7 +47,7 @@ export const scene: SceneExport = {
 export function ErrorTrackingScene(): JSX.Element {
     const { hasSentExceptionEvent, hasSentExceptionEventLoading } = useValues(errorTrackingLogic)
     const { query } = useValues(errorTrackingSceneLogic)
-
+    const floatingContainerRef = useRef<HTMLDivElement>(null)
     const insightProps: InsightLogicProps = {
         dashboardItemId: 'new-ErrorTrackingQuery',
     }
@@ -72,20 +72,22 @@ export function ErrorTrackingScene(): JSX.Element {
     }
 
     return (
-        <ErrorTrackingSetupPrompt>
-            <BindLogic logic={errorTrackingDataNodeLogic} props={{ query, key: insightVizDataNodeKey(insightProps) }}>
-                <Header />
-                {hasSentExceptionEventLoading ? null : hasSentExceptionEvent ? (
-                    <FeedbackNotice text="Error tracking is currently in beta. Thanks for taking part! We'd love to hear what you think." />
-                ) : (
-                    <IngestionStatusCheck />
-                )}
-                <ErrorTrackingFilters />
-                <LemonDivider className="mt-2" />
-                <ErrorTrackingListOptions />
-                <Query query={query} context={context} />
-            </BindLogic>
-        </ErrorTrackingSetupPrompt>
+        <FloatingContainerContext.Provider value={floatingContainerRef}>
+            <ErrorTrackingSetupPrompt>
+                <BindLogic logic={errorTrackingDataNodeLogic} props={{ key: insightVizDataNodeKey(insightProps) }}>
+                    <Header />
+                    {hasSentExceptionEventLoading ? null : hasSentExceptionEvent ? (
+                        <FeedbackNotice text="Error tracking is currently in beta. Thanks for taking part! We'd love to hear what you think." />
+                    ) : (
+                        <IngestionStatusCheck />
+                    )}
+                    <ErrorTrackingFilters />
+                    <LemonDivider className="mt-2" />
+                    <ErrorTrackingListOptions />
+                    <Query query={query} context={context} />
+                </BindLogic>
+            </ErrorTrackingSetupPrompt>
+        </FloatingContainerContext.Provider>
     )
 }
 
@@ -133,15 +135,13 @@ const VolumeColumnHeader: QueryContextColumnTitleComponent = ({ columnName }) =>
 const CustomGroupTitleColumn: QueryContextColumnComponent = (props) => {
     const { selectedIssueIds } = useValues(errorTrackingSceneLogic)
     const { setSelectedIssueIds } = useActions(errorTrackingSceneLogic)
-
     const record = props.record as ErrorTrackingIssue
-
     const checked = selectedIssueIds.includes(record.id)
 
     return (
-        <div className="flex items-start gap-x-1.5 group">
+        <div className="flex items-start gap-x-2 group my-1">
             <LemonCheckbox
-                className="pt-1"
+                className="h-[1.2rem]"
                 checked={checked}
                 onChange={(newValue) => {
                     setSelectedIssueIds(
@@ -151,34 +151,34 @@ const CustomGroupTitleColumn: QueryContextColumnComponent = (props) => {
                     )
                 }}
             />
-            <LemonTableLink
-                title={record.name || 'Unknown Type'}
-                description={
-                    <div className="deprecated-space-y-1">
-                        <div className="line-clamp-1">{record.description}</div>
-                        <div className="deprecated-space-x-1">
-                            <TZLabel time={record.first_seen} className="border-dotted border-b" delayMs={750} />
-                            <span>|</span>
-                            {record.last_seen ? (
-                                <TZLabel time={record.last_seen} className="border-dotted border-b" delayMs={750} />
-                            ) : (
-                                <LemonSkeleton />
-                            )}
-                            <span>|</span>
-                            <span>
-                                <LemonTag>{STATUS_LABEL[record.status]}</LemonTag>
-                            </span>
-                        </div>
+
+            <div className="flex flex-col gap-[2px]">
+                <Link
+                    className="flex-1 pr-12"
+                    to={urls.errorTrackingIssue(record.id)}
+                    onClick={() => {
+                        const issueLogic = errorTrackingIssueSceneLogic({ id: record.id })
+                        issueLogic.mount()
+                        issueLogic.actions.setIssue(record)
+                    }}
+                >
+                    <div className="flex items-center font-semibold h-[1.2rem] text-[1.2em]">
+                        {record.name || 'Unknown Type'}
                     </div>
-                }
-                className="flex-1 pr-12"
-                to={urls.errorTrackingIssue(record.id)}
-                onClick={() => {
-                    const issueLogic = errorTrackingIssueSceneLogic({ id: record.id })
-                    issueLogic.mount()
-                    issueLogic.actions.setIssue(record)
-                }}
-            />
+                </Link>
+                <div className="line-clamp-1 text-gray-600">{record.description}</div>
+                <div className="flex gap-1 items-center text-gray-500">
+                    <StatusIndicator size="xsmall" status={record.status} />
+                    <span>|</span>
+                    <TZLabel time={record.first_seen} className="border-dotted border-b text-xs" delayMs={750} />
+                    <span>|</span>
+                    {record.last_seen ? (
+                        <TZLabel time={record.last_seen} className="border-dotted border-b text-xs" delayMs={750} />
+                    ) : (
+                        <LemonSkeleton />
+                    )}
+                </div>
+            </div>
         </div>
     )
 }
diff --git a/frontend/src/scenes/error-tracking/errorTrackingActivityDescriber.tsx b/frontend/src/scenes/error-tracking/errorTrackingActivityDescriber.tsx
index 267260a7c6fd..891286af5117 100644
--- a/frontend/src/scenes/error-tracking/errorTrackingActivityDescriber.tsx
+++ b/frontend/src/scenes/error-tracking/errorTrackingActivityDescriber.tsx
@@ -27,7 +27,7 @@ function AssigneeRenderer({ assignee }: { assignee: ErrorTrackingIssueAssignee }
 
     useEffect(() => {
         ensureAssigneeTypesLoaded()
-    }, [])
+    }, [ensureAssigneeTypesLoaded])
 
     return (
         <AssigneeDisplay assignee={assignee}>
diff --git a/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx b/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx
index e41a3609b213..264b38029ecb 100644
--- a/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx
+++ b/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx
@@ -17,17 +17,22 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
     path(['scenes', 'error-tracking', 'errorTrackingDataNodeLogic']),
     props({} as ErrorTrackingDataNodeLogicProps),
 
-    connect(({ key, query }: ErrorTrackingDataNodeLogicProps) => ({
-        values: [dataNodeLogic({ key, query }), ['response']],
-        actions: [dataNodeLogic({ key, query }), ['setResponse', 'loadData']],
-    })),
+    connect(({ key, query }: ErrorTrackingDataNodeLogicProps) => {
+        const nodeLogic = dataNodeLogic({ key, query, refresh: 'blocking' })
+        return {
+            values: [nodeLogic, ['response']],
+            actions: [nodeLogic, ['setResponse', 'loadData']],
+        }
+    }),
 
     actions({
         mergeIssues: (ids: string[]) => ({ ids }),
         resolveIssues: (ids: string[]) => ({ ids }),
         suppressIssues: (ids: string[]) => ({ ids }),
+        activateIssues: (ids: string[]) => ({ ids }),
         assignIssues: (ids: string[], assignee: ErrorTrackingIssue['assignee']) => ({ ids, assignee }),
         assignIssue: (id: string, assignee: ErrorTrackingIssue['assignee']) => ({ id, assignee }),
+        reloadData: () => ({}),
     }),
 
     selectors({
@@ -35,6 +40,9 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
     }),
 
     listeners(({ values, actions }) => ({
+        reloadData: async () => {
+            actions.loadData('force_blocking')
+        },
         mergeIssues: async ({ ids }) => {
             const { results } = values
 
@@ -49,16 +57,16 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
                 actions.setResponse({
                     ...values.response,
                     results: results
-                        // remove merged issues
                         .filter(({ id }) => !mergingIds.includes(id))
                         .map((issue) =>
                             // replace primary issue
                             mergedIssue.id === issue.id ? mergedIssue : issue
                         ),
                 })
+
                 posthog.capture('error_tracking_issue_merged', { primary: primaryIssue.id })
                 await api.errorTracking.mergeInto(primaryIssue.id, mergingIds)
-                actions.loadData(true)
+                actions.reloadData()
             }
         },
         resolveIssues: async ({ ids }) => {
@@ -67,12 +75,16 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
             // optimistically update local results
             actions.setResponse({
                 ...values.response,
-                // remove resolved issues
-                results: results.filter(({ id }) => !ids.includes(id)),
+                results: results.map((issue) => {
+                    if (ids.includes(issue.id)) {
+                        return { ...issue, status: 'resolved' }
+                    }
+                    return issue
+                }),
             })
             posthog.capture('error_tracking_issue_bulk_resolve')
-            await api.errorTracking.bulkResolve(ids)
-            actions.loadData(true)
+            await api.errorTracking.bulkMarkStatus(ids, 'resolved')
+            actions.reloadData()
         },
         suppressIssues: async ({ ids }) => {
             const { results } = values
@@ -80,12 +92,35 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
             // optimistically update local results
             actions.setResponse({
                 ...values.response,
-                // remove suppressed issues
-                results: results.filter(({ id }) => !ids.includes(id)),
+                results: results.map((issue) => {
+                    if (ids.includes(issue.id)) {
+                        return { ...issue, status: 'suppressed' }
+                    }
+                    return issue
+                }),
             })
+
             posthog.capture('error_tracking_issue_bulk_suppress')
-            await api.errorTracking.bulkSuppress(ids)
-            actions.loadData(true)
+            await api.errorTracking.bulkMarkStatus(ids, 'suppressed')
+            actions.reloadData()
+        },
+        activateIssues: async ({ ids }) => {
+            const { results } = values
+
+            // optimistically update local results
+            actions.setResponse({
+                ...values.response,
+                results: results.map((issue) => {
+                    if (ids.includes(issue.id)) {
+                        return { ...issue, status: 'active' }
+                    }
+                    return issue
+                }),
+            })
+
+            posthog.capture('error_tracking_issue_bulk_activate')
+            await api.errorTracking.bulkMarkStatus(ids, 'active')
+            actions.reloadData()
         },
 
         assignIssues: async ({ ids, assignee }) => {
@@ -94,7 +129,6 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
             // optimistically update local results
             actions.setResponse({
                 ...values.response,
-                // remove resolved issues
                 results: results.map((issue) =>
                     // replace primary issue
                     ids.includes(issue.id) ? { ...issue, assignee } : issue
@@ -102,7 +136,7 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
             })
             posthog.capture('error_tracking_issue_bulk_assign')
             await api.errorTracking.bulkAssign(ids, assignee)
-            actions.loadData(true)
+            actions.reloadData()
         },
         assignIssue: async ({ id, assignee }) => {
             const response = values.response
@@ -115,7 +149,7 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
                     // optimistically update local results
                     actions.setResponse({ ...response, results: results })
                     await api.errorTracking.assignIssue(issue.id, assignee)
-                    actions.loadData(true)
+                    actions.reloadData()
                 }
             }
         },
diff --git a/frontend/src/scenes/error-tracking/issue/BulkActions.tsx b/frontend/src/scenes/error-tracking/issue/BulkActions.tsx
new file mode 100644
index 000000000000..84957641c238
--- /dev/null
+++ b/frontend/src/scenes/error-tracking/issue/BulkActions.tsx
@@ -0,0 +1,103 @@
+import { LemonButton } from '@posthog/lemon-ui'
+import { useActions, useValues } from 'kea'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+import { AssigneeSelect } from '../AssigneeSelect'
+import { errorTrackingDataNodeLogic } from '../errorTrackingDataNodeLogic'
+import { errorTrackingSceneLogic } from '../errorTrackingSceneLogic'
+import { GenericSelect } from './GenericSelect'
+import { IssueStatus, StatusIndicator } from './Indicator'
+
+export function BulkActions(): JSX.Element {
+    const { mergeIssues, assignIssues, resolveIssues, suppressIssues, activateIssues } =
+        useActions(errorTrackingDataNodeLogic)
+    const { selectedIssueIds } = useValues(errorTrackingSceneLogic)
+    const { setSelectedIssueIds } = useActions(errorTrackingSceneLogic)
+    const { results } = useValues(errorTrackingDataNodeLogic)
+
+    const hasAtLeastOneIssue = selectedIssueIds.length > 0
+    const hasAtLeastTwoIssues = selectedIssueIds.length >= 2
+
+    const currentStatus = results
+        .map((issue: ErrorTrackingIssue) => issue.status as IssueStatus)
+        .reduce<IssueStatus | undefined | null>((acc, status) => {
+            if (acc === null) {
+                return status
+            }
+            if (acc === undefined) {
+                return undefined
+            }
+            if (acc !== status) {
+                return undefined
+            }
+            return acc
+        }, null)
+
+    return (
+        <>
+            {hasAtLeastOneIssue ? (
+                <>
+                    <LemonButton type="secondary" size="small" onClick={() => setSelectedIssueIds([])}>
+                        Unselect all
+                    </LemonButton>
+                    <LemonButton
+                        disabledReason={!hasAtLeastTwoIssues ? 'Select at least two issues to merge' : null}
+                        type="secondary"
+                        size="small"
+                        onClick={() => {
+                            mergeIssues(selectedIssueIds)
+                            setSelectedIssueIds([])
+                        }}
+                    >
+                        Merge
+                    </LemonButton>
+                    <GenericSelect
+                        size="small"
+                        current={undefined}
+                        values={['active', 'resolved', 'suppressed'].filter((status) => status !== currentStatus)}
+                        placeholder="Mark as"
+                        renderValue={(value) => {
+                            return <StatusIndicator status={value as IssueStatus} size="small" withTooltip={true} />
+                        }}
+                        onChange={(value) => {
+                            switch (value) {
+                                case 'resolved':
+                                    resolveIssues(selectedIssueIds)
+                                    setSelectedIssueIds([])
+                                    break
+                                case 'suppressed':
+                                    suppressIssues(selectedIssueIds)
+                                    setSelectedIssueIds([])
+                                    break
+                                case 'active':
+                                    activateIssues(selectedIssueIds)
+                                    setSelectedIssueIds([])
+                                    break
+                                default:
+                                    break
+                            }
+                        }}
+                    />
+                    <AssigneeSelect
+                        type="secondary"
+                        size="small"
+                        showName
+                        showIcon={false}
+                        unassignedLabel="Assign"
+                        assignee={null}
+                        onChange={(assignee) => assignIssues(selectedIssueIds, assignee)}
+                    />
+                </>
+            ) : (
+                <LemonButton
+                    type="secondary"
+                    size="small"
+                    onClick={() => setSelectedIssueIds(results.map((issue: ErrorTrackingIssue) => issue.id))}
+                >
+                    Select all
+                </LemonButton>
+            )}
+        </>
+    )
+}
diff --git a/frontend/src/scenes/error-tracking/issue/Events.tsx b/frontend/src/scenes/error-tracking/issue/Events.tsx
index 46edcb23eefd..3307d52bfd74 100644
--- a/frontend/src/scenes/error-tracking/issue/Events.tsx
+++ b/frontend/src/scenes/error-tracking/issue/Events.tsx
@@ -1,15 +1,9 @@
-import { LemonCollapse, LemonDivider } from '@posthog/lemon-ui'
-import { useActions, useValues } from 'kea'
-import PanelLayout, { SettingsButton } from 'lib/components/PanelLayout/PanelLayout'
-import { useState } from 'react'
+import { useValues } from 'kea'
+import { stackFrameLogic } from 'lib/components/Errors/stackFrameLogic'
+import { ChainedStackTraces } from 'lib/components/Errors/StackTraces'
 
-import { ErrorTrackingFilters } from '../ErrorTrackingFilters'
-import { errorTrackingIssueSceneLogic, EventsMode } from '../errorTrackingIssueSceneLogic'
-import { getExceptionAttributes, hasStacktrace, isThirdPartyScriptError } from '../utils'
-import { Overview } from './Overview'
-import RecordingPanel from './panels/RecordingPanel'
-import StackTracePanel from './panels/StacktracePanel'
-import { EventsTab } from './tabs/EventsTab'
+import { errorTrackingIssueSceneLogic } from '../errorTrackingIssueSceneLogic'
+import { getExceptionAttributes, hasAnyInAppFrames } from '../utils'
 
 export type ErrorTrackingIssueEventContent = {
     hasStack: boolean
@@ -25,82 +19,13 @@ export type ErrorTrackingIssueEventsPanel = {
     className?: string
 }
 
-const PANELS = [StackTracePanel, RecordingPanel] as ErrorTrackingIssueEventsPanel[]
+export const Stacktrace = (): JSX.Element => {
+    const { issueProperties } = useValues(errorTrackingIssueSceneLogic)
 
-export const Events = (): JSX.Element => {
-    const { issueProperties, eventsMode } = useValues(errorTrackingIssueSceneLogic)
-    const { setEventsMode } = useActions(errorTrackingIssueSceneLogic)
-    const [activeKeys, setActiveKeys] = useState<ErrorTrackingIssueEventsPanel['key'][]>(['stacktrace'])
+    const { showAllFrames } = useValues(stackFrameLogic)
 
-    const { value, exceptionList } = getExceptionAttributes(issueProperties)
+    const { exceptionList } = getExceptionAttributes(issueProperties)
+    const hasAnyInApp = hasAnyInAppFrames(exceptionList)
 
-    const props = {
-        hasStack: hasStacktrace(exceptionList),
-        hasRecording: issueProperties['$session_id'] && issueProperties['$recording_status'] === 'active',
-        isThirdPartyScript: isThirdPartyScriptError(value),
-    }
-
-    const panels = PANELS.filter(({ hasContent }) => hasContent(props)).map(({ key, Header, Content, className }) => ({
-        key,
-        content: <Content {...props} />,
-        header: typeof Header === 'string' ? Header : <Header active={activeKeys.includes(key)} {...props} />,
-        className,
-    }))
-
-    return (
-        <>
-            <PanelLayout.PanelSettings title="Events" border="bottom">
-                {/* TODO: follow up PR when we support this in the events query */}
-                {/* {eventsMode != EventsMode.All && (
-                    <SettingsMenu
-                        highlightWhenActive={false}
-                        items={[
-                            {
-                                label: 'Earliest',
-                                onClick: () => setEventsMode(EventsMode.Earliest),
-                                active: eventsMode === EventsMode.Earliest,
-                            },
-                            {
-                                label: 'Latest',
-                                onClick: () => setEventsMode(EventsMode.Latest),
-                                active: eventsMode === EventsMode.Latest,
-                            },
-                            {
-                                label: 'Recommended',
-                                onClick: () => setEventsMode(EventsMode.Recommended),
-                                active: eventsMode === EventsMode.Recommended,
-                            },
-                        ]}
-                        label={capitalizeFirstLetter(eventsMode)}
-                    />
-                )} */}
-                <SettingsButton
-                    label={eventsMode === EventsMode.All ? 'Close' : 'View all events'}
-                    active
-                    onClick={() => setEventsMode(eventsMode === EventsMode.All ? EventsMode.Latest : EventsMode.All)}
-                />
-            </PanelLayout.PanelSettings>
-            {eventsMode === EventsMode.All ? (
-                <div className="m-4">
-                    <ErrorTrackingFilters />
-                    <LemonDivider thick className="mt-2 mb-0" />
-                    <EventsTab />
-                </div>
-            ) : (
-                <>
-                    <Overview />
-                    <LemonDivider className="mt-2 mb-0" />
-                    {panels.length > 0 && (
-                        <LemonCollapse
-                            embedded
-                            multiple
-                            activeKeys={activeKeys}
-                            onChange={setActiveKeys}
-                            panels={panels}
-                        />
-                    )}
-                </>
-            )}
-        </>
-    )
+    return <ChainedStackTraces showAllFrames={hasAnyInApp ? showAllFrames : true} exceptionList={exceptionList} />
 }
diff --git a/frontend/src/scenes/error-tracking/issue/GenericSelect.tsx b/frontend/src/scenes/error-tracking/issue/GenericSelect.tsx
new file mode 100644
index 000000000000..300a8db11744
--- /dev/null
+++ b/frontend/src/scenes/error-tracking/issue/GenericSelect.tsx
@@ -0,0 +1,30 @@
+import { LemonSelect } from '@posthog/lemon-ui'
+
+export function GenericSelect<T extends string | number | boolean | null>({
+    current,
+    size,
+    onChange,
+    values,
+    placeholder,
+    renderValue,
+}: {
+    current: T | undefined
+    size?: 'xsmall' | 'small' | 'medium' | 'large'
+    onChange: (value: T) => void
+    values: T[]
+    placeholder?: string
+    renderValue: (key: T) => string | JSX.Element
+}): JSX.Element {
+    return (
+        <LemonSelect
+            onChange={onChange}
+            value={current}
+            placeholder={placeholder}
+            options={values.map((key) => ({
+                value: key,
+                label: renderValue(key),
+            }))}
+            size={size}
+        />
+    )
+}
diff --git a/frontend/src/scenes/error-tracking/issue/Indicator.tsx b/frontend/src/scenes/error-tracking/issue/Indicator.tsx
new file mode 100644
index 000000000000..284b57d2ae2a
--- /dev/null
+++ b/frontend/src/scenes/error-tracking/issue/Indicator.tsx
@@ -0,0 +1,74 @@
+import { LemonBadge, Tooltip } from '@posthog/lemon-ui'
+import clsx from 'clsx'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+export type IssueStatus = ErrorTrackingIssue['status']
+
+export type Intent = 'data' | 'primary' | 'success' | 'warning' | 'danger' | 'muted' | undefined
+
+export const sizeVariants = {
+    xsmall: 'gap-1 text-xs',
+    small: 'gap-2 text-sm',
+    medium: 'gap-3 text-base',
+    large: 'gap-3 text-lg',
+}
+
+interface LabelIndicatorProps {
+    intent: Intent
+    label: string
+    size: 'xsmall' | 'small' | 'medium' | 'large'
+    tooltip?: string
+}
+
+export function LabelIndicator({ intent, label, size, tooltip }: LabelIndicatorProps): JSX.Element {
+    return (
+        <Tooltip title={tooltip} placement="right">
+            <div className={clsx('flex items-center', sizeVariants[size])}>
+                <LemonBadge status={intent} size="small" />
+                <div>{label}</div>
+            </div>
+        </Tooltip>
+    )
+}
+
+export const STATUS_LABEL: Record<ErrorTrackingIssue['status'], string> = {
+    active: 'Active',
+    archived: 'Archived',
+    resolved: 'Resolved',
+    pending_release: 'Pending release',
+    suppressed: 'Suppressed',
+}
+
+export const STATUS_INTENT: Record<ErrorTrackingIssue['status'], Intent> = {
+    active: 'warning',
+    archived: 'muted',
+    resolved: 'success',
+    pending_release: 'muted',
+    suppressed: 'danger',
+}
+
+export const STATUS_TOOLTIP: Record<ErrorTrackingIssue['status'], string | undefined> = {
+    suppressed: 'Stop capturing this issue',
+    active: undefined,
+    archived: undefined,
+    resolved: undefined,
+    pending_release: undefined,
+}
+
+interface StatusIndicatorProps {
+    status: IssueStatus
+    size?: 'xsmall' | 'small' | 'medium' | 'large'
+    withTooltip?: boolean
+}
+
+export function StatusIndicator({ status, size = 'small', withTooltip }: StatusIndicatorProps): JSX.Element {
+    return (
+        <LabelIndicator
+            intent={STATUS_INTENT[status]}
+            size={size}
+            label={STATUS_LABEL[status]}
+            tooltip={withTooltip ? STATUS_TOOLTIP[status] : undefined}
+        />
+    )
+}
diff --git a/frontend/src/scenes/error-tracking/issue/Metadata.tsx b/frontend/src/scenes/error-tracking/issue/Metadata.tsx
index 563daf491677..f848d8cbe087 100644
--- a/frontend/src/scenes/error-tracking/issue/Metadata.tsx
+++ b/frontend/src/scenes/error-tracking/issue/Metadata.tsx
@@ -6,6 +6,8 @@ import { ClampedText } from 'lib/lemon-ui/ClampedText'
 import { humanFriendlyLargeNumber } from 'lib/utils'
 import { errorTrackingIssueSceneLogic } from 'scenes/error-tracking/errorTrackingIssueSceneLogic'
 
+import { SparklinePanel } from './Sparkline'
+
 export const Metadata = (): JSX.Element => {
     const { issue, issueLoading } = useValues(errorTrackingIssueSceneLogic)
 
@@ -32,7 +34,7 @@ export const Metadata = (): JSX.Element => {
     )
 
     return (
-        <div className="deprecated-space-y-1">
+        <div className="space-y-3">
             {issue && issue.description ? <ClampedText text={issue.description} lines={2} /> : <LemonSkeleton />}
             <div className="flex flex-1 justify-between">
                 <div className="flex items-end deprecated-space-x-6">
@@ -71,6 +73,7 @@ export const Metadata = (): JSX.Element => {
                     </div>
                 </div>
             </div>
+            <SparklinePanel />
         </div>
     )
 }
diff --git a/frontend/src/scenes/error-tracking/issue/Overview.tsx b/frontend/src/scenes/error-tracking/issue/Overview.tsx
index d38ba7a20412..579bbd627435 100644
--- a/frontend/src/scenes/error-tracking/issue/Overview.tsx
+++ b/frontend/src/scenes/error-tracking/issue/Overview.tsx
@@ -8,16 +8,9 @@ export const Overview = (): JSX.Element => {
 
     const { synthetic, level, browser, os, library, unhandled } = getExceptionAttributes(issueProperties)
 
-    const TableRow = ({ label, value }: { label: string; value: string | undefined }): JSX.Element => (
-        <tr>
-            <td className="text-secondary">{label}</td>
-            <td>{value ?? <div className="italic">unknown</div>}</td>
-        </tr>
-    )
-
     if (issueLoading) {
         return (
-            <div className="deprecated-space-y-2">
+            <div className="space-y-2 p-2">
                 <LemonSkeleton />
                 <LemonSkeleton.Row repeat={2} />
             </div>
@@ -25,28 +18,23 @@ export const Overview = (): JSX.Element => {
     }
 
     return (
-        <div className="px-1">
-            <div className="grid grid-cols-2 gap-2">
-                <table>
-                    {[
-                        { label: 'Level', value: level },
-                        { label: 'Synthetic', value: synthetic },
-                        { label: 'Library', value: library },
-                        { label: 'Unhandled', value: unhandled },
-                    ].map((row, index) => (
-                        <TableRow key={index} {...row} />
-                    ))}
-                </table>
-                <table>
-                    {[
-                        { label: 'Browser', value: browser },
-                        { label: 'OS', value: os },
-                        { label: 'URL', value: issueProperties['$current_url'] },
-                    ].map((row, index) => (
-                        <TableRow key={index} {...row} />
-                    ))}
-                </table>
-            </div>
+        <div className="grid grid-cols-2 gap-2">
+            {[
+                { label: 'Level', value: level },
+                { label: 'Synthetic', value: synthetic },
+                { label: 'Library', value: library },
+                { label: 'Unhandled', value: unhandled },
+                { label: 'Browser', value: browser },
+                { label: 'OS', value: os },
+                { label: 'URL', value: issueProperties['$current_url'] },
+            ]
+                .filter((row) => row.value !== undefined)
+                .map((row, index) => (
+                    <div key={index} className="flex items-center justify-between">
+                        <span className="font-semibold w-full">{row.label}</span>
+                        <span className="w-full truncate">{row.value + ''}</span>
+                    </div>
+                ))}
         </div>
     )
 }
diff --git a/frontend/src/scenes/error-tracking/issue/widgets/DetailsWidget.tsx b/frontend/src/scenes/error-tracking/issue/widgets/DetailsWidget.tsx
new file mode 100644
index 000000000000..7d8ee455378d
--- /dev/null
+++ b/frontend/src/scenes/error-tracking/issue/widgets/DetailsWidget.tsx
@@ -0,0 +1,31 @@
+import { LemonWidget } from '@posthog/lemon-ui'
+import { useValues } from 'kea'
+import ViewRecordingButton from 'lib/components/ViewRecordingButton/ViewRecordingButton'
+import { errorTrackingIssueSceneLogic } from 'scenes/error-tracking/errorTrackingIssueSceneLogic'
+import { getSessionId } from 'scenes/error-tracking/utils'
+
+import { Overview } from '../Overview'
+
+export function DetailsWidget(): JSX.Element {
+    const { issueProperties } = useValues(errorTrackingIssueSceneLogic)
+    const sessionId = getSessionId(issueProperties)
+    return (
+        <LemonWidget
+            title="Details"
+            actions={
+                <ViewRecordingButton
+                    sessionId={sessionId}
+                    timestamp={issueProperties.timestamp}
+                    inModal={true}
+                    size="xsmall"
+                    type="primary"
+                    disabledReason={sessionId ? '' : 'No recording available'}
+                />
+            }
+        >
+            <div className="p-2">
+                <Overview />
+            </div>
+        </LemonWidget>
+    )
+}
diff --git a/frontend/src/scenes/error-tracking/issue/widgets/StacktraceWidget.tsx b/frontend/src/scenes/error-tracking/issue/widgets/StacktraceWidget.tsx
new file mode 100644
index 000000000000..d5ea270f31ae
--- /dev/null
+++ b/frontend/src/scenes/error-tracking/issue/widgets/StacktraceWidget.tsx
@@ -0,0 +1,88 @@
+import { IconFilter, IconSort } from '@posthog/icons'
+import { LemonButton, LemonWidget } from '@posthog/lemon-ui'
+import { useActions, useValues } from 'kea'
+import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage'
+import { stackFrameLogic } from 'lib/components/Errors/stackFrameLogic'
+import { ChainedStackTraces } from 'lib/components/Errors/StackTraces'
+import { ErrorTrackingException } from 'lib/components/Errors/types'
+
+import { errorTrackingIssueSceneLogic } from '../../errorTrackingIssueSceneLogic'
+import { getExceptionAttributes, hasAnyInAppFrames } from '../../utils'
+
+export function StacktraceWidget(): JSX.Element {
+    const { issueProperties } = useValues(errorTrackingIssueSceneLogic)
+
+    const { showAllFrames, frameOrderReversed } = useValues(stackFrameLogic)
+    const { setShowAllFrames, reverseFrameOrder } = useActions(stackFrameLogic)
+    const { exceptionList } = getExceptionAttributes(issueProperties)
+
+    const hasStacktrace = exceptionList.length > 0
+    const hasAnyInApp = hasAnyInAppFrames(exceptionList)
+    const orderedExceptions = applyFrameOrder(exceptionList, frameOrderReversed)
+
+    return (
+        <LemonWidget
+            title="Stacktrace"
+            actions={
+                <div className="flex gap-2">
+                    {hasStacktrace && (
+                        <LemonButton
+                            className="space-x-2"
+                            type="tertiary"
+                            size="xsmall"
+                            onClick={() => reverseFrameOrder(!frameOrderReversed)}
+                        >
+                            <span className="me-1">{frameOrderReversed ? 'First call' : 'Last call'}</span>
+                            <IconSort />
+                        </LemonButton>
+                    )}
+                    {hasAnyInApp && (
+                        <LemonButton
+                            className="space-x-2"
+                            type="tertiary"
+                            size="xsmall"
+                            onClick={() => setShowAllFrames(!showAllFrames)}
+                        >
+                            <span className="me-1">{showAllFrames ? 'Full stack' : 'In app'}</span>
+                            <IconFilter />
+                        </LemonButton>
+                    )}
+                </div>
+            }
+        >
+            <div className="p-2">
+                {hasStacktrace && (
+                    <ChainedStackTraces
+                        showAllFrames={hasAnyInApp ? showAllFrames : true}
+                        exceptionList={orderedExceptions}
+                    />
+                )}
+                {!hasStacktrace && (
+                    <EmptyMessage
+                        title="No stacktrace available"
+                        description="Make sure sdk is setup correctly or contact support if problem persists"
+                        buttonText="Check documentation"
+                        buttonTo="https://posthog.com/docs/error-tracking/installation"
+                    />
+                )}
+            </div>
+        </LemonWidget>
+    )
+}
+
+function applyFrameOrder(
+    exceptionList: ErrorTrackingException[],
+    frameOrderReversed: boolean
+): ErrorTrackingException[] {
+    if (frameOrderReversed) {
+        exceptionList = exceptionList
+            .map((exception) => {
+                if (exception.stacktrace) {
+                    exception.stacktrace.frames = exception.stacktrace.frames.slice().reverse()
+                }
+                return exception
+            })
+            .reverse()
+    }
+    return exceptionList
+}
diff --git a/frontend/src/scenes/error-tracking/utils.ts b/frontend/src/scenes/error-tracking/utils.ts
index 6fa0de63bf22..6663959114fa 100644
--- a/frontend/src/scenes/error-tracking/utils.ts
+++ b/frontend/src/scenes/error-tracking/utils.ts
@@ -112,6 +112,10 @@ export function getExceptionAttributes(
     }
 }
 
+export function getSessionId(properties: Record<string, any>): string | undefined {
+    return properties['$session_id']
+}
+
 export function hasStacktrace(exceptionList: ErrorTrackingException[]): boolean {
     return exceptionList?.length > 0 && exceptionList.some((e) => !!e.stacktrace)
 }
diff --git a/posthog/api/error_tracking.py b/posthog/api/error_tracking.py
index 1c1894cf0c41..346d09ec1a15 100644
--- a/posthog/api/error_tracking.py
+++ b/posthog/api/error_tracking.py
@@ -142,10 +142,14 @@ def assign(self, request, **kwargs):
     @action(methods=["POST"], detail=False)
     def bulk(self, request, **kwargs):
         action = request.data.get("action")
+        status = request.data.get("status")
         issues = self.queryset.filter(id__in=request.data.get("ids", []))
 
         with transaction.atomic():
-            if action == "resolve":
+            if action == "set_status":
+                new_status = get_status_from_string(status)
+                if new_status is None:
+                    raise ValidationError("Invalid status")
                 for issue in issues:
                     log_activity(
                         organization_id=self.organization.id,
@@ -163,13 +167,13 @@ def bulk(self, request, **kwargs):
                                     action="changed",
                                     field="status",
                                     before=issue.status,
-                                    after=ErrorTrackingIssue.Status.RESOLVED,
+                                    after=new_status,
                                 )
                             ],
                         ),
                     )
 
-                issues.update(status=ErrorTrackingIssue.Status.RESOLVED)
+                issues.update(status=new_status)
             elif action == "assign":
                 assignee = request.data.get("assignee", None)
 
@@ -177,31 +181,6 @@ def bulk(self, request, **kwargs):
                     assign_issue(
                         issue, assignee, self.organization, request.user, self.team_id, is_impersonated_session(request)
                     )
-            elif action == "suppress":
-                for issue in issues:
-                    log_activity(
-                        organization_id=self.organization.id,
-                        team_id=self.team_id,
-                        user=request.user,
-                        was_impersonated=is_impersonated_session(request),
-                        item_id=issue.id,
-                        scope="ErrorTrackingIssue",
-                        activity="updated",
-                        detail=Detail(
-                            name=issue.name,
-                            changes=[
-                                Change(
-                                    type="ErrorTrackingIssue",
-                                    action="changed",
-                                    field="status",
-                                    before=issue.status,
-                                    after=ErrorTrackingIssue.Status.SUPPRESSED,
-                                )
-                            ],
-                        ),
-                    )
-
-                issues.update(status=ErrorTrackingIssue.Status.SUPPRESSED)
 
         return Response({"success": True})
 
@@ -232,6 +211,17 @@ def activity(self, request: request.Request, **kwargs):
         return activity_page_response(activity_page, limit, page, request)
 
 
+def get_status_from_string(status: str) -> ErrorTrackingIssue.Status | None:
+    match status:
+        case "active":
+            return ErrorTrackingIssue.Status.ACTIVE
+        case "resolved":
+            return ErrorTrackingIssue.Status.RESOLVED
+        case "suppressed":
+            return ErrorTrackingIssue.Status.SUPPRESSED
+    return None
+
+
 def assign_issue(issue: ErrorTrackingIssue, assignee, organization, user, team_id, was_impersonated):
     assignment_before = ErrorTrackingIssueAssignment.objects.filter(issue_id=issue.id).first()
     serialized_assignment_before = (
diff --git a/posthog/api/test/test_error_tracking.py b/posthog/api/test/test_error_tracking.py
index 3fbc80153e4a..9bf1b23a488d 100644
--- a/posthog/api/test/test_error_tracking.py
+++ b/posthog/api/test/test_error_tracking.py
@@ -320,7 +320,7 @@ def test_error_tracking_issue_bulk_resolve(self):
 
         self.client.post(
             f"/api/projects/{self.team.id}/error_tracking/issue/bulk",
-            data={"ids": [issue_one.id, issue_two.id], "action": "resolve"},
+            data={"ids": [issue_one.id, issue_two.id], "action": "set_status", "status": "resolved"},
         )
 
         issue_one.refresh_from_db()


Reviews:
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR introduces significant UI and functionality improvements to the error tracking system, focusing on bulk operations and data consistency.

- Added new `BulkActions` component with consolidated controls for merging, resolving, suppressing and assigning multiple issues at once
- Implemented cache key mechanism in `errorTrackingLogic` using MD5 hashing to ensure proper data refresh after issue updates
- Introduced new widget-based layout with `StacktraceWidget`, `DetailsWidget` and other components for better organization of issue details
- Added stack trace order toggle functionality in `stackFrameLogic` allowing users to switch between first/last call views
- Improved status management by consolidating multiple bulk actions into a single `bulkMarkStatus` method with color-coded status indicators

<sub>29 file(s) reviewed, 21 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=posthog_30019)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T14:55:30Z
user: login: daibhin
body: Left a couple of comments. Some really nice improvements here. Main thing I'm concerned about is the addition of the `cacheKey`. I wouldn't like us to work around the caching of the query endpoint unless it's absolutely necessary
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:02:39Z
user: login: hpouillot
body: I changed few things mainly on the front and I'll check about 'force_blocking' param
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T18:54:18Z
user: login: hpouillot
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T18:57:30Z
user: login: oliverb123
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T10:01:32Z
user: login: daibhin
body: Couple of small comments. Will leave it up to you to fix here or follow up with another PR if necessary
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T11:20:59Z

Review comments:
diff_hunk: @@ -36,72 +39,74 @@ export const STATUS_LABEL: Record<ErrorTrackingIssue['status'], string> = {
 export function ErrorTrackingIssueScene(): JSX.Element {
     const { issue } = useValues(errorTrackingIssueSceneLogic)
     const { updateIssue, initIssue, assignIssue } = useActions(errorTrackingIssueSceneLogic)
+    const [activeTab, setActiveTab] = useState('stacktrace')
 
     useEffect(() => {
         initIssue()
-    }, [])
+    }, [initIssue])
 
     return (
         <ErrorTrackingSetupPrompt>
             <>
                 <PageHeader
                     buttons={
-                        issue ? (
-                            issue.status === 'active' ? (
-                                <div className="flex divide-x gap-x-2">
-                                    <AssigneeSelect
-                                        assignee={issue.assignee}
-                                        onChange={assignIssue}
-                                        type="secondary"
-                                        showName
-                                    />
-                                    <div className="flex pl-2 gap-x-2">
-                                        <LemonButton
-                                            type="secondary"
-                                            onClick={() => updateIssue({ status: 'archived' })}
-                                        >
-                                            Archive
-                                        </LemonButton>
-                                        <LemonButton type="primary" onClick={() => updateIssue({ status: 'resolved' })}>
-                                            Resolve
-                                        </LemonButton>
-                                        <LemonButton
-                                            type="secondary"
-                                            status="danger"
-                                            tooltip="Stop capturing these errors"
-                                            onClick={() => updateIssue({ status: 'suppressed' })}
-                                        >
-                                            Suppress
-                                        </LemonButton>
-                                    </div>
-                                </div>
-                            ) : (
-                                <LemonButton
+                        <div className="flex divide-x gap-x-2">
+                            {issue && issue.status == 'active' && (
+                                <AssigneeSelect
+                                    assignee={issue.assignee}
+                                    onChange={assignIssue}
                                     type="secondary"
-                                    className="upcasefirst-letter:uppercase"
-                                    onClick={() => updateIssue({ status: 'active' })}
-                                    tooltip="Mark as active"
-                                >
-                                    {STATUS_LABEL[issue.status]}
-                                </LemonButton>
-                            )
-                        ) : (
-                            false
-                        )
+                                    showName
+                                />
+                            )}
+                            {issue && (
+                                <GenericSelect
+                                    size="small"
+                                    current={issue.status}
+                                    values={['active', 'resolved', 'suppressed']}
path: frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx
user: login: greptile-apps[bot]
body: logic: Status values array excludes 'archived' and 'pending_release' options that are defined in STATUS_LABEL. Either document why these are excluded or update for consistency
created_at: 2025-03-17T14:51:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -36,72 +39,74 @@ export const STATUS_LABEL: Record<ErrorTrackingIssue['status'], string> = {
 export function ErrorTrackingIssueScene(): JSX.Element {
     const { issue } = useValues(errorTrackingIssueSceneLogic)
     const { updateIssue, initIssue, assignIssue } = useActions(errorTrackingIssueSceneLogic)
+    const [activeTab, setActiveTab] = useState('stacktrace')
 
     useEffect(() => {
         initIssue()
-    }, [])
+    }, [initIssue])
 
     return (
         <ErrorTrackingSetupPrompt>
             <>
                 <PageHeader
                     buttons={
-                        issue ? (
-                            issue.status === 'active' ? (
-                                <div className="flex divide-x gap-x-2">
-                                    <AssigneeSelect
-                                        assignee={issue.assignee}
-                                        onChange={assignIssue}
-                                        type="secondary"
-                                        showName
-                                    />
-                                    <div className="flex pl-2 gap-x-2">
-                                        <LemonButton
-                                            type="secondary"
-                                            onClick={() => updateIssue({ status: 'archived' })}
-                                        >
-                                            Archive
-                                        </LemonButton>
-                                        <LemonButton type="primary" onClick={() => updateIssue({ status: 'resolved' })}>
-                                            Resolve
-                                        </LemonButton>
-                                        <LemonButton
-                                            type="secondary"
-                                            status="danger"
-                                            tooltip="Stop capturing these errors"
-                                            onClick={() => updateIssue({ status: 'suppressed' })}
-                                        >
-                                            Suppress
-                                        </LemonButton>
-                                    </div>
-                                </div>
-                            ) : (
-                                <LemonButton
+                        <div className="flex divide-x gap-x-2">
+                            {issue && issue.status == 'active' && (
path: frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx
user: login: greptile-apps[bot]
body: style: Use strict equality (===) instead of loose equality (==) for type safety

```suggestion
                            {issue && issue.status === 'active' && (
```
created_at: 2025-03-17T14:51:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -36,72 +39,74 @@ export const STATUS_LABEL: Record<ErrorTrackingIssue['status'], string> = {
 export function ErrorTrackingIssueScene(): JSX.Element {
     const { issue } = useValues(errorTrackingIssueSceneLogic)
     const { updateIssue, initIssue, assignIssue } = useActions(errorTrackingIssueSceneLogic)
+    const [activeTab, setActiveTab] = useState('stacktrace')
 
     useEffect(() => {
         initIssue()
-    }, [])
+    }, [initIssue])
 
     return (
         <ErrorTrackingSetupPrompt>
             <>
                 <PageHeader
                     buttons={
-                        issue ? (
-                            issue.status === 'active' ? (
-                                <div className="flex divide-x gap-x-2">
-                                    <AssigneeSelect
-                                        assignee={issue.assignee}
-                                        onChange={assignIssue}
-                                        type="secondary"
-                                        showName
-                                    />
-                                    <div className="flex pl-2 gap-x-2">
-                                        <LemonButton
-                                            type="secondary"
-                                            onClick={() => updateIssue({ status: 'archived' })}
-                                        >
-                                            Archive
-                                        </LemonButton>
-                                        <LemonButton type="primary" onClick={() => updateIssue({ status: 'resolved' })}>
-                                            Resolve
-                                        </LemonButton>
-                                        <LemonButton
-                                            type="secondary"
-                                            status="danger"
-                                            tooltip="Stop capturing these errors"
-                                            onClick={() => updateIssue({ status: 'suppressed' })}
-                                        >
-                                            Suppress
-                                        </LemonButton>
-                                    </div>
-                                </div>
-                            ) : (
-                                <LemonButton
+                        <div className="flex divide-x gap-x-2">
+                            {issue && issue.status == 'active' && (
+                                <AssigneeSelect
+                                    assignee={issue.assignee}
+                                    onChange={assignIssue}
                                     type="secondary"
-                                    className="upcasefirst-letter:uppercase"
-                                    onClick={() => updateIssue({ status: 'active' })}
-                                    tooltip="Mark as active"
-                                >
-                                    {STATUS_LABEL[issue.status]}
-                                </LemonButton>
-                            )
-                        ) : (
-                            false
-                        )
+                                    showName
+                                />
+                            )}
+                            {issue && (
+                                <GenericSelect
+                                    size="small"
+                                    current={issue.status}
+                                    values={['active', 'resolved', 'suppressed']}
+                                    placeholder="Mark as"
+                                    renderValue={(value) => {
+                                        return <StatusTag status={value as IssueStatus} size="small" />
+                                    }}
+                                    onChange={(value) => {
+                                        updateIssue({ status: value })
+                                    }}
+                                />
+                            )}
+                        </div>
                     }
                 />
                 {issue ? (
                     <div className="ErrorTrackingIssue">
                         <Metadata />
-                        <LemonDivider className="my-4" />
-                        <PanelLayout>
-                            <PanelLayout.Container column primary>
-                                <SparklinePanel />
-                                <PanelLayout.Panel primary>
-                                    <Events />
-                                </PanelLayout.Panel>
-                            </PanelLayout.Container>
-                        </PanelLayout>
+                        <LemonTabs
+                            activeKey={activeTab}
+                            onChange={(key) => setActiveTab(key)}
+                            tabs={[
+                                {
+                                    key: 'stacktrace',
+                                    label: 'Overview',
+                                    content: (
+                                        <div className="space-y-2">
+                                            <DetailsWidget />
+                                            <StacktraceWidget />
+                                        </div>
+                                    ),
+                                },
+                                {
+                                    key: 'events',
+                                    label: 'Events',
+                                    content: (
+                                        <div className="space-y-2">
+                                            <ErrorTrackingFilters />
+                                            <div className="border-1 overflow-hidden border-accent-primary border-primary rounded bg-surface-primary relative">
path: frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx
user: login: greptile-apps[bot]
body: style: border-1 and border-primary classes are duplicating border definitions. Consider consolidating
created_at: 2025-03-17T14:51:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,7 +35,7 @@ export const AssigneeSelect = ({
 
     useEffect(() => {
         ensureAssigneeTypesLoaded()
-    }, [])
+    }, [ensureAssigneeTypesLoaded])
path: frontend/src/scenes/error-tracking/AssigneeSelect.tsx
user: login: greptile-apps[bot]
body: logic: Adding ensureAssigneeTypesLoaded to the dependency array could cause infinite re-renders if the action is not memoized in the logic file
created_at: 2025-03-17T14:51:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+import { LemonButton } from '@posthog/lemon-ui'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+import { AssigneeSelect } from '../AssigneeSelect'
+import { GenericSelect } from './StatusSelect'
+import { IssueStatus, StatusTag } from './StatusTag'
+
+export interface BulkActionsProps {
+    issueSelection: ErrorTrackingIssue[]
+    onAssign: (issueIds: string[], assigneeId: ErrorTrackingIssue['assignee']) => void
+    onMerge: (issueIds: string[]) => void
+    onResolve: (issueIds: string[]) => void
+    onSuppress: (issueIds: string[]) => void
+    onActivate: (issueIds: string[]) => void
+    onSelectAll: () => void
+    onClearSelection: () => void
+}
+
+export function BulkActions({
+    onMerge,
+    onResolve,
+    onSuppress,
+    onActivate,
+    onAssign,
+    issueSelection,
+    onSelectAll,
+    onClearSelection,
+}: BulkActionsProps): JSX.Element {
+    const hasAtLeastOneIssue = issueSelection.length > 0
+    const hasAtLeastTwoIssues = issueSelection.length >= 2
+    const selectedIssueIds = issueSelection.map((issue) => issue.id)
+    const currentStatus = issueSelection
+        .map((issue) => issue.status)
+        .reduce<string | undefined | null>((acc, status) => {
+            if (acc === null) {
+                return status
+            }
+            if (acc === undefined) {
+                return undefined
+            }
+            if (acc !== status) {
+                return undefined
+            }
+            return acc
+        }, null)
path: frontend/src/scenes/error-tracking/issue/BulkActions.tsx
user: login: greptile-apps[bot]
body: style: The currentStatus reducer could be simplified using Array.every() or Array.some() for better readability and performance. The current implementation is more complex than necessary.
created_at: 2025-03-17T14:53:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+import { LemonButton } from '@posthog/lemon-ui'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+import { AssigneeSelect } from '../AssigneeSelect'
+import { GenericSelect } from './StatusSelect'
+import { IssueStatus, StatusTag } from './StatusTag'
+
+export interface BulkActionsProps {
+    issueSelection: ErrorTrackingIssue[]
+    onAssign: (issueIds: string[], assigneeId: ErrorTrackingIssue['assignee']) => void
+    onMerge: (issueIds: string[]) => void
+    onResolve: (issueIds: string[]) => void
+    onSuppress: (issueIds: string[]) => void
+    onActivate: (issueIds: string[]) => void
+    onSelectAll: () => void
+    onClearSelection: () => void
+}
+
+export function BulkActions({
+    onMerge,
+    onResolve,
+    onSuppress,
+    onActivate,
+    onAssign,
+    issueSelection,
+    onSelectAll,
+    onClearSelection,
+}: BulkActionsProps): JSX.Element {
+    const hasAtLeastOneIssue = issueSelection.length > 0
+    const hasAtLeastTwoIssues = issueSelection.length >= 2
+    const selectedIssueIds = issueSelection.map((issue) => issue.id)
+    const currentStatus = issueSelection
+        .map((issue) => issue.status)
+        .reduce<string | undefined | null>((acc, status) => {
+            if (acc === null) {
+                return status
+            }
+            if (acc === undefined) {
+                return undefined
+            }
+            if (acc !== status) {
+                return undefined
+            }
+            return acc
+        }, null)
+
+    return (
+        <>
+            {hasAtLeastOneIssue ? (
+                <>
+                    <LemonButton type="secondary" size="small" onClick={() => onClearSelection()}>
+                        Unselect all
+                    </LemonButton>
+                    <LemonButton
+                        disabledReason={!hasAtLeastTwoIssues ? 'Select at least two issues to merge' : null}
+                        type="secondary"
+                        size="small"
+                        onClick={() => {
+                            onMerge(selectedIssueIds)
+                            onClearSelection()
+                        }}
+                    >
+                        Merge
+                    </LemonButton>
+                    <GenericSelect
+                        size="small"
+                        current={undefined}
+                        values={['active', 'resolved', 'suppressed'].filter((status) => status !== currentStatus)}
path: frontend/src/scenes/error-tracking/issue/BulkActions.tsx
user: login: greptile-apps[bot]
body: logic: Setting current={undefined} while having a currentStatus variable is confusing. Consider using currentStatus here for consistency.
created_at: 2025-03-17T14:53:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+import { LemonButton } from '@posthog/lemon-ui'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+import { AssigneeSelect } from '../AssigneeSelect'
+import { GenericSelect } from './StatusSelect'
+import { IssueStatus, StatusTag } from './StatusTag'
+
+export interface BulkActionsProps {
+    issueSelection: ErrorTrackingIssue[]
+    onAssign: (issueIds: string[], assigneeId: ErrorTrackingIssue['assignee']) => void
+    onMerge: (issueIds: string[]) => void
+    onResolve: (issueIds: string[]) => void
+    onSuppress: (issueIds: string[]) => void
+    onActivate: (issueIds: string[]) => void
+    onSelectAll: () => void
+    onClearSelection: () => void
+}
+
+export function BulkActions({
+    onMerge,
+    onResolve,
+    onSuppress,
+    onActivate,
+    onAssign,
+    issueSelection,
+    onSelectAll,
+    onClearSelection,
+}: BulkActionsProps): JSX.Element {
+    const hasAtLeastOneIssue = issueSelection.length > 0
+    const hasAtLeastTwoIssues = issueSelection.length >= 2
+    const selectedIssueIds = issueSelection.map((issue) => issue.id)
+    const currentStatus = issueSelection
+        .map((issue) => issue.status)
+        .reduce<string | undefined | null>((acc, status) => {
+            if (acc === null) {
+                return status
+            }
+            if (acc === undefined) {
+                return undefined
+            }
+            if (acc !== status) {
+                return undefined
+            }
+            return acc
+        }, null)
+
+    return (
+        <>
+            {hasAtLeastOneIssue ? (
+                <>
+                    <LemonButton type="secondary" size="small" onClick={() => onClearSelection()}>
+                        Unselect all
+                    </LemonButton>
+                    <LemonButton
+                        disabledReason={!hasAtLeastTwoIssues ? 'Select at least two issues to merge' : null}
+                        type="secondary"
+                        size="small"
+                        onClick={() => {
+                            onMerge(selectedIssueIds)
+                            onClearSelection()
+                        }}
+                    >
+                        Merge
+                    </LemonButton>
+                    <GenericSelect
+                        size="small"
+                        current={undefined}
+                        values={['active', 'resolved', 'suppressed'].filter((status) => status !== currentStatus)}
path: frontend/src/scenes/error-tracking/issue/BulkActions.tsx
user: login: greptile-apps[bot]
body: style: The status values are hardcoded. Consider importing these from a constants file or using the IssueStatus type to prevent maintenance issues.

```suggestion
                        values={Object.values(IssueStatus).filter((status) => status !== currentStatus)}
```
created_at: 2025-03-17T14:53:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+import { LemonButton } from '@posthog/lemon-ui'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+import { AssigneeSelect } from '../AssigneeSelect'
+import { GenericSelect } from './StatusSelect'
+import { IssueStatus, StatusTag } from './StatusTag'
+
+export interface BulkActionsProps {
+    issueSelection: ErrorTrackingIssue[]
+    onAssign: (issueIds: string[], assigneeId: ErrorTrackingIssue['assignee']) => void
+    onMerge: (issueIds: string[]) => void
+    onResolve: (issueIds: string[]) => void
+    onSuppress: (issueIds: string[]) => void
+    onActivate: (issueIds: string[]) => void
+    onSelectAll: () => void
+    onClearSelection: () => void
+}
+
+export function BulkActions({
+    onMerge,
+    onResolve,
+    onSuppress,
+    onActivate,
+    onAssign,
+    issueSelection,
+    onSelectAll,
+    onClearSelection,
+}: BulkActionsProps): JSX.Element {
+    const hasAtLeastOneIssue = issueSelection.length > 0
+    const hasAtLeastTwoIssues = issueSelection.length >= 2
+    const selectedIssueIds = issueSelection.map((issue) => issue.id)
+    const currentStatus = issueSelection
+        .map((issue) => issue.status)
+        .reduce<string | undefined | null>((acc, status) => {
+            if (acc === null) {
+                return status
+            }
+            if (acc === undefined) {
+                return undefined
+            }
+            if (acc !== status) {
+                return undefined
+            }
+            return acc
+        }, null)
+
+    return (
+        <>
+            {hasAtLeastOneIssue ? (
+                <>
+                    <LemonButton type="secondary" size="small" onClick={() => onClearSelection()}>
+                        Unselect all
+                    </LemonButton>
+                    <LemonButton
+                        disabledReason={!hasAtLeastTwoIssues ? 'Select at least two issues to merge' : null}
+                        type="secondary"
+                        size="small"
+                        onClick={() => {
+                            onMerge(selectedIssueIds)
+                            onClearSelection()
+                        }}
+                    >
+                        Merge
+                    </LemonButton>
+                    <GenericSelect
+                        size="small"
+                        current={undefined}
+                        values={['active', 'resolved', 'suppressed'].filter((status) => status !== currentStatus)}
+                        placeholder="Mark as"
+                        renderValue={(value) => {
+                            return <StatusTag status={value as IssueStatus} size="small" />
+                        }}
+                        onChange={(value) => {
+                            switch (value) {
+                                case 'resolved':
+                                    onResolve(selectedIssueIds)
+                                    onClearSelection()
+                                    break
+                                case 'suppressed':
+                                    onSuppress(selectedIssueIds)
+                                    onClearSelection()
+                                    break
+                                case 'active':
+                                    onActivate(selectedIssueIds)
+                                    onClearSelection()
+                                    break
+                                default:
+                                    break
+                            }
+                        }}
+                    />
+                    <AssigneeSelect
+                        type="secondary"
+                        size="small"
+                        showName
+                        showIcon={false}
+                        unassignedLabel="Assign"
+                        assignee={null}
path: frontend/src/scenes/error-tracking/issue/BulkActions.tsx
user: login: greptile-apps[bot]
body: logic: Setting assignee to null while potentially having multiple selected issues with different assignees could be misleading. Consider showing a placeholder or current assignee if all selected issues have the same one.
created_at: 2025-03-17T14:53:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+import { LemonButton } from '@posthog/lemon-ui'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+import { AssigneeSelect } from '../AssigneeSelect'
+import { GenericSelect } from './StatusSelect'
+import { IssueStatus, StatusTag } from './StatusTag'
+
+export interface BulkActionsProps {
+    issueSelection: ErrorTrackingIssue[]
+    onAssign: (issueIds: string[], assigneeId: ErrorTrackingIssue['assignee']) => void
+    onMerge: (issueIds: string[]) => void
+    onResolve: (issueIds: string[]) => void
+    onSuppress: (issueIds: string[]) => void
+    onActivate: (issueIds: string[]) => void
+    onSelectAll: () => void
+    onClearSelection: () => void
+}
+
+export function BulkActions({
+    onMerge,
+    onResolve,
+    onSuppress,
+    onActivate,
+    onAssign,
+    issueSelection,
+    onSelectAll,
+    onClearSelection,
+}: BulkActionsProps): JSX.Element {
+    const hasAtLeastOneIssue = issueSelection.length > 0
+    const hasAtLeastTwoIssues = issueSelection.length >= 2
+    const selectedIssueIds = issueSelection.map((issue) => issue.id)
+    const currentStatus = issueSelection
+        .map((issue) => issue.status)
+        .reduce<string | undefined | null>((acc, status) => {
+            if (acc === null) {
+                return status
+            }
+            if (acc === undefined) {
+                return undefined
+            }
+            if (acc !== status) {
+                return undefined
+            }
+            return acc
+        }, null)
+
+    return (
+        <>
+            {hasAtLeastOneIssue ? (
+                <>
+                    <LemonButton type="secondary" size="small" onClick={() => onClearSelection()}>
+                        Unselect all
+                    </LemonButton>
+                    <LemonButton
+                        disabledReason={!hasAtLeastTwoIssues ? 'Select at least two issues to merge' : null}
+                        type="secondary"
+                        size="small"
+                        onClick={() => {
+                            onMerge(selectedIssueIds)
+                            onClearSelection()
+                        }}
+                    >
+                        Merge
+                    </LemonButton>
+                    <GenericSelect
+                        size="small"
+                        current={undefined}
+                        values={['active', 'resolved', 'suppressed'].filter((status) => status !== currentStatus)}
+                        placeholder="Mark as"
+                        renderValue={(value) => {
+                            return <StatusTag status={value as IssueStatus} size="small" />
+                        }}
+                        onChange={(value) => {
+                            switch (value) {
+                                case 'resolved':
+                                    onResolve(selectedIssueIds)
+                                    onClearSelection()
+                                    break
+                                case 'suppressed':
+                                    onSuppress(selectedIssueIds)
+                                    onClearSelection()
+                                    break
+                                case 'active':
+                                    onActivate(selectedIssueIds)
+                                    onClearSelection()
+                                    break
+                                default:
+                                    break
+                            }
+                        }}
path: frontend/src/scenes/error-tracking/issue/BulkActions.tsx
user: login: greptile-apps[bot]
body: style: The switch statement repeats onClearSelection() in every case. Consider moving it after the switch to reduce duplication.

```suggestion
                        onChange={(value) => {
                            switch (value) {
                                case 'resolved':
                                    onResolve(selectedIssueIds)
                                    break
                                case 'suppressed':
                                    onSuppress(selectedIssueIds)
                                    break
                                case 'active':
                                    onActivate(selectedIssueIds)
                                    break
                                default:
                                    return
                            }
                            onClearSelection()
                        }}
```
created_at: 2025-03-17T14:53:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,45 +8,33 @@ export const Overview = (): JSX.Element => {
 
     const { synthetic, level, browser, os, library, unhandled } = getExceptionAttributes(issueProperties)
 
-    const TableRow = ({ label, value }: { label: string; value: string | undefined }): JSX.Element => (
-        <tr>
-            <td className="text-secondary">{label}</td>
-            <td>{value ?? <div className="italic">unknown</div>}</td>
-        </tr>
-    )
-
     if (issueLoading) {
         return (
-            <div className="deprecated-space-y-2">
+            <div className="space-y-2 p-2">
                 <LemonSkeleton />
                 <LemonSkeleton.Row repeat={2} />
             </div>
         )
     }
 
     return (
-        <div className="px-1">
-            <div className="grid grid-cols-2 gap-2">
-                <table>
-                    {[
-                        { label: 'Level', value: level },
-                        { label: 'Synthetic', value: synthetic },
-                        { label: 'Library', value: library },
-                        { label: 'Unhandled', value: unhandled },
-                    ].map((row, index) => (
-                        <TableRow key={index} {...row} />
-                    ))}
-                </table>
-                <table>
-                    {[
-                        { label: 'Browser', value: browser },
-                        { label: 'OS', value: os },
-                        { label: 'URL', value: issueProperties['$current_url'] },
-                    ].map((row, index) => (
-                        <TableRow key={index} {...row} />
-                    ))}
-                </table>
-            </div>
+        <div className="grid grid-cols-2 gap-2">
+            {[
+                { label: 'Level', value: level },
+                { label: 'Synthetic', value: synthetic },
+                { label: 'Library', value: library },
+                { label: 'Unhandled', value: unhandled },
+                { label: 'Browser', value: browser },
+                { label: 'OS', value: os },
+                { label: 'URL', value: issueProperties['$current_url'] },
+            ]
+                .filter((row) => row.value !== undefined)
path: frontend/src/scenes/error-tracking/issue/Overview.tsx
user: login: greptile-apps[bot]
body: logic: filtering out undefined values but not null or empty string values could lead to inconsistent display of data
created_at: 2025-03-17T14:53:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,45 +8,33 @@ export const Overview = (): JSX.Element => {
 
     const { synthetic, level, browser, os, library, unhandled } = getExceptionAttributes(issueProperties)
 
-    const TableRow = ({ label, value }: { label: string; value: string | undefined }): JSX.Element => (
-        <tr>
-            <td className="text-secondary">{label}</td>
-            <td>{value ?? <div className="italic">unknown</div>}</td>
-        </tr>
-    )
-
     if (issueLoading) {
         return (
-            <div className="deprecated-space-y-2">
+            <div className="space-y-2 p-2">
                 <LemonSkeleton />
                 <LemonSkeleton.Row repeat={2} />
             </div>
         )
     }
 
     return (
-        <div className="px-1">
-            <div className="grid grid-cols-2 gap-2">
-                <table>
-                    {[
-                        { label: 'Level', value: level },
-                        { label: 'Synthetic', value: synthetic },
-                        { label: 'Library', value: library },
-                        { label: 'Unhandled', value: unhandled },
-                    ].map((row, index) => (
-                        <TableRow key={index} {...row} />
-                    ))}
-                </table>
-                <table>
-                    {[
-                        { label: 'Browser', value: browser },
-                        { label: 'OS', value: os },
-                        { label: 'URL', value: issueProperties['$current_url'] },
-                    ].map((row, index) => (
-                        <TableRow key={index} {...row} />
-                    ))}
-                </table>
-            </div>
+        <div className="grid grid-cols-2 gap-2">
+            {[
+                { label: 'Level', value: level },
+                { label: 'Synthetic', value: synthetic },
+                { label: 'Library', value: library },
+                { label: 'Unhandled', value: unhandled },
+                { label: 'Browser', value: browser },
+                { label: 'OS', value: os },
+                { label: 'URL', value: issueProperties['$current_url'] },
+            ]
+                .filter((row) => row.value !== undefined)
+                .map((row, index) => (
+                    <div key={index} className="flex items-center justify-between">
+                        <span className="font-semibold w-full">{row.label}</span>
+                        <span className="w-full">{row.value + ''}</span>
path: frontend/src/scenes/error-tracking/issue/Overview.tsx
user: login: greptile-apps[bot]
body: logic: using value + '' to convert to string is unsafe and could cause runtime errors if value is null

```suggestion
                        <span className="w-full">{String(row.value ?? '')}</span>
```
created_at: 2025-03-17T14:53:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+import { LemonSelect } from '@posthog/lemon-ui'
+
+export function GenericSelect<T extends string | number | boolean | null>({
+    current,
+    size,
+    onChange,
+    values,
+    placeholder,
+    renderValue,
+}: {
+    current: T | undefined
path: frontend/src/scenes/error-tracking/issue/StatusSelect.tsx
user: login: greptile-apps[bot]
body: logic: current being undefined while T doesn't include undefined could lead to type mismatches at runtime
created_at: 2025-03-17T14:53:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,45 +8,33 @@ export const Overview = (): JSX.Element => {
 
     const { synthetic, level, browser, os, library, unhandled } = getExceptionAttributes(issueProperties)
 
-    const TableRow = ({ label, value }: { label: string; value: string | undefined }): JSX.Element => (
-        <tr>
-            <td className="text-secondary">{label}</td>
-            <td>{value ?? <div className="italic">unknown</div>}</td>
-        </tr>
-    )
-
     if (issueLoading) {
         return (
-            <div className="deprecated-space-y-2">
+            <div className="space-y-2 p-2">
                 <LemonSkeleton />
                 <LemonSkeleton.Row repeat={2} />
             </div>
         )
     }
 
     return (
-        <div className="px-1">
-            <div className="grid grid-cols-2 gap-2">
-                <table>
-                    {[
-                        { label: 'Level', value: level },
-                        { label: 'Synthetic', value: synthetic },
-                        { label: 'Library', value: library },
-                        { label: 'Unhandled', value: unhandled },
-                    ].map((row, index) => (
-                        <TableRow key={index} {...row} />
-                    ))}
-                </table>
-                <table>
-                    {[
-                        { label: 'Browser', value: browser },
-                        { label: 'OS', value: os },
-                        { label: 'URL', value: issueProperties['$current_url'] },
-                    ].map((row, index) => (
-                        <TableRow key={index} {...row} />
-                    ))}
-                </table>
-            </div>
+        <div className="grid grid-cols-2 gap-2">
+            {[
+                { label: 'Level', value: level },
+                { label: 'Synthetic', value: synthetic },
+                { label: 'Library', value: library },
+                { label: 'Unhandled', value: unhandled },
+                { label: 'Browser', value: browser },
+                { label: 'OS', value: os },
+                { label: 'URL', value: issueProperties['$current_url'] },
+            ]
+                .filter((row) => row.value !== undefined)
+                .map((row, index) => (
+                    <div key={index} className="flex items-center justify-between">
path: frontend/src/scenes/error-tracking/issue/Overview.tsx
user: login: greptile-apps[bot]
body: style: using index as key in map function could cause issues with React's reconciliation if items are reordered

```suggestion
                    <div key={row.label} className="flex items-center justify-between">
```
created_at: 2025-03-17T14:53:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+import clsx from 'clsx'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+export type IssueStatus = ErrorTrackingIssue['status']
+
+export const STATUS_LABEL: Record<ErrorTrackingIssue['status'], string> = {
+    active: 'Active',
+    archived: 'Archived',
+    resolved: 'Resolved',
+    pending_release: 'Pending release',
+    suppressed: 'Suppressed',
+}
+
+export const STATUS_INTENT: Record<ErrorTrackingIssue['status'], Intent> = {
+    active: 'yellow',
+    archived: 'key',
+    resolved: 'blue',
+    pending_release: 'key',
+    suppressed: 'red',
+}
path: frontend/src/scenes/error-tracking/issue/StatusTag.tsx
user: login: greptile-apps[bot]
body: style: Both 'archived' and 'pending_release' use the same 'key' intent - this may make it difficult for users to distinguish between these different states visually
created_at: 2025-03-17T14:53:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+import clsx from 'clsx'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+export type IssueStatus = ErrorTrackingIssue['status']
+
+export const STATUS_LABEL: Record<ErrorTrackingIssue['status'], string> = {
+    active: 'Active',
+    archived: 'Archived',
+    resolved: 'Resolved',
+    pending_release: 'Pending release',
+    suppressed: 'Suppressed',
+}
+
+export const STATUS_INTENT: Record<ErrorTrackingIssue['status'], Intent> = {
+    active: 'yellow',
+    archived: 'key',
+    resolved: 'blue',
+    pending_release: 'key',
+    suppressed: 'red',
+}
+
+export type Intent = 'yellow' | 'blue' | 'red' | 'key'
+
+export const INTENT_CLASS: Record<Intent, string> = {
+    yellow: 'bg-brand-yellow',
+    blue: 'bg-brand-blue',
+    key: 'bg-brand-key',
+    red: 'bg-brand-red',
+}
+
+export function IndicatorTag({
+    intent,
+    label,
+    size,
+}: {
+    intent: 'blue' | 'yellow' | 'red' | 'key'
+    label: string
path: frontend/src/scenes/error-tracking/issue/StatusTag.tsx
user: login: greptile-apps[bot]
body: logic: Intent type in props is manually defined and could get out of sync with the Intent type on line 23. Should use the defined type: intent: Intent

```suggestion
    intent: Intent
    label: string
```
created_at: 2025-03-17T14:53:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+import { useValues } from 'kea'
+import ViewRecordingButton from 'lib/components/ViewRecordingButton/ViewRecordingButton'
+import { errorTrackingIssueSceneLogic } from 'scenes/error-tracking/errorTrackingIssueSceneLogic'
+import { getSessionId } from 'scenes/error-tracking/utils'
+
+import { Overview } from '../Overview'
+import { Widget } from './Widget'
+
+export function DetailsWidget(): JSX.Element {
+    const { issueProperties } = useValues(errorTrackingIssueSceneLogic)
+    const sessionId = getSessionId(issueProperties)
+    return (
+        <Widget.Root>
+            <Widget.Header title="Details">
+                <ViewRecordingButton
+                    sessionId={sessionId}
+                    timestamp={issueProperties.timestamp}
+                    inModal={true}
+                    size="xsmall"
+                    type="primary"
+                    disabledReason={sessionId ? '' : 'No recording available'}
path: frontend/src/scenes/error-tracking/issue/widgets/DetailsWidget.tsx
user: login: greptile-apps[bot]
body: style: disabledReason should be more descriptive about why no recording is available (e.g. 'No session recording found for this error')
created_at: 2025-03-17T14:53:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+function WidgetHeader({ title, children }: { title: string; children?: React.ReactNode }): JSX.Element {
+    return (
+        <div className="flex justify-between items-center border-b-1 border-primary px-2 py-1 ">
path: frontend/src/scenes/error-tracking/issue/widgets/Widget.tsx
user: login: greptile-apps[bot]
body: syntax: extra space at end of className string
created_at: 2025-03-17T14:53:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,73 @@
+import { IconFilter, IconSort } from '@posthog/icons'
+import { LemonButton } from '@posthog/lemon-ui'
+import { useActions, useValues } from 'kea'
+import { stackFrameLogic } from 'lib/components/Errors/stackFrameLogic'
+import { ChainedStackTraces } from 'lib/components/Errors/StackTraces'
+import { ErrorTrackingException } from 'lib/components/Errors/types'
+
+import { errorTrackingIssueSceneLogic } from '../../errorTrackingIssueSceneLogic'
+import { getExceptionAttributes, hasAnyInAppFrames } from '../../utils'
+import { Widget } from './Widget'
+
+export function StacktraceWidget(): JSX.Element {
+    const { issueProperties } = useValues(errorTrackingIssueSceneLogic)
+
+    const { showAllFrames, frameOrderReversed } = useValues(stackFrameLogic)
+    const { setShowAllFrames, reverseFrameOrder } = useActions(stackFrameLogic)
+    const { exceptionList } = getExceptionAttributes(issueProperties)
+
+    const hasAnyInApp = hasAnyInAppFrames(exceptionList)
+    const orderedExceptions = applyFrameOrder(exceptionList, frameOrderReversed)
+
+    return (
+        <Widget.Root>
+            <Widget.Header title="Stacktrace">
+                <div className="flex gap-2">
+                    <LemonButton
+                        className="space-x-2"
+                        type="tertiary"
+                        size="xsmall"
+                        onClick={() => reverseFrameOrder(!frameOrderReversed)}
+                    >
+                        <span className="me-1">{frameOrderReversed ? 'First call' : 'Last call'}</span>
+                        <IconSort />
+                    </LemonButton>
+                    {hasAnyInApp && (
+                        <LemonButton
+                            className="space-x-2"
+                            type="tertiary"
+                            size="xsmall"
+                            onClick={() => setShowAllFrames(!showAllFrames)}
+                        >
+                            <span className="me-1">{showAllFrames ? 'Full stack' : 'In app'}</span>
+                            <IconFilter />
+                        </LemonButton>
+                    )}
+                </div>
+            </Widget.Header>
+            <Widget.Body>
+                <ChainedStackTraces
+                    showAllFrames={hasAnyInApp ? showAllFrames : true}
+                    exceptionList={orderedExceptions}
+                />
+            </Widget.Body>
+        </Widget.Root>
+    )
+}
+
+function applyFrameOrder(
+    exceptionList: ErrorTrackingException[],
+    frameOrderReversed: boolean
+): ErrorTrackingException[] {
+    if (frameOrderReversed) {
+        exceptionList = exceptionList
+            .map((exception) => {
path: frontend/src/scenes/error-tracking/issue/widgets/StacktraceWidget.tsx
user: login: greptile-apps[bot]
body: logic: Direct mutation of input parameter. Consider cloning exceptionList first to avoid side effects.

```suggestion
        const clonedList = JSON.parse(JSON.stringify(exceptionList))
            .map((exception) => {
```
created_at: 2025-03-17T14:53:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,73 @@
+import { IconFilter, IconSort } from '@posthog/icons'
+import { LemonButton } from '@posthog/lemon-ui'
+import { useActions, useValues } from 'kea'
+import { stackFrameLogic } from 'lib/components/Errors/stackFrameLogic'
+import { ChainedStackTraces } from 'lib/components/Errors/StackTraces'
+import { ErrorTrackingException } from 'lib/components/Errors/types'
+
+import { errorTrackingIssueSceneLogic } from '../../errorTrackingIssueSceneLogic'
+import { getExceptionAttributes, hasAnyInAppFrames } from '../../utils'
+import { Widget } from './Widget'
+
+export function StacktraceWidget(): JSX.Element {
+    const { issueProperties } = useValues(errorTrackingIssueSceneLogic)
+
+    const { showAllFrames, frameOrderReversed } = useValues(stackFrameLogic)
+    const { setShowAllFrames, reverseFrameOrder } = useActions(stackFrameLogic)
+    const { exceptionList } = getExceptionAttributes(issueProperties)
+
+    const hasAnyInApp = hasAnyInAppFrames(exceptionList)
+    const orderedExceptions = applyFrameOrder(exceptionList, frameOrderReversed)
+
+    return (
+        <Widget.Root>
+            <Widget.Header title="Stacktrace">
+                <div className="flex gap-2">
+                    <LemonButton
+                        className="space-x-2"
+                        type="tertiary"
+                        size="xsmall"
+                        onClick={() => reverseFrameOrder(!frameOrderReversed)}
+                    >
+                        <span className="me-1">{frameOrderReversed ? 'First call' : 'Last call'}</span>
+                        <IconSort />
+                    </LemonButton>
+                    {hasAnyInApp && (
+                        <LemonButton
+                            className="space-x-2"
+                            type="tertiary"
+                            size="xsmall"
+                            onClick={() => setShowAllFrames(!showAllFrames)}
+                        >
+                            <span className="me-1">{showAllFrames ? 'Full stack' : 'In app'}</span>
+                            <IconFilter />
+                        </LemonButton>
+                    )}
+                </div>
+            </Widget.Header>
+            <Widget.Body>
+                <ChainedStackTraces
+                    showAllFrames={hasAnyInApp ? showAllFrames : true}
+                    exceptionList={orderedExceptions}
+                />
+            </Widget.Body>
+        </Widget.Root>
+    )
+}
+
+function applyFrameOrder(
+    exceptionList: ErrorTrackingException[],
+    frameOrderReversed: boolean
+): ErrorTrackingException[] {
+    if (frameOrderReversed) {
+        exceptionList = exceptionList
+            .map((exception) => {
+                if (exception.stacktrace) {
+                    exception.stacktrace.frames = exception.stacktrace.frames.slice().reverse()
path: frontend/src/scenes/error-tracking/issue/widgets/StacktraceWidget.tsx
user: login: greptile-apps[bot]
body: logic: Mutating stacktrace.frames directly could cause issues with React's state management. Consider creating a new exception object with spread operator.

```suggestion
                    exception.stacktrace = {
                        ...exception.stacktrace,
                        frames: [...exception.stacktrace.frames].reverse()
                    }
```
created_at: 2025-03-17T14:53:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,17 +65,20 @@ export const errorTrackingIssueQuery = ({
     issueId,
     dateRange,
     customVolume,
+    cacheKey,
 }: {
     issueId: string
     dateRange: DateRange
     customVolume?: ErrorTrackingSparklineConfig | null
+    cacheKey?: string
path: frontend/src/scenes/error-tracking/queries.ts
user: login: greptile-apps[bot]
body: logic: cacheKey is optional in errorTrackingIssueQuery but required in the Pick type above. This inconsistency could cause type errors
created_at: 2025-03-17T14:54:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -232,6 +211,17 @@ def activity(self, request: request.Request, **kwargs):
         return activity_page_response(activity_page, limit, page, request)
 
 
+def get_status_from_string(status: str) -> ErrorTrackingIssue.Status | None:
+    match status:
+        case "active":
+            return ErrorTrackingIssue.Status.ACTIVE
+        case "resolved":
+            return ErrorTrackingIssue.Status.RESOLVED
+        case "suppressed":
+            return ErrorTrackingIssue.Status.SUPPRESSED
+    return None
path: posthog/api/error_tracking.py
user: login: greptile-apps[bot]
body: logic: Missing 'archived' and 'pending_release' status cases which are defined in the STATUS_LABEL constant in the frontend
created_at: 2025-03-17T14:54:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,11 +97,36 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
             actions.setResponse({
                 ...values.response,
                 // remove suppressed issues
-                results: results.filter(({ id }) => !ids.includes(id)),
+                results: results.map((issue) => {
+                    if (ids.includes(issue.id)) {
+                        return { ...issue, status: 'suppressed' }
path: frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx
user: login: daibhin
body: I'm assuming this is a UI optimisation not to remove them from the list immediately and cause jumping?
created_at: 2025-03-17T15:43:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,11 +97,36 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
             actions.setResponse({
                 ...values.response,
                 // remove suppressed issues
-                results: results.filter(({ id }) => !ids.includes(id)),
+                results: results.map((issue) => {
+                    if (ids.includes(issue.id)) {
+                        return { ...issue, status: 'suppressed' }
+                    }
+                    return issue
+                }),
             })
+
             posthog.capture('error_tracking_issue_bulk_suppress')
-            await api.errorTracking.bulkSuppress(ids)
-            actions.loadData(true)
+            await api.errorTracking.bulkMarkStatus(ids, 'suppressed')
+            actions.reloadData()
+        },
+        activateIssues: async ({ ids }) => {
+            const { results } = values
+
+            // optimistically update local results
+            actions.setResponse({
+                ...values.response,
+                // remove suppressed issues
path: frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx
user: login: daibhin
body: Copy paste error?
created_at: 2025-03-17T15:44:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,24 +18,33 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
     path(['scenes', 'error-tracking', 'errorTrackingDataNodeLogic']),
     props({} as ErrorTrackingDataNodeLogicProps),
 
-    connect(({ key, query }: ErrorTrackingDataNodeLogicProps) => ({
-        values: [dataNodeLogic({ key, query }), ['response']],
-        actions: [dataNodeLogic({ key, query }), ['setResponse', 'loadData']],
-    })),
+    connect(({ key, query }: ErrorTrackingDataNodeLogicProps) => {
+        const nodeLogic = dataNodeLogic({ key, query })
+        return {
+            values: [nodeLogic, ['response']],
+            actions: [nodeLogic, ['setResponse', 'loadData'], errorTrackingLogic, ['refreshCacheKey']],
+        }
+    }),
 
     actions({
         mergeIssues: (ids: string[]) => ({ ids }),
         resolveIssues: (ids: string[]) => ({ ids }),
         suppressIssues: (ids: string[]) => ({ ids }),
+        activateIssues: (ids: string[]) => ({ ids }),
         assignIssues: (ids: string[], assignee: ErrorTrackingIssue['assignee']) => ({ ids, assignee }),
         assignIssue: (id: string, assignee: ErrorTrackingIssue['assignee']) => ({ id, assignee }),
+        reloadData: () => ({}),
     }),
 
     selectors({
         results: [(s) => [s.response], (response): ErrorTrackingIssue[] => (response ? response.results : [])],
     }),
 
     listeners(({ values, actions }) => ({
+        reloadData: async () => {
+            actions.refreshCacheKey()
path: frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx
user: login: daibhin
body: I'm assuming none of the other refresh type options worked? What does this actually do under the hood?
created_at: 2025-03-17T15:45:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -232,6 +211,17 @@ def activity(self, request: request.Request, **kwargs):
         return activity_page_response(activity_page, limit, page, request)
 
 
+def get_status_from_string(status: str) -> ErrorTrackingIssue.Status | None:
path: posthog/api/error_tracking.py
user: login: daibhin
body: Is this just to satisfy the linter or also a form of validation?
created_at: 2025-03-17T15:47:23Z
author_association: CONTRIBUTOR
path: frontend/src/scenes/error-tracking/issue/widgets/Widget.tsx
user: login: daibhin
body: Can we use the `PanelLayout` components here instead or an entirely new "widgets" concept?
created_at: 2025-03-17T15:49:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+import { LemonSelect } from '@posthog/lemon-ui'
+
+export function GenericSelect<T extends string | number | boolean | null>({
path: frontend/src/scenes/error-tracking/issue/StatusSelect.tsx
user: login: daibhin
body: ```suggestion
export function StatusSelect<T extends string | number | boolean | null>({
```
created_at: 2025-03-17T15:50:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -36,72 +39,74 @@ export const STATUS_LABEL: Record<ErrorTrackingIssue['status'], string> = {
 export function ErrorTrackingIssueScene(): JSX.Element {
     const { issue } = useValues(errorTrackingIssueSceneLogic)
     const { updateIssue, initIssue, assignIssue } = useActions(errorTrackingIssueSceneLogic)
+    const [activeTab, setActiveTab] = useState('stacktrace')
 
     useEffect(() => {
         initIssue()
-    }, [])
+    }, [initIssue])
 
     return (
         <ErrorTrackingSetupPrompt>
             <>
                 <PageHeader
                     buttons={
-                        issue ? (
-                            issue.status === 'active' ? (
-                                <div className="flex divide-x gap-x-2">
-                                    <AssigneeSelect
-                                        assignee={issue.assignee}
-                                        onChange={assignIssue}
-                                        type="secondary"
-                                        showName
-                                    />
-                                    <div className="flex pl-2 gap-x-2">
-                                        <LemonButton
-                                            type="secondary"
-                                            onClick={() => updateIssue({ status: 'archived' })}
-                                        >
-                                            Archive
-                                        </LemonButton>
-                                        <LemonButton type="primary" onClick={() => updateIssue({ status: 'resolved' })}>
-                                            Resolve
-                                        </LemonButton>
-                                        <LemonButton
-                                            type="secondary"
-                                            status="danger"
-                                            tooltip="Stop capturing these errors"
-                                            onClick={() => updateIssue({ status: 'suppressed' })}
-                                        >
-                                            Suppress
-                                        </LemonButton>
-                                    </div>
-                                </div>
-                            ) : (
-                                <LemonButton
+                        <div className="flex divide-x gap-x-2">
+                            {issue && issue.status == 'active' && (
+                                <AssigneeSelect
+                                    assignee={issue.assignee}
+                                    onChange={assignIssue}
                                     type="secondary"
-                                    className="upcasefirst-letter:uppercase"
-                                    onClick={() => updateIssue({ status: 'active' })}
-                                    tooltip="Mark as active"
-                                >
-                                    {STATUS_LABEL[issue.status]}
-                                </LemonButton>
-                            )
-                        ) : (
-                            false
-                        )
+                                    showName
+                                />
+                            )}
+                            {issue && (
+                                <GenericSelect
+                                    size="small"
+                                    current={issue.status}
+                                    values={['active', 'resolved', 'suppressed']}
+                                    placeholder="Mark as"
+                                    renderValue={(value) => {
+                                        return <StatusTag status={value as IssueStatus} size="small" />
+                                    }}
+                                    onChange={(value) => {
+                                        updateIssue({ status: value })
+                                    }}
path: frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx
user: login: daibhin
body: ```suggestion
                                    onChange={(status) => updateIssue({ status })}
```
created_at: 2025-03-17T15:53:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+import { LemonButton } from '@posthog/lemon-ui'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+import { AssigneeSelect } from '../AssigneeSelect'
+import { GenericSelect } from './StatusSelect'
+import { IssueStatus, StatusTag } from './StatusTag'
+
+export interface BulkActionsProps {
+    issueSelection: ErrorTrackingIssue[]
+    onAssign: (issueIds: string[], assigneeId: ErrorTrackingIssue['assignee']) => void
+    onMerge: (issueIds: string[]) => void
+    onResolve: (issueIds: string[]) => void
+    onSuppress: (issueIds: string[]) => void
+    onActivate: (issueIds: string[]) => void
+    onSelectAll: () => void
+    onClearSelection: () => void
+}
+
+export function BulkActions({
+    onMerge,
+    onResolve,
+    onSuppress,
+    onActivate,
+    onAssign,
+    issueSelection,
+    onSelectAll,
+    onClearSelection,
path: frontend/src/scenes/error-tracking/issue/BulkActions.tsx
user: login: daibhin
body: Could this be propless? Given the actions are specific to error tracking it feels like we could have all / most of these props internal to the component via `useActions` from the logic
created_at: 2025-03-17T15:55:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6755,6 +6755,9 @@
                         }
                     ]
                 },
+                "cacheKey": {
path: frontend/src/queries/schema.json
user: login: daibhin
body: I'm weary of this cacheKey change. It feels like we're hacking around the system somewhat by causing a cache miss with the random uuid being regenerated. At the very least I would expect `force_blocking` refresh param to invalidate the cache and return new results. Do we understand fully why it is not doing that? If not I suggest we check with product analytics to see if we're using it wrong. It could well be a bug with refresh behaviour but we should fix that instead
created_at: 2025-03-17T15:58:59Z
author_association: CONTRIBUTOR
path: frontend/src/scenes/error-tracking/issue/StatusTag.tsx
user: login: daibhin
body: This almost feels like a lemon component. We already have LemonTag and LemonSnack so I'm hesitant to add another. Could they be extended in any way to support what you're looking to do here?
created_at: 2025-03-17T16:01:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -232,6 +211,17 @@ def activity(self, request: request.Request, **kwargs):
         return activity_page_response(activity_page, limit, page, request)
 
 
+def get_status_from_string(status: str) -> ErrorTrackingIssue.Status | None:
path: posthog/api/error_tracking.py
user: login: hpouillot
body: A mix of both, I just want to make sure unexpected strings are handled properly.
created_at: 2025-03-17T16:22:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+import { LemonButton } from '@posthog/lemon-ui'
+
+import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
+
+import { AssigneeSelect } from '../AssigneeSelect'
+import { GenericSelect } from './StatusSelect'
+import { IssueStatus, StatusTag } from './StatusTag'
+
+export interface BulkActionsProps {
+    issueSelection: ErrorTrackingIssue[]
+    onAssign: (issueIds: string[], assigneeId: ErrorTrackingIssue['assignee']) => void
+    onMerge: (issueIds: string[]) => void
+    onResolve: (issueIds: string[]) => void
+    onSuppress: (issueIds: string[]) => void
+    onActivate: (issueIds: string[]) => void
+    onSelectAll: () => void
+    onClearSelection: () => void
+}
+
+export function BulkActions({
+    onMerge,
+    onResolve,
+    onSuppress,
+    onActivate,
+    onAssign,
+    issueSelection,
+    onSelectAll,
+    onClearSelection,
path: frontend/src/scenes/error-tracking/issue/BulkActions.tsx
user: login: hpouillot
body: Sure, I'm not used to kea yet
created_at: 2025-03-17T16:29:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,11 +97,36 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
             actions.setResponse({
                 ...values.response,
                 // remove suppressed issues
-                results: results.filter(({ id }) => !ids.includes(id)),
+                results: results.map((issue) => {
+                    if (ids.includes(issue.id)) {
+                        return { ...issue, status: 'suppressed' }
path: frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx
user: login: hpouillot
body: Yes, if the user selected "all" status filter, I think we should keep showing suppressed events
created_at: 2025-03-17T17:09:34Z
author_association: CONTRIBUTOR
path: frontend/src/scenes/error-tracking/issue/StatusTag.tsx
user: login: hpouillot
body: LemonTag and LemonSnack won't play well inside a Select but I could probably use a LemonBadge for the color indication.
created_at: 2025-03-17T17:44:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,24 +18,33 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
     path(['scenes', 'error-tracking', 'errorTrackingDataNodeLogic']),
     props({} as ErrorTrackingDataNodeLogicProps),
 
-    connect(({ key, query }: ErrorTrackingDataNodeLogicProps) => ({
-        values: [dataNodeLogic({ key, query }), ['response']],
-        actions: [dataNodeLogic({ key, query }), ['setResponse', 'loadData']],
-    })),
+    connect(({ key, query }: ErrorTrackingDataNodeLogicProps) => {
+        const nodeLogic = dataNodeLogic({ key, query })
+        return {
+            values: [nodeLogic, ['response']],
+            actions: [nodeLogic, ['setResponse', 'loadData'], errorTrackingLogic, ['refreshCacheKey']],
+        }
+    }),
 
     actions({
         mergeIssues: (ids: string[]) => ({ ids }),
         resolveIssues: (ids: string[]) => ({ ids }),
         suppressIssues: (ids: string[]) => ({ ids }),
+        activateIssues: (ids: string[]) => ({ ids }),
         assignIssues: (ids: string[], assignee: ErrorTrackingIssue['assignee']) => ({ ids, assignee }),
         assignIssue: (id: string, assignee: ErrorTrackingIssue['assignee']) => ({ id, assignee }),
+        reloadData: () => ({}),
     }),
 
     selectors({
         results: [(s) => [s.response], (response): ErrorTrackingIssue[] => (response ? response.results : [])],
     }),
 
     listeners(({ values, actions }) => ({
+        reloadData: async () => {
+            actions.refreshCacheKey()
path: frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx
user: login: hpouillot
body: Yes, 'force_blocking' seems to be the refresh type we need but I still hit the cache with it.

https://github.com/PostHog/posthog/blob/9480fb51a0d9ae5ae1e29c198d7d0612d4bc4e23/posthog/hogql_queries/query_runner.py#L87-L88
created_at: 2025-03-17T18:23:05Z
author_association: CONTRIBUTOR
path: frontend/src/scenes/error-tracking/issue/widgets/Widget.tsx
user: login: hpouillot
body: I switched to LemonWidget, I didn't see there was an 'actions' prop.
created_at: 2025-03-17T18:50:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6755,6 +6755,9 @@
                         }
                     ]
                 },
+                "cacheKey": {
path: frontend/src/queries/schema.json
user: login: hpouillot
body: I agree adding this key is far from optimal. I'll check with product analytics team about the force_blocking param
created_at: 2025-03-17T18:52:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+import { LemonSelect } from '@posthog/lemon-ui'
+
+export function GenericSelect<T extends string | number | boolean | null>({
path: frontend/src/scenes/error-tracking/issue/StatusSelect.tsx
user: login: hpouillot
body: The select is actually generic, I just changed the api to allow rendering option based on values
created_at: 2025-03-17T18:57:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,20 +72,22 @@ export function ErrorTrackingScene(): JSX.Element {
     }
 
     return (
-        <ErrorTrackingSetupPrompt>
-            <BindLogic logic={errorTrackingDataNodeLogic} props={{ query, key: insightVizDataNodeKey(insightProps) }}>
-                <Header />
-                {hasSentExceptionEventLoading ? null : hasSentExceptionEvent ? (
-                    <FeedbackNotice text="Error tracking is currently in beta. Thanks for taking part! We'd love to hear what you think." />
-                ) : (
-                    <IngestionStatusCheck />
-                )}
-                <ErrorTrackingFilters />
-                <LemonDivider className="mt-2" />
-                <ErrorTrackingListOptions />
-                <Query query={query} context={context} />
-            </BindLogic>
-        </ErrorTrackingSetupPrompt>
+        <FloatingContainerContext.Provider value={floatingContainerRef}>
path: frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx
user: login: daibhin
body: Why is this needed?
created_at: 2025-03-19T11:18:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,24 +17,32 @@ export const errorTrackingDataNodeLogic = kea<errorTrackingDataNodeLogicType>([
     path(['scenes', 'error-tracking', 'errorTrackingDataNodeLogic']),
     props({} as ErrorTrackingDataNodeLogicProps),
 
-    connect(({ key, query }: ErrorTrackingDataNodeLogicProps) => ({
-        values: [dataNodeLogic({ key, query }), ['response']],
-        actions: [dataNodeLogic({ key, query }), ['setResponse', 'loadData']],
-    })),
+    connect(({ key, query }: ErrorTrackingDataNodeLogicProps) => {
+        const nodeLogic = dataNodeLogic({ key, query, refresh: 'blocking' })
+        return {
+            values: [nodeLogic, ['response']],
+            actions: [nodeLogic, ['setResponse', 'loadData']],
+        }
+    }),
 
     actions({
         mergeIssues: (ids: string[]) => ({ ids }),
         resolveIssues: (ids: string[]) => ({ ids }),
         suppressIssues: (ids: string[]) => ({ ids }),
+        activateIssues: (ids: string[]) => ({ ids }),
         assignIssues: (ids: string[], assignee: ErrorTrackingIssue['assignee']) => ({ ids, assignee }),
         assignIssue: (id: string, assignee: ErrorTrackingIssue['assignee']) => ({ id, assignee }),
+        reloadData: () => ({}),
     }),
 
     selectors({
         results: [(s) => [s.response], (response): ErrorTrackingIssue[] => (response ? response.results : [])],
     }),
 
     listeners(({ values, actions }) => ({
+        reloadData: async () => {
+            actions.loadData('force_blocking')
+        },
path: frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx
user: login: daibhin
body: 👌 

Maybe worth renaming this to `forceReloadData`
created_at: 2025-03-19T11:19:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-17T15:01:21Z
body: **Size Change:** +27 B (0%) 

**Total Size:** 9.74 MB



<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size | Change |
| :--- | :---: | :---: |
| `frontend/dist/toolbar.js` | 9.74 MB | +27 B (0%) |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR
user: login: oliverb123
created_at: 2025-03-17T19:11:51Z
body: This is really nice. Couple of nits from this screen (on safari):
<img width="1241" alt="image" src="https://github.com/user-attachments/assets/424aabf9-a639-46b2-865c-cda06c2f9a46" />


- The url isn't wrapping and goes off screen
- The stacktrace panel appears even though there's no stack trace for this issue

These are probably out of scope, but if you're doing a big refactor anyway and feel like looking at them
- The "details" panel still feels really sparse and too front-and-center vs. the events list or stacktrace - could it be to the right of the sparklines instead?
- The sparklines needs an X axis, or at least some signifier of the time range it's over
- I still wish sparkline bars had a hover state indicating which one is hovered
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-03-18T14:21:53Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 1](https://github.com/PostHog/posthog/pull/30019/commits/ec38b3deb63df2050fb20127873b33760b6bf666))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30019/commits/61fc4b8bfcdcd7b595f53801b446d549d75e8c31).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30019/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-03-18T14:41:27Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 1](https://github.com/PostHog/posthog/pull/30019/commits/bb52509cbafb6f17a34a895eaee62881e4391852))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30019/commits/a479602b831804ef9d8c6d04ad24d10cf1246d86).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30019/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR