Add Telemetry (#466)

* Telemetry v1

* Add package-lock.json to gitignore
This commit is contained in:
Félix Malfait
2023-06-29 17:36:48 -07:00
committed by GitHub
parent 74ea2718ca
commit eb7fb2ba8e
24 changed files with 1281 additions and 14329 deletions

View File

@ -31,6 +31,7 @@
"@casl/ability": "^6.5.0",
"@casl/prisma": "^1.4.0",
"@nestjs/apollo": "^11.0.5",
"@nestjs/axios": "^3.0.0",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.2",
"@nestjs/core": "^9.0.0",
@ -43,6 +44,7 @@
"@paljs/plugins": "^5.3.3",
"@prisma/client": "^4.13.0",
"apollo-server-express": "^3.12.0",
"axios": "^1.4.0",
"bcrypt": "^5.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",

View File

@ -10,6 +10,8 @@ import { GraphQLError } from 'graphql';
import { PrismaModule } from './database/prisma.module';
import { HealthModule } from './health/health.module';
import { AbilityModule } from './ability/ability.module';
import { EventModule } from './core/analytics/event.module';
import GraphQLJSON from 'graphql-type-json';
@Module({
imports: [
@ -21,6 +23,7 @@ import { AbilityModule } from './ability/ability.module';
context: ({ req }) => ({ req }),
driver: ApolloDriver,
autoSchemaFile: true,
resolvers: { JSON: GraphQLJSON },
plugins: [ApolloServerPluginLandingPageLocalDefault()],
formatError: (error: GraphQLError) => {
error.extensions.stacktrace = undefined;
@ -31,6 +34,7 @@ import { AbilityModule } from './ability/ability.module';
HealthModule,
AbilityModule,
CoreModule,
EventModule,
],
providers: [AppService],
})

View File

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

View File

@ -0,0 +1,9 @@
import { ObjectType, Field, Int } from '@nestjs/graphql';
@ObjectType()
export class Event {
@Field(() => Boolean, {
description: 'Boolean that confirms query was dispatched',
})
success: boolean;
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { EventService } from './event.service';
import { EventResolver } from './event.resolver';
import { HttpModule } from '@nestjs/axios';
@Module({
providers: [EventResolver, EventService],
imports: [HttpModule],
})
export class EventModule {}

View File

@ -0,0 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EventResolver } from './event.resolver';
import { EventService } from './event.service';
describe('EventResolver', () => {
let resolver: EventResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EventResolver, EventService],
}).compile();
resolver = module.get<EventResolver>(EventResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,24 @@
import { Resolver, Mutation, Args, Context } from '@nestjs/graphql';
import { EventService } from './event.service';
import { Event } from './event.entity';
import { CreateEventInput } from './dto/create-event.input';
import { OptionalJwtAuthGuard } from 'src/guards/optional-jwt.auth.guard';
import { UseGuards } from '@nestjs/common';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { User, Workspace } from '@prisma/client';
import { AuthUser } from 'src/decorators/auth-user.decorator';
@UseGuards(OptionalJwtAuthGuard)
@Resolver(() => Event)
export class EventResolver {
constructor(private readonly eventService: EventService) {}
@Mutation(() => Event)
createEvent(
@Args() createEventInput: CreateEventInput,
@AuthWorkspace() workspace: Workspace | undefined,
@AuthUser() user: User | undefined,
) {
return this.eventService.create(createEventInput, user, workspace);
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EventService } from './event.service';
describe('EventService', () => {
let service: EventService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EventService],
}).compile();
service = module.get<EventService>(EventService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import { CreateEventInput } from './dto/create-event.input';
import { HttpService } from '@nestjs/axios';
import { anonymize } from 'src/utils/anonymize';
import { User, Workspace } from '@prisma/client';
@Injectable()
export class EventService {
constructor(private readonly httpService: HttpService) {}
create(
createEventInput: CreateEventInput,
user: User | undefined,
workspace: Workspace | undefined,
) {
if (process.env.IS_TELEMETRY_ENABLED === 'false') {
return;
}
const data = {
type: createEventInput.type,
data: {
userUUID: user ? anonymize(user.id) : undefined,
workspaceUUID: workspace ? anonymize(workspace.id) : undefined,
workspaceDomain: workspace ? workspace.domainName : undefined,
...createEventInput.data,
},
};
this.httpService
.post('https://t.twenty.com/api/v1/s2s/event?noToken', data)
.subscribe({
error: () => null,
});
return { success: true };
}
}

View File

@ -6,6 +6,7 @@ import { PersonModule } from './person/person.module';
import { PipelineModule } from './pipeline/pipeline.module';
import { AuthModule } from './auth/auth.module';
import { WorkspaceModule } from './workspace/workspace.module';
import { EventModule } from './analytics/event.module';
@Module({
imports: [
@ -16,6 +17,7 @@ import { WorkspaceModule } from './workspace/workspace.module';
PersonModule,
PipelineModule,
WorkspaceModule,
EventModule,
],
exports: [
AuthModule,
@ -25,6 +27,7 @@ import { WorkspaceModule } from './workspace/workspace.module';
PersonModule,
PipelineModule,
WorkspaceModule,
EventModule,
],
})
export class CoreModule {}

View File

@ -0,0 +1,20 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { getRequest } from 'src/utils/extract-request';
@Injectable()
export class OptionalJwtAuthGuard extends AuthGuard(['jwt']) {
constructor() {
super();
}
getRequest(context: ExecutionContext) {
const request = getRequest(context);
return request;
}
handleRequest(err, user, info) {
if (err || info) return null;
return user;
}
}

View File

@ -0,0 +1,9 @@
import crypto from 'crypto';
export function anonymize(input) {
if (process.env.IS_TELEMETRY_ANONYMIZATION_ENABLED === 'false') {
return input;
}
// md5 shorter than sha-256 and collisions are not a security risk in this use-case
return crypto.createHash('md5').update(input).digest('hex');
}

File diff suppressed because it is too large Load Diff