@sentry/replay 7.59.0-beta.0 → 7.59.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/index.js +474 -337
- package/cjs/index.js.map +1 -1
- package/esm/index.js +474 -337
- package/esm/index.js.map +1 -1
- package/package.json +5 -5
- package/types/constants.d.ts +1 -0
- package/types/constants.d.ts.map +1 -1
- package/types/coreHandlers/handleHistory.d.ts.map +1 -1
- package/types/replay.d.ts +36 -17
- package/types/replay.d.ts.map +1 -1
- package/types/session/Session.d.ts.map +1 -1
- package/types/session/createSession.d.ts +2 -5
- package/types/session/createSession.d.ts.map +1 -1
- package/types/session/getSession.d.ts +17 -0
- package/types/session/getSession.d.ts.map +1 -0
- package/types/types/replay.d.ts +24 -1
- package/types/types/replay.d.ts.map +1 -1
- package/types/util/addEvent.d.ts.map +1 -1
- package/types/util/handleRecordingEmit.d.ts.map +1 -1
- package/types/util/hasSessionStorage.d.ts.map +1 -1
- package/types/util/isSessionExpired.d.ts +6 -0
- package/types/util/isSessionExpired.d.ts.map +1 -0
- package/types/session/checkSessionState.d.ts +0 -10
- package/types/session/checkSessionState.d.ts.map +0 -1
- package/types/session/restoreOrCreateSession.d.ts +0 -8
- package/types/session/restoreOrCreateSession.d.ts.map +0 -1
- package/types/util/sampleSession.d.ts +0 -9
- package/types/util/sampleSession.d.ts.map +0 -1
package/cjs/index.js
CHANGED
|
@@ -16,6 +16,9 @@ const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';
|
|
|
16
16
|
// The idle limit for a session after which recording is paused.
|
|
17
17
|
const SESSION_IDLE_PAUSE_DURATION = 300000; // 5 minutes in ms
|
|
18
18
|
|
|
19
|
+
// The idle limit for a session after which the session expires.
|
|
20
|
+
const SESSION_IDLE_EXPIRE_DURATION = 900000; // 15 minutes in ms
|
|
21
|
+
|
|
19
22
|
// The maximum length of a session
|
|
20
23
|
const MAX_SESSION_LIFE = 3600000; // 60 minutes in ms
|
|
21
24
|
|
|
@@ -3278,26 +3281,26 @@ function addBreadcrumbEvent(replay, breadcrumb) {
|
|
|
3278
3281
|
}
|
|
3279
3282
|
|
|
3280
3283
|
if (['ui.click', 'ui.input'].includes(breadcrumb.category )) {
|
|
3281
|
-
replay.
|
|
3284
|
+
replay.triggerUserActivity();
|
|
3285
|
+
} else {
|
|
3286
|
+
replay.checkAndHandleExpiredSession();
|
|
3282
3287
|
}
|
|
3283
3288
|
|
|
3284
|
-
replay.
|
|
3285
|
-
replay.
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
},
|
|
3296
|
-
});
|
|
3297
|
-
|
|
3298
|
-
// Do not flush after console log messages
|
|
3299
|
-
return breadcrumb.category === 'console';
|
|
3289
|
+
replay.addUpdate(() => {
|
|
3290
|
+
void replay.throttledAddEvent({
|
|
3291
|
+
type: EventType.Custom,
|
|
3292
|
+
// TODO: We were converting from ms to seconds for breadcrumbs, spans,
|
|
3293
|
+
// but maybe we should just keep them as milliseconds
|
|
3294
|
+
timestamp: (breadcrumb.timestamp || 0) * 1000,
|
|
3295
|
+
data: {
|
|
3296
|
+
tag: 'breadcrumb',
|
|
3297
|
+
// normalize to max. 10 depth and 1_000 properties per object
|
|
3298
|
+
payload: utils.normalize(breadcrumb, 10, 1000),
|
|
3299
|
+
},
|
|
3300
3300
|
});
|
|
3301
|
+
|
|
3302
|
+
// Do not flush after console log messages
|
|
3303
|
+
return breadcrumb.category === 'console';
|
|
3301
3304
|
});
|
|
3302
3305
|
}
|
|
3303
3306
|
|
|
@@ -4388,69 +4391,14 @@ function createEventBuffer({ useCompression }) {
|
|
|
4388
4391
|
return new EventBufferArray();
|
|
4389
4392
|
}
|
|
4390
4393
|
|
|
4391
|
-
/**
|
|
4392
|
-
* Given an initial timestamp and an expiry duration, checks to see if current
|
|
4393
|
-
* time should be considered as expired.
|
|
4394
|
-
*/
|
|
4395
|
-
function isExpired(
|
|
4396
|
-
initialTime,
|
|
4397
|
-
expiry,
|
|
4398
|
-
targetTime = +new Date(),
|
|
4399
|
-
) {
|
|
4400
|
-
// Always expired if < 0
|
|
4401
|
-
if (initialTime === null || expiry === undefined || expiry < 0) {
|
|
4402
|
-
return true;
|
|
4403
|
-
}
|
|
4404
|
-
|
|
4405
|
-
// Never expires if == 0
|
|
4406
|
-
if (expiry === 0) {
|
|
4407
|
-
return false;
|
|
4408
|
-
}
|
|
4409
|
-
|
|
4410
|
-
return initialTime + expiry <= targetTime;
|
|
4411
|
-
}
|
|
4412
|
-
|
|
4413
|
-
/** Check the state of the session. */
|
|
4414
|
-
function checkSessionState(
|
|
4415
|
-
session,
|
|
4416
|
-
recordingMode,
|
|
4417
|
-
timeouts,
|
|
4418
|
-
callbacks
|
|
4419
|
-
|
|
4420
|
-
,
|
|
4421
|
-
) {
|
|
4422
|
-
const _isIdle = () => {
|
|
4423
|
-
return isExpired(session.lastActivity, timeouts.sessionIdlePause);
|
|
4424
|
-
};
|
|
4425
|
-
|
|
4426
|
-
const _exceedsMaxLength = () => {
|
|
4427
|
-
return isExpired(session.started, timeouts.maxSessionLife);
|
|
4428
|
-
};
|
|
4429
|
-
|
|
4430
|
-
// When buffering, we never want to expire/end/pause/restart the recording
|
|
4431
|
-
if (recordingMode === 'buffer') {
|
|
4432
|
-
callbacks.onContinue();
|
|
4433
|
-
return;
|
|
4434
|
-
}
|
|
4435
|
-
|
|
4436
|
-
if (_exceedsMaxLength()) {
|
|
4437
|
-
callbacks.onEnd();
|
|
4438
|
-
return;
|
|
4439
|
-
}
|
|
4440
|
-
|
|
4441
|
-
if (_isIdle()) {
|
|
4442
|
-
callbacks.onPause();
|
|
4443
|
-
return;
|
|
4444
|
-
}
|
|
4445
|
-
|
|
4446
|
-
callbacks.ensureResumed();
|
|
4447
|
-
|
|
4448
|
-
callbacks.onContinue();
|
|
4449
|
-
}
|
|
4450
|
-
|
|
4451
4394
|
/** If sessionStorage is available. */
|
|
4452
4395
|
function hasSessionStorage() {
|
|
4453
|
-
|
|
4396
|
+
try {
|
|
4397
|
+
// This can throw, e.g. when being accessed in a sandboxed iframe
|
|
4398
|
+
return 'sessionStorage' in WINDOW && !!WINDOW.sessionStorage;
|
|
4399
|
+
} catch (e) {
|
|
4400
|
+
return false;
|
|
4401
|
+
}
|
|
4454
4402
|
}
|
|
4455
4403
|
|
|
4456
4404
|
/**
|
|
@@ -4476,6 +4424,41 @@ function deleteSession() {
|
|
|
4476
4424
|
}
|
|
4477
4425
|
}
|
|
4478
4426
|
|
|
4427
|
+
/**
|
|
4428
|
+
* Given an initial timestamp and an expiry duration, checks to see if current
|
|
4429
|
+
* time should be considered as expired.
|
|
4430
|
+
*/
|
|
4431
|
+
function isExpired(
|
|
4432
|
+
initialTime,
|
|
4433
|
+
expiry,
|
|
4434
|
+
targetTime = +new Date(),
|
|
4435
|
+
) {
|
|
4436
|
+
// Always expired if < 0
|
|
4437
|
+
if (initialTime === null || expiry === undefined || expiry < 0) {
|
|
4438
|
+
return true;
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
// Never expires if == 0
|
|
4442
|
+
if (expiry === 0) {
|
|
4443
|
+
return false;
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
return initialTime + expiry <= targetTime;
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
/**
|
|
4450
|
+
* Checks to see if session is expired
|
|
4451
|
+
*/
|
|
4452
|
+
function isSessionExpired(session, timeouts, targetTime = +new Date()) {
|
|
4453
|
+
return (
|
|
4454
|
+
// First, check that maximum session length has not been exceeded
|
|
4455
|
+
isExpired(session.started, timeouts.maxSessionLife, targetTime) ||
|
|
4456
|
+
// check that the idle timeout has not been exceeded (i.e. user has
|
|
4457
|
+
// performed an action within the last `sessionIdleExpire` ms)
|
|
4458
|
+
isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime)
|
|
4459
|
+
);
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4479
4462
|
/**
|
|
4480
4463
|
* Given a sample rate, returns true if replay should be sampled.
|
|
4481
4464
|
*
|
|
@@ -4524,20 +4507,24 @@ function makeSession(session) {
|
|
|
4524
4507
|
lastActivity,
|
|
4525
4508
|
segmentId,
|
|
4526
4509
|
sampled,
|
|
4510
|
+
shouldRefresh: true,
|
|
4527
4511
|
};
|
|
4528
4512
|
}
|
|
4529
4513
|
|
|
4514
|
+
/**
|
|
4515
|
+
* Get the sampled status for a session based on sample rates & current sampled status.
|
|
4516
|
+
*/
|
|
4517
|
+
function getSessionSampleType(sessionSampleRate, allowBuffering) {
|
|
4518
|
+
return isSampled(sessionSampleRate) ? 'session' : allowBuffering ? 'buffer' : false;
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4530
4521
|
/**
|
|
4531
4522
|
* Create a new session, which in its current implementation is a Sentry event
|
|
4532
4523
|
* that all replays will be saved to as attachments. Currently, we only expect
|
|
4533
4524
|
* one of these Sentry events per "replay session".
|
|
4534
4525
|
*/
|
|
4535
|
-
function createSession({
|
|
4536
|
-
sampled,
|
|
4537
|
-
stickySession = false,
|
|
4538
|
-
}
|
|
4539
|
-
|
|
4540
|
-
) {
|
|
4526
|
+
function createSession({ sessionSampleRate, allowBuffering, stickySession = false }) {
|
|
4527
|
+
const sampled = getSessionSampleType(sessionSampleRate, allowBuffering);
|
|
4541
4528
|
const session = makeSession({
|
|
4542
4529
|
sampled,
|
|
4543
4530
|
});
|
|
@@ -4575,31 +4562,47 @@ function fetchSession() {
|
|
|
4575
4562
|
}
|
|
4576
4563
|
}
|
|
4577
4564
|
|
|
4578
|
-
/**
|
|
4579
|
-
|
|
4565
|
+
/**
|
|
4566
|
+
* Get or create a session
|
|
4567
|
+
*/
|
|
4568
|
+
function getSession({
|
|
4569
|
+
timeouts,
|
|
4570
|
+
currentSession,
|
|
4580
4571
|
stickySession,
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
}
|
|
4584
|
-
|
|
4585
|
-
)
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
//
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
(
|
|
4594
|
-
|
|
4572
|
+
sessionSampleRate,
|
|
4573
|
+
allowBuffering,
|
|
4574
|
+
}) {
|
|
4575
|
+
// If session exists and is passed, use it instead of always hitting session storage
|
|
4576
|
+
const session = currentSession || (stickySession && fetchSession());
|
|
4577
|
+
|
|
4578
|
+
if (session) {
|
|
4579
|
+
// If there is a session, check if it is valid (e.g. "last activity" time
|
|
4580
|
+
// should be within the "session idle time", and "session started" time is
|
|
4581
|
+
// within "max session time").
|
|
4582
|
+
const isExpired = isSessionExpired(session, timeouts);
|
|
4583
|
+
|
|
4584
|
+
if (!isExpired || (allowBuffering && session.shouldRefresh)) {
|
|
4585
|
+
return { type: 'saved', session };
|
|
4586
|
+
} else if (!session.shouldRefresh) {
|
|
4587
|
+
// This is the case if we have an error session that is completed
|
|
4588
|
+
// (=triggered an error). Session will continue as session-based replay,
|
|
4589
|
+
// and when this session is expired, it will not be renewed until user
|
|
4590
|
+
// reloads.
|
|
4591
|
+
const discardedSession = makeSession({ sampled: false });
|
|
4592
|
+
return { type: 'new', session: discardedSession };
|
|
4593
|
+
} else {
|
|
4594
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.log('[Replay] Session has expired');
|
|
4595
|
+
}
|
|
4596
|
+
// Otherwise continue to create a new session
|
|
4595
4597
|
}
|
|
4596
4598
|
|
|
4597
4599
|
const newSession = createSession({
|
|
4598
4600
|
stickySession,
|
|
4599
|
-
|
|
4601
|
+
sessionSampleRate,
|
|
4602
|
+
allowBuffering,
|
|
4600
4603
|
});
|
|
4601
4604
|
|
|
4602
|
-
return newSession;
|
|
4605
|
+
return { type: 'new', session: newSession };
|
|
4603
4606
|
}
|
|
4604
4607
|
|
|
4605
4608
|
function isCustomEvent(event) {
|
|
@@ -4642,10 +4645,7 @@ async function addEvent(
|
|
|
4642
4645
|
|
|
4643
4646
|
const replayOptions = replay.getOptions();
|
|
4644
4647
|
|
|
4645
|
-
const eventAfterPossibleCallback =
|
|
4646
|
-
typeof replayOptions.beforeAddRecordingEvent === 'function' && isCustomEvent(event)
|
|
4647
|
-
? replayOptions.beforeAddRecordingEvent(event)
|
|
4648
|
-
: event;
|
|
4648
|
+
const eventAfterPossibleCallback = maybeApplyCallback(event, replayOptions.beforeAddRecordingEvent);
|
|
4649
4649
|
|
|
4650
4650
|
if (!eventAfterPossibleCallback) {
|
|
4651
4651
|
return;
|
|
@@ -4666,6 +4666,23 @@ async function addEvent(
|
|
|
4666
4666
|
}
|
|
4667
4667
|
}
|
|
4668
4668
|
|
|
4669
|
+
function maybeApplyCallback(
|
|
4670
|
+
event,
|
|
4671
|
+
callback,
|
|
4672
|
+
) {
|
|
4673
|
+
try {
|
|
4674
|
+
if (typeof callback === 'function' && isCustomEvent(event)) {
|
|
4675
|
+
return callback(event);
|
|
4676
|
+
}
|
|
4677
|
+
} catch (error) {
|
|
4678
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
|
|
4679
|
+
utils.logger.error('[Replay] An error occured in the `beforeAddRecordingEvent` callback, skipping the event...', error);
|
|
4680
|
+
return null;
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
return event;
|
|
4684
|
+
}
|
|
4685
|
+
|
|
4669
4686
|
/** If the event is an error event */
|
|
4670
4687
|
function isErrorEvent(event) {
|
|
4671
4688
|
return !event.type;
|
|
@@ -4912,14 +4929,12 @@ function handleHistorySpanListener(replay) {
|
|
|
4912
4929
|
|
|
4913
4930
|
// Need to collect visited URLs
|
|
4914
4931
|
replay.getContext().urls.push(result.name);
|
|
4915
|
-
replay.
|
|
4932
|
+
replay.triggerUserActivity();
|
|
4916
4933
|
|
|
4917
|
-
replay.
|
|
4918
|
-
replay
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
return false;
|
|
4922
|
-
});
|
|
4934
|
+
replay.addUpdate(() => {
|
|
4935
|
+
createPerformanceSpans(replay, [result]);
|
|
4936
|
+
// Returning false to flush
|
|
4937
|
+
return false;
|
|
4923
4938
|
});
|
|
4924
4939
|
};
|
|
4925
4940
|
}
|
|
@@ -6535,64 +6550,79 @@ function getHandleRecordingEmit(replay) {
|
|
|
6535
6550
|
let hadFirstEvent = false;
|
|
6536
6551
|
|
|
6537
6552
|
return (event, _isCheckout) => {
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
const isCheckout = _isCheckout || !hadFirstEvent;
|
|
6542
|
-
hadFirstEvent = true;
|
|
6543
|
-
|
|
6544
|
-
// The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.
|
|
6545
|
-
replay.addUpdate(() => {
|
|
6546
|
-
// The session is always started immediately on pageload/init, but for
|
|
6547
|
-
// error-only replays, it should reflect the most recent checkout
|
|
6548
|
-
// when an error occurs. Clear any state that happens before this current
|
|
6549
|
-
// checkout. This needs to happen before `addEvent()` which updates state
|
|
6550
|
-
// dependent on this reset.
|
|
6551
|
-
if (replay.recordingMode === 'buffer' && isCheckout) {
|
|
6552
|
-
replay.setInitialState();
|
|
6553
|
-
}
|
|
6553
|
+
// If this is false, it means session is expired, create and a new session and wait for checkout
|
|
6554
|
+
if (!replay.checkAndHandleExpiredSession()) {
|
|
6555
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.warn('[Replay] Received replay event after session expired.');
|
|
6554
6556
|
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
void addEvent(replay, event, isCheckout);
|
|
6557
|
+
return;
|
|
6558
|
+
}
|
|
6558
6559
|
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
}
|
|
6560
|
+
// `_isCheckout` is only set when the checkout is due to `checkoutEveryNms`
|
|
6561
|
+
// We also want to treat the first event as a checkout, so we handle this specifically here
|
|
6562
|
+
const isCheckout = _isCheckout || !hadFirstEvent;
|
|
6563
|
+
hadFirstEvent = true;
|
|
6564
6564
|
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6565
|
+
// The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.
|
|
6566
|
+
replay.addUpdate(() => {
|
|
6567
|
+
// The session is always started immediately on pageload/init, but for
|
|
6568
|
+
// error-only replays, it should reflect the most recent checkout
|
|
6569
|
+
// when an error occurs. Clear any state that happens before this current
|
|
6570
|
+
// checkout. This needs to happen before `addEvent()` which updates state
|
|
6571
|
+
// dependent on this reset.
|
|
6572
|
+
if (replay.recordingMode === 'buffer' && isCheckout) {
|
|
6573
|
+
replay.setInitialState();
|
|
6574
|
+
}
|
|
6575
|
+
|
|
6576
|
+
// We need to clear existing events on a checkout, otherwise they are
|
|
6577
|
+
// incremental event updates and should be appended
|
|
6578
|
+
void addEvent(replay, event, isCheckout);
|
|
6579
|
+
|
|
6580
|
+
// Different behavior for full snapshots (type=2), ignore other event types
|
|
6581
|
+
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
|
|
6582
|
+
if (!isCheckout) {
|
|
6583
|
+
return false;
|
|
6584
|
+
}
|
|
6585
|
+
|
|
6586
|
+
// Additionally, create a meta event that will capture certain SDK settings.
|
|
6587
|
+
// In order to handle buffer mode, this needs to either be done when we
|
|
6588
|
+
// receive checkout events or at flush time.
|
|
6589
|
+
//
|
|
6590
|
+
// `isCheckout` is always true, but want to be explicit that it should
|
|
6591
|
+
// only be added for checkouts
|
|
6592
|
+
void addSettingsEvent(replay, isCheckout);
|
|
6593
|
+
|
|
6594
|
+
// If there is a previousSessionId after a full snapshot occurs, then
|
|
6595
|
+
// the replay session was started due to session expiration. The new session
|
|
6596
|
+
// is started before triggering a new checkout and contains the id
|
|
6597
|
+
// of the previous session. Do not immediately flush in this case
|
|
6598
|
+
// to avoid capturing only the checkout and instead the replay will
|
|
6599
|
+
// be captured if they perform any follow-up actions.
|
|
6600
|
+
if (replay.session && replay.session.previousSessionId) {
|
|
6601
|
+
return true;
|
|
6602
|
+
}
|
|
6603
|
+
|
|
6604
|
+
// When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
|
|
6605
|
+
// this should usually be the timestamp of the checkout event, but to be safe...
|
|
6606
|
+
if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) {
|
|
6607
|
+
const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
|
|
6608
|
+
if (earliestEvent) {
|
|
6609
|
+
replay.session.started = earliestEvent;
|
|
6610
|
+
|
|
6611
|
+
if (replay.getOptions().stickySession) {
|
|
6612
|
+
saveSession(replay.session);
|
|
6583
6613
|
}
|
|
6584
6614
|
}
|
|
6615
|
+
}
|
|
6585
6616
|
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6617
|
+
if (replay.recordingMode === 'session') {
|
|
6618
|
+
// If the full snapshot is due to an initial load, we will not have
|
|
6619
|
+
// a previous session ID. In this case, we want to buffer events
|
|
6620
|
+
// for a set amount of time before flushing. This can help avoid
|
|
6621
|
+
// capturing replays of users that immediately close the window.
|
|
6622
|
+
void replay.flush();
|
|
6623
|
+
}
|
|
6593
6624
|
|
|
6594
|
-
|
|
6595
|
-
});
|
|
6625
|
+
return true;
|
|
6596
6626
|
});
|
|
6597
6627
|
};
|
|
6598
6628
|
}
|
|
@@ -6637,26 +6667,6 @@ function addSettingsEvent(replay, isCheckout) {
|
|
|
6637
6667
|
return addEvent(replay, createOptionsEvent(replay), false);
|
|
6638
6668
|
}
|
|
6639
6669
|
|
|
6640
|
-
/**
|
|
6641
|
-
* Sample a session based on the provided sample rates.
|
|
6642
|
-
*/
|
|
6643
|
-
function sampleSession({
|
|
6644
|
-
errorSampleRate,
|
|
6645
|
-
sessionSampleRate,
|
|
6646
|
-
}
|
|
6647
|
-
|
|
6648
|
-
) {
|
|
6649
|
-
if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
|
|
6650
|
-
return false;
|
|
6651
|
-
}
|
|
6652
|
-
|
|
6653
|
-
if (isSampled(sessionSampleRate)) {
|
|
6654
|
-
return 'session';
|
|
6655
|
-
}
|
|
6656
|
-
|
|
6657
|
-
return errorSampleRate > 0 ? 'buffer' : false;
|
|
6658
|
-
}
|
|
6659
|
-
|
|
6660
6670
|
/**
|
|
6661
6671
|
* Create a replay envelope ready to be sent.
|
|
6662
6672
|
* This includes both the replay event, as well as the recording data.
|
|
@@ -7048,6 +7058,7 @@ class ReplayContainer {
|
|
|
7048
7058
|
*/
|
|
7049
7059
|
__init4() {this.timeouts = {
|
|
7050
7060
|
sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
|
|
7061
|
+
sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
|
|
7051
7062
|
maxSessionLife: MAX_SESSION_LIFE,
|
|
7052
7063
|
}; }
|
|
7053
7064
|
|
|
@@ -7059,30 +7070,35 @@ class ReplayContainer {
|
|
|
7059
7070
|
|
|
7060
7071
|
__init6() {this._flushLock = null;}
|
|
7061
7072
|
|
|
7073
|
+
/**
|
|
7074
|
+
* Timestamp of the last user activity. This lives across sessions.
|
|
7075
|
+
*/
|
|
7076
|
+
__init7() {this._lastActivity = Date.now();}
|
|
7077
|
+
|
|
7062
7078
|
/**
|
|
7063
7079
|
* Is the integration currently active?
|
|
7064
7080
|
*/
|
|
7065
|
-
|
|
7081
|
+
__init8() {this._isEnabled = false;}
|
|
7066
7082
|
|
|
7067
7083
|
/**
|
|
7068
7084
|
* Paused is a state where:
|
|
7069
7085
|
* - DOM Recording is not listening at all
|
|
7070
7086
|
* - Nothing will be added to event buffer (e.g. core SDK events)
|
|
7071
7087
|
*/
|
|
7072
|
-
|
|
7088
|
+
__init9() {this._isPaused = false;}
|
|
7073
7089
|
|
|
7074
7090
|
/**
|
|
7075
7091
|
* Have we attached listeners to the core SDK?
|
|
7076
7092
|
* Note we have to track this as there is no way to remove instrumentation handlers.
|
|
7077
7093
|
*/
|
|
7078
|
-
|
|
7094
|
+
__init10() {this._hasInitializedCoreListeners = false;}
|
|
7079
7095
|
|
|
7080
7096
|
/**
|
|
7081
7097
|
* Function to stop recording
|
|
7082
7098
|
*/
|
|
7083
|
-
|
|
7099
|
+
__init11() {this._stopRecording = null;}
|
|
7084
7100
|
|
|
7085
|
-
|
|
7101
|
+
__init12() {this._context = {
|
|
7086
7102
|
errorIds: new Set(),
|
|
7087
7103
|
traceIds: new Set(),
|
|
7088
7104
|
urls: [],
|
|
@@ -7095,7 +7111,7 @@ class ReplayContainer {
|
|
|
7095
7111
|
recordingOptions,
|
|
7096
7112
|
}
|
|
7097
7113
|
|
|
7098
|
-
) {ReplayContainer.prototype.__init.call(this);ReplayContainer.prototype.__init2.call(this);ReplayContainer.prototype.__init3.call(this);ReplayContainer.prototype.__init4.call(this);ReplayContainer.prototype.__init5.call(this);ReplayContainer.prototype.__init6.call(this);ReplayContainer.prototype.__init7.call(this);ReplayContainer.prototype.__init8.call(this);ReplayContainer.prototype.__init9.call(this);ReplayContainer.prototype.__init10.call(this);ReplayContainer.prototype.__init11.call(this);ReplayContainer.prototype.__init12.call(this);ReplayContainer.prototype.__init13.call(this);ReplayContainer.prototype.__init14.call(this);ReplayContainer.prototype.__init15.call(this);ReplayContainer.prototype.__init16.call(this);ReplayContainer.prototype.__init17.call(this);
|
|
7114
|
+
) {ReplayContainer.prototype.__init.call(this);ReplayContainer.prototype.__init2.call(this);ReplayContainer.prototype.__init3.call(this);ReplayContainer.prototype.__init4.call(this);ReplayContainer.prototype.__init5.call(this);ReplayContainer.prototype.__init6.call(this);ReplayContainer.prototype.__init7.call(this);ReplayContainer.prototype.__init8.call(this);ReplayContainer.prototype.__init9.call(this);ReplayContainer.prototype.__init10.call(this);ReplayContainer.prototype.__init11.call(this);ReplayContainer.prototype.__init12.call(this);ReplayContainer.prototype.__init13.call(this);ReplayContainer.prototype.__init14.call(this);ReplayContainer.prototype.__init15.call(this);ReplayContainer.prototype.__init16.call(this);ReplayContainer.prototype.__init17.call(this);ReplayContainer.prototype.__init18.call(this);
|
|
7099
7115
|
this._recordingOptions = recordingOptions;
|
|
7100
7116
|
this._options = options;
|
|
7101
7117
|
|
|
@@ -7153,13 +7169,37 @@ class ReplayContainer {
|
|
|
7153
7169
|
*/
|
|
7154
7170
|
initializeSampling() {
|
|
7155
7171
|
const { errorSampleRate, sessionSampleRate } = this._options;
|
|
7156
|
-
const sampled = sampleSession({ errorSampleRate, sessionSampleRate });
|
|
7157
7172
|
|
|
7158
|
-
|
|
7173
|
+
// If neither sample rate is > 0, then do nothing - user will need to call one of
|
|
7174
|
+
// `start()` or `startBuffering` themselves.
|
|
7175
|
+
if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
|
|
7176
|
+
return;
|
|
7177
|
+
}
|
|
7178
|
+
|
|
7179
|
+
// Otherwise if there is _any_ sample rate set, try to load an existing
|
|
7180
|
+
// session, or create a new one.
|
|
7181
|
+
const isSessionSampled = this._loadAndCheckSession();
|
|
7182
|
+
|
|
7183
|
+
if (!isSessionSampled) {
|
|
7184
|
+
// This should only occur if `errorSampleRate` is 0 and was unsampled for
|
|
7185
|
+
// session-based replay. In this case there is nothing to do.
|
|
7186
|
+
return;
|
|
7187
|
+
}
|
|
7188
|
+
|
|
7189
|
+
if (!this.session) {
|
|
7190
|
+
// This should not happen, something wrong has occurred
|
|
7191
|
+
this._handleException(new Error('Unable to initialize and create session'));
|
|
7159
7192
|
return;
|
|
7160
7193
|
}
|
|
7161
7194
|
|
|
7162
|
-
this.
|
|
7195
|
+
if (this.session.sampled && this.session.sampled !== 'session') {
|
|
7196
|
+
// If not sampled as session-based, then recording mode will be `buffer`
|
|
7197
|
+
// Note that we don't explicitly check if `sampled === 'buffer'` because we
|
|
7198
|
+
// could have sessions from Session storage that are still `error` from
|
|
7199
|
+
// prior SDK version.
|
|
7200
|
+
this.recordingMode = 'buffer';
|
|
7201
|
+
}
|
|
7202
|
+
|
|
7163
7203
|
this._initializeRecording();
|
|
7164
7204
|
}
|
|
7165
7205
|
|
|
@@ -7179,7 +7219,20 @@ class ReplayContainer {
|
|
|
7179
7219
|
throw new Error('Replay buffering is in progress, call `flush()` to save the replay');
|
|
7180
7220
|
}
|
|
7181
7221
|
|
|
7182
|
-
this.
|
|
7222
|
+
const previousSessionId = this.session && this.session.id;
|
|
7223
|
+
|
|
7224
|
+
const { session } = getSession({
|
|
7225
|
+
timeouts: this.timeouts,
|
|
7226
|
+
stickySession: Boolean(this._options.stickySession),
|
|
7227
|
+
currentSession: this.session,
|
|
7228
|
+
// This is intentional: create a new session-based replay when calling `start()`
|
|
7229
|
+
sessionSampleRate: 1,
|
|
7230
|
+
allowBuffering: false,
|
|
7231
|
+
});
|
|
7232
|
+
|
|
7233
|
+
session.previousSessionId = previousSessionId;
|
|
7234
|
+
this.session = session;
|
|
7235
|
+
|
|
7183
7236
|
this._initializeRecording();
|
|
7184
7237
|
}
|
|
7185
7238
|
|
|
@@ -7192,7 +7245,20 @@ class ReplayContainer {
|
|
|
7192
7245
|
throw new Error('Replay recording is already in progress');
|
|
7193
7246
|
}
|
|
7194
7247
|
|
|
7195
|
-
this.
|
|
7248
|
+
const previousSessionId = this.session && this.session.id;
|
|
7249
|
+
|
|
7250
|
+
const { session } = getSession({
|
|
7251
|
+
timeouts: this.timeouts,
|
|
7252
|
+
stickySession: Boolean(this._options.stickySession),
|
|
7253
|
+
currentSession: this.session,
|
|
7254
|
+
sessionSampleRate: 0,
|
|
7255
|
+
allowBuffering: true,
|
|
7256
|
+
});
|
|
7257
|
+
|
|
7258
|
+
session.previousSessionId = previousSessionId;
|
|
7259
|
+
this.session = session;
|
|
7260
|
+
|
|
7261
|
+
this.recordingMode = 'buffer';
|
|
7196
7262
|
this._initializeRecording();
|
|
7197
7263
|
}
|
|
7198
7264
|
|
|
@@ -7274,7 +7340,8 @@ class ReplayContainer {
|
|
|
7274
7340
|
this.eventBuffer && this.eventBuffer.destroy();
|
|
7275
7341
|
this.eventBuffer = null;
|
|
7276
7342
|
|
|
7277
|
-
// Clear session from session storage
|
|
7343
|
+
// Clear session from session storage, note this means if a new session
|
|
7344
|
+
// is started after, it will not have `previousSessionId`
|
|
7278
7345
|
clearSession(this);
|
|
7279
7346
|
} catch (err) {
|
|
7280
7347
|
this._handleException(err);
|
|
@@ -7287,10 +7354,6 @@ class ReplayContainer {
|
|
|
7287
7354
|
* not as thorough of a shutdown as `stop()`.
|
|
7288
7355
|
*/
|
|
7289
7356
|
pause() {
|
|
7290
|
-
if (this.isPaused()) {
|
|
7291
|
-
return;
|
|
7292
|
-
}
|
|
7293
|
-
|
|
7294
7357
|
this._isPaused = true;
|
|
7295
7358
|
this.stopRecording();
|
|
7296
7359
|
}
|
|
@@ -7302,14 +7365,12 @@ class ReplayContainer {
|
|
|
7302
7365
|
* new DOM checkout.`
|
|
7303
7366
|
*/
|
|
7304
7367
|
resume() {
|
|
7305
|
-
if (!this.
|
|
7368
|
+
if (!this._loadAndCheckSession()) {
|
|
7306
7369
|
return;
|
|
7307
7370
|
}
|
|
7308
7371
|
|
|
7309
7372
|
this._isPaused = false;
|
|
7310
|
-
this.
|
|
7311
|
-
this.startRecording();
|
|
7312
|
-
});
|
|
7373
|
+
this.startRecording();
|
|
7313
7374
|
}
|
|
7314
7375
|
|
|
7315
7376
|
/**
|
|
@@ -7344,8 +7405,20 @@ class ReplayContainer {
|
|
|
7344
7405
|
// starting a new recording
|
|
7345
7406
|
this.recordingMode = 'session';
|
|
7346
7407
|
|
|
7408
|
+
// Once this session ends, we do not want to refresh it
|
|
7347
7409
|
if (this.session) {
|
|
7348
|
-
this.session.
|
|
7410
|
+
this.session.shouldRefresh = false;
|
|
7411
|
+
|
|
7412
|
+
// It's possible that the session lifespan is > max session lifespan
|
|
7413
|
+
// because we have been buffering beyond max session lifespan (we ignore
|
|
7414
|
+
// expiration given that `shouldRefresh` is true). Since we flip
|
|
7415
|
+
// `shouldRefresh`, the session could be considered expired due to
|
|
7416
|
+
// lifespan, which is not what we want. Update session start date to be
|
|
7417
|
+
// the current timestamp, so that session is not considered to be
|
|
7418
|
+
// expired. This means that max replay duration can be MAX_SESSION_LIFE +
|
|
7419
|
+
// (length of buffer), which we are ok with.
|
|
7420
|
+
this._updateUserActivity(activityTime);
|
|
7421
|
+
this._updateSessionActivity(activityTime);
|
|
7349
7422
|
this.session.started = activityTime;
|
|
7350
7423
|
this._maybeSaveSession();
|
|
7351
7424
|
}
|
|
@@ -7383,9 +7456,42 @@ class ReplayContainer {
|
|
|
7383
7456
|
}
|
|
7384
7457
|
|
|
7385
7458
|
/**
|
|
7386
|
-
* Updates the user activity timestamp.
|
|
7459
|
+
* Updates the user activity timestamp and resumes recording. This should be
|
|
7460
|
+
* called in an event handler for a user action that we consider as the user
|
|
7461
|
+
* being "active" (e.g. a mouse click).
|
|
7462
|
+
*/
|
|
7463
|
+
triggerUserActivity() {
|
|
7464
|
+
this._updateUserActivity();
|
|
7465
|
+
|
|
7466
|
+
// This case means that recording was once stopped due to inactivity.
|
|
7467
|
+
// Ensure that recording is resumed.
|
|
7468
|
+
if (!this._stopRecording) {
|
|
7469
|
+
// Create a new session, otherwise when the user action is flushed, it
|
|
7470
|
+
// will get rejected due to an expired session.
|
|
7471
|
+
if (!this._loadAndCheckSession()) {
|
|
7472
|
+
return;
|
|
7473
|
+
}
|
|
7474
|
+
|
|
7475
|
+
// Note: This will cause a new DOM checkout
|
|
7476
|
+
this.resume();
|
|
7477
|
+
return;
|
|
7478
|
+
}
|
|
7479
|
+
|
|
7480
|
+
// Otherwise... recording was never suspended, continue as normalish
|
|
7481
|
+
this.checkAndHandleExpiredSession();
|
|
7482
|
+
|
|
7483
|
+
this._updateSessionActivity();
|
|
7484
|
+
}
|
|
7485
|
+
|
|
7486
|
+
/**
|
|
7487
|
+
* Updates the user activity timestamp *without* resuming
|
|
7488
|
+
* recording. Some user events (e.g. keydown) can be create
|
|
7489
|
+
* low-value replays that only contain the keypress as a
|
|
7490
|
+
* breadcrumb. Instead this would require other events to
|
|
7491
|
+
* create a new replay after a session has expired.
|
|
7387
7492
|
*/
|
|
7388
7493
|
updateUserActivity() {
|
|
7494
|
+
this._updateUserActivity();
|
|
7389
7495
|
this._updateSessionActivity();
|
|
7390
7496
|
}
|
|
7391
7497
|
|
|
@@ -7430,6 +7536,54 @@ class ReplayContainer {
|
|
|
7430
7536
|
return this.session && this.session.id;
|
|
7431
7537
|
}
|
|
7432
7538
|
|
|
7539
|
+
/**
|
|
7540
|
+
* Checks if recording should be stopped due to user inactivity. Otherwise
|
|
7541
|
+
* check if session is expired and create a new session if so. Triggers a new
|
|
7542
|
+
* full snapshot on new session.
|
|
7543
|
+
*
|
|
7544
|
+
* Returns true if session is not expired, false otherwise.
|
|
7545
|
+
* @hidden
|
|
7546
|
+
*/
|
|
7547
|
+
checkAndHandleExpiredSession() {
|
|
7548
|
+
const oldSessionId = this.getSessionId();
|
|
7549
|
+
|
|
7550
|
+
// Prevent starting a new session if the last user activity is older than
|
|
7551
|
+
// SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new
|
|
7552
|
+
// session+recording. This creates noisy replays that do not have much
|
|
7553
|
+
// content in them.
|
|
7554
|
+
if (
|
|
7555
|
+
this._lastActivity &&
|
|
7556
|
+
isExpired(this._lastActivity, this.timeouts.sessionIdlePause) &&
|
|
7557
|
+
this.session &&
|
|
7558
|
+
this.session.sampled === 'session'
|
|
7559
|
+
) {
|
|
7560
|
+
// Pause recording only for session-based replays. Otherwise, resuming
|
|
7561
|
+
// will create a new replay and will conflict with users who only choose
|
|
7562
|
+
// to record error-based replays only. (e.g. the resumed replay will not
|
|
7563
|
+
// contain a reference to an error)
|
|
7564
|
+
this.pause();
|
|
7565
|
+
return;
|
|
7566
|
+
}
|
|
7567
|
+
|
|
7568
|
+
// --- There is recent user activity --- //
|
|
7569
|
+
// This will create a new session if expired, based on expiry length
|
|
7570
|
+
if (!this._loadAndCheckSession()) {
|
|
7571
|
+
return;
|
|
7572
|
+
}
|
|
7573
|
+
|
|
7574
|
+
// Session was expired if session ids do not match
|
|
7575
|
+
const expired = oldSessionId !== this.getSessionId();
|
|
7576
|
+
|
|
7577
|
+
if (!expired) {
|
|
7578
|
+
return true;
|
|
7579
|
+
}
|
|
7580
|
+
|
|
7581
|
+
// Session is expired, trigger a full snapshot (which will create a new session)
|
|
7582
|
+
this._triggerFullSnapshot();
|
|
7583
|
+
|
|
7584
|
+
return false;
|
|
7585
|
+
}
|
|
7586
|
+
|
|
7433
7587
|
/**
|
|
7434
7588
|
* Capture some initial state that can change throughout the lifespan of the
|
|
7435
7589
|
* replay. This is required because otherwise they would be captured at the
|
|
@@ -7482,23 +7636,6 @@ class ReplayContainer {
|
|
|
7482
7636
|
return res;
|
|
7483
7637
|
}
|
|
7484
7638
|
|
|
7485
|
-
/**
|
|
7486
|
-
* Check the state/expiration of the session.
|
|
7487
|
-
* The callback is called when the session is neither paused nor expired.
|
|
7488
|
-
*/
|
|
7489
|
-
checkSessionState(onContinue) {
|
|
7490
|
-
if (!this.session || !this.session.sampled) {
|
|
7491
|
-
return;
|
|
7492
|
-
}
|
|
7493
|
-
|
|
7494
|
-
checkSessionState(this.session, this.recordingMode, this.timeouts, {
|
|
7495
|
-
onPause: () => this.pause(),
|
|
7496
|
-
ensureResumed: () => this.resume(),
|
|
7497
|
-
onEnd: () => this._refreshSession(),
|
|
7498
|
-
onContinue,
|
|
7499
|
-
});
|
|
7500
|
-
}
|
|
7501
|
-
|
|
7502
7639
|
/**
|
|
7503
7640
|
* This will get the parametrized route name of the current page.
|
|
7504
7641
|
* This is only available if performance is enabled, and if an instrumented router is used.
|
|
@@ -7512,31 +7649,6 @@ class ReplayContainer {
|
|
|
7512
7649
|
return lastTransaction.name;
|
|
7513
7650
|
}
|
|
7514
7651
|
|
|
7515
|
-
/**
|
|
7516
|
-
* Initialize a new session.
|
|
7517
|
-
*/
|
|
7518
|
-
_initializeSession(sampled, { forceSampled = false } = {}) {
|
|
7519
|
-
const { stickySession } = this._options;
|
|
7520
|
-
|
|
7521
|
-
// If neither sample rate is > 0, then do nothing - user will need to call one of
|
|
7522
|
-
// `start()` or `startBuffering` themselves.
|
|
7523
|
-
if (!sampled) {
|
|
7524
|
-
return;
|
|
7525
|
-
}
|
|
7526
|
-
|
|
7527
|
-
const session = restoreOrCreateSession({ stickySession, sampled, forceSampled });
|
|
7528
|
-
|
|
7529
|
-
// This shouldn't be possible really, but just in case...
|
|
7530
|
-
if (!session.sampled) {
|
|
7531
|
-
return;
|
|
7532
|
-
}
|
|
7533
|
-
|
|
7534
|
-
this.session = session;
|
|
7535
|
-
this.recordingMode = this.session.sampled === 'buffer' ? 'buffer' : 'session';
|
|
7536
|
-
|
|
7537
|
-
return;
|
|
7538
|
-
}
|
|
7539
|
-
|
|
7540
7652
|
/**
|
|
7541
7653
|
* Initialize and start all listeners to varying events (DOM,
|
|
7542
7654
|
* Performance Observer, Recording, Sentry SDK, etc)
|
|
@@ -7546,7 +7658,7 @@ class ReplayContainer {
|
|
|
7546
7658
|
|
|
7547
7659
|
// this method is generally called on page load or manually - in both cases
|
|
7548
7660
|
// we should treat it as an activity
|
|
7549
|
-
this.
|
|
7661
|
+
this._updateSessionActivity();
|
|
7550
7662
|
|
|
7551
7663
|
this.eventBuffer = createEventBuffer({
|
|
7552
7664
|
useCompression: this._options.useCompression,
|
|
@@ -7557,7 +7669,6 @@ class ReplayContainer {
|
|
|
7557
7669
|
|
|
7558
7670
|
// Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout
|
|
7559
7671
|
this._isEnabled = true;
|
|
7560
|
-
this._isPaused = false;
|
|
7561
7672
|
|
|
7562
7673
|
this.startRecording();
|
|
7563
7674
|
}
|
|
@@ -7572,31 +7683,37 @@ class ReplayContainer {
|
|
|
7572
7683
|
}
|
|
7573
7684
|
|
|
7574
7685
|
/**
|
|
7575
|
-
*
|
|
7576
|
-
*
|
|
7686
|
+
* Loads (or refreshes) the current session.
|
|
7687
|
+
* Returns false if session is not recorded.
|
|
7577
7688
|
*/
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7689
|
+
_loadAndCheckSession() {
|
|
7690
|
+
const { type, session } = getSession({
|
|
7691
|
+
timeouts: this.timeouts,
|
|
7692
|
+
stickySession: Boolean(this._options.stickySession),
|
|
7693
|
+
currentSession: this.session,
|
|
7694
|
+
sessionSampleRate: this._options.sessionSampleRate,
|
|
7695
|
+
allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer',
|
|
7696
|
+
});
|
|
7697
|
+
|
|
7698
|
+
// If session was newly created (i.e. was not loaded from storage), then
|
|
7699
|
+
// enable flag to create the root replay
|
|
7700
|
+
if (type === 'new') {
|
|
7701
|
+
this.setInitialState();
|
|
7582
7702
|
}
|
|
7583
7703
|
|
|
7584
|
-
const
|
|
7585
|
-
|
|
7704
|
+
const currentSessionId = this.getSessionId();
|
|
7705
|
+
if (session.id !== currentSessionId) {
|
|
7706
|
+
session.previousSessionId = currentSessionId;
|
|
7707
|
+
}
|
|
7586
7708
|
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7709
|
+
this.session = session;
|
|
7710
|
+
|
|
7711
|
+
if (!this.session.sampled) {
|
|
7712
|
+
void this.stop('session unsampled');
|
|
7713
|
+
return false;
|
|
7590
7714
|
}
|
|
7591
7715
|
|
|
7592
|
-
|
|
7593
|
-
if (sampled === 'session') {
|
|
7594
|
-
return this.start();
|
|
7595
|
-
}
|
|
7596
|
-
if (sampled === 'buffer') {
|
|
7597
|
-
return this.startBuffering();
|
|
7598
|
-
}
|
|
7599
|
-
});
|
|
7716
|
+
return true;
|
|
7600
7717
|
}
|
|
7601
7718
|
|
|
7602
7719
|
/**
|
|
@@ -7661,7 +7778,7 @@ class ReplayContainer {
|
|
|
7661
7778
|
* be hidden. Likewise, moving a different window to cover the contents of the
|
|
7662
7779
|
* page will also trigger a change to a hidden state.
|
|
7663
7780
|
*/
|
|
7664
|
-
|
|
7781
|
+
__init13() {this._handleVisibilityChange = () => {
|
|
7665
7782
|
if (WINDOW.document.visibilityState === 'visible') {
|
|
7666
7783
|
this._doChangeToForegroundTasks();
|
|
7667
7784
|
} else {
|
|
@@ -7672,7 +7789,7 @@ class ReplayContainer {
|
|
|
7672
7789
|
/**
|
|
7673
7790
|
* Handle when page is blurred
|
|
7674
7791
|
*/
|
|
7675
|
-
|
|
7792
|
+
__init14() {this._handleWindowBlur = () => {
|
|
7676
7793
|
const breadcrumb = createBreadcrumb({
|
|
7677
7794
|
category: 'ui.blur',
|
|
7678
7795
|
});
|
|
@@ -7685,7 +7802,7 @@ class ReplayContainer {
|
|
|
7685
7802
|
/**
|
|
7686
7803
|
* Handle when page is focused
|
|
7687
7804
|
*/
|
|
7688
|
-
|
|
7805
|
+
__init15() {this._handleWindowFocus = () => {
|
|
7689
7806
|
const breadcrumb = createBreadcrumb({
|
|
7690
7807
|
category: 'ui.focus',
|
|
7691
7808
|
});
|
|
@@ -7696,7 +7813,7 @@ class ReplayContainer {
|
|
|
7696
7813
|
};}
|
|
7697
7814
|
|
|
7698
7815
|
/** Ensure page remains active when a key is pressed. */
|
|
7699
|
-
|
|
7816
|
+
__init16() {this._handleKeyboardEvent = (event) => {
|
|
7700
7817
|
handleKeyboardEvent(this, event);
|
|
7701
7818
|
};}
|
|
7702
7819
|
|
|
@@ -7704,35 +7821,71 @@ class ReplayContainer {
|
|
|
7704
7821
|
* Tasks to run when we consider a page to be hidden (via blurring and/or visibility)
|
|
7705
7822
|
*/
|
|
7706
7823
|
_doChangeToBackgroundTasks(breadcrumb) {
|
|
7707
|
-
this.
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
}
|
|
7824
|
+
if (!this.session) {
|
|
7825
|
+
return;
|
|
7826
|
+
}
|
|
7711
7827
|
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
}
|
|
7828
|
+
const expired = isSessionExpired(this.session, this.timeouts);
|
|
7829
|
+
|
|
7830
|
+
if (breadcrumb && !expired) {
|
|
7831
|
+
this._createCustomBreadcrumb(breadcrumb);
|
|
7832
|
+
}
|
|
7833
|
+
|
|
7834
|
+
// Send replay when the page/tab becomes hidden. There is no reason to send
|
|
7835
|
+
// replay if it becomes visible, since no actions we care about were done
|
|
7836
|
+
// while it was hidden
|
|
7837
|
+
void this.conditionalFlush();
|
|
7717
7838
|
}
|
|
7718
7839
|
|
|
7719
7840
|
/**
|
|
7720
7841
|
* Tasks to run when we consider a page to be visible (via focus and/or visibility)
|
|
7721
7842
|
*/
|
|
7722
7843
|
_doChangeToForegroundTasks(breadcrumb) {
|
|
7723
|
-
this.
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7844
|
+
if (!this.session) {
|
|
7845
|
+
return;
|
|
7846
|
+
}
|
|
7847
|
+
|
|
7848
|
+
const isSessionActive = this.checkAndHandleExpiredSession();
|
|
7849
|
+
|
|
7850
|
+
if (!isSessionActive) {
|
|
7851
|
+
// If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION
|
|
7852
|
+
// ms, we will re-use the existing session, otherwise create a new
|
|
7853
|
+
// session
|
|
7854
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.log('[Replay] Document has become active, but session has expired');
|
|
7855
|
+
return;
|
|
7856
|
+
}
|
|
7857
|
+
|
|
7858
|
+
if (breadcrumb) {
|
|
7859
|
+
this._createCustomBreadcrumb(breadcrumb);
|
|
7860
|
+
}
|
|
7861
|
+
}
|
|
7862
|
+
|
|
7863
|
+
/**
|
|
7864
|
+
* Trigger rrweb to take a full snapshot which will cause this plugin to
|
|
7865
|
+
* create a new Replay event.
|
|
7866
|
+
*/
|
|
7867
|
+
_triggerFullSnapshot(checkout = true) {
|
|
7868
|
+
try {
|
|
7869
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.log('[Replay] Taking full rrweb snapshot');
|
|
7870
|
+
record.takeFullSnapshot(checkout);
|
|
7871
|
+
} catch (err) {
|
|
7872
|
+
this._handleException(err);
|
|
7873
|
+
}
|
|
7874
|
+
}
|
|
7875
|
+
|
|
7876
|
+
/**
|
|
7877
|
+
* Update user activity (across session lifespans)
|
|
7878
|
+
*/
|
|
7879
|
+
_updateUserActivity(_lastActivity = Date.now()) {
|
|
7880
|
+
this._lastActivity = _lastActivity;
|
|
7728
7881
|
}
|
|
7729
7882
|
|
|
7730
7883
|
/**
|
|
7731
7884
|
* Updates the session's last activity timestamp
|
|
7732
7885
|
*/
|
|
7733
|
-
_updateSessionActivity(
|
|
7886
|
+
_updateSessionActivity(_lastActivity = Date.now()) {
|
|
7734
7887
|
if (this.session) {
|
|
7735
|
-
this.session.lastActivity =
|
|
7888
|
+
this.session.lastActivity = _lastActivity;
|
|
7736
7889
|
this._maybeSaveSession();
|
|
7737
7890
|
}
|
|
7738
7891
|
}
|
|
@@ -7883,7 +8036,7 @@ class ReplayContainer {
|
|
|
7883
8036
|
* Flush recording data to Sentry. Creates a lock so that only a single flush
|
|
7884
8037
|
* can be active at a time. Do not call this directly.
|
|
7885
8038
|
*/
|
|
7886
|
-
|
|
8039
|
+
__init17() {this._flush = async ({
|
|
7887
8040
|
force = false,
|
|
7888
8041
|
}
|
|
7889
8042
|
|
|
@@ -7893,58 +8046,42 @@ class ReplayContainer {
|
|
|
7893
8046
|
return;
|
|
7894
8047
|
}
|
|
7895
8048
|
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
}
|
|
7901
|
-
checkSessionState(this.session, this.recordingMode, this.timeouts, {
|
|
7902
|
-
onPause: () => {
|
|
7903
|
-
this.pause();
|
|
7904
|
-
resolve();
|
|
7905
|
-
},
|
|
7906
|
-
ensureResumed: () => this.resume(),
|
|
7907
|
-
onEnd: () => {
|
|
7908
|
-
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.error('[Replay] Attempting to finish replay event after session expired.');
|
|
7909
|
-
this._refreshSession();
|
|
7910
|
-
resolve();
|
|
7911
|
-
},
|
|
7912
|
-
onContinue: () => {
|
|
7913
|
-
void this._flushLocked().then(resolve);
|
|
7914
|
-
},
|
|
7915
|
-
});
|
|
7916
|
-
});
|
|
7917
|
-
};}
|
|
8049
|
+
if (!this.checkAndHandleExpiredSession()) {
|
|
8050
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.error('[Replay] Attempting to finish replay event after session expired.');
|
|
8051
|
+
return;
|
|
8052
|
+
}
|
|
7918
8053
|
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
try {
|
|
7924
|
-
// A flush is about to happen, cancel any queued flushes
|
|
7925
|
-
this._debouncedFlush.cancel();
|
|
8054
|
+
if (!this.session) {
|
|
8055
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.error('[Replay] No session found to flush.');
|
|
8056
|
+
return;
|
|
8057
|
+
}
|
|
7926
8058
|
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
if (!this._flushLock) {
|
|
7930
|
-
this._flushLock = this._runFlush();
|
|
7931
|
-
await this._flushLock;
|
|
7932
|
-
this._flushLock = null;
|
|
7933
|
-
return;
|
|
7934
|
-
}
|
|
8059
|
+
// A flush is about to happen, cancel any queued flushes
|
|
8060
|
+
this._debouncedFlush.cancel();
|
|
7935
8061
|
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
// completing) into a single flush.
|
|
8062
|
+
// this._flushLock acts as a lock so that future calls to `_flush()`
|
|
8063
|
+
// will be blocked until this promise resolves
|
|
8064
|
+
if (!this._flushLock) {
|
|
8065
|
+
this._flushLock = this._runFlush();
|
|
7941
8066
|
await this._flushLock;
|
|
7942
|
-
|
|
7943
|
-
|
|
8067
|
+
this._flushLock = null;
|
|
8068
|
+
return;
|
|
7944
8069
|
}
|
|
7945
8070
|
|
|
7946
|
-
|
|
7947
|
-
|
|
8071
|
+
// Wait for previous flush to finish, then call the debounced `_flush()`.
|
|
8072
|
+
// It's possible there are other flush requests queued and waiting for it
|
|
8073
|
+
// to resolve. We want to reduce all outstanding requests (as well as any
|
|
8074
|
+
// new flush requests that occur within a second of the locked flush
|
|
8075
|
+
// completing) into a single flush.
|
|
8076
|
+
|
|
8077
|
+
try {
|
|
8078
|
+
await this._flushLock;
|
|
8079
|
+
} catch (err) {
|
|
8080
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.error(err);
|
|
8081
|
+
} finally {
|
|
8082
|
+
this._debouncedFlush();
|
|
8083
|
+
}
|
|
8084
|
+
};}
|
|
7948
8085
|
|
|
7949
8086
|
/** Save the session, if it is sticky */
|
|
7950
8087
|
_maybeSaveSession() {
|
|
@@ -7954,7 +8091,7 @@ class ReplayContainer {
|
|
|
7954
8091
|
}
|
|
7955
8092
|
|
|
7956
8093
|
/** Handler for rrweb.record.onMutation */
|
|
7957
|
-
|
|
8094
|
+
__init18() {this._onMutationHandler = (mutations) => {
|
|
7958
8095
|
const count = mutations.length;
|
|
7959
8096
|
|
|
7960
8097
|
const mutationLimit = this._options.mutationLimit;
|