-
-
Notifications
You must be signed in to change notification settings - Fork 468
fix(gestures): Prevent duplicate ui.click breadcrumbs from buried window callbacks #5300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e60ca1b
792d200
8c36cbf
b7674b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,9 @@ | |
| import io.sentry.util.Objects; | ||
| import java.io.Closeable; | ||
| import java.io.IOException; | ||
| import java.lang.ref.WeakReference; | ||
| import java.util.ArrayList; | ||
| import java.util.WeakHashMap; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
|
|
@@ -30,6 +33,16 @@ public final class UserInteractionIntegration | |
|
|
||
| private final boolean isAndroidxLifecycleAvailable; | ||
|
|
||
| // WeakReference value, because the callback chain strongly references the wrapper β a strong | ||
| // value would prevent the window from ever being GC'd. | ||
| // | ||
| // All access must be guarded by wrappedWindowsLock β lifecycle callbacks fire on the main | ||
| // thread, but close() may be called from a background thread (e.g. Sentry.close()). | ||
| private final @NotNull WeakHashMap<Window, WeakReference<SentryWindowCallback>> wrappedWindows = | ||
| new WeakHashMap<>(); | ||
|
|
||
| private final @NotNull Object wrappedWindowsLock = new Object(); | ||
|
|
||
| public UserInteractionIntegration( | ||
| final @NotNull Application application, final @NotNull io.sentry.util.LoadClass classLoader) { | ||
| this.application = Objects.requireNonNull(application, "Application is required"); | ||
|
|
@@ -47,19 +60,26 @@ private void startTracking(final @NotNull Activity activity) { | |
| } | ||
|
|
||
| if (scopes != null && options != null) { | ||
| synchronized (wrappedWindowsLock) { | ||
| final @Nullable WeakReference<SentryWindowCallback> cached = wrappedWindows.get(window); | ||
| if (cached != null && cached.get() != null) { | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| Window.Callback delegate = window.getCallback(); | ||
| if (delegate == null) { | ||
| delegate = new NoOpWindowCallback(); | ||
| } | ||
|
|
||
| if (delegate instanceof SentryWindowCallback) { | ||
| // already instrumented | ||
| return; | ||
| } | ||
|
|
||
| final SentryGestureListener gestureListener = | ||
| new SentryGestureListener(activity, scopes, options); | ||
| window.setCallback(new SentryWindowCallback(delegate, activity, gestureListener, options)); | ||
| final SentryWindowCallback wrapper = | ||
| new SentryWindowCallback(delegate, activity, gestureListener, options); | ||
| window.setCallback(wrapper); | ||
| synchronized (wrappedWindowsLock) { | ||
| wrappedWindows.put(window, new WeakReference<>(wrapper)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -71,7 +91,10 @@ private void stopTracking(final @NotNull Activity activity) { | |
| } | ||
| return; | ||
| } | ||
| unwrapWindow(window); | ||
| } | ||
|
|
||
| private void unwrapWindow(final @NotNull Window window) { | ||
| final Window.Callback current = window.getCallback(); | ||
| if (current instanceof SentryWindowCallback) { | ||
| ((SentryWindowCallback) current).stopTracking(); | ||
|
|
@@ -80,6 +103,23 @@ private void stopTracking(final @NotNull Activity activity) { | |
| } else { | ||
| window.setCallback(((SentryWindowCallback) current).getDelegate()); | ||
| } | ||
| synchronized (wrappedWindowsLock) { | ||
| wrappedWindows.remove(window); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // Another wrapper (e.g. Session Replay) sits on top of ours β cutting it out of the chain | ||
| // would break its instrumentation, so we leave the chain alone and just call stopTracking() | ||
| // to release our resources. The upstream wrapper holds a reference to ours, so it'll be | ||
| // GC'd whenever that upstream holder is (typically when the window is destroyed). | ||
| final @Nullable SentryWindowCallback ours; | ||
| synchronized (wrappedWindowsLock) { | ||
| final @Nullable WeakReference<SentryWindowCallback> cached = wrappedWindows.remove(window); | ||
| ours = cached != null ? cached.get() : null; | ||
| } | ||
| if (ours != null) { | ||
| ours.stopTracking(); | ||
| } | ||
|
sentry[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
|
|
@@ -146,6 +186,21 @@ public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { | |
| public void close() throws IOException { | ||
| application.unregisterActivityLifecycleCallbacks(this); | ||
|
|
||
| // Restore original callbacks so a subsequent Sentry.init() starts from a clean chain instead | ||
| // of wrapping on top of our orphaned callback. | ||
| final ArrayList<Window> snapshot; | ||
| synchronized (wrappedWindowsLock) { | ||
| snapshot = new ArrayList<>(wrappedWindows.keySet()); | ||
| } | ||
| for (final Window window : snapshot) { | ||
| if (window != null) { | ||
| unwrapWindow(window); | ||
| } | ||
|
sentry[bot] marked this conversation as resolved.
|
||
| } | ||
| synchronized (wrappedWindowsLock) { | ||
| wrappedWindows.clear(); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Concurrent
|
||
|
|
||
| if (options != null) { | ||
| options.getLogger().log(SentryLevel.DEBUG, "UserInteractionIntegration removed."); | ||
| } | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.