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:
committed by
GitHub
parent
86fbf69e95
commit
cfad24da48
File diff suppressed because one or more lines are too long
@@ -10,7 +10,6 @@ export type PartialWorkspaceMember = Omit<
|
||||
| 'calendarStartDay'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'userId'
|
||||
>;
|
||||
|
||||
export type RoleWithPartialMembers = Omit<Role, 'workspaceMembers'> & {
|
||||
|
||||
+1
@@ -9,5 +9,6 @@ export const PARTIAL_WORKSPACE_MEMBER_QUERY_FRAGMENT = gql`
|
||||
}
|
||||
avatarUrl
|
||||
userEmail
|
||||
userWorkspaceId
|
||||
}
|
||||
`;
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ export const WORKSPACE_MEMBER_QUERY_FRAGMENT = gql`
|
||||
avatarUrl
|
||||
locale
|
||||
userEmail
|
||||
userWorkspaceId
|
||||
timeZone
|
||||
dateFormat
|
||||
timeFormat
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { IconUser, IconBox, useIcons } from 'twenty-ui/display';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconBox, IconUser, useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
|
||||
import { EventLogTable } from '~/generated-metadata/graphql';
|
||||
|
||||
+5
-5
@@ -42,8 +42,8 @@ const DEFAULT_COLUMNS: ColumnConfig[] = [
|
||||
{ id: 'event', label: msg`Event`, minWidth: 100, defaultWidth: 200 },
|
||||
{ id: 'timestamp', label: msg`Timestamp`, minWidth: 100, defaultWidth: 150 },
|
||||
{
|
||||
id: 'userWorkspaceId',
|
||||
label: msg`User Workspace`,
|
||||
id: 'userId',
|
||||
label: msg`User`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 150,
|
||||
},
|
||||
@@ -59,8 +59,8 @@ const OBJECT_EVENT_COLUMNS: ColumnConfig[] = [
|
||||
{ id: 'event', label: msg`Event`, minWidth: 100, defaultWidth: 180 },
|
||||
{ id: 'timestamp', label: msg`Timestamp`, minWidth: 100, defaultWidth: 130 },
|
||||
{
|
||||
id: 'userWorkspaceId',
|
||||
label: msg`User Workspace`,
|
||||
id: 'userId',
|
||||
label: msg`User`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
},
|
||||
@@ -318,7 +318,7 @@ export const EventLogResultsTable = ({
|
||||
<StyledTableCell>
|
||||
{beautifyPastDateRelativeToNow(record.timestamp)}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>{record.userWorkspaceId ?? '-'}</StyledTableCell>
|
||||
<StyledTableCell>{record.userId ?? '-'}</StyledTableCell>
|
||||
{showObjectEventColumns && (
|
||||
<>
|
||||
<StyledTableCell>{record.recordId ?? '-'}</StyledTableCell>
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ export const GET_EVENT_LOGS = gql`
|
||||
records {
|
||||
event
|
||||
timestamp
|
||||
userWorkspaceId
|
||||
userId
|
||||
properties
|
||||
recordId
|
||||
objectMetadataId
|
||||
|
||||
+2
-1
@@ -7,4 +7,5 @@ CREATE TABLE IF NOT EXISTS workspaceEvent
|
||||
`properties` JSON
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY (workspaceId, event, userId, timestamp);
|
||||
ORDER BY (workspaceId, timestamp, event, userId)
|
||||
TTL timestamp + INTERVAL 3 YEAR DELETE;
|
||||
|
||||
+4
-3
@@ -1,10 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS pageview
|
||||
(
|
||||
`name` LowCardinality(String) NOT NULL,
|
||||
`timestamp` DateTime64(3) NOT NULL,
|
||||
`name` LowCardinality(String) NOT NULL,
|
||||
`timestamp` DateTime64(3) NOT NULL,
|
||||
`userId` String DEFAULT '',
|
||||
`workspaceId` String DEFAULT '',
|
||||
`properties` JSON
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY (workspaceId, name, userId, timestamp);
|
||||
ORDER BY (workspaceId, timestamp, name, userId)
|
||||
TTL timestamp + INTERVAL 3 YEAR DELETE;
|
||||
|
||||
+10
-9
@@ -1,13 +1,14 @@
|
||||
CREATE TABLE IF NOT EXISTS objectEvent
|
||||
(
|
||||
`event` LowCardinality(String) NOT NULL,
|
||||
`timestamp` DateTime64(3) NOT NULL,
|
||||
`userId` String DEFAULT '',
|
||||
`workspaceId` String NOT NULL,
|
||||
`recordId` String NOT NULL,
|
||||
`objectMetadataId` String NOT NULL,
|
||||
`properties` JSON,
|
||||
`isCustom` Boolean DEFAULT FALSE,
|
||||
`event` LowCardinality(String) NOT NULL,
|
||||
`timestamp` DateTime64(3) NOT NULL,
|
||||
`userId` String DEFAULT '',
|
||||
`workspaceId` String NOT NULL,
|
||||
`recordId` String NOT NULL,
|
||||
`objectMetadataId` String NOT NULL,
|
||||
`properties` JSON,
|
||||
`isCustom` Boolean DEFAULT FALSE
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY (workspaceId, event, userId, timestamp);
|
||||
ORDER BY (workspaceId, timestamp, event, userId)
|
||||
TTL timestamp + INTERVAL 3 YEAR DELETE;
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
-- Optimize workspaceEvent table for time-series queries
|
||||
-- ClickHouse doesn't allow changing ORDER BY, so we create a new table and migrate data
|
||||
|
||||
-- Step 1: Create new table with optimized structure
|
||||
CREATE TABLE IF NOT EXISTS workspaceEvent_v2
|
||||
(
|
||||
`event` LowCardinality(String) NOT NULL,
|
||||
`timestamp` DateTime64(3) NOT NULL,
|
||||
`userWorkspaceId` String DEFAULT '',
|
||||
`workspaceId` String NOT NULL,
|
||||
`properties` JSON
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY (workspaceId, timestamp, event, userWorkspaceId)
|
||||
TTL timestamp + INTERVAL 3 YEAR DELETE;
|
||||
|
||||
-- Step 2: Migrate existing data (userWorkspaceId will be empty for historical data)
|
||||
INSERT INTO workspaceEvent_v2
|
||||
SELECT event, timestamp, '' as userWorkspaceId, workspaceId, properties
|
||||
FROM workspaceEvent;
|
||||
|
||||
-- Step 3: Atomic swap (EXCHANGE is atomic and instant)
|
||||
EXCHANGE TABLES workspaceEvent AND workspaceEvent_v2;
|
||||
|
||||
-- Step 4: Drop old table (now named workspaceEvent_v2 after swap)
|
||||
DROP TABLE IF EXISTS workspaceEvent_v2;
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
-- Optimize pageview table for time-series queries
|
||||
-- ClickHouse doesn't allow changing ORDER BY, so we create a new table and migrate data
|
||||
|
||||
-- Step 1: Create new table with optimized structure
|
||||
CREATE TABLE IF NOT EXISTS pageview_v2
|
||||
(
|
||||
`name` LowCardinality(String) NOT NULL,
|
||||
`timestamp` DateTime64(3) NOT NULL,
|
||||
`userWorkspaceId` String DEFAULT '',
|
||||
`workspaceId` String DEFAULT '',
|
||||
`properties` JSON
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY (workspaceId, timestamp, name, userWorkspaceId)
|
||||
TTL timestamp + INTERVAL 3 YEAR DELETE;
|
||||
|
||||
-- Step 2: Migrate existing data (userWorkspaceId will be empty for historical data)
|
||||
INSERT INTO pageview_v2
|
||||
SELECT name, timestamp, '' as userWorkspaceId, workspaceId, properties
|
||||
FROM pageview;
|
||||
|
||||
-- Step 3: Atomic swap
|
||||
EXCHANGE TABLES pageview AND pageview_v2;
|
||||
|
||||
-- Step 4: Drop old table
|
||||
DROP TABLE IF EXISTS pageview_v2;
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
-- Optimize objectEvent table for time-series queries and object filtering
|
||||
-- ClickHouse doesn't allow changing ORDER BY, so we create a new table and migrate data
|
||||
|
||||
-- Step 1: Create new table with optimized structure
|
||||
CREATE TABLE IF NOT EXISTS objectEvent_v2
|
||||
(
|
||||
`event` LowCardinality(String) NOT NULL,
|
||||
`timestamp` DateTime64(3) NOT NULL,
|
||||
`userWorkspaceId` String DEFAULT '',
|
||||
`workspaceId` String NOT NULL,
|
||||
`recordId` String NOT NULL,
|
||||
`objectMetadataId` String NOT NULL,
|
||||
`properties` JSON,
|
||||
`isCustom` Boolean DEFAULT FALSE
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY (workspaceId, timestamp, objectMetadataId, recordId, event, userWorkspaceId)
|
||||
TTL timestamp + INTERVAL 3 YEAR DELETE;
|
||||
|
||||
-- Step 2: Migrate existing data (userWorkspaceId will be empty for historical data)
|
||||
INSERT INTO objectEvent_v2
|
||||
SELECT event, timestamp, '' as userWorkspaceId, workspaceId, recordId, objectMetadataId, properties, isCustom
|
||||
FROM objectEvent;
|
||||
|
||||
-- Step 3: Atomic swap
|
||||
EXCHANGE TABLES objectEvent AND objectEvent_v2;
|
||||
|
||||
-- Step 4: Drop old table
|
||||
DROP TABLE IF EXISTS objectEvent_v2;
|
||||
@@ -5,13 +5,19 @@ import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/uti
|
||||
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated';
|
||||
import { type GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||
|
||||
export const fixtures: Array<GenericTrackEvent> = [
|
||||
export type ObjectEventFixture = GenericTrackEvent & {
|
||||
recordId: string;
|
||||
objectMetadataId: string;
|
||||
isCustom?: boolean;
|
||||
};
|
||||
|
||||
export const workspaceEventFixtures: Array<GenericTrackEvent> = [
|
||||
{
|
||||
type: 'track',
|
||||
event: CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
||||
timestamp: '2024-10-24T15:55:35.177',
|
||||
version: '1',
|
||||
userWorkspaceId: '20202020-3957-45c9-be39-337dc4d9100a',
|
||||
userId: '20202020-3957-45c9-be39-337dc4d9100a',
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
properties: {},
|
||||
},
|
||||
@@ -20,35 +26,44 @@ export const fixtures: Array<GenericTrackEvent> = [
|
||||
event: CUSTOM_DOMAIN_DEACTIVATED_EVENT,
|
||||
timestamp: '2024-10-24T15:55:35.177',
|
||||
version: '1',
|
||||
userWorkspaceId: '20202020-3957-45c9-be39-337dc4d9100a',
|
||||
userId: '20202020-3957-45c9-be39-337dc4d9100a',
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
properties: {},
|
||||
},
|
||||
];
|
||||
|
||||
export const objectEventFixtures: Array<ObjectEventFixture> = [
|
||||
{
|
||||
type: 'track',
|
||||
event: OBJECT_RECORD_CREATED_EVENT,
|
||||
timestamp: '2024-10-24T15:55:35.177',
|
||||
version: '1',
|
||||
userWorkspaceId: '20202020-3957-45c9-be39-337dc4d9100a',
|
||||
userId: '20202020-3957-45c9-be39-337dc4d9100a',
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
properties: {},
|
||||
recordId: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||
objectMetadataId: '20202020-1f76-4e46-b33b-58a70e007ba0',
|
||||
},
|
||||
{
|
||||
type: 'track',
|
||||
event: OBJECT_RECORD_UPDATED_EVENT,
|
||||
timestamp: '2024-10-24T15:55:35.177',
|
||||
version: '1',
|
||||
userWorkspaceId: '20202020-3957-45c9-be39-337dc4d9100a',
|
||||
userId: '20202020-3957-45c9-be39-337dc4d9100a',
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
properties: {},
|
||||
recordId: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||
objectMetadataId: '20202020-1f76-4e46-b33b-58a70e007ba0',
|
||||
},
|
||||
{
|
||||
type: 'track',
|
||||
event: OBJECT_RECORD_DELETED_EVENT,
|
||||
timestamp: '2024-10-24T15:55:35.177',
|
||||
version: '1',
|
||||
userWorkspaceId: '20202020-3957-45c9-be39-337dc4d9100a',
|
||||
userId: '20202020-3957-45c9-be39-337dc4d9100a',
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
properties: {},
|
||||
recordId: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||
objectMetadataId: '20202020-1f76-4e46-b33b-58a70e007ba0',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { createClient } from '@clickhouse/client';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
import { fixtures } from './fixtures';
|
||||
import { objectEventFixtures, workspaceEventFixtures } from './fixtures';
|
||||
|
||||
config({
|
||||
path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
|
||||
@@ -15,11 +15,21 @@ const client = createClient({
|
||||
|
||||
async function seedEvents() {
|
||||
try {
|
||||
console.log(`⚡ Seeding ${fixtures.length} events...`);
|
||||
console.log(
|
||||
`⚡ Seeding ${workspaceEventFixtures.length} workspace events...`,
|
||||
);
|
||||
|
||||
await client.insert({
|
||||
table: 'workspaceEvent',
|
||||
values: fixtures,
|
||||
values: workspaceEventFixtures,
|
||||
format: 'JSONEachRow',
|
||||
});
|
||||
|
||||
console.log(`⚡ Seeding ${objectEventFixtures.length} object events...`);
|
||||
|
||||
await client.insert({
|
||||
table: 'objectEvent',
|
||||
values: objectEventFixtures,
|
||||
format: 'JSONEachRow',
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ export class TelemetryListener {
|
||||
payload.events.map(async (eventPayload) => {
|
||||
this.auditService
|
||||
.createContext({
|
||||
userWorkspaceId: eventPayload.userWorkspaceId,
|
||||
userId: eventPayload.userId,
|
||||
workspaceId: payload.workspaceId,
|
||||
})
|
||||
.insertWorkspaceEvent(USER_SIGNUP_EVENT, {});
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
+1
-1
@@ -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>,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export type GenericTrackEvent<E extends string = string> = {
|
||||
properties: any;
|
||||
timestamp: string;
|
||||
version: string;
|
||||
userWorkspaceId?: string;
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { Args, Context, Mutation, Query } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import omit from 'lodash.omit';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { TwoFactorAuthenticationStrategy } from 'twenty-shared/types';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input';
|
||||
import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input';
|
||||
@@ -22,6 +22,7 @@ import { ValidatePasswordResetTokenOutput } from 'src/engine/core-modules/auth/d
|
||||
import { ValidatePasswordResetTokenInput } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.input';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { ApiKeyService } from 'src/engine/core-modules/api-key/services/api-key.service';
|
||||
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||
@@ -70,7 +71,6 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
|
||||
import { AuthProvider } from 'src/engine/decorators/auth/auth-provider.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 { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
@@ -700,7 +700,7 @@ export class AuthResolver {
|
||||
|
||||
const auditService = this.auditService.createContext({
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: impersonatorUserWorkspace.id,
|
||||
userId: impersonatorUserWorkspace.user.id,
|
||||
});
|
||||
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
|
||||
@@ -419,7 +419,7 @@ export class AuthService {
|
||||
|
||||
const analytics = this.auditService.createContext({
|
||||
workspaceId,
|
||||
userWorkspaceId: impersonatorUserWorkspaceId,
|
||||
userId: _impersonatorUserId,
|
||||
});
|
||||
|
||||
analytics.insertWorkspaceEvent('Monitoring', {
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export class EventLogRecord {
|
||||
timestamp: Date;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
userWorkspaceId?: string;
|
||||
userId?: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
properties?: Record<string, unknown>;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ClickHouseModule } from 'src/database/clickHouse/clickHouse.module';
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
import { EventLogsResolver } from './event-logs.resolver';
|
||||
@@ -16,6 +18,7 @@ import { EventLogsService } from './event-logs.service';
|
||||
PermissionsModule,
|
||||
BillingModule,
|
||||
GuardRedirectModule,
|
||||
TypeOrmModule.forFeature([UserWorkspaceEntity]),
|
||||
],
|
||||
providers: [EventLogsResolver, EventLogsService],
|
||||
exports: [EventLogsService],
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { EventLogTable } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
|
||||
import { formatDateForClickHouse } from 'src/database/clickHouse/clickHouse.util';
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
|
||||
import {
|
||||
EventLogsException,
|
||||
@@ -26,7 +29,7 @@ type ClickHouseEventRecord = {
|
||||
event?: string;
|
||||
name?: string;
|
||||
timestamp: string;
|
||||
userWorkspaceId?: string;
|
||||
userId?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
recordId?: string;
|
||||
objectMetadataId?: string;
|
||||
@@ -47,6 +50,8 @@ export class EventLogsService {
|
||||
constructor(
|
||||
private readonly clickHouseService: ClickHouseService,
|
||||
private readonly billingService: BillingService,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
) {}
|
||||
|
||||
async queryEventLogs(
|
||||
@@ -67,7 +72,7 @@ export class EventLogsService {
|
||||
const whereClauses: string[] = ['"workspaceId" = {workspaceId:String}'];
|
||||
const params: Record<string, unknown> = { workspaceId };
|
||||
|
||||
this.applyFilters(
|
||||
await this.applyFilters(
|
||||
whereClauses,
|
||||
params,
|
||||
input.filters,
|
||||
@@ -155,13 +160,13 @@ export class EventLogsService {
|
||||
}
|
||||
}
|
||||
|
||||
private applyFilters(
|
||||
private async applyFilters(
|
||||
whereClauses: string[],
|
||||
params: Record<string, unknown>,
|
||||
filters: EventLogFiltersInput | undefined,
|
||||
eventFieldName: string,
|
||||
table: EventLogTable,
|
||||
): void {
|
||||
): Promise<void> {
|
||||
if (!isDefined(filters)) {
|
||||
return;
|
||||
}
|
||||
@@ -174,8 +179,15 @@ export class EventLogsService {
|
||||
}
|
||||
|
||||
if (isDefined(filters.userWorkspaceId)) {
|
||||
whereClauses.push('"userWorkspaceId" = {userWorkspaceId:String}');
|
||||
params.userWorkspaceId = filters.userWorkspaceId;
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: { id: filters.userWorkspaceId },
|
||||
select: ['userId'],
|
||||
});
|
||||
|
||||
if (isDefined(userWorkspace)) {
|
||||
whereClauses.push('"userId" = {userId:String}');
|
||||
params.userId = userWorkspace.userId;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(filters.dateRange?.start)) {
|
||||
@@ -222,7 +234,7 @@ export class EventLogsService {
|
||||
return {
|
||||
event: eventName,
|
||||
timestamp: new Date(record.timestamp),
|
||||
userWorkspaceId: record.userWorkspaceId,
|
||||
userId: record.userId,
|
||||
properties: record.properties,
|
||||
recordId: record.recordId,
|
||||
objectMetadataId: record.objectMetadataId,
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||
|
||||
@@ -141,7 +141,7 @@ export class ImpersonationService {
|
||||
) {
|
||||
const auditService = this.auditService.createContext({
|
||||
workspaceId: impersonatorUserWorkspace.workspace.id,
|
||||
userWorkspaceId: impersonatorUserWorkspace.id,
|
||||
userId: impersonatorUserWorkspace.userId,
|
||||
});
|
||||
|
||||
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||
|
||||
+2
-2
@@ -68,9 +68,9 @@ describe('ClickHouse Event Registration (integration)', () => {
|
||||
expect(rows.length).toEqual(1);
|
||||
expect(rows[0].properties).toEqual(variables.properties);
|
||||
expect(rows[0].event).toEqual(variables.event);
|
||||
// workspaceId and userWorkspaceId are empty/undefined for unauthenticated requests
|
||||
// workspaceId and userId are empty/undefined for unauthenticated requests
|
||||
expect(rows[0].workspaceId ?? '').toEqual('');
|
||||
expect(rows[0].userWorkspaceId ?? '').toEqual('');
|
||||
expect(rows[0].userId ?? '').toEqual('');
|
||||
expect(rows[0].timestamp).toHaveLength(23);
|
||||
});
|
||||
});
|
||||
|
||||
+5
-5
@@ -36,7 +36,7 @@ describe('Event Logs (integration)', () => {
|
||||
|
||||
const pageviewRecords = Array.from({ length: 25 }, (_, i) => ({
|
||||
workspaceId: testWorkspaceId,
|
||||
userWorkspaceId: testUserWorkspaceId,
|
||||
userId: testUserWorkspaceId,
|
||||
name: i % 2 === 0 ? 'settings/profile' : 'objects/companies',
|
||||
timestamp: new Date(now.getTime() - i * 60000)
|
||||
.toISOString()
|
||||
@@ -47,7 +47,7 @@ describe('Event Logs (integration)', () => {
|
||||
|
||||
const workspaceEventRecords = Array.from({ length: 15 }, (_, i) => ({
|
||||
workspaceId: testWorkspaceId,
|
||||
userWorkspaceId: testUserWorkspaceId,
|
||||
userId: testUserWorkspaceId,
|
||||
event:
|
||||
i % 3 === 0
|
||||
? 'user.login'
|
||||
@@ -63,7 +63,7 @@ describe('Event Logs (integration)', () => {
|
||||
|
||||
const objectEventRecords = Array.from({ length: 20 }, (_, i) => ({
|
||||
workspaceId: testWorkspaceId,
|
||||
userWorkspaceId: testUserWorkspaceId,
|
||||
userId: testUserWorkspaceId,
|
||||
event: i % 2 === 0 ? 'company.created' : 'company.updated',
|
||||
timestamp: new Date(now.getTime() - i * 90000)
|
||||
.toISOString()
|
||||
@@ -127,7 +127,7 @@ describe('Event Logs (integration)', () => {
|
||||
records {
|
||||
event
|
||||
timestamp
|
||||
userWorkspaceId
|
||||
userId
|
||||
properties
|
||||
recordId
|
||||
objectMetadataId
|
||||
@@ -462,7 +462,7 @@ describe('Event Logs (integration)', () => {
|
||||
|
||||
expect(record).toHaveProperty('event');
|
||||
expect(record).toHaveProperty('timestamp');
|
||||
expect(record).toHaveProperty('userWorkspaceId');
|
||||
expect(record).toHaveProperty('userId');
|
||||
expect(record).toHaveProperty('properties');
|
||||
expect(typeof record.event).toBe('string');
|
||||
expect(typeof record.timestamp).toBe('string');
|
||||
|
||||
Reference in New Issue
Block a user