@sentry/replay 7.58.0 → 7.59.0-beta.0
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 +332 -450
- package/cjs/index.js.map +1 -1
- package/esm/index.js +332 -450
- package/esm/index.js.map +1 -1
- package/package.json +5 -5
- package/types/constants.d.ts +0 -1
- package/types/constants.d.ts.map +1 -1
- package/types/coreHandlers/handleHistory.d.ts.map +1 -1
- package/types/replay.d.ts +17 -36
- package/types/replay.d.ts.map +1 -1
- package/types/session/Session.d.ts.map +1 -1
- package/types/session/checkSessionState.d.ts +10 -0
- package/types/session/checkSessionState.d.ts.map +1 -0
- package/types/session/createSession.d.ts +5 -2
- package/types/session/createSession.d.ts.map +1 -1
- package/types/session/restoreOrCreateSession.d.ts +8 -0
- package/types/session/restoreOrCreateSession.d.ts.map +1 -0
- package/types/types/replay.d.ts +1 -24
- package/types/types/replay.d.ts.map +1 -1
- package/types/util/handleRecordingEmit.d.ts.map +1 -1
- package/types/util/sampleSession.d.ts +9 -0
- package/types/util/sampleSession.d.ts.map +1 -0
- package/types/session/getSession.d.ts +0 -17
- package/types/session/getSession.d.ts.map +0 -1
- package/types/util/isSessionExpired.d.ts +0 -6
- package/types/util/isSessionExpired.d.ts.map +0 -1
package/esm/index.js
CHANGED
|
@@ -14,9 +14,6 @@ const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';
|
|
|
14
14
|
// The idle limit for a session after which recording is paused.
|
|
15
15
|
const SESSION_IDLE_PAUSE_DURATION = 300000; // 5 minutes in ms
|
|
16
16
|
|
|
17
|
-
// The idle limit for a session after which the session expires.
|
|
18
|
-
const SESSION_IDLE_EXPIRE_DURATION = 900000; // 15 minutes in ms
|
|
19
|
-
|
|
20
17
|
// The maximum length of a session
|
|
21
18
|
const MAX_SESSION_LIFE = 3600000; // 60 minutes in ms
|
|
22
19
|
|
|
@@ -3279,26 +3276,26 @@ function addBreadcrumbEvent(replay, breadcrumb) {
|
|
|
3279
3276
|
}
|
|
3280
3277
|
|
|
3281
3278
|
if (['ui.click', 'ui.input'].includes(breadcrumb.category )) {
|
|
3282
|
-
replay.
|
|
3283
|
-
} else {
|
|
3284
|
-
replay.checkAndHandleExpiredSession();
|
|
3279
|
+
replay.updateUserActivity();
|
|
3285
3280
|
}
|
|
3286
3281
|
|
|
3287
|
-
replay.
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3282
|
+
replay.checkSessionState(() => {
|
|
3283
|
+
replay.addUpdate(() => {
|
|
3284
|
+
void replay.throttledAddEvent({
|
|
3285
|
+
type: EventType.Custom,
|
|
3286
|
+
// TODO: We were converting from ms to seconds for breadcrumbs, spans,
|
|
3287
|
+
// but maybe we should just keep them as milliseconds
|
|
3288
|
+
timestamp: (breadcrumb.timestamp || 0) * 1000,
|
|
3289
|
+
data: {
|
|
3290
|
+
tag: 'breadcrumb',
|
|
3291
|
+
// normalize to max. 10 depth and 1_000 properties per object
|
|
3292
|
+
payload: normalize(breadcrumb, 10, 1000),
|
|
3293
|
+
},
|
|
3294
|
+
});
|
|
3299
3295
|
|
|
3300
|
-
|
|
3301
|
-
|
|
3296
|
+
// Do not flush after console log messages
|
|
3297
|
+
return breadcrumb.category === 'console';
|
|
3298
|
+
});
|
|
3302
3299
|
});
|
|
3303
3300
|
}
|
|
3304
3301
|
|
|
@@ -4389,6 +4386,66 @@ function createEventBuffer({ useCompression }) {
|
|
|
4389
4386
|
return new EventBufferArray();
|
|
4390
4387
|
}
|
|
4391
4388
|
|
|
4389
|
+
/**
|
|
4390
|
+
* Given an initial timestamp and an expiry duration, checks to see if current
|
|
4391
|
+
* time should be considered as expired.
|
|
4392
|
+
*/
|
|
4393
|
+
function isExpired(
|
|
4394
|
+
initialTime,
|
|
4395
|
+
expiry,
|
|
4396
|
+
targetTime = +new Date(),
|
|
4397
|
+
) {
|
|
4398
|
+
// Always expired if < 0
|
|
4399
|
+
if (initialTime === null || expiry === undefined || expiry < 0) {
|
|
4400
|
+
return true;
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
// Never expires if == 0
|
|
4404
|
+
if (expiry === 0) {
|
|
4405
|
+
return false;
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
return initialTime + expiry <= targetTime;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
/** Check the state of the session. */
|
|
4412
|
+
function checkSessionState(
|
|
4413
|
+
session,
|
|
4414
|
+
recordingMode,
|
|
4415
|
+
timeouts,
|
|
4416
|
+
callbacks
|
|
4417
|
+
|
|
4418
|
+
,
|
|
4419
|
+
) {
|
|
4420
|
+
const _isIdle = () => {
|
|
4421
|
+
return isExpired(session.lastActivity, timeouts.sessionIdlePause);
|
|
4422
|
+
};
|
|
4423
|
+
|
|
4424
|
+
const _exceedsMaxLength = () => {
|
|
4425
|
+
return isExpired(session.started, timeouts.maxSessionLife);
|
|
4426
|
+
};
|
|
4427
|
+
|
|
4428
|
+
// When buffering, we never want to expire/end/pause/restart the recording
|
|
4429
|
+
if (recordingMode === 'buffer') {
|
|
4430
|
+
callbacks.onContinue();
|
|
4431
|
+
return;
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
if (_exceedsMaxLength()) {
|
|
4435
|
+
callbacks.onEnd();
|
|
4436
|
+
return;
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
if (_isIdle()) {
|
|
4440
|
+
callbacks.onPause();
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
callbacks.ensureResumed();
|
|
4445
|
+
|
|
4446
|
+
callbacks.onContinue();
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4392
4449
|
/** If sessionStorage is available. */
|
|
4393
4450
|
function hasSessionStorage() {
|
|
4394
4451
|
return 'sessionStorage' in WINDOW && !!WINDOW.sessionStorage;
|
|
@@ -4417,41 +4474,6 @@ function deleteSession() {
|
|
|
4417
4474
|
}
|
|
4418
4475
|
}
|
|
4419
4476
|
|
|
4420
|
-
/**
|
|
4421
|
-
* Given an initial timestamp and an expiry duration, checks to see if current
|
|
4422
|
-
* time should be considered as expired.
|
|
4423
|
-
*/
|
|
4424
|
-
function isExpired(
|
|
4425
|
-
initialTime,
|
|
4426
|
-
expiry,
|
|
4427
|
-
targetTime = +new Date(),
|
|
4428
|
-
) {
|
|
4429
|
-
// Always expired if < 0
|
|
4430
|
-
if (initialTime === null || expiry === undefined || expiry < 0) {
|
|
4431
|
-
return true;
|
|
4432
|
-
}
|
|
4433
|
-
|
|
4434
|
-
// Never expires if == 0
|
|
4435
|
-
if (expiry === 0) {
|
|
4436
|
-
return false;
|
|
4437
|
-
}
|
|
4438
|
-
|
|
4439
|
-
return initialTime + expiry <= targetTime;
|
|
4440
|
-
}
|
|
4441
|
-
|
|
4442
|
-
/**
|
|
4443
|
-
* Checks to see if session is expired
|
|
4444
|
-
*/
|
|
4445
|
-
function isSessionExpired(session, timeouts, targetTime = +new Date()) {
|
|
4446
|
-
return (
|
|
4447
|
-
// First, check that maximum session length has not been exceeded
|
|
4448
|
-
isExpired(session.started, timeouts.maxSessionLife, targetTime) ||
|
|
4449
|
-
// check that the idle timeout has not been exceeded (i.e. user has
|
|
4450
|
-
// performed an action within the last `sessionIdleExpire` ms)
|
|
4451
|
-
isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime)
|
|
4452
|
-
);
|
|
4453
|
-
}
|
|
4454
|
-
|
|
4455
4477
|
/**
|
|
4456
4478
|
* Given a sample rate, returns true if replay should be sampled.
|
|
4457
4479
|
*
|
|
@@ -4500,24 +4522,20 @@ function makeSession(session) {
|
|
|
4500
4522
|
lastActivity,
|
|
4501
4523
|
segmentId,
|
|
4502
4524
|
sampled,
|
|
4503
|
-
shouldRefresh: true,
|
|
4504
4525
|
};
|
|
4505
4526
|
}
|
|
4506
4527
|
|
|
4507
|
-
/**
|
|
4508
|
-
* Get the sampled status for a session based on sample rates & current sampled status.
|
|
4509
|
-
*/
|
|
4510
|
-
function getSessionSampleType(sessionSampleRate, allowBuffering) {
|
|
4511
|
-
return isSampled(sessionSampleRate) ? 'session' : allowBuffering ? 'buffer' : false;
|
|
4512
|
-
}
|
|
4513
|
-
|
|
4514
4528
|
/**
|
|
4515
4529
|
* Create a new session, which in its current implementation is a Sentry event
|
|
4516
4530
|
* that all replays will be saved to as attachments. Currently, we only expect
|
|
4517
4531
|
* one of these Sentry events per "replay session".
|
|
4518
4532
|
*/
|
|
4519
|
-
function createSession({
|
|
4520
|
-
|
|
4533
|
+
function createSession({
|
|
4534
|
+
sampled,
|
|
4535
|
+
stickySession = false,
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
) {
|
|
4521
4539
|
const session = makeSession({
|
|
4522
4540
|
sampled,
|
|
4523
4541
|
});
|
|
@@ -4555,47 +4573,31 @@ function fetchSession() {
|
|
|
4555
4573
|
}
|
|
4556
4574
|
}
|
|
4557
4575
|
|
|
4558
|
-
/**
|
|
4559
|
-
|
|
4560
|
-
*/
|
|
4561
|
-
function getSession({
|
|
4562
|
-
timeouts,
|
|
4563
|
-
currentSession,
|
|
4576
|
+
/** Either restore a session from sessionStorage, or create a new one. */
|
|
4577
|
+
function restoreOrCreateSession({
|
|
4564
4578
|
stickySession,
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
}
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
//
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
} else if (!session.shouldRefresh) {
|
|
4580
|
-
// This is the case if we have an error session that is completed
|
|
4581
|
-
// (=triggered an error). Session will continue as session-based replay,
|
|
4582
|
-
// and when this session is expired, it will not be renewed until user
|
|
4583
|
-
// reloads.
|
|
4584
|
-
const discardedSession = makeSession({ sampled: false });
|
|
4585
|
-
return { type: 'new', session: discardedSession };
|
|
4586
|
-
} else {
|
|
4587
|
-
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Session has expired');
|
|
4588
|
-
}
|
|
4589
|
-
// Otherwise continue to create a new session
|
|
4579
|
+
sampled,
|
|
4580
|
+
forceSampled,
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
) {
|
|
4584
|
+
const currentSession = stickySession && fetchSession();
|
|
4585
|
+
|
|
4586
|
+
if (currentSession && currentSession.sampled !== sampled && forceSampled) {
|
|
4587
|
+
// If for whatever reason the session from sessionStorage has a different sampling, we force to the new sampling
|
|
4588
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
|
|
4589
|
+
logger.log(`[Replay] Tried to restore session with different sampling: ${currentSession.sampled} !== ${sampled}`);
|
|
4590
|
+
} else if (currentSession) {
|
|
4591
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log(`[Replay] Loaded session from sessionStorage: id=${currentSession.id}`);
|
|
4592
|
+
return currentSession;
|
|
4590
4593
|
}
|
|
4591
4594
|
|
|
4592
4595
|
const newSession = createSession({
|
|
4593
4596
|
stickySession,
|
|
4594
|
-
|
|
4595
|
-
allowBuffering,
|
|
4597
|
+
sampled,
|
|
4596
4598
|
});
|
|
4597
4599
|
|
|
4598
|
-
return
|
|
4600
|
+
return newSession;
|
|
4599
4601
|
}
|
|
4600
4602
|
|
|
4601
4603
|
function isCustomEvent(event) {
|
|
@@ -4908,12 +4910,14 @@ function handleHistorySpanListener(replay) {
|
|
|
4908
4910
|
|
|
4909
4911
|
// Need to collect visited URLs
|
|
4910
4912
|
replay.getContext().urls.push(result.name);
|
|
4911
|
-
replay.
|
|
4913
|
+
replay.updateUserActivity();
|
|
4912
4914
|
|
|
4913
|
-
replay.
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4915
|
+
replay.checkSessionState(() => {
|
|
4916
|
+
replay.addUpdate(() => {
|
|
4917
|
+
createPerformanceSpans(replay, [result]);
|
|
4918
|
+
// Returning false to flush
|
|
4919
|
+
return false;
|
|
4920
|
+
});
|
|
4917
4921
|
});
|
|
4918
4922
|
};
|
|
4919
4923
|
}
|
|
@@ -6529,79 +6533,64 @@ function getHandleRecordingEmit(replay) {
|
|
|
6529
6533
|
let hadFirstEvent = false;
|
|
6530
6534
|
|
|
6531
6535
|
return (event, _isCheckout) => {
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
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
|
-
}
|
|
6554
|
-
|
|
6555
|
-
// We need to clear existing events on a checkout, otherwise they are
|
|
6556
|
-
// incremental event updates and should be appended
|
|
6557
|
-
void addEvent(replay, event, isCheckout);
|
|
6558
|
-
|
|
6559
|
-
// Different behavior for full snapshots (type=2), ignore other event types
|
|
6560
|
-
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
|
|
6561
|
-
if (!isCheckout) {
|
|
6562
|
-
return false;
|
|
6563
|
-
}
|
|
6536
|
+
replay.checkSessionState(() => {
|
|
6537
|
+
// `_isCheckout` is only set when the checkout is due to `checkoutEveryNms`
|
|
6538
|
+
// We also want to treat the first event as a checkout, so we handle this specifically here
|
|
6539
|
+
const isCheckout = _isCheckout || !hadFirstEvent;
|
|
6540
|
+
hadFirstEvent = true;
|
|
6541
|
+
|
|
6542
|
+
// The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.
|
|
6543
|
+
replay.addUpdate(() => {
|
|
6544
|
+
// The session is always started immediately on pageload/init, but for
|
|
6545
|
+
// error-only replays, it should reflect the most recent checkout
|
|
6546
|
+
// when an error occurs. Clear any state that happens before this current
|
|
6547
|
+
// checkout. This needs to happen before `addEvent()` which updates state
|
|
6548
|
+
// dependent on this reset.
|
|
6549
|
+
if (replay.recordingMode === 'buffer' && isCheckout) {
|
|
6550
|
+
replay.setInitialState();
|
|
6551
|
+
}
|
|
6564
6552
|
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
//
|
|
6569
|
-
// `isCheckout` is always true, but want to be explicit that it should
|
|
6570
|
-
// only be added for checkouts
|
|
6571
|
-
void addSettingsEvent(replay, isCheckout);
|
|
6572
|
-
|
|
6573
|
-
// If there is a previousSessionId after a full snapshot occurs, then
|
|
6574
|
-
// the replay session was started due to session expiration. The new session
|
|
6575
|
-
// is started before triggering a new checkout and contains the id
|
|
6576
|
-
// of the previous session. Do not immediately flush in this case
|
|
6577
|
-
// to avoid capturing only the checkout and instead the replay will
|
|
6578
|
-
// be captured if they perform any follow-up actions.
|
|
6579
|
-
if (replay.session && replay.session.previousSessionId) {
|
|
6580
|
-
return true;
|
|
6581
|
-
}
|
|
6553
|
+
// We need to clear existing events on a checkout, otherwise they are
|
|
6554
|
+
// incremental event updates and should be appended
|
|
6555
|
+
void addEvent(replay, event, isCheckout);
|
|
6582
6556
|
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
replay.session.started = earliestEvent;
|
|
6557
|
+
// Different behavior for full snapshots (type=2), ignore other event types
|
|
6558
|
+
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
|
|
6559
|
+
if (!isCheckout) {
|
|
6560
|
+
return false;
|
|
6561
|
+
}
|
|
6589
6562
|
|
|
6590
|
-
|
|
6591
|
-
|
|
6563
|
+
// Additionally, create a meta event that will capture certain SDK settings.
|
|
6564
|
+
// In order to handle buffer mode, this needs to either be done when we
|
|
6565
|
+
// receive checkout events or at flush time.
|
|
6566
|
+
//
|
|
6567
|
+
// `isCheckout` is always true, but want to be explicit that it should
|
|
6568
|
+
// only be added for checkouts
|
|
6569
|
+
void addSettingsEvent(replay, isCheckout);
|
|
6570
|
+
|
|
6571
|
+
// When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
|
|
6572
|
+
// this should usually be the timestamp of the checkout event, but to be safe...
|
|
6573
|
+
if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) {
|
|
6574
|
+
const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
|
|
6575
|
+
if (earliestEvent) {
|
|
6576
|
+
replay.session.started = earliestEvent;
|
|
6577
|
+
|
|
6578
|
+
if (replay.getOptions().stickySession) {
|
|
6579
|
+
saveSession(replay.session);
|
|
6580
|
+
}
|
|
6592
6581
|
}
|
|
6593
6582
|
}
|
|
6594
|
-
}
|
|
6595
6583
|
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6584
|
+
if (replay.recordingMode === 'session') {
|
|
6585
|
+
// If the full snapshot is due to an initial load, we will not have
|
|
6586
|
+
// a previous session ID. In this case, we want to buffer events
|
|
6587
|
+
// for a set amount of time before flushing. This can help avoid
|
|
6588
|
+
// capturing replays of users that immediately close the window.
|
|
6589
|
+
void replay.flush();
|
|
6590
|
+
}
|
|
6603
6591
|
|
|
6604
|
-
|
|
6592
|
+
return true;
|
|
6593
|
+
});
|
|
6605
6594
|
});
|
|
6606
6595
|
};
|
|
6607
6596
|
}
|
|
@@ -6646,6 +6635,26 @@ function addSettingsEvent(replay, isCheckout) {
|
|
|
6646
6635
|
return addEvent(replay, createOptionsEvent(replay), false);
|
|
6647
6636
|
}
|
|
6648
6637
|
|
|
6638
|
+
/**
|
|
6639
|
+
* Sample a session based on the provided sample rates.
|
|
6640
|
+
*/
|
|
6641
|
+
function sampleSession({
|
|
6642
|
+
errorSampleRate,
|
|
6643
|
+
sessionSampleRate,
|
|
6644
|
+
}
|
|
6645
|
+
|
|
6646
|
+
) {
|
|
6647
|
+
if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
|
|
6648
|
+
return false;
|
|
6649
|
+
}
|
|
6650
|
+
|
|
6651
|
+
if (isSampled(sessionSampleRate)) {
|
|
6652
|
+
return 'session';
|
|
6653
|
+
}
|
|
6654
|
+
|
|
6655
|
+
return errorSampleRate > 0 ? 'buffer' : false;
|
|
6656
|
+
}
|
|
6657
|
+
|
|
6649
6658
|
/**
|
|
6650
6659
|
* Create a replay envelope ready to be sent.
|
|
6651
6660
|
* This includes both the replay event, as well as the recording data.
|
|
@@ -7037,7 +7046,6 @@ class ReplayContainer {
|
|
|
7037
7046
|
*/
|
|
7038
7047
|
__init4() {this.timeouts = {
|
|
7039
7048
|
sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
|
|
7040
|
-
sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
|
|
7041
7049
|
maxSessionLife: MAX_SESSION_LIFE,
|
|
7042
7050
|
}; }
|
|
7043
7051
|
|
|
@@ -7049,35 +7057,30 @@ class ReplayContainer {
|
|
|
7049
7057
|
|
|
7050
7058
|
__init6() {this._flushLock = null;}
|
|
7051
7059
|
|
|
7052
|
-
/**
|
|
7053
|
-
* Timestamp of the last user activity. This lives across sessions.
|
|
7054
|
-
*/
|
|
7055
|
-
__init7() {this._lastActivity = Date.now();}
|
|
7056
|
-
|
|
7057
7060
|
/**
|
|
7058
7061
|
* Is the integration currently active?
|
|
7059
7062
|
*/
|
|
7060
|
-
|
|
7063
|
+
__init7() {this._isEnabled = false;}
|
|
7061
7064
|
|
|
7062
7065
|
/**
|
|
7063
7066
|
* Paused is a state where:
|
|
7064
7067
|
* - DOM Recording is not listening at all
|
|
7065
7068
|
* - Nothing will be added to event buffer (e.g. core SDK events)
|
|
7066
7069
|
*/
|
|
7067
|
-
|
|
7070
|
+
__init8() {this._isPaused = false;}
|
|
7068
7071
|
|
|
7069
7072
|
/**
|
|
7070
7073
|
* Have we attached listeners to the core SDK?
|
|
7071
7074
|
* Note we have to track this as there is no way to remove instrumentation handlers.
|
|
7072
7075
|
*/
|
|
7073
|
-
|
|
7076
|
+
__init9() {this._hasInitializedCoreListeners = false;}
|
|
7074
7077
|
|
|
7075
7078
|
/**
|
|
7076
7079
|
* Function to stop recording
|
|
7077
7080
|
*/
|
|
7078
|
-
|
|
7081
|
+
__init10() {this._stopRecording = null;}
|
|
7079
7082
|
|
|
7080
|
-
|
|
7083
|
+
__init11() {this._context = {
|
|
7081
7084
|
errorIds: new Set(),
|
|
7082
7085
|
traceIds: new Set(),
|
|
7083
7086
|
urls: [],
|
|
@@ -7090,7 +7093,7 @@ class ReplayContainer {
|
|
|
7090
7093
|
recordingOptions,
|
|
7091
7094
|
}
|
|
7092
7095
|
|
|
7093
|
-
) {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);
|
|
7096
|
+
) {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);
|
|
7094
7097
|
this._recordingOptions = recordingOptions;
|
|
7095
7098
|
this._options = options;
|
|
7096
7099
|
|
|
@@ -7148,37 +7151,13 @@ class ReplayContainer {
|
|
|
7148
7151
|
*/
|
|
7149
7152
|
initializeSampling() {
|
|
7150
7153
|
const { errorSampleRate, sessionSampleRate } = this._options;
|
|
7154
|
+
const sampled = sampleSession({ errorSampleRate, sessionSampleRate });
|
|
7151
7155
|
|
|
7152
|
-
|
|
7153
|
-
// `start()` or `startBuffering` themselves.
|
|
7154
|
-
if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
|
|
7155
|
-
return;
|
|
7156
|
-
}
|
|
7157
|
-
|
|
7158
|
-
// Otherwise if there is _any_ sample rate set, try to load an existing
|
|
7159
|
-
// session, or create a new one.
|
|
7160
|
-
const isSessionSampled = this._loadAndCheckSession();
|
|
7161
|
-
|
|
7162
|
-
if (!isSessionSampled) {
|
|
7163
|
-
// This should only occur if `errorSampleRate` is 0 and was unsampled for
|
|
7164
|
-
// session-based replay. In this case there is nothing to do.
|
|
7156
|
+
if (!sampled) {
|
|
7165
7157
|
return;
|
|
7166
7158
|
}
|
|
7167
7159
|
|
|
7168
|
-
|
|
7169
|
-
// This should not happen, something wrong has occurred
|
|
7170
|
-
this._handleException(new Error('Unable to initialize and create session'));
|
|
7171
|
-
return;
|
|
7172
|
-
}
|
|
7173
|
-
|
|
7174
|
-
if (this.session.sampled && this.session.sampled !== 'session') {
|
|
7175
|
-
// If not sampled as session-based, then recording mode will be `buffer`
|
|
7176
|
-
// Note that we don't explicitly check if `sampled === 'buffer'` because we
|
|
7177
|
-
// could have sessions from Session storage that are still `error` from
|
|
7178
|
-
// prior SDK version.
|
|
7179
|
-
this.recordingMode = 'buffer';
|
|
7180
|
-
}
|
|
7181
|
-
|
|
7160
|
+
this._initializeSession(sampled);
|
|
7182
7161
|
this._initializeRecording();
|
|
7183
7162
|
}
|
|
7184
7163
|
|
|
@@ -7198,20 +7177,7 @@ class ReplayContainer {
|
|
|
7198
7177
|
throw new Error('Replay buffering is in progress, call `flush()` to save the replay');
|
|
7199
7178
|
}
|
|
7200
7179
|
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
const { session } = getSession({
|
|
7204
|
-
timeouts: this.timeouts,
|
|
7205
|
-
stickySession: Boolean(this._options.stickySession),
|
|
7206
|
-
currentSession: this.session,
|
|
7207
|
-
// This is intentional: create a new session-based replay when calling `start()`
|
|
7208
|
-
sessionSampleRate: 1,
|
|
7209
|
-
allowBuffering: false,
|
|
7210
|
-
});
|
|
7211
|
-
|
|
7212
|
-
session.previousSessionId = previousSessionId;
|
|
7213
|
-
this.session = session;
|
|
7214
|
-
|
|
7180
|
+
this._initializeSession('session', { forceSampled: true });
|
|
7215
7181
|
this._initializeRecording();
|
|
7216
7182
|
}
|
|
7217
7183
|
|
|
@@ -7224,20 +7190,7 @@ class ReplayContainer {
|
|
|
7224
7190
|
throw new Error('Replay recording is already in progress');
|
|
7225
7191
|
}
|
|
7226
7192
|
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
const { session } = getSession({
|
|
7230
|
-
timeouts: this.timeouts,
|
|
7231
|
-
stickySession: Boolean(this._options.stickySession),
|
|
7232
|
-
currentSession: this.session,
|
|
7233
|
-
sessionSampleRate: 0,
|
|
7234
|
-
allowBuffering: true,
|
|
7235
|
-
});
|
|
7236
|
-
|
|
7237
|
-
session.previousSessionId = previousSessionId;
|
|
7238
|
-
this.session = session;
|
|
7239
|
-
|
|
7240
|
-
this.recordingMode = 'buffer';
|
|
7193
|
+
this._initializeSession('buffer', { forceSampled: true });
|
|
7241
7194
|
this._initializeRecording();
|
|
7242
7195
|
}
|
|
7243
7196
|
|
|
@@ -7319,8 +7272,7 @@ class ReplayContainer {
|
|
|
7319
7272
|
this.eventBuffer && this.eventBuffer.destroy();
|
|
7320
7273
|
this.eventBuffer = null;
|
|
7321
7274
|
|
|
7322
|
-
// Clear session from session storage
|
|
7323
|
-
// is started after, it will not have `previousSessionId`
|
|
7275
|
+
// Clear session from session storage
|
|
7324
7276
|
clearSession(this);
|
|
7325
7277
|
} catch (err) {
|
|
7326
7278
|
this._handleException(err);
|
|
@@ -7333,6 +7285,10 @@ class ReplayContainer {
|
|
|
7333
7285
|
* not as thorough of a shutdown as `stop()`.
|
|
7334
7286
|
*/
|
|
7335
7287
|
pause() {
|
|
7288
|
+
if (this.isPaused()) {
|
|
7289
|
+
return;
|
|
7290
|
+
}
|
|
7291
|
+
|
|
7336
7292
|
this._isPaused = true;
|
|
7337
7293
|
this.stopRecording();
|
|
7338
7294
|
}
|
|
@@ -7344,12 +7300,14 @@ class ReplayContainer {
|
|
|
7344
7300
|
* new DOM checkout.`
|
|
7345
7301
|
*/
|
|
7346
7302
|
resume() {
|
|
7347
|
-
if (!this.
|
|
7303
|
+
if (!this.isPaused()) {
|
|
7348
7304
|
return;
|
|
7349
7305
|
}
|
|
7350
7306
|
|
|
7351
7307
|
this._isPaused = false;
|
|
7352
|
-
this.
|
|
7308
|
+
this.checkSessionState(() => {
|
|
7309
|
+
this.startRecording();
|
|
7310
|
+
});
|
|
7353
7311
|
}
|
|
7354
7312
|
|
|
7355
7313
|
/**
|
|
@@ -7384,20 +7342,8 @@ class ReplayContainer {
|
|
|
7384
7342
|
// starting a new recording
|
|
7385
7343
|
this.recordingMode = 'session';
|
|
7386
7344
|
|
|
7387
|
-
// Once this session ends, we do not want to refresh it
|
|
7388
7345
|
if (this.session) {
|
|
7389
|
-
this.session.
|
|
7390
|
-
|
|
7391
|
-
// It's possible that the session lifespan is > max session lifespan
|
|
7392
|
-
// because we have been buffering beyond max session lifespan (we ignore
|
|
7393
|
-
// expiration given that `shouldRefresh` is true). Since we flip
|
|
7394
|
-
// `shouldRefresh`, the session could be considered expired due to
|
|
7395
|
-
// lifespan, which is not what we want. Update session start date to be
|
|
7396
|
-
// the current timestamp, so that session is not considered to be
|
|
7397
|
-
// expired. This means that max replay duration can be MAX_SESSION_LIFE +
|
|
7398
|
-
// (length of buffer), which we are ok with.
|
|
7399
|
-
this._updateUserActivity(activityTime);
|
|
7400
|
-
this._updateSessionActivity(activityTime);
|
|
7346
|
+
this.session.lastActivity = activityTime;
|
|
7401
7347
|
this.session.started = activityTime;
|
|
7402
7348
|
this._maybeSaveSession();
|
|
7403
7349
|
}
|
|
@@ -7435,42 +7381,9 @@ class ReplayContainer {
|
|
|
7435
7381
|
}
|
|
7436
7382
|
|
|
7437
7383
|
/**
|
|
7438
|
-
* Updates the user activity timestamp
|
|
7439
|
-
* called in an event handler for a user action that we consider as the user
|
|
7440
|
-
* being "active" (e.g. a mouse click).
|
|
7441
|
-
*/
|
|
7442
|
-
triggerUserActivity() {
|
|
7443
|
-
this._updateUserActivity();
|
|
7444
|
-
|
|
7445
|
-
// This case means that recording was once stopped due to inactivity.
|
|
7446
|
-
// Ensure that recording is resumed.
|
|
7447
|
-
if (!this._stopRecording) {
|
|
7448
|
-
// Create a new session, otherwise when the user action is flushed, it
|
|
7449
|
-
// will get rejected due to an expired session.
|
|
7450
|
-
if (!this._loadAndCheckSession()) {
|
|
7451
|
-
return;
|
|
7452
|
-
}
|
|
7453
|
-
|
|
7454
|
-
// Note: This will cause a new DOM checkout
|
|
7455
|
-
this.resume();
|
|
7456
|
-
return;
|
|
7457
|
-
}
|
|
7458
|
-
|
|
7459
|
-
// Otherwise... recording was never suspended, continue as normalish
|
|
7460
|
-
this.checkAndHandleExpiredSession();
|
|
7461
|
-
|
|
7462
|
-
this._updateSessionActivity();
|
|
7463
|
-
}
|
|
7464
|
-
|
|
7465
|
-
/**
|
|
7466
|
-
* Updates the user activity timestamp *without* resuming
|
|
7467
|
-
* recording. Some user events (e.g. keydown) can be create
|
|
7468
|
-
* low-value replays that only contain the keypress as a
|
|
7469
|
-
* breadcrumb. Instead this would require other events to
|
|
7470
|
-
* create a new replay after a session has expired.
|
|
7384
|
+
* Updates the user activity timestamp.
|
|
7471
7385
|
*/
|
|
7472
7386
|
updateUserActivity() {
|
|
7473
|
-
this._updateUserActivity();
|
|
7474
7387
|
this._updateSessionActivity();
|
|
7475
7388
|
}
|
|
7476
7389
|
|
|
@@ -7515,54 +7428,6 @@ class ReplayContainer {
|
|
|
7515
7428
|
return this.session && this.session.id;
|
|
7516
7429
|
}
|
|
7517
7430
|
|
|
7518
|
-
/**
|
|
7519
|
-
* Checks if recording should be stopped due to user inactivity. Otherwise
|
|
7520
|
-
* check if session is expired and create a new session if so. Triggers a new
|
|
7521
|
-
* full snapshot on new session.
|
|
7522
|
-
*
|
|
7523
|
-
* Returns true if session is not expired, false otherwise.
|
|
7524
|
-
* @hidden
|
|
7525
|
-
*/
|
|
7526
|
-
checkAndHandleExpiredSession() {
|
|
7527
|
-
const oldSessionId = this.getSessionId();
|
|
7528
|
-
|
|
7529
|
-
// Prevent starting a new session if the last user activity is older than
|
|
7530
|
-
// SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new
|
|
7531
|
-
// session+recording. This creates noisy replays that do not have much
|
|
7532
|
-
// content in them.
|
|
7533
|
-
if (
|
|
7534
|
-
this._lastActivity &&
|
|
7535
|
-
isExpired(this._lastActivity, this.timeouts.sessionIdlePause) &&
|
|
7536
|
-
this.session &&
|
|
7537
|
-
this.session.sampled === 'session'
|
|
7538
|
-
) {
|
|
7539
|
-
// Pause recording only for session-based replays. Otherwise, resuming
|
|
7540
|
-
// will create a new replay and will conflict with users who only choose
|
|
7541
|
-
// to record error-based replays only. (e.g. the resumed replay will not
|
|
7542
|
-
// contain a reference to an error)
|
|
7543
|
-
this.pause();
|
|
7544
|
-
return;
|
|
7545
|
-
}
|
|
7546
|
-
|
|
7547
|
-
// --- There is recent user activity --- //
|
|
7548
|
-
// This will create a new session if expired, based on expiry length
|
|
7549
|
-
if (!this._loadAndCheckSession()) {
|
|
7550
|
-
return;
|
|
7551
|
-
}
|
|
7552
|
-
|
|
7553
|
-
// Session was expired if session ids do not match
|
|
7554
|
-
const expired = oldSessionId !== this.getSessionId();
|
|
7555
|
-
|
|
7556
|
-
if (!expired) {
|
|
7557
|
-
return true;
|
|
7558
|
-
}
|
|
7559
|
-
|
|
7560
|
-
// Session is expired, trigger a full snapshot (which will create a new session)
|
|
7561
|
-
this._triggerFullSnapshot();
|
|
7562
|
-
|
|
7563
|
-
return false;
|
|
7564
|
-
}
|
|
7565
|
-
|
|
7566
7431
|
/**
|
|
7567
7432
|
* Capture some initial state that can change throughout the lifespan of the
|
|
7568
7433
|
* replay. This is required because otherwise they would be captured at the
|
|
@@ -7615,6 +7480,23 @@ class ReplayContainer {
|
|
|
7615
7480
|
return res;
|
|
7616
7481
|
}
|
|
7617
7482
|
|
|
7483
|
+
/**
|
|
7484
|
+
* Check the state/expiration of the session.
|
|
7485
|
+
* The callback is called when the session is neither paused nor expired.
|
|
7486
|
+
*/
|
|
7487
|
+
checkSessionState(onContinue) {
|
|
7488
|
+
if (!this.session || !this.session.sampled) {
|
|
7489
|
+
return;
|
|
7490
|
+
}
|
|
7491
|
+
|
|
7492
|
+
checkSessionState(this.session, this.recordingMode, this.timeouts, {
|
|
7493
|
+
onPause: () => this.pause(),
|
|
7494
|
+
ensureResumed: () => this.resume(),
|
|
7495
|
+
onEnd: () => this._refreshSession(),
|
|
7496
|
+
onContinue,
|
|
7497
|
+
});
|
|
7498
|
+
}
|
|
7499
|
+
|
|
7618
7500
|
/**
|
|
7619
7501
|
* This will get the parametrized route name of the current page.
|
|
7620
7502
|
* This is only available if performance is enabled, and if an instrumented router is used.
|
|
@@ -7628,6 +7510,31 @@ class ReplayContainer {
|
|
|
7628
7510
|
return lastTransaction.name;
|
|
7629
7511
|
}
|
|
7630
7512
|
|
|
7513
|
+
/**
|
|
7514
|
+
* Initialize a new session.
|
|
7515
|
+
*/
|
|
7516
|
+
_initializeSession(sampled, { forceSampled = false } = {}) {
|
|
7517
|
+
const { stickySession } = this._options;
|
|
7518
|
+
|
|
7519
|
+
// If neither sample rate is > 0, then do nothing - user will need to call one of
|
|
7520
|
+
// `start()` or `startBuffering` themselves.
|
|
7521
|
+
if (!sampled) {
|
|
7522
|
+
return;
|
|
7523
|
+
}
|
|
7524
|
+
|
|
7525
|
+
const session = restoreOrCreateSession({ stickySession, sampled, forceSampled });
|
|
7526
|
+
|
|
7527
|
+
// This shouldn't be possible really, but just in case...
|
|
7528
|
+
if (!session.sampled) {
|
|
7529
|
+
return;
|
|
7530
|
+
}
|
|
7531
|
+
|
|
7532
|
+
this.session = session;
|
|
7533
|
+
this.recordingMode = this.session.sampled === 'buffer' ? 'buffer' : 'session';
|
|
7534
|
+
|
|
7535
|
+
return;
|
|
7536
|
+
}
|
|
7537
|
+
|
|
7631
7538
|
/**
|
|
7632
7539
|
* Initialize and start all listeners to varying events (DOM,
|
|
7633
7540
|
* Performance Observer, Recording, Sentry SDK, etc)
|
|
@@ -7637,7 +7544,7 @@ class ReplayContainer {
|
|
|
7637
7544
|
|
|
7638
7545
|
// this method is generally called on page load or manually - in both cases
|
|
7639
7546
|
// we should treat it as an activity
|
|
7640
|
-
this.
|
|
7547
|
+
this.updateUserActivity();
|
|
7641
7548
|
|
|
7642
7549
|
this.eventBuffer = createEventBuffer({
|
|
7643
7550
|
useCompression: this._options.useCompression,
|
|
@@ -7648,6 +7555,7 @@ class ReplayContainer {
|
|
|
7648
7555
|
|
|
7649
7556
|
// Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout
|
|
7650
7557
|
this._isEnabled = true;
|
|
7558
|
+
this._isPaused = false;
|
|
7651
7559
|
|
|
7652
7560
|
this.startRecording();
|
|
7653
7561
|
}
|
|
@@ -7662,37 +7570,31 @@ class ReplayContainer {
|
|
|
7662
7570
|
}
|
|
7663
7571
|
|
|
7664
7572
|
/**
|
|
7665
|
-
*
|
|
7666
|
-
*
|
|
7573
|
+
* Refresh a session that has ended, either when it exceeded the max. age or when it was inactive for too long.
|
|
7574
|
+
* This means there was a sampled & sent session before - this will never be called while the session is buffering.
|
|
7667
7575
|
*/
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
currentSession: this.session,
|
|
7673
|
-
sessionSampleRate: this._options.sessionSampleRate,
|
|
7674
|
-
allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer',
|
|
7675
|
-
});
|
|
7676
|
-
|
|
7677
|
-
// If session was newly created (i.e. was not loaded from storage), then
|
|
7678
|
-
// enable flag to create the root replay
|
|
7679
|
-
if (type === 'new') {
|
|
7680
|
-
this.setInitialState();
|
|
7681
|
-
}
|
|
7682
|
-
|
|
7683
|
-
const currentSessionId = this.getSessionId();
|
|
7684
|
-
if (session.id !== currentSessionId) {
|
|
7685
|
-
session.previousSessionId = currentSessionId;
|
|
7576
|
+
_refreshSession() {
|
|
7577
|
+
// To avoid firing this multiple times, we check if we are even recording
|
|
7578
|
+
if (!this.isEnabled()) {
|
|
7579
|
+
return;
|
|
7686
7580
|
}
|
|
7687
7581
|
|
|
7688
|
-
|
|
7582
|
+
const { errorSampleRate, sessionSampleRate } = this._options;
|
|
7583
|
+
const sampled = sampleSession({ errorSampleRate, sessionSampleRate });
|
|
7689
7584
|
|
|
7690
|
-
if (!this.session
|
|
7691
|
-
void this.stop('session
|
|
7692
|
-
return
|
|
7585
|
+
if (!sampled || !this.session) {
|
|
7586
|
+
void this.stop('session expired without new session');
|
|
7587
|
+
return;
|
|
7693
7588
|
}
|
|
7694
7589
|
|
|
7695
|
-
|
|
7590
|
+
void this.stop('session expired with refreshing').then(() => {
|
|
7591
|
+
if (sampled === 'session') {
|
|
7592
|
+
return this.start();
|
|
7593
|
+
}
|
|
7594
|
+
if (sampled === 'buffer') {
|
|
7595
|
+
return this.startBuffering();
|
|
7596
|
+
}
|
|
7597
|
+
});
|
|
7696
7598
|
}
|
|
7697
7599
|
|
|
7698
7600
|
/**
|
|
@@ -7757,7 +7659,7 @@ class ReplayContainer {
|
|
|
7757
7659
|
* be hidden. Likewise, moving a different window to cover the contents of the
|
|
7758
7660
|
* page will also trigger a change to a hidden state.
|
|
7759
7661
|
*/
|
|
7760
|
-
|
|
7662
|
+
__init12() {this._handleVisibilityChange = () => {
|
|
7761
7663
|
if (WINDOW.document.visibilityState === 'visible') {
|
|
7762
7664
|
this._doChangeToForegroundTasks();
|
|
7763
7665
|
} else {
|
|
@@ -7768,7 +7670,7 @@ class ReplayContainer {
|
|
|
7768
7670
|
/**
|
|
7769
7671
|
* Handle when page is blurred
|
|
7770
7672
|
*/
|
|
7771
|
-
|
|
7673
|
+
__init13() {this._handleWindowBlur = () => {
|
|
7772
7674
|
const breadcrumb = createBreadcrumb({
|
|
7773
7675
|
category: 'ui.blur',
|
|
7774
7676
|
});
|
|
@@ -7781,7 +7683,7 @@ class ReplayContainer {
|
|
|
7781
7683
|
/**
|
|
7782
7684
|
* Handle when page is focused
|
|
7783
7685
|
*/
|
|
7784
|
-
|
|
7686
|
+
__init14() {this._handleWindowFocus = () => {
|
|
7785
7687
|
const breadcrumb = createBreadcrumb({
|
|
7786
7688
|
category: 'ui.focus',
|
|
7787
7689
|
});
|
|
@@ -7792,7 +7694,7 @@ class ReplayContainer {
|
|
|
7792
7694
|
};}
|
|
7793
7695
|
|
|
7794
7696
|
/** Ensure page remains active when a key is pressed. */
|
|
7795
|
-
|
|
7697
|
+
__init15() {this._handleKeyboardEvent = (event) => {
|
|
7796
7698
|
handleKeyboardEvent(this, event);
|
|
7797
7699
|
};}
|
|
7798
7700
|
|
|
@@ -7800,71 +7702,35 @@ class ReplayContainer {
|
|
|
7800
7702
|
* Tasks to run when we consider a page to be hidden (via blurring and/or visibility)
|
|
7801
7703
|
*/
|
|
7802
7704
|
_doChangeToBackgroundTasks(breadcrumb) {
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
const expired = isSessionExpired(this.session, this.timeouts);
|
|
7808
|
-
|
|
7809
|
-
if (breadcrumb && !expired) {
|
|
7810
|
-
this._createCustomBreadcrumb(breadcrumb);
|
|
7811
|
-
}
|
|
7705
|
+
this.checkSessionState(() => {
|
|
7706
|
+
if (breadcrumb) {
|
|
7707
|
+
this._createCustomBreadcrumb(breadcrumb);
|
|
7708
|
+
}
|
|
7812
7709
|
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7710
|
+
// Send replay when the page/tab becomes hidden. There is no reason to send
|
|
7711
|
+
// replay if it becomes visible, since no actions we care about were done
|
|
7712
|
+
// while it was hidden
|
|
7713
|
+
void this.conditionalFlush();
|
|
7714
|
+
});
|
|
7817
7715
|
}
|
|
7818
7716
|
|
|
7819
7717
|
/**
|
|
7820
7718
|
* Tasks to run when we consider a page to be visible (via focus and/or visibility)
|
|
7821
7719
|
*/
|
|
7822
7720
|
_doChangeToForegroundTasks(breadcrumb) {
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
if (!isSessionActive) {
|
|
7830
|
-
// If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION
|
|
7831
|
-
// ms, we will re-use the existing session, otherwise create a new
|
|
7832
|
-
// session
|
|
7833
|
-
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Document has become active, but session has expired');
|
|
7834
|
-
return;
|
|
7835
|
-
}
|
|
7836
|
-
|
|
7837
|
-
if (breadcrumb) {
|
|
7838
|
-
this._createCustomBreadcrumb(breadcrumb);
|
|
7839
|
-
}
|
|
7840
|
-
}
|
|
7841
|
-
|
|
7842
|
-
/**
|
|
7843
|
-
* Trigger rrweb to take a full snapshot which will cause this plugin to
|
|
7844
|
-
* create a new Replay event.
|
|
7845
|
-
*/
|
|
7846
|
-
_triggerFullSnapshot(checkout = true) {
|
|
7847
|
-
try {
|
|
7848
|
-
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Taking full rrweb snapshot');
|
|
7849
|
-
record.takeFullSnapshot(checkout);
|
|
7850
|
-
} catch (err) {
|
|
7851
|
-
this._handleException(err);
|
|
7852
|
-
}
|
|
7853
|
-
}
|
|
7854
|
-
|
|
7855
|
-
/**
|
|
7856
|
-
* Update user activity (across session lifespans)
|
|
7857
|
-
*/
|
|
7858
|
-
_updateUserActivity(_lastActivity = Date.now()) {
|
|
7859
|
-
this._lastActivity = _lastActivity;
|
|
7721
|
+
this.checkSessionState(() => {
|
|
7722
|
+
if (breadcrumb) {
|
|
7723
|
+
this._createCustomBreadcrumb(breadcrumb);
|
|
7724
|
+
}
|
|
7725
|
+
});
|
|
7860
7726
|
}
|
|
7861
7727
|
|
|
7862
7728
|
/**
|
|
7863
7729
|
* Updates the session's last activity timestamp
|
|
7864
7730
|
*/
|
|
7865
|
-
_updateSessionActivity(
|
|
7731
|
+
_updateSessionActivity(lastActivity = Date.now()) {
|
|
7866
7732
|
if (this.session) {
|
|
7867
|
-
this.session.lastActivity =
|
|
7733
|
+
this.session.lastActivity = lastActivity;
|
|
7868
7734
|
this._maybeSaveSession();
|
|
7869
7735
|
}
|
|
7870
7736
|
}
|
|
@@ -8015,7 +7881,7 @@ class ReplayContainer {
|
|
|
8015
7881
|
* Flush recording data to Sentry. Creates a lock so that only a single flush
|
|
8016
7882
|
* can be active at a time. Do not call this directly.
|
|
8017
7883
|
*/
|
|
8018
|
-
|
|
7884
|
+
__init16() {this._flush = async ({
|
|
8019
7885
|
force = false,
|
|
8020
7886
|
}
|
|
8021
7887
|
|
|
@@ -8025,42 +7891,58 @@ class ReplayContainer {
|
|
|
8025
7891
|
return;
|
|
8026
7892
|
}
|
|
8027
7893
|
|
|
8028
|
-
|
|
8029
|
-
(
|
|
8030
|
-
|
|
8031
|
-
|
|
7894
|
+
return new Promise(resolve => {
|
|
7895
|
+
if (!this.session) {
|
|
7896
|
+
resolve();
|
|
7897
|
+
return;
|
|
7898
|
+
}
|
|
7899
|
+
checkSessionState(this.session, this.recordingMode, this.timeouts, {
|
|
7900
|
+
onPause: () => {
|
|
7901
|
+
this.pause();
|
|
7902
|
+
resolve();
|
|
7903
|
+
},
|
|
7904
|
+
ensureResumed: () => this.resume(),
|
|
7905
|
+
onEnd: () => {
|
|
7906
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] Attempting to finish replay event after session expired.');
|
|
7907
|
+
this._refreshSession();
|
|
7908
|
+
resolve();
|
|
7909
|
+
},
|
|
7910
|
+
onContinue: () => {
|
|
7911
|
+
void this._flushLocked().then(resolve);
|
|
7912
|
+
},
|
|
7913
|
+
});
|
|
7914
|
+
});
|
|
7915
|
+
};}
|
|
8032
7916
|
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
7917
|
+
/**
|
|
7918
|
+
* Flush, while locking so any future flush attemps while this is ongoing are queued.
|
|
7919
|
+
*/
|
|
7920
|
+
async _flushLocked() {
|
|
7921
|
+
try {
|
|
7922
|
+
// A flush is about to happen, cancel any queued flushes
|
|
7923
|
+
this._debouncedFlush.cancel();
|
|
8037
7924
|
|
|
8038
|
-
|
|
8039
|
-
|
|
7925
|
+
// this._flushLock acts as a lock so that future calls to `_flush()`
|
|
7926
|
+
// will be blocked until this promise resolves
|
|
7927
|
+
if (!this._flushLock) {
|
|
7928
|
+
this._flushLock = this._runFlush();
|
|
7929
|
+
await this._flushLock;
|
|
7930
|
+
this._flushLock = null;
|
|
7931
|
+
return;
|
|
7932
|
+
}
|
|
8040
7933
|
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
7934
|
+
// Wait for previous flush to finish, then call the debounced `_flush()`.
|
|
7935
|
+
// It's possible there are other flush requests queued and waiting for it
|
|
7936
|
+
// to resolve. We want to reduce all outstanding requests (as well as any
|
|
7937
|
+
// new flush requests that occur within a second of the locked flush
|
|
7938
|
+
// completing) into a single flush.
|
|
8045
7939
|
await this._flushLock;
|
|
8046
|
-
|
|
8047
|
-
|
|
7940
|
+
} catch (error) {
|
|
7941
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(error);
|
|
8048
7942
|
}
|
|
8049
7943
|
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
// to resolve. We want to reduce all outstanding requests (as well as any
|
|
8053
|
-
// new flush requests that occur within a second of the locked flush
|
|
8054
|
-
// completing) into a single flush.
|
|
8055
|
-
|
|
8056
|
-
try {
|
|
8057
|
-
await this._flushLock;
|
|
8058
|
-
} catch (err) {
|
|
8059
|
-
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(err);
|
|
8060
|
-
} finally {
|
|
8061
|
-
this._debouncedFlush();
|
|
8062
|
-
}
|
|
8063
|
-
};}
|
|
7944
|
+
this._debouncedFlush();
|
|
7945
|
+
}
|
|
8064
7946
|
|
|
8065
7947
|
/** Save the session, if it is sticky */
|
|
8066
7948
|
_maybeSaveSession() {
|
|
@@ -8070,7 +7952,7 @@ class ReplayContainer {
|
|
|
8070
7952
|
}
|
|
8071
7953
|
|
|
8072
7954
|
/** Handler for rrweb.record.onMutation */
|
|
8073
|
-
|
|
7955
|
+
__init17() {this._onMutationHandler = (mutations) => {
|
|
8074
7956
|
const count = mutations.length;
|
|
8075
7957
|
|
|
8076
7958
|
const mutationLimit = this._options.mutationLimit;
|