@serhii.mazur/directus-gu-logs 1.0.9 → 1.0.11

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 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.printLogs("myFunction", "Something went wrong");
84
+ await logs.logError("myFunction", "Something went wrong", true);
84
85
 
85
86
  // Persist error log without Slack notification
86
- await logs.printLogs("myFunction", "Something went wrong", false);
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.createSlackNotification("Deploy failed on main", "CI Alert");
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
- ### `printLogs(functionName, error, notifySlack?)`
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
- ### `createSlackNotification(message, subject?)`
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
 
@@ -14,8 +14,11 @@ 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
- createSlackNotification(message: string, customSubject?: string | null): Promise<void>;
21
+ createNotification(message: string, customSubject?: string | null, recipientOverride?: string | null, collection?: string | null, item?: string | null, notifySlack?: boolean): Promise<void>;
22
+ notifyEmail(message: string, customSubject?: string | null, recipientOverride?: string | null, collection?: string | null, item?: string | null): Promise<void>;
23
+ notifySlack(message: string, customSubject?: string | null): Promise<void>;
21
24
  }
@@ -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 printLogs(functionName, error, notifySlack = true) {
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.info({ msg: `🚀 [${this.extension}] ${functionName}:`, error });
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", meta);
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,24 @@ export class Logs {
76
89
  item: id,
77
90
  });
78
91
  }
79
- catch (error) {
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 });
94
+ }
95
+ }
96
+ async createNotification(message, customSubject = null, recipientOverride = null, collection = null, item = null, notifySlack = true) {
97
+ const slackEnabled = await this.slack.isEnabled();
98
+ if (slackEnabled && notifySlack) {
99
+ try {
100
+ await this.notifySlack(`*Error:* ${message}`, customSubject);
101
+ return;
102
+ }
103
+ catch (err) {
104
+ this.context.logger.error({ msg: "❌ Slack notification failed", error: err });
105
+ }
81
106
  }
107
+ await this.notifyEmail(message, customSubject, recipientOverride, collection, item);
82
108
  }
83
- async createNotification(message, customSubject = null, recipientOverride = null, collection = null, item = null) {
109
+ async notifyEmail(message, customSubject = null, recipientOverride = null, collection = null, item = null) {
84
110
  const meta = await this.getProjectMeta();
85
111
  await this.directus.notify(message, meta, {
86
112
  subject: customSubject,
@@ -89,8 +115,8 @@ export class Logs {
89
115
  item,
90
116
  });
91
117
  }
92
- async createSlackNotification(message, customSubject = null) {
118
+ async notifySlack(message, customSubject = null) {
93
119
  const meta = await this.getProjectMeta();
94
- await this.slack.notify(message, customSubject, meta);
120
+ await this.slack.notify(message, meta, customSubject);
95
121
  }
96
122
  }
@@ -2,8 +2,15 @@ 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
- getWebHookUrl(): Promise<any>;
9
+ private getEnvironmentIcon;
10
+ private buildBlocks;
7
11
  send(payload: SlackPayload): Promise<void>;
8
- notify(message: string, subject: string | null | undefined, meta: ProjectMeta): Promise<void>;
12
+ notify(message: string, meta: ProjectMeta, subject?: string | null): Promise<void>;
13
+ isEnabled(): Promise<boolean>;
14
+ private getConfig;
15
+ clearConfigCache(): void;
9
16
  }
@@ -2,52 +2,98 @@ 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
- async getWebHookUrl() {
7
- const settings = await this.context.database.select("slack_webhook_url").from("global").first();
8
- return settings?.slack_webhook_url;
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
+ { type: "divider" },
24
+ {
25
+ type: "header",
26
+ text: { type: "plain_text", text: `${envIcon} ${title}: ${this.extension}` },
27
+ },
28
+ {
29
+ type: "section",
30
+ fields: [
31
+ { type: "mrkdwn", text: `*Project*\n${meta.projectName}` },
32
+ { type: "mrkdwn", text: `*Environment*\n${meta.environment}` },
33
+ { type: "mrkdwn", text: `*Timestamp (UTC)*\n${meta.timestamp}` },
34
+ { type: "mrkdwn", text: `*Backend*\n${meta.backendUrl}` },
35
+ ],
36
+ },
37
+ { type: "section", text: { type: "mrkdwn", text: message } },
38
+ { type: "divider" },
39
+ ];
9
40
  }
