diff --git a/.github/actions/select-xcode/action.yml b/.github/actions/select-xcode/action.yml
index 6f85417..96ccc23 100644
--- a/.github/actions/select-xcode/action.yml
+++ b/.github/actions/select-xcode/action.yml
@@ -16,8 +16,11 @@ runs:
run: |
set -uo pipefail
- # The packages advertise net10.0-ios26.0, and .NET for iOS only builds that target
- # framework against an Xcode carrying the matching iOS SDK:
+ # A copy of DatadogNet.iOS's select-xcode action - keep the two in sync. It selects by
+ # the *iOS* SDK line deliberately even here: Mac Catalyst builds against the iOS SDK, so
+ # the packages' net10.0-maccatalyst26.0 has exactly the same Xcode requirement as the iOS
+ # repository's net10.0-ios26.0, and .NET only builds either against an Xcode carrying the
+ # matching SDK:
#
# error : This version of .NET for iOS (26.0.11017) requires Xcode 26.0.
# The current version of Xcode is 26.5.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 479bcb2..a4c71e4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -45,6 +45,9 @@ jobs:
steps:
- uses: actions/checkout@v4
+ - name: Check README versions
+ run: ./build/CheckReadmeVersions.sh
+
- name: Select Xcode
uses: ./.github/actions/select-xcode
@@ -103,6 +106,17 @@ jobs:
if: steps.xcframeworks.outputs.cache-hit != 'true'
run: ./build/BuildXcFrameworks.sh "${{ steps.native.outputs.version }}"
+ # The dSYMs are the only symbolication data these binaries will ever have - Datadog does not
+ # publish Catalyst builds, so nobody else holds them. They live in libs/dsyms (cached with
+ # the frameworks, never packed) and the release workflow attaches them to the GitHub release.
+ - name: Upload dSYMs
+ uses: actions/upload-artifact@v4
+ with:
+ name: dsyms
+ path: libs/dsyms
+ if-no-files-found: error
+ retention-days: 7
+
- name: Pack all packages
run: ./build/BuildNugets.sh "${{ inputs.version }}" "${{ steps.native.outputs.version }}"
@@ -164,3 +178,51 @@ jobs:
--configuration Debug \
-p:RuntimeIdentifier=maccatalyst-arm64 \
-p:DatadogPackageVersion="${{ inputs.version }}"
+
+ # The whole model of this repository rests on the binding sources being verbatim copies of
+ # DatadogNet.iOS's - the Catalyst head of the façade compiles against them on that assumption.
+ # This job turns "do not edit the copies here" from prose into a failing check: it checks out
+ # DatadogNet.iOS at the commit build/ios-bindings-source.txt records (written by the sync
+ # script), re-runs the sync, and fails on any difference. A sync recorded from an uncommitted
+ # iOS tree disarms the guard with a warning until a clean sync replaces it.
+ binding-drift:
+ name: binding sources match DatadogNet.iOS
+ timeout-minutes: 10
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Read the recorded iOS source commit
+ id: source
+ run: |
+ file=build/ios-bindings-source.txt
+ if [ ! -f "${file}" ]; then
+ echo "::error::${file} is missing - run build/SyncBindingsFromiOS.sh from a committed DatadogNet.iOS checkout"
+ exit 1
+ fi
+ ref=$(head -1 "${file}" | cut -d' ' -f1)
+ if grep -q 'dirty' "${file}"; then
+ echo "::warning::the last binding sync was taken from an uncommitted DatadogNet.iOS tree, so the drift guard is disarmed. Re-run build/SyncBindingsFromiOS.sh once the iOS changes are committed."
+ echo "armed=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "armed=true" >> "$GITHUB_OUTPUT"
+ fi
+ echo "ref=${ref}" >> "$GITHUB_OUTPUT"
+
+ - name: Check out DatadogNet.iOS at the recorded commit
+ if: steps.source.outputs.armed == 'true'
+ uses: actions/checkout@v4
+ with:
+ repository: sbokatuk/DatadogNet.iOS
+ ref: ${{ steps.source.outputs.ref }}
+ path: .ios-sync
+
+ - name: Re-run the sync and fail on any difference
+ if: steps.source.outputs.armed == 'true'
+ run: |
+ ./build/SyncBindingsFromiOS.sh "${GITHUB_WORKSPACE}/.ios-sync"
+ if ! git diff --exit-code -- src/; then
+ echo "::error::binding sources differ from DatadogNet.iOS@${{ steps.source.outputs.ref }}. They are verbatim copies by design - make the change in DatadogNet.iOS, re-run build/SyncBindingsFromiOS.sh, and commit both."
+ exit 1
+ fi
+ echo "Binding sources are byte-identical to DatadogNet.iOS@${{ steps.source.outputs.ref }}."
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b578d2e..372cdbd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -85,6 +85,12 @@ jobs:
name: nuget-packages
path: artifacts
+ - name: Download dSYMs
+ uses: actions/download-artifact@v4
+ with:
+ name: dsyms
+ path: dsyms
+
# nuget.org is published to first: a GitHub release that links to packages which failed to
# upload would be worse than a release created a moment late.
# Deliberately immediately before the push: the issued key is valid for one hour, and each
@@ -140,6 +146,10 @@ jobs:
echo
echo "Bound against [dd-sdk-ios \`${native}\`](https://github.com/DataDog/dd-sdk-ios/releases/tag/${native}), built from source for Mac Catalyst, targeting \`net8.0-maccatalyst18.0\`, \`net9.0-maccatalyst18.0\` and \`net10.0-maccatalyst26.0\`."
echo
+ echo "\`dsyms-${VERSION}.zip\` below carries the dSYMs for these exact binaries - Datadog publishes"
+ echo "no Catalyst builds, so this release is the only place they exist. Upload them to Datadog for"
+ echo "crash symbolication."
+ echo
echo "> The first three components of \`${VERSION}\` are the dd-sdk-ios version; the fourth is this"
echo "> repository's binding revision, which advances when the bindings or packaging change while"
echo "> the native binaries stay put."
@@ -178,13 +188,18 @@ jobs:
cat release-notes.md >> "$GITHUB_STEP_SUMMARY"
+ - name: Package dSYMs for the release
+ run: (cd dsyms && zip -qry "../dsyms-${VERSION}.zip" .)
+
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
# The packages themselves are not attached: nuget.org is the distribution channel, and
- # the notes link to every published package instead.
+ # the notes link to every published package instead. The dSYMs are attached, because
+ # this release is the only place symbolication data for these binaries exists.
gh release create "${GITHUB_REF_NAME}" \
+ "dsyms-${VERSION}.zip" \
--title "${VERSION}" \
--notes-file release-notes.md \
${{ needs.version.outputs.prerelease == 'true' && '--prerelease' || '' }}
diff --git a/Directory.Build.props b/Directory.Build.props
index 263d58c..6888024 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,7 +18,7 @@
script needs Xcode rather than just curl.
-->
3.14.0
- 1
+ 2
$(DatadogNativeVersion).$(DatadogBindingRevision)
+
+ $(MSBuildThisFileDirectory)docs/release-notes/$(VersionPrefix).md
+ $([System.IO.File]::ReadAllText('$(DatadogReleaseNotesFile)'))
+ https://github.com/sbokatuk/DatadogNet.Mac/releases/tag/v$(VersionPrefix)
+
+
diff --git a/README.md b/README.md
index f73ff57..839fcd0 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,11 @@ Every package targets `net8.0-maccatalyst18.0`, `net9.0-maccatalyst18.0` and
`net10.0-maccatalyst26.0`, with `SupportedOSPlatformVersion` 15.0 (macOS 12) - the floor both the
.NET 9 maccatalyst workload and Datadog's own Catalyst support statement impose.
+> **net8 sunset.** The net8 head is already past its platform support window — the net8 mobile
+> workloads left support with MAUI 8 on 14 May 2025 — and ships for the apps that still target
+> it. So the decision does not persist by inertia: **the net8 head is dropped in the first
+> release after .NET 8 itself leaves support on 10 November 2026**, in step with DatadogNet.iOS.
+
## Installing
```sh
@@ -69,8 +74,8 @@ bindings:
```xml
-
-
+
+
```
@@ -117,7 +122,11 @@ make that possible; the script documents both.
The binding definitions (`ApiDefinitions.cs`, `StructsAndEnums.cs`, `Additions/`) are **verbatim
copies from DatadogNet.iOS**, refreshed by
[build/SyncBindingsFromiOS.sh](build/SyncBindingsFromiOS.sh). Do not edit them here: fix them in
-the iOS repository and re-sync, so the two platforms cannot drift.
+the iOS repository and re-sync, so the two platforms cannot drift. That is enforced, not asked
+politely: the sync records the iOS commit it copied from in `build/ios-bindings-source.txt`, and
+CI's `binding-drift` job re-runs the sync against exactly that commit and fails on any
+difference. (`shims/` is deliberately outside the sync — see the script's header for what must
+happen if the iOS Flags shim ever ships.)
### Layout
diff --git a/build/BuildNugets.sh b/build/BuildNugets.sh
index a2e42a0..29e9362 100755
--- a/build/BuildNugets.sh
+++ b/build/BuildNugets.sh
@@ -16,7 +16,8 @@ set -e
# Packages are written to ../artifacts.
#
# Each .NET SDK's maccatalyst workload supports only two target frameworks - the .NET 9 band
-# builds net8/net9, the .NET 10 band builds net9/net10 - so this runs two passes and merges them,
+# builds net8/net9, and the .NET 10 band contributes net10 (Datadog.Binding.props points its pass
+# at net10.0-maccatalyst26.0 alone) - so this runs two passes and merges them,
# exactly as DatadogNet.iOS does. The repository's global.json pins the .NET 9 SDK, so the second
# pass is invoked from a scratch directory carrying its own global.json, since the SDK is resolved
# from the working directory.
diff --git a/build/BuildXcFrameworks.sh b/build/BuildXcFrameworks.sh
index 4c1d247..7778082 100755
--- a/build/BuildXcFrameworks.sh
+++ b/build/BuildXcFrameworks.sh
@@ -64,7 +64,11 @@ fi
DD_REPO="$WORK/dd-sdk-ios"
OTEL_REPO="$WORK/opentelemetry-swift-packages"
ARCHIVES="$WORK/archives"
-DSYMS="$WORK/dsyms"
+# Under libs/, not under $WORK: $WORK is a mktemp directory removed on exit, and dSYMs that only
+# ever exist there cannot be attached to a release - which is the one thing they are for. Next to
+# the frameworks they survive the build, ride the same CI cache, and are never packed (the binding
+# projects reference libs/.xcframework by name; nothing globs libs/).
+DSYMS="$LIBS/dsyms"
echo "==> building dd-sdk-ios $DATADOG_VERSION + OpenTelemetryApi $OTEL_VERSION for Mac Catalyst"
echo " work directory: $WORK"
@@ -80,6 +84,39 @@ if [ ! -d "$OTEL_REPO" ]; then
git clone --depth 1 --branch "$OTEL_VERSION" https://github.com/DataDog/opentelemetry-swift-packages.git "$OTEL_REPO"
fi
+# ---------------------------------------------------------------------------------------------
+# Guard the second pin. DatadogOtelVersion is maintained by hand in Directory.Build.props and
+# must match the OpenTelemetryApi version the checked-out dd-sdk-ios tag pins in its
+# Cartfile.resolved - bumping the native version and forgetting the OTEL line would otherwise
+# silently build the wrong OpenTelemetryApi. Checked here, against the actual checkout, so the
+# mismatch fails the build instead of shipping.
+# ---------------------------------------------------------------------------------------------
+
+CARTFILE="$DD_REPO/Cartfile.resolved"
+if [ "$DATADOG_SKIP_OTEL_CHECK" = "1" ]; then
+ echo "==> skipping the DatadogOtelVersion check (DATADOG_SKIP_OTEL_CHECK=1)"
+else
+ resolved=""
+ if [ -f "$CARTFILE" ]; then
+ resolved=$(grep -i 'opentelemetry' "$CARTFILE" | grep -oE '"[0-9][A-Za-z0-9._-]*"' | tail -1 | tr -d '"')
+ fi
+ if [ -z "$resolved" ]; then
+ echo "error: could not read the OpenTelemetryApi version from $CARTFILE." >&2
+ echo " dd-sdk-ios $DATADOG_VERSION no longer pins it there. Find where the new tag pins" >&2
+ echo " it, update DatadogOtelVersion in Directory.Build.props, and update this check." >&2
+ echo " DATADOG_SKIP_OTEL_CHECK=1 skips it if the pin has genuinely moved." >&2
+ exit 1
+ fi
+ if [ "$resolved" != "$OTEL_VERSION" ]; then
+ echo "error: DatadogOtelVersion is $OTEL_VERSION, but dd-sdk-ios $DATADOG_VERSION pins OpenTelemetryApi $resolved." >&2
+ echo " Update DatadogOtelVersion in Directory.Build.props to $resolved. Building with a" >&2
+ echo " mismatched pin links the Datadog frameworks against one OpenTelemetryApi and" >&2
+ echo " ships another." >&2
+ exit 1
+ fi
+ echo "==> DatadogOtelVersion $OTEL_VERSION matches dd-sdk-ios $DATADOG_VERSION's Cartfile.resolved"
+fi
+
# ---------------------------------------------------------------------------------------------
# Patch the Datadog Xcode project for Catalyst. Two things stand between the source - which
# compiles for Catalyst - and an archive that actually builds:
@@ -107,21 +144,56 @@ import sys
path = sys.argv[1]
text = open(path).read()
+# Every patch below is a text replacement against an Xcode-generated file, and a replacement
+# whose pattern has drifted out from under it is a silent no-op that only surfaces later, as a
+# cryptic xcodebuild failure. So each patch asserts it either changed something or found the
+# already-patched state (a reused DATADOG_BUILD_DIR checkout is patched twice without harm), and
+# anything else fails loudly here, naming the patch that missed.
+failures = []
+
+supports_no = text.count("SUPPORTS_MACCATALYST = NO;")
+supports_yes = text.count("SUPPORTS_MACCATALYST = YES;")
text = text.replace("SUPPORTS_MACCATALYST = NO;", "SUPPORTS_MACCATALYST = YES;")
+if supports_no == 0 and supports_yes == 0:
+ failures.append(
+ "SUPPORTS_MACCATALYST: found neither '= NO;' to patch nor an existing '= YES;'. "
+ "Upstream has moved or reformatted the setting (an .xcconfig, perhaps); without it the "
+ "archive step rejects the Catalyst destination outright.")
# The singular form appears on link-phase entries and cannot carry two values.
+singular = text.count("platformFilter = ios;")
+singular_patched = text.count("platformFilters = (ios, maccatalyst, );")
text = text.replace("platformFilter = ios;", "platformFilters = (ios, maccatalyst, );")
-# List form, both the inline and the one-value-per-line layout. Idempotent, so a reused
-# DATADOG_BUILD_DIR checkout is patched twice without harm.
+# List form, both the inline and the one-value-per-line layout.
+lists_patched = 0
def add_maccatalyst(match):
+ global lists_patched
body = match.group(1)
if "maccatalyst" in body or "ios" not in body:
return match.group(0)
+ lists_patched += 1
return match.group(0).replace("ios,", "ios, maccatalyst,", 1)
text = re.sub(r"platformFilters = \(([^)]*)\)", add_maccatalyst, text)
+if singular + singular_patched + lists_patched == 0 and "maccatalyst" not in text:
+ failures.append(
+ "platformFilter(s): nothing was patched and no filter mentions maccatalyst. Catalyst "
+ "would silently drop inter-framework dependencies and the archive would die with "
+ "\"unable to resolve module dependency: 'DatadogInternal'\". If upstream has removed "
+ "platform filters entirely this check can go; otherwise the patterns need updating.")
+
+if failures:
+ print("error: the pbxproj patches no longer match upstream's project file:", file=sys.stderr)
+ for failure in failures:
+ print(" * " + failure, file=sys.stderr)
+ print(" Review the patch block in build/BuildXcFrameworks.sh against " + path, file=sys.stderr)
+ sys.exit(1)
+
+print(" patched SUPPORTS_MACCATALYST on %d target(s), %d singular and %d list platform filter(s)"
+ % (supports_no, singular, lists_patched))
+
open(path, "w").write(text)
EOF
@@ -234,5 +306,5 @@ echo
echo "==> built into $LIBS:"
ls "$LIBS"
echo
-echo " dSYMs (for a GitHub release / Datadog symbolication) are in:"
+echo " dSYMs (attached to the GitHub release, for Datadog crash symbolication) are in:"
echo " $DSYMS"
diff --git a/build/CheckReadmeVersions.sh b/build/CheckReadmeVersions.sh
new file mode 100755
index 0000000..ecb271f
--- /dev/null
+++ b/build/CheckReadmeVersions.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+# Fails when README.md pins a package version that is not the one this repository currently
+# builds. The install snippets are copy-paste starting points, and a hardcoded version there goes
+# stale silently on every release - it would go stale the same way the sibling repositories' snippets did. Running
+# this in CI makes the version bump before a release drag the README along with it.
+#
+# What is checked: every pin, and the
+# device-check example (run-simulator-tests.sh ...). Prose that explains the version
+# *scheme* ("3.14.0.1 wraps dd-sdk-ios 3.14.0") is deliberately not checked -
+# it describes the format, not the current release.
+set -eu
+
+root="$(cd "$(dirname "$0")/.." && pwd)"
+readme="$root/README.md"
+props="$root/Directory.Build.props"
+
+prop() {
+ sed -n "s/.*<$1>\(.*\)<\/$1>.*/\1/p" "$props" | head -1
+}
+
+version="$(prop DatadogNativeVersion).$(prop DatadogBindingRevision)"
+
+bad=0
+
+old_ifs=$IFS
+IFS='
+'
+for pin in $(grep -oE 'Include="DatadogNet[^"]*" +Version="[0-9][^"]*"' "$readme"); do
+ id=$(printf '%s' "$pin" | sed -E 's/Include="([^"]*)".*/\1/')
+ ver=$(printf '%s' "$pin" | sed -E 's/.*Version="([^"]*)"/\1/')
+ if [ "$ver" != "$version" ]; then
+ echo "README pins $id $ver, but the current version is $version" >&2
+ bad=1
+ fi
+done
+
+for token in $(grep -oE 'run-(simulator|emulator)-tests\.sh +[0-9][0-9.]*' "$readme" | grep -oE '[0-9][0-9.]*$'); do
+ if [ "$token" != "$version" ]; then
+ echo "README runs the device checks at $token, but the current version is $version" >&2
+ bad=1
+ fi
+done
+IFS=$old_ifs
+
+if [ "$bad" -ne 0 ]; then
+ echo "CheckReadmeVersions: README.md is stale - update the versions above (current: $version)" >&2
+ exit 1
+fi
+
+echo "CheckReadmeVersions: README.md agrees with Directory.Build.props ($version)"
diff --git a/build/SyncBindingsFromiOS.sh b/build/SyncBindingsFromiOS.sh
index 450a23e..be211fc 100755
--- a/build/SyncBindingsFromiOS.sh
+++ b/build/SyncBindingsFromiOS.sh
@@ -18,6 +18,18 @@ set -e
# Copies ApiDefinitions.cs, StructsAndEnums.cs and Additions/ for every module. Run it after
# bumping DatadogNativeVersion to whatever the iOS repository binds, diff, and commit. Do not
# edit the copied files here - edit them in DatadogNet.iOS and re-sync.
+#
+# The commit the copies were taken from is recorded in build/ios-bindings-source.txt, and CI's
+# binding-drift job re-runs this sync against exactly that commit and fails on any difference -
+# so "do not edit here" is an enforced invariant, not a convention. A sync taken from an iOS
+# checkout with uncommitted changes records a dirty marker instead, which disarms the guard
+# (loudly) until a clean sync records a real commit.
+#
+# shims/ is deliberately NOT synced. The iOS repository's shims/DatadogFlagsObjc is an unshipped
+# prototype whose build recipe targets iOS; if it ever ships - giving DatadogFlags a real ObjC
+# surface - this script must grow shims/ handling with a Catalyst build target at the same time,
+# or the next sync will copy an ApiDefinitions.cs that references a shim this repository cannot
+# build.
cd "$(dirname "$0")"
@@ -56,5 +68,23 @@ for module in $MODULES; do
echo "synced DatadogNet.$module.Mac"
done
-echo
+# Record where the copies came from, for the CI drift guard.
+REF_FILE="$ROOT/build/ios-bindings-source.txt"
+if git -C "$IOS_REPO" rev-parse HEAD >/dev/null 2>&1; then
+ sha=$(git -C "$IOS_REPO" rev-parse HEAD)
+ if [ -n "$(git -C "$IOS_REPO" status --porcelain -- src/ 2>/dev/null)" ]; then
+ printf '%s dirty\n' "$sha" > "$REF_FILE"
+ echo
+ echo "WARNING: the iOS checkout has uncommitted binding changes; recorded $sha as dirty."
+ echo " The CI drift guard is disarmed until a sync from a committed tree."
+ else
+ printf '%s\n' "$sha" > "$REF_FILE"
+ echo
+ echo "Recorded DatadogNet.iOS@$sha in build/ios-bindings-source.txt."
+ fi
+else
+ echo
+ echo "WARNING: $IOS_REPO is not a git checkout; build/ios-bindings-source.txt not updated."
+fi
+
echo "Review with 'git diff' before committing."
diff --git a/build/ios-bindings-source.txt b/build/ios-bindings-source.txt
new file mode 100644
index 0000000..ec2cbd1
--- /dev/null
+++ b/build/ios-bindings-source.txt
@@ -0,0 +1 @@
+bf6c3f449fded41993521484ebd43e30f3489fde
diff --git a/build/merge-packages.py b/build/merge-packages.py
index c1f0018..39060fa 100644
--- a/build/merge-packages.py
+++ b/build/merge-packages.py
@@ -1,22 +1,24 @@
#!/usr/bin/env python3
"""Merge target-framework assets from one set of NuGet packages into another.
-No single .NET SDK can build net8.0-ios, net9.0-ios and net10.0-ios together: each SDK's iOS
-workload supports only the current target framework and the previous one. The packages are
-therefore built in two passes (see BuildNugets.sh) and merged here into one package per id.
+A copy of DatadogNet.iOS's build/merge-packages.py - keep the two in sync; the logic is
+target-framework-agnostic and this file should never diverge beyond this docstring.
+
+Here it merges the Mac Catalyst passes: no single .NET SDK builds net8.0-maccatalyst,
+net9.0-maccatalyst and net10.0-maccatalyst together - each SDK's maccatalyst workload supports
+only the current target framework and the previous one - so BuildNugets.sh packs twice and this
+merges the results into one package per id.
For every package in PRIMARY, any lib// tree that exists in the matching ADDITIONAL package
but not in PRIMARY is copied across, and any ADDITIONAL declares and
PRIMARY does not is lifted across too. Everything else comes from PRIMARY unchanged.
-The two are tracked independently on purpose: DatadogNet.Objc.iOS is a dependency-only
-meta-package with no lib/ folders, so deciding what to merge from lib/ alone left it declaring no
-dependencies at all for net10.
-
-The dependency group is copied rather than synthesised as an empty one. Ten of the eleven packages
-here declare dependencies on their siblings, and an empty group would tell NuGet that a net10.0-ios
-consumer needs none of them - so DatadogNet.Objc.iOS would restore on net10 without DatadogCore,
-DatadogRUM or any of the rest, and the app would fail at link time with undefined Swift symbols.
+The lib/ trees and the dependency groups are tracked independently, and the dependency group is
+copied rather than synthesised as an empty one: every package here declares same-version sibling
+dependencies, and an empty group would tell NuGet a net10 consumer needs none of them. (In the
+iOS repository the meta-package DatadogNet.Objc.iOS - dependencies, no lib/ - is what made both
+choices load-bearing; this repository has no meta-package, but the reasoning holds for the
+grafted dependency groups all the same.)
Usage: merge-packages.py PRIMARY_DIR ADDITIONAL_DIR OUTPUT_DIR
"""
diff --git a/docs/release-notes/3.14.0.2.md b/docs/release-notes/3.14.0.2.md
new file mode 100644
index 0000000..8c23c78
--- /dev/null
+++ b/docs/release-notes/3.14.0.2.md
@@ -0,0 +1,57 @@
+# 3.14.0.2
+
+Binding-only release. The native SDK is unchanged — still
+[dd-sdk-ios 3.14.0](https://github.com/DataDog/dd-sdk-ios/releases/tag/3.14.0), built from source
+for Mac Catalyst — and so are the package ids, namespaces and API. The fourth component advances
+for the changes below, which came out of a full ergonomics review of the four DatadogNet
+repositories.
+
+## dSYMs are no longer thrown away
+
+The build always produced dSYMs and then lost them with the temporary directory — and for
+Catalyst binaries that Datadog does not publish, they are the only symbolication data that will
+ever exist. They now land in `libs/dsyms`, ride the CI cache with the frameworks, and every
+release from this one on carries a `dsyms-.zip` attachment. Upload it to Datadog to
+symbolicate crash reports from apps on these packages.
+
+## The fragile seams now fail loudly
+
+- **The pbxproj patches assert they applied.** Building dd-sdk-ios for Catalyst requires patching
+ `SUPPORTS_MACCATALYST` and the platform filters in an Xcode-generated project file. Each patch
+ now verifies it changed something (or found the already-patched state) and fails naming the
+ patch that missed — so an upstream project-file reformat breaks the *patch step* with a clear
+ message instead of the *archive step* with "unable to resolve module dependency".
+- **`DatadogOtelVersion` is checked, not trusted.** The second manually-maintained pin must match
+ the OpenTelemetryApi version the checked-out dd-sdk-ios tag records in its `Cartfile.resolved`;
+ a bump that forgets it now fails the build instead of silently building the wrong OTEL.
+- **The verbatim-copy invariant is enforced.** The binding sources are copies of
+ DatadogNet.iOS's; the sync script now records the exact iOS commit it copied from
+ (`build/ios-bindings-source.txt`), and CI's `binding-drift` job re-runs the sync against that
+ commit and fails on any difference — a direct edit here, or a forgotten re-sync, is a red
+ build rather than a latent iOS/Catalyst behaviour split.
+
+## Synced binding improvements
+
+Re-synced from DatadogNet.iOS (see its `3.14.0.3` notes for the detail): doc summaries on the
+main entry types reach IntelliSense here too, the `DDLogger` null-attributes behaviour is
+documented as deliberate — a `[NullAllowed]` would trade a managed exception for a native crash —
+and the trim/AOT analyzers are on with the surface analysing clean.
+
+## Smaller improvements
+
+- **nuget.org now shows these notes**: `docs/release-notes/.md` is packed into
+ `PackageReleaseNotes`, with the releases page as fallback.
+- CI fails when the README's versions drift from `Directory.Build.props`.
+- The tooling copied from DatadogNet.iOS (`merge-packages.py`, the `select-xcode` action,
+ `BuildNugets.sh`) no longer carries that repository's comments verbatim: each file now says
+ what is true *here* and marks itself as a keep-in-sync copy.
+
+## net8 sunset
+
+Stated policy, so the decision does not persist by inertia: the `net8.0-maccatalyst18.0` head is
+dropped in the first release after .NET 8 leaves support on **10 November 2026**, in step with
+DatadogNet.iOS.
+
+## Upgrading from 3.14.0.1
+
+Nothing to change: same native SDK, same API.
diff --git a/src/Datadog.Binding.props b/src/Datadog.Binding.props
index 0509cec..fec0265 100644
--- a/src/Datadog.Binding.props
+++ b/src/Datadog.Binding.props
@@ -77,6 +77,15 @@
here with the binding definitions themselves.
-->
$(NoWarn);CS1591;CS0108;CS0109
+
+
+ true
+ true
diff --git a/src/DatadogNet.Core.Mac/ApiDefinitions.cs b/src/DatadogNet.Core.Mac/ApiDefinitions.cs
index a8b5848..867c0ee 100644
--- a/src/DatadogNet.Core.Mac/ApiDefinitions.cs
+++ b/src/DatadogNet.Core.Mac/ApiDefinitions.cs
@@ -38,6 +38,7 @@ interface DDServerDateProvider
}
// @interface DDConfiguration
+ /// What DDDatadog.InitializeWithConfiguration starts: client token, environment, site, service, and batching/upload behaviour.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDConfiguration
@@ -102,6 +103,8 @@ interface DDCrossPlatformExtension
}
// @interface DDDatadog
+ /// The Datadog SDK entry point: initialisation, user and account info, tracking consent, verbosity and data clearing.
+ /// Initialise once, as early as possible, with InitializeWithConfiguration. Docs: https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/setup/ios/
[BaseType (typeof(NSObject))]
interface DDDatadog
{
@@ -215,6 +218,7 @@ interface DDSharedContext
}
// @interface DDSite
+ /// The Datadog site the SDK uploads to, as static factories: Us1(), Us3(), Us5(), Eu1(), Ap1(), Ap2(), Us1_fed(). Match your organisation's region - the wrong site is the most common reason nothing appears in Datadog.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDSite
@@ -253,6 +257,7 @@ interface DDSite
}
// @interface DDTrackingConsent
+ /// Tracking consent, as static factories: Granted(), NotGranted(), and Pending() - which collects and holds data on the device until consent is decided.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDTrackingConsent
@@ -271,6 +276,7 @@ interface DDTrackingConsent
}
// @interface DDURLSessionInstrumentation
+ /// Automatic RUM resources and distributed tracing for NSUrlSession. Prefer the generic Enable<TDelegate>() from the Additions layer over passing a raw class handle.
[BaseType (typeof(NSObject))]
interface DDURLSessionInstrumentation
{
diff --git a/src/DatadogNet.CrashReporting.Mac/ApiDefinitions.cs b/src/DatadogNet.CrashReporting.Mac/ApiDefinitions.cs
index 6c9e3d4..bf518f8 100644
--- a/src/DatadogNet.CrashReporting.Mac/ApiDefinitions.cs
+++ b/src/DatadogNet.CrashReporting.Mac/ApiDefinitions.cs
@@ -6,6 +6,7 @@
namespace DatadogCrashReporting
{
// @interface DDCrashReporter
+ /// KSCrash-based crash capture. Enable() after core initialisation; crashes are reported through RUM on the next launch, so RUM must be enabled too.
[BaseType (typeof(NSObject))]
interface DDCrashReporter
{
diff --git a/src/DatadogNet.Logs.Mac/ApiDefinitions.cs b/src/DatadogNet.Logs.Mac/ApiDefinitions.cs
index 4cb98d6..a8fff60 100644
--- a/src/DatadogNet.Logs.Mac/ApiDefinitions.cs
+++ b/src/DatadogNet.Logs.Mac/ApiDefinitions.cs
@@ -6,6 +6,7 @@
namespace DatadogLogs
{
// @interface DDLogEvent
+ /// One log entry, as the event mapper sees it before upload.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDLogEvent
@@ -300,6 +301,15 @@ interface DDLogEventUserInfo
}
// @interface DDLogger
+ /// Sends logs to Datadog, one method group per level. The Additions layer's Create(...) is the ergonomic factory, and its Log(level, message, exception, attributes) accepts where these raw methods, faithful to upstream, do not.
+ //
+ // The error/attributes parameters on the per-level methods below carry no [NullAllowed], and
+ // that is deliberate, not an omission: upstream declares them _Nonnull, and the Swift
+ // implementation takes a non-optional [String: Any] (Logs+objc.swift), so a nil smuggled
+ // through a [NullAllowed] binding would trap in the bridging thunk - a native crash instead of
+ // today's managed ArgumentNullException. A caller with no attributes uses the message-only
+ // overload, or the Additions layer's Log(level, message, exception?, attributes?), which
+ // accepts null for both and converts.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDLogger
@@ -386,6 +396,7 @@ interface DDLogger
}
// @interface DDLoggerConfiguration
+ /// How one logger reports: service, name, network info, RUM and Trace bundling, remote sampling and threshold, console echo.
[BaseType (typeof(NSObject))]
interface DDLoggerConfiguration
{
@@ -418,6 +429,7 @@ interface DDLoggerConfiguration
}
// @interface DDLogs
+ /// Enables log collection. EnableWith once, after core initialisation and before creating loggers; the static attribute methods apply to every logger.
[BaseType (typeof(NSObject))]
interface DDLogs
{
@@ -447,6 +459,7 @@ interface DDLogs
}
// @interface DDLogsConfiguration
+ /// Options for DDLogs.EnableWith: a custom endpoint, and SetEventMapper - the on-device hook that can redact a log or return to drop it.
[BaseType (typeof(NSObject))]
interface DDLogsConfiguration
{
diff --git a/src/DatadogNet.RUM.Mac/ApiDefinitions.cs b/src/DatadogNet.RUM.Mac/ApiDefinitions.cs
index e2cc4f7..32ae041 100644
--- a/src/DatadogNet.RUM.Mac/ApiDefinitions.cs
+++ b/src/DatadogNet.RUM.Mac/ApiDefinitions.cs
@@ -120,6 +120,7 @@ interface DDDefaultSwiftUIRUMViewsPredicate : DDSwiftUIRUMViewsPredicate
}
// @interface DDDefaultUIKitRUMActionsPredicate
+ /// The default UIKit action-tracking predicate: taps become RUM actions named after the control.
[BaseType (typeof(NSObject))]
interface DDDefaultUIKitRUMActionsPredicate : DDUIKitRUMActionsPredicate
{
@@ -128,6 +129,7 @@ interface DDDefaultUIKitRUMActionsPredicate : DDUIKitRUMActionsPredicate
}
// @interface DDDefaultUIKitRUMViewsPredicate
+ /// The default UIKit view-tracking predicate: roughly one RUM view per view controller.
[BaseType (typeof(NSObject))]
interface DDDefaultUIKitRUMViewsPredicate : DDUIKitRUMViewsPredicate
{
@@ -170,6 +172,7 @@ interface DDProfilingOptions
}
// @interface DDRUM
+ /// Enables Real User Monitoring. EnableWith(DDRUMConfiguration) once, after core initialisation. Docs: https://docs.datadoghq.com/real_user_monitoring/
[BaseType (typeof(NSObject))]
interface DDRUM
{
@@ -744,6 +747,7 @@ interface DDRUMActionEventView
}
// @interface DDRUMConfiguration
+ /// RUM options: the application id, sampling, the UIKit view and action tracking predicates, frustrations, vitals and long tasks.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDRUMConfiguration
@@ -2132,6 +2136,7 @@ interface DDRUMLongTaskEventView
}
// @interface DDRUMMonitor
+ /// Reports RUM views, actions, resources and errors; Shared() is the instance. The Additions layer adds using-scoped views, Dictionary<string, object?> attributes, exception overloads and GetCurrentSessionIdAsync.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDRUMMonitor
diff --git a/src/DatadogNet.SessionReplay.Mac/ApiDefinitions.cs b/src/DatadogNet.SessionReplay.Mac/ApiDefinitions.cs
index 6b83230..e11527b 100644
--- a/src/DatadogNet.SessionReplay.Mac/ApiDefinitions.cs
+++ b/src/DatadogNet.SessionReplay.Mac/ApiDefinitions.cs
@@ -7,6 +7,7 @@
namespace DatadogSessionReplay
{
// @interface DDSessionReplay
+ /// Enables Session Replay. Requires RUM. EnableWith(DDSessionReplayConfiguration); recording control is on the instance.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDSessionReplay
@@ -37,6 +38,7 @@ interface DDSessionReplay
}
// @interface DDSessionReplayConfiguration
+ /// Replay options: the sample rate and the three on-device privacy levels - required arguments, so what is redacted before upload is always an explicit choice.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDSessionReplayConfiguration
@@ -70,6 +72,7 @@ interface DDSessionReplayConfiguration
}
// @interface DDSessionReplayPrivacyOverrides
+ /// Per-view overrides of the global privacy levels, reached through a view's GetDdSessionReplayPrivacyOverrides().
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDSessionReplayPrivacyOverrides
diff --git a/src/DatadogNet.Trace.Mac/ApiDefinitions.cs b/src/DatadogNet.Trace.Mac/ApiDefinitions.cs
index f40d79c..ea5f986 100644
--- a/src/DatadogNet.Trace.Mac/ApiDefinitions.cs
+++ b/src/DatadogNet.Trace.Mac/ApiDefinitions.cs
@@ -134,6 +134,7 @@ interface OTTracer
}
// @interface DDB3HTTPHeadersWriter
+ /// Writes B3-format distributed-tracing headers.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDB3HTTPHeadersWriter
@@ -146,6 +147,7 @@ interface DDB3HTTPHeadersWriter
}
// @interface DDHTTPHeadersWriter
+ /// Writes Datadog-format distributed-tracing headers, for continuing a trace into your backend by hand.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDHTTPHeadersWriter
@@ -158,6 +160,7 @@ interface DDHTTPHeadersWriter
}
// @interface DDTrace
+ /// Enables APM distributed tracing. EnableWith(DDTraceConfiguration) once, after core initialisation.
[BaseType (typeof(NSObject))]
interface DDTrace
{
@@ -171,6 +174,7 @@ interface DDTrace
}
// @interface DDTraceConfiguration
+ /// Trace options: sampling, network info, RUM bundling and URLSession first-party tracing.
[BaseType (typeof(NSObject))]
interface DDTraceConfiguration
{
@@ -230,6 +234,7 @@ interface DDTraceURLSessionTracking
}
// @interface DDTracer
+ /// The OpenTracing-style tracer: build spans, inject and extract trace headers. The Additions layer adds SetError(Exception), one-call header injection and trace/span-id reading.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDTracer
@@ -268,6 +273,7 @@ interface DDTracer
}
// @interface DDW3CHTTPHeadersWriter
+ /// Writes W3C tracecontext distributed-tracing headers.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDW3CHTTPHeadersWriter
diff --git a/src/DatadogNet.WebViewTracking.Mac/ApiDefinitions.cs b/src/DatadogNet.WebViewTracking.Mac/ApiDefinitions.cs
index c7ebd32..b8f541d 100644
--- a/src/DatadogNet.WebViewTracking.Mac/ApiDefinitions.cs
+++ b/src/DatadogNet.WebViewTracking.Mac/ApiDefinitions.cs
@@ -7,6 +7,7 @@
namespace DatadogWebViewTracking
{
// @interface DDWebViewTracking
+ /// Bridges RUM events and logs out of a WKWebView whose page runs the Datadog Browser SDK, into the surrounding native session.
[BaseType (typeof(NSObject))]
[DisableDefaultCtor]
interface DDWebViewTracking