UNPKG

@ninetailed/experience.js-plugin-insights

Version:

Ninetailed SDK plugin for Ninetailed Insights

346 lines (333 loc) 12.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var radash = require('radash'); var experience_js = require('@ninetailed/experience.js'); var experience_jsShared = require('@ninetailed/experience.js-shared'); var retry = require('async-retry'); var experience_jsPluginAnalytics = require('@ninetailed/experience.js-plugin-analytics'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var retry__default = /*#__PURE__*/_interopDefaultLegacy(retry); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } const BASE_URL = 'https://ingest.insights.ninetailed.co'; const DEFAULT_ENVIRONMENT = 'main'; class HttpError extends Error { constructor(message, status = 500) { super(message); this.status = status; Object.setPrototypeOf(this, HttpError.prototype); } } class NinetailedInsightsApiClient { constructor({ clientId, environment = DEFAULT_ENVIRONMENT, url = BASE_URL }) { this.clientId = clientId; this.environment = environment; this.url = url; } logRequestError(error, { requestName }) { if (error instanceof Error) { if (error.name === 'AbortError') { experience_jsShared.logger.warn(`${requestName} request aborted due to network issues. This request is not retryable.`); } else { experience_jsShared.logger.error(`${requestName} request failed with error: [${error.name}] ${error.message}`); } } } makeRequest(url, payload, name, options) { const { useBeacon = false, timeout = 3000, retries = 1, minRetryTimeout = 0 } = options; const requestUrl = this.constructUrl(url); if (useBeacon) { const blobData = new Blob([JSON.stringify(payload)], { type: 'text/plain' }); navigator.sendBeacon(requestUrl, blobData); return; } return retry__default["default"](bail => __awaiter(this, void 0, void 0, function* () { try { const response = yield experience_jsShared.fetchTimeout(requestUrl, { method: 'POST', headers: this.constructHeaders(), body: JSON.stringify(payload), timeout }); if (response.status === 503) { throw new HttpError(`${name} request failed with status: "[${response.status}] ${response.statusText}".`, 503); } if (!response.ok) { bail(new Error(`${name} request failed with status: "[${response.status}] ${response.statusText} - traceparent: ${response.headers.get('traceparent')}". This request is not retryable`)); return null; } experience_jsShared.logger.debug(`${name} response: `, response); return response; } catch (error) { if (error instanceof HttpError && error.status === 503) { throw error; } if (error instanceof Error) { bail(error); } else { bail(new Error(`${name} request failed with an unknown error. This request is not retryable.`)); } return null; } }), { retries: retries, minTimeout: minRetryTimeout, onRetry: (error, attempt) => experience_jsShared.logger.error(`${error.message} Retrying (attempt ${attempt}).`) }); } sendEventBatches(batches, options = {}) { return __awaiter(this, void 0, void 0, function* () { const requestName = 'Send component view batches'; experience_jsShared.logger.info(`Sending ${requestName} request.`); experience_jsShared.logger.debug(`${requestName} request Body: `, batches); try { yield this.makeRequest(`/v1/organizations/${this.clientId}/environments/${this.environment}/events`, batches, requestName, options); experience_jsShared.logger.debug(`${requestName} request succesfully completed.`); } catch (error) { this.logRequestError(error, { requestName }); // Abort errors caused by timeouts should not bubble up and be reported by third-party tools (e.g. Sentry) if (!(error instanceof Error) || error.name !== 'AbortError') { throw error; } } }); } constructUrl(path) { const url = new URL(path, this.url); return url.toString(); } constructHeaders() { const headers = new Map(); headers.set('Content-Type', 'application/json'); return Object.fromEntries(headers); } } var _a, _b, _c; class NinetailedInsightsPlugin extends experience_jsPluginAnalytics.NinetailedPlugin { constructor({ url } = {}) { super(); this.name = 'ninetailed:insights'; this.seenElements = new WeakMap(); this.seenVariables = new Map(); this.events = []; this.eventsQueue = []; this.initialize = ({ instance }) => __awaiter(this, void 0, void 0, function* () { this.instance = instance; }); this.onHasSeenElement = ({ payload }) => { var _d; const { element, experience, variant, variantIndex } = payload; // eslint-disable-next-line @typescript-eslint/no-unused-vars const elementPayloadWithoutElement = __rest(payload, ["element"]); const componentId = variant.id; if (typeof componentId === 'undefined') { return; } const elementPayloads = this.seenElements.get(payload.element) || []; const isElementAlreadySeenWithPayload = elementPayloads.some(elementPayload => { return radash.isEqual(elementPayload, elementPayloadWithoutElement); }); if (isElementAlreadySeenWithPayload) { return; } this.seenElements.set(element, [...elementPayloads, elementPayloadWithoutElement]); /** * Intentionally sending a COMPONENT_START event instead of COMPONENT. * The NinetailedPrivacyPlugin, when used, will listen to COMPONENT_START and abort it if no consent is given. * If COMPONENT_START is aborted, the COMPONENT event will not be sent. * If NinetailedPrivacyPlugin is not used, the COMPONENT_START event will trigger the COMPONENT event. * * This behavior of the analytics library can be seen in the source code here: * https://github.com/DavidWells/analytics/blob/ba02d13d8b9d092cf24835b65f4f90af18f2740b/packages/analytics-core/src/index.js#L577 */ (_d = this.instance) === null || _d === void 0 ? void 0 : _d.dispatch({ type: experience_js.COMPONENT_START, componentId, componentType: 'Entry', variantIndex, experienceId: experience === null || experience === void 0 ? void 0 : experience.id }); }; this[_a] = ({ payload }) => { if (!this.eventBuilder) { experience_jsShared.logger.error('EventBuilder is not injected. Cannot build event. Skipping.'); return; } const { componentId, experienceId, variantIndex, componentType } = payload; const event = this.eventBuilder.component(componentId, componentType, experienceId, variantIndex); this.events.push(event); if (this.shouldFlushEventsQueue()) { if (this.profile) { this.createEventsBatch(this.profile); } this.flushEventsQueue(); } }; this.onHasSeenVariable = ({ payload }) => { var _d; const { variant, variantIndex, experienceId } = payload; // eslint-disable-next-line @typescript-eslint/no-unused-vars const variablePayloadWithoutVariable = __rest(payload, ["variable"]); const componentId = variant.id; if (typeof componentId === 'undefined') { return; } const variablePayloads = this.seenVariables.get(componentId) || []; const isVariableAlreadySeenWithPayload = variablePayloads.some(variablePayload => { return radash.isEqual(variablePayload, variablePayloadWithoutVariable); }); if (isVariableAlreadySeenWithPayload) { return; } this.seenVariables.set(componentId, [...variablePayloads, variablePayloadWithoutVariable]); /** * Intentionally sending a COMPONENT_START event instead of COMPONENT. * The NinetailedPrivacyPlugin, when used, will listen to COMPONENT_START and abort it if no consent is given. * If COMPONENT_START is aborted, the COMPONENT event will not be sent. * If NinetailedPrivacyPlugin is not used, the COMPONENT_START event will trigger the COMPONENT event. * * This behavior of the analytics library can be seen in the source code here: * https://github.com/DavidWells/analytics/blob/ba02d13d8b9d092cf24835b65f4f90af18f2740b/packages/analytics-core/src/index.js#L577 */ (_d = this.instance) === null || _d === void 0 ? void 0 : _d.dispatch({ type: experience_js.COMPONENT_START, componentId, componentType: 'Variable', variantIndex, experienceId }); }; this[_b] = ({ payload }) => { var _d; const { profile } = payload; const previousProfile = (_d = this.profile) !== null && _d !== void 0 ? _d : profile; if (previousProfile) { this.createEventsBatch(previousProfile); } this.profile = profile !== null && profile !== void 0 ? profile : undefined; this.seenElements = new WeakMap(); }; this[_c] = () => { if (!this.profile) { return; } this.createEventsBatch(this.profile); this.flushEventsQueue(true); }; this.insightsApiClientUrl = url; } createEventsBatch(previousProfile) { if (this.events.length === 0) { return; } const profileBatch = { profile: previousProfile, events: this.events }; this.eventsQueue.push(profileBatch); this.events = []; } shouldFlushEventsQueue() { return this.eventsQueue.map(({ events }) => events).flat().length + this.events.length === NinetailedInsightsPlugin.MAX_EVENTS; } flushEventsQueue(useBeacon = false) { var _d; if (this.eventsQueue.length === 0) { return; } (_d = this.insightsApiClient) === null || _d === void 0 ? void 0 : _d.sendEventBatches(this.eventsQueue, { useBeacon }); this.eventsQueue = []; } setCredentials(credentials) { this.insightsApiClient = new NinetailedInsightsApiClient({ url: this.insightsApiClientUrl, clientId: credentials.clientId, environment: credentials.environment }); } setEventBuilder(eventBuilder) { this.eventBuilder = eventBuilder; } } _a = experience_js.COMPONENT, _b = experience_js.PROFILE_CHANGE, _c = experience_js.PAGE_HIDDEN; NinetailedInsightsPlugin.MAX_EVENTS = 25; exports.NinetailedInsightsApiClient = NinetailedInsightsApiClient; exports.NinetailedInsightsPlugin = NinetailedInsightsPlugin; exports["default"] = NinetailedInsightsPlugin;