diff --git a/packages/altair-api/.env.e2e b/packages/altair-api/.env.e2e index 78ac920fa1..c253b6fb3f 100644 --- a/packages/altair-api/.env.e2e +++ b/packages/altair-api/.env.e2e @@ -7,7 +7,7 @@ POSTGRES_DB=tests POSTGRES_USER=prisma POSTGRES_PASSWORD=prisma DATABASE_URL=postgresql://prisma:prisma@localhost:5434/tests?schema=public -NEW_RELIC_APP_NAME=altairgraphql.test +# NEW_RELIC_APP_NAME=altairgraphql.test NEW_RELIC_LICENSE_KEY=test-key NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED=true STRIPE_SECRET_KEY=sk_test_xxx diff --git a/packages/altair-api/src/auth/auth.service.ts b/packages/altair-api/src/auth/auth.service.ts index fa11f4d4da..14aada3de8 100644 --- a/packages/altair-api/src/auth/auth.service.ts +++ b/packages/altair-api/src/auth/auth.service.ts @@ -12,9 +12,11 @@ import { SecurityConfig } from 'src/common/config'; import { ChangePasswordInput } from './models/change-password.input'; import { PasswordService } from './password/password.service'; import { IToken } from '@altairgraphql/api-utils'; +import { getAgent } from 'src/newrelic/newrelic'; @Injectable() export class AuthService { + private readonly agent = getAgent(); constructor( private readonly jwtService: JwtService, private readonly prisma: PrismaService, @@ -99,6 +101,8 @@ export class AuthService { } getLoginResponse(user: User) { + this.agent?.incrementMetric('auth.login.success'); + return { id: user.id, email: user.email, @@ -121,6 +125,7 @@ export class AuthService { */ getShortLivedEventsToken(userId: string): string { const securityConfig = this.configService.get('security'); + this.agent?.incrementMetric('auth.events_token.generate'); return this.jwtService.sign( { userId }, { diff --git a/packages/altair-api/src/auth/user/user.service.ts b/packages/altair-api/src/auth/user/user.service.ts index 221d3b3e19..3e2faf7ea4 100644 --- a/packages/altair-api/src/auth/user/user.service.ts +++ b/packages/altair-api/src/auth/user/user.service.ts @@ -12,10 +12,12 @@ import { StripeService } from 'src/stripe/stripe.service'; import { ProviderInfo } from '../models/provider-info.dto'; import { SignupInput } from '../models/signup.input'; import { UpdateUserInput } from '../models/update-user.input'; +import { getAgent } from 'src/newrelic/newrelic'; @Injectable() export class UserService { private readonly logger = new Logger(UserService.name); + private readonly agent = getAgent(); constructor( private readonly prisma: PrismaService, private readonly stripeService: StripeService @@ -301,12 +303,16 @@ export class UserService { } async getProUsers() { - return this.prisma.user.findMany({ + const proUsers = await this.prisma.user.findMany({ where: { UserPlan: { planRole: PRO_PLAN_ID, }, }, }); + + this.agent?.recordMetric('users.pro.count', proUsers.length); + + return proUsers; } } diff --git a/packages/altair-api/src/credit/credit.service.ts b/packages/altair-api/src/credit/credit.service.ts index 7ef7f15ff2..fe0a108412 100644 --- a/packages/altair-api/src/credit/credit.service.ts +++ b/packages/altair-api/src/credit/credit.service.ts @@ -8,11 +8,14 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { PrismaService } from 'nestjs-prisma'; import { UserService } from 'src/auth/user/user.service'; +import { getAgent } from 'src/newrelic/newrelic'; import { StripeService } from 'src/stripe/stripe.service'; @Injectable() export class CreditService { private readonly logger = new Logger(CreditService.name); + private readonly agent = getAgent(); + constructor( private readonly prisma: PrismaService, private readonly userService: UserService, @@ -42,8 +45,13 @@ export class CreditService { where: { userId }, }); if (!creditBalance) { + this.agent?.incrementMetric('credit.balance.not_found'); throw new BadRequestException('User has no credits'); } + this.agent?.recordMetric( + 'credit.balance.total', + creditBalance.fixedCredits + creditBalance.monthlyCredits + ); return { fixed: creditBalance.fixedCredits, monthly: creditBalance.monthlyCredits, @@ -123,6 +131,8 @@ export class CreditService { }); }); + this.agent?.recordMetric('credit.monthly.refill_count', proUsers.length); + await Promise.all(creditBalanceRecords); } } diff --git a/packages/altair-api/src/email/email.service.ts b/packages/altair-api/src/email/email.service.ts index 0b521e5895..aac4d6fdf4 100644 --- a/packages/altair-api/src/email/email.service.ts +++ b/packages/altair-api/src/email/email.service.ts @@ -5,10 +5,13 @@ import { renderWelcomeEmail } from '@altairgraphql/emails'; import { UserService } from 'src/auth/user/user.service'; import { Config } from 'src/common/config'; import { User } from '@altairgraphql/db'; +import { Agent, getAgent } from 'src/newrelic/newrelic'; @Injectable() export class EmailService { private resend: Resend; + private agent = getAgent(); + constructor( private configService: ConfigService, private readonly userService: UserService @@ -52,12 +55,8 @@ export class EmailService { async sendWelcomeEmail(userId: string) { const user = await this.userService.mustGetUser(userId); - const { data, error } = await this.resend.emails.send({ - from: - this.configService.get('email.defaultFrom', { infer: true }) ?? - 'info@mail.altairgraphql.dev', + const { data, error } = await this.sendEmail({ to: user.email, - replyTo: this.configService.get('email.replyTo', { infer: true }), subject: 'Welcome to Altair GraphQL Cloud', html: await renderWelcomeEmail({ username: this.getFirstName(user) }), }); @@ -70,12 +69,9 @@ export class EmailService { async sendGoodbyeEmail(userId: string) { const user = await this.userService.mustGetUser(userId); - const { data, error } = await this.resend.emails.send({ - from: - this.configService.get('email.defaultFrom', { infer: true }) ?? - 'info@mail.altairgraphql.dev', + + const { data, error } = await this.sendEmail({ to: user.email, - replyTo: this.configService.get('email.replyTo', { infer: true }), subject: 'Sorry to see you go 👋🏾', html: `Hey ${this.getFirstName(user)},

