wingbot 3.45.2 → 3.46.0-alpha.10

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.10",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -9,7 +9,7 @@
9
9
  "doc": "npm run doc:gql && node ./bin/makeApiDoc.js && cpy ./CHANGELOG.md ./doc && gitbook install ./doc && gitbook build ./doc && rimraf -rf ./docs && rimraf --rf ./doc/CHANGELOG.md && move-cli ./doc/_book ./docs",
10
10
  "test": "npm run test:lint && npm run test:coverage && npm run test:coverage:threshold",
11
11
  "test:coverage": "nyc --reporter=html mocha ./test && nyc report",
12
- "test:coverage:threshold": "nyc check-coverage --lines 90 --functions 90 --branches 80",
12
+ "test:coverage:threshold": "nyc check-coverage --lines 90 --functions 89 --branches 80",
13
13
  "test:backend": "mocha ./test",
14
14
  "test:lint": "eslint --ext .js src test *.js plugins"
15
15
  },
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
+ DISAMBIGUATION_SELECTED: 'dis',
66
+ DISAMBIGUATION_OFFERED: 'dio',
67
+
68
+ // REPORT: 'report'
69
+ REPORT_FEEDBACK: 'fdb',
70
+ HANDOVER_OCCURRED: 'hum'
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.DISAMBIGUATION_SELECTED]: 'Disambiguation: selected',
98
+ [TrackingCategory.DISAMBIGUATION_OFFERED]: 'Disambiguation: offered',
99
+
100
+ // REPORT: 'report'
101
+ [TrackingCategory.REPORT_FEEDBACK]: 'User: Feedback',
102
+ [TrackingCategory.HANDOVER_OCCURRED]: 'Bot: Handover out'
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,61 @@ 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} [lastAction]
40
+ * @prop {string} [skill]
41
+ * @prop {string} [text]
42
+ * @prop {string} [expected]
43
+ * @prop {boolean} expectedTaken
44
+ * @prop {boolean} isContextUpdate
45
+ * @prop {boolean} isAttachment
46
+ * @prop {boolean} isNotification
47
+ * @prop {boolean} isQuickReply
48
+ * @prop {boolean} isPassThread
49
+ * @prop {boolean} isPostback
50
+ * @prop {boolean} isText
51
+ * @prop {boolean} didHandover
52
+ * @prop {boolean} withUser
53
+ * @prop {string} [userId]
54
+ * @prop {number} [feedback]
55
+ * @prop {string} [winnerAction]
56
+ * @prop {string} [winnerIntent]
57
+ * @prop {string[]|string} [winnerEntities]
58
+ * @prop {number} [winnerScore]
59
+ * @prop {boolean} [winnerTaken]
60
+ * @prop {string} [intent]
61
+ * @prop {number} [intentScore]
62
+ * @prop {string[]|string} [entities]
63
+ * @prop {string[]|string} allActions
64
+ * @prop {boolean} nonInteractive
65
+ *
66
+ * @typedef {Event & ConversationEventExtension} ConversationEvent
67
+ */
68
+
69
+ /**
70
+ * @typedef {object} PageViewEventExtension
71
+ * @prop {string} [lastAction]
72
+ * @prop {string} [prevAction]
73
+ * @prop {string} [skill]
74
+ * @prop {string[]|string} allActions
75
+ * @prop {boolean} nonInteractive
76
+ * @prop {boolean} isGoto
77
+ * @prop {boolean} withUser
78
+ *
79
+ * @typedef {Event & PageViewEventExtension} PageViewEvent
80
+ */
81
+
82
+ /**
83
+ * @typedef {ConversationEvent | Event | PageViewEvent} TrackingEvent
31
84
  */
32
85
 
33
86
  /**
@@ -35,6 +88,13 @@ const Ai = require('../Ai');
35
88
  * @prop {number} [sessionCount]
36
89
  * @prop {string} [lang]
37
90
  * @prop {string} [action]
91
+ * @prop {string} [snapshot]
92
+ * @prop {string} [botId]
93
+ * @prop {boolean} [didHandover]
94
+ * @prop {number|null} [feedback]
95
+ * @prop {string} [timeZone]
96
+ * @prop {number} [sessionStart]
97
+ * @prop {number} [sessionDuration]
38
98
  */
