Connect EventTracker to TB endpoint (#7240)

#7091 
EventTrackers send information of events to the TinyBird instance:

In order to test:

1. Set ANALYTICS_ENABLED= true and TELEMETRY_ENABLED=true in
evironment-variables.ts
2. Set the TINYBIRD_TOKEN in environment variables (go to TiniyBird
Tokens)
3. Log in to twenty's TinyBird and go to datasources/analytics_events in
twenty_analytics workspace
4. Run twenty and navigate it
5. New events will be logged in the datasources, containing their
timestamp, sessionId and payload.

<img width="1189" alt="Screenshot 2024-09-24 at 17 23 01"
src="https://github.com/user-attachments/assets/85375897-504d-4e75-98e4-98e6a9671f98">
Example of payload when user is not logged in

```
{"hostName":"localhost",
"pathname":"/welcome",
"locale":"en-US",
"userAgent":"Mozilla/5.0",
"href":"http://localhost:3001/welcome",
"referrer":"",
"timeZone":"Europe/Barcelona"}
```
Example of payload when user is logged in
```
{"userId":"2020202",
"workspaceId":"202",
"workspaceDisplayName":"Apple",
"workspaceDomainName":"apple.dev",
"hostName":"localhost",
"pathname":"/objects/companies",
"locale":"en-US",
"userAgent":"Mozilla/5.0Chrome/128.0.0.0Safari/537.36",
"href":"http://localhost:3001/objects/companies",
"referrer":"",
"timeZone":"Europe/Paris"}
```

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Ana Sofia Marin Alexandre
2024-09-26 10:53:10 +02:00
committed by GitHub
parent c9e882f4c0
commit 16bb1f22e4
28 changed files with 273 additions and 187 deletions

View File

@ -1,14 +1,16 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { AnalyticsService } from './analytics.service';
import { AnalyticsResolver } from './analytics.resolver';
import { AnalyticsService } from './analytics.service';
const TINYBIRD_BASE_URL = 'https://api.eu-central-1.aws.tinybird.co/v0';
@Module({
providers: [AnalyticsResolver, AnalyticsService],
imports: [
HttpModule.register({
baseURL: 'https://t.twenty.com/api/v1/s2s',
baseURL: TINYBIRD_BASE_URL,
}),
],
exports: [AnalyticsService],

View File

@ -31,9 +31,6 @@ export class AnalyticsResolver {
createAnalyticsInput,
user?.id,
workspace?.id,
workspace?.displayName,
workspace?.domainName,
this.environmentService.get('SERVER_URL') ?? request.hostname,
);
}
}

View File

@ -1,16 +1,19 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common';
import { AxiosRequestConfig } from 'axios';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
type CreateEventInput = {
type: string;
data: object;
action: string;
payload: object;
};
@Injectable()
export class AnalyticsService {
private readonly logger = new Logger(AnalyticsService.name);
private readonly datasource = 'event';
constructor(
private readonly environmentService: EnvironmentService,
@ -21,30 +24,42 @@ export class AnalyticsService {
createEventInput: CreateEventInput,
userId: string | null | undefined,
workspaceId: string | null | undefined,
workspaceDisplayName: string | undefined,
workspaceDomainName: string | undefined,
hostName: string | undefined,
) {
if (!this.environmentService.get('TELEMETRY_ENABLED')) {
if (this.environmentService.get('ANALYTICS_ENABLED')) {
return { success: true };
}
const data = {
type: createEventInput.type,
data: {
hostname: hostName,
userUUID: userId,
workspaceUUID: workspaceId,
workspaceDisplayName: workspaceDisplayName,
workspaceDomainName: workspaceDomainName,
...createEventInput.data,
action: createEventInput.action,
timestamp: new Date().toISOString(),
version: '1',
payload: {
userId: userId,
workspaceId: workspaceId,
...createEventInput.payload,
},
};
const config: AxiosRequestConfig = {
headers: {
Authorization:
'Bearer ' + this.environmentService.get('TINYBIRD_TOKEN'),
},
};
try {
await this.httpService.axiosRef.post('/v1', data);
} catch {
this.logger.error('Failed to send analytics event');
await this.httpService.axiosRef.post(
`/events?name=${this.datasource}`,
data,
config,
);
} catch (error) {
this.logger.error('Error occurred:', error);
if (error.response) {
this.logger.error(
`Error response body: ${JSON.stringify(error.response.data)}`,
);
}
return { success: false };
}

View File

@ -1,16 +1,16 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsString } from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
import { IsNotEmpty, IsString, IsObject } from 'class-validator';
@ArgsType()
export class CreateAnalyticsInput {
@Field({ description: 'Type of the event' })
@IsNotEmpty()
@IsString()
type: string;
action: string;
@Field(() => graphqlTypeJson, { description: 'Event data in JSON format' })
@Field(() => graphqlTypeJson, { description: 'Event payload in JSON format' })
@IsObject()
data: JSON;
payload: JSON;
}

View File

@ -76,9 +76,6 @@ export class ClientConfig {
@Field(() => AuthProviders, { nullable: false })
authProviders: AuthProviders;
@Field(() => Telemetry, { nullable: false })
telemetry: Telemetry;
@Field(() => Billing, { nullable: false })
billing: Billing;

View File

@ -1,4 +1,4 @@
import { Resolver, Query } from '@nestjs/graphql';
import { Query, Resolver } from '@nestjs/graphql';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@ -17,9 +17,6 @@ export class ClientConfigResolver {
password: this.environmentService.get('AUTH_PASSWORD_ENABLED'),
microsoft: this.environmentService.get('AUTH_MICROSOFT_ENABLED'),
},
telemetry: {
enabled: this.environmentService.get('TELEMETRY_ENABLED'),
},
billing: {
isBillingEnabled: this.environmentService.get('IS_BILLING_ENABLED'),
billingUrl: this.environmentService.get('BILLING_PLAN_REQUIRED_LINK'),

View File

@ -7,43 +7,44 @@ import { AISQLQueryModule } from 'src/engine/core-modules/ai-sql-query/ai-sql-qu
import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { CacheStorageModule } from 'src/engine/core-modules/cache-storage/cache-storage.module';
import { TimelineCalendarEventModule } from 'src/engine/core-modules/calendar/timeline-calendar-event.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { HealthModule } from 'src/engine/core-modules/health/health.module';
import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timeline-messaging.module';
import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module';
import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { WorkflowTriggerApiModule } from 'src/engine/core-modules/workflow/workflow-trigger-api.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { fileStorageModuleFactory } from 'src/engine/core-modules/file-storage/file-storage.module-factory';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { LoggerModule } from 'src/engine/core-modules/logger/logger.module';
import { loggerModuleFactory } from 'src/engine/core-modules/logger/logger.module-factory';
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
import { messageQueueModuleFactory } from 'src/engine/core-modules/message-queue/message-queue.module-factory';
import { ExceptionHandlerModule } from 'src/engine/core-modules/exception-handler/exception-handler.module';
import { exceptionHandlerModuleFactory } from 'src/engine/core-modules/exception-handler/exception-handler.module-factory';
import { EmailModule } from 'src/engine/core-modules/email/email.module';
import { emailModuleFactory } from 'src/engine/core-modules/email/email.module-factory';
import { CaptchaModule } from 'src/engine/core-modules/captcha/captcha.module';
import { captchaModuleFactory } from 'src/engine/core-modules/captcha/captcha.module-factory';
import { CacheStorageModule } from 'src/engine/core-modules/cache-storage/cache-storage.module';
import { EmailModule } from 'src/engine/core-modules/email/email.module';
import { emailModuleFactory } from 'src/engine/core-modules/email/email.module-factory';
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { ExceptionHandlerModule } from 'src/engine/core-modules/exception-handler/exception-handler.module';
import { exceptionHandlerModuleFactory } from 'src/engine/core-modules/exception-handler/exception-handler.module-factory';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { fileStorageModuleFactory } from 'src/engine/core-modules/file-storage/file-storage.module-factory';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { HealthModule } from 'src/engine/core-modules/health/health.module';
import { LLMChatModelModule } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.module';
import { llmChatModelModuleFactory } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.module-factory';
import { LLMTracingModule } from 'src/engine/core-modules/llm-tracing/llm-tracing.module';
import { llmTracingModuleFactory } from 'src/engine/core-modules/llm-tracing/llm-tracing.module-factory';
import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless.module';
import { LoggerModule } from 'src/engine/core-modules/logger/logger.module';
import { loggerModuleFactory } from 'src/engine/core-modules/logger/logger.module-factory';
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
import { messageQueueModuleFactory } from 'src/engine/core-modules/message-queue/message-queue.module-factory';
import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timeline-messaging.module';
import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module';
import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module';
import { serverlessModuleFactory } from 'src/engine/core-modules/serverless/serverless-module.factory';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless.module';
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { WorkflowTriggerApiModule } from 'src/engine/core-modules/workflow/workflow-trigger-api.module';
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { FileModule } from './file/file.module';
import { ClientConfigModule } from './client-config/client-config.module';
import { AnalyticsModule } from './analytics/analytics.module';
import { ClientConfigModule } from './client-config/client-config.module';
import { FileModule } from './file/file.module';
@Module({
imports: [
@ -66,6 +67,7 @@ import { AnalyticsModule } from './analytics/analytics.module';
WorkflowTriggerApiModule,
WorkspaceEventEmitterModule,
ActorModule,
TelemetryModule,
EnvironmentModule.forRoot({}),
FileStorageModule.forRootAsync({
useFactory: fileStorageModuleFactory,

View File

@ -16,20 +16,20 @@ import {
} from 'class-validator';
import { EmailDriver } from 'src/engine/core-modules/email/interfaces/email.interface';
import { AwsRegion } from 'src/engine/core-modules/environment/interfaces/aws-region.interface';
import { NodeEnvironment } from 'src/engine/core-modules/environment/interfaces/node-environment.interface';
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
import { LLMChatModelDriver } from 'src/engine/core-modules/llm-chat-model/interfaces/llm-chat-model.interface';
import { LLMTracingDriver } from 'src/engine/core-modules/llm-tracing/interfaces/llm-tracing.interface';
import { AwsRegion } from 'src/engine/core-modules/environment/interfaces/aws-region.interface';
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
import { IsDuration } from 'src/engine/core-modules/environment/decorators/is-duration.decorator';
import { IsAWSRegion } from 'src/engine/core-modules/environment/decorators/is-aws-region.decorator';
import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator';
import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator';
import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/cast-to-boolean.decorator';
import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/cast-to-boolean.decorator';
import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator';
import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator';
import { CastToStringArray } from 'src/engine/core-modules/environment/decorators/cast-to-string-array.decorator';
import { IsAWSRegion } from 'src/engine/core-modules/environment/decorators/is-aws-region.decorator';
import { IsDuration } from 'src/engine/core-modules/environment/decorators/is-duration.decorator';
import { IsStrictlyLowerThan } from 'src/engine/core-modules/environment/decorators/is-strictly-lower-than.decorator';
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces';
@ -88,6 +88,15 @@ export class EnvironmentVariables {
@IsBoolean()
TELEMETRY_ENABLED = true;
@CastToBoolean()
@IsOptional()
@IsBoolean()
ANALYTICS_ENABLED = false;
@IsString()
@ValidateIf((env) => env.ANALYTICS_ENABLED)
TINYBIRD_TOKEN: string;
@CastToPositiveNumber()
@IsNumber()
@IsOptional()

View File

@ -0,0 +1,15 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { TelemetryService } from './telemetry.service';
@Module({
providers: [TelemetryService],
imports: [
HttpModule.register({
baseURL: 'https://t.twenty.com/api/v2',
}),
],
exports: [TelemetryService],
})
export class TelemetryModule {}

View File

@ -0,0 +1,55 @@
import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
type CreateEventInput = {
action: string;
payload: object;
};
@Injectable()
export class TelemetryService {
private readonly logger = new Logger(TelemetryService.name);
constructor(
private readonly environmentService: EnvironmentService,
private readonly httpService: HttpService,
) {}
async create(
createEventInput: CreateEventInput,
userId: string | null | undefined,
workspaceId: string | null | undefined,
) {
if (!this.environmentService.get('TELEMETRY_ENABLED')) {
return { success: true };
}
const data = {
action: createEventInput.action,
timestamp: new Date().toISOString(),
version: '1',
payload: {
userId: userId,
workspaceId: workspaceId,
...createEventInput.payload,
},
};
try {
await this.httpService.axiosRef.post(`/selfHostingEvent`, data);
} catch (error) {
this.logger.error('Error occurred:', error);
if (error.response) {
this.logger.error(
`Error response body: ${JSON.stringify(error.response.data)}`,
);
}
return { success: false };
}
return { success: true };
}
}

View File

@ -5,19 +5,19 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { assert } from 'src/utils/assert';
import {
AppToken,
AppTokenType,
} from 'src/engine/core-modules/app-token/app-token.entity';
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { assert } from 'src/utils/assert';
export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
constructor(