wingbot 3.45.2 → 3.46.0-alpha.2

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.2",
3
+ "version": "3.46.0-alpha.2",
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,
@@ -202,9 +204,14 @@ class BotApp {
202
204
  analyticsStorage.setDefaultLogger(log);
203
205
  }
204
206
 
207
+ // @ts-ignore
208
+ const { snapshot = null, botId = null } = this._bot;
209
+
205
210
  const { onInteraction, onEvent } = onInteractionHandler({
206
211
  log,
207
212
  anonymize: this._textFilter,
213
+ snapshot,
214
+ botId,
208
215
  ...options
209
216
  }, analyticsStorage);
210
217
 
@@ -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,110 @@
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
+ // PAGE_VIEW: 'page_view'
46
+ PAGE_VIEW_FIRST: 'pf',
47
+ PAGE_VIEW_SUBSEQUENT: 'pp',
48
+
49
+ // CONVERSATION_EVENT: 'conversation'
50
+ STICKER: 'sti',
51
+ IMAGE: 'img',
52
+ LOCATION: 'loc',
53
+ ATTACHMENT: 'att',
54
+ TEXT: 'txt',
55
+ QUICK_REPLY: 'qr',
56
+ OPT_IN: 'oin',
57
+ REFERRAL: 'ref',
58
+ POSTBACK_BUTTON: 'btn',
59
+ URL_LINK: 'url',
60
+ OTHER: 'oth',
61
+ HANDOVER_TO_BOT: 'bot',
62
+
63
+ // TRAINING: 'train'
64
+ INTENT_DETECTION: 'int',
65
+ HANDOVER_OCCURRED: 'hum',
66
+ DISAMBIGUATION_SELECTED: 'dis',
67
+ DISAMBIGUATION_OFFERED: 'dio',
68
+
69
+ // REPORT: 'report'
70
+ REPORT_FEEDBACK: 'fdb'
71
+ };
72
+
73
+ /**
74
+ * @type {Object<TrackingCategory,string>}
75
+ */
76
+ const CATEGORY_LABELS = {
77
+ // PAGE_VIEW: 'page_view'
78
+ [TrackingCategory.PAGE_VIEW_FIRST]: 'Bot: Interaction',
79
+ [TrackingCategory.PAGE_VIEW_SUBSEQUENT]: 'Bot: Sub-interaction',
80
+
81
+ // CONVERSATION_EVENT: 'conversation'
82
+ [TrackingCategory.STICKER]: 'User: Sticker',
83
+ [TrackingCategory.IMAGE]: 'User: Image',
84
+ [TrackingCategory.LOCATION]: 'User: Location',
85
+ [TrackingCategory.ATTACHMENT]: 'User: Attachement', // yes, with typo
86
+ [TrackingCategory.TEXT]: 'User: Text',
87
+ [TrackingCategory.QUICK_REPLY]: 'User: Quick reply',
88
+ [TrackingCategory.OPT_IN]: 'Entry: Optin',
89
+ [TrackingCategory.REFERRAL]: 'Entry: Referral',
90
+ [TrackingCategory.POSTBACK_BUTTON]: 'User: Button - bot',
91
+ [TrackingCategory.URL_LINK]: 'User: Button - url',
92
+ [TrackingCategory.OTHER]: 'User: Other',
93
+ [TrackingCategory.HANDOVER_TO_BOT]: 'Entry: Handover in',
94
+
95
+ // TRAINING: 'train'
96
+ [TrackingCategory.INTENT_DETECTION]: 'Intent: Detection',
97
+ [TrackingCategory.HANDOVER_OCCURRED]: 'Bot: Handover out',
98
+ [TrackingCategory.DISAMBIGUATION_SELECTED]: 'Disambiguation: selected',
99
+ [TrackingCategory.DISAMBIGUATION_OFFERED]: 'Disambiguation: offered',
100
+
101
+ // REPORT: 'report'
102
+ [TrackingCategory.REPORT_FEEDBACK]: 'User: Feedback'
103
+ };
104
+
105
+ module.exports = {
106
+ CATEGORY_LABELS,
107
+ TrackingType,
108
+ TrackingCategory,
109
+ ResponseFlag
110
+ };
@@ -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,11 +26,51 @@ 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]
34
+ * @prop {string} [lang]
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} ConversationEventExtension
39
+ * @prop {string} [skill]
40
+ * @prop {string} [text]
41
+ * @prop {string} [expected]
42
+ * @prop {boolean} expectedTaken
43
+ * @prop {boolean} isContextUpdate
44
+ * @prop {boolean} isAttachment
45
+ * @prop {boolean} isNotification
46
+ * @prop {boolean} isQuickReply
47
+ * @prop {boolean} isPassThread
48
+ * @prop {boolean} isPostback
49
+ * @prop {boolean} isText
50
+ * @prop {boolean} didHandover
51
+ * @prop {boolean} withUser
52
+ * @prop {string} [userId]
53
+ * @prop {number} [feedback]
54
+ * @prop {number} sessionStart
55
+ * @prop {number} sessionDuration
56
+ * @prop {string} [winnerAction]
57
+ * @prop {string} [winnerIntent]
58
+ * @prop {string[]|string} [winnerEntities]
59
+ * @prop {number} [winnerScore]
60
+ * @prop {boolean} [winnerTaken]
61
+ * @prop {string} [intent]
62
+ * @prop {number} [intentScore]
63
+ * @prop {string[]|string} [entities]
64
+ * @prop {string[]|string} allActions
65
+ * @prop {boolean} nonInteractive
66
+ * @prop {string} [snapshot]
67
+ * @prop {string} [botId]
68
+ *
69
+ * @typedef {Event & ConversationEventExtension} ConversationEvent
70
+ */
71
+
72
+ /**
73
+ * @typedef {ConversationEvent | Event} TrackingEvent
31
74
  */
