From eda887f14154856688c844873d9b56eb6c2147a6 Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Thu, 23 Jul 2026 14:10:07 +0300 Subject: [PATCH 1/4] add missing funcs --- Directory.Build.props | 2 +- build/packages.tsv | 33 ++++ .../Additions/DatadogAttributes.cs | 19 +- .../Additions/LoggerExtensions.cs | 14 ++ .../Additions/RumMonitorExtensions.cs | 162 ++++++++++++++++++ .../Additions/DatadogPropagationExtensions.cs | 123 +++++++++++++ .../Additions/DatadogSpanExtensions.cs | 95 ++++++++++ 7 files changed, 445 insertions(+), 3 deletions(-) create mode 100644 src/DatadogNet.TraceApi.Android/Additions/DatadogPropagationExtensions.cs create mode 100644 src/DatadogNet.TraceApi.Android/Additions/DatadogSpanExtensions.cs diff --git a/Directory.Build.props b/Directory.Build.props index 5ebacaf..16da222 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,7 +14,7 @@ not bound here at all. --> 3.12.1 - 1 + 2 $(DatadogNativeVersion).$(DatadogBindingRevision) diff --git a/build/packages.tsv b/build/packages.tsv index 8387982..9d68d25 100644 --- a/build/packages.tsv +++ b/build/packages.tsv @@ -21,3 +21,36 @@ SessionReplayCompose dd-sdk-android-session-replay-compose SessionReplay,Interna Ndk dd-sdk-android-ndk Core,Internal WebView dd-sdk-android-webview Core,Internal OkHttp dd-sdk-android-okhttp Internal,RUM,Trace + +# ---------------------------------------------------------------------------------------------- +# NOT BOUND, and why. +# +# Datadog publishes 27 dd-sdk-android* artifacts; the 13 above are the ones a .NET or MAUI app can +# use. This section exists so "is X missing on purpose?" has an answer here rather than in someone's +# memory. Nothing reads it - it is documentation, kept next to the list it is about. +# +# dd-sdk-android An umbrella/BOM artifact, not a library. +# -gradle-plugin A Gradle build plugin. Nothing to bind. +# -benchmark-internal Datadog's own benchmarking harness, not product API. +# +# -ktx Kotlin extension functions. They bind as static methods on synthetic +# -rx *Kt classes taking Kotlin function interfaces, and every one of them +# -rum-coroutines wraps a Kotlin idiom - coroutines, Rx, scope functions - that has no +# -trace-coroutines C# meaning. A caller would write more code than they saved. +# +# -coil Instrument third-party Kotlin/Java libraries. A MAUI app draws +# -fresco through MAUI handlers and loads images through MAUI, so none of +# -glide these libraries is present to instrument. Add the artifact yourself +# -sqldelight if you host native views that use one. +# -timber +# +# -compose RUM auto-instrumentation for Jetpack Compose - distinct from +# session-replay-compose, which IS bound. Nothing in MAUI draws +# Compose. Worth revisiting for an app hosting Compose content. +# +# -tv Android TV. Worth revisiting if .NET for Android TV becomes a +# target. +# +# -trace-otel OpenTelemetry interop. Reachable in principle, but a .NET app that +# -okhttp-otel wants OTel semantics is better served by the OpenTelemetry .NET SDK +# exporting to Datadog's OTLP intake than by a binding over Java's. diff --git a/src/DatadogNet.Core.Android/Additions/DatadogAttributes.cs b/src/DatadogNet.Core.Android/Additions/DatadogAttributes.cs index 2829db0..9b91e7b 100644 --- a/src/DatadogNet.Core.Android/Additions/DatadogAttributes.cs +++ b/src/DatadogNet.Core.Android/Additions/DatadogAttributes.cs @@ -47,8 +47,23 @@ public static class DatadogAttributes return converted; } - /// Converts one value, naming the attribute in any error so it can be found. - private static Java.Lang.Object ToJava(object? value, string key) => value switch + /// + /// Converts a single value into the the Datadog SDK expects. + /// + /// The value. + /// + /// The attribute name. Used only to name the value in any error, so that a rejected attribute + /// can be found without guessing which one it was. + /// + /// The value has no Java representation. + /// + /// Several members take a bare value rather than a map - RumMonitor.addAttribute, + /// addFeatureFlagEvaluation, Logs.addAttribute, Logger.addAttribute - and + /// they all need the same conversion applies per entry. Without this a + /// caller either hand-wraps the value, which is what exists to avoid, or + /// round-trips a one-element dictionary through to get at the result. + /// + public static Java.Lang.Object ToJava(object? value, string key) => value switch { null => null!, diff --git a/src/DatadogNet.Logs.Android/Additions/LoggerExtensions.cs b/src/DatadogNet.Logs.Android/Additions/LoggerExtensions.cs index 1fe3d53..abe33e7 100644 --- a/src/DatadogNet.Logs.Android/Additions/LoggerExtensions.cs +++ b/src/DatadogNet.Logs.Android/Additions/LoggerExtensions.cs @@ -66,4 +66,18 @@ public static void Log( exception?.ToString()!, DatadogAttributes.From(attributes)); } + + /// Adds an attribute to every subsequent entry from this logger. + /// + /// The generated overload takes a Java.Lang.Object, so a logger-wide attribute has to be + /// hand-wrapped - which is what exists to avoid for the + /// map-taking members. + /// + public static void AddAttribute(this Logger logger, string key, object? value) + { + ArgumentNullException.ThrowIfNull(logger); + ArgumentNullException.ThrowIfNull(key); + + logger.AddAttribute(key, DatadogAttributes.ToJava(value, key)); + } } diff --git a/src/DatadogNet.RUM.Android/Additions/RumMonitorExtensions.cs b/src/DatadogNet.RUM.Android/Additions/RumMonitorExtensions.cs index 85e58dc..7caf2ac 100644 --- a/src/DatadogNet.RUM.Android/Additions/RumMonitorExtensions.cs +++ b/src/DatadogNet.RUM.Android/Additions/RumMonitorExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Com.Datadog.Android; namespace Com.Datadog.Android.Rum; @@ -108,6 +109,167 @@ public static void AddError( stacktrace: null!, DatadogAttributes.From(attributes)); } + + /// Adds an attribute to every subsequent RUM event. + /// + /// The generated overload takes a Java.Lang.Object, so setting a global attribute means + /// hand-wrapping the value - which is exactly what exists to + /// avoid for the map-taking members. + /// + public static void AddAttribute(this IRumMonitor monitor, string key, object? value) + { + ArgumentNullException.ThrowIfNull(monitor); + ArgumentNullException.ThrowIfNull(key); + + monitor.AddAttribute(key, DatadogAttributes.ToJava(value, key)); + } + + /// Records that a feature flag was evaluated, so RUM events can be split by variant. + public static void AddFeatureFlagEvaluation(this IRumMonitor monitor, string name, object? value) + { + ArgumentNullException.ThrowIfNull(monitor); + ArgumentNullException.ThrowIfNull(name); + + monitor.AddFeatureFlagEvaluation(name, DatadogAttributes.ToJava(value, name)); + } + + /// Begins tracking a network request as a RUM resource. + public static void StartResource( + this IRumMonitor monitor, + string key, + RumResourceMethod method, + string url, + IReadOnlyDictionary? attributes = null) + { + ArgumentNullException.ThrowIfNull(monitor); + + monitor.StartResource(key, method, url, DatadogAttributes.From(attributes)); + } + + /// Completes a resource begun by . + /// + /// The generated overload takes java.lang.Integer and java.lang.Long, because both + /// are nullable in Kotlin - so a C# caller has to box through Java.Lang.Integer.ValueOf + /// and remember that a null means "not known" rather than zero. Nullable value types say the + /// same thing in the language the caller is already writing. + /// + public static void StopResource( + this IRumMonitor monitor, + string key, + int? statusCode, + long? size, + RumResourceKind kind, + IReadOnlyDictionary? attributes = null) + { + ArgumentNullException.ThrowIfNull(monitor); + + monitor.StopResource( + key, + statusCode is { } code ? Java.Lang.Integer.ValueOf(code) : null, + size is { } bytes ? Java.Lang.Long.ValueOf(bytes) : null, + kind, + DatadogAttributes.From(attributes)); + } + + /// Completes a resource that failed, from a message. + /// + /// Guards a trap the C# signature does not show: stackTrace is String in Kotlin, + /// not String?, so passing null reaches Java's own null check and throws + /// + /// NullPointerException: Parameter specified as non-null is null: method + /// DatadogRumMonitor.stopResourceWithError, parameter stackTrace + /// + /// - unlike errorType beside it, and unlike addErrorWithStacktrace, both of which + /// are genuinely nullable. Nothing in the generated binding distinguishes the three. + /// + public static void StopResourceWithError( + this IRumMonitor monitor, + string key, + string message, + int? statusCode = null, + string? stackTrace = null, + string? errorType = null, + RumErrorSource? source = null, + IReadOnlyDictionary? attributes = null) + { + ArgumentNullException.ThrowIfNull(monitor); + ArgumentNullException.ThrowIfNull(message); + + monitor.StopResourceWithError( + key, + statusCode is { } code ? Java.Lang.Integer.ValueOf(code) : null, + message, + source ?? RumErrorSource.Network, + stackTrace: stackTrace ?? string.Empty, + errorType: errorType!, + DatadogAttributes.From(attributes)); + } + + /// Completes a resource that failed, from a .NET exception. + /// + /// The generated overloads take either a Java.Lang.Throwable, which a C# app does not + /// have, or six positional arguments including two nullable strings. This uses the string form + /// so the managed type, message and stack all reach Datadog. + /// + public static void StopResourceWithError( + this IRumMonitor monitor, + string key, + Exception exception, + int? statusCode = null, + RumErrorSource? source = null, + IReadOnlyDictionary? attributes = null) + { + ArgumentNullException.ThrowIfNull(monitor); + ArgumentNullException.ThrowIfNull(exception); + + monitor.StopResourceWithError( + key, + statusCode is { } code ? Java.Lang.Integer.ValueOf(code) : null, + exception.Message, + source ?? RumErrorSource.Network, + stackTrace: exception.ToString(), + errorType: exception.GetType().FullName!, + DatadogAttributes.From(attributes)); + } + + /// + /// The id of the current RUM session, or if there is none. + /// + /// + /// The generated GetCurrentSessionId takes a + /// kotlin.jvm.functions.Function1, which C# cannot express as a lambda at all: it binds + /// as an interface, so calling it requires declaring a subclass + /// implementing IFunction1 and returning null for Kotlin's Unit. That is a lot of + /// ceremony for a value most apps want in order to put it on a support ticket. + /// + /// dd-sdk-ios takes an ordinary block for the same call and needs none of this. + /// + /// + public static Task GetCurrentSessionIdAsync(this IRumMonitor monitor) + { + ArgumentNullException.ThrowIfNull(monitor); + + // RunContinuationsAsynchronously: the callback arrives on whichever thread the SDK answers + // on, and a synchronous continuation would run the caller's await-resumption there too. + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + monitor.GetCurrentSessionId(new SessionIdCallback(completion)); + + return completion.Task; + } + + /// Adapts getCurrentSessionId's Kotlin lambda to a completion source. + private sealed class SessionIdCallback(TaskCompletionSource completion) + : Java.Lang.Object, Kotlin.Jvm.Functions.IFunction1 + { + public Java.Lang.Object? Invoke(Java.Lang.Object? sessionId) + { + completion.TrySetResult(sessionId?.ToString()); + + // Kotlin's Unit, which the binding maps to null for a Unit-returning lambda. + return null; + } + } } /// diff --git a/src/DatadogNet.TraceApi.Android/Additions/DatadogPropagationExtensions.cs b/src/DatadogNet.TraceApi.Android/Additions/DatadogPropagationExtensions.cs new file mode 100644 index 0000000..d10be75 --- /dev/null +++ b/src/DatadogNet.TraceApi.Android/Additions/DatadogPropagationExtensions.cs @@ -0,0 +1,123 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using Com.Datadog.Android.Trace.Api.Span; + +namespace Com.Datadog.Android.Trace.Api.Propagation; + +/// +/// Distributed tracing propagation, in a form C# can call. +/// +/// +/// DatadogPropagation.inject is declared in Kotlin as +/// +/// fun <C> inject(context: DatadogSpanContext, carrier: C, setter: (C, String, String) -> Unit) +/// +/// and that trailing lambda is the problem. Kotlin function types bind as +/// Kotlin.Jvm.Functions.IFunction3 — an interface, not a delegate — so C# cannot pass a +/// lambda, a method group, or an . Calling it means declaring a +/// subclass that implements IFunction3, marshalling three +/// parameters back to strings, and returning +/// for Kotlin's Unit. +/// +/// Every consumer who wants distributed tracing has to write that, and getting it wrong does not +/// fail loudly: the request simply goes out with no trace headers, and the trace stops at the app. +/// dd-sdk-ios needs none of it — OTTracer.inject takes an ordinary writer object. +/// +/// +/// Not wrapped: extract. Its signature nests a second Kotlin lambda — the SDK hands +/// you a visitor you call per header, whose Boolean return governs whether iteration +/// continues — and that contract is not documented anywhere the binding can see. Guessing it would +/// produce something that silently reads one header and stops. Extraction is also the rare +/// direction for a mobile app, which receives responses rather than serving requests. +/// +/// +public static class DatadogPropagationExtensions +{ + /// + /// Writes the trace headers for into a new dictionary. + /// + /// The tracer's propagation, from DatadogTracer.Propagate(). + /// The span context to propagate, from DatadogSpan.Context(). + /// + /// The headers to add to the outgoing request. Which headers appear depends on the tracing + /// header types the tracer was built with; a single call writes all of them. + /// + /// + /// + /// var span = GlobalDatadogTracer.Get ().BuildSpan (new Java.Lang.String ("http.request")).Start (); + /// + /// foreach (var header in GlobalDatadogTracer.Get ().Propagate ().Inject (span.Context ())) + /// request.Headers.TryAddWithoutValidation (header.Key, header.Value); + /// + /// + public static IDictionary Inject( + this IDatadogPropagation propagation, + IDatadogSpanContext context) + { + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + propagation.Inject(context, headers); + + return headers; + } + + /// Writes the trace headers for into an existing dictionary. + /// The tracer's propagation. + /// The span context to propagate. + /// The dictionary to write into. Existing keys are replaced. + public static void Inject( + this IDatadogPropagation propagation, + IDatadogSpanContext context, + IDictionary headers) + { + ArgumentNullException.ThrowIfNull(headers); + + propagation.Inject(context, (name, value) => headers[name] = value); + } + + /// Writes the trace headers for through a callback. + /// The tracer's propagation. + /// The span context to propagate. + /// Called once per header, with its name and value. + /// + /// The form to use when the destination is not a dictionary — an HttpRequestMessage, a + /// gRPC metadata collection, a message envelope. + /// + public static void Inject( + this IDatadogPropagation propagation, + IDatadogSpanContext context, + Action setter) + { + ArgumentNullException.ThrowIfNull(propagation); + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(setter); + + // The carrier is the value the SDK hands back to the setter untouched, and this closes over + // the real destination instead - so it only has to be a non-null Java object. It cannot be + // `new Java.Lang.Object()`, whose constructor is protected. + propagation.Inject(context, new Java.Lang.String(string.Empty), new HeaderSetter(setter)); + } + + /// Adapts inject's Kotlin lambda to a C# delegate. + private sealed class HeaderSetter(Action setter) + : Java.Lang.Object, Kotlin.Jvm.Functions.IFunction3 + { + public Java.Lang.Object? Invoke( + Java.Lang.Object? carrier, + Java.Lang.Object? name, + Java.Lang.Object? value) + { + // The SDK never passes a null name, but a null value is cheaper to tolerate than to + // crash an app's request pipeline over. + if (name is not null) + { + setter(name.ToString()!, value?.ToString() ?? string.Empty); + } + + // Kotlin's Unit, which the binding maps to null for a Unit-returning lambda. + return null; + } + } +} diff --git a/src/DatadogNet.TraceApi.Android/Additions/DatadogSpanExtensions.cs b/src/DatadogNet.TraceApi.Android/Additions/DatadogSpanExtensions.cs new file mode 100644 index 0000000..c7d3207 --- /dev/null +++ b/src/DatadogNet.TraceApi.Android/Additions/DatadogSpanExtensions.cs @@ -0,0 +1,95 @@ +#nullable enable + +using System; + +namespace Com.Datadog.Android.Trace.Api.Span; + +/// Describing a span's outcome, in a form C# can call directly. +public static class DatadogSpanExtensions +{ + /// Marks the span as failed, from a .NET exception. + /// The span. + /// The exception that failed it. + /// + /// addThrowable takes a java.lang.Throwable, which a .NET exception is not — so + /// the managed type, message and stack have to be set as separate fields for any of them to + /// reach Datadog. Getting that split wrong is quiet: the span is marked as an error, and the + /// APM error panel shows nothing to act on. + /// + /// setError takes a java.lang.Boolean rather than a bool, so even the flag + /// needs boxing. + /// + /// + public static void SetError(this IDatadogSpan span, Exception exception) + { + ArgumentNullException.ThrowIfNull(span); + ArgumentNullException.ThrowIfNull(exception); + + span.SetError( + exception.GetType().FullName ?? exception.GetType().Name, + exception.Message, + exception.ToString()); + } + + /// Marks the span as failed, from an error kind, message and stack. + /// The span. + /// The error type. + /// The error message. + /// The stack, attached as an error log on the span. + public static void SetError( + this IDatadogSpan span, + string kind, + string message, + string? stack = null) + { + ArgumentNullException.ThrowIfNull(span); + ArgumentNullException.ThrowIfNull(kind); + ArgumentNullException.ThrowIfNull(message); + + // These are real members in 3.x. In 2.x io.opentracing.Span had no setError at all, and a + // caller had to set an "error" tag plus four log fields by convention - where getting one + // field name wrong produced a span that looked fine and was never counted as an error. + span.SetError(Java.Lang.Boolean.True!); + span.SetErrorMessage($"{kind}: {message}"); + + if (stack is not null) + span.LogErrorMessage(stack); + } + + /// + /// The trace id, rendered the way Datadog's own instrumentation renders it. + /// + /// The span. + /// 32 lowercase hexadecimal characters, or an empty string if there is no context. + /// + /// DatadogTraceId.ToHexString(), which is what DatadogInterceptor writes as + /// _dd.trace_id when it links a RUM resource to its APM trace. Anything else — including + /// ToLong(), which silently returns the low half of a 128-bit id — produces a string the + /// backend will not correlate on. + /// + public static string GetTraceId(this IDatadogSpan span) + { + ArgumentNullException.ThrowIfNull(span); + + return span.Context()?.TraceId?.ToHexString() ?? string.Empty; + } + + /// + /// The span id, rendered the way Datadog's own instrumentation renders it. + /// + /// The span. + /// The decimal form, or an empty string if there is no context. + /// + /// Decimal, and deliberately not hexadecimal like . The asymmetry is + /// Datadog's wire format: DatadogInterceptor writes _dd.span_id as + /// String.valueOf(long). + /// + public static string GetSpanId(this IDatadogSpan span) + { + ArgumentNullException.ThrowIfNull(span); + + return span.Context() is { } context + ? context.SpanId.ToString(System.Globalization.CultureInfo.InvariantCulture) + : string.Empty; + } +} From c63c3b9d41c4b50babff6a190358b43bc5c56469 Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Thu, 23 Jul 2026 14:18:12 +0300 Subject: [PATCH 2/4] release notes added --- docs/release-notes/3.12.1.2.md | 147 +++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 docs/release-notes/3.12.1.2.md diff --git a/docs/release-notes/3.12.1.2.md b/docs/release-notes/3.12.1.2.md new file mode 100644 index 0000000..5ba118e --- /dev/null +++ b/docs/release-notes/3.12.1.2.md @@ -0,0 +1,147 @@ +## What's changed + +Binding-only release. The native SDK is unchanged — still +[dd-sdk-android 3.12.1](https://github.com/DataDog/dd-sdk-android/releases/tag/3.12.1) — and so are +the package IDs, namespaces and every existing signature. Upgrading is a version bump. + +Everything here is an **addition to the convenience layer**, and every one exists because the +generated binding leaves a member technically reachable but effectively unusable from C#. + +The two that matter most are blocked by Kotlin function types, which bind as *interfaces* rather +than delegates: you cannot pass a lambda, a method group, or an `Action<>`, so calling them means +declaring a `Java.Lang.Object` subclass and returning `null` for Kotlin's `Unit`. That is a lot of +ceremony for a header setter and a session id. + +> **Package versions are `.`.** `3.12.1.2` is +> dd-sdk-android `3.12.1`, binding revision `2`. The fourth component belongs to this repository and +> advances when the bindings or packaging change while the native artifacts stay put. + +## Added + +### Trace header injection — `DatadogPropagation.Inject` + +The one that matters most. Distributed tracing is the reason most apps enable Trace at all, and this +was the wall in front of it: + +```kotlin +fun inject(context: DatadogSpanContext, carrier: C, setter: (C, String, String) -> Unit) +``` + +That trailing lambda binds as `Kotlin.Jvm.Functions.IFunction3`. Getting it wrong does not fail +loudly — the request simply goes out with no trace headers, and the trace stops at the app. + +```csharp +var span = GlobalDatadogTracer.Get ().BuildSpan ("http.request").Start (); + +foreach (var header in GlobalDatadogTracer.Get ().Propagate ().Inject (span.Context ())) + request.Headers.TryAddWithoutValidation (header.Key, header.Value); +``` + +Three forms: one returning a new dictionary, one writing into a dictionary you own, and one taking +an `Action` for carriers that are not dictionaries — an `HttpRequestMessage`, gRPC +metadata, a message envelope. + +**`extract` is deliberately not wrapped.** Its signature nests a *second* Kotlin lambda — the SDK +hands you a visitor you call per header, whose `Boolean` return governs whether iteration continues +— and that contract is not documented anywhere the binding can see. Guessing it would produce +something that silently reads one header and stops. Extraction is also the rare direction for a +mobile app, which receives responses rather than serving requests. + +### Span outcome and identity — `DatadogSpanExtensions` + +```csharp +span.SetError (exception); // or SetError (kind, message, stack) +var traceId = span.GetTraceId (); // 32 lowercase hex +var spanId = span.GetSpanId (); // decimal +``` + +`SetError` exists because `addThrowable` takes a `java.lang.Throwable`, which a .NET exception is +not — so the managed type, message and stack have to be set as separate fields for any of them to +reach Datadog. `setError` additionally takes a `java.lang.Boolean`, so even the flag needs boxing. + +`GetTraceId` and `GetSpanId` render the ids **the way dd-sdk-android's own `DatadogInterceptor` +does** when it links a RUM resource to its APM trace: `DatadogTraceId.toHexString()` and +`String.valueOf(long)`. The asymmetry — hex for one, decimal for the other — is Datadog's wire +format rather than a choice, and matching it exactly is what makes the correlation work. +`toHexString()` is `toHexStringPadded(…, 32)` on both `DD128bTraceId` and `DD64bTraceId`, so the +result is always 32 characters; a 64-bit id is the low half behind sixteen zeros, never a +16-character string. + +### Session id — `RumMonitorExtensions.GetCurrentSessionIdAsync` + +```csharp +var sessionId = await GlobalRumMonitor.Get ().GetCurrentSessionIdAsync (); +``` + +`getCurrentSessionId` takes a `kotlin.jvm.functions.Function1`, so reading a value most apps want in +order to put it on a support ticket previously required an `IFunction1` implementation returning +`null` for Kotlin's `Unit`. + +### Single-value attributes + +`DatadogAttributes.ToJava (value, key)` is now **public**. `RumMonitor.addAttribute`, +`addFeatureFlagEvaluation` and `Logger.addAttribute` all take a bare `java.lang.Object` while +`DatadogAttributes` only exposed the map form, so a caller either hand-wrapped the value — which is +exactly what `DatadogAttributes` exists to avoid — or round-tripped a one-element dictionary. + +```csharp +monitor.AddAttribute ("cart.id", cartId); +monitor.AddFeatureFlagEvaluation ("new-checkout", true); +logger.AddAttribute ("tenant", tenantId); +``` + +### Resource overloads + +`StartResource`, `StopResource` and two `StopResourceWithError` forms on `RumMonitorExtensions`, +taking `int?`/`long?` instead of `Java.Lang.Integer`/`Java.Lang.Long`, and an `Exception` overload. +The exception form also guards a trap the C# signature does not show: `stackTrace` is `String` in +Kotlin, **not** `String?`, so passing null reaches Java's own null check and throws — unlike +`errorType` beside it, and unlike `addErrorWithStacktrace`, both of which are genuinely nullable. + +## One thing deliberately *not* added + +An earlier draft added a `BuildSpan(string)` overload, on the grounds that 3.x `DatadogTracer` +declares `buildSpan(CharSequence)` and callers were writing +`BuildSpan(new Java.Lang.String(name))`. That was wrong: the generator already emits +`IDatadogTracerExtensions.BuildSpan(IDatadogTracer, string)` in the same namespace, and a second one +made the call ambiguous. If you have been wrapping the operation name, you only need the `using`: + +```csharp +using Com.Datadog.Android.Trace.Api.Tracer; +``` + +## Documentation + +`build/packages.tsv` now records **what is not bound, and why** — all fourteen `dd-sdk-android*` +artifacts Datadog publishes that this repository does not wrap, each with a reason. Datadog ships +27; thirteen are bound. The rest fall into four groups: Kotlin-ecosystem integrations whose idioms +have no C# meaning (`-ktx`, `-rx`, the two coroutine modules), instrumentation for Java libraries a +.NET app does not use (`-glide`, `-coil`, `-fresco`, `-timber`, `-sqldelight`), things that are not +libraries (`-gradle-plugin`, the BOM), and three worth revisiting — `-compose`, `-tv` and the two +OpenTelemetry modules. + +The file already listed what *is* bound; the point of the addition is that "is X missing on purpose?" +now has an answer in the repository rather than in someone's memory. + +## Tests + +**94 package-layout tests pass.** No tests were added: the additions are C# on top of the same +native artifacts, so nothing about the package shape changed. + +**The on-emulator suite in this repository does not yet cover the new members.** They were verified +end to end through [DatadogNet](https://github.com/sbokatuk/DatadogNet)'s device suite instead, +which drives them on a real emulator — `Inject`, `GetTraceId`, `GetSpanId`, +`GetCurrentSessionIdAsync` and `ToJava` are all on its paths, and the trace check asserts that the +id is 32 lowercase hex characters, that its low half matches the `x-datadog-trace-id` header the SDK +itself emitted, and that it agrees with `traceparent` across all 128 bits. 21/21 pass. Adding the +equivalent checks here is worth doing and has not been done. + +## Upgrading from 3.12.1.1 + +```diff +- ++ +``` + +Nothing is removed or renamed, and the native `.aar` files are byte-for-byte the same. All thirteen +packages move together, as they depend on each other at an exact version. From 1607646e495ede69380cf188b771839a1425cc10 Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Thu, 23 Jul 2026 14:35:18 +0300 Subject: [PATCH 3/4] Cover the new convenience API on device, and rebuild the sample The emulator suite enabled Trace and stopped there: EnablesTrace registered a tracer and never started a span, so the tracing path was configured and never driven. That is the class of gap a trace-id defect slips through. Four checks close it, taking the suite from 13 to 17: - a span started, tagged, errored, logged and finished, with its ids asserted by shape rather than for non-emptiness: 32 lowercase hex characters and not all zeros for the trace, decimal for the span - header injection cross-checked two ways - against traceparent across all 128 bits, which is derived independently of GetTraceId, and against the low 64 bits the Datadog header carries in decimal - plus the delegate form, which must write the same count as the dictionary form - the single-value attribute overloads on RUM, feature flags and a logger, with DatadogAttributes.ToJava asserted directly - the session id through GetCurrentSessionIdAsync, asserted non-null; a null means the Kotlin callback never fired, which is what the Task wrapper exists to surface The harness gained async support for the last of those. The sample was still largely the MAUI project template. It now matches the iOS sample: sections for RUM, Trace and Logs with an on-screen activity log, so it is useful without a Datadog account to send to. It also carried a bug worth not shipping in a sample - OnDisappearing disposed the view scope and then immediately started a new view, leaving one open for the rest of the session, collecting every action and error that followed. 17/17 on an arm64 emulator; 94/94 package tests. Co-Authored-By: Claude Opus 4.8 --- docs/release-notes/3.12.1.2.md | 41 ++- samples/DatadogNet.Android.Example/Datadog.cs | 3 + .../DatadogNet.Android.Example/MainPage.xaml | 92 +++--- .../MainPage.xaml.cs | 265 ++++++++++++++---- .../MainActivity.cs | 6 +- .../SmokeTests.cs | 171 ++++++++++- 6 files changed, 478 insertions(+), 100 deletions(-) diff --git a/docs/release-notes/3.12.1.2.md b/docs/release-notes/3.12.1.2.md index 5ba118e..032a746 100644 --- a/docs/release-notes/3.12.1.2.md +++ b/docs/release-notes/3.12.1.2.md @@ -125,16 +125,37 @@ now has an answer in the repository rather than in someone's memory. ## Tests -**94 package-layout tests pass.** No tests were added: the additions are C# on top of the same -native artifacts, so nothing about the package shape changed. - -**The on-emulator suite in this repository does not yet cover the new members.** They were verified -end to end through [DatadogNet](https://github.com/sbokatuk/DatadogNet)'s device suite instead, -which drives them on a real emulator — `Inject`, `GetTraceId`, `GetSpanId`, -`GetCurrentSessionIdAsync` and `ToJava` are all on its paths, and the trace check asserts that the -id is 32 lowercase hex characters, that its low half matches the `x-datadog-trace-id` header the SDK -itself emitted, and that it agrees with `traceparent` across all 128 bits. 21/21 pass. Adding the -equivalent checks here is worth doing and has not been done. +**94 package-layout tests pass**, and the on-emulator suite grows from **13 checks to 17**, all +running against the packed packages. + +Until now this suite enabled Trace and stopped there — `EnablesTrace` registered a tracer and never +started a span — so the tracing path was configured and never driven. That 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. +- **Headers are injected 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. The + delegate form is driven too, and must write the same number of headers as the dictionary form. +- **The single-value attribute overloads** are driven on RUM, feature flags and a logger, with + `ToJava` asserted directly. +- **The session id** is read through `GetCurrentSessionIdAsync` and asserted non-null; a null means + the Kotlin callback never fired, which is the failure the `Task` wrapper exists to surface. + +The harness gained `async` support to do the last of those. + +## Sample + +`samples/DatadogNet.Android.Example` was still largely the MAUI project template — a hovercraft, a +click counter, and two Datadog calls. It is now the same shape as the iOS sample: sections for RUM, +Trace and Logs, and an on-screen activity log so it is useful without a Datadog account to send to. +The new **Trace** section traces an outgoing `HttpRequestMessage` end to end and records a failed +span. + +It also had a real bug worth not shipping in a sample: `OnDisappearing` disposed the view scope and +then immediately started a *new* view, leaving one open for the rest of the session — collecting +every action and error that followed. Fixed. ## Upgrading from 3.12.1.1 diff --git a/samples/DatadogNet.Android.Example/Datadog.cs b/samples/DatadogNet.Android.Example/Datadog.cs index 38121b8..fe08172 100644 --- a/samples/DatadogNet.Android.Example/Datadog.cs +++ b/samples/DatadogNet.Android.Example/Datadog.cs @@ -42,6 +42,9 @@ public static class Datadog public static Logger Logger => logger ?? throw new InvalidOperationException("Datadog.Initialize has not been called."); + /// Whether real credentials were supplied, so events actually reach Datadog. + public static bool IsConfigured => !ClientToken.StartsWith('<'); + /// /// Initialises the SDK and every feature this sample uses. /// diff --git a/samples/DatadogNet.Android.Example/MainPage.xaml b/samples/DatadogNet.Android.Example/MainPage.xaml index 385480d..d1d7a15 100644 --- a/samples/DatadogNet.Android.Example/MainPage.xaml +++ b/samples/DatadogNet.Android.Example/MainPage.xaml @@ -1,41 +1,65 @@ - + + x:Class="DatadogNet.Android.Example.MainPage" + Title="DatadogNet"> - - - -