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:
@ -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 {}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export type AnalyticsCommonPropertiesType = 'timestamp' | 'version';
|
||||
export type IdentifierType = 'workspaceId' | 'userId';
|
||||
@ -1,54 +0,0 @@
|
||||
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';
|
||||
|
||||
export const fixtures: Array<GenericTrackEvent> = [
|
||||
{
|
||||
type: 'track',
|
||||
event: CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
||||
timestamp: '2024-10-24T15:55:35.177',
|
||||
version: '1',
|
||||
userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
type: 'track',
|
||||
event: CUSTOM_DOMAIN_DEACTIVATED_EVENT,
|
||||
timestamp: '2024-10-24T15:55:35.177',
|
||||
version: '1',
|
||||
userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
type: 'track',
|
||||
event: OBJECT_RECORD_CREATED_EVENT,
|
||||
timestamp: '2024-10-24T15:55:35.177',
|
||||
version: '1',
|
||||
userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
type: 'track',
|
||||
event: OBJECT_RECORD_UPDATED_EVENT,
|
||||
timestamp: '2024-10-24T15:55:35.177',
|
||||
version: '1',
|
||||
userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
type: 'track',
|
||||
event: OBJECT_RECORD_DELETED_EVENT,
|
||||
timestamp: '2024-10-24T15:55:35.177',
|
||||
version: '1',
|
||||
userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
properties: {},
|
||||
},
|
||||
];
|
||||
@ -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.
|
||||
|
||||
@ -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',
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
});
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
@ -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 {}
|
||||
@ -0,0 +1,50 @@
|
||||
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 { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Processor(MessageQueue.entityEventsToDbQueue)
|
||||
export class CreateAuditLogFromInternalEvent {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
||||
private readonly workspaceMemberService: WorkspaceMemberRepository,
|
||||
private readonly auditService: AuditService,
|
||||
) {}
|
||||
|
||||
@Process(CreateAuditLogFromInternalEvent.name)
|
||||
async handle(
|
||||
workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>,
|
||||
): Promise<void> {
|
||||
for (const eventData of workspaceEventBatch.events) {
|
||||
// We remove "before" and "after" property for a cleaner/slimmer event payload
|
||||
const eventProperties =
|
||||
'diff' in eventData.properties
|
||||
? {
|
||||
...eventData.properties,
|
||||
diff: eventData.properties.diff,
|
||||
}
|
||||
: eventData.properties;
|
||||
|
||||
const analytics = this.auditService.createContext({
|
||||
workspaceId: workspaceEventBatch.workspaceId,
|
||||
userId: eventData.userId,
|
||||
});
|
||||
|
||||
if (workspaceEventBatch.name.endsWith('.updated')) {
|
||||
analytics.track(OBJECT_RECORD_UPDATED_EVENT, eventProperties);
|
||||
} else if (workspaceEventBatch.name.endsWith('.created')) {
|
||||
analytics.track(OBJECT_RECORD_CREATED_EVENT, eventProperties);
|
||||
} else if (workspaceEventBatch.name.endsWith('.deleted')) {
|
||||
analytics.track(OBJECT_RECORD_DELETED_EVENT, eventProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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', {});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export type AuditCommonPropertiesType = 'timestamp' | 'version';
|
||||
export type IdentifierType = 'workspaceId' | 'userId';
|
||||
@ -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 =
|
||||
@ -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',
|
||||
});
|
||||
@ -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'),
|
||||
@ -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
|
||||
@ -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;
|
||||
@ -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
|
||||
@ -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({
|
||||
@ -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({
|
||||
@ -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({
|
||||
@ -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;
|
||||
@ -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'),
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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;
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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(),
|
||||
},
|
||||
},
|
||||
...[
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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],
|
||||
|
||||
Reference in New Issue
Block a user