Skip to content

Commit 770ae30

Browse files
authored
fix(profiling): Guard from throwing if profiler constructor throws (#7328)
* fix(profiling): guard from throwing if profiler constructor throws * lint
1 parent 95a5f48 commit 770ae30

File tree

3 files changed

+142
-8
lines changed

3 files changed

+142
-8
lines changed

packages/browser/src/profiling/hubextensions.ts

+48-5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@ import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from
33
import { logger, uuid4 } from '@sentry/utils';
44

55
import { WINDOW } from '../helpers';
6-
import type { JSSelfProfile, JSSelfProfiler, ProcessedJSSelfProfile } from './jsSelfProfiling';
6+
import type {
7+
JSSelfProfile,
8+
JSSelfProfiler,
9+
JSSelfProfilerConstructor,
10+
ProcessedJSSelfProfile,
11+
} from './jsSelfProfiling';
712
import { sendProfile } from './sendProfile';
813

914
// Max profile duration.
1015
const MAX_PROFILE_DURATION_MS = 30_000;
16+
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
17+
// once, it will always fail and this allows us to early return.
18+
let PROFILING_CONSTRUCTOR_FAILED = false;
1119

1220
// While we experiment, per transaction sampling interval will be more flexible to work with.
1321
type StartTransaction = (
@@ -20,7 +28,7 @@ type StartTransaction = (
2028
* Check if profiler constructor is available.
2129
* @param maybeProfiler
2230
*/
23-
function isJSProfilerSupported(maybeProfiler: unknown): maybeProfiler is typeof JSSelfProfiler {
31+
function isJSProfilerSupported(maybeProfiler: unknown): maybeProfiler is typeof JSSelfProfilerConstructor {
2432
return typeof maybeProfiler === 'function';
2533
}
2634

@@ -49,8 +57,9 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
4957
*/
5058
function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
5159
// Feature support check first
52-
const JSProfiler = WINDOW.Profiler;
53-
if (!isJSProfilerSupported(JSProfiler)) {
60+
const JSProfilerConstructor = WINDOW.Profiler;
61+
62+
if (!isJSProfilerSupported(JSProfilerConstructor)) {
5463
if (__DEBUG_BUILD__) {
5564
logger.log(
5665
'[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.',
@@ -67,6 +76,14 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
6776
return transaction;
6877
}
6978

79+
// If constructor failed once, it will always fail, so we can early return.
80+
if (PROFILING_CONSTRUCTOR_FAILED) {
81+
if (__DEBUG_BUILD__) {
82+
logger.log('[Profiling] Profiling has been disabled for the duration of the current user session.');
83+
}
84+
return transaction;
85+
}
86+
7087
const client = getCurrentHub().getClient();
7188
const options = client && client.getOptions();
7289

@@ -91,7 +108,29 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
91108
const samplingIntervalMS = 10;
92109
// Start the profiler
93110
const maxSamples = Math.floor(MAX_PROFILE_DURATION_MS / samplingIntervalMS);
94-
const profiler = new JSProfiler({ sampleInterval: samplingIntervalMS, maxBufferSize: maxSamples });
111+
let profiler: JSSelfProfiler | undefined;
112+
113+
// Attempt to initialize the profiler constructor, if it fails, we disable profiling for the current user session.
114+
// This is likely due to a missing 'Document-Policy': 'js-profiling' header. We do not want to throw an error if this happens
115+
// as we risk breaking the user's application, so just disable profiling and log an error.
116+
try {
117+
profiler = new JSProfilerConstructor({ sampleInterval: samplingIntervalMS, maxBufferSize: maxSamples });
118+
} catch (e) {
119+
if (__DEBUG_BUILD__) {
120+
logger.log(
121+
"[Profiling] Failed to initialize the Profiling constructor, this is likely due to a missing 'Document-Policy': 'js-profiling' header.",
122+
);
123+
logger.log('[Profiling] Disabling profiling for current user session.');
124+
}
125+
PROFILING_CONSTRUCTOR_FAILED = true;
126+
}
127+
128+
// We failed to construct the profiler, fallback to original transaction - there is no need to log
129+
// anything as we already did that in the try/catch block.
130+
if (!profiler) {
131+
return transaction;
132+
}
133+
95134
if (__DEBUG_BUILD__) {
96135
logger.log(`[Profiling] started profiling transaction: ${transaction.name || transaction.description}`);
97136
}
@@ -118,6 +157,10 @@ function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
118157
if (!transaction) {
119158
return;
120159
}
160+
// Satisfy the type checker, but profiler will always be defined here.
161+
if (!profiler) {
162+
return;
163+
}
121164
if (processedProfile) {
122165
if (__DEBUG_BUILD__) {
123166
logger.log(

packages/browser/src/profiling/jsSelfProfiling.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,21 @@ export interface ProcessedJSSelfProfile extends JSSelfProfile {
3232

3333
type BufferFullCallback = (trace: JSSelfProfile) => void;
3434

35-
interface JSSelfProfiler {
35+
export interface JSSelfProfiler {
3636
sampleInterval: number;
3737
stopped: boolean;
3838

3939
stop: () => Promise<JSSelfProfile>;
4040
addEventListener(event: 'samplebufferfull', callback: BufferFullCallback): void;
4141
}
4242

43-
export declare const JSSelfProfiler: {
43+
export declare const JSSelfProfilerConstructor: {
4444
new (options: { sampleInterval: number; maxBufferSize: number }): JSSelfProfiler;
4545
};
4646

4747
declare global {
4848
interface Window {
49-
Profiler: typeof JSSelfProfiler | undefined;
49+
Profiler: typeof JSSelfProfilerConstructor | undefined;
5050
}
5151
}
5252

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { TextDecoder, TextEncoder } from 'util';
2+
// @ts-ignore patch the encoder on the window, else importing JSDOM fails (deleted in afterAll)
3+
const patchedEncoder = (!global.window.TextEncoder && (global.window.TextEncoder = TextEncoder)) || true;
4+
// @ts-ignore patch the encoder on the window, else importing JSDOM fails (deleted in afterAll)
5+
const patchedDecoder = (!global.window.TextDecoder && (global.window.TextDecoder = TextDecoder)) || true;
6+
7+
import { getCurrentHub } from '@sentry/core';
8+
import type { Transaction } from '@sentry/types';
9+
import { JSDOM } from 'jsdom';
10+
11+
import { onProfilingStartRouteTransaction } from '../../../src';
12+
13+
// @ts-ignore store a reference so we can reset it later
14+
const globalDocument = global.document;
15+
// @ts-ignore store a reference so we can reset it later
16+
const globalWindow = global.window;
17+
// @ts-ignore store a reference so we can reset it later
18+
const globalLocation = global.location;
19+
20+
describe('BrowserProfilingIntegration', () => {
21+
beforeEach(() => {
22+
const dom = new JSDOM();
23+
// @ts-ignore need to override global document
24+
global.document = dom.window.document;
25+
// @ts-ignore need to override global document
26+
global.window = dom.window;
27+
// @ts-ignore need to override global document
28+
global.location = dom.window.location;
29+
30+
const hub = getCurrentHub();
31+
const client: any = {
32+
getDsn() {
33+
return {};
34+
},
35+
getTransport() {
36+
return {
37+
send() {},
38+
};
39+
},
40+
getOptions() {
41+
return {
42+
profilesSampleRate: 1,
43+
};
44+
},
45+
};
46+
47+
hub.bindClient(client);
48+
});
49+
50+
// Reset back to previous values
51+
afterEach(() => {
52+
// @ts-ignore need to override global document
53+
global.document = globalDocument;
54+
// @ts-ignore need to override global document
55+
global.window = globalWindow;
56+
// @ts-ignore need to override global document
57+
global.location = globalLocation;
58+
});
59+
afterAll(() => {
60+
// @ts-ignore patch the encoder on the window, else importing JSDOM fails
61+
patchedEncoder && delete global.window.TextEncoder;
62+
// @ts-ignore patch the encoder on the window, else importing JSDOM fails
63+
patchedDecoder && delete global.window.TextDecoder;
64+
});
65+
66+
it('does not throw if Profiler is not available', () => {
67+
// @ts-ignore force api to be undefined
68+
global.window.Profiler = undefined;
69+
// set sampled to true so that profiling does not early return
70+
const mockTransaction = { sampled: true } as Transaction;
71+
expect(() => onProfilingStartRouteTransaction(mockTransaction)).not.toThrow();
72+
});
73+
it('does not throw if constructor throws', () => {
74+
const spy = jest.fn();
75+
76+
class Profiler {
77+
constructor() {
78+
spy();
79+
throw new Error('Profiler constructor error');
80+
}
81+
}
82+
83+
// set sampled to true so that profiling does not early return
84+
const mockTransaction = { sampled: true } as Transaction;
85+
86+
// @ts-ignore override with our own constructor
87+
global.window.Profiler = Profiler;
88+
expect(() => onProfilingStartRouteTransaction(mockTransaction)).not.toThrow();
89+
expect(spy).toHaveBeenCalled();
90+
});
91+
});

0 commit comments

Comments
 (0)