Summary
AutoTracingPlugin._wrap_module discovers class members with inspect.getmembers, which returns the underlying plain function for a @staticmethod. _rebind then setattrs a plain wrapper function back onto the class without re-wrapping it in staticmethod(...).
After that, accessing the attribute through an instance re-engages the descriptor protocol and binds the instance as the first positional argument. Any static method whose first parameter is a real parameter now receives self in that slot.
plugins/auto_tracing_plugin.py (2.7.0), _wrap_module around L145 and _rebind around L164:
for member_name, member in inspect.getmembers(attr):
...
if not inspect.isfunction(member):
continue
...
self._rebind(attr, member_name, member)
# _rebind
setattr(owner, name, auto_tracing_helpers.build_tracing_wrapper(fn, self._tracer, self._caps))
Reproduction
import inspect
class C:
@staticmethod
def static(a, b=None):
return ("static", a, b)
# what _wrap_module sees:
member = dict(inspect.getmembers(C))["static"]
assert inspect.isfunction(member) # True -- passes the filter
# what _rebind does:
def make_wrapper(fn):
def wrapper(*a, **kw):
return fn(*a, **kw)
return wrapper
setattr(C, "static", make_wrapper(member))
C().static(a=1)
# TypeError: C.static() got multiple values for argument 'a'
Observed in the wild against google.genai.Client._get_api_client, which is a @staticmethod whose first parameter happens to be named vertexai:
TypeError: Client._get_api_client() got multiple values for argument 'vertexai'
The instance lands in vertexai positionally and collides with the explicit vertexai= keyword at the call site.
Scope, checked rather than assumed
| member kind |
getmembers returns |
isfunction |
reaches _rebind |
staticmethod |
plain function |
True |
yes, corrupted |
classmethod |
bound method |
False |
no, skipped |
| ordinary method |
plain function |
True |
yes, correct (binding self is right) |
So classmethod escapes by accident, via the isfunction filter. Only staticmethod is affected.
Suggested fix
Determine the descriptor kind from the class __dict__ rather than the getmembers result, and re-wrap before setattr:
raw = owner.__dict__.get(name) if isinstance(owner, type) else None
wrapper = auto_tracing_helpers.build_tracing_wrapper(fn, self._tracer, self._caps)
if isinstance(raw, staticmethod):
wrapper = staticmethod(wrapper)
setattr(owner, name, wrapper)
inspect.getattr_static would work too. Happy to open a PR if that would help.
Related: _add_agent_scope cannot be constrained to first-party modules
Separate but compounding. _add_agent_scope walks objects reachable from the agent to depth 30 and adds any module prefix it finds to the plugin scope. Passing extra_scope_prefixes=("my_package",) still resulted in google.genai.client being instrumented, because something in the agent object graph resolved to it.
Combined with the bug above, enabling the plugin instruments arbitrary third-party modules and then corrupts any staticmethod it finds there. In one short trial that broke two unrelated call paths.
Would a strict allowlist mode for _add_agent_scope be something you would consider?
Worth saying
The plugin does work where this does not bite. Spans on ordinary functions carried genuinely useful adk.fn.arg.* and adk.fn.return detail with no hand instrumentation, which is exactly what we were evaluating it for. These two issues are what stopped us adopting it, not the idea.
Version: google-adk 2.7.0, Python 3.13.
Summary
AutoTracingPlugin._wrap_modulediscovers class members withinspect.getmembers, which returns the underlying plain function for a@staticmethod._rebindthensetattrs a plain wrapper function back onto the class without re-wrapping it instaticmethod(...).After that, accessing the attribute through an instance re-engages the descriptor protocol and binds the instance as the first positional argument. Any static method whose first parameter is a real parameter now receives
selfin that slot.plugins/auto_tracing_plugin.py(2.7.0),_wrap_modulearound L145 and_rebindaround L164:Reproduction
Observed in the wild against
google.genai.Client._get_api_client, which is a@staticmethodwhose first parameter happens to be namedvertexai:The instance lands in
vertexaipositionally and collides with the explicitvertexai=keyword at the call site.Scope, checked rather than assumed
getmembersreturnsisfunction_rebindstaticmethodclassmethodselfis right)So
classmethodescapes by accident, via theisfunctionfilter. Onlystaticmethodis affected.Suggested fix
Determine the descriptor kind from the class
__dict__rather than thegetmembersresult, and re-wrap beforesetattr:inspect.getattr_staticwould work too. Happy to open a PR if that would help.Related:
_add_agent_scopecannot be constrained to first-party modulesSeparate but compounding.
_add_agent_scopewalks objects reachable from the agent to depth 30 and adds any module prefix it finds to the plugin scope. Passingextra_scope_prefixes=("my_package",)still resulted ingoogle.genai.clientbeing instrumented, because something in the agent object graph resolved to it.Combined with the bug above, enabling the plugin instruments arbitrary third-party modules and then corrupts any
staticmethodit finds there. In one short trial that broke two unrelated call paths.Would a strict allowlist mode for
_add_agent_scopebe something you would consider?Worth saying
The plugin does work where this does not bite. Spans on ordinary functions carried genuinely useful
adk.fn.arg.*andadk.fn.returndetail with no hand instrumentation, which is exactly what we were evaluating it for. These two issues are what stopped us adopting it, not the idea.Version:
google-adk2.7.0, Python 3.13.