Repository: getsentry/snuba
Pull request #6981: feat(eap-items): use eap items in all endpoints except get trace
Author: login: davidtsuk
Created: 2025-03-19T17:54:32Z | Merged: 2025-03-19T23:34:07Z

This is a continuation of the work required to migrate to from `eap_spans` to `eap_items`


Diff:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index be1e1be4bad..bffb74f0157 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -380,6 +380,7 @@ jobs:
               tests/sentry/uptime/endpoints/test_organization_uptime_stats.py \
               tests/sentry/api/endpoints/test_organization_traces.py \
               tests/sentry/api/endpoints/test_organization_spans_fields.py \
+              tests/sentry/api/endpoints/test_organization_spans_fields_stats.py \
               tests/sentry/sentry_metrics/querying \
               -vv --cov . --cov-report="xml:.artifacts/snuba.coverage.xml"
 
diff --git a/snuba/datasets/configuration/events_analytics_platform/storages/eap_items.yaml b/snuba/datasets/configuration/events_analytics_platform/storages/eap_items.yaml
index 61ab262146d..1825a9286bb 100644
--- a/snuba/datasets/configuration/events_analytics_platform/storages/eap_items.yaml
+++ b/snuba/datasets/configuration/events_analytics_platform/storages/eap_items.yaml
@@ -219,6 +219,10 @@ query_processors:
   - processor: UUIDColumnProcessor
     args:
       columns: [trace_id]
+  - processor: HexIntColumnProcessor
+    args:
+      columns: [item_id]
+      size: 32
   - processor: PrewhereProcessor
     args:
       prewhere_candidates:
diff --git a/snuba/query/processors/physical/hexint_column_processor.py b/snuba/query/processors/physical/hexint_column_processor.py
index 62c96a41e3f..14a918d362b 100644
--- a/snuba/query/processors/physical/hexint_column_processor.py
+++ b/snuba/query/processors/physical/hexint_column_processor.py
@@ -15,14 +15,20 @@
 
 
 class HexIntColumnProcessor(BaseTypeConverter):
-    def __init__(self, columns: Set[str]) -> None:
+    def __init__(self, columns: Set[str], size: int = 16) -> None:
+        """
+        size is the number of characters in the hex string representation of the integer (e.g. 32 for 128 bit integers)
+        """
         super().__init__(columns, optimize_ordering=True)
-        self._size = 16
+        self._size = size
 
     def _translate_literal(self, exp: Literal) -> Literal:
         try:
             assert isinstance(exp.value, str)
-            return Literal(alias=exp.alias, value=int(exp.value, self._size))
+            # 128 bit integers in clickhouse need to be referenced as strings
+            if self._size == 32:
+                return Literal(alias=exp.alias, value=str(int(exp.value, 16)))
+            return Literal(alias=exp.alias, value=int(exp.value, 16))
         except (AssertionError, ValueError):
             raise ColumnTypeError("Invalid hexint", should_report=False)
 
diff --git a/snuba/utils/constants.py b/snuba/utils/constants.py
index d0c0296b1a6..7c5b678ea64 100644
--- a/snuba/utils/constants.py
+++ b/snuba/utils/constants.py
@@ -12,6 +12,8 @@
 # This will affect migrations and querying.
 ATTRIBUTE_BUCKETS = 20
 
+ATTRIBUTE_BUCKETS_EAP_ITEMS = 40
+
 # number of buckets in eap_items_1_local table
 ITEM_ATTRIBUTE_BUCKETS = 40
 
diff --git a/snuba/web/rpc/v1/endpoint_get_traces.py b/snuba/web/rpc/v1/endpoint_get_traces.py
index e6811893334..8bd946a805d 100644
--- a/snuba/web/rpc/v1/endpoint_get_traces.py
+++ b/snuba/web/rpc/v1/endpoint_get_traces.py
@@ -9,7 +9,11 @@
     GetTracesResponse,
     TraceAttribute,
 )
-from sentry_protos.snuba.v1.request_common_pb2 import PageToken, TraceItemType
+from sentry_protos.snuba.v1.request_common_pb2 import (
+    PageToken,
+    RequestMeta,
+    TraceItemType,
+)
 from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey, AttributeValue
 from sentry_protos.snuba.v1.trace_item_filter_pb2 import TraceItemFilter
 
@@ -42,6 +46,8 @@
 from snuba.web.rpc.common.exceptions import BadSnubaRPCRequestException
 from snuba.web.rpc.v1.resolvers.R_eap_spans.common.common import (
     attribute_key_to_expression,
+    attribute_key_to_expression_eap_items,
+    use_eap_items_table,
 )
 
 _DEFAULT_ROW_LIMIT = 10_000
@@ -134,32 +140,69 @@
 }
 
 
+def _get_attribute_expression(
+    attribute_name: str,
+    attribute_type: AttributeKey.Type.ValueType,
+    request_meta: RequestMeta,
+) -> Expression:
+    if use_eap_items_table(request_meta):
+        return attribute_key_to_expression_eap_items(
+            AttributeKey(name=attribute_name, type=attribute_type)
+        )
+    else:
+        return attribute_key_to_expression(
+            AttributeKey(name=attribute_name, type=attribute_type)
+        )
+
+
 def _attribute_to_expression(
-    trace_attribute: TraceAttribute,
-    *conditions: Expression,
+    trace_attribute: TraceAttribute, *conditions: Expression, request_meta: RequestMeta
 ) -> Expression:
-    def _get_root_span_attribute(attribute_name: str) -> Expression:
+    def _get_root_span_attribute(
+        attribute_name: str, attribute_type: AttributeKey.Type.ValueType
+    ) -> Expression:
         return f.argMinIf(
-            column(attribute_name),
-            column("start_timestamp"),
-            f.equals(column("parent_span_id"), literal("0" * 16)),
+            _get_attribute_expression(attribute_name, attribute_type, request_meta),
+            _get_attribute_expression(
+                "sentry.start_timestamp",
+                AttributeKey.Type.TYPE_DOUBLE,
+                request_meta,
+            ),
+            f.equals(
+                _get_attribute_expression(
+                    "sentry.parent_span_id", AttributeKey.Type.TYPE_STRING, request_meta
+                ),
+                literal("0" * 16),
+            ),
             alias=alias,
         )
 
-    def _get_earliest_span_attribute(attribute_name: str) -> Expression:
+    def _get_earliest_span_attribute(
+        attribute_name: str, attribute_type: AttributeKey.Type.ValueType
+    ) -> Expression:
         return f.argMin(
-            column(attribute_name),
-            column("start_timestamp"),
+            _get_attribute_expression(attribute_name, attribute_type, request_meta),
+            _get_attribute_expression(
+                "sentry.start_timestamp",
+                AttributeKey.Type.TYPE_DOUBLE,
+                request_meta,
+            ),
             alias=alias,
         )
 
