Skip to content

Commit 98151e2

Browse files
committed
Merge branch 'refs/heads/main' into stefanosiano/fix/strict-mode-followup
2 parents 4a910a4 + b77456b commit 98151e2

File tree

39 files changed

+1926
-206
lines changed

39 files changed

+1926
-206
lines changed

.github/workflows/danger.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Danger
22

33
on:
44
pull_request:
5-
types: [opened, synchronize, reopened, edited, ready_for_review]
5+
types: [opened, synchronize, reopened, edited, ready_for_review, labeled, unlabeled]
66

77
jobs:
88
danger:

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,40 @@
44

55
### Features
66

7+
- Attach MDC properties to logs as attributes ([#4786](https://github.com/getsentry/sentry-java/pull/4786))
8+
- MDC properties set using supported logging frameworks (Logback, Log4j2, java.util.Logging) are now attached to structured logs as attributes.
9+
- The attribute reflected on the log is `mdc.<key>`, where `<key>` is the original key in the MDC.
10+
- This means that you will be able to filter/aggregate logs in the product based on these properties.
11+
- Only properties with keys matching the configured `contextTags` are sent as log attributes.
12+
- You can configure which properties are sent using `options.setContextTags` if initalizing manually, or by specifying a comma-separated list of keys with a `context-tags` entry in `sentry.properties` or `sentry.context-tags` in `application.properties`.
13+
- Note that keys containing spaces are not supported.
714
- Add experimental Sentry Android Distribution module for integrating with Sentry Build Distribution to check for and install updates ([#4804](https://github.com/getsentry/sentry-java/pull/4804))
15+
- Allow passing a different `Handler` to `SystemEventsBreadcrumbsIntegration` and `AndroidConnectionStatusProvider` so their callbacks are deliver to that handler ([#4808](https://github.com/getsentry/sentry-java/pull/4808))
16+
- Session Replay: Add new _experimental_ Canvas Capture Strategy ([#4777](https://github.com/getsentry/sentry-java/pull/4777))
17+
- A new screenshot capture strategy that uses Android's Canvas API for more accurate and reliable text and image masking
18+
- Any `.drawText()` or `.drawBitmap()` calls are replaced by rectangles, ensuring no text or images are present in the resulting output
19+
- Note: If this strategy is used, all text and images will be masked, regardless of any masking configuration
20+
- To enable this feature, set the `screenshotStrategy`, either via code:
21+
```kotlin
22+
SentryAndroid.init(context) { options ->
23+
options.sessionReplay.screenshotStrategy = ScreenshotStrategyType.CANVAS
24+
}
25+
```
26+
or AndroidManifest.xml:
27+
```xml
28+
<application>
29+
<meta-data android:name="io.sentry.session-replay.screenshot-strategy" android:value="canvas" />
30+
</application>
31+
```
832

933
### Fixes
1034

1135
- Avoid StrictMode warnings (followup) ([#4809](https://github.com/getsentry/sentry-java/pull/4809))
1236
- Avoid StrictMode warnings ([#4724](https://github.com/getsentry/sentry-java/pull/4724))
1337
- Use logger from options for JVM profiler ([#4771](https://github.com/getsentry/sentry-java/pull/4771))
1438
- Session Replay: Avoid deadlock when pausing replay if no connection ([#4788](https://github.com/getsentry/sentry-java/pull/4788))
39+
- Session Replay: Fix capturing roots with no windows ([#4805](https://github.com/getsentry/sentry-java/pull/4805))
40+
- Session Replay: Fix `java.lang.IllegalArgumentException: width and height must be > 0` ([#4805](https://github.com/getsentry/sentry-java/pull/4805))
1541

1642
### Miscellaneous
1743

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime
9191
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" }
9292
androidx-sqlite = { module = "androidx.sqlite:sqlite", version = "2.5.2" }
9393
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.2.1" }
94+
androidx-browser = { module = "androidx.browser:browser", version = "1.8.0" }
9495
coil-compose = { module = "io.coil-kt:coil-compose", version = "2.6.0" }
9596
commons-compress = {module = "org.apache.commons:commons-compress", version = "1.25.0"}
9697
context-propagation = { module = "io.micrometer:context-propagation", version = "1.1.0" }

sentry-android-core/api/sentry-android-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerfo
446446

447447
public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable {
448448
public fun <init> (Landroid/content/Context;)V
449+
public fun <init> (Landroid/content/Context;Landroid/os/Handler;)V
449450
public fun <init> (Landroid/content/Context;Ljava/util/List;)V
450451
public fun close ()V
451452
public static fun getDefaultActions ()Ljava/util/List;

sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.sentry.ILogger;
77
import io.sentry.InitPriority;
88
import io.sentry.ProfileLifecycle;
9+
import io.sentry.ScreenshotStrategyType;
910
import io.sentry.SentryFeedbackOptions;
1011
import io.sentry.SentryIntegrationPackageStorage;
1112
import io.sentry.SentryLevel;
@@ -111,6 +112,7 @@ final class ManifestMetadataReader {
111112
static final String REPLAYS_MASK_ALL_IMAGES = "io.sentry.session-replay.mask-all-images";
112113

113114
static final String REPLAYS_DEBUG = "io.sentry.session-replay.debug";
115+
static final String REPLAYS_SCREENSHOT_STRATEGY = "io.sentry.session-replay.screenshot-strategy";
114116

115117
static final String FORCE_INIT = "io.sentry.force-init";
116118

@@ -476,6 +478,16 @@ static void applyMetadata(
476478

477479
options.getSessionReplay().setDebug(readBool(metadata, logger, REPLAYS_DEBUG, false));
478480

481+
final @Nullable String screenshotStrategyRaw =
482+
readString(metadata, logger, REPLAYS_SCREENSHOT_STRATEGY, null);
483+
if (screenshotStrategyRaw != null) {
484+
if ("canvas".equals(screenshotStrategyRaw.toLowerCase(Locale.ROOT))) {
485+
options.getSessionReplay().setScreenshotStrategy(ScreenshotStrategyType.CANVAS);
486+
} else {
487+
// always default to PIXEL_COPY
488+
options.getSessionReplay().setScreenshotStrategy(ScreenshotStrategyType.PIXEL_COPY);
489+
}
490+
}
479491
options.setIgnoredErrors(readList(metadata, logger, IGNORED_ERRORS));
480492

481493
final @Nullable List<String> includes = readList(metadata, logger, IN_APP_INCLUDES);

sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ private static void deduplicateIntegrations(
232232

233233
final List<Integration> timberIntegrations = new ArrayList<>();
234234
final List<Integration> fragmentIntegrations = new ArrayList<>();
235+
final List<Integration> systemEventsIntegrations = new ArrayList<>();
235236

236237
for (final Integration integration : options.getIntegrations()) {
237238
if (isFragmentAvailable) {
@@ -244,6 +245,9 @@ private static void deduplicateIntegrations(
244245
timberIntegrations.add(integration);
245246
}
246247
}
248+
if (integration instanceof SystemEventsBreadcrumbsIntegration) {
249+
systemEventsIntegrations.add(integration);
250+
}
247251
}
248252

249253
if (fragmentIntegrations.size() > 1) {
@@ -259,5 +263,12 @@ private static void deduplicateIntegrations(
259263
options.getIntegrations().remove(integration);
260264
}
261265
}
266+
267+
if (systemEventsIntegrations.size() > 1) {
268+
for (int i = 0; i < systemEventsIntegrations.size() - 1; i++) {
269+
final Integration integration = systemEventsIntegrations.get(i);
270+
options.getIntegrations().remove(integration);
271+
}
272+
}
262273
}
263274
}

sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,24 @@ public final class SystemEventsBreadcrumbsIntegration
7272
private final @NotNull AutoClosableReentrantLock receiverLock = new AutoClosableReentrantLock();
7373
// Track previous battery state to avoid duplicate breadcrumbs when values haven't changed
7474
private @Nullable BatteryState previousBatteryState;
75+
@TestOnly @Nullable Handler customHandler = null;
7576

7677
public SystemEventsBreadcrumbsIntegration(final @NotNull Context context) {
77-
this(context, getDefaultActionsInternal());
78+
this(context, getDefaultActionsInternal(), null);
79+
}
80+
81+
public SystemEventsBreadcrumbsIntegration(
82+
final @NotNull Context context, final @NotNull Handler handler) {
83+
this(context, getDefaultActionsInternal(), handler);
7884
}
7985

8086
SystemEventsBreadcrumbsIntegration(
81-
final @NotNull Context context, final @NotNull String[] actions) {
87+
final @NotNull Context context,
88+
final @NotNull String[] actions,
89+
final @Nullable Handler handler) {
8290
this.context = ContextUtils.getApplicationContext(context);
8391
this.actions = actions;
92+
this.customHandler = handler;
8493
}
8594

8695
public SystemEventsBreadcrumbsIntegration(
@@ -143,7 +152,7 @@ private void registerReceiver(
143152
filter.addAction(item);
144153
}
145154
}
146-
if (handlerThread == null) {
155+
if (customHandler == null && handlerThread == null) {
147156
handlerThread =
148157
new HandlerThread(
149158
"SystemEventsReceiver", Process.THREAD_PRIORITY_BACKGROUND);
@@ -154,7 +163,12 @@ private void registerReceiver(
154163
// official docs
155164

156165
// onReceive will be called on this handler thread
157-
final @NotNull Handler handler = new Handler(handlerThread.getLooper());
166+
@NotNull Handler handler;
167+
if (customHandler != null) {
168+
handler = customHandler;
169+
} else {
170+
handler = new Handler(handlerThread.getLooper());
171+
}
158172
ContextUtils.registerReceiver(context, options, receiver, filter, handler);
159173
if (!isReceiverRegistered.getAndSet(true)) {
160174
options

sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import android.net.Network;
99
import android.net.NetworkCapabilities;
1010
import android.os.Build;
11+
import android.os.Handler;
1112
import androidx.annotation.NonNull;
1213
import androidx.annotation.RequiresApi;
1314
import io.sentry.IConnectionStatusProvider;
@@ -42,6 +43,7 @@ public final class AndroidConnectionStatusProvider
4243
private final @NotNull BuildInfoProvider buildInfoProvider;
4344
private final @NotNull ICurrentDateProvider timeProvider;
4445
private final @NotNull List<IConnectionStatusObserver> connectionStatusObservers;
46+
private final @Nullable Handler handler;
4547
private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
4648
private volatile @Nullable NetworkCallback networkCallback;
4749

@@ -68,16 +70,26 @@ public final class AndroidConnectionStatusProvider
6870
private static final long CACHE_TTL_MS = 2 * 60 * 1000L; // 2 minutes
6971
private final @NotNull AtomicBoolean isConnected = new AtomicBoolean(false);
7072

71-
@SuppressLint("InlinedApi")
7273
public AndroidConnectionStatusProvider(
7374
@NotNull Context context,
7475
@NotNull SentryOptions options,
7576
@NotNull BuildInfoProvider buildInfoProvider,
7677
@NotNull ICurrentDateProvider timeProvider) {
78+
this(context, options, buildInfoProvider, timeProvider, null);
79+
}
80+
81+
@SuppressLint("InlinedApi")
82+
public AndroidConnectionStatusProvider(
83+
@NotNull Context context,
84+
@NotNull SentryOptions options,
85+
@NotNull BuildInfoProvider buildInfoProvider,
86+
@NotNull ICurrentDateProvider timeProvider,
87+
@Nullable Handler handler) {
7788
this.context = ContextUtils.getApplicationContext(context);
7889
this.options = options;
7990
this.buildInfoProvider = buildInfoProvider;
8091
this.timeProvider = timeProvider;
92+
this.handler = handler;
8193
this.connectionStatusObservers = new ArrayList<>();
8294

8395
capabilities[0] = NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -326,7 +338,8 @@ private boolean hasSignificantTransportChanges(
326338
}
327339
};
328340

329-
if (registerNetworkCallback(context, options.getLogger(), buildInfoProvider, callback)) {
341+
if (registerNetworkCallback(
342+
context, options.getLogger(), buildInfoProvider, handler, callback)) {
330343
networkCallback = callback;
331344
options.getLogger().log(SentryLevel.DEBUG, "Network callback registered successfully");
332345
} else {
@@ -744,6 +757,7 @@ static boolean registerNetworkCallback(
744757
final @NotNull Context context,
745758
final @NotNull ILogger logger,
746759
final @NotNull BuildInfoProvider buildInfoProvider,
760+
final @Nullable Handler handler,
747761
final @NotNull NetworkCallback networkCallback) {
748762
if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.N) {
749763
logger.log(SentryLevel.DEBUG, "NetworkCallbacks need Android N+.");
@@ -758,7 +772,11 @@ static boolean registerNetworkCallback(
758772
return false;
759773
}
760774
try {
761-
connectivityManager.registerDefaultNetworkCallback(networkCallback);
775+
if (handler != null) {
776+
connectivityManager.registerDefaultNetworkCallback(networkCallback, handler);
777+
} else {
778+
connectivityManager.registerDefaultNetworkCallback(networkCallback);
779+
}
762780
} catch (Throwable t) {
763781
logger.log(SentryLevel.WARNING, "registerDefaultNetworkCallback failed", t);
764782
return false;

sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1835,4 +1835,51 @@ class ManifestMetadataReaderTest {
18351835
// Assert
18361836
assertFalse(fixture.options.feedbackOptions.isShowBranding)
18371837
}
1838+
1839+
@Test
1840+
fun `applyMetadata reads screenshot strategy canvas to options`() {
1841+
// Arrange
1842+
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_SCREENSHOT_STRATEGY to "canvas")
1843+
val context = fixture.getContext(metaData = bundle)
1844+
1845+
// Act
1846+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1847+
1848+
// Assert
1849+
assertEquals(
1850+
io.sentry.ScreenshotStrategyType.CANVAS,
1851+
fixture.options.sessionReplay.screenshotStrategy,
1852+
)
1853+
}
1854+
1855+
@Test
1856+
fun `applyMetadata reads screenshot strategy and defaults to PIXEL_COPY for unknown value`() {
1857+
// Arrange
1858+
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_SCREENSHOT_STRATEGY to "unknown")
1859+
val context = fixture.getContext(metaData = bundle)
1860+
1861+
// Act
1862+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1863+
1864+
// Assert
1865+
assertEquals(
1866+
io.sentry.ScreenshotStrategyType.PIXEL_COPY,
1867+
fixture.options.sessionReplay.screenshotStrategy,
1868+
)
1869+
}
1870+
1871+
@Test
1872+
fun `applyMetadata reads screenshot strategy and keeps default if not found`() {
1873+
// Arrange
1874+
val context = fixture.getContext()
1875+
1876+
// Act
1877+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1878+
1879+
// Assert
1880+
assertEquals(
1881+
io.sentry.ScreenshotStrategyType.PIXEL_COPY,
1882+
fixture.options.sessionReplay.screenshotStrategy,
1883+
)
1884+
}
18381885
}

sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.app.ApplicationExitInfo
66
import android.content.Context
77
import android.os.Build
88
import android.os.Bundle
9+
import android.os.Handler
910
import android.os.Looper
1011
import android.os.SystemClock
1112
import androidx.test.core.app.ApplicationProvider
@@ -237,13 +238,19 @@ class SentryAndroidTest {
237238
}
238239

239240
@Test
240-
fun `deduplicates fragment and timber integrations`() {
241+
fun `deduplicates fragment, timber and system events integrations`() {
241242
var refOptions: SentryAndroidOptions? = null
242-
243243
fixture.initSut(autoInit = true) {
244244
it.addIntegration(FragmentLifecycleIntegration(ApplicationProvider.getApplicationContext()))
245245

246246
it.addIntegration(SentryTimberIntegration(minEventLevel = FATAL, minBreadcrumbLevel = DEBUG))
247+
248+
it.addIntegration(
249+
SystemEventsBreadcrumbsIntegration(
250+
ApplicationProvider.getApplicationContext(),
251+
CustomHandler(Looper.getMainLooper()),
252+
)
253+
)
247254
refOptions = it
248255
}
249256

@@ -256,6 +263,11 @@ class SentryAndroidTest {
256263
// fragment integration is not auto-installed in the test, since the context is not Application
257264
// but we just verify here that the single integration is preserved
258265
assertEquals(refOptions!!.integrations.filterIsInstance<FragmentLifecycleIntegration>().size, 1)
266+
267+
val systemEventsIntegrations =
268+
refOptions!!.integrations.filterIsInstance<SystemEventsBreadcrumbsIntegration>()
269+
assertEquals(systemEventsIntegrations.size, 1)
270+
assertTrue(systemEventsIntegrations.first().customHandler is CustomHandler)
259271
}
260272

261273
@Test
@@ -580,3 +592,5 @@ fun initForTest(context: Context, logger: ILogger) {
580592
fun initForTest(context: Context) {
581593
SentryAndroid.init(context)
582594
}
595+
596+
class CustomHandler(looper: Looper) : Handler(looper)

0 commit comments

Comments
 (0)