@serhii.mazur/directus-gu-logs 1.0.9 → 1.0.10
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.MD +6 -5
- package/dist/services/Logs.d.ts +4 -2
- package/dist/services/Logs.js +31 -9
- package/dist/services/SlackNotifier.d.ts +8 -2
- package/dist/services/SlackNotifier.js +69 -28
- package/dist/types.d.ts +1 -0
- package/package.json +2 -2
- package/src/services/Logs.ts +32 -8
- package/src/services/SlackNotifier.ts +80 -28
- package/src/types.ts +1 -0
package/README.MD
CHANGED
|
@@ -24,6 +24,7 @@ Add the following field to the `global` singleton collection:
|
|
|
24
24
|
| ------------------------ | ------------------------------- | -------------------------------------------- |
|
|
25
25
|
| `error_notice_recipient` | m2m → directus_users (junction) | Recipients for error notifications |
|
|
26
26
|
| `slack_webhook_url` | input | Incoming webhook URL for Slack notifications |
|
|
27
|
+
| `slack_notifications` | boolen | Toggle Slack notifications |
|
|
27
28
|
|
|
28
29
|
All linked users will receive internal Directus notifications when errors occur (unless a recipient override is passed).
|
|
29
30
|
|
|
@@ -80,10 +81,10 @@ import { Logs } from "@serhii.mazur/directus-gu-logs";
|
|
|
80
81
|
const logs = new Logs(context, "my-extension");
|
|
81
82
|
|
|
82
83
|
// Persist error log to DB + notify Slack
|
|
83
|
-
await logs.
|
|
84
|
+
await logs.logError("myFunction", "Something went wrong", true);
|
|
84
85
|
|
|
85
86
|
// Persist error log without Slack notification
|
|
86
|
-
await logs.
|
|
87
|
+
await logs.logError("myFunction", "Something went wrong");
|
|
87
88
|
|
|
88
89
|
// Create a Directus activity record
|
|
89
90
|
await logs.createActivity("create", "collection_name", "item_id");
|
|
@@ -101,7 +102,7 @@ await logs.createNotification("An error occurred", "Custom Subject", "user_id");
|
|
|
101
102
|
await logs.createNotification("An error occurred", "Custom Subject", null, "collection_name", "item_id");
|
|
102
103
|
|
|
103
104
|
// Send Slack notification directly
|
|
104
|
-
await logs.
|
|
105
|
+
await logs.notifySlack("Deploy failed on main", "CI Alert");
|
|
105
106
|
```
|
|
106
107
|
|
|
107
108
|
### Advanced (direct class usage)
|
|
@@ -150,7 +151,7 @@ constructor(
|
|
|
150
151
|
|
|
151
152
|
## 🧠 How It Works
|
|
152
153
|
|
|
153
|
-
### `
|
|
154
|
+
### `logError(functionName, error, notifySlack?)`
|
|
154
155
|
|
|
155
156
|
1. Writes a structured entry to the logs collection
|
|
156
157
|
2. If `notifySlack` is `true` (default), sends a Slack Block Kit message with extension name, function, and error
|
|
@@ -174,7 +175,7 @@ Then sends one in-app notification per recipient. Each notification includes:
|
|
|
174
175
|
- The message body
|
|
175
176
|
- Environment label, backend URL, and UTC timestamp
|
|
176
177
|
|
|
177
|
-
### `
|
|
178
|
+
### `notifySlack(message, subject?)`
|
|
178
179
|
|
|
179
180
|
Sends a formatted Slack Block Kit message via `slack_webhook_url`. Silently skips if the var is not set. The payload includes project name, environment, extension name, backend URL, and the message body.
|
|
180
181
|
|
package/dist/services/Logs.d.ts
CHANGED
|
@@ -14,8 +14,10 @@ export declare class Logs {
|
|
|
14
14
|
/**
|
|
15
15
|
* Persists an error entry to the logs collection, optionally notifying via Slack.
|
|
16
16
|
*/
|
|
17
|
+
logError(functionName: string, error: string, notifySlack?: boolean): Promise<void>;
|
|
18
|
+
/** @deprecated Use logError() instead */
|
|
17
19
|
printLogs(functionName: string, error: string, notifySlack?: boolean): Promise<void>;
|
|
18
20
|
createActivity(action: string, collection: string, id: PrimaryKey): Promise<void>;
|
|
19
|
-
createNotification(message: string, customSubject?: string | null, recipientOverride?: string | null, collection?: string | null, item?: string | null): Promise<void>;
|
|
20
|
-
|
|
21
|
+
createNotification(message: string, customSubject?: string | null, recipientOverride?: string | null, collection?: string | null, item?: string | null, notifySlack?: boolean): Promise<void>;
|
|
22
|
+
notifySlack(message: string, customSubject?: string | null): Promise<void>;
|
|
21
23
|
}
|
package/dist/services/Logs.js
CHANGED
|
@@ -17,6 +17,15 @@ export class Logs {
|
|
|
17
17
|
projectName: settings?.project_name ?? "Unknown Project",
|
|
18
18
|
backendUrl: process.env.BACKEND_URL ?? this.context.env?.PUBLIC_URL ?? "Unknown URL",
|
|
19
19
|
environment: process.env.BRANCH ?? "dev",
|
|
20
|
+
timestamp: new Intl.DateTimeFormat("en-US", {
|
|
21
|
+
month: "2-digit",
|
|
22
|
+
day: "2-digit",
|
|
23
|
+
year: "numeric",
|
|
24
|
+
hour: "2-digit",
|
|
25
|
+
minute: "2-digit",
|
|
26
|
+
hour12: false,
|
|
27
|
+
timeZone: "UTC",
|
|
28
|
+
}).format(new Date()),
|
|
20
29
|
};
|
|
21
30
|
}
|
|
22
31
|
async createOne(data) {
|
|
@@ -30,7 +39,7 @@ export class Logs {
|
|
|
30
39
|
/**
|
|
31
40
|
* Persists an error entry to the logs collection, optionally notifying via Slack.
|
|
32
41
|
*/
|
|
33
|
-
async
|
|
42
|
+
async logError(functionName, error, notifySlack = false) {
|
|
34
43
|
const data = {
|
|
35
44
|
collection: this.collectionName,
|
|
36
45
|
date_created: new Date().toISOString(),
|
|
@@ -40,7 +49,7 @@ export class Logs {
|
|
|
40
49
|
};
|
|
41
50
|
try {
|
|
42
51
|
await this.createOne(data);
|
|
43
|
-
this.context.logger.
|
|
52
|
+
this.context.logger.error({ msg: `🚀 [${this.extension}] ${functionName}:`, error });
|
|
44
53
|
}
|
|
45
54
|
catch (err) {
|
|
46
55
|
this.context.logger.error({ msg: "❌ Failed to save log entry", error: err });
|
|
@@ -49,13 +58,17 @@ export class Logs {
|
|
|
49
58
|
try {
|
|
50
59
|
if (notifySlack) {
|
|
51
60
|
const meta = await this.getProjectMeta();
|
|
52
|
-
await this.slack.notify(`*Function:* ${functionName}\n*Error:* ${error}`, "Extension Error"
|
|
61
|
+
await this.slack.notify(`*Function:* ${functionName}\n*Error:* ${error}`, meta, "Extension Error");
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
catch (err) {
|
|
56
|
-
this.context.logger.error({ msg: "Slack failed", err });
|
|
65
|
+
this.context.logger.error({ msg: "Slack failed", error: err });
|
|
57
66
|
}
|
|
58
67
|
}
|
|
68
|
+
/** @deprecated Use logError() instead */
|
|
69
|
+
async printLogs(functionName, error, notifySlack = false) {
|
|
70
|
+
return this.logError(functionName, error, notifySlack);
|
|
71
|
+
}
|
|
59
72
|
async createActivity(action, collection, id) {
|
|
60
73
|
try {
|
|
61
74
|
const schema = await this.getSchema();
|
|
@@ -76,11 +89,11 @@ export class Logs {
|
|
|
76
89
|
item: id,
|
|
77
90
|
});
|
|
78
91
|
}
|
|
79
|
-
catch (
|
|
80
|
-
this.context.logger.error({ msg: "❌ Failed to create activity log", error });
|
|
92
|
+
catch (err) {
|
|
93
|
+
this.context.logger.error({ msg: "❌ Failed to create activity log", error: err });
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
|
-
async createNotification(message, customSubject = null, recipientOverride = null, collection = null, item = null) {
|
|
96
|
+
async createNotification(message, customSubject = null, recipientOverride = null, collection = null, item = null, notifySlack = true) {
|
|
84
97
|
const meta = await this.getProjectMeta();
|
|
85
98
|
await this.directus.notify(message, meta, {
|
|
86
99
|
subject: customSubject,
|
|
@@ -88,9 +101,18 @@ export class Logs {
|
|
|
88
101
|
collection,
|
|
89
102
|
item,
|
|
90
103
|
});
|
|
104
|
+
try {
|
|
105
|
+
if (notifySlack) {
|
|
106
|
+
const meta = await this.getProjectMeta();
|
|
107
|
+
await this.slack.notify(`*Error:* ${message}`, meta, customSubject);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
this.context.logger.error({ msg: "Slack failed", error: err });
|
|
112
|
+
}
|
|
91
113
|
}
|
|
92
|
-
async
|
|
114
|
+
async notifySlack(message, customSubject = null) {
|
|
93
115
|
const meta = await this.getProjectMeta();
|
|
94
|
-
await this.slack.notify(message,
|
|
116
|
+
await this.slack.notify(message, meta, customSubject);
|
|
95
117
|
}
|
|
96
118
|
}
|
|
@@ -2,8 +2,14 @@ import type { ApiExtensionContext, ProjectMeta, SlackPayload } from "../types";
|
|
|
2
2
|
export declare class SlackNotifier {
|
|
3
3
|
private readonly context;
|
|
4
4
|
private readonly extension;
|
|
5
|
+
private configCache;
|
|
6
|
+
private configCachedAt;
|
|
7
|
+
private readonly CONFIG_CACHE_TTL;
|
|
5
8
|
constructor(context: ApiExtensionContext, extension: string);
|
|
6
|
-
|
|
9
|
+
private getEnvironmentIcon;
|
|
10
|
+
private buildBlocks;
|
|
7
11
|
send(payload: SlackPayload): Promise<void>;
|
|
8
|
-
notify(message: string,
|
|
12
|
+
notify(message: string, meta: ProjectMeta, subject?: string | null): Promise<void>;
|
|
13
|
+
private getConfig;
|
|
14
|
+
clearConfigCache(): void;
|
|
9
15
|
}
|
|
@@ -2,52 +2,93 @@ export class SlackNotifier {
|
|
|
2
2
|
constructor(context, extension) {
|
|
3
3
|
this.context = context;
|
|
4
4
|
this.extension = extension;
|
|
5
|
+
this.configCachedAt = 0;
|
|
6
|
+
this.CONFIG_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
5
7
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
getEnvironmentIcon(environment) {
|
|
9
|
+
switch (environment.toLowerCase()) {
|
|
10
|
+
case "main":
|
|
11
|
+
case "master":
|
|
12
|
+
return "🚨🔴";
|
|
13
|
+
case "staging":
|
|
14
|
+
return "🟠";
|
|
15
|
+
case "develop":
|
|
16
|
+
return "🟡";
|
|
17
|
+
default:
|
|
18
|
+
return "⚪";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
buildBlocks(title, envIcon, message, meta) {
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
type: "header",
|
|
25
|
+
text: { type: "plain_text", text: `${envIcon} ${title}: ${this.extension}` },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: "section",
|
|
29
|
+
fields: [
|
|
30
|
+
{ type: "mrkdwn", text: `*Project*\n${meta.projectName}` },
|
|
31
|
+
{ type: "mrkdwn", text: `*Environment*\n${meta.environment}` },
|
|
32
|
+
{ type: "mrkdwn", text: `*Timestamp (UTC)*\n${meta.timestamp}` },
|
|
33
|
+
{ type: "mrkdwn", text: `*Backend*\n${meta.backendUrl}` },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{ type: "divider" },
|
|
37
|
+
{ type: "section", text: { type: "mrkdwn", text: message } },
|
|
38
|
+
];
|
|
9
39
|
}
|
|
10
40
|
async send(payload) {
|
|
11
|
-
const
|
|
12
|
-
if (!
|
|
13
|
-
this.context.logger.
|
|
41
|
+
const config = await this.getConfig();
|
|
42
|
+
if (!config) {
|
|
43
|
+
this.context.logger.warn({ msg: "⚠️ Slack config not found" });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!Boolean(config?.slack_notifications)) {
|
|
47
|
+
this.context.logger.info({ msg: "⚠️ Slack notifications are disabled" });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!config.slack_webhook_url) {
|
|
51
|
+
this.context.logger.warn({ msg: "⚠️ Slack webhook URL is not configured" });
|
|
14
52
|
return;
|
|
15
53
|
}
|
|
16
54
|
try {
|
|
17
|
-
await fetch(
|
|
55
|
+
await fetch(config.slack_webhook_url, {
|
|
18
56
|
method: "POST",
|
|
19
57
|
headers: { "Content-Type": "application/json" },
|
|
20
58
|
body: JSON.stringify(payload),
|
|
21
59
|
});
|
|
60
|
+
this.context.logger.debug({ msg: `✅ Slack notification sent [${this.extension}]` });
|
|
22
61
|
}
|
|
23
62
|
catch (error) {
|
|
24
63
|
this.context.logger.error({ msg: "❌ Failed to send Slack message", error });
|
|
25
64
|
}
|
|
26
65
|
}
|
|
27
|
-
async notify(message, subject = null
|
|
66
|
+
async notify(message, meta, subject = null) {
|
|
28
67
|
const title = subject ?? "Directus Error Notification";
|
|
68
|
+
const envIcon = this.getEnvironmentIcon(meta.environment);
|
|
29
69
|
await this.send({
|
|
30
70
|
text: title,
|
|
31
|
-
blocks:
|
|
32
|
-
{
|
|
33
|
-
type: "header",
|
|
34
|
-
text: { type: "plain_text", text: `🚨 ${title}` },
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
type: "section",
|
|
38
|
-
fields: [
|
|
39
|
-
{ type: "mrkdwn", text: `*Project*\n${meta.projectName}` },
|
|
40
|
-
{ type: "mrkdwn", text: `*Environment*\n${meta.environment}` },
|
|
41
|
-
{ type: "mrkdwn", text: `*Extension*\n${this.extension}` },
|
|
42
|
-
{ type: "mrkdwn", text: `*Backend*\n${meta.backendUrl}` },
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
{ type: "divider" },
|
|
46
|
-
{
|
|
47
|
-
type: "section",
|
|
48
|
-
text: { type: "mrkdwn", text: message },
|
|
49
|
-
},
|
|
50
|
-
],
|
|
71
|
+
blocks: this.buildBlocks(title, envIcon, message, meta),
|
|
51
72
|
});
|
|
52
73
|
}
|
|
74
|
+
async getConfig() {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
if (this.configCache && now - this.configCachedAt < this.CONFIG_CACHE_TTL) {
|
|
77
|
+
return this.configCache;
|
|
78
|
+
}
|
|
79
|
+
const result = await this.context.database
|
|
80
|
+
.select("slack_webhook_url", "slack_notifications")
|
|
81
|
+
.from("global")
|
|
82
|
+
.first();
|
|
83
|
+
if (result) {
|
|
84
|
+
this.configCache = result;
|
|
85
|
+
this.configCachedAt = now;
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
clearConfigCache() {
|
|
90
|
+
this.configCache = undefined;
|
|
91
|
+
this.configCachedAt = 0;
|
|
92
|
+
this.context.logger.debug({ msg: "🔄 Slack config cache cleared" });
|
|
93
|
+
}
|
|
53
94
|
}
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serhii.mazur/directus-gu-logs",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"description": "Reusable logging utility for Directus extensions — persists error entries, tracks activity, and sends notifications via Slack and Directus inbox.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"scripts": {
|
package/src/services/Logs.ts
CHANGED
|
@@ -26,6 +26,15 @@ export class Logs {
|
|
|
26
26
|
projectName: settings?.project_name ?? "Unknown Project",
|
|
27
27
|
backendUrl: process.env.BACKEND_URL ?? this.context.env?.PUBLIC_URL ?? "Unknown URL",
|
|
28
28
|
environment: process.env.BRANCH ?? "dev",
|
|
29
|
+
timestamp: new Intl.DateTimeFormat("en-US", {
|
|
30
|
+
month: "2-digit",
|
|
31
|
+
day: "2-digit",
|
|
32
|
+
year: "numeric",
|
|
33
|
+
hour: "2-digit",
|
|
34
|
+
minute: "2-digit",
|
|
35
|
+
hour12: false,
|
|
36
|
+
timeZone: "UTC",
|
|
37
|
+
}).format(new Date()),
|
|
29
38
|
};
|
|
30
39
|
}
|
|
31
40
|
|
|
@@ -43,7 +52,7 @@ export class Logs {
|
|
|
43
52
|
/**
|
|
44
53
|
* Persists an error entry to the logs collection, optionally notifying via Slack.
|
|
45
54
|
*/
|
|
46
|
-
async
|
|
55
|
+
async logError(functionName: string, error: string, notifySlack = false): Promise<void> {
|
|
47
56
|
const data: LogEntry = {
|
|
48
57
|
collection: this.collectionName,
|
|
49
58
|
date_created: new Date().toISOString(),
|
|
@@ -55,7 +64,7 @@ export class Logs {
|
|
|
55
64
|
try {
|
|
56
65
|
await this.createOne(data);
|
|
57
66
|
|
|
58
|
-
this.context.logger.
|
|
67
|
+
this.context.logger.error({ msg: `🚀 [${this.extension}] ${functionName}:`, error });
|
|
59
68
|
} catch (err) {
|
|
60
69
|
this.context.logger.error({ msg: "❌ Failed to save log entry", error: err });
|
|
61
70
|
return;
|
|
@@ -64,13 +73,18 @@ export class Logs {
|
|
|
64
73
|
try {
|
|
65
74
|
if (notifySlack) {
|
|
66
75
|
const meta = await this.getProjectMeta();
|
|
67
|
-
await this.slack.notify(`*Function:* ${functionName}\n*Error:* ${error}`, "Extension Error"
|
|
76
|
+
await this.slack.notify(`*Function:* ${functionName}\n*Error:* ${error}`, meta, "Extension Error");
|
|
68
77
|
}
|
|
69
78
|
} catch (err) {
|
|
70
|
-
this.context.logger.error({ msg: "Slack failed", err });
|
|
79
|
+
this.context.logger.error({ msg: "Slack failed", error: err });
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
82
|
|
|
83
|
+
/** @deprecated Use logError() instead */
|
|
84
|
+
async printLogs(functionName: string, error: string, notifySlack = false): Promise<void> {
|
|
85
|
+
return this.logError(functionName, error, notifySlack);
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
async createActivity(action: string, collection: string, id: PrimaryKey): Promise<void> {
|
|
75
89
|
try {
|
|
76
90
|
const schema = await this.getSchema();
|
|
@@ -92,8 +106,8 @@ export class Logs {
|
|
|
92
106
|
origin: accountability?.origin ?? null,
|
|
93
107
|
item: id,
|
|
94
108
|
});
|
|
95
|
-
} catch (
|
|
96
|
-
this.context.logger.error({ msg: "❌ Failed to create activity log", error });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
this.context.logger.error({ msg: "❌ Failed to create activity log", error: err });
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -103,6 +117,7 @@ export class Logs {
|
|
|
103
117
|
recipientOverride: string | null = null,
|
|
104
118
|
collection: string | null = null,
|
|
105
119
|
item: string | null = null,
|
|
120
|
+
notifySlack = true,
|
|
106
121
|
): Promise<void> {
|
|
107
122
|
const meta = await this.getProjectMeta();
|
|
108
123
|
|
|
@@ -112,11 +127,20 @@ export class Logs {
|
|
|
112
127
|
collection,
|
|
113
128
|
item,
|
|
114
129
|
});
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
if (notifySlack) {
|
|
133
|
+
const meta = await this.getProjectMeta();
|
|
134
|
+
await this.slack.notify(`*Error:* ${message}`, meta, customSubject);
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
this.context.logger.error({ msg: "Slack failed", error: err });
|
|
138
|
+
}
|
|
115
139
|
}
|
|
116
140
|
|
|
117
|
-
async
|
|
141
|
+
async notifySlack(message: string, customSubject: string | null = null): Promise<void> {
|
|
118
142
|
const meta = await this.getProjectMeta();
|
|
119
143
|
|
|
120
|
-
await this.slack.notify(message,
|
|
144
|
+
await this.slack.notify(message, meta, customSubject);
|
|
121
145
|
}
|
|
122
146
|
}
|
|
@@ -1,61 +1,113 @@
|
|
|
1
1
|
import type { ApiExtensionContext, ProjectMeta, SlackPayload } from "../types";
|
|
2
2
|
|
|
3
3
|
export class SlackNotifier {
|
|
4
|
+
private configCache: { slack_webhook_url: string | null; slack_notifications: boolean } | undefined;
|
|
5
|
+
private configCachedAt: number = 0;
|
|
6
|
+
private readonly CONFIG_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
7
|
+
|
|
4
8
|
constructor(
|
|
5
9
|
private readonly context: ApiExtensionContext,
|
|
6
10
|
private readonly extension: string,
|
|
7
11
|
) {}
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
private getEnvironmentIcon(environment: string): string {
|
|
14
|
+
switch (environment.toLowerCase()) {
|
|
15
|
+
case "main":
|
|
16
|
+
case "master":
|
|
17
|
+
return "🚨🔴";
|
|
18
|
+
case "staging":
|
|
19
|
+
return "🟠";
|
|
20
|
+
case "develop":
|
|
21
|
+
return "🟡";
|
|
22
|
+
default:
|
|
23
|
+
return "⚪";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
11
26
|
|
|
12
|
-
|
|
27
|
+
private buildBlocks(title: string, envIcon: string, message: string, meta: ProjectMeta) {
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
type: "header",
|
|
31
|
+
text: { type: "plain_text", text: `${envIcon} ${title}: ${this.extension}` },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: "section",
|
|
35
|
+
fields: [
|
|
36
|
+
{ type: "mrkdwn", text: `*Project*\n${meta.projectName}` },
|
|
37
|
+
{ type: "mrkdwn", text: `*Environment*\n${meta.environment}` },
|
|
38
|
+
{ type: "mrkdwn", text: `*Timestamp (UTC)*\n${meta.timestamp}` },
|
|
39
|
+
{ type: "mrkdwn", text: `*Backend*\n${meta.backendUrl}` },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{ type: "divider" },
|
|
43
|
+
{ type: "section", text: { type: "mrkdwn", text: message } },
|
|
44
|
+
];
|
|
13
45
|
}
|
|
14
46
|
|
|
15
47
|
async send(payload: SlackPayload): Promise<void> {
|
|
16
|
-
const
|
|
48
|
+
const config = await this.getConfig();
|
|
49
|
+
|
|
50
|
+
if (!config) {
|
|
51
|
+
this.context.logger.warn({ msg: "⚠️ Slack config not found" });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
17
54
|
|
|
18
|
-
if (!
|
|
19
|
-
this.context.logger.info({ msg: "⚠️
|
|
55
|
+
if (!Boolean(config?.slack_notifications)) {
|
|
56
|
+
this.context.logger.info({ msg: "⚠️ Slack notifications are disabled" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!config!.slack_webhook_url) {
|
|
61
|
+
this.context.logger.warn({ msg: "⚠️ Slack webhook URL is not configured" });
|
|
20
62
|
return;
|
|
21
63
|
}
|
|
22
64
|
|
|
23
65
|
try {
|
|
24
|
-
await fetch(
|
|
66
|
+
await fetch(config.slack_webhook_url, {
|
|
25
67
|
method: "POST",
|
|
26
68
|
headers: { "Content-Type": "application/json" },
|
|
27
69
|
body: JSON.stringify(payload),
|
|
28
70
|
});
|
|
71
|
+
|
|
72
|
+
this.context.logger.debug({ msg: `✅ Slack notification sent [${this.extension}]` });
|
|
29
73
|
} catch (error) {
|
|
30
74
|
this.context.logger.error({ msg: "❌ Failed to send Slack message", error });
|
|
31
75
|
}
|
|
32
76
|
}
|
|
33
77
|
|
|
34
|
-
async notify(message: string, subject: string | null = null
|
|
78
|
+
async notify(message: string, meta: ProjectMeta, subject: string | null = null): Promise<void> {
|
|
35
79
|
const title = subject ?? "Directus Error Notification";
|
|
80
|
+
const envIcon = this.getEnvironmentIcon(meta.environment);
|
|
36
81
|
|
|
37
82
|
await this.send({
|
|
38
83
|
text: title,
|
|
39
|
-
blocks:
|
|
40
|
-
{
|
|
41
|
-
type: "header",
|
|
42
|
-
text: { type: "plain_text", text: `🚨 ${title}` },
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
type: "section",
|
|
46
|
-
fields: [
|
|
47
|
-
{ type: "mrkdwn", text: `*Project*\n${meta.projectName}` },
|
|
48
|
-
{ type: "mrkdwn", text: `*Environment*\n${meta.environment}` },
|
|
49
|
-
{ type: "mrkdwn", text: `*Extension*\n${this.extension}` },
|
|
50
|
-
{ type: "mrkdwn", text: `*Backend*\n${meta.backendUrl}` },
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
{ type: "divider" },
|
|
54
|
-
{
|
|
55
|
-
type: "section",
|
|
56
|
-
text: { type: "mrkdwn", text: message },
|
|
57
|
-
},
|
|
58
|
-
],
|
|
84
|
+
blocks: this.buildBlocks(title, envIcon, message, meta),
|
|
59
85
|
});
|
|
60
86
|
}
|
|
87
|
+
|
|
88
|
+
private async getConfig(): Promise<{ slack_webhook_url: string | null; slack_notifications: boolean } | undefined> {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
|
|
91
|
+
if (this.configCache && now - this.configCachedAt < this.CONFIG_CACHE_TTL) {
|
|
92
|
+
return this.configCache;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = await this.context.database
|
|
96
|
+
.select("slack_webhook_url", "slack_notifications")
|
|
97
|
+
.from("global")
|
|
98
|
+
.first();
|
|
99
|
+
|
|
100
|
+
if (result) {
|
|
101
|
+
this.configCache = result;
|
|
102
|
+
this.configCachedAt = now;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
clearConfigCache(): void {
|
|
109
|
+
this.configCache = undefined;
|
|
110
|
+
this.configCachedAt = 0;
|
|
111
|
+
this.context.logger.debug({ msg: "🔄 Slack config cache cleared" });
|
|
112
|
+
}
|
|
61
113
|
}
|