Skip to content

Commit 70598d6

Browse files
author
Luca Forstner
authored
Merge pull request #9361 from getsentry/prepare-release/7.75.1
meta(changelog): Update changelog for 7.75.1
2 parents 68660ac + 6fa0d7c commit 70598d6

File tree

7 files changed

+307
-144
lines changed

7 files changed

+307
-144
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
## 7.75.1
8+
9+
- feat(browser): Allow collecting of pageload profiles (#9317)
10+
- fix(browser): Correct timestamp on pageload profiles (#9350)
11+
- fix(nextjs): Use webpack plugin release value to inject release (#9348)
12+
713
## 7.75.0
814

915
### Important Changes

packages/browser/src/profiling/hubextensions.ts

Lines changed: 27 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
/* eslint-disable complexity */
2-
import { getCurrentHub } from '@sentry/core';
32
import type { Transaction } from '@sentry/types';
4-
import { logger, uuid4 } from '@sentry/utils';
3+
import { logger, timestampInSeconds, uuid4 } from '@sentry/utils';
54

65
import { WINDOW } from '../helpers';
7-
import type { JSSelfProfile, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling';
8-
import { addProfileToMap, isValidSampleRate } from './utils';
9-
10-
export const MAX_PROFILE_DURATION_MS = 30_000;
11-
// Keep a flag value to avoid re-initializing the profiler constructor. If it fails
12-
// once, it will always fail and this allows us to early return.
13-
let PROFILING_CONSTRUCTOR_FAILED = false;
14-
15-
/**
16-
* Check if profiler constructor is available.
17-
* @param maybeProfiler
18-
*/
19-
function isJSProfilerSupported(maybeProfiler: unknown): maybeProfiler is typeof JSSelfProfilerConstructor {
20-
return typeof maybeProfiler === 'function';
21-
}
6+
import type { JSSelfProfile } from './jsSelfProfiling';
7+
import {
8+
addProfileToGlobalCache,
9+
isAutomatedPageLoadTransaction,
10+
MAX_PROFILE_DURATION_MS,
11+
shouldProfileTransaction,
12+
startJSSelfProfile,
13+
} from './utils';
2214

2315
/**
2416
* Safety wrapper for startTransaction for the unlikely case that transaction starts before tracing is imported -
@@ -35,98 +27,29 @@ export function onProfilingStartRouteTransaction(transaction: Transaction | unde
3527
return transaction;
3628
}
3729

38-
return wrapTransactionWithProfiling(transaction);
30+
if (shouldProfileTransaction(transaction)) {
31+
return startProfileForTransaction(transaction);
32+
}
33+
34+
return transaction;
3935
}
4036

4137
/**
4238
* Wraps startTransaction and stopTransaction with profiling related logic.
43-
* startProfiling is called after the call to startTransaction in order to avoid our own code from
39+
* startProfileForTransaction is called after the call to startTransaction in order to avoid our own code from
4440
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
4541
*/
46-
export function wrapTransactionWithProfiling(transaction: Transaction): Transaction {
47-
// Feature support check first
48-
const JSProfilerConstructor = WINDOW.Profiler;
49-
50-
if (!isJSProfilerSupported(JSProfilerConstructor)) {
51-
if (__DEBUG_BUILD__) {
52-
logger.log(
53-
'[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.',
54-
);
55-
}
56-
return transaction;
57-
}
58-
59-
// If constructor failed once, it will always fail, so we can early return.
60-
if (PROFILING_CONSTRUCTOR_FAILED) {
61-
if (__DEBUG_BUILD__) {
62-
logger.log('[Profiling] Profiling has been disabled for the duration of the current user session.');
63-
}
64-
return transaction;
65-
}
66-
67-
const client = getCurrentHub().getClient();
68-
const options = client && client.getOptions();
69-
if (!options) {
70-
__DEBUG_BUILD__ && logger.log('[Profiling] Profiling disabled, no options found.');
71-
return transaction;
42+
export function startProfileForTransaction(transaction: Transaction): Transaction {
43+
// Start the profiler and get the profiler instance.
44+
let startTimestamp: number | undefined;
45+
if (isAutomatedPageLoadTransaction(transaction)) {
46+
startTimestamp = timestampInSeconds() * 1000;
7247
}
7348

74-
// @ts-expect-error profilesSampleRate is not part of the browser options yet
75-
const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate;
49+
const profiler = startJSSelfProfile();
7650

77-
// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
78-
// only valid values are booleans or numbers between 0 and 1.)
79-
if (!isValidSampleRate(profilesSampleRate)) {
80-
__DEBUG_BUILD__ && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
81-
return transaction;
82-
}
83-
84-
// if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
85-
if (!profilesSampleRate) {
86-
__DEBUG_BUILD__ &&
87-
logger.log(
88-
'[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
89-
);
90-
return transaction;
91-
}
92-
93-
// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
94-
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
95-
const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
96-
// Check if we should sample this profile
97-
if (!sampled) {
98-
__DEBUG_BUILD__ &&
99-
logger.log(
100-
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
101-
profilesSampleRate,
102-
)})`,
103-
);
104-
return transaction;
105-
}
106-
107-
// From initial testing, it seems that the minimum value for sampleInterval is 10ms.
108-
const samplingIntervalMS = 10;
109-
// Start the profiler
110-
const maxSamples = Math.floor(MAX_PROFILE_DURATION_MS / samplingIntervalMS);
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.
51+
// We failed to construct the profiler, fallback to original transaction.
52+
// No need to log anything as this has already been logged in startProfile.
13053
if (!profiler) {
13154
return transaction;
13255
}
@@ -172,19 +95,9 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
17295
return null;
17396
}
17497

175-
// This is temporary - we will use the collected span data to evaluate
176-
// if deferring txn.finish until profiler resolves is a viable approach.
177-
const stopProfilerSpan = transaction.startChild({
178-
description: 'profiler.stop',
179-
op: 'profiler',
180-
origin: 'auto.profiler.browser',
181-
});
182-
18398
return profiler
18499
.stop()
185-
.then((p: JSSelfProfile): null => {
186-
stopProfilerSpan.finish();
187-
100+
.then((profile: JSSelfProfile): null => {
188101
if (maxDurationTimeoutID) {
189102
WINDOW.clearTimeout(maxDurationTimeoutID);
190103
maxDurationTimeoutID = undefined;
@@ -195,7 +108,7 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
195108
}
196109

197110
// In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile.
198-
if (!p) {
111+
if (!profile) {
199112
if (__DEBUG_BUILD__) {
200113
logger.log(
201114
`[Profiling] profiler returned null profile for: ${transaction.name || transaction.description}`,
@@ -205,11 +118,10 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
205118
return null;
206119
}
207120

208-
addProfileToMap(profileId, p);
121+
addProfileToGlobalCache(profileId, profile);
209122
return null;
210123
})
211124
.catch(error => {
212-
stopProfilerSpan.finish();
213125
if (__DEBUG_BUILD__) {
214126
logger.log('[Profiling] error while stopping profiler:', error);
215127
}
@@ -245,7 +157,7 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact
245157
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
246158
void onProfileHandler().then(
247159
() => {
248-
transaction.setContext('profile', { profile_id: profileId });
160+
transaction.setContext('profile', { profile_id: profileId, start_timestamp: startTimestamp });
249161
originalFinish();
250162
},
251163
() => {

packages/browser/src/profiling/integration.ts

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/type
22
import type { Profile } from '@sentry/types/src/profiling';
33
import { logger } from '@sentry/utils';
44

5-
import type { BrowserClient } from './../client';
6-
import { wrapTransactionWithProfiling } from './hubextensions';
5+
import { startProfileForTransaction } from './hubextensions';
76
import type { ProfiledEvent } from './utils';
87
import {
98
addProfilesToEnvelope,
109
createProfilingEvent,
1110
findProfiledTransactionsFromEnvelope,
12-
PROFILE_MAP,
11+
getActiveProfilesCount,
12+
isAutomatedPageLoadTransaction,
13+
shouldProfileTransaction,
14+
takeProfileFromGlobalCache,
1315
} from './utils';
1416

1517
/**
@@ -37,16 +39,29 @@ export class BrowserProfilingIntegration implements Integration {
3739
*/
3840
public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
3941
this.getCurrentHub = getCurrentHub;
40-
const client = this.getCurrentHub().getClient() as BrowserClient;
42+
43+
const hub = this.getCurrentHub();
44+
const client = hub.getClient();
45+
const scope = hub.getScope();
46+
47+
const transaction = scope.getTransaction();
48+
49+
if (transaction && isAutomatedPageLoadTransaction(transaction)) {
50+
if (shouldProfileTransaction(transaction)) {
51+
startProfileForTransaction(transaction);
52+
}
53+
}
4154

4255
if (client && typeof client.on === 'function') {
4356
client.on('startTransaction', (transaction: Transaction) => {
44-
wrapTransactionWithProfiling(transaction);
57+
if (shouldProfileTransaction(transaction)) {
58+
startProfileForTransaction(transaction);
59+
}
4560
});
4661

4762
client.on('beforeEnvelope', (envelope): void => {
4863
// if not profiles are in queue, there is nothing to add to the envelope.
49-
if (!PROFILE_MAP['size']) {
64+
if (!getActiveProfilesCount()) {
5065
return;
5166
}
5267

@@ -59,7 +74,14 @@ export class BrowserProfilingIntegration implements Integration {
5974

6075
for (const profiledTransaction of profiledTransactionEvents) {
6176
const context = profiledTransaction && profiledTransaction.contexts;
62-
const profile_id = context && context['profile'] && (context['profile']['profile_id'] as string);
77+
const profile_id = context && context['profile'] && context['profile']['profile_id'];
78+
const start_timestamp = context && context['profile'] && context['profile']['start_timestamp'];
79+
80+
if (typeof profile_id !== 'string') {
81+
__DEBUG_BUILD__ &&
82+
logger.log('[Profiling] cannot find profile for a transaction without a profile context');
83+
continue;
84+
}
6385

6486
if (!profile_id) {
6587
__DEBUG_BUILD__ &&
@@ -72,15 +94,18 @@ export class BrowserProfilingIntegration implements Integration {
7294
delete context.profile;
7395
}
7496

75-
const profile = PROFILE_MAP.get(profile_id);
97+
const profile = takeProfileFromGlobalCache(profile_id);
7698
if (!profile) {
7799
__DEBUG_BUILD__ && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
78100
continue;
79101
}
80102

81-
PROFILE_MAP.delete(profile_id);
82-
const profileEvent = createProfilingEvent(profile_id, profile, profiledTransaction as ProfiledEvent);
83-
103+
const profileEvent = createProfilingEvent(
104+
profile_id,
105+
start_timestamp as number | undefined,
106+
profile,
107+
profiledTransaction as ProfiledEvent,
108+
);
84109
if (profileEvent) {
85110
profilesToAddToEnvelope.push(profileEvent);
86111
}

packages/browser/src/profiling/jsSelfProfiling.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,12 @@ export type JSSelfProfile = {
2626
samples: JSSelfProfileSample[];
2727
};
2828

29-
type BufferFullCallback = (trace: JSSelfProfile) => void;
30-
3129
export interface JSSelfProfiler {
3230
sampleInterval: number;
3331
stopped: boolean;
3432

3533
stop: () => Promise<JSSelfProfile>;
36-
addEventListener(event: 'samplebufferfull', callback: BufferFullCallback): void;
34+
addEventListener(event: 'samplebufferfull', callback: (trace: JSSelfProfile) => void): void;
3735
}
3836

3937
export declare const JSSelfProfilerConstructor: {

0 commit comments

Comments
 (0)