@tiledesk/tiledesk-server 2.18.4 → 2.18.16

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/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
7
7
 
8
+ # 2.18.13
9
+ - Added Analytics tracking
10
+
11
+ # 2.18.12
12
+ - Added messages filtering on download transcript via csv and pdf
13
+
14
+ # 2.18.11
15
+ - Added endpoint to recovery urls preview
16
+
17
+ # 2.18.10
18
+ - Updated tybot-connector to 2.0.51
19
+
20
+ # 2.18.9
21
+ - Updated tybot-connector to 2.0.49
22
+
23
+ # 2.18.8
24
+ - Refactored transcript templates
25
+
26
+ # 2.18.7
27
+ - Added possibility to export Answered and Unanswered questions
28
+
29
+ # 2.18.6
30
+ - Modified sitemap deletion behavior to also remove all associated URLs.
31
+
32
+ # 2.18.5
33
+ - Fixed bug on update url content on Knwoledge Base
34
+
8
35
  # 2.18.4
9
36
  - Added HyDE support for Knowledge Base Q&A
10
37
  - Introduced Cache (cRag) functionality in Knowledge Base Q&A
package/app.js CHANGED
@@ -125,6 +125,7 @@ var widgets = require('./routes/widget');
125
125
  var widgetsLoader = require('./routes/widgetLoader');
126
126
  var openai = require('./routes/openai');
127
127
  var llm = require('./routes/llm');
128
+ var urlPreview = require('./routes/urlPreview');
128
129
  var quotes = require('./routes/quotes');
129
130
  var integration = require('./routes/integration')
130
131
  var kbsettings = require('./routes/kbsettings');
@@ -636,6 +637,7 @@ app.use('/:projectid/segments',[passport.authenticate(['basic', 'jwt'], { sessio
636
637
 
637
638
  app.use('/:projectid/llm', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], llm);
638
639
  app.use('/:projectid/openai', openai);
640
+ app.use('/:projectid/url-preview', urlPreview);
639
641
  app.use('/:projectid/quotes', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('agent', ['bot','subscription'])], quotes)
640
642
 
641
643
  app.use('/:projectid/integration', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], integration )
@@ -724,5 +726,4 @@ app.use((err, req, res, next) => {
724
726
  });
725
727
 
726
728
 
727
-
728
729
  module.exports = app;
@@ -0,0 +1,12 @@
1
+ const EventEmitter = require('events');
2
+
3
+ class KbEvent extends EventEmitter {
4
+ constructor() {
5
+ super();
6
+ this.queueEnabled = false;
7
+ }
8
+ }
9
+
10
+ const kbEvent = new KbEvent();
11
+
12
+ module.exports = kbEvent;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ const EventEmitter = require('events');
4
+
5
+ class WebhookEvent extends EventEmitter {}
6
+
7
+ const webhookEvent = new WebhookEvent();
8
+
9
+ module.exports = webhookEvent;
package/jobs.js CHANGED
@@ -114,6 +114,17 @@ async function main()
114
114
  let multiWorkerQueue = require('@tiledesk/tiledesk-multi-worker');
115
115
  jobsManager.listenMultiWorker(multiWorkerQueue);
116
116
 
117
+ try {
118
+ this.analyticsPublisher = require('./pubmodules/analytics-publisher');
119
+ this.analyticsPublisher.listen();
120
+ winston.info("AnalyticsPublisher initialized.");
121
+ } catch(err) {
122
+ if (err.code === 'MODULE_NOT_FOUND') {
123
+ winston.info("PubModulesManager init analyticsPublisher module not found");
124
+ } else {
125
+ winston.info("PubModulesManager error initializing analyticsPublisher module", err);
126
+ }
127
+ }
117
128
 
118
129
  winston.info("Jobs started");
119
130
 
@@ -128,4 +139,4 @@ function panic(error)
128
139
  }
129
140
 
130
141
  // https://stackoverflow.com/a/46916601/1478566
