@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 +27 -0
- package/app.js +2 -1
- package/event/kbEvent.js +12 -0
- package/event/webhookEvent.js +9 -0
- package/jobs.js +12 -1
- package/lib/analyticsClient.js +60 -0
- package/package.json +2 -2
- package/pubmodules/analytics-publisher/index.js +437 -0
- package/pubmodules/pubModulesManager.js +14 -0
- package/routes/answered.js +73 -0
- package/routes/kb.js +22 -2
- package/routes/project_user.js +51 -20
- package/routes/public-request.js +305 -239
- package/routes/request.js +11 -1
- package/routes/unanswered.js +71 -0
- package/routes/urlPreview.js +57 -0
- package/routes/webhook.js +2 -0
- package/services/Scheduler.js +13 -0
- package/services/aiManager.js +30 -0
- package/services/urlPreviewService.js +50 -0
- package/utils/jobs-worker-queue-manager/JobManagerV2.js +23 -12
- package/utils/jobs-worker-queue-manager/queueManagerClassV2.js +270 -270
- package/utils/transcriptTimezone.js +101 -0
- package/views/messages-layout.jade +130 -0
- package/views/messages.jade +23 -22
- package/views/messages_old.jade +11 -4
- package/.env.sample +0 -141
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;
|
package/event/kbEvent.js
ADDED
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
|
+
"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.
|
|
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() {
|