UNPKG

wingbot

Version:

Enterprise Messaging Bot Conversation Engine

273 lines (230 loc) 7.85 kB
/** * @author David Menger */ 'use strict'; const fetch = require('node-fetch').default; /** @typedef {import('./onInteractionHandler').Event} Event */ /** @typedef {import('./onInteractionHandler').IAnalyticsStorage} IAnalyticsStorage */ /** @typedef {import('./onInteractionHandler').GAUser} GAUser */ /** @typedef {import('./onInteractionHandler').SessionMetadata} SessionMetadata */ /** @typedef {import('./onInteractionHandler').IGALogger} IGALogger */ /** @typedef {import('node-fetch').RequestInit} RequestInit */ /** * @typedef {object} FetchResult * @param {number} status * @param {string} [statusText] * @param {Promise<object>} json */ /** * @callback MockFetch * @param {string} url * @param {RequestInit} [options] * @returns {Promise<FetchResult>} */ /** * @typedef {object} GAOptions * @prop {string} measurementId * @prop {string} apiSecret * @prop {boolean} [debug] * @prop {IGALogger} [log] * @prop {MockFetch} [fetch] */ /** * @class GA4 * @implements {IAnalyticsStorage} */ class GA4 { /** * * @param {GAOptions} options */ constructor (options) { this._options = options; /** @type {IGALogger} */ this._logger = options.log || console; this._urlQuery = `measurement_id=${encodeURIComponent(options.measurementId)}&api_secret=${encodeURIComponent(options.apiSecret)}`; this._url = `https://www.google-analytics.com/mp/collect?${this._urlQuery}`; this.hasExtendedEvents = true; this._fetch = options.fetch || fetch; } /** * @param {IGALogger} logger */ setDefaultLogger (logger) { if (this._logger === console) { this._logger = logger; } } /** * * @param {string} pageId * @param {string} senderId * @param {string} sessionId * @param {SessionMetadata} [metadata] * @param {number} [ts] * @param {boolean} [nonInteractive] * @returns {Promise} */ async createUserSession ( pageId, senderId, sessionId, metadata = {}, ts = Date.now(), nonInteractive = false ) { const uafvl = 'wingbot'; const { lang = '', sessionCount = 1, action = '/' } = metadata; const event = { v: 2, tid: this._options.measurementId, _p: Math.round(2147483647 * Math.random()), sr: '1x1', _dbg: this._options.debug ? 1 : 0, ul: lang, // language cid: this._conversationId(pageId, senderId), // dl: 'https://wingbot-web-staging.flyto.cloud/dp', dp: action, dr: '', // referral (url) dt: action === '/' ? '(none)' : action.replace(/-/g, ' '), // en: 'page_view', // event name en: 'scroll', 'epn.percent_scrolled': 100, uafvl, // must have sct: sessionCount, // session count (int) seg: nonInteractive ? 0 : 1, // session engagement (boolean) sid: sessionId, // session id (string) // this was sent during the first visit _fv: sessionCount === 1, // first visit (bool) _nsi: 1, // new session id (bool) _ss: 1, // session start (bool) _ee: 1, // ? page_view event parameter (??? event engagement ??) _s: 1, // session hit count (was 1 every request), _et: ts // event time (number) }; if (this._options.debug) { this._logger.log('GA4: starting session', event); } const query = Object.entries(event) .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) .join('&'); // const url = 'https://region1.google-analytics.com/g/collect'; const url = 'https://www.google-analytics.com/g/collect'; const res = await this._fetch(`${url}?${query}`, { method: 'POST', headers: { 'user-agent': uafvl } }); if (res.status >= 400) { this._logger.error('GA4: failed to create session', { url, query, status: res.status }); } } _conversationId (pageId, senderId) { return `${pageId}.${senderId}`; } /** * * @param {string} pageId * @param {string} senderId * @param {string} sessionId * @param {Event[]} events * @param {GAUser} [user] * @param {number} [ts] * @returns {Promise} */ async storeEvents ( pageId, senderId, sessionId, events, user = null, ts = Date.now() ) { if (events.length === 0) { return; } const body = { client_id: this._conversationId(pageId, senderId), timestamp_micros: ts * 1000, non_personalized_ads: false, events: events.map((e) => { const { type: name, ...params } = e; Object.entries(params) .forEach(([k, v]) => { if (v === null) { params[k] = '(none)'; } }); // Object.assign(params, { session_id: sessionId }); switch (name) { case 'page_view': return { name, params: { page_path: e.action, page_title: e.action .replace(/^\/+/, '') .replace(/[-]+/g, ' ') .replace(/[/]+/g, ' - '), ...params } }; default: return { name, params }; } }) }; if (user) { const { id, ...other } = user; Object.assign(body, { user_id: id, user_properties: Object.fromEntries( Object.entries(other) .map(([key, value]) => [key, { value }]) ) }); } const params = { method: 'POST', body: JSON.stringify(body) }; let err; let res; try { res = await this._fetch(this._url, params); if (res.status >= 400) { throw new Error(`${res.statusText} [${res.status}]`); } if (!this._options.debug) { return; } } catch (e) { err = e; } let { message = 'GENERIC FAIL' } = err || { message: null }; let validationMessages = []; try { const dbg = await this._fetch(`https://www.google-analytics.com/debug/mp/collect?${this._urlQuery}`, params); if (dbg.status >= 300) { throw new Error(`${dbg.statusText} [${dbg.status}]`); } ({ validationMessages = [] } = await dbg.json()); message = (validationMessages[0] || { description: message }).description; } catch (e) { this._logger.log('GA4 debug failed', e); message += ` +(${e.message})`; } if (validationMessages.length || message) { this._logger.log('GA4: validationMessages', validationMessages); this._logger.error(`GA4: fail: ${message} [${res ? res.status : 0}]`, params.body); } else { this._logger.log(`GA4: debug [${res ? res.status : 0}]`, params.body); } } } module.exports = GA4;