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