diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 15d0668c6db4..519d3cbc59eb 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -201,73 +201,14 @@ function startSessionTracking(): void { 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(); - } + hub.captureSession(); // We want to create a session for every navigation as well addInstrumentationHandler({ callback: () => { - if ( - !getCurrentHub() - .getScope() - ?.getSession() - ) { - hub.startSession(); - hub.endSession(); - } + hub.startSession(); + hub.captureSession(); }, type: 'history', }); diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 8a801ca79b0e..e0f7ab781c1a 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -156,6 +156,8 @@ export abstract class BaseClient implement logger.warn('Discarded session because of missing release'); } else { this._sendSession(session); + // After sending, we set init false to inidcate it's not the first occurence + session.update({ init: false }); } } @@ -252,6 +254,7 @@ export abstract class BaseClient implement userAgent, errors: session.errors + Number(errored || crashed), }); + this.captureSession(session); } /** Deliver captured session to Sentry */ diff --git a/packages/hub/src/hub.ts b/packages/hub/src/hub.ts index 1a3fb04ec00a..a25a77f841b1 100644 --- a/packages/hub/src/hub.ts +++ b/packages/hub/src/hub.ts @@ -13,6 +13,7 @@ import { IntegrationClass, Primitive, SessionContext, + SessionStatus, Severity, Span, SpanContext, @@ -360,10 +361,33 @@ export class Hub implements HubInterface { /** * @inheritDoc */ - public startSession(context?: SessionContext): Session { - // End existing session if there's one - this.endSession(); + public captureSession(endSession: boolean = false): void { + // both send the update and pull the session from the scope + if (endSession) { + return this.endSession(); + } + + // only send the update + this._sendSessionUpdate(); + } + + /** + * @inheritDoc + */ + public endSession(): void { + this.getStackTop() + ?.scope?.getSession() + ?.close(); + this._sendSessionUpdate(); + + // the session is over; take it off of the scope + this.getStackTop()?.scope?.setSession(); + } + /** + * @inheritDoc + */ + public startSession(context?: SessionContext): Session { const { scope, client } = this.getStackTop(); const { release, environment } = (client && client.getOptions()) || {}; const session = new Session({ @@ -372,26 +396,34 @@ export class Hub implements HubInterface { ...(scope && { user: scope.getUser() }), ...context, }); + if (scope) { + // End existing session if there's one + const currentSession = scope.getSession && scope.getSession(); + if (currentSession && currentSession.status === SessionStatus.Ok) { + currentSession.update({ status: SessionStatus.Exited }); + } + this.endSession(); + + // Afterwards we set the new session on the scope scope.setSession(session); } + return session; } /** - * @inheritDoc + * Sends the current Session on the scope */ - public endSession(): void { + private _sendSessionUpdate(): void { const { scope, client } = this.getStackTop(); if (!scope) return; const session = scope.getSession && scope.getSession(); if (session) { - session.close(); if (client && client.captureSession) { client.captureSession(session); } - scope.setSession(); } } diff --git a/packages/hub/src/session.ts b/packages/hub/src/session.ts index 3adde8a883a3..e911a72077bf 100644 --- a/packages/hub/src/session.ts +++ b/packages/hub/src/session.ts @@ -16,6 +16,7 @@ export class Session implements SessionInterface { public status: SessionStatus = SessionStatus.Ok; public environment?: string; public ipAddress?: string; + public init: boolean = true; constructor(context?: Omit) { if (context) { @@ -42,6 +43,9 @@ export class Session implements SessionInterface { // Good enough uuid validation. — Kamil this.sid = context.sid.length === 32 ? context.sid : uuid4(); } + if (context.init !== undefined) { + this.init = context.init; + } if (context.did) { this.did = `${context.did}`; } @@ -103,7 +107,7 @@ export class Session implements SessionInterface { } { return dropUndefinedKeys({ sid: `${this.sid}`, - init: true, + init: this.init, started: new Date(this.started).toISOString(), timestamp: new Date(this.timestamp).toISOString(), status: this.status, diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index a2d3218c2e78..22948f3f2024 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -222,4 +222,10 @@ export interface Hub { * Ends the session that lives on the current scope and sends it to Sentry */ endSession(): void; + + /** + * Sends the current session on the scope to Sentry + * @param endSession If set the session will be marked as exited and removed from the scope + */ + captureSession(endSession: boolean): void; } diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 2e479044e24f..c31a58224155 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -35,6 +35,7 @@ export interface Session extends SessionContext { export interface SessionContext { sid?: string; did?: string; + init?: boolean; timestamp?: number; started?: number; duration?: number;