"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.TelemetrySender = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _rxjs = require("rxjs"); var _operators = require("rxjs/operators"); var _public = require("@kbn/kibana-utils-plugin/public"); var _constants = require("../../common/constants"); var _is_report_interval_expired = require("../../common/is_report_interval_expired"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0 and the Server Side Public License, v 1; you may not use this file except * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ class TelemetrySender { static getRetryDelay(retryCount) { return 60 * (1000 * Math.min(Math.pow(2, retryCount), 64)); // 120s, 240s, 480s, 960s, 1920s, 3840s, 3840s, 3840s } constructor(telemetryService, refreshConfig) { (0, _defineProperty2.default)(this, "lastReported", void 0); (0, _defineProperty2.default)(this, "storage", void 0); (0, _defineProperty2.default)(this, "sendIfDue$", void 0); (0, _defineProperty2.default)(this, "retryCount", 0); (0, _defineProperty2.default)(this, "updateLastReported", lastReported => { this.lastReported = lastReported; // we are the only code that manipulates this key, so it's safe to blindly overwrite the whole object this.storage.set(_constants.LOCALSTORAGE_KEY, { lastReport: `${this.lastReported}` }); }); /** * Using the local and SO's `lastReported` values, it decides whether the last report should be considered as expired * @returns `true` if a new report should be generated. `false` otherwise. */ (0, _defineProperty2.default)(this, "isReportDue", async () => { // Try to decide with the local `lastReported` to avoid querying the server if (!(0, _is_report_interval_expired.isReportIntervalExpired)(this.lastReported)) { // If it is not expired locally, there's no need to send it again yet. return false; } // Double-check with the server's value const globalLastReported = await this.telemetryService.fetchLastReported(); if (globalLastReported) { // Update the local value to avoid repetitions of this request (it was already expired, so it doesn't really matter if the server's value is older) this.updateLastReported(globalLastReported); } return (0, _is_report_interval_expired.isReportIntervalExpired)(globalLastReported); }); /** * Returns `true` when the page is visible and active in the browser. */ (0, _defineProperty2.default)(this, "isActiveWindow", () => { // Using `document.hasFocus()` instead of `document.visibilityState` because the latter may return "visible" // if 2 windows are open side-by-side because they are "technically" visible. return document.hasFocus(); }); /** * Using configuration, page visibility state and the lastReported dates, * it decides whether a new telemetry report should be sent. * @returns `true` if a new report should be sent. `false` otherwise. */ (0, _defineProperty2.default)(this, "shouldSendReport", async () => { if (this.isActiveWindow() && this.telemetryService.canSendTelemetry()) { if (await this.isReportDue()) { /* * If we think it should send telemetry (local optIn config is `true` and the last report is expired), * let's refresh the config and make sure optIn is still true. * * This change is to ensure that if the user opts-out of telemetry, background tabs realize about it without needing to refresh the page or navigate to another app. * * We are checking twice to avoid making too many requests to fetch the SO: * `sendIfDue` is triggered every minute or when the page regains focus. * If the previously fetched config already dismisses the telemetry, there's no need to fetch the telemetry config. * * The edge case is: if previously opted-out and the user opts-in, background tabs won't realize about that until they navigate to another app. * We are fine with that compromise for now. */ await this.refreshConfig(); return this.telemetryService.canSendTelemetry(); } } return false; }); (0, _defineProperty2.default)(this, "sendIfDue", async () => { if (!(await this.shouldSendReport())) { return; } // optimistically update the report date and reset the retry counter for a new time report interval window this.updateLastReported(Date.now()); this.retryCount = 0; await this.sendUsageData(); }); (0, _defineProperty2.default)(this, "sendUsageData", async () => { try { const telemetryUrl = this.telemetryService.getTelemetryUrl(); const telemetryPayload = await this.telemetryService.fetchTelemetry(); await Promise.all(telemetryPayload.map(async ({ clusterUuid, stats }) => await fetch(telemetryUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Elastic-Stack-Version': this.telemetryService.currentKibanaVersion, 'X-Elastic-Cluster-ID': clusterUuid, 'X-Elastic-Content-Encoding': _constants.PAYLOAD_CONTENT_ENCODING }, body: stats }))); await this.telemetryService.updateLastReported().catch(() => {}); // Let's catch the error. Worst-case scenario another Telemetry report will be generated somewhere else. } catch (err) { // ignore err and try again but after a longer wait period. this.retryCount = this.retryCount + 1; if (this.retryCount < 20) { // exponentially backoff the time between subsequent retries to up to 19 attempts, after which we give up until the next report is due window.setTimeout(this.sendUsageData, TelemetrySender.getRetryDelay(this.retryCount)); } else { /* eslint no-console: ["error", { allow: ["warn"] }] */ console.warn(`TelemetrySender.sendUsageData exceeds number of retry attempts with ${err.message}`); } } }); (0, _defineProperty2.default)(this, "startChecking", () => { if (!this.sendIfDue$) { // Trigger sendIfDue... this.sendIfDue$ = (0, _rxjs.merge)( // ... periodically (0, _rxjs.interval)(60000), // ... when it regains `focus` (0, _rxjs.fromEvent)(window, 'focus') // Using `window` instead of `document` because Chrome only emits on the first one. ).pipe((0, _operators.exhaustMap)(this.sendIfDue)).subscribe(); } }); (0, _defineProperty2.default)(this, "stop", () => { var _this$sendIfDue$; (_this$sendIfDue$ = this.sendIfDue$) === null || _this$sendIfDue$ === void 0 ? void 0 : _this$sendIfDue$.unsubscribe(); }); this.telemetryService = telemetryService; this.refreshConfig = refreshConfig; this.storage = new _public.Storage(window.localStorage); const attributes = this.storage.get(_constants.LOCALSTORAGE_KEY); if (attributes) { this.lastReported = parseInt(attributes.lastReport, 10); } } } exports.TelemetrySender = TelemetrySender;