39
99
 
40
100
  /**
@@ -53,11 +113,12 @@ const Ai = require('../Ai');
53
113
  * @param {string} pageId
54
114
  * @param {string} senderId
55
115
  * @param {string} sessionId
56
- * @param {Event[]} events
116
+ * @param {TrackingEvent[]} events
57
117
  * @param {GAUser} [user]
58
118
  * @param {number} [ts]
59
119
  * @param {boolean} [nonInteractive]
60
120
  * @param {boolean} [sessionStarted]
121
+ * @param {SessionMetadata} [metadata]
61
122
  * @returns {Promise}
62
123
  */
63
124
 
@@ -72,6 +133,10 @@ const Ai = require('../Ai');
72
133
  * @prop {StoreEvents} storeEvents
73
134
  * @prop {CreateUserSession} createUserSession
74
135
  * @prop {boolean} [hasExtendedEvents]
136
+ * @prop {boolean} [supportsArrays]
137
+ * @prop {boolean} [useDescriptiveCategories]
138
+ * @prop {boolean} [useExtendedScalars]
139
+ * @prop {boolean} [parallelSessionInsert]
75
140
  */
76
141
 
77
142
  /**
@@ -88,7 +153,7 @@ const Ai = require('../Ai');
88
153
 
89
154
  /**
90
155
  * @typedef {object} TrackingEvents
91
- * @prop {Event[]} events
156
+ * @prop {TrackingEvent[]} events
92
157
  */
93
158
 
