Update clickhouse tables (#11905)
Following a discussion with @Bonapara - changing the base tables
This commit is contained in:
@ -27,7 +27,7 @@
|
|||||||
"outDir": "dist/assets"
|
"outDir": "dist/assets"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": "**/database/clickhouse/migrations/*.sql",
|
"include": "**/database/clickHouse/migrations/*.sql",
|
||||||
"outDir": "dist/src"
|
"outDir": "dist/src"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
"worker:prod": "node dist/src/queue-worker/queue-worker",
|
"worker:prod": "node dist/src/queue-worker/queue-worker",
|
||||||
"database:init:prod": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate:prod",
|
"database:init:prod": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate:prod",
|
||||||
"database:migrate:prod": "npx -y typeorm migration:run -d dist/src/database/typeorm/metadata/metadata.datasource && npx -y typeorm migration:run -d dist/src/database/typeorm/core/core.datasource",
|
"database:migrate:prod": "npx -y typeorm migration:run -d dist/src/database/typeorm/metadata/metadata.datasource && npx -y typeorm migration:run -d dist/src/database/typeorm/core/core.datasource",
|
||||||
"clickhouse:migrate:prod": "node dist/src/database/clickhouse/migrations/run-migrations.js",
|
"clickhouse:migrate:prod": "node dist/src/database/clickHouse/migrations/run-migrations.js",
|
||||||
"typeorm": "../../node_modules/typeorm/.bin/typeorm"
|
"typeorm": "../../node_modules/typeorm/.bin/typeorm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS auditEvent
|
|
||||||
(
|
|
||||||
`event` LowCardinality(String),
|
|
||||||
`timestamp` DateTime64(3),
|
|
||||||
`userId` String DEFAULT '',
|
|
||||||
`workspaceId` String DEFAULT '',
|
|
||||||
`properties` JSON
|
|
||||||
)
|
|
||||||
ENGINE = MergeTree
|
|
||||||
ORDER BY (event, workspaceId, userId, timestamp);
|
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS workspaceEvent
|
||||||
|
(
|
||||||
|
`event` LowCardinality(String) NOT NULL,
|
||||||
|
`timestamp` DateTime64(3) NOT NULL,
|
||||||
|
`userId` String DEFAULT '',
|
||||||
|
`workspaceId` String NOT NULL,
|
||||||
|
`properties` JSON
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree
|
||||||
|
ORDER BY (workspaceId, event, userId, timestamp);
|
||||||
@ -1,10 +1,10 @@
|
|||||||
CREATE TABLE IF NOT EXISTS pageview
|
CREATE TABLE IF NOT EXISTS pageview
|
||||||
(
|
(
|
||||||
`name` LowCardinality(String),
|
`name` LowCardinality(String) NOT NULL,
|
||||||
`timestamp` DateTime64(3),
|
`timestamp` DateTime64(3) NOT NULL,
|
||||||
`properties` JSON,
|
|
||||||
`userId` String DEFAULT '',
|
`userId` String DEFAULT '',
|
||||||
`workspaceId` String DEFAULT ''
|
`workspaceId` String DEFAULT '',
|
||||||
|
`properties` JSON
|
||||||
)
|
)
|
||||||
ENGINE = MergeTree
|
ENGINE = MergeTree
|
||||||
ORDER BY (name, workspaceId, userId, timestamp);
|
ORDER BY (workspaceId, name, userId, timestamp);
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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);
|
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS objectEvent
|
||||||
|
(
|
||||||
|
`event` LowCardinality(String) NOT NULL,
|
||||||
|
`timestamp` DateTime64(3) NOT NULL,
|
||||||
|
`userId` String DEFAULT '',
|
||||||
|
`workspaceId` String NOT NULL,
|
||||||
|
`recordId` String NOT NULL,
|
||||||
|
`objectMetadataId` String NOT NULL,
|
||||||
|
`properties` JSON,
|
||||||
|
`isCustom` Boolean DEFAULT FALSE,
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree
|
||||||
|
ORDER BY (workspaceId, event, userId, timestamp);
|
||||||
@ -10,7 +10,7 @@ config({
|
|||||||
override: true,
|
override: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const clickhouseUrl = () => {
|
const clickHouseUrl = () => {
|
||||||
const url = process.env.CLICKHOUSE_URL;
|
const url = process.env.CLICKHOUSE_URL;
|
||||||
|
|
||||||
if (url) return url;
|
if (url) return url;
|
||||||
@ -21,7 +21,7 @@ const clickhouseUrl = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function ensureDatabaseExists() {
|
async function ensureDatabaseExists() {
|
||||||
const [url, database] = clickhouseUrl().split(/\/(?=[^/]*$)/);
|
const [url, database] = clickHouseUrl().split(/\/(?=[^/]*$)/);
|
||||||
const client = createClient({
|
const client = createClient({
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
@ -74,7 +74,7 @@ async function runMigrations() {
|
|||||||
await ensureDatabaseExists();
|
await ensureDatabaseExists();
|
||||||
|
|
||||||
const client = createClient({
|
const client = createClient({
|
||||||
url: clickhouseUrl(),
|
url: clickHouseUrl(),
|
||||||
clickhouse_settings: {
|
clickhouse_settings: {
|
||||||
allow_experimental_json_type: 1,
|
allow_experimental_json_type: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
|
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-created';
|
||||||
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-deactivated';
|
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-delete';
|
||||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
|
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-updated';
|
||||||
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-delete';
|
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
|
||||||
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-updated';
|
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated';
|
||||||
import { GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const fixtures: Array<GenericTrackEvent> = [
|
export const fixtures: Array<GenericTrackEvent> = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -18,7 +18,7 @@ async function seedEvents() {
|
|||||||
console.log(`⚡ Seeding ${fixtures.length} events...`);
|
console.log(`⚡ Seeding ${fixtures.length} events...`);
|
||||||
|
|
||||||
await client.insert({
|
await client.insert({
|
||||||
table: 'auditEvent',
|
table: 'workspaceEvent',
|
||||||
values: fixtures,
|
values: fixtures,
|
||||||
format: 'JSONEachRow',
|
format: 'JSONEachRow',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
|
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 { 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 { 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 { USER_SIGNUP_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/user/user-signup';
|
||||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
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 { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service';
|
||||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
@ -26,7 +26,7 @@ export class TelemetryListener {
|
|||||||
userId: eventPayload.userId,
|
userId: eventPayload.userId,
|
||||||
workspaceId: payload.workspaceId,
|
workspaceId: payload.workspaceId,
|
||||||
})
|
})
|
||||||
.track(USER_SIGNUP_EVENT, {});
|
.insertWorkspaceEvent(USER_SIGNUP_EVENT, {});
|
||||||
|
|
||||||
this.telemetryService.create(
|
this.telemetryService.create(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6,7 +6,11 @@ This module provides analytics tracking functionality for the Twenty application
|
|||||||
|
|
||||||
### Tracking Events
|
### Tracking Events
|
||||||
|
|
||||||
The `AuditService` provides a `createContext` 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 three methods:
|
||||||
|
|
||||||
|
- `insertWorkspaceEvent`: For tracking workspace-level events
|
||||||
|
- `createObjectEvent`: For tracking object-level events that include record and metadata IDs
|
||||||
|
- `createPageviewEvent`: For tracking page views
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
@ -24,10 +28,22 @@ export class MyService {
|
|||||||
userId: 'user-id',
|
userId: 'user-id',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track an event
|
// Track a workspace event
|
||||||
// The event name will be autocompleted
|
analytics.insertWorkspaceEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
||||||
// The properties will be type-checked based on the event name
|
|
||||||
analytics.track(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
// Track an object event
|
||||||
|
analytics.createObjectEvent(OBJECT_RECORD_CREATED_EVENT, {
|
||||||
|
recordId: 'record-id',
|
||||||
|
objectMetadataId: 'object-metadata-id',
|
||||||
|
// other properties
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track a pageview
|
||||||
|
analytics.createPageviewEvent('page-name', {
|
||||||
|
href: '/path',
|
||||||
|
locale: 'en-US',
|
||||||
|
// other properties
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -97,8 +113,9 @@ Creates an analytics context with the given user ID and workspace ID.
|
|||||||
|
|
||||||
Returns an object with the following methods:
|
Returns an object with the following methods:
|
||||||
|
|
||||||
- `track<T extends TrackEventName>(event: T, properties: TrackEventProperties<T>)`: Tracks an event with the given name and properties
|
- `insertWorkspaceEvent<T extends TrackEventName>(event: T, properties: TrackEventProperties<T>)`: Tracks a workspace-level event
|
||||||
- `pageview(name: string, properties: Partial<PageviewProperties>)`: Tracks a pageview with the given name and properties
|
- `createObjectEvent<T extends TrackEventName>(event: T, properties: TrackEventProperties<T> & { recordId: string; objectMetadataId: string })`: Tracks an object-level event
|
||||||
|
- `createPageviewEvent(name: string, properties: Partial<PageviewProperties>)`: Tracks a pageview
|
||||||
|
|
||||||
### Types
|
### Types
|
||||||
|
|
||||||
@ -128,16 +145,4 @@ This approach makes it easier to add new events without having to modify a compl
|
|||||||
|
|
||||||
#### PageviewProperties
|
#### PageviewProperties
|
||||||
|
|
||||||
A type that defines the structure of pageview properties:
|
Properties for pageview events, including href, locale, pathname, referrer, sessionId, timeZone, and userAgent.
|
||||||
|
|
||||||
```typescript
|
|
||||||
type PageviewProperties = {
|
|
||||||
href: string;
|
|
||||||
locale: string;
|
|
||||||
pathname: string;
|
|
||||||
referrer: string;
|
|
||||||
sessionId: string;
|
|
||||||
timeZone: string;
|
|
||||||
userAgent: string;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|||||||
@ -38,11 +38,14 @@ describe('AuditResolver', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a valid pageview input', async () => {
|
it('should handle a valid pageview input', async () => {
|
||||||
const mockPageview = jest.fn().mockResolvedValue('Pageview created');
|
const mockInsertPageviewEvent = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue('Pageview created');
|
||||||
|
|
||||||
auditService.createContext.mockReturnValue({
|
auditService.createContext.mockReturnValue({
|
||||||
pageview: mockPageview,
|
createPageviewEvent: mockInsertPageviewEvent,
|
||||||
track: jest.fn(),
|
insertWorkspaceEvent: jest.fn(),
|
||||||
|
createObjectEvent: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const input = {
|
const input = {
|
||||||
@ -60,16 +63,19 @@ describe('AuditResolver', () => {
|
|||||||
workspaceId: 'workspace-1',
|
workspaceId: 'workspace-1',
|
||||||
userId: 'user-1',
|
userId: 'user-1',
|
||||||
});
|
});
|
||||||
expect(mockPageview).toHaveBeenCalledWith('Test Page', {});
|
expect(mockInsertPageviewEvent).toHaveBeenCalledWith('Test Page', {});
|
||||||
expect(result).toBe('Pageview created');
|
expect(result).toBe('Pageview created');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a valid track input', async () => {
|
it('should handle a valid track input', async () => {
|
||||||
const mockTrack = jest.fn().mockResolvedValue('Track created');
|
const mockInsertWorkspaceEvent = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue('Track created');
|
||||||
|
|
||||||
auditService.createContext.mockReturnValue({
|
auditService.createContext.mockReturnValue({
|
||||||
track: mockTrack,
|
insertWorkspaceEvent: mockInsertWorkspaceEvent,
|
||||||
pageview: jest.fn(),
|
createObjectEvent: jest.fn(),
|
||||||
|
createPageviewEvent: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const input = {
|
const input = {
|
||||||
@ -87,10 +93,54 @@ describe('AuditResolver', () => {
|
|||||||
workspaceId: 'workspace-2',
|
workspaceId: 'workspace-2',
|
||||||
userId: 'user-2',
|
userId: 'user-2',
|
||||||
});
|
});
|
||||||
expect(mockTrack).toHaveBeenCalledWith('Custom Domain Activated', {});
|
expect(mockInsertWorkspaceEvent).toHaveBeenCalledWith(
|
||||||
|
'Custom Domain Activated',
|
||||||
|
{},
|
||||||
|
);
|
||||||
expect(result).toBe('Track created');
|
expect(result).toBe('Track created');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle object event creation', async () => {
|
||||||
|
const mockInsertObjectEvent = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue('Object event created');
|
||||||
|
|
||||||
|
auditService.createContext.mockReturnValue({
|
||||||
|
insertWorkspaceEvent: jest.fn(),
|
||||||
|
createObjectEvent: mockInsertObjectEvent,
|
||||||
|
createPageviewEvent: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
event: 'Object Record Created' as const,
|
||||||
|
recordId: 'test-record-id',
|
||||||
|
objectMetadataId: 'test-object-metadata-id',
|
||||||
|
properties: { additionalData: 'test-data' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await resolver.createObjectEvent(
|
||||||
|
input,
|
||||||
|
{ id: 'workspace-3' } as Workspace,
|
||||||
|
{ id: 'user-3' } as User,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(auditService.createContext).toHaveBeenCalledWith({
|
||||||
|
workspaceId: 'workspace-3',
|
||||||
|
userId: 'user-3',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockInsertObjectEvent).toHaveBeenCalledWith(
|
||||||
|
'Object Record Created',
|
||||||
|
{
|
||||||
|
additionalData: 'test-data',
|
||||||
|
recordId: 'test-record-id',
|
||||||
|
objectMetadataId: 'test-object-metadata-id',
|
||||||
|
isCustom: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(result).toBe('Object event created');
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw an AuditException for invalid input', async () => {
|
it('should throw an AuditException for invalid input', async () => {
|
||||||
const invalidInput = { type: 'invalid' };
|
const invalidInput = { type: 'invalid' };
|
||||||
|
|
||||||
@ -103,4 +153,18 @@ describe('AuditResolver', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an AuditException when workspace is missing for createObjectEvent', async () => {
|
||||||
|
const input = {
|
||||||
|
event: 'Object Record Created' as const,
|
||||||
|
recordId: 'test-record-id',
|
||||||
|
objectMetadataId: 'test-object-metadata-id',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolver.createObjectEvent(input, undefined, undefined),
|
||||||
|
).rejects.toThrowError(
|
||||||
|
new AuditException('Missing workspace', AuditExceptionCode.INVALID_INPUT),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
AuditException,
|
AuditException,
|
||||||
AuditExceptionCode,
|
AuditExceptionCode,
|
||||||
} from 'src/engine/core-modules/audit/audit.exception';
|
} from 'src/engine/core-modules/audit/audit.exception';
|
||||||
|
import { CreateObjectEventInput } from 'src/engine/core-modules/audit/dtos/create-object-event.input';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||||
@ -22,7 +23,7 @@ export class AuditResolver {
|
|||||||
constructor(private readonly auditService: AuditService) {}
|
constructor(private readonly auditService: AuditService) {}
|
||||||
|
|
||||||
// preparing for new name
|
// preparing for new name
|
||||||
async auditTrack(
|
async createPageview(
|
||||||
@Args()
|
@Args()
|
||||||
createAnalyticsInput: CreateAnalyticsInputV2,
|
createAnalyticsInput: CreateAnalyticsInputV2,
|
||||||
@AuthWorkspace() workspace: Workspace | undefined,
|
@AuthWorkspace() workspace: Workspace | undefined,
|
||||||
@ -31,6 +32,33 @@ export class AuditResolver {
|
|||||||
return this.trackAnalytics(createAnalyticsInput, workspace, user);
|
return this.trackAnalytics(createAnalyticsInput, workspace, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Analytics)
|
||||||
|
async createObjectEvent(
|
||||||
|
@Args()
|
||||||
|
createObjectEventInput: CreateObjectEventInput,
|
||||||
|
@AuthWorkspace() workspace: Workspace | undefined,
|
||||||
|
@AuthUser({ allowUndefined: true }) user: User | undefined,
|
||||||
|
) {
|
||||||
|
if (!workspace) {
|
||||||
|
throw new AuditException(
|
||||||
|
'Missing workspace',
|
||||||
|
AuditExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyticsContext = this.auditService.createContext({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
userId: user?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return analyticsContext.createObjectEvent(createObjectEventInput.event, {
|
||||||
|
...createObjectEventInput.properties,
|
||||||
|
recordId: createObjectEventInput.recordId,
|
||||||
|
objectMetadataId: createObjectEventInput.objectMetadataId,
|
||||||
|
isCustom: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation(() => Analytics)
|
@Mutation(() => Analytics)
|
||||||
async trackAnalytics(
|
async trackAnalytics(
|
||||||
@Args()
|
@Args()
|
||||||
@ -44,14 +72,16 @@ export class AuditResolver {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isPageviewAnalyticsInput(createAnalyticsInput)) {
|
if (isPageviewAnalyticsInput(createAnalyticsInput)) {
|
||||||
return analyticsContext.pageview(
|
return analyticsContext.createPageviewEvent(
|
||||||
createAnalyticsInput.name,
|
createAnalyticsInput.name,
|
||||||
createAnalyticsInput.properties ?? {},
|
createAnalyticsInput.properties ?? {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTrackAnalyticsInput(createAnalyticsInput)) {
|
if (isTrackAnalyticsInput(createAnalyticsInput)) {
|
||||||
return analyticsContext.track(
|
// For track events, we need to determine if it's a workspace or object event
|
||||||
|
// Since we don't have recordId and objectMetadataId in the input, we use insertWorkspaceEvent
|
||||||
|
return analyticsContext.insertWorkspaceEvent(
|
||||||
createAnalyticsInput.event,
|
createAnalyticsInput.event,
|
||||||
createAnalyticsInput.properties ?? {},
|
createAnalyticsInput.properties ?? {},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { ArgsType, Field } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
|
||||||
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
|
||||||
|
import { TrackEventName } from 'src/engine/core-modules/audit/types/events.type';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class CreateObjectEventInput {
|
||||||
|
@Field(() => String)
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
event: TrackEventName;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
recordId: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
objectMetadataId: string;
|
||||||
|
|
||||||
|
@Field(() => GraphQLJSON, { nullable: true })
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
properties?: Record<string, any>;
|
||||||
|
}
|
||||||
@ -1,23 +1,16 @@
|
|||||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
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_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/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_DELETED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-delete';
|
||||||
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-updated';
|
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-updated';
|
||||||
import { ObjectRecordEvent } from 'src/engine/core-modules/event-emitter/types/object-record-event.event';
|
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 { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.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 { 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 { 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)
|
@Processor(MessageQueue.entityEventsToDbQueue)
|
||||||
export class CreateAuditLogFromInternalEvent {
|
export class CreateAuditLogFromInternalEvent {
|
||||||
constructor(
|
constructor(private readonly auditService: AuditService) {}
|
||||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
|
||||||
private readonly workspaceMemberService: WorkspaceMemberRepository,
|
|
||||||
private readonly auditService: AuditService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Process(CreateAuditLogFromInternalEvent.name)
|
@Process(CreateAuditLogFromInternalEvent.name)
|
||||||
async handle(
|
async handle(
|
||||||
@ -38,12 +31,25 @@ export class CreateAuditLogFromInternalEvent {
|
|||||||
userId: eventData.userId,
|
userId: eventData.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Since these are object record events, we use createObjectEvent
|
||||||
if (workspaceEventBatch.name.endsWith('.updated')) {
|
if (workspaceEventBatch.name.endsWith('.updated')) {
|
||||||
analytics.track(OBJECT_RECORD_UPDATED_EVENT, eventProperties);
|
analytics.createObjectEvent(OBJECT_RECORD_UPDATED_EVENT, {
|
||||||
|
...eventProperties,
|
||||||
|
recordId: eventData.recordId,
|
||||||
|
objectMetadataId: eventData.objectMetadata.id,
|
||||||
|
});
|
||||||
} else if (workspaceEventBatch.name.endsWith('.created')) {
|
} else if (workspaceEventBatch.name.endsWith('.created')) {
|
||||||
analytics.track(OBJECT_RECORD_CREATED_EVENT, eventProperties);
|
analytics.createObjectEvent(OBJECT_RECORD_CREATED_EVENT, {
|
||||||
|
...eventProperties,
|
||||||
|
recordId: eventData.recordId,
|
||||||
|
objectMetadataId: eventData.objectMetadata.id,
|
||||||
|
});
|
||||||
} else if (workspaceEventBatch.name.endsWith('.deleted')) {
|
} else if (workspaceEventBatch.name.endsWith('.deleted')) {
|
||||||
analytics.track(OBJECT_RECORD_DELETED_EVENT, eventProperties);
|
analytics.createObjectEvent(OBJECT_RECORD_DELETED_EVENT, {
|
||||||
|
...eventProperties,
|
||||||
|
recordId: eventData.recordId,
|
||||||
|
objectMetadataId: eventData.objectMetadata.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
import { AuditContextMock } from 'test/utils/audit-context.mock';
|
import { AuditContextMock } from 'test/utils/audit-context.mock';
|
||||||
|
|
||||||
import { ClickHouseService } from 'src/database/clickHouse/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 { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
|
||||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
|
||||||
@ -58,29 +58,37 @@ describe('AuditService', () => {
|
|||||||
it('should create a valid context object', () => {
|
it('should create a valid context object', () => {
|
||||||
const context = service.createContext(mockUserIdAndWorkspaceId);
|
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||||
|
|
||||||
expect(context).toHaveProperty('track');
|
expect(context).toHaveProperty('insertWorkspaceEvent');
|
||||||
expect(context).toHaveProperty('pageview');
|
expect(context).toHaveProperty('createObjectEvent');
|
||||||
|
expect(context).toHaveProperty('createPageviewEvent');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call track with correct parameters', async () => {
|
it('should call insertWorkspaceEvent with correct parameters', async () => {
|
||||||
const trackSpy = jest.fn().mockResolvedValue({ success: true });
|
const insertWorkspaceEventSpy = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ success: true });
|
||||||
const mockContext = AuditContextMock({
|
const mockContext = AuditContextMock({
|
||||||
track: trackSpy,
|
insertWorkspaceEvent: insertWorkspaceEventSpy,
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(service, 'createContext').mockReturnValue(mockContext);
|
jest.spyOn(service, 'createContext').mockReturnValue(mockContext);
|
||||||
|
|
||||||
const context = service.createContext(mockUserIdAndWorkspaceId);
|
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||||
|
|
||||||
await context.track(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
await context.insertWorkspaceEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
||||||
|
|
||||||
expect(trackSpy).toHaveBeenCalledWith(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
expect(insertWorkspaceEventSpy).toHaveBeenCalledWith(
|
||||||
|
CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
||||||
|
{},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call pageview with correct parameters', async () => {
|
it('should call createPageviewEvent with correct parameters', async () => {
|
||||||
const pageviewSpy = jest.fn().mockResolvedValue({ success: true });
|
const createPageviewEventSpy = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ success: true });
|
||||||
const mockContext = AuditContextMock({
|
const mockContext = AuditContextMock({
|
||||||
pageview: pageviewSpy,
|
createPageviewEvent: createPageviewEventSpy,
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(service, 'createContext').mockReturnValue(mockContext);
|
jest.spyOn(service, 'createContext').mockReturnValue(mockContext);
|
||||||
@ -96,26 +104,43 @@ describe('AuditService', () => {
|
|||||||
userAgent: '',
|
userAgent: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
await context.pageview('page-view', testPageviewProperties);
|
await context.createPageviewEvent('page-view', testPageviewProperties);
|
||||||
|
|
||||||
expect(pageviewSpy).toHaveBeenCalledWith(
|
expect(createPageviewEventSpy).toHaveBeenCalledWith(
|
||||||
'page-view',
|
'page-view',
|
||||||
testPageviewProperties,
|
testPageviewProperties,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return success when track is called', async () => {
|
it('should return success when insertWorkspaceEvent is called', async () => {
|
||||||
const context = service.createContext(mockUserIdAndWorkspaceId);
|
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||||
|
|
||||||
const result = await context.track(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
const result = await context.insertWorkspaceEvent(
|
||||||
|
CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
expect(result).toEqual({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return success when pageview is called', async () => {
|
it('should return success when createPageviewEvent is called', async () => {
|
||||||
const context = service.createContext(mockUserIdAndWorkspaceId);
|
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||||
|
|
||||||
const result = await context.pageview('page-view', {});
|
const result = await context.createPageviewEvent('page-view', {});
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success when createObjectEvent is called', async () => {
|
||||||
|
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||||
|
|
||||||
|
const result = await context.createObjectEvent(
|
||||||
|
CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
||||||
|
{
|
||||||
|
recordId: 'test-record-id',
|
||||||
|
objectMetadataId: 'test-object-metadata-id',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
expect(result).toEqual({ success: true });
|
||||||
});
|
});
|
||||||
|
|||||||
@ -35,16 +35,31 @@ export class AuditService {
|
|||||||
: {};
|
: {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
track: <T extends TrackEventName>(
|
insertWorkspaceEvent: <T extends TrackEventName>(
|
||||||
event: T,
|
event: T,
|
||||||
properties: TrackEventProperties<T>,
|
properties: TrackEventProperties<T>,
|
||||||
) =>
|
) =>
|
||||||
this.preventIfDisabled(() =>
|
this.preventIfDisabled(() =>
|
||||||
this.clickHouseService.insert('auditEvent', [
|
this.clickHouseService.insert('workspaceEvent', [
|
||||||
{ ...userIdAndWorkspaceId, ...makeTrackEvent(event, properties) },
|
{ ...userIdAndWorkspaceId, ...makeTrackEvent(event, properties) },
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
pageview: (name: string, properties: Partial<PageviewProperties>) =>
|
createObjectEvent: <T extends TrackEventName>(
|
||||||
|
event: T,
|
||||||
|
properties: TrackEventProperties<T> & {
|
||||||
|
recordId: string;
|
||||||
|
objectMetadataId: string;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
this.preventIfDisabled(() =>
|
||||||
|
this.clickHouseService.insert('objectEvent', [
|
||||||
|
{ ...userIdAndWorkspaceId, ...makeTrackEvent(event, properties) },
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
createPageviewEvent: (
|
||||||
|
name: string,
|
||||||
|
properties: Partial<PageviewProperties>,
|
||||||
|
) =>
|
||||||
this.preventIfDisabled(() =>
|
this.preventIfDisabled(() =>
|
||||||
this.clickHouseService.insert('pageview', [
|
this.clickHouseService.insert('pageview', [
|
||||||
{ ...userIdAndWorkspaceId, ...makePageview(name, properties) },
|
{ ...userIdAndWorkspaceId, ...makePageview(name, properties) },
|
||||||
|
|||||||
@ -1,43 +1,43 @@
|
|||||||
import {
|
|
||||||
CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
|
||||||
CustomDomainActivatedTrackEvent,
|
|
||||||
} from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
|
|
||||||
import {
|
|
||||||
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/audit/utils/events/track/monitoring/monitoring';
|
|
||||||
import {
|
import {
|
||||||
OBJECT_RECORD_CREATED_EVENT,
|
OBJECT_RECORD_CREATED_EVENT,
|
||||||
ObjectRecordCreatedTrackEvent,
|
ObjectRecordCreatedTrackEvent,
|
||||||
} from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
|
} from 'src/engine/core-modules/audit/utils/events/object-event/object-record-created';
|
||||||
import {
|
import {
|
||||||
OBJECT_RECORD_DELETED_EVENT,
|
OBJECT_RECORD_DELETED_EVENT,
|
||||||
ObjectRecordDeletedTrackEvent,
|
ObjectRecordDeletedTrackEvent,
|
||||||
} from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-delete';
|
} from 'src/engine/core-modules/audit/utils/events/object-event/object-record-delete';
|
||||||
import {
|
import {
|
||||||
OBJECT_RECORD_UPDATED_EVENT,
|
OBJECT_RECORD_UPDATED_EVENT,
|
||||||
ObjectRecordUpdatedTrackEvent,
|
ObjectRecordUpdatedTrackEvent,
|
||||||
} from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-updated';
|
} from 'src/engine/core-modules/audit/utils/events/object-event/object-record-updated';
|
||||||
|
import {
|
||||||
|
CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
||||||
|
CustomDomainActivatedTrackEvent,
|
||||||
|
} from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
|
||||||
|
import {
|
||||||
|
CUSTOM_DOMAIN_DEACTIVATED_EVENT,
|
||||||
|
CustomDomainDeactivatedTrackEvent,
|
||||||
|
} from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated';
|
||||||
|
import {
|
||||||
|
MONITORING_EVENT,
|
||||||
|
MonitoringTrackEvent,
|
||||||
|
} from 'src/engine/core-modules/audit/utils/events/workspace-event/monitoring/monitoring';
|
||||||
import {
|
import {
|
||||||
SERVERLESS_FUNCTION_EXECUTED_EVENT,
|
SERVERLESS_FUNCTION_EXECUTED_EVENT,
|
||||||
ServerlessFunctionExecutedTrackEvent,
|
ServerlessFunctionExecutedTrackEvent,
|
||||||
} from 'src/engine/core-modules/audit/utils/events/track/serverless-function/serverless-function-executed';
|
} from 'src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed';
|
||||||
import {
|
import {
|
||||||
USER_SIGNUP_EVENT,
|
USER_SIGNUP_EVENT,
|
||||||
UserSignupTrackEvent,
|
UserSignupTrackEvent,
|
||||||
} from 'src/engine/core-modules/audit/utils/events/track/user/user-signup';
|
} from 'src/engine/core-modules/audit/utils/events/workspace-event/user/user-signup';
|
||||||
import {
|
import {
|
||||||
WEBHOOK_RESPONSE_EVENT,
|
WEBHOOK_RESPONSE_EVENT,
|
||||||
WebhookResponseTrackEvent,
|
WebhookResponseTrackEvent,
|
||||||
} from 'src/engine/core-modules/audit/utils/events/track/webhook/webhook-response';
|
} from 'src/engine/core-modules/audit/utils/events/workspace-event/webhook/webhook-response';
|
||||||
import {
|
import {
|
||||||
WORKSPACE_ENTITY_CREATED_EVENT,
|
WORKSPACE_ENTITY_CREATED_EVENT,
|
||||||
WorkspaceEntityCreatedTrackEvent,
|
WorkspaceEntityCreatedTrackEvent,
|
||||||
} from 'src/engine/core-modules/audit/utils/events/track/workspace-entity/workspace-entity-created';
|
} from 'src/engine/core-modules/audit/utils/events/workspace-event/workspace-entity/workspace-entity-created';
|
||||||
|
|
||||||
// Define all track event names
|
// Define all track event names
|
||||||
export type TrackEventName =
|
export type TrackEventName =
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
eventsRegistry,
|
eventsRegistry,
|
||||||
GenericTrackEvent,
|
GenericTrackEvent,
|
||||||
} from 'src/engine/core-modules/audit/utils/events/track/track';
|
} from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
const common = (): Record<AuditCommonPropertiesType, string> => ({
|
const common = (): Record<AuditCommonPropertiesType, string> => ({
|
||||||
timestamp: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
timestamp: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const OBJECT_RECORD_CREATED_EVENT = 'Object Record Created' as const;
|
export const OBJECT_RECORD_CREATED_EVENT = 'Object Record Created' as const;
|
||||||
export const objectRecordCreatedSchema = z.object({
|
export const objectRecordCreatedSchema = z.object({
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const OBJECT_RECORD_DELETED_EVENT = 'Object Record Deleted' as const;
|
export const OBJECT_RECORD_DELETED_EVENT = 'Object Record Deleted' as const;
|
||||||
export const objectRecordDeletedSchema = z.object({
|
export const objectRecordDeletedSchema = z.object({
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const OBJECT_RECORD_UPDATED_EVENT = 'Object Record Updated' as const;
|
export const OBJECT_RECORD_UPDATED_EVENT = 'Object Record Updated' as const;
|
||||||
export const objectRecordUpdatedSchema = z.object({
|
export const objectRecordUpdatedSchema = z.object({
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const CUSTOM_DOMAIN_ACTIVATED_EVENT = 'Custom Domain Activated' as const;
|
export const CUSTOM_DOMAIN_ACTIVATED_EVENT = 'Custom Domain Activated' as const;
|
||||||
export const customDomainActivatedSchema = z
|
export const customDomainActivatedSchema = z
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const CUSTOM_DOMAIN_DEACTIVATED_EVENT =
|
export const CUSTOM_DOMAIN_DEACTIVATED_EVENT =
|
||||||
'Custom Domain Deactivated' as const;
|
'Custom Domain Deactivated' as const;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const MONITORING_EVENT = 'Monitoring' as const;
|
export const MONITORING_EVENT = 'Monitoring' as const;
|
||||||
export const monitoringSchema = z
|
export const monitoringSchema = z
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const SERVERLESS_FUNCTION_EXECUTED_EVENT =
|
export const SERVERLESS_FUNCTION_EXECUTED_EVENT =
|
||||||
'Serverless Function Executed' as const;
|
'Serverless Function Executed' as const;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const USER_SIGNUP_EVENT = 'User Signup' as const;
|
export const USER_SIGNUP_EVENT = 'User Signup' as const;
|
||||||
export const userSignupSchema = z
|
export const userSignupSchema = z
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const WEBHOOK_RESPONSE_EVENT = 'Webhook Response' as const;
|
export const WEBHOOK_RESPONSE_EVENT = 'Webhook Response' as const;
|
||||||
export const webhookResponseSchema = z
|
export const webhookResponseSchema = z
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
export const WORKSPACE_ENTITY_CREATED_EVENT =
|
export const WORKSPACE_ENTITY_CREATED_EVENT =
|
||||||
'Workspace Entity Created' as const;
|
'Workspace Entity Created' as const;
|
||||||
@ -14,7 +14,7 @@ import { Request, Response } from 'express';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
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_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
|
||||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||||
import {
|
import {
|
||||||
DomainManagerException,
|
DomainManagerException,
|
||||||
@ -91,7 +91,7 @@ export class CloudflareController {
|
|||||||
...workspaceUpdated,
|
...workspaceUpdated,
|
||||||
});
|
});
|
||||||
|
|
||||||
await analytics.track(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
await analytics.insertWorkspaceEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send();
|
return res.status(200).send();
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
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_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/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 { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated';
|
||||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
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 { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||||
@ -422,7 +422,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
analytics.track(
|
analytics.insertWorkspaceEvent(
|
||||||
workspace.isCustomDomainEnabled
|
workspace.isCustomDomainEnabled
|
||||||
? CUSTOM_DOMAIN_ACTIVATED_EVENT
|
? CUSTOM_DOMAIN_ACTIVATED_EVENT
|
||||||
: CUSTOM_DOMAIN_DEACTIVATED_EVENT,
|
: CUSTOM_DOMAIN_DEACTIVATED_EVENT,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/i
|
|||||||
import { ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
import { ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
||||||
|
|
||||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.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 { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed';
|
||||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
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 { 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';
|
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||||
@ -148,7 +148,7 @@ export class ServerlessFunctionService {
|
|||||||
.createContext({
|
.createContext({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
})
|
})
|
||||||
.track(SERVERLESS_FUNCTION_EXECUTED_EVENT, {
|
.insertWorkspaceEvent(SERVERLESS_FUNCTION_EXECUTED_EVENT, {
|
||||||
duration: resultServerlessFunction.duration,
|
duration: resultServerlessFunction.duration,
|
||||||
status: resultServerlessFunction.status,
|
status: resultServerlessFunction.status,
|
||||||
...(resultServerlessFunction.error && {
|
...(resultServerlessFunction.error && {
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export class MessagingMonitoringService {
|
|||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
})
|
})
|
||||||
.track(MONITORING_EVENT, {
|
.insertWorkspaceEvent(MONITORING_EVENT, {
|
||||||
eventName: `messaging.${eventName}`,
|
eventName: `messaging.${eventName}`,
|
||||||
connectedAccountId,
|
connectedAccountId,
|
||||||
messageChannelId,
|
messageChannelId,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Logger } from '@nestjs/common';
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.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 { WEBHOOK_RESPONSE_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/webhook/webhook-response';
|
||||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
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 { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||||
@ -78,13 +78,13 @@ export class CallWebhookJob {
|
|||||||
|
|
||||||
const success = response.status >= 200 && response.status < 300;
|
const success = response.status >= 200 && response.status < 300;
|
||||||
|
|
||||||
analytics.track(WEBHOOK_RESPONSE_EVENT, {
|
analytics.insertWorkspaceEvent(WEBHOOK_RESPONSE_EVENT, {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
success,
|
success,
|
||||||
...commonPayload,
|
...commonPayload,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
analytics.track(WEBHOOK_RESPONSE_EVENT, {
|
analytics.insertWorkspaceEvent(WEBHOOK_RESPONSE_EVENT, {
|
||||||
success: false,
|
success: false,
|
||||||
...commonPayload,
|
...commonPayload,
|
||||||
...(err.response && { status: err.response.status }),
|
...(err.response && { status: err.response.status }),
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import process from 'process';
|
|||||||
import { ClickHouseClient, createClient } from '@clickhouse/client';
|
import { ClickHouseClient, createClient } from '@clickhouse/client';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
|
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
|
||||||
import { GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
import { GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||||
|
|
||||||
describe('ClickHouse Event Registration (integration)', () => {
|
describe('ClickHouse Event Registration (integration)', () => {
|
||||||
let clickHouseClient: ClickHouseClient;
|
let clickHouseClient: ClickHouseClient;
|
||||||
@ -17,7 +17,7 @@ describe('ClickHouse Event Registration (integration)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await clickHouseClient.query({
|
await clickHouseClient.query({
|
||||||
query: 'TRUNCATE TABLE auditEvent',
|
query: 'TRUNCATE TABLE workspaceEvent',
|
||||||
format: 'JSONEachRow',
|
format: 'JSONEachRow',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -39,7 +39,7 @@ describe('ClickHouse Event Registration (integration)', () => {
|
|||||||
|
|
||||||
const variables = {
|
const variables = {
|
||||||
type: 'TRACK',
|
type: 'TRACK',
|
||||||
event: OBJECT_RECORD_CREATED_EVENT,
|
event: CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
||||||
properties: {},
|
properties: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,8 +56,8 @@ describe('ClickHouse Event Registration (integration)', () => {
|
|||||||
const queryResult = await clickHouseClient.query({
|
const queryResult = await clickHouseClient.query({
|
||||||
query: `
|
query: `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM auditEvent
|
FROM workspaceEvent
|
||||||
WHERE event = '${OBJECT_RECORD_CREATED_EVENT}' AND timestamp >= now() - INTERVAL 1 SECOND
|
WHERE event = '${CUSTOM_DOMAIN_ACTIVATED_EVENT}' AND timestamp >= now() - INTERVAL 1 SECOND
|
||||||
|
|
||||||
`,
|
`,
|
||||||
format: 'JSONEachRow',
|
format: 'JSONEachRow',
|
||||||
@ -1,19 +1,31 @@
|
|||||||
import { TrackEventName } from 'src/engine/core-modules/audit/types/events.type';
|
import { TrackEventName } from 'src/engine/core-modules/audit/types/events.type';
|
||||||
|
|
||||||
export const AuditContextMock = (params?: {
|
export const AuditContextMock = (params?: {
|
||||||
track?:
|
insertWorkspaceEvent?:
|
||||||
| ((
|
| ((
|
||||||
event: TrackEventName,
|
event: TrackEventName,
|
||||||
properties: any,
|
properties: any,
|
||||||
) => Promise<{ success: boolean }>)
|
) => Promise<{ success: boolean }>)
|
||||||
| jest.Mock<any, any>;
|
| jest.Mock<any, any>;
|
||||||
pageview?:
|
createObjectEvent?:
|
||||||
|
| ((
|
||||||
|
event: TrackEventName,
|
||||||
|
properties: any,
|
||||||
|
) => Promise<{ success: boolean }>)
|
||||||
|
| jest.Mock<any, any>;
|
||||||
|
createPageviewEvent?:
|
||||||
| ((name: string, properties: any) => Promise<{ success: boolean }>)
|
| ((name: string, properties: any) => Promise<{ success: boolean }>)
|
||||||
| jest.Mock<any, any>;
|
| jest.Mock<any, any>;
|
||||||
}) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
track: params?.track ?? jest.fn().mockResolvedValue({ success: true }),
|
insertWorkspaceEvent:
|
||||||
pageview:
|
params?.insertWorkspaceEvent ??
|
||||||
params?.pageview ?? jest.fn().mockResolvedValue({ success: true }),
|
jest.fn().mockResolvedValue({ success: true }),
|
||||||
|
createObjectEvent:
|
||||||
|
params?.createObjectEvent ??
|
||||||
|
jest.fn().mockResolvedValue({ success: true }),
|
||||||
|
createPageviewEvent:
|
||||||
|
params?.createPageviewEvent ??
|
||||||
|
jest.fn().mockResolvedValue({ success: true }),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user