-
-
Notifications
You must be signed in to change notification settings - Fork 468
Expand file tree
/
Copy pathUncaughtExceptionHandlerIntegration.java
More file actions
278 lines (244 loc) · 10.4 KB
/
UncaughtExceptionHandlerIntegration.java
File metadata and controls
278 lines (244 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
package io.sentry;
import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
import com.jakewharton.nopen.annotation.Open;
import io.sentry.exception.ExceptionMechanismException;
import io.sentry.hints.BlockingFlushHint;
import io.sentry.hints.EventDropReason;
import io.sentry.hints.SessionEnd;
import io.sentry.hints.TransactionEnd;
import io.sentry.protocol.Mechanism;
import io.sentry.protocol.SentryId;
import io.sentry.util.AutoClosableReentrantLock;
import io.sentry.util.HintUtils;
import io.sentry.util.Objects;
import java.io.Closeable;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
/**
* Sends any uncaught exception to Sentry, then passes the exception on to the pre-existing uncaught
* exception handler.
*/
public final class UncaughtExceptionHandlerIntegration
implements Integration, Thread.UncaughtExceptionHandler, Closeable {
/** Reference to the pre-existing uncaught exception handler. */
private @Nullable Thread.UncaughtExceptionHandler defaultExceptionHandler;
private static final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
private @Nullable IScopes scopes;
private @Nullable SentryOptions options;
private boolean registered = false;
private final @NotNull UncaughtExceptionHandler threadAdapter;
public UncaughtExceptionHandlerIntegration() {
this(UncaughtExceptionHandler.Adapter.getInstance());
}
UncaughtExceptionHandlerIntegration(final @NotNull UncaughtExceptionHandler threadAdapter) {
this.threadAdapter = threadAdapter;
}
@Override
public final void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
if (registered) {
options
.getLogger()
.log(
SentryLevel.ERROR,
"Attempt to register a UncaughtExceptionHandlerIntegration twice.");
return;
}
registered = true;
this.scopes = Objects.requireNonNull(scopes, "Scopes are required");
this.options = Objects.requireNonNull(options, "SentryOptions is required");
this.options
.getLogger()
.log(
SentryLevel.DEBUG,
"UncaughtExceptionHandlerIntegration enabled: %s",
this.options.isEnableUncaughtExceptionHandler());
if (this.options.isEnableUncaughtExceptionHandler()) {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
final Thread.UncaughtExceptionHandler currentHandler =
threadAdapter.getDefaultUncaughtExceptionHandler();
if (currentHandler != null) {
this.options
.getLogger()
.log(
SentryLevel.DEBUG,
"default UncaughtExceptionHandler class='"
+ currentHandler.getClass().getName()
+ "'");
if (currentHandler instanceof UncaughtExceptionHandlerIntegration) {
final UncaughtExceptionHandlerIntegration currentHandlerIntegration =
(UncaughtExceptionHandlerIntegration) currentHandler;
if (currentHandlerIntegration.scopes != null
&& scopes.getGlobalScope() == currentHandlerIntegration.scopes.getGlobalScope()) {
defaultExceptionHandler = currentHandlerIntegration.defaultExceptionHandler;
} else {
defaultExceptionHandler = currentHandler;
}
} else {
defaultExceptionHandler = currentHandler;
}
}
threadAdapter.setDefaultUncaughtExceptionHandler(this);
}
this.options
.getLogger()
.log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration installed.");
addIntegrationToSdkVersion("UncaughtExceptionHandler");
}
}
@Override
public void uncaughtException(Thread thread, Throwable thrown) {
if (options != null && scopes != null) {
options.getLogger().log(SentryLevel.INFO, "Uncaught exception received.");
try {
final UncaughtExceptionHint exceptionHint =
new UncaughtExceptionHint(options.getFlushTimeoutMillis(), options.getLogger());
final Throwable throwable = getUnhandledThrowable(thread, thrown);
final SentryEvent event = new SentryEvent(throwable);
event.setLevel(SentryLevel.FATAL);
final ITransaction transaction = scopes.getTransaction();
if (transaction == null && event.getEventId() != null) {
// if there's no active transaction on scope, this event can trigger flush notification
exceptionHint.setFlushable(event.getEventId());
}
final Hint hint = HintUtils.createWithTypeCheckHint(exceptionHint);
final @NotNull SentryId sentryId = scopes.captureEvent(event, hint);
final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID);
final EventDropReason eventDropReason = HintUtils.getEventDropReason(hint);
// in case the event has been dropped by multithreaded deduplicator, the other threads will
// crash the app without a chance to persist the main event so we have to special-case this
if (!isEventDropped
|| EventDropReason.MULTITHREADED_DEDUPLICATION.equals(eventDropReason)) {
// Block until the event is flushed to disk
if (!exceptionHint.waitFlush()) {
options
.getLogger()
.log(
SentryLevel.WARNING,
"Timed out waiting to flush event to disk before crashing. Event: %s",
event.getEventId());
}
}
} catch (Throwable e) {
options
.getLogger()
.log(SentryLevel.ERROR, "Error sending uncaught exception to Sentry.", e);
}
if (defaultExceptionHandler != null) {
options.getLogger().log(SentryLevel.INFO, "Invoking inner uncaught exception handler.");
defaultExceptionHandler.uncaughtException(thread, thrown);
} else {
if (options.isPrintUncaughtStackTrace()) {
thrown.printStackTrace();
}
}
}
}
@TestOnly
@NotNull
static Throwable getUnhandledThrowable(
final @NotNull Thread thread, final @NotNull Throwable thrown) {
final Mechanism mechanism = new Mechanism();
mechanism.setHandled(false);
mechanism.setType("UncaughtExceptionHandler");
return new ExceptionMechanismException(mechanism, thrown, thread);
}
/**
* Remove this UncaughtExceptionHandlerIntegration from the exception handler chain.
*
* <p>If this integration is currently the default handler, restore the initial handler, if this
* integration is not the current default call removeFromHandlerTree
*/
@Override
public void close() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
if (this == threadAdapter.getDefaultUncaughtExceptionHandler()) {
threadAdapter.setDefaultUncaughtExceptionHandler(defaultExceptionHandler);
if (options != null) {
options
.getLogger()
.log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration removed.");
}
} else {
removeFromHandlerTree(threadAdapter.getDefaultUncaughtExceptionHandler());
}
}
}
/**
* Intermediary method before calling the actual recursive method. Used to initialize HashSet to
* keep track of visited handlers to avoid infinite recursion in case of cycles in the chain.
*/
private void removeFromHandlerTree(@Nullable Thread.UncaughtExceptionHandler currentHandler) {
removeFromHandlerTree(currentHandler, new HashSet<>());
}
/**
* Recursively traverses the chain of UncaughtExceptionHandlerIntegrations to find and remove this
* specific integration instance.
*
* <p>Checks if this instance is the defaultExceptionHandler of the current handler, if so replace
* with its own defaultExceptionHandler, thus removing it from the chain.
*
* <p>If not, recursively calls itself on the next handler in the chain.
*
* <p>Recursion stops if the current handler is not an instance of
* UncaughtExceptionHandlerIntegration, the handler was found and removed or a cycle was detected.
*
* @param currentHandler The current handler in the chain to examine
* @param visited Set of already visited handlers to detect cycles
*/
private void removeFromHandlerTree(
@Nullable Thread.UncaughtExceptionHandler currentHandler,
@NotNull Set<Thread.UncaughtExceptionHandler> visited) {
if (currentHandler == null) {
if (options != null) {
options.getLogger().log(SentryLevel.DEBUG, "Found no UncaughtExceptionHandler to remove.");
}
return;
}
if (!visited.add(currentHandler)) {
if (options != null) {
options
.getLogger()
.log(
SentryLevel.WARNING,
"Cycle detected in UncaughtExceptionHandler chain while removing handler.");
}
return;
}
if (!(currentHandler instanceof UncaughtExceptionHandlerIntegration)) {
return;
}
final UncaughtExceptionHandlerIntegration currentHandlerIntegration =
(UncaughtExceptionHandlerIntegration) currentHandler;
if (this == currentHandlerIntegration.defaultExceptionHandler) {
currentHandlerIntegration.defaultExceptionHandler = defaultExceptionHandler;
if (options != null) {
options.getLogger().log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration removed.");
}
} else {
removeFromHandlerTree(currentHandlerIntegration.defaultExceptionHandler, visited);
}
}
@Open // open for tests
@ApiStatus.Internal
public static class UncaughtExceptionHint extends BlockingFlushHint
implements SessionEnd, TransactionEnd {
private final AtomicReference<SentryId> flushableEventId = new AtomicReference<>();
public UncaughtExceptionHint(final long flushTimeoutMillis, final @NotNull ILogger logger) {
super(flushTimeoutMillis, logger);
}
@Override
public boolean isFlushable(final @Nullable SentryId eventId) {
final SentryId unwrapped = flushableEventId.get();
return unwrapped != null && unwrapped.equals(eventId);
}
@Override
public void setFlushable(final @NotNull SentryId eventId) {
flushableEventId.set(eventId);
}
}
}