Skip to content

feat(observability): solution attribution via native AWS_SDK_UA_APP_ID (#319, alt to #338)#345

Draft
scottschreckengaust wants to merge 6 commits into
mainfrom
feat/319-sdk-user-agent-appid
Draft

feat(observability): solution attribution via native AWS_SDK_UA_APP_ID (#319, alt to #338)#345
scottschreckengaust wants to merge 6 commits into
mainfrom
feat/319-sdk-user-agent-appid

Conversation

@scottschreckengaust

@scottschreckengaust scottschreckengaust commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Draft — feature-complete, in review. The simplified alternative to #338 for issue #319. Both PRs are intentionally open in draft so reviewers can compare the two approaches side by side and pick one. Do not merge yet.

Closes #319 (one of two candidate implementations — see #338 for the other)

The two options, side by side

#338 (raw-path) this PR #345 (native app-id)
app/uksb-wt64nei4u6#{stack} hand-built via raw customUserAgent/user_agent_extra because / isn't a legal app-id char SDK-native AWS_SDK_UA_APP_ID env — # separator is legal, so zero client code
md/uksb-wt64nei4u6#{component} static, baked once static, baked once (same)
#{TRACE} per-request before-send handler + JS middleware + module trace state + ~60 withAbcaTrace() wraps dropped — correlation owned by X-Ray / structured-log request ids (#245)
{STACKNAME} sanitize-then-clip-34 custom routine none — CFN names are [A-Za-z0-9-], already a subset of the app-id charset
Customer opt-out n/a -c sdkUaAppId='' (deploy) or AWS_SDK_UA_APP_ID='' (CLI) — built-in

The key unlock (confirmed against installed botocore 1.43.9 + JS v3): using # instead of / as the app/ separator keeps the segment on the SDK's native app-id field, which both SDKs read from AWS_SDK_UA_APP_ID automatically. That removes the only reason #338 needed the raw user-agent path and the entire trace plane.

What's implemented (all complete)

  • Helpersagent/src/ua.py, cdk/src/handlers/shared/ua.ts, cli/src/ua.ts: each owns only the static md/#{component} segment + a defensive sanitizeUaValue(). Wire-capture tests on all three assert both segments land on a real outbound User-Agent header (and app/ is omitted when AWS_SDK_UA_APP_ID is unset/empty).
  • Agent — session-level user_agent_extra (scoped + plain sessions) covers every tenant_client/tenant_resource; new platform_client() carries the md/ segment for the 8 direct boto3.client sites. No before-send appender.
  • JS handlers...abcaUserAgent() spread into all 60 SDK client sites across 43 files. No middleware.
  • CLIapplyDefaultAppId() at startup + ...abcaUserAgent() on all 18 AWS SDK client sites; auth.test.ts asserts the Cognito client receives the md/ pair.
  • CDK env threading — stack-level SolutionUaAspect sets AWS_SDK_UA_APP_ID on every Lambda (current + future, structurally); AgentCore runtime + ECS container set it explicitly; per-surface ABCA_COMPONENT (api/orchestr/webhook, integrations via a per-construct ComponentUaAspect). Template assertions added.
  • DocsAGENTS.md "Common mistakes" bullet for future SDK clients.

Verification

  • agent //agent:quality green: 1074 tests, 78.98% coverage, ruff + ty clean.
  • cdk //cdk:test green: 2061 tests (114 suites), compile clean, eslint 0 errors.
  • cli //cli:build green: 365+ tests, eslint clean.
  • docs //docs:build green; no Starlight mutation.
  • Local cdk synth fails only on the pre-existing ec2:DescribeAvailabilityZones cred gap on this workstation (same as main and feat(observability): inject solution into outbound AWS SDK User-Agent (#319) #338) — CI's build job runs the real synth.

🤖 Generated with Claude Code

…319)

Alternative to PR #338: emit the app/ segment via the SDK-native
AWS_SDK_UA_APP_ID env var (botocore + JS v3 read it automatically) using
'#' instead of '/' as the separator, so no raw-user-agent-path machinery
is needed. Each ua module owns only the static md/#{component} segment;
there is no per-request {TRACE} handle, no before-send/middleware, and no
module trace state — request correlation stays with X-Ray / structured
logs (#245), and connection pools are never re-pinned.

CloudFormation stack names are [A-Za-z0-9-], a subset of both the
UA-token and app-id charsets, so {STACKNAME} needs no sanitization; the
only sanitize() left is defensive cover on the component label.

This commit adds the three mirrored helpers + tests (agent/src/ua.py,
cdk/src/handlers/shared/ua.ts, cli/src/ua.ts). Each has a wire-capture
test asserting both app/ and md/ segments land on a real outbound
User-Agent header (and that app/ is omitted when AWS_SDK_UA_APP_ID is
unset/empty — the customer opt-out). 12 agent + 12 cdk + 10 cli tests,
100% coverage on the new modules. Wiring the client sites + CDK env
threading follow in subsequent commits.

Part of #319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scottschreckengaust and others added 5 commits June 13, 2026 18:36
…sites (#319)

Session-level user_agent_extra on both the scoped (refreshable) and the
plain ambient session covers every tenant_client/tenant_resource caller.
New platform_client() carries the static md/ segment (merged into any
caller Config) for the 8 direct boto3.client sites that bypass the
session by design — logs x5 (shell, server x2, telemetry x2), secrets
manager x2 (config), bedrock-agentcore x1 (memory) — plus the ambient
STS client used for role chaining.

No per-request trace handle and no before-send appender: the md/ segment
is fully static, so cached clients and the singleton session pool are
never re-pinned. The app/ segment is contributed separately by the SDK
from AWS_SDK_UA_APP_ID (threaded by CDK, next commit).

4 new aws_session tests assert the md/ segment rides platform_client,
the unscoped tenant_client, a merged caller Config, and the scoped
session. Full agent suite green (1070 tests).

Part of #319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Spread ...abcaUserAgent() into all 60 SDK v3 client constructors across
43 handler files (DynamoDB/Secrets Manager/Lambda/Bedrock/ECS/
BedrockAgentCore). DocumentClient sites instrument the inner
DynamoDBClient (shared middleware stack). No withAbcaTrace/middleware —
the md/ segment is fully static, so module-level cached clients keep
their connection pools; the app/ segment rides native AWS_SDK_UA_APP_ID
(threaded next commit).

No behavior change beyond the UA header: all 2051 existing CDK tests
pass unmodified (the new spread arg merges into the constructor config
the tests already assert on / mock).

Part of #319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
)

applyDefaultAppId() at startup defaults AWS_SDK_UA_APP_ID to the
solution id (only when unset — an explicit '' opts out) so the CLI's
own SDK calls carry the app/ segment with no per-site code. Spread
...abcaUserAgent() into all 18 AWS SDK v3 client sites (Cognito x3,
Secrets Manager, CloudFormation, DynamoDB) across auth/admin/github/
slack/linear; the bgagent REST ApiClient is not an AWS SDK client and
is untouched.

auth.test.ts asserts the Cognito client constructor receives the md/
customUserAgent pair. Full CLI suite green (365 + new tests).

Part of #319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…319)

The `app/` segment is now SDK-native: a stack-level SolutionUaAspect sets
AWS_SDK_UA_APP_ID=uksb-wt64nei4u6#{stackName} on every Lambda (current and
future, structurally), and the AgentCore runtime + ECS container set the
same value explicitly (the Lambda-only aspect can't reach them). botocore
and JS v3 both read AWS_SDK_UA_APP_ID natively, so no client code builds
the app/ segment. `-c sdkUaAppId=''` opts the whole stack out; any other
`-c sdkUaAppId=` value overrides.

The `md/#{component}` label is per-surface ABCA_COMPONENT: 'api'
(task-api commonEnv), 'orchestr' (orchestrator/reconcilers/cleanup/fanout),
'webhook' (slack/linear/github-screenshot integrations, via a per-construct
ComponentUaAspect so every function in the integration — including future
ones — is labeled without editing each env block).

buildAppId() centralizes the value: defaults to uksb-wt64nei4u6#{stack},
sanitizes a non-CFN override, clips to the documented 50-char cap, and
returns undefined for the empty-string opt-out. CloudFormation stack names
are [A-Za-z0-9-] (already app-id-safe), so no stack-name sanitization is
needed in the default path.

New tests: solution-ua-aspect.test.ts (buildAppId vectors + both aspects);
task-api/orchestrator template assertions for the component label. Full
CDK suite green (2061 tests). Local synth fails only on the pre-existing
ec2:DescribeAvailabilityZones cred gap (CI runs the real synth).

Part of #319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New "Common mistakes" bullet directing agent/handler/CLI code to the
per-surface ua helpers and explaining the app/ (SDK-native via
AWS_SDK_UA_APP_ID) vs md/ (explicit per-surface label) split, plus the
customer opt-out. Root-level file — no Starlight sync needed.

Part of #319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(observability): inject solution into outbound AWS SDK User-Agent

1 participant