dicoshot-nest 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +415 -0
- package/README.md +387 -69
- package/dist/app.controller.js.map +1 -1
- package/dist/app.module.js.map +1 -1
- package/dist/decorators/dicoshot-notify.decorator.d.ts +8 -0
- package/dist/decorators/dicoshot-notify.decorator.js +8 -0
- package/dist/decorators/dicoshot-notify.decorator.js.map +1 -0
- package/dist/dicoshot.constants.d.ts +1 -0
- package/dist/dicoshot.constants.js +2 -1
- package/dist/dicoshot.constants.js.map +1 -1
- package/dist/dicoshot.listener.js.map +1 -1
- package/dist/dicoshot.module.js +3 -0
- package/dist/dicoshot.module.js.map +1 -1
- package/dist/dicoshot.service.d.ts +6 -3
- package/dist/dicoshot.service.js +17 -8
- package/dist/dicoshot.service.js.map +1 -1
- package/dist/filters/dicoshot-exception.filter.js +16 -8
- package/dist/filters/dicoshot-exception.filter.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/interceptors/dicoshot-notify.interceptor.d.ts +10 -0
- package/dist/interceptors/dicoshot-notify.interceptor.js +45 -0
- package/dist/interceptors/dicoshot-notify.interceptor.js.map +1 -0
- package/dist/interceptors/dicoshot.interceptor.d.ts +1 -1
- package/dist/interceptors/dicoshot.interceptor.js +37 -24
- package/dist/interceptors/dicoshot.interceptor.js.map +1 -1
- package/dist/main.js.map +1 -1
- package/dist/utils/stack.util.d.ts +3 -0
- package/dist/utils/stack.util.js +20 -0
- package/dist/utils/stack.util.js.map +1 -0
- package/package.json +9 -19
- package/dist/tsconfig.build.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -1,98 +1,416 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## Project setup
|
|
1
|
+
# dicoshot-nest
|
|
2
|
+
|
|
3
|
+
English | [한국어](README.ko.md)
|
|
4
|
+
|
|
5
|
+
An SDK that automatically notifies a Discord channel about NestJS application startup/shutdown, unhandled exceptions, and slow responses. Works by simply registering the module.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Automatic notifications**: Startup/shutdown notifications via `OnApplicationBootstrap`/`OnApplicationShutdown` hooks, no extra code required
|
|
10
|
+
- **Automatic exception notifications**: A global `ExceptionFilter` immediately sends unhandled exceptions to Discord (can include stack trace and request body), with the exact throw site (`file:line:col`) surfaced in a dedicated `Location` field
|
|
11
|
+
- **Slow response notifications**: A `NestInterceptor` notifies when response time exceeds a threshold
|
|
12
|
+
- **Custom messages**: Inject `DicoshotService` to send arbitrary Discord messages at any time, or annotate a handler with `@DicoshotNotify()` to send one automatically on success
|
|
13
|
+
- **Failure isolation**: Webhook delivery failures never block app startup/shutdown/request handling (only a WARN log is emitted)
|
|
14
|
+
- **MSA-friendly**: hostname and applicationName are automatically included in messages, making it easy to tell instances apart
|
|
15
|
+
- **Automatic environment detection**: `NODE_ENV` and `npm_package_version` are included automatically
|
|
16
|
+
- **Duplicate notification prevention**: The same error won't be notified twice even when filter and interceptor are both enabled
|
|
17
|
+
- **Localized notifications**: Titles and field labels (Service, Environment, Status, Location, ...) can be sent in English, Korean, Japanese, or Chinese via `locale` — or any other language by passing your own translations
|
|
18
|
+
|
|
19
|
+
## Module structure
|
|
20
|
+
|
|
21
|
+
| Module | Description |
|
|
22
|
+
| --------------- | ----------------------------------------------------------------------------------- |
|
|
23
|
+
| `dicoshot-core` | Pure TypeScript with no NestJS dependency. Message model, webhook client |
|
|
24
|
+
| `dicoshot-nest` | NestJS DynamicModule, lifecycle hooks, ExceptionFilter, Interceptor |
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
29
27
|
|
|
30
28
|
```bash
|
|
31
|
-
|
|
29
|
+
npm install dicoshot-nest
|
|
32
30
|
```
|
|
33
31
|
|
|
34
|
-
##
|
|
32
|
+
## Quick start
|
|
35
33
|
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
```typescript
|
|
35
|
+
// app.module.ts
|
|
36
|
+
import { Module } from '@nestjs/common';
|
|
37
|
+
import { DicoshotModule } from 'dicoshot-nest';
|
|
38
|
+
|
|
39
|
+
@Module({
|
|
40
|
+
imports: [
|
|
41
|
+
DicoshotModule.register({
|
|
42
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
43
|
+
applicationName: 'my-service',
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
46
|
+
})
|
|
47
|
+
export class AppModule {}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
When you run the app, the following embeds arrive in the Discord channel.
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
- **On startup**: green embed, title "🟢 Application Started", includes service name/environment/version/hostname/timestamp
|
|
53
|
+
- **On shutdown**: red embed, title "🔴 Application Stopped"
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
## ConfigService integration
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
DicoshotModule.registerAsync({
|
|
59
|
+
imports: [ConfigModule],
|
|
60
|
+
useFactory: (config: ConfigService) => ({
|
|
61
|
+
webhookUrl: config.get('DISCORD_WEBHOOK_URL'),
|
|
62
|
+
applicationName: config.get('APP_NAME'),
|
|
63
|
+
}),
|
|
64
|
+
inject: [ConfigService],
|
|
65
|
+
// filter/interceptor must be specified separately at the registerAsync() call site.
|
|
66
|
+
// (They determine whether APP_FILTER/APP_INTERCEPTOR are registered on the
|
|
67
|
+
// DynamicModule, so they're fixed at registration time regardless of the
|
|
68
|
+
// async factory's result.)
|
|
69
|
+
filter: true,
|
|
70
|
+
interceptor: { slowThreshold: 2000 },
|
|
71
|
+
});
|
|
45
72
|
```
|
|
46
73
|
|
|
47
|
-
##
|
|
74
|
+
## Sending custom messages directly
|
|
48
75
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
76
|
+
Inject `DicoshotService` to send Discord messages from anywhere in your code.
|
|
77
|
+
|
|
78
|
+
### sendCustom() — convenience method
|
|
52
79
|
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
```typescript
|
|
81
|
+
@Injectable()
|
|
82
|
+
export class DeployService {
|
|
83
|
+
constructor(private readonly dicoshot: DicoshotService) {}
|
|
55
84
|
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
async onDeployComplete(version: string) {
|
|
86
|
+
await this.dicoshot.sendCustom({
|
|
87
|
+
title: 'Deploy complete',
|
|
88
|
+
description: `v${version} deployed successfully`,
|
|
89
|
+
color: 'success', // 'success' | 'danger' | 'warning' | 'info'
|
|
90
|
+
fields: [{ name: 'Version', value: `v${version}`, inline: true }],
|
|
91
|
+
mention: '<@&123456789012345678>', // appended after the description
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
58
95
|
```
|
|
59
96
|
|
|
60
|
-
|
|
97
|
+
Color presets:
|
|
61
98
|
|
|
62
|
-
|
|
99
|
+
| Value | Color |
|
|
100
|
+
| ----------- | ------ |
|
|
101
|
+
| `'success'` | Green |
|
|
102
|
+
| `'danger'` | Red |
|
|
103
|
+
| `'warning'` | Yellow |
|
|
104
|
+
| `'info'` | Blue |
|
|
63
105
|
|
|
64
|
-
|
|
106
|
+
You can also specify a hex value directly: `color: 0xFF0000`
|
|
65
107
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
108
|
+
### send() — raw method
|
|
109
|
+
|
|
110
|
+
Build the Discord embed structure yourself and send it.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
await this.dicoshot.send({
|
|
114
|
+
embeds: [
|
|
115
|
+
{
|
|
116
|
+
title: 'Notification',
|
|
117
|
+
description: '...',
|
|
118
|
+
color: 0x57f287,
|
|
119
|
+
fields: [{ name: 'Version', value: 'v1.2.3', inline: true }],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
> Neither method throws. Both resolve to a `boolean` indicating whether the message was delivered; failures are logged as a WARN, consistent with the rest of the SDK's failure-isolation behavior.
|
|
126
|
+
|
|
127
|
+
### `@DicoshotNotify()` — decorator
|
|
128
|
+
|
|
129
|
+
Annotate a controller (or any Nest-pipeline) handler to send a custom message automatically once it resolves successfully. No `@UseInterceptors()` setup needed — the interceptor is registered globally and is a no-op on handlers without the decorator. Nothing is sent if the handler throws (use `filter`/`interceptor` for error notifications).
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
@Controller('orders')
|
|
133
|
+
export class OrderController {
|
|
134
|
+
@Post()
|
|
135
|
+
@DicoshotNotify({ title: 'New order created', color: 'success' })
|
|
136
|
+
create(@Body() dto: CreateOrderDto) {
|
|
137
|
+
return this.orderService.create(dto);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Post(':id/deploy')
|
|
141
|
+
@DicoshotNotify({
|
|
142
|
+
title: (args) => `Deploy complete: ${args[0]}`, // args = handler arguments, in order
|
|
143
|
+
description: (_args, result) => `Result: ${JSON.stringify(result)}`,
|
|
144
|
+
color: 'success',
|
|
145
|
+
})
|
|
146
|
+
deploy(@Param('id') id: string) {
|
|
147
|
+
return this.orderService.deploy(id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
69
150
|
```
|
|
70
151
|
|
|
71
|
-
|
|
152
|
+
`title`/`description` accept either a static string or a `(args, result) => string` function, where `args` is the handler's argument array and `result` is its return value.
|
|
153
|
+
|
|
154
|
+
## Automatic exception notifications (`filter`)
|
|
155
|
+
|
|
156
|
+
Enabling the `filter` option registers `DicoshotExceptionFilter` as a global `APP_FILTER`. By default, it notifies Discord for unhandled exceptions and any error with an HTTP status of `500` or above (4xx `HttpException`s like `NotFoundException` are not notified unless `minStatus` is lowered). Non-HTTP contexts (WebSocket, RPC, etc.) are ignored and not notified.
|
|
157
|
+
|
|
158
|
+
When a stack trace is available, the notification includes a `Location` field with the exact `file:line:col` where the error was thrown — extracted from the top stack frame — so you don't have to scan the full `Stack Trace` block to find it.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
DicoshotModule.register({
|
|
162
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
163
|
+
applicationName: 'order-service',
|
|
164
|
+
filter: true, // or a FilterOptions object
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This is what arrives in Discord:
|
|
169
|
+
|
|
170
|
+
> 🚨 **[production] order-service — TypeError**
|
|
171
|
+
> `Cannot read properties of undefined (reading 'id')`
|
|
172
|
+
>
|
|
173
|
+
> **Service** `order-service` **Environment** `production` **Status** `500`
|
|
174
|
+
> **Method** `POST` **Path** `/orders`
|
|
175
|
+
>
|
|
176
|
+
> **Location**
|
|
177
|
+
> `/app/src/order/order.service.ts:42:18`
|
|
178
|
+
>
|
|
179
|
+
> **Request Body**
|
|
180
|
+
> ```json
|
|
181
|
+
> {"productId":"abc123","quantity":2}
|
|
182
|
+
> ```
|
|
183
|
+
>
|
|
184
|
+
> **Stack Trace**
|
|
185
|
+
> ```
|
|
186
|
+
> TypeError: Cannot read properties of undefined (reading 'id')
|
|
187
|
+
> at OrderService.create (/app/src/order/order.service.ts:42:18)
|
|
188
|
+
> at OrderController.create (/app/src/order/order.controller.ts:15:30)
|
|
189
|
+
> ```
|
|
190
|
+
|
|
191
|
+
`Location` is always the very first line of `Stack Trace` — it's pulled out into its own field so you can see where the error actually happened without scrolling through the rest of the trace.
|
|
192
|
+
|
|
193
|
+
### FilterOptions
|
|
72
194
|
|
|
73
|
-
|
|
195
|
+
| Key | Default | Description |
|
|
196
|
+
| ---------------- | ------- | ------------------------------------------------------------------------------------ |
|
|
197
|
+
| `minStatus` | `500` | Only notify for status codes at or above this value (unhandled exceptions are always treated as `500`) |
|
|
198
|
+
| `ignore` | - | Array of HTTP status codes to skip notifying (e.g. `[404]`) |
|
|
199
|
+
| `environment` | - | Only notify in this environment (`string` or `string[]`, based on `NODE_ENV`) |
|
|
200
|
+
| `mention` | - | Mention string to add to the embed body (e.g. `'<@&ROLE_ID>'`) |
|
|
201
|
+
| `throttle` | - | Suppress repeated notifications for the same error (class+method+path) for N seconds |
|
|
202
|
+
| `includeStack` | `true` | Whether to include the stack trace |
|
|
203
|
+
| `includeRequest` | `true` | Whether to include the request body |
|
|
74
204
|
|
|
75
|
-
|
|
205
|
+
```typescript
|
|
206
|
+
DicoshotModule.register({
|
|
207
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
208
|
+
filter: {
|
|
209
|
+
ignore: [404, 401],
|
|
210
|
+
environment: 'production',
|
|
211
|
+
mention: '<@&123456789012345678>',
|
|
212
|
+
throttle: 60, // notify the same error at most once every 60 seconds
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Error notifications are sent to `webhooks.error` if configured, otherwise to the default `webhookUrl`.
|
|
218
|
+
|
|
219
|
+
## Slow response / error notifications (`interceptor`)
|
|
220
|
+
|
|
221
|
+
Enabling the `interceptor` option registers `DicoshotInterceptor` as a global `APP_INTERCEPTOR`, notifying Discord when response time exceeds the threshold. When `filter` is not enabled, the interceptor also handles error notifications (when `filter` is enabled, the interceptor skips error notifications to avoid duplicates) — including the same `Location` and `Stack Trace` fields described above.
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
DicoshotModule.register({
|
|
225
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
226
|
+
interceptor: true, // or an InterceptorOptions object
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### InterceptorOptions
|
|
231
|
+
|
|
232
|
+
| Key | Default | Description |
|
|
233
|
+
| --------------- | ------- | ---------------------------------------------------------------------- |
|
|
234
|
+
| `slowThreshold` | `3000` | Threshold (ms) for considering a response slow |
|
|
235
|
+
| `excludePaths` | - | Array of path prefixes excluded from notifications (e.g. `['/health']`) |
|
|
236
|
+
| `onlyErrors` | `false` | If `true`, disables slow response notifications and only sends error notifications |
|
|
237
|
+
| `minStatus` | `500` | Only notify error responses at or above this status (unhandled exceptions are always treated as `500`) |
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
DicoshotModule.register({
|
|
241
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
242
|
+
interceptor: {
|
|
243
|
+
slowThreshold: 1500,
|
|
244
|
+
excludePaths: ['/health', '/metrics'],
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
```
|
|
76
248
|
|
|
77
|
-
|
|
78
|
-
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
|
79
|
-
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
|
80
|
-
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
|
81
|
-
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
|
82
|
-
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
|
83
|
-
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
|
84
|
-
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
|
249
|
+
Slow response notifications are sent to `webhooks.slow` if configured, otherwise to the default `webhookUrl`.
|
|
85
250
|
|
|
86
|
-
##
|
|
251
|
+
## Webhook retry (`retry`)
|
|
252
|
+
|
|
253
|
+
If a webhook delivery fails (network error, Discord 5xx, rate limit, etc.), it's retried with exponential backoff. If Discord returns `429` with a `Retry-After` header, that value takes priority; otherwise the wait is `backoffMs * 2^attempt`.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
DicoshotModule.register({
|
|
257
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
258
|
+
retry: true, // or a RetryOptions object. Disabled by default (no retries)
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### RetryOptions
|
|
263
|
+
|
|
264
|
+
| Key | Default | Description |
|
|
265
|
+
| ----------- | ------- | -------------------------------------------------------------------- |
|
|
266
|
+
| `attempts` | `2` | Number of retries after the initial attempt fails |
|
|
267
|
+
| `backoffMs` | `500` | Wait time (ms) before the first retry. Doubles on each subsequent attempt |
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
DicoshotModule.register({
|
|
271
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
272
|
+
retry: { attempts: 3, backoffMs: 300 },
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
If all retries fail, the final error is propagated to the caller. `DicoshotListener`/`DicoshotExceptionFilter`/`DicoshotInterceptor` catch this error and only emit a WARN log, so it doesn't affect app behavior.
|
|
277
|
+
|
|
278
|
+
## Configuration
|
|
279
|
+
|
|
280
|
+
| Key | Default | Description |
|
|
281
|
+
| ------------------ | ------------ | ----------------------------------------------------------------------------------------- |
|
|
282
|
+
| `webhookUrl` | - | Discord webhook URL. Auto-disabled if not set |
|
|
283
|
+
| `enabled` | `true` | Global enable toggle (applies to startup/shutdown notifications) |
|
|
284
|
+
| `notifyOnStartup` | `true` | Whether to send a startup notification |
|
|
285
|
+
| `notifyOnShutdown` | `true` | Whether to send a shutdown notification |
|
|
286
|
+
| `applicationName` | - | Service name shown in the embed |
|
|
287
|
+
| `locale` | `'en'` | Language for notification titles/field labels: `'en'` \| `'ko'` \| `'ja'` \| `'zh'`, or a custom [`DicoshotMessages`](#example-custom-locale) object |
|
|
288
|
+
| `timeoutMs` | `5000` | HTTP timeout (ms) |
|
|
289
|
+
| `webhooks.error` | - | Dedicated webhook URL for exception notifications (falls back to `webhookUrl`) |
|
|
290
|
+
| `webhooks.slow` | - | Dedicated webhook URL for slow response notifications (falls back to `webhookUrl`) |
|
|
291
|
+
| `filter` | `false` | Enable automatic exception notifications (`boolean` or [`FilterOptions`](#filteroptions)) |
|
|
292
|
+
| `interceptor` | `false` | Enable slow response/error notifications (`boolean` or [`InterceptorOptions`](#interceptoroptions)) |
|
|
293
|
+
| `retry` | `false` | Enable retries on webhook delivery failure (`boolean` or [`RetryOptions`](#retryoptions)) |
|
|
294
|
+
|
|
295
|
+
### Example: notify only on startup
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
DicoshotModule.register({
|
|
299
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
300
|
+
notifyOnShutdown: false,
|
|
301
|
+
applicationName: 'order-service',
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Example: notifications in another language
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
DicoshotModule.register({
|
|
309
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
310
|
+
applicationName: '주문-서비스',
|
|
311
|
+
locale: 'ko', // 'en' | 'ko' | 'ja' | 'zh'
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Titles and field labels (Service, Environment, Status, Location, ...) are translated. Values that come from your runtime — the exception class name (`TypeError`, `NotFoundException`, ...), the error message, the stack trace, the request path — are sent as-is and aren't translated.
|
|
316
|
+
|
|
317
|
+
### Example: custom locale
|
|
318
|
+
|
|
319
|
+
Not all of your team's users speak one of the built-in languages? Pass a full `DicoshotMessages` object instead of a locale code and every title/field label is taken from it.
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { DicoshotMessages } from 'dicoshot-nest';
|
|
323
|
+
|
|
324
|
+
const fr: DicoshotMessages = {
|
|
325
|
+
startupTitle: '🟢 Application démarrée',
|
|
326
|
+
shutdownTitle: '🔴 Application arrêtée',
|
|
327
|
+
slowResponseLabel: 'Réponse lente',
|
|
328
|
+
field: {
|
|
329
|
+
service: 'Service',
|
|
330
|
+
environment: 'Environnement',
|
|
331
|
+
version: 'Version',
|
|
332
|
+
hostname: 'Hôte',
|
|
333
|
+
time: 'Heure',
|
|
334
|
+
status: 'Statut',
|
|
335
|
+
method: 'Méthode',
|
|
336
|
+
path: 'Chemin',
|
|
337
|
+
duration: 'Durée',
|
|
338
|
+
location: 'Emplacement',
|
|
339
|
+
stackTrace: 'Pile d\'appels',
|
|
340
|
+
requestBody: 'Corps de la requête',
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
DicoshotModule.register({
|
|
345
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
346
|
+
locale: fr,
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Example: separate channels for errors and slow responses
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
DicoshotModule.register({
|
|
354
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL, // for startup/shutdown notifications
|
|
355
|
+
webhooks: {
|
|
356
|
+
error: process.env.DISCORD_ERROR_WEBHOOK_URL, // for exception notifications
|
|
357
|
+
slow: process.env.DISCORD_SLOW_WEBHOOK_URL, // for slow response notifications
|
|
358
|
+
},
|
|
359
|
+
filter: { environment: 'production' },
|
|
360
|
+
interceptor: { slowThreshold: 2000 },
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Example: inject the webhook URL via an environment variable
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
DicoshotModule.register({
|
|
368
|
+
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`webhookUrl` is optional — if it's unset or empty, the SDK is automatically disabled, so no errors occur in local development.
|
|
373
|
+
|
|
374
|
+
## How it works
|
|
375
|
+
|
|
376
|
+
1. `DicoshotModule.register()`/`registerAsync()` registers the options in the NestJS DI container.
|
|
377
|
+
2. `DicoshotNotifyInterceptor` is always registered globally as `APP_INTERCEPTOR`; it's a no-op except on handlers annotated with `@DicoshotNotify()`. If the `filter`/`interceptor` options are enabled, `DicoshotExceptionFilter`/`DicoshotInterceptor` are also registered globally as `APP_FILTER`/`APP_INTERCEPTOR`.
|
|
378
|
+
3. `DicoshotListener` subscribes to NestJS lifecycle hooks and sends startup/shutdown messages at `onApplicationBootstrap()`/`onApplicationShutdown()`.
|
|
379
|
+
4. When an exception occurs during request handling, `DicoshotExceptionFilter` (or `DicoshotInterceptor` if `filter` is disabled) sends an error message.
|
|
380
|
+
5. When response time exceeds `slowThreshold`, `DicoshotInterceptor` sends a slow response message.
|
|
381
|
+
6. When a handler annotated with `@DicoshotNotify()` resolves successfully, `DicoshotNotifyInterceptor` sends the configured custom message.
|
|
382
|
+
7. Every webhook call swallows its own exceptions and only emits a WARN log on failure, so app behavior is never affected.
|
|
383
|
+
|
|
384
|
+
## MSA environments
|
|
385
|
+
|
|
386
|
+
- Each service uses `dicoshot-nest` independently.
|
|
387
|
+
- `applicationName` and `hostname` are automatically included in messages, making it easy to identify which service and which instance sent them.
|
|
388
|
+
- In environments with frequent pod restarts (e.g. K8s), you can disable only startup notifications with `notifyOnStartup: false`, or disable everything with `enabled: false`.
|
|
389
|
+
- Configure `filter.throttle` per service to keep a repeated error from flooding the Discord channel.
|
|
390
|
+
|
|
391
|
+
## Requirements
|
|
392
|
+
|
|
393
|
+
- Node.js 18+
|
|
394
|
+
- NestJS 10+
|
|
395
|
+
|
|
396
|
+
## Build
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
# Build everything
|
|
400
|
+
npm run build --workspaces
|
|
401
|
+
|
|
402
|
+
# Build individually
|
|
403
|
+
cd dicoshot-core && npm run build
|
|
404
|
+
cd dicoshot-nest && npm run build
|
|
405
|
+
```
|
|
87
406
|
|
|
88
|
-
|
|
407
|
+
## Out of scope (current version)
|
|
89
408
|
|
|
90
|
-
|
|
409
|
+
The following features are not yet included.
|
|
91
410
|
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
|
411
|
+
- Asynchronous queueing (persisting messages that fail permanently beyond retries)
|
|
412
|
+
- Simultaneous delivery to multiple webhooks or other platforms like Slack
|
|
95
413
|
|
|
96
414
|
## License
|
|
97
415
|
|
|
98
|
-
|
|
416
|
+
[MIT License](LICENSE) © 2026 DicoShot
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.controller.js","sourceRoot":"","sources":["../src/app.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;
|
|
1
|
+
{"version":3,"file":"app.controller.js","sourceRoot":"","sources":["../src/app.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;AAEjD,+CAA2C;AAGpC,IAAM,aAAa,GAAnB,MAAM,aAAa;IACK;IAA7B,YAA6B,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAGvD,QAAQ;QACN,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;IACpC,CAAC;CACF,CAAA;AAPY,sCAAa;AAIxB;IADC,IAAA,YAAG,GAAE;;;;6CAGL;wBANU,aAAa;IADzB,IAAA,mBAAU,GAAE;qCAE8B,wBAAU;GADxC,aAAa,CAOzB"}
|
package/dist/app.module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;
|
|
1
|
+
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AAExC,qDAAiD;AACjD,+CAA2C;AAOpC,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IALrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,EAAE;QACX,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,CAAC;KACxB,CAAC;GACW,SAAS,CAAG"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ColorPreset } from '../dicoshot.service';
|
|
2
|
+
export type DicoshotNotifyResolver<T> = T | ((args: unknown[], result: unknown) => T);
|
|
3
|
+
export interface DicoshotNotifyOptions {
|
|
4
|
+
title: DicoshotNotifyResolver<string>;
|
|
5
|
+
description?: DicoshotNotifyResolver<string>;
|
|
6
|
+
color?: ColorPreset | number;
|
|
7
|
+
}
|
|
8
|
+
export declare const DicoshotNotify: (options: DicoshotNotifyOptions) => MethodDecorator;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DicoshotNotify = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const dicoshot_constants_1 = require("../dicoshot.constants");
|
|
6
|
+
const DicoshotNotify = (options) => (0, common_1.SetMetadata)(dicoshot_constants_1.DICOSHOT_NOTIFY_METADATA, options);
|
|
7
|
+
exports.DicoshotNotify = DicoshotNotify;
|
|
8
|
+
//# sourceMappingURL=dicoshot-notify.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dicoshot-notify.decorator.js","sourceRoot":"","sources":["../../src/decorators/dicoshot-notify.decorator.ts"],"names":[],"mappings":";;;AAAA,2CAA6C;AAG7C,8DAAiE;AAc1D,MAAM,cAAc,GAAG,CAAC,OAA8B,EAAmB,EAAE,CAChF,IAAA,oBAAW,EAAC,6CAAwB,EAAE,OAAO,CAAC,CAAC;AADpC,QAAA,cAAc,kBACsB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DICOSHOT_CLIENT = exports.DICOSHOT_OPTIONS = void 0;
|
|
3
|
+
exports.DICOSHOT_NOTIFY_METADATA = exports.DICOSHOT_CLIENT = exports.DICOSHOT_OPTIONS = void 0;
|
|
4
4
|
exports.DICOSHOT_OPTIONS = 'DICOSHOT_OPTIONS';
|
|
5
5
|
exports.DICOSHOT_CLIENT = 'DICOSHOT_CLIENT';
|
|
6
|
+
exports.DICOSHOT_NOTIFY_METADATA = 'DICOSHOT_NOTIFY_METADATA';
|
|
6
7
|
//# sourceMappingURL=dicoshot.constants.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dicoshot.constants.js","sourceRoot":"","sources":["../src/dicoshot.constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,gBAAgB,GAAG,kBAAkB,CAAC;AACtC,QAAA,eAAe,GAAG,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"dicoshot.constants.js","sourceRoot":"","sources":["../src/dicoshot.constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,gBAAgB,GAAG,kBAAkB,CAAC;AACtC,QAAA,eAAe,GAAG,iBAAiB,CAAC;AACpC,QAAA,wBAAwB,GAAG,0BAA0B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dicoshot.listener.js","sourceRoot":"","sources":["../src/dicoshot.listener.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAMwB;
|
|
1
|
+
{"version":3,"file":"dicoshot.listener.js","sourceRoot":"","sources":["../src/dicoshot.listener.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAMwB;AAGxB,iDAAmE;AAEnE,6DAAyE;AAGlE,IAAM,gBAAgB,wBAAtB,MAAM,gBAAgB;IAKkB;IACD;IAL3B,MAAM,GAAG,IAAI,eAAM,CAAC,kBAAgB,CAAC,IAAI,CAAC,CAAC;IAC3C,OAAO,CAAiB;IAEzC,YAC6C,OAAwB,EACzB,MAA0B;QADzB,YAAO,GAAP,OAAO,CAAiB;QACzB,WAAM,GAAN,MAAM,CAAoB;QAEpE,IAAI,CAAC,OAAO,GAAG,IAAI,8BAAc,CAAC,OAAO,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,sBAAsB;QAC1B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,KAAK;YAAE,OAAO;QACvE,IAAI,IAAI,CAAC,OAAO,CAAC,eAAe,KAAK,KAAK;YAAE,OAAO;QACnD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QACjD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,wCAAyC,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;IAED,KAAK,CAAC,qBAAqB;QACzB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,KAAK;YAAE,OAAO;QACvE,IAAI,IAAI,CAAC,OAAO,CAAC,gBAAgB,KAAK,KAAK;YAAE,OAAO;QACpD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;QAClD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yCAA0C,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;CACF,CAAA;AA9BY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,mBAAU,GAAE;IAMR,WAAA,IAAA,eAAM,EAAC,qCAAgB,CAAC,CAAA;IACxB,WAAA,IAAA,eAAM,EAAC,oCAAe,CAAC,CAAA;6CAA0B,kCAAkB;GAN3D,gBAAgB,CA8B5B"}
|
package/dist/dicoshot.module.js
CHANGED
|
@@ -15,6 +15,7 @@ const dicoshot_constants_1 = require("./dicoshot.constants");
|
|
|
15
15
|
const dicoshot_listener_1 = require("./dicoshot.listener");
|
|
16
16
|
const dicoshot_service_1 = require("./dicoshot.service");
|
|
17
17
|
const dicoshot_exception_filter_1 = require("./filters/dicoshot-exception.filter");
|
|
18
|
+
const dicoshot_notify_interceptor_1 = require("./interceptors/dicoshot-notify.interceptor");
|
|
18
19
|
const dicoshot_interceptor_1 = require("./interceptors/dicoshot.interceptor");
|
|
19
20
|
const clientProvider = {
|
|
20
21
|
provide: dicoshot_constants_1.DICOSHOT_CLIENT,
|
|
@@ -27,6 +28,7 @@ function buildProviders(options) {
|
|
|
27
28
|
clientProvider,
|
|
28
29
|
dicoshot_listener_1.DicoshotListener,
|
|
29
30
|
dicoshot_service_1.DicoshotService,
|
|
31
|
+
{ provide: core_1.APP_INTERCEPTOR, useClass: dicoshot_notify_interceptor_1.DicoshotNotifyInterceptor },
|
|
30
32
|
];
|
|
31
33
|
if (options.filter) {
|
|
32
34
|
providers.push({ provide: core_1.APP_FILTER, useClass: dicoshot_exception_filter_1.DicoshotExceptionFilter });
|
|
@@ -62,6 +64,7 @@ let DicoshotModule = DicoshotModule_1 = class DicoshotModule {
|
|
|
62
64
|
clientProvider,
|
|
63
65
|
dicoshot_listener_1.DicoshotListener,
|
|
64
66
|
dicoshot_service_1.DicoshotService,
|
|
67
|
+
{ provide: core_1.APP_INTERCEPTOR, useClass: dicoshot_notify_interceptor_1.DicoshotNotifyInterceptor },
|
|
65
68
|
];
|
|
66
69
|
if (filter) {
|
|
67
70
|
providers.push({ provide: core_1.APP_FILTER, useClass: dicoshot_exception_filter_1.DicoshotExceptionFilter });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dicoshot.module.js","sourceRoot":"","sources":["../src/dicoshot.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,
|
|
1
|
+
{"version":3,"file":"dicoshot.module.js","sourceRoot":"","sources":["../src/dicoshot.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAAiF;AACjF,uCAA2D;AAE3D,iDAKuB;AAEvB,6DAAyE;AACzE,2DAAuD;AACvD,yDAAqD;AACrD,mFAA8E;AAC9E,4FAAuF;AACvF,8EAA0E;AAiB1E,MAAM,cAAc,GAAa;IAC/B,OAAO,EAAE,oCAAe;IACxB,UAAU,EAAE,CAAC,OAAwB,EAAE,EAAE,CAAC,IAAI,kCAAkB,CAAC,OAAO,CAAC;IACzE,MAAM,EAAE,CAAC,qCAAgB,CAAC;CAC3B,CAAC;AAEF,SAAS,cAAc,CAAC,OAAwB;IAC9C,MAAM,SAAS,GAAe;QAC5B,EAAE,OAAO,EAAE,qCAAgB,EAAE,QAAQ,EAAE,OAAO,EAAE;QAChD,cAAc;QACd,oCAAgB;QAChB,kCAAe;QAEf,EAAE,OAAO,EAAE,sBAAe,EAAE,QAAQ,EAAE,uDAAyB,EAAE;KAClE,CAAC;IAEF,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,iBAAU,EAAE,QAAQ,EAAE,mDAAuB,EAAE,CAAC,CAAC;IAC7E,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,sBAAe,EAAE,QAAQ,EAAE,0CAAmB,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAGM,IAAM,cAAc,sBAApB,MAAM,cAAc;IACzB,MAAM,CAAC,QAAQ,CAAC,OAAwB;QACtC,OAAO;YACL,MAAM,EAAE,gBAAc;YACtB,SAAS,EAAE,cAAc,CAAC,OAAO,CAAC;YAClC,OAAO,EAAE,CAAC,kCAAe,CAAC;SAC3B,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,aAAa,CAAC,EACnB,UAAU,EACV,MAAM,EACN,OAAO,EACP,MAAM,EACN,WAAW,GACU;QACrB,MAAM,eAAe,GAAa;YAChC,OAAO,EAAE,qCAAgB;YACzB,UAAU,EAAE,KAAK,EAAE,GAAG,IAAe,EAAE,EAAE;gBACvC,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC;gBAEvC,OAAO;oBACL,GAAG,IAAI;oBACP,GAAG,CAAC,MAAM,KAAK,SAAS,IAAI,EAAE,MAAM,EAAE,CAAC;oBACvC,GAAG,CAAC,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,CAAC;iBAClD,CAAC;YACJ,CAAC;YACD,MAAM,EAAG,MAAa,IAAI,EAAE;SAC7B,CAAC;QAEF,MAAM,SAAS,GAAe;YAC5B,eAAe;YACf,cAAc;YACd,oCAAgB;YAChB,kCAAe;YAEf,EAAE,OAAO,EAAE,sBAAe,EAAE,QAAQ,EAAE,uDAAyB,EAAE;SAClE,CAAC;QAEF,IAAI,MAAM,EAAE,CAAC;YACX,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,iBAAU,EAAE,QAAQ,EAAE,mDAAuB,EAAE,CAAC,CAAC;QAC7E,CAAC;QAED,IAAI,WAAW,EAAE,CAAC;YAChB,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,sBAAe,EAAE,QAAQ,EAAE,0CAAmB,EAAE,CAAC,CAAC;QAC9E,CAAC;QAED,OAAO;YACL,MAAM,EAAE,gBAAc;YACtB,OAAO,EAAE,OAAO,IAAI,EAAE;YACtB,SAAS;YACT,OAAO,EAAE,CAAC,kCAAe,CAAC;SAC3B,CAAC;IACJ,CAAC;CACF,CAAA;AAtDY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,cAAc,CAsD1B"}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import type { DiscordMessage } from 'dicoshot-core';
|
|
1
|
+
import type { DiscordField, DiscordMessage } from 'dicoshot-core';
|
|
2
2
|
import { DicoshotClientImpl } from 'dicoshot-core';
|
|
3
3
|
export type ColorPreset = 'success' | 'danger' | 'warning' | 'info';
|
|
4
4
|
export interface CustomMessageOptions {
|
|
5
5
|
title: string;
|
|
6
6
|
description?: string;
|
|
7
7
|
color?: ColorPreset | number;
|
|
8
|
+
fields?: DiscordField[];
|
|
9
|
+
mention?: string;
|
|
8
10
|
}
|
|
9
11
|
export declare class DicoshotService {
|
|
10
12
|
private readonly client;
|
|
13
|
+
private readonly logger;
|
|
11
14
|
constructor(client: DicoshotClientImpl);
|
|
12
|
-
send(message: DiscordMessage): Promise<
|
|
13
|
-
sendCustom({ title, description, color }: CustomMessageOptions): Promise<
|
|
15
|
+
send(message: DiscordMessage): Promise<boolean>;
|
|
16
|
+
sendCustom({ title, description, color, fields, mention, }: CustomMessageOptions): Promise<boolean>;
|
|
14
17
|
}
|