Repository: PostHog/posthog
Pull request #31374: feat(err): add pagination to symbol set table
Author: login: hpouillot
Created: 2025-04-18T16:56:09Z | Merged: 2025-04-22T08:16:54Z

## Problem

- Symbol set table is limited to 100 results

## Changes

- Reorder symbol sets per creation date desc
- Add filters on status
- Add pagination

<img width="908" alt="image" src="https://github.com/user-attachments/assets/a3d85807-2e37-45b0-b6ac-3365cd07c902" />




Diff:
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png
index fa677584329d..bf4b006aec52 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project--light.png b/frontend/__snapshots__/scenes-other-settings--settings-project--light.png
index bd9b9672b9ce..276553e4b3ac 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-project--light.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png
index 8e6335522880..680496abff13 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png
index 882e4a2be85f..2410c18c463a 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png
index fa677584329d..bf4b006aec52 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png
index bd9b9672b9ce..276553e4b3ac 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png
index fa677584329d..bf4b006aec52 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png
index bd9b9672b9ce..276553e4b3ac 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--dark.png
index fa677584329d..bf4b006aec52 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--light.png
index bd9b9672b9ce..276553e4b3ac 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--light.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--dark.png
index fa677584329d..bf4b006aec52 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--light.png
index bd9b9672b9ce..276553e4b3ac 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--light.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--dark.png
index fa677584329d..bf4b006aec52 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--light.png
index bd9b9672b9ce..276553e4b3ac 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--light.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--dark.png
index fa677584329d..bf4b006aec52 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--dark.png differ
diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--light.png
index bd9b9672b9ce..276553e4b3ac 100644
Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--light.png differ
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 3e9369c4b7d0..5aab1762642f 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -140,6 +140,7 @@ import {
     ErrorTrackingStackFrame,
     ErrorTrackingStackFrameRecord,
     ErrorTrackingSymbolSet,
+    SymbolSetStatusFilter,
 } from './components/Errors/types'
 import {
     ACTIVITY_PAGE_SIZE,
@@ -2233,16 +2234,27 @@ const api = {
                 .create({ data: { ids: mergingIssueIds } })
         },
 
-        async updateSymbolSet(id: ErrorTrackingSymbolSet['id'], data: FormData): Promise<void> {
-            return await new ApiRequest().errorTrackingSymbolSet(id).update({ data })
-        },
+        symbolSets: {
+            async list({
+                status,
+                offset = 0,
+                limit = 100,
+            }: {
+                status?: SymbolSetStatusFilter
+                offset: number
+                limit: number
+            }): Promise<CountedPaginatedResponse<ErrorTrackingSymbolSet>> {
+                const queryString = { order_by: '-created_at', status, offset, limit }
+                return await new ApiRequest().errorTrackingSymbolSets().withQueryString(toParams(queryString)).get()
+            },
 
-        async deleteSymbolSet(id: ErrorTrackingSymbolSet['id']): Promise<void> {
-            return await new ApiRequest().errorTrackingSymbolSet(id).delete()
-        },
+            async update(id: ErrorTrackingSymbolSet['id'], data: FormData): Promise<void> {
+                return await new ApiRequest().errorTrackingSymbolSet(id).update({ data })
+            },
 
-        async symbolSets(): Promise<{ results: ErrorTrackingSymbolSet[] }> {
-            return await new ApiRequest().errorTrackingSymbolSets().get()
+            async delete(id: ErrorTrackingSymbolSet['id']): Promise<void> {
+                return await new ApiRequest().errorTrackingSymbolSet(id).delete()
+            },
         },
 
         async symbolSetStackFrames(
diff --git a/frontend/src/lib/components/Errors/types.ts b/frontend/src/lib/components/Errors/types.ts
index db96580f17db..86c4079e5d3f 100644
--- a/frontend/src/lib/components/Errors/types.ts
+++ b/frontend/src/lib/components/Errors/types.ts
@@ -60,3 +60,6 @@ export interface ErrorTrackingSymbolSet {
     storage_ptr: string | null
     failure_reason: string | null
 }
+
+export type SymbolSetStatus = 'valid' | 'invalid'
+export type SymbolSetStatusFilter = SymbolSetStatus | 'all'
diff --git a/frontend/src/scenes/error-tracking/configuration/symbol-sets/ErrorTrackingSymbolSets.tsx b/frontend/src/scenes/error-tracking/configuration/symbol-sets/ErrorTrackingSymbolSets.tsx
index 70c672d5f5a2..8d2026c48e72 100644
--- a/frontend/src/scenes/error-tracking/configuration/symbol-sets/ErrorTrackingSymbolSets.tsx
+++ b/frontend/src/scenes/error-tracking/configuration/symbol-sets/ErrorTrackingSymbolSets.tsx
@@ -1,8 +1,17 @@
-import { IconRevert, IconTrash, IconUpload } from '@posthog/icons'
-import { LemonButton, LemonCollapse, LemonDialog, LemonTable, LemonTableColumns, LemonTabs } from '@posthog/lemon-ui'
+import { IconCheckCircle, IconRevert, IconTrash, IconUpload, IconWarning } from '@posthog/icons'
+import {
+    LemonButton,
+    LemonCollapse,
+    LemonDialog,
+    LemonSegmentedButton,
+    LemonTable,
+    LemonTableColumns,
+    LemonTabs,
+    Tooltip,
+} from '@posthog/lemon-ui'
 import { useActions, useValues } from 'kea'
 import { stackFrameLogic } from 'lib/components/Errors/stackFrameLogic'
-import { ErrorTrackingSymbolSet } from 'lib/components/Errors/types'
+import { ErrorTrackingSymbolSet, SymbolSetStatusFilter } from 'lib/components/Errors/types'
 import { JSONViewer } from 'lib/components/JSONViewer'
 import { humanFriendlyDetailedTime } from 'lib/utils'
 import { useEffect, useState } from 'react'
@@ -10,10 +19,24 @@ import { useEffect, useState } from 'react'
 import { errorTrackingSymbolSetLogic } from './errorTrackingSymbolSetLogic'
 import { SymbolSetUploadModal } from './SymbolSetUploadModal'
 
+const SYMBOL_SET_FILTER_OPTIONS = [
+    {
+        label: <IconCheckCircle />,
+        value: 'valid',
+    },
+    {
+        label: <IconWarning />,
+        value: 'invalid',
+    },
+    {
+        label: 'All',
+        value: 'all',
+    },
+] as { label: string; value: SymbolSetStatusFilter }[]
+
 export function ErrorTrackingSymbolSets(): JSX.Element {
-    const logic = errorTrackingSymbolSetLogic()
-    const { missingSymbolSets, validSymbolSets } = useValues(logic)
-    const { loadSymbolSets } = useActions(logic)
+    const { symbolSetStatusFilter } = useValues(errorTrackingSymbolSetLogic)
+    const { loadSymbolSets, setSymbolSetStatusFilter } = useActions(errorTrackingSymbolSetLogic)
 
     useEffect(() => {
         loadSymbolSets()
@@ -29,51 +52,74 @@ export function ErrorTrackingSymbolSets(): JSX.Element {
                 Cases where it was not possible are listed below. Source maps can be uploaded retroactively but changes
                 will only apply to all future exceptions ingested.
             </p>
-            {missingSymbolSets.length > 0 && (
-                <SymbolSetTable id="missing" dataSource={missingSymbolSets} pageSize={5} missing />
-            )}
-            {(validSymbolSets.length > 0 || missingSymbolSets.length === 0) && (
-                <SymbolSetTable id="valid" dataSource={validSymbolSets} pageSize={10} />
-            )}
+            <div className="space-y-2">
+                <div className="flex justify-end items-center gap-2">
+                    <span className="mb-0">Status:</span>
+                    <LemonSegmentedButton
+                        size="xsmall"
+                        value={symbolSetStatusFilter}
+                        options={SYMBOL_SET_FILTER_OPTIONS}
+                        onChange={setSymbolSetStatusFilter}
+                    />
+                </div>
+                <SymbolSetTable />
+            </div>
             <SymbolSetUploadModal />
         </div>
     )
 }
 
-const SymbolSetTable = ({
-    id,
-    dataSource,
-    pageSize,
-    missing,
-}: {
-    id: string
-    dataSource: ErrorTrackingSymbolSet[]
-    pageSize: number
-    missing?: boolean
-}): JSX.Element => {
-    const { symbolSetsLoading } = useValues(errorTrackingSymbolSetLogic)
+const SymbolSetTable = (): JSX.Element => {
+    // @ts-expect-error: automagical typing does not work here for some obscure reason
+    const { pagination, symbolSets, symbolSetResponseLoading } = useValues(errorTrackingSymbolSetLogic)
     const { deleteSymbolSet, setUploadSymbolSetId } = useActions(errorTrackingSymbolSetLogic)
 
     const columns: LemonTableColumns<ErrorTrackingSymbolSet> = [
-        { title: missing && 'Missing symbol sets', dataIndex: 'ref' },
+        {
+            title: 'Source',
+            width: 200,
+            render: (_, { ref }) => {
+                return (
+                    <div className="truncate w-100 overflow-hidden rtl py-0.5" title={ref}>
+                        {ref}
+                    </div>
+                )
+            },
+        },
+        {
+            title: 'Status',
+            render: (_, { failure_reason }) => {
+                return (
+                    <Tooltip title={failure_reason} placement="top">
+                        {failure_reason ? (
+                            <span className="text-danger cursor-pointer">
+                                <IconWarning /> Missing
+                            </span>
+                        ) : (
+                            <span className="text-success">
+                                <IconCheckCircle /> Uploaded
+                            </span>
+                        )}
+                    </Tooltip>
+                )
+            },
+        },
         { title: 'Created At', dataIndex: 'created_at', render: (data) => humanFriendlyDetailedTime(data as string) },
         {
             dataIndex: 'id',
-            render: (_, { id }) => {
+            align: 'right',
+            render: (_, { id, failure_reason }) => {
                 return (
-                    <div className="flex justify-end deprecated-space-x-1">
+                    <div className="flex justify-end items-center gap-1">
                         <LemonButton
-                            type={missing ? 'primary' : 'secondary'}
+                            type={failure_reason ? 'primary' : 'tertiary'}
                             size="xsmall"
-                            tooltip={missing ? 'Upload symbol set' : 'Replace symbol set'}
-                            icon={missing ? <IconUpload /> : <IconRevert />}
+                            tooltip={failure_reason ? 'Upload symbol set' : 'Replace symbol set'}
+                            icon={failure_reason ? <IconUpload /> : <IconRevert />}
                             onClick={() => setUploadSymbolSetId(id)}
-                            className="py-1"
-                        >
-                            {missing && 'Upload'}
-                        </LemonButton>
+                        />
                         <LemonButton
-                            type="secondary"
+                            type="tertiary"
                             size="xsmall"
                             tooltip="Delete symbol set"
                             icon={<IconTrash />}
@@ -92,7 +138,6 @@ const SymbolSetTable = ({
                                     },
                                 })
                             }
-                            className="py-1"
                         />
                     </div>
                 )
@@ -100,18 +145,13 @@ const SymbolSetTable = ({
         },
     ]
 
-    if (missing) {
-        columns.splice(1, 0, { title: 'Failure reason', dataIndex: 'failure_reason' })
-    }
-
     return (
         <LemonTable
-            id={id}
-            showHeader={missing}
-            pagination={{ pageSize }}
+            id="symbol-sets"
+            pagination={pagination}
             columns={columns}
-            loading={symbolSetsLoading}
-            dataSource={dataSource}
+            loading={symbolSetResponseLoading}
+            dataSource={symbolSets}
             expandable={{
                 noIndent: true,
                 expandedRowRender: function RenderPropertiesTable(symbolSet) {
diff --git a/frontend/src/scenes/error-tracking/configuration/symbol-sets/errorTrackingSymbolSetLogic.tsx b/frontend/src/scenes/error-tracking/configuration/symbol-sets/errorTrackingSymbolSetLogic.tsx
index 1fd0b24909ab..9e7aceaf3ae5 100644
--- a/frontend/src/scenes/error-tracking/configuration/symbol-sets/errorTrackingSymbolSetLogic.tsx
+++ b/frontend/src/scenes/error-tracking/configuration/symbol-sets/errorTrackingSymbolSetLogic.tsx
@@ -1,9 +1,9 @@
 import { lemonToast } from '@posthog/lemon-ui'
-import { actions, kea, path, reducers, selectors } from 'kea'
+import { actions, defaults, kea, listeners, path, reducers, selectors } from 'kea'
 import { forms } from 'kea-forms'
 import { loaders } from 'kea-loaders'
-import api from 'lib/api'
-import { ErrorTrackingSymbolSet } from 'lib/components/Errors/types'
+import api, { CountedPaginatedResponse } from 'lib/api'
+import { ErrorTrackingSymbolSet, SymbolSetStatusFilter } from 'lib/components/Errors/types'
 import { Scene } from 'scenes/sceneTypes'
 import { urls } from 'scenes/urls'
 
@@ -11,70 +11,61 @@ import { Breadcrumb } from '~/types'
 
 import type { errorTrackingSymbolSetLogicType } from './errorTrackingSymbolSetLogicType'
 
-export enum ErrorGroupTab {
-    Overview = 'overview',
-    Breakdowns = 'breakdowns',
-}
-
 export type SymbolSetUpload = SourceMapUpload
+export const RESULTS_PER_PAGE = 20
 
-export interface SourceMapUpload {
+export type SourceMapUpload = {
     minified: File
     sourceMap: File
 }
 
+export type ErrorTrackingSymbolSetResponse = CountedPaginatedResponse<ErrorTrackingSymbolSet>
+
 export const errorTrackingSymbolSetLogic = kea<errorTrackingSymbolSetLogicType>([
     path(['scenes', 'error-tracking', 'errorTrackingSymbolSetLogic']),
 
     actions({
-        setUploadSymbolSetId: (id: ErrorTrackingSymbolSet['id'] | null) => ({ id }),
+        loadSymbolSets: () => {},
+        deleteSymbolSet: (id: string) => ({ id }),
+        setUploadSymbolSetId: (id: string | null) => ({ id }),
+        setSymbolSetStatusFilter: (status: SymbolSetStatusFilter) => ({ status }),
+        setPage: (page: number) => ({ page }),
+    }),
+
+    defaults({
+        page: 1 as number,
+        symbolSetResponse: null as ErrorTrackingSymbolSetResponse | null,
+        symbolSetStatusFilter: 'all' as SymbolSetStatusFilter,
+        uploadSymbolSetId: null as string | null,
     }),
 
     reducers({
-        uploadSymbolSetId: [
-            null as string | null,
-            {
-                setUploadSymbolSetId: (_, { id }) => id,
-            },
-        ],
+        uploadSymbolSetId: {
+            setUploadSymbolSetId: (_, { id }) => id,
+        },
+        symbolSetStatusFilter: {
+            setSymbolSetStatusFilter: (_, { status }) => status,
+        },
+        page: {
+            setPage: (_, { page }) => page,
+            setSymbolSetStatusFilter: () => 1,
+        },
     }),
 
     loaders(({ values }) => ({
-        symbolSets: [
-            [] as ErrorTrackingSymbolSet[],
-            {
-                loadSymbolSets: async () => {
-                    const response = await api.errorTracking.symbolSets()
-                    return response.results
-                },
-                deleteSymbolSet: async (id) => {
-                    await api.errorTracking.deleteSymbolSet(id)
-                    const newValues = [...values.symbolSets]
-                    return newValues.filter((v) => v.id !== id)
-                },
+        symbolSetResponse: {
+            loadSymbolSets: async (_, breakpoint) => {
+                await breakpoint(100)
+                const res = await api.errorTracking.symbolSets.list({
+                    status: values.symbolSetStatusFilter,
+                    limit: RESULTS_PER_PAGE,
+                    offset: (values.page - 1) * RESULTS_PER_PAGE,
+                })
+                return res
             },
-        ],
+        },
     })),
 
-    selectors({
-        breadcrumbs: [
-            () => [],
-            (): Breadcrumb[] => [
-                {
-                    key: Scene.ErrorTracking,
-                    name: 'Error tracking',
-                    path: urls.errorTracking(),
-                },
-                {
-                    key: Scene.ErrorTrackingConfiguration,
-                    name: 'Configuration',
-                },
-            ],
-        ],
-        validSymbolSets: [(s) => [s.symbolSets], (symbolSets) => symbolSets.filter((s) => !!s.storage_ptr)],
-        missingSymbolSets: [(s) => [s.symbolSets], (symbolSets) => symbolSets.filter((s) => !s.storage_ptr)],
-    }),
-
     forms(({ values, actions }) => ({
         uploadSymbolSet: {
             defaults: { minified: [], sourceMap: [] } as { minified: File[]; sourceMap: File[] },
@@ -95,7 +86,7 @@ export const errorTrackingSymbolSetLogic = kea<errorTrackingSymbolSetLogicType>(
                 const formData = new FormData()
                 formData.append('minified', minifiedSrc)
                 formData.append('source_map', sourceMapSrc)
-                await api.errorTracking.updateSymbolSet(id, formData)
+                await api.errorTracking.symbolSets.update(id, formData)
                 actions.setUploadSymbolSetId(null)
                 actions.loadSymbolSets()
                 actions.resetUploadSymbolSet()
@@ -103,4 +94,50 @@ export const errorTrackingSymbolSetLogic = kea<errorTrackingSymbolSetLogicType>(
             },
         },
     })),
+
+    listeners(({ actions }) => ({
+        deleteSymbolSet: async ({ id }: { id: ErrorTrackingSymbolSet['id'] }) => {
+            await api.errorTracking.symbolSets.delete(id)
+            lemonToast.success('Symbol set deleted')
+            actions.loadSymbolSets()
+        },
+        setSymbolSetStatusFilter: () => actions.loadSymbolSets(),
+        setPage: () => actions.loadSymbolSets(),
+    })),
+
+    selectors(({ actions }) => ({
+        breadcrumbs: [
+            () => [],
+            (): Breadcrumb[] => [
+                {
+                    key: Scene.ErrorTracking,
+                    name: 'Error tracking',
+                    path: urls.errorTracking(),
+                },
+                {
+                    key: Scene.ErrorTrackingConfiguration,
+                    name: 'Configuration',
+                },
+            ],
+        ],
+        symbolSets: [
+            (s) => [s.symbolSetResponse],
+            (response: ErrorTrackingSymbolSetResponse): ErrorTrackingSymbolSet[] => {
+                return response?.results || []
+            },
+        ],
+        pagination: [
+            (s) => [s.page, s.symbolSetResponse],
+            (page: number, symbolSetResponse: ErrorTrackingSymbolSetResponse) => {
+                return {
+                    controlled: true,
+                    pageSize: RESULTS_PER_PAGE,
+                    currentPage: page,
+                    entryCount: symbolSetResponse?.count ?? 0,
+                    onBackward: () => actions.setPage(page - 1),
+                    onForward: () => actions.setPage(page + 1),
+                }
+            },
+        ],
+    })),
 ])
diff --git a/posthog/api/error_tracking.py b/posthog/api/error_tracking.py
index da3c4bd48b80..fef91ac0b5f5 100644
--- a/posthog/api/error_tracking.py
+++ b/posthog/api/error_tracking.py
@@ -317,7 +317,22 @@ class ErrorTrackingSymbolSetViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSe
     parser_classes = [MultiPartParser, FileUploadParser]
 
     def safely_get_queryset(self, queryset):
-        return queryset.filter(team_id=self.team.id)
+        queryset = queryset.filter(team_id=self.team.id)
+        params = self.request.GET.dict()
+        status = params.get("status")
+        order_by = params.get("order_by")
+
+        if status == "valid":
+            queryset = queryset.filter(storage_ptr__isnull=False)
+        elif status == "invalid":
+            queryset = queryset.filter(storage_ptr__isnull=True)
+
+        if order_by:
+            allowed_fields = ["created_at", "-created_at", "ref", "-ref"]
+            if order_by in allowed_fields:
+                queryset = queryset.order_by(order_by)
+
+        return queryset
 
     def destroy(self, request, *args, **kwargs):
         symbol_set = self.get_object()


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

This PR adds pagination and status filtering to the symbol set table, replacing the previous 100-result limit with a more scalable solution that includes ordering by creation date.

- Added `SymbolSetStatus` ('valid'|'invalid') and `SymbolSetStatusFilter` types in `frontend/src/lib/components/Errors/types.ts` to support filtering
- Potential security risk in `posthog/api/error_tracking.py` with unvalidated `order_by` parameter passed directly to queryset
- Missing default ordering specification in `safely_get_queryset` when `order_by` is not provided
- Status filter implementation in `ErrorTrackingSymbolSetViewSet` could be optimized by combining conditions
- Params extraction using `dict()` in error tracking API could be made more robust

<sub>4 file(s) reviewed, 3 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_31374)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T16:57:44Z
user: login: oliverb123
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T08:05:45Z

Review comments:
diff_hunk: @@ -1,80 +1,71 @@
 import { lemonToast } from '@posthog/lemon-ui'
-import { actions, kea, path, reducers, selectors } from 'kea'
+import { actions, defaults, kea, listeners, path, reducers, selectors } from 'kea'
 import { forms } from 'kea-forms'
 import { loaders } from 'kea-loaders'
-import api from 'lib/api'
-import { ErrorTrackingSymbolSet } from 'lib/components/Errors/types'
+import api, { CountedPaginatedResponse } from 'lib/api'
+import { ErrorTrackingSymbolSet, SymbolSetStatusFilter } from 'lib/components/Errors/types'
 import { Scene } from 'scenes/sceneTypes'
 import { urls } from 'scenes/urls'
 
 import { Breadcrumb } from '~/types'
 
 import type { errorTrackingSymbolSetLogicType } from './errorTrackingSymbolSetLogicType'
 
-export enum ErrorGroupTab {
-    Overview = 'overview',
-    Breakdowns = 'breakdowns',
-}
-
 export type SymbolSetUpload = SourceMapUpload
+export const RESULTS_PER_PAGE = 20
 
-export interface SourceMapUpload {
+export type SourceMapUpload = {
     minified: File
     sourceMap: File
 }
 
+export type ErrorTrackingSymbolSetResponse = CountedPaginatedResponse<ErrorTrackingSymbolSet>
+
 export const errorTrackingSymbolSetLogic = kea<errorTrackingSymbolSetLogicType>([
     path(['scenes', 'error-tracking', 'errorTrackingSymbolSetLogic']),
 
     actions({
-        setUploadSymbolSetId: (id: ErrorTrackingSymbolSet['id'] | null) => ({ id }),
+        loadSymbolSets: () => {},
+        deleteSymbolSet: (id: string) => ({ id }),
+        setUploadSymbolSetId: (id: string | null) => ({ id }),
+        setSymbolSetStatusFilter: (status: SymbolSetStatusFilter) => ({ status }),
+        setPage: (page: number) => ({ page }),
+    }),
+
+    defaults({
+        page: 1 as number,
+        symbolSetResponse: null as ErrorTrackingSymbolSetResponse | null,
+        symbolSetStatusFilter: 'all' as SymbolSetStatusFilter,
+        uploadSymbolSetId: null as string | null,
     }),
 
     reducers({
-        uploadSymbolSetId: [
-            null as string | null,
-            {
-                setUploadSymbolSetId: (_, { id }) => id,
-            },
-        ],
+        uploadSymbolSetId: {
+            setUploadSymbolSetId: (_, { id }) => id,
+        },
+        symbolSetStatusFilter: {
+            setSymbolSetStatusFilter: (_, { status }) => status,
+        },
+        page: {
+            setPage: (_, { page }) => page,
+            setSymbolSetStatusFilter: () => 1,
+        },
path: frontend/src/scenes/error-tracking/configuration/symbol-sets/errorTrackingSymbolSetLogic.tsx
user: login: greptile-apps[bot]
body: logic: resetting page to 1 on filter change but not handling edge case where current page is higher than total pages after filter
created_at: 2025-04-18T16:56:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,12 +86,58 @@ export const errorTrackingSymbolSetLogic = kea<errorTrackingSymbolSetLogicType>(
                 const formData = new FormData()
                 formData.append('minified', minifiedSrc)
                 formData.append('source_map', sourceMapSrc)
-                await api.errorTracking.updateSymbolSet(id, formData)
+                await api.errorTracking.symbolSets.update(id, formData)
                 actions.setUploadSymbolSetId(null)
                 actions.loadSymbolSets()
                 actions.resetUploadSymbolSet()
                 lemonToast.success('Source map uploaded')
             },
         },
     })),
+
+    listeners(({ actions }) => ({
+        deleteSymbolSet: async ({ id }: { id: ErrorTrackingSymbolSet['id'] }) => {
+            await api.errorTracking.symbolSets.delete(id)
+            lemonToast.success('Symbol set deleted')
+            actions.loadSymbolSets()
+        },
+        setSymbolSetStatusFilter: () => actions.loadSymbolSets(),
+        setPage: () => actions.loadSymbolSets(),
+    })),
+
+    selectors(({ actions }) => ({
+        breadcrumbs: [
+            () => [],
+            (): Breadcrumb[] => [
+                {
+                    key: Scene.ErrorTracking,
+                    name: 'Error tracking',
+                    path: urls.errorTracking(),
+                },
+                {
+                    key: Scene.ErrorTrackingConfiguration,
+                    name: 'Configuration',
+                },
+            ],
+        ],
+        symbolSets: [
+            (s) => [s.symbolSetResponse],
+            (response: ErrorTrackingSymbolSetResponse): ErrorTrackingSymbolSet[] => {
+                return response?.results || []
+            },
+        ],
+        pagination: [
+            (s) => [s.page, s.symbolSetResponse],
+            (page: number, symbolSetResponse: ErrorTrackingSymbolSetResponse) => {
+                return {
+                    controlled: true,
+                    pageSize: RESULTS_PER_PAGE,
+                    currentPage: page,
+                    entryCount: symbolSetResponse?.count ?? 0,
+                    onBackward: () => actions.setPage(page - 1),
+                    onForward: () => actions.setPage(page + 1),
+                }
path: frontend/src/scenes/error-tracking/configuration/symbol-sets/errorTrackingSymbolSetLogic.tsx
user: login: greptile-apps[bot]
body: logic: pagination controls allow navigation to invalid pages (negative or beyond max)

```suggestion
                    entryCount: symbolSetResponse?.count ?? 0,
                    onBackward: page > 1 ? () => actions.setPage(page - 1) : undefined,
                    onForward: page * RESULTS_PER_PAGE < (symbolSetResponse?.count ?? 0) ? () => actions.setPage(page + 1) : undefined,
                }
```
created_at: 2025-04-18T16:56:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -317,7 +317,20 @@ class ErrorTrackingSymbolSetViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSe
     parser_classes = [MultiPartParser, FileUploadParser]
 
     def safely_get_queryset(self, queryset):
-        return queryset.filter(team_id=self.team.id)
+        queryset = queryset.filter(team_id=self.team.id)
+        params = self.request.GET.dict()
+        status = params.get("status")
+        order_by = params.get("order_by")
+
+        if status == "valid":
+            queryset = queryset.filter(storage_ptr__isnull=False)
+        elif status == "invalid":
+            queryset = queryset.filter(storage_ptr__isnull=True)
+
+        if order_by:
+            queryset = queryset.order_by(order_by)
path: posthog/api/error_tracking.py
user: login: greptile-apps[bot]
body: logic: Direct use of order_by parameter without validation could allow SQL injection. Validate against allowed fields.

```suggestion
        if order_by:
            allowed_fields = ['created_at', '-created_at', 'ref', '-ref']
            if order_by in allowed_fields:
                queryset = queryset.order_by(order_by)
```
created_at: 2025-04-18T16:57:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: posthog-bot
created_at: 2025-04-18T17:07:33Z
body: ## 📸 UI snapshots have been updated

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

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

Triggered by [this commit](https://github.com/PostHog/posthog/pull/31374/commits/42f56f0d3bbd306ee8fbfce07a035ba2eb9bf6ec).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/31374/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-18T17:18:27Z
body: **Size Change:** +162 B (0%) 

**Total Size:** 13.7 MB



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

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

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR