Symptom: dataset=discover + event.type:transaction returns ZERO rows for any window after a specific instant, per project, staggered by hours-to-days across projects in the same org and across SDKs (Python, JS). No config change on your side. Org-level isDynamicallySampled=false, desiredSampleRate=1.0, so it is not dynamic sampling. Not a quota either: exceeding quota yields outcome=rate_limited/usage_exceeded, and on AM3 plans there is no transactions quota category at all (transactions bill as spans).
Confirm in two API calls:
GET /api/0/organizations/{org}/stats_v2/?statsPeriod=3d&interval=1h&field=sum(quantity)&groupBy=outcome&groupBy=reason&category=transaction_indexed&project={id}-acceptedgoes to 0 at the cutover hour and an equalfiltered/discardedseries starts, tracking the still-acceptedtransaction(metrics) series hour for hour.GET /api/0/projects/{org}/{proj}/-> thefeaturesarray containsdiscard-transaction.
Mechanics (relay relay-server/src/processing/transactions/types/output.rs, forward_store): extracted spans are sent to the store FIRST, then if project has Feature::DiscardTransaction { reject with Outcome::Filtered(FilterStatKey::Discarded) } else { send_event(...) }. Profiles are sent independently afterwards. So spans, span_indexed, transaction metrics, profiles and errors all keep flowing; only the indexed transaction event dies. FilterStatKey::Discarded serializes to reason "discarded". Do NOT confuse it with reason "filtered-transaction", the customer-configurable transaction-name deny-list / health-check inbound filter, which usually runs at high volume alongside it.
Gate: projects:discard-transaction, registered ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False in src/sentry/features/temporary.py. Sentry-controlled staged rollout; no customer setting turns it off.
You will not be told this is happening
Audited every push channel after the fact. Nothing announced the rollout:
GET /api/0/broadcasts/(the in-app "what's new" feed): mirrors the public changelog, nothing about transactions.- sentry.io/changelog: no entry.
- status page: unrelated ingestion-delay incidents only.
- The only pre-announcement was a Zendesk FAQ (Dec 2025) promising deprecation "likely by Nov 2025 ... we will update our product messaging" - the flag actually flipped ~9 months later with no follow-up date ever published.
- The "product messaging" turned out to be a web-UI-only banner, gated on the org flag
performance-transaction-deprecation-banner(alongsidedeprecate-discoveranddiscover-saved-queries-deprecation). Invisible to every API and CLI consumer.
Timeline corroboration if you need to date it: getsentry/self-hosted 26.7.0 (2026-07-16) shipped "transactions -> spans migration flags" (PR #4402); SaaS flips followed weeks later.
Trap that hides all of this: the org features array is EMPTY by default. GET /api/0/organizations/{org}/ returns "features": []. You must ask:
GET /api/0/organizations/{org}/?include_feature_flags=1 # -> 102 flags for usReading the default empty array as "no relevant feature flags" is a false negative that cost one investigation session a wrong conclusion. The project payload also embeds the full set under organization.features.
Three consequences
- Performance-issue detection can go silently dead. N+1 Query / Slow DB Query detectors run in
event_manager.save_transaction_events -> _detect_performance_problems, which needs the transaction event relay no longer sends. The replacement inspans/consumers/process_segments/message.pyonly emits occurrences when relay set_performance_issues_spanson the segment, requiring the org featureorganizations:performance-issues-spans(INTERNAL, default off) plus the optionspans.process-segments.detect-performance-problems.detectors-enabled. Check with theinclude_feature_flagscall above: if you havediscard-transactionWITHOUTperformance-issues-spans, both paths are off and no performance issue can ever be created again. Verify empirically withdataset=issuePlatformsorted-timestamp: the last occurrence will predate the cutover. Practical fallout: "the Sentry issue went quiet" stops being valid acceptance for a perf fix; only error issues still work that way. Compensate with a db-spans-per-request query (dataset=spans,query=span.op:db, fieldstransaction,count_sample(),count_unique(trace)) and in-repo SQL-count regression tests. - Retrospective window collapses. Spans retain 30d where indexed transactions retained 90d (
profileFunctionsis also ~30d). Detect by running the same count at 7/14/30/60/90d: the spans count plateaus (identical at 60d and 90d) while discover keeps climbing. Discover still answers PRE-cutover windows until that history expires, so archive any >30d baseline you depend on. - No billing change and usually no broken alerts. Span acceptance is flat across the cutover (span storage was already on), and Sentry auto-migrates transaction-based dashboard widgets and alerts to spans.
Tooling migration: dataset=discover + event.type:transaction becomes dataset=spans + is_transaction:1; aggregates match (7d parity: 20,100 spans vs 19,426 discover, the delta being the post-cutover spans-only tail). p50/p95/avg(span.duration), failure_rate(), count_unique(user), count_unique(trace), epm(), sum(span.self_time), http_response_rate(5) all work; apdex()/user_misery() need explicit args; transaction.duration no longer exists. Beware that count() on spans is extrapolated - see the companion lesson on count_sample().