Skip to content

Conversation

@s1gr1d
Copy link
Member

@s1gr1d s1gr1d commented Sep 12, 2025

Adds the trace lifecycle mode and sends profile_chunk envelopes. Also adds test for either overlapping root spans (one chunk) or single root spans (multiple chunks).

The "manual" mode comes in another PR to keep this from growing too large.

Browser trace-lifecycle profiler (v2):

  • Starts when the first sampled root span starts
  • Stops when the last sampled root span ends
  • While running, periodically stops and restarts the JS self-profiling API to collect chunks

Profiles are emitted as standalone profile_chunk envelopes either when:

  • there are no more sampled root spans, or
  • the 60s chunk timer elapses while profiling is running.

Handling never-ending root spans
In the trace lifecycle, profiling continues as long as a root span is active. To prevent profiling endlessly, each root span has its own profile timeout and is terminated if it is too long (5 minutes). If another root span is still active, profiling will continue regardless.

part of #17279


Note

Adds UI profiling trace lifecycle mode that samples sessions, streams profile_chunk envelopes, and attaches thread data, with accompanying tests and type options.

  • Browser Profiling (UI Profiling v2):
    • Add profileLifecycle: 'trace' with session sampling via profileSessionSampleRate; defaults lifecycle to manual when unspecified.
    • Stream profiling as profile_chunk envelopes; periodic chunking (60s) and 5‑min root-span timeout.
    • New BrowserTraceLifecycleProfiler manages start/stop across root spans and chunk sending.
    • Attach profiled thread data to events/spans; warn if trace mode without tracing.
  • Profiling Utils:
    • Convert JS self profile to continuous format; validate chunks; main/worker thread constants; helper to attach thread info.
    • Split legacy logic: hasLegacyProfiling, shouldProfileSpanLegacy, shouldProfileSession.
  • Integration Changes:
    • Browser integration branches between legacy and trace lifecycle; adds processEvent to attach thread data.
    • Minor fix in startProfileForSpan (processed profile handling).
  • Tests:
    • Add Playwright suites for trace lifecycle (multiple chunks, overlapping spans) and adjust legacy tests.
    • Add unit tests for lifecycle behavior, warnings, profiler_id reuse, and option defaults.
  • Types/Config:
    • Extend BrowserClientProfilingOptions with profileSessionSampleRate and profileLifecycle; refine Node types docs.
    • Size-limit: add entry for @sentry/browser incl. Tracing, Profiling (48 KB).

Written by Cursor Bugbot for commit 765f89d. This will update automatically on new commits. Configure here.

@s1gr1d s1gr1d requested a review from JonasBa September 12, 2025 11:00
@github-actions
Copy link
Contributor

github-actions bot commented Sep 12, 2025

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.
⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 8,966 - 9,492 -6%
GET With Sentry 1,431 16% 1,394 +3%
GET With Sentry (error only) 6,244 70% 6,107 +2%
POST Baseline 1,218 - 1,205 +1%
POST With Sentry 561 46% 528 +6%
POST With Sentry (error only) 1,081 89% 1,077 +0%
MYSQL Baseline 3,380 - 3,354 +1%
MYSQL With Sentry 483 14% 508 -5%
MYSQL With Sentry (error only) 2,740 81% 2,745 -0%

View base workflow run

Copy link
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I had some comments and remarks but most of them really are just nits about defensiveness in type checking. Feel free to apply them as you see fit. My general thinking here is that we don't have to be too defensive on types we defined in the SDK (like options, span ids, etc), as discussed in the call today.

}

// Adding client hook to attach profiles to transaction events before they are sent.
client.on('beforeSendEvent', attachProfiledThreadToEvent);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: is there a specific reason why we use beforeSendEvent rather than e.g. an event processor? My thinking is that by using beforeSendEvent, users cannot filter on the added data in beforeSend (because it's only added to the event after beforeSend). But no need to change if there's a reason!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, I added this because profiling-node is using the same approach:

this._client.on('beforeSendEvent', this._onBeforeSendThreadContextAssignment.bind(this));

But I can also add this via processEvent on the integration.

Comment on lines +149 to +154
const startTimeSec = (profile1.samples[0] as any).timestamp as number;
const endTimeSec = (profile1.samples[profile1.samples.length - 1] as any).timestamp as number;
const durationSec = endTimeSec - startTimeSec;

// Should be at least 20ms based on our setTimeout(21) in the test
expect(durationSec).toBeGreaterThan(0.2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: instead of relying on an inferred metric, you could just check if sample count > 1

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as other comment:

I'm checking the samples count: expect(profile1.samples.length).toBeGreaterThanOrEqual(2);
However, there are more than two samples because the fibonacci function is called a couple of times. So I added this additional check for the duration.

console.log('child span');
});

