wingbot 3.44.0-alpha.1 → 3.44.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.
package/index.js CHANGED
@@ -45,6 +45,8 @@ const { disambiguationQuickReply, quickReplyAction } = require('./src/utils/quic
45
45
  const { getUpdate, getValue, getSetState } = require('./src/utils/getUpdate');
46
46
  const { vars } = require('./src/utils/stateVariables');
47
47
  const compileWithState = require('./src/utils/compileWithState');
48
+ const onInteractionHandler = require('./src/analytics/onInteractionHandler');
49
+ const GA4 = require('./src/analytics/GA4');
48
50
  const plugins = require('./plugins/plugins.json');
49
51
  const {
50
52
  bufferloader,
@@ -123,5 +125,9 @@ module.exports = {
123
125
  // flags
124
126
  ...flags,
125
127
 
126
- wingbotVersion
128
+ wingbotVersion,
129
+
130
+ // ANALYTICS
131
+ onInteractionHandler,
132
+ GA4
127
133
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.44.0-alpha.1",
3
+ "version": "3.44.1",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/BotApp.js CHANGED
@@ -10,6 +10,7 @@ const BotAppSender = require('./BotAppSender');
10
10
  const Processor = require('./Processor');
11
11
  const ReturnSender = require('./ReturnSender');
12
12
  const headersToAuditMeta = require('./utils/headersToAuditMeta');
13
+ const onInteractionHandler = require('./analytics/onInteractionHandler');
13
14
 
14
15
  const DEFAULT_API_URL = 'https://orchestrator-api.wingbot.ai';
15
16
 
@@ -25,6 +26,9 @@ const DEFAULT_API_URL = 'https://orchestrator-api.wingbot.ai';
25
26
  /** @typedef {import('./BotAppSender').TlsOptions} TlsOptions */
26
27
  /** @typedef {import('./ReturnSender').ReturnSenderOptions} ReturnSenderOptions */
27
28
 
29
+ /** @typedef {import('./analytics/onInteractionHandler').IAnalyticsStorage} IAnalyticsStorage */
30
+ /** @typedef {import('./analytics/onInteractionHandler').HandlerConfig} HandlerConfig */
31
+
28
32
  /**
29
33
  * @typedef {object} BotAppOptions
30
34
  * @prop {string|Promise<string>} secret
@@ -95,6 +99,8 @@ class BotApp {
95
99
  this._appId = appId;
96
100
  this._auditLog = auditLog;
97
101
  this._tls = tls;
102
+ this._logger = options.log || console;
103
+ this._textFilter = options.textFilter;
98
104
 
99
105
  let { apiUrl } = options;
100
106
 
@@ -171,6 +177,34 @@ class BotApp {
171
177
  return this._processor;
172
178
  }
173
179
 
180
+ /**
181
+ *
182
+ * @param {IAnalyticsStorage} analyticsStorage
183
+ * @param {HandlerConfig} [options]
184
+ * @returns {this}
185
+ * @example
186
+ * const { GA4 } = require('wingbot');
187
+ *
188
+ * botApp.registerAnalyticsStorage(new GA4({
189
+ * measurementId: 'G-123456,
190
+ * apiSecret: 'apisecret'
191
+ * }))
192
+ */
193
+ registerAnalyticsStorage (analyticsStorage, options = {}) {
194
+ const log = this._logger || options.log;
195
+
196
+ analyticsStorage.setDefaultLogger(log);
197
+
198
+ const handler = onInteractionHandler({
199
+ log,
200
+ anonymize: this._textFilter,
201
+ ...options
202
+ }, analyticsStorage);
203
+
204
+ this.processor.on('interaction', handler);
205
+ return this;
206
+ }
207
+
174
208
  _errorResponse (message, status) {
175
209
  return {
176
210
  statusCode: status,
package/src/Processor.js CHANGED
@@ -65,11 +65,18 @@ const { mergeState, isUserInteraction } = require('./utils/stateVariables');
65
65
  * @type {InteractionEvent}
66
66
  */
67
67
 
68
+ /**
69
+ * @typedef {object} ILogger
70
+ * @prop {Function} log
71
+ * @prop {Function} warn
72
+ * @prop {Function} error
73
+ */
74
+
68
75
  /**
69
76
  *
70
77
  * @typedef {object} ProcessorOptions
71
78
  * @prop {string} [appUrl] - url basepath for relative links
72
- * @prop {object} [stateStorage] - chatbot state storage
79
+ * @prop {IStateStorage} [stateStorage] - chatbot state storage
73
80
  * @prop {object} [tokenStorage] - frontend token storage
74
81
  * @prop {Function} [translator] - text translate function
75
82
  * @prop {number} [timeout] - chat sesstion lock duration (30000)
@@ -78,7 +85,7 @@ const { mergeState, isUserInteraction } = require('./utils/stateVariables');
78
85
  * @prop {number} [retriesWhenWaiting] - number of attampts (6)
79
86
  * @prop {Function} [nameFromState] - override the name translator
80
87
  * @prop {boolean|AutoTypingConfig} [autoTyping] - enable or disable automatic typing
81
- * @prop {Function} [log] - console like error logger
88
+ * @prop {ILogger} [log] - console like error logger
82
89
  * @prop {object} [defaultState] - default chat state
83
90
  * @prop {boolean} [autoSeen] - send seen automatically
84
91
  * @prop {number} [redirectLimit] - maximum number of redirects at single request
@@ -103,6 +110,13 @@ const { mergeState, isUserInteraction } = require('./utils/stateVariables');
103
110
  * @prop {string|null} [meta.targetAction]
104
111
  */
105
112
 
113
+ /**
114
+ * @typedef {object} IStateStorage
115
+ * @prop {Function} saveState
116
+ * @prop {Function} getState
117
+ * @prop {Function} getOrCreateAndLock
118
+ */
119
+
106
120
  function NAME_FROM_STATE (state) {
107
121
  if (state.user && state.user.firstName) {
108
122
  return `${state.user.firstName} ${state.user.lastName}`;
@@ -157,7 +171,7 @@ class Processor extends EventEmitter {
157
171
  this.reducer = reducer;
158
172
 
159
173
  /**
160
- * @type {StateStorage}
174
+ * @type {IStateStorage}
161
175
  */
162
176
  this.stateStorage = this.options.stateStorage;
163
177
 
@@ -236,7 +250,7 @@ class Processor extends EventEmitter {
236
250
 
237
251
  async _reportError (pageId, err, event, senderId = null) {
238
252
  if (err.code === 204) {
239
- this.options.log.info(`nothing sent: ${err.message}`, event);
253
+ this.options.log.log(`nothing sent: ${err.message}`, event);
240
254
  return;
241
255
  }
242
256
  if (err.code !== 403) {
@@ -0,0 +1,272 @@
1
+ /**
2
+ * @author David Menger
3
+ */
4
+ 'use strict';
5
+
6
+ const fetch = require('node-fetch').default;
7
+
8
+ /** @typedef {import('./onInteractionHandler').Event} Event */
9
+ /** @typedef {import('./onInteractionHandler').IAnalyticsStorage} IAnalyticsStorage */
10
+ /** @typedef {import('./onInteractionHandler').GAUser} GAUser */
11
+ /** @typedef {import('./onInteractionHandler').SessionMetadata} SessionMetadata */
12
+ /** @typedef {import('./onInteractionHandler').IGALogger} IGALogger */
13
+
14
+ /** @typedef {import('node-fetch').RequestInit} RequestInit */
15
+
16
+ /**
17
+ * @typedef {object} FetchResult
18
+ * @param {number} status
19
+ * @param {string} [statusText]
20
+ * @param {Promise<object>} json
21
+ */
22
+
23
+ /**
24
+ * @callback MockFetch
25
+ * @param {string} url
26
+ * @param {RequestInit} [options]
27
+ * @returns {Promise<FetchResult>}
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} GAOptions
32
+ * @prop {string} measurementId
33
+ * @prop {string} apiSecret
34
+ * @prop {boolean} [debug]
35
+ * @prop {IGALogger} [log]
36
+ * @prop {MockFetch} [fetch]
37
+ */
38
+ /**
39
+ * @class GA4
40
+ * @implements {IAnalyticsStorage}
41
+ */
42
+ class GA4 {
43
+
44
+ /**
45
+ *
46
+ * @param {GAOptions} options
47
+ */
48
+ constructor (options) {
49
+ this._options = options;
50
+
51
+ /** @type {IGALogger} */
52
+ this._logger = options.log || console;
53
+
54
+ this._urlQuery = `measurement_id=${encodeURIComponent(options.measurementId)}&api_secret=${encodeURIComponent(options.apiSecret)}`;
55
+ this._url = `https://www.google-analytics.com/mp/collect?${this._urlQuery}`;
56
+
57
+ this.hasExtendedEvents = true;
58
+
59
+ this._fetch = options.fetch || fetch;
60
+ }
61
+
62
+ /**
63
+ * @param {IGALogger} logger
64
+ */
65
+ setDefaultLogger (logger) {
66
+ if (this._logger === console) {
67
+ this._logger = logger;
68
+ }
69
+ }
70
+
71
+ /**
72
+ *
73
+ * @param {string} pageId
74
+ * @param {string} senderId
75
+ * @param {string} sessionId
76
+ * @param {SessionMetadata} [metadata]
77
+ * @param {number} [ts]
78
+ * @param {boolean} [nonInteractive]
79
+ * @returns {Promise}
80
+ */
81
+ async createUserSession (
82
+ pageId,
83
+ senderId,
84
+ sessionId,
85
+ metadata = {},
86
+ ts = Date.now(),
87
+ nonInteractive = false
88
+ ) {
89
+ const uafvl = 'wingbot';
90
+
91
+ const { lang = '', sessionCount = 1, action = '/' } = metadata;
92
+
93
+ const event = {
94
+ v: 2,
95
+ tid: this._options.measurementId,
96
+ _p: Math.round(2147483647 * Math.random()),
97
+ sr: '1x1',
98
+ _dbg: this._options.debug ? 1 : 0,
99
+ ul: lang, // language
100
+ cid: this._conversationId(pageId, senderId),
101
+
102
+ // dl: 'https://wingbot-web-staging.flyto.cloud/dp',
103
+ dp: action,
104
+ dr: '', // referral (url)
105
+ dt: action === '/' ? '(none)' : action.replace(/-/g, ' '),
106
+
107
+ // en: 'page_view', // event name
108
+ en: 'scroll',
109
+ 'epn.percent_scrolled': 100,
110
+
111
+ uafvl, // must have
112
+
113
+ sct: sessionCount, // session count (int)
114
+ seg: nonInteractive ? 0 : 1, // session engagement (boolean)
115
+ sid: sessionId, // session id (string)
116
+
117
+ // this was sent during the first visit
118
+ _fv: sessionCount === 1, // first visit (bool)
119
+ _nsi: 1, // new session id (bool)
120
+ _ss: 1, // session start (bool)
121
+
122
+ _ee: 1, // ? page_view event parameter (??? event engagement ??)
123
+
124
+ _s: 1, // session hit count (was 1 every request),
125
+ _et: ts // event time (number)
126
+ };
127
+
128
+ if (this._options.debug) {
129
+ this._logger.log('GA4: starting session', event);
130
+ }
131
+
132
+ const query = Object.entries(event)
133
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
134
+ .join('&');
135
+
136
+ // const url = 'https://region1.google-analytics.com/g/collect';
137
+ const url = 'https://www.google-analytics.com/g/collect';
138
+
139
+ const res = await this._fetch(`${url}?${query}`, {
140
+ method: 'POST',
141
+ headers: {
142
+ 'user-agent': uafvl
143
+ }
144
+ });
145
+
146
+ if (res.status >= 400) {
147
+ this._logger.error('GA4: failed to create session', {
148
+ url,
149
+ query,
150
+ status: res.status
151
+ });
152
+ }
153
+ }
154
+
155
+ _conversationId (pageId, senderId) {
156
+ return `${pageId}.${senderId}`;
157
+ }
158
+
159
+ /**
160
+ *
161
+ * @param {string} pageId
162
+ * @param {string} senderId
163
+ * @param {string} sessionId
164
+ * @param {Event[]} events
165
+ * @param {GAUser} [user]
166
+ * @param {number} [ts]
167
+ * @returns {Promise}
168
+ */
169
+ async storeEvents (
170
+ pageId,
171
+ senderId,
172
+ sessionId,
173
+ events,
174
+ user = null,
175
+ ts = Date.now()
176
+ ) {
177
+ if (events.length === 0) {
178
+ return;
179
+ }
180
+
181
+ const body = {
182
+ client_id: this._conversationId(pageId, senderId),
183
+ timestamp_micros: ts * 1000,
184
+ non_personalized_ads: false,
185
+ events: events.map((e) => {
186
+ const { type: name, ...params } = e;
187
+ Object.entries(params)
188
+ .forEach(([k, v]) => {
189
+ if (v === null) {
190
+ params[k] = '(none)';
191
+ }
192
+ });
193
+ // Object.assign(params, { session_id: sessionId });
194
+ switch (name) {
195
+ case 'page_view':
196
+ return {
197
+ name,
198
+ params: {
199
+ page_path: e.action,
200
+ page_title: e.action
201
+ .replace(/^\/+/, '')
202
+ .replace(/[-]+/g, ' ')
203
+ .replace(/[/]+/g, ' - '),
204
+ ...params
205
+ }
206
+ };
207
+ default:
208
+ return { name, params };
209
+ }
210
+ })
211
+ };
212
+
213
+ if (user) {
214
+ const { id, ...other } = user;
215
+
216
+ Object.assign(body, {
217
+ user_id: id,
218
+ user_properties: Object.fromEntries(
219
+ Object.entries(other)
220
+ .map(([key, value]) => [key, { value }])
221
+ )
222
+ });
223
+ }
224
+
225
+ const params = {
226
+ method: 'POST',
227
+ body: JSON.stringify(body)
228
+ };
229
+
230
+ let err;
231
+ let res;
232
+ try {
233
+ res = await this._fetch(this._url, params);
234
+
235
+ if (res.status >= 400) {
236
+ throw new Error(`${res.statusText} [${res.status}]`);
237
+ }
238
+ if (!this._options.debug) {
239
+ return;
240
+ }
241
+ } catch (e) {
242
+ err = e;
243
+ }
244
+
245
+ let { message = 'GENERIC FAIL' } = err || { message: null };
246
+ let validationMessages = [];
247
+ try {
248
+ const dbg = await this._fetch(`https://www.google-analytics.com/debug/mp/collect?${this._urlQuery}`, params);
249
+
250
+ if (dbg.status >= 300) {
251
+ throw new Error(`${dbg.statusText} [${dbg.status}]`);
252
+ }
253
+
254
+ ({ validationMessages = [] } = await dbg.json());
255
+
256
+ message = (validationMessages[0] || { description: message }).description;
257
+ } catch (e) {
258
+ this._logger.log('GA4 debug failed', e);
259
+ message += ` +(${e.message})`;
260
+ }
261
+
262
+ if (validationMessages.length || message) {
263
+ this._logger.log('GA4: validationMessages', validationMessages);
264
+ this._logger.error(`GA4: fail: ${message} [${res ? res.status : 0}]`, params.body);
265
+ } else {
266
+ this._logger.log(`GA4: debug [${res ? res.status : 0}]`, params.body);
267
+ }
268
+ }
269
+
270
+ }
271
+
272
+ module.exports = GA4;
@@ -0,0 +1,393 @@
1
+ /**
2
+ * @author wingbot.ai
3
+ */
4
+ 'use strict';
5
+
6
+ const { replaceDiacritics } = require('webalize');
7
+ const Ai = require('../Ai');
8
+
9
+ /** @typedef {import('../Processor').InteractionEvent} InteractionEvent */
10
+ /** @typedef {import('../Request')} Request */
11
+
12
+ /**
13
+ * @typedef {object} GAUser
14
+ * @prop {string} [id]
15
+ */
16
+
17
+ /**
18
+ * @typedef IGALogger
19
+ * @prop {Function} log
20
+ * @prop {Function} error
21
+ */
22
+
23
+ /**
24
+ * @typedef {object} Event
25
+ * @prop {'conversation'|'page_view'|string} type
26
+ * @prop {string} [category]
27
+ * @prop {string} [action]
28
+ * @prop {string} [label]
29
+ * @prop {number} [value]
30
+ */
31
+
32
+ /**
33
+ * @typedef {object} SessionMetadata
34
+ * @prop {number} [sessionCount]
35
+ * @prop {string} [lang]
36
+ * @prop {string} [action]
37
+ */
38
+
39
+ /**
40
+ * @callback CreateUserSession
41
+ * @param {string} pageId
42
+ * @param {string} senderId
43
+ * @param {string} sessionId
44
+ * @param {SessionMetadata} [metadata]
45
+ * @param {number} [ts]
46
+ * @param {boolean} [nonInteractive]
47
+ * @returns {Promise}
48
+ */
49
+
50
+ /**
51
+ * @callback StoreEvents
52
+ * @param {string} pageId
53
+ * @param {string} senderId
54
+ * @param {string} sessionId
55
+ * @param {Event[]} events
56
+ * @param {GAUser} [user]
57
+ * @param {number} [ts]
58
+ * @param {boolean} [nonInteractive]
59
+ * @param {boolean} [sessionStarted]
60
+ * @returns {Promise}
61
+ */
62
+
63
+ /**
64
+ * @callback LoggerSetter
65
+ * @param {IGALogger} logger
66
+ */
67
+
68
+ /**
69
+ * @typedef {object} IAnalyticsStorage
70
+ * @prop {LoggerSetter} setDefaultLogger - console like logger
71
+ * @prop {StoreEvents} storeEvents
72
+ * @prop {CreateUserSession} createUserSession
73
+ * @prop {boolean} [hasExtendedEvents]
74
+ */
75
+
76
+ /**
77
+ * @callback UserExtractor
78
+ * @param {Request} req
79
+ * @returns {object & GAUser}
80
+ */
81
+
82
+ /**
83
+ * @callback Anonymizer
84
+ * @param {string} text
85
+ * @returns {string}
86
+ */
87
+
88
+ /**
89
+ * @typedef {object} TrackingEvents
90
+ * @prop {Event[]} events
91
+ */
92
+
93
+ /**
94
+ * @typedef {object} IConfidenceProvider
95
+ * @prop {number} confidence
96
+ */
97
+
98
+ /**
99
+ * @typedef {object} HandlerConfig
100
+ * @prop {boolean} [enabled] - default true
101
+ * @prop {boolean} [throwException] - default false
102
+ * @prop {IGALogger} [log] - console like logger
103
+ * @prop {Anonymizer} [anonymize] - text anonymization function
104
+ * @prop {UserExtractor} [userExtractor] - text anonymization function
105
+ */
106
+
107
+ /**
108
+ * @callback IInteractionHandler
109
+ * @param {InteractionEvent} params
110
+ * @returns {Promise}
111
+ */
112
+
113
+ /**
114
+ *
115
+ * @param {HandlerConfig} config
116
+ * @param {IAnalyticsStorage} analyticsStorage
117
+ * @param {IConfidenceProvider} [ai]
118
+ * @returns {IInteractionHandler}
119
+ */
120
+ function onInteractionHandler (
121
+ {
122
+ enabled = true,
123
+ throwException = false,
124
+ log = console,
125
+ anonymize = (x) => x,
126
+ userExtractor = (req) => null // eslint-disable-line no-unused-vars
127
+ },
128
+ analyticsStorage,
129
+ ai = Ai.ai
130
+ ) {
131
+
132
+ /**
133
+ * @param {InteractionEvent} params
134
+ */
135
+ async function onInteraction ({
136
+ req,
137
+ actions,
138
+ lastAction,
139
+ // state,
140
+ // data,
141
+ skill,
142
+ tracking
143
+ }) {
144
+ if (!enabled) {
145
+ return;
146
+ }
147
+ try {
148
+ const nonInteractive = !!req.campaign;
149
+ const {
150
+ pageId,
151
+ senderId,
152
+ timestamp
153
+ } = req;
154
+
155
+ const {
156
+ _snew: createSession,
157
+ _sct: sessionCount,
158
+ _sid: sessionId,
159
+ lang
160
+ } = req.state;
161
+
162
+ const [action = '(none)', ...otherActions] = actions;
163
+
164
+ if (createSession) {
165
+ const metadata = {
166
+ sessionCount,
167
+ lang,
168
+ action
169
+ };
170
+
171
+ await analyticsStorage.createUserSession(
172
+ pageId,
173
+ senderId,
174
+ sessionId,
175
+ metadata,
176
+ timestamp,
177
+ nonInteractive
178
+ );
179
+ }
180
+
181
+ const [{
182
+ intent = '',
183
+ score = 0
184
+ } = {}] = req.intents;
185
+
186
+ const text = req.isConfidentInput()
187
+ ? '*****'
188
+ : anonymize(
189
+ replaceDiacritics(req.text()).replace(/\s+/g, ' ').toLowerCase().trim()
190
+ );
191
+
192
+ let winnerAction = '';
193
+ let winnerScore = 0;
194
+ let winnerIntent = '';
195
+ let winnerEntities = [];
196
+ let winnerTaken = false;
197
+
198
+ const winners = req.aiActions();
199
+
200
+ if (winners.length > 0) {
201
+ [{
202
+ action: winnerAction = '(none)',
203
+ sort: winnerScore = 0,
204
+ intent: { intent: winnerIntent, entities: winnerEntities = [] }
205
+ }] = winners;
206
+
207
+ winnerTaken = action === winnerAction;
208
+ }
209
+
210
+ const expected = req.expected() ? req.expected().action : '';
211
+
212
+ const isContextUpdate = req.isSetContext();
213
+ const isNotification = !!req.campaign;
214
+ const isAttachment = req.isAttachment();
215
+ const isQuickReply = req.isQuickReply();
216
+ const isPassThread = !!req.event.pass_thread_control;
217
+ const isText = !isQuickReply && req.isText();
218
+ const isPostback = req.isPostBack();
219
+
220
+ const allActions = actions.join(',');
221
+ const requestAction = req.action();
222
+
223
+ const events = [];
224
+
225
+ const actionMeta = {
226
+ requestAction: req.action() || '(none)',
227
+ expected,
228
+ expectedTaken: requestAction === expected,
229
+ isContextUpdate,
230
+ isAttachment,
231
+ isNotification,
232
+ isQuickReply,
233
+ isPassThread,
234
+ isText,
235
+ isPostback,
236
+ winnerAction,
237
+ winnerIntent,
238
+ winnerEntities: winnerEntities.map((e) => e.entity).join(','),
239
+ winnerScore,
240
+ winnerTaken,
241
+ intent,
242
+ intentScore: score,
243
+ entities: req.entities.map((e) => e.entity).join(','),
244
+ text,
245
+ allActions
246
+ };
247
+
248
+ events.push({
249
+ type: 'page_view',
250
+ action,
251
+ allActions,
252
+ nonInteractive,
253
+ lastAction,
254
+ prevAction: lastAction,
255
+ skill,
256
+ lang,
257
+ cd1: req.state.lang,
258
+ ...(analyticsStorage.hasExtendedEvents ? {} : actionMeta)
259
+ });
260
+
261
+ let prevAction = action;
262
+
263
+ events.push(
264
+ ...otherActions.map((a) => {
265
+ const r = {
266
+ type: 'page_view',
267
+ action: a,
268
+ allActions,
269
+ nonInteractive: false,
270
+ lastAction,
271
+ prevAction,
272
+ skill,
273
+ isGoto: true,
274
+ ...(analyticsStorage.hasExtendedEvents
275
+ ? { lang }
276
+ : { cd1: lang })
277
+ };
278
+
279
+ prevAction = a;
280
+ return r;
281
+ })
282
+ );
283
+
284
+ events.push(
285
+ ...tracking.events.map(({
286
+ type, category, action: eventAction, label, value
287
+ }) => ({
288
+ lastAction,
289
+ type,
290
+ category,
291
+ action: eventAction,
292
+ label,
293
+ value,
294
+ ...(analyticsStorage.hasExtendedEvents
295
+ ? { lang }
296
+ : { cd1: lang })
297
+ }))
298
+ );
299
+
300
+ if (!nonInteractive) {
301
+
302
+ if (req.isText()) {
303
+ events.push({
304
+ type: 'ai',
305
+ // @ts-ignore
306
+ lastAction,
307
+ category: 'Intent: Detection',
308
+ intent,
309
+ action,
310
+ label: text,
311
+ value: score >= ai.confidence ? 0 : 1,
312
+ ...(analyticsStorage.hasExtendedEvents
313
+ ? { lang }
314
+ : { cd1: lang })
315
+ });
316
+ }
317
+
318
+ const notHandled = actions.some((a) => a.match(/\*$/)) && !req.isQuickReply();
319
+
320
+ let actionCategory = 'User: ';
321
+ let label = '(none)';
322
+ const value = notHandled ? 1 : 0;
323
+
324
+ if (req.isSticker()) {
325
+ actionCategory += 'Sticker';
326
+ label = req.attachmentUrl(0);
327
+ } else if (req.isImage()) {
328
+ actionCategory += 'Image';
329
+ label = req.attachmentUrl(0);
330
+ } else if (req.hasLocation()) {
331
+ actionCategory += 'Location';
332
+ const { lat, long } = req.getLocation();
333
+ label = `${lat}, ${long}`;
334
+ } else if (isAttachment) {
335
+ actionCategory += 'Attachement';
336
+ label = req.attachment(0).type;
337
+ } else if (isText) {
338
+ actionCategory += 'Text';
339
+ label = text;
340
+ } else if (isQuickReply) {
341
+ actionCategory += 'Quick reply';
342
+ label = text;
343
+ } else if (req.isReferral() || req.isOptin()) {
344
+ actionCategory = req.isOptin()
345
+ ? 'Entry: Optin'
346
+ : 'Entry: Referral';
347
+ } else if (isPostback) {
348
+ actionCategory += 'Button - bot';
349
+ label = req.data.postback.title || '(unknown)';
350
+ } else {
351
+ actionCategory += 'Other';
352
+ }
353
+
354
+ events.push({
355
+ ...(analyticsStorage.hasExtendedEvents ? actionMeta : {}),
356
+ type: 'conversation',
357
+ // @ts-ignore
358
+ lastAction,
359
+ category: actionCategory,
360
+ action,
361
+ label,
362
+ value,
363
+ ...(analyticsStorage.hasExtendedEvents
364
+ ? { lang }
365
+ : { cd1: lang })
366
+ });
367
+ }
368
+
369
+ const user = userExtractor(req);
370
+
371
+ await analyticsStorage.storeEvents(
372
+ pageId,
373
+ senderId,
374
+ sessionId,
375
+ // @ts-ignore
376
+ events,
377
+ user,
378
+ timestamp,
379
+ nonInteractive,
380
+ createSession
381
+ );
382
+ } catch (e) {
383
+ if (throwException) {
384
+ throw e;
385
+ }
386
+ log.error('failed sending logs', e);
387
+ }
388
+ }
389
+
390
+ return onInteraction;
391
+ }
392
+
393
+ module.exports = onInteractionHandler;