10
41
  async send(payload) {
11
- const webhook = await this.getWebHookUrl();
12
- if (!webhook) {
13
- this.context.logger.info({ msg: "⚠️ SLACK_WEBHOOK_URL is not configured" });
42
+ const config = await this.getConfig();
43
+ if (!config) {
44
+ this.context.logger.warn({ msg: "⚠️ Slack config not found" });
45
+ return;
46
+ }
47
+ if (!Boolean(config?.slack_notifications)) {
48
+ this.context.logger.info({ msg: "⚠️ Slack notifications are disabled" });
49
+ return;
50
+ }
51
+ if (!config.slack_webhook_url) {
52
+ this.context.logger.warn({ msg: "⚠️ Slack webhook URL is not configured" });
14
53
  return;
15
54
  }
16
55
  try {
17
- await fetch(webhook, {
56
+ await fetch(config.slack_webhook_url, {
18
57
  method: "POST",
19
58
  headers: { "Content-Type": "application/json" },
20
59
  body: JSON.stringify(payload),
21
60
  });
61
+ this.context.logger.debug({ msg: `✅ Slack notification sent [${this.extension}]` });
22
62
  }
23
63
  catch (error) {
24
64
  this.context.logger.error({ msg: "❌ Failed to send Slack message", error });
25
65
  }
26
66
  }
27
- async notify(message, subject = null, meta) {
67
+ async notify(message, meta, subject = null) {
28
68
  const title = subject ?? "Directus Error Notification";
69
+ const envIcon = this.getEnvironmentIcon(meta.environment);
29
70
  await this.send({
30
71
  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
- ],
72
+ blocks: this.buildBlocks(title, envIcon, message, meta),
51
73
  });
52
74
  }
75
+ async isEnabled() {
76
+ const config = await this.getConfig();
77
+ return Boolean(config?.slack_notifications && config?.slack_webhook_url);
78
+ }
79
+ async getConfig() {
80
+ const now = Date.now();
81
+ if (this.configCache && now - this.configCachedAt < this.CONFIG_CACHE_TTL) {
82
+ return this.configCache;
83
+ }
84
+ const result = await this.context.database
85
+ .select("slack_webhook_url", "slack_notifications")
86
+ .from("global")
87
+ .first();
88
+ if (result) {
89
+ this.configCache = result;
90
+ this.configCachedAt = now;
91
+ }
92
+ return result;
93
+ }
94
+ clearConfigCache() {
95
+ this.configCache = undefined;
96
+ this.configCachedAt = 0;
97
+ this.context.logger.debug({ msg: "🔄 Slack config cache cleared" });
98
+ }
53
99
  }
package/dist/types.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface ProjectMeta {
16
16
  projectName: string;
17
17
  backendUrl: string;
18
18
  environment: string;
19
+ timestamp: string;
19
20
  }
20
21
  export interface SlackBlock {
21
22
  type: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serhii.mazur/directus-gu-logs",
3
- "version": "1.0.9",
4
- "description": "Helper class Logs for using in Directus extensions",
3
+ "version": "1.0.11",
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": {
@@ -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 printLogs(functionName: string, error: string, notifySlack = true): Promise<void> {
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.info({ msg: `🚀 [${this.extension}] ${functionName}:`, error });
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", meta);
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 (error) {
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,28 @@ export class Logs {
103
117
  recipientOverride: string | null = null,
104
118
  collection: string | null = null,
105
119
  item: string | null = null,
120
+ notifySlack = true,
121
+ ): Promise<void> {
122
+ const slackEnabled = await this.slack.isEnabled();
123
+
124
+ if (slackEnabled && notifySlack) {
125
+ try {
126
+ await this.notifySlack(`*Error:* ${message}`, customSubject);
127
+ return;
128
+ } catch (err) {
129
+ this.context.logger.error({ msg: "❌ Slack notification failed", error: err });
130
+ }
131
+ }
132
+
133
+ await this.notifyEmail(message, customSubject, recipientOverride, collection, item);
134
+ }
135
+
136
+ async notifyEmail(
137
+ message: string,
138
+ customSubject: string | null = null,
139
+ recipientOverride: string | null = null,
140
+ collection: string | null = null,
141
+ item: string | null = null,
106
142
  ): Promise<void> {
107
143
  const meta = await this.getProjectMeta();
108
144
 
@@ -114,9 +150,9 @@ export class Logs {
114
150
  });
115
151
  }
116
152
 
117
- async createSlackNotification(message: string, customSubject: string | null = null): Promise<void> {
153
+ async notifySlack(message: string, customSubject: string | null = null): Promise<void> {
118
154
  const meta = await this.getProjectMeta();
119
155
 
120
- await this.slack.notify(message, customSubject, meta);
156
+ await this.slack.notify(message, meta, customSubject);
121
157
  }
122
158
  }
@@ -1,61 +1,119 @@
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
- async getWebHookUrl() {
10
- const settings = await this.context.database.select("slack_webhook_url").from("global").first();
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
- return settings?.slack_webhook_url;
27
+ private buildBlocks(title: string, envIcon: string, message: string, meta: ProjectMeta) {
28
+ return [
29
+ { type: "divider" },
30
+ {
31
+ type: "header",
32
+ text: { type: "plain_text", text: `${envIcon} ${title}: ${this.extension}` },
33
+ },
34
+ {
35
+ type: "section",
36
+ fields: [
37
+ { type: "mrkdwn", text: `*Project*\n${meta.projectName}` },
38
+ { type: "mrkdwn", text: `*Environment*\n${meta.environment}` },
39
+ { type: "mrkdwn", text: `*Timestamp (UTC)*\n${meta.timestamp}` },
40
+ { type: "mrkdwn", text: `*Backend*\n${meta.backendUrl}` },
41
+ ],
42
+ },
43
+ { type: "section", text: { type: "mrkdwn", text: message } },
44
+ { type: "divider" },
45
+ ];
13
46
  }
14
47
 
15
48
  async send(payload: SlackPayload): Promise<void> {
16
- const webhook = await this.getWebHookUrl();
49
+ const config = await this.getConfig();
50
+
51
+ if (!config) {
52
+ this.context.logger.warn({ msg: "⚠️ Slack config not found" });
53
+ return;
54
+ }
17
55
 
18
- if (!webhook) {
19
- this.context.logger.info({ msg: "⚠️ SLACK_WEBHOOK_URL is not configured" });
56
+ if (!Boolean(config?.slack_notifications)) {
57
+ this.context.logger.info({ msg: "⚠️ Slack notifications are disabled" });
58
+ return;
59
+ }
60
+
61
+ if (!config!.slack_webhook_url) {
62
+ this.context.logger.warn({ msg: "⚠️ Slack webhook URL is not configured" });
20
63
  return;
21
64
  }
22
65
 
23
66
  try {
24
- await fetch(webhook, {
67
+ await fetch(config.slack_webhook_url, {
25
68
  method: "POST",
26
69
  headers: { "Content-Type": "application/json" },
27
70
  body: JSON.stringify(payload),
28
71
  });
72
+
73
+ this.context.logger.debug({ msg: `✅ Slack notification sent [${this.extension}]` });
29
74
  } catch (error) {
30
75
  this.context.logger.error({ msg: "❌ Failed to send Slack message", error });
31
76
  }
32
77
  }
33
78
 
34
- async notify(message: string, subject: string | null = null, meta: ProjectMeta): Promise<void> {
79
+ async notify(message: string, meta: ProjectMeta, subject: string | null = null): Promise<void> {
35
80
  const title = subject ?? "Directus Error Notification";
81
+ const envIcon = this.getEnvironmentIcon(meta.environment);
36
82
 
37
83
  await this.send({
38
84
  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
- ],
85
+ blocks: this.buildBlocks(title, envIcon, message, meta),
59
86
  });
60
87
  }
88
+
89
+ async isEnabled(): Promise<boolean> {
90
+ const config = await this.getConfig();
91
+ return Boolean(config?.slack_notifications && config?.slack_webhook_url);
92
+ }
93
+
94
+ private async getConfig(): Promise<{ slack_webhook_url: string | null; slack_notifications: boolean } | undefined> {
95
+ const now = Date.now();
96
+
97
+ if (this.configCache && now - this.configCachedAt < this.CONFIG_CACHE_TTL) {
98
+ return this.configCache;
99
+ }
100
+
101
+ const result = await this.context.database
102
+ .select("slack_webhook_url", "slack_notifications")
103
+ .from("global")
104
+ .first();
105
+
106
+ if (result) {
107
+ this.configCache = result;
108
+ this.configCachedAt = now;
109
+ }
110
+
111
+ return result;
112
+ }
113
+
114
+ clearConfigCache(): void {
115
+ this.configCache = undefined;
116
+ this.configCachedAt = 0;
117
+ this.context.logger.debug({ msg: "🔄 Slack config cache cleared" });
118
+ }
61
119
  }
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@ export interface ProjectMeta {
17
17
  projectName: string;
18
18
  backendUrl: string;
19
19
  environment: string;
20
+ timestamp: string;
20
21
  }
21
22
 
22
23
  export interface SlackBlock {