// Timeout to prevent flaky tests. Integration samples every 20ms, if function is too fast it might not get sampled
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my comment about relying on sample count above might mitigate this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm checking the samples count: expect(profile1.samples.length).toBeGreaterThanOrEqual(2);
However, there are more than two samples because the fibonacci function is called a couple of times. So I added this additional check for the duration.

`[Profiling] Root span ${spanJSON.description} ended. Active root spans: ${this._activeRootSpanCount}`,
);
if (this._activeRootSpanCount === 0) {
this._collectCurrentChunk().catch(() => /* no catch */ {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no catch?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I added logs in case it catches :)

}

this._activeRootSpanIds.delete(spanId);
this._activeRootSpanCount = Math.max(0, this._activeRootSpanCount - 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a debugLog here where you check that

if debug && activeRootSpanCount - 1 < 0 
	we have an SDK bug, span tracking is broken

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deleted _activeRootSpanCount now to just use this._activeRootSpanIds.size. I figured, I don't need the extra tracking.

};

export type BrowserClientProfilingOptions = {
// todo: add deprecation warning for profilesSampleRate: @deprecated Use `profileSessionSampleRate` and `profileLifecycle` instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still relevant?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would add the deprecation note after "manual" mode is working as well.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 30, 2025

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 24.64 kB - -
@sentry/browser - with treeshaking flags 23.14 kB - -
@sentry/browser (incl. Tracing) 40.99 kB - -
@sentry/browser (incl. Tracing, Replay) 79.31 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 68.99 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 84.02 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 96.17 kB - -
@sentry/browser (incl. Feedback) 41.33 kB - -
@sentry/browser (incl. sendFeedback) 29.3 kB - -
@sentry/browser (incl. FeedbackAsync) 34.26 kB - -
@sentry/react 26.35 kB - -
@sentry/react (incl. Tracing) 42.99 kB - -
@sentry/vue 29.13 kB - -
@sentry/vue (incl. Tracing) 42.79 kB - -
@sentry/svelte 24.66 kB - -
CDN Bundle 26.94 kB - -
CDN Bundle (incl. Tracing) 41.65 kB - -
CDN Bundle (incl. Tracing, Replay) 77.91 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 83.37 kB - -
CDN Bundle - uncompressed 78.95 kB - -
CDN Bundle (incl. Tracing) - uncompressed 123.53 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 238.57 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 251.33 kB - -
@sentry/nextjs (client) 45.13 kB - -
@sentry/sveltekit (client) 41.42 kB - -
@sentry/node-core 50.78 kB - -
@sentry/node 154.41 kB - -
@sentry/node - without tracing 92.66 kB - -
@sentry/aws-serverless 106.35 kB -0.01% -1 B 🔽
@sentry/browser (incl. Tracing, Profiling) 45.28 kB added added

View base workflow run

@s1gr1d s1gr1d requested review from JonasBa and Lms24 September 30, 2025 12:15
@s1gr1d s1gr1d marked this pull request as ready for review September 30, 2025 12:18
cursor[bot]

This comment was marked as outdated.

Copy link
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing my previous suggestions! Good to go from my end!

* - Presence of samples, stacks, frames
* - Required metadata fields
*/
export function validateProfileChunk(chunk: ProfileChunk): { valid: boolean; reason?: string } {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bundle size suggestion (feel free to ignore): we can probably save some more bytes here by only returning { valid: true } or {reason: string} (i.e. avoid returning valid: false a bunch of times).

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.


this.stop();
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Root Span Timeout Not Cleared

When a root span ends, its associated timeout is not cleared from _rootSpanTimeouts. This leaves the timeout active, potentially leading to memory or resource leaks and unnecessary callbacks.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the timeouts are cleared in stop

@s1gr1d s1gr1d merged commit d551d23 into develop Oct 21, 2025
194 checks passed
@s1gr1d s1gr1d deleted the sig/browserProfiling-newAPI branch October 21, 2025 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants