Skip to content

Commit eb6c749

Browse files
committed
feat: track interception-hook dispatches in telemetry
`HookDispatchedEvent` was already emitted from the dispatcher but never landed in Feature Usage. Wire it through `hook_dispatched_span` so hook adoption and abort outcomes (e.g. policy checks) show up in the same ClickHouse aggregation as other features.
1 parent c5b9d9a commit eb6c749

3 files changed

Lines changed: 95 additions & 1 deletion

File tree

lib/crewai/src/crewai/events/event_listener.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
MethodExecutionPausedEvent,
5555
MethodExecutionStartedEvent,
5656
)
57+
from crewai.events.types.hook_events import HookDispatchedEvent
5758
from crewai.events.types.knowledge_events import (
5859
KnowledgeQueryCompletedEvent,
5960
KnowledgeQueryFailedEvent,
@@ -875,5 +876,12 @@ def on_crew_kickoff_hooks(_: Any, event: CrewKickoffStartedEvent) -> None:
875876
if has_hooks:
876877
self._telemetry.feature_usage_span("hooks:registered")
877878

879+
@crewai_event_bus.on(HookDispatchedEvent)
880+
def on_hook_dispatched(_: Any, event: HookDispatchedEvent) -> None:
881+
self._telemetry.hook_dispatched_span(
882+
interception_point=event.interception_point,
883+
outcome=event.outcome,
884+
)
885+
878886

879887
event_listener = EventListener()

lib/crewai/src/crewai/telemetry/telemetry.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1148,7 +1148,8 @@ def feature_usage_span(self, feature: str) -> None:
11481148
11491149
Args:
11501150
feature: Feature identifier, e.g. "planning:creation",
1151-
"mcp:connection", "a2a:delegation".
1151+
"mcp:connection", "a2a:delegation",
1152+
"hooks:pre_tool_call", "hooks:aborted".
11521153
"""
11531154

11541155
def _operation() -> None:
@@ -1160,6 +1161,21 @@ def _operation() -> None:
11601161

11611162
self._safe_telemetry_operation(_operation)
11621163

1164+
def hook_dispatched_span(
1165+
self,
1166+
interception_point: str,
1167+
outcome: str,
1168+
) -> None:
1169+
"""Records an interception-hook dispatch via Feature Usage.
1170+
1171+
Emits ``hooks:<point>`` on every dispatch, plus ``hooks:aborted`` when
1172+
a hook aborted the operation (e.g. a policy check). No reasons,
1173+
payloads, or other user content are recorded.
1174+
"""
1175+
self.feature_usage_span(f"hooks:{interception_point}")
1176+
if outcome == "aborted":
1177+
self.feature_usage_span("hooks:aborted")
1178+
11631179
def coding_agent_span(self) -> None:
11641180
"""Records which AI coding assistant (if any) is running this process.
11651181

lib/crewai/tests/telemetry/test_telemetry.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,73 @@ def init_in_thread():
230230
mock_holder["logger"].debug.assert_any_call(
231231
"Skipping signal handler registration: not running in main thread"
232232
)
233+
234+
235+
def test_hook_dispatched_span_counts_point_usage():
236+
with (
237+
patch.dict(
238+
os.environ,
239+
{
240+
"CREWAI_DISABLE_TELEMETRY": "false",
241+
"CREWAI_DISABLE_TRACKING": "false",
242+
"OTEL_SDK_DISABLED": "false",
243+
},
244+
),
245+
patch("crewai.telemetry.telemetry.TracerProvider"),
246+
):
247+
telemetry = Telemetry()
248+
with patch.object(telemetry, "feature_usage_span") as feature_usage_span:
249+
telemetry.hook_dispatched_span("pre_tool_call", "proceeded")
250+
251+
feature_usage_span.assert_called_once_with("hooks:pre_tool_call")
252+
253+
254+
def test_hook_dispatched_span_counts_aborts():
255+
with (
256+
patch.dict(
257+
os.environ,
258+
{
259+
"CREWAI_DISABLE_TELEMETRY": "false",
260+
"CREWAI_DISABLE_TRACKING": "false",
261+
"OTEL_SDK_DISABLED": "false",
262+
},
263+
),
264+
patch("crewai.telemetry.telemetry.TracerProvider"),
265+
):
266+
telemetry = Telemetry()
267+
with patch.object(telemetry, "feature_usage_span") as feature_usage_span:
268+
telemetry.hook_dispatched_span("pre_tool_call", "aborted")
269+
270+
feature_usage_span.assert_any_call("hooks:pre_tool_call")
271+
feature_usage_span.assert_any_call("hooks:aborted")
272+
assert feature_usage_span.call_count == 2
273+
274+
275+
def test_event_listener_tracks_hook_dispatched_events():
276+
from crewai.events.event_bus import crewai_event_bus
277+
from crewai.events.event_listener import event_listener
278+
from crewai.events.types.hook_events import HookDispatchedEvent
279+
280+
with (
281+
crewai_event_bus.scoped_handlers(),
282+
patch.object(
283+
event_listener._telemetry,
284+
"hook_dispatched_span",
285+
) as hook_dispatched_span,
286+
):
287+
event_listener.setup_listeners(crewai_event_bus)
288+
crewai_event_bus.emit(
289+
"test",
290+
HookDispatchedEvent(
291+
interception_point="pre_tool_call",
292+
outcome="aborted",
293+
hook_count=1,
294+
duration_ms=1.5,
295+
),
296+
)
297+
crewai_event_bus.flush()
298+
299+
hook_dispatched_span.assert_called_once_with(
300+
interception_point="pre_tool_call",
301+
outcome="aborted",
302+
)

0 commit comments

Comments
 (0)