-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathhubextensions.ts
More file actions
259 lines (227 loc) · 9.75 KB
/
hubextensions.ts
File metadata and controls
259 lines (227 loc) · 9.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
import { getCurrentHub, getMainCarrier } from '@sentry/core';
import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types';
import { logger, uuid4 } from '@sentry/utils';
import { WINDOW } from '../helpers';
import type { JSSelfProfile, JSSelfProfiler, ProcessedJSSelfProfile } from './jsSelfProfiling';
import { sendProfile } from './sendProfile';
// Max profile duration.
const MAX_PROFILE_DURATION_MS = 30_000;
// While we experiment, per transaction sampling interval will be more flexible to work with.
type StartTransaction = (
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
) => Transaction | undefined;
/**
* Check if profiler constructor is available.
* @param maybeProfiler
*/
function isJSProfilerSupported(maybeProfiler: unknown): maybeProfiler is typeof JSSelfProfiler {
return typeof maybeProfiler === 'function';
}
/**
* Safety wrapper for startTransaction for the unlikely case that transaction starts before tracing is imported -
* if that happens we want to avoid throwing an error from profiling code.
* see https://github.com/getsentry/sentry-javascript/issues/4731.
*
* @experimental
*/
export function onProfilingStartRouteTransaction(transaction: Transaction | undefined): Transaction | undefined {
if (!transaction) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is undefined, skipping profiling');
}
return transaction;
}
return wrapTransactionWithProfiling(transaction);
}
/**
* Wraps startTransaction and stopTransaction with profiling related logic.
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
// Feature support check first
const JSProfiler = WINDOW.Profiler;
if (!isJSProfilerSupported(JSProfiler)) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.',
);
}
return transaction;
}
// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate.
if (!transaction.sampled) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is not sampled, skipping profiling');
}
return transaction;
}
const client = getCurrentHub().getClient();
const options = client && client.getOptions();
// @ts-ignore not part of the browser options yet
const profilesSampleRate = (options && options.profilesSampleRate) || 0;
if (profilesSampleRate === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.');
}
return transaction;
}
// Check if we should sample this profile
if (Math.random() > profilesSampleRate) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Skip profiling transaction due to sampling.');
}
return transaction;
}
// From initial testing, it seems that the minimum value for sampleInterval is 10ms.
const samplingIntervalMS = 10;
// Start the profiler
const maxSamples = Math.floor(MAX_PROFILE_DURATION_MS / samplingIntervalMS);
const profiler = new JSProfiler({ sampleInterval: samplingIntervalMS, maxBufferSize: maxSamples });
if (__DEBUG_BUILD__) {
logger.log(`[Profiling] started profiling transaction: ${transaction.name || transaction.description}`);
}
// We create "unique" transaction names to avoid concurrent transactions with same names
// from being ignored by the profiler. From here on, only this transaction name should be used when
// calling the profiler methods. Note: we log the original name to the user to avoid confusion.
const profileId = uuid4();
// A couple of important things to note here:
// `CpuProfilerBindings.stopProfiling` will be scheduled to run in 30seconds in order to exceed max profile duration.
// Whichever of the two (transaction.finish/timeout) is first to run, the profiling will be stopped and the gathered profile
// will be processed when the original transaction is finished. Since onProfileHandler can be invoked multiple times in the
// event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler
// is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler.
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
let processedProfile: ProcessedJSSelfProfile | null = null;
/**
* Idempotent handler for profile stop
*/
function onProfileHandler(): void {
// Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
if (!transaction) {
return;
}
if (processedProfile) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] profile for:',
transaction.name || transaction.description,
'already exists, returning early',
);
}
return;
}
profiler
.stop()
.then((p: JSSelfProfile): void => {
if (maxDurationTimeoutID) {
WINDOW.clearTimeout(maxDurationTimeoutID);
maxDurationTimeoutID = undefined;
}
if (__DEBUG_BUILD__) {
logger.log(`[Profiling] stopped profiling of transaction: ${transaction.name || transaction.description}`);
}
// In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile.
if (!p) {
if (__DEBUG_BUILD__) {
logger.log(
`[Profiling] profiler returned null profile for: ${transaction.name || transaction.description}`,
'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started',
);
}
return;
}
// If a profile has less than 2 samples, it is not useful and should be discarded.
if (p.samples.length < 2) {
return;
}
processedProfile = { ...p, profile_id: profileId };
sendProfile(profileId, processedProfile);
})
.catch(error => {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] error while stopping profiler:', error);
}
return null;
});
}
// Enqueue a timeout to prevent profiles from running over max duration.
let maxDurationTimeoutID: number | undefined = WINDOW.setTimeout(() => {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] max profile duration elapsed, stopping profiling for:',
transaction.name || transaction.description,
);
}
void onProfileHandler();
}, MAX_PROFILE_DURATION_MS);
// We need to reference the original finish call to avoid creating an infinite loop
const originalFinish = transaction.finish.bind(transaction);
/**
* Wraps startTransaction and stopTransaction with profiling related logic.
* startProfiling is called after the call to startTransaction in order to avoid our own code from
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
*/
function profilingWrappedTransactionFinish(): Promise<Transaction> {
if (!transaction) {
return originalFinish();
}
// onProfileHandler should always return the same profile even if this is called multiple times.
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
onProfileHandler();
// Set profile context
transaction.setContext('profile', { profile_id: profileId });
return originalFinish();
}
transaction.finish = profilingWrappedTransactionFinish;
return transaction;
}
/**
* Wraps startTransaction with profiling logic. This is done automatically by the profiling integration.
*/
function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction {
return function wrappedStartTransaction(
this: Hub,
transactionContext: TransactionContext,
customSamplingContext?: CustomSamplingContext,
): Transaction | undefined {
const transaction: Transaction | undefined = startTransaction.call(this, transactionContext, customSamplingContext);
if (transaction === undefined) {
if (__DEBUG_BUILD__) {
logger.log('[Profiling] Transaction is undefined, skipping profiling');
}
return transaction;
}
return wrapTransactionWithProfiling(transaction);
};
}
/**
* Patches startTransaction and stopTransaction with profiling logic.
*/
export function addProfilingExtensionMethods(): void {
const carrier = getMainCarrier();
if (!carrier.__SENTRY__) {
if (__DEBUG_BUILD__) {
logger.log("[Profiling] Can't find main carrier, profiling won't work.");
}
return;
}
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};
if (!carrier.__SENTRY__.extensions['startTransaction']) {
if (__DEBUG_BUILD__) {
logger.log(
'[Profiling] startTransaction does not exists, profiling will not work. Make sure you import @sentry/tracing package before @sentry/profiling-node as import order matters.',
);
}
return;
}
if (__DEBUG_BUILD__) {
logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...');
}
carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling(
// This is already patched by sentry/tracing, we are going to re-patch it...
carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction,
);
}