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
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>2.17.0</DatadogNativeVersion>
<DatadogBindingRevision>1</DatadogBindingRevision>
<DatadogBindingRevision>2</DatadogBindingRevision>
<VersionPrefix>$(DatadogNativeVersion).$(DatadogBindingRevision)</VersionPrefix>

<Authors>s.bokatuk</Authors>
Expand Down
75 changes: 66 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ DDRUM.EnableWith(new DDRUMConfiguration(applicationID: "<RUM_APPLICATION_ID>"));
- [Installing](#installing)
- [Usage](#usage)
- [Convenience API](#convenience-api)
- [API coverage](#api-coverage)
- [Migrating from `DatadogCore.iOS` / `DatadogObjc.iOS`](#migrating-from-datadogcoreios--datadogobjcios)
- [How this repository works](#how-this-repository-works)
- [Building locally](#building-locally)
Expand All @@ -44,8 +45,8 @@ DDRUM.EnableWith(new DDRUMConfiguration(applicationID: "<RUM_APPLICATION_ID>"));
## Packages

Eleven packages, one per native framework in the Datadog release. Versions are
`<dd-sdk-ios version>.<binding revision>` — `2.17.0.1` is dd-sdk-ios **2.17.0**, binding revision
**1**. The fourth component belongs to this repository and advances when the bindings or packaging
`<dd-sdk-ios version>.<binding revision>` — `2.17.0.2` is dd-sdk-ios **2.17.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.

| Package | Wraps | Depends on | What it is for |
Expand All @@ -65,7 +66,7 @@ change while the native binaries stay put.
Most apps need one line:

```xml
<PackageReference Include="DatadogNet.Objc.iOS" Version="2.17.0.1" />
<PackageReference Include="DatadogNet.Objc.iOS" Version="2.17.0.2" />
```

Add `DatadogNet.CrashReporting.iOS` for crash reporting and `DatadogNet.WebViewTracking.iOS` for
Expand All @@ -87,7 +88,7 @@ OS-provided Swift runtime, which is only ABI-stable from 12.2.

```xml
<ItemGroup>
<PackageReference Include="DatadogNet.Objc.iOS" Version="2.17.0.1" />
<PackageReference Include="DatadogNet.Objc.iOS" Version="2.17.0.2" />
</ItemGroup>
```

Expand All @@ -100,7 +101,7 @@ Windows head does not try to restore them:

```xml
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
<PackageReference Include="DatadogNet.Objc.iOS" Version="2.17.0.1" />
<PackageReference Include="DatadogNet.Objc.iOS" Version="2.17.0.2" />
</ItemGroup>
```

Expand Down Expand Up @@ -270,6 +271,7 @@ still there; nothing is hidden or renamed.
| six methods per level, plus an `NSError` you do not have | `logger.Log(level, message, exception?, attributes?)` |
| `DDDatadog.SetUserInfoWithId(id, null, null, empty)` | `DDDatadog.SetUserInfo(id)` |
| `DDTrackingConsent.Granted` (a class, not an enum) | `DDDatadog.SetTrackingConsent(TrackingConsent.Granted)` |
| `DDURLSessionInstrumentation.EnableWithConfiguration(...)` with a `Class` as a raw `IntPtr` | `DDURLSessionInstrumentation.Enable<MyDelegate>()` |

The view scope is the one worth adopting everywhere: the raw API is a `StartViewWithKey` /
`StopViewWithKey` pair matched by string key, and a view left open by an early return or an
Expand All @@ -279,6 +281,61 @@ Attribute values may be strings, any numeric type, `bool`, `DateTime`, `DateTime
enums, `NSObject`s, arrays, and nested dictionaries. Anything else throws `ArgumentException`
rather than being silently dropped.

### Redacting events before upload

Both RUM and Logs let you rewrite or drop events on the device, before anything is uploaded. This
is the supported way to keep PII out of Datadog.

```csharp
var logs = new DDLogsConfiguration(customEndpoint: null);
logs.SetEventMapper(logEvent =>
{
logEvent.Message = Redact(logEvent.Message);
return logEvent; // or return null to drop the event entirely
});
DDLogs.EnableWith(logs);
```

RUM has five equivalents — `SetViewEventMapper`, `SetActionEventMapper`, `SetResourceEventMapper`,
`SetErrorEventMapper` and `SetLongTaskEventMapper` — registered on `DDRUMConfiguration` before
`DDRUM.EnableWith`. Mappers can only be set at configuration time.

### Network instrumentation

```csharp
DDURLSessionInstrumentation.Enable<MySessionDelegate>();
```

`MySessionDelegate` must be an `NSObject` implementing `INSUrlSessionDataDelegate` and carrying a
`[Register]` attribute. The raw binding takes the delegate class as an `IntPtr`, which accepts
`IntPtr.Zero` happily and then instruments nothing; the generic form resolves the Objective-C class
and throws if it does not exist.

---

## API coverage

The bindings cover **346 of the 347 public Objective-C types** Datadog declares for 2.17.0, and
every documented feature is reachable from C#. Verified by diffing the bound selectors against the
`-Swift.h` header each xcframework ships, not against the documentation.

The one unbound type is **`DDUIPressRUMActionsPredicate`**, which reports Siri Remote presses. The
protocol is declared in the iOS header, but nothing in the iOS slice accepts it — the property that
consumes it exists only on tvOS — so it is unreachable from a `net*-ios` app.

Three groups of *members* are also deliberately not bound:

- **`DDRUMLongTaskEventLongTaskScripts` and `DDRUMVitalEventVital.details`** — reachable only from
inside a long-task or vital event mapper, and describe JavaScript long tasks, which a native iOS
app does not produce.
- **Four `DDTelemetryConfigurationEventTelemetryConfiguration` properties** covering Session Replay
privacy levels. These are read-only fields on a *telemetry* event describing what the SDK
reported about itself; they are not configuration knobs. The real knob,
`DDSessionReplayConfiguration.DefaultPrivacyLevel`, is bound.
- **Three `URLSession` overloads on `DatadogURLSessionDelegate`** — see
[the note in that binding](src/DatadogNet.Internal.iOS/ApiDefinitions.cs); binding them crashes
every consuming app at startup, and they remain callable under their inherited names.

---

## Migrating from `DatadogCore.iOS` / `DatadogObjc.iOS`
Expand All @@ -288,7 +345,7 @@ edit — no `using` directive and no call site changes.

```diff
-<PackageReference Include="DatadogObjc.iOS" Version="2.17.0.1" />
+<PackageReference Include="DatadogNet.Objc.iOS" Version="2.17.0.1" />
+<PackageReference Include="DatadogNet.Objc.iOS" Version="2.17.0.2" />
```

| Old | New |
Expand Down Expand Up @@ -317,7 +374,7 @@ selector is already registered on the member 'DidFinishCollectingMetrics'.
```

**`CrashReporter` is versioned with everything else.** It was `1.11.2.1`, tracking PLCrashReporter
upstream; it is now `2.17.0.1` like the rest, because it ships inside the same Datadog release and
upstream; it is now `2.17.0.2` like the rest, because it ships inside the same Datadog release and
the old numbering made it impossible to tell which Datadog build a given package belonged to.

**`net7.0-ios` is gone, `net9`/`net10` are new.** The old packages targeted `net7.0-ios16.1` and
Expand Down Expand Up @@ -390,7 +447,7 @@ dotnet test tests/DatadogNet.iOS.PackageTests
Run the on-simulator smoke tests against the packed packages:

```bash
./.github/scripts/run-simulator-tests.sh 2.17.0.1 net9.0-ios18.0
./.github/scripts/run-simulator-tests.sh 2.17.0.2 net9.0-ios18.0
```

Build and run the sample:
Expand Down Expand Up @@ -436,7 +493,7 @@ dropping a package — update the `FRAMEWORKS` list, add or remove the binding p

## Releasing

Tag it. `v2.17.0.1` builds, tests, publishes all eleven packages to nuget.org via trusted
Tag it. `v2.17.0.2` builds, tests, publishes all eleven packages to nuget.org via trusted
publishing, and creates a GitHub release. The tag drives which native SDK is bound, so an older
line can be released by tagging it.

Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/2.17.0.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ there is no long-lived API key.

## Changed

**Packages are about 74 MB in total, down from what shipping every slice would cost.** The upstream
**The eleven packages come to about 80 MB in total, against roughly a gigabyte if every slice shipped.** The upstream
archive carries tvOS slices for every framework, macCatalyst/macOS/watchOS/visionOS for two of them,
and a full set of dSYMs — none of it reachable from a `net*-ios` binding, and all of it embedded
once per target framework. `FetchXcFrameworks.sh` strips to the two iOS slices and rewrites each
Expand Down
106 changes: 106 additions & 0 deletions docs/release-notes/2.17.0.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
## What's changed

Binding-only release. The native SDK is unchanged — still **dd-sdk-ios 2.17.0** — and so are the
package IDs, namespaces and every existing signature. Upgrading is a version bump.

This release closes two gaps found by diffing the bound selectors against the `-Swift.h` headers
each xcframework actually ships, rather than against Datadog's documentation. Both were places
where the generated binding silently under-reported what the native SDK can do.

> **Package versions are `<dd-sdk-ios version>.<binding revision>`.** `2.17.0.2` is dd-sdk-ios
> `2.17.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

**Log event mapping — `DDLogsConfiguration.SetEventMapper`, and the whole `DDLogEvent` model.**

Objective Sharpie had omitted all of it: ten `DDLogEvent*` classes, four enums, and
`setEventMapper:` itself. The effect was that RUM had all five of its event mappers while Logs had
none — so there was no supported way to redact or drop a log before it left the device, which is
the mechanism behind most "keep PII out of Datadog" requirements. Transcribed by hand from the
shipped header.

```csharp
var logs = new DDLogsConfiguration(customEndpoint: null);
logs.SetEventMapper(logEvent =>
{
logEvent.Message = Redact(logEvent.Message);
return logEvent; // or return null to drop the event entirely
});
DDLogs.EnableWith(logs);
```

The mapper receives a fully populated `DDLogEvent`: message, status, error (kind, message, stack,
fingerprint, binary images), logger and service names, thread, app version and build, OS, device,
user info, network connection info, mobile carrier info, attributes and tags. `Message`, `Tags`,
the error fields and the two attribute dictionaries are writable; the rest are read-only, exactly
as the native header declares them.

New types: `DDLogEvent`, `DDLogEventAttributes`, `DDLogEventBinaryImage`, `DDLogEventCarrierInfo`,
`DDLogEventDd`, `DDLogEventDeviceInfo`, `DDLogEventError`, `DDLogEventNetworkConnectionInfo`,
`DDLogEventOperatingSystem`, `DDLogEventUserInfo`, and the enums `DDLogEventStatus`,
`DDLogEventReachability`, `DDLogEventRadioAccessTechnology` and `DDLogEventInterface`.

**Type-based `DDURLSessionInstrumentation`.**

`DDURLSessionInstrumentation` is what Datadog documents for automatic RUM resource collection and
distributed tracing, but it takes an Objective-C `Class`, which reaches C# as a bare `IntPtr`.
Nothing in the generated signature suggests you must pass `Class.GetHandle(typeof(T))`, and the
native call accepts `IntPtr.Zero` without complaint — then instruments nothing, silently.

```csharp
DDURLSessionInstrumentation.Enable<MySessionDelegate>();
// ...
DDURLSessionInstrumentation.Disable<MySessionDelegate>();
```

`Enable<T>()`, `Disable<T>()`, their `Type`-taking overloads, and
`DDURLSessionInstrumentationConfiguration.Create<T>()` resolve the Objective-C class from a managed
type and throw `ArgumentException` if it is not registered. `DelegateType` exposes the same value
as a `Type` rather than an `IntPtr`. The delegate must be an `NSObject` implementing
`INSUrlSessionDataDelegate` and carrying a `[Register]` attribute.

The `IntPtr` members are untouched, so existing code keeps working.

## Tests

The on-simulator suite grows from 14 checks to 17, all running against the packed packages on both
`net9.0-ios18.0` and `net10.0-ios26.0`:

- **RUM event mappers are invoked.** The mappers are registered on the real configuration before
`DDRUM.EnableWith` and asserted to have fired after events are produced. This is the only part of
the binding that passes a managed delegate into Swift and gets a Swift object back, so if block
marshalling were wrong anywhere it would be wrong here — and the failure would surface as a crash
inside the SDK's event-writing path, far from the call that registered the mapper.
- **A Logs event mapper redacts a message**, and the rewritten event survives back into Swift.
- **A URLSession delegate is instrumented by type**, and a type the Objective-C runtime has never
heard of is rejected rather than silently instrumenting nothing.

## Documentation

The README gains an **API coverage** section stating what is bound and what is not, measured rather
than asserted: **346 of the 347 public Objective-C types** Datadog declares for 2.17.0.

The one unbound type is `DDUIPressRUMActionsPredicate`, which reports Siri Remote presses. The
protocol is declared in the iOS header, but nothing in the iOS slice accepts it — the property that
consumes it exists only on tvOS — so it is unreachable from a `net*-ios` app.

Three groups of members are also deliberately unbound, and the section says why: the JavaScript
long-task and vital detail models reachable only from inside a mapper, four read-only *telemetry*
fields that look like Session Replay privacy knobs but are not (the real knob,
`DDSessionReplayConfiguration.DefaultPrivacyLevel`, was already bound), and the three
`DatadogURLSessionDelegate` overloads whose binding crashed every consuming app at startup in the
old packages.

The Convenience API table and the Usage section now cover event mapping and network instrumentation.

## Upgrading from 2.17.0.1

```diff
-<PackageReference Include="DatadogNet.Objc.iOS" Version="2.17.0.1" />
+<PackageReference Include="DatadogNet.Objc.iOS" Version="2.17.0.2" />
```

Nothing is removed or renamed, and the native binaries are byte-for-byte the same build. All eleven
packages move together, as they depend on each other at an exact version.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Nullable annotations are enabled per file rather than for the project: the generated
// binding sources are not written against a nullable context, and switching the whole
// project over would bury real warnings here under hundreds of generated ones.
#nullable enable

using System;
using Foundation;
using ObjCRuntime;

namespace DatadogObjc
{
public partial class DDURLSessionInstrumentationConfiguration
{
/// <summary>
/// Creates a configuration that instruments the given <c>NSUrlSessionDataDelegate</c> type.
/// </summary>
/// <param name="delegateType">
/// The delegate class to instrument. Must be an <see cref="NSObject"/> subclass that
/// implements <see cref="INSUrlSessionDataDelegate"/> and is registered with the
/// Objective-C runtime.
/// </param>
/// <remarks>
/// The native initializer takes an Objective-C <c>Class</c>, which reaches C# as a bare
/// <see cref="IntPtr"/>. Passing the right value means knowing to call
/// <c>Class.GetHandle (typeof (T))</c> - which nothing in the generated signature hints at,
/// and which silently yields <see cref="IntPtr.Zero"/> for a type the runtime does not know,
/// leaving instrumentation quietly disabled rather than failing.
/// </remarks>
/// <exception cref="ArgumentException">
/// <paramref name="delegateType"/> is not a registered Objective-C class.
/// </exception>
public static DDURLSessionInstrumentationConfiguration Create (Type delegateType) =>
new DDURLSessionInstrumentationConfiguration (HandleFor (delegateType, nameof (delegateType)));

/// <inheritdoc cref="Create(Type)"/>
/// <typeparam name="TDelegate">The delegate class to instrument.</typeparam>
public static DDURLSessionInstrumentationConfiguration Create<TDelegate> ()
where TDelegate : NSObject, INSUrlSessionDataDelegate =>
Create (typeof (TDelegate));

/// <summary>The delegate class being instrumented.</summary>
/// <remarks>
/// The typed view of <see cref="DelegateClass"/>, which is an <see cref="IntPtr"/> holding
/// an Objective-C <c>Class</c>. Returns <see langword="null"/> if the class does not map
/// back to a managed type.
/// </remarks>
public Type? DelegateType {
get => DelegateClass == IntPtr.Zero ? null : Class.Lookup (new Class (DelegateClass));
set => DelegateClass = value is null ? IntPtr.Zero : HandleFor (value, nameof (value));
}

internal static IntPtr HandleFor (Type delegateType, string parameterName)
{
if (delegateType is null)
throw new ArgumentNullException (parameterName);

var handle = Class.GetHandle (delegateType);
if (handle == IntPtr.Zero) {
throw new ArgumentException (
$"'{delegateType.FullName}' is not a registered Objective-C class. It must derive from " +
"NSObject, implement INSUrlSessionDataDelegate, and carry a [Register] attribute.",
parameterName);
}

return handle;
}
}

public partial class DDURLSessionInstrumentation
{
/// <summary>Starts instrumenting the given delegate type for RUM resource and trace collection.</summary>
/// <remarks>
/// Shorthand for
/// <c>EnableWithConfiguration (DDURLSessionInstrumentationConfiguration.Create (delegateType))</c>.
/// Enable this once, before creating the <c>NSUrlSession</c> that uses the delegate.
/// </remarks>
public static void Enable (Type delegateType) =>
EnableWithConfiguration (DDURLSessionInstrumentationConfiguration.Create (delegateType));

/// <inheritdoc cref="Enable(Type)"/>
/// <typeparam name="TDelegate">The delegate class to instrument.</typeparam>
public static void Enable<TDelegate> ()
where TDelegate : NSObject, INSUrlSessionDataDelegate =>
Enable (typeof (TDelegate));

/// <summary>Stops instrumenting the given delegate type.</summary>
public static void Disable (Type delegateType) =>
DisableWithDelegateClass (
DDURLSessionInstrumentationConfiguration.HandleFor (delegateType, nameof (delegateType)));

/// <inheritdoc cref="Disable(Type)"/>
/// <typeparam name="TDelegate">The delegate class to stop instrumenting.</typeparam>
public static void Disable<TDelegate> ()
where TDelegate : NSObject, INSUrlSessionDataDelegate =>
Disable (typeof (TDelegate));
}
}
Loading
Loading