Skip to content

Commit b7c3e47

Browse files
Merge pull request #70 from bugsnag/session-tracking
Session tracking
2 parents 1578bbd + e2287c2 commit b7c3e47

30 files changed

+1445
-44
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 3.2.0-beta (2018-06-14)
4+
5+
**Important**: This is a beta release which introduces automatic tracking of sessions, which by
6+
default are captured for each HTTP request received via the Servlet API
7+
To disable this data collection, call `bugsnag.setAutoCaptureSessions(false)`.
8+
If you wish to use a custom strategy for tracking sessions, call `bugsnag.startSession()` in the
9+
appropriate place within your application.
10+
[Jamie Lynch](https://github.com/fractalwrench)
11+
[#70](https://github.com/bugsnag/bugsnag-java/pull/70)
12+
13+
**Deprecation notice**
14+
`setEndpoints(String notify, String session)` is now the preferred way to configure custom endpoints,
15+
if you are using Bugsnag On-Premise.
16+
317
## 3.1.6 (2018-05-03)
418
* Make preemptive copy of map filtering specified keys
519
[Leandro Aparecido](https://github.com/lehphyro)

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ dependencies {
3030
compileOnly 'javax.servlet:javax.servlet-api:3.1.0'
3131

3232
testCompile 'junit:junit:4.12'
33-
testCompile 'org.slf4j:slf4j-jdk14:1.7.25'
33+
testCompile 'org.slf4j:slf4j-simple:1.7.25'
3434
testCompile 'javax.servlet:javax.servlet-api:3.1.0'
3535
testCompile 'org.mockito:mockito-core:2.10.0'
3636
}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version=3.1.6
1+
version=3.2.0-beta
22
group=com.bugsnag
33

44
# Default properties
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
#Mon Jan 29 14:52:51 GMT 2018
12
distributionBase=GRADLE_USER_HOME
23
distributionPath=wrapper/dists
34
zipStoreBase=GRADLE_USER_HOME
45
zipStorePath=wrapper/dists
5-
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-bin.zip
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip

src/main/java/com/bugsnag/Bugsnag.java

Lines changed: 179 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,32 @@
77
import org.slf4j.Logger;
88
import org.slf4j.LoggerFactory;
99

10+
import java.lang.Thread.UncaughtExceptionHandler;
1011
import java.net.Proxy;
12+
import java.util.Collections;
13+
import java.util.Date;
14+
import java.util.Set;
15+
import java.util.concurrent.RejectedExecutionHandler;
16+
import java.util.concurrent.ScheduledThreadPoolExecutor;
17+
import java.util.concurrent.ThreadPoolExecutor;
18+
import java.util.concurrent.TimeUnit;
1119

1220
public class Bugsnag {
1321
private static final Logger LOGGER = LoggerFactory.getLogger(Bugsnag.class);
22+
private static final int SHUTDOWN_TIMEOUT_MS = 5000;
23+
private static final int SESSION_TRACKING_PERIOD_MS = 60000;
24+
private static final int CORE_POOL_SIZE = 1;
25+
26+
private ScheduledThreadPoolExecutor sessionExecutorService =
27+
new ScheduledThreadPoolExecutor(CORE_POOL_SIZE, new RejectedExecutionHandler() {
28+
@Override
29+
public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
30+
LOGGER.error("Rejected execution for sessionExecutorService");
31+
}
32+
});
1433

1534
private Configuration config;
35+
private final SessionTracker sessionTracker;
1636

1737
//
1838
// Constructors
@@ -39,11 +59,45 @@ public Bugsnag(String apiKey, boolean sendUncaughtExceptions) {
3959
}
4060

4161
config = new Configuration(apiKey);
62+
sessionTracker = new SessionTracker(config);
4263

4364
// Automatically send unhandled exceptions to Bugsnag using this Bugsnag
4465
if (sendUncaughtExceptions) {
4566
ExceptionHandler.enable(this);
4667
}
68+
addSessionTrackingShutdownHook();
69+
scheduleSessionFlushes();
70+
}
71+
72+
private void scheduleSessionFlushes() {
73+
sessionExecutorService.scheduleAtFixedRate(new Runnable() {
74+
@Override
75+
public void run() {
76+
sessionTracker.flushSessions(new Date());
77+
}
78+
}, SESSION_TRACKING_PERIOD_MS, SESSION_TRACKING_PERIOD_MS, TimeUnit.MILLISECONDS);
79+
}
80+
81+
private void addSessionTrackingShutdownHook() {
82+
Runtime.getRuntime().addShutdownHook(new Thread() {
83+
@Override
84+
public void run() {
85+
sessionTracker.shutdown();
86+
sessionExecutorService.shutdown();
87+
try {
88+
if (!sessionExecutorService
89+
.awaitTermination(SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
90+
LOGGER.warn("Shutdown of 'session tracking' threads"
91+
+ " took too long - forcing a shutdown");
92+
sessionExecutorService.shutdownNow();
93+
}
94+
} catch (InterruptedException ex) {
95+
LOGGER.warn("Shutdown of 'session tracking' thread "
96+
+ "was interrupted - forcing a shutdown");
97+
sessionExecutorService.shutdownNow();
98+
}
99+
}
100+
});
47101
}
48102

49103
//
@@ -106,13 +160,30 @@ public void setDelivery(Delivery delivery) {
106160
config.delivery = delivery;
107161
}
108162

163+
164+
/**
165+
* Set the method of delivery for Bugsnag sessions. By default we'll
166+
* send sessions asynchronously using a thread pool to
167+
* https://sessions.bugsnag.com, but you can override this to use a
168+
* different sending technique or endpoint (for example, if you are using
169+
* Bugsnag On-Premise).
170+
*
171+
* @param delivery the delivery mechanism to use
172+
* @see Delivery
173+
*/
174+
public void setSessionDelivery(Delivery delivery) {
175+
config.sessionDelivery = delivery;
176+
}
177+
109178
/**
110179
* Set the endpoint to deliver Bugsnag errors report to. This is a convenient
111180
* shorthand for bugsnag.getDelivery().setEndpoint();
112181
*
113182
* @param endpoint the endpoint to send reports to
114183
* @see #setDelivery
184+
* @deprecated use {@link Configuration#setEndpoints(String, String)} instead
115185
*/
186+
@Deprecated
116187
public void setEndpoint(String endpoint) {
117188
if (config.delivery instanceof HttpDelivery) {
118189
((HttpDelivery) config.delivery).setEndpoint(endpoint);
@@ -163,7 +234,7 @@ public void setProjectPackages(String... projectPackages) {
163234
}
164235

165236
/**
166-
* Set a proxy to use when delivering Bugsnag error reports. This is a convenient
237+
* Set a proxy to use when delivering Bugsnag error reports and sessions. This is a convenient
167238
* shorthand for bugsnag.getDelivery().setProxy();
168239
*
169240
* @param proxy the proxy to use to send reports
@@ -172,6 +243,9 @@ public void setProxy(Proxy proxy) {
172243
if (config.delivery instanceof HttpDelivery) {
173244
((HttpDelivery) config.delivery).setProxy(proxy);
174245
}
246+
if (config.sessionDelivery instanceof HttpDelivery) {
247+
((HttpDelivery) config.sessionDelivery).setProxy(proxy);
248+
}
175249
}
176250

177251
/**
@@ -198,7 +272,7 @@ public void setSendThreads(boolean sendThreads) {
198272
}
199273

200274
/**
201-
* Set a timeout (in ms) to use when delivering Bugsnag error reports.
275+
* Set a timeout (in ms) to use when delivering Bugsnag error reports and sessions.
202276
* This is a convenient shorthand for bugsnag.getDelivery().setTimeout();
203277
*
204278
* @param timeout the timeout to set (in ms)
@@ -208,6 +282,9 @@ public void setTimeout(int timeout) {
208282
if (config.delivery instanceof HttpDelivery) {
209283
((HttpDelivery) config.delivery).setTimeout(timeout);
210284
}
285+
if (config.sessionDelivery instanceof HttpDelivery) {
286+
((HttpDelivery) config.sessionDelivery).setTimeout(timeout);
287+
}
211288
}
212289

213290

@@ -369,17 +446,101 @@ public boolean notify(Report report, Callback reportCallback) {
369446
return false;
370447
}
371448

449+
// increment session handled/unhandled count
450+
Session session = sessionTracker.getSession();
451+
452+
if (session != null) {
453+
if (report.getUnhandled()) {
454+
session.incrementUnhandledCount();
455+
} else {
456+
session.incrementHandledCount();
457+
}
458+
report.setSession(session);
459+
}
460+
372461
// Build the notification
373462
Notification notification = new Notification(config, report);
374463

375464
// Deliver the notification
376465
LOGGER.debug("Reporting error to Bugsnag");
377466

378-
config.delivery.deliver(config.serializer, notification);
467+
config.delivery.deliver(config.serializer, notification, config.getErrorApiHeaders());
379468

380469
return true;
381470
}
382471

472+
/**
473+
* Manually starts tracking a new session.
474+
*
475+
* Note: sessions are currently tracked on a per-thread basis. Therefore, if this method were
476+
* called from Thread A then Thread B, two sessions would be considered 'active'. Any custom
477+
* strategy used to track sessions should take this into account.
478+
*
479+
* Automatic session tracking can be enabled via
480+
* {@link Bugsnag#setAutoCaptureSessions(boolean)}, which will automatically create a new
481+
* session for each request
482+
*/
483+
public void startSession() {
484+
sessionTracker.startSession(new Date(), false);
485+
}
486+
487+
/**
488+
* Sets whether or not Bugsnag should automatically capture and report User
489+
* sessions for each request.
490+
* <p>
491+
* By default this behavior is disabled.
492+
*
493+
* @param autoCaptureSessions whether sessions should be captured automatically
494+
*/
495+
public void setAutoCaptureSessions(boolean autoCaptureSessions) {
496+
config.setAutoCaptureSessions(autoCaptureSessions);
497+
}
498+
499+
/**
500+
* Retrieves whether or not Bugsnag should automatically capture
501+
* and report User sessions for each request.
502+
* @return whether sessions should be auto captured
503+
*/
504+
public boolean shouldAutoCaptureSessions() {
505+
return config.shouldAutoCaptureSessions();
506+
}
507+
508+
/**
509+
* Set the endpoint to deliver Bugsnag sessions to. This is a convenient
510+
* shorthand for bugsnag.getSessionDelivery().setEndpoint();
511+
*
512+
* @param endpoint the endpoint to send reports to
513+
* @see #setDelivery
514+
* @deprecated use {@link Configuration#setEndpoints(String, String)} instead
515+
*/
516+
@Deprecated
517+
public void setSessionEndpoint(String endpoint) {
518+
if (config.sessionDelivery instanceof HttpDelivery) {
519+
((HttpDelivery) config.sessionDelivery).setEndpoint(endpoint);
520+
}
521+
}
522+
523+
/**
524+
* Set the endpoints to send data to. By default we'll send error reports to
525+
* https://notify.bugsnag.com, and sessions to https://sessions.bugsnag.com, but you can
526+
* override this if you are using Bugsnag Enterprise to point to your own Bugsnag endpoint.
527+
*
528+
* Please note that it is recommended that you set both endpoints. If the notify endpoint is
529+
* missing, an exception will be thrown. If the session endpoint is missing, a warning will be
530+
* logged and sessions will not be sent automatically.
531+
*
532+
* Note that if you are setting a custom {@link Delivery}, this method should be called after
533+
* the custom implementation has been set.
534+
*
535+
* @param notify the notify endpoint
536+
* @param sessions the sessions endpoint
537+
*
538+
* @throws IllegalArgumentException if the notify endpoint is empty or null
539+
*/
540+
public void setEndpoints(String notify, String sessions) throws IllegalArgumentException {
541+
config.setEndpoints(notify, sessions);
542+
}
543+
383544
/**
384545
* Close the connection to Bugsnag and unlink the exception handler.
385546
*/
@@ -388,4 +549,19 @@ public void close() {
388549
config.delivery.close();
389550
ExceptionHandler.disable(this);
390551
}
552+
553+
/**
554+
* Retrieves all instances of {@link Bugsnag} which are registered to
555+
* catch uncaught exceptions.
556+
*
557+
* @return clients which catch uncaught exceptions
558+
*/
559+
public static Set<Bugsnag> uncaughtExceptionClients() {
560+
UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
561+
if (handler instanceof ExceptionHandler) {
562+
ExceptionHandler bugsnagHandler = (ExceptionHandler)handler;
563+
return Collections.unmodifiableSet(bugsnagHandler.uncaughtExceptionClients());
564+
}
565+
return Collections.emptySet();
566+
}
391567
}

0 commit comments

Comments
 (0)