wingbot 3.45.1 → 3.46.0-alpha.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
@@ -52,7 +52,11 @@ const {
52
52
  bufferloader,
53
53
  MemoryStateStorage
54
54
  } = require('./src/tools');
55
- const flags = require('./src/flags');
55
+ const {
56
+ TrackingCategory,
57
+ TrackingType,
58
+ ResponseFlag
59
+ } = require('./src/analytics/consts');
56
60
 
57
61
  const { version: wingbotVersion } = require('./package.json');
58
62
 
@@ -122,8 +126,10 @@ module.exports = {
122
126
  // tests
123
127
  ConversationTester,
124
128
 
125
- // flags
126
- ...flags,
129
+ // flags & tracking
130
+ TrackingCategory,
131
+ TrackingType,
132
+ ResponseFlag,
127
133
 
128
134
  wingbotVersion,
129
135
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.45.1",
3
+ "version": "3.46.0-alpha.1",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/BotApp.js CHANGED
@@ -116,6 +116,8 @@ class BotApp {
116
116
  this._senderLogger = chatLogStorage;
117
117
  this._verify = promisify(jwt.verify);
118
118
 
119
+ this._bot = bot;
120
+
119
121
  this._processor = new Processor(bot, {
120
122
  ...processorOptions,
121
123
  secret,
@@ -198,11 +200,18 @@ class BotApp {
198
200
  registerAnalyticsStorage (analyticsStorage, options = {}) {
199
201
  const log = this._logger || options.log;
200
202
 
201
- analyticsStorage.setDefaultLogger(log);
203
+ if (typeof analyticsStorage.setDefaultLogger === 'function') {
204
+ analyticsStorage.setDefaultLogger(log);
205
+ }
206
+
207
+ // @ts-ignore
208
+ const { snapshot = null, botId = null } = this._bot;
202
209
 
203
210
  const { onInteraction, onEvent } = onInteractionHandler({
204
211
  log,
205
212
  anonymize: this._textFilter,
213
+ snapshot,
214
+ botId,
206
215
  ...options
207
216
  }, analyticsStorage);
208
217
 
@@ -221,7 +230,7 @@ class BotApp {
221
230
  * @param {boolean} [nonInteractive]
222
231
  */
223
232
  async trackEvent (pageId, senderId, event, ts = Date.now(), nonInteractive = false) {
224
- const state = this._processor.stateStorage.getState(senderId, pageId);
233
+ const state = await this._processor.stateStorage.getState(senderId, pageId);
225
234
 
226
235
  if (!state) {
227
236
  throw new Error(`State ${pageId}:${senderId} not found. Ensure the #trackEvent() method was called after the conversation has started`);
@@ -39,6 +39,7 @@ const MESSAGE_RESOLVER_NAME = 'botbuild.message';
39
39
  * @typedef {object} Route
40
40
  * @prop {number} id
41
41
  * @prop {string|null} path
42
+ * @prop {string|null} [skill]
42
43
  * @prop {Resolver[]} resolvers
43
44
  * @prop {boolean} [isFallback]
44
45
  * @prop {string[]} [aiTags]
@@ -238,6 +239,14 @@ class BuildRouter extends Router {
238
239
  }
239
240
  }
240
241
 
242
+ get snapshot () {
243
+ return this._snapshot;
244
+ }
245
+
246
+ get botId () {
247
+ return this._botId;
248
+ }
249
+
241
250
  /**
242
251
  * @returns {C}
243
252
  */
@@ -674,9 +683,10 @@ class BuildRouter extends Router {
674
683
  *
675
684
  * @param {TransformedRoute} route
676
685
  * @param {boolean} nextRouteIsSameResponder
686
+ * @param {string} includedBlockId
677
687
  * @returns {Middleware<S,C>[]}
678
688
  */
679
- _buildRouteHead (route, nextRouteIsSameResponder) {
689
+ _buildRouteHead (route, nextRouteIsSameResponder, includedBlockId) {
680
690
  const resolvers = [];
681
691
 
682
692
  if (!route.isFallback) {
@@ -712,6 +722,11 @@ class BuildRouter extends Router {
712
722
  if (bounceResolver) {
713
723
  resolvers.push(bounceResolver);
714
724
  }
725
+ } else if (!includedBlockId && route.skill) {
726
+ resolvers.push((req, res) => {
727
+ res.trackAsSkill(route.skill);
728
+ return Router.CONTINUE;
729
+ });
715
730
  }
716
731
  }
717
732
 
@@ -752,7 +767,7 @@ class BuildRouter extends Router {
752
767
  };
753
768
 
754
769
  const resolvers = [
755
- ...this._buildRouteHead(route, nextRouteIsSameResponder),
770
+ ...this._buildRouteHead(route, nextRouteIsSameResponder, includedBlockId),
756
771
  ...this.buildResolvers(route.resolvers, route, buildInfo)
757
772
  ];
758
773
 
package/src/Processor.js CHANGED
@@ -16,6 +16,9 @@ const { mergeState, isUserInteraction } = require('./utils/stateVariables');
16
16
  /** @typedef {import('./ReducerWrapper')} ReducerWrapper */
17
17
  /** @typedef {import('./Router')} Router */
18
18
  /** @typedef {import('./BuildRouter')} BuildRouter */
19
+ /** @typedef {import('./analytics/consts').TrackingCategory} TrackingCategory */
20
+ /** @typedef {import('./analytics/consts').TrackingType} TrackingType */
21
+ /** @typedef {import('./analytics/consts').ResponseFlag} ResponseFlag */
19
22
 
20
23
  /**
21
24
  * @typedef {object} AutoTypingConfig
@@ -35,8 +38,8 @@ const { mergeState, isUserInteraction } = require('./utils/stateVariables');
35
38
 
36
39
  /**
37
40
  * @typedef {object} TrackingEvent
38
- * @prop {string} type
39
- * @prop {string} category
41
+ * @prop {TrackingType} type
42
+ * @prop {TrackingCategory} category
40
43
  * @prop {string} action
41
44
  * @prop {string} label
42
45
  * @prop {number} value
@@ -55,7 +58,10 @@ const { mergeState, isUserInteraction } = require('./utils/stateVariables');
55
58
  * @prop {object} state
56
59
  * @prop {object} data
57
60
  * @prop {string|null} skill
58
- * @prop {TrackingObject} tracking
61
+ * @prop {TrackingObject} tracking - deprecated
62
+ * @prop {TrackingEvent[]} events
63
+ * @prop {ResponseFlag|null} flag
64
+ * @prop {boolean} nonInteractive
59
65
  */
60
66
 
61
67
  /**
@@ -134,6 +140,19 @@ function NAME_FROM_STATE (state) {
134
140
  }
135
141
 
136
142
  const MAX_TS = 9999999999999;
143
+ const CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
144
+
145
+ function toBase (number) {
146
+ let result = '';
147
+ let integer = number;
148
+
149
+ do {
150
+ result = CHARS[integer % 62] + result;
151
+ integer = Math.floor(integer / 62);
152
+ } while (integer > 0);
153
+
154
+ return result;
155
+ }
137
156
 
138
157
  /**
139
158
  * Messaging event processor
@@ -381,7 +400,7 @@ class Processor extends EventEmitter {
381
400
  } = await this
382
401
  ._processMessage(message, pageId, messageSender, responderData, preloadPromise));
383
402
 
384
- await this._emitInteractionEvent(req, messageSender, state, data);
403
+ await this._emitInteractionEvent(req, res, messageSender, state, data);
385
404
 
386
405
  return messageSender.finished(req, res, null, errorHandler);
387
406
  } catch (e) {
@@ -394,12 +413,13 @@ class Processor extends EventEmitter {
394
413
  /**
395
414
  *
396
415
  * @param {Request} req
416
+ * @param {Responder} res
397
417
  * @param {ReturnSender} messageSender
398
418
  * @param {object} state
399
419
  * @param {object} data
400
420
  * @returns {Promise}
401
421
  */
402
- _emitInteractionEvent (req, messageSender, state, data) {
422
+ _emitInteractionEvent (req, res, messageSender, state, data) {
403
423
  const shouldNotTrack = data._initialEventShouldNotBeTracked === true;
404
424
 
405
425
  if (shouldNotTrack) {
@@ -408,7 +428,10 @@ class Processor extends EventEmitter {
408
428
 
409
429
  const { _lastAction: lastAction = null } = req.state;
410
430
  const actions = messageSender.visitedInteractions;
411
- const skill = state._trackAsSkill || null;
431
+ const skill = typeof res.newState._trackAsSkill === 'undefined'
432
+ ? (req.state._trackAsSkill || null)
433
+ : res.newState._trackAsSkill;
434
+ const { events = [] } = messageSender.tracking;
412
435
 
413
436
  const event = {
414
437
  req,
@@ -417,7 +440,10 @@ class Processor extends EventEmitter {
417
440
  state,
418
441
  data,
419
442
  skill,
420
- tracking: messageSender.tracking
443
+ tracking: messageSender.tracking,
444
+ events,
445
+ flag: res.senderMeta.flag,
446
+ nonInteractive: !isUserInteraction(req)
421
447
  };
422
448
 
423
449
  return Promise.allSettled([
@@ -575,27 +601,35 @@ class Processor extends EventEmitter {
575
601
  let {
576
602
  _sct: sessionCount = 0,
577
603
  _sid: sessionId = null,
578
- _segStamp: ts = 0,
604
+ _sst: sessionStart = 0,
605
+ _sts: sessionTs = (state._segStamp || 0),
579
606
  _snew: sessionCreated
580
607
  } = state;
581
608
 
609
+ const interactive = isUserInteraction(req);
610
+
582
611
  if ((isUserInteraction(req)
583
- && (ts + this.options.sessionDuration) < Date.now())
612
+ && (sessionTs + this.options.sessionDuration) < Date.now())
584
613
  || !sessionId) {
585
614
 
615
+ sessionStart = timestamp;
616
+ sessionTs = timestamp;
586
617
  sessionId = Processor._createSessionId(req.pageId, req.senderId, timestamp);
587
618
  sessionCount++;
588
619
  sessionCreated = true;
589
620
  } else {
590
621
  sessionCreated = false;
591
- }
592
622
 
593
- ts = timestamp;
623
+ if (interactive) {
624
+ sessionTs = timestamp;
625
+ }
626
+ }
594
627
 
595
628
  Object.assign(state, {
596
629
  _sct: sessionCount,
597
630
  _sid: sessionId,
598
- _segStamp: ts,
631
+ _sst: sessionStart,
632
+ _sts: sessionTs,
599
633
  _snew: sessionCreated
600
634
  });
601
635
  } else {
@@ -785,21 +819,18 @@ class Processor extends EventEmitter {
785
819
  .digest('hex');
786
820
 
787
821
  return senderHash.match(/[a-f0-9]{1,13}/g)
788
- .map((v) => parseInt(v, 16).toString(36))
822
+ .map((v) => toBase(parseInt(v, 16)))
789
823
  .join('');
790
824
  }
791
825
 
792
826
  static _createSessionId (pageId, senderId, timestamp = Date.now()) {
793
- const senderShort = Processor._shakeShort(`${senderId}|${pageId}}`, 9);
827
+ const senderShort = Processor._shakeShort(`${senderId}|${pageId}}`, 12);
794
828
 
795
- const rand = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
796
- .toString(36);
829
+ const rand = toBase(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
797
830
 
798
- const randTS = Math.floor(Date.now() % 1000)
799
- .toString(36);
831
+ const randTS = toBase(Math.floor(Date.now() % 10000));
800
832
 
801
- const ts = Math.floor(MAX_TS - timestamp)
802
- .toString(36);
833
+ const ts = toBase(Math.floor(MAX_TS - timestamp));
803
834
 
804
835
  // console.log({
805
836
  // base: `${ts}.${senderShort}`.length,
@@ -811,7 +842,7 @@ class Processor extends EventEmitter {
811
842
  // });
812
843
 
813
844
  return `${ts}.${senderShort}`
814
- .padEnd(26, randTS)
845
+ .padEnd(28, randTS)
815
846
  .padEnd(32, rand);
816
847
  }
817
848
 
package/src/Request.js CHANGED
@@ -6,7 +6,7 @@
6
6
  const Ai = require('./Ai');
7
7
  const { tokenize, parseActionPayload } = require('./utils');
8
8
  const { quickReplyAction } = require('./utils/quickReplies');
9
- const { FLAG_DISAMBIGUATION_SELECTED } = require('./flags');
9
+ const { ResponseFlag } = require('./analytics/consts');
10
10
  const { getSetState } = require('./utils/getUpdate');
11
11
  const { vars, checkSetState } = require('./utils/stateVariables');
12
12
  const OrchestratorClient = require('./OrchestratorClient');
@@ -391,7 +391,7 @@ class Request {
391
391
  data: {
392
392
  ...data,
393
393
  _senderMeta: {
394
- flag: FLAG_DISAMBIGUATION_SELECTED,
394
+ flag: ResponseFlag.DISAMBIGUATION_SELECTED,
395
395
  likelyIntent: intent.intent,
396
396
  disambText: text
397
397
  }
@@ -801,6 +801,7 @@ class Request {
801
801
  * @returns {Action|null}
802
802
  */
803
803
  expected () {
804
+ // @ts-ignore
804
805
  return this.state._expected || null;
805
806
  }
806
807
 
@@ -816,6 +817,7 @@ class Request {
816
817
  */
817
818
  expectedKeywords (justOnce = false) {
818
819
  const {
820
+ // @ts-ignore
819
821
  _expectedKeywords: exKeywords
820
822
  } = this.state;
821
823
 
@@ -855,7 +857,9 @@ class Request {
855
857
  */
856
858
  expectedContext (justOnce = false, includeKeywords = false) {
857
859
  const ad = this.actionData();
860
+ // @ts-ignore
858
861
  const expected = ad._useExpected || this.state._expected;
862
+ // @ts-ignore
859
863
  const confident = this.state._expectedConfidentInput;
860
864
 
861
865
  const ret = {};
@@ -1097,6 +1101,7 @@ class Request {
1097
1101
  * @returns {boolean}
1098
1102
  */
1099
1103
  isConfidentInput () {
1104
+ // @ts-ignore
1100
1105
  return this.state._expectedConfidentInput === true;
1101
1106
  }
1102
1107
 
@@ -1123,11 +1128,15 @@ class Request {
1123
1128
  res = parseActionPayload(this.message.quick_reply);
1124
1129
  }
1125
1130
 
1131
+ // @ts-ignore
1126
1132
  if (!res && this.state._expectedKeywords) {
1133
+ // @ts-ignore
1127
1134
  res = this._actionByExpectedKeywords(this.state._expected);
1128
1135
  }
1129
1136
 
1137
+ // @ts-ignore
1130
1138
  if (!res && this.state._expected) {
1139
+ // @ts-ignore
1131
1140
  res = parseActionPayload(this.state._expected);
1132
1141
  }
1133
1142
 
@@ -1215,7 +1224,9 @@ class Request {
1215
1224
  }
1216
1225
 
1217
1226
  _getLocalPathRegexp () {
1227
+ // @ts-ignore
1218
1228
  if (this.state._lastVisitedPath) {
1229
+ // @ts-ignore
1219
1230
  return new RegExp(`^${this.state._lastVisitedPath}/[^/]+`);
1220
1231
  }
1221
1232
  let expected = this.expected();
@@ -1295,6 +1306,7 @@ class Request {
1295
1306
  }
1296
1307
 
1297
1308
  _actionByExpectedKeywords (expected) {
1309
+ // @ts-ignore
1298
1310
  if (!this.state._expectedKeywords) {
1299
1311
  return null;
1300
1312
  }
@@ -1315,8 +1327,10 @@ class Request {
1315
1327
 
1316
1328
  _resolveQuickReplyActions () {
1317
1329
  if (this._quickReplyActions === null) {
1330
+ // @ts-ignore
1318
1331
  if (this.state._expectedKeywords) {
1319
1332
  this._quickReplyActions = quickReplyAction(
1333
+ // @ts-ignore
1320
1334
  this.state._expectedKeywords,
1321
1335
  this,
1322
1336
  Ai.ai
@@ -1377,6 +1391,7 @@ class Request {
1377
1391
  */
1378
1392
  expectedEntities () {
1379
1393
  const {
1394
+ // @ts-ignore
1380
1395
  _expectedKeywords: exKeywords
1381
1396
  } = this.state;
1382
1397
 
package/src/Responder.js CHANGED
@@ -9,7 +9,7 @@ const ButtonTemplate = require('./templates/ButtonTemplate');
9
9
  const GenericTemplate = require('./templates/GenericTemplate');
10
10
  const ListTemplate = require('./templates/ListTemplate');
11
11
  const { makeAbsolute, makeQuickReplies } = require('./utils');
12
- const { FLAG_DISAMBIGUATION_OFFERED, FLAG_DO_NOT_LOG } = require('./flags');
12
+ const { ResponseFlag } = require('./analytics/consts');
13
13
  const { checkSetState } = require('./utils/stateVariables');
14
14
  const {
15
15
  FEATURE_VOICE,
@@ -23,6 +23,9 @@ const TYPE_MESSAGE_TAG = 'MESSAGE_TAG';
23
23
  const EXCEPTION_HOPCOUNT_THRESHOLD = 5;
24
24
 
25
25
  /** @typedef {import('./Request')} Request */
26
+ /** @typedef {import('./ReturnSender').UploadResult} UploadResult */
27
+ /** @typedef {import('./analytics/consts').TrackingCategory} TrackingCategory */
28
+ /** @typedef {import('./analytics/consts').TrackingType} TrackingType */
26
29
 
27
30
  /**
28
31
  * @enum {string} ExpectedInput
@@ -48,7 +51,7 @@ Object.freeze(ExpectedInput);
48
51
 
49
52
  /**
50
53
  * @typedef {object} SenderMeta
51
- * @prop {string|null} flag
54
+ * @prop {ResponseFlag|null} flag
52
55
  * @prop {string} [likelyIntent]
53
56
  * @prop {string} [disambText]
54
57
  * @prop {string[]} [disambiguationIntents]
@@ -71,8 +74,6 @@ Object.freeze(ExpectedInput);
71
74
  * @returns {VoiceControl}
72
75
  */
73
76
 
74
- /** @typedef {import('./ReturnSender').UploadResult} UploadResult */
75
-
76
77
  /**
77
78
  * Instance of responder is passed as second parameter of handler (res)
78
79
  *
@@ -200,7 +201,7 @@ class Responder {
200
201
  * @returns {this}
201
202
  */
202
203
  doNotLogTheEvent () {
203
- this._senderMeta = { flag: FLAG_DO_NOT_LOG };
204
+ this._senderMeta = { flag: ResponseFlag.DO_NOT_LOG };
204
205
  return this;
205
206
  }
206
207
 
@@ -209,8 +210,8 @@ class Responder {
209
210
  * Events are aggregated within ReturnSender and can be caught
210
211
  * within Processor's `interaction` event (event.tracking.events)
211
212
  *
212
- * @param {string} type - (log,report,conversation,audit,user)
213
- * @param {string} category
213
+ * @param {TrackingType} type - (log,report,conversation,audit,user,training)
214
+ * @param {TrackingCategory} category
214
215
  * @param {string} [action]
215
216
  * @param {string} [label]
216
217
  * @param {number} [value]
@@ -471,7 +472,7 @@ class Responder {
471
472
 
472
473
  if (disambiguationIntents.length > 0) {
473
474
  this._senderMeta = {
474
- flag: FLAG_DISAMBIGUATION_OFFERED,
475
+ flag: ResponseFlag.DISAMBIGUATION_OFFERED,
475
476
  disambiguationIntents
476
477
  };
477
478
  }
@@ -1015,6 +1016,8 @@ class Responder {
1015
1016
  $hopCount++;
1016
1017
  }
1017
1018
 
1019
+ this._senderMeta = { flag: ResponseFlag.HANDOVER };
1020
+
1018
1021
  if (data === null) {
1019
1022
  metadata = JSON.stringify({
1020
1023
  data: { $hopCount }
@@ -5,10 +5,11 @@
5
5
 
6
6
  const ai = require('./Ai');
7
7
  const { FEATURE_PHRASES, FEATURE_TRACKING } = require('./features');
8
- const { FLAG_DO_NOT_LOG } = require('./flags');
8
+ const { ResponseFlag } = require('./analytics/consts');
9
9
 
10
10
  /** @typedef {import('./Request')} Request */
11
11
  /** @typedef {import('./Responder')} Responder */
12
+ /** @typedef {import('./Processor').TrackingObject} TrackingObject */
12
13
 
13
14
  /**
14
15
  * @typedef {object} ChatLogStorage
@@ -155,6 +156,9 @@ class ReturnSender {
155
156
  });
156
157
  }
157
158
 
159
+ /**
160
+ * @returns {TrackingObject}
161
+ */
158
162
  get tracking () {
159
163
  return this._tracking;
160
164
  }
@@ -531,7 +535,7 @@ class ReturnSender {
531
535
  };
532
536
  }
533
537
 
534
- if (!this._logger || meta.flag === FLAG_DO_NOT_LOG) {
538
+ if (!this._logger || meta.flag === ResponseFlag.DO_NOT_LOG) {
535
539
  // noop
536
540
  } else if (error) {
537
541
  await Promise.resolve(this._logger
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @author David Menger
3
+ */
4
+ 'use strict';
5
+
6
+ /**
7
+ * @enum {string}
8
+ */
9
+ const ResponseFlag = {
10
+ /**
11
+ * Disambiguation quick reply was selected
12
+ */
13
+ DISAMBIGUATION_SELECTED: 'd',
14
+
15
+ /**
16
+ * Disambiguation occured - user was asked to choose the right meaning
17
+ */
18
+ DISAMBIGUATION_OFFERED: 'o',
19
+
20
+ /**
21
+ * Do not log the event
22
+ */
23
+ DO_NOT_LOG: '!',
24
+
25
+ /**
26
+ * Handover occurred
27
+ */
28
+ HANDOVER: 'h'
29
+ };
30
+
31
+ /**
32
+ * @enum {string}
33
+ */
34
+ const TrackingType = { // max length 12
35
+ CONVERSATION_EVENT: 'conversation',
36
+ TRAINING: 'train',
37
+ PAGE_VIEW: 'page_view',
38
+ REPORT: 'report'
39
+ };
40
+
41
+ /**
42
+ * @enum {string} TrackingCategory
43
+ */
44
+ const TrackingCategory = { // max length 3
45
+ // CONVERSATION_EVENT: 'conversation'
46
+ STICKER: 'sti',
47
+ IMAGE: 'img',
48
+ LOCATION: 'loc',
49
+ ATTACHMENT: 'att',
50
+ TEXT: 'txt',
51
+ QUICK_REPLY: 'qr',
52
+ OPT_IN: 'oin',
53
+ REFERRAL: 'ref',
54
+ POSTBACK_BUTTON: 'btn',
55
+ URL_LINK: 'url',
56
+ OTHER: 'oth',
57
+ HANDOVER_TO_BOT: 'bot',
58
+
59
+ // TRAINING: 'train'
60
+ INTENT_DETECTION: 'int',
61
+ HANDOVER_OCCURRED: 'hum',
62
+ DISAMBIGUATION_SELECTED: 'dis',
63
+ DISAMBIGUATION_OFFERED: 'dio',
64
+
65
+ // REPORT: 'report'
66
+ REPORT_FEEDBACK: 'fdb'
67
+ };
68
+
69
+ /**
70
+ * @type {Object<TrackingCategory,string>}
71
+ */
72
+ const CATEGORY_LABELS = {
73
+ // CONVERSATION_EVENT: 'conversation'
74
+ [TrackingCategory.STICKER]: 'User: Sticker',
75
+ [TrackingCategory.IMAGE]: 'User: Image',
76
+ [TrackingCategory.LOCATION]: 'User: Location',
77
+ [TrackingCategory.ATTACHMENT]: 'User: Attachement', // yes, with typo
78
+ [TrackingCategory.TEXT]: 'User: Text',
79
+ [TrackingCategory.QUICK_REPLY]: 'User: Quick reply',
80
+ [TrackingCategory.OPT_IN]: 'Entry: Optin',
81
+ [TrackingCategory.REFERRAL]: 'Entry: Referral',
82
+ [TrackingCategory.POSTBACK_BUTTON]: 'User: Button - bot',
83
+ [TrackingCategory.URL_LINK]: 'User: Button - url',
84
+ [TrackingCategory.OTHER]: 'User: Other',
85
+ [TrackingCategory.HANDOVER_TO_BOT]: 'Entry: Handover in',
86
+
87
+ // TRAINING: 'train'
88
+ [TrackingCategory.INTENT_DETECTION]: 'Intent: Detection',
89
+ [TrackingCategory.HANDOVER_OCCURRED]: 'Bot: Handover out',
90
+ [TrackingCategory.DISAMBIGUATION_SELECTED]: 'Disambiguation: selected',
91
+ [TrackingCategory.DISAMBIGUATION_OFFERED]: 'Disambiguation: offered',
92
+
93
+ // REPORT: 'report'
94
+ [TrackingCategory.REPORT_FEEDBACK]: 'User: Feedback'
95
+ };
96
+
97
+ module.exports = {
98
+ CATEGORY_LABELS,
99
+ TrackingType,
100
+ TrackingCategory,
101
+ ResponseFlag
102
+ };
@@ -3,8 +3,11 @@
3
3
  */
4
4
  'use strict';
5
5
 
6
- const { replaceDiacritics } = require('webalize');
6
+ const { replaceDiacritics, webalize } = require('webalize');
7
7
  const Ai = require('../Ai');
8
+ const {
9
+ TrackingType, TrackingCategory, CATEGORY_LABELS, ResponseFlag
10
+ } = require('./consts');
8
11
 
9
12
  /** @typedef {import('../Processor').InteractionEvent} InteractionEvent */
10
13
  /** @typedef {import('../Processor').IInteractionHandler} IInteractionHandler */
@@ -23,8 +26,8 @@ const Ai = require('../Ai');
23
26
 
24
27
  /**
25
28
  * @typedef {object} Event
26
- * @prop {'conversation'|'page_view'|string} type
27
- * @prop {string} [category]
29
+ * @prop {TrackingType} type
30
+ * @prop {TrackingCategory} [category]
28
31
  * @prop {string} [action]
29
32
  * @prop {string} [label]
30
33
  * @prop {number} [value]
@@ -35,6 +38,8 @@ const Ai = require('../Ai');
35
38
  * @prop {number} [sessionCount]
36
39
  * @prop {string} [lang]
37
40
  * @prop {string} [action]
41
+ * @prop {string} [snapshot]
42
+ * @prop {string} [botId]
38
43
  */
39
44
 
40
45
  /**
@@ -45,6 +50,7 @@ const Ai = require('../Ai');
45
50
  * @param {SessionMetadata} [metadata]
46
51
  * @param {number} [ts]
47
52
  * @param {boolean} [nonInteractive]
53
+ * @param {string} [timeZone]
48
54
  * @returns {Promise}
49
55
  */
50
56
 
@@ -58,6 +64,7 @@ const Ai = require('../Ai');
58
64
  * @param {number} [ts]
59
65
  * @param {boolean} [nonInteractive]
60
66
  * @param {boolean} [sessionStarted]
67
+ * @param {string} [timeZone]
61
68
  * @returns {Promise}
62
69
  */
63
70
 
@@ -72,6 +79,9 @@ const Ai = require('../Ai');
72
79
  * @prop {StoreEvents} storeEvents
73
80
  * @prop {CreateUserSession} createUserSession
74
81
  * @prop {boolean} [hasExtendedEvents]
82
+ * @prop {boolean} [supportsArrays]
83
+ * @prop {boolean} [useDescriptiveCategories]
84
+ * @prop {boolean} [useExtendedScalars]
75
85
  */
76
86
 
77
87
  /**
@@ -98,6 +108,9 @@ const Ai = require('../Ai');
98
108
 
99
109
  /**
100
110
  * @typedef {object} HandlerConfig
111
+ * @prop {string} [snapshot]
112
+ * @prop {string} [botId]
113
+ * @prop {string} [timeZone] - default UTC
101
114
  * @prop {boolean} [enabled] - default true
102
115
  * @prop {boolean} [throwException] - default false
103
116
  * @prop {IGALogger} [log] - console like logger
@@ -134,12 +147,26 @@ function onInteractionHandler (
134
147
  enabled = true,
135
148
  throwException = false,
136
149
  log = console,
150
+ snapshot,
151
+ botId,
152
+ timeZone = 'UTC',
137
153
  anonymize = (x) => x,
138
154
  userExtractor = (state) => null // eslint-disable-line no-unused-vars
139
155
  },
140
156
  analyticsStorage,
141
157
  ai = Ai.ai
142
158
  ) {
159
+ const {
160
+ supportsArrays = false,
161
+ useExtendedScalars = false,
162
+ hasExtendedEvents = false,
163
+ useDescriptiveCategories = true
164
+ } = analyticsStorage;
165
+
166
+ const asArray = (data = []) => (supportsArrays ? data : data.join(','));
167
+ const asCategory = (cat) => (useDescriptiveCategories && CATEGORY_LABELS[cat]) || cat;
168
+ const noneAction = useExtendedScalars ? null : '(none)';
169
+ const noneValue = useExtendedScalars ? -1 : 0;
143
170
 
144
171
  /**
145
172
  * @param {InteractionEvent} params
@@ -151,13 +178,14 @@ function onInteractionHandler (
151
178
  // state,
152
179
  // data,
153
180
  skill,
154
- tracking
181
+ events,
182
+ flag,
183
+ nonInteractive
155
184
  }) {
156
185
  if (!enabled) {
157
186
  return;
158
187
  }
159
188
  try {
160
- const nonInteractive = !!req.campaign;
161
189
  const {
162
190
  pageId,
163
191
  senderId,
@@ -168,16 +196,20 @@ function onInteractionHandler (
168
196
  _snew: createSession,
169
197
  _sct: sessionCount,
170
198
  _sid: sessionId,
199
+ _sst: sessionStart,
200
+ _sts: sessionTs,
171
201
  lang
172
202
  } = req.state;
173
203
 
174
- const [action = '(none)', ...otherActions] = actions;
204
+ const [action = noneAction, ...otherActions] = actions;
175
205
 
176
206
  if (createSession) {
177
207
  const metadata = {
178
208
  sessionCount,
179
209
  lang,
180
- action
210
+ action,
211
+ snapshot,
212
+ botId
181
213
  };
182
214
 
183
215
  await analyticsStorage.createUserSession(
@@ -186,7 +218,8 @@ function onInteractionHandler (
186
218
  sessionId,
187
219
  metadata,
188
220
  timestamp,
189
- nonInteractive
221
+ nonInteractive,
222
+ timeZone
190
223
  );
191
224
  }
192
225
 
@@ -211,7 +244,7 @@ function onInteractionHandler (
211
244
 
212
245
  if (winners.length > 0) {
213
246
  [{
214
- action: winnerAction = '(none)',
247
+ action: winnerAction = noneAction,
215
248
  sort: winnerScore = 0,
216
249
  intent: { intent: winnerIntent, entities: winnerEntities = [] }
217
250
  }] = winners;
@@ -221,6 +254,15 @@ function onInteractionHandler (
221
254
 
222
255
  const expected = req.expected() ? req.expected().action : '';
223
256
 
257
+ const feedbackEvent = events.find((e) => e.type === TrackingType.REPORT
258
+ && e.category === TrackingCategory.REPORT_FEEDBACK);
259
+ const feedback = feedbackEvent
260
+ ? feedbackEvent.value
261
+ : noneValue;
262
+ const didHandover = flag === ResponseFlag.HANDOVER;
263
+
264
+ const user = userExtractor(req.state);
265
+
224
266
  const isContextUpdate = req.isSetContext();
225
267
  const isNotification = !!req.campaign;
226
268
  const isAttachment = req.isAttachment();
@@ -229,13 +271,17 @@ function onInteractionHandler (
229
271
  const isText = !isQuickReply && req.isText();
230
272
  const isPostback = req.isPostBack();
231
273
 
232
- const allActions = actions.join(',');
274
+ const allActions = asArray(actions);
233
275
  const requestAction = req.action();
234
276
 
235
- const events = [];
277
+ const trackEvents = [];
278
+
279
+ const langsExtension = hasExtendedEvents
280
+ ? { lang }
281
+ : { cd1: lang };
236
282
 
237
283
  const actionMeta = {
238
- requestAction: req.action() || '(none)',
284
+ requestAction: req.action() || noneAction,
239
285
  expected,
240
286
  expectedTaken: requestAction === expected,
241
287
  isContextUpdate,
@@ -245,20 +291,28 @@ function onInteractionHandler (
245
291
  isPassThread,
246
292
  isText,
247
293
  isPostback,
294
+ didHandover,
295
+ withUser: user !== null && !!user.id,
296
+ sessionStart,
297
+ sessionDuration: sessionTs - sessionStart,
298
+ feedback,
299
+ skill: webalize(skill),
300
+ snapshot,
301
+ botId,
248
302
  winnerAction,
249
303
  winnerIntent,
250
- winnerEntities: winnerEntities.map((e) => e.entity).join(','),
304
+ winnerEntities: asArray(winnerEntities.map((e) => e.entity)),
251
305
  winnerScore,
252
306
  winnerTaken,
253
307
  intent,
254
308
  intentScore: score,
255
- entities: req.entities.map((e) => e.entity).join(','),
309
+ entities: asArray(req.entities.map((e) => e.entity)),
256
310
  text,
257
311
  allActions
258
312
  };
259
313
 
260
- events.push({
261
- type: 'page_view',
314
+ trackEvents.push({
315
+ type: TrackingType.PAGE_VIEW,
262
316
  action,
263
317
  allActions,
264
318
  nonInteractive,
@@ -267,15 +321,15 @@ function onInteractionHandler (
267
321
  skill,
268
322
  lang,
269
323
  cd1: req.state.lang,
270
- ...(analyticsStorage.hasExtendedEvents ? {} : actionMeta)
324
+ ...(hasExtendedEvents ? {} : actionMeta)
271
325
  });
272
326
 
273
327
  let prevAction = action;
274
328
 
275
- events.push(
329
+ trackEvents.push(
276
330
  ...otherActions.map((a) => {
277
331
  const r = {
278
- type: 'page_view',
332
+ type: TrackingType.PAGE_VIEW,
279
333
  action: a,
280
334
  allActions,
281
335
  nonInteractive: false,
@@ -283,9 +337,7 @@ function onInteractionHandler (
283
337
  prevAction,
284
338
  skill,
285
339
  isGoto: true,
286
- ...(analyticsStorage.hasExtendedEvents
287
- ? { lang }
288
- : { cd1: lang })
340
+ ...langsExtension
289
341
  };
290
342
 
291
343
  prevAction = a;
@@ -293,8 +345,8 @@ function onInteractionHandler (
293
345
  })
294
346
  );
295
347
 
296
- events.push(
297
- ...tracking.events.map(({
348
+ trackEvents.push(
349
+ ...events.map(({
298
350
  type, category, action: eventAction, label, value
299
351
  }) => ({
300
352
  lastAction,
@@ -303,93 +355,88 @@ function onInteractionHandler (
303
355
  action: eventAction,
304
356
  label,
305
357
  value,
306
- ...(analyticsStorage.hasExtendedEvents
307
- ? { lang }
308
- : { cd1: lang })
358
+ ...langsExtension
309
359
  }))
310
360
  );
311
361
 
312
362
  if (!nonInteractive) {
313
363
 
314
364
  if (req.isText()) {
315
- events.push({
316
- type: 'ai',
365
+ trackEvents.push({
366
+ type: TrackingType.TRAINING,
317
367
  // @ts-ignore
318
368
  lastAction,
319
- category: 'Intent: Detection',
369
+ category: asCategory(TrackingCategory.INTENT_DETECTION),
320
370
  intent,
321
371
  action,
322
372
  label: text,
323
373
  value: score >= ai.confidence ? 0 : 1,
324
- ...(analyticsStorage.hasExtendedEvents
325
- ? { lang }
326
- : { cd1: lang })
374
+ ...langsExtension
327
375
  });
328
376
  }
329
377
 
330
378
  const notHandled = actions.some((a) => a.match(/\*$/)) && !req.isQuickReply();
331
379
 
332
- let actionCategory = 'User: ';
333
- let label = '(none)';
380
+ let actionCategory;
381
+ let label = noneAction;
334
382
  const value = notHandled ? 1 : 0;
335
383
 
336
- if (req.isSticker()) {
337
- actionCategory += 'Sticker';
384
+ if (isPassThread) {
385
+ actionCategory = TrackingCategory.HANDOVER_TO_BOT;
386
+ } else if (req.isSticker()) {
387
+ actionCategory = TrackingCategory.STICKER;
338
388
  label = req.attachmentUrl(0);
339
389
  } else if (req.isImage()) {
340
- actionCategory += 'Image';
390
+ actionCategory = TrackingCategory.IMAGE;
341
391
  label = req.attachmentUrl(0);
342
392
  } else if (req.hasLocation()) {
343
- actionCategory += 'Location';
393
+ actionCategory = TrackingCategory.LOCATION;
344
394
  const { lat, long } = req.getLocation();
345
395
  label = `${lat}, ${long}`;
346
396
  } else if (isAttachment) {
347
- actionCategory += 'Attachement';
397
+ actionCategory = TrackingCategory.ATTACHMENT;
348
398
  label = req.attachment(0).type;
349
399
  } else if (isText) {
350
- actionCategory += 'Text';
400
+ actionCategory = TrackingCategory.TEXT;
351
401
  label = text;
352
402
  } else if (isQuickReply) {
353
- actionCategory += 'Quick reply';
403
+ actionCategory = TrackingCategory.QUICK_REPLY;
354
404
  label = text;
355
- } else if (req.isReferral() || req.isOptin()) {
356
- actionCategory = req.isOptin()
357
- ? 'Entry: Optin'
358
- : 'Entry: Referral';
405
+ } else if (req.isOptin()) {
406
+ actionCategory = TrackingCategory.OPT_IN;
407
+ } else if (req.isReferral()) {
408
+ actionCategory = TrackingCategory.REFERRAL;
359
409
  } else if (isPostback) {
360
- actionCategory += 'Button - bot';
410
+ actionCategory = TrackingCategory.POSTBACK_BUTTON;
361
411
  label = req.data.postback.title || '(unknown)';
362
412
  } else {
363
- actionCategory += 'Other';
413
+ actionCategory = TrackingCategory.OTHER;
364
414
  }
365
415
 
366
- events.push({
416
+ trackEvents.push({
367
417
  ...(analyticsStorage.hasExtendedEvents ? actionMeta : {}),
368
- type: 'conversation',
418
+ type: TrackingType.CONVERSATION_EVENT,
369
419
  // @ts-ignore
370
420
  lastAction,
371
- category: actionCategory,
421
+ category: asCategory(actionCategory),
372
422
  action,
373
423
  label,
374
424
  value,
375
- ...(analyticsStorage.hasExtendedEvents
376
- ? { lang }
377
- : { cd1: lang })
425
+ ...langsExtension
378
426
  });
379
427
  }
380
428
 
381
- const user = userExtractor(req.state);
382
-
383
429
  await analyticsStorage.storeEvents(
384
430
  pageId,
385
431
  senderId,
386
432
  sessionId,
387
433
  // @ts-ignore
388
- events,
434
+ trackEvents,
389
435
  user,
390
436
  timestamp,
391
437
  nonInteractive,
392
- createSession
438
+ createSession,
439
+ timeZone
393
440
  );
394
441
  } catch (e) {
395
442
  if (throwException) {
@@ -432,7 +479,7 @@ function onInteractionHandler (
432
479
  [{
433
480
  // @ts-ignore
434
481
  lastAction,
435
- ...(analyticsStorage.hasExtendedEvents
482
+ ...(hasExtendedEvents
436
483
  ? { lang }
437
484
  : { cd1: lang }),
438
485
  ...event
package/src/flags.js CHANGED
@@ -18,8 +18,14 @@ const FLAG_DISAMBIGUATION_OFFERED = 'o';
18
18
  */
19
19
  const FLAG_DO_NOT_LOG = '!';
20
20
 
21
+ /**
22
+ * Handover occurred
23
+ */
24
+ const FLAG_HANDOVER = 'h';
25
+
21
26
  module.exports = {
22
27
  FLAG_DISAMBIGUATION_SELECTED,
23
28
  FLAG_DISAMBIGUATION_OFFERED,
24
- FLAG_DO_NOT_LOG
29
+ FLAG_DO_NOT_LOG,
30
+ FLAG_HANDOVER
25
31
  };
@@ -5,7 +5,7 @@
5
5
 
6
6
  const { makeAbsolute } = require('./pathUtils');
7
7
  const { tokenize } = require('./tokenizer');
8
- const { FLAG_DISAMBIGUATION_SELECTED } = require('../flags');
8
+ const { ResponseFlag } = require('../analytics/consts');
9
9
  const { checkSetState } = require('./stateVariables');
10
10
 
11
11
  /** @typedef {import('../Request')} Request */
@@ -240,7 +240,7 @@ function makeQuickReplies (replies, path = '', translate = (w) => w, quickReplyC
240
240
  }
241
241
 
242
242
  if (data._senderMeta
243
- && data._senderMeta.flag === FLAG_DISAMBIGUATION_SELECTED) {
243
+ && data._senderMeta.flag === ResponseFlag.DISAMBIGUATION_SELECTED) {
244
244
 
245
245
  const { likelyIntent } = data._senderMeta;
246
246
  disambiguationIntents.push(likelyIntent);
@@ -411,7 +411,7 @@ function disambiguationQuickReply (title, likelyIntent, disambText, action, data
411
411
  data: {
412
412
  ...data,
413
413
  _senderMeta: {
414
- flag: FLAG_DISAMBIGUATION_SELECTED,
414
+ flag: ResponseFlag.DISAMBIGUATION_SELECTED,
415
415
  likelyIntent,
416
416
  disambText
417
417
  }
@@ -108,9 +108,12 @@ function checkSetState (setState, newState) {
108
108
  * @returns {boolean}
109
109
  */
110
110
  function isUserInteraction (req) {
111
- return req.isMessage() || req.isPostBack()
112
- || req.isReferral() || req.isAttachment()
113
- || req.isTextOrIntent();
111
+ return !req.campaign
112
+ && !req.event.pass_thread_control
113
+ && !req.isSetContext()
114
+ && (req.isMessage() || req.isPostBack()
115
+ || req.isReferral() || req.isAttachment()
116
+ || req.isTextOrIntent());
114
117
  }
115
118
 
116
119
  /**