diff --git a/Directory.Build.props b/Directory.Build.props
index 161e2a8..5c011fb 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.
-->
2.17.0
- 1
+ 2
$(DatadogNativeVersion).$(DatadogBindingRevision)
s.bokatuk
diff --git a/README.md b/README.md
index 8c207a3..bb16fda 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,7 @@ DDRUM.EnableWith(new DDRUMConfiguration(applicationID: ""));
- [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)
@@ -44,8 +45,8 @@ DDRUM.EnableWith(new DDRUMConfiguration(applicationID: ""));
## Packages
Eleven packages, one per native framework in the Datadog release. Versions are
-`.` — `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
+`.` — `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 |
@@ -65,7 +66,7 @@ change while the native binaries stay put.
Most apps need one line:
```xml
-
+
```
Add `DatadogNet.CrashReporting.iOS` for crash reporting and `DatadogNet.WebViewTracking.iOS` for
@@ -87,7 +88,7 @@ OS-provided Swift runtime, which is only ABI-stable from 12.2.
```xml
-
+
```
@@ -100,7 +101,7 @@ Windows head does not try to restore them:
```xml
-
+
```
@@ -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()` |
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
@@ -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` 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`
@@ -288,7 +345,7 @@ edit — no `using` directive and no call site changes.
```diff
-
-+
++
```
| Old | New |
@@ -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
@@ -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:
@@ -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.
diff --git a/docs/release-notes/2.17.0.1.md b/docs/release-notes/2.17.0.1.md
index 6099250..b34cebe 100644
--- a/docs/release-notes/2.17.0.1.md
+++ b/docs/release-notes/2.17.0.1.md
@@ -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
diff --git a/docs/release-notes/2.17.0.2.md b/docs/release-notes/2.17.0.2.md
new file mode 100644
index 0000000..fa4decd
--- /dev/null
+++ b/docs/release-notes/2.17.0.2.md
@@ -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 `.`.** `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();
+// ...
+DDURLSessionInstrumentation.Disable();
+```
+
+`Enable()`, `Disable()`, their `Type`-taking overloads, and
+`DDURLSessionInstrumentationConfiguration.Create()` 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
+-
++
+```
+
+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.
diff --git a/src/DatadogNet.Objc.iOS/Additions/DDURLSessionInstrumentation.Ergonomics.cs b/src/DatadogNet.Objc.iOS/Additions/DDURLSessionInstrumentation.Ergonomics.cs
new file mode 100644
index 0000000..ba7edf4
--- /dev/null
+++ b/src/DatadogNet.Objc.iOS/Additions/DDURLSessionInstrumentation.Ergonomics.cs
@@ -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
+ {
+ ///
+ /// Creates a configuration that instruments the given NSUrlSessionDataDelegate type.
+ ///
+ ///
+ /// The delegate class to instrument. Must be an subclass that
+ /// implements and is registered with the
+ /// Objective-C runtime.
+ ///
+ ///
+ /// The native initializer takes an Objective-C Class, which reaches C# as a bare
+ /// . Passing the right value means knowing to call
+ /// Class.GetHandle (typeof (T)) - which nothing in the generated signature hints at,
+ /// and which silently yields for a type the runtime does not know,
+ /// leaving instrumentation quietly disabled rather than failing.
+ ///
+ ///
+ /// is not a registered Objective-C class.
+ ///
+ public static DDURLSessionInstrumentationConfiguration Create (Type delegateType) =>
+ new DDURLSessionInstrumentationConfiguration (HandleFor (delegateType, nameof (delegateType)));
+
+ ///
+ /// The delegate class to instrument.
+ public static DDURLSessionInstrumentationConfiguration Create ()
+ where TDelegate : NSObject, INSUrlSessionDataDelegate =>
+ Create (typeof (TDelegate));
+
+ /// The delegate class being instrumented.
+ ///
+ /// The typed view of , which is an holding
+ /// an Objective-C Class. Returns if the class does not map
+ /// back to a managed type.
+ ///
+ 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
+ {
+ /// Starts instrumenting the given delegate type for RUM resource and trace collection.
+ ///
+ /// Shorthand for
+ /// EnableWithConfiguration (DDURLSessionInstrumentationConfiguration.Create (delegateType)).
+ /// Enable this once, before creating the NSUrlSession that uses the delegate.
+ ///
+ public static void Enable (Type delegateType) =>
+ EnableWithConfiguration (DDURLSessionInstrumentationConfiguration.Create (delegateType));
+
+ ///
+ /// The delegate class to instrument.
+ public static void Enable ()
+ where TDelegate : NSObject, INSUrlSessionDataDelegate =>
+ Enable (typeof (TDelegate));
+
+ /// Stops instrumenting the given delegate type.
+ public static void Disable (Type delegateType) =>
+ DisableWithDelegateClass (
+ DDURLSessionInstrumentationConfiguration.HandleFor (delegateType, nameof (delegateType)));
+
+ ///
+ /// The delegate class to stop instrumenting.
+ public static void Disable ()
+ where TDelegate : NSObject, INSUrlSessionDataDelegate =>
+ Disable (typeof (TDelegate));
+ }
+}
diff --git a/src/DatadogNet.Objc.iOS/ApiDefinitions.cs b/src/DatadogNet.Objc.iOS/ApiDefinitions.cs
index 2470d86..1621fd6 100644
--- a/src/DatadogNet.Objc.iOS/ApiDefinitions.cs
+++ b/src/DatadogNet.Objc.iOS/ApiDefinitions.cs
@@ -456,6 +456,13 @@ interface DDLogsConfiguration
[Export ("initWithCustomEndpoint:")]
[DesignatedInitializer]
NativeHandle Constructor ([NullAllowed] NSUrl customEndpoint);
+
+ // -(void)setEventMapper:(DDLogEvent * _Nullable (^ _Nonnull)(DDLogEvent * _Nonnull))mapper;
+ // Return the event to keep it, a modified event to rewrite it, or null to drop it entirely.
+ // This is the Logs counterpart of DDRUMConfiguration's five event mappers, and the only
+ // supported way to redact a log before it leaves the device.
+ [Export ("setEventMapper:")]
+ void SetEventMapper (Func mapper);
}
// @interface DDNSURLSessionDelegate : NSObject
@@ -5643,4 +5650,244 @@ partial interface OTSpanContext
[Export ("forEachBaggageItem:")]
void ForEachBaggageItem (Func callback);
}
+
+ // ---------------------------------------------------------------------------------------
+ // Log event model, reached through DDLogsConfiguration.SetEventMapper.
+ //
+ // Objective Sharpie emitted none of this - not the ten DDLogEvent* classes, not the three
+ // enums, and not setEventMapper: itself - so the previous bindings could not redact or drop a
+ // log before it was uploaded, even though RUM had all five of its mappers. Transcribed by hand
+ // from DatadogObjc-Swift.h.
+ //
+ // Only the properties the header declares as writable are bound with setters; the rest are
+ // read-only there, and making them settable here would compile but silently do nothing.
+ // ---------------------------------------------------------------------------------------
+
+ // @interface DDLogEventAttributes : NSObject
+ [BaseType (typeof(NSObject), Name = "_TtC11DatadogObjc20DDLogEventAttributes")]
+ [DisableDefaultCtor]
+ interface DDLogEventAttributes
+ {
+ // @property (copy, nonatomic) NSDictionary * _Nonnull userAttributes;
+ [Export ("userAttributes", ArgumentSemantic.Copy)]
+ NSDictionary UserAttributes { get; set; }
+ }
+
+ // @interface DDLogEventBinaryImage : NSObject
+ [BaseType (typeof(NSObject), Name = "_TtC11DatadogObjc21DDLogEventBinaryImage")]
+ [DisableDefaultCtor]
+ interface DDLogEventBinaryImage
+ {
+ [NullAllowed, Export ("arch")]
+ string Arch { get; }
+
+ [Export ("isSystem")]
+ bool IsSystem { get; }
+
+ [NullAllowed, Export ("loadAddress")]
+ string LoadAddress { get; }
+
+ [NullAllowed, Export ("maxAddress")]
+ string MaxAddress { get; }
+
+ [Export ("name")]
+ string Name { get; }
+
+ [Export ("uuid")]
+ string Uuid { get; }
+ }
+
+ // @interface DDLogEventError : NSObject
+ [BaseType (typeof(NSObject), Name = "_TtC11DatadogObjc15DDLogEventError")]
+ [DisableDefaultCtor]
+ interface DDLogEventError
+ {
+ [NullAllowed, Export ("kind")]
+ string Kind { get; set; }
+
+ [NullAllowed, Export ("message")]
+ string Message { get; set; }
+
+ [NullAllowed, Export ("stack")]
+ string Stack { get; set; }
+
+ [Export ("sourceType")]
+ string SourceType { get; set; }
+
+ [NullAllowed, Export ("fingerprint")]
+ string Fingerprint { get; set; }
+
+ [NullAllowed, Export ("binaryImages", ArgumentSemantic.Copy)]
+ DDLogEventBinaryImage[] BinaryImages { get; set; }
+ }
+
+ // @interface DDLogEventCarrierInfo : NSObject
+ [BaseType (typeof(NSObject), Name = "_TtC11DatadogObjc21DDLogEventCarrierInfo")]
+ [DisableDefaultCtor]
+ interface DDLogEventCarrierInfo
+ {
+ [NullAllowed, Export ("carrierName")]
+ string CarrierName { get; }
+
+ [NullAllowed, Export ("carrierISOCountryCode")]
+ string CarrierIsoCountryCode { get; }
+
+ [Export ("carrierAllowsVOIP")]
+ bool CarrierAllowsVoip { get; }
+
+ [Export ("radioAccessTechnology")]
+ DDLogEventRadioAccessTechnology RadioAccessTechnology { get; }
+ }
+
+ // @interface DDLogEventDeviceInfo : NSObject
+ [BaseType (typeof(NSObject), Name = "_TtC11DatadogObjc20DDLogEventDeviceInfo")]
+ [DisableDefaultCtor]
+ interface DDLogEventDeviceInfo
+ {
+ [Export ("brand")]
+ string Brand { get; }
+
+ [Export ("name")]
+ string Name { get; }
+
+ [Export ("model")]
+ string Model { get; }
+
+ [Export ("architecture")]
+ string Architecture { get; }
+ }
+
+ // @interface DDLogEventDd : NSObject
+ [BaseType (typeof(NSObject), Name = "_TtC11DatadogObjc12DDLogEventDd")]
+ [DisableDefaultCtor]
+ interface DDLogEventDd
+ {
+ [Export ("device", ArgumentSemantic.Strong)]
+ DDLogEventDeviceInfo Device { get; }
+ }
+
+ // @interface DDLogEventNetworkConnectionInfo : NSObject
+ [BaseType (typeof(NSObject), Name = "_TtC11DatadogObjc31DDLogEventNetworkConnectionInfo")]
+ [DisableDefaultCtor]
+ interface DDLogEventNetworkConnectionInfo
+ {
+ [Export ("reachability")]
+ DDLogEventReachability Reachability { get; }
+
+ [NullAllowed, Export ("availableInterfaces", ArgumentSemantic.Copy)]
+ NSNumber[] AvailableInterfaces { get; }
+
+ [NullAllowed, Export ("supportsIPv4", ArgumentSemantic.Strong)]
+ NSNumber SupportsIPv4 { get; }
+
+ [NullAllowed, Export ("supportsIPv6", ArgumentSemantic.Strong)]
+ NSNumber SupportsIPv6 { get; }
+
+ [NullAllowed, Export ("isExpensive", ArgumentSemantic.Strong)]
+ NSNumber IsExpensive { get; }
+
+ [NullAllowed, Export ("isConstrained", ArgumentSemantic.Strong)]
+ NSNumber IsConstrained { get; }
+ }
+
+ // @interface DDLogEventOperatingSystem : NSObject
+ [BaseType (typeof(NSObject), Name = "_TtC11DatadogObjc25DDLogEventOperatingSystem")]
+ [DisableDefaultCtor]
+ interface DDLogEventOperatingSystem
+ {
+ [Export ("name")]
+ string Name { get; }
+
+ [Export ("version")]
+ string Version { get; }
+
+ [NullAllowed, Export ("build")]
+ string Build { get; }
+ }
+
+ // @interface DDLogEventUserInfo : NSObject
+ [BaseType (typeof(NSObject), Name = "_TtC11DatadogObjc18DDLogEventUserInfo")]
+ [DisableDefaultCtor]
+ interface DDLogEventUserInfo
+ {
+ [NullAllowed, Export ("id")]
+ string Id { get; }
+
+ [NullAllowed, Export ("name")]
+ string Name { get; }
+
+ [NullAllowed, Export ("email")]
+ string Email { get; }
+
+ [Export ("extraInfo", ArgumentSemantic.Copy)]
+ NSDictionary ExtraInfo { get; set; }
+ }
+
+ // @interface DDLogEvent : NSObject
+ [BaseType (typeof(NSObject), Name = "_TtC11DatadogObjc10DDLogEvent")]
+ [DisableDefaultCtor]
+ interface DDLogEvent
+ {
+ [Export ("date", ArgumentSemantic.Copy)]
+ NSDate Date { get; }
+
+ [Export ("status")]
+ DDLogEventStatus Status { get; }
+
+ // One of the two properties worth writing to: redacting a message in place is the common
+ // reason to install a log mapper at all.
+ [Export ("message")]
+ string Message { get; set; }
+
+ [NullAllowed, Export ("error", ArgumentSemantic.Strong)]
+ DDLogEventError Error { get; }
+
+ [Export ("serviceName")]
+ string ServiceName { get; }
+
+ [Export ("environment")]
+ string Environment { get; }
+
+ [Export ("loggerName")]
+ string LoggerName { get; }
+
+ [Export ("loggerVersion")]
+ string LoggerVersion { get; }
+
+ [NullAllowed, Export ("threadName")]
+ string ThreadName { get; }
+
+ [Export ("applicationVersion")]
+ string ApplicationVersion { get; }
+
+ [Export ("applicationBuildNumber")]
+ string ApplicationBuildNumber { get; }
+
+ [NullAllowed, Export ("buildId")]
+ string BuildId { get; }
+
+ [NullAllowed, Export ("variant")]
+ string Variant { get; }
+
+ [Export ("dd", ArgumentSemantic.Strong)]
+ DDLogEventDd Dd { get; }
+
+ [Export ("os", ArgumentSemantic.Strong)]
+ DDLogEventOperatingSystem Os { get; }
+
+ [Export ("userInfo", ArgumentSemantic.Strong)]
+ DDLogEventUserInfo UserInfo { get; }
+
+ [NullAllowed, Export ("networkConnectionInfo", ArgumentSemantic.Strong)]
+ DDLogEventNetworkConnectionInfo NetworkConnectionInfo { get; }
+
+ [NullAllowed, Export ("mobileCarrierInfo", ArgumentSemantic.Strong)]
+ DDLogEventCarrierInfo MobileCarrierInfo { get; }
+
+ [Export ("attributes", ArgumentSemantic.Strong)]
+ DDLogEventAttributes Attributes { get; }
+
+ [NullAllowed, Export ("tags", ArgumentSemantic.Copy)]
+ string[] Tags { get; set; }
+ }
}
diff --git a/src/DatadogNet.Objc.iOS/StructsAndEnums.cs b/src/DatadogNet.Objc.iOS/StructsAndEnums.cs
index 9267697..53c5e9c 100644
--- a/src/DatadogNet.Objc.iOS/StructsAndEnums.cs
+++ b/src/DatadogNet.Objc.iOS/StructsAndEnums.cs
@@ -1033,4 +1033,60 @@ public enum DDUploadFrequency : long
Average = 1,
Rare = 2
}
+
+ // The log-event model reached by DDLogsConfiguration.SetEventMapper. Objective Sharpie omitted
+ // the whole family, so the previous bindings had no way to inspect or redact a log before
+ // upload; these are transcribed from DatadogObjc-Swift.h.
+
+ [Native]
+ public enum DDLogEventStatus : long
+ {
+ Debug = 0,
+ Info = 1,
+ Notice = 2,
+ Warn = 3,
+ Error = 4,
+ Critical = 5,
+ Emergency = 6
+ }
+
+ [Native]
+ public enum DDLogEventReachability : long
+ {
+ Yes = 0,
+ Maybe = 1,
+ No = 2
+ }
+
+ [Native]
+ public enum DDLogEventRadioAccessTechnology : long
+ {
+ Gprs = 0,
+ Edge = 1,
+ Wcdma = 2,
+ Hsdpa = 3,
+ Hsupa = 4,
+ Cdma1x = 5,
+ CdmaEvdoRev0 = 6,
+ CdmaEvdoRevA = 7,
+ CdmaEvdoRevB = 8,
+ Ehrpd = 9,
+ Lte = 10,
+ Unknown = 11
+ }
+
+ /// Network interface kinds reported in DDLogEventNetworkConnectionInfo.AvailableInterfaces.
+ ///
+ /// That property is an NSNumber[] because Objective-C has no typed enum arrays, so the
+ /// values only mean anything once cast to this.
+ ///
+ [Native]
+ public enum DDLogEventInterface : long
+ {
+ Wifi = 0,
+ WiredEthernet = 1,
+ Cellular = 2,
+ Loopback = 3,
+ Other = 4
+ }
}
diff --git a/tests/DatadogNet.iOS.DeviceTests/SmokeTests.cs b/tests/DatadogNet.iOS.DeviceTests/SmokeTests.cs
index 49b51ad..fd406ba 100644
--- a/tests/DatadogNet.iOS.DeviceTests/SmokeTests.cs
+++ b/tests/DatadogNet.iOS.DeviceTests/SmokeTests.cs
@@ -65,6 +65,9 @@ public static class SmokeTests
new("exposes WebView tracking", ExposesWebViewTracking),
new("exposes PLCrashReporter", ExposesCrashReporter),
new("drives RUM and Logs through the ergonomic overloads", ErgonomicOverloadsWork),
+ new("invokes a RUM event mapper", EventMapperIsInvoked),
+ new("invokes a Logs event mapper and redacts a message", LogEventMapperRedacts),
+ new("instruments a URLSession delegate by type", InstrumentsUrlSessionByType),
new("stops the RUM session and the SDK instance", StopsCleanly),
];
@@ -133,6 +136,110 @@ private static void ErgonomicOverloadsWork()
Report("view scopes, attribute conversion, logger and consent helpers all behaved");
}
+ /// Counts mapper invocations, incremented from the blocks registered when features are enabled.
+ private static int viewEventsMapped;
+ private static int actionEventsMapped;
+ private static int logEventsMapped;
+ private static int logEventsRedacted;
+
+ ///
+ /// An NSUrlSession delegate for to instrument.
+ ///
+ ///
+ /// [Register] matters: DDURLSessionInstrumentation takes an Objective-C Class, so the type has
+ /// to be visible to the Objective-C runtime for the lookup to resolve to anything.
+ ///
+ [Register(nameof(InstrumentedSessionDelegate))]
+ private sealed class InstrumentedSessionDelegate : NSUrlSessionDataDelegate
+ {
+ }
+
+ ///
+ /// Checks that the RUM event mappers actually fire, and that an event can be handed back to
+ /// Swift from managed code.
+ ///
+ ///
+ /// Mappers are how an app redacts or drops events before they are uploaded, so they are the
+ /// mechanism behind every "scrub the PII out of this" requirement. They are also the only part
+ /// of the binding that passes a managed delegate into Swift and gets a Swift object back out,
+ /// so if block marshalling is wrong anywhere it is wrong here - and the failure mode is a crash
+ /// inside the SDK's event-writing path, long after the call that registered the mapper.
+ ///
+ /// The mappers are registered on the real configuration in , before
+ /// RUM is enabled, because that is the only time they can be set. This check then runs after
+ /// has produced events and asserts the blocks were actually reached.
+ ///
+ ///
+ private static void EventMapperIsInvoked()
+ {
+ // Events are mapped on the SDK's own queue as they are written, not synchronously with the
+ // call that produced them, so give that queue a moment before concluding anything.
+ var deadline = DateTime.UtcNow.AddSeconds(5);
+ while (DateTime.UtcNow < deadline && (viewEventsMapped == 0 || actionEventsMapped == 0))
+ {
+ Thread.Sleep(100);
+ }
+
+ Assert(viewEventsMapped > 0, "The view event mapper was never invoked.");
+ Assert(actionEventsMapped > 0, "The action event mapper was never invoked.");
+
+ Report($"mappers invoked: {viewEventsMapped} view, {actionEventsMapped} action");
+ }
+
+ ///
+ /// Checks that a Logs event mapper fires and that a rewritten message survives back into Swift.
+ ///
+ private static void LogEventMapperRedacts()
+ {
+ var deadline = DateTime.UtcNow.AddSeconds(5);
+ while (DateTime.UtcNow < deadline && logEventsRedacted == 0)
+ {
+ Thread.Sleep(100);
+ }
+
+ Assert(logEventsMapped > 0, "The log event mapper was never invoked.");
+ Assert(logEventsRedacted > 0, "The log event mapper never saw the message it was meant to redact.");
+
+ Report($"log events mapped: {logEventsMapped}, redacted: {logEventsRedacted}");
+ }
+
+ ///
+ /// Checks the type-based URLSession instrumentation helpers.
+ ///
+ ///
+ /// This is the API Datadog documents for automatic resource tracking, and the raw binding takes
+ /// the delegate class as a bare IntPtr - a signature that accepts IntPtr.Zero happily and then
+ /// instruments nothing. The helpers resolve the Objective-C class from a managed Type and throw
+ /// if it does not exist, which is what this checks in both directions.
+ ///
+ private static void InstrumentsUrlSessionByType()
+ {
+ DDURLSessionInstrumentation.Enable();
+
+ var configuration = DDURLSessionInstrumentationConfiguration.Create();
+ Assert(configuration.DelegateClass != IntPtr.Zero, "The configuration resolved a null delegate class.");
+ Assert(
+ configuration.DelegateType == typeof(InstrumentedSessionDelegate),
+ $"DelegateType round-tripped to {configuration.DelegateType?.Name ?? "null"}.");
+
+ // A type the Objective-C runtime has never heard of must be rejected rather than silently
+ // instrumenting nothing.
+ var rejected = false;
+ try
+ {
+ DDURLSessionInstrumentation.Enable(typeof(SmokeTests));
+ }
+ catch (ArgumentException)
+ {
+ rejected = true;
+ }
+
+ Assert(rejected, "A non-Objective-C type was accepted as a session delegate.");
+
+ DDURLSessionInstrumentation.Disable();
+ Report("instrumented and disabled a URLSession delegate by type");
+ }
+
/// Every framework shipped as a dynamic framework, which is all but CrashReporter.
private static readonly string[] DynamicFrameworks =
[
@@ -247,6 +354,20 @@ private static void EnablesRum()
UiKitActionsPredicate = new DDDefaultUIKitRUMActionsPredicate(),
};
+ // Registered before EnableWith, which is the only point at which mappers can be set.
+ // Returning the event unchanged is the identity case; returning null would drop it.
+ configuration.SetViewEventMapper(view =>
+ {
+ Interlocked.Increment(ref viewEventsMapped);
+ return view;
+ });
+
+ configuration.SetActionEventMapper(action =>
+ {
+ Interlocked.Increment(ref actionEventsMapped);
+ return action;
+ });
+
DDRUM.EnableWith(configuration);
Assert(DDRUMMonitor.Shared is not null, "DDRUMMonitor.Shared was null after enabling RUM.");
@@ -275,7 +396,26 @@ private static void DrivesRum()
private static void EnablesLogsAndWritesEveryLevel()
{
- DDLogs.EnableWith(new DDLogsConfiguration(LocalEndpoint));
+ var logsConfiguration = new DDLogsConfiguration(LocalEndpoint);
+
+ // Registered before EnableWith, the only point at which a mapper can be set. Neither the
+ // DDLogEvent model nor setEventMapper: was bound before - Objective Sharpie omitted the
+ // whole family - so this is the first release in which a log can be redacted or dropped
+ // before upload.
+ logsConfiguration.SetEventMapper(logEvent =>
+ {
+ Interlocked.Increment(ref logEventsMapped);
+
+ if (logEvent.Message.Contains("secret", StringComparison.Ordinal))
+ {
+ logEvent.Message = "[redacted]";
+ Interlocked.Increment(ref logEventsRedacted);
+ }
+
+ return logEvent;
+ });
+
+ DDLogs.EnableWith(logsConfiguration);
// The designated initializer takes all eight settings; there is no parameterless form.
var logger = DDLogger.CreateWith(new DDLoggerConfiguration(
@@ -302,6 +442,9 @@ private static void EnablesLogsAndWritesEveryLevel()
logger.RemoveTagWithKey("suite");
logger.RemoveAttributeForKey("attempt");
+ // Picked up by the mapper registered above and rewritten to "[redacted]".
+ logger.Info("this contains a secret value");
+
Report("wrote six levels and round-tripped a tag and an attribute");
}