Skip to content

Commit a73d3ef

Browse files
committed
feat: Sessions Health Tracking
1 parent 5421dc9 commit a73d3ef

File tree

21 files changed

+561
-46
lines changed

21 files changed

+561
-46
lines changed

packages/browser/src/backend.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export interface BrowserOptions extends Options {
2929

3030
/** @deprecated use {@link Options.denyUrls} instead. */
3131
blacklistUrls?: Array<string | RegExp>;
32+
33+
/**
34+
* A flag enabling Sessions Tracking feature.
35+
* By default Sessions Tracking is disabled.
36+
*/
37+
autoSessionTracking?: boolean;
3238
}
3339

3440
/**

packages/browser/src/sdk.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getCurrentHub, initAndBind, Integrations as CoreIntegrations } from '@sentry/core';
2-
import { getGlobalObject, SyncPromise } from '@sentry/utils';
2+
import { getGlobalObject, logger, SyncPromise } from '@sentry/utils';
33

44
import { BrowserOptions } from './backend';
55
import { BrowserClient } from './client';
@@ -84,7 +84,15 @@ export function init(options: BrowserOptions = {}): void {
8484
options.release = window.SENTRY_RELEASE.id;
8585
}
8686
}
87+
if (options.autoSessionTracking === undefined) {
88+
options.autoSessionTracking = false;
89+
}
90+
8791
initAndBind(BrowserClient, options);
92+
93+
if (options.autoSessionTracking) {
94+
startSessionTracking();
95+
}
8896
}
8997

9098
/**
@@ -166,3 +174,54 @@ export function close(timeout?: number): PromiseLike<boolean> {
166174
export function wrap(fn: (...args: any) => any): any {
167175
return internalWrap(fn)();
168176
}
177+
178+
/**
179+
* Enable automatic Session Tracking for the initial page load.
180+
*/
181+
function startSessionTracking(): void {
182+
const window = getGlobalObject<Window>();
183+
const hub = getCurrentHub();
184+
let fcpResolved = false;
185+
let loadResolved = false;
186+
const possiblyEndSession = (): void => {
187+
if (fcpResolved && loadResolved) {
188+
hub.endSession();
189+
}
190+
};
191+
192+
hub.startSession();
193+
194+
window.addEventListener('load', () => {
195+
loadResolved = true;
196+
possiblyEndSession();
197+
});
198+
199+
try {
200+
let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity;
201+
document.addEventListener(
202+
'visibilitychange',
203+
event => {
204+
firstHiddenTime = Math.min(firstHiddenTime, event.timeStamp);
205+
},
206+
{ once: true },
207+
);
208+
209+
const po = new PerformanceObserver((entryList, po) => {
210+
entryList.getEntries().forEach(entry => {
211+
if (entry.name === 'first-contentful-paint' && entry.startTime < firstHiddenTime) {
212+
po.disconnect();
213+
fcpResolved = true;
214+
possiblyEndSession();
215+
}
216+
});
217+
});
218+
219+
po.observe({
220+
type: 'paint',
221+
buffered: true,
222+
});
223+
} catch (e) {
224+
fcpResolved = true;
225+
possiblyEndSession();
226+
}
227+
}

packages/browser/src/transports/base.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { API } from '@sentry/core';
2-
import { Event, Response, Status, Transport, TransportOptions } from '@sentry/types';
2+
import { Event, Response, SentryRequestType, Status, Transport, TransportOptions } from '@sentry/types';
33
import { logger, parseRetryAfterHeader, PromiseBuffer, SentryError } from '@sentry/utils';
44

