Skip to content

Commit 4b2d249

Browse files
authored
feat: Sessions Health Tracking (#2973)
* feat: Sessions Health Tracking * Rework _processEvent
1 parent b66cfb7 commit 4b2d249

File tree

21 files changed

+598
-109
lines changed

21 files changed

+598
-109
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: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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,67 @@ 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+
185+
/**
186+
* We should be using `Promise.all([windowLoaded, firstContentfulPaint])` here,
187+
* but, as always, it's not available in the IE10-11. Thanks IE.
188+
*/
189+
let loadResolved = document.readyState === 'complete';
190+
let fcpResolved = false;
191+
const possiblyEndSession = (): void => {
192+
if (fcpResolved && loadResolved) {
193+
hub.endSession();
194+
}
195+
};
196+
const resolveWindowLoaded = (): void => {
197+
loadResolved = true;
198+
possiblyEndSession();
199+
window.removeEventListener('load', resolveWindowLoaded);
200+
};
201+
202+
hub.startSession();
203+
204+
if (!loadResolved) {
205+
// IE doesn't support `{ once: true }` for event listeners, so we have to manually
206+
// attach and then detach it once completed.
207+
window.addEventListener('load', resolveWindowLoaded);
208+
}
209+
210+
try {
211+
const po = new PerformanceObserver((entryList, po) => {
212+
entryList.getEntries().forEach(entry => {
213+
if (entry.name === 'first-contentful-paint' && entry.startTime < firstHiddenTime) {
214+
po.disconnect();
215+
fcpResolved = true;
216+
possiblyEndSession();
217+
}
218+
});
219+
});
220+
221+
// There's no need to even attach this listener if `PerformanceObserver` constructor will fail,
222+
// so we do it below here.
223+
let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity;
224+
document.addEventListener(
225+
'visibilitychange',
226+
event => {
227+
firstHiddenTime = Math.min(firstHiddenTime, event.timeStamp);
228+
},
229+
{ once: true },
230+
);
231+
232+
po.observe({
233+
type: 'paint',
234+
buffered: true,
235+
});
236+
} catch (e) {
237+
fcpResolved = true;
238+
possiblyEndSession();
239+
}
240+
}

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: 23 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 { getGlobalObject, supportsReferrerPolicy, SyncPromise } from '@sentry/utils';
44

55
import { BaseTransport } from './base';
@@ -12,19 +12,32 @@ 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 originalPayload Original payload used to create SentryRequest
28+
*/
29+
private _sendRequest(sentryRequest: SentryRequest, originalPayload: Event | Session): PromiseLike<Response> {
30+
if (this._isRateLimited(sentryRequest.type)) {
1831
return Promise.reject({
19-
event,
20-
reason: `Transport locked till ${this._disabledUntil(eventType)} due to too many requests.`,
32+
event: originalPayload,
33+
type: sentryRequest.type,
34+
reason: `Transport locked till ${this._disabledUntil(sentryRequest.type)} due to too many requests.`,
2135
status: 429,
2236
});
2337
}
2438

25-
const sentryReq = eventToSentryRequest(event, this._api);
2639
const options: RequestInit = {
27-
body: sentryReq.body,
40+
body: sentryRequest.body,
2841
method: 'POST',
2942
// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
3043
// https://caniuse.com/#feat=referrer-policy
@@ -42,13 +55,13 @@ export class FetchTransport extends BaseTransport {
4255
return this._buffer.add(
4356
new SyncPromise<Response>((resolve, reject) => {
4457
global
45-
.fetch(sentryReq.url, options)
58+
.fetch(sentryRequest.url, options)
4659
.then(response => {
4760
const headers = {
4861
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
4962
'retry-after': response.headers.get('Retry-After'),
5063
};
51-
this._handleResponse({ eventType, response, headers, resolve, reject });
64+
this._handleResponse({ requestType: sentryRequest.type, response, headers, resolve, reject });
5265
})
5366
.catch(reject);
5467
}),

packages/browser/src/transports/xhr.ts

Lines changed: 23 additions & 11 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,30 @@ 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 originalPayload Original payload used to create SentryRequest
26+
*/
27+
private _sendRequest(sentryRequest: SentryRequest, originalPayload: Event | Session): PromiseLike<Response> {
28+
if (this._isRateLimited(sentryRequest.type)) {
1629
return Promise.reject({
17-
event,
18-
reason: `Transport locked till ${this._disabledUntil(eventType)} due to too many requests.`,
30+
event: originalPayload,
31+
type: sentryRequest.type,
32+
reason: `Transport locked till ${this._disabledUntil(sentryRequest.type)} due to too many requests.`,
1933
status: 429,
2034
});
2135
}
2236

23-
const sentryReq = eventToSentryRequest(event, this._api);
24-
2537
return this._buffer.add(
2638
new SyncPromise<Response>((resolve, reject) => {
2739
const request = new XMLHttpRequest();
@@ -32,17 +44,17 @@ export class XHRTransport extends BaseTransport {
3244
'x-sentry-rate-limits': request.getResponseHeader('X-Sentry-Rate-Limits'),
3345
'retry-after': request.getResponseHeader('Retry-After'),
3446
};
35-
this._handleResponse({ eventType, response: request, headers, resolve, reject });
47+
this._handleResponse({ requestType: sentryRequest.type, response: request, headers, resolve, reject });
3648
}
3749
};
3850

39-
request.open('POST', sentryReq.url);
51+
request.open('POST', sentryRequest.url);
4052
for (const header in this.options.headers) {
4153
if (this.options.headers.hasOwnProperty(header)) {
4254
request.setRequestHeader(header, this.options.headers[header]);
4355
}
4456
}
45-
request.send(sentryReq.body);
57+
request.send(sentryRequest.body);
4658
}),
4759
);
4860
}

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("Dropping session because custom transport doesn't implement sendSession");
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
*/

0 commit comments

Comments
 (0)