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
1 change: 1 addition & 0 deletions demo-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies {

coreLibraryDesugaring(libs.desugarJdkLibs)

implementation("io.opentelemetry.android:instrumentation-compose")
implementation("io.opentelemetry.android:android-agent") //parent dir
implementation("io.opentelemetry.android:instrumentation-sessions")
implementation(libs.androidx.core.ktx)
Expand Down
2 changes: 2 additions & 0 deletions demo-app/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ dependencyResolutionManagement {

includeBuild("..") {
dependencySubstitution {
substitute(module("io.opentelemetry.android:instrumentation-compose"))
.using(project(":instrumentation:compose"))
substitute(module("io.opentelemetry.android:android-agent"))
.using(project(":android-agent"))
substitute(module("io.opentelemetry.android:instrumentation-sessions"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import io.opentelemetry.api.metrics.LongCounter
import io.opentelemetry.api.trace.SpanKind
Expand All @@ -28,7 +30,9 @@ fun MainOtelButton(icon: Painter,
Spacer(modifier = Modifier.height(5.dp))
Button(
onClick = { generateClickEvent(clickCounter) },
modifier = Modifier.padding(20.dp),
modifier = Modifier.padding(20.dp).semantics{
onClick("MainOtelButton") { true }
},
colors = ButtonDefaults.buttonColors(containerColor = Color.Black),
content = {
Image(
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ androidPlugin = "8.10.1"
junitKtx = "1.2.1"
autoService = "1.1.1"
androidx-navigation = "2.7.7"
compose = "1.5.4"

[libraries]
opentelemetry-platform-alpha = { module = "io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha", version.ref = "opentelemetry-instrumentation-alpha" }
Expand Down Expand Up @@ -43,6 +44,7 @@ opentelemetry-exporter-otlp = { module = "io.opentelemetry:opentelemetry-exporte
volley = "com.android.volley:volley:1.2.1"
auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" }
auto-service-processor = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
compose = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }

#Test tools
opentelemetry-sdk-testing = { module = "io.opentelemetry:opentelemetry-sdk-testing" }
Expand Down
36 changes: 36 additions & 0 deletions instrumentation/compose/click/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

# Compose Instrumentation

Status: development

## Compose version
`1.3.0` to `1.5.4`

This instrumentation has the ability to generate events when the user
performs click actions. A click is not differentiated from touch or other
input pointer events.

When an Activity becomes active, the instrumentation begins tracking
its window by registering a callback that receives events.

This instrumentation is not currently enabled by default.

## Telemetry

Data produced by this instrumentation will have an instrumentation scope
name of `io.opentelemetry.android.instrumentation.compose.click`.
This instrumentation produces the following telemetry:

### Clicks

* Type: Event
* Name: `app.screen.click`
* Description: This event is emitted when the user taps or clicks on the screen.
* See the [semantic convention definition](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/app/app.md#event-appscreenclick)
for more details.

* Type: Event
* Name: `event.app.widget.click`
* Description: This event is emitted when the user taps on a composable that is clickable.
* See the [semantic convention definition](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/app/app.md#event-appwidgetclick)
for more details.
33 changes: 33 additions & 0 deletions instrumentation/compose/click/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
plugins {
id("otel.android-library-conventions")
id("otel.publish-conventions")
}

description = "OpenTelemetry Android compose click instrumentation"

android {
namespace = "io.opentelemetry.android.instrumentation.compose.click"

defaultConfig {
consumerProguardFiles("consumer-rules.pro")
}
}

dependencies {
api(project(":services"))
api(libs.opentelemetry.api)
api(platform(libs.opentelemetry.platform.alpha))
api(project(":instrumentation:android-instrumentation"))

compileOnly(libs.compose)
implementation(libs.opentelemetry.api.incubator)
implementation(libs.opentelemetry.instrumentation.apiSemconv)
implementation(libs.opentelemetry.semconv.incubating)

testImplementation(project(":test-common"))
testImplementation(project(":session"))

testImplementation(libs.compose)
testImplementation(libs.robolectric)
testImplementation(libs.androidx.test.core)
}
8 changes: 8 additions & 0 deletions instrumentation/compose/click/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Keep the Compose internals class name. We need this for compose click capture.
-keep class androidx.compose.foundation.ClickableElement {
<fields>;
}
-keep class androidx.compose.foundation.CombinedClickableElement {
<fields>;
}
-keepnames class androidx.compose.foundation.selection.ToggleableElement
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.compose.click

import android.app.Activity
import io.opentelemetry.android.internal.services.visiblescreen.activities.DefaultingActivityLifecycleCallbacks

internal class ComposeClickActivityCallback(
private val composeClickEventGenerator: ComposeClickEventGenerator,
) : DefaultingActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
composeClickEventGenerator.startTracking(activity.window)
}

override fun onActivityPaused(activity: Activity) {
composeClickEventGenerator.stopTracking()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")

package io.opentelemetry.instrumentation.compose.click

import android.view.MotionEvent
import android.view.Window
import androidx.compose.ui.node.LayoutNode
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder
import io.opentelemetry.api.incubator.logs.ExtendedLogger
import io.opentelemetry.semconv.incubating.AppIncubatingAttributes.APP_SCREEN_COORDINATE_X
import io.opentelemetry.semconv.incubating.AppIncubatingAttributes.APP_SCREEN_COORDINATE_Y
import io.opentelemetry.semconv.incubating.AppIncubatingAttributes.APP_WIDGET_ID
import io.opentelemetry.semconv.incubating.AppIncubatingAttributes.APP_WIDGET_NAME
import java.lang.ref.WeakReference

internal class ComposeClickEventGenerator(
private val eventLogger: ExtendedLogger,
private val composeLayoutNodeUtil: ComposeLayoutNodeUtil = ComposeLayoutNodeUtil(),
private val composeTapTargetDetector: ComposeTapTargetDetector = ComposeTapTargetDetector(composeLayoutNodeUtil),
) {
private var windowRef: WeakReference<Window>? = null

fun startTracking(window: Window) {
windowRef = WeakReference(window)
val currentCallback = window.callback
window.callback = WindowCallbackWrapper(currentCallback, this)
}

fun generateClick(motionEvent: MotionEvent?) {
windowRef?.get()?.let { window ->
if (motionEvent != null && motionEvent.actionMasked == MotionEvent.ACTION_UP) {
createEvent(APP_SCREEN_CLICK_EVENT_NAME)
.setAttribute(APP_SCREEN_COORDINATE_Y, motionEvent.y.toLong())
.setAttribute(APP_SCREEN_COORDINATE_X, motionEvent.x.toLong())
.emit()

composeTapTargetDetector.findTapTarget(window.decorView, motionEvent.x, motionEvent.y)?.let { layoutNode ->
createEvent(VIEW_CLICK_EVENT_NAME)
.setAllAttributes(createNodeAttributes(layoutNode))
.emit()
}
}
}
}

fun stopTracking() {
windowRef?.get()?.run {
if (callback is WindowCallbackWrapper) {
callback = (callback as WindowCallbackWrapper).unwrap()
}
}
windowRef = null
}

private fun createEvent(name: String): ExtendedLogRecordBuilder =
eventLogger
.logRecordBuilder()
.setEventName(name)

private fun createNodeAttributes(node: LayoutNode): Attributes {
val builder = Attributes.builder()
builder.put(APP_WIDGET_NAME, composeTapTargetDetector.nodeToName(node))
builder.put(APP_WIDGET_ID, node.semanticsId.toString())

composeLayoutNodeUtil.getLayoutNodePositionInWindow(node)?.let {
builder.put(APP_SCREEN_COORDINATE_X, it.x.toLong())
builder.put(APP_SCREEN_COORDINATE_Y, it.y.toLong())
}
return builder.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.compose.click

import com.google.auto.service.AutoService
import io.opentelemetry.android.instrumentation.AndroidInstrumentation
import io.opentelemetry.android.instrumentation.InstallationContext
import io.opentelemetry.api.incubator.logs.ExtendedLogger

@AutoService(AndroidInstrumentation::class)
class ComposeClickInstrumentation : AndroidInstrumentation {
override val name: String = "compose.click"

override fun install(ctx: InstallationContext) {
ctx.application.registerActivityLifecycleCallbacks(
ComposeClickActivityCallback(
ComposeClickEventGenerator(
ctx.openTelemetry
.logsBridge
.loggerBuilder("io.opentelemetry.android.instrumentation.compose.click")
.build() as ExtendedLogger,
),
),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")

package io.opentelemetry.instrumentation.compose.click

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.node.LayoutNode

const val APP_SCREEN_CLICK_EVENT_NAME = "app.screen.click"
const val VIEW_CLICK_EVENT_NAME = "event.app.widget.click"

internal class ComposeLayoutNodeUtil {
internal fun getLayoutNodeBoundsInWindow(node: LayoutNode): Rect? =
try {
node.layoutDelegate.outerCoordinator.coordinates
.boundsInWindow()
} catch (_: Exception) {
null
}

internal fun getLayoutNodePositionInWindow(node: LayoutNode): Offset? =
try {
node.layoutDelegate.outerCoordinator.coordinates
.positionInWindow()
} catch (_: Exception) {
null
}
}
Loading