-    def _get_earliest_frontend_span_attribute(attribute_name: str) -> Expression:
-        span_op = attribute_key_to_expression(
-            AttributeKey(name="sentry.op", type=AttributeKey.Type.TYPE_STRING)
+    def _get_earliest_frontend_span_attribute(
+        attribute_name: str, attribute_type: AttributeKey.Type.ValueType
+    ) -> Expression:
+        span_op = _get_attribute_expression(
+            "sentry.op", AttributeKey.Type.TYPE_STRING, request_meta
         )
         return f.argMinIf(
-            column(attribute_name),
-            column("start_timestamp"),
+            _get_attribute_expression(attribute_name, attribute_type, request_meta),
+            _get_attribute_expression(
+                "sentry.start_timestamp",
+                AttributeKey.Type.TYPE_DOUBLE,
+                request_meta,
+            ),
             or_cond(
                 f.equals(span_op, literal("pageload")),
                 f.equals(span_op, literal("navigation")),
@@ -175,32 +218,68 @@ def _get_earliest_frontend_span_attribute(attribute_name: str) -> Expression:
 
         if key == TraceAttribute.Key.KEY_START_TIMESTAMP:
             return f.cast(
-                f.min(column("start_timestamp")), clickhouse_type, alias=alias
+                f.min(
+                    _get_attribute_expression(
+                        "sentry.start_timestamp",
+                        AttributeKey.Type.TYPE_DOUBLE,
+                        request_meta,
+                    )
+                ),
+                clickhouse_type,
+                alias=alias,
             )
         elif key == TraceAttribute.Key.KEY_END_TIMESTAMP:
-            return f.cast(f.max(column("end_timestamp")), clickhouse_type, alias=alias)
+            return f.cast(
+                f.max(
+                    _get_attribute_expression(
+                        "sentry.end_timestamp",
+                        AttributeKey.Type.TYPE_DOUBLE,
+                        request_meta,
+                    )
+                ),
+                clickhouse_type,
+                alias=alias,
+            )
         elif key == TraceAttribute.Key.KEY_TOTAL_ITEM_COUNT:
             return f.count(alias=alias)
         elif key == TraceAttribute.Key.KEY_FILTERED_ITEM_COUNT:
             return f.countIf(*conditions, alias=alias)
         elif key == TraceAttribute.Key.KEY_ROOT_SPAN_NAME:
-            return _get_root_span_attribute("name")
+            return _get_root_span_attribute(
+                "sentry.name", AttributeKey.Type.TYPE_STRING
+            )
         elif key == TraceAttribute.Key.KEY_ROOT_SPAN_DURATION_MS:
-            return _get_root_span_attribute("duration_ms")
+            return _get_root_span_attribute(
+                "sentry.duration_ms", AttributeKey.Type.TYPE_DOUBLE
+            )
         elif key == TraceAttribute.Key.KEY_ROOT_SPAN_PROJECT_ID:
-            return _get_root_span_attribute("project_id")
+            return _get_root_span_attribute(
+                "sentry.project_id", AttributeKey.Type.TYPE_INT
+            )
         elif key == TraceAttribute.Key.KEY_EARLIEST_SPAN_NAME:
-            return _get_earliest_span_attribute("name")
+            return _get_earliest_span_attribute(
+                "sentry.name", AttributeKey.Type.TYPE_STRING
+            )
         elif key == TraceAttribute.Key.KEY_EARLIEST_SPAN_PROJECT_ID:
-            return _get_earliest_span_attribute("project_id")
+            return _get_earliest_span_attribute(
+                "sentry.project_id", AttributeKey.Type.TYPE_INT
+            )
         elif key == TraceAttribute.Key.KEY_EARLIEST_SPAN_DURATION_MS:
-            return _get_earliest_span_attribute("duration_ms")
+            return _get_earliest_span_attribute(
+                "sentry.duration_ms", AttributeKey.Type.TYPE_DOUBLE
+            )
         elif key == TraceAttribute.Key.KEY_EARLIEST_FRONTEND_SPAN:
-            return _get_earliest_frontend_span_attribute("name")
+            return _get_earliest_frontend_span_attribute(
+                "sentry.name", AttributeKey.Type.TYPE_STRING
+            )
         elif key == TraceAttribute.Key.KEY_EARLIEST_FRONTEND_SPAN_PROJECT_ID:
-            return _get_earliest_frontend_span_attribute("project_id")
+            return _get_earliest_frontend_span_attribute(
+                "sentry.project_id", AttributeKey.Type.TYPE_INT
+            )
         elif key == TraceAttribute.Key.KEY_EARLIEST_FRONTEND_SPAN_DURATION_MS:
-            return _get_earliest_frontend_span_attribute("duration_ms")
+            return _get_earliest_frontend_span_attribute(
+                "sentry.duration_ms", AttributeKey.Type.TYPE_DOUBLE
+            )
         else:
             return f.cast(column(attribute_name), clickhouse_type, alias=alias)
 
@@ -352,7 +431,9 @@ def _list_trace_ids(
     ) -> dict[str, int]:
         trace_item_filters_expression = trace_item_filters_to_expression(
             _select_supported_filters(request.filters),
-            attribute_key_to_expression,
+            attribute_key_to_expression_eap_items
+            if use_eap_items_table(request.meta)
+            else attribute_key_to_expression,
         )
         selected_columns: list[SelectedExpression] = [
             SelectedExpression(
@@ -364,19 +445,30 @@ def _list_trace_ids(
                 ),
             ),
             SelectedExpression(
-                name="_sort_timestamp",
+                name="timestamp",
                 expression=f.cast(
-                    column("_sort_timestamp"),
+                    column(
+                        "timestamp"
+                        if use_eap_items_table(request.meta)
+                        else "_sort_timestamp"
+                    ),
                     "UInt32",
-                    alias="_sort_timestamp",
+                    alias="timestamp",
                 ),
             ),
         ]
-        entity = Entity(
-            key=EntityKey("eap_spans"),
-            schema=get_entity(EntityKey("eap_spans")).get_data_model(),
-            sample=None,
-        )
+        if use_eap_items_table(request.meta):
+            entity = Entity(
+                key=EntityKey("eap_items"),
+                schema=get_entity(EntityKey("eap_items")).get_data_model(),
+                sample=None,
+            )
+        else:
+            entity = Entity(
+                key=EntityKey("eap_spans"),
+                schema=get_entity(EntityKey("eap_spans")).get_data_model(),
+                sample=None,
+            )
         query = Query(
             from_clause=entity,
             selected_columns=selected_columns,
@@ -387,7 +479,7 @@ def _list_trace_ids(
             order_by=[
                 OrderBy(
                     direction=OrderByDirection.DESC,
-                    expression=column("_sort_timestamp"),
+                    expression=column("timestamp"),
                 ),
             ],
             limitby=LimitBy(limit=1, columns=[column("trace_id")]),
@@ -403,7 +495,7 @@ def _list_trace_ids(
         )
         trace_ids: dict[str, int] = {}
         for row in results.result.get("data", []):
-            trace_ids[row["trace_id"]] = row["_sort_timestamp"]
+            trace_ids[row["trace_id"]] = row["timestamp"]
         return trace_ids
 
     def _get_metadata_for_traces(
@@ -413,7 +505,9 @@ def _get_metadata_for_traces(
     ) -> list[GetTracesResponse.Trace]:
         trace_item_filters_expression = trace_item_filters_to_expression(
             _select_supported_filters(request.filters),
-            attribute_key_to_expression,
+            attribute_key_to_expression_eap_items
+            if use_eap_items_table(request.meta)
+            else attribute_key_to_expression,
         )
 
         selected_columns: list[SelectedExpression] = []
@@ -427,6 +521,7 @@ def _get_metadata_for_traces(
                     expression=_attribute_to_expression(
                         trace_attribute,
                         trace_item_filters_expression,
+                        request_meta=request.meta,
                     ),
                 )
             )
@@ -441,15 +536,23 @@ def _get_metadata_for_traces(
                     expression=_attribute_to_expression(
                         trace_attribute,
                         trace_item_filters_expression,
+                        request_meta=request.meta,
                     ),
                 )
             )
 
-        entity = Entity(
-            key=EntityKey("eap_spans"),
-            schema=get_entity(EntityKey("eap_spans")).get_data_model(),
-            sample=None,
-        )
+        if use_eap_items_table(request.meta):
+            entity = Entity(
+                key=EntityKey("eap_items"),
+                schema=get_entity(EntityKey("eap_items")).get_data_model(),
+                sample=None,
+            )
+        else:
+            entity = Entity(
+                key=EntityKey("eap_spans"),
+                schema=get_entity(EntityKey("eap_spans")).get_data_model(),
+                sample=None,
+            )
         timestamps = trace_ids.values()
         query = Query(
             from_clause=entity,
@@ -476,6 +579,7 @@ def _get_metadata_for_traces(
                     TraceAttribute(
                         key=TraceAttribute.Key.KEY_TRACE_ID,
                     ),
+                    request_meta=request.meta,
                 ),
             ],
             order_by=[
diff --git a/snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py b/snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
index 31b228f4709..1bcd828a1a0 100644
--- a/snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
+++ b/snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
@@ -40,6 +40,23 @@
     "sentry.end_timestamp",
 }
 
+# These are attributes that were not stored in attr_str_ or attr_num_ in eap_spans because they were stored in columns.
+# Since we store these in the attribute columns in eap_items, we need to exclude them in endpoints that don't expect them to be in the attribute columns.
+ATTRIBUTES_TO_EXCLUDE_IN_EAP_ITEMS: Final[Set[str]] = {
+    "sentry.raw_description",
+    "sentry.transaction",
+    "sentry.start_timestamp_precise",
+    "sentry.end_timestamp_precise",
+    "sentry.duration_ms",
+    "sentry.event_id",
+    "sentry.exclusive_time_ms",
+    "sentry.is_segment",
+    "sentry.parent_span_id",
+    "sentry.profile_id",
+    "sentry.received",
+    "sentry.segment_id",
+}
+
 COLUMN_PREFIX: str = "sentry."
 
 NORMALIZED_COLUMNS_EAP_ITEMS: Final[
@@ -57,6 +74,7 @@
         AttributeKey.Type.TYPE_STRING
     ],  # this gets converted from a uuid to a string in a storage processor
     f"{COLUMN_PREFIX}item_id": [AttributeKey.Type.TYPE_STRING],
+    f"{COLUMN_PREFIX}sampling_weight": [AttributeKey.Type.TYPE_DOUBLE],
 }
 
 PROTO_TYPE_TO_CLICKHOUSE_TYPE: Final[Mapping[AttributeKey.Type.ValueType, str]] = {
@@ -75,11 +93,14 @@
     AttributeKey.Type.TYPE_BOOLEAN: "attributes_float",
 }
 
+# We have renamed some attributes in eap_items, so to avoid breaking changes we need to map the old names to the new names
 ATTRIBUTE_MAPPINGS: Final[Mapping[str, str]] = {
     "sentry.name": "sentry.raw_description",
     "sentry.description": "sentry.normalized_description",
     "sentry.span_id": "sentry.item_id",
     "sentry.segment_name": "sentry.transaction",
+    "sentry.start_timestamp": "sentry.start_timestamp_precise",
+    "sentry.end_timestamp": "sentry.end_timestamp_precise",
 }
 
 
@@ -90,12 +111,31 @@ def use_eap_items_table(request_meta: RequestMeta) -> bool:
     if settings.USE_EAP_ITEMS_TABLE:
         return True
 
+    use_eap_items_orgs = state.get_str_config("use_eap_items_orgs")
+    eap_items_enabled_orgs = []
+    use_eap_items_for_all_orgs = True
+    if use_eap_items_orgs:
+        try:
+            eap_items_enabled_orgs = list(
+                map(int, use_eap_items_orgs.strip("[]").split(","))
+            )
+            use_eap_items_for_all_orgs = False
+        except ValueError:
+            pass
+
+    turned_on_for_org = (
+        use_eap_items_for_all_orgs
+        or request_meta.organization_id in eap_items_enabled_orgs
+    )
+
     use_eap_items_table_start_timestamp_seconds = state.get_int_config(
         "use_eap_items_table_start_timestamp_seconds"
     )
+
     if (
-        state.get_config("use_eap_items_table", False)
+        state.get_int_config("use_eap_items_table", 0)
         and use_eap_items_table_start_timestamp_seconds is not None
+        and turned_on_for_org
     ):
         return (
             request_meta.start_timestamp.seconds
@@ -112,31 +152,42 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             f"attribute key {attr_key.name} must have a type specified"
         )
 
-    if attr_key.name in NORMALIZED_COLUMNS_EAP_ITEMS:
-        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name]:
+    converted_attr_name = ATTRIBUTE_MAPPINGS.get(attr_key.name, attr_key.name)
+    if converted_attr_name in NORMALIZED_COLUMNS_EAP_ITEMS:
+        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[converted_attr_name]:
             formatted_attribute_types = ", ".join(
-                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name])
+                map(
+                    AttributeKey.Type.Name,
+                    NORMALIZED_COLUMNS_EAP_ITEMS[converted_attr_name],
+                )
             )
             raise BadSnubaRPCRequestException(
                 f"Attribute {attr_key.name} must be one of [{formatted_attribute_types}], got {AttributeKey.Type.Name(attr_key.type)}"
             )
 
