@tiledesk/tiledesk-server 2.26.0 → 2.27.1

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.
@@ -0,0 +1,36 @@
1
+
2
+ function parseList(value) {
3
+ if (!value) {
4
+ return [];
5
+ }
6
+ return value.split(",").map(function (t) { return t.trim(); }).filter(function (t) { return t.length > 0; });
7
+ }
8
+
9
+ /**
10
+ * Selects the routing keys the worker binds to, based on INCLUDE_QUEUE / EXCLUDE_QUEUE
11
+ * (comma separated routing keys, mutually exclusive).
12
+ * Returns { enabled, disabled, unknown, filtered }.
13
+ */
14
+ function resolveTopics(allTopics, includeValue, excludeValue) {
15
+ const include = parseList(includeValue);
16
+ const exclude = parseList(excludeValue);
17
+
18
+ if (include.length > 0 && exclude.length > 0) {
19
+ throw new Error("INCLUDE_QUEUE and EXCLUDE_QUEUE are mutually exclusive. Set only one of them.");
20
+ }
21
+
22
+ const selection = include.length > 0 ? include : exclude;
23
+ const unknown = selection.filter(function (t) { return allTopics.indexOf(t) === -1; });
24
+
25
+ let enabled;
26
+ if (include.length > 0) {
27
+ enabled = allTopics.filter(function (t) { return include.indexOf(t) !== -1; });
28
+ } else {
29
+ enabled = allTopics.filter(function (t) { return exclude.indexOf(t) === -1; });
30
+ }
31
+ const disabled = allTopics.filter(function (t) { return enabled.indexOf(t) === -1; });
32
+
33
+ return { enabled: enabled, disabled: disabled, unknown: unknown, filtered: selection.length > 0 };
34
+ }
35
+
36
+ module.exports = { parseList: parseList, resolveTopics: resolveTopics };
@@ -8,6 +8,8 @@ const botEvent = require('../../event/botEvent');
8
8
  const authEvent = require('../../event/authEvent');
9
9
  const kbEvent = require('../../event/kbEvent');
10
10
  const projectUserUpdateContextUtil = require('../../utils/projectUserUpdateContextUtil');
11
+ const queueTopicsFilter = require('./queueTopicsFilter');
12
+ const chat21WebhookWorker = require('./chat21WebhookWorker');
11
13
  // https://elements.heroku.com/addons/cloudamqp
12
14
  // https://gist.github.com/carlhoerberg/006b01ac17a0a94859ba#file-reconnect-js
13
15
  // http://www.rabbitmq.com/tutorials/tutorial-one-javascript.html
@@ -36,6 +38,63 @@ var exchange = process.env.QUEUE_EXCHANGE_TOPIC || 'amq.topic';
36
38
  var queueName = process.env.QUEUE_NAME || 'jobs';
37
39
  winston.info("Durable queue: " + durable + " Persistent queue: " + persistent + " Exchange topic: " + exchange+ " Queue name: " + queueName);
38
40
 
