Skip to content

Commit 7246a22

Browse files
authored
fix(release-health): Prevent sending terminal status session updates (#3701)
* fix(release-health): Prevent sending terminal status session updates Drops sending session updates for sessions that are already in terminal states and caps the number of errors for session at 1
1 parent 345fda7 commit 7246a22

File tree

8 files changed

+145
-27
lines changed

8 files changed

+145
-27
lines changed

packages/core/src/baseclient.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
228228
protected _updateSessionFromEvent(session: Session, event: Event): void {
229229
let crashed = false;
230230
let errored = false;
231-
let userAgent;
232231
const exceptions = event.exception && event.exception.values;
233232

234233
if (exceptions) {
@@ -243,24 +242,19 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
243242
}
244243
}
245244

246-
const user = event.user;
247-
if (!session.userAgent) {
248-
const headers = event.request ? event.request.headers : {};
249-
for (const key in headers) {
250-
if (key.toLowerCase() === 'user-agent') {
251-
userAgent = headers[key];
252-
break;
253-
}
254-
}
255-
}
245+
// A session is updated and that session update is sent in only one of the two following scenarios:
246+
// 1. Session with non terminal status and 0 errors + an error occurred -> Will set error count to 1 and send update
247+
// 2. Session with non terminal status and 1 error + a crash occurred -> Will set status crashed and send update
248+
const sessionNonTerminal = session.status === SessionStatus.Ok;
249+
const shouldUpdateAndSend = (sessionNonTerminal && session.errors === 0) || (sessionNonTerminal && crashed);
256250

257-
session.update({
258-
...(crashed && { status: SessionStatus.Crashed }),
259-
user,
260-
userAgent,
261-
errors: session.errors + Number(errored || crashed),
262-
});
263-
this.captureSession(session);
251+
if (shouldUpdateAndSend) {
252+
session.update({
253+
...(crashed && { status: SessionStatus.Crashed }),
254+
errors: session.errors || Number(errored || crashed),
255+
});
256+
this.captureSession(session);
257+
}
264258
}
265259

266260
/** Deliver captured session to Sentry */

