When before_send_transaction keeps transactions at known per-duration-bucket rates (e.g. 100% of >=5s, 50% of 2-5s, 10% of 1-2s, 0.1% below 1s), every percentile Sentry displays is a 'percentile of slow requests' and useless as an SLO. But the true distribution is recoverable because the keep-rates are known: query counts per bucket and multiply by the inverse keep-rate.
Per transaction name, run four count queries against /organizations/{org}/events/?dataset=discover:
transaction.duration:<1000-> count x1000transaction.duration:>=1000 transaction.duration:<2000-> x10transaction.duration:>=2000 transaction.duration:<5000-> x2transaction.duration:>=5000-> x1 (exact)
The >=5s bucket is EXACT (100% kept), so 'requests/day exceeding N seconds' is a real number even under heavy sampling — that is the metric to alert/trend on. Example yield: an endpoint whose sampled p50 read 14.6s turned out to be ~95% sub-second with a fat exact tail of ~33 requests/day at 5-50s (bimodal, not uniformly slow) — a completely different optimization target than the UI suggested.
Related API notes for scripting this: dataset=profileFunctions with query=is_application:1 and sum(function.duration) reproduces the /explore/profiles UI's slowest-functions table, but function durations are NANOSECONDS (transaction durations are ms); dataset=spans can return empty for 30d windows while 14d works, so keep span queries to shorter periods. The de-bias weights must be updated in lockstep with any change to the sampling ladder.