131
- main().catch(panic).finally(clearInterval.bind(null, setInterval(a=>a, 1E9)));
142
+ main().catch(panic).finally(clearInterval.bind(null, setInterval(a=>a, 1E9)));
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+
3
+ const axios = require("axios");
4
+ const crypto = require("crypto");
5
+ const winston = require("../config/winston");
6
+
7
+ const INGEST_BASE_URL = process.env.ANALYTICS_INGEST_URL;
8
+ const INGEST_API_KEY = process.env.ANALYTICS_INGEST_API_KEY;
9
+ const SOURCE_SERVICE = "tiledesk-server";
10
+ const EVENT_VERSION = "1.0";
11
+
12
+ const client = axios.create({
13
+ baseURL: INGEST_BASE_URL || "http://localhost:3099", // placeholder; disabled when INGEST_BASE_URL unset
14
+ timeout: 5000,
15
+ headers: INGEST_API_KEY ? { "X-Api-Key": INGEST_API_KEY } : {},
16
+ });
17
+
18
+ /**
19
+ * Build a full analytics event envelope.
20
+ * Mirrors the tiledesk-llm _build_envelope() pattern so both services
21
+ * produce identical envelope shapes, enabling event_id-based deduplication
22
+ * in the consumer.
23
+ */
24
+ function _buildEnvelope(eventType, idProject, payload) {
25
+ return {
26
+ event_id: crypto.randomUUID(),
27
+ event_type: eventType,
28
+ timestamp: new Date().toISOString(),
29
+ id_project: idProject,
30
+ source_service: SOURCE_SERVICE,
31
+ event_version: EVENT_VERSION,
32
+ payload: payload,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Fire-and-forget analytics event. No-op when ANALYTICS_INGEST_URL is unset.
38
+ *
39
+ * @param {string} eventType - e.g. 'conversation.created'
40
+ * @param {string} idProject - Tiledesk project ID
41
+ * @param {object} payload - event-specific fields
42
+ */
43
+ function track(eventType, idProject, payload) {
44
+ if (!INGEST_BASE_URL) return;
45
+ if (!idProject) return;
46
+
47
+ var body = _buildEnvelope(eventType, idProject, payload);
48
+
49
+ client.post("/events", body).catch(function (err) {
50
+ var status = err.response && err.response.status;
51
+ var detail =
52
+ (err.response && err.response.data && err.response.data.error) ||
53
+ err.message;
54
+ winston.warn(
55
+ "[analytics] Failed to track " + eventType + ": " + status + " " + detail,
56
+ );
57
+ });
58
+ }
59
+
60
+ module.exports = { track: track };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-server",
3
3
  "description": "The Tiledesk server module",
4
- "version": "2.18.4",
4
+ "version": "2.18.16",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -49,7 +49,7 @@
49
49
  "@tiledesk/tiledesk-rasa-connector": "^1.0.10",
50
50
  "@tiledesk/tiledesk-sms-connector": "^0.1.13",
51
51
  "@tiledesk/tiledesk-telegram-connector": "^0.1.14",
52
- "@tiledesk/tiledesk-tybot-connector": "^2.0.48",
52
+ "@tiledesk/tiledesk-tybot-connector": "^2.0.52",
53
53
  "@tiledesk/tiledesk-voice-twilio-connector": "^0.3.2",
54
54
  "@tiledesk/tiledesk-vxml-connector": "^0.1.91",
55
55
  "@tiledesk/tiledesk-whatsapp-connector": "1.0.26",
@@ -0,0 +1,437 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * analytics-publisher pubmodule
5
+ *
6
+ * Listens to internal tiledesk-server events and forwards analytics
7
+ * events to the ingest sidecar via lib/analyticsClient.
8
+ *
9
+ * Fire-and-forget: no event is awaited and no error propagates to callers.
10
+ * Fully disabled (no listeners registered) when ANALYTICS_INGEST_URL is unset.
11
+ *
12
+ * All payloads are validated against the @tiledesk-analytics/contracts Zod
13
+ * schemas (packages/contracts/src/payloads/*.ts). Field names and nullability
14
+ * here must match those schemas exactly.
15
+ */
16
+
17
+ var requestEvent = require("../../event/requestEvent");
18
+ var messageEvent = require("../../event/messageEvent");
19
+ var authEvent = require("../../event/authEvent");
20
+ var webhookEvent = require("../../event/webhookEvent");
21
+ var kbEvent = require("../../event/kbEvent");
22
+ var departmentEvent = require("../../event/departmentEvent");
23
+ var botEvent = require("../../event/botEvent");
24
+ var { track } = require("../../lib/analyticsClient");
25
+
26
+ // ─── helpers ────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Convert a boolean user_available value to the analytics status enum.
30
+ * Returns null for any non-boolean input so callers can guard before emitting.
31
+ * The contract enum is: 'available' | 'unavailable' | 'busy'.
32
+ * tiledesk-server only sets available/unavailable; 'busy' is reserved.
33
+ */
34
+ function availabilityLabel(boolVal) {
35
+ if (boolVal === true) return "available";
36
+ if (boolVal === false) return "unavailable";
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * Derive sender_type enum value from a raw sender string.
42
+ * Contract enum: 'user' | 'agent' | 'bot'
43
+ *
44
+ * Uses request.lead.lead_id (the visitor's ID) to distinguish a human visitor
45
+ * from a human agent — without this check every non-bot sender falls through
46
+ * to 'user', misclassifying agent messages.
47
+ */
48
+ function senderType(sender, request) {
49
+ let bot, agent = undefined;
50
+ if(request.participantsBots)
51
+ bot = request.participantsBots.find(participant => participant.includes(sender));
52
+ if(request.participantsAgents)
53
+ agent = request.participantsAgents.find(participant => participant.includes(sender));
54
+
55
+ if(bot)
56
+ return "bot";
57
+ if(agent)
58
+ return "agent";
59
+
60
+ return "user";
61
+ }
62
+
63
+ /**
64
+ * Return the first participant that is not a bot (i.e. the visitor/user).
65
+ */
66
+ function firstVisitorId(participants) {
67
+ var list = participants || [];
68
+ for (var i = 0; i < list.length; i++) {
69
+ if (!list[i].startsWith("bot_")) return list[i];
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Extract a stable string ID from a department field (ObjectId, object, or string).
76
+ * Returns null when no department is set.
77
+ */
78
+ function departmentId(dept) {
79
+ if (!dept) return null;
80
+ if (typeof dept === "string") return dept;
81
+ return (dept._id || dept.id || "").toString() || null;
82
+ }
83
+
84
+ /**
85
+ * Extract the human-readable name from a populated department object.
86
+ * Falls back to the ID string so callers always get a non-null value when
87
+ * a department exists.
88
+ */
89
+ function departmentName(dept) {
90
+ if (!dept) return null;
91
+ if (typeof dept === "string") return dept; // bare string ID — use as-is
92
+ return dept.name || (dept._id && dept._id.toString()) || null;
93
+ }
94
+
95
+ /**
96
+ * Return the _id of a Mongoose document or plain object as a string.
97
+ */
98
+ function toStringId(doc) {
99
+ if (!doc) return null;
100
+ if (typeof doc === "string") return doc;
101
+ return (doc._id || doc.id || doc).toString() || null;
102
+ }
103
+
104
+ // ─── listeners ──────────────────────────────────────────────────────────────
105
+
106
+ function listen() {
107
+ // ── 1. conversation.created ────────────────────────────────────────────────
108
+ // Contract: packages/contracts/src/payloads/conversation-created.ts
109
+ // id_request string (required)
110
+ // request_id string (required)
111
+ // department string|null
112
+ // channel string
113
+ // first_response_time number|null
114
+ // tags string[] (default [])
115
+ // tag string|null (optional)
116
+ // with_bot boolean (default false)
117
+ // visitor_id string|null (optional)
118
+
119
+ function trackConversation(request) {
120
+ if (request.preflight === true) return;
121
+
122
+ var dept = request.department;
123
+
124
+ track("conversation.created", request.id_project, {
125
+ id_request: request.request_id || toStringId(request),
126
+ request_id: request.request_id || toStringId(request),
127
+ department: departmentId(dept),
128
+ channel: (request.channel && request.channel.name) || "web",
129
+ first_response_time: null,
130
+ with_bot: request.hasBot || false,
131
+ visitor_id: firstVisitorId(request.participants),
132
+ });
133
+ }
134
+
135
+ requestEvent.on("request.update.preflight", function (request) {
136
+ trackConversation(request);
137
+ })
138
+
139
+ requestEvent.on("request.create", function (request) {
140
+ trackConversation(request);
141
+ });
142
+
143
+ // ── 2. conversation.closed ─────────────────────────────────────────────────
144
+ // Contract: packages/contracts/src/payloads/conversation-closed.ts
145
+ // id_request string (required)
146
+ // request_id string (required)
147
+ // closed_by string (required, non-null)
148
+ // close_reason string|null
149
+ // duration_seconds number
150
+ // waiting_time_seconds number|null
151
+ // satisfaction_rating number 1-5 | null
152
+ requestEvent.on("request.close", function (request) {
153
+ var createdAt = new Date(request.createdAt);
154
+ var closedAt = request.closed_at ? new Date(request.closed_at) : new Date();
155
+ var durationSeconds =
156
+ request.duration != null
157
+ ? Math.round(request.duration / 1000)
158
+ : Math.round((closedAt - createdAt) / 1000);
159
+
160
+ // Normalise rating: must be an integer 1–5 or null.
161
+ var rawRating = request.rating;
162
+ var satisfactionRating = null;
163
+ if (rawRating != null) {
164
+ var r = parseInt(rawRating, 10);
165
+ satisfactionRating = r >= 1 && r <= 5 ? r : null;
166
+ }
167
+
168
+ // TODO: da splittare rispetto al rating
169
+ track("conversation.closed", request.id_project, {
170
+ id_request: request.request_id || toStringId(request),
171
+ request_id: request.request_id || toStringId(request),
172
+ closed_by: request.closed_by || "system",
173
+ close_reason: null,
174
+ duration_seconds: durationSeconds,
175
+ waiting_time_seconds:
176
+ request.waiting_time != null
177
+ ? Math.round(request.waiting_time / 1000)
178
+ : null,
179
+ satisfaction_rating: satisfactionRating,
180
+ });
181
+ });
182
+
183
+ // ── 3. message.sent ────────────────────────────────────────────────────────
184
+ // Contract: packages/contracts/src/payloads/message-sent.ts
185
+ // id_message string (required)
186
+ // id_request string (required)
187
+ // sender_id string (required, non-null)
188
+ // sender_type 'user'|'agent'|'bot'
189
+ // message_type string (required, non-null)
190
+ // has_attachment boolean (default false)
191
+ // language string|null
192
+ messageEvent.on("message.create", function (messageJson) {
193
+ var sender = messageJson.sender;
194
+
195
+ if(sender === 'system') return; // skip system messages
196
+
197
+ // recipient is the room/request ID (chat21 convention)
198
+ var idRequest = messageJson.recipient || null;
199
+ if (!idRequest) return; // cannot emit without a conversation reference
200
+
201
+ track("message.sent", messageJson.id_project, {
202
+ id_message: (messageJson._id || messageJson.id || "").toString(),
203
+ id_request: idRequest,
204
+ sender_id: sender || "unknown", // required non-null — fallback to 'unknown'
205
+ sender_type: senderType(sender, messageJson.request),
206
+ message_type: messageJson.type || "text", // required non-null — fallback to 'text'
207
+ has_attachment: !!(messageJson.metadata && messageJson.metadata.src),
208
+ language: messageJson.language || null,
209
+ });
210
+ });
211
+
212
+ // ── 5. handover_to_human ──────────────────────────────────────────────────
213
+ // Contract: packages/contracts/src/payloads/handover-to-human.ts
214
+ // id_request string (required)
215
+ // human_id string|null
216
+ // reason string|null
217
+ // department_id string|null
218
+ // waiting_time_seconds number int>=0 | null
219
+ // agent_id string|null (optional)
220
+ // trigger_intent string|null (optional)
221
+ requestEvent.on("request.participants.update", function (data) {
222
+ var request = data.request || {};
223
+ var removedParticipants = data.removedParticipants || [];
224
+ var addedParticipants = data.addedParticipants || [];
225
+
226
+ var botRemoved = removedParticipants.some(function (p) {
227
+ return p.startsWith("bot_");
228
+ });
229
+ var humanAdded = addedParticipants.some(function (p) {
230
+ return !p.startsWith("bot_");
231
+ });
232
+ if (!botRemoved || !humanAdded) return;
233
+
234
+ var botId =
235
+ removedParticipants.find(function (p) {
236
+ return p.startsWith("bot_");
237
+ }) || null;
238
+ var humanId =
239
+ addedParticipants.find(function (p) {
240
+ return !p.startsWith("bot_");
241
+ }) || null;
242
+
243
+ var waitingTimeSecs = null;
244
+ if (request.waiting_time != null) {
245
+ waitingTimeSecs = Math.round(request.waiting_time / 1000);
246
+ }
247
+
248
+ track("handover_to_human", request.id_project, {
249
+ id_request: request.request_id || toStringId(request),
250
+ human_id: humanId,
251
+ reason: null,
252
+ department_id: departmentId(request.department),
253
+ waiting_time_seconds: waitingTimeSecs,
254
+ agent_id: null,
255
+ trigger_intent: null,
256
+ });
257
+ });
258
+
259
+
260
+ // ── 5. project_user.activated ─────────────────────────────────────────────
261
+ // Contract: packages/contracts/src/payloads/project-user-activated.ts
262
+ // id_user string (required)
263
+ // user_email string (required, email format — skip if unavailable)
264
+ // role string (required)
265
+ // invited_by string|null
266
+ // TODO: check
267
+ authEvent.on("project_user.invite", function (event) {
268
+ console.log("project_user.invite", event);
269
+ var pu = event.savedProject_userPopulated || event.updatedPuserPopulated;
270
+ if (!pu) return;
271
+
272
+ var user = pu.id_user;
273
+ var email = (user && user.email) || null;
274
+
275
+ // user_email is required as a valid email string — skip rather than send
276
+ // a payload that will be rejected with 422 by the ingest sidecar.
277
+ if (!email) return;
278
+
279
+ var userId = pu.uuid_user || (user && toStringId(user)) || null;
280
+ if (!userId) return;
281
+
282
+ track("project_user.activated", pu.id_project, {
283
+ id_user: userId,
284
+ user_email: email,
285
+ role: pu.role || "agent",
286
+ invited_by: (event.req && event.req.user && event.req.user.id) || null,
287
+ });
288
+ });
289
+
290
+ // ── 7. human.status_changed ───────────────────────────────────────────────
291
+ // Contract: packages/contracts/src/payloads/human-status-changed.ts
292
+ // human_id string (required)
293
+ // previous_status 'available'|'unavailable'|'busy'
294
+ // new_status 'available'|'unavailable'|'busy'
295
+ // TODO: check
296
+ authEvent.on("project_user.update.agent", function (event) {
297
+ console.log("project_user.update.agent", event);
298
+ var pu = event.updatedProject_userPopulated;
299
+ if (!pu) return;
300
+ if (pu.user_available === undefined) return; // not a status-change update
301
+
302
+ var prevBool = event.previousUserAvailable;
303
+ if (prevBool === pu.user_available) return; // no actual change
304
+
305
+ var prevStatus = availabilityLabel(prevBool);
306
+ var newStatus = availabilityLabel(pu.user_available);
307
+
308
+ // Both statuses must be valid enum members — skip if either resolves to null.
309
+ if (!prevStatus || !newStatus) return;
310
+
311
+ var humanId = toStringId(pu.id_user);
312
+ if (!humanId) return;
313
+
314
+ track("human.status_changed", pu.id_project, {
315
+ human_id: humanId,
316
+ previous_status: prevStatus,
317
+ new_status: newStatus,
318
+ });
319
+ });
320
+
321
+ // ── 7. department.assignment ──────────────────────────────────────────────
322
+ // Contract: packages/contracts/src/payloads/department-assignment.ts
323
+ // id_request string (required)
324
+ // department_id string (required, non-null)
325
+ // department_name string (required, non-null)
326
+ // assigned_by string|null
327
+ // routing_type string
328
+ requestEvent.on("request.department.update", function (requestComplete) {
329
+ console.log("request.department.update", requestComplete);
330
+ var dept = requestComplete.department;
331
+
332
+ var deptId = departmentId(dept);
333
+ var deptName = departmentName(dept) || deptId;
334
+
335
+ // Both department_id and department_name are required non-null in the contract.
336
+ if (!deptId) return;
337
+
338
+ track("department.assignment", requestComplete.id_project, {
339
+ id_request: requestComplete.request_id || toStringId(requestComplete),
340
+ department_id: deptId,
341
+ department_name: deptName,
342
+ assigned_by: requestComplete.updatedBy || null,
343
+ routing_type: "auto",
344
+ });
345
+ });
346
+
347
+ // ── 10. webhook.triggered ─────────────────────────────────────────────────
348
+ // Emitted by routes/webhook.js when a chatbot automation is triggered via
349
+ // the public webhook URL (production runs only — dev-mode runs are excluded).
350
+ // webhook_id string (required)
351
+ // agent_id string (required)
352
+ // block_id string (required)
353
+ // async boolean
354
+ // request_id string|null — synthetic automation-request-... identifier
355
+ webhookEvent.on("webhook.triggered", function ({ webhook, payload }) {
356
+ if (!webhook || !webhook.id_project) return;
357
+
358
+ track("webhook.triggered", webhook.id_project, {
359
+ webhook_id: webhook.webhook_id,
360
+ agent_id: webhook.chatbot_id,
361
+ block_id: webhook.block_id,
362
+ async: webhook.async,
363
+ request_id:
364
+ (payload && (payload.preloaded_request_id || payload.request_id)) ||
365
+ null,
366
+ });
367
+ });
368
+
369
+ // ── 11. kb.metadata_updated ───────────────────────────────────────────────
370
+ // Emitted by routes/kb.js on namespace create and update (rename).
371
+ // kb_id = Namespace.id (logical string key, NOT the ObjectId _id)
372
+ // kb_name = Namespace.name
373
+ // The consumer writes these into the kb_dimensions ReplacingMergeTree table
374
+ // so that dashboard queries always resolve the current KB name.
375
+ kbEvent.on("kb.namespace.create", function ({ savedNamespace, project_id }) {
376
+ if (!savedNamespace || !project_id) return;
377
+ track("kb.metadata_updated", project_id, {
378
+ kb_id: savedNamespace.id,
379
+ kb_name: savedNamespace.name,
380
+ });
381
+ });
382
+
383
+ kbEvent.on("kb.namespace.update", function ({ updatedNamespace }) {
384
+ if (!updatedNamespace || !updatedNamespace.id_project) return;
385
+ track("kb.metadata_updated", updatedNamespace.id_project, {
386
+ kb_id: updatedNamespace.id,
387
+ kb_name: updatedNamespace.name,
388
+ });
389
+ });
390
+
391
+ // ── 12. department.metadata_updated ──────────────────────────────────────
392
+ // Emitted by routes/department.js on department create, PUT, and PATCH.
393
+ // department_id = Department._id.toString()
394
+ // department_name = Department.name
395
+ // The consumer writes these into the department_dimensions table so that
396
+ // dashboard queries always resolve the current department name.
397
+ departmentEvent.on("department.create", function (savedDepartment) {
398
+ if (!savedDepartment || !savedDepartment.id_project) return;
399
+ track("department.metadata_updated", savedDepartment.id_project, {
400
+ department_id: savedDepartment._id.toString(),
401
+ department_name: savedDepartment.name,
402
+ });
403
+ });
404
+
405
+ departmentEvent.on("department.update", function (updatedDepartment) {
406
+ if (!updatedDepartment || !updatedDepartment.id_project) return;
407
+ track("department.metadata_updated", updatedDepartment.id_project, {
408
+ department_id: updatedDepartment._id.toString(),
409
+ department_name: updatedDepartment.name,
410
+ });
411
+ });
412
+
413
+ // ── 13. agent.metadata_updated ────────────────────────────────────────────
414
+ // Emitted by routes/faq_kb.js on bot create and update (rename, attribute
415
+ // changes, language, etc.).
416
+ // agent_id = Faq_kb._id.toString()
417
+ // agent_name = Faq_kb.name
418
+ // The consumer writes these into the agent_dimensions ReplacingMergeTree so
419
+ // that dashboard queries always resolve the current bot name.
420
+ botEvent.on("faqbot.create", function (savedBot) {
421
+ if (!savedBot || !savedBot.id_project || !savedBot.name) return;
422
+ track("agent.metadata_updated", savedBot.id_project, {
423
+ agent_id: savedBot.root_id || savedBot._id.toString(),
424
+ agent_name: savedBot.name,
425
+ });
426
+ });
427
+
428
+ botEvent.on("faqbot.update", function (updatedBot) {
429
+ if (!updatedBot || !updatedBot.id_project || !updatedBot.name) return;
430
+ track("agent.metadata_updated", updatedBot.id_project, {
431
+ agent_id: updatedBot.root_id || updatedBot._id.toString(),
432
+ agent_name: updatedBot.name,
433
+ });
434
+ });
435
+ }
436
+
437
+ module.exports = { listen: listen };
@@ -74,6 +74,8 @@ class PubModulesManager {
74
74
  this.cache = undefined;
75
75
 
76
76
  this.dialogFlow = undefined;
77
+
78
+ this.analyticsPublisher = undefined;
77
79
  }
78
80
 
79
81
 
@@ -595,6 +597,18 @@ class PubModulesManager {
595
597
  winston.error("PubModulesManager error initializing init dialogFlow module", err);
596
598
  }
597
599
  }
600
+
601
+ try {
602
+ this.analyticsPublisher = require('./analytics-publisher');
603
+ this.analyticsPublisher.listen();
604
+ winston.info("PubModulesManager analyticsPublisher initialized.");
605
+ } catch(err) {
606
+ if (err.code == 'MODULE_NOT_FOUND') {
607
+ winston.info("PubModulesManager init analyticsPublisher module not found");
608
+ } else {
609
+ winston.info("PubModulesManager error initializing analyticsPublisher module", err);
610
+ }
611
+ }
598
612
  }
599
613
 
600
614
  start() {