fix(telemetry): detect runtime markers by presence, split paas from serverless

Three findings from the CodeRabbit, code-quality and Cursor reviews.

The runtime loop tested truthiness while constants.py documented presence,
so a platform exporting a bare CI= fell through to the TTY fallback and
was mislabelled as an ordinary local run. Presence is now what it says.
The assistant markers keep truthiness deliberately: there an empty value
means the tool set a placeholder rather than claiming the session.

DYNO and WEBSITE_INSTANCE_ID marked Heroku dynos and Azure App Service
instances as serverless, and since serverless is checked first they could
never reach the container label. They move to a paas context, which is
what they are: long-lived containers rather than per-invocation
functions. AWS_EXECUTION_ENV is dropped entirely - it is set on ECS and
EC2 as well as Lambda, and AWS_LAMBDA_FUNCTION_NAME already covers Lambda
without the collision.

The container probe no longer wraps os.path.exists in a try/except.
os.path.exists handles OSError internally and returns False, so the
handler guarded a condition that cannot occur and only hid the intent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joao Moura
2026-08-07 15:22:36 -07:00
parent 73bfae300d
commit 96e40ed3a4
3 changed files with 48 additions and 8 deletions

View File

@@ -105,19 +105,19 @@ def detect_runtime_context() -> str:
"unknown" when that check cannot be made. The result is always a member
of KNOWN_RUNTIME_CONTEXTS.
"""
# Presence, not truthiness: a platform that exports an empty CI= is still
# CI, unlike the assistant markers where an empty value means the tool set
# a placeholder rather than claiming the session.
for context_name, env_vars in RUNTIME_CONTEXT_ENV_MARKERS:
if any(os.environ.get(env_var) for env_var in env_vars):
if any(env_var in os.environ for env_var in env_vars):
return context_name
for env_var, expected, context_name in _EDITOR_TERM_MARKERS:
if os.environ.get(env_var) == expected:
return context_name
try:
if os.path.exists(_DOCKER_ENV_PATH):
return "container"
except OSError:
pass
if os.path.exists(_DOCKER_ENV_PATH):
return "container"
try:
return "interactive" if sys.stdout.isatty() else "non_interactive"

View File

@@ -25,6 +25,7 @@ __all__ = [
"MAX_FILE_NAME_LENGTH",
"NOTEBOOK_ENV_VARS",
"NOT_SPECIFIED",
"PAAS_ENV_VARS",
"RUNTIME_CONTEXT_ENV_MARKERS",
"SERVERLESS_ENV_VARS",
"TRAINED_AGENTS_DATA_FILE",
@@ -98,12 +99,17 @@ CI_ENV_VARS: Final[tuple[str, ...]] = (
"TRAVIS",
)
SERVERLESS_ENV_VARS: Final[tuple[str, ...]] = (
"AWS_EXECUTION_ENV",
"AWS_LAMBDA_FUNCTION_NAME",
"DYNO",
"FUNCTION_TARGET",
"K_SERVICE",
"VERCEL",
)
# Managed application platforms, kept apart from serverless: their markers are
# set for long-lived containers rather than per-invocation functions, and
# checking them under "serverless" would have claimed every Heroku dyno and
# Azure App Service instance before the container check could see them.
PAAS_ENV_VARS: Final[tuple[str, ...]] = (
"DYNO",
"WEBSITE_INSTANCE_ID",
)
HOSTED_IDE_ENV_VARS: Final[tuple[str, ...]] = (
@@ -123,6 +129,7 @@ CONTAINER_ENV_VARS: Final[tuple[str, ...]] = ("KUBERNETES_SERVICE_HOST",)
RUNTIME_CONTEXT_ENV_MARKERS: Final[tuple[tuple[str, tuple[str, ...]], ...]] = (
("ci", CI_ENV_VARS),
("serverless", SERVERLESS_ENV_VARS),
("paas", PAAS_ENV_VARS),
("hosted_ide", HOSTED_IDE_ENV_VARS),
("notebook", NOTEBOOK_ENV_VARS),
("container", CONTAINER_ENV_VARS),

View File

@@ -577,3 +577,36 @@ def test_all_common_attributes_land_on_exported_spans(clean_env, monkeypatch):
assert exported["coding_agent"] == "claude_code"
assert exported["runtime_context"] == "ci"
assert exported["project_id"] == "proj-123"
def test_runtime_markers_are_detected_by_presence(clean_env, monkeypatch):
"""An empty value still means the platform set the marker.
Some platforms export a bare `CI=`; truthiness checks would drop those
runs to the TTY fallback and mislabel them as ordinary local executions.
"""
monkeypatch.setattr("os.path.exists", lambda path: False)
clean_env.setenv("CI", "")
assert detect_runtime_context() == "ci"
def test_managed_platforms_are_not_reported_as_serverless(clean_env):
"""Long-lived managed platforms must not claim the serverless label.
DYNO and WEBSITE_INSTANCE_ID mark Heroku dynos and Azure App Service
instances, which are containers rather than per-invocation functions.
"""
clean_env.setenv("DYNO", "web.1")
assert detect_runtime_context() == "paas"
clean_env.delenv("DYNO")
clean_env.setenv("WEBSITE_INSTANCE_ID", "abc123")
assert detect_runtime_context() == "paas"
def test_serverless_markers_still_win_over_paas(clean_env):
clean_env.setenv("DYNO", "web.1")
clean_env.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-fn")
assert detect_runtime_context() == "serverless"