41
+ // Routing keys bound to the jobs queue (QUEUE_NAME) on the tiledesk exchange.
42
+ const JOBS_TOPICS = [
43
+ "request_create",
44
+ "request_update_preflight",
45
+ "request_participants_update",
46
+ "request_participants_join",
47
+ "request_participants_leave",
48
+ "request_assigned",
49
+ "request_update",
50
+ "request_close",
51
+ "request_close_extended",
52
+ "request_workingStatus_update",
53
+ "message_create",
54
+ "project_user_update",
55
+ "project_user_invite",
56
+ "project_user_invite_pending",
57
+ "project_user_delete",
58
+ "faqbot_update",
59
+ "faqbot_created",
60
+ "faqbot_deleted",
61
+ "faqbot_publish",
62
+ "kb_namespace_create",
63
+ "kb_namespace_delete",
64
+ "kb_contents_add",
65
+ "kb_contents_delete",
66
+ "kb_content_delete",
67
+ "lead_create",
68
+ "lead_update",
69
+ "lead_fullname_email_update",
70
+ "request_snapshot_update"
71
+ ];
72
+ // Consumed from its own queue (CHAT21_WEBHOOK_QUEUE_NAME), see chat21WebhookWorker.js.
73
+ const CHAT21_WEBHOOK_TOPIC = chat21WebhookWorker.CHAT21_WEBHOOK_ROUTING_KEY;
74
+ const ALL_WORKER_TOPICS = JOBS_TOPICS.concat([CHAT21_WEBHOOK_TOPIC]);
75
+
76
+ var topicsSelection = { enabled: ALL_WORKER_TOPICS, disabled: [], unknown: [], filtered: false };
77
+ var enabledJobsTopics = JOBS_TOPICS;
78
+ var disabledJobsTopics = [];
79
+ var chat21WebhookEnabled = true;
80
+
81
+ // Applies INCLUDE_QUEUE / EXCLUDE_QUEUE so that dedicated worker replicas can be deployed.
82
+ // Replicas with different filters on the jobs topics must use different QUEUE_NAME values:
83
+ // bindings belong to the queue, so replicas sharing a queue share its bindings.
84
+ function resolveWorkerTopics() {
85
+ topicsSelection = queueTopicsFilter.resolveTopics(ALL_WORKER_TOPICS, process.env.INCLUDE_QUEUE, process.env.EXCLUDE_QUEUE);
86
+ if (topicsSelection.unknown.length > 0) {
87
+ winston.warn("INCLUDE_QUEUE/EXCLUDE_QUEUE contain unknown routing keys: " + topicsSelection.unknown.join(","));
88
+ }
89
+ enabledJobsTopics = JOBS_TOPICS.filter(function(t) { return topicsSelection.enabled.indexOf(t) !== -1; });
90
+ disabledJobsTopics = JOBS_TOPICS.filter(function(t) { return topicsSelection.enabled.indexOf(t) === -1; });
91
+ chat21WebhookEnabled = topicsSelection.enabled.indexOf(CHAT21_WEBHOOK_TOPIC) !== -1;
92
+ winston.info("Worker topics enabled: " + topicsSelection.enabled.join(","));
93
+ if (topicsSelection.disabled.length > 0) {
94
+ winston.info("Worker topics disabled: " + topicsSelection.disabled.join(","));
95
+ }
96
+ }
97
+
39
98
 
