Skip to content

Commit 9808c04

Browse files
feat: mobile-dotnet support init attributes (#420)
## Summary Make attributes work <img width="1055" height="342" alt="image" src="https://github.com/user-attachments/assets/659d75f4-3de6-49f5-87c7-c8b348ba5414" /> <!-- Ideally, there is an attached GitHub issue that will describe the "why". If relevant, use this section to call out any additional information you'd like to _highlight_ to the reviewer. --> ## How did you test this change? <!-- Frontend - Leave a screencast or a screenshot to visually describe the changes. --> ## Are there any deployment considerations? <!-- Backend - Do we need to consider migrations or backfilling data? --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces API surface changes (plugin construction and new `ObservabilityOptions.Attributes`) and alters native bridge initialization data, which could impact consumers and telemetry attribution if types/serialization differ across platforms. > > **Overview** > Enables passing custom `attributes` through `ObservabilityOptions` into the native Android/iOS Observability SDKs as OpenTelemetry resource attributes, including new cross-platform dictionary converters and updated native bridge option types. > > Simplifies plugin usage by removing the `Builder().Build()` pattern for `ObservabilityPlugin`/`SessionReplayPlugin` in favor of direct constructors, updates docs/sample accordingly, and wires `Identify` hook callbacks (`BeforeIdentify`/`AfterIdentify`) through the .NET hook chain and native proxies. > > Also adjusts packaging/build defaults to use the local Client SDK (`UseLocalClientSdk=true`) and tweaks the MAUI sample to use async identify and pass a test attributes dictionary. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a209fa6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 05432b7 commit 9808c04

File tree

25 files changed

+316
-324
lines changed

25 files changed

+316
-324
lines changed

sdk/@launchdarkly/mobile-dotnet/.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,3 @@ FodyWeavers.xsd
405405

406406
# JetBrains Rider
407407
*.sln.iml
408-
409-
# Machine-specific build overrides
410-
Directory.Build.props

sdk/@launchdarkly/mobile-dotnet/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,18 @@ public static class MauiProgram
3333

3434
var ldConfig = Configuration.Builder(mobileKey, ConfigurationBuilder.AutoEnvAttributes.Enabled)
3535
.Plugins(new PluginConfigurationBuilder()
36-
.Add(ObservabilityPlugin.Builder(new ObservabilityOptions(
36+
.Add(new ObservabilityPlugin(new ObservabilityOptions(
3737
isEnabled: true,
3838
serviceName: "maui-sample-app"
39-
)).Build())
40-
.Add(SessionReplayPlugin.Builder(new SessionReplayOptions(
39+
)))
40+
.Add(new SessionReplayPlugin(new SessionReplayOptions(
4141
isEnabled: true,
4242
privacy: new SessionReplayOptions.PrivacyOptions(
4343
maskTextInputs: true,
4444
maskWebViews: false,
4545
maskLabels: false
4646
)
47-
)).Build())
47+
)))
4848
).Build();
4949

5050
var context = LaunchDarkly.Sdk.Context.New("maui-user-key");
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.example.LDObserve
2+
3+
interface BridgeLogger {
4+
fun debug(message: String)
5+
fun info(message: String)
6+
fun error(message: String)
7+
}
8+
9+
class SystemOutBridgeLogger : BridgeLogger {
10+
override fun debug(message: String) {
11+
System.out.println(message)
12+
}
13+
14+
override fun info(message: String) {
15+
System.out.println(message)
16+
}
17+
18+
override fun error(message: String) {
19+
System.err.println(message)
20+
}
21+
}

sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/ObservabilityBridge.kt

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.launchdarkly.LDNative
22

33
import android.app.Application
4+
import com.example.LDObserve.BridgeLogger
5+
import com.example.LDObserve.SystemOutBridgeLogger
46
import com.launchdarkly.observability.BuildConfig
57
import com.launchdarkly.observability.client.TelemetryInspector
68
import com.launchdarkly.observability.plugin.Observability
@@ -26,6 +28,7 @@ public class LDObservabilityOptions {
2628
@JvmField var otlpEndpoint: String = ""
2729
@JvmField var backendUrl: String = ""
2830
@JvmField var contextFriendlyName: String? = null
31+
@JvmField var attributes: HashMap<String, Any?>? = null
2932

3033
constructor()
3134

@@ -35,14 +38,16 @@ public class LDObservabilityOptions {
3538
serviceVersion: String,
3639
otlpEndpoint: String,
3740
backendUrl: String,
38-
contextFriendlyName: String?
41+
contextFriendlyName: String?,
42+
attributes: HashMap<String, Any?>? = null
3943
) {
4044
this.isEnabled = isEnabled
4145
this.serviceName = serviceName
4246
this.serviceVersion = serviceVersion
4347
this.otlpEndpoint = otlpEndpoint
4448
this.backendUrl = backendUrl
4549
this.contextFriendlyName = contextFriendlyName
50+
this.attributes = attributes
4651
}
4752
}
4853

@@ -88,15 +93,28 @@ public class LDSessionReplayOptions {
8893
}
8994
}
9095

