Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
impossible to tell which Datadog release a given CrashReporter package belonged to.
-->
<DatadogNativeVersion>3.14.0</DatadogNativeVersion>
<DatadogBindingRevision>1</DatadogBindingRevision>
<DatadogBindingRevision>2</DatadogBindingRevision>
<VersionPrefix>$(DatadogNativeVersion).$(DatadogBindingRevision)</VersionPrefix>

<Authors>s.bokatuk</Authors>
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TDelegate>()` |
| 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<T>`) 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
Expand Down
209 changes: 209 additions & 0 deletions docs/release-notes/3.14.0.2.md
Original file line number Diff line number Diff line change
@@ -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 `<dd-sdk-ios version>.<binding revision>`.** `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<string, object?> { ["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<T>`) 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
-<PackageReference Include="DatadogNet.RUM.iOS" Version="3.14.0.1" />
+<PackageReference Include="DatadogNet.RUM.iOS" Version="3.14.0.2" />
```

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.
13 changes: 13 additions & 0 deletions samples/DatadogNet.iOS.Example/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@
<Button Text="Track a unit of work as a view"
Clicked="OnTrackWork" />

<Button Text="Show the current session id"
Clicked="OnShowSessionId" />

<BoxView HeightRequest="1" Color="LightGray" />

<Label Text="Trace" FontSize="18" FontAttributes="Bold" />

<Button Text="Trace an outgoing HTTP request"
Clicked="OnTraceRequest" />

<Button Text="Record a failed span"
Clicked="OnFailedSpan" />

<BoxView HeightRequest="1" Color="LightGray" />

<Label Text="Logs" FontSize="18" FontAttributes="Bold" />
Expand Down
Loading
Loading