Skip to content

Commit 5a0ff29

Browse files
authored
Merge pull request #3054 from DataDog/jmoskovich/rum-10163/coil3-support
RUM-10163: Add Coil3 Integration
2 parents aac24ca + 207e756 commit 5a0ff29

File tree

29 files changed

+636
-21
lines changed

29 files changed

+636
-21
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ This project hosts the following modules:
4040
- `integrations/***`: a set of libraries integrating Datadog products in third party libraries:
4141
- `integrations/dd-sdk-android-apollo`: a lightweight library providing a bridge integration between Datadog SDK and [Apollo Kotlin](https://github.com/apollographql/apollo-kotlin)
4242
- `integrations/dd-sdk-android-coil`: a lightweight library providing a bridge integration between Datadog SDK and [Coil](https://coil-kt.github.io/coil/);
43+
- `integrations/dd-sdk-android-coil3`: a lightweight library providing a bridge integration between Datadog SDK and [Coil 3](https://coil-kt.github.io/coil/);
4344
- `integrations/dd-sdk-android-compose`: a lightweight library providing a bridge integration between Datadog SDK and [Jetpack Compose](https://developer.android.com/jetpack/compose);
4445
- `integrations/dd-sdk-android-fresco`: a lightweight library providing a bridge integration between Datadog SDK and [Fresco](https://frescolib.org/);
4546
- `integrations/dd-sdk-android-glide`: a lightweight library providing a bridge integration between Datadog SDK and [Glide](https://bumptech.github.io/glide/);

LICENSE-3rdparty.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import,com.squareup.sqldelight,Apache-2.0,"Copyright 2016 Square, Inc"
6363
import,com.fasterxml.jackson,Apache-2.0,"Copyright 2020 Datadog, Inc."
6464
import,com.datadoghq,Apache-2.0,"Copyright 2017 Datadog, Inc"
6565
import,io.coil-kt,Apache-2.0,Copyright 2021 Coil Contributors
66+
import,io.coil-kt.coil3,Apache-2.0,Copyright 2021 Coil Contributors
6667
import,io.opentelemetry,Apache-2.0,Copyright 2019 The OpenTelemetry Authors
6768
import,io.opentracing,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors
6869
import,io.opentracing.contrib,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors
@@ -72,6 +73,7 @@ import,org.jetbrains,Apache-2.0,Copyright 2010-2019 JetBrains s.r.o. and Kotlin
7273
import,org.jetbrains.kotlin,Apache-2.0,Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors
7374
import,org.jetbrains.kotlinx,Apache-2.0,Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors
7475
import,org.chromium.net,Apache-2.0,"Copyright 2015 The Chromium Authors"
76+
import,org.jetbrains.skiko,Apache-2.0,Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors
7577
import,org.reactivestreams,CC0,Copyright 2014 Reactive Streams
7678
import(test),androidx.autofill,Apache-2.0,Copyright 2018 The Android Open Source Project
7779
import(test),androidx.concurrent,Apache-2.0,Copyright 2018 The Android Open Source Project

ci/pipelines/default-pipeline.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ analysis:detekt-custom:
119119
- ./gradlew :features:dd-sdk-android-trace-otel:customDetektRules
120120
- ./gradlew :features:dd-sdk-android-webview:customDetektRules
121121
- ./gradlew :integrations:dd-sdk-android-coil:customDetektRules
122+
- ./gradlew :integrations:dd-sdk-android-coil3:customDetektRules
122123
- ./gradlew :integrations:dd-sdk-android-compose:customDetektRules
123124
- ./gradlew :integrations:dd-sdk-android-cronet:customDetektRules
124125
- ./gradlew :integrations:dd-sdk-android-fresco:customDetektRules
@@ -783,6 +784,23 @@ publish:release-coil:
783784
paths:
784785
- integrations/dd-sdk-android-coil/verification-metadata.xml
785786

787+
publish:release-coil3:
788+
tags: [ "arch:amd64" ]
789+
only:
790+
- tags
791+
- develop
792+
image: $CI_IMAGE_DOCKER
793+
stage: publish
794+
timeout: 30m
795+
script:
796+
- !reference [.snippets, set-publishing-credentials]
797+
- ./gradlew :integrations:dd-sdk-android-coil3:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon
798+
artifacts:
799+
when: on_success
800+
expire_in: 7 days
801+
paths:
802+
- integrations/dd-sdk-android-coil3/verification-metadata.xml
803+
786804
publish:release-compose:
787805
tags: [ "arch:amd64" ]
788806
only:
@@ -1117,6 +1135,7 @@ notify:merge-verification-metadata:
11171135
- publish:release-session-replay-compose
11181136
- publish:release-webview
11191137
- publish:release-coil
1138+
- publish:release-coil3
11201139
- publish:release-compose
11211140
- publish:release-cronet
11221141
- publish:release-fresco

detekt_custom_safe_calls.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,7 @@ datadog:
584584
- "java.lang.ref.WeakReference.constructor(kotlin.Nothing?)"
585585
- "java.lang.ref.WeakReference.constructor(kotlin.String?)"
586586
- "java.lang.ref.WeakReference.get()"
587+
- "java.lang.reflect.isStatic(kotlin.Int)"
587588
- "java.lang.reflect.Field.getSafe(kotlin.Any?)"
588589
- "java.lang.reflect.Field.accessible()"
589590
- "java.lang.StringBuilder.append(kotlin.Char)"

features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/reflection/ComposeReflection.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.datadog.android.api.InternalLogger
1212
import com.datadog.android.api.feature.FeatureSdkCore
1313
import java.lang.reflect.Field
1414
import java.lang.reflect.Method
15+
import java.lang.reflect.Modifier.isStatic
1516

1617
@Suppress("StringLiteralDuplication")
1718
internal object ComposeReflection {
@@ -179,17 +180,18 @@ internal fun Method.accessible(): Method {
179180

180181
@Suppress("TooGenericExceptionCaught")
181182
internal fun Field.getSafe(target: Any?): Any? {
183+
if (target == null && !isStatic(modifiers)) {
184+
return null
185+
}
182186
return try {
187+
@Suppress("UnsafeThirdPartyFunctionCall") // null is checked above so no npe possible
183188
get(target)
184189
} catch (e: IllegalAccessException) {
185190
logReflectionException(name, LOG_TYPE_FIELD, LOG_REASON_FIELD_NO_ACCESSIBLE, e)
186191
null
187192
} catch (e: IllegalArgumentException) {
188193
logReflectionException(name, LOG_TYPE_FIELD, LOG_REASON_INCOMPATIBLE_TYPE, e)
189194
null
190-
} catch (e: NullPointerException) {
191-
logNullPointerException(name, LOG_TYPE_FIELD, e)
192-
null
193195
} catch (e: ExceptionInInitializerError) {
194196
logReflectionException(name, LOG_TYPE_FIELD, LOG_REASON_INITIALIZATION_ERROR, e)
195197
null

features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/ReflectionUtils.kt

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,8 @@ internal class ReflectionUtils {
159159
fun getCoil3AsyncImagePainter(semanticsNode: SemanticsNode): Painter? {
160160
// Check if Coil3 ContentPainterNode is present first to optimize the performance
161161
// by skipping the node chain iteration
162-
if (PainterNodeClass == null) {
163-
return null
164-
}
162+
if (PainterNodeClass == null) return null
163+
165164
val layoutNode = LayoutNodeField?.getSafe(semanticsNode)
166165
val nodeChain = NodesFieldOfLayoutNode?.getSafe(layoutNode)
167166
val headNode = HeadFieldOfNodeChain?.getSafe(nodeChain) as? Modifier.Node
@@ -176,8 +175,7 @@ internal class ReflectionUtils {
176175
currentNode = ChildFieldOfModifierNode?.getSafe(currentNode) as? Modifier.Node
177176
}
178177
val asyncImagePainter = PainterFieldOfPainterNode?.getSafe(painterNode)
179-
val painter =
180-
asyncImagePainter?.let { PainterMethodOfAsync3ImagePainter?.invoke(it) }
178+
val painter = asyncImagePainter?.let { PainterMethodOfAsync3ImagePainter?.invoke(it) }
181179
return painter as? Painter
182180
}
183181

@@ -205,9 +203,8 @@ internal class ReflectionUtils {
205203
fun getAsyncImagePainter(semanticsNode: SemanticsNode): Painter? {
206204
// Check if Coil AsyncImagePainter is present first to optimize the performance
207205
// by skipping the modifier iteration
208-
if (AsyncImagePainterClass == null) {
209-
return null
210-
}
206+
if (AsyncImagePainterClass == null) return null
207+
211208
val asyncPainter = semanticsNode.layoutInfo.getModifierInfo().firstNotNullOfOrNull {
212209
if (ContentPainterModifierClass?.isInstance(it.modifier) == true) {
213210
PainterFieldOfContentPainterModifier?.getSafe(it.modifier)

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ kotlinXmlBuilder = "1.9.3"
8585
sqlDelight = "1.5.5"
8686
coil = "1.0.0"
8787
coilCompose = "2.1.0"
88+
coil3 = "3.0.4"
8889
fresco = "2.3.0"
8990
glide = "4.11.0"
9091
picasso = "2.8"
@@ -237,6 +238,9 @@ kotlinXmlBuilder = {module="org.redundent:kotlin-xml-builder", version.ref="kotl
237238
sqlDelight = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqlDelight" }
238239
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
239240
coilCompose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
241+
coil3 = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" }
242+
coil3Compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3" }
243+
coil3NetworkOkHttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3" }
240244

241245
frescoCore = { module = "com.facebook.fresco:fresco", version.ref = "fresco" }
242246
frescoOkHttp3 = { module = "com.facebook.fresco:imagepipeline-okhttp3", version.ref = "fresco" }

integrations/dd-sdk-android-coil/src/test/kotlin/com/datadog/android/coil/DatadogCoilRequestListenerTest.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import kotlin.reflect.jvm.isAccessible
4949
@MockitoSettings(strictness = Strictness.LENIENT)
5050
internal class DatadogCoilRequestListenerTest {
5151

52-
lateinit var underTest: DatadogCoilRequestListener
52+
private lateinit var testable: DatadogCoilRequestListener
5353

5454
@Mock
5555
lateinit var mockRumMonitor: RumMonitor
@@ -68,7 +68,7 @@ internal class DatadogCoilRequestListenerTest {
6868
isAccessible = true
6969
call(GlobalRumMonitor::class.objectInstance, mockRumMonitor, mockSdkCore)
7070
}
71-
underTest = DatadogCoilRequestListener(mockSdkCore)
71+
testable = DatadogCoilRequestListener(mockSdkCore)
7272
}
7373

7474
@AfterEach
@@ -88,7 +88,7 @@ internal class DatadogCoilRequestListenerTest {
8888
mockRequest = mockImageRequest(fakePath)
8989

9090
// WHEN
91-
underTest.onError(mockRequest, fakeException)
91+
testable.onError(mockRequest, fakeException)
9292

9393
// THEN
9494
val argumentCaptor = argumentCaptor<Map<String, Any?>>()
@@ -114,7 +114,7 @@ internal class DatadogCoilRequestListenerTest {
114114
mockRequest = mockImageRequest(mockUri)
115115

116116
// WHEN
117-
underTest.onError(mockRequest, fakeException)
117+
testable.onError(mockRequest, fakeException)
118118

119119
// THEN
120120
val argumentCaptor = argumentCaptor<Map<String, Any?>>()
@@ -137,7 +137,7 @@ internal class DatadogCoilRequestListenerTest {
137137
mockRequest = mockImageRequest(fakeHttpUrl)
138138

139139
// WHEN
140-
underTest.onError(mockRequest, fakeException)
140+
testable.onError(mockRequest, fakeException)
141141

142142
// THEN
143143
val argumentCaptor = argumentCaptor<Map<String, Any?>>()
@@ -160,7 +160,7 @@ internal class DatadogCoilRequestListenerTest {
160160
mockRequest = mockImageRequest(fakeFile)
161161

162162
// WHEN
163-
underTest.onError(mockRequest, fakeException)
163+
testable.onError(mockRequest, fakeException)
164164

165165
// THEN
166166
val argumentCaptor = argumentCaptor<Map<String, Any?>>()
@@ -183,7 +183,7 @@ internal class DatadogCoilRequestListenerTest {
183183
mockRequest = mockImageRequest(mockDrawable)
184184

185185
// WHEN
186-
underTest.onError(mockRequest, fakeException)
186+
testable.onError(mockRequest, fakeException)
187187

188188
// THEN
189189
val argumentCaptor = argumentCaptor<Map<String, Any?>>()
@@ -203,7 +203,7 @@ internal class DatadogCoilRequestListenerTest {
203203
mockRequest = mockImageRequest(mockDrawable)
204204

205205
// WHEN
206-
underTest.onError(mockRequest, fakeException)
206+
testable.onError(mockRequest, fakeException)
207207

208208
// THEN
209209
val argumentCaptor = argumentCaptor<Map<String, Any?>>()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Built application files
2+
*.apk
3+
*.ap_
4+
*.aab
5+
6+
# Files for the ART/Dalvik VM
7+
*.dex
8+
9+
# Java class files
10+
*.class
11+
12+
# Generated files
13+
bin/
14+
gen/
15+
out/
16+
17+
# Gradle files
18+
build/
19+
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Datadog Integration for Coil 3
2+
3+
## Getting Started
4+
5+
To include the Datadog integration for [Coil 3][1] in your project, add the
6+
following to your application's `build.gradle` file.
7+
8+
```groovy
9+
dependencies {
10+
implementation "com.datadoghq:dd-sdk-android-rum:<latest-version>"
11+
implementation "com.datadoghq:dd-sdk-android-okhttp:<latest-version>"
12+
implementation "com.datadoghq:dd-sdk-android-coil3:<latest-version>"
13+
}
14+
```
15+
16+
### Initial Setup
17+
18+
1. Setup RUM monitoring, see the dedicated [Datadog Android RUM Collection documentation][2] to learn how.
19+
2. Setup OkHttp instrumentation with Datadog RUM SDK, see the [dedicated documentation][3] to learn how.
20+
21+
Follow Coil 3's [documentation][4] to:
22+
23+
- Create your own `ImageLoader` by providing your own `OkHttpClient` (configured with `DatadogInterceptor`) using `OkHttpNetworkFetcherFactory`.
24+
25+
```kotlin
26+
val okHttpClient = OkHttpClient.Builder()
27+
.addInterceptor(DatadogInterceptor.Builder(tracedHosts).build())
28+
.build()
29+
30+
val imageLoader = ImageLoader.Builder(context)
31+
.components {
32+
add(OkHttpNetworkFetcherFactory(okHttpClient))
33+
}
34+
.build()
35+
36+
SingletonImageLoader.setSafe { imageLoader }
37+
```
38+
39+
- Decorate the `ImageRequest.Builder` with the `DatadogCoilRequestListener` whenever you perform an image loading request.
40+
41+
```kotlin
42+
imageView.load(uri) {
43+
listener(DatadogCoilRequestListener())
44+
}
45+
```
46+
47+
With this setup:
48+
- The `DatadogInterceptor` on OkHttpClient automatically tracks Coil's network requests (creating APM Traces and RUM Resource events)
49+
- The `DatadogCoilRequestListener` reports image loading failures (creating RUM Error events)
50+
51+
## Contributing
52+
53+
For details on contributing, read the
54+
[Contributing Guide](../../CONTRIBUTING.md).
55+
56+
## License
57+
58+
[Apache License, v2.0](../../LICENSE)
59+
60+
[1]: https://github.com/coil-kt/coil
61+
[2]: https://docs.datadoghq.com/real_user_monitoring/android/?tab=kotlin
62+
[3]: https://docs.datadoghq.com/real_user_monitoring/android/advanced_configuration/?tab=kotlin#automatically-track-network-requests
63+
[4]: https://coil-kt.github.io/coil/upgrading_to_coil3/
64+

0 commit comments

Comments
 (0)