Skip to content

Commit 8a14967

Browse files
authored
fix(audit-logs): list an organization's audit log through its single scope predicate again (#9438)
#9419 read a listing's page ids as one keyset scan per organization workspace plus the organization-level rows. That bounds an unfiltered listing, but a filter the workspace keyset index cannot apply in order (an action, a resource, an actor) turns each workspace's scan into a bitmap AND of that workspace's whole history with the filter's index, then a sort, once per workspace. For a busy organization a filtered listing went from one scan of the filter's index to many times that, and slow replica reads are what recovery conflicts cancel. Restore the listing to the single organization-scope predicate it used before, which lets the planner pick the filter's index, keep the org-level index from 0444 (the predicate's organization-level rows can use it), and keep the scope integration test against the restored signature. The per-workspace plan test described the reverted shape and goes.
1 parent 59bbfd5 commit 8a14967

7 files changed

Lines changed: 72 additions & 258 deletions

File tree

‎apps/sim/app/api/v1/audit-logs/route.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ import { generateId } from '@sim/utils/id'
2525
import { type NextRequest, NextResponse } from 'next/server'
2626
import { v1ListAuditLogsContract } from '@/lib/api/contracts/v1/audit-logs'
2727
import { parseRequest } from '@/lib/api/server'
28-
import { buildFilterConditions, getOrgWorkspaceIds, queryAuditLogs } from '@/lib/audit-logs/query'
28+
import {
29+
buildFilterConditions,
30+
buildOrgScopeCondition,
31+
getOrgWorkspaceIds,
32+
queryAuditLogs,
33+
} from '@/lib/audit-logs/query'
2934
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
3035
import { validateV1EnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth'
3136
import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format'
@@ -94,12 +99,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
9499
)
95100
}
96101

97-
const scope = {
102+
const scopeCondition = buildOrgScopeCondition({
98103
organizationId,
99104
orgWorkspaceIds,
100105
orgMemberIds,
101106
includeDeparted: params.includeDeparted,
102-
}
107+
})
103108
const filterConditions = buildFilterConditions({
104109
action: params.action,
105110
resourceType: params.resourceType,
@@ -111,8 +116,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
111116
})
112117

113118
const { data, nextCursor } = await queryAuditLogs(
114-
scope,
115-
filterConditions,
119+
[scopeCondition, ...filterConditions],
116120
params.limit,
117121
params.cursor
118122
)

