Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Sessions Health Tracking #2973

Merged
merged 5 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
kamilogorek marked this conversation as resolved.
Show resolved Hide resolved
*/
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (entry.name === 'first-contentful-paint' && entry.startTime < firstHiddenTime) {
if (entry.name === 'first-contentful-paint' && entry.startTime < firstHiddenTime && !fcpResolved) {

Copy link
Member

@dashed dashed Oct 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should guarantee that the if condition is run at most once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observer is disconnected after the first call, so I'm not sure how it'd be ever called twice?

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