94
159
  /**
@@ -98,6 +163,9 @@ const Ai = require('../Ai');
98
163
 
99
164
  /**
100
165
  * @typedef {object} HandlerConfig
166
+ * @prop {string} [snapshot]
167
+ * @prop {string} [botId]
168
+ * @prop {string} [timeZone] - default UTC
101
169
  * @prop {boolean} [enabled] - default true
102
170
  * @prop {boolean} [throwException] - default false
103
171
  * @prop {IGALogger} [log] - console like logger
@@ -134,12 +202,27 @@ function onInteractionHandler (
134
202
  enabled = true,
135
203
  throwException = false,
136
204
  log = console,
205
+ snapshot,
206
+ botId,
207
+ timeZone = 'UTC',
137
208
  anonymize = (x) => x,
138
209
  userExtractor = (state) => null // eslint-disable-line no-unused-vars
139
210
  },
140
211
  analyticsStorage,
141
212
  ai = Ai.ai
142
213
  ) {
214
+ const {
215
+ supportsArrays = false,
216
+ useExtendedScalars = false,
217
+ hasExtendedEvents = false,
218
+ useDescriptiveCategories = true,
219
+ parallelSessionInsert = false
220
+ } = analyticsStorage;
221
+
222
+ const asArray = (data = []) => (supportsArrays ? data : data.join(','));
223
+ const asCategory = (cat) => (useDescriptiveCategories && CATEGORY_LABELS[cat]) || cat;
224
+ const noneAction = useExtendedScalars ? null : '(none)';
225
+ const noneValue = useExtendedScalars ? null : 0;
143
226
 
144
227
  /**
145
228
  * @param {InteractionEvent} params
@@ -151,13 +234,14 @@ function onInteractionHandler (
151
234
  // state,
152
235
  // data,
153
236
  skill,
154
- tracking
237
+ events,
238
+ flag,
239
+ nonInteractive
155
240
  }) {
156
241
  if (!enabled) {
157
242
  return;
158
243
  }
159
244
  try {
160
- const nonInteractive = !!req.campaign;
161
245
  const {
162
246
  pageId,
163
247
  senderId,
@@ -168,19 +252,52 @@ function onInteractionHandler (
168
252
  _snew: createSession,
169
253
  _sct: sessionCount,
170
254
  _sid: sessionId,
255
+ _sst: sessionStart,
256
+ _sts: sessionTs,
171
257
  lang
172
258
  } = req.state;
173
259
 
174
- const [action = '(none)', ...otherActions] = actions;
260
+ const trackEvents = [];
261
+
262
+ const [action = noneAction, ...otherActions] = actions;
263
+
264
+ const feedbackEvent = events.find((e) => e.type === TrackingType.REPORT
265
+ && e.category === TrackingCategory.REPORT_FEEDBACK);
266
+ const feedback = feedbackEvent
267
+ ? feedbackEvent.value
268
+ : noneValue;
269
+ let didHandover = flag === ResponseFlag.HANDOVER;
270
+ const hasHandoverEvent = events
271
+ .some((e) => e.category === TrackingCategory.HANDOVER_OCCURRED);
272
+
273
+ if (didHandover && !hasHandoverEvent) {
274
+ trackEvents.push({
275
+ type: TrackingType.REPORT,
276
+ category: asCategory(TrackingCategory.HANDOVER_OCCURRED),
277
+ action: null,
278
+ label: null,
279
+ value: noneValue
280
+ });
281
+ } else if (hasHandoverEvent) {
282
+ didHandover = true;
283
+ }
175
284
 
176
- if (createSession) {
177
- const metadata = {
178
- sessionCount,
179
- lang,
180
- action
181
- };
285
+ const metadata = {
286
+ sessionCount,
287
+ lang,
288
+ action,
289
+ snapshot,
290
+ botId,
291
+ didHandover,
292
+ feedback,
293
+ timeZone,
294
+ sessionStart,
295
+ sessionDuration: sessionTs - sessionStart
296
+ };
182
297
 
183
- await analyticsStorage.createUserSession(
298
+ let sessionPromise;
299
+ if (createSession) {
300
+ sessionPromise = analyticsStorage.createUserSession(
184
301
  pageId,
185
302
  senderId,
186
303
  sessionId,
@@ -188,6 +305,11 @@ function onInteractionHandler (
188
305
  timestamp,
189
306
  nonInteractive
190
307
  );
308
+
309
+ if (!parallelSessionInsert) {
310
+ await sessionPromise;
311
+ sessionPromise = null;
312
+ }
191
313
  }
192
314
 
193
315
  const [{
@@ -200,6 +322,7 @@ function onInteractionHandler (
200
322
  : anonymize(
201
323
  replaceDiacritics(req.text()).replace(/\s+/g, ' ').toLowerCase().trim()
202
324
  );
325
+ const useSkill = (skill && webalize(skill)) || noneAction;
203
326
 
204
327
  let winnerAction = '';
205
328
  let winnerScore = 0;
@@ -211,7 +334,7 @@ function onInteractionHandler (
211
334
 
212
335
  if (winners.length > 0) {
213
336
  [{
214
- action: winnerAction = '(none)',
337
+ action: winnerAction = noneAction,
215
338
  sort: winnerScore = 0,
216
339
  intent: { intent: winnerIntent, entities: winnerEntities = [] }
217
340
  }] = winners;
@@ -220,6 +343,7 @@ function onInteractionHandler (
220
343
  }
221
344
 
222
345
  const expected = req.expected() ? req.expected().action : '';
346
+ const user = userExtractor(req.state);
223
347
 
224
348
  const isContextUpdate = req.isSetContext();
225
349
  const isNotification = !!req.campaign;
@@ -229,13 +353,17 @@ function onInteractionHandler (
229
353
  const isText = !isQuickReply && req.isText();
230
354
  const isPostback = req.isPostBack();
231
355
 
232
- const allActions = actions.join(',');
356
+ const allActions = asArray(actions);
233
357
  const requestAction = req.action();
234
358
 
235
- const events = [];
359
+ const langsExtension = hasExtendedEvents
360
+ ? { lang }
361
+ : { cd1: lang };
362
+
363
+ const withUser = user !== null && !!user.id;
236
364
 
237
365
  const actionMeta = {
238
- requestAction: req.action() || '(none)',
366
+ requestAction: req.action() || noneAction,
239
367
  expected,
240
368
  expectedTaken: requestAction === expected,
241
369
  isContextUpdate,
@@ -245,47 +373,59 @@ function onInteractionHandler (
245
373
  isPassThread,
246
374
  isText,
247
375
  isPostback,
376
+ didHandover,
377
+ withUser,
378
+ feedback,
379
+ skill: useSkill,
248
380
  winnerAction,
249
381
  winnerIntent,
250
- winnerEntities: winnerEntities.map((e) => e.entity).join(','),
382
+ winnerEntities: asArray(winnerEntities.map((e) => e.entity)),
251
383
  winnerScore,
252
384
  winnerTaken,
253
385
  intent,
254
386
  intentScore: score,
255
- entities: req.entities.map((e) => e.entity).join(','),
387
+ entities: asArray(req.entities.map((e) => e.entity)),
256
388
  text,
257
389
  allActions
258
390
  };
259
391
 
260
- events.push({
261
- type: 'page_view',
392
+ const notHandled = actions.some((a) => a.match(/\*$/)) && !req.isQuickReply();
393
+ const value = notHandled ? 1 : 0;
394
+
395
+ trackEvents.push({
396
+ type: TrackingType.PAGE_VIEW,
397
+ category: asCategory(TrackingCategory.PAGE_VIEW_FIRST),
262
398
  action,
399
+ label: (isText || isQuickReply ? text : null),
400
+ value,
263
401
  allActions,
264
402
  nonInteractive,
265
403
  lastAction,
404
+ // @ts-ignore
266
405
  prevAction: lastAction,
267
- skill,
268
- lang,
269
- cd1: req.state.lang,
270
- ...(analyticsStorage.hasExtendedEvents ? {} : actionMeta)
406
+ skill: useSkill,
407
+ isGoto: false,
408
+ withUser,
409
+ ...langsExtension,
410
+ ...(hasExtendedEvents ? {} : actionMeta)
271
411
  });
272
412
 
273
413
  let prevAction = action;
274
414
 
275
- events.push(
415
+ trackEvents.push(
276
416
  ...otherActions.map((a) => {
277
417
  const r = {
278
- type: 'page_view',
418
+ type: TrackingType.PAGE_VIEW,
419
+ category: asCategory(TrackingCategory.PAGE_VIEW_SUBSEQUENT),
279
420
  action: a,
421
+ value: 0,
280
422
  allActions,
281
423
  nonInteractive: false,
282
424
  lastAction,
283
425
  prevAction,
284
- skill,
426
+ skill: useSkill,
285
427
  isGoto: true,
286
- ...(analyticsStorage.hasExtendedEvents
287
- ? { lang }
288
- : { cd1: lang })
428
+ ...langsExtension
289
429
  };
290
430
 
291
431
  prevAction = a;
@@ -293,104 +433,98 @@ function onInteractionHandler (
293
433
  })
294
434
  );
295
435
 
296
- events.push(
297
- ...tracking.events.map(({
298
- type, category, action: eventAction, label, value
436
+ trackEvents.push(
437
+ ...events.map(({
438
+ type, category, action: eventAction, label, value: eVal
299
439
  }) => ({
300
440
  lastAction,
301
441
  type,
302
- category,
442
+ category: asCategory(category),
303
443
  action: eventAction,
304
444
  label,
305
- value,
306
- ...(analyticsStorage.hasExtendedEvents
307
- ? { lang }
308
- : { cd1: lang })
445
+ value: eVal,
446
+ ...langsExtension
309
447
  }))
310
448
  );
311
449
 
312
450
  if (!nonInteractive) {
313
451
 
314
452
  if (req.isText()) {
315
- events.push({
316
- type: 'ai',
453
+ trackEvents.push({
454
+ type: TrackingType.TRAINING,
317
455
  // @ts-ignore
318
456
  lastAction,
319
- category: 'Intent: Detection',
457
+ category: asCategory(TrackingCategory.INTENT_DETECTION),
320
458
  intent,
321
459
  action,
322
460
  label: text,
323
461
  value: score >= ai.confidence ? 0 : 1,
324
- ...(analyticsStorage.hasExtendedEvents
325
- ? { lang }
326
- : { cd1: lang })
462
+ ...langsExtension
327
463
  });
328
464
  }
329
465
 
330
- const notHandled = actions.some((a) => a.match(/\*$/)) && !req.isQuickReply();
331
-
332
- let actionCategory = 'User: ';
333
- let label = '(none)';
334
- const value = notHandled ? 1 : 0;
466
+ let actionCategory;
467
+ let label = noneAction;
335
468
 
336
- if (req.isSticker()) {
337
- actionCategory += 'Sticker';
469
+ if (isPassThread) {
470
+ actionCategory = TrackingCategory.HANDOVER_TO_BOT;
471
+ } else if (req.isSticker()) {
472
+ actionCategory = TrackingCategory.STICKER;
338
473
  label = req.attachmentUrl(0);
339
474
  } else if (req.isImage()) {
340
- actionCategory += 'Image';
475
+ actionCategory = TrackingCategory.IMAGE;
341
476
  label = req.attachmentUrl(0);
342
477
  } else if (req.hasLocation()) {
343
- actionCategory += 'Location';
478
+ actionCategory = TrackingCategory.LOCATION;
344
479
  const { lat, long } = req.getLocation();
345
480
  label = `${lat}, ${long}`;
346
481
  } else if (isAttachment) {
347
- actionCategory += 'Attachement';
482
+ actionCategory = TrackingCategory.ATTACHMENT;
348
483
  label = req.attachment(0).type;
349
484
  } else if (isText) {
350
- actionCategory += 'Text';
485
+ actionCategory = TrackingCategory.TEXT;
351
486
  label = text;
352
487
  } else if (isQuickReply) {
353
- actionCategory += 'Quick reply';
488
+ actionCategory = TrackingCategory.QUICK_REPLY;
354
489
  label = text;
355
- } else if (req.isReferral() || req.isOptin()) {
356
- actionCategory = req.isOptin()
357
- ? 'Entry: Optin'
358
- : 'Entry: Referral';
490
+ } else if (req.isOptin()) {
491
+ actionCategory = TrackingCategory.OPT_IN;
492
+ } else if (req.isReferral()) {
493
+ actionCategory = TrackingCategory.REFERRAL;
359
494
  } else if (isPostback) {
360
- actionCategory += 'Button - bot';
361
- label = req.data.postback.title || '(unknown)';
495
+ actionCategory = TrackingCategory.POSTBACK_BUTTON;
496
+ label = req.event.postback.title || (useExtendedScalars ? null : '(unknown)');
362
497
  } else {
363
- actionCategory += 'Other';
498
+ actionCategory = TrackingCategory.OTHER;
364
499
  }
365
500
 
366
- events.push({
501
+ trackEvents.push({
367
502
  ...(analyticsStorage.hasExtendedEvents ? actionMeta : {}),
368
- type: 'conversation',
369
- // @ts-ignore
503
+ type: TrackingType.CONVERSATION_EVENT,
370
504
  lastAction,
371
- category: actionCategory,
505
+ category: asCategory(actionCategory),
372
506
  action,
373
507
  label,
374
508
  value,
375
- ...(analyticsStorage.hasExtendedEvents
376
- ? { lang }
377
- : { cd1: lang })
509
+ ...langsExtension
378
510
  });
379
511
  }
380
512
 
381
- const user = userExtractor(req.state);
382
-
383
- await analyticsStorage.storeEvents(
384
- pageId,
385
- senderId,
386
- sessionId,
387
- // @ts-ignore
388
- events,
389
- user,
390
- timestamp,
391
- nonInteractive,
392
- createSession
393
- );
513
+ await Promise.all([
514
+ analyticsStorage.storeEvents(
515
+ pageId,
516
+ senderId,
517
+ sessionId,
518
+ // @ts-ignore
519
+ trackEvents,
520
+ user,
521
+ timestamp,
522
+ nonInteractive,
523
+ createSession,
524
+ metadata
525
+ ),
526
+ sessionPromise
527
+ ]);
394
528
  } catch (e) {
395
529
  if (throwException) {
396
530
  throw e;
@@ -432,7 +566,7 @@ function onInteractionHandler (
432
566
  [{
433
567
  // @ts-ignore
434
568
  lastAction,
435
- ...(analyticsStorage.hasExtendedEvents
569
+ ...(hasExtendedEvents
436
570
  ? { lang }
437
571
  : { cd1: lang }),
438
572
  ...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
  /**