diff --git a/.github/scripts/run-emulator-tests.sh b/.github/scripts/run-emulator-tests.sh index dc492a7..56e0d88 100755 --- a/.github/scripts/run-emulator-tests.sh +++ b/.github/scripts/run-emulator-tests.sh @@ -24,11 +24,26 @@ POLL_INTERVAL=5 REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" PROJECT="${REPO_ROOT}/tests/DatadogNet.Android.DeviceTests/DatadogNet.Android.DeviceTests.csproj" -# The .NET 9 band builds net8/net9 and the .NET 10 band builds net9/net10, so pick the SDK that -# owns the requested target framework. The SDK is resolved from the working directory, and the -# repository's global.json pins .NET 9, hence the scratch directory. +# The SDK band is chosen by the *Android API level* in the target framework, not by the .NET +# version alone, because that is what decides which workload owns the runtime packs: +# +# net8.0-android34.0 -> android 34.0.x, in the .NET 8 band +# net9.0-android35.0 -> android 35.0.x, in the .NET 9 band +# net10.0-android36.0 -> android 36.0.x, in the .NET 10 band +# +# The .NET 9 band compiles a net8 app happily - it has the API 34 *reference* packs - and then +# fails at packaging time, because it has no API 34 *runtime* packs and they cannot be restored +# from NuGet: +# +# error NETSDK1112: The runtime pack for Microsoft.Android.Runtime.34.android-x64 was not +# downloaded. Try running a NuGet restore with the RuntimeIdentifier 'android-x64'. +# +# The restore that error suggests does not help; the packs come from the workload. The SDK is +# resolved from the working directory, and this repository's global.json pins .NET 9, hence the +# scratch directory below. case "${TARGET_FRAMEWORK}" in net10.0-*) sdk_major=10 ;; + net8.0-*) sdk_major=8 ;; *) sdk_major=9 ;; esac diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ea0f61..bc40027 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,13 +18,15 @@ on: required: false default: '' type: string - e2e-target-framework: + e2e-target-frameworks: description: > - Which of the packages' target frameworks the emulator smoke test runs against. Defaults - to net10, whose assets are produced by the merge step in BuildNugets.sh and so are the - ones worth proving at runtime. + Which of the packages' target frameworks the emulator smoke tests run against, as a JSON + array. Defaults to the two extremes: net8, the oldest asset set and the one nothing else + exercises, and net10, whose assets are produced by the merge step in BuildNugets.sh and so + are the only ones that could be grafted in wrong. net9 sits between them and comes out of + the same pack pass as net8, so testing it too would buy little for the runner minutes. required: false - default: net10.0-android36.0 + default: '["net8.0-android34.0", "net10.0-android36.0"]' type: string e2e-api-level: description: Android API level the emulator smoke test boots. @@ -190,34 +192,49 @@ jobs: -p:DatadogPackageVersion="${{ inputs.version }}" ) e2e: - name: emulator smoke test + name: emulator smoke test (${{ matrix.target-framework }}) timeout-minutes: 60 needs: pack runs-on: ubuntu-latest + strategy: + # fail-fast off: when one target framework breaks it is worth knowing whether the other did + # too, since "net8 only" and "both" point at very different causes. + fail-fast: false + matrix: + target-framework: ${{ fromJSON(inputs.e2e-target-frameworks) }} steps: - uses: actions/checkout@v4 - name: Set up .NET uses: actions/setup-dotnet@v4 with: + # 8.0.x is here for the net8 matrix leg only - see the workload step below. dotnet-version: | + 8.0.x 9.0.x 10.0.x - name: Install Android workloads run: | - # The device app is built for the target framework under test, so install the workload - # for the band that owns it. global.json pins .NET 9; the app may need .NET 10. - mkdir -p "${RUNNER_TEMP}/sdk10" - ( cd "${RUNNER_TEMP}/sdk10" \ - && dotnet new globaljson --sdk-version "$(dotnet --list-sdks | grep '^10\.' | tail -1 | cut -d' ' -f1)" --force \ - && dotnet workload install android ) - dotnet workload install android + # The device app is built for the target framework under test, and the band that owns it + # is decided by the *API level* rather than the .NET version - the .NET 9 band has API 34 + # reference packs but no API 34 runtime packs, so a net8 app compiles and then fails to + # package. See run-emulator-tests.sh. Install all three bands' workloads so any target + # framework in the matrix can build. + for sdk in 8 9 10; do + version="$(dotnet --list-sdks | grep "^${sdk}\." | tail -1 | cut -d' ' -f1)" + [ -z "${version}" ] && continue + + mkdir -p "${RUNNER_TEMP}/sdk${sdk}" + ( cd "${RUNNER_TEMP}/sdk${sdk}" \ + && dotnet new globaljson --sdk-version "${version}" --force \ + && dotnet workload install android ) + done - name: Set up the Android SDK uses: android-actions/setup-android@v3 with: - packages: 'platforms;android-35 platforms;android-36' + packages: 'platforms;android-34 platforms;android-35 platforms;android-36' - name: Download packages uses: actions/download-artifact@v4 @@ -243,13 +260,13 @@ jobs: force-avd-creation: false emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim disable-animations: true - script: ./.github/scripts/run-emulator-tests.sh "${{ inputs.version }}" "${{ inputs.e2e-target-framework }}" + script: ./.github/scripts/run-emulator-tests.sh "${{ inputs.version }}" "${{ matrix.target-framework }}" - name: Upload emulator logs if: always() uses: actions/upload-artifact@v4 with: - name: emulator-test-logs + name: emulator-test-logs-${{ matrix.target-framework }} path: emulator-tests.log if-no-files-found: ignore retention-days: 7 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/docs/release-notes/3.12.1.2.md b/docs/release-notes/3.12.1.2.md new file mode 100644 index 0000000..6d9954c --- /dev/null +++ b/docs/release-notes/3.12.1.2.md @@ -0,0 +1,197 @@ +## 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**, 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. + +### The e2e now runs on two bands + +It ran against `net10.0-android36.0` only. It is now a matrix over **net8 and net10** — the two +extremes, matching what the DatadogNet façade does. net8 is the oldest asset set and nothing else +exercised it; net10's assets come out of the merge step in `BuildNugets.sh` and so are the only ones +that could be grafted in wrong. net9 sits between them and comes out of the same pack pass as net8. + +Standing net8 up found three real problems, none of which anything else would have caught: + +- **The SDK band has to be chosen by Android API level, not .NET version.** The .NET 9 band has API + 34 *reference* packs but no API 34 *runtime* packs, so a net8 app compiles and then fails to + package with `NETSDK1112`. `run-emulator-tests.sh` now maps `net8.0-* → .NET 8`. +- **`NU1107` on `Xamarin.AndroidX.SavedState`.** Navigation.Common wants `>= 1.4.0` while + SavedState.Ktx pins `[1.3.2, 1.3.3)`. The .NET 9 and 10 SDKs resolve the higher version and warn; + the .NET 8 SDK refuses. Pinned to 1.4.0 **on the net8 head only** — pinning it everywhere is a + downgrade on net10, where Fragment 1.8.9.3 wants `>= 1.5.0.1`, and fails the other way with + `NU1605`. Each band gets what its own graph settles on, which is what a consumer on that band + would get. **Every net8 consumer of these packages needs this pin**, so it is worth knowing about + even if you never run the e2e. +- **`XA1018`: no `AndroidManifest.xml`.** The .NET 9 and 10 Android SDKs generate one; the .NET 8 + SDK defaults `$(AndroidManifest)` to the project root and fails because nothing is there. + +One honest limitation: **`DatadogNet.SessionReplayCompose.Android` is excluded from the net8 leg**, +because it genuinely has no net8 asset — its last net8 build pins an AndroidX `SavedState` old +enough to collide with the rest of the graph. The module check is compiled not to look for the +Compose class there, so it keeps failing for the right reasons rather than being quietly weakened. + +17/17 on both legs. + +## 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 +- ++ +``` + +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. 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"> - - - -