32
75
 
33
76
  /**
@@ -35,6 +78,8 @@ const Ai = require('../Ai');
35
78
  * @prop {number} [sessionCount]
36
79
  * @prop {string} [lang]
37
80
  * @prop {string} [action]
81
+ * @prop {string} [snapshot]
82
+ * @prop {string} [botId]
38
83
  */
39
84
 
40
85
  /**
@@ -45,6 +90,7 @@ const Ai = require('../Ai');
45
90
  * @param {SessionMetadata} [metadata]
46
91
  * @param {number} [ts]
47
92
  * @param {boolean} [nonInteractive]
93
+ * @param {string} [timeZone]
48
94
  * @returns {Promise}
49
95
  */
50
96
 
@@ -53,11 +99,12 @@ const Ai = require('../Ai');
53
99
  * @param {string} pageId
54
100
  * @param {string} senderId
55
101
  * @param {string} sessionId
56
- * @param {Event[]} events
102
+ * @param {TrackingEvent[]} events
57
103
  * @param {GAUser} [user]
58
104
  * @param {number} [ts]
59
105
  * @param {boolean} [nonInteractive]
60
106
  * @param {boolean} [sessionStarted]
107
+ * @param {string} [timeZone]
61
108
  * @returns {Promise}
62
109
  */
63
110
 
@@ -72,6 +119,9 @@ const Ai = require('../Ai');
72
119
  * @prop {StoreEvents} storeEvents
73
120
  * @prop {CreateUserSession} createUserSession
74
121
  * @prop {boolean} [hasExtendedEvents]
122
+ * @prop {boolean} [supportsArrays]
123
+ * @prop {boolean} [useDescriptiveCategories]
124
+ * @prop {boolean} [useExtendedScalars]
75
125
  */
76
126
 
77
127
  /**
@@ -88,7 +138,7 @@ const Ai = require('../Ai');
88
138
 
89
139
  /**
90
140
  * @typedef {object} TrackingEvents
91
- * @prop {Event[]} events
141
+ * @prop {TrackingEvent[]} events
92
142
  */
93
143
 