packages/hub/src/hub.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,10 +424,16 @@ export class Hub implements HubInterface {
424424
public startSession(context?: SessionContext): Session {
425425
const { scope, client } = this.getStackTop();
426426
const { release, environment } = (client && client.getOptions()) || {};
427+
428+
// Will fetch userAgent if called from browser sdk
429+
const global = getGlobalObject<{ navigator?: { userAgent?: string } }>();
430+
const { userAgent } = global.navigator || {};
431+
427432
const session = new Session({
428433
release,
429434
environment,
430435
...(scope && { user: scope.getUser() }),
436+
...(userAgent && { userAgent }),
431437
...context,
432438
});
433439

packages/hub/src/session.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ export class Session implements SessionInterface {
3333
// eslint-disable-next-line complexity
3434
public update(context: SessionContext = {}): void {
3535
if (context.user) {
36-
if (context.user.ip_address) {
36+
if (!this.ipAddress && context.user.ip_address) {
3737
this.ipAddress = context.user.ip_address;
3838
}
3939

40-
if (!context.did) {
40+
if (!this.did && !context.did) {
4141
this.did = context.user.id || context.user.email || context.user.username;
4242
}
4343
}
@@ -53,7 +53,7 @@ export class Session implements SessionInterface {
5353
if (context.init !== undefined) {
5454
this.init = context.init;
5555
}
56-
if (context.did) {
56+
if (!this.did && context.did) {
5757
this.did = `${context.did}`;
5858
}
5959
if (typeof context.started === 'number') {
@@ -73,10 +73,10 @@ export class Session implements SessionInterface {
7373
if (context.environment) {
7474
this.environment = context.environment;
7575
}
76-
if (context.ipAddress) {
76+
if (!this.ipAddress && context.ipAddress) {
7777
this.ipAddress = context.ipAddress;
7878
}
79-
if (context.userAgent) {
79+
if (!this.userAgent && context.userAgent) {
8080
this.userAgent = context.userAgent;
8181
}
8282
if (typeof context.errors === 'number') {

packages/hub/test/session.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ describe('Session', () => {
6868
['sets an environment', { environment: 'staging' }, { attrs: { environment: 'staging' } }],
6969
['sets an ipAddress', { ipAddress: '0.0.0.0' }, { attrs: { ip_address: '0.0.0.0' } }],
7070
[
71-
'overwrites user ip_address did with custom ipAddress',
71+
'should not overwrite user ip_address did with custom ipAddress',
7272
{ ipAddress: '0.0.0.0', user: { ip_address: '1.1.1.1' } },
73-
{ attrs: { ip_address: '0.0.0.0' } },
73+
{ attrs: { ip_address: '1.1.1.1' } },
7474
],
7575
['sets an userAgent', { userAgent: 'Mozilla/5.0' }, { attrs: { user_agent: 'Mozilla/5.0' } }],
7676
['sets errors', { errors: 3 }, { errors: 3 }],

packages/node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"test:watch": "jest --watch",
6161
"test:express": "node test/manual/express-scope-separation/start.js",
6262
"test:webpack": "cd test/manual/webpack-domain/ && yarn && node npm-build.js",
63-
"test:release-health": "node test/manual/release-health/single-session/healthy-session.js && node test/manual/release-health/single-session/caught-exception-errored-session.js && node test/manual/release-health/single-session/uncaught-exception-crashed-session.js && node test/manual/release-health/single-session/unhandled-rejection-crashed-session.js && node test/manual/release-health/session-aggregates/aggregates-disable-single-session.js",
63+
"test:release-health": "node test/manual/release-health/single-session/healthy-session.js && node test/manual/release-health/single-session/caught-exception-errored-session.js && node test/manual/release-health/single-session/uncaught-exception-crashed-session.js && node test/manual/release-health/single-session/unhandled-rejection-crashed-session.js && node test/manual/release-health/session-aggregates/aggregates-disable-single-session.js && node test/manual/release-health/single-session/errors-in-session-capped-to-one.js & node test/manual/release-health/single-session/terminal-state-sessions-sent-once.js",
6464
"pack": "npm pack",
6565
"circularDepCheck": "madge --circular src/index.ts"
6666
},
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
const Sentry = require('../../../../dist');
2+
const {
3+
assertSessions,
4+
constructStrippedSessionObject,
5+
BaseDummyTransport,
6+
validateSessionCountFunction,
7+
} = require('../test-utils');
8+
9+
const sessionCounts = {
10+
sessionCounter: 0,
11+
expectedSessions: 2,
12+
};
13+
14+
validateSessionCountFunction(sessionCounts);
15+
16+
class DummyTransport extends BaseDummyTransport {
17+
sendSession(session) {
18+
sessionCounts.sessionCounter++;
19+
if (sessionCounts.sessionCounter === 1) {
20+
assertSessions(constructStrippedSessionObject(session), {
21+
init: true,
22+
status: 'ok',
23+
errors: 1,
24+
release: '1.1',
25+
});
26+
} else if (sessionCounts.sessionCounter === 2) {
27+
assertSessions(constructStrippedSessionObject(session), {
28+
init: false,
29+
status: 'exited',
30+
errors: 1,
31+
release: '1.1',
32+
});
33+
}
34+
return super.sendSession(session);
35+
}
36+
}
37+
38+
Sentry.init({
39+
dsn: 'http://[email protected]/1337',
40+
release: '1.1',
41+
transport: DummyTransport,
42+
autoSessionTracking: true,
43+
});
44+
/**
45+
* The following code snippet will throw multiple errors, and thereby send session updates everytime an error is
46+
* captured. However, the number of errors in the session should be capped at 1, regardless of how many errors there are
47+
*/
48+
for (let i = 0; i < 2; i++) {
49+
Sentry.captureException(new Error('hello world'));
50+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const Sentry = require('../../../../dist');
2+
const {
3+
assertSessions,
4+
constructStrippedSessionObject,
5+
BaseDummyTransport,
6+
validateSessionCountFunction,
7+
} = require('../test-utils');
8+
9+
const sessionCounts = {
10+
sessionCounter: 0,
11+
expectedSessions: 1,
12+
};
13+
14+
validateSessionCountFunction(sessionCounts);
15+
16+
class DummyTransport extends BaseDummyTransport {
17+
sendSession(session) {
18+
sessionCounts.sessionCounter++;
19+
20+
if (sessionCounts.sessionCounter === 1) {
21+
assertSessions(constructStrippedSessionObject(session), {
22+
init: true,
23+
status: 'crashed',
24+
errors: 1,
25+
release: '1.1',
26+
});
27+
}
28+
return super.sendSession(session);
29+
}
30+
}
31+
32+
Sentry.init({
33+
dsn: 'http://[email protected]/1337',
34+
release: '1.1',
35+
transport: DummyTransport,
36+
autoSessionTracking: true,
37+
});
38+
39+
/**
40+
* The following code snippet will throw an exception of `mechanism.handled` equal to `false`, and so this session
41+
* is treated as a Crashed Session.
42+
* However we want to ensure that once a crashed terminal state is achieved, no more session updates are sent regardless
43+
* of whether more crashes happen or not
44+
*/
45+
new Promise(function(resolve, reject) {
46+
reject();
47+
}).then(function() {
48+
console.log('Promise Resolved');
49+
});
50+
51+
new Promise(function(resolve, reject) {
52+
reject();
53+
}).then(function() {
54+
console.log('Promise Resolved');
55+
});

packages/node/test/manual/release-health/test-utils.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,17 @@ class BaseDummyTransport {
2626
}
2727
}
2828

29-
module.exports = { assertSessions, constructStrippedSessionObject, BaseDummyTransport };
29+
function validateSessionCountFunction(sessionCounts) {
30+
process.on('exit', exitCode => {
31+
const { sessionCounter, expectedSessions } = sessionCounts;
32+
if (sessionCounter !== expectedSessions) {
33+
console.log(`FAIL: Expected ${expectedSessions} Sessions, Received ${sessionCounter}.`);
34+
process.exitCode = 1;
35+
}
36+
if ((exitCode === 0) & !process.exitCode) {
37+
console.log('SUCCESS: All application mode sessions were sent to node transport as expected');
38+
}
39+
});
40+
}
41+
42+
module.exports = { assertSessions, constructStrippedSessionObject, BaseDummyTransport, validateSessionCountFunction };

0 commit comments

Comments
 (0)