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"); }