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
+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, {
|
||||
|
||||
Reference in New Issue
Block a user