Skip to content

Commit f11f660

Browse files
xumapleclaude
andcommitted
Skip LangSmith tracing for built-in Temporal queries
Built-in queries like __temporal_workflow_metadata, __stack_trace, and __enhanced_stack_trace are fired automatically by infrastructure (e.g. the Temporal Web UI) and are not user-facing. Filter them out of LangSmith traces when add_temporal_runs=True to reduce noise. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0b86253 commit f11f660

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

temporalio/contrib/langsmith/_interceptor.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@
3636

3737
HEADER_KEY = "_temporal-langsmith-context"
3838

39+
_BUILTIN_QUERIES: frozenset[str] = frozenset(
40+
{
41+
"__stack_trace",
42+
"__enhanced_stack_trace",
43+
}
44+
)
45+
3946
# ---------------------------------------------------------------------------
4047
# Context helpers
4148
# ---------------------------------------------------------------------------
@@ -824,6 +831,8 @@ async def handle_signal(self, input: temporalio.worker.HandleSignalInput) -> Non
824831
return await super().handle_signal(input)
825832

826833
async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any:
834+
if input.query.startswith("__temporal") or input.query in _BUILTIN_QUERIES:
835+
return await super().handle_query(input)
827836
with self._workflow_maybe_run(f"HandleQuery:{input.query}", input.headers):
828837
return await super().handle_query(input)
829838

tests/contrib/langsmith/test_integration.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,3 +1190,93 @@ async def test_nexus_direct_traceable_without_temporal_runs(
11901190
assert (
11911191
hierarchy == expected
11921192
), f"Hierarchy mismatch.\nExpected:\n{expected}\nActual:\n{hierarchy}"
1193+
1194+
1195+
# ---------------------------------------------------------------------------
1196+
# TestBuiltinQueryFiltering
1197+
# ---------------------------------------------------------------------------
1198+
1199+
1200+
@workflow.defn
1201+
class QueryFilteringWorkflow:
1202+
"""Workflow with a user query and a signal to complete."""
1203+
1204+
def __init__(self) -> None:
1205+
self._complete = False
1206+
1207+
@workflow.run
1208+
async def run(self) -> str:
1209+
await workflow.wait_condition(lambda: self._complete)
1210+
return "done"
1211+
1212+
@workflow.signal
1213+
def complete(self) -> None:
1214+
self._complete = True
1215+
1216+
@workflow.query
1217+
def my_query(self) -> str:
1218+
return "query-result"
1219+
1220+
1221+
class TestBuiltinQueryFiltering:
1222+
"""Verifies __temporal_ prefixed queries are not traced."""
1223+
1224+
async def test_temporal_prefixed_query_not_traced(
1225+
self,
1226+
client: Client,
1227+
) -> None:
1228+
"""__temporal_workflow_metadata query should not produce a trace,
1229+
but user-defined queries should still be traced.
1230+
1231+
Uses add_temporal_runs=False on the query client to suppress
1232+
client-side QueryWorkflow traces, isolating the test to
1233+
worker-side HandleQuery traces only.
1234+
"""
1235+
1236+
task_queue = f"query-filter-{uuid.uuid4()}"
1237+
collector = InMemoryRunCollector()
1238+
mock_ls = make_mock_ls_client(collector)
1239+
1240+
# Worker client: add_temporal_runs=True so HandleQuery traces are created
1241+
worker_client = _make_temporal_client(client, mock_ls, add_temporal_runs=True)
1242+
# Query client: add_temporal_runs=False to suppress client-side traces
1243+
query_client = _make_temporal_client(client, mock_ls, add_temporal_runs=False)
1244+
1245+
async with new_worker(
1246+
worker_client,
1247+
QueryFilteringWorkflow,
1248+
task_queue=task_queue,
1249+
max_cached_workflows=0,
1250+
) as worker:
1251+
handle = await query_client.start_workflow(
1252+
QueryFilteringWorkflow.run,
1253+
id=f"query-filter-{uuid.uuid4()}",
1254+
task_queue=worker.task_queue,
1255+
)
1256+
1257+
# Wait for workflow to start by polling the user query
1258+
assert await _poll_query(
1259+
handle,
1260+
QueryFilteringWorkflow.my_query,
1261+
expected="query-result",
1262+
), "Workflow never started"
1263+
1264+
collector.clear()
1265+
1266+
# Built-in queries — should NOT be traced
1267+
await handle.query("__temporal_workflow_metadata")
1268+
await handle.query("__stack_trace")
1269+
await handle.query("__enhanced_stack_trace")
1270+
1271+
# User query — should be traced
1272+
await handle.query(QueryFilteringWorkflow.my_query)
1273+
1274+
await handle.signal(QueryFilteringWorkflow.complete)
1275+
assert await handle.result() == "done"
1276+
1277+
# Built-in queries should be absent; only user query and signal remain.
1278+
traces = dump_traces(collector)
1279+
assert traces == [
1280+
["HandleQuery:my_query"],
1281+
["HandleSignal:complete"],
1282+
], f"Unexpected traces: {traces}"

0 commit comments

Comments
 (0)