‎apps/sim/lib/audit-logs/application/export-audit-logs.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { AUDIT_LOG_CSV_HEADER, toAuditLogCsvRow } from '@/lib/audit-logs/csv'
55
import {
66
type AuditLogFilterParams,
77
buildFilterConditions,
8+
buildOrgScopeCondition,
89
getOrgWorkspaceIds,
910
queryAuditLogs,
1011
} from '@/lib/audit-logs/query'
@@ -36,13 +37,13 @@ export const exportAuditLogs = defineAuthorizedAuditLogUseCase({
3637
) {
3738
throw new OrchestrationError('validation', 'actorId is not a member of your organization')
3839
}
39-
const scope = {
40+
const scope = buildOrgScopeCondition({
4041
organizationId: context.organizationId,
4142
orgWorkspaceIds: workspaceIds,
4243
orgMemberIds: context.orgMemberIds,
4344
includeDeparted: input.includeDeparted,
44-
}
45-
const conditions = buildFilterConditions(input.filters)
45+
})
46+
const conditions = [scope, ...buildFilterConditions(input.filters)]
4647
const lines = [AUDIT_LOG_CSV_HEADER]
4748
let rowCount = 0
4849
let cursor: string | undefined
@@ -51,7 +52,6 @@ export const exportAuditLogs = defineAuthorizedAuditLogUseCase({
5152
pages: while (rowCount < EXPORT_MAX_ROWS && bytes < EXPORT_MAX_BYTES) {
5253
request?.signal?.throwIfAborted()
5354
const page = await queryAuditLogs(
54-
scope,
5555
conditions,
5656
Math.min(EXPORT_PAGE_ROWS, EXPORT_MAX_ROWS - rowCount),
5757
cursor,

‎apps/sim/lib/audit-logs/application/list-audit-logs.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { auditLogOperations } from '@/lib/audit-logs/application/operations'
33
import {
44
type AuditLogFilterParams,
55
buildFilterConditions,
6+
buildOrgScopeCondition,
67
decodeAuditLogCursor,
78
getOrgWorkspaceIds,
89
queryAuditLogs,
@@ -31,12 +32,12 @@ export const listAuditLogs = defineAuthorizedAuditLogUseCase({
3132
if (input.filters.workspaceId && !orgWorkspaceIds.includes(input.filters.workspaceId)) {
3233
throw new OrchestrationError('validation', 'workspaceId does not belong to your organization')
3334
}
34-
const scope = {
35+
const scopeCondition = buildOrgScopeCondition({
3536
organizationId: context.organizationId,
3637
orgWorkspaceIds,
3738
orgMemberIds: context.orgMemberIds,
3839
includeDeparted: input.includeDeparted,
39-
}
40+
})
4041
if (
4142
context.workspaceId &&
4243
input.filters.workspaceId &&
@@ -47,6 +48,6 @@ export const listAuditLogs = defineAuthorizedAuditLogUseCase({
4748
...input.filters,
4849
...(context.workspaceId ? { workspaceId: context.workspaceId } : {}),
4950
})
50-
return queryAuditLogs(scope, filterConditions, input.limit, input.cursor)
51+
return queryAuditLogs([scopeCondition, ...filterConditions], input.limit, input.cursor)
5152
},
5253
})

‎apps/sim/lib/audit-logs/list-plans.integration.ts‎

Lines changed: 0 additions & 120 deletions
This file was deleted.

‎apps/sim/lib/audit-logs/list-scope.integration.ts‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/
44
import { generateId } from '@sim/utils/id'
55
import { and, desc, eq, inArray, type SQL, sql } from 'drizzle-orm'
66
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
7-
import {
8-
type AuditLogListScope,
9-
buildOrgScopeCondition,
10-
queryAuditLogs,
11-
} from '@/lib/audit-logs/query'
7+
import { buildOrgScopeCondition, type OrgScopeParams, queryAuditLogs } from '@/lib/audit-logs/query'
128

139
const organizationId = generateId()
1410
const otherOrganizationId = generateId()
@@ -68,7 +64,7 @@ const fixtureIds = rows.map((row) => row.id)
6864
const auditLogTimestamp = sql`date_trunc('milliseconds', ${auditLog.createdAt})`
6965

7066
/** The listing's contract as one predicate: every row in scope, newest first. */
71-
async function referenceOrder(scope: AuditLogListScope, filters: SQL<unknown>[]) {
67+
async function referenceOrder(scope: OrgScopeParams, filters: SQL<unknown>[]) {
7268
const matching = await db
7369
.select({ id: auditLog.id })
7470
.from(auditLog)
@@ -78,15 +74,20 @@ async function referenceOrder(scope: AuditLogListScope, filters: SQL<unknown>[])
7874
}
7975

8076
async function paginate(
81-
scope: AuditLogListScope,
77+
scope: OrgScopeParams,
8278
filters: SQL<unknown>[],
8379
limit: number,
8480
maxBytes?: number
8581
) {
8682
const seen: string[] = []
8783
let cursor: string | undefined
8884
for (let page = 0; page <= rows.length; page++) {
89-
const result = await queryAuditLogs(scope, filters, limit, cursor, maxBytes)
85+
const result = await queryAuditLogs(
86+
[buildOrgScopeCondition(scope), ...filters],
87+
limit,
88+
cursor,
89+
maxBytes
90+
)
9091
expect(result.truncated ?? false).toBe(false)
9192
seen.push(...result.data.map((row) => row.id))
9293
if (!result.nextCursor) return seen
@@ -146,7 +147,7 @@ describe('queryAuditLogs scope against real Postgres', () => {
146147
])(
147148
'pages through exactly the single-predicate result (%o)',
148149
async ({ includeDeparted, action }) => {
149-
const scope: AuditLogListScope = {
150+
const scope: OrgScopeParams = {
150151
organizationId,
151152
orgWorkspaceIds: [workspaceA, workspaceB, workspaceA],
152153
orgMemberIds: [memberId],
@@ -164,7 +165,7 @@ describe('queryAuditLogs scope against real Postgres', () => {
164165
)
165166

166167
it('reads only organization-level rows when the organization has no workspaces', async () => {
167-
const scope: AuditLogListScope = {
168+
const scope: OrgScopeParams = {
168169
organizationId,
169170
orgWorkspaceIds: [],
170171
orgMemberIds: [],
@@ -185,7 +186,7 @@ describe('queryAuditLogs scope against real Postgres', () => {
185186
})
186187

187188
it('narrows to one workspace when the filter names it', async () => {
188-
const scope: AuditLogListScope = {
189+
const scope: OrgScopeParams = {
189190
organizationId,
190191
orgWorkspaceIds: [workspaceA, workspaceB],
191192
orgMemberIds: [memberId],

‎apps/sim/lib/audit-logs/query.integration.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,12 @@ describe('Audit pagination and drain tenant coverage in PostgreSQL', () => {
5353
{ limit: 1, maxBytes: undefined, boundary: 'row limit' },
5454
{ limit: 10, maxBytes: 1000, boundary: 'byte limit' },
5555
])(
56-
'returns every organization event once across a $boundary within one PostgreSQL millisecond',
56+
'returns every event once across a $boundary within one PostgreSQL millisecond',
5757
async ({ limit, maxBytes }) => {
5858
const seen: string[] = []
5959
let cursor: string | undefined
6060
for (let pageIndex = 0; pageIndex < ids.length + 1; pageIndex++) {
6161
const page = await runtime.query.queryAuditLogs(
62-
{ organizationId, orgWorkspaceIds: [], orgMemberIds: [], includeDeparted: true },
6362
[runtime.inArray(runtime.auditLog.id, ids)],
6463
limit,
6564
cursor,
@@ -73,7 +72,8 @@ describe('Audit pagination and drain tenant coverage in PostgreSQL', () => {
7372
if (!page.nextCursor) break
7473
cursor = page.nextCursor
7574
}
76-
expect(seen).toEqual(ids.slice(0, 3).reverse())
75+
expect(seen).toHaveLength(ids.length)
76+
expect(new Set(seen)).toEqual(new Set(ids))
7777
}
7878
)
7979

0 commit comments

Comments
 (0)