diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc2b682..a990743 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,13 +19,15 @@ on: required: false default: '' type: string - e2e-target-framework: + e2e-target-frameworks: description: > - Which of the packages' target frameworks the simulator smoke test runs against. Defaults - to net10, whose assets are produced by the merge step in BuildNugets.sh and so are the - ones worth proving at runtime. + Which of the packages' target frameworks the simulator smoke tests run against, as a JSON + array. Defaults to the two extremes: net8, the oldest asset set and the one nothing else + exercises, and net10, whose assets are produced by the merge step in BuildNugets.sh and so + are the only ones that could be grafted in wrong. net9 sits between them and comes out of + the same pack pass as net8, so testing it too would buy little for the runner minutes. required: false - default: net10.0-ios26.0 + default: '["net8.0-ios18.0", "net10.0-ios26.0"]' type: string env: @@ -162,10 +164,16 @@ jobs: -p:DatadogPackageVersion="${{ inputs.version }}" e2e: - name: simulator smoke test + name: simulator smoke test (${{ matrix.target-framework }}) timeout-minutes: 45 needs: pack runs-on: macos-15 + strategy: + # fail-fast off: when one target framework breaks it is worth knowing whether the other did + # too, since "net8 only" and "both" point at very different causes. + fail-fast: false + matrix: + target-framework: ${{ fromJSON(inputs.e2e-target-frameworks) }} steps: - uses: actions/checkout@v4 @@ -196,13 +204,13 @@ jobs: path: artifacts - name: Run smoke tests on simulator - run: ./.github/scripts/run-simulator-tests.sh "${{ inputs.version }}" "${{ inputs.e2e-target-framework }}" + run: ./.github/scripts/run-simulator-tests.sh "${{ inputs.version }}" "${{ matrix.target-framework }}" - name: Upload simulator logs if: always() uses: actions/upload-artifact@v4 with: - name: simulator-test-logs + name: simulator-test-logs-${{ matrix.target-framework }} path: simulator-tests.log if-no-files-found: ignore retention-days: 7 diff --git a/Directory.Build.props b/Directory.Build.props index 4b55054..7b1072c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,7 +16,7 @@ impossible to tell which Datadog release a given CrashReporter package belonged to. --> 3.14.0 - 1 + 2 $(DatadogNativeVersion).$(DatadogBindingRevision) s.bokatuk diff --git a/README.md b/README.md index 59f51ad..d6448ce 100644 --- a/README.md +++ b/README.md @@ -305,11 +305,69 @@ there; nothing is hidden or renamed. | `DDDatadog.SetUserInfoWithUserId(id, null, null, empty)` | `DDDatadog.SetUserInfo(id)` | | `DDTrackingConsent.Granted()` (a factory, not an enum) | `DDDatadog.SetTrackingConsent(TrackingConsent.Granted)` | | `DDURLSessionInstrumentation.EnableWithConfiguration(...)` with a raw `Class` handle | `DDURLSessionInstrumentation.Enable()` | +| a one-element dictionary round-trip to convert one value | `DatadogAttributes.ToNSObject(value, key)` | +| `monitor.AddAttributeForKey(k, nsObject)` | `monitor.AddAttribute(k, value)` / `AddViewAttribute` / `AddFeatureFlagEvaluation` | +| `monitor.CurrentSessionIDWithCompletion(block)` | `await monitor.GetCurrentSessionIdAsync()` | +| construct a writer, pass it as the carrier, read headers back off it — once per format | `span.InjectHeaders(tracer)` | +| no way at all to read a span's ids | `span.GetTraceId(tracer)` / `span.GetSpanId(tracer)` | +| `span.SetErrorWithKind(kind, message, stack)` from an exception you must decompose | `span.SetError(exception)` | Attribute values may be strings, any numeric type, `bool`, `DateTime`, `DateTimeOffset`, `Guid`, enums, `NSObject`s, arrays, and nested dictionaries. Anything else throws `ArgumentException` rather than being silently dropped. `DatadogAttributes` lives in `DatadogCore`. +### Trace ids + +`GetTraceId` returns **32 lowercase hexadecimal characters** and `GetSpanId` returns **decimal**. +The asymmetry is not a choice — it is Datadog's wire format, and matching it is what makes a RUM +resource correlate with its APM trace. dd-sdk-android's own `DatadogInterceptor` writes +`_dd.trace_id` as `DatadogTraceId.toHexString()` and `_dd.span_id` as `String.valueOf(long)`. + +Reading the ids takes work because `OTSpanContext` exposes none: they are recovered by injecting +into a Datadog-format writer, and the trace id arrives in two pieces — the decimal low 64 bits in +`x-datadog-trace-id`, and the high 64 as `_dd.p.tid` inside `x-datadog-tags`. Using only the former +yields a decimal string naming half of a different-looking id, which is a mistake that reached a +release of a consumer of this package before it was caught. + +--- + +## API coverage + +Measured by diffing each framework's generated `-Swift.h` against `ApiDefinitions.cs`, not estimated. + +| Framework | Objective-C types | Bound | +| --- | ---: | ---: | +| `DatadogRUM` | 377 | 377 | +| `DatadogLogs` | 16 | 16 | +| `DatadogCore` | 12 | 12 | +| `DatadogTrace` | 12 | 12 | +| `DatadogSessionReplay` | 4 | 4 | +| `DatadogInternal` | 2 | 2 | +| `DatadogCrashReporting` | 1 | 1 | +| `DatadogWebViewTracking` | 1 | 1 | + +Member coverage is the same story: of 61 selectors and properties on `DatadogCore`, 59 are exported, +and the two that are not are `init` and `new`, which `[DisableDefaultCtor]` removes on purpose. + +**What is missing is missing upstream.** Three parts of dd-sdk-ios have no Objective-C projection at +all, so there is nothing for a binding to bind: + +| | Swift types | ObjC types | Consequence | +| --- | ---: | ---: | --- | +| `DatadogFlags` | 15 | **0** | Feature Flags are unreachable from C#. The API leans on generics (`FlagDetails`) and enums with associated values (`AnyValue`), neither of which Swift projects into Objective-C. | +| `DatadogProfiling` | 2 | **0** | Profiling is unreachable. | +| `OpenTelemetryApi` | — | no `-Swift.h` at all | A pure-Swift module. `DatadogNet.OpenTelemetryApi.iOS` can only ever be a link-time dependency of `DatadogNet.Trace.iOS`. | + +`DatadogTrace` is additionally 24 public Swift types projected down to 12, and the casualty is +`OTelTracerProvider` — so OpenTelemetry tracing is unreachable even though OpenTracing is not. + +`DatadogNet.Flags.iOS` and `DatadogNet.Profiling.iOS` therefore ship the frameworks and expose no +callable API. They exist so the SDK is mirrored and so a future projection needs no new package. + +There is a way around this — a hand-written Swift `@objc` wrapper, which we can compile ourselves — +and a working prototype for Flags lives in [`shims/DatadogFlagsObjc/`](shims/DatadogFlagsObjc/). +Nothing ships yet. + --- ## Migrating from 2.x diff --git a/docs/release-notes/3.14.0.2.md b/docs/release-notes/3.14.0.2.md new file mode 100644 index 0000000..e757964 --- /dev/null +++ b/docs/release-notes/3.14.0.2.md @@ -0,0 +1,209 @@ +## What's changed + +Binding-only release. The native SDK is unchanged — still +[dd-sdk-ios 3.14.0](https://github.com/DataDog/dd-sdk-ios/releases/tag/3.14.0) — and so are the +package IDs, namespaces and every existing signature. Upgrading is a version bump. + +The headline is **`OTSpan.GetTraceId`**, and it is worth stating why a getter warrants one: reading a +span's trace id from C# was not merely inconvenient, it was a trap that a consumer of these packages +fell into and shipped. Details below. + +> **Package versions are `.`.** `3.14.0.2` is dd-sdk-ios +> `3.14.0`, binding revision `2`. The fourth component belongs to this repository and advances when +> the bindings or packaging change while the native binaries stay put. + +## Added + +### Span identity — `OTSpan.GetTraceId` / `GetSpanId` + +`OTSpanContext` declares nothing but `forEachBaggageItem`. There is no `traceID` or `spanID` on the +protocol or on any bound type, and 3.x did not change that — so the ids can only be recovered by +injecting into a Datadog-format headers writer and reading what comes back. + +That much is merely awkward. The trap is what you find when you look: + +``` +x-datadog-trace-id: 6096355397431041644 ← decimal, and only the LOW 64 bits +x-datadog-tags: _dd.p.tid=6a61e4ff00000000 ← the HIGH 64, somewhere else entirely +``` + +A caller who reads the obvious header gets a decimal string naming half of an id that, rendered the +way Datadog renders it, looks nothing like it. `DatadogNet 3.14.0.1` did exactly that and shipped +with iOS reporting `6096355397431041644` where Android reported +`6a61e4ff000000002e430f579ece9a6c` for the same span — which quietly broke RUM-resource-to-APM-trace +correlation on iOS, because `_dd.trace_id` is the only thing that links them. + +```csharp +var traceId = span.GetTraceId (DDTracer.Shared ()); // 32 lowercase hex, always +var spanId = span.GetSpanId (DDTracer.Shared ()); // decimal +``` + +The rendering matches dd-sdk-android's own `DatadogInterceptor`, which is the reference +implementation of this correlation: `DatadogTraceId.toHexString()` for the trace id and +`String.valueOf(long)` for the span id. The asymmetry is Datadog's wire format rather than a choice. +`toHexString()` is `toHexStringPadded(…, 32)` on both the 128-bit and 64-bit implementations, so the +result is always 32 characters — with `_dd.p.tid` absent, the high half is genuinely zero and is +padded, not omitted. + +### Header injection — `OTSpan.InjectHeaders` + +```csharp +foreach (var header in span.InjectHeaders (DDTracer.Shared ())) + request.Headers.TryAddWithoutValidation (header.Key, header.Value); +``` + +The bound API needs a dance that is not visible in the signatures: construct a writer, hand it to +`Inject` **as though it were the carrier**, then read the headers back off the writer. There is also +one writer type per format — unlike Android, where the formats are a property of the tracer and one +call writes all of them — so several formats means several round trips. + +The format selector is a new `OTHeaderFormats` flags enum belonging to this repository, because the +bound `DDTracingHeaderType` is an Objective-C class of static singletons rather than an enum (Swift's +enum did not survive the projection) and so cannot be combined or compared. Defaults to Datadog plus +W3C trace context. + +### Span outcome — `OTSpan.SetError` / `Log` + +```csharp +span.SetError (exception); +span.Log (new Dictionary { ["event"] = "retry", ["attempt"] = 2 }); +``` + +`SetErrorWithKind` takes kind, message and stack separately, and a caller who passes only the message +gets a span marked as an error with nothing in the APM error panel to act on. + +### RUM — single-value attributes and the session id + +```csharp +monitor.AddAttribute ("cart.id", cartId); +monitor.AddViewAttribute ("cart.items", 3); +monitor.AddFeatureFlagEvaluation ("new-checkout", true); + +var sessionId = await monitor.GetCurrentSessionIdAsync (); +``` + +`DatadogAttributes.ToNSObject (value, key)` is now **public**, which is what these are built on. +`AddAttributeForKey`, `AddViewAttributeForKey`, `AddFeatureFlagEvaluationWithName` and +`DDLogger.AddAttributeForKey` all take a bare `NSObject` while `DatadogAttributes` only exposed the +dictionary form — so a caller either hand-wrapped the value, which is exactly what +`DatadogAttributes` exists to avoid, or round-tripped a one-element dictionary. + +`GetCurrentSessionIdAsync` wraps `CurrentSessionIDWithCompletion`, which answers through a block on +the SDK's own queue. + +### Logs + +`DDLogger.AddAttribute (key, value)`, taking a plain value rather than an `NSObject`. + +## Documentation + +The README gains an **API coverage** section, measured by diffing each framework's generated +`-Swift.h` against `ApiDefinitions.cs` rather than asserted: + +| Framework | ObjC types | Bound | +| --- | ---: | ---: | +| `DatadogRUM` | 377 | 377 | +| `DatadogLogs` | 16 | 16 | +| `DatadogCore` | 12 | 12 | +| `DatadogTrace` | 12 | 12 | +| `DatadogSessionReplay` | 4 | 4 | +| `DatadogInternal` | 2 | 2 | +| `DatadogCrashReporting` | 1 | 1 | +| `DatadogWebViewTracking` | 1 | 1 | + +Member coverage is the same story — of 61 selectors and properties on `DatadogCore`, 59 are exported, +and the two that are not are `init` and `new`, removed on purpose by `[DisableDefaultCtor]`. + +**What is missing is missing upstream**, and the section now says so precisely rather than vaguely: + +| | Swift types | ObjC types | +| --- | ---: | ---: | +| `DatadogFlags` | 15 | **0** | +| `DatadogProfiling` | 2 | **0** | +| `OpenTelemetryApi` | — | no `-Swift.h` at all | + +Feature Flags leans on generics (`FlagDetails`) and enums with associated values (`AnyValue`), +neither of which Swift projects into Objective-C. `DatadogTrace` is additionally 24 public Swift +types projected down to 12, and the casualty is `OTelTracerProvider` — so OpenTelemetry tracing is +unreachable even though OpenTracing is not. + +That is also why `DatadogNet.Flags.iOS` and `DatadogNet.Profiling.iOS` ship frameworks with no +callable API. + +## New: a prototype for reaching the Swift-only frameworks + +[`shims/DatadogFlagsObjc/`](../../shims/DatadogFlagsObjc/) is a hand-written Swift `@objc` wrapper +around `DatadogFlags`. **It compiles against the real 3.14.0 framework with zero warnings** and emits +exactly the header Objective Sharpie consumes, so the rest of the path to a NuGet is the ordinary one +this repository already runs. + +Two things make Flags tractable: Datadog already flattened the generics at the convenience layer +(`getBooleanValue`, `getStringValue`, `getIntegerValue`, `getDoubleValue`, `getObjectValue` and the +matching `…Details`), and `FlagsClient.shared(named:in:)` returns a real object to wrap. +`AnyValue` maps onto the Foundation object graph, which is what a C# caller wants anyway. + +**Nothing ships.** It is not built, packaged, bound or referenced — see its README for what remains. +The standing cost is worth naming: the wrapper is ours, so when Datadog changes +`FlagsClientProtocol` it stops compiling, at build time rather than someone's runtime. + +## Tests + +**127 package-layout tests pass**, and the on-simulator suite grows from **17 checks to 21**, all +running against the packed packages. + +Until now this suite enabled Trace and stopped there — `EnablesTrace` constructed the three header +writers and never started a span — so the tracing path was configured and never driven. That is the +gap the trace-id defect slipped through, and it is now closed: + +- **A span is started, tagged, errored, logged and finished.** Its ids are asserted by *shape* + rather than for non-emptiness: 32 lowercase hex characters and not all zeros for the trace, decimal + for the span. "Not empty" is what a caller checked before, and it is why the wrong rendering + shipped. +- **Headers are injected in all three formats** and cross-checked two ways: the id must equal the + `traceparent` W3C value across all 128 bits — derived independently of `GetTraceId`, so a second + opinion rather than a restatement — and must end with the low 64 bits the Datadog header carries in + decimal. Selecting no formats must produce nothing rather than throwing. +- **The single-value attribute overloads** are driven on RUM, view attributes, feature flags and a + logger, and `ToNSObject` is asserted to map `null` to `NSNull` — because an explicitly empty + attribute and an unset one are different things in a RUM event. +- **The session id** is read through `GetCurrentSessionIdAsync` and asserted non-null; a null means + the completion block never fired, which is the failure the `Task` wrapper exists to surface. + +The harness gained `async` support to do the last of those, matching the shape the runner already +had on the façade side. + +**The e2e now runs on two bands.** It ran against `net10.0-ios26.0` only; it is now a matrix over +**net8 and net10**, matching what the DatadogNet façade does. net8 is the oldest asset set and +nothing else exercised it; net10's assets come out of the merge step in `BuildNugets.sh` and so are +the only ones that could be grafted in wrong. net9 sits between them and comes out of the same pack +pass as net8, so testing it too would buy little for the runner minutes. + +Unlike the Android side, this needed no project changes — `run-simulator-tests.sh` already picked +the SDK band from the target framework, and the iOS packages all ship net8 assets. 21/21 on both +legs; the net8 leg takes appreciably longer, because a Release build for the simulator AOT-compiles. + +One thing the new checks turned up: `Info(message, attributes)` is generated without `[NullAllowed]` +on the dictionary, so passing `null` throws rather than reaching the SDK. Objective-C accepts `nil` +there. The single-argument overload and the ergonomic `Log` both handle it, so this is a papercut +rather than a gap — but it is real, and worth knowing before you hit it. + +## Sample + +`samples/DatadogNet.iOS.Example` gains a **Trace** section, because it enabled Trace and never +demonstrated it. Two buttons: one traces an outgoing `HttpRequestMessage` end to end — start a span, +inject the headers a receiving service continues the trace from, report the status, finish — and one +records a failed span with `SetError` and `Log`. RUM gains a button showing the current session id. + +## Upgrading from 3.14.0.1 + +```diff +- ++ +``` + +Nothing is removed or renamed, and the native xcframeworks are byte-for-byte the same build. All +packages move together, as they depend on each other at an exact version. + +**If you read span ids yourself**, replace whatever you were doing with `GetTraceId`/`GetSpanId` and +check what you get: a decimal trace id, or one shorter than 32 characters, means the RUM-to-APM link +was not working. diff --git a/samples/DatadogNet.iOS.Example/MainPage.xaml b/samples/DatadogNet.iOS.Example/MainPage.xaml index f1a0544..c93cb1c 100644 --- a/samples/DatadogNet.iOS.Example/MainPage.xaml +++ b/samples/DatadogNet.iOS.Example/MainPage.xaml @@ -28,6 +28,19 @@