94
144
  /**
@@ -98,6 +148,9 @@ const Ai = require('../Ai');
98
148
 
99
149
  /**
100
150
  * @typedef {object} HandlerConfig
151
+ * @prop {string} [snapshot]
152
+ * @prop {string} [botId]
153
+ * @prop {string} [timeZone] - default UTC
101
154
  * @prop {boolean} [enabled] - default true
102
155
  * @prop {boolean} [throwException] - default false
103
156
  * @prop {IGALogger} [log] - console like logger
@@ -134,12 +187,26 @@ function onInteractionHandler (
134
187
  enabled = true,
135
188
  throwException = false,
136
189
  log = console,
190
+ snapshot,
191
+ botId,
192
+ timeZone = 'UTC',
137
193
  anonymize = (x) => x,
138
194
  userExtractor = (state) => null // eslint-disable-line no-unused-vars
139
195
  },
140
196
  analyticsStorage,
141
197
  ai = Ai.ai
142
198
  ) {
199
+ const {
200
+ supportsArrays = false,
201
+ useExtendedScalars = false,
202
+ hasExtendedEvents = false,
203
+ useDescriptiveCategories = true
204
+ } = analyticsStorage;
205
+
206
+ const asArray = (data = []) => (supportsArrays ? data : data.join(','));
207
+ const asCategory = (cat) => (useDescriptiveCategories && CATEGORY_LABELS[cat]) || cat;
208
+ const noneAction = useExtendedScalars ? null : '(none)';
209
+ const noneValue = useExtendedScalars ? -1 : 0;
143
210
 
144
211
  /**
145
212
  * @param {InteractionEvent} params
@@ -151,13 +218,14 @@ function onInteractionHandler (
151
218
  // state,
152
219
  // data,
153
220
  skill,
154
- tracking
221
+ events,
222
+ flag,
223
+ nonInteractive
155
224
  }) {
156
225
  if (!enabled) {
157
226
  return;
158
227
  }
159
228
  try {
160
- const nonInteractive = !!req.campaign;
161
229
  const {
162
230
  pageId,
163
231
  senderId,
@@ -168,16 +236,20 @@ function onInteractionHandler (
168
236
  _snew: createSession,
169
237
  _sct: sessionCount,
170
238
  _sid: sessionId,
239
+ _sst: sessionStart,
240
+ _sts: sessionTs,
171
241
  lang
172
242
  } = req.state;
173
243
 
174
- const [action = '(none)', ...otherActions] = actions;
244
+ const [action = noneAction, ...otherActions] = actions;
175
245
 
176
246
  if (createSession) {
177
247
  const metadata = {
178
248
  sessionCount,
179
249
  lang,
180
- action
250
+ action,
251
+ snapshot,
252
+ botId
181
253
  };
182
254
 
183
255
  await analyticsStorage.createUserSession(
@@ -186,7 +258,8 @@ function onInteractionHandler (
186
258
  sessionId,
187
259
  metadata,
188
260
  timestamp,
189
- nonInteractive
261
+ nonInteractive,
262
+ timeZone
190
263
  );
191
264
  }
192
265
 
@@ -211,7 +284,7 @@ function onInteractionHandler (
211
284
 
212
285
  if (winners.length > 0) {
213
286
  [{
214
- action: winnerAction = '(none)',
287
+ action: winnerAction = noneAction,
215
288
  sort: winnerScore = 0,
216
289
  intent: { intent: winnerIntent, entities: winnerEntities = [] }
217
290
  }] = winners;
@@ -221,6 +294,15 @@ function onInteractionHandler (
221
294
 
222
295
  const expected = req.expected() ? req.expected().action : '';
223
296
 
297
+ const feedbackEvent = events.find((e) => e.type === TrackingType.REPORT
298
+ && e.category === TrackingCategory.REPORT_FEEDBACK);
299
+ const feedback = feedbackEvent
300
+ ? feedbackEvent.value
301
+ : noneValue;
302
+ const didHandover = flag === ResponseFlag.HANDOVER;
303
+
304
+ const user = userExtractor(req.state);
305
+
224
306
  const isContextUpdate = req.isSetContext();
225
307
  const isNotification = !!req.campaign;
226
308
  const isAttachment = req.isAttachment();
@@ -229,13 +311,17 @@ function onInteractionHandler (
229
311
  const isText = !isQuickReply && req.isText();
230
312
  const isPostback = req.isPostBack();
231
313
 
232
- const allActions = actions.join(',');
314
+ const allActions = asArray(actions);
233
315
  const requestAction = req.action();
234
316
 
235
- const events = [];
317
+ const trackEvents = [];
318
+
319
+ const langsExtension = hasExtendedEvents
320
+ ? { lang }
321
+ : { cd1: lang };
236
322
 
237
323
  const actionMeta = {
238
- requestAction: req.action() || '(none)',
324
+ requestAction: req.action() || noneAction,
239
325
  expected,
240
326
  expectedTaken: requestAction === expected,
241
327
  isContextUpdate,
@@ -245,20 +331,29 @@ function onInteractionHandler (
245
331
  isPassThread,
246
332
  isText,
247
333
  isPostback,
334
+ didHandover,
335
+ withUser: user !== null && !!user.id,
336
+ sessionStart,
337
+ sessionDuration: sessionTs - sessionStart,
338
+ feedback,
339
+ skill: webalize(skill),
340
+ snapshot,
341
+ botId,
248
342
  winnerAction,
249
343
  winnerIntent,
250
- winnerEntities: winnerEntities.map((e) => e.entity).join(','),
344
+ winnerEntities: asArray(winnerEntities.map((e) => e.entity)),
251
345
  winnerScore,
252
346
  winnerTaken,
253
347
  intent,
254
348
  intentScore: score,
255
- entities: req.entities.map((e) => e.entity).join(','),
349
+ entities: asArray(req.entities.map((e) => e.entity)),
256
350
  text,
257
351
  allActions
258
352
  };
259
353
 
260
- events.push({
261
- type: 'page_view',
354
+ trackEvents.push({
355
+ type: TrackingType.PAGE_VIEW,
356
+ category: asCategory(TrackingCategory.PAGE_VIEW_FIRST),
262
357
  action,
263
358
  allActions,
264
359
  nonInteractive,
@@ -267,15 +362,16 @@ function onInteractionHandler (
267
362
  skill,
268
363
  lang,
269
364
  cd1: req.state.lang,
270
- ...(analyticsStorage.hasExtendedEvents ? {} : actionMeta)
365
+ ...(hasExtendedEvents ? {} : actionMeta)
271
366
  });
272
367
 
273
368
  let prevAction = action;
274
369
 
275
- events.push(
370
+ trackEvents.push(
276
371
  ...otherActions.map((a) => {
277
372
  const r = {
278
- type: 'page_view',
373
+ type: TrackingType.PAGE_VIEW,
374
+ category: asCategory(TrackingCategory.PAGE_VIEW_SUBSEQUENT),
279
375
  action: a,
280
376
  allActions,
281
377
  nonInteractive: false,
@@ -283,9 +379,7 @@ function onInteractionHandler (
283
379
  prevAction,
284
380
  skill,
285
381
  isGoto: true,
286
- ...(analyticsStorage.hasExtendedEvents
287
- ? { lang }
288
- : { cd1: lang })
382
+ ...langsExtension
289
383
  };
290
384
 
291
385
  prevAction = a;
@@ -293,8 +387,8 @@ function onInteractionHandler (
293
387
  })
294
388
  );
295
389
 
296
- events.push(
297
- ...tracking.events.map(({
390
+ trackEvents.push(
391
+ ...events.map(({
298
392
  type, category, action: eventAction, label, value
299
393
  }) => ({
300
394
  lastAction,
@@ -303,93 +397,88 @@ function onInteractionHandler (
303
397
  action: eventAction,
304
398
  label,
305
399
  value,
306
- ...(analyticsStorage.hasExtendedEvents
307
- ? { lang }
308
- : { cd1: lang })
400
+ ...langsExtension
309
401
  }))
310
402
  );
311
403
 
312
404
  if (!nonInteractive) {
313
405
 
314
406
  if (req.isText()) {
315
- events.push({
316
- type: 'ai',
407
+ trackEvents.push({
408
+ type: TrackingType.TRAINING,
317
409
  // @ts-ignore
318
410
  lastAction,
319
- category: 'Intent: Detection',
411
+ category: asCategory(TrackingCategory.INTENT_DETECTION),
320
412
  intent,
321
413
  action,
322
414
  label: text,
323
415
  value: score >= ai.confidence ? 0 : 1,
324
- ...(analyticsStorage.hasExtendedEvents
325
- ? { lang }
326
- : { cd1: lang })
416
+ ...langsExtension
327
417
  });
328
418
  }
329
419
 
330
420
  const notHandled = actions.some((a) => a.match(/\*$/)) && !req.isQuickReply();
331
421
 
332
- let actionCategory = 'User: ';
333
- let label = '(none)';
422
+ let actionCategory;
423
+ let label = noneAction;
334
424
  const value = notHandled ? 1 : 0;
335
425
 
336
- if (req.isSticker()) {
337
- actionCategory += 'Sticker';
426
+ if (isPassThread) {
427
+ actionCategory = TrackingCategory.HANDOVER_TO_BOT;
428
+ } else if (req.isSticker()) {
429
+ actionCategory = TrackingCategory.STICKER;
338
430
  label = req.attachmentUrl(0);
339
431
  } else if (req.isImage()) {
340
- actionCategory += 'Image';
432
+ actionCategory = TrackingCategory.IMAGE;
341
433
  label = req.attachmentUrl(0);
342
434
  } else if (req.hasLocation()) {
343
- actionCategory += 'Location';
435
+ actionCategory = TrackingCategory.LOCATION;
344
436
  const { lat, long } = req.getLocation();
345
437
  label = `${lat}, ${long}`;
346
438
  } else if (isAttachment) {
347
- actionCategory += 'Attachement';
439
+ actionCategory = TrackingCategory.ATTACHMENT;
348
440
  label = req.attachment(0).type;
349
441
  } else if (isText) {
350
- actionCategory += 'Text';
442
+ actionCategory = TrackingCategory.TEXT;
351
443
  label = text;
352
444
  } else if (isQuickReply) {
353
- actionCategory += 'Quick reply';
445
+ actionCategory = TrackingCategory.QUICK_REPLY;
354
446
  label = text;
355
- } else if (req.isReferral() || req.isOptin()) {
356
- actionCategory = req.isOptin()
357
- ? 'Entry: Optin'
358
- : 'Entry: Referral';
447
+ } else if (req.isOptin()) {
448
+ actionCategory = TrackingCategory.OPT_IN;
449
+ } else if (req.isReferral()) {
450
+ actionCategory = TrackingCategory.REFERRAL;
359
451
  } else if (isPostback) {
360
- actionCategory += 'Button - bot';
452
+ actionCategory = TrackingCategory.POSTBACK_BUTTON;
361
453
  label = req.data.postback.title || '(unknown)';
362
454
  } else {
363
- actionCategory += 'Other';
455
+ actionCategory = TrackingCategory.OTHER;
364
456
  }
365
457
 
366
- events.push({
458
+ trackEvents.push({
367
459
  ...(analyticsStorage.hasExtendedEvents ? actionMeta : {}),
368
- type: 'conversation',
460
+ type: TrackingType.CONVERSATION_EVENT,
369
461
  // @ts-ignore
370
462
  lastAction,
371
- category: actionCategory,
463
+ category: asCategory(actionCategory),
372
464
  action,
373
465
  label,
374
466
  value,
375
- ...(analyticsStorage.hasExtendedEvents
376
- ? { lang }
377
- : { cd1: lang })
467
+ ...langsExtension
378
468
  });
379
469
  }
380
470
 
381
- const user = userExtractor(req.state);
382
-
383
471
  await analyticsStorage.storeEvents(
384
472
  pageId,
385
473
  senderId,
386
474
  sessionId,
387
475
  // @ts-ignore
388
- events,
476
+ trackEvents,
389
477
  user,
390
478
  timestamp,
391
479
  nonInteractive,
392
- createSession
480
+ createSession,
481
+ timeZone
393
482
  );
394
483
  } catch (e) {
395
484
  if (throwException) {
@@ -432,7 +521,7 @@ function onInteractionHandler (
432
521
  [{
433
522
  // @ts-ignore
434
523
  lastAction,
435
- ...(analyticsStorage.hasExtendedEvents
524
+ ...(hasExtendedEvents
436
525
  ? { lang }
437
526
  : { cd1: lang }),
438
527
  ...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
  /**