Update what is being audit logged (#11833)

No need to audit log workflow runs as it's already a form of audit log.
Add more audit log for other objects
Rename MessagingTelemetry to MessagingMonitoring
Merge Analytics and Audit in one (Audit)

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2025-05-04 14:35:41 +02:00
committed by GitHub
parent b1994f3707
commit 49b7f5255f
101 changed files with 948 additions and 1032 deletions

View File

@ -625,11 +625,8 @@ export type FeatureFlagDto = {
export enum FeatureFlagKey {
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
IsCopilotEnabled = 'IsCopilotEnabled',
IsCustomDomainEnabled = 'IsCustomDomainEnabled',
IsEventObjectEnabled = 'IsEventObjectEnabled',
IsJsonFilterEnabled = 'IsJsonFilterEnabled',
IsNewRelationEnabled = 'IsNewRelationEnabled',
IsPermissionsV2Enabled = 'IsPermissionsV2Enabled',

View File

@ -1,5 +1,5 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -575,11 +575,8 @@ export type FeatureFlagDto = {
export enum FeatureFlagKey {
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
IsCopilotEnabled = 'IsCopilotEnabled',
IsCustomDomainEnabled = 'IsCustomDomainEnabled',
IsEventObjectEnabled = 'IsEventObjectEnabled',
IsJsonFilterEnabled = 'IsJsonFilterEnabled',
IsNewRelationEnabled = 'IsNewRelationEnabled',
IsPermissionsV2Enabled = 'IsPermissionsV2Enabled',

View File

@ -9,13 +9,11 @@ import { SettingsApprovedAccessDomainsListCard } from '@/settings/security/compo
import { ToggleImpersonate } from '@/settings/workspace/components/ToggleImpersonate';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { Tag } from 'twenty-ui/components';
import { H2Title, IconLock } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { Tag } from 'twenty-ui/components';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledContainer = styled.div`
width: 100%;
@ -36,9 +34,6 @@ export const SettingsSecurity = () => {
const { t } = useLingui();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const IsApprovedAccessDomainsEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsApprovedAccessDomainsEnabled,
);
return (
<SubMenuTopBarContainer
@ -68,15 +63,13 @@ export const SettingsSecurity = () => {
/>
<SettingsSSOIdentitiesProvidersListCard />
</StyledSection>
{IsApprovedAccessDomainsEnabled && (
<StyledSection>
<H2Title
title={t`Approved Domains`}
description={t`Anyone with an email address at these domains is allowed to sign up for this workspace.`}
/>
<SettingsApprovedAccessDomainsListCard />
</StyledSection>
)}
<StyledSection>
<H2Title
title={t`Approved Domains`}
description={t`Anyone with an email address at these domains is allowed to sign up for this workspace.`}
/>
<SettingsApprovedAccessDomainsListCard />
</StyledSection>
<Section>
<StyledContainer>
<H2Title

View File

@ -208,14 +208,14 @@
"executor": "nx:run-commands",
"options": {
"cwd": "packages/twenty-server",
"command": "nx ts-node-no-deps -- src/database/clickhouse/migrations/run-migrations.ts"
"command": "nx ts-node-no-deps -- src/database/clickHouse/migrations/run-migrations.ts"
}
},
"clickhouse:seed": {
"executor": "nx:run-commands",
"options": {
"cwd": "packages/twenty-server",
"command": "nx ts-node-no-deps -- src/database/clickhouse/seeds/run-seeds.ts"
"command": "nx ts-node-no-deps -- src/database/clickHouse/seeds/run-seeds.ts"
}
},
"lingui:extract": {

View File

@ -28,6 +28,7 @@ import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { ModulesModule } from 'src/modules/modules.module';
import { ClickHouseModule } from './database/clickHouse/clickHouse.module';
import { CoreEngineModule } from './engine/core-modules/core-engine.module';
import { I18nModule } from './engine/core-modules/i18n/i18n.module';
@ -52,6 +53,7 @@ const MIGRATED_REST_METHODS = [
useClass: GraphQLConfigService,
}),
TwentyORMModule,
ClickHouseModule,
// Core engine module, contains all the core modules
CoreEngineModule,
// Modules module, contains all business logic modules

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { ClickHouseService } from './clickHouse.service';
@Module({
imports: [TwentyConfigModule],
providers: [ClickHouseService],
exports: [ClickHouseService],
})
export class ClickHouseModule {}

View File

@ -0,0 +1,264 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ClickHouseClient } from '@clickhouse/client';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ClickHouseService } from './clickHouse.service';
// Mock the createClient function from @clickhouse/client
jest.mock('@clickhouse/client', () => ({
createClient: jest.fn().mockReturnValue({
insert: jest.fn().mockResolvedValue({}),
query: jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue([{ test: 'data' }]),
}),
ping: jest.fn().mockResolvedValue({ success: true }),
close: jest.fn().mockResolvedValue({}),
exec: jest.fn().mockResolvedValue({}),
}),
}));
describe('ClickHouseService', () => {
let service: ClickHouseService;
let twentyConfigService: TwentyConfigService;
let mockClickHouseClient: jest.Mocked<ClickHouseClient>;
beforeEach(async () => {
jest.clearAllMocks();
mockClickHouseClient = {
insert: jest.fn().mockResolvedValue({}),
query: jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue([{ test: 'data' }]),
}),
ping: jest.fn().mockResolvedValue({ success: true }),
close: jest.fn().mockResolvedValue({}),
exec: jest.fn().mockResolvedValue({}),
} as unknown as jest.Mocked<ClickHouseClient>;
const module: TestingModule = await Test.createTestingModule({
providers: [
ClickHouseService,
{
provide: TwentyConfigService,
useValue: {
get: jest.fn((key) => {
if (key === 'CLICKHOUSE_URL') return 'http://localhost:8123';
return undefined;
}),
},
},
],
}).compile();
service = module.get<ClickHouseService>(ClickHouseService);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
// Set the mock client
(service as any).mainClient = mockClickHouseClient;
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('constructor', () => {
it('should not initialize clickhouse client when clickhouse is disabled', async () => {
jest.spyOn(twentyConfigService, 'get').mockImplementation((key) => {
if (key === 'CLICKHOUSE_URL') return '';
return undefined;
});
const newModule: TestingModule = await Test.createTestingModule({
providers: [
ClickHouseService,
{
provide: TwentyConfigService,
useValue: twentyConfigService,
},
],
}).compile();
const newService = newModule.get<ClickHouseService>(ClickHouseService);
expect((newService as any).mainClient).toBeUndefined();
});
});
describe('insert', () => {
it('should insert data into clickhouse and return success', async () => {
const testData = [{ id: 1, name: 'test' }];
const result = await service.insert('test_table', testData);
expect(result).toEqual({ success: true });
expect(mockClickHouseClient.insert).toHaveBeenCalledWith({
table: 'test_table',
values: testData,
format: 'JSONEachRow',
});
});
it('should return failure when clickhouse client is not defined', async () => {
(service as any).mainClient = undefined;
const testData = [{ id: 1, name: 'test' }];
const result = await service.insert('test_table', testData);
expect(result).toEqual({ success: false });
});
it('should handle errors and return failure', async () => {
const testError = new Error('Test error');
mockClickHouseClient.insert.mockRejectedValueOnce(testError);
const testData = [{ id: 1, name: 'test' }];
const result = await service.insert('test_table', testData);
expect(result).toEqual({ success: false });
// Since the service uses logger.error instead of exceptionHandlerService.captureExceptions,
// we don't need to assert on exceptionHandlerService
});
});
describe('select', () => {
it('should execute a query and return results', async () => {
const query = 'SELECT * FROM test_table WHERE id = {id:Int32}';
const params = { id: 1 };
mockClickHouseClient.query.mockResolvedValueOnce({
json: jest.fn().mockResolvedValueOnce([{ id: 1, name: 'test' }]),
} as any);
const result = await service.select(query, params);
expect(result).toEqual([{ id: 1, name: 'test' }]);
expect(mockClickHouseClient.query).toHaveBeenCalledWith({
query,
format: 'JSONEachRow',
query_params: params,
});
});
it('should return empty array when clickhouse client is not defined', async () => {
(service as any).mainClient = undefined;
const query = 'SELECT * FROM test_table';
const result = await service.select(query);
expect(result).toEqual([]);
});
it('should handle errors and return empty array', async () => {
const testError = new Error('Test error');
mockClickHouseClient.query.mockRejectedValueOnce(testError);
const query = 'SELECT * FROM test_table';
const result = await service.select(query);
expect(result).toEqual([]);
// Since the service uses logger.error instead of exceptionHandlerService.captureExceptions,
// we don't need to assert on exceptionHandlerService
});
});
describe('createDatabase', () => {
it('should create a database and return true', async () => {
const result = await service.createDatabase('test_db');
expect(result).toBe(true);
expect(mockClickHouseClient.exec).toHaveBeenCalledWith({
query: 'CREATE DATABASE IF NOT EXISTS test_db',
});
});
it('should return false when clickhouse client is not defined', async () => {
(service as any).mainClient = undefined;
const result = await service.createDatabase('test_db');
expect(result).toBe(false);
});
});
describe('dropDatabase', () => {
it('should drop a database and return true', async () => {
const result = await service.dropDatabase('test_db');
expect(result).toBe(true);
expect(mockClickHouseClient.exec).toHaveBeenCalledWith({
query: 'DROP DATABASE IF EXISTS test_db',
});
});
it('should return false when clickhouse client is not defined', async () => {
(service as any).mainClient = undefined;
const result = await service.dropDatabase('test_db');
expect(result).toBe(false);
});
});
describe('connectToClient', () => {
it('should connect to a client and return it', async () => {
jest
.spyOn(service, 'connectToClient')
.mockResolvedValueOnce(mockClickHouseClient);
const result = await service.connectToClient('test-client');
expect(result).toBe(mockClickHouseClient);
});
it('should reuse an existing client if available', async () => {
// Set up a client in the map
(service as any).clients.set('test-client', mockClickHouseClient);
const result = await service.connectToClient('test-client');
expect(result).toBe(mockClickHouseClient);
});
});
describe('disconnectFromClient', () => {
it('should disconnect from a client', async () => {
// Set up a client in the map
(service as any).clients.set('test-client', mockClickHouseClient);
await service.disconnectFromClient('test-client');
expect(mockClickHouseClient.close).toHaveBeenCalled();
expect((service as any).clients.has('test-client')).toBe(false);
});
it('should do nothing if client does not exist', async () => {
await service.disconnectFromClient('non-existent-client');
expect(mockClickHouseClient.close).not.toHaveBeenCalled();
});
});
describe('lifecycle hooks', () => {
it('should ping server on module init', async () => {
await service.onModuleInit();
expect(mockClickHouseClient.ping).toHaveBeenCalled();
});
it('should close all clients on module destroy', async () => {
// Set up a couple of clients
(service as any).clients.set('client1', mockClickHouseClient);
(service as any).clients.set('client2', mockClickHouseClient);
await service.onModuleDestroy();
// One for mainClient, and two for the clients in the map
expect(mockClickHouseClient.close).toHaveBeenCalledTimes(3);
});
});
});

View File

@ -0,0 +1,230 @@
import {
Injectable,
Logger,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { ClickHouseClient, createClient } from '@clickhouse/client';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class ClickHouseService implements OnModuleInit, OnModuleDestroy {
private mainClient: ClickHouseClient | undefined;
private clients: Map<string, ClickHouseClient> = new Map();
private isClientInitializing: Map<string, boolean> = new Map();
private readonly logger = new Logger(ClickHouseService.name);
constructor(private readonly twentyConfigService: TwentyConfigService) {
if (this.twentyConfigService.get('CLICKHOUSE_URL')) {
this.mainClient = createClient({
url: this.twentyConfigService.get('CLICKHOUSE_URL'),
compression: {
response: true,
request: true,
},
clickhouse_settings: {
async_insert: 1,
wait_for_async_insert: 1,
},
application: 'twenty',
});
}
}
public getMainClient(): ClickHouseClient | undefined {
return this.mainClient;
}
public async connectToClient(
clientId: string,
url?: string,
): Promise<ClickHouseClient | undefined> {
if (!this.twentyConfigService.get('CLICKHOUSE_URL')) {
return undefined;
}
// Wait for a bit before trying again if another initialization is in progress
while (this.isClientInitializing.get(clientId)) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
if (this.clients.has(clientId)) {
return this.clients.get(clientId);
}
this.isClientInitializing.set(clientId, true);
try {
const clientInstance = await this.createAndInitializeClient(url);
this.clients.set(clientId, clientInstance);
return clientInstance;
} catch (err) {
this.logger.error(
`Error connecting to ClickHouse client ${clientId}`,
err,
);
return undefined;
} finally {
this.isClientInitializing.delete(clientId);
}
}
private async createAndInitializeClient(
url?: string,
): Promise<ClickHouseClient> {
const client = createClient({
url: url ?? this.twentyConfigService.get('CLICKHOUSE_URL'),
compression: {
response: true,
request: true,
},
clickhouse_settings: {
async_insert: 1,
wait_for_async_insert: 1,
},
application: 'twenty',
});
// Ping to check connection
await client.ping();
return client;
}
public async disconnectFromClient(clientId: string) {
if (!this.clients.has(clientId)) {
return;
}
const client = this.clients.get(clientId);
if (client) {
await client.close();
}
this.clients.delete(clientId);
}
async onModuleInit() {
if (this.mainClient) {
// Just ping to verify the connection
try {
await this.mainClient.ping();
} catch (err) {
this.logger.error('Error connecting to ClickHouse', err);
}
}
}
async onModuleDestroy() {
// Close main client
if (this.mainClient) {
await this.mainClient.close();
}
// Close all other clients
for (const [, client] of this.clients) {
await client.close();
}
}
public async insert<T extends Record<string, any>>(
table: string,
values: T[],
clientId?: string,
): Promise<{ success: boolean }> {
try {
const client = clientId
? await this.connectToClient(clientId)
: this.mainClient;
if (!client) {
return { success: false };
}
await client.insert({
table,
values,
format: 'JSONEachRow',
});
return { success: true };
} catch (err) {
this.logger.error('Error inserting data into ClickHouse', err);
return { success: false };
}
}
// Method to execute a select query
public async select<T>(
query: string,
params?: Record<string, any>,
clientId?: string,
): Promise<T[]> {
try {
const client = clientId
? await this.connectToClient(clientId)
: this.mainClient;
if (!client) {
return [];
}
const resultSet = await client.query({
query,
format: 'JSONEachRow',
query_params: params,
});
const result = await resultSet.json<T>();
return Array.isArray(result) ? result : [];
} catch (err) {
this.logger.error('Error executing select query in ClickHouse', err);
return [];
}
}
public async createDatabase(databaseName: string): Promise<boolean> {
try {
if (!this.mainClient) {
return false;
}
await this.mainClient.exec({
query: `CREATE DATABASE IF NOT EXISTS ${databaseName}`,
});
return true;
} catch (err) {
this.logger.error('Error creating database in ClickHouse', err);
return false;
}
}
public async dropDatabase(databaseName: string): Promise<boolean> {
try {
if (!this.mainClient) {
return false;
}
await this.mainClient.exec({
query: `DROP DATABASE IF EXISTS ${databaseName}`,
});
return true;
} catch (err) {
this.logger.error('Error dropping database in ClickHouse', err);
return false;
}
}
}

View File

@ -1,4 +1,4 @@
CREATE TABLE IF NOT EXISTS events
CREATE TABLE IF NOT EXISTS auditEvent
(
`event` LowCardinality(String),
`timestamp` DateTime64(3),
@ -7,4 +7,4 @@ CREATE TABLE IF NOT EXISTS events
`properties` JSON
)
ENGINE = MergeTree
ORDER BY (event, workspaceId, timestamp);
ORDER BY (event, workspaceId, userId, timestamp);

View File

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS externalEvent
(
`event` LowCardinality(String) NOT NULL,
`timestamp` DateTime64(3) NOT NULL,
`userId` String DEFAULT '',
`workspaceId` String NOT NULL,
`objectId` String NOT NULL,
`objectType` LowCardinality(String), -- TBC if it should really be a LowCardinality given custom objects
`properties` JSON
)
ENGINE = MergeTree
ORDER BY (event, workspaceId, userId, timestamp);

View File

@ -36,7 +36,7 @@ async function ensureDatabaseExists() {
async function ensureMigrationTable(client: ClickHouseClient) {
await client.command({
query: `
CREATE TABLE IF NOT EXISTS migrations (
CREATE TABLE IF NOT EXISTS _migration (
filename String,
applied_at DateTime DEFAULT now()
) ENGINE = MergeTree()
@ -50,7 +50,7 @@ async function hasMigrationBeenRun(
client: ClickHouseClient,
): Promise<boolean> {
const resultSet = await client.query({
query: `SELECT count() as count FROM migrations WHERE filename = {filename:String}`,
query: `SELECT count() as count FROM _migration WHERE filename = {filename:String}`,
query_params: { filename },
format: 'JSON',
});
@ -61,7 +61,7 @@ async function hasMigrationBeenRun(
async function recordMigration(filename: string, client: ClickHouseClient) {
await client.insert({
table: 'migrations',
table: '_migration',
values: [{ filename }],
format: 'JSONEachRow',
});

View File

@ -1,9 +1,9 @@
import { GenericTrackEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-created';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-deactivated';
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-updated';
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-delete';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-deactivated';
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-delete';
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-updated';
import { GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const fixtures: Array<GenericTrackEvent> = [
{

View File

@ -2,7 +2,7 @@
import { createClient } from '@clickhouse/client';
import { config } from 'dotenv';
import { fixtures } from 'src/engine/core-modules/analytics/utils/fixtures/fixtures';
import { fixtures } from './fixtures';
config({
path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
@ -18,7 +18,7 @@ async function seedEvents() {
console.log(`⚡ Seeding ${fixtures.length} events...`);
await client.insert({
table: 'events',
table: 'auditEvent',
values: fixtures,
format: 'JSONEachRow',
});

View File

@ -25,11 +25,6 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IsEventObjectEnabled,
workspaceId: workspaceId,
value: false,
},
{
key: FeatureFlagKey.IsStripeIntegrationEnabled,
workspaceId: workspaceId,
@ -40,21 +35,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IsAnalyticsV2Enabled,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IsCustomDomainEnabled,
workspaceId: workspaceId,
value: false,
},
{
key: FeatureFlagKey.IsApprovedAccessDomainsEnabled,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IsUniqueIndexesEnabled,
workspaceId: workspaceId,

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { CreateAuditLogFromInternalEvent } from 'src/engine/core-modules/audit/jobs/create-audit-log-from-internal-event';
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
@ -12,11 +13,10 @@ import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/t
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { SubscriptionsJob } from 'src/engine/subscriptions/subscriptions.job';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
import { CreateAuditLogFromInternalEvent } from 'src/modules/timeline/jobs/create-audit-log-from-internal-event';
import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job';
import { CallWebhookJobsJob } from 'src/modules/webhook/jobs/call-webhook-jobs.job';
import { SubscriptionsJob } from 'src/engine/subscriptions/subscriptions.job';
@Injectable()
export class EntityEventsToDbListener {

View File

@ -1,17 +1,17 @@
import { Injectable } from '@nestjs/common';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { USER_SIGNUP_EVENT } from 'src/engine/core-modules/audit/utils/events/track/user/user-signup';
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
import { USER_SIGNUP_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/user/user-signup';
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
@Injectable()
export class TelemetryListener {
constructor(
private readonly analyticsService: AnalyticsService,
private readonly auditService: AuditService,
private readonly telemetryService: TelemetryService,
) {}
@ -21,8 +21,8 @@ export class TelemetryListener {
) {
await Promise.all(
payload.events.map(async (eventPayload) => {
this.analyticsService
.createAnalyticsContext({
this.auditService
.createContext({
userId: eventPayload.userId,
workspaceId: payload.workspaceId,
})

View File

@ -5,15 +5,15 @@ import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-qu
import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener';
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@ -27,7 +27,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
WorkspaceQueryHookModule,
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
TypeOrmModule.forFeature([FeatureFlag], 'core'),
AnalyticsModule,
AuditModule,
TelemetryModule,
FileModule,
FeatureFlagModule,

View File

@ -1,15 +0,0 @@
import { Module } from '@nestjs/common';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { ClickhouseService } from 'src/engine/core-modules/analytics/services/clickhouse.service';
import { AnalyticsResolver } from './analytics.resolver';
import { AnalyticsService } from './services/analytics.service';
@Module({
providers: [AnalyticsResolver, AnalyticsService, ClickhouseService],
imports: [JwtModule],
exports: [AnalyticsService],
})
export class AnalyticsModule {}

View File

@ -1,70 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
makePageview,
makeTrackEvent,
} from 'src/engine/core-modules/analytics/utils/analytics.utils';
import { ClickhouseService } from 'src/engine/core-modules/analytics/services/clickhouse.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import {
TrackEventName,
TrackEventProperties,
} from 'src/engine/core-modules/analytics/types/events.type';
import { PageviewProperties } from 'src/engine/core-modules/analytics/utils/events/pageview/pageview';
import {
AnalyticsException,
AnalyticsExceptionCode,
} from 'src/engine/core-modules/analytics/analytics.exception';
@Injectable()
export class AnalyticsService {
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly clickhouseService: ClickhouseService,
) {}
createAnalyticsContext(context?: {
workspaceId?: string | null | undefined;
userId?: string | null | undefined;
}) {
const userIdAndWorkspaceId = context
? {
...(context.userId ? { userId: context.userId } : {}),
...(context.workspaceId ? { workspaceId: context.workspaceId } : {}),
}
: {};
return {
track: <T extends TrackEventName>(
event: T,
properties: TrackEventProperties<T>,
) =>
this.preventAnalyticsIfDisabled(() =>
this.clickhouseService.pushEvent({
...userIdAndWorkspaceId,
...makeTrackEvent(event, properties),
}),
),
pageview: (name: string, properties: Partial<PageviewProperties>) =>
this.preventAnalyticsIfDisabled(() =>
this.clickhouseService.pushEvent({
...userIdAndWorkspaceId,
...makePageview(name, properties),
}),
),
};
}
private preventAnalyticsIfDisabled(
sendEventOrPageviewFunction: () => Promise<{ success: boolean }>,
) {
if (!this.twentyConfigService.get('ANALYTICS_ENABLED')) {
return { success: true };
}
try {
return sendEventOrPageviewFunction();
} catch (err) {
return new AnalyticsException(err, AnalyticsExceptionCode.INVALID_INPUT);
}
}
}

View File

@ -1,159 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
import {
makePageview,
makeTrackEvent,
} from 'src/engine/core-modules/analytics/utils/analytics.utils';
import { ClickhouseService } from './clickhouse.service';
// Mock the createClient function from @clickhouse/client
jest.mock('@clickhouse/client', () => ({
createClient: jest.fn().mockReturnValue({
insert: jest.fn().mockResolvedValue({}),
}),
}));
describe('ClickhouseService', () => {
let service: ClickhouseService;
let twentyConfigService: TwentyConfigService;
let exceptionHandlerService: ExceptionHandlerService;
let mockClickhouseClient: { insert: jest.Mock };
const mockPageview = makePageview('Home', {
href: 'https://example.com/test',
locale: 'en-US',
pathname: '/test',
referrer: 'https://example.com',
sessionId: 'test-session-id',
timeZone: 'UTC',
userAgent: 'test-user-agent',
});
const mockEvent = makeTrackEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
beforeEach(async () => {
jest.clearAllMocks();
mockClickhouseClient = {
insert: jest.fn().mockResolvedValue({}),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
ClickhouseService,
{
provide: TwentyConfigService,
useValue: {
get: jest.fn((key) => {
if (key === 'ANALYTICS_ENABLED') return true;
if (key === 'CLICKHOUSE_URL') return 'http://localhost:8123';
return null;
}),
},
},
{
provide: ExceptionHandlerService,
useValue: {
captureExceptions: jest.fn(),
},
},
],
}).compile();
service = module.get<ClickhouseService>(ClickhouseService);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
exceptionHandlerService = module.get<ExceptionHandlerService>(
ExceptionHandlerService,
);
// Set the mock client
// @ts-expect-error accessing private property for testing
service.clickhouseClient = mockClickhouseClient;
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('constructor', () => {
it('should not initialize clickhouse client when analytics is disabled', async () => {
jest.spyOn(twentyConfigService, 'get').mockImplementation((key) => {
if (key === 'ANALYTICS_ENABLED') return false;
});
const newModule: TestingModule = await Test.createTestingModule({
providers: [
ClickhouseService,
{
provide: TwentyConfigService,
useValue: twentyConfigService,
},
{
provide: ExceptionHandlerService,
useValue: exceptionHandlerService,
},
],
}).compile();
const newService = newModule.get<ClickhouseService>(ClickhouseService);
// @ts-expect-error accessing private property for testing
expect(newService.clickhouseClient).toBeUndefined();
});
});
describe('pushEvent', () => {
it('should insert event into clickhouse and return success', async () => {
const result = await service.pushEvent(mockEvent);
expect(result).toEqual({ success: true });
const { type: _type, ...rest } = mockEvent;
expect(mockClickhouseClient.insert).toHaveBeenCalledWith({
table: 'events',
values: [rest],
format: 'JSONEachRow',
});
});
it('should insert pageview into clickhouse and return success', async () => {
const result = await service.pushEvent(mockPageview);
expect(result).toEqual({ success: true });
const { type: _type, ...rest } = mockPageview;
expect(mockClickhouseClient.insert).toHaveBeenCalledWith({
table: 'pageview',
values: [rest],
format: 'JSONEachRow',
});
});
it('should return success when clickhouse client is not defined', async () => {
// @ts-expect-error accessing private property for testing
service.clickhouseClient = undefined;
const result = await service.pushEvent(mockEvent);
expect(result).toEqual({ success: true });
});
it('should handle errors and return failure', async () => {
const testError = new Error('Test error');
mockClickhouseClient.insert.mockRejectedValueOnce(testError);
const result = await service.pushEvent(mockEvent);
expect(result).toEqual({ success: false });
expect(exceptionHandlerService.captureExceptions).toHaveBeenCalledWith([
testError,
]);
});
});
});

View File

@ -1,60 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ClickHouseClient, createClient } from '@clickhouse/client';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import {
makePageview,
makeTrackEvent,
} from 'src/engine/core-modules/analytics/utils/analytics.utils';
@Injectable()
export class ClickhouseService {
private clickhouseClient: ClickHouseClient | undefined;
constructor(
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly twentyConfigService: TwentyConfigService,
) {
if (twentyConfigService.get('ANALYTICS_ENABLED')) {
this.clickhouseClient = createClient({
url: twentyConfigService.get('CLICKHOUSE_URL'),
compression: {
response: true,
request: true,
},
clickhouse_settings: {
async_insert: 1,
wait_for_async_insert: 1,
},
});
}
}
async pushEvent(
data: (
| ReturnType<typeof makeTrackEvent>
| ReturnType<typeof makePageview>
) & { userId?: string | null; workspaceId?: string | null },
) {
try {
if (!this.clickhouseClient) {
return { success: true };
}
const { type, ...rest } = data;
await this.clickhouseClient.insert({
table: type === 'page' ? 'pageview' : 'events',
values: [rest],
format: 'JSONEachRow',
});
return { success: true };
} catch (err) {
this.exceptionHandlerService.captureExceptions([err]);
return { success: false };
}
}
}

View File

@ -1,2 +0,0 @@
export type AnalyticsCommonPropertiesType = 'timestamp' | 'version';
export type IdentifierType = 'workspaceId' | 'userId';

View File

@ -6,20 +6,20 @@ This module provides analytics tracking functionality for the Twenty application
### Tracking Events
The `AnalyticsService` provides a `createAnalyticsContext` method that returns an object with a `track` method. The `track` method is used to track events.
The `AuditService` provides a `createContext` method that returns an object with a `track` method. The `track` method is used to track events.
```typescript
import { Injectable } from '@nestjs/common';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
@Injectable()
export class MyService {
constructor(private readonly analyticsService: AnalyticsService) {}
constructor(private readonly auditService: AuditService) {}
async doSomething() {
// Create an analytics context
const analytics = this.analyticsService.createAnalyticsContext({
const analytics = this.auditService.createContext({
workspaceId: 'workspace-id',
userId: 'user-id',
});
@ -87,9 +87,9 @@ export type TrackEventProperties<T extends TrackEventName> = T extends keyof Tra
## API
### AnalyticsService
### AuditService
#### createAnalyticsContext(context?)
#### createContext(context?)
Creates an analytics context with the given user ID and workspace ID.

View File

@ -1,12 +1,12 @@
import { CustomException } from 'src/utils/custom-exception';
export class AnalyticsException extends CustomException {
constructor(message: string, code: AnalyticsExceptionCode) {
export class AuditException extends CustomException {
constructor(message: string, code: AuditExceptionCode) {
super(message, code);
}
}
export enum AnalyticsExceptionCode {
export enum AuditExceptionCode {
INVALID_TYPE = 'INVALID_TYPE',
INVALID_INPUT = 'INVALID_INPUT',
}

View File

@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { ClickHouseModule } from 'src/database/clickHouse/clickHouse.module';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { AuditResolver } from './audit.resolver';
import { AuditService } from './services/audit.service';
@Module({
providers: [AuditResolver, AuditService],
imports: [JwtModule, ClickHouseModule],
exports: [AuditService],
})
export class AuditModule {}

View File

@ -1,36 +1,36 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import {
AnalyticsException,
AnalyticsExceptionCode,
} from 'src/engine/core-modules/analytics/analytics.exception';
AuditException,
AuditExceptionCode,
} from 'src/engine/core-modules/audit/audit.exception';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AnalyticsResolver } from './analytics.resolver';
import { AuditResolver } from './audit.resolver';
import { AnalyticsService } from './services/analytics.service';
import { AuditService } from './services/audit.service';
describe('AnalyticsResolver', () => {
let resolver: AnalyticsResolver;
let analyticsService: jest.Mocked<AnalyticsService>;
describe('AuditResolver', () => {
let resolver: AuditResolver;
let auditService: jest.Mocked<AuditService>;
beforeEach(async () => {
analyticsService = {
createAnalyticsContext: jest.fn(),
auditService = {
createContext: jest.fn(),
} as any;
const module: TestingModule = await Test.createTestingModule({
providers: [
AnalyticsResolver,
AuditResolver,
{
provide: AnalyticsService,
useValue: analyticsService,
provide: AuditService,
useValue: auditService,
},
],
}).compile();
resolver = module.get<AnalyticsResolver>(AnalyticsResolver);
resolver = module.get<AuditResolver>(AuditResolver);
});
it('should be defined', () => {
@ -40,7 +40,7 @@ describe('AnalyticsResolver', () => {
it('should handle a valid pageview input', async () => {
const mockPageview = jest.fn().mockResolvedValue('Pageview created');
analyticsService.createAnalyticsContext.mockReturnValue({
auditService.createContext.mockReturnValue({
pageview: mockPageview,
track: jest.fn(),
});
@ -56,7 +56,7 @@ describe('AnalyticsResolver', () => {
{ id: 'user-1' } as User,
);
expect(analyticsService.createAnalyticsContext).toHaveBeenCalledWith({
expect(auditService.createContext).toHaveBeenCalledWith({
workspaceId: 'workspace-1',
userId: 'user-1',
});
@ -67,7 +67,7 @@ describe('AnalyticsResolver', () => {
it('should handle a valid track input', async () => {
const mockTrack = jest.fn().mockResolvedValue('Track created');
analyticsService.createAnalyticsContext.mockReturnValue({
auditService.createContext.mockReturnValue({
track: mockTrack,
pageview: jest.fn(),
});
@ -83,7 +83,7 @@ describe('AnalyticsResolver', () => {
{ id: 'user-2' } as User,
);
expect(analyticsService.createAnalyticsContext).toHaveBeenCalledWith({
expect(auditService.createContext).toHaveBeenCalledWith({
workspaceId: 'workspace-2',
userId: 'user-2',
});
@ -91,15 +91,15 @@ describe('AnalyticsResolver', () => {
expect(result).toBe('Track created');
});
it('should throw an AnalyticsException for invalid input', async () => {
it('should throw an AuditException for invalid input', async () => {
const invalidInput = { type: 'invalid' };
await expect(
resolver.trackAnalytics(invalidInput as any, undefined, undefined),
).rejects.toThrowError(
new AnalyticsException(
new AuditException(
'Invalid analytics input',
AnalyticsExceptionCode.INVALID_TYPE,
AuditExceptionCode.INVALID_TYPE,
),
);
});

View File

@ -1,35 +1,34 @@
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import {
AuditException,
AuditExceptionCode,
} from 'src/engine/core-modules/audit/audit.exception';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import {
AnalyticsException,
AnalyticsExceptionCode,
} from 'src/engine/core-modules/analytics/analytics.exception';
import { AnalyticsService } from './services/analytics.service';
import {
CreateAnalyticsInput,
CreateAnalyticsInputV2,
isPageviewAnalyticsInput,
isTrackAnalyticsInput,
} from './dtos/create-analytics.input';
import { Analytics } from './entities/analytics.entity';
import { AuditService } from './services/audit.service';
@Resolver(() => Analytics)
export class AnalyticsResolver {
constructor(private readonly analyticsService: AnalyticsService) {}
export class AuditResolver {
constructor(private readonly auditService: AuditService) {}
// deprecated
@Mutation(() => Analytics)
track(
@Args() _createAnalyticsInput: CreateAnalyticsInput,
@AuthWorkspace() _workspace: Workspace | undefined,
@AuthUser({ allowUndefined: true }) _user: User | undefined,
// preparing for new name
async auditTrack(
@Args()
createAnalyticsInput: CreateAnalyticsInputV2,
@AuthWorkspace() workspace: Workspace | undefined,
@AuthUser({ allowUndefined: true }) user: User | undefined,
) {
return { success: true };
return this.trackAnalytics(createAnalyticsInput, workspace, user);
}
@Mutation(() => Analytics)
@ -39,7 +38,7 @@ export class AnalyticsResolver {
@AuthWorkspace() workspace: Workspace | undefined,
@AuthUser({ allowUndefined: true }) user: User | undefined,
) {
const analyticsContext = this.analyticsService.createAnalyticsContext({
const analyticsContext = this.auditService.createContext({
workspaceId: workspace?.id,
userId: user?.id,
});
@ -58,9 +57,9 @@ export class AnalyticsResolver {
);
}
throw new AnalyticsException(
throw new AuditException(
'Invalid analytics input',
AnalyticsExceptionCode.INVALID_TYPE,
AuditExceptionCode.INVALID_TYPE,
);
}
}

View File

@ -9,8 +9,8 @@ import {
} from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { TrackEventName } from 'src/engine/core-modules/analytics/types/events.type';
import { PageviewProperties } from 'src/engine/core-modules/analytics/utils/events/pageview/pageview';
import { TrackEventName } from 'src/engine/core-modules/audit/types/events.type';
import { PageviewProperties } from 'src/engine/core-modules/audit/utils/events/pageview/pageview';
enum AnalyticsType {
PAGEVIEW = 'pageview',

View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { CreateAuditLogFromInternalEvent } from 'src/engine/core-modules/audit/jobs/create-audit-log-from-internal-event';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
TimelineActivityModule,
AuditModule,
],
providers: [CreateAuditLogFromInternalEvent],
})
export class AuditJobModule {}

View File

@ -1,26 +1,22 @@
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-delete';
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-updated';
import { ObjectRecordEvent } from 'src/engine/core-modules/event-emitter/types/object-record-event.event';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
import { AuditLogRepository } from 'src/modules/timeline/repositiories/audit-log.repository';
import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-updated';
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-created';
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-delete';
@Processor(MessageQueue.entityEventsToDbQueue)
export class CreateAuditLogFromInternalEvent {
constructor(
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberService: WorkspaceMemberRepository,
@InjectObjectMetadataRepository(AuditLogWorkspaceEntity)
private readonly auditLogRepository: AuditLogRepository,
private readonly analyticsService: AnalyticsService,
private readonly auditService: AuditService,
) {}
@Process(CreateAuditLogFromInternalEvent.name)
@ -28,43 +24,26 @@ export class CreateAuditLogFromInternalEvent {
workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>,
): Promise<void> {
for (const eventData of workspaceEventBatch.events) {
let workspaceMemberId: string | null = null;
if (eventData.userId) {
const workspaceMember = await this.workspaceMemberService.getByIdOrFail(
eventData.userId,
workspaceEventBatch.workspaceId,
);
workspaceMemberId = workspaceMember.id;
}
await this.auditLogRepository.insert(
workspaceEventBatch.name,
// We remove "before" and "after" property for a cleaner/slimmer event payload
const eventProperties =
'diff' in eventData.properties
? {
// we remove "before" and "after" property for a cleaner/slimmer event payload
...eventData.properties,
diff: eventData.properties.diff,
}
: eventData.properties,
workspaceMemberId,
workspaceEventBatch.name.split('.')[0],
eventData.objectMetadata.id,
eventData.recordId,
workspaceEventBatch.workspaceId,
);
: eventData.properties;
const analytics = this.analyticsService.createAnalyticsContext({
const analytics = this.auditService.createContext({
workspaceId: workspaceEventBatch.workspaceId,
userId: eventData.userId,
});
if (workspaceEventBatch.name.endsWith('.updated')) {
analytics.track(OBJECT_RECORD_UPDATED_EVENT, eventData.properties);
analytics.track(OBJECT_RECORD_UPDATED_EVENT, eventProperties);
} else if (workspaceEventBatch.name.endsWith('.created')) {
analytics.track(OBJECT_RECORD_CREATED_EVENT, eventData.properties);
analytics.track(OBJECT_RECORD_CREATED_EVENT, eventProperties);
} else if (workspaceEventBatch.name.endsWith('.deleted')) {
analytics.track(OBJECT_RECORD_DELETED_EVENT, eventData.properties);
analytics.track(OBJECT_RECORD_DELETED_EVENT, eventProperties);
}
}
}

View File

@ -1,24 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AnalyticsContextMock } from 'test/utils/analytics-context.mock';
import { AuditContextMock } from 'test/utils/audit-context.mock';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ClickhouseService } from 'src/engine/core-modules/analytics/services/clickhouse.service';
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { AnalyticsService } from './analytics.service';
import { AuditService } from './audit.service';
describe('AnalyticsService', () => {
let service: AnalyticsService;
describe('AuditService', () => {
let service: AuditService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AnalyticsService,
provide: AuditService,
useValue: {
createAnalyticsContext: AnalyticsContextMock,
createContext: AuditContextMock,
},
},
{
@ -28,7 +28,7 @@ describe('AnalyticsService', () => {
},
},
{
provide: ClickhouseService,
provide: ClickHouseService,
useValue: {
pushEvent: jest.fn(),
},
@ -42,21 +42,21 @@ describe('AnalyticsService', () => {
],
}).compile();
service = module.get<AnalyticsService>(AnalyticsService);
service = module.get<AuditService>(AuditService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('createAnalyticsContext', () => {
describe('createContext', () => {
const mockUserIdAndWorkspaceId = {
userId: 'test-user-id',
workspaceId: 'test-workspace-id',
};
it('should create a valid context object', () => {
const context = service.createAnalyticsContext(mockUserIdAndWorkspaceId);
const context = service.createContext(mockUserIdAndWorkspaceId);
expect(context).toHaveProperty('track');
expect(context).toHaveProperty('pageview');
@ -64,15 +64,13 @@ describe('AnalyticsService', () => {
it('should call track with correct parameters', async () => {
const trackSpy = jest.fn().mockResolvedValue({ success: true });
const mockContext = AnalyticsContextMock({
const mockContext = AuditContextMock({
track: trackSpy,
});
jest
.spyOn(service, 'createAnalyticsContext')
.mockReturnValue(mockContext);
jest.spyOn(service, 'createContext').mockReturnValue(mockContext);
const context = service.createAnalyticsContext(mockUserIdAndWorkspaceId);
const context = service.createContext(mockUserIdAndWorkspaceId);
await context.track(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
@ -81,15 +79,13 @@ describe('AnalyticsService', () => {
it('should call pageview with correct parameters', async () => {
const pageviewSpy = jest.fn().mockResolvedValue({ success: true });
const mockContext = AnalyticsContextMock({
const mockContext = AuditContextMock({
pageview: pageviewSpy,
});
jest
.spyOn(service, 'createAnalyticsContext')
.mockReturnValue(mockContext);
jest.spyOn(service, 'createContext').mockReturnValue(mockContext);
const context = service.createAnalyticsContext(mockUserIdAndWorkspaceId);
const context = service.createContext(mockUserIdAndWorkspaceId);
const testPageviewProperties = {
href: '/test-url',
locale: '',
@ -109,7 +105,7 @@ describe('AnalyticsService', () => {
});
it('should return success when track is called', async () => {
const context = service.createAnalyticsContext(mockUserIdAndWorkspaceId);
const context = service.createContext(mockUserIdAndWorkspaceId);
const result = await context.track(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
@ -117,7 +113,7 @@ describe('AnalyticsService', () => {
});
it('should return success when pageview is called', async () => {
const context = service.createAnalyticsContext(mockUserIdAndWorkspaceId);
const context = service.createContext(mockUserIdAndWorkspaceId);
const result = await context.pageview('page-view', {});

View File

@ -0,0 +1,68 @@
import { Injectable } from '@nestjs/common';
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
import {
AuditException,
AuditExceptionCode,
} from 'src/engine/core-modules/audit/audit.exception';
import {
TrackEventName,
TrackEventProperties,
} from 'src/engine/core-modules/audit/types/events.type';
import {
makePageview,
makeTrackEvent,
} from 'src/engine/core-modules/audit/utils/analytics.utils';
import { PageviewProperties } from 'src/engine/core-modules/audit/utils/events/pageview/pageview';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class AuditService {
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly clickHouseService: ClickHouseService,
) {}
createContext(context?: {
workspaceId?: string | null | undefined;
userId?: string | null | undefined;
}) {
const userIdAndWorkspaceId = context
? {
...(context.userId ? { userId: context.userId } : {}),
...(context.workspaceId ? { workspaceId: context.workspaceId } : {}),
}
: {};
return {
track: <T extends TrackEventName>(
event: T,
properties: TrackEventProperties<T>,
) =>
this.preventIfDisabled(() =>
this.clickHouseService.insert('auditEvent', [
{ ...userIdAndWorkspaceId, ...makeTrackEvent(event, properties) },
]),
),
pageview: (name: string, properties: Partial<PageviewProperties>) =>
this.preventIfDisabled(() =>
this.clickHouseService.insert('pageview', [
{ ...userIdAndWorkspaceId, ...makePageview(name, properties) },
]),
),
};
}
private preventIfDisabled(
sendEventOrPageviewFunction: () => Promise<{ success: boolean }>,
) {
if (!this.twentyConfigService.get('CLICKHOUSE_URL')) {
return { success: true };
}
try {
return sendEventOrPageviewFunction();
} catch (err) {
return new AuditException(err, AuditExceptionCode.INVALID_INPUT);
}
}
}

View File

@ -0,0 +1,2 @@
export type AuditCommonPropertiesType = 'timestamp' | 'version';
export type IdentifierType = 'workspaceId' | 'userId';

View File

@ -1,43 +1,43 @@
import {
WEBHOOK_RESPONSE_EVENT,
WebhookResponseTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/webhook/webhook-response';
import {
SERVERLESS_FUNCTION_EXECUTED_EVENT,
ServerlessFunctionExecutedTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/serverless-function/serverless-function-executed';
import {
CUSTOM_DOMAIN_DEACTIVATED_EVENT,
CustomDomainDeactivatedTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-deactivated';
import {
CUSTOM_DOMAIN_ACTIVATED_EVENT,
CustomDomainActivatedTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
} from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
import {
WORKSPACE_ENTITY_CREATED_EVENT,
WorkspaceEntityCreatedTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/workspace-entity/workspace-entity-created';
import {
USER_SIGNUP_EVENT,
UserSignupTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/user/user-signup';
CUSTOM_DOMAIN_DEACTIVATED_EVENT,
CustomDomainDeactivatedTrackEvent,
} from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-deactivated';
import {
MONITORING_EVENT,
MonitoringTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/monitoring/monitoring';
} from 'src/engine/core-modules/audit/utils/events/track/monitoring/monitoring';
import {
OBJECT_RECORD_CREATED_EVENT,
ObjectRecordCreatedTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-created';
import {
OBJECT_RECORD_UPDATED_EVENT,
ObjectRecordUpdatedTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-updated';
} from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
import {
OBJECT_RECORD_DELETED_EVENT,
ObjectRecordDeletedTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-delete';
} from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-delete';
import {
OBJECT_RECORD_UPDATED_EVENT,
ObjectRecordUpdatedTrackEvent,
} from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-updated';
import {
SERVERLESS_FUNCTION_EXECUTED_EVENT,
ServerlessFunctionExecutedTrackEvent,
} from 'src/engine/core-modules/audit/utils/events/track/serverless-function/serverless-function-executed';
import {
USER_SIGNUP_EVENT,
UserSignupTrackEvent,
} from 'src/engine/core-modules/audit/utils/events/track/user/user-signup';
import {
WEBHOOK_RESPONSE_EVENT,
WebhookResponseTrackEvent,
} from 'src/engine/core-modules/audit/utils/events/track/webhook/webhook-response';
import {
WORKSPACE_ENTITY_CREATED_EVENT,
WorkspaceEntityCreatedTrackEvent,
} from 'src/engine/core-modules/audit/utils/events/track/workspace-entity/workspace-entity-created';
// Define all track event names
export type TrackEventName =

View File

@ -1,20 +1,20 @@
import { format } from 'date-fns';
import { AnalyticsCommonPropertiesType } from 'src/engine/core-modules/analytics/types/common.type';
import {
PageviewProperties,
pageviewSchema,
} from 'src/engine/core-modules/analytics/utils/events/pageview/pageview';
import { AuditCommonPropertiesType } from 'src/engine/core-modules/audit/types/common.type';
import {
TrackEventName,
TrackEventProperties,
} from 'src/engine/core-modules/analytics/types/events.type';
} from 'src/engine/core-modules/audit/types/events.type';
import {
PageviewProperties,
pageviewSchema,
} from 'src/engine/core-modules/audit/utils/events/pageview/pageview';
import {
eventsRegistry,
GenericTrackEvent,
} from 'src/engine/core-modules/analytics/utils/events/track/track';
} from 'src/engine/core-modules/audit/utils/events/track/track';
const common = (): Record<AnalyticsCommonPropertiesType, string> => ({
const common = (): Record<AuditCommonPropertiesType, string> => ({
timestamp: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
version: '1',
});

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { baseEventSchema } from 'src/engine/core-modules/analytics/utils/events/common/base-schemas';
import { baseEventSchema } from 'src/engine/core-modules/audit/utils/events/common/base-schemas';
export const pageviewSchema = baseEventSchema.extend({
type: z.literal('page'),

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const CUSTOM_DOMAIN_ACTIVATED_EVENT = 'Custom Domain Activated' as const;
export const customDomainActivatedSchema = z

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const CUSTOM_DOMAIN_DEACTIVATED_EVENT =
'Custom Domain Deactivated' as const;

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const MONITORING_EVENT = 'Monitoring' as const;
export const monitoringSchema = z

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const OBJECT_RECORD_CREATED_EVENT = 'Object Record Created' as const;
export const objectRecordCreatedSchema = z.object({

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const OBJECT_RECORD_DELETED_EVENT = 'Object Record Deleted' as const;
export const objectRecordDeletedSchema = z.object({

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const OBJECT_RECORD_UPDATED_EVENT = 'Object Record Updated' as const;
export const objectRecordUpdatedSchema = z.object({

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const SERVERLESS_FUNCTION_EXECUTED_EVENT =
'Serverless Function Executed' as const;

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { baseEventSchema } from 'src/engine/core-modules/analytics/utils/events/common/base-schemas';
import { baseEventSchema } from 'src/engine/core-modules/audit/utils/events/common/base-schemas';
export const genericTrackSchema = baseEventSchema.extend({
type: z.literal('track'),

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const USER_SIGNUP_EVENT = 'User Signup' as const;
export const userSignupSchema = z

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const WEBHOOK_RESPONSE_EVENT = 'Webhook Response' as const;
export const webhookResponseSchema = z

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
export const WORKSPACE_ENTITY_CREATED_EVENT =
'Workspace Entity Created' as const;

View File

@ -51,7 +51,7 @@ import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { AnalyticsModule } from './analytics/analytics.module';
import { AuditModule } from './audit/audit.module';
import { ClientConfigModule } from './client-config/client-config.module';
import { FileModule } from './file/file.module';
@ -59,7 +59,7 @@ import { FileModule } from './file/file.module';
imports: [
TwentyConfigModule.forRoot(),
HealthModule,
AnalyticsModule,
AuditModule,
AuthModule,
BillingModule,
ClientConfigModule,
@ -128,7 +128,7 @@ import { FileModule } from './file/file.module';
SearchModule,
],
exports: [
AnalyticsModule,
AuditModule,
AuthModule,
FeatureFlagModule,
TimelineMessagingModule,

View File

@ -13,8 +13,8 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Request, Response } from 'express';
import { Repository } from 'typeorm';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
import {
DomainManagerException,
@ -36,7 +36,7 @@ export class CloudflareController {
private readonly domainManagerService: DomainManagerService,
private readonly customDomainService: CustomDomainService,
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly analyticsService: AnalyticsService,
private readonly auditService: AuditService,
) {}
@Post(['cloudflare/custom-hostname-webhooks', 'webhooks/cloudflare'])
@ -60,7 +60,7 @@ export class CloudflareController {
if (!workspace) return;
const analytics = this.analyticsService.createAnalyticsContext({
const analytics = this.auditService.createContext({
workspaceId: workspace.id,
});

View File

@ -2,9 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Request, Response } from 'express';
import { AuditContextMock } from 'test/utils/audit-context.mock';
import { Repository } from 'typeorm';
import { AnalyticsContextMock } from 'test/utils/analytics-context.mock';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { CloudflareController } from 'src/engine/core-modules/domain-manager/controllers/cloudflare.controller';
import { CustomDomainValidRecords } from 'src/engine/core-modules/domain-manager/dtos/custom-domain-valid-records';
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
@ -13,7 +14,6 @@ import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handl
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
describe('CloudflareController - customHostnameWebhooks', () => {
let controller: CloudflareController;
@ -64,9 +64,9 @@ describe('CloudflareController - customHostnameWebhooks', () => {
},
},
{
provide: AnalyticsService,
provide: AuditService,
useValue: {
createAnalyticsContext: AnalyticsContextMock,
createContext: AuditContextMock,
},
},
],

View File

@ -1,14 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { CloudflareController } from 'src/engine/core-modules/domain-manager/controllers/cloudflare.controller';
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Module({
imports: [AnalyticsModule, TypeOrmModule.forFeature([Workspace], 'core')],
imports: [AuditModule, TypeOrmModule.forFeature([Workspace], 'core')],
providers: [DomainManagerService, CustomDomainService],
exports: [DomainManagerService, CustomDomainService],
controllers: [CloudflareController],

View File

@ -2,12 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing';
import Cloudflare from 'cloudflare';
import { CustomHostnameCreateResponse } from 'cloudflare/resources/custom-hostnames/custom-hostnames';
import { AnalyticsContextMock } from 'test/utils/analytics-context.mock';
import { AuditContextMock } from 'test/utils/audit-context.mock';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { DomainManagerException } from 'src/engine/core-modules/domain-manager/domain-manager.exception';
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainManagerException } from 'src/engine/core-modules/domain-manager/domain-manager.exception';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
jest.mock('cloudflare');
@ -28,9 +28,9 @@ describe('CustomDomainService', () => {
},
},
{
provide: AnalyticsService,
provide: AuditService,
useValue: {
createAnalyticsContext: AnalyticsContextMock,
createContext: AuditContextMock,
},
},
{

View File

@ -1,15 +1,12 @@
export enum FeatureFlagKey {
IsEventObjectEnabled = 'IS_EVENT_OBJECT_ENABLED',
IsAirtableIntegrationEnabled = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IsStripeIntegrationEnabled = 'IS_STRIPE_INTEGRATION_ENABLED',
IsCopilotEnabled = 'IS_COPILOT_ENABLED',
IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED',
IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED',
IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED',
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',
IsCustomDomainEnabled = 'IS_CUSTOM_DOMAIN_ENABLED',
IsApprovedAccessDomainsEnabled = 'IS_APPROVED_ACCESS_DOMAINS_ENABLED',
IsNewRelationEnabled = 'IS_NEW_RELATION_ENABLED',
IsPermissionsV2Enabled = 'IS_PERMISSIONS_V2_ENABLED',
}

View File

@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { AuditJobModule } from 'src/engine/core-modules/audit/jobs/audit-job.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
@ -19,6 +20,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { CleanOnboardingWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-onboarding-workspaces.job';
import { CleanSuspendedWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job';
import { CleanWorkspaceDeletionWarningUserVarsJob } from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job';
@ -32,7 +34,6 @@ import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
import { WebhookJobModule } from 'src/modules/webhook/jobs/webhook-job.module';
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
@Module({
imports: [
@ -60,6 +61,7 @@ import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.modu
FavoriteModule,
WorkspaceCleanerModule,
SubscriptionsModule,
AuditJobModule,
],
providers: [
CleanSuspendedWorkspacesJob,

View File

@ -16,6 +16,6 @@ export enum ConfigVariablesGroup {
ServerlessConfig = 'serverless-config',
SSL = 'ssl',
SupportChatConfig = 'support-chat-config',
AnalyticsConfig = 'analytics-config',
AnalyticsConfig = 'audit-config',
TokensDuration = 'tokens-duration',
}

View File

@ -7,7 +7,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
@ -46,7 +46,7 @@ import { UserService } from './services/user.service';
OnboardingModule,
TypeOrmModule.forFeature([KeyValuePair, UserWorkspace], 'core'),
UserVarsModule,
AnalyticsModule,
AuditModule,
DomainManagerModule,
UserRoleModule,
FeatureFlagModule,

View File

@ -20,7 +20,6 @@ import { In, Repository } from 'typeorm';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
import {
AuthException,
AuthExceptionCode,
@ -75,7 +74,6 @@ export class UserResolver {
private readonly onboardingService: OnboardingService,
private readonly userVarService: UserVarsService,
private readonly fileService: FileService,
private readonly analyticsService: AnalyticsService,
private readonly domainManagerService: DomainManagerService,
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository<UserWorkspace>,

View File

@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
@ -25,7 +26,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
describe('WorkspaceService', () => {
let service: WorkspaceService;
@ -76,9 +76,9 @@ describe('WorkspaceService', () => {
},
},
{
provide: AnalyticsService,
provide: AuditService,
useValue: {
createAnalyticsContext: jest.fn(),
createContext: jest.fn(),
},
},
...[

View File

@ -8,6 +8,9 @@ import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-deactivated';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
@ -44,9 +47,6 @@ import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags';
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-deactivated';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
@ -70,7 +70,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private readonly domainManagerService: DomainManagerService,
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly permissionsService: PermissionsService,
private readonly analyticsService: AnalyticsService,
private readonly auditService: AuditService,
private readonly customDomainService: CustomDomainService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
@InjectMessageQueue(MessageQueue.deleteCascadeQueue)
@ -418,7 +418,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
workspace.isCustomDomainEnabled = isCustomDomainWorking;
await this.workspaceRepository.save(workspace);
const analytics = this.analyticsService.createAnalyticsContext({
const analytics = this.auditService.createContext({
workspaceId: workspace.id,
});

View File

@ -5,6 +5,7 @@ import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
@ -24,7 +25,6 @@ import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
import { Workspace } from './workspace.entity';
@ -55,7 +55,7 @@ import { WorkspaceService } from './services/workspace.service';
TypeORMModule,
PermissionsModule,
WorkspaceCacheStorageModule,
AnalyticsModule,
AuditModule,
RoleModule,
],
services: [WorkspaceService],

View File

@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
@ -19,7 +19,7 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles
TypeOrmModule.forFeature([FeatureFlag], 'core'),
FileModule,
ThrottlerModule,
AnalyticsModule,
AuditModule,
],
providers: [ServerlessFunctionService, ServerlessFunctionResolver],
exports: [ServerlessFunctionService],

View File

@ -10,7 +10,8 @@ import { IsNull, Not, Repository } from 'typeorm';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/serverless-function/serverless-function-executed';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
@ -35,7 +36,6 @@ import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/serverless-function/serverless-function-executed';
@Injectable()
export class ServerlessFunctionService {
@ -46,7 +46,7 @@ export class ServerlessFunctionService {
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
private readonly throttlerService: ThrottlerService,
private readonly twentyConfigService: TwentyConfigService,
private readonly analyticsService: AnalyticsService,
private readonly auditService: AuditService,
@InjectMessageQueue(MessageQueue.serverlessFunctionQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@ -144,8 +144,8 @@ export class ServerlessFunctionService {
version,
);
this.analyticsService
.createAnalyticsContext({
this.auditService
.createContext({
workspaceId,
})
.track(SERVERLESS_FUNCTION_EXECUTED_EVENT, {

View File

@ -3,6 +3,7 @@ export const EXCLUDED_MIDDLEWARE_OPERATIONS = [
'GetWorkspaceFromInviteHash',
'Track',
'TrackAnalytics',
'AuditTrack',
'CheckUserExists',
'GetLoginTokenFromCredentials',
'GetAuthTokensFromLoginToken',

View File

@ -1,10 +1,8 @@
import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository';
import { AuditLogRepository } from 'src/modules/timeline/repositiories/audit-log.repository';
import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
export const metadataToRepositoryMapping = {
AuditLogWorkspaceEntity: AuditLogRepository,
BlocklistWorkspaceEntity: BlocklistRepository,
TimelineActivityWorkspaceEntity: TimelineActivityRepository,
WorkspaceMemberWorkspaceEntity: WorkspaceMemberRepository,

View File

@ -21,8 +21,6 @@ import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-obj
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity';
import { BehavioralEventWorkspaceEntity } from 'src/modules/timeline/standard-objects/behavioral-event.workspace-entity';
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
import { ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
@ -31,19 +29,17 @@ import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity';
import { WorkflowAutomatedTriggerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity';
import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { WorkflowAutomatedTriggerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
// TODO: Maybe we should automate this with the DiscoverService of Nest.JS
export const standardObjectMetadataDefinitions = [
ApiKeyWorkspaceEntity,
AuditLogWorkspaceEntity,
AttachmentWorkspaceEntity,
BehavioralEventWorkspaceEntity,
BlocklistWorkspaceEntity,
CalendarEventWorkspaceEntity,
CalendarChannelWorkspaceEntity,

View File

@ -4,7 +4,6 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { API_KEY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
@ -21,7 +20,6 @@ import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync
labelIdentifierStandardId: API_KEY_STANDARD_FIELD_IDS.name,
})
@WorkspaceIsSystem()
@WorkspaceIsNotAuditLogged()
export class ApiKeyWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: API_KEY_STANDARD_FIELD_IDS.name,

View File

@ -1,14 +1,13 @@
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
@ -27,7 +26,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
labelIdentifierStandardId: BLOCKLIST_STANDARD_FIELD_IDS.handle,
})
@WorkspaceIsSystem()
@WorkspaceIsNotAuditLogged()
export class BlocklistWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: BLOCKLIST_STANDARD_FIELD_IDS.handle,

View File

@ -11,7 +11,6 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
@ -33,7 +32,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
labelIdentifierStandardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.handle,
})
@WorkspaceIsSystem()
@WorkspaceIsNotAuditLogged()
export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.handle,

View File

@ -4,12 +4,12 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
@ -28,7 +28,6 @@ import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.favorite,
@ -38,7 +37,6 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met
description: msg`A favorite that can be accessed from the left menu`,
icon: STANDARD_OBJECT_ICONS.favorite,
})
@WorkspaceIsNotAuditLogged()
@WorkspaceIsSystem()
export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({

View File

@ -19,7 +19,7 @@ import {
MessageImportSyncStep,
} from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
import { MessagingPartialMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-partial-message-list-fetch.service';
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
export type MessagingMessageListFetchJobData = {
messageChannelId: string;
@ -36,7 +36,7 @@ export class MessagingMessageListFetchJob {
constructor(
private readonly messagingFullMessageListFetchService: MessagingFullMessageListFetchService,
private readonly messagingPartialMessageListFetchService: MessagingPartialMessageListFetchService,
private readonly messagingTelemetryService: MessagingTelemetryService,
private readonly messagingMonitoringService: MessagingMonitoringService,
private readonly twentyORMManager: TwentyORMManager,
private readonly connectedAccountRefreshTokensService: ConnectedAccountRefreshTokensService,
private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService,
@ -46,7 +46,7 @@ export class MessagingMessageListFetchJob {
async handle(data: MessagingMessageListFetchJobData): Promise<void> {
const { messageChannelId, workspaceId } = data;
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'message_list_fetch_job.triggered',
messageChannelId,
workspaceId,
@ -65,7 +65,7 @@ export class MessagingMessageListFetchJob {
});
if (!messageChannel) {
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'message_list_fetch_job.error.message_channel_not_found',
messageChannelId,
workspaceId,
@ -94,7 +94,7 @@ export class MessagingMessageListFetchJob {
switch (error.code) {
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED:
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND:
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: `refresh_token.error.insufficient_permissions`,
workspaceId,
connectedAccountId: messageChannel.connectedAccountId,
@ -121,7 +121,7 @@ export class MessagingMessageListFetchJob {
`Fetching partial message list for workspace ${workspaceId} and messageChannelId ${messageChannel.id}`,
);
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'partial_message_list_fetch.started',
workspaceId,
connectedAccountId: messageChannel.connectedAccount.id,
@ -134,7 +134,7 @@ export class MessagingMessageListFetchJob {
workspaceId,
);
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'partial_message_list_fetch.completed',
workspaceId,
connectedAccountId: messageChannel.connectedAccount.id,
@ -148,7 +148,7 @@ export class MessagingMessageListFetchJob {
`Fetching full message list for workspace ${workspaceId} and account ${messageChannel.connectedAccount.id}`,
);
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'full_message_list_fetch.started',
workspaceId,
connectedAccountId: messageChannel.connectedAccount.id,
@ -160,7 +160,7 @@ export class MessagingMessageListFetchJob {
workspaceId,
);
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'full_message_list_fetch.completed',
workspaceId,
connectedAccountId: messageChannel.connectedAccount.id,

View File

@ -10,8 +10,7 @@ import {
MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
export type MessagingMessagesImportJobData = {
messageChannelId: string;
workspaceId: string;
@ -24,7 +23,7 @@ export type MessagingMessagesImportJobData = {
export class MessagingMessagesImportJob {
constructor(
private readonly messagingMessagesImportService: MessagingMessagesImportService,
private readonly messagingTelemetryService: MessagingTelemetryService,
private readonly messagingMonitoringService: MessagingMonitoringService,
private readonly twentyORMManager: TwentyORMManager,
) {}
@ -32,7 +31,7 @@ export class MessagingMessagesImportJob {
async handle(data: MessagingMessagesImportJobData): Promise<void> {
const { messageChannelId, workspaceId } = data;
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'messages_import.triggered',
workspaceId,
messageChannelId,
@ -51,7 +50,7 @@ export class MessagingMessagesImportJob {
});
if (!messageChannel) {
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'messages_import.error.message_channel_not_found',
messageChannelId,
workspaceId,

View File

@ -20,8 +20,7 @@ import { MessagingGetMessagesService } from 'src/modules/messaging/message-impor
import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
describe('MessagingMessagesImportService', () => {
let service: MessagingMessagesImportService;
let messageChannelSyncStatusService: MessageChannelSyncStatusService;
@ -78,7 +77,7 @@ describe('MessagingMessagesImportService', () => {
},
},
{
provide: MessagingTelemetryService,
provide: MessagingMonitoringService,
useValue: {
track: jest.fn().mockResolvedValue(undefined),
},

View File

@ -26,8 +26,7 @@ import {
} from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
import { filterEmails } from 'src/modules/messaging/message-import-manager/utils/filter-emails.util';
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
@Injectable()
export class MessagingMessagesImportService {
private readonly logger = new Logger(MessagingMessagesImportService.name);
@ -38,7 +37,7 @@ export class MessagingMessagesImportService {
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
private readonly saveMessagesAndEnqueueContactCreationService: MessagingSaveMessagesAndEnqueueContactCreationService,
private readonly connectedAccountRefreshTokensService: ConnectedAccountRefreshTokensService,
private readonly messagingTelemetryService: MessagingTelemetryService,
private readonly messagingMonitoringService: MessagingMonitoringService,
@InjectObjectMetadataRepository(BlocklistWorkspaceEntity)
private readonly blocklistRepository: BlocklistRepository,
private readonly emailAliasManagerService: EmailAliasManagerService,
@ -62,7 +61,7 @@ export class MessagingMessagesImportService {
return;
}
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'messages_import.started',
workspaceId,
connectedAccountId: messageChannel.connectedAccountId,
@ -87,7 +86,7 @@ export class MessagingMessagesImportService {
switch (error.code) {
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED:
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND:
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: `refresh_token.error.insufficient_permissions`,
workspaceId,
connectedAccountId: messageChannel.connectedAccountId,
@ -208,7 +207,7 @@ export class MessagingMessagesImportService {
messageChannel: MessageChannelWorkspaceEntity,
workspaceId: string,
) {
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'messages_import.completed',
workspaceId,
connectedAccountId: messageChannel.connectedAccountId,

View File

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@ -22,7 +22,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
@Module({
imports: [
TypeOrmModule.forFeature([FeatureFlag, Workspace], 'core'),
AnalyticsModule,
AuditModule,
ContactCreationManagerModule,
WorkspaceDataSourceModule,
ObjectMetadataRepositoryModule.forFeature([

View File

@ -13,7 +13,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
export const MESSAGING_MESSAGE_CHANNEL_SYNC_STATUS_MONITORING_CRON_PATTERN =
'2/10 * * * *'; //Every 10 minutes, starting at 2 minutes past the hour
@ -27,7 +27,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
private readonly messagingTelemetryService: MessagingTelemetryService,
private readonly messagingMonitoringService: MessagingMonitoringService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
@ -40,7 +40,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
async handle(): Promise<void> {
this.logger.log('Starting message channel sync status monitoring...');
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: 'message_channel.monitoring.sync_status.start',
message: 'Starting message channel sync status monitoring',
});
@ -66,7 +66,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
if (!messageChannel.syncStatus) {
continue;
}
await this.messagingTelemetryService.track({
await this.messagingMonitoringService.track({
eventName: `message_channel.monitoring.sync_status.${snakeCase(
messageChannel.syncStatus,
)}`,

View File

@ -1,18 +1,18 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { MessagingMessageChannelSyncStatusMonitoringCronCommand } from 'src/modules/messaging/monitoring/crons/commands/messaging-message-channel-sync-status-monitoring.cron.command';
import { MessagingMessageChannelSyncStatusMonitoringCronJob } from 'src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job';
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
@Module({
imports: [
AnalyticsModule,
AuditModule,
MessagingCommonModule,
BillingModule,
TypeOrmModule.forFeature([Workspace], 'core'),
@ -21,8 +21,8 @@ import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/serv
providers: [
MessagingMessageChannelSyncStatusMonitoringCronCommand,
MessagingMessageChannelSyncStatusMonitoringCronJob,
MessagingTelemetryService,
MessagingMonitoringService,
],
exports: [MessagingTelemetryService],
exports: [MessagingMonitoringService],
})
export class MessagingMonitoringModule {}

View File

@ -0,0 +1,47 @@
import { Injectable } from '@nestjs/common';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
type MessagingMonitoringTrackInput = {
eventName: string;
workspaceId?: string;
userId?: string;
connectedAccountId?: string;
messageChannelId?: string;
message?: string;
};
@Injectable()
export class MessagingMonitoringService {
constructor(private readonly auditService: AuditService) {}
public async track({
eventName,
workspaceId,
userId,
connectedAccountId,
messageChannelId,
message,
}: MessagingMonitoringTrackInput): Promise<void> {
const _eventName = eventName;
const _workspaceId = workspaceId;
const _userId = userId;
const _connectedAccountId = connectedAccountId;
const _messageChannelId = messageChannelId;
const _message = message;
// TODO: replace once we have Prometheus
/*
await this.auditService
.createContext({
userId,
workspaceId,
})
.track(MONITORING_EVENT, {
eventName: `messaging.${eventName}`,
connectedAccountId,
messageChannelId,
message,
}); */
}
}

View File

@ -1,39 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
import { MONITORING_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/monitoring/monitoring';
type MessagingTelemetryTrackInput = {
eventName: string;
workspaceId?: string;
userId?: string;
connectedAccountId?: string;
messageChannelId?: string;
message?: string;
};
@Injectable()
export class MessagingTelemetryService {
constructor(private readonly analyticsService: AnalyticsService) {}
public async track({
eventName,
workspaceId,
userId,
connectedAccountId,
messageChannelId,
message,
}: MessagingTelemetryTrackInput): Promise<void> {
await this.analyticsService
.createAnalyticsContext({
userId,
workspaceId,
})
.track(MONITORING_EVENT, {
eventName: `messaging.${eventName}`,
connectedAccountId,
messageChannelId,
message,
});
}
}

View File

@ -1,25 +1,17 @@
import { Module } from '@nestjs/common';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { CreateAuditLogFromInternalEvent } from 'src/modules/timeline/jobs/create-audit-log-from-internal-event';
import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job';
import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity';
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([
WorkspaceMemberWorkspaceEntity,
AuditLogWorkspaceEntity,
]),
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
TimelineActivityModule,
AnalyticsModule,
],
providers: [
CreateAuditLogFromInternalEvent,
UpsertTimelineActivityFromInternalEvent,
AuditModule,
],
providers: [UpsertTimelineActivityFromInternalEvent],
})
export class TimelineJobModule {}

View File

@ -1,38 +0,0 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@Injectable()
export class AuditLogRepository {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async insert(
name: string,
properties: object | null,
workspaceMemberId: string | null,
objectName: string,
objectMetadataId: string,
recordId: string,
workspaceId: string,
): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."auditLog"
("name", "properties", "workspaceMemberId", "objectName", "objectMetadataId", "recordId")
VALUES ($1, $2, $3, $4, $5, $6)`,
[
name,
properties,
workspaceMemberId,
objectName,
objectMetadataId,
recordId,
],
workspaceId,
);
}
}

View File

@ -1,103 +0,0 @@
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
import { AUDIT_LOGS_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.auditLog,
namePlural: 'auditLogs',
labelSingular: msg`Audit Log`,
labelPlural: msg`Audit Logs`,
description: msg`An audit log of actions performed in the system`,
icon: STANDARD_OBJECT_ICONS.auditLog,
labelIdentifierStandardId: AUDIT_LOGS_STANDARD_FIELD_IDS.name,
})
@WorkspaceIsSystem()
export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.name,
type: FieldMetadataType.TEXT,
label: msg`Event name`,
description: msg`Event name/type`,
icon: 'IconAbc',
})
name: string;
@WorkspaceField({
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.properties,
type: FieldMetadataType.RAW_JSON,
label: msg`Event details`,
description: msg`Json value for event details`,
icon: 'IconListDetails',
})
@WorkspaceIsNullable()
properties: JSON | null;
@WorkspaceField({
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.context,
type: FieldMetadataType.RAW_JSON,
label: msg`Event context`,
description: msg`Json object to provide context (user, device, workspace, etc.)`,
icon: 'IconListDetails',
})
@WorkspaceIsNullable()
context: JSON | null;
@WorkspaceField({
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.objectName,
type: FieldMetadataType.TEXT,
label: msg`Object name`,
description: msg`Object name`,
icon: 'IconAbc',
})
objectName: string;
@WorkspaceField({
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.objectMetadataId,
type: FieldMetadataType.TEXT,
label: msg`Object metadata id`,
description: msg`Object metadata id`,
icon: 'IconAbc',
})
objectMetadataId: string;
@WorkspaceField({
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.recordId,
type: FieldMetadataType.UUID,
label: msg`Record id`,
description: msg`Record id`,
icon: 'IconAbc',
})
@WorkspaceIsNullable()
recordId: string | null;
@WorkspaceRelation({
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.workspaceMember,
type: RelationType.MANY_TO_ONE,
label: msg`Workspace Member`,
description: msg`Event workspace member`,
icon: 'IconCircleUser',
inverseSideTarget: () => WorkspaceMemberWorkspaceEntity,
inverseSideFieldKey: 'auditLogs',
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity> | null;
@WorkspaceJoinColumn('workspaceMember')
workspaceMemberId: string | null;
}

View File

@ -1,92 +0,0 @@
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { BEHAVIORAL_EVENT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.behavioralEvent,
namePlural: 'behavioralEvents',
labelSingular: msg`Behavioral Event`,
labelPlural: msg`Behavioral Events`,
description: msg`An event related to user behavior`,
icon: STANDARD_OBJECT_ICONS.behavioralEvent,
})
@WorkspaceIsSystem()
@WorkspaceGate({
featureFlag: FeatureFlagKey.IsEventObjectEnabled,
})
export class BehavioralEventWorkspaceEntity extends BaseWorkspaceEntity {
/**
*
* Common in Segment, Rudderstack, etc.
* = Track, Screen, Page...
* But doesn't feel that useful.
* Let's try living without it.
*
@WorkspaceField({
standardId: behavioralEventStandardFieldIds.type,
type: FieldMetadataType.TEXT,
label: msg`Event type`,
description: msg`Event type`,
icon: 'IconAbc',
})
type: string;
*/
@WorkspaceField({
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.name,
type: FieldMetadataType.TEXT,
label: msg`Event name`,
description: msg`Event name`,
icon: 'IconAbc',
})
name: string;
@WorkspaceField({
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.properties,
type: FieldMetadataType.RAW_JSON,
label: msg`Event details`,
description: msg`Json value for event details`,
icon: 'IconListDetails',
})
@WorkspaceIsNullable()
properties: JSON | null;
@WorkspaceField({
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.context,
type: FieldMetadataType.RAW_JSON,
label: msg`Event context`,
description: msg`Json object to provide context (user, device, workspace, etc.)`,
icon: 'IconListDetails',
})
@WorkspaceIsNullable()
context: JSON | null;
@WorkspaceField({
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.objectName,
type: FieldMetadataType.TEXT,
label: msg`Object name`,
description: msg`If the event is related to a particular object`,
icon: 'IconAbc',
})
objectName: string;
@WorkspaceField({
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.recordId,
type: FieldMetadataType.UUID,
label: msg`Object id`,
description: msg`Event name/type`,
icon: 'IconAbc',
})
@WorkspaceIsNullable()
recordId: string | null;
}

View File

@ -7,11 +7,11 @@ import { Relation } from 'typeorm';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
@ -20,7 +20,6 @@ import { VIEW_FIELD_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/work
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
registerEnumType(AGGREGATE_OPERATIONS, {
name: 'AggregateOperations',
@ -34,7 +33,6 @@ registerEnumType(AGGREGATE_OPERATIONS, {
description: msg`(System) View Fields`,
icon: STANDARD_OBJECT_ICONS.viewField,
})
@WorkspaceIsNotAuditLogged()
@WorkspaceIsSystem()
@WorkspaceIndex(['fieldMetadataId', 'viewId'], {
isUnique: true,

View File

@ -4,10 +4,10 @@ import { Relation } from 'typeorm';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
@ -15,7 +15,6 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re
import { VIEW_FILTER_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
export enum ViewFilterGroupLogicalOperator {
AND = 'AND',
@ -31,7 +30,6 @@ export enum ViewFilterGroupLogicalOperator {
description: msg`(System) View Filter Groups`,
icon: 'IconFilterBolt',
})
@WorkspaceIsNotAuditLogged()
@WorkspaceIsSystem()
export class ViewFilterGroupWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({

View File

@ -4,10 +4,10 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
@ -16,7 +16,6 @@ import { VIEW_FILTER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/wor
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.viewFilter,
@ -26,7 +25,6 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met
description: msg`(System) View Filters`,
icon: STANDARD_OBJECT_ICONS.viewFilter,
})
@WorkspaceIsNotAuditLogged()
@WorkspaceIsSystem()
export class ViewFilterWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({

View File

@ -3,10 +3,10 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
@ -15,7 +15,6 @@ import { VIEW_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/work
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.viewGroup,
@ -25,7 +24,6 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met
description: msg`(System) View Groups`,
icon: STANDARD_OBJECT_ICONS.viewGroup,
})
@WorkspaceIsNotAuditLogged()
@WorkspaceIsSystem()
export class ViewGroupWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({

View File

@ -4,11 +4,11 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
@ -17,7 +17,6 @@ import { VIEW_SORT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/works
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.viewSort,
@ -27,7 +26,6 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met
description: msg`(System) View Sorts`,
icon: STANDARD_OBJECT_ICONS.viewSort,
})
@WorkspaceIsNotAuditLogged()
@WorkspaceIsSystem()
@WorkspaceIndex(['fieldMetadataId', 'viewId'], {
isUnique: true,

View File

@ -11,7 +11,6 @@ import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runne
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
@ -43,7 +42,6 @@ registerEnumType(ViewOpenRecordInType, {
icon: STANDARD_OBJECT_ICONS.view,
labelIdentifierStandardId: VIEW_STANDARD_FIELD_IDS.name,
})
@WorkspaceIsNotAuditLogged()
@WorkspaceIsSystem()
export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({

View File

@ -3,11 +3,11 @@ import { Logger } from '@nestjs/common';
import crypto from 'crypto';
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { WEBHOOK_RESPONSE_EVENT } from 'src/engine/core-modules/audit/utils/events/track/webhook/webhook-response';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { WEBHOOK_RESPONSE_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/webhook/webhook-response';
export type CallWebhookJobData = {
targetUrl: string;
@ -26,7 +26,7 @@ export class CallWebhookJob {
private readonly logger = new Logger(CallWebhookJob.name);
constructor(
private readonly httpService: HttpService,
private readonly analyticsService: AnalyticsService,
private readonly auditService: AuditService,
) {}
private generateSignature(
@ -47,7 +47,7 @@ export class CallWebhookJob {
webhookId: data.webhookId,
eventName: data.eventName,
};
const analytics = this.analyticsService.createAnalyticsContext({
const analytics = this.auditService.createContext({
workspaceId: data.workspaceId,
});

View File

@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { CallWebhookJobsJob } from 'src/modules/webhook/jobs/call-webhook-jobs.job';
import { CallWebhookJob } from 'src/modules/webhook/jobs/call-webhook.job';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
@Module({
imports: [HttpModule, AnalyticsModule],
imports: [HttpModule, AuditModule],
providers: [CallWebhookJobsJob, CallWebhookJob],
})
export class WebhookJobModule {}

View File

@ -5,7 +5,6 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WEBHOOK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
@ -21,7 +20,6 @@ import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync
icon: STANDARD_OBJECT_ICONS.webhook,
labelIdentifierStandardId: WEBHOOK_STANDARD_FIELD_IDS.targetUrl,
})
@WorkspaceIsNotAuditLogged()
@WorkspaceIsSystem()
export class WebhookWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({

View File

@ -9,6 +9,7 @@ import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/compos
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
@ -54,6 +55,7 @@ export type WorkflowRunOutput = {
labelIdentifierStandardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.name,
icon: STANDARD_OBJECT_ICONS.workflowRun,
})
@WorkspaceIsNotAuditLogged()
export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.name,

View File

@ -15,7 +15,6 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSearchable } from 'src/engine/twenty-orm/decorators/workspace-is-searchable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@ -35,7 +34,6 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity';
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
export enum WorkspaceMemberDateFormatEnum {
@ -81,7 +79,6 @@ export const SEARCH_FIELDS_FOR_WORKSPACE_MEMBER: FieldTypeAndNameMetadata[] = [
imageIdentifierStandardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.avatarUrl,
})
@WorkspaceIsSystem()
@WorkspaceIsNotAuditLogged()
@WorkspaceIsSearchable()
export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
@ -347,19 +344,6 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsSystem()
timelineActivities: Relation<TimelineActivityWorkspaceEntity[]>;
@WorkspaceRelation({
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.auditLogs,
type: RelationType.ONE_TO_MANY,
label: msg`Audit Logs`,
description: msg`Audit Logs linked to the workspace member`,
icon: 'IconTimelineEvent',
inverseSideTarget: () => AuditLogWorkspaceEntity,
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
auditLogs: Relation<AuditLogWorkspaceEntity[]>;
@WorkspaceField({
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.searchVector,
type: FieldMetadataType.TS_VECTOR,

View File

@ -1,30 +1,30 @@
import process from 'process';
import { ClickHouseClient, createClient } from '@clickhouse/client';
import request from 'supertest';
import { createClient, ClickHouseClient } from '@clickhouse/client';
import { GenericTrackEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-created';
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
import { GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
describe('ClickHouse Event Registration (integration)', () => {
let clickhouseClient: ClickHouseClient;
let clickHouseClient: ClickHouseClient;
beforeAll(async () => {
jest.useRealTimers();
clickhouseClient = createClient({
clickHouseClient = createClient({
url: process.env.CLICKHOUSE_URL,
});
await clickhouseClient.query({
query: 'TRUNCATE TABLE events',
await clickHouseClient.query({
query: 'TRUNCATE TABLE auditEvent',
format: 'JSONEachRow',
});
});
afterAll(async () => {
if (clickhouseClient) {
await clickhouseClient.close();
if (clickHouseClient) {
await clickHouseClient.close();
}
});
@ -53,10 +53,10 @@ describe('ClickHouse Event Registration (integration)', () => {
expect(response.status).toBe(200);
expect(response.body.data.trackAnalytics.success).toBe(true);
const queryResult = await clickhouseClient.query({
const queryResult = await clickHouseClient.query({
query: `
SELECT *
FROM events
FROM auditEvent
WHERE event = '${OBJECT_RECORD_CREATED_EVENT}' AND timestamp >= now() - INTERVAL 1 SECOND
`,

View File

@ -1,65 +0,0 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('auditLogsResolver (e2e)', () => {
it('should find many auditLogs', () => {
const queryData = {
query: `
query auditLogs {
auditLogs {
edges {
node {
name
properties
context
objectName
objectMetadataId
recordId
id
createdAt
updatedAt
deletedAt
workspaceMemberId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.auditLogs;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const auditLogs = edges[0].node;
expect(auditLogs).toHaveProperty('name');
expect(auditLogs).toHaveProperty('properties');
expect(auditLogs).toHaveProperty('context');
expect(auditLogs).toHaveProperty('objectName');
expect(auditLogs).toHaveProperty('objectMetadataId');
expect(auditLogs).toHaveProperty('recordId');
expect(auditLogs).toHaveProperty('id');
expect(auditLogs).toHaveProperty('createdAt');
expect(auditLogs).toHaveProperty('updatedAt');
expect(auditLogs).toHaveProperty('deletedAt');
expect(auditLogs).toHaveProperty('workspaceMemberId');
}
});
});
});

Some files were not shown because too many files have changed in this diff Show More