@@ -100,6 +96,32 @@ export class EmailService { return { data, error }; } + private async sendEmail({ + to, + subject, + html, + }: { + to: string; + subject: string; + html: string; + }) { + const { data, error } = await this.resend.emails.send({ + from: + this.configService.get('email.defaultFrom', { infer: true }) ?? + 'info@mail.altairgraphql.dev', + to, + replyTo: this.configService.get('email.replyTo', { infer: true }), + subject, + html, + }); + if (error) { + this.agent?.incrementMetric('email.send.error'); + } + + this.agent?.incrementMetric('email.send.success'); + return { data, error }; + } + private getFirstName(user: User) { return user.firstName ?? user.email; } diff --git a/packages/altair-api/src/newrelic/newrelic.interceptor.ts b/packages/altair-api/src/newrelic/newrelic.interceptor.ts index ef5aa5dd0f..184b218a48 100644 --- a/packages/altair-api/src/newrelic/newrelic.interceptor.ts +++ b/packages/altair-api/src/newrelic/newrelic.interceptor.ts @@ -7,22 +7,22 @@ import { } from '@nestjs/common'; import { Observable, tap } from 'rxjs'; import { inspect } from 'util'; +import { getAgent } from './newrelic'; @Injectable() export class NewrelicInterceptor implements NestInterceptor { constructor(private readonly logger: LoggerService) {} intercept(context: ExecutionContext, next: CallHandler): Observable { const log = this.logger; - if (!process.env.NEW_RELIC_APP_NAME) { + const agent = getAgent(); + if (!agent) { return next.handle(); } - // eslint-disable-next-line @typescript-eslint/no-var-requires - const newrelic = require('newrelic'); this.logger.log( `Newrelic Interceptor before: ${inspect(context.getHandler().name)}` ); - return newrelic.startWebTransaction(context.getHandler().name, function () { - const transaction = newrelic.getTransaction(); + return agent.startWebTransaction(context.getHandler().name, function () { + const transaction = agent.getTransaction(); return next.handle().pipe( tap(() => { log.log( diff --git a/packages/altair-api/src/newrelic/newrelic.ts b/packages/altair-api/src/newrelic/newrelic.ts new file mode 100644 index 0000000000..eb1e0476a2 --- /dev/null +++ b/packages/altair-api/src/newrelic/newrelic.ts @@ -0,0 +1,29 @@ +import type { + startWebTransaction, + getTransaction, + recordMetric, + incrementMetric, +} from 'newrelic'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const newrelic = require('newrelic'); + +export interface Agent { + startWebTransaction: typeof startWebTransaction; + getTransaction: typeof getTransaction; + recordMetric: typeof recordMetric; + incrementMetric: typeof incrementMetric; +} + +const prodAgent: Agent = { + startWebTransaction: newrelic.startWebTransaction, + getTransaction: newrelic.getTransaction, + recordMetric: newrelic.recordMetric, + incrementMetric: newrelic.incrementMetric, +}; + +export const getAgent = () => { + if (process.env.NEW_RELIC_APP_NAME) { + return prodAgent; + } + return; +}; diff --git a/packages/altair-api/src/queries/queries.service.ts b/packages/altair-api/src/queries/queries.service.ts index 717f42c877..4066d5a08a 100644 --- a/packages/altair-api/src/queries/queries.service.ts +++ b/packages/altair-api/src/queries/queries.service.ts @@ -12,11 +12,13 @@ import { queryItemWhereOwnerOrMember, collectionWhereOwnerOrMember, } from 'src/common/where-clauses'; +import { getAgent } from 'src/newrelic/newrelic'; const DEFAULT_QUERY_REVISION_LIMIT = 10; @Injectable() export class QueriesService { + private readonly agent = getAgent(); constructor( private readonly prisma: PrismaService, private readonly userService: UserService, @@ -85,6 +87,8 @@ export class QueriesService { this.eventService.emit(EVENTS.QUERY_UPDATE, { id: res.id }); + this.agent?.incrementMetric('query.create'); + return res; } @@ -140,15 +144,21 @@ export class QueriesService { this.eventService.emit(EVENTS.QUERY_UPDATE, { id: res.id }); + this.agent?.incrementMetric('query.create'); + return res; } - findAll(userId: string) { - return this.prisma.queryItem.findMany({ + async findAll(userId: string) { + const res = await this.prisma.queryItem.findMany({ where: { ...queryItemWhereOwnerOrMember(userId), }, }); + + this.agent?.recordMetric('query.list.count', res.length); + + return res; } async findOne(userId: string, id: string) { @@ -205,17 +215,21 @@ export class QueriesService { } async count(userId: string, ownOnly = true) { - return this.prisma.queryItem.count({ + const cnt = await this.prisma.queryItem.count({ where: { ...(ownOnly ? queryItemWhereOwner(userId) : queryItemWhereOwnerOrMember(userId)), }, }); + + this.agent?.recordMetric('query.list.count', cnt); + + return cnt; } - listRevisions(userId: string, queryId: string) { - return this.prisma.queryItemRevision.findMany({ + async listRevisions(userId: string, queryId: string) { + const res = await this.prisma.queryItemRevision.findMany({ where: { queryItem: { id: queryId, @@ -232,6 +246,10 @@ export class QueriesService { }, }, }); + + this.agent?.recordMetric('query.revision.list.count', res.length); + + return res; } async restoreRevision(userId: string, revisionId: string) { @@ -358,6 +376,8 @@ export class QueriesService { }); } + this.agent?.incrementMetric('query.revision.create'); + return res; } } diff --git a/packages/altair-api/src/query-collections/query-collections.service.ts b/packages/altair-api/src/query-collections/query-collections.service.ts index 9f1bca38d6..f14967809c 100644 --- a/packages/altair-api/src/query-collections/query-collections.service.ts +++ b/packages/altair-api/src/query-collections/query-collections.service.ts @@ -12,9 +12,11 @@ import { collectionWhereOwner, collectionWhereOwnerOrMember, } from 'src/common/where-clauses'; +import { getAgent } from 'src/newrelic/newrelic'; @Injectable() export class QueryCollectionsService { + private readonly agent = getAgent(); constructor( private readonly prisma: PrismaService, private readonly userService: UserService, @@ -101,11 +103,13 @@ export class QueryCollectionsService { }); this.eventService.emit(EVENTS.COLLECTION_UPDATE, { id: res.id }); + this.agent?.incrementMetric('query_collection.create'); + return res; } - findAll(userId: string) { - return this.prisma.queryCollection.findMany({ + async findAll(userId: string) { + const res = await this.prisma.queryCollection.findMany({ where: { ...collectionWhereOwnerOrMember(userId), }, @@ -113,6 +117,10 @@ export class QueryCollectionsService { queries: true, }, }); + + this.agent?.recordMetric('query_collection.list.count', res.length); + + return res; } findOne(userId: string, id: string) { @@ -169,13 +177,17 @@ export class QueryCollectionsService { } async count(userId: string, ownOnly = true) { - return this.prisma.queryCollection.count({ + const cnt = await this.prisma.queryCollection.count({ where: { ...(ownOnly ? collectionWhereOwner(userId) : collectionWhereOwnerOrMember(userId)), }, }); + + this.agent?.recordMetric('query_collection.list.count', cnt); + + return cnt; } private async getWorkspaceOwnerId(workspaceId: string) { diff --git a/packages/altair-api/src/team-memberships/team-memberships.service.ts b/packages/altair-api/src/team-memberships/team-memberships.service.ts index e39f79c2dc..225ab49a1c 100644 --- a/packages/altair-api/src/team-memberships/team-memberships.service.ts +++ b/packages/altair-api/src/team-memberships/team-memberships.service.ts @@ -4,9 +4,11 @@ import { UserService } from 'src/auth/user/user.service'; import { InvalidRequestException } from 'src/exceptions/invalid-request.exception'; import { CreateTeamMembershipDto } from './dto/create-team-membership.dto'; import { UpdateTeamMembershipDto } from './dto/update-team-membership.dto'; +import { getAgent } from 'src/newrelic/newrelic'; @Injectable() export class TeamMembershipsService { + private readonly agent = getAgent(); constructor( private prisma: PrismaService, private userService: UserService @@ -64,17 +66,23 @@ export class TeamMembershipsService { await this.updateSubscriptionQuantity(userId); + this.agent?.incrementMetric('team.membership.added'); + return res; } async findAllByTeamOwner(userId: string) { - return this.prisma.teamMembership.findMany({ + const res = await this.prisma.teamMembership.findMany({ where: { team: { ownerId: userId, }, }, }); + + this.agent?.recordMetric('team.membership.count_by_owner', res.length); + + return res; } async findAll(userId: string, teamId: string) { @@ -98,7 +106,7 @@ export class TeamMembershipsService { ); } - return this.prisma.teamMembership.findMany({ + const res = await this.prisma.teamMembership.findMany({ where: { teamId, }, @@ -112,6 +120,10 @@ export class TeamMembershipsService { }, }, }); + + this.agent?.recordMetric('team.membership.count', res.length); + + return res; } findOne(userId: string, id: string) { diff --git a/packages/altair-api/src/teams/teams.service.ts b/packages/altair-api/src/teams/teams.service.ts index 8d1aef450c..cb8ef2187d 100644 --- a/packages/altair-api/src/teams/teams.service.ts +++ b/packages/altair-api/src/teams/teams.service.ts @@ -5,9 +5,11 @@ import { UserService } from 'src/auth/user/user.service'; import { InvalidRequestException } from 'src/exceptions/invalid-request.exception'; import { CreateTeamDto } from './dto/create-team.dto'; import { UpdateTeamDto } from './dto/update-team.dto'; +import { getAgent } from 'src/newrelic/newrelic'; @Injectable() export class TeamsService { + private readonly agent = getAgent(); constructor( private prisma: PrismaService, private userService: UserService @@ -29,7 +31,7 @@ export class TeamsService { ); } - return this.prisma.team.create({ + const res = await this.prisma.team.create({ data: { ...createTeamDto, ownerId: userId, @@ -41,10 +43,14 @@ export class TeamsService { }, }, }); + + this.agent?.incrementMetric('team.created'); + + return res; } - findAll(userId: string) { - return this.prisma.team.findMany({ + async findAll(userId: string) { + const res = await this.prisma.team.findMany({ where: { ...this.ownerOrMemberWhere(userId), }, @@ -57,6 +63,10 @@ export class TeamsService { }, }, }); + + this.agent?.recordMetric('team.list.count', res.length); + + return res; } findOne(userId: string, id: string) { diff --git a/packages/altair-api/src/workspaces/workspaces.service.ts b/packages/altair-api/src/workspaces/workspaces.service.ts index 2389b205f3..54d8e5ef67 100644 --- a/packages/altair-api/src/workspaces/workspaces.service.ts +++ b/packages/altair-api/src/workspaces/workspaces.service.ts @@ -7,20 +7,26 @@ import { workspaceWhereOwner, workspaceWhereOwnerOrMember, } from 'src/common/where-clauses'; +import { getAgent } from 'src/newrelic/newrelic'; @Injectable() export class WorkspacesService { + private readonly agent = getAgent(); constructor(private readonly prisma: PrismaService) {} create(userId: string, createWorkspaceDto: CreateWorkspaceDto) { return 'This action adds a new workspace'; } - findAll(userId: string): Promise { - return this.prisma.workspace.findMany({ + async findAll(userId: string): Promise { + const res = await this.prisma.workspace.findMany({ where: { ...workspaceWhereOwnerOrMember(userId), }, }); + + this.agent?.recordMetric('workspace.list.count', res.length); + + return res; } findOne(userId: string, id: string): Promise {