UNPKG

jest-sentry-environment

Version:
346 lines (329 loc) 15.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var Sentry = require('@sentry/browser'); var utils = require('@sentry/utils'); var rrweb = require('rrweb'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var Sentry__namespace = /*#__PURE__*/_interopNamespace(Sentry); /*! ***************************************************************************** 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. ***************************************************************************** */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; 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()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } const fetchNode = require('./node-ponyfill'); const fetch$1 = fetchNode.fetch.bind({}); fetch$1.polyfill = true; if (!global.fetch) { global.fetch = fetch$1; global.Response = fetchNode.Response; global.Headers = fetchNode.Headers; global.Request = fetchNode.Request; } var VISIBILITY_CHANGE_TIMEOUT = 5000; var SentryReplay = /** @class */ (function () { function SentryReplay(_a) { if (_a === void 0) { _a = {}; } var _this = this; var _b = _a.idleTimeout, idleTimeout = _b === void 0 ? 15000 : _b, _c = _a.rrwebConfig, _d = _c === void 0 ? {} : _c, _e = _d.checkoutEveryNms, checkoutEveryNms = _e === void 0 ? 5 * 60 * 1000 : _e, // default checkout time of 5 minutes _f = _d.maskAllInputs, // default checkout time of 5 minutes maskAllInputs = _f === void 0 ? true : _f, rrwebRecordOptions = __rest(_d, ["checkoutEveryNms", "maskAllInputs"]); this.name = SentryReplay.id; /** * Buffer of rrweb events that will be serialized as JSON and saved as an attachment to a Sentry event */ this.events = []; this.handleVisibilityChange = function () { console.log('visibility change: ', document.visibilityState, _this.visibilityChangeTimer); if (document.visibilityState === 'visible' && _this.visibilityChangeTimer === null) { // Page has become active/visible again after `VISIBILITY_CHANGE_TIMEOUT` // ms have elapsed, which means we will consider this a new session _this.triggerNewSession(); return; } // Send replay when the page/tab becomes hidden _this.finishReplayEvent(); // VISIBILITY_CHANGE_TIMEOUT gives the user buffer room it come back to the // page before we create a new session. _this.visibilityChangeTimer = window.setTimeout(function () { _this.visibilityChangeTimer = null; }, VISIBILITY_CHANGE_TIMEOUT); }; this.rrwebRecordOptions = __assign({ checkoutEveryNms: checkoutEveryNms, maskAllInputs: maskAllInputs }, rrwebRecordOptions); this.replayId = utils.uuid4(); this.createReplayEvent(); this.events = []; rrweb.record(__assign(__assign({}, this.rrwebRecordOptions), { emit: function (event, isCheckout) { // console.log('record', { isCheckout, event }); // "debounce" by `idleTimeout`, how often we save replay events i.e. we // will save events only if 15 seconds have elapsed since the last // event // // TODO: We probably want to have a hard timeout where we save // so that it does not grow infinitely and we never have a replay // saved if (_this.timeout) { window.clearTimeout(_this.timeout); } // Always create a new Sentry event on checkouts and clear existing rrweb events if (isCheckout) { _this.createRootEvent(); _this.events = [event]; } else { _this.events.push(event); } // Set timer to send attachment to Sentry, will be cancelled if an // event happens before `idleTimeout` elapses _this.timeout = window.setTimeout(function () { console.log('idle timeout hit'); _this.finishReplayEvent(); }, idleTimeout); // TODO: } })); this.addListeners(); } SentryReplay.attachmentUrlFromDsn = function (dsn, eventId) { var host = dsn.host, path = dsn.path, projectId = dsn.projectId, port = dsn.port, protocol = dsn.protocol, user = dsn.user; return "".concat(protocol, "://").concat(host).concat(port !== '' ? ":".concat(port) : '').concat(path !== '' ? "/".concat(path) : '', "/api/").concat(projectId, "/events/").concat(eventId, "/attachments/?sentry_key=").concat(user, "&sentry_version=7&sentry_client=replay"); }; SentryReplay.prototype.addListeners = function () { document.addEventListener('visibilitychange', this.handleVisibilityChange); }; Object.defineProperty(SentryReplay.prototype, "instance", { /** * Get integration's instance on the current hub */ get: function () { return Sentry__namespace.getCurrentHub().getIntegration(SentryReplay); }, enumerable: false, configurable: true }); /** * Creates a new replay "session". This will create a new Sentry event and * then trigger rrweb to take a full snapshot. */ SentryReplay.prototype.triggerNewSession = function () { console.log('triggering new session'); rrweb.record.takeFullSnapshot(true); }; /** * Creates the Sentry event that all replays will be saved to as attachments. * Currently, we only expect one of these per "replay session" (which is not * explicitly defined yet). */ SentryReplay.prototype.createRootEvent = function () { console.log('create root event'); var self = this.instance; if (!self) return; // TODO: If there's a transaction active, we should attach to that? // --> actually that won't work as-is because we need an event id to upload an attachment // const transaction = Sentry.getCurrentHub().getScope().getTransaction(); // Otherwise create a transaction to attach event to var transaction = Sentry__namespace.getCurrentHub().startTransaction({ name: 'sentry-replay', tags: { hasReplay: true, replayId: this.replayId, }, }); console.log(transaction); // We have to finish the transaction to get an event ID to be able to // upload an attachment for that event // @ts-expect-error This returns an eventId (string), but is not typed as such self.eventId = transaction.finish(); return self.eventId; }; SentryReplay.prototype.createReplayEvent = function () { this.replayEvent = Sentry__namespace.startTransaction({ name: 'sentry-replay-event', tags: { rootReplayId: this.eventId, replayId: this.replayId, }, }); return this.replayEvent; }; SentryReplay.prototype.finishReplayEvent = function () { var _a; console.log('finish replay event'); var self = this.instance; if (!self) return; var eventId = self.eventId || this.createRootEvent(); if (!eventId) { console.error('[Sentry]: No transaction, no replay'); return; } this.sendReplay(eventId); // Close out existing replay event and create a new one (_a = this.replayEvent) === null || _a === void 0 ? void 0 : _a.finish(); this.createReplayEvent(); }; SentryReplay.prototype.hasSendBeacon = function () { return 'navigator' in window && 'sendBeacon' in window.navigator; }; SentryReplay.prototype.request = function (endpoint, events) { return __awaiter(this, void 0, void 0, function () { var stringifiedPayload, formData, ex_1; return __generator(this, function (_a) { switch (_a.label) { case 0: stringifiedPayload = JSON.stringify({ events: events }); formData = new FormData(); formData.append('rrweb', new Blob([stringifiedPayload], { type: 'application/json', }), "rrweb-".concat(new Date().getTime(), ".json")); // If sendBeacon is supported and payload is smol enough... if (this.hasSendBeacon() && stringifiedPayload.length <= 65536) { console.log('sending via beacon'); window.navigator.sendBeacon(endpoint, formData); return [2 /*return*/]; } _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); console.log(process.version); console.log('sending via fetch', typeof fetch); return [4 /*yield*/, fetch(endpoint, { method: 'POST', body: formData, })]; case 2: _a.sent(); return [3 /*break*/, 4]; case 3: ex_1 = _a.sent(); // we have to catch this otherwise it throws an infinite loop in Sentry console.error(ex_1); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; SentryReplay.prototype.sendReplay = function (eventId) { return __awaiter(this, void 0, void 0, function () { var self, client, endpoint, ex_2; return __generator(this, function (_a) { switch (_a.label) { case 0: self = this.instance; if (!self) return [2 /*return*/]; _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); // short circuit if theres no events to replay if (!self.events.length) return [2 /*return*/]; client = Sentry__namespace.getCurrentHub().getClient(); endpoint = SentryReplay.attachmentUrlFromDsn(client.getDsn(), eventId); return [4 /*yield*/, this.request(endpoint, self.events)]; case 2: _a.sent(); self.events = []; return [2 /*return*/, true]; case 3: ex_2 = _a.sent(); console.error(ex_2); return [2 /*return*/, false]; case 4: return [2 /*return*/]; } }); }); }; SentryReplay.prototype.setupOnce = function () { var _this = this; Sentry__namespace.addGlobalEventProcessor(function (event) { event.tags = __assign(__assign({}, event.tags), { replayId: _this.replayId }); return event; }); }; SentryReplay.id = 'SentryReplay'; return SentryReplay; }()); exports.SentryReplay = SentryReplay; //# sourceMappingURL=index.js.map