+        # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        # In eap_items, they're just integers, there's no issue fitting a 64 bit integer in a 128 bit integer.
+        # The problem is that this is just our internal representation, but when a user interacts with span_id through EAP, it's treated as a hex string with 16 characters.
+        # However, a 128 bit integer cannot be represented as a 16 character hex string (16 characters can at most represent a 64 bit integer). Hence,
+        # by default we represent item_id as a 32 character hex string. Since the user expects 16 characters and we know that a span_id will currently never use the full 128 bits,
+        # it's safe to get rid of the first 16 characters since we know those will just be padding.
+        if attr_key.name == "sentry.span_id":
+            return f.right(
+                column(converted_attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias
+            )
+
         return f.CAST(
-            column(attr_key.name[len(COLUMN_PREFIX) :]),
+            column(converted_attr_name[len(COLUMN_PREFIX) :]),
             PROTO_TYPE_TO_CLICKHOUSE_TYPE[attr_key.type],
             alias=alias,
         )
 
     if attr_key.type in PROTO_TYPE_TO_ATTRIBUTE_COLUMN:
-        attr_name = attr_key.name
-        if attr_key.name in ATTRIBUTE_MAPPINGS:
-            attr_name = ATTRIBUTE_MAPPINGS[attr_key.name]
-
         if attr_key.type == AttributeKey.Type.TYPE_BOOLEAN:
             return f.CAST(
                 SubscriptableReference(
                     column=column(PROTO_TYPE_TO_ATTRIBUTE_COLUMN[attr_key.type]),
-                    key=literal(attr_name),
+                    key=literal(converted_attr_name),
                     alias=None,
                 ),
                 "Nullable(Boolean)",
@@ -146,7 +197,7 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             return f.CAST(
                 SubscriptableReference(
                     column=column(PROTO_TYPE_TO_ATTRIBUTE_COLUMN[attr_key.type]),
-                    key=literal(attr_name),
+                    key=literal(converted_attr_name),
                     alias=None,
                 ),
                 "Nullable(Int64)",
@@ -154,7 +205,7 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             )
         return SubscriptableReference(
             column=column(PROTO_TYPE_TO_ATTRIBUTE_COLUMN[attr_key.type]),
-            key=literal(attr_name),
+            key=literal(converted_attr_name),
             alias=alias,
         )
 
diff --git a/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_time_series.py b/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_time_series.py
index 9db29ef9207..d072f2cfea4 100644
--- a/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_time_series.py
+++ b/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_time_series.py
@@ -266,7 +266,6 @@ def _proto_expression_to_ast_expression(
 
 
 def _build_query(request: TimeSeriesRequest) -> Query:
-    # TODO: This is hardcoded still
     if use_eap_items_table(request.meta):
         entity = Entity(
             key=EntityKey("eap_items"),
diff --git a/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_trace_item_stats.py b/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_trace_item_stats.py
index 65652d9671e..94e33982e37 100644
--- a/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_trace_item_stats.py
+++ b/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_trace_item_stats.py
@@ -11,7 +11,7 @@
     TraceItemStatsResponse,
     TraceItemStatsResult,
 )
-from sentry_protos.snuba.v1.request_common_pb2 import TraceItemType
+from sentry_protos.snuba.v1.request_common_pb2 import RequestMeta, TraceItemType
 
 from snuba.attribution.appid import AppID
 from snuba.attribution.attribution_info import AttributionInfo
@@ -20,12 +20,21 @@
 from snuba.datasets.pluggable_dataset import PluggableDataset
 from snuba.query import LimitBy, OrderBy, OrderByDirection, SelectedExpression
 from snuba.query.data_source.simple import Entity
-from snuba.query.dsl import arrayJoin, column, count, tupleElement
+from snuba.query.dsl import (
+    arrayJoin,
+    column,
+    count,
+    in_cond,
+    literal,
+    literals_array,
+    not_cond,
+    tupleElement,
+)
 from snuba.query.expressions import FunctionCall, Literal
 from snuba.query.logical import Query
 from snuba.query.query_settings import HTTPQuerySettings
 from snuba.request import Request as SnubaRequest
-from snuba.utils.constants import ATTRIBUTE_BUCKETS
+from snuba.utils.constants import ATTRIBUTE_BUCKETS, ATTRIBUTE_BUCKETS_EAP_ITEMS
 from snuba.web.query import run_query
 from snuba.web.rpc.common.common import (
     base_conditions_and,
@@ -39,7 +48,10 @@
 from snuba.web.rpc.common.exceptions import BadSnubaRPCRequestException
 from snuba.web.rpc.v1.resolvers import ResolverTraceItemStats
 from snuba.web.rpc.v1.resolvers.R_eap_spans.common.common import (
+    ATTRIBUTES_TO_EXCLUDE_IN_EAP_ITEMS,
     attribute_key_to_expression,
+    attribute_key_to_expression_eap_items,
+    use_eap_items_table,
 )
 
 _DEFAULT_ROW_LIMIT = 10_000
@@ -52,6 +64,7 @@
 
 def _transform_results(
     results: Iterable[Dict[str, Any]],
+    request_meta: RequestMeta,
 ) -> Iterable[AttributeDistribution]:
 
     # Maintain the order of keys, so it is in descending order
@@ -100,23 +113,49 @@ def _build_attr_distribution_snuba_request(
 def _build_attr_distribution_query(
     in_msg: TraceItemStatsRequest, distributions_params: AttributeDistributionsRequest
 ) -> Query:
-    entity = Entity(
-        key=EntityKey("eap_spans"),
-        schema=get_entity(EntityKey("eap_spans")).get_data_model(),
-        sample=None,
-    )
+    if use_eap_items_table(in_msg.meta):
+        entity = Entity(
+            key=EntityKey("eap_items"),
+            schema=get_entity(EntityKey("eap_items")).get_data_model(),
+            sample=None,
+        )
+    else:
+        entity = Entity(
+            key=EntityKey("eap_spans"),
+            schema=get_entity(EntityKey("eap_spans")).get_data_model(),
+            sample=None,
+        )
 
     concat_attr_maps = FunctionCall(
         alias="attr_str_concat",
         function_name="mapConcat",
-        parameters=tuple(column(f"attr_str_{i}") for i in range(ATTRIBUTE_BUCKETS)),
+        parameters=tuple(
+            column(
+                f"attributes_string_{i}"
+                if use_eap_items_table(in_msg.meta)
+                else f"attr_str_{i}"
+            )
+            for i in range(
+                ATTRIBUTE_BUCKETS_EAP_ITEMS
+                if use_eap_items_table(in_msg.meta)
+                else ATTRIBUTE_BUCKETS
+            )
+        ),
     )
     attrs_string_keys = tupleElement(
-        "attr_key", arrayJoin("attr_str", concat_attr_maps), Literal(None, 1)
+        "attr_key",
+        arrayJoin(
+            "attributes_string" if use_eap_items_table(in_msg.meta) else "attr_str",
+            concat_attr_maps,
+        ),
+        Literal(None, 1),
     )
     attrs_string_values = tupleElement(
         "attr_value",
-        arrayJoin("attr_str", concat_attr_maps),
+        arrayJoin(
+            "attributes_string" if use_eap_items_table(in_msg.meta) else "attr_str",
+            concat_attr_maps,
+        ),
         Literal(None, 2),
     )
 
@@ -136,7 +175,10 @@ def _build_attr_distribution_query(
     ]
 
     trace_item_filters_expression = trace_item_filters_to_expression(
-        in_msg.filter, attribute_key_to_expression
+        in_msg.filter,
+        attribute_key_to_expression_eap_items
+        if use_eap_items_table(in_msg.meta)
+        else attribute_key_to_expression,
     )
 
     query = Query(
@@ -145,6 +187,14 @@ def _build_attr_distribution_query(
         condition=base_conditions_and(
             in_msg.meta,
             trace_item_filters_expression,
+            not_cond(
+                in_cond(
+                    attrs_string_keys,
+                    literals_array(
+                        None, list(map(literal, ATTRIBUTES_TO_EXCLUDE_IN_EAP_ITEMS))
+                    ),
+                ),
+            ),
         ),
         order_by=[
             OrderBy(
@@ -201,7 +251,9 @@ def resolve(self, in_msg: TraceItemStatsRequest) -> TraceItemStatsResponse:
                     timer=self._timer,
                 )
 
-                attributes = _transform_results(query_res.result.get("data", []))
+                attributes = _transform_results(
+                    query_res.result.get("data", []), in_msg.meta
+                )
                 result.attribute_distributions.CopyFrom(
                     AttributeDistributions(attributes=attributes)
                 )
diff --git a/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_trace_item_table.py b/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_trace_item_table.py
index 9be0507633f..d7469f888b2 100644
--- a/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_trace_item_table.py
+++ b/snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_trace_item_table.py
@@ -11,7 +11,11 @@
     TraceItemTableRequest,
     TraceItemTableResponse,
 )
-from sentry_protos.snuba.v1.request_common_pb2 import PageToken, TraceItemType
+from sentry_protos.snuba.v1.request_common_pb2 import (
+    PageToken,
+    RequestMeta,
+    TraceItemType,
+)
 from sentry_protos.snuba.v1.trace_item_attribute_pb2 import ExtrapolationMode
 
 from snuba.attribution.appid import AppID
@@ -49,7 +53,10 @@
 from snuba.web.rpc.v1.resolvers.common.trace_item_table import convert_results
 from snuba.web.rpc.v1.resolvers.R_eap_spans.common.common import (
     apply_virtual_columns,
+    apply_virtual_columns_eap_items,
     attribute_key_to_expression,
+    attribute_key_to_expression_eap_items,
+    use_eap_items_table,
 )
 
 _DEFAULT_ROW_LIMIT = 10_000
@@ -62,7 +69,9 @@
 }
 
 
-def aggregation_filter_to_expression(agg_filter: AggregationFilter) -> Expression:
+def aggregation_filter_to_expression(
+    agg_filter: AggregationFilter, request_meta: RequestMeta
+) -> Expression:
     op_to_expr = {
         AggregationComparisonFilter.OP_LESS_THAN: f.less,
         AggregationComparisonFilter.OP_GREATER_THAN: f.greater,
@@ -82,7 +91,9 @@ def aggregation_filter_to_expression(agg_filter: AggregationFilter) -> Expressio
             return op_expr(
                 aggregation_to_expression(
                     agg_filter.comparison_filter.conditional_aggregation,
-                    attribute_key_to_expression,
+                    attribute_key_to_expression_eap_items
+                    if use_eap_items_table(request_meta)
+                    else attribute_key_to_expression,
                 ),
                 agg_filter.comparison_filter.val,
             )
@@ -93,7 +104,7 @@ def aggregation_filter_to_expression(agg_filter: AggregationFilter) -> Expressio
                 )
             return and_cond(
                 *(
-                    aggregation_filter_to_expression(x)
+                    aggregation_filter_to_expression(x, request_meta)
                     for x in agg_filter.and_filter.filters
                 )
             )
@@ -104,7 +115,7 @@ def aggregation_filter_to_expression(agg_filter: AggregationFilter) -> Expressio
                 )
             return or_cond(
                 *(
-                    aggregation_filter_to_expression(x)
+                    aggregation_filter_to_expression(x, request_meta)
                     for x in agg_filter.or_filter.filters
                 )
             )
@@ -116,6 +127,7 @@ def aggregation_filter_to_expression(agg_filter: AggregationFilter) -> Expressio
 
 def _convert_order_by(
     order_by: Sequence[TraceItemTableRequest.OrderBy],
+    request_meta: RequestMeta,
 ) -> Sequence[OrderBy]:
     res: list[OrderBy] = []
     for x in order_by:
@@ -124,7 +136,9 @@ def _convert_order_by(
             res.append(
                 OrderBy(
                     direction=direction,
-                    expression=attribute_key_to_expression(x.column.key),
+                    expression=attribute_key_to_expression_eap_items(x.column.key)
+                    if use_eap_items_table(request_meta)
+                    else attribute_key_to_expression(x.column.key),
                 )
             )
         elif x.column.HasField("conditional_aggregation"):
@@ -132,7 +146,10 @@ def _convert_order_by(
                 OrderBy(
                     direction=direction,
                     expression=aggregation_to_expression(
-                        x.column.conditional_aggregation, attribute_key_to_expression
+                        x.column.conditional_aggregation,
+                        attribute_key_to_expression_eap_items
+                        if use_eap_items_table(request_meta)
+                        else attribute_key_to_expression,
                     ),
                 )
             )
@@ -140,13 +157,15 @@ def _convert_order_by(
             res.append(
                 OrderBy(
                     direction=direction,
-                    expression=_formula_to_expression(x.column.formula),
+                    expression=_formula_to_expression(x.column.formula, request_meta),
                 )
             )
     return res
 
 
-def _get_reliability_context_columns(column: Column) -> list[SelectedExpression]:
+def _get_reliability_context_columns(
+    column: Column, request_meta: RequestMeta
+) -> list[SelectedExpression]:
     """
     extrapolated aggregates need to request extra columns to calculate the reliability of the result.
     this function returns the list of columns that need to be requested.
@@ -160,7 +179,10 @@ def _get_reliability_context_columns(column: Column) -> list[SelectedExpression]
     ):
         context_columns = []
         confidence_interval_column = get_confidence_interval_column(
-            column.conditional_aggregation, attribute_key_to_expression
+            column.conditional_aggregation,
+            attribute_key_to_expression_eap_items
+            if use_eap_items_table(request_meta)
+            else attribute_key_to_expression,
         )
         if confidence_interval_column is not None:
             context_columns.append(
@@ -171,10 +193,16 @@ def _get_reliability_context_columns(column: Column) -> list[SelectedExpression]
             )
 
         average_sample_rate_column = get_average_sample_rate_column(
-            column.conditional_aggregation, attribute_key_to_expression
+            column.conditional_aggregation,
+            attribute_key_to_expression_eap_items
+            if use_eap_items_table(request_meta)
+            else attribute_key_to_expression,
         )
         count_column = get_count_column(
-            column.conditional_aggregation, attribute_key_to_expression
+            column.conditional_aggregation,
+            attribute_key_to_expression_eap_items
+            if use_eap_items_table(request_meta)
+            else attribute_key_to_expression,
         )
         context_columns.append(
             SelectedExpression(
@@ -189,28 +217,37 @@ def _get_reliability_context_columns(column: Column) -> list[SelectedExpression]
     return []
 
 
-def _formula_to_expression(formula: Column.BinaryFormula) -> Expression:
+def _formula_to_expression(
+    formula: Column.BinaryFormula, request_meta: RequestMeta
+) -> Expression:
     return OP_TO_EXPR[formula.op](
-        _column_to_expression(formula.left),
-        _column_to_expression(formula.right),
+        _column_to_expression(formula.left, request_meta),
+        _column_to_expression(formula.right, request_meta),
     )
 
 
-def _column_to_expression(column: Column) -> Expression:
+def _column_to_expression(column: Column, request_meta: RequestMeta) -> Expression:
     """
     Given a column protobuf object, translates it into a Expression object and returns it.
     """
     if column.HasField("key"):
-        return attribute_key_to_expression(column.key)
+        return (
+            attribute_key_to_expression_eap_items(column.key)
+            if use_eap_items_table(request_meta)
+            else attribute_key_to_expression(column.key)
+        )
     elif column.HasField("conditional_aggregation"):
         function_expr = aggregation_to_expression(
-            column.conditional_aggregation, attribute_key_to_expression
+            column.conditional_aggregation,
+            attribute_key_to_expression_eap_items
+            if use_eap_items_table(request_meta)
+            else attribute_key_to_expression,
         )
         # aggregation label may not be set and the column label takes priority anyways.
         function_expr = replace(function_expr, alias=column.label)
         return function_expr
     elif column.HasField("formula"):
-        formula_expr = _formula_to_expression(column.formula)
+        formula_expr = _formula_to_expression(column.formula, request_meta)
         formula_expr = replace(formula_expr, alias=column.label)
         return formula_expr
     else:
@@ -220,12 +257,18 @@ def _column_to_expression(column: Column) -> Expression:
 
 
 def _build_query(request: TraceItemTableRequest) -> Query:
-    # TODO: This is hardcoded still
-    entity = Entity(
-        key=EntityKey("eap_spans"),
-        schema=get_entity(EntityKey("eap_spans")).get_data_model(),
-        sample=None,
-    )
+    if use_eap_items_table(request.meta):
+        entity = Entity(
+            key=EntityKey("eap_items"),
+            schema=get_entity(EntityKey("eap_items")).get_data_model(),
+            sample=None,
+        )
+    else:
+        entity = Entity(
+            key=EntityKey("eap_spans"),
+            schema=get_entity(EntityKey("eap_spans")).get_data_model(),
+            sample=None,
+        )
 
     selected_columns = []
     for column in request.columns:
@@ -234,10 +277,11 @@ def _build_query(request: TraceItemTableRequest) -> Query:
         # the returned attribute value
         selected_columns.append(
             SelectedExpression(
-                name=column.label, expression=_column_to_expression(column)
+                name=column.label,
+                expression=_column_to_expression(column, request.meta),
             )
         )
-        selected_columns.extend(_get_reliability_context_columns(column))
+        selected_columns.extend(_get_reliability_context_columns(column, request.meta))
 
     res = Query(
         from_clause=entity,
@@ -245,24 +289,36 @@ def _build_query(request: TraceItemTableRequest) -> Query:
         condition=base_conditions_and(
             request.meta,
             trace_item_filters_to_expression(
-                request.filter, attribute_key_to_expression
+                request.filter,
+                attribute_key_to_expression_eap_items
+                if use_eap_items_table(request.meta)
+                else attribute_key_to_expression,
             ),
         ),
-        order_by=_convert_order_by(request.order_by),
+        order_by=_convert_order_by(request.order_by, request.meta),
         groupby=[
-            attribute_key_to_expression(attr_key) for attr_key in request.group_by
+            attribute_key_to_expression_eap_items(attr_key)
+            if use_eap_items_table(request.meta)
+            else attribute_key_to_expression(attr_key)
+            for attr_key in request.group_by
         ],
         # Only support offset page tokens for now
         offset=request.page_token.offset,
         # protobuf sets limit to 0 by default if it is not set,
         # give it a default value that will actually return data
         limit=request.limit if request.limit > 0 else _DEFAULT_ROW_LIMIT,
-        having=aggregation_filter_to_expression(request.aggregation_filter)
+        having=aggregation_filter_to_expression(
+            request.aggregation_filter, request.meta
+        )
         if request.HasField("aggregation_filter")
         else None,
     )
     treeify_or_and_conditions(res)
-    apply_virtual_columns(res, request.virtual_column_contexts)
+    apply_virtual_columns_eap_items(
+        res, request.virtual_column_contexts
+    ) if use_eap_items_table(request.meta) else apply_virtual_columns(
+        res, request.virtual_column_contexts
+    )
     add_existence_check_to_subscriptable_references(res)
     return res
 
diff --git a/snuba/web/rpc/v1/resolvers/R_ourlogs/common/attribute_key_to_expression.py b/snuba/web/rpc/v1/resolvers/R_ourlogs/common/attribute_key_to_expression.py
index c48340382ee..1af7e2ebe2d 100644
--- a/snuba/web/rpc/v1/resolvers/R_ourlogs/common/attribute_key_to_expression.py
+++ b/snuba/web/rpc/v1/resolvers/R_ourlogs/common/attribute_key_to_expression.py
@@ -34,7 +34,7 @@ def attribute_key_to_expression(attr_key: AttributeKey) -> Expression:
 
     if attr_key.name == "sentry.item_id":
         if attr_key.type == AttributeKey.Type.TYPE_STRING:
-            return f.hex(column("item_id"), alias=alias)
+            return column("item_id", alias=alias)
         raise BadSnubaRPCRequestException(
             f"Attribute {attr_key.name} must be requested as a string, got {attr_key.type}"
         )
diff --git a/snuba/web/rpc/v1/resolvers/R_ourlogs/resolver_trace_item_details.py b/snuba/web/rpc/v1/resolvers/R_ourlogs/resolver_trace_item_details.py
index a74277423e6..b85b504d5e0 100644
--- a/snuba/web/rpc/v1/resolvers/R_ourlogs/resolver_trace_item_details.py
+++ b/snuba/web/rpc/v1/resolvers/R_ourlogs/resolver_trace_item_details.py
@@ -56,9 +56,7 @@ def _build_query(request: TraceItemDetailsRequest) -> Query:
             SelectedExpression(
                 "timestamp", f.toUnixTimestamp(column("timestamp"), alias="timestamp")
             ),
-            SelectedExpression(
-                "hex_item_id", f.hex(column("item_id"), alias="hex_item_id")
-            ),
+            SelectedExpression("hex_item_id", column("item_id", alias="hex_item_id")),
             SelectedExpression("trace_id", column("trace_id", alias="trace_id")),
             SelectedExpression(
                 "organization_id", column("organization_id", alias="organization_id")
@@ -91,7 +89,7 @@ def _build_query(request: TraceItemDetailsRequest) -> Query:
             f.equals(column("item_type"), TraceItemType.TRACE_ITEM_TYPE_LOG),
             f.equals(
                 column("item_id"),
-                f.reinterpretAsUInt128(f.reverse(f.unhex(literal(request.item_id)))),
+                literal(request.item_id),
             ),
             trace_item_filters_to_expression(
                 request.filter, attribute_key_to_expression
diff --git a/tests/web/rpc/test_common.py b/tests/web/rpc/test_common.py
index 0ef9b80eb84..9fccd996032 100644
--- a/tests/web/rpc/test_common.py
+++ b/tests/web/rpc/test_common.py
@@ -133,3 +133,17 @@ def test_use_eap_items_table(self, snuba_set_config: SnubaSetConfig) -> None:
                 referrer="force_use_eap_spans_table.test",
             )
         )
+
+        snuba_set_config("use_eap_items_table", True)
+        snuba_set_config("use_eap_items_orgs", "[2, 3]")
+        assert not use_eap_items_table(
+            RequestMeta(organization_id=1, start_timestamp=Timestamp(seconds=10))
+        )
+        assert use_eap_items_table(
+            RequestMeta(organization_id=2, start_timestamp=Timestamp(seconds=10))
+        )
+
+        snuba_set_config("use_eap_items_orgs", "wrong format")
+        assert use_eap_items_table(
+            RequestMeta(organization_id=1, start_timestamp=Timestamp(seconds=10))
+        )
diff --git a/tests/web/rpc/v1/test_endpoint_get_traces.py b/tests/web/rpc/v1/test_endpoint_get_traces.py
index 3a1d3d4ce65..cea5e30d876 100644
--- a/tests/web/rpc/v1/test_endpoint_get_traces.py
+++ b/tests/web/rpc/v1/test_endpoint_get_traces.py
@@ -31,6 +31,7 @@
 from snuba.web.rpc.common.exceptions import BadSnubaRPCRequestException
 from snuba.web.rpc.v1.endpoint_get_traces import EndpointGetTraces
 from tests.base import BaseApiTest
+from tests.conftest import SnubaSetConfig
 from tests.helpers import write_raw_unprocessed_events
 
 _RELEASE_TAG = "backend@24.7.0.dev0+c45b49caed1e5fcbf70097ab3f434b487c359b6b"
@@ -62,7 +63,7 @@ def gen_message(
         timestamp += random.random()
     return {
         "description": span_name,
-        "duration_ms": 152,
+        "duration_ms": 1000,
         "event_id": uuid.uuid4().hex,
         "exclusive_time_ms": 0.228,
         "is_segment": is_segment,
@@ -739,3 +740,18 @@ def test_with_data_and_aggregated_fields_ignore_case_on_non_strings_error(
             BadSnubaRPCRequestException, match="Cannot ignore case on non-string values"
         ):
             EndpointGetTraces().execute(message)
+
+
+@pytest.mark.clickhouse_db
+@pytest.mark.redis_db
+class TestGetTracesEAPItems(TestGetTraces):
+    """
+    Run the tests again, but this time on the eap_items table as well to ensure it also works.
+    """
+
+    @pytest.fixture(autouse=True)
+    def use_eap_items_table(
+        self, snuba_set_config: SnubaSetConfig, redis_db: None
+    ) -> None:
+        snuba_set_config("use_eap_items_table", True)
+        snuba_set_config("use_eap_items_table_start_timestamp_seconds", 0)
diff --git a/tests/web/rpc/v1/test_endpoint_trace_item_details.py b/tests/web/rpc/v1/test_endpoint_trace_item_details.py
index 8871ee24ec8..1ba9b8fa7be 100644
--- a/tests/web/rpc/v1/test_endpoint_trace_item_details.py
+++ b/tests/web/rpc/v1/test_endpoint_trace_item_details.py
@@ -1,5 +1,5 @@
 import uuid
-from datetime import datetime, timedelta
+from datetime import UTC, datetime, timedelta
 from typing import Any, Mapping, MutableMapping, Union
 
 import pytest
@@ -60,7 +60,7 @@ def gen_log_message(
     }
 
 
-BASE_TIME = datetime.utcnow().replace(minute=0, second=0, microsecond=0) - timedelta(
+BASE_TIME = datetime.now(UTC).replace(minute=0, second=0, microsecond=0) - timedelta(
     minutes=180
 )
 
diff --git a/tests/web/rpc/v1/test_endpoint_trace_item_stats.py b/tests/web/rpc/v1/test_endpoint_trace_item_stats.py
index e74898797dc..c079985ce00 100644
--- a/tests/web/rpc/v1/test_endpoint_trace_item_stats.py
+++ b/tests/web/rpc/v1/test_endpoint_trace_item_stats.py
@@ -23,6 +23,7 @@
 from snuba.datasets.storages.storage_key import StorageKey
 from snuba.web.rpc.v1.endpoint_trace_item_stats import EndpointTraceItemStats
 from tests.base import BaseApiTest
+from tests.conftest import SnubaSetConfig
 from tests.helpers import write_raw_unprocessed_events
 
 _RELEASE_TAG = "backend@24.7.0.dev0+c45b49caed1e5fcbf70097ab3f434b487c359b6b"
@@ -214,6 +215,31 @@ def test_basic_with_data(self, setup_teardown: Any) -> None:
 
         assert match
 
+    def test_backwards_compatibility(self, setup_teardown: Any) -> None:
+        ts = Timestamp(seconds=int(BASE_TIME.timestamp()))
+        hour_ago = int((BASE_TIME - timedelta(hours=3)).timestamp())
+        message = TraceItemStatsRequest(
+            meta=RequestMeta(
+                project_ids=[1, 2, 3],
+                organization_id=1,
+                cogs_category="something",
+                referrer="something",
+                start_timestamp=Timestamp(seconds=hour_ago),
+                end_timestamp=ts,
+                trace_item_type=TraceItemType.TRACE_ITEM_TYPE_SPAN,
+            ),
+            stats_types=[
+                StatsType(
+                    attribute_distributions=AttributeDistributionsRequest(
+                        max_buckets=10, max_attributes=100
+                    )
+                )
+            ],
+        )
+
+        response = EndpointTraceItemStats().execute(message)
+        assert len(response.results[0].attribute_distributions.attributes) == 30
+
     def test_with_filter(self, setup_teardown: Any) -> None:
         ts = Timestamp(seconds=int(BASE_TIME.timestamp()))
         hour_ago = int((BASE_TIME - timedelta(hours=3)).timestamp())
@@ -267,3 +293,18 @@ def test_with_filter(self, setup_teardown: Any) -> None:
             expected_low_cardinality_stats
             in response.results[0].attribute_distributions.attributes
         )
+
+
+@pytest.mark.clickhouse_db
+@pytest.mark.redis_db
+class TestTraceItemAttributesStatsEAPItems(TestTraceItemAttributesStats):
+    """
+    Run the tests again, but this time on the eap_items table as well to ensure it also works.
+    """
+
+    @pytest.fixture(autouse=True)
+    def use_eap_items_table(
+        self, snuba_set_config: SnubaSetConfig, redis_db: None
+    ) -> None:
+        snuba_set_config("use_eap_items_table", True)
+        snuba_set_config("use_eap_items_table_start_timestamp_seconds", 0)
diff --git a/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py b/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py
index 8683acbbf87..fac0176d5dd 100644
--- a/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py
+++ b/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py
@@ -56,6 +56,7 @@
     _apply_labels_to_columns,
 )
 from tests.base import BaseApiTest
+from tests.conftest import SnubaSetConfig
 from tests.helpers import write_raw_unprocessed_events
 
 _RELEASE_TAG = "backend@24.7.0.dev0+c45b49caed1e5fcbf70097ab3f434b487c359b6b"
@@ -174,6 +175,14 @@ def write_eap_span(
         ],
     )
 
+    write_raw_unprocessed_events(
+        get_storage(StorageKey("eap_items")),  # type: ignore
+        [
+            gen_message(timestamp, measurements=measurements, tags=tags)
+            for _ in range(count)
+        ],
+    )
+
 
 BASE_TIME = datetime.utcnow().replace(minute=0, second=0, microsecond=0) - timedelta(
     minutes=180
@@ -1901,12 +1910,12 @@ def test_reliability_with_conditional_aggregation(self) -> None:
                     aggregation=AttributeAggregation(
                         aggregate=Function.FUNCTION_AVG,
                         key=AttributeKey(
-                            type=AttributeKey.TYPE_DOUBLE, name="sentry.sampling_factor"
+                            type=AttributeKey.TYPE_DOUBLE, name="sentry.sampling_weight"
                         ),
-                        label="avg_sample(sampling_rate)",
+                        label="avg_sample(sampling_weight)",
                         extrapolation_mode=ExtrapolationMode.EXTRAPOLATION_MODE_NONE,
                     ),
-                    label="avg_sample(sampling_rate)",
+                    label="avg_sample(sampling_weight)",
                 ),
                 Column(
                     aggregation=AttributeAggregation(
@@ -1923,12 +1932,12 @@ def test_reliability_with_conditional_aggregation(self) -> None:
                     aggregation=AttributeAggregation(
                         aggregate=Function.FUNCTION_MIN,
                         key=AttributeKey(
-                            type=AttributeKey.TYPE_DOUBLE, name="sentry.sampling_factor"
+                            type=AttributeKey.TYPE_DOUBLE, name="sentry.sampling_weight"
                         ),
-                        label="min(sampling_rate)",
+                        label="min(sampling_weight)",
                         extrapolation_mode=ExtrapolationMode.EXTRAPOLATION_MODE_SAMPLE_WEIGHTED,
                     ),
-                    label="min(sampling_rate)",
+                    label="min(sampling_weight)",
                 ),
                 Column(
                     aggregation=AttributeAggregation(
@@ -1946,9 +1955,9 @@ def test_reliability_with_conditional_aggregation(self) -> None:
         response = EndpointTraceItemTable().execute(message)
         assert response.column_values == [
             TraceItemColumnValues(
-                attribute_name="avg_sample(sampling_rate)",
+                attribute_name="avg_sample(sampling_weight)",
                 results=[
-                    AttributeValue(val_double=0.475),
+                    AttributeValue(val_double=5.5),
                 ],
             ),
             TraceItemColumnValues(
@@ -1959,9 +1968,9 @@ def test_reliability_with_conditional_aggregation(self) -> None:
                 reliabilities=[Reliability.RELIABILITY_LOW],
             ),
             TraceItemColumnValues(
-                attribute_name="min(sampling_rate)",
+                attribute_name="min(sampling_weight)",
                 results=[
-                    AttributeValue(val_double=0.1),
+                    AttributeValue(val_double=1),
                 ],
             ),
             TraceItemColumnValues(
@@ -2973,6 +2982,45 @@ def test_formula_aggregation(self) -> None:
             ),
         ]
 
+    def test_span_id_column(self) -> None:
+        write_eap_span(BASE_TIME)
+        ts = Timestamp(seconds=int(BASE_TIME.timestamp()))
+        hour_ago = int((BASE_TIME - timedelta(hours=12)).timestamp())
+        message = TraceItemTableRequest(
+            meta=RequestMeta(
+                project_ids=[1, 2, 3],
+                organization_id=1,
+                cogs_category="something",
+                referrer="something",
+                start_timestamp=Timestamp(seconds=hour_ago),
+                end_timestamp=ts,
+                trace_item_type=TraceItemType.TRACE_ITEM_TYPE_SPAN,
+            ),
+            columns=[
+                Column(
+                    key=AttributeKey(
+                        type=AttributeKey.TYPE_STRING, name="sentry.span_id"
+                    )
+                ),
+            ],
+            filter=TraceItemFilter(
+                comparison_filter=ComparisonFilter(
+                    key=AttributeKey(
+                        type=AttributeKey.TYPE_STRING, name="sentry.span_id"
+                    ),
+                    op=ComparisonFilter.OP_EQUALS,
+                    value=AttributeValue(val_str="123456781234567d"),
+                )
+            ),
+        )
+        response = EndpointTraceItemTable().execute(message)
+        assert response.column_values == [
+            TraceItemColumnValues(
+                attribute_name="sentry.span_id",
+                results=[AttributeValue(val_str="123456781234567d")],
+            )
+        ]
+
 
 class TestUtils:
     def test_apply_labels_to_columns_backward_compat(self) -> None:
@@ -3038,3 +3086,18 @@ def test_apply_labels_to_columns(self) -> None:
         _apply_labels_to_columns(message)
         assert message.columns[0].label == "avg(custom_measurement)"
         assert message.columns[1].label == "avg(custom_measurement_2)"
+
+
+@pytest.mark.clickhouse_db
+@pytest.mark.redis_db
+class TestTraceItemTableEAPItems(TestTraceItemTable):
+    """
+    Run the tests again, but this time on the eap_items table as well to ensure it also works.
+    """
+
+    @pytest.fixture(autouse=True)
+    def use_eap_items_table(
+        self, snuba_set_config: SnubaSetConfig, redis_db: None
+    ) -> None:
+        snuba_set_config("use_eap_items_table", True)
+        snuba_set_config("use_eap_items_table_start_timestamp_seconds", 0)
diff --git a/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table_extrapolation.py b/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table_extrapolation.py
index 1801caceabc..3a2ce2236e9 100644
--- a/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table_extrapolation.py
+++ b/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table_extrapolation.py
@@ -32,6 +32,7 @@
 from snuba.datasets.storages.storage_key import StorageKey
 from snuba.web.rpc.v1.endpoint_trace_item_table import EndpointTraceItemTable
 from tests.base import BaseApiTest
+from tests.conftest import SnubaSetConfig
 from tests.helpers import write_raw_unprocessed_events
 from tests.web.rpc.v1.test_endpoint_trace_item_table.test_endpoint_trace_item_table import (
     write_eap_span,
@@ -979,3 +980,18 @@ def test_aggregation_with_nulls(self) -> None:
                 ],
             ),
         ]
+
+
+@pytest.mark.clickhouse_db
+@pytest.mark.redis_db
+class TestTraceItemTableWithExtrapolationEAPItems(TestTraceItemTableWithExtrapolation):
+    """
+    Run the tests again, but this time on the eap_items table as well to ensure it also works.
+    """
+
+    @pytest.fixture(autouse=True)
+    def use_eap_items_table(
+        self, snuba_set_config: SnubaSetConfig, redis_db: None
+    ) -> None:
+        snuba_set_config("use_eap_items_table", True)
+        snuba_set_config("use_eap_items_table_start_timestamp_seconds", 0)


Reviews:
user: login: xurui-c
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T21:30:46Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T21:31:28Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T21:34:41Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T21:34:59Z
user: login: onkar
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T21:35:25Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T21:35:52Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T21:36:14Z
user: login: onkar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T21:38:49Z
user: login: volokluev
body: some questions

state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T21:42:47Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T21:52:08Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T21:53:43Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T21:54:48Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T21:55:47Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T21:59:56Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:00:52Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:09:35Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T22:20:19Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:20:37Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:20:42Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:21:03Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T22:22:18Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:24:20Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:26:59Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T22:28:31Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T22:30:49Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T22:33:41Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:35:31Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:43:30Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:52:47Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T23:09:28Z
user: login: volokluev
body: great job
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-19T23:10:27Z
user: login: davidtsuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T23:11:59Z
user: login: volokluev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T23:13:17Z

Review comments:
diff_hunk: @@ -15,14 +15,17 @@
 
 
 class HexIntColumnProcessor(BaseTypeConverter):
-    def __init__(self, columns: Set[str]) -> None:
+    def __init__(self, columns: Set[str], size: int = 16) -> None:
         super().__init__(columns, optimize_ordering=True)
-        self._size = 16
+        self._size = size
 
     def _translate_literal(self, exp: Literal) -> Literal:
         try:
             assert isinstance(exp.value, str)
-            return Literal(alias=exp.alias, value=int(exp.value, self._size))
+            # 128 bit integers in clickhouse need to be referenced as strings
+            if self._size == 32:
+                return Literal(alias=exp.alias, value=str(int(exp.value, 16)))
+            return Literal(alias=exp.alias, value=int(exp.value, 16))
         except (AssertionError, ValueError):
             raise ColumnTypeError("Invalid hexint", should_report=False)
path: snuba/query/processors/physical/hexint_column_processor.py
user: login: onkar
body: Should this be:
```
try:
    parsed_value = int(exp.value, 16)
    return Literal(alias=exp.alias, value=str(parsed_value) if self._size == 32 else parsed_value)
except ValueError:
    raise ColumnTypeError("Invalid hexint", should_report=False)
```
Currently, the literal can be sometimes a `str` or an `int`. Does it make sense to standardize it to one type?
created_at: 2025-03-19T21:30:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -220,12 +257,18 @@ def _column_to_expression(column: Column) -> Expression:
 
 
 def _build_query(request: TraceItemTableRequest) -> Query:
-    # TODO: This is hardcoded still
-    entity = Entity(
-        key=EntityKey("eap_spans"),
-        schema=get_entity(EntityKey("eap_spans")).get_data_model(),
-        sample=None,
-    )
+    if use_eap_items_table(request.meta):
+        entity = Entity(
+            key=EntityKey("eap_items"),
+            schema=get_entity(EntityKey("eap_items")).get_data_model(),
+            sample=None,
+        )
+    else:
+        entity = Entity(
+            key=EntityKey("eap_spans"),
+            schema=get_entity(EntityKey("eap_spans")).get_data_model(),
+            sample=None,
+        )
path: snuba/web/rpc/v1/resolvers/R_eap_spans/resolver_trace_item_table.py
user: login: xurui-c
body: nit, nonblocking
this part is the same as in `resolver_time_series.py` right? can we consolidate?
created_at: 2025-03-19T21:30:45Z
author_association: MEMBER
diff_hunk: @@ -15,14 +15,17 @@
 
 
 class HexIntColumnProcessor(BaseTypeConverter):
-    def __init__(self, columns: Set[str]) -> None:
+    def __init__(self, columns: Set[str], size: int = 16) -> None:
         super().__init__(columns, optimize_ordering=True)
-        self._size = 16
+        self._size = size
path: snuba/query/processors/physical/hexint_column_processor.py
user: login: volokluev
body: you should specify what "size" means, not very clear just looking at the signature or variable name
created_at: 2025-03-19T21:31:27Z
author_association: MEMBER
diff_hunk: @@ -40,6 +40,21 @@
     "sentry.end_timestamp",
 }
 
+ATTRIBUTES_TO_EXCLUDE_IN_EAP_ITEMS: Final[Set[str]] = {
+    "sentry.raw_description",
+    "sentry.transaction",
+    "sentry.start_timestamp_precise",
+    "sentry.end_timestamp_precise",
+    "sentry.duration_ms",
+    "sentry.event_id",
+    "sentry.exclusive_time_ms",
+    "sentry.is_segment",
+    "sentry.parent_span_id",
+    "sentry.profile_id",
+    "sentry.received",
+    "sentry.segment_id",
+}
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: can you add a comment to explain what this is for?
created_at: 2025-03-19T21:34:40Z
author_association: MEMBER
diff_hunk: @@ -57,6 +72,7 @@
         AttributeKey.Type.TYPE_STRING
     ],  # this gets converted from a uuid to a string in a storage processor
     f"{COLUMN_PREFIX}item_id": [AttributeKey.Type.TYPE_STRING],
+    f"{COLUMN_PREFIX}sampling_weight": [AttributeKey.Type.TYPE_DOUBLE],
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: sampling weight is a double?
created_at: 2025-03-19T21:34:59Z
author_association: MEMBER
diff_hunk: @@ -80,6 +96,8 @@
     "sentry.description": "sentry.normalized_description",
     "sentry.span_id": "sentry.item_id",
     "sentry.segment_name": "sentry.transaction",
+    "sentry.start_timestamp": "sentry.start_timestamp_precise",
+    "sentry.end_timestamp": "sentry.end_timestamp_precise",
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: can you add a comment explaining what these are as well?


created_at: 2025-03-19T21:35:52Z
author_association: MEMBER
diff_hunk: @@ -80,6 +96,8 @@
     "sentry.description": "sentry.normalized_description",
     "sentry.span_id": "sentry.item_id",
     "sentry.segment_name": "sentry.transaction",
+    "sentry.start_timestamp": "sentry.start_timestamp_precise",
+    "sentry.end_timestamp": "sentry.end_timestamp_precise",
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: I may need to use them in my work too but I need to understand how they're meant to be used
created_at: 2025-03-19T21:36:14Z
author_association: MEMBER
diff_hunk: @@ -112,26 +130,30 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             f"attribute key {attr_key.name} must have a type specified"
         )
 
-    if attr_key.name in NORMALIZED_COLUMNS_EAP_ITEMS:
-        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name]:
+    attr_name = attr_key.name
+    if attr_key.name in ATTRIBUTE_MAPPINGS:
+        attr_name = ATTRIBUTE_MAPPINGS[attr_key.name]
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: nit: 
```suggestion
    attr_name = ATTRIBUTE_MAPPINGS.get(attr_key.name, attr_key.name)
```
created_at: 2025-03-19T21:37:14Z
author_association: MEMBER
diff_hunk: @@ -15,14 +15,17 @@
 
 
 class HexIntColumnProcessor(BaseTypeConverter):
-    def __init__(self, columns: Set[str]) -> None:
+    def __init__(self, columns: Set[str], size: int = 16) -> None:
         super().__init__(columns, optimize_ordering=True)
-        self._size = 16
+        self._size = size
 
     def _translate_literal(self, exp: Literal) -> Literal:
         try:
             assert isinstance(exp.value, str)
-            return Literal(alias=exp.alias, value=int(exp.value, self._size))
+            # 128 bit integers in clickhouse need to be referenced as strings
+            if self._size == 32:
+                return Literal(alias=exp.alias, value=str(int(exp.value, 16)))
+            return Literal(alias=exp.alias, value=int(exp.value, 16))
         except (AssertionError, ValueError):
             raise ColumnTypeError("Invalid hexint", should_report=False)
path: snuba/query/processors/physical/hexint_column_processor.py
user: login: onkar
body: Sorry, the code is wrong. I meant should it be converted to either a `str` or an `int`?
created_at: 2025-03-19T21:38:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,26 +130,30 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             f"attribute key {attr_key.name} must have a type specified"
         )
 
-    if attr_key.name in NORMALIZED_COLUMNS_EAP_ITEMS:
-        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name]:
+    attr_name = attr_key.name
+    if attr_key.name in ATTRIBUTE_MAPPINGS:
+        attr_name = ATTRIBUTE_MAPPINGS[attr_key.name]
+
+    if attr_name in NORMALIZED_COLUMNS_EAP_ITEMS:
+        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_name]:
             formatted_attribute_types = ", ".join(
-                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name])
+                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_name])
             )
             raise BadSnubaRPCRequestException(
                 f"Attribute {attr_key.name} must be one of [{formatted_attribute_types}], got {AttributeKey.Type.Name(attr_key.type)}"
             )
 
+        # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        if attr_key.name == "sentry.span_id":
+            return f.right(column(attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias)
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: what is the backwards incompatible behavior that happens if you send back the full span_id?I imagine that this may actually break the client which would use this span id to look something up
created_at: 2025-03-19T21:38:53Z
author_association: MEMBER
diff_hunk: @@ -62,7 +63,7 @@ def gen_message(
         timestamp += random.random()
     return {
         "description": span_name,
-        "duration_ms": 152,
+        "duration_ms": 1000,
path: tests/web/rpc/v1/test_endpoint_get_traces.py
user: login: volokluev
body: why the change
created_at: 2025-03-19T21:41:51Z
author_association: MEMBER
diff_hunk: @@ -112,26 +130,30 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             f"attribute key {attr_key.name} must have a type specified"
         )
 
-    if attr_key.name in NORMALIZED_COLUMNS_EAP_ITEMS:
-        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name]:
+    attr_name = attr_key.name
+    if attr_key.name in ATTRIBUTE_MAPPINGS:
+        attr_name = ATTRIBUTE_MAPPINGS[attr_key.name]
+
+    if attr_name in NORMALIZED_COLUMNS_EAP_ITEMS:
+        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_name]:
             formatted_attribute_types = ", ".join(
-                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name])
+                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_name])
             )
             raise BadSnubaRPCRequestException(
                 f"Attribute {attr_key.name} must be one of [{formatted_attribute_types}], got {AttributeKey.Type.Name(attr_key.type)}"
             )
 
+        # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        if attr_key.name == "sentry.span_id":
+            return f.right(column(attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias)
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: span_id is a UInt64 while item_id is a UInt128, so in order to maintain backwards compatibility, we only care about the last 8 bytes for span_id

for example, imagine a query that looks like `select * where span_id = '<8 byte hex string>'`. this wouldn't work if we replaced span_id with item_id
created_at: 2025-03-19T21:52:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,7 +63,7 @@ def gen_message(
         timestamp += random.random()
     return {
         "description": span_name,
-        "duration_ms": 152,
+        "duration_ms": 1000,
path: tests/web/rpc/v1/test_endpoint_get_traces.py
user: login: davidtsuk
body: I answered this last time:

> In eap_spans we calculated the duration ourselves by doing end_timestamp - start_timestamp. In eap_items, we store duration_ms directly from the message, so this change is to account for that.

created_at: 2025-03-19T21:53:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,26 +130,30 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             f"attribute key {attr_key.name} must have a type specified"
         )
 
-    if attr_key.name in NORMALIZED_COLUMNS_EAP_ITEMS:
-        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name]:
+    attr_name = attr_key.name
+    if attr_key.name in ATTRIBUTE_MAPPINGS:
+        attr_name = ATTRIBUTE_MAPPINGS[attr_key.name]
+
+    if attr_name in NORMALIZED_COLUMNS_EAP_ITEMS:
+        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_name]:
             formatted_attribute_types = ", ".join(
-                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name])
+                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_name])
             )
             raise BadSnubaRPCRequestException(
                 f"Attribute {attr_key.name} must be one of [{formatted_attribute_types}], got {AttributeKey.Type.Name(attr_key.type)}"
             )
 
+        # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        if attr_key.name == "sentry.span_id":
+            return f.right(column(attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias)
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: so when we store a span in eap_items, they all have the form:

`00000000f123dbee`

?
created_at: 2025-03-19T21:54:48Z
author_association: MEMBER
diff_hunk: @@ -62,7 +63,7 @@ def gen_message(
         timestamp += random.random()
     return {
         "description": span_name,
-        "duration_ms": 152,
+        "duration_ms": 1000,
path: tests/web/rpc/v1/test_endpoint_get_traces.py
user: login: volokluev
body: cool
created_at: 2025-03-19T21:55:47Z
author_association: MEMBER
diff_hunk: @@ -57,6 +72,7 @@
         AttributeKey.Type.TYPE_STRING
     ],  # this gets converted from a uuid to a string in a storage processor
     f"{COLUMN_PREFIX}item_id": [AttributeKey.Type.TYPE_STRING],
+    f"{COLUMN_PREFIX}sampling_weight": [AttributeKey.Type.TYPE_DOUBLE],
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: this is what we did with eap_spans, so I kept it the same
created_at: 2025-03-19T21:59:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,14 +15,17 @@
 
 
 class HexIntColumnProcessor(BaseTypeConverter):
-    def __init__(self, columns: Set[str]) -> None:
+    def __init__(self, columns: Set[str], size: int = 16) -> None:
         super().__init__(columns, optimize_ordering=True)
-        self._size = 16
+        self._size = size
 
     def _translate_literal(self, exp: Literal) -> Literal:
         try:
             assert isinstance(exp.value, str)
-            return Literal(alias=exp.alias, value=int(exp.value, self._size))
+            # 128 bit integers in clickhouse need to be referenced as strings
+            if self._size == 32:
+                return Literal(alias=exp.alias, value=str(int(exp.value, 16)))
+            return Literal(alias=exp.alias, value=int(exp.value, 16))
         except (AssertionError, ValueError):
             raise ColumnTypeError("Invalid hexint", should_report=False)
path: snuba/query/processors/physical/hexint_column_processor.py
user: login: davidtsuk
body: in order to maintain backwards compatibility, I didn't want my change to affect other parts of the codebase. this change was added due to a quirk with how clickhouse handles 128 bit integers
created_at: 2025-03-19T22:00:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,26 +130,30 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             f"attribute key {attr_key.name} must have a type specified"
         )
 
-    if attr_key.name in NORMALIZED_COLUMNS_EAP_ITEMS:
-        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name]:
+    attr_name = attr_key.name
+    if attr_key.name in ATTRIBUTE_MAPPINGS:
+        attr_name = ATTRIBUTE_MAPPINGS[attr_key.name]
+
+    if attr_name in NORMALIZED_COLUMNS_EAP_ITEMS:
+        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_name]:
             formatted_attribute_types = ", ".join(
-                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name])
+                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_name])
             )
             raise BadSnubaRPCRequestException(
                 f"Attribute {attr_key.name} must be one of [{formatted_attribute_types}], got {AttributeKey.Type.Name(attr_key.type)}"
             )
 
+        # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        if attr_key.name == "sentry.span_id":
+            return f.right(column(attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias)
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: In eap_items, they're just integers, there's no issue fitting a 64 bit integer in a 128 bit integer. The problem is that this is just our internal representation, but when a user interacts with span_id through EAP, it's treated as a hex string with 16 characters. However, a 128 bit integer cannot be represented as a 16 character hex string (16 characters can at most represent a 64 bit integer). Hence, by default we represent item_id as a 32 character hex string. Since the user expects 16 characters and we know that a span_id will currently never use the full 128 bits, it's safe to get rid of the first 16 characters since we know those will just be padding.
created_at: 2025-03-19T22:09:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,26 +130,30 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             f"attribute key {attr_key.name} must have a type specified"
         )
 
-    if attr_key.name in NORMALIZED_COLUMNS_EAP_ITEMS:
-        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name]:
+    attr_name = attr_key.name
+    if attr_key.name in ATTRIBUTE_MAPPINGS:
+        attr_name = ATTRIBUTE_MAPPINGS[attr_key.name]
+
+    if attr_name in NORMALIZED_COLUMNS_EAP_ITEMS:
+        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_name]:
             formatted_attribute_types = ", ".join(
-                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name])
+                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_name])
             )
             raise BadSnubaRPCRequestException(
                 f"Attribute {attr_key.name} must be one of [{formatted_attribute_types}], got {AttributeKey.Type.Name(attr_key.type)}"
             )
 
+        # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        if attr_key.name == "sentry.span_id":
+            return f.right(column(attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias)
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: ok that's helpful context. I think your above comment can be put into a code comment above.

Also is there a test in snuba that reads the span_id with this truncation mechanism?
created_at: 2025-03-19T22:20:19Z
author_association: MEMBER
diff_hunk: @@ -15,14 +15,17 @@
 
 
 class HexIntColumnProcessor(BaseTypeConverter):
-    def __init__(self, columns: Set[str]) -> None:
+    def __init__(self, columns: Set[str], size: int = 16) -> None:
         super().__init__(columns, optimize_ordering=True)
-        self._size = 16
+        self._size = size
path: snuba/query/processors/physical/hexint_column_processor.py
user: login: davidtsuk
body: added a comment
created_at: 2025-03-19T22:20:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +40,21 @@
     "sentry.end_timestamp",
 }
 
+ATTRIBUTES_TO_EXCLUDE_IN_EAP_ITEMS: Final[Set[str]] = {
+    "sentry.raw_description",
+    "sentry.transaction",
+    "sentry.start_timestamp_precise",
+    "sentry.end_timestamp_precise",
+    "sentry.duration_ms",
+    "sentry.event_id",
+    "sentry.exclusive_time_ms",
+    "sentry.is_segment",
+    "sentry.parent_span_id",
+    "sentry.profile_id",
+    "sentry.received",
+    "sentry.segment_id",
+}
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: done
created_at: 2025-03-19T22:20:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -80,6 +96,8 @@
     "sentry.description": "sentry.normalized_description",
     "sentry.span_id": "sentry.item_id",
     "sentry.segment_name": "sentry.transaction",
+    "sentry.start_timestamp": "sentry.start_timestamp_precise",
+    "sentry.end_timestamp": "sentry.end_timestamp_precise",
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: added a comment, let me know if it's unclear
created_at: 2025-03-19T22:21:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,13 +111,24 @@ def use_eap_items_table(request_meta: RequestMeta) -> bool:
     if settings.USE_EAP_ITEMS_TABLE:
         return True
 
+    use_eap_items_orgs = state.get_config("use_eap_items_orgs")
+    if use_eap_items_orgs:
+        use_eap_items_orgs = map(int, use_eap_items_orgs.strip("[]").split(","))
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: someone mistypes an input in runtime config they can take down the entire API
created_at: 2025-03-19T22:22:17Z
author_association: MEMBER
diff_hunk: @@ -108,13 +111,24 @@ def use_eap_items_table(request_meta: RequestMeta) -> bool:
     if settings.USE_EAP_ITEMS_TABLE:
         return True
 
+    use_eap_items_orgs = state.get_config("use_eap_items_orgs")
+    if use_eap_items_orgs:
+        use_eap_items_orgs = map(int, use_eap_items_orgs.strip("[]").split(","))
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: good point
created_at: 2025-03-19T22:24:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,26 +130,30 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             f"attribute key {attr_key.name} must have a type specified"
         )
 
-    if attr_key.name in NORMALIZED_COLUMNS_EAP_ITEMS:
-        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name]:
+    attr_name = attr_key.name
+    if attr_key.name in ATTRIBUTE_MAPPINGS:
+        attr_name = ATTRIBUTE_MAPPINGS[attr_key.name]
+
+    if attr_name in NORMALIZED_COLUMNS_EAP_ITEMS:
+        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_name]:
             formatted_attribute_types = ", ".join(
-                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name])
+                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_name])
             )
             raise BadSnubaRPCRequestException(
                 f"Attribute {attr_key.name} must be one of [{formatted_attribute_types}], got {AttributeKey.Type.Name(attr_key.type)}"
             )
 
+        # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        if attr_key.name == "sentry.span_id":
+            return f.right(column(attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias)
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: I believe all the snuba tests that use `span_id` assume it's 16 characters. For example: https://github.com/getsentry/snuba/blob/9ba51e7db6d9850a237e583dc6731047f86dbc46/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py#L443
created_at: 2025-03-19T22:26:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,13 +111,24 @@ def use_eap_items_table(request_meta: RequestMeta) -> bool:
     if settings.USE_EAP_ITEMS_TABLE:
         return True
 
+    use_eap_items_orgs = state.get_config("use_eap_items_orgs")
+    if use_eap_items_orgs:
+        use_eap_items_orgs = map(int, use_eap_items_orgs.strip("[]").split(","))
+
     use_eap_items_table_start_timestamp_seconds = state.get_int_config(
         "use_eap_items_table_start_timestamp_seconds"
     )
+
     if (
         state.get_config("use_eap_items_table", False)
         and use_eap_items_table_start_timestamp_seconds is not None
     ):
+        if (
+            use_eap_items_orgs
+            and request_meta.organization_id not in use_eap_items_orgs
+        ):
+            return False
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: this logic is a bit weird. You'd have to eventually put in every org id to turn it on for everyone. I think instead it should be:

1. `use_eap_items_table_all` - turned on for everyone
2. `turned_on_for_org`-  `request_meta.organization_id in use_eap_items_orgs`
3. `within_items_timerange` - aka ` request_meta.start_timestamp.seconds  >= use_eap_items_table_start_timestamp_seconds` 

`return within_items_timerange and (use_eap_items_table_all or turned_on_for_org)`
created_at: 2025-03-19T22:28:31Z
author_association: MEMBER
diff_hunk: @@ -112,26 +130,30 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             f"attribute key {attr_key.name} must have a type specified"
         )
 
-    if attr_key.name in NORMALIZED_COLUMNS_EAP_ITEMS:
-        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name]:
+    attr_name = attr_key.name
+    if attr_key.name in ATTRIBUTE_MAPPINGS:
+        attr_name = ATTRIBUTE_MAPPINGS[attr_key.name]
+
+    if attr_name in NORMALIZED_COLUMNS_EAP_ITEMS:
+        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_name]:
             formatted_attribute_types = ", ".join(
-                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name])
+                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_name])
             )
             raise BadSnubaRPCRequestException(
                 f"Attribute {attr_key.name} must be one of [{formatted_attribute_types}], got {AttributeKey.Type.Name(attr_key.type)}"
             )
 
+        # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        if attr_key.name == "sentry.span_id":
+            return f.right(column(attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias)
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: but this test is not searching by span_id. What I'm saying is that if we write a 64bit span id to the eap_items table, and we search by that 16 character hex string, we should get that span back. There should be a test for that
created_at: 2025-03-19T22:30:49Z
author_association: MEMBER
diff_hunk: @@ -155,6 +155,11 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             )
 
         # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        # In eap_items, they're just integers, there's no issue fitting a 64 bit integer in a 128 bit integer.
+        # The problem is that this is just our internal representation, but when a user interacts with span_id through EAP, it's treated as a hex string with 16 characters.
+        # However, a 128 bit integer cannot be represented as a 16 character hex string (16 characters can at most represent a 64 bit integer). Hence,
+        # by default we represent item_id as a 32 character hex string. Since the user expects 16 characters and we know that a span_id will currently never use the full 128 bits,
+        # it's safe to get rid of the first 16 characters since we know those will just be padding.
         if attr_key.name == "sentry.span_id":
             return f.right(column(attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias)
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: also here, we're using `attr_key.name` and in the expression it's `attr_name[len(COLUMN_PREFIX) :]` should those be the same?
created_at: 2025-03-19T22:33:41Z
author_association: MEMBER
diff_hunk: @@ -155,6 +155,11 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             )
 
         # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        # In eap_items, they're just integers, there's no issue fitting a 64 bit integer in a 128 bit integer.
+        # The problem is that this is just our internal representation, but when a user interacts with span_id through EAP, it's treated as a hex string with 16 characters.
+        # However, a 128 bit integer cannot be represented as a 16 character hex string (16 characters can at most represent a 64 bit integer). Hence,
+        # by default we represent item_id as a 32 character hex string. Since the user expects 16 characters and we know that a span_id will currently never use the full 128 bits,
+        # it's safe to get rid of the first 16 characters since we know those will just be padding.
         if attr_key.name == "sentry.span_id":
             return f.right(column(attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias)
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: They're intentionally not the same because one of them refers to the external attr_key name and the other refers to the internal name. I realize now the naming is probably confusing, so I'll update it.
created_at: 2025-03-19T22:35:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,13 +111,24 @@ def use_eap_items_table(request_meta: RequestMeta) -> bool:
     if settings.USE_EAP_ITEMS_TABLE:
         return True
 
+    use_eap_items_orgs = state.get_config("use_eap_items_orgs")
+    if use_eap_items_orgs:
+        use_eap_items_orgs = map(int, use_eap_items_orgs.strip("[]").split(","))
+
     use_eap_items_table_start_timestamp_seconds = state.get_int_config(
         "use_eap_items_table_start_timestamp_seconds"
     )
+
     if (
         state.get_config("use_eap_items_table", False)
         and use_eap_items_table_start_timestamp_seconds is not None
     ):
+        if (
+            use_eap_items_orgs
+            and request_meta.organization_id not in use_eap_items_orgs
+        ):
+            return False
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: I've fixed up the logic. The idea is `use_eap_items_table_all` will be true by default unless a specific list of orgs is provided
created_at: 2025-03-19T22:43:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,26 +130,30 @@ def attribute_key_to_expression_eap_items(attr_key: AttributeKey) -> Expression:
             f"attribute key {attr_key.name} must have a type specified"
         )
 
-    if attr_key.name in NORMALIZED_COLUMNS_EAP_ITEMS:
-        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name]:
+    attr_name = attr_key.name
+    if attr_key.name in ATTRIBUTE_MAPPINGS:
+        attr_name = ATTRIBUTE_MAPPINGS[attr_key.name]
+
+    if attr_name in NORMALIZED_COLUMNS_EAP_ITEMS:
+        if attr_key.type not in NORMALIZED_COLUMNS_EAP_ITEMS[attr_name]:
             formatted_attribute_types = ", ".join(
-                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_key.name])
+                map(AttributeKey.Type.Name, NORMALIZED_COLUMNS_EAP_ITEMS[attr_name])
             )
             raise BadSnubaRPCRequestException(
                 f"Attribute {attr_key.name} must be one of [{formatted_attribute_types}], got {AttributeKey.Type.Name(attr_key.type)}"
             )
 
+        # To maintain backwards compatibility with the old span_id column, we only need the last 16 characters of the item_id
+        if attr_key.name == "sentry.span_id":
+            return f.right(column(attr_name[len(COLUMN_PREFIX) :]), 16, alias=alias)
+
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: done
created_at: 2025-03-19T22:52:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,24 +111,32 @@ def use_eap_items_table(request_meta: RequestMeta) -> bool:
     if settings.USE_EAP_ITEMS_TABLE:
         return True
 
-    use_eap_items_orgs = state.get_config("use_eap_items_orgs")
+    use_eap_items_orgs = state.get_str_config("use_eap_items_orgs")
+    eap_items_enabled_orgs = []
+    use_eap_items_for_all_orgs = True
     if use_eap_items_orgs:
-        use_eap_items_orgs = map(int, use_eap_items_orgs.strip("[]").split(","))
+        try:
+            eap_items_enabled_orgs = list(
+                map(int, use_eap_items_orgs.strip("[]").split(","))
+            )
+            use_eap_items_for_all_orgs = False
+        except ValueError:
+            pass
+
+    turned_on_for_org = (
+        use_eap_items_for_all_orgs
+        or request_meta.organization_id in eap_items_enabled_orgs
+    )
 
     use_eap_items_table_start_timestamp_seconds = state.get_int_config(
         "use_eap_items_table_start_timestamp_seconds"
     )
 
     if (
-        state.get_config("use_eap_items_table", False)
+        state.get_int_config("use_eap_items_table", 0)
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: nit: is this config still necessary?
created_at: 2025-03-19T23:09:28Z
author_association: MEMBER
diff_hunk: @@ -111,24 +111,32 @@ def use_eap_items_table(request_meta: RequestMeta) -> bool:
     if settings.USE_EAP_ITEMS_TABLE:
         return True
 
-    use_eap_items_orgs = state.get_config("use_eap_items_orgs")
+    use_eap_items_orgs = state.get_str_config("use_eap_items_orgs")
+    eap_items_enabled_orgs = []
+    use_eap_items_for_all_orgs = True
     if use_eap_items_orgs:
-        use_eap_items_orgs = map(int, use_eap_items_orgs.strip("[]").split(","))
+        try:
+            eap_items_enabled_orgs = list(
+                map(int, use_eap_items_orgs.strip("[]").split(","))
+            )
+            use_eap_items_for_all_orgs = False
+        except ValueError:
+            pass
+
+    turned_on_for_org = (
+        use_eap_items_for_all_orgs
+        or request_meta.organization_id in eap_items_enabled_orgs
+    )
 
     use_eap_items_table_start_timestamp_seconds = state.get_int_config(
         "use_eap_items_table_start_timestamp_seconds"
     )
 
     if (
-        state.get_config("use_eap_items_table", False)
+        state.get_int_config("use_eap_items_table", 0)
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: davidtsuk
body: just for extra safety i guess and an easy way to turn it off if needed
created_at: 2025-03-19T23:11:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,24 +111,32 @@ def use_eap_items_table(request_meta: RequestMeta) -> bool:
     if settings.USE_EAP_ITEMS_TABLE:
         return True
 
-    use_eap_items_orgs = state.get_config("use_eap_items_orgs")
+    use_eap_items_orgs = state.get_str_config("use_eap_items_orgs")
+    eap_items_enabled_orgs = []
+    use_eap_items_for_all_orgs = True
     if use_eap_items_orgs:
-        use_eap_items_orgs = map(int, use_eap_items_orgs.strip("[]").split(","))
+        try:
+            eap_items_enabled_orgs = list(
+                map(int, use_eap_items_orgs.strip("[]").split(","))
+            )
+            use_eap_items_for_all_orgs = False
+        except ValueError:
+            pass
+
+    turned_on_for_org = (
+        use_eap_items_for_all_orgs
+        or request_meta.organization_id in eap_items_enabled_orgs
+    )
 
     use_eap_items_table_start_timestamp_seconds = state.get_int_config(
         "use_eap_items_table_start_timestamp_seconds"
     )
 
     if (
-        state.get_config("use_eap_items_table", False)
+        state.get_int_config("use_eap_items_table", 0)
path: snuba/web/rpc/v1/resolvers/R_eap_spans/common/common.py
user: login: volokluev
body: up to you if you wanna keep it, I probably wouldn't
created_at: 2025-03-19T23:13:16Z
author_association: MEMBER

PR comments:
user: login: volokluev
created_at: 2025-03-19T18:04:47Z
body: What was the change that fixed the sentry CI breakage? Is that test running in CI now?
author_association: MEMBER
user: login: davidtsuk
created_at: 2025-03-19T18:17:16Z
body: > What was the change that fixed the sentry CI breakage? Is that test running in CI now?

I added the test to CI, but I haven't fixed the breakage yet.
author_association: CONTRIBUTOR
user: login: davidtsuk
created_at: 2025-03-19T19:57:24Z
body: @volokluev @mcannizz I added the test to the CI and made it backwards compatible now. The problem was that the test wasn't expecting to receive the new attributes we were adding to the attributes columns, which were previously stored in their own columns. I have excluded them from the results and added a test to ensure that we return the same number of attributes as before.
author_association: CONTRIBUTOR
user: login: volokluev
created_at: 2025-03-19T22:08:28Z
body: In the runtime config, are you able to turn this on for one org at a time or is it an all or nothing thing?
author_association: MEMBER
user: login: davidtsuk
created_at: 2025-03-19T22:10:16Z
body: > In the runtime config, are you able to turn this on for one org at a time or is it an all or nothing thing?

I was planning to add that option in a separate PR, but since I'm already addressing other comments I'll just do it in this PR.
author_association: CONTRIBUTOR