40
99
  function start() {
41
100
  amqp.connect(url, function(err, conn) {
@@ -122,6 +181,13 @@ function publish(exchange, routingKey, content) {
122
181
  // A worker that acks messages only if processed succesfully
123
182
  // var channel;
124
183
  function startWorker() {
184
+ if (chat21WebhookEnabled) {
185
+ chat21WebhookWorker.start(amqpConn, closeOnErr);
186
+ }
187
+ if (enabledJobsTopics.length === 0) {
188
+ winston.info("Queue " + queueName + " not consumed: no jobs topic enabled by INCLUDE_QUEUE/EXCLUDE_QUEUE");
189
+ return;
190
+ }
125
191
  amqpConn.createChannel(function(err, ch) {
126
192
  if (closeOnErr(err)) return;
127
193
  ch.on("error", function(err) {
@@ -139,143 +205,21 @@ function startWorker() {
139
205
  ch.assertQueue(queueName, { durable: durable }, function(err, _ok) {
140
206
  // ch.assertQueue("jobs", { durable: true }, function(err, _ok) {
141
207
  if (closeOnErr(err)) return;
142
- ch.bindQueue(_ok.queue, exchange, "request_create", {}, function(err3, oka) {
143
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_create");
144
- winston.info("Data queue", oka)
145
- });
146
- ch.bindQueue(_ok.queue, exchange, "request_update_preflight", {}, function(err3, oka) {
147
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_update_preflight");
208
+ enabledJobsTopics.forEach(function(topic) {
209
+ ch.bindQueue(_ok.queue, exchange, topic, {}, function(err3, oka) {
210
+ winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: "+topic);
148
211
  winston.info("Data queue", oka)
149
- });
150
- ch.bindQueue(_ok.queue, exchange, "request_participants_update", {}, function(err3, oka) {
151
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_participants_update");
152
- winston.info("Data queue", oka)
153
- });
154
-
155
- ch.bindQueue(_ok.queue, exchange, "request_participants_join", {}, function(err3, oka) {
156
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_participants_join");
157
- winston.info("Data queue", oka)
158
- });
159
-
160
- ch.bindQueue(_ok.queue, exchange, "request_participants_leave", {}, function(err3, oka) {
161
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_participants_leave");
162
- winston.info("Data queue", oka)
163
- });
164
-
165
- ch.bindQueue(_ok.queue, exchange, "request_assigned", {}, function(err3, oka) {
166
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_assigned");
167
- winston.info("Data queue", oka)
168
- });
169
-
170
- ch.bindQueue(_ok.queue, exchange, "request_update", {}, function(err3, oka) {
171
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_update");
172
- winston.info("Data queue", oka)
173
- });
174
-
175
- ch.bindQueue(_ok.queue, exchange, "request_close", {}, function(err3, oka) {
176
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_close");
177
- winston.info("Data queue", oka)
178
- });
179
-
180
- ch.bindQueue(_ok.queue, exchange, "request_close_extended", {}, function(err3, oka) {
181
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_close_extended");
182
- winston.info("Data queue", oka)
183
- });
184
-
185
- ch.bindQueue(_ok.queue, exchange, "request_workingStatus_update", {}, function(err3, oka) {
186
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_workingStatus_update");
187
- winston.info("Data queue", oka)
188
- });
189
-
190
- ch.bindQueue(_ok.queue, exchange, "message_create", {}, function(err3, oka) {
191
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: message_create");
192
- winston.info("Data queue", oka)
193
- });
194
- ch.bindQueue(_ok.queue, exchange, "project_user_update", {}, function(err3, oka) {
195
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_update");
196
- winston.info("Data queue", oka)
197
- });
198
-
199
- ch.bindQueue(_ok.queue, exchange, "project_user_invite", {}, function(err3, oka) {
200
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_invite");
201
- winston.info("Data queue", oka)
202
- });
203
-
204
- ch.bindQueue(_ok.queue, exchange, "project_user_invite_pending", {}, function(err3, oka) {
205
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_invite_pending");
206
- winston.info("Data queue", oka)
207
- });
208
-
209
- ch.bindQueue(_ok.queue, exchange, "project_user_delete", {}, function(err3, oka) {
210
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: project_user_delete");
211
- winston.info("Data queue", oka)
212
- });
213
-
214
- ch.bindQueue(_ok.queue, exchange, "faqbot_update", {}, function(err3, oka) {
215
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_update");
216
- winston.info("Data queue", oka)
217
- });
218
-
219
- ch.bindQueue(_ok.queue, exchange, "faqbot_created", {}, function(err3, oka) {
220
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_created");
221
- winston.info("Data queue", oka)
222
- });
223
-
224
- ch.bindQueue(_ok.queue, exchange, "faqbot_deleted", {}, function(err3, oka) {
225
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_deleted");
226
- winston.info("Data queue", oka)
227
- });
228
-
229
- ch.bindQueue(_ok.queue, exchange, "faqbot_publish", {}, function(err3, oka) {
230
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: faqbot_publish");
231
- winston.info("Data queue", oka)
232
- });
233
-
234
- ch.bindQueue(_ok.queue, exchange, "kb_namespace_create", {}, function(err3, oka) {
235
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_namespace_create");
236
- winston.info("Data queue", oka)
237
- });
238
-
239
- ch.bindQueue(_ok.queue, exchange, "kb_namespace_delete", {}, function(err3, oka) {
240
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_namespace_delete");
241
- winston.info("Data queue", oka)
242
- });
243
-
244
- ch.bindQueue(_ok.queue, exchange, "kb_contents_add", {}, function(err3, oka) {
245
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_contents_add");
246
- winston.info("Data queue", oka)
247
- });
248
-
249
- ch.bindQueue(_ok.queue, exchange, "kb_contents_delete", {}, function(err3, oka) {
250
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_contents_delete");
251
- winston.info("Data queue", oka)
252
- });
253
-
254
- ch.bindQueue(_ok.queue, exchange, "kb_content_delete", {}, function(err3, oka) {
255
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: kb_content_delete");
256
- winston.info("Data queue", oka)
257
- });
258
-
259
- ch.bindQueue(_ok.queue, exchange, "lead_create", {}, function(err3, oka) {
260
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: lead_create");
261
- winston.info("Data queue", oka)
262
- });
263
-
264
- ch.bindQueue(_ok.queue, exchange, "lead_update", {}, function(err3, oka) {
265
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: lead_update");
266
- winston.info("Data queue", oka)
267
- });
268
-
269
- ch.bindQueue(_ok.queue, exchange, "lead_fullname_email_update", {}, function(err3, oka) {
270
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: lead_fullname_email_update");
271
- winston.info("Data queue", oka)
272
- });
273
-
274
- ch.bindQueue(_ok.queue, exchange, "request_snapshot_update", {}, function(err3, oka) {
275
- winston.info("Queue bind: "+_ok.queue+ " err: "+err3+ " key: request_snapshot_update");
276
- winston.info("Data queue", oka)
212
+ });
277
213
  });
278
214
 
215
+ if (topicsSelection.filtered) {
216
+ // Remove bindings left on the queue by a previous configuration.
217
+ disabledJobsTopics.forEach(function(topic) {
218
+ ch.unbindQueue(_ok.queue, exchange, topic, {}, function(err3) {
219
+ winston.info("Queue unbind: "+_ok.queue+ " err: "+err3+ " key: "+topic);
220
+ });
221
+ });
222
+ }
279
223
 
280
224
  ch.consume(queueName, processMsg, { noAck: false });
281
225
  winston.info("Worker is started");
@@ -804,6 +748,12 @@ function listen() {
804
748
  }
805
749
 
806
750
  if (process.env.QUEUE_ENABLED === "true") {
751
+ try {
752
+ resolveWorkerTopics();
753
+ } catch (e) {
754
+ winston.error("Invalid worker queue configuration: " + e.message);
755
+ process.exit(1);
756
+ }
807
757
  requestEvent.queueEnabled = true;
808
758
  messageEvent.queueEnabled = true;
809
759
  authEvent.queueEnabled = true;
@@ -0,0 +1,131 @@
1
+ // During the test the env variable is set to test
2
+ process.env.NODE_ENV = 'test';
3
+
4
+ const { expect } = require('chai');
5
+ const queueTopicsFilter = require('../queueTopicsFilter');
6
+ const chat21WebhookWorker = require('../chat21WebhookWorker');
7
+
8
+ const TOPICS = ['request_create', 'message_create', 'chat21.webhook.event'];
9
+
10
+ function makeMsg(body, redelivered) {
11
+ return {
12
+ content: Buffer.from(typeof body === 'string' ? body : JSON.stringify(body)),
13
+ fields: { redelivered: !!redelivered }
14
+ };
15
+ }
16
+
17
+ function httpError(status) {
18
+ const err = new Error('Request failed with status code ' + status);
19
+ err.response = { status: status };
20
+ return err;
21
+ }
22
+
23
+ describe('queue worker', function () {
24
+
25
+ describe('INCLUDE_QUEUE / EXCLUDE_QUEUE', function () {
26
+ it('enables every topic when no filter is set', function () {
27
+ const res = queueTopicsFilter.resolveTopics(TOPICS, undefined, '');
28
+ expect(res.enabled).to.deep.equal(TOPICS);
29
+ expect(res.disabled).to.deep.equal([]);
30
+ expect(res.filtered).to.equal(false);
31
+ });
32
+
33
+ it('INCLUDE_QUEUE enables only the listed topics', function () {
34
+ const res = queueTopicsFilter.resolveTopics(TOPICS, ' chat21.webhook.event ', undefined);
35
+ expect(res.enabled).to.deep.equal(['chat21.webhook.event']);
36
+ expect(res.disabled).to.deep.equal(['request_create', 'message_create']);
37
+ expect(res.filtered).to.equal(true);
38
+ });
39
+
40
+ it('EXCLUDE_QUEUE disables the listed topics', function () {
41
+ const res = queueTopicsFilter.resolveTopics(TOPICS, undefined, 'chat21.webhook.event,request_create');
42
+ expect(res.enabled).to.deep.equal(['message_create']);
43
+ expect(res.disabled).to.deep.equal(['request_create', 'chat21.webhook.event']);
44
+ });
45
+
46
+ it('reports unknown routing keys', function () {
47
+ const res = queueTopicsFilter.resolveTopics(TOPICS, 'message_create,not_a_topic', undefined);
48
+ expect(res.enabled).to.deep.equal(['message_create']);
49
+ expect(res.unknown).to.deep.equal(['not_a_topic']);
50
+ });
51
+
52
+ it('rejects INCLUDE_QUEUE and EXCLUDE_QUEUE set together', function () {
53
+ expect(function () {
54
+ queueTopicsFilter.resolveTopics(TOPICS, 'message_create', 'request_create');
55
+ }).to.throw(/mutually exclusive/);
56
+ });
57
+ });
58
+
59
+ describe('chat21 webhook event handling', function () {
60
+ const event = { event_type: 'message-sent', app_id: 'tilechat', data: { text: 'hello' } };
61
+
62
+ it('posts the event body to the chat21 webhook API and acks', async function () {
63
+ const calls = [];
64
+ const outcome = await chat21WebhookWorker.handleMessage(makeMsg(event), {
65
+ apiUrl: 'http://tiledesk/chat21/requests',
66
+ post: function (url, body) {
67
+ calls.push({ url: url, body: body });
68
+ return Promise.resolve({ status: 200 });
69
+ }
70
+ });
71
+ expect(outcome).to.equal('ack');
72
+ expect(calls).to.deep.equal([{ url: 'http://tiledesk/chat21/requests', body: event }]);
73
+ });
74
+
75
+ it('acks events rejected with a 4xx (e.g. not a support message)', async function () {
76
+ const outcome = await chat21WebhookWorker.handleMessage(makeMsg(event), {
77
+ post: function () { return Promise.reject(httpError(400)); }
78
+ });
79
+ expect(outcome).to.equal('ack');
80
+ });
81
+
82
+ it('requeues on 5xx the first time and discards on redelivery', async function () {
83
+ const post = function () { return Promise.reject(httpError(500)); };
84
+ expect(await chat21WebhookWorker.handleMessage(makeMsg(event, false), { post: post })).to.equal('requeue');
85
+ expect(await chat21WebhookWorker.handleMessage(makeMsg(event, true), { post: post })).to.equal('ack');
86
+ });
87
+
88
+ it('requeues when the API is unreachable', async function () {
89
+ const outcome = await chat21WebhookWorker.handleMessage(makeMsg(event, true), {
90
+ post: function () { return Promise.reject(new Error('connect ECONNREFUSED')); }
91
+ });
92
+ expect(outcome).to.equal('requeue');
93
+ });
94
+
95
+ it('discards malformed messages', async function () {
96
+ let called = false;
97
+ const outcome = await chat21WebhookWorker.handleMessage(makeMsg('not json'), {
98
+ post: function () { called = true; return Promise.resolve({ status: 200 }); }
99
+ });
100
+ expect(outcome).to.equal('ack');
101
+ expect(called).to.equal(false);
102
+ });
103
+ });
104
+
105
+ describe('rate limiter', function () {
106
+ it('runs immediately when unlimited', function () {
107
+ let runs = 0;
108
+ const limiter = chat21WebhookWorker.createRateLimiter(0);
109
+ for (let i = 0; i < 5; i++) limiter.schedule(function () { runs++; });
110
+ expect(runs).to.equal(5);
111
+ });
112
+
113
+ it('spreads executions according to the rate', function (done) {
114
+ const starts = [];
115
+ const limiter = chat21WebhookWorker.createRateLimiter(20); // one every 50ms
116
+ const begin = Date.now();
117
+ for (let i = 0; i < 5; i++) {
118
+ limiter.schedule(function () {
119
+ starts.push(Date.now() - begin);
120
+ if (starts.length === 5) {
121
+ expect(starts[4]).to.be.at.least(190);
122
+ for (let j = 1; j < starts.length; j++) {
123
+ expect(starts[j] - starts[j - 1]).to.be.at.least(40);
124
+ }
125
+ done();
126
+ }
127
+ });
128
+ }
129
+ });
130
+ });
131
+ });
@@ -0,0 +1,110 @@
1
+ const os = require('os');
2
+ const winston = require('../config/winston');
3
+ const Chat21SyncTrace = require('../models/chat21SyncTrace');
4
+
5
+ const MAX_STRING_LENGTH = 2000;
6
+ const enabled = process.env.CHAT21_SYNC_TRACE_ENABLED === "true";
7
+ const host = os.hostname();
8
+
9
+ if (enabled) {
10
+ winston.info("[Chat21SyncTracer] Chat21 sync tracing enabled");
11
+ // The TTL index must exist even when MONGOOSE_AUTOINDEX is false
12
+ Chat21SyncTrace.createIndexes().catch((err) => {
13
+ winston.error("[Chat21SyncTracer] Error creating indexes: ", err);
14
+ });
15
+ }
16
+
17
+ function truncate(value) {
18
+ if (typeof value !== 'string' || value.length <= MAX_STRING_LENGTH) {
19
+ return value;
20
+ }
21
+ return value.substring(0, MAX_STRING_LENGTH) + "...[truncated]";
22
+ }
23
+
24
+ function describeError(err) {
25
+ if (!err) {
26
+ return err;
27
+ }
28
+ if (err instanceof Error) {
29
+ return { message: err.message, code: err.code, stack: truncate(err.stack) };
30
+ }
31
+ return describeResponse(err);
32
+ }
33
+
34
+ // Serialized as a string: chat21 responses may contain keys that MongoDB does not accept (e.g. "$" or ".")
35
+ function describeResponse(body) {
36
+ if (body === undefined) {
37
+ return "undefined";
38
+ }
39
+ try {
40
+ return truncate(typeof body === 'string' ? body : JSON.stringify(body));
41
+ } catch (err) {
42
+ return truncate(String(body));
43
+ }
44
+ }
45
+
46
+ function describeRequest(request) {
47
+ if (!request) {
48
+ return request;
49
+ }
50
+ return {
51
+ status: request.status,
52
+ participants: Array.isArray(request.participants) ? Array.from(request.participants) : request.participants,
53
+ channelOutbound: request.channelOutbound?.name,
54
+ lead_id: request.lead?.lead_id
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Saves a trace document. Never throws and never blocks the caller.
60
+ * @param {string} event the traced flow, e.g. 'request.close'
61
+ * @param {{request_id: string, id_project: *}} ref
62
+ * @param {string} step
63
+ * @param {'info'|'success'|'error'|'skipped'} outcome
64
+ * @param {object} [data]
65
+ */
66
+ function trace(event, ref, step, outcome, data) {
67
+ if (!enabled) {
68
+ return;
69
+ }
70
+ try {
71
+ Chat21SyncTrace.create({
72
+ request_id: ref?.request_id,
73
+ id_project: ref?.id_project ? String(ref.id_project) : undefined,
74
+ event: event,
75
+ step: step,
76
+ outcome: outcome,
77
+ data: data,
78
+ host: host,
79
+ pid: process.pid
80
+ }).catch((err) => {
81
+ winston.error("[Chat21SyncTracer] Error saving trace " + event + " " + step + ": ", err);
82
+ });
83
+ } catch (err) {
84
+ winston.error("[Chat21SyncTracer] Error creating trace " + event + " " + step + ": ", err);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Traces a chat21 sdk call as <action>.sent, then <action>.response or <action>.error.
90
+ * The returned promise settles exactly like the one returned by callFn.
91
+ */
92
+ function traceCall(event, ref, action, data, callFn) {
93
+ trace(event, ref, action + '.sent', 'info', data);
94
+ return callFn().then((response) => {
95
+ trace(event, ref, action + '.response', 'success', { ...data, response: describeResponse(response) });
96
+ return response;
97
+ }, (err) => {
98
+ trace(event, ref, action + '.error', 'error', { ...data, error: describeError(err) });
99
+ throw err;
100
+ });
101
+ }
102
+
103
+ module.exports = {
104
+ enabled,
105
+ trace,
106
+ traceCall,
107
+ describeError,
108
+ describeResponse,
109
+ describeRequest
110
+ };