Skip to content

Commit

Permalink
feat: Sessions Health Tracking (#2973)
Browse files Browse the repository at this point in the history
* feat: Sessions Health Tracking
* Rework _processEvent
  • Loading branch information
kamilogorek committed Oct 15, 2020
1 parent b66cfb7 commit 4b2d249
Show file tree
Hide file tree
Showing 21 changed files with 598 additions and 109 deletions.
6 changes: 6 additions & 0 deletions packages/browser/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export interface BrowserOptions extends Options {

/** @deprecated use {@link Options.denyUrls} instead. */
blacklistUrls?: Array<string | RegExp>;

/**
* A flag enabling Sessions Tracking feature.
* By default Sessions Tracking is disabled.
*/
autoSessionTracking?: boolean;
}

/**
Expand Down
72 changes: 72 additions & 0 deletions packages/browser/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,15 @@ export function init(options: BrowserOptions = {}): void {
options.release = window.SENTRY_RELEASE.id;
}
}
if (options.autoSessionTracking === undefined) {
options.autoSessionTracking = false;
}

initAndBind(BrowserClient, options);

if (options.autoSessionTracking) {
startSessionTracking();
}
}

/**
Expand Down Expand Up @@ -166,3 +174,67 @@ export function close(timeout?: number): PromiseLike<boolean> {
export function wrap(fn: (...args: any) => any): any {
return internalWrap(fn)();
}

/**
* Enable automatic Session Tracking for the initial page load.
*/
function startSessionTracking(): void {
const window = getGlobalObject<Window>();
const hub = getCurrentHub();

/**
* We should be using `Promise.all([windowLoaded, firstContentfulPaint])` here,
* but, as always, it's not available in the IE10-11. Thanks IE.
*/
let loadResolved = document.readyState === 'complete';
let fcpResolved = false;
const possiblyEndSession = (): void => {
if (fcpResolved && loadResolved) {
hub.endSession();
}
};
const resolveWindowLoaded = (): void => {
loadResolved = true;
possiblyEndSession();
window.removeEventListener('load', resolveWindowLoaded);
};

hub.startSession();

if (!loadResolved) {
// IE doesn't support `{ once: true }` for event listeners, so we have to manually
// attach and then detach it once completed.
window.addEventListener('load', resolveWindowLoaded);
}

try {
const po = new PerformanceObserver((entryList, po) => {
entryList.getEntries().forEach(entry => {
if (entry.name === 'first-contentful-paint' && entry.startTime < firstHiddenTime) {
po.disconnect();
fcpResolved = true;
possiblyEndSession();
}
});
});

// There's no need to even attach this listener if `PerformanceObserver` constructor will fail,
// so we do it below here.
let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity;
document.addEventListener(
'visibilitychange',
event => {
firstHiddenTime = Math.min(firstHiddenTime, event.timeStamp);
},
{ once: true },
);

po.observe({
type: 'paint',
buffered: true,
});
} catch (e) {
fcpResolved = true;
possiblyEndSession();
}
}
8 changes: 4 additions & 4 deletions packages/browser/src/transports/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { API } from '@sentry/core';
import { Event, Response, Status, Transport, TransportOptions } from '@sentry/types';
import { Event, Response, SentryRequestType, Status, Transport, TransportOptions } from '@sentry/types';
import { logger, parseRetryAfterHeader, PromiseBuffer, SentryError } from '@sentry/utils';

/** Base Transport class implementation */
Expand Down Expand Up @@ -42,13 +42,13 @@ export abstract class BaseTransport implements Transport {
* Handle Sentry repsonse for promise-based transports.
*/
protected _handleResponse({
eventType,
requestType,
response,
headers,
resolve,
reject,
}: {
eventType: string;
requestType: SentryRequestType;
response: globalThis.Response | XMLHttpRequest;
headers: Record<string, string | null>;
resolve: (value?: Response | PromiseLike<Response> | null | undefined) => void;
Expand All @@ -60,7 +60,7 @@ export abstract class BaseTransport implements Transport {
* https://developer.mozilla.org/en-US/docs/Web/API/Headers/get
*/
const limited = this._handleRateLimit(headers);
if (limited) logger.warn(`Too many requests, backing off till: ${this._disabledUntil(eventType)}`);
if (limited) logger.warn(`Too many requests, backing off till: ${this._disabledUntil(requestType)}`);

if (status === Status.Success) {
resolve({ status });
Expand Down
33 changes: 23 additions & 10 deletions packages/browser/src/transports/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { eventToSentryRequest } from '@sentry/core';
import { Event, Response } from '@sentry/types';
import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core';
import { Event, Response, SentryRequest, Session } from '@sentry/types';
import { getGlobalObject, supportsReferrerPolicy, SyncPromise } from '@sentry/utils';

import { BaseTransport } from './base';
Expand All @@ -12,19 +12,32 @@ export class FetchTransport extends BaseTransport {
* @inheritDoc
*/
public sendEvent(event: Event): PromiseLike<Response> {
const eventType = event.type || 'event';
return this._sendRequest(eventToSentryRequest(event, this._api), event);
}

/**
* @inheritDoc
*/
public sendSession(session: Session): PromiseLike<Response> {
return this._sendRequest(sessionToSentryRequest(session, this._api), session);
}

if (this._isRateLimited(eventType)) {
/**
* @param sentryRequest Prepared SentryRequest to be delivered
* @param originalPayload Original payload used to create SentryRequest
*/
private _sendRequest(sentryRequest: SentryRequest, originalPayload: Event | Session): PromiseLike<Response> {
if (this._isRateLimited(sentryRequest.type)) {
return Promise.reject({
event,
reason: `Transport locked till ${this._disabledUntil(eventType)} due to too many requests.`,
event: originalPayload,
type: sentryRequest.type,
reason: `Transport locked till ${this._disabledUntil(sentryRequest.type)} due to too many requests.`,
status: 429,
});
}

const sentryReq = eventToSentryRequest(event, this._api);
const options: RequestInit = {
body: sentryReq.body,
body: sentryRequest.body,
method: 'POST',
// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
// https://caniuse.com/#feat=referrer-policy
Expand All @@ -42,13 +55,13 @@ export class FetchTransport extends BaseTransport {
return this._buffer.add(
new SyncPromise<Response>((resolve, reject) => {
global
.fetch(sentryReq.url, options)
.fetch(sentryRequest.url, options)
.then(response => {
const headers = {
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
'retry-after': response.headers.get('Retry-After'),
};
this._handleResponse({ eventType, response, headers, resolve, reject });
this._handleResponse({ requestType: sentryRequest.type, response, headers, resolve, reject });
})
.catch(reject);
}),
Expand Down
34 changes: 23 additions & 11 deletions packages/browser/src/transports/xhr.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { eventToSentryRequest } from '@sentry/core';
import { Event, Response } from '@sentry/types';
import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core';
import { Event, Response, SentryRequest, Session } from '@sentry/types';
import { SyncPromise } from '@sentry/utils';

import { BaseTransport } from './base';
Expand All @@ -10,18 +10,30 @@ export class XHRTransport extends BaseTransport {
* @inheritDoc
*/
public sendEvent(event: Event): PromiseLike<Response> {
const eventType = event.type || 'event';
return this._sendRequest(eventToSentryRequest(event, this._api), event);
}

if (this._isRateLimited(eventType)) {
/**
* @inheritDoc
*/
public sendSession(session: Session): PromiseLike<Response> {
return this._sendRequest(sessionToSentryRequest(session, this._api), session);
}

/**
* @param sentryRequest Prepared SentryRequest to be delivered
* @param originalPayload Original payload used to create SentryRequest
*/
private _sendRequest(sentryRequest: SentryRequest, originalPayload: Event | Session): PromiseLike<Response> {
if (this._isRateLimited(sentryRequest.type)) {
return Promise.reject({
event,
reason: `Transport locked till ${this._disabledUntil(eventType)} due to too many requests.`,
event: originalPayload,
type: sentryRequest.type,
reason: `Transport locked till ${this._disabledUntil(sentryRequest.type)} due to too many requests.`,
status: 429,
});
}

const sentryReq = eventToSentryRequest(event, this._api);

return this._buffer.add(
new SyncPromise<Response>((resolve, reject) => {
const request = new XMLHttpRequest();
Expand All @@ -32,17 +44,17 @@ export class XHRTransport extends BaseTransport {
'x-sentry-rate-limits': request.getResponseHeader('X-Sentry-Rate-Limits'),
'retry-after': request.getResponseHeader('Retry-After'),
};
this._handleResponse({ eventType, response: request, headers, resolve, reject });
this._handleResponse({ requestType: sentryRequest.type, response: request, headers, resolve, reject });
}
};

request.open('POST', sentryReq.url);
request.open('POST', sentryRequest.url);
for (const header in this.options.headers) {
if (this.options.headers.hasOwnProperty(header)) {
request.setRequestHeader(header, this.options.headers[header]);
}
}
request.send(sentryReq.body);
request.send(sentryRequest.body);
}),
);
}
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/basebackend.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Event, EventHint, Options, Severity, Transport } from '@sentry/types';
import { Event, EventHint, Options, Session, Severity, Transport } from '@sentry/types';
import { logger, SentryError } from '@sentry/utils';

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

/** Submits the session to Sentry */
sendSession(session: Session): void;

/**
* Returns the transport that is used by the backend.
* Please note that the transport gets lazy initialized so it will only be there once the first event has been sent.
Expand Down Expand Up @@ -93,6 +96,20 @@ export abstract class BaseBackend<O extends Options> implements Backend {
});
}

/**
* @inheritDoc
*/
public sendSession(session: Session): void {
if (!this._transport.sendSession) {
logger.warn("Dropping session because custom transport doesn't implement sendSession");
return;
}

this._transport.sendSession(session).then(null, reason => {
logger.error(`Error while sending session: ${reason}`);
});
}

/**
* @inheritDoc
*/
Expand Down

0 comments on commit 4b2d249

Please sign in to comment.