jest-sentry-environment
Version:
Sentry instrumentation for jest
346 lines (329 loc) • 15.9 kB
JavaScript
;
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