[SDTEST-3770] New exporter pipeline (OTel-native spans, logs, coverage)#258
Merged
Conversation
Rewire SpansExporter / LogsExporter / CoverageExporter to drive the
typed test-optimization API services (SpansApi / LogsApi /
TestImpactAnalysisApi) instead of the legacy RequestBuilder +
DataUploader stack. The exporters' public surface is unchanged —
EventsExporter still exposes exportEvent, the URL-based coverage entry,
the configuration-API facade, and SpanExporter conformance, so no
consumer in DatadogSDKTesting had to move.
Internals:
- DataFormat gains DataFormatType and a JSONFileHeader-driven init that
builds a `{...header..., "<batch>": [...]}` envelope around each
feature's entries.
- FilesOrchestrator gains FilesOrchestratorType; getWritableFile now
returns (file, isNew) so FileWriter can emit `prefix` on a fresh file
and `separator` on reuse. New closeWritableFile()/getAllReadableFiles()
back the synchronous flush path.
- FileWriter takes an injected encoder + entity label, supports
update(dataFormat:) and closeCurrentFile() (under barrier); writeSync
throws.
- FileReader: readNextBatch -> getNextBatch() throws; markBatchAsRead
throws; new getRemainingBatches() returns a Batch.Iterator.
- DataUploader -> ClosureDataUploader: adapts (Data) async
throws(HTTPClient.RequestError) -> Void to DataUploadStatus, driven
through the existing waitForAsync shim.
- DataUploadWorker: extract DataUploadWorkerType (update / flush /
stop); flush() synchronously drains pending batches with retry,
honoring server Retry-After via Delay.set(delay:); shutdown is now
stop().
- DataUploadStatus gains waitTime + an (httpCode:headers:) init that
parses Retry-After / X-RateLimit-Reset.
- FeatureStorage + FeatureUpload collapse into FeatureStoreAndUpload.
- TestImpactAnalysisApi gains uploadCoverage(batch:) (multipart POST
to /api/v2/citestcov).
- Deleted Upload/RequestBuilder.swift + Upload/MultipartFormDataRequest.swift
(and their Xcode project entries).
- Consolidated on JSONEncoder.apiEncoder: outputFormatting gains
.withoutEscapingSlashes so existing URL/string serialization stays
byte-for-byte; JSONEncoder.default() factory removed.
- APIServiceConfig gains init(configuration:) so sub-exporter tests
can build a SpansApiService/LogsApiService directly without
duplicating the field mapping in EventsExporter.
Tests:
- Persistence/Upload/Spans/Logs tests updated for the new init shapes
and method names; new DataFormat.formatFileContents(_:) test helper
lets FileReaderTests seed a file with prefix-included contents and
assert the read result preserves the prefix.
- MockClosureDataUploader replaces the old DataUploader-backed mock
in the upload worker/uploader tests, keeping MockHTTPClient as the
request recorder.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… Resource Phase 2 of the exporter refactor. The data path now follows the OTel contract end-to-end: the SDK delivers SpanData / ReadableLogRecord through the protocols, and the per-feature exporters read service / version / environment / SDK info from `SpanData.resource` instead of carrying them as init args. EventsExporter's facade (exportEvent, SpanExporter conformance, the configuration-API methods) is unchanged. Internals: - New OpenTelemetry+Extensions.swift: typed accessors on `Resource` (`service`, `applicationName`, `applicationVersion`, `environment`, `sdkName`, `sdkLanguage`, `sdkVersion`) backed by `SemanticConventions`, `SpanData.testSessionId/SuiteId/ModuleId`, and `ResultCodeCompatible &&` overloads on `SpanExporterResultCode` / `ExportResult`. - `DDSpan(spanData:)` drops the `serviceName` / `applicationVersion` arguments and reads them off `spanData.resource`. - `DDLog` gains an init taking `(spanId, traceId, timestamp, status, message, resource, attributes)`; `DDLog(event:span:)` and a new `DDLog(log: ReadableLogRecord, span: SpanContext)` use it. The legacy `DDLog(event:span:configuration:)` is gone. `Status` gains a `init(severity: Severity)` bridge. - `SpansExporter` conforms to `OpenTelemetrySdk.SpanExporter`. Adds `setMetadata(_:)` which rebuilds the headerful `DataFormat` (keeping the per-runtime `runtime-id` pinned) and rotates the writable file via `FeatureStoreAndUpload.update(dataFormat:)`. - `LogsExporter` conforms to `OpenTelemetrySdk.LogRecordExporter`, exposing `export(logRecords:) -> ExportResult`, `forceFlush`, and `shutdown(timeout:)`. The stored `configuration` field is gone. - New `SpanEventsLogExporterAdapter`: a `SpanExporter` that forwards each span's events to `LogsExporter.exportLogs(fromSpan:)`. Wrapped with the real `SpansExporter` inside an `OpenTelemetrySdk.MultiSpanExporter`, so calling `EventsExporter.export(spans:)` lands both spans on the spans pipeline and span events on the logs pipeline transparently. - `EventsExporter` exposes `setMetadata(_:)` on the protocol; it forwards to `spansExporter.setMetadata` (the only feature whose file header embeds metadata). - `DDTracer` builds a `Resource` populated from `Config` / `Environment` / `DDTestMonitor.tracerVersion` and passes it to `TracerProviderBuilder.with(resource:)` so the new resource-reads in the encoders see real values. Inner `DDTracer.init` gains an optional `resource` parameter, defaulting to an empty `Resource()` for existing call sites that don't supply one (e.g. tests). Tests: - New `SpanCompositesTests` covering `SpanEventsLogExporterAdapter` end-to-end against `MockBackend`. - `SpanSanitizerTests` updated for the simplified `DDSpan(spanData:)` initializer. - Pre-existing `EventsExporterTests` already covers the `MultiSpanExporter`-driven span-and-logs flow and passes unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Phase 3 of the exporter refactor. The coverage pipeline now takes pre-parsed `CoverageRecord`s instead of a (URL, parser) pair, matching the OTel-style boundary used by the spans/logs exporters: the caller owns the profraw lifecycle, parses it, and hands the exporter a typed record carrying the OTel anchors and an explicit `Context` (test vs. suite). Internals: - `CoverageRecord` (public) carries `name`, parsed `CoverageInfo`, `workspacePath`, `Resource`, `InstrumentationScopeInfo`, and a `Context` enum (`.test(testSpanId:, suiteId:, sessionId:)` / `.suite(suiteSpanId:, sessionId:)`). Convenience accessors expose `sessionId`, `suiteId`, `testId`, and `isSuite`. - `CoverageExporterType` (public) — `export(coverageRecords:, explicitTimeout:) -> ExportResult`, `forceFlush(explicitTimeout:)`, `shutdown(explicitTimeout:)`, plus no-arg convenience overloads. `CoverageExporter` conforms; its old URL-based `exportCoverage(coverage:parser:...)` entry point is gone. The exporter still translates each record into a `TestCodeCoverage` on disk (suite-level records serialize `span_id: 0`). - `EventsExporterProtocol` swaps `export(coverage:parser:...)` for `export(coverageRecords:)`. `EventsExporter` no longer imports `CodeCoverageParser`. Consumer side: - `DDCoverageHelper.endTest` switches its signature from raw `UInt64`s to `SpanId`s. The helper now owns the full per-test flow: it parses the profraw via `CoverageProcessor.filesCovered(in:)`, builds a `.test` `CoverageRecord`, calls `exporter.export(coverageRecords:)`, and applies the debug-save / delete decision based on `configuration.debug.saveCodeCoverageFiles`. - `TestImpactAnalysis.testDidFinish` passes `SpanId` values straight through (no more `.rawValue` unwrapping). - `Mocks.CoverageCollector` in the test mocks adopts the new `TestCoverageCollector` signature; the existing UInt64-keyed `has` helper keeps the same shape for the assertions that still use it. Tests: - New `CoverageExporterTests` covering both the test-level and suite-level record paths end-to-end against `MockBackend`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
PipelineIntegrationTests drives all three exporter paths in a single
test against MockBackend and asserts the wire payloads:
- A span carrying a stdout-capture-shaped event ("logString" + a
"message" attribute) — exercises the SpanEventsLogExporterAdapter
that converts span events into log entries.
- A native OTel LogRecord emitted via a LoggerProvider whose
SimpleLogRecordProcessor is wired to LogsExporter — exercises
LogsExporter.export(logRecords:) (severity -> DDLog.Status, body ->
message, span context -> dd.trace_id / dd.span_id).
- A CoverageRecord submitted through
EventsExporter.export(coverageRecords:) — exercises the
CoverageRecord -> TestCodeCoverage conversion.
The test then verifies, per pipeline, that:
- The span's service / version / name come from the Resource.
- Both logs carry the same span context as the source span (correlation
via dd.trace_id / dd.span_id) and pick up service / version from
the same Resource.
- The OTel log's severity (`.warn`) maps to `status: warn` on the wire.
- The coverage payload carries the expected session / suite / span IDs.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
End-to-end coverage for the three telemetry pipelines the SDK now
ships: stdout capture -> span event -> SpanEventsLogExporterAdapter ->
LogsExporter, native OTel LogRecord -> LoggerProvider -> LogsExporter,
and per-test code coverage -> CoverageExporter.
OTel LogRecord plumbing:
- EventsExporter conforms to OpenTelemetrySdk.LogRecordExporter via
extension; export(logRecords:) and forceFlush(explicitTimeout:) are
forwarded to the wrapped LogsExporter and shutdown(explicitTimeout:)
is shared with the SpanExporter conformance. EventsExporterProtocol
inherits from both SpanExporter and LogRecordExporter.
- DDTracer.init builds a LoggerProviderSdk with the EventsExporter as
the LogRecordExporter (NoopLogRecordExporter when traces are
disabled), the same Resource as the TracerProvider, and a
SimpleLogRecordProcessor; the provider is registered globally via
OpenTelemetry.registerLoggerProvider, so any consumer can emit logs
through `OpenTelemetry.instance.loggerProvider` and they will land in
the same upload pipeline as the span-event-derived ones.
Integration tests:
- IntegrationTests-UnitTests links OpenTelemetryApi so the child test
bundle can call OpenTelemetry directly.
- IntegrationTests-UnitTests.xctestplan enables LLVM code-coverage
instrumentation; without it DDCoverageHelper has nothing to parse
and the coverage pipeline never fires.
- IntegrationTests/UnitTests/XCTestSmokeTests.swift gains
XCStdoutOTelAndCoverage, and SwiftTestingSmokeTests.swift gains
STStdoutOTelAndCoverage. Each child test print()s a line (stdout
pipeline), emits an OTel LogRecord at Severity.warn (OTel pipeline),
and passes (coverage pipeline runs implicitly via
DDCoverageHelper.endTest).
- IntegrationTests/Runner/UnitTests+XCTestSmoke.swift and
UnitTests+SwiftTestingSmoke.swift gain
`stdoutOTelAndCoverage` outer runner tests that:
* Drive the child test under the real SDK in a subprocess, with
MockBackend settings enabling itr/codeCoverage and the
DD_CIVISIBILITY_CODE_COVERAGE_ENABLED env var on.
* Assert one test span arrived (status pass, expected
resource/name/suite).
* Assert both log entries are present on the wire, and that the
OTel-shaped one carries status `warn` (Severity.warn ->
DDLog.Status.warn).
* Assert at least one coverage payload arrived.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes the loop on the native OTel data path: the EventsExporter no longer exposes `exportEvent` or `exportLogs(fromSpan:)`, and producers hand the SDK either a `SpanData` (via SimpleSpanProcessor + SpanExporter) or a `ReadableLogRecord` (via LogRecordExporter) — the upload pipeline consumes only OTel-shaped inputs. Spans path: - Sessions / modules / suites no longer write pre-encoded `SessionEnvelope` / `ModuleEnvelope` / `SuiteEnvelope` JSON directly into the spans pipeline. Each emits an `OpenTelemetrySdk.SpanSdk` through a new `DDTracer.createLifecycleSpan(name:spanId:startTime: attributes:)` helper that wires `SpanSdk.startSpan` with the registered span processor / clock / resource and a `SpanContext` whose `spanId` is the lifecycle object's controlled id (preserving the `test_session_id` / `test_module_id` / `test_suite_id` semantics on the wire). The span flows through the registered `SimpleSpanProcessor → EventsExporter → MultiSpanExporter → SpansExporter` pipeline. - `SpansExporter.exportSpan` switches on `span.attributes["type"]` and routes to per-type encoders: `test` -> `CITestEnvelope`, `test_session_end` -> new `TestSessionEnvelope`, `test_module_end` -> new `TestModuleEnvelope`, `test_suite_end` -> new `TestSuiteEnvelope`, otherwise `SpanEnvelope`. - New `TestLifecycleEncoders.swift` adds `DDTestSessionSpan` / `DDTestModuleSpan` / `DDTestSuiteSpan` plus their envelopes. They read everything they need from `SpanData` (start, duration, status, attributes, name, resource) and reproduce today's Session/Module/Suite content byte-for-byte (no `trace_id` / `span_id` / `parent_id`, just `test_*_id` plus start/duration/error/name/resource/service/meta/metrics). - `Session`, `Module`, `Suite` drop their `Encodable` conformance and the bespoke `encode(to:)` + `*Envelope` types. Logs path: - `SpanEventsLogExporterAdapter` no longer calls `LogsExporter.exportLogs(fromSpan:)`. It builds a `ReadableLogRecord` per `SpanData.Event` (carrying span context, resource, instrumentation scope, severity inferred from the optional `status` attribute, the event's attributes, and `eventName = event.name`) and hands the batch to the wrapped `LogRecordExporter` via `export(logRecords:)`. The adapter's init now takes a `LogRecordExporter` instead of a concrete `LogsExporter`. - `LogsExporter.exportLogs(fromSpan:)` is removed. `DDLog(event:span:)` follows. - `LogsExporterTests` is rewritten against `export(logRecords:)`. `EventsExporterProtocol` loses `exportEvent`; the concrete `EventsExporter` loses both that method and the `SpanEventsLogExporterAdapter(logsExporter:)` initialiser. The remaining public surface — `export(spans:)`, `export(logRecords:)`, `export(coverageRecords:)`, `setMetadata`, the configuration-API facade — is now uniformly typed against OTel inputs. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The integration tests now verify the SDK linked the data correctly, not just that each pipeline produced *something*: - Capture the test span's trace_id / span_id / test_session_id / test_module_id / test_suite_id from `backend.allTestSpans`. - For both logs (stdout and OTel), assert `dd.trace_id` and `dd.span_id` on the wire match the test span — confirming the SpanEventsLogExporterAdapter and LogRecordExporter pipelines both stitch span context onto the log records. - For coverage, look up the payload by `spanId == testSpan.spanId` rather than blindly grabbing the first entry, then assert `testSessionId` / `testSuiteId` match the test span. Same expectations apply to the XCTest and Swift Testing flavors. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
`Config.enableStdoutInstrumentation` (and the companion stderr flag) default to `false`. They only flip on via `DD_CIVISIBILITY_LOGS_ENABLED` (umbrella) or the per-stream `DD_ENABLE_STDOUT_INSTRUMENTATION` / `DD_ENABLE_STDERR_INSTRUMENTATION` env vars. The integration test harness didn't set any of them, so the child SDK never installed the StdoutCapture hook — `print()` went out untouched and no `logString` span event was attached to the test span. The OTel log path (LoggerProvider) is governed by the same umbrella flag, so it was also a no-op. Add `DD_CIVISIBILITY_LOGS_ENABLED=true` to both flavor configs so the child process actually installs the capture hook and the logger provider for the duration of the test. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The capture hooks (`StdoutCapture` / `StderrCapture`) and the test-error context path in `DDTest` used to attach their content to the active test span via `span.addEvent(name: "logString", ...)`. The `SpanEventsLogExporterAdapter` then turned those events into log entries on flush. This added a layer of indirection — log payloads piggy-backed on span data instead of being first-class telemetry. `DDTracer` now owns a cached `LoggerSdk` built from the same LoggerProvider it registers with `OpenTelemetry`, and the public `logString` / `logError` / `logString(...timeIntervalSinceSpanStart:)` APIs emit OTel `LogRecord`s directly: - `setBody(.string(message))` — message goes out as the log body, no longer as a `"message"` attribute. - `setSeverity(.info)` for stdout and step lines, `.error` for test-assertion error context. - `setAttributes(testAttributes())` — keeps `test.name` / `test.suite` / `test.module` on the log record. - `setTimestamp(...)` only when the caller provided one (e.g. the parsed system-log date or the UI-step offset). - Trace context auto-injection (`includeTraceContext` defaults to true on `LoggerBuilderSdk`) attaches the active span's `dd.trace_id` / `dd.span_id` automatically. The UI-test launch-context path (`logStringAppUITested`) no longer synthesises an aux span; it now emits a `LogRecord` with the launch `SpanContext` set explicitly via `setSpanContext(...)`. The wire shape is unchanged — the OTel `LogRecord` flows through the registered `SimpleLogRecordProcessor` to `LogsExporter`, which writes the same `DDLog` JSON as before (body becomes the `message` field, severity drives `status`, span context drives `dd.trace_id` / `dd.span_id`). The `SpanEventsLogExporterAdapter` stays in place as a general OTel convenience for consumers that attach events to their own spans, but the SDK no longer drives it from internal capture. Tests: - `StdoutCaptureTests` / `StderrCaptureTests` assert the buffer is drained and that the active span has no events (the old assertion was on `spanData.events.count == 1`). Round-trip emission is exercised by the IntegrationTests' `stdoutOTelAndCoverage` cases. - `DDTracerTests.testLogStringAppUI` flips from "an aux span must be emitted" to "no span must be emitted — the message must go out as a LogRecord" to lock in the new behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The capture tests previously asserted only that the span wasn't
mutated; they didn't verify the LogRecord was actually emitted. Plug
in OTel's own `InMemoryLogRecordExporter` so the tests can introspect
the wire payload directly.
- DDTracer gains an optional `logRecordExporter: LogRecordExporter?`
init parameter (defaults nil → existing behaviour). When set, the
internal `LoggerProviderSdk` wires that exporter via
`SimpleLogRecordProcessor` instead of the EventsExporter /
NoopLogRecordExporter pair. The convenience init threads it
through too.
- `StdoutCaptureTests` / `StderrCaptureTests` install a DDTracer
backed by an `InMemoryLogRecordExporter` as the global
`DDTestMonitor.tracer` (which `StdoutCapture` / `StderrCapture`
route through), drive `print()` / `StderrCapture.stderrMessage`,
then assert on `getFinishedLogRecords()`:
* body matches the captured text (system-log prefix stripped /
newline preserved as appropriate)
* severity is `.info`
* `spanContext.spanId` / `traceId` correlate to the active test
span — confirming the LoggerSdk's auto-injection of span
context works end-to-end
* for stderr the parsed system-log timestamp lands on
`record.timestamp`
* the empty-buffer-without-newline case produces zero records
Original tracer is restored in `tearDown`.
- `DDTracerTests.testLogStringAppUI` flips from
"no aux span is produced" to "no aux span AND the LogRecord
carries the launch traceId/spanId from env" — verifies
explicit `setSpanContext(launchSpanContext)` works.
`@testable import OpenTelemetrySdk` is needed because
`InMemoryLogRecordExporter()` is `public` on the class but its
synthesised init is `internal`.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Mirror the spans / logs pipeline for coverage so the SDK side owns the
parser dependency and the exporter module stays parser-agnostic. Also
move all threading into the processor so the provider's API stays
synchronous.
Data shapes (EventsExporter):
- `CoverageContext` enum lifted out of the old `CoverageRecord.Context`
and shared between the URL-stage record and the parsed payload.
- `CoverageFile { name: String, bitmap: Data }` is the wire-ready
per-file entry; no `CoverageInfo` dependency.
- `CoverageData` (renamed from `CoverageRecord`) is the processed
payload — `files: [CoverageFile]`, plus the OTel anchors.
- `CoverageRecord` is now the URL-stage input: `coverageFileURL: URL`
plus the OTel anchors.
Pipeline (EventsExporter):
- New `CoverageProcessor` protocol — `onEnd(record:)`, `forceFlush`,
`shutdown`. The protocol is the only coverage surface that lives in
`EventsExporter`; concrete implementations belong on the SDK side.
- `CoverageExporterType.export(coverageData:, explicitTimeout:)`
replaces `export(coverageRecords:)`. `EventsExporter` conforms.
- `TestCodeCoverage` builds from `[CoverageFile]` directly and does
workspace-prefix stripping inline. The old `CoverageInfo`-based init
and the `coveredLines` extension are gone.
- `import CodeCoverageParser` removed from every file in
`EventsExporter`; the Xcode project drops the package product /
target dependency on `CodeCoverageParser` from the `EventsExporter`
target.
SDK side (DatadogSDKTesting):
- `DDCoverageHelper` → `CodeCoverageProvider`. Modelled after
`TracerSdk` / `LoggerSdk`: one per process, owns the LLVM gatherer
and a `CoverageProcessor`.
- `TestCoverageCollector` protocol reshaped from
`startTest()` / `endTest(testSessionId:testSuiteId:testSpanId:)` into
`startCoverage(context:) -> ActiveCoverage?`. The returned handle
has a single `end()` method (idempotent) that stops LLVM gathering
and ships the record. `ActiveCoverage` exposes its `context`.
- New `BackgroundCoverageProcessor` (was `SimpleCoverageProcessor`
before; renamed because it owns an `OperationQueue` and is no
longer simple). Builds the queue from a `CodeCoveragePriority`,
parses each `CoverageRecord` URL on a background op, builds
`CoverageData`, and forwards to a `CoverageExporterType`.
`forceFlush` / `shutdown` drain the queue before calling through to
the exporter. Optional `cleanupCoverageFiles` flag controls whether
the queued op deletes the profraw after parsing.
- `LLVMCoverageProcessor.swift` carries a typealias for
`CodeCoverage.CoverageProcessor`, isolated in a file that doesn't
import `EventsExporter` so the OTel-protocol name stays unambiguous
in `CodeCoverageProvider` (which imports the new protocol).
- `CodeCoverageProvider` no longer owns threading — `endTest` /
`stop()` become straight-line handoffs to the processor.
`init` passes `cleanupCoverageFiles: !debug` so debug-saved profraws
survive.
`TestImpactAnalysis`:
- Holds a single `Synced<CoverageState>` slot (`collector` +
`active: ActiveCoverage?`) instead of a separate stored
`coverage` property and ad-hoc bookkeeping. `testWillBegin` calls
`collector.startCoverage(context:)` and stashes the handle; on
conflict (overlapping session) it ends the prior, logs the error,
and nils out the collector — once cleared coverage stays off for
the rest of the run, no separate `disabled` flag needed.
- `testDidFinish` pops `state.active` and calls `.end()`.
- `import OpenTelemetryApi` added for `SpanId`.
Tests:
- `CodeCoverageModelTests` (EventsExporter) moves to
`Tests/DatadogSDKTesting/Coverage/CoverageBitmapTests.swift` since
the bitmap-encoding logic now lives in the SDK module
(`CoverageFile.init(file:)`).
- `CoverageExporterTests` / `PipelineIntegrationTests` build
`CoverageData` directly and assert against `export(coverageData:)`.
- `Mocks.CoverageCollector` follows the new protocol: nested
`Active: ActiveCoverage` calls back into the parent on `end()`.
- Stale `LLVMSimpleCoverageFormat.swift` (unused, orphaned) removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
CoverageExporter writes to a fixed Caches subpath shared by every test in the class, so the first test's payload was re-uploaded by the second test's MockBackend reader. Wipe the directory in setUp/tearDown. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Collapses DDTestSessionSpan / DDTestModuleSpan / DDTestSuiteSpan and the CITestEnvelope test-span path into a single TestSpan with a SpanType discriminator, leaving DDSpan responsible only for generic spans. Test/session/module/suite events now share one encoder, write the inner `type` field (matches .NET wire format), and strip resource/type/version from meta so per-event payloads carry only test-specific tags — common metadata like language/library_version/test_session.name flows through SpanMetadata instead. Test models expose a unified `attributes: [String: TestAttributeValue]` with `get(tag:)` / `get(metric:)` in place of separate `tags` / `metrics` dicts. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This comment has been minimized.
This comment has been minimized.
The TestSpan refactor moved `resource` to a top-level field on the event (matching .NET) and stripped it from meta. Update the smoke integration suites to assert on `span.resource` instead of `meta[DDGenericTags.resource]`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The xctestplan files declare an explicit allowlist of environment variables to forward from xcodebuild's process env to the test bundle subprocess via `$(VAR)` substitution. `DD_CIVISIBILITY_LOGS_ENABLED` and `DD_CIVISIBILITY_CODE_COVERAGE_ENABLED` weren't in the list, so the `XcodeTestRunner.Config.environment` overrides set in `stdoutOTelAndCoverage` never reached the child SDK. The umbrella logs flag stayed off, `StdoutCapture` was never installed, `print()` from the test bypassed the SDK entirely, and the OTel `LoggerProvider` was also a no-op — so backend.allLogs came back empty. Add both keys to the unit-tests and UI-tests xctestplans so the runner can flip them on per-test. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Code coverage used to be initialised inside TestImpactAnalysisFactory and driven from inside the TIA feature's per-test hooks, which meant coverage couldn't run unless TIA was also enabled — even though it has its own enable gate (`DD_CIVISIBILITY_CODE_COVERAGE_ENABLED` + backend `code_coverage` setting). Pull coverage into its own `CodeCoverage` TestHooksFeature with a matching `CodeCoverageFactory.isEnabled(config:env:remote:)`. TIA keeps its skip / correlation-id / itr-tags responsibilities; coverage owns the LLVM-gathering state machine, the `testCodeCoverageEnabled` tag and the `testCoverageLines` metric on session/module spans. DDTestMonitor now runs an independent `coverageSetup` BlockOperation off the same `updateTracerConfig` dependency and stores the feature in `coverage`, which is registered alongside `tia` in `activeFeatures`. The `AdditionalTags(codeCoverage:)` fallback now keys off `coverage == nil` rather than `tia == nil`. Also fixes the MockBackend's settings/known-tests/test-management responses to echo the request's JSON:API `data.id`. The SDK rejects responses whose id doesn't match the request's, so hardcoding `"id": "1"` broke `tracerSettings` (and cascaded into TIA / EFD / ATR / Known Tests / Test Management all logging "backend config can't be loaded") under the integration-test mock. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Code coverage is now a standalone feature with its own enable gate, so the integration test no longer needs to flip on ITR/TIA. Leaving `backend.settings.itrEnabled` at its default (`false`) exercises the no-TIA-but-coverage path end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ression Refactors the JSON:API response shape into a protocol family (APIResponseAttributesHasType / HasId / BrokenId / NoId) so each endpoint can declare what shape its response id and type take instead of relying on a single static equality check. This unblocks fixtures like the known-tests / test-management responses where the backend returns a non-matching id, and the TIA endpoint where the response carries no id at all. Also lifts the Content-Encoding: deflate header out of `APIServiceConfig.defaultHeaders` and onto the per-API services (spans, logs, telemetry, TIA coverage, git pack uploads) so each endpoint can opt in conditionally based on `payloadCompression`. The HTTPClient gains a `deflate(_:)` step that inflates the request body when the header is set, and the multipart upload variant exposes `addHTTPHeader` for the per-service injection. Knock-on fix: `MockBackend.route(...)`'s `/api/v2/citestcov` endpoint now feeds the inflated body into `parseCoveragePayload`, matching how the other endpoints already handled compressed payloads. Drops the unused OpenTelemetryApi product dependency from the IntegrationTests-UnitTests target in the xcodeproj. Cleans up the commented-out `withUnsafeTemporaryAllocation` branch in `Data.deflated` (the iOS-15+/macOS-12+ implementation now ships unconditionally). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
`SpansExporter`, `LogsExporter` and `CoverageExporter` no longer carry
their own hardcoded `com.datadog.civisibility/{spans,logs,coverage}/v1`
Caches paths. Each takes a `storage: Directory` parameter and creates a
`v1` subdirectory under it. `EventsExporter.init(config:, storage:)`
fans the supplied root out by calling
`storage.createSubdirectory(path: "spans" / "logs" / "coverage")` and
passing the result to each sub-exporter.
In production, `DDTracer` pulls the root from
`DDTestMonitor.cacheManager?.session(feature: "exporter")`, so the
on-disk layout is now
`<bundleId>/<commit>/<sessionId>/exporter/{spans,logs,coverage}/v1/...`
alongside the other per-session feature dirs (`tia`, `crash`,
`known_tests`, …). If the cache manager is unavailable the exporter is
skipped and a log line is emitted.
Tests create a fresh `Directory.temporary().createSubdirectory(path:
UUID().uuidString)` per case (with `defer { try? storage.delete() }`)
so they no longer share a Caches subpath. `CoverageExporterTests` lost
its `wipeCoverageDirectory` setUp/tearDown plumbing — no longer needed.
`LibraryConfigurationServiceThrowTests` got a small
`makeExporter(baseURL:)` helper that tracks the created storages and
deletes them in `tearDown`.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
`HTTPClientType` now declares just the two async methods — `send(request:)` returning the `HTTPURLResponse` and `sendWithResponse(request:)` returning the body — typed-throwing `HTTPClient.RequestError`. The callback variants (`send(request:completion:)` / `sendWithResult(request:completion:)`) and the internal `runAsync` helper are gone. `HTTPClient` implements the two async methods directly via a shared `perform(_:_:)` helper that wraps `URLSession.dataTask` in a `withCheckedContinuation` and maps the (data, response, error) tuple through the existing `httpClientResult` / `httpClientResultWithData` adapters. `MockHTTPClient` mirrors the new signatures. `MockClosureDataUploader` (the sync `DataUploaderType` shim in CoreMocks) drops its `DispatchSemaphore` callback wait and bridges to async through `waitForAsync`. `HTTPClientTests` convert to `async throws` — annotated `@MainActor` so `ServerMock` still initialises on the main thread (it precondition-checks that). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- `GitUploader`: move the "already-uploaded" sentinel check from `init`
into `sendGitInfo(repositoryURL:commit:)`. Constructing the uploader
no longer fails when the upload has already happened; the per-call
guard now returns `true` (success — nothing to do) instead of
refusing to build the feature. Trailing slash on the packfile temp
directory path so `git pack-objects` writes into the directory
rather than treating the trailing component as a file prefix.
- `GitUploadApi.uploadPackFiles`: drain the entire `withThrowingTaskGroup`
with `while try await group.next() != nil {}` instead of returning
after the first child — otherwise the rest of the uploads were
effectively cancelled. Adds explicit `self.` capture inside the
child-task closure.
- API response shapes: `GitUploadApi.Commit` switches from
`APIResponseAttributesHasId` to `APIResponseAttributesNoId` (the
packfile commit endpoint doesn't echo an id). `Array` now conforms
to `APIAttributesAutoId` / `APIAttributesNoId` /
`APIResponseAttributesNoId` so collection responses get the no-id
rule for free; the manual `isIdValid` override is gone. Adds
`APIResponseAttributesIgnoreType` as a sibling marker to
`HasType` for endpoints that don't gate on the response `type`.
- `SettingsResponse`: reorder protocol conformances to
`HasType, HasId` for consistency with the rest of the API services.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
`MultipartFormURLRequest.append(data:withName:contentType:)` makes the `fileName:` parameter optional (`String? = nil`); when absent, the `Content-Disposition` header no longer carries a `filename="…"` attribute. Renames the parameter from `filename:` to `fileName:` to match Swift API guidelines. Callers that don't actually represent a file drop the argument: - `TestImpactAnalysisApi.uploadCoverage(...)` no longer attaches bogus `CoverageBatch.json` / `DummyEvent.json` filenames to the `coverage` and `event` parts. - `GitUploadApi.uploadPackFile(...)` drops the filename from the `pushedSha` JSON part (kept on the binary `packfile` part where it actually carries the commit-named blob). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… types
`APICommonData` now refines `CustomDebugStringConvertible` with a
default implementation that renders the wrapper as
`{"type": "<apiType>", "id": "<id>", "attributes": <attributes>}`
(omitting `id` when nil and `attributes` when `APIVoidValue`).
`APIEnvelope` gets the matching `{"meta": <meta>, "data": <data>}` /
`{"data": <data>}` template. Both render in snake_case-ish JSON so the
`log.debug("…request: \(request)")` lines line up with the wire shape
the encoder produces.
Conforms the per-service Attribute types — Settings (request +
response, including nested EFD and TestManagement), KnownTests (TestsRequest /
TestsResponse plus the PageInfo request/response halves), TestManagement
(TestsRequest / TestsResponse), TIA (TestsRequest / TestsResponse +
Meta), and GitUpload's `Commit` / `CommitRequestMeta` — to
`CustomDebugStringConvertible` with hand-written JSON-ish renderings.
String fields are quoted, optionals print `null`, and nested
`[String: JSONGeneric]` payloads (configurations, etc.) reuse
`JSONGeneric.object(...).debugDescription` so they already format as
JSON. Multi-field renderings split across `+`-concatenated lines for
readability. `GitUploadApi.searchCommits` switches its log format
from `[meta: …, data: …]` to the new JSON-shaped `{"meta": …,
"data": …}` envelope.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The `Xcode_26.1` image on GitHub-hosted `macos-26` runners is currently broken (Apple-side toolchain regression), so drop it from the unit-tests and integration-tests matrices. The release workflow's `XCODE_VERSION` moves from 26.1 to 26.2. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
daniel-mohedano
approved these changes
May 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports the EventsExporter / DatadogSDKTesting data pipelines onto the typed async API wrappers (SDTEST-3768) and the OTel-native exporter protocols. After this stack the SDK only feeds the upload pipeline with
SpanData,ReadableLogRecord, andCoverageRecord— there are no directexportEvent/exportLogs(fromSpan:)calls left, and the coverage feature follows the same OTel-style provider/processor/exporter split that spans and logs already use.Sequence of commits (in order):
c69c1d9— port file/upload pipeline to the new API wrappers (typedClosureDataUploader+FeatureStoreAndUpload,JSONFileHeader-drivenDataFormat, headerful files, sync drain on flush).RequestBuilder/MultipartFormDataRequestdeleted.9dac658—SpansExporter: OpenTelemetrySdk.SpanExporter,LogsExporter: LogRecordExporter. Encoders read service / version / environment / SDK info fromSpanData.resource.DDTracerbuilds aResourcefromConfig/Environment/tracerVersion.2aabc89— record-based coverage path (CoverageRecord+export(coverageRecords:)); the URL-drivenexport(coverage:parser:…)entry is gone.9bc7b77— in-process pipeline integration test (spans + logs + coverage round-trip againstMockBackend).3867747— full-SDK integration tests: child XCTest + Swift Testing test classes, outer runner asserts spans / stdout-log / OTel-log / coverage payloads on the wire.46f35ea— sessions / modules / suites become real OTel spans,SpanEventsLogExporterAdapterroutes span events as logs viaMultiSpanExporter.exportEventandexportLogs(fromSpan:)removed from the public surface.e30cd13— integration tests strengthened:dd.trace_id/dd.span_idon each log entry match the test span, coverage payload looked up byspanId == testSpan.spanId.6e341c6—DD_CIVISIBILITY_LOGS_ENABLED=truein the integration-test harness so stdout capture + LoggerProvider are actually wired in the child SDK.753f798— stdout / stderr captures emit OTelLogRecords through the registered LoggerProvider instead of attaching to the active span.70a1966— capture tests intercept with OTel SDK'sInMemoryLogRecordExporter;DDTracer(logRecordExporter:)lets tests plug in custom exporters.ac76e7c— coverage architecture rework: newCoverageProcessorprotocol (inEventsExporter),BackgroundCoverageProcessorimpl (inDatadogSDKTesting, owns theOperationQueue),CodeCoverageProvider(TracerSdk-shaped),CoverageRecord→CoverageDatarename + new URL-basedCoverageRecord,CodeCoverageParserdependency removed fromEventsExportertarget.d05d627— isolateCoverageExporterTestsfrom sharedCachesstate so the suite no longer flakes when other tests share a process.de43c5f— collapseDDTestSessionSpan/DDTestModuleSpan/DDTestSuiteSpanand theCITestEnvelopetest-span path into a singleTestSpanwith aSpanTypediscriminator;DDSpanis generic-only. Events now write the innertypefield (matches .NET wire format) and stripresource/type/versionfrom per-eventmeta; environment / git / CI / runtime metadata flows throughSpanMetadatainstead. Test models expose a unifiedattributes: [String: TestAttributeValue]withget(tag:)/get(metric:)in place of separatetags/metricsdicts.a10110b— gitignore.claude/agent state.8a7483a— fix integration smoke suites for the new wire shape: assert onspan.resource(top-level field) instead ofmeta[DDGenericTags.resource].96295b9— forwardDD_CIVISIBILITY_LOGS_ENABLEDandDD_CIVISIBILITY_CODE_COVERAGE_ENABLEDfrom the runner to the child test bundle via the xctestplan env-var allowlist (without this the runner'sconfig.environmentoverrides never reached the child SDK).3934f69— splitCodeCoverageout ofTestImpactAnalysisinto its ownTestHooksFeature+ factory with an independentisEnabledgate (config.codeCoverageEnabled && remote.itr.codeCoverage); coverage now runs in its ownDDTestMonitor.coverageSetupblock and no longer requires ITR/TIA. Also fixesMockBackendsettings / known-tests / test-management responses to echo the request's JSON:APIdata.idso the SDK accepts them (hardcoded"id": "1"was failing the SDK'sidMismatchcheck and cascading "backend config can't be loaded" through every feature).08ca963— exercise the no-TIA coverage path:stdoutOTelAndCoverageintegration tests leavebackend.settings.itrEnabledat its default (false) and only flip oncodeCoverage.56ac468— tighten JSON:API response validation: newAPIResponseAttributesHasType/HasId/BrokenId/NoIdprotocol family lets each endpoint declare its actual id/type shape, replacing the globalid == self.idcheck that didn't fit known-tests / test-management (broken id) or TIA (no id). Also liftsContent-Encoding: deflateoff the global default headers and onto the per-API services (spans, logs, telemetry, TIA coverage, git pack uploads), with the HTTPClient compressing the body when the header is set. Knock-on:MockBackend.route(...)now feeds the inflated body intoparseCoveragePayloadso/api/v2/citestcovworks under compression.4a0ea93—SpansExporter/LogsExporter/CoverageExportertake astorage: Directoryparameter (EventsExporter.init(config:, storage:)fans it out asspans/,logs/,coverage/subdirs) instead of constructing a hardcodedcom.datadog.civisibility/.../v1path under/Library/Caches. Production wiresstoragefromcacheManager.session(feature: "exporter")so exporter files now live under the per-session directory; tests pass a per-caseDirectory.temporary().createSubdirectory(path: UUID().uuidString).CoverageExporterTestsloses its setUp/tearDown wipe (no shared path);LibraryConfigurationServiceThrowTestsgains amakeExporter(baseURL:)helper that tracks and cleans up storages.6e43d19— makeHTTPClientTypeasync-only. Protocol now declares justsend(request:)andsendWithResponse(request:)returning the response / body with typedHTTPClient.RequestErrorthrows; the callback variants andrunAsynchelper are deleted.HTTPClientperforms each request through a sharedperform(_:_:)that wrapsURLSession.dataTaskin awithCheckedContinuation.MockHTTPClientmirrors the new signatures,MockClosureDataUploaderbridges to async viawaitForAsync, andHTTPClientTestsconverts toasync throws(@MainActorto satisfyServerMock's main-thread precondition).a8fae60— fix git upload + API response-shape corner cases.GitUploadermoves its "already-uploaded" sentinel check frominitintosendGitInfo(...)(returningtrueinstead of failing construction), and trailing-slashes the packfile temp directory path sogit pack-objectswrites into it.GitUploadApi.uploadPackFilesdrains the entirewithThrowingTaskGroup(while try await group.next() != nil {}) instead of returning after the first child task — otherwise the remaining uploads were cancelled.GitUploadApi.Commitswitches toAPIResponseAttributesNoId(the packfile commit endpoint doesn't echo an id), andArraypicks upAPIAttributesNoId/APIResponseAttributesNoIdso collection responses get the no-id rule for free. AddsAPIResponseAttributesIgnoreTypeas a sibling toHasTypefor future endpoints that don't gate on the response type, and reorders theSettingsResponseconformances toHasType, HasIdfor consistency.367765d—MultipartFormURLRequest.append(data:withName:contentType:)makes thefileName:argument optional (was requiredfilename: String); when absent, theContent-Dispositionheader omits thefilename="…"attribute. Callers that don't actually represent a file drop it:TestImpactAnalysisApi.uploadCoverage(...)no longer attaches the bogusCoverageBatch.json/DummyEvent.jsonfilenames to thecoverage/eventparts;GitUploadApi.uploadPackFile(...)drops it from the JSONpushedShapart (kept on the binarypackfilepart).1f063e8— JSON-shaped debug descriptions for API request/response types.APICommonDatarefinesCustomDebugStringConvertiblewith a default impl that renders the wrapper as{"type": "<apiType>", "id": "<id>", "attributes": <attributes>}(omittingidwhen nil andattributeswhenAPIVoidValue);APIEnvelopemirrors with{"meta": …, "data": …}. The per-service Attribute types (Settings request + response with nested EFD / TestManagement, KnownTests TestsRequest / TestsResponse with PageInfo halves, TestManagement TestsRequest / TestsResponse, TIA TestsRequest / TestsResponse with Meta, GitUpload'sCommit/CommitRequestMeta) conform toCustomDebugStringConvertiblewith hand-written JSON-ish renderings — string fields quoted, optionals printnull, and nested[String: JSONGeneric]payloads reuseJSONGeneric.object(...).debugDescription.GitUploadApi.searchCommitsswitches its log format from[meta: …, data: …]to the new envelope shape.b8f436c— dropXcode_26.1from the unit-tests and integration-tests CI matrices (toolchain image is currently broken on GitHub-hostedmacos-26runners) and move the release workflow'sXCODE_VERSIONfrom 26.1 to 26.2.Test plan
xcodebuild -scheme EventsExporter test— green locally (103 cases).xcodebuild -scheme DatadogSDKTesting test— green locally; covers refactored encoders,SpanEventsLogTests/TestSpanTests,CoverageExporterTests,CoverageBitmapTests,StdoutCaptureTests/StderrCaptureTests(now usingInMemoryLogRecordExporter),TestImpactAnalysisTests/TestImpactAnalysisSwiftTestingTests(TIA split from coverage),DDTracerTests.testLogStringAppUI.xcodebuild -scheme IntegrationTests-UnitTests build-for-testing— child target builds clean formacosx,arch=arm64.IntegrationTestssuite (XCTest + Swift Testing smoke, 20/20 green locally), including the newstdoutOTelAndCoveragecases inUnitTests+XCTestSmoke.swiftandUnitTests+SwiftTestingSmoke.swift— the latter now runs with ITR/TIA off to verify coverage works standalone.🤖 Generated with Claude Code