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