55
/** Base Transport class implementation */
@@ -42,13 +42,13 @@ export abstract class BaseTransport implements Transport {
4242
* Handle Sentry repsonse for promise-based transports.
4343
*/
4444
protected _handleResponse({
45-
eventType,
45+
requestType,
4646
response,
4747
headers,
4848
resolve,
4949
reject,
5050
}: {
51-
eventType: string;
51+
requestType: SentryRequestType;
5252
response: globalThis.Response | XMLHttpRequest;
5353
headers: Record<string, string | null>;
5454
resolve: (value?: Response | PromiseLike<Response> | null | undefined) => void;
@@ -60,7 +60,7 @@ export abstract class BaseTransport implements Transport {
6060
* https://developer.mozilla.org/en-US/docs/Web/API/Headers/get
6161
*/
6262
const limited = this._handleRateLimit(headers);
63-
if (limited) logger.warn(`Too many requests, backing off till: ${this._disabledUntil(eventType)}`);
63+
if (limited) logger.warn(`Too many requests, backing off till: ${this._disabledUntil(requestType)}`);
6464

6565
if (status === Status.Success) {
6666
resolve({ status });

packages/browser/src/transports/fetch.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { eventToSentryRequest } from '@sentry/core';
2-
import { Event, Response } from '@sentry/types';
1+
import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core';
2+
import { Event, Response, SentryRequest, Session } from '@sentry/types';
33
import { getGlobalObject, supportsReferrerPolicy, SyncPromise } from '@sentry/utils';
44

55
import { BaseTransport } from './base';
@@ -12,19 +12,31 @@ export class FetchTransport extends BaseTransport {
1212
* @inheritDoc
1313
*/
1414
public sendEvent(event: Event): PromiseLike<Response> {
15-
const eventType = event.type || 'event';
15+
return this._sendRequest(eventToSentryRequest(event, this._api), event);
16+
}
17+
18+
/**
19+
* @inheritDoc
20+
*/
21+
public sendSession(session: Session): PromiseLike<Response> {
22+
return this._sendRequest(sessionToSentryRequest(session, this._api), session);
23+
}
1624

17-
if (this._isRateLimited(eventType)) {
25+
/**
26+
* @param sentryRequest Prepared SentryRequest to be delivered
27+
* @param event original payload used to create SentryRequest
28+
*/
29+
private _sendRequest(sentryRequest: SentryRequest, event: Event | Session): PromiseLike<Response> {
30+
if (this._isRateLimited(sentryRequest.type)) {
1831
return Promise.reject({
1932
event,
20-
reason: `Transport locked till ${this._disabledUntil(eventType)} due to too many requests.`,
33+
reason: `Transport locked till ${this._disabledUntil(sentryRequest.type)} due to too many requests.`,
2134
status: 429,
2235
});
2336
}
2437

25-
const sentryReq = eventToSentryRequest(event, this._api);
2638
const options: RequestInit = {
27-
body: sentryReq.body,
39+
body: sentryRequest.body,
2840
method: 'POST',
2941
// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
3042
// https://caniuse.com/#feat=referrer-policy
@@ -42,13 +54,13 @@ export class FetchTransport extends BaseTransport {
4254
return this._buffer.add(
4355
new SyncPromise<Response>((resolve, reject) => {
4456
global
45-
.fetch(sentryReq.url, options)
57+
.fetch(sentryRequest.url, options)
4658
.then(response => {
4759
const headers = {
4860
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
4961
'retry-after': response.headers.get('Retry-After'),
5062
};
51-
this._handleResponse({ eventType, response, headers, resolve, reject });
63+
this._handleResponse({ requestType: sentryRequest.type, response, headers, resolve, reject });
5264
})
5365
.catch(reject);
5466
}),

packages/browser/src/transports/xhr.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { eventToSentryRequest } from '@sentry/core';
2-
import { Event, Response } from '@sentry/types';
1+
import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core';
2+
import { Event, Response, SentryRequest, Session } from '@sentry/types';
33
import { SyncPromise } from '@sentry/utils';
44

55
import { BaseTransport } from './base';
@@ -10,18 +10,29 @@ export class XHRTransport extends BaseTransport {
1010
* @inheritDoc
1111
*/
1212
public sendEvent(event: Event): PromiseLike<Response> {
13-
const eventType = event.type || 'event';
13+
return this._sendRequest(eventToSentryRequest(event, this._api), event);
14+
}
1415

15-
if (this._isRateLimited(eventType)) {
16+
/**
17+
* @inheritDoc
18+
*/
19+
public sendSession(session: Session): PromiseLike<Response> {
20+
return this._sendRequest(sessionToSentryRequest(session, this._api), session);
21+
}
22+
23+
/**
24+
* @param sentryRequest Prepared SentryRequest to be delivered
25+
* @param event original payload used to create SentryRequest
26+
*/
27+
private _sendRequest(sentryRequest: SentryRequest, event: Event | Session): PromiseLike<Response> {
28+
if (this._isRateLimited(sentryRequest.type)) {
1629
return Promise.reject({
1730
event,
18-
reason: `Transport locked till ${this._disabledUntil(eventType)} due to too many requests.`,
31+
reason: `Transport locked till ${this._disabledUntil(sentryRequest.type)} due to too many requests.`,
1932
status: 429,
2033
});
2134
}
2235

23-
const sentryReq = eventToSentryRequest(event, this._api);
24-
2536
return this._buffer.add(
2637
new SyncPromise<Response>((resolve, reject) => {
2738
const request = new XMLHttpRequest();
@@ -32,17 +43,17 @@ export class XHRTransport extends BaseTransport {
3243
'x-sentry-rate-limits': request.getResponseHeader('X-Sentry-Rate-Limits'),
3344
'retry-after': request.getResponseHeader('Retry-After'),
3445
};
35-
this._handleResponse({ eventType, response: request, headers, resolve, reject });
46+
this._handleResponse({ requestType: sentryRequest.type, response: request, headers, resolve, reject });
3647
}
3748
};
3849

39-
request.open('POST', sentryReq.url);
50+
request.open('POST', sentryRequest.url);
4051
for (const header in this.options.headers) {
4152
if (this.options.headers.hasOwnProperty(header)) {
4253
request.setRequestHeader(header, this.options.headers[header]);
4354
}
4455
}
45-
request.send(sentryReq.body);
56+
request.send(sentryRequest.body);
4657
}),
4758
);
4859
}

packages/core/src/basebackend.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Event, EventHint, Options, Severity, Transport } from '@sentry/types';
1+
import { Event, EventHint, Options, Session, Severity, Transport } from '@sentry/types';
22
import { logger, SentryError } from '@sentry/utils';
33

44
import { NoopTransport } from './transports/noop';
@@ -34,6 +34,9 @@ export interface Backend {
3434
/** Submits the event to Sentry */
3535
sendEvent(event: Event): void;
3636

37+
/** Submits the session to Sentry */
38+
sendSession(session: Session): void;
39+
3740
/**
3841
* Returns the transport that is used by the backend.
3942
* Please note that the transport gets lazy initialized so it will only be there once the first event has been sent.
@@ -93,6 +96,20 @@ export abstract class BaseBackend<O extends Options> implements Backend {
9396
});
9497
}
9598

99+
/**
100+
* @inheritDoc
101+
*/
102+
public sendSession(session: Session): void {
103+
if (!this._transport.sendSession) {
104+
logger.warn('sendSession not implemented for a transport used');
105+
return;
106+
}
107+
108+
this._transport.sendSession(session).then(null, reason => {
109+
logger.error(`Error while sending session: ${reason}`);
110+
});
111+
}
112+
96113
/**
97114
* @inheritDoc
98115
*/

packages/core/src/baseclient.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
/* eslint-disable max-lines */
2-
import { Scope } from '@sentry/hub';
3-
import { Client, Event, EventHint, Integration, IntegrationClass, Options, Severity } from '@sentry/types';
2+
import { Scope, Session } from '@sentry/hub';
3+
import {
4+
Client,
5+
Event,
6+
EventHint,
7+
Integration,
8+
IntegrationClass,
9+
Options,
10+
SessionStatus,
11+
Severity,
12+
} from '@sentry/types';
413
import {
514
dateTimestampInSeconds,
615
Dsn,
@@ -141,6 +150,17 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
141150
return eventId;
142151
}
143152

153+
/**
154+
* @inheritDoc
155+
*/
156+
public captureSession(session: Session): void {
157+
if (!session.release) {
158+
logger.warn('Discarded session because of missing release');
159+
} else {
160+
this._sendSession(session);
161+
}
162+
}
163+
144164
/**
145165
* @inheritDoc
146166
*/
@@ -199,6 +219,49 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
199219
}
200220
}
201221

222+
/** Updates existing session based on the provided event */
223+
protected _updateSessionFromEvent(session: Session, event: Event): void {
224+
let crashed = false;
225+
let errored = false;
226+
let userAgent;
227+
const exceptions = event.exception && event.exception.values;
228+
229+
if (exceptions) {
230+
errored = true;
231+
232+
for (const ex of exceptions) {
233+
const mechanism = ex.mechanism;
234+
if (mechanism && mechanism.handled === false) {
235+
crashed = true;
236+
break;
237+
}
238+
}
239+
}
240+
241+
const user = event.user;
242+
if (!session.userAgent) {
243+
const headers = event.request ? event.request.headers : {};
244+
for (const key in headers) {
245+
if (key.toLowerCase() === 'user-agent') {
246+
userAgent = headers[key];
247+
break;
248+
}
249+
}
250+
}
251+
252+
session.update({
253+
...(crashed && { status: SessionStatus.Crashed }),
254+
user,
255+
userAgent,
256+
errors: session.errors + Number(errored || crashed),
257+
});
258+
}
259+
260+
/** Deliver captured session to Sentry */
261+
protected _sendSession(session: Session): void {
262+
this._getBackend().sendSession(session);
263+
}
264+
202265
/** Waits for the client to be done with processing. */
203266
protected _isClientProcessing(timeout?: number): PromiseLike<{ ready: boolean; interval: number }> {
204267
return new SyncPromise<{ ready: boolean; interval: number }>(resolve => {
@@ -434,7 +497,15 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
434497
const isInternalException =
435498
hint && hint.data && (hint.data as { [key: string]: unknown }).__sentry__ === true;
436499
// We skip beforeSend in case of transactions
500+
437501
if (isInternalException || !beforeSend || isTransaction) {
502+
if (!isTransaction) {
503+
const session = scope && scope.getSession();
504+
if (session) {
505+
this._updateSessionFromEvent(session, finalEvent);
506+
}
507+
}
508+
438509
this._sendEvent(finalEvent);
439510
resolve(finalEvent);
440511
return;
@@ -454,6 +525,11 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
454525
return;
455526
}
456527

528+
const session = scope && scope.getSession();
529+
if (session) {
530+
this._updateSessionFromEvent(session, finalEvent);
531+
}
532+
457533
// From here on we are really async
458534
this._sendEvent(finalEvent);
459535
resolve(finalEvent);

0 commit comments

Comments
 (0)