feat: add metrics
This commit is contained in:
parent
c26a6e48b9
commit
7a0db391b0
@ -9,6 +9,8 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
env_file:
|
env_file:
|
||||||
- .env.production.local
|
- .env.production.local
|
||||||
|
expose:
|
||||||
|
- '9102'
|
||||||
networks:
|
networks:
|
||||||
- teacinema
|
- teacinema
|
||||||
|
|
||||||
|
|||||||
@ -30,11 +30,13 @@
|
|||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@teacinema/contracts": "^1.1.0",
|
"@teacinema/contracts": "^1.1.0",
|
||||||
"@teacinema/core": "^1.0.9",
|
"@teacinema/core": "^1.0.9",
|
||||||
|
"@willsoto/nestjs-prometheus": "^6.0.2",
|
||||||
"amqp-connection-manager": "^5.0.0",
|
"amqp-connection-manager": "^5.0.0",
|
||||||
"amqplib": "^0.10.9",
|
"amqplib": "^0.10.9",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
|
"prom-client": "^15.1.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { MailModule } from './infra/mail/mail.module'
|
|||||||
import { RmqModule } from './infra/rmq/rmq.module'
|
import { RmqModule } from './infra/rmq/rmq.module'
|
||||||
import { SmsModule } from './infra/sms/sms.module'
|
import { SmsModule } from './infra/sms/sms.module'
|
||||||
import { NotificationsModule } from './modules/notifications/notifications.module'
|
import { NotificationsModule } from './modules/notifications/notifications.module'
|
||||||
|
import { ObservabilityModule } from './observability/observability.module'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -20,6 +21,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul
|
|||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
RmqModule,
|
RmqModule,
|
||||||
|
ObservabilityModule,
|
||||||
NotificationsModule,
|
NotificationsModule,
|
||||||
MailModule,
|
MailModule,
|
||||||
SmsModule
|
SmsModule
|
||||||
|
|||||||
@ -1,11 +1,21 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common'
|
import { Injectable, Logger } from '@nestjs/common'
|
||||||
import { RmqContext } from '@nestjs/microservices'
|
import { RmqContext } from '@nestjs/microservices'
|
||||||
|
import { InjectMetric } from '@willsoto/nestjs-prometheus'
|
||||||
|
import { Counter } from 'prom-client'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RmqService {
|
export class RmqService {
|
||||||
|
private readonly SERVICE_NAME = 'notification-service'
|
||||||
private logger = new Logger(RmqService.name)
|
private logger = new Logger(RmqService.name)
|
||||||
|
|
||||||
public ack(context: RmqContext) {
|
public constructor(
|
||||||
|
@InjectMetric('rmq_events_ack_total')
|
||||||
|
private readonly ackTotal: Counter<string>,
|
||||||
|
@InjectMetric('rmq_events_nack_total')
|
||||||
|
private readonly nackTotal: Counter<string>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public ack(context: RmqContext, event: string) {
|
||||||
const channel = context.getChannelRef()
|
const channel = context.getChannelRef()
|
||||||
const msg = context.getMessage()
|
const msg = context.getMessage()
|
||||||
const tag = msg?.fields?.deliveryTag
|
const tag = msg?.fields?.deliveryTag
|
||||||
@ -14,11 +24,16 @@ export class RmqService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.ackTotal.inc({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event
|
||||||
|
})
|
||||||
|
|
||||||
channel.ack(msg)
|
channel.ack(msg)
|
||||||
this.logger.debug(`ACK (pattern: ${context.getPattern()}, tag: ${tag}): }`)
|
this.logger.debug(`ACK (pattern: ${context.getPattern()}, tag: ${tag}): }`)
|
||||||
}
|
}
|
||||||
|
|
||||||
public nack(context: RmqContext, requeue = false) {
|
public nack(context: RmqContext, event: string, requeue = false) {
|
||||||
const channel = context.getChannelRef()
|
const channel = context.getChannelRef()
|
||||||
const msg = context.getMessage()
|
const msg = context.getMessage()
|
||||||
const tag = msg?.fields?.deliveryTag
|
const tag = msg?.fields?.deliveryTag
|
||||||
@ -28,6 +43,12 @@ export class RmqService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
channel.nack(msg, false, requeue)
|
channel.nack(msg, false, requeue)
|
||||||
|
|
||||||
|
this.nackTotal.inc({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event
|
||||||
|
})
|
||||||
|
|
||||||
if (requeue) {
|
if (requeue) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`NACK response (pattern: ${context.getPattern()}, tag: ${tag}): }`
|
`NACK response (pattern: ${context.getPattern()}, tag: ${tag}): }`
|
||||||
|
|||||||
@ -24,6 +24,6 @@ async function bootstrap() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await app.startAllMicroservices()
|
await app.startAllMicroservices()
|
||||||
await app.init()
|
await app.listen(9102)
|
||||||
}
|
}
|
||||||
bootstrap()
|
bootstrap()
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import type {
|
|||||||
OtpRequestedEvent,
|
OtpRequestedEvent,
|
||||||
PhoneChangedEvent
|
PhoneChangedEvent
|
||||||
} from '@teacinema/contracts'
|
} from '@teacinema/contracts'
|
||||||
|
import { InjectMetric } from '@willsoto/nestjs-prometheus'
|
||||||
|
import { Counter, Histogram } from 'prom-client'
|
||||||
|
|
||||||
import { RmqService } from '../../infra/rmq/rmq.service'
|
import { RmqService } from '../../infra/rmq/rmq.service'
|
||||||
|
|
||||||
@ -12,11 +14,16 @@ import { NotificationsService } from './notifications.service'
|
|||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class NotificationsController {
|
export class NotificationsController {
|
||||||
|
private readonly SERVICE_NAME = 'notification-service'
|
||||||
private readonly logger = new Logger(NotificationsController.name)
|
private readonly logger = new Logger(NotificationsController.name)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly notificationsService: NotificationsService,
|
private readonly notificationsService: NotificationsService,
|
||||||
private readonly rmqService: RmqService
|
private readonly rmqService: RmqService,
|
||||||
|
@InjectMetric('rmq_event_processing_duration_seconds')
|
||||||
|
private readonly processingDuration: Histogram<string>,
|
||||||
|
@InjectMetric('rmq_events_total')
|
||||||
|
private readonly eventsTotal: Counter<string>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@EventPattern('auth.otp.requested')
|
@EventPattern('auth.otp.requested')
|
||||||
@ -24,12 +31,32 @@ export class NotificationsController {
|
|||||||
@Payload() data: OtpRequestedEvent,
|
@Payload() data: OtpRequestedEvent,
|
||||||
@Ctx() ctx: RmqContext
|
@Ctx() ctx: RmqContext
|
||||||
) {
|
) {
|
||||||
|
const event = 'auth.otp.requested'
|
||||||
|
const endTimer = this.processingDuration.startTimer({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.notificationsService.sendOtp(data)
|
await this.notificationsService.sendOtp(data)
|
||||||
this.rmqService.ack(ctx)
|
|
||||||
|
this.eventsTotal.inc({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event,
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.rmqService.ack(ctx, event)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.eventsTotal.inc({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event,
|
||||||
|
status: 'error'
|
||||||
|
})
|
||||||
this.logger.error(`OTP processing error ${e.message ?? e}}`)
|
this.logger.error(`OTP processing error ${e.message ?? e}}`)
|
||||||
this.rmqService.nack(ctx)
|
this.rmqService.nack(ctx, event)
|
||||||
|
} finally {
|
||||||
|
endTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,12 +65,30 @@ export class NotificationsController {
|
|||||||
@Payload() data: PhoneChangedEvent,
|
@Payload() data: PhoneChangedEvent,
|
||||||
@Ctx() ctx: RmqContext
|
@Ctx() ctx: RmqContext
|
||||||
) {
|
) {
|
||||||
|
const event = 'account.phone.changed'
|
||||||
|
const endTimer = this.processingDuration.startTimer({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.notificationsService.phoneChanged(data)
|
await this.notificationsService.phoneChanged(data)
|
||||||
this.rmqService.ack(ctx)
|
this.eventsTotal.inc({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event,
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
this.rmqService.ack(ctx, event)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.eventsTotal.inc({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event,
|
||||||
|
status: 'error'
|
||||||
|
})
|
||||||
this.logger.error(`Phone changing processing error ${e.message ?? e}}`)
|
this.logger.error(`Phone changing processing error ${e.message ?? e}}`)
|
||||||
this.rmqService.nack(ctx)
|
this.rmqService.nack(ctx, event)
|
||||||
|
} finally {
|
||||||
|
endTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,12 +97,30 @@ export class NotificationsController {
|
|||||||
@Payload() data: EmailChangedEvent,
|
@Payload() data: EmailChangedEvent,
|
||||||
@Ctx() ctx: RmqContext
|
@Ctx() ctx: RmqContext
|
||||||
) {
|
) {
|
||||||
|
const event = 'account.email.changed'
|
||||||
|
const endTimer = this.processingDuration.startTimer({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.notificationsService.emailChanged(data)
|
await this.notificationsService.emailChanged(data)
|
||||||
this.rmqService.ack(ctx)
|
this.eventsTotal.inc({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event,
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
this.rmqService.ack(ctx, event)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.eventsTotal.inc({
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
event,
|
||||||
|
status: 'error'
|
||||||
|
})
|
||||||
this.logger.error(`Email changing processing error ${e.message ?? e}}`)
|
this.logger.error(`Email changing processing error ${e.message ?? e}}`)
|
||||||
this.rmqService.nack(ctx)
|
this.rmqService.nack(ctx, event)
|
||||||
|
} finally {
|
||||||
|
endTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/observability/metrics/metrics.module.ts
Normal file
48
src/observability/metrics/metrics.module.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common'
|
||||||
|
import {
|
||||||
|
makeCounterProvider,
|
||||||
|
makeHistogramProvider,
|
||||||
|
PrometheusModule
|
||||||
|
} from '@willsoto/nestjs-prometheus'
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
PrometheusModule.register({
|
||||||
|
path: '/metrics',
|
||||||
|
defaultMetrics: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
makeHistogramProvider({
|
||||||
|
name: 'rmq_event_processing_duration_seconds',
|
||||||
|
help: 'RabbitMQ event processing duration',
|
||||||
|
labelNames: ['service', 'event'],
|
||||||
|
buckets: [0.05, 0.1, 0.2, 0.5, 1, 2, 5]
|
||||||
|
}),
|
||||||
|
makeCounterProvider({
|
||||||
|
name: 'rmq_events_total',
|
||||||
|
help: 'Total RMQ events processed',
|
||||||
|
labelNames: ['service', 'event', 'status']
|
||||||
|
}),
|
||||||
|
makeCounterProvider({
|
||||||
|
name: 'rmq_events_ack_total',
|
||||||
|
help: 'Total ACKed RMQ events',
|
||||||
|
labelNames: ['service', 'event']
|
||||||
|
}),
|
||||||
|
makeCounterProvider({
|
||||||
|
name: 'rmq_events_nack_total',
|
||||||
|
help: 'Total NACKed RMQ events',
|
||||||
|
labelNames: ['service', 'event']
|
||||||
|
})
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
'PROM_METRIC_RMQ_EVENT_PROCESSING_DURATION_SECONDS',
|
||||||
|
'PROM_METRIC_RMQ_EVENTS_TOTAL',
|
||||||
|
'PROM_METRIC_RMQ_EVENTS_ACK_TOTAL',
|
||||||
|
'PROM_METRIC_RMQ_EVENTS_NACK_TOTAL'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class MetricsModule {}
|
||||||
8
src/observability/observability.module.ts
Normal file
8
src/observability/observability.module.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { MetricsModule } from './metrics/metrics.module'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [MetricsModule]
|
||||||
|
})
|
||||||
|
export class ObservabilityModule {}
|
||||||
30
yarn.lock
30
yarn.lock
@ -1150,6 +1150,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323"
|
resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323"
|
||||||
integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==
|
integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==
|
||||||
|
|
||||||
|
"@opentelemetry/api@^1.4.0":
|
||||||
|
version "1.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
|
||||||
|
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
|
||||||
|
|
||||||
"@paralleldrive/cuid2@^2.2.2":
|
"@paralleldrive/cuid2@^2.2.2":
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz#3d62ea9e7be867d3fa94b9897fab5b0ae187d784"
|
resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz#3d62ea9e7be867d3fa94b9897fab5b0ae187d784"
|
||||||
@ -1813,6 +1818,11 @@
|
|||||||
"@webassemblyjs/ast" "1.14.1"
|
"@webassemblyjs/ast" "1.14.1"
|
||||||
"@xtuc/long" "4.2.2"
|
"@xtuc/long" "4.2.2"
|
||||||
|
|
||||||
|
"@willsoto/nestjs-prometheus@^6.0.2":
|
||||||
|
version "6.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@willsoto/nestjs-prometheus/-/nestjs-prometheus-6.0.2.tgz#d603764a923848442ed092411716c0bf211de01f"
|
||||||
|
integrity sha512-ePyLZYdIrOOdlOWovzzMisIgviXqhPVzFpSMKNNhn6xajhRHeBsjAzSdpxZTc6pnjR9hw1lNAHyKnKl7lAPaVg==
|
||||||
|
|
||||||
"@xtuc/ieee754@^1.2.0":
|
"@xtuc/ieee754@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||||
@ -2151,6 +2161,11 @@ binary-extensions@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||||
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
|
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
|
||||||
|
|
||||||
|
bintrees@1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8"
|
||||||
|
integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==
|
||||||
|
|
||||||
bl@^4.1.0:
|
bl@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
||||||
@ -5509,6 +5524,14 @@ preview-email@^3.0.19:
|
|||||||
pug "^3.0.3"
|
pug "^3.0.3"
|
||||||
uuid "^9.0.1"
|
uuid "^9.0.1"
|
||||||
|
|
||||||
|
prom-client@^15.1.3:
|
||||||
|
version "15.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.3.tgz#69fa8de93a88bc9783173db5f758dc1c69fa8fc2"
|
||||||
|
integrity sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/api" "^1.4.0"
|
||||||
|
tdigest "^0.1.1"
|
||||||
|
|
||||||
promise-breaker@^6.0.0:
|
promise-breaker@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/promise-breaker/-/promise-breaker-6.0.0.tgz#107d2b70f161236abdb4ac5a736c7eb8df489d0f"
|
resolved "https://registry.yarnpkg.com/promise-breaker/-/promise-breaker-6.0.0.tgz#107d2b70f161236abdb4ac5a736c7eb8df489d0f"
|
||||||
@ -6210,6 +6233,13 @@ tapable@^2.2.1, tapable@^2.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6"
|
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6"
|
||||||
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
|
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
|
||||||
|
|
||||||
|
tdigest@^0.1.1:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced"
|
||||||
|
integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==
|
||||||
|
dependencies:
|
||||||
|
bintrees "1.0.2"
|
||||||
|
|
||||||
terser-webpack-plugin@^5.3.16:
|
terser-webpack-plugin@^5.3.16:
|
||||||
version "5.3.16"
|
version "5.3.16"
|
||||||
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz#741e448cc3f93d8026ebe4f7ef9e4afacfd56330"
|
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz#741e448cc3f93d8026ebe4f7ef9e4afacfd56330"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user