Skip to content

Commit a42e2db

Browse files
Merge branch 'develop' into kw-rn-profiling-share-code
2 parents 8f3e774 + 05cf332 commit a42e2db

File tree

18 files changed

+338
-58
lines changed

18 files changed

+338
-58
lines changed

packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ window.Sentry = Sentry;
44
window.Replay = new Sentry.Replay({
55
flushMinDelay: 500,
66
flushMaxDelay: 500,
7-
_experiments: {
8-
mutationLimit: 250,
9-
},
7+
mutationLimit: 250,
108
});
119

1210
Sentry.init({

packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { expect } from '@playwright/test';
22

33
import { sentryTest } from '../../../../utils/fixtures';
4-
import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';
4+
import {
5+
getReplayRecordingContent,
6+
getReplaySnapshot,
7+
shouldSkipReplayTest,
8+
waitForReplayRequest,
9+
} from '../../../../utils/replayHelpers';
510

611
sentryTest(
7-
'handles large mutations with _experiments.mutationLimit configured',
12+
'handles large mutations by stopping replay when `mutationLimit` configured',
813
async ({ getLocalTestPath, page, forceFlushReplay, browserName }) => {
914
if (shouldSkipReplayTest() || ['webkit', 'firefox'].includes(browserName)) {
1015
sentryTest.skip();
@@ -34,36 +39,29 @@ sentryTest(
3439
await forceFlushReplay();
3540
const res1 = await reqPromise1;
3641

37-
const reqPromise2 = waitForReplayRequest(page);
42+
// replay should be stopped due to mutation limit
43+
let replay = await getReplaySnapshot(page);
44+
expect(replay.session).toBe(undefined);
45+
expect(replay._isEnabled).toBe(false);
3846

3947
void page.click('#button-modify');
4048
await forceFlushReplay();
41-
const res2 = await reqPromise2;
4249

43-
const reqPromise3 = waitForReplayRequest(page);
44-
45-
void page.click('#button-remove');
50+
await page.click('#button-remove');
4651
await forceFlushReplay();
47-
const res3 = await reqPromise3;
4852

4953
const replayData0 = getReplayRecordingContent(res0);
50-
const replayData1 = getReplayRecordingContent(res1);
51-
const replayData2 = getReplayRecordingContent(res2);
52-
const replayData3 = getReplayRecordingContent(res3);
53-
5454
expect(replayData0.fullSnapshots.length).toBe(1);
5555
expect(replayData0.incrementalSnapshots.length).toBe(0);
5656

57-
// This includes both a full snapshot as well as some incremental snapshots
58-
expect(replayData1.fullSnapshots.length).toBe(1);
57+
// Breadcrumbs (click and mutation);
58+
const replayData1 = getReplayRecordingContent(res1);
59+
expect(replayData1.fullSnapshots.length).toBe(0);
5960
expect(replayData1.incrementalSnapshots.length).toBeGreaterThan(0);
61+
expect(replayData1.breadcrumbs.map(({ category }) => category).sort()).toEqual(['replay.mutations', 'ui.click']);
6062

61-
// This does not trigger mutations, for whatever reason - so no full snapshot either!
62-
expect(replayData2.fullSnapshots.length).toBe(0);
63-
expect(replayData2.incrementalSnapshots.length).toBeGreaterThan(0);
64-
65-
// This includes both a full snapshot as well as some incremental snapshots
66-
expect(replayData3.fullSnapshots.length).toBe(1);
67-
expect(replayData3.incrementalSnapshots.length).toBeGreaterThan(0);
63+
replay = await getReplaySnapshot(page);
64+
expect(replay.session).toBe(undefined);
65+
expect(replay._isEnabled).toBe(false);
6866
},
6967
);

packages/replay/src/integration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export class Replay implements Integration {
6060
maskAllInputs = true,
6161
blockAllMedia = true,
6262

63+
mutationBreadcrumbLimit = 750,
64+
mutationLimit = 10_000,
65+
6366
networkDetailAllowUrls = [],
6467
networkCaptureBodies = true,
6568
networkRequestHeaders = [],
@@ -127,6 +130,8 @@ export class Replay implements Integration {
127130
blockAllMedia,
128131
maskAllInputs,
129132
maskAllText,
133+
mutationBreadcrumbLimit,
134+
mutationLimit,
130135
networkDetailAllowUrls,
131136
networkCaptureBodies,
132137
networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders),

packages/replay/src/replay.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-lines */ // TODO: We might want to split this file up
22
import { EventType, record } from '@sentry-internal/rrweb';
33
import { captureException, getCurrentHub } from '@sentry/core';
4-
import type { Breadcrumb, ReplayRecordingMode } from '@sentry/types';
4+
import type { Breadcrumb, ReplayRecordingMode, Transaction } from '@sentry/types';
55
import { logger } from '@sentry/utils';
66

77
import {
@@ -68,6 +68,12 @@ export class ReplayContainer implements ReplayContainerInterface {
6868
*/
6969
public recordingMode: ReplayRecordingMode = 'session';
7070

71+
/**
72+
* The current or last active transcation.
73+
* This is only available when performance is enabled.
74+
*/
75+
public lastTransaction?: Transaction;
76+
7177
/**
7278
* These are here so we can overwrite them in tests etc.
7379
* @hidden
@@ -614,6 +620,19 @@ export class ReplayContainer implements ReplayContainerInterface {
614620
return res;
615621
}
616622

623+
/**
624+
* This will get the parametrized route name of the current page.
625+
* This is only available if performance is enabled, and if an instrumented router is used.
626+
*/
627+
public getCurrentRoute(): string | undefined {
628+
const lastTransaction = this.lastTransaction || getCurrentHub().getScope().getTransaction();
629+
if (!lastTransaction || !['route', 'custom'].includes(lastTransaction.metadata.source)) {
630+
return undefined;
631+
}
632+
633+
return lastTransaction.name;
634+
}
635+
617636
/**
618637
* Initialize and start all listeners to varying events (DOM,
619638
* Performance Observer, Recording, Sentry SDK, etc)
@@ -1056,8 +1075,8 @@ export class ReplayContainer implements ReplayContainerInterface {
10561075
private _onMutationHandler = (mutations: unknown[]): boolean => {
10571076
const count = mutations.length;
10581077

1059-
const mutationLimit = this._options._experiments.mutationLimit || 0;
1060-
const mutationBreadcrumbLimit = this._options._experiments.mutationBreadcrumbLimit || 1000;
1078+
const mutationLimit = this._options.mutationLimit;
1079+
const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit;
10611080
const overMutationLimit = mutationLimit && count > mutationLimit;
10621081

10631082
// Create a breadcrumb if a lot of mutations happen at the same time
@@ -1067,15 +1086,15 @@ export class ReplayContainer implements ReplayContainerInterface {
10671086
category: 'replay.mutations',
10681087
data: {
10691088
count,
1089+
limit: overMutationLimit,
10701090
},
10711091
});
10721092
this._createCustomBreadcrumb(breadcrumb);
10731093
}
10741094

1095+
// Stop replay if over the mutation limit
10751096
if (overMutationLimit) {
1076-
// We want to skip doing an incremental snapshot if there are too many mutations
1077-
// Instead, we do a full snapshot
1078-
this._triggerFullSnapshot(false);
1097+
void this.stop('mutationLimit');
10791098
return false;
10801099
}
10811100

packages/replay/src/types.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
ReplayRecordingData,
55
ReplayRecordingMode,
66
SentryWrappedXMLHttpRequest,
7+
Transaction,
78
XhrBreadcrumbHint,
89
} from '@sentry/types';
910

@@ -272,6 +273,20 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
272273
*/
273274
maskAllText: boolean;
274275

276+
/**
277+
* A high number of DOM mutations (in a single event loop) can cause
278+
* performance regressions in end-users' browsers. This setting will create
279+
* a breadcrumb in the recording when the limit has been reached.
280+
*/
281+
mutationBreadcrumbLimit: number;
282+
283+
/**
284+
* A high number of DOM mutations (in a single event loop) can cause
285+
* performance regressions in end-users' browsers. This setting will cause
286+
* recording to stop when the limit has been reached.
287+
*/
288+
mutationLimit: number;
289+
275290
/**
276291
* Callback before adding a custom recording event
277292
*
@@ -295,8 +310,6 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
295310
_experiments: Partial<{
296311
captureExceptions: boolean;
297312
traceInternals: boolean;
298-
mutationLimit: number;
299-
mutationBreadcrumbLimit: number;
300313
slowClicks: {
301314
threshold: number;
302315
timeout: number;
@@ -523,6 +536,7 @@ export interface ReplayContainer {
523536
session: Session | undefined;
524537
recordingMode: ReplayRecordingMode;
525538
timeouts: Timeouts;
539+
lastTransaction?: Transaction;
526540
throttledAddEvent: (
527541
event: RecordingEvent,
528542
isCheckout?: boolean,
@@ -547,6 +561,7 @@ export interface ReplayContainer {
547561
getSessionId(): string | undefined;
548562
checkAndHandleExpiredSession(): boolean | void;
549563
setInitialState(): void;
564+
getCurrentRoute(): string | undefined;
550565
}
551566

552567
export interface ReplayPerformanceEntry<T> {

packages/replay/src/util/addGlobalListeners.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ export function addGlobalListeners(replay: ReplayContainer): void {
4040
dsc.replay_id = replayId;
4141
}
4242
});
43+
44+
client.on('startTransaction', transaction => {
45+
replay.lastTransaction = transaction;
46+
});
47+
48+
// We may be missing the initial startTransaction due to timing issues,
49+
// so we capture it on finish again.
50+
client.on('finishTransaction', transaction => {
51+
replay.lastTransaction = transaction;
52+
});
4353
}
4454
}
4555

packages/replay/test/utils/setupReplayContainer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const DEFAULT_OPTIONS = {
1515
networkCaptureBodies: true,
1616
networkRequestHeaders: [],
1717
networkResponseHeaders: [],
18+
mutationLimit: 1500,
19+
mutationBreadcrumbLimit: 500,
1820
_experiments: {},
1921
};
2022

@@ -54,6 +56,8 @@ export const DEFAULT_OPTIONS_EVENT_PAYLOAD = {
5456
maskAllText: false,
5557
maskAllInputs: false,
5658
useCompression: DEFAULT_OPTIONS.useCompression,
59+
mutationLimit: DEFAULT_OPTIONS.mutationLimit,
60+
mutationBreadcrumbLimit: DEFAULT_OPTIONS.mutationBreadcrumbLimit,
5761
networkDetailHasUrls: DEFAULT_OPTIONS.networkDetailAllowUrls.length > 0,
5862
networkCaptureBodies: DEFAULT_OPTIONS.networkCaptureBodies,
5963
networkRequestHeaders: DEFAULT_OPTIONS.networkRequestHeaders.length > 0,

packages/sveltekit/.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ module.exports = {
1717
project: ['tsconfig.test.json'],
1818
},
1919
},
20+
{
21+
files: ['src/vite/**', 'src/server/**'],
22+
rules: {
23+
'@sentry-internal/sdk/no-optional-chaining': 'off',
24+
},
25+
},
2026
],
2127
extends: ['../../.eslintrc.js'],
2228
};

packages/sveltekit/src/server/utils.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import type { DynamicSamplingContext, StackFrame, TraceparentData } from '@sentry/types';
2-
import { baggageHeaderToDynamicSamplingContext, basename, extractTraceparentData } from '@sentry/utils';
2+
import {
3+
baggageHeaderToDynamicSamplingContext,
4+
basename,
5+
escapeStringForRegex,
6+
extractTraceparentData,
7+
GLOBAL_OBJ,
8+
join,
9+
} from '@sentry/utils';
310
import type { RequestEvent } from '@sveltejs/kit';
411

512
import { WRAPPED_MODULE_SUFFIX } from '../vite/autoInstrument';
13+
import type { GlobalWithSentryValues } from '../vite/injectGlobalValues';
614

715
/**
816
* Takes a request event and extracts traceparent and DSC data
@@ -35,7 +43,8 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame {
3543
if (!frame.filename) {
3644
return frame;
3745
}
38-
46+
const globalWithSentryValues: GlobalWithSentryValues = GLOBAL_OBJ;
47+
const svelteKitBuildOutDir = globalWithSentryValues.__sentry_sveltekit_output_dir;
3948
const prefix = 'app:///';
4049

4150
// Check if the frame filename begins with `/` or a Windows-style prefix such as `C:\`
@@ -48,8 +57,16 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame {
4857
.replace(/\\/g, '/') // replace all `\\` instances with `/`
4958
: frame.filename;
5059

51-
const base = basename(filename);
52-
frame.filename = `${prefix}${base}`;
60+
let strippedFilename;
61+
if (svelteKitBuildOutDir) {
62+
strippedFilename = filename.replace(
63+
new RegExp(`^.*${escapeStringForRegex(join(svelteKitBuildOutDir, 'server'))}/`),
64+
'',
65+
);
66+
} else {
67+
strippedFilename = basename(filename);
68+
}
69+
frame.filename = `${prefix}${strippedFilename}`;
5370
}
5471

5572
delete frame.module;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { InternalGlobal } from '@sentry/utils';
2+
3+
export type GlobalSentryValues = {
4+
__sentry_sveltekit_output_dir?: string;
5+
};
6+
7+
/**
8+
* Extend the `global` type with custom properties that are
9+
* injected by the SvelteKit SDK at build time.
10+
* @see packages/sveltekit/src/vite/sourcemaps.ts
11+
*/
12+
export type GlobalWithSentryValues = InternalGlobal & GlobalSentryValues;
13+
14+
export const VIRTUAL_GLOBAL_VALUES_FILE = '\0sentry-inject-global-values-file';
15+
16+
/**
17+
* @returns code that injects @param globalSentryValues into the global object.
18+
*/
19+
export function getGlobalValueInjectionCode(globalSentryValues: GlobalSentryValues): string {
20+
if (Object.keys(globalSentryValues).length === 0) {
21+
return '';
22+
}
23+
24+
const sentryGlobal = '_global';
25+
26+
const globalCode = `var ${sentryGlobal} =
27+
typeof window !== 'undefined' ?
28+
window :
29+
typeof globalThis !== 'undefined' ?
30+
globalThis :
31+
typeof global !== 'undefined' ?
32+
global :
33+
typeof self !== 'undefined' ?
34+
self :
35+
{};`;
36+
const injectedValuesCode = Object.entries(globalSentryValues)
37+
.map(([key, value]) => `${sentryGlobal}["${key}"] = ${JSON.stringify(value)};`)
38+
.join('\n');
39+
40+
return `${globalCode}\n${injectedValuesCode}\n`;
41+
}

packages/sveltekit/src/vite/sentryVitePlugins.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}
103103
const pluginOptions = {
104104
...mergedOptions.sourceMapsUploadOptions,
105105
debug: mergedOptions.debug, // override the plugin's debug flag with the one from the top-level options
106+
adapter: mergedOptions.adapter,
106107
};
107108
sentryPlugins.push(await makeCustomSentryVitePlugin(pluginOptions));
108109
}

0 commit comments

Comments
 (0)