Fix empty user id clickhouse (#18238)

- Fixes:
- Make Workspace User select work; previously, it didn't work as we were
not fetching the workspace users correctly
  - Send Object Events with valid record id and object id

## Audit logs demo


https://github.com/user-attachments/assets/92437037-d253-4810-a138-7c709550755d
This commit is contained in:
Baptiste Devessier
2026-02-27 16:58:44 +01:00
committed by GitHub
parent 86fbf69e95
commit cfad24da48
29 changed files with 146 additions and 171 deletions
@@ -4,6 +4,7 @@ import {
AuditException,
AuditExceptionCode,
} from 'src/engine/core-modules/audit/audit.exception';
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuditResolver } from './audit.resolver';
@@ -55,12 +56,12 @@ describe('AuditResolver', () => {
const result = await resolver.trackAnalytics(
input,
{ id: 'workspace-1' } as WorkspaceEntity,
'user-workspace-1',
{ id: 'user-1' } as UserEntity,
);
expect(auditService.createContext).toHaveBeenCalledWith({
workspaceId: 'workspace-1',
userWorkspaceId: 'user-workspace-1',
userId: 'user-1',
});
expect(mockInsertPageviewEvent).toHaveBeenCalledWith('Test Page', {});
expect(result).toBe('Pageview created');
@@ -85,12 +86,12 @@ describe('AuditResolver', () => {
const result = await resolver.trackAnalytics(
input,
{ id: 'workspace-2' } as WorkspaceEntity,
'user-workspace-2',
{ id: 'user-2' } as UserEntity,
);
expect(auditService.createContext).toHaveBeenCalledWith({
workspaceId: 'workspace-2',
userWorkspaceId: 'user-workspace-2',
userId: 'user-2',
});
expect(mockInsertWorkspaceEvent).toHaveBeenCalledWith(
'Custom Domain Activated',
@@ -120,12 +121,12 @@ describe('AuditResolver', () => {
const result = await resolver.createObjectEvent(
input,
{ id: 'workspace-3' } as WorkspaceEntity,
'user-workspace-3',
{ id: 'user-3' } as UserEntity,
);
expect(auditService.createContext).toHaveBeenCalledWith({
workspaceId: 'workspace-3',
userWorkspaceId: 'user-workspace-3',
userId: 'user-3',
});
expect(mockInsertObjectEvent).toHaveBeenCalledWith(
@@ -1,6 +1,7 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation } from '@nestjs/graphql';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { AuditExceptionFilter } from 'src/engine/core-modules/audit/audit-exception-filter';
import {
AuditException,
@@ -9,10 +10,10 @@ import {
import { CreateObjectEventInput } from 'src/engine/core-modules/audit/dtos/create-object-event.input';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@@ -37,13 +38,9 @@ export class AuditResolver {
createAnalyticsInput: CreateAnalyticsInputV2,
@AuthWorkspace({ allowUndefined: true })
workspace: WorkspaceEntity | undefined,
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
@AuthUser({ allowUndefined: true }) user: UserEntity | undefined,
) {
return this.trackAnalytics(
createAnalyticsInput,
workspace,
userWorkspaceId,
);
return this.trackAnalytics(createAnalyticsInput, workspace, user);
}
@Mutation(() => Analytics)
@@ -52,7 +49,7 @@ export class AuditResolver {
@Args()
createObjectEventInput: CreateObjectEventInput,
@AuthWorkspace() workspace: WorkspaceEntity | undefined,
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
@AuthUser({ allowUndefined: true }) user: UserEntity | undefined,
) {
if (!workspace) {
throw new AuditException(
@@ -63,7 +60,7 @@ export class AuditResolver {
const analyticsContext = this.auditService.createContext({
workspaceId: workspace.id,
userWorkspaceId,
userId: user?.id,
});
return analyticsContext.createObjectEvent(createObjectEventInput.event, {
@@ -81,11 +78,11 @@ export class AuditResolver {
createAnalyticsInput: CreateAnalyticsInputV2,
@AuthWorkspace({ allowUndefined: true })
workspace: WorkspaceEntity | undefined,
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
@AuthUser({ allowUndefined: true }) user: UserEntity | undefined,
) {
const analyticsContext = this.auditService.createContext({
workspaceId: workspace?.id,
userWorkspaceId,
userId: user?.id,
});
if (isPageviewAnalyticsInput(createAnalyticsInput)) {
@@ -30,7 +30,7 @@ export class CreateAuditLogFromInternalEvent {
const auditService = this.auditService.createContext({
workspaceId: workspaceEventBatch.workspaceId,
userWorkspaceId: eventData.userWorkspaceId,
userId: eventData.userId,
});
// Since these are object record events, we use createObjectEvent
@@ -25,14 +25,12 @@ export class AuditService {
createContext(context?: {
workspaceId?: string | null | undefined;
userWorkspaceId?: string | null | undefined;
userId?: string | null | undefined;
}) {
const contextFields = context
? {
...(context.workspaceId ? { workspaceId: context.workspaceId } : {}),
...(context.userWorkspaceId
? { userWorkspaceId: context.userWorkspaceId }
: {}),
...(context.userId ? { userId: context.userId } : {}),
}
: {};
@@ -51,13 +49,27 @@ export class AuditService {
properties: TrackEventProperties<T> & {
recordId: string;
objectMetadataId: string;
isCustom?: boolean;
},
) =>
this.preventIfDisabled(() =>
) => {
const { recordId, objectMetadataId, isCustom, ...restProperties } =
properties;
return this.preventIfDisabled(() =>
this.clickHouseService.insert('objectEvent', [
{ ...contextFields, ...makeTrackEvent(event, properties) },
{
...contextFields,
...makeTrackEvent(
event,
restProperties as unknown as TrackEventProperties<T>,
),
recordId,
objectMetadataId,
isCustom,
},
]),
),
);
},
createPageviewEvent: (
name: string,
properties: Partial<PageviewProperties>,
@@ -15,7 +15,7 @@ export type GenericTrackEvent<E extends string = string> = {
properties: any;
timestamp: string;
version: string;
userWorkspaceId?: string;
userId?: string;
workspaceId?: string;
};