Skip to content

Feature flags on transactions are not sent to Sentry #5023

@adinauer

Description

@adinauer

How do you use Sentry?

Sentry Saas (sentry.io)

Version

2.40.0

Steps to Reproduce

Start a transaction and call add_feature_flag, then finish the transaction and have it sent to Sentry.

Example traces:

Here's a repro script:

"""
Demo script showing how feature flags work with transactions in Sentry Python SDK.
"""

import sentry_sdk
from sentry_sdk import start_transaction, start_span
from sentry_sdk.feature_flags import add_feature_flag
import time


def main():
    # Initialize Sentry SDK with 100% transaction sampling
    sentry_sdk.init(
        dsn="https://a2e6d6ad22e4fabbbcb47b62a920c820@o447951.ingest.us.sentry.io/4510243434266624",  # Replace with your DSN
        traces_sample_rate=1.0,  # Capture 100% of transactions
        debug=True,  # Enable debug mode to see what's being sent
    )

    print("=" * 60)
    print("Testing Feature Flags with Transactions")
    print("=" * 60)

    # Scenario 1: Feature flags added directly to transaction
    print("\n[Scenario 1] Adding flags directly to transaction...")
    with start_transaction(name="transaction_with_flags", op="test"):
        add_feature_flag("feature_on_transaction", True)
        add_feature_flag("another_flag", False)
        time.sleep(0.1)

    # Scenario 2: Feature flags added to child spans
    print("\n[Scenario 2] Adding flags to child spans...")
    with start_transaction(name="transaction_with_child_spans", op="test"):
        add_feature_flag("flag_before_span", True)
        
        with start_span(op="child_operation", name="first_child_span"):
            add_feature_flag("flag_in_first_child", True)
            add_feature_flag("flag_in_first_child_2", False)
            time.sleep(0.05)
        
        with start_span(op="another_operation", name="second_child_span"):
            add_feature_flag("flag_in_second_child", True)
            time.sleep(0.05)

    # Scenario 3: Many flags to test the 10-flag limit on spans
    print("\n[Scenario 3] Testing span flag limit (10 max)...")
    with start_transaction(name="transaction_with_many_flags", op="test"):
        with start_span(op="heavy_flagging", name="span_with_many_flags"):
            for i in range(15):  # Try to add 15 flags (only 10 will be stored)
                add_feature_flag(f"flag_{i}", i % 2 == 0)
            time.sleep(0.05)

    # Scenario 4: Feature flags + error event
    print("\n[Scenario 4] Adding flags with error capture...")
    with start_transaction(name="transaction_with_error", op="test"):
        add_feature_flag("flag_before_error", True)
        
        with start_span(op="problematic_operation", name="span_before_error"):
            add_feature_flag("flag_in_span_before_error", False)
            time.sleep(0.05)
        
        # Capture an error to see flags in error context
        try:
            result = 1 / 0
        except ZeroDivisionError as e:
            sentry_sdk.capture_exception(e)

    # Scenario 5: Using lower-level APIs
    print("\n[Scenario 5] Using lower-level APIs (scope and span direct access)...")
    with start_transaction(name="transaction_low_level_api", op="test"):
        # Get isolation scope and add flags directly
        isolation_scope = sentry_sdk.get_isolation_scope()
        isolation_scope.flags.set("scope_only_flag", True)
        isolation_scope.flags.set("another_scope_flag", False)
        
        with start_span(op="custom_operation", name="span_with_direct_flags") as span:
            # Add flag directly to span only (won't be in scope)
            span.set_flag("flag.evaluation.span_only_flag", True)
            span.set_flag("flag.evaluation.another_span_flag", False)
            
            # Compare with high-level API (adds to both scope and span)
            add_feature_flag("both_scope_and_span_flag", True)
            time.sleep(0.05)

    print("\n" + "=" * 60)
    print("All scenarios completed!")
    print("=" * 60)
    print("\nCheck your Sentry dashboard to see:")
    print("- Transaction events with flags in spans[].data")
    print("- Error events with flags in contexts.flags.values")
    print("- Note: Flags on transaction itself (not child spans) are NOT sent")
    print("\nAPI Options:")
    print("  1. add_feature_flag(name, value) - Recommended")
    print("     Adds to BOTH isolation_scope.flags AND current span")
    print("  2. isolation_scope.flags.set(name, value)")
    print("     Adds ONLY to scope (appears in error events)")
    print("  3. span.set_flag(key, value)")
    print("     Adds ONLY to span (appears in transaction events)")
    print("=" * 60)

    # Give time for events to be sent
    sentry_sdk.flush(timeout=2.0)


if __name__ == "__main__":
    main()

Expected Result

Feature flag is visible in Sentry UI, similar to how it shows up for spans.

In this screenshot you can see what this looks like for spans:
Image

Actual Result

Feature flag is missing.

In this screenshot you can see there's no feature flags on the transaction itself:
Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions