Skip to content

feat: header-based client-type classification, user_type on spans - #37

Merged
vreshch merged 1 commit into
masterfrom
feat/header-user-type
Aug 14, 2026
Merged

feat: header-based client-type classification, user_type on spans#37
vreshch merged 1 commit into
masterfrom
feat/header-user-type

Conversation

@vreshch

@vreshch vreshch commented Aug 14, 2026

Copy link
Copy Markdown
Member

Why

Test traffic is currently identified by regexing user_agent.original in the admin console, and every consumer hand-rolls its own classify for the request-log user_type. The estate already declares itself: the e2e suite sends X-Client-Type: test, service-to-service calls send service. This makes that header the kit's own classifier and puts the result on SPANS, so the admin console can filter tool/entry spans on attributes_string['user_type'] instead of guessing from the UA.

What

New src/client-type.ts - mirrors web packages/shared/src/client-type.ts and the Vector VRL edge rules 1:1 (header service/test wins -> test UA -> bot UA / scanner path -> user). One deliberate superset: the test UA regex also matches puppeteer. No new runtime deps.

New exports from @agentage/observability:

  • CLIENT_TYPE_HEADER, USER_TYPE_FIELD, UserType (const + type), type ClientTypeInput
  • classifyClientType({ header, userAgent, path }): UserType
  • contextWithUserType(userType, ctx?), userTypeFromContext(ctx?), stampUserType(span?)

Span stamping

  • createRequestLog classifies at request ENTRY (was: at finish), stamps user_type on the active span, and runs next() inside a context carrying user_type in OTel baggage.
  • withSpan stamps every span it creates from that context.
  • setMcpTool stamps the active tool span - MCP consumers get it with no call-site change.
  • stampUserType(span) is exported for spans consumers create themselves.

request-log now defaults classify to the kit classifier (header -> UA -> path). Injected classifiers keep winning unchanged; () => undefined drops the field. Only canonical UserType values propagate to spans; a custom classifier's own vocabulary still lands on the log line.

Behaviour changes to know

  • classify is invoked at request entry rather than at finish (it needs to exist while the request runs). Callers whose classifier reads state set by later middleware would see a different value - none in the estate do (all read headers/UA/path).
  • Consumers that pass no classify gain a user_type field on every request line.

Tests

test/client-type.test.ts (header precedence, UA fallback, scanner paths, span attribute, baggage propagation, no-op without classification) + request-log cases (header classification, UA/path fallback, span stamp + descendant read, opt-out). test/stack-context-manager.ts registers a synchronous context manager, since the bare API ships a noop one. 238 tests pass.

Release

package.json bumped to 0.16.0 + CHANGELOG entry. NOT published: publishing needs a chore(release): 0.16.0 squash-merge subject on master, which is the merge step, not this PR.

Verify

npm ci && npm run verify green locally (type-check + lint + format:check + 238 tests + build + dist smoke).

@vreshch
vreshch marked this pull request as ready for review August 14, 2026 16:39
@vreshch
vreshch merged commit c575c41 into master Aug 14, 2026
1 check passed
@vreshch
vreshch deleted the feat/header-user-type branch August 14, 2026 16:39
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.

1 participant