91-
public class ObservabilityBridge {
92-
private fun printException(prefix: String, t: Throwable) {
93-
System.out.println("$prefix ${t::class.java.name}: ${t.message}")
94-
val writer = java.io.StringWriter()
95-
val printWriter = java.io.PrintWriter(writer)
96-
t.printStackTrace(printWriter)
97-
printWriter.flush()
98-
System.out.println(writer.toString())
96+
internal fun buildResourceAttributes(source: HashMap<String, Any?>?): Attributes {
97+
if (source.isNullOrEmpty()) return Attributes.empty()
98+
val builder = Attributes.builder()
99+
source.forEach { (key, value) ->
100+
when (value) {
101+
is String -> builder.put(AttributeKey.stringKey(key), value)
102+
is Boolean -> builder.put(AttributeKey.booleanKey(key), value)
103+
is Long -> builder.put(AttributeKey.longKey(key), value)
104+
is Int -> builder.put(AttributeKey.longKey(key), value.toLong())
105+
is Double -> builder.put(AttributeKey.doubleKey(key), value)
106+
is Float -> builder.put(AttributeKey.doubleKey(key), value.toDouble())
107+
null -> {}
108+
else -> builder.put(AttributeKey.stringKey(key), value.toString())
109+
}
99110
}
111+
return builder.build()
112+
}
113+
114+
public class ObservabilityBridge(
115+
private val logger: BridgeLogger = SystemOutBridgeLogger()
116+
) {
117+
var isDebug: Boolean = true
100118

101119
public fun getHookProxy(): RealObservabilityHookProxy? {
102120
val real = LDObserve.hookProxy ?: return null
@@ -142,18 +160,16 @@ public class ObservabilityBridge {
142160
replay: LDSessionReplayOptions,
143161
observabilityVersion: String
144162
) {
145-
// System.out.println("LD:ObservabilityBridge start called 7")
163+
// logger.debug("LD:ObservabilityBridge start called 7")
146164

147-
val resourceAttributes = try { Attributes.builder()
148-
// .put(AttributeKey.stringKey("service.name"), observability.serviceName)
149-
// .put(AttributeKey.stringKey("service.version"), observabilityVersion)
150-
.build()
165+
val resourceAttributes = try {
166+
buildResourceAttributes(observability.attributes)
151167
} catch (t: Throwable) {
152168
printException("LD:resourceAttributes failed to build resourceAttributes", t)
153169
throw t
154170
}
155171

156-
//System.out.println("LD:ObservabilityBridge resourceAttributes called")
172+
//logger.debug("LD:ObservabilityBridge resourceAttributes called")
157173

158174
val nativeObservabilityOptions = try {
159175
com.launchdarkly.observability.api.ObservabilityOptions(
@@ -210,7 +226,7 @@ public class ObservabilityBridge {
210226
throw t
211227
}
212228

213-
System.out.println(
229+
logger.info(
214230
"LD:ObservabilityBridge Session replay enabled=${nativeSessionReplayOptions.enabled}, " +
215231
"backendUrl=${nativeObservabilityOptions.backendUrl}"
216232
)
@@ -244,12 +260,20 @@ public class ObservabilityBridge {
244260

245261
try {
246262
LDClient.init(app, ldConfig, context)
247-
//System.out.println("LD:ObservabilityBridge LDClient.init completed")
263+
//logger.info("LD:ObservabilityBridge LDClient.init completed")
248264
} catch (t: Throwable) {
249265
printException("LD:ObservabilityBridge LDClient.init failed", t)
250266
throw t
251267
}
252268
}
253269

254-
255-
}
270+
private fun printException(prefix: String, t: Throwable) {
271+
logger.error("$prefix ${t::class.java.name}: ${t.message}")
272+
val writer = java.io.StringWriter()
273+
val printWriter = java.io.PrintWriter(writer)
274+
t.printStackTrace(printWriter)
275+
printWriter.flush()
276+
logger.error(writer.toString())
277+
}
278+
}
279+

sdk/@launchdarkly/mobile-dotnet/android/native/settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pluginManagement {
88
id("com.android.library") version "8.13.2"
99
}
1010
}
11+
1112
dependencyResolutionManagement {
1213
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
1314
repositories {

sdk/@launchdarkly/mobile-dotnet/macios/LDObserve.MaciOS.Binding/ApiDefinition.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ interface ObjcObservabilityOptions
2525

2626
[Export("backendUrl")]
2727
string BackendUrl { get; set; }
28+
29+
[NullAllowed, Export("attributes")]
30+
NSDictionary Attributes { get; set; }
2831
}
2932

3033
[BaseType(typeof(NSObject))]

sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/ObservabilityBridge.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public final class ObjcObservabilityOptions: NSObject {
1818
@objc public var serviceVersion: String = ""
1919
@objc public var otlpEndpoint: String = ""
2020
@objc public var backendUrl: String = ""
21+
@objc public var attributes: NSDictionary?
2122

2223
@objc public override init() {
2324
super.init()
@@ -50,6 +51,21 @@ public final class ObjcEnvironmentMetadata: NSObject {
5051
}
5152
}
5253

54+
internal func buildResourceAttributes(_ source: NSDictionary?) -> [String: AttributeValue] {
55+
guard let source = source as? [String: Any], !source.isEmpty else {
56+
return [:]
57+
}
58+
var result = [String: AttributeValue](minimumCapacity: source.count)
59+
for (key, value) in source {
60+
if let av = AttributeValue(value) {
61+
result[key] = av
62+
} else {
63+
result[key] = .string(String(describing: value))
64+
}
65+
}
66+
return result
67+
}
68+
5369
@objc(ObservabilityBridge)
5470
public final class ObservabilityBridge: NSObject {
5571

@@ -77,6 +93,7 @@ public final class ObservabilityBridge: NSObject {
7793
serviceVersion: observability.serviceVersion,
7894
otlpEndpoint: observability.otlpEndpoint,
7995
backendUrl: observability.backendUrl,
96+
resourceAttributes: buildResourceAttributes(observability.attributes),
8097
crashReporting: .init(source: .none),
8198
instrumentation: .init(
8299
urlSession: .disabled,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project>
2+
<PropertyGroup>
3+
<Version>0.4.0</Version>
4+
<Authors>LaunchDarkly</Authors>
5+
<Owners>LaunchDarkly</Owners>
6+
<Company>LaunchDarkly</Company>
7+
<Copyright>Copyright 2026 Catamorphic, Co</Copyright>
8+
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
9+
<PackageProjectUrl>https://github.com/launchdarkly/observability-sdk</PackageProjectUrl>
10+
<RepositoryUrl>https://github.com/launchdarkly/observability-sdk</RepositoryUrl>
11+
<PackageOutputPath>$(MSBuildProjectDirectory)\..\nupkgs</PackageOutputPath>
12+
</PropertyGroup>
13+
</Project>

sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,13 @@
22
<PropertyGroup>
33
<!-- Packaging-only project -->
44
<TargetFrameworks>net9.0-android;net9.0-ios</TargetFrameworks>
5-
<UseLocalClientSdk>false</UseLocalClientSdk>
5+
<UseLocalClientSdk>true</UseLocalClientSdk>
6+
<PackageId>LaunchDarkly.SessionReplay</PackageId>
7+
<PackageDescription>LD Session Replay package for .NET MAUI</PackageDescription>
68
<NoBuild>true</NoBuild>
79
<IncludeBuildOutput>false</IncludeBuildOutput>
810
<IsPackable>true</IsPackable>
9-
<PackageId>LaunchDarkly.SessionReplay</PackageId>
10-
<Version>0.3.16</Version>
11-
<Authors>LaunchDarkly</Authors>
12-
<Owners>LaunchDarkly</Owners>
13-
<Company>LaunchDarkly</Company>
14-
<Copyright>Copyright 2026 Catamorphic, Co</Copyright>
15-
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
16-
<PackageDescription>LD Session Replay package for .NET MAUI</PackageDescription>
17-
<PackageOutputPath>$(MSBuildProjectDirectory)\..\nupkgs</PackageOutputPath>
18-
<PackageProjectUrl>https://github.com/launchdarkly/observability-sdk</PackageProjectUrl>
19-
<RepositoryUrl>https://github.com/launchdarkly/observability-sdk</RepositoryUrl>
11+
2012
<PackageReadmeFile>README.md</PackageReadmeFile>
2113
<NoWarn>$(NoWarn);NU1012;NU1605;NU1608</NoWarn>
2214
</PropertyGroup>

sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.csproj

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<TargetFrameworks>net9.0-android;net9.0-ios</TargetFrameworks>
44
<!-- Use MAUI workload references to avoid runtime version mismatches -->
55
<UseMaui>true</UseMaui>
6-
<UseLocalClientSdk>false</UseLocalClientSdk>
6+
<UseLocalClientSdk>true</UseLocalClientSdk>
77
<SkipValidateMauiImplicitPackageReferences>true</SkipValidateMauiImplicitPackageReferences>
88
<ImplicitUsings>enable</ImplicitUsings>
99
<Nullable>enable</Nullable>
@@ -15,12 +15,8 @@
1515

1616
<!-- NuGet packaging -->
1717
<IsPackable>true</IsPackable>
18-
<PackageId>LDObservability</PackageId>
19-
<Version>0.3.16</Version>
20-
<Authors>LD</Authors>
18+
<PackageId>LaunchDarkly.SessionReplay</PackageId>
2119
<PackageDescription>LD Observability bindings aggregator for .NET (Android/iOS).</PackageDescription>
22-
<RepositoryUrl>https://example.local/ldobservability</RepositoryUrl>
23-
<PackageOutputPath>$(MSBuildProjectDirectory)\..\nupkgs</PackageOutputPath>
2420
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
2521
<IncludeReferencedProjects>true</IncludeReferencedProjects>
2622
<!-- This repo intentionally pins some AndroidX versions to match MAUI's expectations. -->
@@ -53,8 +49,11 @@
5349

5450
<ItemGroup Condition="'$(UseLocalClientSdk)' == 'true'">
5551
<ProjectReference Include="..\..\..\..\..\dotnet-core\pkgs\sdk\client\src\LaunchDarkly.ClientSdk.csproj"
56-
SetTargetFramework="TargetFramework=netstandard2.0"
57-
GlobalPropertiesToRemove="RuntimeIdentifier" />
52+
SetTargetFramework="TargetFramework=netstandard2.0;BUILDFRAMEWORKS=netstandard2.0"
53+
SkipGetTargetFrameworkProperties="true"
54+
PrivateAssets="all"
55+
AdditionalProperties="SignAssembly=false"
56+
GlobalPropertiesToRemove="RuntimeIdentifier;TargetPlatformIdentifier;SupportedOSPlatformVersion;UseMaui;SkipValidateMauiImplicitPackageReferences" />
5857
</ItemGroup>
5958

6059
<ItemGroup Condition="'$(UseLocalClientSdk)' != 'true'">

0 commit comments

Comments
 (0)