Repository navigation
feat: setEndUser stamps enduser.id on spans and log records - #51
Conversation
OtelZone.setEndUser(String? id) links every span started and every log record
emitted from the next one on to an account, as the semantic convention's
`enduser.id`. null (or an empty string) stops it. The package never sets it
itself, so a build that does not call it exports no enduser.id anywhere.
Spans are stamped in onStart by a span processor appended after the pipeline:
a span is exported at its end, after every processor has seen it start, so the
order does not matter. Log records are stamped in onEmit by a log processor that
has to be first, because BatchLogRecordProcessor queues a clone of each record
and a stamp made after it lands on an original nobody exports; the provider's
processor list is append-only, so start() hands the stamper to OTel.initialize
and builds the rest of the logs pipeline behind it with the SDK's own
LogsConfiguration.configureLoggerProvider. A test pins the ordering constraint
so it fails the day the SDK stops cloning.
The id is exempt from `redact` on spans; a log record's is added after the
bridge has scrubbed the record, so it never meets the redactor. Measured over
200,000 random ids of each kind, an unanchored \d{9} (the README's own example)
masks about 1 in 10,000 cuids and 1 in 33 UUIDs. Every other attribute is
scrubbed exactly as before, and only the exact key is exempt.
Spans and records already made keep what they had: clearing stops linking, it
does not recall. Recovered native crash reports are not stamped, since they
describe the previous run. The call never throws, touches no timer, microtask
or zone value, and is safe before start(): the id is kept and applied from the
first span and record after a successful start. Stamping never throws or fails
a future into the SDK, which does not await its processors. Neither stamper is
installed when OTEL_SDK_DISABLED or OTEL_TRACES_EXPORTER=none builds no
pipeline.
This is ticket T12 of FEAT-44, and EXT-27 in the vaam-apps/vaam-apps
core-revamp list.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XLy7TiPAxtVjPKEwQ9iwBa
|
So the failing case is the Everything else is green on this commit: analyze and test, Super-linter, both conventional-commit checks, Trivy, Swift/iOS, Kotlin (on the one re-run, after a Generated by Claude Code |
Summary
Adds
OtelZone.setEndUser(String? id). From the next span or log record on, every span started and every log record emitted carries the semantic convention'senduser.id.null(or an empty string) stops it. The id is exempt fromredact; everything else is scrubbed exactly as before.Intent
Ticket T12 of FEAT-44 (account-linked analytics, approved by the owner on 2026-10-08), which is EXT-27 in
docs/core-revamp.mdof vaam-apps/vaam-apps. The Vaam app links its spans to an account only while the account's consent row says yes, and needs this package to stamp and clear the id. Merging this cuts v0.6.0 through release-please, which the app'sotel_zonebump (T15) waits on.The package never sets the id by itself. A build that does not call
setEndUserexports noenduser.idanywhere, so this changes nothing for an app that does not opt in.Scope
lib/src/end-user.dart(new):EndUserSpanProcessorandEndUserLogRecordProcessor, and theenduser.idkey (read from the SDK's generated semconv registry, pinned to its literal by a test).lib/src/otel-zone.dart:setEndUser,endUserId, and the wiring instart().lib/src/span-redaction.dart: the exemption for the exact keyenduser.id.README.md("Linking telemetry to an account"), API docs onsetEndUser,RedactingSpanExporterandOtelZoneConfig.redact.CHANGELOG.mdis generated by release-please from this squash commit; it is not edited by hand.Not in scope: the app side (T15), and anything in the native Android/iOS code.
How it works
onStartby a span processor appended after the pipeline. A span is exported at its end, after every processor has seen it start, so the order does not matter. It is installed only when a trace pipeline exists, soOTEL_SDK_DISABLEDandOTEL_TRACES_EXPORTER=nonestill leave no processor behind.onEmit, and that processor has to be first.BatchLogRecordProcessor.onEmitqueues a clone of each record, so a stamp made after it lands on an original nobody exports, and the provider's processor list is append-only.start()therefore hands the stamper toOTel.initializeand builds the rest of the logs pipeline behind it with the SDK's ownLogsConfiguration.configureLoggerProvider, with the argumentsinitializewould have passed.test/end-user-log-order_test.dartproves the constraint is real, and is written to fail the day the SDK stops cloning.enduser.idis passed through whole by the span scrubber. A log record's is added after the bridge has scrubbed the record, so it never meetsredact. Only the exact key is exempt (tested againstenduser.pseudo.id,user.enduser.id,Enduser.idand others).Behaviour worth knowing
setEndUseris one field assignment, with no lock, timer, microtask or zone value, so any zone may call it and the last call wins. Stamping never throws or fails a future into the SDK, which does not await its processors. State is per isolate.start(). The ticket asked for a no-op while the SDK is not up. I made it keep the value instead of dropping it: it still cannot throw and nothing can be observed from it, but an id set a moment beforestart()completes is not silently lost. If the SDK never starts, nothing is ever stamped. This is a judgment call; say if a literal no-op is wanted.Verification
Run locally with Flutter 3.48.0-0.4.pre (the version
ci.ymlpins), after deleting both lockfiles, on top of #49 (thedartastic_opentelemetry_apicap, merged):dart format --output=none --set-exit-if-changed .: 47 files, 0 changed.flutter analyze: no issues.flutter test: 249 passed, 0 failed (217 before this change, 32 new).OtelZone.start()and a real loopback collector, and decode the SDK's own protobuf: spans and logs carry the id from the call to the clear and not before or after, with aredactthat masks nine digits in a row (and does mask the id's neighbours); the same through the spooling exporter and dartastic's own trace pipeline, with the id set beforestart();OTEL_LOGS_EXPORTER=nonestill sends no logs.initializefails both wire tests; dropping an id set beforestart()fails 2 tests.@conventional-commits/parser, and a nested-paren control line correctly fails.\d{9}(the README's ownredactexample) masks about 1 in 10,000 cuids (0.009%) and 1 in 33 UUID v4s (3.1%); an anchored^\+?\d{9,12}$masks neither. So a cuid can trip a phone rule, rarely, and that is what the exemption is for.Not run here: the Kotlin, Swift and emulator jobs of CI (no native code changed), and a real device.
android crash harness (API 34)has been red onmainsince 2026-10-02 because the emulator hangs partway through the run (evidence in the comment on #49); it is not affected by this change either way.Risk Assessment
redactwill not catch a phone number passed as the id; documented in the API docs and the README.setEndUser. The logs pipeline is now built in two steps by the same SDK function;OTEL_LOGS_EXPORTER, the OTLP headers and the spool are covered by tests.dartastic_opentelemetry_apicap from fix: cap dartastic_opentelemetry_api below 1.0.0-rc.4 #49, so a fresh resolve gets rc.3.Reviewer Focus
OtelZone.start()and the comment above it._scrubAttributes: the exemption is by exact key, in span, event, link and scope attributes alike.start()is what you want (see above).🤖 Generated with Claude Code
https://claude.ai/code/session_01XLy7TiPAxtVjPKEwQ9iwBa
Generated by Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.