Socket
Socket
Sign inDemoInstall

@sentry/replay

Package Overview
Dependencies
Maintainers
12
Versions
230
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sentry/replay - npm Package Compare versions

Comparing version 0.2.0-0 to 0.2.0-1

361

dist/index.es.js
import * as Sentry from '@sentry/browser';
import { isDebugBuild, logger, uuid4 } from '@sentry/utils';
import { record } from 'rrweb';
import { logger as logger$1, isDebugBuild } from '@sentry/utils';

@@ -161,3 +161,145 @@ /*! *****************************************************************************

var VISIBILITY_CHANGE_TIMEOUT = 5000;
/**
* Given an initial timestamp and an expiry duration, checks to see if current
* time should be considered as expired.
*/
function isExpired(initialTime, expiry, targetTime) {
if (targetTime === void 0) { targetTime = +new Date(); }
// Always expired if < 0
if (expiry < 0) {
return true;
}
// Never expires if == 0
if (expiry === 0) {
return false;
}
return initialTime + expiry <= targetTime;
}
/**
* Checks to see if session is expired
*/
function isSessionExpired(session, expiry, targetTime) {
if (targetTime === void 0) { targetTime = +new Date(); }
return isExpired(session.lastActivity, expiry, targetTime);
}
function wrapLogger(logFn) {
return function wrappedLog() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (!isDebugBuild()) {
return;
}
return logFn.call.apply(logFn, __spreadArray([logger$1, '[Replay]'], args, false));
};
}
var logger = __assign(__assign({}, logger$1), { error: wrapLogger(logger$1.error), warn: wrapLogger(logger$1.warn), log: wrapLogger(logger$1.log) });
var REPLAY_SESSION_KEY = 'sentryReplaySession';
function saveSession(session) {
var hasSessionStorage = 'sessionStorage' in window;
if (!hasSessionStorage) {
return;
}
try {
window.sessionStorage.setItem(REPLAY_SESSION_KEY, JSON.stringify(session));
}
catch (_a) {
// this shouldn't happen
}
}
/**
* Create a new session, which in its current implementation is a Sentry event
* that all replays will be saved to as attachments. Currently, we only expect
* one of these Sentry events per "replay session".
*/
function createSession(_a) {
var _b = _a.stickySession, stickySession = _b === void 0 ? false : _b;
var currentDate = new Date().getTime();
// Create root replay event, this is where attachments will be saved
var transaction = Sentry.getCurrentHub().startTransaction({
name: 'sentry-replay',
tags: {
isReplayRoot: 'yes',
},
});
// 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
var id = transaction.finish();
logger.log("Creating new session: ".concat(id));
var session = {
id: id,
spanId: transaction.spanId,
traceId: transaction.traceId,
started: currentDate,
lastActivity: currentDate,
};
if (stickySession) {
saveSession(session);
}
return session;
}
function fetchSession() {
var hasSessionStorage = 'sessionStorage' in window;
if (!hasSessionStorage) {
return null;
}
try {
return JSON.parse(window.sessionStorage.getItem(REPLAY_SESSION_KEY));
}
catch (_a) {
return null;
}
}
/**
* Get or create a session
*/
function getSession(_a) {
var expiry = _a.expiry, stickySession = _a.stickySession;
var session = stickySession && fetchSession();
if (session) {
// If there is a session, check if it is valid (e.g. "last activity" time should be within the "session idle time")
// TODO: We should probably set a max age on this as well
var isExpired = isSessionExpired(session, expiry);
if (!isExpired) {
logger.log("Using existing session: ".concat(session.id));
return session;
}
else {
logger.log("Session has expired");
}
// Otherwise continue to create a new session
}
var newSession = createSession({ stickySession: stickySession });
return newSession;
}
function updateSessionActivity(_a) {
var stickySession = _a.stickySession;
// Nothing to do if there are no sticky sessions
if (!stickySession) {
return;
}
var existingSession = fetchSession();
// If user manually deleted from session storage, create a new session
if (!existingSession) {
// TBD: There was an issue here where sessions weren't saving and this
// caused lots of transactions to be created
return createSession({ stickySession: stickySession });
}
var newSession = __assign(__assign({}, existingSession), { lastActivity: new Date().getTime() });
saveSession(newSession);
return newSession;
}
var VISIBILITY_CHANGE_TIMEOUT = 60000; // 1 minute
var SESSION_IDLE_DURATION = 900000; // 15 minutes
var SentryReplay = /** @class */ (function () {

@@ -167,3 +309,5 @@ function SentryReplay(_a) {

var _this = this;
var _b = _a.idleTimeout, idleTimeout = _b === void 0 ? 15000 : _b, _c = _a.rrwebConfig, _d = _c === void 0 ? {} : _c, _e = _d.maskAllInputs, maskAllInputs = _e === void 0 ? true : _e, rrwebRecordOptions = __rest(_d, ["maskAllInputs"]);
var _b = _a.uploadMinDelay, uploadMinDelay = _b === void 0 ? 5000 : _b, _c = _a.uploadMaxDelay, uploadMaxDelay = _c === void 0 ? 15000 : _c, _d = _a.stickySession, stickySession = _d === void 0 ? false : _d, // TBD: Making this opt-in for now
_e = _a.rrwebConfig, // TBD: Making this opt-in for now
_f = _e === void 0 ? {} : _e, _g = _f.maskAllInputs, maskAllInputs = _g === void 0 ? true : _g, rrwebRecordOptions = __rest(_f, ["maskAllInputs"]);
/**

@@ -178,2 +322,7 @@ * @inheritDoc

this.performanceEvents = [];
/**
* The timestamp of the first event since the last flush.
* This is used to determine if the maximum allowed time has passed before we should flush events again.
*/
this.initialEventTimestampSinceFlush = null;
this.performanceObserver = null;

@@ -192,54 +341,36 @@ /**

this.handleVisibilityChange = function () {
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
//
// TBD if this is the behavior we want
_this.isDebug &&
logger.log('[Replay] document has become active, creating new "session"');
_this.triggerNewSession();
var isExpired = isSessionExpired(_this.session, VISIBILITY_CHANGE_TIMEOUT);
if (isExpired) {
if (document.visibilityState === 'visible') {
// If the user has come back to the page within VISIBILITY_CHANGE_TIMEOUT
// ms, we will re-use the existing session, otherwise create a new
// session
logger.log('Document has become active, but session has expired');
_this.triggerFullSnapshot();
}
// We definitely want to return if visibilityState is "visible", and I
// think we also want to do the same when "hidden", as there shouldn't be
// any action to take if user has gone idle for a long period and then
// comes back to hide the tab. We don't trigger a full snapshot because
// we don't want to start a new session as they immediately have hidden
// the tab.
return;
}
// Send replay when the page/tab becomes hidden
_this.finishReplayEvent();
// VISIBILITY_CHANGE_TIMEOUT gives the user buffer room to come back to the
// page before we create a new session.
_this.visibilityChangeTimer = window.setTimeout(function () {
_this.visibilityChangeTimer = null;
}, VISIBILITY_CHANGE_TIMEOUT);
// Otherwise if session is not expired...
// Update with current timestamp as the last session activity
// Only updating session on visibility change to be conservative about
// writing to session storage. This could be changed in the future.
updateSessionActivity({
stickySession: _this.options.stickySession,
});
// Send replay when the page/tab becomes hidden. There is no reason to send
// replay if it becomes visible, since no actions we care about were done
// while it was hidden
if (document.visibilityState !== 'visible') {
_this.finishReplayEvent();
}
};
this.rrwebRecordOptions = __assign({ maskAllInputs: maskAllInputs }, rrwebRecordOptions);
// Creates a new replay ID everytime we initialize the plugin (e.g. on every pageload).
// TBD on behavior here (e.g. should this be saved to localStorage/cookies)
this.replayId = uuid4();
this.options = { uploadMinDelay: uploadMinDelay, uploadMaxDelay: uploadMaxDelay, stickySession: stickySession };
this.events = [];
record(__assign(__assign({}, this.rrwebRecordOptions), { emit: function (event, isCheckout) {
// "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) {
console.log('$$$$$ IS CHECKOUT');
_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 () {
_this.isDebug &&
logger.log('[Replay] rrweb timeout hit, finishing replay event');
_this.finishReplayEvent();
}, idleTimeout);
} }));
this.addListeners();
}

@@ -250,9 +381,2 @@ SentryReplay.attachmentUrlFromDsn = function (dsn, eventId) {

};
Object.defineProperty(SentryReplay.prototype, "isDebug", {
get: function () {
return isDebugBuild();
},
enumerable: false,
configurable: true
});
Object.defineProperty(SentryReplay.prototype, "instance", {

@@ -270,13 +394,71 @@ /**

var _this = this;
this.loadSession({ expiry: SESSION_IDLE_DURATION });
// If there is no session, then something bad has happened - can't continue
if (!this.session) {
throw new Error('Invalid session');
}
// Tag all (non replay) events that get sent to Sentry with the current
// replay ID so that we can reference them later in the UI
Sentry.addGlobalEventProcessor(function (event) {
event.tags = __assign(__assign({}, event.tags), { replayId: _this.replayId });
event.tags = __assign(__assign({}, event.tags), { replayId: _this.session.id });
return event;
});
record(__assign(__assign({}, this.rrwebRecordOptions), { emit: function (event, isCheckout) {
// We want to batch uploads of replay events. Save events only if
// `<uploadMinDelay>` milliseconds have elapsed since the last event
// *OR* if `<uploadMaxDelay>` milliseconds have elapsed.
var now = new Date().getTime();
// Timestamp of the first replay event since the last flush, this gets
// reset when we finish the replay event
if (!_this.initialEventTimestampSinceFlush) {
_this.initialEventTimestampSinceFlush = now;
}
var uploadMaxDelayExceeded = isExpired(_this.initialEventTimestampSinceFlush, _this.options.uploadMaxDelay, now);
// Do not finish the replay event if we receive a new replay event
// unless `<uploadMaxDelay>` ms have elapsed since the last time we
// finished the replay
if (_this.timeout && !uploadMaxDelayExceeded) {
window.clearTimeout(_this.timeout);
}
// We need to clear existing events on a checkout, otherwise they are
// incremental event updates and should be appended
if (isCheckout) {
_this.events = [event];
}
else {
_this.events.push(event);
}
// This event type is a fullsnapshot, we should save immediately when this occurs
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
if (event.type === 2) {
// A fullsnapshot happens on initial load and if we need to start a
// new replay due to idle timeout. In the later case we will need to
// create a new session before finishing the replay.
_this.loadSession({ expiry: SESSION_IDLE_DURATION });
_this.finishReplayEvent();
return;
}
// Set timer to finish replay event and send replay attachment to
// Sentry. Will be cancelled if an event happens before `uploadMinDelay`
// elapses.
_this.timeout = window.setTimeout(function () {
logger.log('rrweb timeout hit, finishing replay event');
_this.finishReplayEvent();
}, _this.options.uploadMinDelay);
} }));
this.addListeners();
// XXX: this needs to be in `setupOnce` vs `constructor`, otherwise SDK is
// not fully initialized and the event will not get properly sent to Sentry
this.createRootEvent();
this.createReplayEvent();
};
/**
* Loads a session from storage, or creates a new one
*/
SentryReplay.prototype.loadSession = function (_a) {
var expiry = _a.expiry;
this.session = getSession({
expiry: expiry,
stickySession: this.options.stickySession,
});
};
SentryReplay.prototype.addListeners = function () {

@@ -293,33 +475,10 @@ document.addEventListener('visibilitychange', this.handleVisibilityChange);

/**
* Creates a new replay "session". This will create a new Sentry event and
* then trigger rrweb to take a full snapshot.
* Trigger rrweb to take a full snapshot which will cause this plugin to
* create a new Replay event.
*/
SentryReplay.prototype.triggerNewSession = function () {
this.isDebug && logger.log('[Replay] taking full rrweb snapshot');
SentryReplay.prototype.triggerFullSnapshot = function () {
logger.log('Taking full rrweb snapshot');
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 () {
// TODO: Figure out if we need to do this, when this gets called from `setupOnce`, `this.instance` is still undefined.
// if (!this.instance) return;
this.isDebug && logger.log("[Replay] creating root replay event");
// Create a transaction to attach event to
var transaction = Sentry.getCurrentHub().startTransaction({
name: 'sentry-replay',
tags: {
hasReplay: 'yes',
replayId: this.replayId,
},
});
// 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
this.eventId = transaction.finish();
return this.eventId;
};
/**
* This is our pseudo replay event disguised as a transaction. It will be

@@ -331,8 +490,9 @@ * used to store performance entries and breadcrumbs for every incremental

var _this = this;
console.log('createReplayEvent rootReplayId', this.eventId);
logger.log('CreateReplayEvent rootReplayId', this.session.id);
this.replayEvent = Sentry.startTransaction({
name: 'sentry-replay-event',
parentSpanId: this.session.spanId,
traceId: this.session.traceId,
tags: {
rootReplayId: this.eventId,
replayId: this.replayId,
replayId: this.session.id,
},

@@ -376,8 +536,18 @@ });

var _a;
var eventId = this.eventId || this.createRootEvent();
if (!eventId) {
console.error('[Sentry]: No transaction, no replay');
// Ensure that our existing session has not expired
var isExpired = isSessionExpired(this.session, SESSION_IDLE_DURATION);
if (isExpired) {
// TBD: If it is expired, we do not send any events...we could send to
// the expired session, but not sure if that's great
console.error(new Error('Attempting to finish replay event after session expired.'));
return;
}
this.sendReplay(eventId);
if (!this.session.id) {
console.error(new Error('[Sentry]: No transaction, no replay'));
return;
}
this.sendReplay(this.session.id);
this.initialEventTimestampSinceFlush = null;
// TBD: Alternatively we could update this after every rrweb event
this.session.lastActivity = new Date().getTime();
// include performance entries

@@ -411,4 +581,3 @@ this.addPerformanceEntries();

if (this.hasSendBeacon() && stringifiedPayload.length <= 65536) {
this.isDebug &&
logger.log("[Replay] uploading attachment via sendBeacon()");
logger.log("uploading attachment via sendBeacon()");
window.navigator.sendBeacon(endpoint, formData);

@@ -420,3 +589,3 @@ return [2 /*return*/];

_a.trys.push([1, 3, , 4]);
this.isDebug && logger.log("[Replay] uploading attachment via fetch()");
logger.log("uploading attachment via fetch()");
// Otherwise use `fetch`, which *WILL* get cancelled on page reloads/unloads

@@ -423,0 +592,0 @@ return [4 /*yield*/, fetch(endpoint, {

@@ -6,4 +6,4 @@ 'use strict';

var Sentry = require('@sentry/browser');
var rrweb = require('rrweb');
var utils = require('@sentry/utils');
var rrweb = require('rrweb');

@@ -186,3 +186,145 @@ function _interopNamespace(e) {

var VISIBILITY_CHANGE_TIMEOUT = 5000;
/**
* Given an initial timestamp and an expiry duration, checks to see if current
* time should be considered as expired.
*/
function isExpired(initialTime, expiry, targetTime) {
if (targetTime === void 0) { targetTime = +new Date(); }
// Always expired if < 0
if (expiry < 0) {
return true;
}
// Never expires if == 0
if (expiry === 0) {
return false;
}
return initialTime + expiry <= targetTime;
}
/**
* Checks to see if session is expired
*/
function isSessionExpired(session, expiry, targetTime) {
if (targetTime === void 0) { targetTime = +new Date(); }
return isExpired(session.lastActivity, expiry, targetTime);
}
function wrapLogger(logFn) {
return function wrappedLog() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (!utils.isDebugBuild()) {
return;
}
return logFn.call.apply(logFn, __spreadArray([utils.logger, '[Replay]'], args, false));
};
}
var logger = __assign(__assign({}, utils.logger), { error: wrapLogger(utils.logger.error), warn: wrapLogger(utils.logger.warn), log: wrapLogger(utils.logger.log) });
var REPLAY_SESSION_KEY = 'sentryReplaySession';
function saveSession(session) {
var hasSessionStorage = 'sessionStorage' in window;
if (!hasSessionStorage) {
return;
}
try {
window.sessionStorage.setItem(REPLAY_SESSION_KEY, JSON.stringify(session));
}
catch (_a) {
// this shouldn't happen
}
}
/**
* Create a new session, which in its current implementation is a Sentry event
* that all replays will be saved to as attachments. Currently, we only expect
* one of these Sentry events per "replay session".
*/
function createSession(_a) {
var _b = _a.stickySession, stickySession = _b === void 0 ? false : _b;
var currentDate = new Date().getTime();
// Create root replay event, this is where attachments will be saved
var transaction = Sentry__namespace.getCurrentHub().startTransaction({
name: 'sentry-replay',
tags: {
isReplayRoot: 'yes',
},
});
// 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
var id = transaction.finish();
logger.log("Creating new session: ".concat(id));
var session = {
id: id,
spanId: transaction.spanId,
traceId: transaction.traceId,
started: currentDate,
lastActivity: currentDate,
};
if (stickySession) {
saveSession(session);
}
return session;
}
function fetchSession() {
var hasSessionStorage = 'sessionStorage' in window;
if (!hasSessionStorage) {
return null;
}
try {
return JSON.parse(window.sessionStorage.getItem(REPLAY_SESSION_KEY));
}
catch (_a) {
return null;
}
}
/**
* Get or create a session
*/
function getSession(_a) {
var expiry = _a.expiry, stickySession = _a.stickySession;
var session = stickySession && fetchSession();
if (session) {
// If there is a session, check if it is valid (e.g. "last activity" time should be within the "session idle time")
// TODO: We should probably set a max age on this as well
var isExpired = isSessionExpired(session, expiry);
if (!isExpired) {
logger.log("Using existing session: ".concat(session.id));
return session;
}
else {
logger.log("Session has expired");
}
// Otherwise continue to create a new session
}
var newSession = createSession({ stickySession: stickySession });
return newSession;
}
function updateSessionActivity(_a) {
var stickySession = _a.stickySession;
// Nothing to do if there are no sticky sessions
if (!stickySession) {
return;
}
var existingSession = fetchSession();
// If user manually deleted from session storage, create a new session
if (!existingSession) {
// TBD: There was an issue here where sessions weren't saving and this
// caused lots of transactions to be created
return createSession({ stickySession: stickySession });
}
var newSession = __assign(__assign({}, existingSession), { lastActivity: new Date().getTime() });
saveSession(newSession);
return newSession;
}
var VISIBILITY_CHANGE_TIMEOUT = 60000; // 1 minute
var SESSION_IDLE_DURATION = 900000; // 15 minutes
var SentryReplay = /** @class */ (function () {

@@ -192,3 +334,5 @@ function SentryReplay(_a) {

var _this = this;
var _b = _a.idleTimeout, idleTimeout = _b === void 0 ? 15000 : _b, _c = _a.rrwebConfig, _d = _c === void 0 ? {} : _c, _e = _d.maskAllInputs, maskAllInputs = _e === void 0 ? true : _e, rrwebRecordOptions = __rest(_d, ["maskAllInputs"]);
var _b = _a.uploadMinDelay, uploadMinDelay = _b === void 0 ? 5000 : _b, _c = _a.uploadMaxDelay, uploadMaxDelay = _c === void 0 ? 15000 : _c, _d = _a.stickySession, stickySession = _d === void 0 ? false : _d, // TBD: Making this opt-in for now
_e = _a.rrwebConfig, // TBD: Making this opt-in for now
_f = _e === void 0 ? {} : _e, _g = _f.maskAllInputs, maskAllInputs = _g === void 0 ? true : _g, rrwebRecordOptions = __rest(_f, ["maskAllInputs"]);
/**

@@ -203,2 +347,7 @@ * @inheritDoc

this.performanceEvents = [];
/**
* The timestamp of the first event since the last flush.
* This is used to determine if the maximum allowed time has passed before we should flush events again.
*/
this.initialEventTimestampSinceFlush = null;
this.performanceObserver = null;

@@ -217,54 +366,36 @@ /**

this.handleVisibilityChange = function () {
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
//
// TBD if this is the behavior we want
_this.isDebug &&
utils.logger.log('[Replay] document has become active, creating new "session"');
_this.triggerNewSession();
var isExpired = isSessionExpired(_this.session, VISIBILITY_CHANGE_TIMEOUT);
if (isExpired) {
if (document.visibilityState === 'visible') {
// If the user has come back to the page within VISIBILITY_CHANGE_TIMEOUT
// ms, we will re-use the existing session, otherwise create a new
// session
logger.log('Document has become active, but session has expired');
_this.triggerFullSnapshot();
}
// We definitely want to return if visibilityState is "visible", and I
// think we also want to do the same when "hidden", as there shouldn't be
// any action to take if user has gone idle for a long period and then
// comes back to hide the tab. We don't trigger a full snapshot because
// we don't want to start a new session as they immediately have hidden
// the tab.
return;
}
// Send replay when the page/tab becomes hidden
_this.finishReplayEvent();
// VISIBILITY_CHANGE_TIMEOUT gives the user buffer room to come back to the
// page before we create a new session.
_this.visibilityChangeTimer = window.setTimeout(function () {
_this.visibilityChangeTimer = null;
}, VISIBILITY_CHANGE_TIMEOUT);
// Otherwise if session is not expired...
// Update with current timestamp as the last session activity
// Only updating session on visibility change to be conservative about
// writing to session storage. This could be changed in the future.
updateSessionActivity({
stickySession: _this.options.stickySession,
});
// Send replay when the page/tab becomes hidden. There is no reason to send
// replay if it becomes visible, since no actions we care about were done
// while it was hidden
if (document.visibilityState !== 'visible') {
_this.finishReplayEvent();
}
};
this.rrwebRecordOptions = __assign({ maskAllInputs: maskAllInputs }, rrwebRecordOptions);
// Creates a new replay ID everytime we initialize the plugin (e.g. on every pageload).
// TBD on behavior here (e.g. should this be saved to localStorage/cookies)
this.replayId = utils.uuid4();
this.options = { uploadMinDelay: uploadMinDelay, uploadMaxDelay: uploadMaxDelay, stickySession: stickySession };
this.events = [];
rrweb.record(__assign(__assign({}, this.rrwebRecordOptions), { emit: function (event, isCheckout) {
// "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) {
console.log('$$$$$ IS CHECKOUT');
_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 () {
_this.isDebug &&
utils.logger.log('[Replay] rrweb timeout hit, finishing replay event');
_this.finishReplayEvent();
}, idleTimeout);
} }));
this.addListeners();
}

@@ -275,9 +406,2 @@ SentryReplay.attachmentUrlFromDsn = function (dsn, eventId) {

};
Object.defineProperty(SentryReplay.prototype, "isDebug", {
get: function () {
return utils.isDebugBuild();
},
enumerable: false,
configurable: true
});
Object.defineProperty(SentryReplay.prototype, "instance", {

@@ -295,13 +419,71 @@ /**

var _this = this;
this.loadSession({ expiry: SESSION_IDLE_DURATION });
// If there is no session, then something bad has happened - can't continue
if (!this.session) {
throw new Error('Invalid session');
}
// Tag all (non replay) events that get sent to Sentry with the current
// replay ID so that we can reference them later in the UI
Sentry__namespace.addGlobalEventProcessor(function (event) {
event.tags = __assign(__assign({}, event.tags), { replayId: _this.replayId });
event.tags = __assign(__assign({}, event.tags), { replayId: _this.session.id });
return event;
});
rrweb.record(__assign(__assign({}, this.rrwebRecordOptions), { emit: function (event, isCheckout) {
// We want to batch uploads of replay events. Save events only if
// `<uploadMinDelay>` milliseconds have elapsed since the last event
// *OR* if `<uploadMaxDelay>` milliseconds have elapsed.
var now = new Date().getTime();
// Timestamp of the first replay event since the last flush, this gets
// reset when we finish the replay event
if (!_this.initialEventTimestampSinceFlush) {
_this.initialEventTimestampSinceFlush = now;
}
var uploadMaxDelayExceeded = isExpired(_this.initialEventTimestampSinceFlush, _this.options.uploadMaxDelay, now);
// Do not finish the replay event if we receive a new replay event
// unless `<uploadMaxDelay>` ms have elapsed since the last time we
// finished the replay
if (_this.timeout && !uploadMaxDelayExceeded) {
window.clearTimeout(_this.timeout);
}
// We need to clear existing events on a checkout, otherwise they are
// incremental event updates and should be appended
if (isCheckout) {
_this.events = [event];
}
else {
_this.events.push(event);
}
// This event type is a fullsnapshot, we should save immediately when this occurs
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
if (event.type === 2) {
// A fullsnapshot happens on initial load and if we need to start a
// new replay due to idle timeout. In the later case we will need to
// create a new session before finishing the replay.
_this.loadSession({ expiry: SESSION_IDLE_DURATION });
_this.finishReplayEvent();
return;
}
// Set timer to finish replay event and send replay attachment to
// Sentry. Will be cancelled if an event happens before `uploadMinDelay`
// elapses.
_this.timeout = window.setTimeout(function () {
logger.log('rrweb timeout hit, finishing replay event');
_this.finishReplayEvent();
}, _this.options.uploadMinDelay);
} }));
this.addListeners();
// XXX: this needs to be in `setupOnce` vs `constructor`, otherwise SDK is
// not fully initialized and the event will not get properly sent to Sentry
this.createRootEvent();
this.createReplayEvent();
};
/**
* Loads a session from storage, or creates a new one
*/
SentryReplay.prototype.loadSession = function (_a) {
var expiry = _a.expiry;
this.session = getSession({
expiry: expiry,
stickySession: this.options.stickySession,
});
};
SentryReplay.prototype.addListeners = function () {

@@ -318,33 +500,10 @@ document.addEventListener('visibilitychange', this.handleVisibilityChange);

/**
* Creates a new replay "session". This will create a new Sentry event and
* then trigger rrweb to take a full snapshot.
* Trigger rrweb to take a full snapshot which will cause this plugin to
* create a new Replay event.
*/
SentryReplay.prototype.triggerNewSession = function () {
this.isDebug && utils.logger.log('[Replay] taking full rrweb snapshot');
SentryReplay.prototype.triggerFullSnapshot = function () {
logger.log('Taking full rrweb snapshot');
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 () {
// TODO: Figure out if we need to do this, when this gets called from `setupOnce`, `this.instance` is still undefined.
// if (!this.instance) return;
this.isDebug && utils.logger.log("[Replay] creating root replay event");
// Create a transaction to attach event to
var transaction = Sentry__namespace.getCurrentHub().startTransaction({
name: 'sentry-replay',
tags: {
hasReplay: 'yes',
replayId: this.replayId,
},
});
// 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
this.eventId = transaction.finish();
return this.eventId;
};
/**
* This is our pseudo replay event disguised as a transaction. It will be

@@ -356,8 +515,9 @@ * used to store performance entries and breadcrumbs for every incremental

var _this = this;
console.log('createReplayEvent rootReplayId', this.eventId);
logger.log('CreateReplayEvent rootReplayId', this.session.id);
this.replayEvent = Sentry__namespace.startTransaction({
name: 'sentry-replay-event',
parentSpanId: this.session.spanId,
traceId: this.session.traceId,
tags: {
rootReplayId: this.eventId,
replayId: this.replayId,
replayId: this.session.id,
},

@@ -401,8 +561,18 @@ });

var _a;
var eventId = this.eventId || this.createRootEvent();
if (!eventId) {
console.error('[Sentry]: No transaction, no replay');
// Ensure that our existing session has not expired
var isExpired = isSessionExpired(this.session, SESSION_IDLE_DURATION);
if (isExpired) {
// TBD: If it is expired, we do not send any events...we could send to
// the expired session, but not sure if that's great
console.error(new Error('Attempting to finish replay event after session expired.'));
return;
}
this.sendReplay(eventId);
if (!this.session.id) {
console.error(new Error('[Sentry]: No transaction, no replay'));
return;
}
this.sendReplay(this.session.id);
this.initialEventTimestampSinceFlush = null;
// TBD: Alternatively we could update this after every rrweb event
this.session.lastActivity = new Date().getTime();
// include performance entries

@@ -436,4 +606,3 @@ this.addPerformanceEntries();

if (this.hasSendBeacon() && stringifiedPayload.length <= 65536) {
this.isDebug &&
utils.logger.log("[Replay] uploading attachment via sendBeacon()");
logger.log("uploading attachment via sendBeacon()");
window.navigator.sendBeacon(endpoint, formData);

@@ -445,3 +614,3 @@ return [2 /*return*/];

_a.trys.push([1, 3, , 4]);
this.isDebug && utils.logger.log("[Replay] uploading attachment via fetch()");
logger.log("uploading attachment via fetch()");
// Otherwise use `fetch`, which *WILL* get cancelled on page reloads/unloads

@@ -448,0 +617,0 @@ return [4 /*yield*/, fetch(endpoint, {

{
"name": "@sentry/replay",
"version": "0.2.0-0",
"version": "0.2.0-1",
"description": "User replays for Sentry",

@@ -5,0 +5,0 @@ "main": "dist/index.js",

@@ -16,3 +16,3 @@ # sentry-replay

```shell
npm install --save @sentry/replay rrweb
npm install --save @sentry/browser @sentry/replay @sentry/tracing rrweb
```

@@ -23,3 +23,3 @@

```shell
yarn add @sentry/replay rrweb
yarn add @sentry/replay @sentry/browser @sentry/replay @sentry/tracing rrweb
```

@@ -29,24 +29,10 @@

To set up the integration add the following to your Sentry initialization:
To set up the integration add the following to your Sentry initialization. Several options are supported and passable via the integration constructor.
See the rrweb documentation for advice on configuring these values.
```javascript
import * as Sentry from '@sentry/browser';
import { SentryReplay } from '@sentry/replay';
Sentry.init({
dsn: '__DSN__',
integrations: [
new SentryReplay({
// ...options
}),
],
// ...
});
```
Several options are supported and passable via the integration constructor:
```javascript
import * as Sentry from '@sentry/browser';
import { SentryReplay } from '@sentry/replay';
import '@sentry/tracing';

@@ -57,8 +43,6 @@ Sentry.init({

new SentryReplay({
// default is empty
checkoutEveryNth: 100,
// default is 5 minutes
checkoutEveryNms: 15 * 60 * 1000,
// on by default
maskAllInputs: false,
stickySession: true, // Default is false
rrwebOptions: {
maskAllInputs: false, // Default is true
},
}),

@@ -70,2 +54,1 @@ ],

See the rrweb documentation for advice on configuring these values.

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc