This is an automated email from the ASF dual-hosted git repository.

hussein-awala pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 11b49e75593 Add extra field filtering for asset events (#64611)
11b49e75593 is described below

commit 11b49e755930351e5b322655db506b5fe3cd858e
Author: Hussein Awala <[email protected]>
AuthorDate: Tue Jul 7 20:27:34 2026 +0200

    Add extra field filtering for asset events (#64611)
---
 .../docs/authoring-and-scheduling/assets.rst       |  20 ++
 airflow-core/docs/migrations-ref.rst               |   4 +-
 .../src/airflow/api_fastapi/common/parameters.py   |  56 ++++++
 .../core_api/openapi/v2-rest-api-generated.yaml    |  12 ++
 .../api_fastapi/core_api/routes/public/assets.py   |   3 +
 .../execution_api/routes/asset_events.py           |  38 +++-
 airflow-core/src/airflow/migrations/env.py         |  10 +
 ...124_3_4_0_add_gin_index_on_asset_event_extra.py |  57 ++++++
 .../src/airflow/ui/openapi-gen/queries/common.ts   |   5 +-
 .../ui/openapi-gen/queries/ensureQueryData.ts      |   6 +-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |   6 +-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |   6 +-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |   6 +-
 .../ui/openapi-gen/requests/services.gen.ts        |   2 +
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |   4 +
 airflow-core/src/airflow/utils/db.py               |   1 +
 airflow-core/src/airflow/utils/sqlalchemy.py       |  55 +++++-
 .../core_api/routes/public/test_assets.py          |  81 ++++++++
 .../versions/head/test_asset_events.py             | 207 +++++++++++++++++++++
 airflow-core/tests/unit/utils/test_db.py           |   2 +
 task-sdk/src/airflow/sdk/api/client.py             |  12 +-
 task-sdk/src/airflow/sdk/execution_time/comms.py   |   2 +
 task-sdk/src/airflow/sdk/execution_time/context.py |   8 +
 .../airflow/sdk/execution_time/schema/schema.json  |  30 +++
 .../src/airflow/sdk/execution_time/supervisor.py   |   2 +
 task-sdk/tests/task_sdk/api/test_client.py         |  62 ++++++
 .../tests/task_sdk/execution_time/test_context.py  |  45 +++++
 .../task_sdk/execution_time/test_supervisor.py     |  88 +++++++++
 28 files changed, 813 insertions(+), 17 deletions(-)

diff --git a/airflow-core/docs/authoring-and-scheduling/assets.rst 
b/airflow-core/docs/authoring-and-scheduling/assets.rst
index b5561392587..2a7990d233b 100644
--- a/airflow-core/docs/authoring-and-scheduling/assets.rst
+++ b/airflow-core/docs/authoring-and-scheduling/assets.rst
@@ -247,6 +247,26 @@ Inlet asset events can be read with the ``inlet_events`` 
accessor in the executi
 
 Each value in the ``inlet_events`` mapping is a sequence-like object that 
orders past events of a given asset by ``timestamp``, earliest to latest. It 
supports most of Python's list interface, so you can use ``[-1]`` to access the 
last event, ``[-2:]`` for the last two, etc. The accessor is lazy and only hits 
the database when you access items inside it.
 
+The accessor also supports chaining methods to filter events before fetching 
them. For example, to retrieve only events where specific ``extra`` keys match 
given values:
+
+.. code-block:: python
+
+    @task(inlets=[regional_sales])
+    def process_high_priority(*, inlet_events):
+        events = inlet_events[regional_sales].extra("region", 
"us").extra("env", "prod")
+        for event in events:
+            print(event.extra)
+
+The ``.extra(key, value)`` method can be chained multiple times; all 
conditions are combined with AND logic. Other chaining methods include 
``.after(timestamp)``, ``.before(timestamp)``, ``.ascending()``, and 
``.limit(n)``.
+
+The REST API also supports filtering asset events by extra key-value pairs 
using the ``extra`` query parameter (repeat for multiple conditions):
+
+.. code-block:: bash
+
+    curl 
"http://<airflow-host>/api/v2/assets/events?extra=region%3Dus&extra=env%3Dprod"
+
+Each ``extra`` value uses ``key=value`` format. Multiple entries are combined 
with AND logic.
+
 Dependency between ``@asset``, ``@task``, and classic operators
 ---------------------------------------------------------------
 
diff --git a/airflow-core/docs/migrations-ref.rst 
b/airflow-core/docs/migrations-ref.rst
index d2cfde62352..975d35981fe 100644
--- a/airflow-core/docs/migrations-ref.rst
+++ b/airflow-core/docs/migrations-ref.rst
@@ -39,7 +39,9 @@ Here's the list of all the Database Migrations that are 
executed via when you ru
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
 | Revision ID             | Revises ID       | Airflow Version   | Description 
                                                 |
 
+=========================+==================+===================+==============================================================+
-| ``d2f4e1b3c5a7`` (head) | ``9ff64e1c35d3`` | ``3.3.0``         | Add 
partition_date to asset_partition_dag_run.               |
+| ``5a5d3253e946`` (head) | ``d2f4e1b3c5a7`` | ``3.4.0``         | Add GIN 
index on asset_event.extra for PostgreSQL.           |
++-------------------------+------------------+-------------------+--------------------------------------------------------------+
+| ``d2f4e1b3c5a7``        | ``9ff64e1c35d3`` | ``3.3.0``         | Add 
partition_date to asset_partition_dag_run.               |
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
 | ``9ff64e1c35d3``        | ``dd5f3a8e2b91`` | ``3.3.0``         | Add indexes 
on dag_run.created_dag_version_id and            |
 |                         |                  |                   | 
task_instance.dag_version_id.                                |
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py 
b/airflow-core/src/airflow/api_fastapi/common/parameters.py
index 935b547c1a7..7b235f07de3 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py
@@ -68,6 +68,7 @@ from airflow.models.taskinstance import TaskInstance
 from airflow.models.variable import Variable
 from airflow.models.xcom import XComModel
 from airflow.typing_compat import Self
+from airflow.utils.sqlalchemy import JsonContains
 from airflow.utils.state import DagRunState, TaskInstanceState
 from airflow.utils.types import DagRunType
 
@@ -553,6 +554,60 @@ def prefix_search_param_factory(
     return depends_prefix_search
 
 
+class _JsonKVFilter(BaseParam[dict[str, str]]):
+    """
+    Filter on a JSON column by multiple key-value pairs (AND logic).
+
+    Uses dialect-aware SQL: ``@>`` (JSONB containment, GIN-indexable) on
+    PostgreSQL, ``JSON_CONTAINS`` on MySQL, and ``JSON_EXTRACT`` on SQLite.
+    """
+
+    def __init__(
+        self,
+        attribute: ColumnElement,
+        value: dict[str, str] | None = None,
+        skip_none: bool = True,
+    ) -> None:
+        super().__init__(skip_none=skip_none)
+        self.attribute: ColumnElement = attribute
+        self.value = value
+
+    def to_orm(self, select: Select) -> Select:
+        if not self.value:
+            return select
+        return select.where(JsonContains(self.attribute, self.value))
+
+    @classmethod
+    def depends(cls, *args: Any, **kwargs: Any) -> Self:
+        raise NotImplementedError("Use json_kv_filter_factory instead.")
+
+
+def json_kv_filter_factory(
+    attribute: ColumnElement,
+    param_name: str = "extra",
+) -> Callable[[list[str]], _JsonKVFilter]:
+    DESCRIPTION = (
+        "Filter by JSON key-value pairs. Repeat for multiple conditions (AND 
logic). "
+        "Format: key=value (e.g. extra=region=us&extra=env=prod)."
+    )
+
+    def depends_json_kv(
+        values: list[str] = Query(alias=param_name, default_factory=list, 
description=DESCRIPTION),
+    ) -> _JsonKVFilter:
+        kv_dict: dict[str, str] = {}
+        for item in values:
+            if "=" not in item:
+                raise HTTPException(
+                    status_code=HTTP_422_UNPROCESSABLE_CONTENT,
+                    detail=f"Invalid {param_name} parameter format: {item!r}. 
Expected 'key=value'.",
+                )
+            k, v = item.split("=", 1)
+            kv_dict[k] = v
+        return _JsonKVFilter(attribute, kv_dict or None)
+
+    return depends_json_kv
+
+
 class SortParam(BaseParam[list[str]]):
     """Order result by the attribute."""
 
@@ -1587,6 +1642,7 @@ QueryAssetAliasNamePrefixPatternSearch = Annotated[
 QueryAssetDagIdPatternSearch = Annotated[
     _DagIdAssetReferenceFilter, Depends(_DagIdAssetReferenceFilter.depends)
 ]
+QueryAssetEventExtraFilter = Annotated[_JsonKVFilter, 
Depends(json_kv_filter_factory(AssetEvent.extra))]
 QueryPartitionedDagRunHasCreatedDagRunIdFilter = Annotated[
     FilterParam[bool | None],
     Depends(
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index 571d9cbeb9d..334fe6dae8d 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -478,6 +478,18 @@ paths:
           \ stays index-compatible under locale-aware collations \u2014 e.g. 
`test_`\
           \ effectively matches items starting with `test`, and `s3://` 
matches items\
           \ starting with `s3`."
+      - name: extra
+        in: query
+        required: false
+        schema:
+          type: array
+          items:
+            type: string
+          description: 'Filter by JSON key-value pairs. Repeat for multiple 
conditions
+            (AND logic). Format: key=value (e.g. 
extra=region=us&extra=env=prod).'
+          title: Extra
+        description: 'Filter by JSON key-value pairs. Repeat for multiple 
conditions
+          (AND logic). Format: key=value (e.g. 
extra=region=us&extra=env=prod).'
       - name: timestamp_gte
         in: query
         required: false
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py
index a19f4c7da77..d21b1c1cbb9 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py
@@ -37,6 +37,7 @@ from airflow.api_fastapi.common.parameters import (
     QueryAssetAliasNamePatternSearch,
     QueryAssetAliasNamePrefixPatternSearch,
     QueryAssetDagIdPatternSearch,
+    QueryAssetEventExtraFilter,
     QueryAssetNamePatternSearch,
     QueryAssetNamePrefixPatternSearch,
     QueryLimit,
@@ -321,6 +322,7 @@ def get_asset_events(
     ],
     name_pattern: QueryAssetNamePatternSearch,
     name_prefix_pattern: QueryAssetNamePrefixPatternSearch,
+    extra_filter: QueryAssetEventExtraFilter,
     timestamp_range: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("timestamp", AssetEvent))],
     session: SessionDep,
 ) -> AssetEventCollectionResponse:
@@ -339,6 +341,7 @@ def get_asset_events(
             source_map_index,
             name_pattern,
             name_prefix_pattern,
+            extra_filter,
             timestamp_range,
         ],
         order_by=order_by,
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py
index d0ecc3d3ada..b2a98a9de60 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/asset_events.py
@@ -30,6 +30,7 @@ from airflow.api_fastapi.execution_api.datamodels.asset_event 
import (
     AssetEventsResponse,
 )
 from airflow.models.asset import AssetAliasModel, AssetEvent, AssetModel
+from airflow.utils.sqlalchemy import JsonContains
 
 router = APIRouter(
     responses={
@@ -44,7 +45,7 @@ def _get_asset_events_through_sql_clauses(
 ) -> AssetEventsResponse:
     order_by_clause = AssetEvent.timestamp.asc() if ascending else 
AssetEvent.timestamp.desc()
     asset_events_query = 
select(AssetEvent).join(join_clause).where(where_clause).order_by(order_by_clause)
-    if limit:
+    if limit is not None:
         asset_events_query = asset_events_query.limit(limit)
     asset_events = session.scalars(asset_events_query)
     return AssetEventsResponse.model_validate(
@@ -73,6 +74,23 @@ def _get_asset_events_through_sql_clauses(
     )
 
 
+def _parse_extra_params(extra: list[str] | None) -> dict[str, str]:
+    """Parse repeated ``key=value`` query params into a dict."""
+    result: dict[str, str] = {}
+    for item in extra or []:
+        if "=" not in item:
+            raise HTTPException(
+                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+                detail={
+                    "reason": "Invalid parameter",
+                    "message": f"Invalid extra parameter format: {item!r}. 
Expected 'key=value'.",
+                },
+            )
+        k, v = item.split("=", 1)
+        result[k] = v
+    return result
+
+
 @router.get("/by-asset")
 def get_asset_event_by_asset_name_uri(
     name: Annotated[str | None, Query(description="The name of the Asset")],
@@ -82,6 +100,12 @@ def get_asset_event_by_asset_name_uri(
     before: Annotated[UtcDateTime | None, Query(description="The end of the 
time range")] = None,
     ascending: Annotated[bool, Query(description="Whether to sort results in 
ascending order")] = True,
     limit: Annotated[int | None, Query(description="The maximum number of 
results to return")] = None,
+    extra: Annotated[
+        list[str] | None,
+        Query(
+            description="Filter by extra JSON key-value pairs. Format: 
key=value. Repeat for AND logic.",
+        ),
+    ] = None,
 ) -> AssetEventsResponse:
     if name and uri:
         where_clause = and_(AssetModel.name == name, AssetModel.uri == uri)
@@ -102,6 +126,9 @@ def get_asset_event_by_asset_name_uri(
         where_clause = and_(where_clause, AssetEvent.timestamp >= after)
     if before:
         where_clause = and_(where_clause, AssetEvent.timestamp <= before)
+    extra_dict = _parse_extra_params(extra)
+    if extra_dict:
+        where_clause = and_(where_clause, JsonContains(AssetEvent.extra, 
extra_dict))
 
     return _get_asset_events_through_sql_clauses(
         join_clause=AssetEvent.asset,
@@ -120,12 +147,21 @@ def get_asset_event_by_asset_alias(
     before: Annotated[UtcDateTime | None, Query(description="The end of the 
time range")] = None,
     ascending: Annotated[bool, Query(description="Whether to sort results in 
ascending order")] = True,
     limit: Annotated[int | None, Query(description="The maximum number of 
results to return")] = None,
+    extra: Annotated[
+        list[str] | None,
+        Query(
+            description="Filter by extra JSON key-value pairs. Format: 
key=value. Repeat for AND logic.",
+        ),
+    ] = None,
 ) -> AssetEventsResponse:
     where_clause = AssetAliasModel.name == name
     if after:
         where_clause = and_(where_clause, AssetEvent.timestamp >= after)
     if before:
         where_clause = and_(where_clause, AssetEvent.timestamp <= before)
+    extra_dict = _parse_extra_params(extra)
+    if extra_dict:
+        where_clause = and_(where_clause, JsonContains(AssetEvent.extra, 
extra_dict))
 
     return _get_asset_events_through_sql_clauses(
         join_clause=AssetEvent.source_aliases,
diff --git a/airflow-core/src/airflow/migrations/env.py 
b/airflow-core/src/airflow/migrations/env.py
index dfb46ab6c37..f32a003a415 100644
--- a/airflow-core/src/airflow/migrations/env.py
+++ b/airflow-core/src/airflow/migrations/env.py
@@ -27,6 +27,12 @@ from alembic import context
 from airflow import models, settings
 from airflow.utils.db import compare_server_default, compare_type
 
+_POSTGRES_ONLY_INDEXES = frozenset(
+    {
+        "idx_asset_event_extra_gin",
+    }
+)
+
 
 def include_object(_, name, type_, *args):
     """Filter objects for autogenerating revisions."""
@@ -36,6 +42,10 @@ def include_object(_, name, type_, *args):
     # Only create migrations for objects that are in the target metadata
     if type_ == "table" and name not in target_metadata.tables:
         return False
+    # Indexes created by raw SQL in migrations (e.g. Postgres GIN) are not
+    # represented in the SQLAlchemy model; hide them from autogenerate.
+    if type_ == "index" and name in _POSTGRES_ONLY_INDEXES:
+        return False
     return True
 
 
diff --git 
a/airflow-core/src/airflow/migrations/versions/0124_3_4_0_add_gin_index_on_asset_event_extra.py
 
b/airflow-core/src/airflow/migrations/versions/0124_3_4_0_add_gin_index_on_asset_event_extra.py
new file mode 100644
index 00000000000..bffa7eb0f03
--- /dev/null
+++ 
b/airflow-core/src/airflow/migrations/versions/0124_3_4_0_add_gin_index_on_asset_event_extra.py
@@ -0,0 +1,57 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Add GIN index on asset_event.extra for PostgreSQL.
+
+Revision ID: 5a5d3253e946
+Revises: d2f4e1b3c5a7
+Create Date: 2026-04-01 23:00:00.000000
+
+"""
+
+from __future__ import annotations
+
+from alembic import op
+from sqlalchemy import text
+
+# revision identifiers, used by Alembic.
+revision = "5a5d3253e946"
+down_revision = "d2f4e1b3c5a7"
+branch_labels = None
+depends_on = None
+airflow_version = "3.4.0"
+
+
+def upgrade():
+    """Add GIN index on asset_event.extra for PostgreSQL only."""
+    conn = op.get_bind()
+    if conn.dialect.name == "postgresql":
+        op.execute(
+            text(
+                "CREATE INDEX IF NOT EXISTS idx_asset_event_extra_gin "
+                "ON asset_event USING GIN ((extra::jsonb) jsonb_ops)"
+            )
+        )
+
+
+def downgrade():
+    """Remove GIN index on asset_event.extra."""
+    conn = op.get_bind()
+    if conn.dialect.name == "postgresql":
+        op.execute(text("DROP INDEX IF EXISTS idx_asset_event_extra_gin"))
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
index 9b07c3c8649..769f3eb49fa 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -36,8 +36,9 @@ export const UseAssetServiceGetAssetAliasKeyFn = ({ 
assetAliasId }: {
 export type AssetServiceGetAssetEventsDefaultResponse = 
Awaited<ReturnType<typeof AssetService.getAssetEvents>>;
 export type AssetServiceGetAssetEventsQueryResult<TData = 
AssetServiceGetAssetEventsDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
 export const useAssetServiceGetAssetEventsKey = "AssetServiceGetAssetEvents";
-export const UseAssetServiceGetAssetEventsKeyFn = ({ assetId, limit, 
namePattern, namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, 
sourceRunId, sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte 
}: {
+export const UseAssetServiceGetAssetEventsKeyFn = ({ assetId, extra, limit, 
namePattern, namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, 
sourceRunId, sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte 
}: {
   assetId?: number;
+  extra?: string[];
   limit?: number;
   namePattern?: string;
   namePrefixPattern?: string;
@@ -51,7 +52,7 @@ export const UseAssetServiceGetAssetEventsKeyFn = ({ assetId, 
limit, namePattern
   timestampGte?: string;
   timestampLt?: string;
   timestampLte?: string;
-} = {}, queryKey?: Array<unknown>) => [useAssetServiceGetAssetEventsKey, 
...(queryKey ?? [{ assetId, limit, namePattern, namePrefixPattern, offset, 
orderBy, sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }])];
+} = {}, queryKey?: Array<unknown>) => [useAssetServiceGetAssetEventsKey, 
...(queryKey ?? [{ assetId, extra, limit, namePattern, namePrefixPattern, 
offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, 
timestampGt, timestampGte, timestampLt, timestampLte }])];
 export type AssetServiceGetAssetQueuedEventsDefaultResponse = 
Awaited<ReturnType<typeof AssetService.getAssetQueuedEvents>>;
 export type AssetServiceGetAssetQueuedEventsQueryResult<TData = 
AssetServiceGetAssetQueuedEventsDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
 export const useAssetServiceGetAssetQueuedEventsKey = 
"AssetServiceGetAssetQueuedEvents";
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index 501d6158503..712d2ee99dd 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -83,6 +83,7 @@ export const ensureUseAssetServiceGetAssetAliasData = 
(queryClient: QueryClient,
 *
 * **Performance note:** this full-match pattern is evaluated as ``ILIKE 
'%term%'`` and most of the time prevents the database from using B-tree 
indexes, which can be very slow on large tables. Prefer the equivalent 
``name_prefix_pattern`` parameter when possible.
 * @param data.namePrefixPattern Prefix match — returns items whose value 
starts with the given string (case-sensitive, index-friendly). Use the pipe `|` 
operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard 
characters (`%`, `_`) are treated as literal characters. Trailing 
non-alphanumeric characters in the prefix are stripped before matching so the 
range scan stays index-compatible under locale-aware collations — e.g. `test_` 
effectively matches items starting with `te [...]
+* @param data.extra Filter by JSON key-value pairs. Repeat for multiple 
conditions (AND logic). Format: key=value (e.g. extra=region=us&extra=env=prod).
 * @param data.timestampGte
 * @param data.timestampGt
 * @param data.timestampLte
@@ -90,8 +91,9 @@ export const ensureUseAssetServiceGetAssetAliasData = 
(queryClient: QueryClient,
 * @returns AssetEventCollectionResponse Successful Response
 * @throws ApiError
 */
-export const ensureUseAssetServiceGetAssetEventsData = (queryClient: 
QueryClient, { assetId, limit, namePattern, namePrefixPattern, offset, orderBy, 
sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }: {
+export const ensureUseAssetServiceGetAssetEventsData = (queryClient: 
QueryClient, { assetId, extra, limit, namePattern, namePrefixPattern, offset, 
orderBy, sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }: {
   assetId?: number;
+  extra?: string[];
   limit?: number;
   namePattern?: string;
   namePrefixPattern?: string;
@@ -105,7 +107,7 @@ export const ensureUseAssetServiceGetAssetEventsData = 
(queryClient: QueryClient
   timestampGte?: string;
   timestampLt?: string;
   timestampLte?: string;
-} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }), queryFn: 
() => AssetService.getAssetEvents({ assetId, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }) });
+} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }), queryFn: 
() => AssetService.getAssetEvents({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, [...]
 /**
 * Get Asset Queued Events
 * Get queued asset events for an asset.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index d4ade79763f..1614a33ac5d 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -83,6 +83,7 @@ export const prefetchUseAssetServiceGetAssetAlias = 
(queryClient: QueryClient, {
 *
 * **Performance note:** this full-match pattern is evaluated as ``ILIKE 
'%term%'`` and most of the time prevents the database from using B-tree 
indexes, which can be very slow on large tables. Prefer the equivalent 
``name_prefix_pattern`` parameter when possible.
 * @param data.namePrefixPattern Prefix match — returns items whose value 
starts with the given string (case-sensitive, index-friendly). Use the pipe `|` 
operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard 
characters (`%`, `_`) are treated as literal characters. Trailing 
non-alphanumeric characters in the prefix are stripped before matching so the 
range scan stays index-compatible under locale-aware collations — e.g. `test_` 
effectively matches items starting with `te [...]
+* @param data.extra Filter by JSON key-value pairs. Repeat for multiple 
conditions (AND logic). Format: key=value (e.g. extra=region=us&extra=env=prod).
 * @param data.timestampGte
 * @param data.timestampGt
 * @param data.timestampLte
@@ -90,8 +91,9 @@ export const prefetchUseAssetServiceGetAssetAlias = 
(queryClient: QueryClient, {
 * @returns AssetEventCollectionResponse Successful Response
 * @throws ApiError
 */
-export const prefetchUseAssetServiceGetAssetEvents = (queryClient: 
QueryClient, { assetId, limit, namePattern, namePrefixPattern, offset, orderBy, 
sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }: {
+export const prefetchUseAssetServiceGetAssetEvents = (queryClient: 
QueryClient, { assetId, extra, limit, namePattern, namePrefixPattern, offset, 
orderBy, sourceDagId, sourceMapIndex, sourceRunId, sourceTaskId, timestampGt, 
timestampGte, timestampLt, timestampLte }: {
   assetId?: number;
+  extra?: string[];
   limit?: number;
   namePattern?: string;
   namePrefixPattern?: string;
@@ -105,7 +107,7 @@ export const prefetchUseAssetServiceGetAssetEvents = 
(queryClient: QueryClient,
   timestampGte?: string;
   timestampLt?: string;
   timestampLte?: string;
-} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }), queryFn: 
() => AssetService.getAssetEvents({ assetId, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }) });
+} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }), queryFn: 
() => AssetService.getAssetEvents({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, t [...]
 /**
 * Get Asset Queued Events
 * Get queued asset events for an asset.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index a8cd6066357..9f326c03b1b 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -83,6 +83,7 @@ export const useAssetServiceGetAssetAlias = <TData = 
Common.AssetServiceGetAsset
 *
 * **Performance note:** this full-match pattern is evaluated as ``ILIKE 
'%term%'`` and most of the time prevents the database from using B-tree 
indexes, which can be very slow on large tables. Prefer the equivalent 
``name_prefix_pattern`` parameter when possible.
 * @param data.namePrefixPattern Prefix match — returns items whose value 
starts with the given string (case-sensitive, index-friendly). Use the pipe `|` 
operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard 
characters (`%`, `_`) are treated as literal characters. Trailing 
non-alphanumeric characters in the prefix are stripped before matching so the 
range scan stays index-compatible under locale-aware collations — e.g. `test_` 
effectively matches items starting with `te [...]
+* @param data.extra Filter by JSON key-value pairs. Repeat for multiple 
conditions (AND logic). Format: key=value (e.g. extra=region=us&extra=env=prod).
 * @param data.timestampGte
 * @param data.timestampGt
 * @param data.timestampLte
@@ -90,8 +91,9 @@ export const useAssetServiceGetAssetAlias = <TData = 
Common.AssetServiceGetAsset
 * @returns AssetEventCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useAssetServiceGetAssetEvents = <TData = 
Common.AssetServiceGetAssetEventsDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ assetId, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }: {
+export const useAssetServiceGetAssetEvents = <TData = 
Common.AssetServiceGetAssetEventsDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }: {
   assetId?: number;
+  extra?: string[];
   limit?: number;
   namePattern?: string;
   namePrefixPattern?: string;
@@ -105,7 +107,7 @@ export const useAssetServiceGetAssetEvents = <TData = 
Common.AssetServiceGetAsse
   timestampGte?: string;
   timestampLt?: string;
   timestampLte?: string;
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }, 
queryKey), queryFn: () => AssetService.getAssetEvents({ assetId, limit, 
namePattern, namePrefixPattern, offset, orderBy, sourceDag [...]
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }, 
queryKey), queryFn: () => AssetService.getAssetEvents({ assetId, extra, limit, 
namePattern, namePrefixPattern, offset, orde [...]
 /**
 * Get Asset Queued Events
 * Get queued asset events for an asset.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index 384cfb52b01..d2d51a73acd 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -83,6 +83,7 @@ export const useAssetServiceGetAssetAliasSuspense = <TData = 
Common.AssetService
 *
 * **Performance note:** this full-match pattern is evaluated as ``ILIKE 
'%term%'`` and most of the time prevents the database from using B-tree 
indexes, which can be very slow on large tables. Prefer the equivalent 
``name_prefix_pattern`` parameter when possible.
 * @param data.namePrefixPattern Prefix match — returns items whose value 
starts with the given string (case-sensitive, index-friendly). Use the pipe `|` 
operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard 
characters (`%`, `_`) are treated as literal characters. Trailing 
non-alphanumeric characters in the prefix are stripped before matching so the 
range scan stays index-compatible under locale-aware collations — e.g. `test_` 
effectively matches items starting with `te [...]
+* @param data.extra Filter by JSON key-value pairs. Repeat for multiple 
conditions (AND logic). Format: key=value (e.g. extra=region=us&extra=env=prod).
 * @param data.timestampGte
 * @param data.timestampGt
 * @param data.timestampLte
@@ -90,8 +91,9 @@ export const useAssetServiceGetAssetAliasSuspense = <TData = 
Common.AssetService
 * @returns AssetEventCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useAssetServiceGetAssetEventsSuspense = <TData = 
Common.AssetServiceGetAssetEventsDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ assetId, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }: {
+export const useAssetServiceGetAssetEventsSuspense = <TData = 
Common.AssetServiceGetAssetEventsDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }: {
   assetId?: number;
+  extra?: string[];
   limit?: number;
   namePattern?: string;
   namePrefixPattern?: string;
@@ -105,7 +107,7 @@ export const useAssetServiceGetAssetEventsSuspense = <TData 
= Common.AssetServic
   timestampGte?: string;
   timestampLt?: string;
   timestampLte?: string;
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }, 
queryKey), queryFn: () => AssetService.getAssetEvents({ assetId, limit, 
namePattern, namePrefixPattern, offset, orderBy, s [...]
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseAssetServiceGetAssetEventsKeyFn({ assetId, extra, limit, namePattern, 
namePrefixPattern, offset, orderBy, sourceDagId, sourceMapIndex, sourceRunId, 
sourceTaskId, timestampGt, timestampGte, timestampLt, timestampLte }, 
queryKey), queryFn: () => AssetService.getAssetEvents({ assetId, extra, limit, 
namePattern, namePrefixPattern, offs [...]
 /**
 * Get Asset Queued Events
 * Get queued asset events for an asset.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index 7f1229ddb9c..06842b72e7d 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -124,6 +124,7 @@ export class AssetService {
      *
      * **Performance note:** this full-match pattern is evaluated as ``ILIKE 
'%term%'`` and most of the time prevents the database from using B-tree 
indexes, which can be very slow on large tables. Prefer the equivalent 
``name_prefix_pattern`` parameter when possible.
      * @param data.namePrefixPattern Prefix match — returns items whose value 
starts with the given string (case-sensitive, index-friendly). Use the pipe `|` 
operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard 
characters (`%`, `_`) are treated as literal characters. Trailing 
non-alphanumeric characters in the prefix are stripped before matching so the 
range scan stays index-compatible under locale-aware collations — e.g. `test_` 
effectively matches items starting wit [...]
+     * @param data.extra Filter by JSON key-value pairs. Repeat for multiple 
conditions (AND logic). Format: key=value (e.g. extra=region=us&extra=env=prod).
      * @param data.timestampGte
      * @param data.timestampGt
      * @param data.timestampLte
@@ -146,6 +147,7 @@ export class AssetService {
                 source_map_index: data.sourceMapIndex,
                 name_pattern: data.namePattern,
                 name_prefix_pattern: data.namePrefixPattern,
+                extra: data.extra,
                 timestamp_gte: data.timestampGte,
                 timestamp_gt: data.timestampGt,
                 timestamp_lte: data.timestampLte,
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 2d8295b54b9..103c071ec87 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -2851,6 +2851,10 @@ export type GetAssetAliasResponse = unknown;
 
 export type GetAssetEventsData = {
     assetId?: number | null;
+    /**
+     * Filter by JSON key-value pairs. Repeat for multiple conditions (AND 
logic). Format: key=value (e.g. extra=region=us&extra=env=prod).
+     */
+    extra?: Array<(string)>;
     limit?: number;
     /**
      * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Use 
the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions 
are **not** supported.
diff --git a/airflow-core/src/airflow/utils/db.py 
b/airflow-core/src/airflow/utils/db.py
index f24f3636132..f02ef037174 100644
--- a/airflow-core/src/airflow/utils/db.py
+++ b/airflow-core/src/airflow/utils/db.py
@@ -117,6 +117,7 @@ _REVISION_HEADS_MAP: dict[str, str] = {
     "3.1.8": "509b94a1042d",
     "3.2.0": "1d6611b6ab7c",
     "3.3.0": "d2f4e1b3c5a7",
+    "3.4.0": "5a5d3253e946",
 }
 
 # Prefix used to identify tables holding data moved during migration.
diff --git a/airflow-core/src/airflow/utils/sqlalchemy.py 
b/airflow-core/src/airflow/utils/sqlalchemy.py
index c47d8fd4796..095d0de4741 100644
--- a/airflow-core/src/airflow/utils/sqlalchemy.py
+++ b/airflow-core/src/airflow/utils/sqlalchemy.py
@@ -20,6 +20,7 @@ from __future__ import annotations
 import contextlib
 import copy
 import datetime
+import json
 import logging
 from collections.abc import Generator
 from typing import TYPE_CHECKING, Any
@@ -28,8 +29,9 @@ from sqlalchemy import TIMESTAMP, PickleType, String, event, 
nullsfirst, text
 from sqlalchemy.dialects import mysql
 from sqlalchemy.dialects.postgresql import JSONB
 from sqlalchemy.ext.compiler import compiles
+from sqlalchemy.sql.expression import ColumnElement
 from sqlalchemy.sql.functions import FunctionElement
-from sqlalchemy.types import JSON, Text, TypeDecorator
+from sqlalchemy.types import JSON, NullType, Text, TypeDecorator
 
 from airflow._shared.timezones.timezone import make_naive, utc
 from airflow.configuration import conf
@@ -45,7 +47,6 @@ if TYPE_CHECKING:
     from sqlalchemy.exc import OperationalError
     from sqlalchemy.orm import Session
     from sqlalchemy.sql import Select
-    from sqlalchemy.sql.elements import ColumnElement
     from sqlalchemy.types import TypeEngine
 
     from airflow.typing_compat import Self
@@ -133,6 +134,56 @@ def _random_db_uuid_sqlite(element, compiler, **kw):
     return "uuid4()"
 
 
+class JsonContains(ColumnElement):
+    """
+    Dialect-aware JSON containment check.
+
+    Compiles to ``@>`` on PostgreSQL (GIN-indexable), ``JSON_CONTAINS`` on
+    MySQL, and per-key ``json_extract`` comparisons on SQLite.
+
+    All dialects use bound parameters to avoid SQL injection.
+    """
+
+    inherit_cache = False
+    type = NullType()
+
+    def __init__(self, column, kv_dict: dict[str, str]):
+        self.column = column
+        self.kv_dict = kv_dict
+
+
+@compiles(JsonContains, "postgresql")
+def _pg_json_contains(element, compiler, **kw):
+    from sqlalchemy import cast, literal
+
+    col = cast(element.column, JSONB)
+    param = literal(json.dumps(element.kv_dict)).cast(JSONB)
+    expr = col.contains(param)
+    return compiler.process(expr, **kw)
+
+
+@compiles(JsonContains, "mysql")
+def _mysql_json_contains(element, compiler, **kw):
+    from sqlalchemy import bindparam, func
+
+    param = bindparam(None, json.dumps(element.kv_dict), expanding=False)
+    expr = func.JSON_CONTAINS(element.column, param)
+    return compiler.process(expr == 1, **kw)
+
+
+@compiles(JsonContains)
+def _default_json_contains(element, compiler, **kw):
+    from sqlalchemy import and_, func, literal
+
+    clauses = []
+    for k, v in element.kv_dict.items():
+        path = f"$.{k}"
+        clauses.append(func.json_extract(element.column, literal(path)) == 
literal(v))
+    if len(clauses) == 1:
+        return compiler.process(clauses[0], **kw)
+    return compiler.process(and_(*clauses), **kw)
+
+
 class UtcDateTime(TypeDecorator):
     """
     Similar to :class:`~sqlalchemy.types.TIMESTAMP` with ``timezone=True`` 
option, with some differences.
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
index 3487fa28598..112138b46ab 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
@@ -1078,6 +1078,87 @@ class TestGetAssetEvents(TestAssets):
         }
 
 
+class TestGetAssetEventsExtraFilter(TestAssets):
+    @pytest.fixture
+    def _setup(self, session):
+        self.create_assets(num=2, session=session)
+        events = [
+            AssetEvent(
+                asset_id=1,
+                extra={"region": "us", "env": "prod"},
+                source_task_id="t1",
+                source_dag_id="d1",
+                source_run_id="r1",
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                asset_id=1,
+                extra={"region": "eu", "env": "prod"},
+                source_task_id="t1",
+                source_dag_id="d1",
+                source_run_id="r2",
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                asset_id=2,
+                extra={"region": "us", "env": "staging"},
+                source_task_id="t2",
+                source_dag_id="d2",
+                source_run_id="r3",
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                asset_id=1,
+                extra={},
+                source_task_id="t1",
+                source_dag_id="d1",
+                source_run_id="r4",
+                timestamp=DEFAULT_DATE,
+            ),
+        ]
+        session.add_all(events)
+        session.commit()
+
+    @pytest.mark.usefixtures("_setup")
+    @pytest.mark.parametrize(
+        ("params", "expected_count"),
+        [
+            ({"extra": "region=us"}, 2),
+            ({"extra": "region=eu"}, 1),
+            ({"extra": "env=prod"}, 2),
+            ({"extra": "env=staging"}, 1),
+            ({"extra": "region=ap"}, 0),
+            ({"extra": "nonexistent=us"}, 0),
+            ({}, 4),
+        ],
+    )
+    def test_extra_filter(self, test_client, params, expected_count):
+        response = test_client.get("/assets/events", params=params)
+        assert response.status_code == 200
+        assert response.json()["total_entries"] == expected_count
+
+    @pytest.mark.usefixtures("_setup")
+    def test_extra_filter_combined_with_asset_id(self, test_client):
+        response = test_client.get("/assets/events", params={"extra": 
"region=us", "asset_id": "1"})
+        assert response.status_code == 200
+        assert response.json()["total_entries"] == 1
+
+    @pytest.mark.usefixtures("_setup")
+    @pytest.mark.parametrize(
+        ("params", "expected_count"),
+        [
+            ([("extra", "region=us"), ("extra", "env=prod")], 1),
+            ([("extra", "region=eu"), ("extra", "env=prod")], 1),
+            ([("extra", "region=us"), ("extra", "env=staging")], 1),
+            ([("extra", "region=eu"), ("extra", "env=staging")], 0),
+        ],
+    )
+    def test_extra_filter_multiple_keys(self, test_client, params, 
expected_count):
+        response = test_client.get("/assets/events", params=params)
+        assert response.status_code == 200
+        assert response.json()["total_entries"] == expected_count
+
+
 class TestGetAssetEndpoint(TestAssets):
     @provide_session
     def test_should_respond_200(self, test_client, *, session):
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
index e3839f19eaf..6300265472a 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
@@ -521,3 +521,210 @@ class TestGetAssetEventByAssetAlias:
                 },
             ]
         }
+
+
+class TestGetAssetEventByAssetExtraFilter:
+    @pytest.fixture
+    def test_events_with_extra(self, session):
+        asset = AssetModel(
+            id=1,
+            name="test_asset",
+            uri="s3://bucket/key",
+            group="asset",
+            extra={},
+            created_at=DEFAULT_DATE,
+            updated_at=DEFAULT_DATE,
+        )
+        asset_active = AssetActive.for_asset(asset)
+        session.add_all([asset, asset_active])
+        session.flush()
+
+        events = [
+            AssetEvent(
+                id=1,
+                asset_id=1,
+                extra={"region": "us", "env": "prod"},
+                source_dag_id="d1",
+                source_task_id="t1",
+                source_run_id="r1",
+                source_map_index=-1,
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                id=2,
+                asset_id=1,
+                extra={"region": "eu", "env": "prod"},
+                source_dag_id="d1",
+                source_task_id="t1",
+                source_run_id="r2",
+                source_map_index=-1,
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                id=3,
+                asset_id=1,
+                extra={"region": "us", "env": "staging"},
+                source_dag_id="d1",
+                source_task_id="t1",
+                source_run_id="r3",
+                source_map_index=-1,
+                timestamp=DEFAULT_DATE,
+            ),
+        ]
+        session.add_all(events)
+        session.commit()
+        yield events
+        for e in events:
+            session.delete(e)
+        session.delete(asset_active)
+        session.delete(asset)
+        session.commit()
+
+    @pytest.mark.usefixtures("test_events_with_extra")
+    def test_filter_by_extra_key_value(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params=[("name", "test_asset"), ("uri", "s3://bucket/key"), 
("extra", "region=us")],
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 2
+        assert all(e["extra"]["region"] == "us" for e in data["asset_events"])
+
+    @pytest.mark.usefixtures("test_events_with_extra")
+    def test_filter_by_extra_env(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params=[("name", "test_asset"), ("uri", "s3://bucket/key"), 
("extra", "env=prod")],
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 2
+
+    @pytest.mark.usefixtures("test_events_with_extra")
+    def test_no_extra_filter_returns_all(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={"name": "test_asset", "uri": "s3://bucket/key"},
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == 3
+
+    @pytest.mark.usefixtures("test_events_with_extra")
+    def test_filter_nonexistent_key(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params=[("name", "test_asset"), ("uri", "s3://bucket/key"), 
("extra", "missing=val")],
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == 0
+
+    @pytest.mark.usefixtures("test_events_with_extra")
+    @pytest.mark.parametrize(
+        ("extra_params", "expected_count"),
+        [
+            ([("extra", "region=us"), ("extra", "env=prod")], 1),
+            ([("extra", "region=eu"), ("extra", "env=prod")], 1),
+            ([("extra", "region=us"), ("extra", "env=staging")], 1),
+            ([("extra", "region=eu"), ("extra", "env=staging")], 0),
+        ],
+    )
+    def test_filter_multiple_extra_keys(self, client, extra_params, 
expected_count):
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params=[("name", "test_asset"), ("uri", "s3://bucket/key"), 
*extra_params],
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == expected_count
+
+
+class TestGetAssetEventByAssetAliasExtraFilter:
+    @pytest.fixture
+    def test_alias_events_with_extra(self, session):
+        asset = AssetModel(
+            id=1,
+            name="test_asset",
+            uri="s3://bucket/key",
+            group="asset",
+            extra={},
+            created_at=DEFAULT_DATE,
+            updated_at=DEFAULT_DATE,
+        )
+        asset_active = AssetActive.for_asset(asset)
+        session.add_all([asset, asset_active])
+        session.flush()
+
+        events = [
+            AssetEvent(
+                id=1,
+                asset_id=1,
+                extra={"tier": "gold", "region": "us"},
+                source_dag_id="d1",
+                source_task_id="t1",
+                source_run_id="r1",
+                source_map_index=-1,
+                timestamp=DEFAULT_DATE,
+            ),
+            AssetEvent(
+                id=2,
+                asset_id=1,
+                extra={"tier": "silver", "region": "eu"},
+                source_dag_id="d1",
+                source_task_id="t1",
+                source_run_id="r2",
+                source_map_index=-1,
+                timestamp=DEFAULT_DATE,
+            ),
+        ]
+        session.add_all(events)
+        session.flush()
+
+        alias = AssetAliasModel(id=1, name="test_alias")
+        alias.asset_events = events
+        alias.assets.append(asset)
+        session.add(alias)
+        session.commit()
+        yield events
+        session.delete(alias)
+        for e in events:
+            session.delete(e)
+        session.delete(asset_active)
+        session.delete(asset)
+        session.commit()
+
+    @pytest.mark.usefixtures("test_alias_events_with_extra")
+    def test_filter_by_extra_key_value(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params=[("name", "test_alias"), ("extra", "tier=gold")],
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data["asset_events"]) == 1
+        assert data["asset_events"][0]["extra"]["tier"] == "gold"
+
+    @pytest.mark.usefixtures("test_alias_events_with_extra")
+    def test_no_extra_filter_returns_all(self, client):
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params={"name": "test_alias"},
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == 2
+
+    @pytest.mark.usefixtures("test_alias_events_with_extra")
+    @pytest.mark.parametrize(
+        ("extra_params", "expected_count"),
+        [
+            ([("extra", "tier=gold"), ("extra", "region=us")], 1),
+            ([("extra", "tier=gold"), ("extra", "region=eu")], 0),
+            ([("extra", "tier=silver"), ("extra", "region=eu")], 1),
+        ],
+    )
+    def test_filter_multiple_extra_keys(self, client, extra_params, 
expected_count):
+        response = client.get(
+            "/execution/asset-events/by-asset-alias",
+            params=[("name", "test_alias"), *extra_params],
+        )
+        assert response.status_code == 200
+        assert len(response.json()["asset_events"]) == expected_count
diff --git a/airflow-core/tests/unit/utils/test_db.py 
b/airflow-core/tests/unit/utils/test_db.py
index e54cc6bf4bb..c81859edbdc 100644
--- a/airflow-core/tests/unit/utils/test_db.py
+++ b/airflow-core/tests/unit/utils/test_db.py
@@ -180,6 +180,8 @@ class TestDb:
             lambda t: t[0] == "add_index" and t[1].name == 
"idx_ab_user_username",
             lambda t: t[0] == "remove_index" and t[1].name == 
"idx_ab_register_user_username",
             lambda t: t[0] == "remove_index" and t[1].name == 
"idx_ab_user_username",
+            # Postgres-only GIN index created by raw SQL in migration, not in 
SQLAlchemy model
+            lambda t: t[0] == "remove_index" and t[1].name == 
"idx_asset_event_extra_gin",
         ]
 
         for ignore in ignores:
diff --git a/task-sdk/src/airflow/sdk/api/client.py 
b/task-sdk/src/airflow/sdk/api/client.py
index 55668c9f3bd..74a68005371 100644
--- a/task-sdk/src/airflow/sdk/api/client.py
+++ b/task-sdk/src/airflow/sdk/api/client.py
@@ -858,6 +858,7 @@ class AssetEventOperations:
         before: datetime | None = None,
         ascending: bool = True,
         limit: int | None = None,
+        extra: dict[str, str] | None = None,
     ) -> AssetEventsResponse:
         """Get Asset event from the API server."""
         common_params: dict[str, Any] = {}
@@ -866,15 +867,20 @@ class AssetEventOperations:
         if before:
             common_params["before"] = before.isoformat()
         common_params["ascending"] = ascending
-        if limit:
+        if limit is not None:
             common_params["limit"] = limit
+        extra_params: list[tuple[str, str]] = []
+        if extra:
+            extra_params = [("extra", f"{k}={v}") for k, v in extra.items()]
         if name or uri:
             resp = self.client.get(
-                "asset-events/by-asset", params={"name": name, "uri": uri, 
**common_params}
+                "asset-events/by-asset",
+                params=[*{"name": name, "uri": uri, **common_params}.items(), 
*extra_params],
             )
         elif alias_name:
             resp = self.client.get(
-                "asset-events/by-asset-alias", params={"name": alias_name, 
**common_params}
+                "asset-events/by-asset-alias",
+                params=[*{"name": alias_name, **common_params}.items(), 
*extra_params],
             )
         else:
             raise ValueError("Either `name`, `uri` or `alias_name` must be 
provided")
diff --git a/task-sdk/src/airflow/sdk/execution_time/comms.py 
b/task-sdk/src/airflow/sdk/execution_time/comms.py
index a97df1df8b0..6e9e49b2676 100644
--- a/task-sdk/src/airflow/sdk/execution_time/comms.py
+++ b/task-sdk/src/airflow/sdk/execution_time/comms.py
@@ -1142,6 +1142,7 @@ class GetAssetEventByAsset(BaseModel):
     before: AwareDatetime | None = None
     limit: int | None = None
     ascending: bool = True
+    extra: dict[str, str] | None = None
     type: Literal["GetAssetEventByAsset"] = "GetAssetEventByAsset"
 
 
@@ -1151,6 +1152,7 @@ class GetAssetEventByAssetAlias(BaseModel):
     before: AwareDatetime | None = None
     limit: int | None = None
     ascending: bool = True
+    extra: dict[str, str] | None = None
     type: Literal["GetAssetEventByAssetAlias"] = "GetAssetEventByAssetAlias"
 
 
diff --git a/task-sdk/src/airflow/sdk/execution_time/context.py 
b/task-sdk/src/airflow/sdk/execution_time/context.py
index cd8941944b6..7b89164f895 100644
--- a/task-sdk/src/airflow/sdk/execution_time/context.py
+++ b/task-sdk/src/airflow/sdk/execution_time/context.py
@@ -1088,6 +1088,7 @@ class InletEventsAccessor(Sequence["AssetEventResult"]):
     _before: str | datetime | None
     _ascending: bool
     _limit: int | None
+    _extra: dict[str, str]
     _asset_name: str | None
     _asset_uri: str | None
     _alias_name: str | None
@@ -1102,6 +1103,7 @@ class InletEventsAccessor(Sequence["AssetEventResult"]):
         self._before = None
         self._ascending = True
         self._limit = None
+        self._extra: dict[str, str] = {}
 
     def after(self, after: str) -> Self:
         self._after = after
@@ -1123,6 +1125,11 @@ class InletEventsAccessor(Sequence["AssetEventResult"]):
         self._reset_cache()
         return self
 
+    def extra(self, key: str, value: str) -> Self:
+        self._extra[key] = value
+        self._reset_cache()
+        return self
+
     @functools.cached_property
     def _asset_events(self) -> list[AssetEventResult]:
         from airflow.sdk.execution_time.comms import (
@@ -1138,6 +1145,7 @@ class InletEventsAccessor(Sequence["AssetEventResult"]):
             "before": self._before,
             "ascending": self._ascending,
             "limit": self._limit,
+            "extra": self._extra or None,
         }
 
         msg: ToSupervisor
diff --git a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json 
b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
index e6ce8aa3d06..643bda0a5c6 100644
--- a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
+++ b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
@@ -1846,6 +1846,21 @@
           "title": "Ascending",
           "type": "boolean"
         },
+        "extra": {
+          "anyOf": [
+            {
+              "additionalProperties": {
+                "type": "string"
+              },
+              "type": "object"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "title": "Extra"
+        },
         "type": {
           "const": "GetAssetEventByAsset",
           "default": "GetAssetEventByAsset",
@@ -1909,6 +1924,21 @@
           "title": "Ascending",
           "type": "boolean"
         },
+        "extra": {
+          "anyOf": [
+            {
+              "additionalProperties": {
+                "type": "string"
+              },
+              "type": "object"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "title": "Extra"
+        },
         "type": {
           "const": "GetAssetEventByAssetAlias",
           "default": "GetAssetEventByAssetAlias",
diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py 
b/task-sdk/src/airflow/sdk/execution_time/supervisor.py
index a96523d5a6a..447f7c7594d 100644
--- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py
+++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py
@@ -1774,6 +1774,7 @@ class ActivitySubprocess(WatchedSubprocess):
                 before=msg.before,
                 ascending=msg.ascending,
                 limit=msg.limit,
+                extra=msg.extra,
             )
             asset_event_result = 
AssetEventsResult.from_asset_events_response(asset_event_resp)
             resp = asset_event_result
@@ -1785,6 +1786,7 @@ class ActivitySubprocess(WatchedSubprocess):
                 before=msg.before,
                 ascending=msg.ascending,
                 limit=msg.limit,
+                extra=msg.extra,
             )
             asset_event_result = 
AssetEventsResult.from_asset_events_response(asset_event_resp)
             resp = asset_event_result
diff --git a/task-sdk/tests/task_sdk/api/test_client.py 
b/task-sdk/tests/task_sdk/api/test_client.py
index 92b970f7ee5..205b12bd4f2 100644
--- a/task-sdk/tests/task_sdk/api/test_client.py
+++ b/task-sdk/tests/task_sdk/api/test_client.py
@@ -1216,6 +1216,68 @@ class TestAssetEventOperations:
         assert result.asset_events[0].asset.name == "this_asset"
         assert result.asset_events[0].asset.uri == "s3://bucket/key"
 
+    def test_extra_dict_param_passed(self):
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            params = request.url.params
+            extra_pairs = [v for k, v in params.multi_items() if k == "extra"]
+            assert sorted(extra_pairs) == sorted(["region=us", "env=prod"])
+            return httpx.Response(
+                status_code=200,
+                json={
+                    "asset_events": [
+                        {
+                            "id": 1,
+                            "asset": {"name": "a", "uri": "s3://b", "group": 
"asset"},
+                            "created_dagruns": [],
+                            "timestamp": "2023-01-01T00:00:00Z",
+                        }
+                    ]
+                },
+            )
+
+        client = make_client(transport=httpx.MockTransport(handle_request))
+        result = client.asset_events.get(name="a", uri="s3://b", 
extra={"region": "us", "env": "prod"})
+        assert isinstance(result, AssetEventsResponse)
+        assert len(result.asset_events) == 1
+
+    def test_extra_dict_with_alias(self):
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            params = request.url.params
+            assert request.url.path == "/asset-events/by-asset-alias"
+            extra_pairs = [v for k, v in params.multi_items() if k == "extra"]
+            assert extra_pairs == ["env=prod"]
+            return httpx.Response(
+                status_code=200,
+                json={
+                    "asset_events": [
+                        {
+                            "id": 1,
+                            "asset": {"name": "a", "uri": "s3://b", "group": 
"asset"},
+                            "created_dagruns": [],
+                            "timestamp": "2023-01-01T00:00:00Z",
+                        }
+                    ]
+                },
+            )
+
+        client = make_client(transport=httpx.MockTransport(handle_request))
+        result = client.asset_events.get(alias_name="my_alias", extra={"env": 
"prod"})
+        assert isinstance(result, AssetEventsResponse)
+        assert len(result.asset_events) == 1
+
+    def test_extra_none_not_sent(self):
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            params = request.url.params
+            assert "extra" not in params
+            return httpx.Response(
+                status_code=200,
+                json={"asset_events": []},
+            )
+
+        client = make_client(transport=httpx.MockTransport(handle_request))
+        result = client.asset_events.get(name="a", uri="s3://b")
+        assert isinstance(result, AssetEventsResponse)
+
 
 class TestAssetOperations:
     @pytest.mark.parametrize(
diff --git a/task-sdk/tests/task_sdk/execution_time/test_context.py 
b/task-sdk/tests/task_sdk/execution_time/test_context.py
index fd40feb571f..4bb88b713eb 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_context.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_context.py
@@ -912,6 +912,51 @@ class TestInletEventAccessor:
             name="test_uri", uri="test://test/", after=None, before=None, 
limit=10, ascending=False
         )
 
+    def test__get_item__with_extra_filters(self, sample_inlet_evnets_accessor, 
mock_supervisor_comms):
+        asset_event_resp = AssetEventResult(
+            id=1,
+            created_dagruns=[],
+            timestamp=timezone.utcnow(),
+            asset=AssetResponse(name="test_uri", uri="test_uri", 
group="asset"),
+        )
+        events_result = AssetEventsResult(asset_events=[asset_event_resp])
+        mock_supervisor_comms.send.side_effect = [events_result] * 3
+
+        list(sample_inlet_evnets_accessor[TEST_ASSET].extra("region", "us"))
+        list(sample_inlet_evnets_accessor[TEST_ASSET].extra("region", 
"us").extra("env", "prod"))
+        list(sample_inlet_evnets_accessor[TEST_ASSET].extra("region", 
"us").extra("env", "prod").limit(5))
+
+        assert mock_supervisor_comms.send.call_count == 3
+
+        calls = mock_supervisor_comms.send.call_args_list
+        assert calls[0][0][0] == GetAssetEventByAsset(
+            name="test_uri",
+            uri="test://test/",
+            after=None,
+            before=None,
+            limit=None,
+            ascending=True,
+            extra={"region": "us"},
+        )
+        assert calls[1][0][0] == GetAssetEventByAsset(
+            name="test_uri",
+            uri="test://test/",
+            after=None,
+            before=None,
+            limit=None,
+            ascending=True,
+            extra={"region": "us", "env": "prod"},
+        )
+        assert calls[2][0][0] == GetAssetEventByAsset(
+            name="test_uri",
+            uri="test://test/",
+            after=None,
+            before=None,
+            limit=5,
+            ascending=True,
+            extra={"region": "us", "env": "prod"},
+        )
+
     @pytest.mark.parametrize(
         ("name", "uri", "expected_key"),
         (
diff --git a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py 
b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py
index 9bf0ef9d13b..870b2e6deed 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py
@@ -1940,6 +1940,7 @@ REQUEST_TEST_CASES = [
                 "before": None,
                 "limit": None,
                 "ascending": True,
+                "extra": None,
             },
             response=AssetEventsResult(
                 asset_events=[
@@ -1983,6 +1984,7 @@ REQUEST_TEST_CASES = [
                 "before": timezone.parse("2024-10-15T12:00:00Z"),
                 "limit": 5,
                 "ascending": False,
+                "extra": None,
             },
             response=AssetEventsResult(
                 asset_events=[
@@ -2019,6 +2021,7 @@ REQUEST_TEST_CASES = [
                 "before": None,
                 "limit": None,
                 "ascending": True,
+                "extra": None,
             },
             response=AssetEventsResult(
                 asset_events=[
@@ -2062,6 +2065,7 @@ REQUEST_TEST_CASES = [
                 "before": timezone.parse("2024-10-15T12:00:00Z"),
                 "limit": 5,
                 "ascending": False,
+                "extra": None,
             },
             response=AssetEventsResult(
                 asset_events=[
@@ -2098,6 +2102,7 @@ REQUEST_TEST_CASES = [
                 "before": None,
                 "limit": None,
                 "ascending": True,
+                "extra": None,
             },
             response=AssetEventsResult(
                 asset_events=[
@@ -2141,6 +2146,7 @@ REQUEST_TEST_CASES = [
                 "before": timezone.parse("2024-10-15T12:00:00Z"),
                 "limit": 5,
                 "ascending": False,
+                "extra": None,
             },
             response=AssetEventsResult(
                 asset_events=[
@@ -2176,6 +2182,7 @@ REQUEST_TEST_CASES = [
                 "before": None,
                 "limit": None,
                 "ascending": True,
+                "extra": None,
             },
             response=AssetEventsResult(
                 asset_events=[
@@ -2217,6 +2224,7 @@ REQUEST_TEST_CASES = [
                 "before": timezone.parse("2024-10-15T12:00:00Z"),
                 "limit": 5,
                 "ascending": False,
+                "extra": None,
             },
             response=AssetEventsResult(
                 asset_events=[
@@ -2231,6 +2239,86 @@ REQUEST_TEST_CASES = [
         ),
         test_id="get_asset_events_by_asset_alias_with_filters",
     ),
+    RequestTestCase(
+        message=GetAssetEventByAsset(
+            uri="s3://bucket/obj",
+            name="test",
+            extra={"region": "us"},
+        ),
+        expected_body={
+            "asset_events": [
+                {
+                    "id": 1,
+                    "timestamp": timezone.parse("2024-10-31T12:00:00Z"),
+                    "asset": {"name": "asset", "uri": "s3://bucket/obj", 
"group": "asset"},
+                    "created_dagruns": [],
+                }
+            ],
+            "type": "AssetEventsResult",
+        },
+        client_mock=ClientMock(
+            method_path="asset_events.get",
+            kwargs={
+                "uri": "s3://bucket/obj",
+                "name": "test",
+                "after": None,
+                "before": None,
+                "limit": None,
+                "ascending": True,
+                "extra": {"region": "us"},
+            },
+            response=AssetEventsResult(
+                asset_events=[
+                    AssetEventResponse(
+                        id=1,
+                        asset=AssetResponse(name="asset", 
uri="s3://bucket/obj", group="asset"),
+                        created_dagruns=[],
+                        timestamp=timezone.parse("2024-10-31T12:00:00Z"),
+                    ),
+                ],
+            ),
+        ),
+        test_id="get_asset_events_with_extra_filter",
+    ),
+    RequestTestCase(
+        message=GetAssetEventByAssetAlias(
+            alias_name="test_alias",
+            extra={"env": "prod"},
+        ),
+        expected_body={
+            "asset_events": [
+                {
+                    "id": 1,
+                    "timestamp": timezone.parse("2024-10-31T12:00:00Z"),
+                    "asset": {"name": "asset", "uri": "s3://bucket/obj", 
"group": "asset"},
+                    "created_dagruns": [],
+                }
+            ],
+            "type": "AssetEventsResult",
+        },
+        client_mock=ClientMock(
+            method_path="asset_events.get",
+            kwargs={
+                "alias_name": "test_alias",
+                "after": None,
+                "before": None,
+                "limit": None,
+                "ascending": True,
+                "extra": {"env": "prod"},
+            },
+            response=AssetEventsResult(
+                asset_events=[
+                    AssetEventResponse(
+                        id=1,
+                        asset=AssetResponse(name="asset", 
uri="s3://bucket/obj", group="asset"),
+                        created_dagruns=[],
+                        timestamp=timezone.parse("2024-10-31T12:00:00Z"),
+                    )
+                ]
+            ),
+        ),
+        test_id="get_asset_events_by_alias_with_extra_filter",
+    ),
     RequestTestCase(
         message=ValidateInletsAndOutlets(ti_id=TI_ID),
         expected_body={

Reply via email to