Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ open class BaseApplication : Application() {
.anonymous(true)
.build()

LDClient.init(this@BaseApplication, ldConfig, context, 1)
LDClient.init(this@BaseApplication, ldConfig, context, 0)

if (testUrl == null) {
// intervenes in E2E tests by trigger spans
Expand All @@ -98,7 +98,7 @@ open class BaseApplication : Application() {
}

// example on creating OBS/SR without flagging
open fun realIndependentInit() {
open fun realInitIndependent() {
val effectiveOptions = testUrl?.let {
observabilityOptions.copy(backendUrl = it, otlpEndpoint = it)
} ?: observabilityOptions
Expand All @@ -107,25 +107,29 @@ open class BaseApplication : Application() {
.anonymous(true)
.build()

LDObserve.init(
application = this@BaseApplication,
mobileKey = LAUNCHDARKLY_MOBILE_KEY,
ldContext = context,
options = effectiveOptions,
replayOptions = ReplayOptions(
enabled = false,
privacyProfile = PrivacyProfile(
maskText = false,
maskWebViews = true,
maskViews = listOf(
view(ImageView::class.java),
),
maskXMLViewIds = listOf("smoothieTitle")
Thread {
LDObserve.init(
application = this@BaseApplication,
mobileKey = LAUNCHDARKLY_MOBILE_KEY,
ldContext = context,
options = effectiveOptions,
replayOptions = ReplayOptions(
enabled = false,
privacyProfile = PrivacyProfile(
maskText = false,
maskWebViews = true,
maskViews = listOf(
view(ImageView::class.java),
),
maskXMLViewIds = listOf("smoothieTitle")
)
)
)
)

LDReplay.start()
LDReplay.start()

}.start()

}

fun flagEvaluation() {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<PackageId>LaunchDarkly.SessionReplay</PackageId>
<Version>0.9.17</Version>
<Version>0.10.1</Version>
<UseLocalClientSdk>false</UseLocalClientSdk>
<Authors>LaunchDarkly</Authors>
<Owners>LaunchDarkly</Owners>
Expand Down
4 changes: 3 additions & 1 deletion sdk/@launchdarkly/mobile-dotnet/sample/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ public static MauiApp CreateMauiApp()
).Build();

var context = LaunchDarkly.Sdk.Context.New("maui-user-key");
var client = LdClient.Init(ldConfig, context, TimeSpan.FromSeconds(10));
var client = Task.Run(() => LdClient.InitAsync(ldConfig, context)).GetAwaiter().GetResult();

//var client = LdClient.Init(ldConfig, context, TimeSpan.FromSeconds(0));
Comment thread
abelonogov-ld marked this conversation as resolved.
var feature1 = client.BoolVariation("feature1", false);
Console.WriteLine($"feature1 sync value ={feature1}");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.launchdarkly.observability.client

import com.launchdarkly.observability.BuildConfig
import com.launchdarkly.observability.api.ObservabilityOptions
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.sdk.resources.Resource

/**
* Default Highlight/OTel "distro" attributes attached to every observability [Resource].
*
* Exposed (internally) so the [com.launchdarkly.observability.plugin.Observability] LDClient
* plugin can seed its mutable `distroAttributes` field with the same defaults the standalone
* [com.launchdarkly.observability.sdk.LDObserve.init] path uses, keeping the two init paths
* in sync without duplicating string literals.
*/
internal val DEFAULT_DISTRO_ATTRIBUTES: Map<String, String> = mapOf(
"telemetry.distro.name" to "launchdarkly-observability-android",
"telemetry.distro.version" to BuildConfig.OBSERVABILITY_SDK_VERSION,
)

/**
* Builds the OpenTelemetry [Resource] attached to every observability signal emitted by this SDK.
*
* Single source of truth for resource shape, used by both initialization paths:
* - [com.launchdarkly.observability.plugin.Observability.onPluginsReady] (LDClient plugin path),
* which flattens its [com.launchdarkly.sdk.android.integrations.EnvironmentMetadata] into the
* [applicationId], [applicationVersion], and [sdkVersion] params.
* - [com.launchdarkly.observability.sdk.LDObserve.init] (standalone path), which has no
* LDClient metadata and simply omits those params.
*
* Attribute precedence (later wins for duplicate keys):
* 1. OpenTelemetry default resource attributes, minus `service.name`.
* 2. `highlight.project_id` = [sdkKey].
* 3. [distroAttributes] (defaults to [DEFAULT_DISTRO_ATTRIBUTES] — `telemetry.distro.{name,version}`).
* 4. Caller-supplied [ObservabilityOptions.resourceAttributes].
* 5. `launchdarkly.application.id`, `launchdarkly.application.version`, `launchdarkly.sdk.version`
* when provided (LDClient plugin path only).
*
* The metadata-derived attributes (5) come *after* the user's [ObservabilityOptions.resourceAttributes]
* so user overrides cannot accidentally clobber LDClient identity. If a user genuinely wants to
* override `launchdarkly.*`, they shouldn't be using these attribute names anyway.
*
* @param sdkKey LaunchDarkly mobile key; written as `highlight.project_id`.
* @param options Observability options; [ObservabilityOptions.resourceAttributes] is appended.
* @param distroAttributes Distro identification, defaults to [DEFAULT_DISTRO_ATTRIBUTES].
* The LDClient plugin passes its mutable `distroAttributes` field here
* so callers that customize the field still see their changes.
* @param applicationId `launchdarkly.application.id`, or `null` to omit.
* @param applicationVersion `launchdarkly.application.version`, or `null` to omit.
* @param sdkVersion `launchdarkly.sdk.version`, or `null` to omit. Already formatted
* (typically `"$sdkName/$sdkVersion"`) — this helper does not compose it.
*/
internal fun buildObservabilityResource(
sdkKey: String,
options: ObservabilityOptions,
distroAttributes: Map<String, String> = DEFAULT_DISTRO_ATTRIBUTES,
applicationId: String? = null,
applicationVersion: String? = null,
sdkVersion: String? = null,
): Resource {
val builder = Attributes.builder()

Resource.getDefault().attributes.forEach { key, value ->
if (key.key != "service.name") {
@Suppress("UNCHECKED_CAST")
builder.put(key as AttributeKey<Any>, value)
}
}

builder.put("highlight.project_id", sdkKey)

distroAttributes.forEach { (key, value) ->
builder.put(AttributeKey.stringKey(key), value)
}

builder.putAll(options.resourceAttributes)

applicationId?.let { builder.put("launchdarkly.application.id", it) }
applicationVersion?.let { builder.put("launchdarkly.application.version", it) }
sdkVersion?.let { builder.put("launchdarkly.sdk.version", it) }

return Resource.create(builder.build())
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.launchdarkly.observability.sampling.SamplingConfig
import com.launchdarkly.observability.sampling.SamplingLogProcessor
import com.launchdarkly.observability.traces.EventSpanProcessor
import com.launchdarkly.observability.traces.OtlpTraceExporter
import com.launchdarkly.observability.util.requireMainThread
import io.opentelemetry.android.OpenTelemetryRum
import io.opentelemetry.android.OpenTelemetryRumBuilder
import io.opentelemetry.android.config.OtelRumConfig
Expand Down Expand Up @@ -108,6 +109,8 @@ class ObservabilityService(
private val scope = CoroutineScope(DispatcherProviderHolder.current.io + SupervisorJob())

init {
requireMainThread { "ObservabilityService must be initialized on the main thread" }

registerOtlpExporters()
val otelRumConfig = createOtelRumConfig()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ import android.app.Application
import com.launchdarkly.observability.context.ObserveLogger
import com.launchdarkly.observability.BuildConfig
import com.launchdarkly.observability.api.ObservabilityOptions
import com.launchdarkly.observability.client.DEFAULT_DISTRO_ATTRIBUTES
import com.launchdarkly.observability.client.ObservabilityService
import com.launchdarkly.observability.client.ObservabilityContext
import com.launchdarkly.observability.client.TelemetryInspector
import com.launchdarkly.observability.client.buildObservabilityResource
import com.launchdarkly.observability.sdk.LDObserve
import com.launchdarkly.sdk.android.LDClient
import com.launchdarkly.sdk.android.integrations.EnvironmentMetadata
import com.launchdarkly.sdk.android.integrations.Hook
import com.launchdarkly.sdk.android.integrations.Plugin
import com.launchdarkly.sdk.android.integrations.PluginMetadata
import com.launchdarkly.sdk.android.integrations.RegistrationCompleteResult
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.sdk.resources.Resource
import java.util.Collections

/**
Expand Down Expand Up @@ -54,10 +53,7 @@ class Observability(
private val mobileKey: String,
private val options: ObservabilityOptions = ObservabilityOptions() // new instance has reasonable defaults
) : Plugin() {
var distroAttributes: Map<String, String> = mapOf(
"telemetry.distro.name" to SDK_NAME,
"telemetry.distro.version" to BuildConfig.OBSERVABILITY_SDK_VERSION
)
var distroAttributes: Map<String, String> = DEFAULT_DISTRO_ATTRIBUTES
private val logger: ObserveLogger
private val observabilityHook = ObservabilityHook()
private var observabilityClient: ObservabilityService? = null
Expand All @@ -77,16 +73,16 @@ class Observability(
override fun register(client: LDClient, metadata: EnvironmentMetadata?) {
this.client = client
val sdkKey = metadata?.credential ?: ""
if (mobileKey == sdkKey) {
LDObserve.context = ObservabilityContext(
sdkKey = sdkKey,
options = options,
application = application,
logger = logger
)
} else {
if (mobileKey != sdkKey) {
logger.warn("ObservabilityContext could not be initialized for sdkKey: $sdkKey")
return
}
LDObserve.context = ObservabilityContext(
sdkKey = sdkKey,
options = options,
application = application,
logger = logger
)
}

override fun getHooks(metadata: EnvironmentMetadata?): MutableList<Hook> {
Expand All @@ -96,50 +92,45 @@ class Observability(
override fun onPluginsReady(result: RegistrationCompleteResult?, metadata: EnvironmentMetadata?) {
val sdkKey = metadata?.credential ?: ""

client?.let { lDClient ->
if (mobileKey == sdkKey) {
val attributes = Attributes.builder()
Resource.getDefault().attributes.forEach { key, value ->
if (key.key != "service.name") {
@Suppress("UNCHECKED_CAST")
attributes.put(key as AttributeKey<Any>, value)
}
}
attributes.put("highlight.project_id", sdkKey)
distroAttributes.forEach { (key, value) ->
attributes.put(AttributeKey.stringKey(key), value)
}
attributes.putAll(options.resourceAttributes)

metadata?.applicationInfo?.applicationId?.let {
attributes.put("launchdarkly.application.id", it)
}

metadata?.applicationInfo?.applicationVersion?.let {
attributes.put("launchdarkly.application.version", it)
}

metadata?.sdkMetadata?.name?.let { sdkName ->
metadata.sdkMetadata?.version?.let { sdkVersion ->
attributes.put("launchdarkly.sdk.version", "$sdkName/$sdkVersion")
}
}

val builtResource = Resource.create(attributes.build())
LDObserve.context?.resourceAttributes = builtResource.attributes
if (client == null) {
logger.error("Observability could not be initialized: LDClient is null in onPluginsReady")
return
}
if (mobileKey != sdkKey) {
logger.warn("Observability could not be initialized for sdkKey: $sdkKey")
return
}

val client = ObservabilityService(
application, sdkKey, builtResource, logger, options,
)
observabilityClient = client
LDObserve.context?.sessionManager = client.sessionManager
LDObserve.init(client)
val resource = buildObservabilityResource(
sdkKey = sdkKey,
options = options,
distroAttributes = distroAttributes,
applicationId = metadata?.applicationInfo?.applicationId,
applicationVersion = metadata?.applicationInfo?.applicationVersion,
sdkVersion = composeLaunchDarklySdkVersion(metadata),
)
LDObserve.context?.resourceAttributes = resource.attributes

val observabilityService = ObservabilityService(
application, sdkKey, resource, logger, options,
)
observabilityClient = observabilityService
LDObserve.context?.sessionManager = observabilityService.sessionManager
LDObserve.init(observabilityService)

observabilityHook.delegate = observabilityService.hookExporter
}

observabilityHook.delegate = client.hookExporter
} else {
logger.warn("Observability could not be initialized for sdkKey: $sdkKey")
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
/**
* Combines `EnvironmentMetadata.sdkMetadata.{name, version}` into the single
* `launchdarkly.sdk.version` attribute value (`"$name/$version"`). Returns `null` if
* either piece is missing, in which case [buildObservabilityResource] omits the attribute.
*/
private fun composeLaunchDarklySdkVersion(metadata: EnvironmentMetadata?): String? {
val sdk = metadata?.sdkMetadata ?: return null
val name = sdk.name ?: return null
val version = sdk.version ?: return null
return "$name/$version"
}

fun getTelemetryInspector(): TelemetryInspector? {
Expand All @@ -148,6 +139,5 @@ class Observability(

companion object {
const val PLUGIN_NAME = "@launchdarkly/observability-android"
const val SDK_NAME = "launchdarkly-observability-android"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.launchdarkly.observability.replay
import com.launchdarkly.observability.network.SamplingConfigResponse
import com.launchdarkly.observability.sampling.SamplingConfig
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand Down Expand Up @@ -136,6 +137,7 @@ open class IntEnumSerializer<T : Enum<T>>(
}
}

@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class EventNode(
val type: NodeType,
Expand Down
Loading
Loading