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