diff --git a/.size-limit.js b/.size-limit.js index 45324236f6ac..c6e86836fd4c 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -15,7 +15,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init'), gzip: true, - limit: '24 KB', + limit: '24.1 KB', modifyWebpackConfig: function (config) { const webpack = require('webpack'); const TerserPlugin = require('terser-webpack-plugin'); diff --git a/dev-packages/node-integration-tests/suites/sessions/server.ts b/dev-packages/node-integration-tests/suites/sessions/server.ts index 62b154accd45..df2587aacfd4 100644 --- a/dev-packages/node-integration-tests/suites/sessions/server.ts +++ b/dev-packages/node-integration-tests/suites/sessions/server.ts @@ -1,11 +1,16 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; -import type { SessionFlusher } from '@sentry/core'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + integrations: [ + Sentry.httpIntegration({ + // Flush after 2 seconds (to avoid waiting for the default 60s) + sessionFlushingDelayMS: 2_000, + }), + ], }); import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; @@ -13,14 +18,6 @@ import express from 'express'; const app = express(); -// eslint-disable-next-line deprecation/deprecation -const flusher = (Sentry.getClient() as Sentry.NodeClient)['_sessionFlusher'] as SessionFlusher; - -// Flush after 2 seconds (to avoid waiting for the default 60s) -setTimeout(() => { - flusher?.flush(); -}, 2000); - app.get('/test/success', (_req, res) => { res.send('Success!'); }); diff --git a/docs/migration/v8-to-v9.md b/docs/migration/v8-to-v9.md index 775ea5c8fc3a..cd257fd987e8 100644 --- a/docs/migration/v8-to-v9.md +++ b/docs/migration/v8-to-v9.md @@ -218,6 +218,7 @@ The Sentry metrics beta has ended and the metrics API has been removed from the - Deprecated `RequestSessionStatus` type. No replacements. - Deprecated `SessionFlusherLike` type. No replacements. - Deprecated `SessionFlusher`. No replacements. +- Deprecated `initSessionFlusher` on `ServerRuntimeClient`. No replacements. The `httpIntegration` will flush sessions by itself. ## `@sentry/nestjs` diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 727fec079b55..80badfe3fa9d 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -32,6 +32,7 @@ import type { } from './types-hoist'; import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; +import { DEFAULT_ENVIRONMENT } from './constants'; import { getCurrentScope, getIsolationScope, getTraceContextFromScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; @@ -55,6 +56,7 @@ import { prepareEvent } from './utils/prepareEvent'; import { showSpanDropWarning } from './utils/spanUtils'; const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; +const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing or non-string release'; /** * Base implementation for all JavaScript SDK clients. @@ -236,13 +238,9 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public captureSession(session: Session): void { - if (!(typeof session.release === 'string')) { - DEBUG_BUILD && logger.warn('Discarded session because of missing or non-string release'); - } else { - this.sendSession(session); - // After sending, we set init false to indicate it's not the first occurrence - updateSession(session, { init: false }); - } + this.sendSession(session); + // After sending, we set init false to indicate it's not the first occurrence + updateSession(session, { init: false }); } /** @@ -371,6 +369,25 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public sendSession(session: Session | SessionAggregates): void { + // Backfill release and environment on session + const { release: clientReleaseOption, environment: clientEnvironmentOption = DEFAULT_ENVIRONMENT } = this._options; + if ('aggregates' in session) { + const sessionAttrs = session.attrs || {}; + if (!sessionAttrs.release && !clientReleaseOption) { + DEBUG_BUILD && logger.warn(MISSING_RELEASE_FOR_SESSION_ERROR); + return; + } + sessionAttrs.release = sessionAttrs.release || clientReleaseOption; + sessionAttrs.environment = sessionAttrs.environment || clientEnvironmentOption; + } else { + if (!session.release && !clientReleaseOption) { + DEBUG_BUILD && logger.warn(MISSING_RELEASE_FOR_SESSION_ERROR); + return; + } + session.release = session.release || clientReleaseOption; + session.environment = session.environment || clientEnvironmentOption; + } + const env = createSessionEnvelope(session, this._dsn, this._options._metadata, this._options.tunnel); // sendEnvelope should not throw diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 02ae30058b04..d9ccca362e43 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -14,7 +14,6 @@ import type { User, } from './types-hoist'; -import { DEFAULT_ENVIRONMENT } from './constants'; import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { CaptureContext } from './scope'; @@ -265,18 +264,13 @@ export function addEventProcessor(callback: EventProcessor): void { * @returns the new active session */ export function startSession(context?: SessionContext): Session { - const client = getClient(); const isolationScope = getIsolationScope(); const currentScope = getCurrentScope(); - const { release, environment = DEFAULT_ENVIRONMENT } = (client && client.getOptions()) || {}; - // Will fetch userAgent if called from browser sdk const { userAgent } = GLOBAL_OBJ.navigator || {}; const session = makeSession({ - release, - environment, user: currentScope.getUser() || isolationScope.getUser(), ...(userAgent && { userAgent }), ...context, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 98cb1bda04e9..036c31e9b1d2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -47,8 +47,6 @@ export { export { setAsyncContextStrategy } from './asyncContext'; export { getGlobalSingleton, getMainCarrier } from './carrier'; export { makeSession, closeSession, updateSession } from './session'; -// eslint-disable-next-line deprecation/deprecation -export { SessionFlusher } from './sessionflusher'; export { Scope } from './scope'; export type { CaptureContext, ScopeContext, ScopeData } from './scope'; export { notifyEventProcessors } from './eventProcessors'; diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 775944abf898..3f66d2053b34 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -12,7 +12,6 @@ import type { Extras, Primitive, PropagationContext, - RequestSession, Session, SeverityLevel, Span, @@ -50,11 +49,17 @@ export interface ScopeContext { contexts: Contexts; tags: { [key: string]: Primitive }; fingerprint: string[]; - // eslint-disable-next-line deprecation/deprecation - requestSession: RequestSession; propagationContext: PropagationContext; } +// TODO(v9): Add `normalizedRequest` +export interface SdkProcessingMetadata { + [key: string]: unknown; + requestSession?: { + status: 'ok' | 'errored' | 'crashed'; + }; +} + /** * Normalized data of the Scope, ready to be used. */ @@ -67,7 +72,7 @@ export interface ScopeData { contexts: Contexts; attachments: Attachment[]; propagationContext: PropagationContext; - sdkProcessingMetadata: { [key: string]: unknown }; + sdkProcessingMetadata: SdkProcessingMetadata; fingerprint: string[]; level?: SeverityLevel; transactionName?: string; @@ -112,7 +117,7 @@ export class Scope { * A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get * sent to Sentry */ - protected _sdkProcessingMetadata: { [key: string]: unknown }; + protected _sdkProcessingMetadata: SdkProcessingMetadata; /** Fingerprint */ protected _fingerprint?: string[]; @@ -131,10 +136,6 @@ export class Scope { /** Session */ protected _session?: Session; - /** Request Mode Session Status */ - // eslint-disable-next-line deprecation/deprecation - protected _requestSession?: RequestSession; - /** The client on this scope */ protected _client?: Client; @@ -183,7 +184,6 @@ export class Scope { newScope._transactionName = this._transactionName; newScope._fingerprint = this._fingerprint; newScope._eventProcessors = [...this._eventProcessors]; - newScope._requestSession = this._requestSession; newScope._attachments = [...this._attachments]; newScope._sdkProcessingMetadata = { ...this._sdkProcessingMetadata }; newScope._propagationContext = { ...this._propagationContext }; @@ -271,27 +271,6 @@ export class Scope { return this._user; } - /** - * Get the request session from this scope. - * - * @deprecated Use `getSession()` and `setSession()` instead of `getRequestSession()` and `setRequestSession()`; - */ - // eslint-disable-next-line deprecation/deprecation - public getRequestSession(): RequestSession | undefined { - return this._requestSession; - } - - /** - * Set the request session for this scope. - * - * @deprecated Use `getSession()` and `setSession()` instead of `getRequestSession()` and `setRequestSession()`; - */ - // eslint-disable-next-line deprecation/deprecation - public setRequestSession(requestSession?: RequestSession): this { - this._requestSession = requestSession; - return this; - } - /** * Set an object that will be merged into existing tags on the scope, * and will be sent as tags data with the event. @@ -422,13 +401,12 @@ export class Scope { const scopeToMerge = typeof captureContext === 'function' ? captureContext(this) : captureContext; - const [scopeInstance, requestSession] = + const scopeInstance = scopeToMerge instanceof Scope - ? // eslint-disable-next-line deprecation/deprecation - [scopeToMerge.getScopeData(), scopeToMerge.getRequestSession()] + ? scopeToMerge.getScopeData() : isPlainObject(scopeToMerge) - ? [captureContext as ScopeContext, (captureContext as ScopeContext).requestSession] - : []; + ? (captureContext as ScopeContext) + : undefined; const { tags, extra, user, contexts, level, fingerprint = [], propagationContext } = scopeInstance || {}; @@ -452,10 +430,6 @@ export class Scope { this._propagationContext = propagationContext; } - if (requestSession) { - this._requestSession = requestSession; - } - return this; } @@ -473,7 +447,6 @@ export class Scope { this._level = undefined; this._transactionName = undefined; this._fingerprint = undefined; - this._requestSession = undefined; this._session = undefined; _setSpanForScope(this, undefined); this._attachments = []; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index e1d89c1d067b..479682d06998 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -17,7 +17,6 @@ import { createCheckInEnvelope } from './checkin'; import { getIsolationScope, getTraceContextFromScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { Scope } from './scope'; -import { SessionFlusher } from './sessionflusher'; import { getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan, @@ -42,9 +41,6 @@ export interface ServerRuntimeClientOptions extends ClientOptions extends BaseClient { - // eslint-disable-next-line deprecation/deprecation - protected _sessionFlusher: SessionFlusher | undefined; - /** * Creates a new Edge SDK instance. * @param options Configuration options for this SDK. @@ -83,22 +79,7 @@ export class ServerRuntimeClient< * @inheritDoc */ public captureException(exception: unknown, hint?: EventHint, scope?: Scope): string { - // Check if `_sessionFlusher` exists because it is initialized (defined) only when the `autoSessionTracking` is enabled. - // The expectation is that session aggregates are only sent when `autoSessionTracking` is enabled. - // TODO(v9): Our goal in the future is to not have the `autoSessionTracking` option and instead rely on integrations doing the creation and sending of sessions. We will not have a central kill-switch for sessions. - // TODO(v9): This should move into the httpIntegration. - // eslint-disable-next-line deprecation/deprecation - if (this._options.autoSessionTracking && this._sessionFlusher) { - // eslint-disable-next-line deprecation/deprecation - const requestSession = getIsolationScope().getRequestSession(); - - // Necessary checks to ensure this is code block is executed only within a request - // Should override the status only if `requestSession.status` is `Ok`, which is its initial stage - if (requestSession && requestSession.status === 'ok') { - requestSession.status = 'errored'; - } - } - + setCurrentRequestSessionErroredOrCrashed(hint); return super.captureException(exception, hint, scope); } @@ -106,63 +87,15 @@ export class ServerRuntimeClient< * @inheritDoc */ public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string { - // Check if `_sessionFlusher` exists because it is initialized only when the `autoSessionTracking` is enabled. - // The expectation is that session aggregates are only sent when `autoSessionTracking` is enabled. - // TODO(v9): Our goal in the future is to not have the `autoSessionTracking` option and instead rely on integrations doing the creation and sending of sessions. We will not have a central kill-switch for sessions. - // TODO(v9): This should move into the httpIntegration. - // eslint-disable-next-line deprecation/deprecation - if (this._options.autoSessionTracking && this._sessionFlusher) { - const eventType = event.type || 'exception'; - const isException = - eventType === 'exception' && event.exception && event.exception.values && event.exception.values.length > 0; - - // If the event is of type Exception, then a request session should be captured - if (isException) { - // eslint-disable-next-line deprecation/deprecation - const requestSession = getIsolationScope().getRequestSession(); - - // Ensure that this is happening within the bounds of a request, and make sure not to override - // Session Status if Errored / Crashed - if (requestSession && requestSession.status === 'ok') { - requestSession.status = 'errored'; - } - } + // If the event is of type Exception, then a request session should be captured + const isException = !event.type && event.exception && event.exception.values && event.exception.values.length > 0; + if (isException) { + setCurrentRequestSessionErroredOrCrashed(hint); } return super.captureEvent(event, hint, scope); } - /** - * - * @inheritdoc - */ - public close(timeout?: number): PromiseLike { - if (this._sessionFlusher) { - this._sessionFlusher.close(); - } - return super.close(timeout); - } - - /** - * Initializes an instance of SessionFlusher on the client which will aggregate and periodically flush session data. - * - * NOTICE: This method will implicitly create an interval that is periodically called. - * To clean up this resources, call `.close()` when you no longer intend to use the client. - * Not doing so will result in a memory leak. - */ - public initSessionFlusher(): void { - const { release, environment } = this._options; - if (!release) { - DEBUG_BUILD && logger.warn('Cannot initialize an instance of SessionFlusher if no release is provided!'); - } else { - // eslint-disable-next-line deprecation/deprecation - this._sessionFlusher = new SessionFlusher(this, { - release, - environment, - }); - } - } - /** * Create a cron monitor check in and send it to Sentry. * @@ -173,7 +106,7 @@ export class ServerRuntimeClient< public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string { const id = 'checkInId' in checkIn && checkIn.checkInId ? checkIn.checkInId : uuid4(); if (!this._isEnabled()) { - DEBUG_BUILD && logger.warn('SDK not enabled, will not capture checkin.'); + DEBUG_BUILD && logger.warn('SDK not enabled, will not capture check-in.'); return id; } @@ -227,20 +160,6 @@ export class ServerRuntimeClient< return id; } - /** - * Method responsible for capturing/ending a request session by calling `incrementSessionStatusCount` to increment - * appropriate session aggregates bucket - * - * @deprecated This method should not be used or extended. It's functionality will move into the `httpIntegration` and not be part of any public API. - */ - protected _captureRequestSession(): void { - if (!this._sessionFlusher) { - DEBUG_BUILD && logger.warn('Discarded request mode session because autoSessionTracking option was disabled'); - } else { - this._sessionFlusher.incrementSessionStatusCount(); - } - } - /** * @inheritDoc */ @@ -285,3 +204,20 @@ export class ServerRuntimeClient< return [dynamicSamplingContext, traceContext]; } } + +function setCurrentRequestSessionErroredOrCrashed(eventHint?: EventHint): void { + const requestSession = getIsolationScope().getScopeData().sdkProcessingMetadata.requestSession; + if (requestSession) { + // We mutate instead of doing `setSdkProcessingMetadata` because the http integration stores away a particular + // isolationScope. If that isolation scope is forked, setting the processing metadata here will not mutate the + // original isolation scope that the http integration stored away. + const isHandledException = eventHint?.mechanism?.handled ?? true; + // A request session can go from "errored" -> "crashed" but not "crashed" -> "errored". + // Crashed (unhandled exception) is worse than errored (handled exception). + if (isHandledException && requestSession.status !== 'crashed') { + requestSession.status = 'errored'; + } else if (!isHandledException) { + requestSession.status = 'crashed'; + } + } +} diff --git a/packages/core/src/sessionflusher.ts b/packages/core/src/sessionflusher.ts deleted file mode 100644 index 2434023bf797..000000000000 --- a/packages/core/src/sessionflusher.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { getIsolationScope } from './currentScopes'; -import type { - AggregationCounts, - Client, - RequestSessionStatus, - SessionAggregates, - SessionFlusherLike, -} from './types-hoist'; -import { dropUndefinedKeys } from './utils-hoist/object'; - -type ReleaseHealthAttributes = { - environment?: string; - release: string; -}; - -/** - * @deprecated `SessionFlusher` is deprecated and will be removed in the next major version of the SDK. - */ -// TODO(v9): The goal for the SessionFlusher is to become a stupidly simple mechanism to aggregate "Sessions" (actually "RequestSessions"). It should probably live directly inside the Http integration/instrumentation. -// eslint-disable-next-line deprecation/deprecation -export class SessionFlusher implements SessionFlusherLike { - public readonly flushTimeout: number; - private _pendingAggregates: Map; - private _sessionAttrs: ReleaseHealthAttributes; - // We adjust the type here to add the `unref()` part, as setInterval can technically return a number or a NodeJS.Timer - private readonly _intervalId: ReturnType & { unref?: () => void }; - private _isEnabled: boolean; - private _client: Client; - - public constructor(client: Client, attrs: ReleaseHealthAttributes) { - this._client = client; - this.flushTimeout = 60; - this._pendingAggregates = new Map(); - this._isEnabled = true; - - // Call to setInterval, so that flush is called every 60 seconds. - this._intervalId = setInterval(() => this.flush(), this.flushTimeout * 1000); - if (this._intervalId.unref) { - this._intervalId.unref(); - } - this._sessionAttrs = attrs; - } - - /** Checks if `pendingAggregates` has entries, and if it does flushes them by calling `sendSession` */ - public flush(): void { - const sessionAggregates = this.getSessionAggregates(); - if (sessionAggregates.aggregates.length === 0) { - return; - } - this._pendingAggregates = new Map(); - this._client.sendSession(sessionAggregates); - } - - /** Massages the entries in `pendingAggregates` and returns aggregated sessions */ - public getSessionAggregates(): SessionAggregates { - const aggregates: AggregationCounts[] = Array.from(this._pendingAggregates.values()); - - const sessionAggregates: SessionAggregates = { - attrs: this._sessionAttrs, - aggregates, - }; - return dropUndefinedKeys(sessionAggregates); - } - - /** JSDoc */ - public close(): void { - clearInterval(this._intervalId); - this._isEnabled = false; - this.flush(); - } - - /** - * Wrapper function for _incrementSessionStatusCount that checks if the instance of SessionFlusher is enabled then - * fetches the session status of the request from `Scope.getRequestSession().status` on the scope and passes them to - * `_incrementSessionStatusCount` along with the start date - */ - public incrementSessionStatusCount(): void { - if (!this._isEnabled) { - return; - } - const isolationScope = getIsolationScope(); - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - - if (requestSession && requestSession.status) { - this._incrementSessionStatusCount(requestSession.status, new Date()); - // This is not entirely necessarily but is added as a safe guard to indicate the bounds of a request and so in - // case captureRequestSession is called more than once to prevent double count - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession(undefined); - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ - } - } - - /** - * Increments status bucket in pendingAggregates buffer (internal state) corresponding to status of - * the session received - */ - // eslint-disable-next-line deprecation/deprecation - private _incrementSessionStatusCount(status: RequestSessionStatus, date: Date): number { - // Truncate minutes and seconds on Session Started attribute to have one minute bucket keys - const sessionStartedTrunc = new Date(date).setSeconds(0, 0); - - // corresponds to aggregated sessions in one specific minute bucket - // for example, {"started":"2021-03-16T08:00:00.000Z","exited":4, "errored": 1} - let aggregationCounts = this._pendingAggregates.get(sessionStartedTrunc); - if (!aggregationCounts) { - aggregationCounts = { started: new Date(sessionStartedTrunc).toISOString() }; - this._pendingAggregates.set(sessionStartedTrunc, aggregationCounts); - } - - switch (status) { - case 'errored': - aggregationCounts.errored = (aggregationCounts.errored || 0) + 1; - return aggregationCounts.errored; - case 'ok': - aggregationCounts.exited = (aggregationCounts.exited || 0) + 1; - return aggregationCounts.exited; - default: - aggregationCounts.crashed = (aggregationCounts.crashed || 0) + 1; - return aggregationCounts.crashed; - } - } -} diff --git a/packages/core/src/types-hoist/index.ts b/packages/core/src/types-hoist/index.ts index 82cae72af76d..e74eca7ae927 100644 --- a/packages/core/src/types-hoist/index.ts +++ b/packages/core/src/types-hoist/index.ts @@ -103,12 +103,6 @@ export type { Session, SessionContext, SessionStatus, - // eslint-disable-next-line deprecation/deprecation - RequestSession, - // eslint-disable-next-line deprecation/deprecation - RequestSessionStatus, - // eslint-disable-next-line deprecation/deprecation - SessionFlusherLike, SerializedSession, } from './session'; diff --git a/packages/core/src/types-hoist/session.ts b/packages/core/src/types-hoist/session.ts index 47cfa348acbb..589c71f9094c 100644 --- a/packages/core/src/types-hoist/session.ts +++ b/packages/core/src/types-hoist/session.ts @@ -1,13 +1,5 @@ import type { User } from './user'; -/** - * @deprecated This type is deprecated and will be removed in the next major version of the SDK. - */ -export interface RequestSession { - // eslint-disable-next-line deprecation/deprecation - status?: RequestSessionStatus; -} - export interface Session { sid: string; did?: string | number; @@ -40,11 +32,6 @@ export type SessionContext = Partial; export type SessionStatus = 'ok' | 'exited' | 'crashed' | 'abnormal'; -/** - * @deprecated This type is deprecated and will be removed in the next major version of the SDK. - */ -export type RequestSessionStatus = 'ok' | 'errored' | 'crashed'; - /** JSDoc */ export interface SessionAggregates { attrs?: { @@ -54,27 +41,14 @@ export interface SessionAggregates { aggregates: Array; } -/** - * @deprecated This type is deprecated and will be removed in the next major version of the SDK. - */ -export interface SessionFlusherLike { - /** - * Increments the Session Status bucket in SessionAggregates Object corresponding to the status of the session - * captured - */ - incrementSessionStatusCount(): void; - - /** Empties Aggregate Buckets and Sends them to Transport Buffer */ - flush(): void; - - /** Clears setInterval and calls flush */ - close(): void; -} - export interface AggregationCounts { + /** ISO Timestamp rounded to the second */ started: string; - errored?: number; + /** Number of sessions that did not have errors */ exited?: number; + /** Number of sessions that had handled errors */ + errored?: number; + /** Number of sessions that had unhandled errors */ crashed?: number; } diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index d6463b73d9d3..508a72e8857b 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -356,7 +356,6 @@ const captureContextKeys: readonly ScopeContextProperty[] = [ 'contexts', 'tags', 'fingerprint', - 'requestSession', 'propagationContext', ] as const; diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index 5f8be2050a9e..628c040f8d16 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -162,7 +162,6 @@ describe('parseEventHintOrCaptureContext', () => { contexts: { os: { name: 'linux' } }, tags: { foo: 'bar' }, fingerprint: ['xx', 'yy'], - requestSession: { status: 'ok' }, propagationContext: { traceId: 'xxx', spanId: 'yyy', @@ -175,9 +174,9 @@ describe('parseEventHintOrCaptureContext', () => { it('triggers a TS error if trying to mix ScopeContext & EventHint', () => { const actual = parseEventHintOrCaptureContext({ + mechanism: { handled: false }, // @ts-expect-error We are specifically testing that this errors! user: { id: 'xxx' }, - mechanism: { handled: false }, }); // ScopeContext takes presedence in this case, but this is actually not supported diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index 76130e779fed..6a2bab364d4e 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -6,7 +6,7 @@ import { withIsolationScope, withScope, } from '../../src'; -import type { Breadcrumb, Client, Event, RequestSessionStatus } from '../../src/types-hoist'; +import type { Breadcrumb, Client, Event } from '../../src/types-hoist'; import { Scope } from '../../src/scope'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; @@ -259,15 +259,6 @@ describe('Scope', () => { expect(parentScope['_extra']).toEqual(scope['_extra']); }); - test('_requestSession clone', () => { - const parentScope = new Scope(); - // eslint-disable-next-line deprecation/deprecation - parentScope.setRequestSession({ status: 'errored' }); - const scope = parentScope.clone(); - // eslint-disable-next-line deprecation/deprecation - expect(parentScope.getRequestSession()).toEqual(scope.getRequestSession()); - }); - test('parent changed inheritance', () => { const parentScope = new Scope(); const scope = parentScope.clone(); @@ -286,26 +277,6 @@ describe('Scope', () => { expect(scope['_extra']).toEqual({ a: 2 }); }); - test('child override should set the value of parent _requestSession', () => { - // Test that ensures if the status value of `status` of `_requestSession` is changed in a child scope - // that it should also change in parent scope because we are copying the reference to the object - const parentScope = new Scope(); - // eslint-disable-next-line deprecation/deprecation - parentScope.setRequestSession({ status: 'errored' }); - - const scope = parentScope.clone(); - // eslint-disable-next-line deprecation/deprecation - const requestSession = scope.getRequestSession(); - if (requestSession) { - requestSession.status = 'ok'; - } - - // eslint-disable-next-line deprecation/deprecation - expect(parentScope.getRequestSession()).toEqual({ status: 'ok' }); - // eslint-disable-next-line deprecation/deprecation - expect(scope.getRequestSession()).toEqual({ status: 'ok' }); - }); - test('should clone propagation context', () => { const parentScope = new Scope(); const scope = parentScope.clone(); @@ -322,12 +293,9 @@ describe('Scope', () => { scope.setUser({ id: '1' }); scope.setFingerprint(['abcd']); scope.addBreadcrumb({ message: 'test' }); - // eslint-disable-next-line deprecation/deprecation - scope.setRequestSession({ status: 'ok' }); expect(scope['_extra']).toEqual({ a: 2 }); scope.clear(); expect(scope['_extra']).toEqual({}); - expect(scope['_requestSession']).toEqual(undefined); expect(scope['_propagationContext']).toEqual({ traceId: expect.any(String), spanId: expect.any(String), @@ -356,8 +324,6 @@ describe('Scope', () => { scope.setUser({ id: '1337' }); scope.setLevel('info'); scope.setFingerprint(['foo']); - // eslint-disable-next-line deprecation/deprecation - scope.setRequestSession({ status: 'ok' }); }); test('given no data, returns the original scope', () => { @@ -405,7 +371,6 @@ describe('Scope', () => { localScope.setUser({ id: '42' }); localScope.setLevel('warning'); localScope.setFingerprint(['bar']); - (localScope as any)._requestSession = { status: 'ok' }; const updatedScope = scope.update(localScope) as any; @@ -427,7 +392,6 @@ describe('Scope', () => { expect(updatedScope._user).toEqual({ id: '42' }); expect(updatedScope._level).toEqual('warning'); expect(updatedScope._fingerprint).toEqual(['bar']); - expect(updatedScope._requestSession.status).toEqual('ok'); // @ts-expect-error accessing private property for test expect(updatedScope._propagationContext).toEqual(localScope._propagationContext); }); @@ -450,7 +414,6 @@ describe('Scope', () => { expect(updatedScope._user).toEqual({ id: '1337' }); expect(updatedScope._level).toEqual('info'); expect(updatedScope._fingerprint).toEqual(['foo']); - expect(updatedScope._requestSession.status).toEqual('ok'); }); test('given a plain object, it should merge two together, with the passed object having priority', () => { @@ -461,8 +424,6 @@ describe('Scope', () => { level: 'warning' as const, tags: { bar: '3', baz: '4' }, user: { id: '42' }, - // eslint-disable-next-line deprecation/deprecation - requestSession: { status: 'errored' as RequestSessionStatus }, propagationContext: { traceId: '8949daf83f4a4a70bee4c1eb9ab242ed', spanId: 'a024ad8fea82680e', @@ -490,7 +451,6 @@ describe('Scope', () => { expect(updatedScope._user).toEqual({ id: '42' }); expect(updatedScope._level).toEqual('warning'); expect(updatedScope._fingerprint).toEqual(['bar']); - expect(updatedScope._requestSession).toEqual({ status: 'errored' }); expect(updatedScope._propagationContext).toEqual({ traceId: '8949daf83f4a4a70bee4c1eb9ab242ed', spanId: 'a024ad8fea82680e', diff --git a/packages/core/test/lib/sessionflusher.test.ts b/packages/core/test/lib/sessionflusher.test.ts deleted file mode 100644 index 53fe930b58c2..000000000000 --- a/packages/core/test/lib/sessionflusher.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { Client } from '../../src/types-hoist'; - -import { SessionFlusher } from '../../src/sessionflusher'; - -describe('Session Flusher', () => { - let sendSession: jest.Mock; - let mockClient: Client; - - beforeEach(() => { - jest.useFakeTimers(); - sendSession = jest.fn(() => Promise.resolve({ status: 'success' })); - mockClient = { - sendSession, - } as unknown as Client; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - test('test incrementSessionStatusCount updates the internal SessionFlusher state', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); - - const date = new Date('2021-04-08T12:18:23.043Z'); - let count = (flusher as any)._incrementSessionStatusCount('ok', date); - expect(count).toEqual(1); - count = (flusher as any)._incrementSessionStatusCount('ok', date); - expect(count).toEqual(2); - count = (flusher as any)._incrementSessionStatusCount('errored', date); - expect(count).toEqual(1); - date.setMinutes(date.getMinutes() + 1); - count = (flusher as any)._incrementSessionStatusCount('ok', date); - expect(count).toEqual(1); - count = (flusher as any)._incrementSessionStatusCount('errored', date); - expect(count).toEqual(1); - - expect(flusher.getSessionAggregates().aggregates).toEqual([ - { errored: 1, exited: 2, started: '2021-04-08T12:18:00.000Z' }, - { errored: 1, exited: 1, started: '2021-04-08T12:19:00.000Z' }, - ]); - expect(flusher.getSessionAggregates().attrs).toEqual({ release: '1.0.0', environment: 'dev' }); - }); - - test('test undefined attributes are excluded, on incrementSessionStatusCount call', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.0' }); - - const date = new Date('2021-04-08T12:18:23.043Z'); - (flusher as any)._incrementSessionStatusCount('ok', date); - (flusher as any)._incrementSessionStatusCount('errored', date); - - expect(flusher.getSessionAggregates()).toEqual({ - aggregates: [{ errored: 1, exited: 1, started: '2021-04-08T12:18:00.000Z' }], - attrs: { release: '1.0.0' }, - }); - }); - - test('flush is called every 60 seconds after initialisation of an instance of SessionFlusher', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); - jest.advanceTimersByTime(59000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(0); - jest.advanceTimersByTime(2000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(58000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(2000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(2); - }); - - test('sendSessions is called on flush if sessions were captured', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); - const date = new Date('2021-04-08T12:18:23.043Z'); - (flusher as any)._incrementSessionStatusCount('ok', date); - (flusher as any)._incrementSessionStatusCount('ok', date); - - expect(sendSession).toHaveBeenCalledTimes(0); - - jest.advanceTimersByTime(61000); - - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - expect(sendSession).toHaveBeenCalledWith( - expect.objectContaining({ - attrs: { release: '1.0.0', environment: 'dev' }, - aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }], - }), - ); - }); - - test('sendSessions is not called on flush if no sessions were captured', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); - - expect(sendSession).toHaveBeenCalledTimes(0); - jest.advanceTimersByTime(61000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - expect(sendSession).toHaveBeenCalledTimes(0); - }); - - test('calling close on SessionFlusher should disable SessionFlusher', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.x' }); - flusher.close(); - expect((flusher as any)._isEnabled).toEqual(false); - }); - - test('calling close on SessionFlusher will force call flush', () => { - // eslint-disable-next-line deprecation/deprecation - const flusher = new SessionFlusher(mockClient, { release: '1.0.x' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); - const date = new Date('2021-04-08T12:18:23.043Z'); - (flusher as any)._incrementSessionStatusCount('ok', date); - (flusher as any)._incrementSessionStatusCount('ok', date); - flusher.close(); - - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - expect(sendSession).toHaveBeenCalledWith( - expect.objectContaining({ - attrs: { release: '1.0.x' }, - aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }], - }), - ); - }); -}); diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 355815ec0689..7e16856ff023 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -1,10 +1,12 @@ +/* eslint-disable max-lines */ import type * as http from 'node:http'; import type { IncomingMessage, RequestOptions } from 'node:http'; import type * as https from 'node:https'; +import type { EventEmitter } from 'node:stream'; import { VERSION } from '@opentelemetry/core'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; -import type { RequestEventData, SanitizedRequestData, Scope } from '@sentry/core'; +import type { AggregationCounts, Client, RequestEventData, SanitizedRequestData, Scope } from '@sentry/core'; import { addBreadcrumb, getBreadcrumbLogLevelFromHttpStatusCode, @@ -18,9 +20,9 @@ import { withIsolationScope, } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; -import type { NodeClient } from '../../sdk/client'; import { getRequestUrl } from '../../utils/getRequestUrl'; import { getRequestInfo } from './vendor/getRequestInfo'; + type Http = typeof http; type Https = typeof https; @@ -42,6 +44,21 @@ type SentryHttpInstrumentationOptions = InstrumentationConfig & { * @param request Contains the {@type RequestOptions} object used to make the outgoing request. */ ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean; + + /** + * Whether the integration should create [Sessions](https://docs.sentry.io/product/releases/health/#sessions) for incoming requests to track the health and crash-free rate of your releases in Sentry. + * Read more about Release Health: https://docs.sentry.io/product/releases/health/ + * + * Defaults to `true`. + */ + trackIncomingRequestsAsSessions?: boolean; + + /** + * Number of milliseconds until sessions tracked with `trackIncomingRequestsAsSessions` will be flushed as a session aggregate. + * + * Defaults to `60000` (60s). + */ + sessionFlushingDelayMS?: number; }; // We only want to capture request bodies up to 1mb. @@ -134,6 +151,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase(); - // eslint-disable-next-line deprecation/deprecation - if (client && client.getOptions().autoSessionTracking) { - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession({ status: 'ok' }); - } - // attempt to update the scope's `transactionName` based on the request URL // Ideally, framework instrumentations coming after the HttpInstrumentation // update the transactionName once we get a parameterized route. @@ -163,6 +174,14 @@ export class SentryHttpInstrumentation extends InstrumentationBase { return original.apply(this, [event, ...args]); }); @@ -363,8 +382,8 @@ function patchRequestToCaptureBody(req: IncomingMessage, isolationScope: Scope): if (event === 'data') { const callback = new Proxy(listener, { apply: (target, thisArg, args: Parameters) => { - // If we have already read more than the max body length, we stop addiing chunks - // To avoid growing the memory indefinitely if a respons is e.g. streamed + // If we have already read more than the max body length, we stop adding chunks + // To avoid growing the memory indefinitely if a response is e.g. streamed if (getChunksSize() < MAX_BODY_BYTE_LENGTH) { const chunk = args[0] as Buffer; chunks.push(chunk); @@ -432,3 +451,78 @@ function patchRequestToCaptureBody(req: IncomingMessage, isolationScope: Scope): // ignore errors if we can't patch stuff } } + +/** + * Starts a session and tracks it in the context of a given isolation scope. + * When the passed response is finished, the session is put into a task and is + * aggregated with other sessions that may happen in a certain time window + * (sessionFlushingDelayMs). + * + * The sessions are always aggregated by the client that is on the current scope + * at the time of ending the response (if there is one). + */ +// Exported for unit tests +export function recordRequestSession({ + requestIsolationScope, + response, + sessionFlushingDelayMS, +}: { requestIsolationScope: Scope; response: EventEmitter; sessionFlushingDelayMS?: number }): void { + requestIsolationScope.setSDKProcessingMetadata({ + requestSession: { status: 'ok' }, + }); + response.once('close', () => { + // We need to grab the client off the current scope instead of the isolation scope because the isolation scope doesn't hold any client out of the box. + const client = getClient(); + const requestSession = requestIsolationScope.getScopeData().sdkProcessingMetadata.requestSession; + + if (client && requestSession) { + DEBUG_BUILD && logger.debug(`Recorded request session with status: ${requestSession.status}`); + + const roundedDate = new Date(); + roundedDate.setSeconds(0, 0); + const dateBucketKey = roundedDate.toISOString(); + + const existingClientAggregate = clientToRequestSessionAggregatesMap.get(client); + const bucket = existingClientAggregate?.[dateBucketKey] || { exited: 0, crashed: 0, errored: 0 }; + bucket[({ ok: 'exited', crashed: 'crashed', errored: 'errored' } as const)[requestSession.status]]++; + + if (existingClientAggregate) { + existingClientAggregate[dateBucketKey] = bucket; + } else { + DEBUG_BUILD && logger.debug('Opened new request session aggregate.'); + const newClientAggregate = { [dateBucketKey]: bucket }; + clientToRequestSessionAggregatesMap.set(client, newClientAggregate); + + const flushPendingClientAggregates = (): void => { + clearTimeout(timeout); + unregisterClientFlushHook(); + clientToRequestSessionAggregatesMap.delete(client); + + const aggregatePayload: AggregationCounts[] = Object.entries(newClientAggregate).map( + ([timestamp, value]) => ({ + started: timestamp, + exited: value.exited, + errored: value.errored, + crashed: value.crashed, + }), + ); + client.sendSession({ aggregates: aggregatePayload }); + }; + + const unregisterClientFlushHook = client.on('flush', () => { + DEBUG_BUILD && logger.debug('Sending request session aggregate due to client flush'); + flushPendingClientAggregates(); + }); + const timeout = setTimeout(() => { + DEBUG_BUILD && logger.debug('Sending request session aggregate due to flushing schedule'); + flushPendingClientAggregates(); + }, sessionFlushingDelayMS).unref(); + } + } + }); +} + +const clientToRequestSessionAggregatesMap = new Map< + Client, + { [timestampRoundedToSeconds: string]: { exited: number; crashed: number; errored: number } } +>(); diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts index f06e12979074..b63754bb017f 100644 --- a/packages/node/src/integrations/http/index.ts +++ b/packages/node/src/integrations/http/index.ts @@ -3,8 +3,7 @@ import { diag } from '@opentelemetry/api'; import type { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import type { Span } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { getClient } from '@sentry/opentelemetry'; +import { defineIntegration, getClient } from '@sentry/core'; import { generateInstrumentOnce } from '../../otel/instrument'; import type { NodeClient } from '../../sdk/client'; import type { HTTPModuleRequestIncomingMessage } from '../../transports/http-module'; @@ -38,12 +37,16 @@ interface HttpOptions { * Read more about Release Health: https://docs.sentry.io/product/releases/health/ * * Defaults to `true`. - * - * Note: If `autoSessionTracking` is set to `false` in `Sentry.init()` or the Client owning this integration, this option will be ignored. */ - // TODO(v9): Remove the note above. trackIncomingRequestsAsSessions?: boolean; + /** + * Number of milliseconds until sessions tracked with `trackIncomingRequestsAsSessions` will be flushed as a session aggregate. + * + * Defaults to `60000` (60s). + */ + sessionFlushingDelayMS?: number; + /** * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`. * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. @@ -94,13 +97,17 @@ interface HttpOptions { }; } -export const instrumentSentryHttp = generateInstrumentOnce<{ +const instrumentSentryHttp = generateInstrumentOnce<{ breadcrumbs?: HttpOptions['breadcrumbs']; ignoreOutgoingRequests?: HttpOptions['ignoreOutgoingRequests']; + trackIncomingRequestsAsSessions?: HttpOptions['trackIncomingRequestsAsSessions']; + sessionFlushingDelayMS?: HttpOptions['sessionFlushingDelayMS']; }>(`${INTEGRATION_NAME}.sentry`, options => { return new SentryHttpInstrumentation({ breadcrumbs: options?.breadcrumbs, ignoreOutgoingRequests: options?.ignoreOutgoingRequests, + trackIncomingRequestsAsSessions: options?.trackIncomingRequestsAsSessions, + sessionFlushingDelayMS: options?.sessionFlushingDelayMS, }); }); @@ -128,24 +135,6 @@ export function _shouldInstrumentSpans(options: HttpOptions, clientOptions: Part return typeof options.spans === 'boolean' ? options.spans : !clientOptions.skipOpenTelemetrySetup; } -/** - * Instrument the HTTP and HTTPS modules. - */ -const instrumentHttp = (options: HttpOptions = {}): void => { - const instrumentSpans = _shouldInstrumentSpans(options, getClient()?.getOptions()); - - // This is the "regular" OTEL instrumentation that emits spans - if (instrumentSpans) { - const instrumentationConfig = getConfigWithDefaults(options); - instrumentOtelHttp(instrumentationConfig); - } - - // This is the Sentry-specific instrumentation that isolates requests & creates breadcrumbs - // Note that this _has_ to be wrapped after the OTEL instrumentation, - // otherwise the isolation will not work correctly - instrumentSentryHttp(options); -}; - /** * The http integration instruments Node's internal http and https modules. * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. @@ -154,7 +143,18 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) => return { name: INTEGRATION_NAME, setupOnce() { - instrumentHttp(options); + const instrumentSpans = _shouldInstrumentSpans(options, getClient()?.getOptions()); + + // This is the "regular" OTEL instrumentation that emits spans + if (instrumentSpans) { + const instrumentationConfig = getConfigWithDefaults(options); + instrumentOtelHttp(instrumentationConfig); + } + + // This is the Sentry-specific instrumentation that isolates requests & creates breadcrumbs + // Note that this _has_ to be wrapped after the OTEL instrumentation, + // otherwise the isolation will not work correctly + instrumentSentryHttp(options); }, }; }); @@ -227,19 +227,6 @@ function getConfigWithDefaults(options: Partial = {}): HttpInstrume options.instrumentation?.requestHook?.(span, req); }, responseHook: (span, res) => { - const client = getClient(); - - if ( - client && - // eslint-disable-next-line deprecation/deprecation - client.getOptions().autoSessionTracking !== false && - options.trackIncomingRequestsAsSessions !== false - ) { - setImmediate(() => { - client['_captureRequestSession'](); - }); - } - options.instrumentation?.responseHook?.(span, res); }, applyCustomAttributesOnSpan: ( diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts index edd868ac7935..2c2cc5d31789 100644 --- a/packages/node/src/integrations/tracing/express.ts +++ b/packages/node/src/integrations/tracing/express.ts @@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, captureException, defineIntegration, - getClient, getDefaultIsolationScope, getIsolationScope, logger, @@ -13,7 +12,6 @@ import { } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import { generateInstrumentOnce } from '../../otel/instrument'; -import type { NodeClient } from '../../sdk/client'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; @@ -121,31 +119,8 @@ export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressMid const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError; if (shouldHandleError(error)) { - const client = getClient(); - // eslint-disable-next-line deprecation/deprecation - if (client && client.getOptions().autoSessionTracking) { - // Check if the `SessionFlusher` is instantiated on the client to go into this branch that marks the - // `requestSession.status` as `Crashed`, and this check is necessary because the `SessionFlusher` is only - // instantiated when the the`requestHandler` middleware is initialised, which indicates that we should be - // running in SessionAggregates mode - const isSessionAggregatesMode = client['_sessionFlusher'] !== undefined; - if (isSessionAggregatesMode) { - // eslint-disable-next-line deprecation/deprecation - const requestSession = getIsolationScope().getRequestSession(); - // If an error bubbles to the `errorHandler`, then this is an unhandled error, and should be reported as a - // Crashed session. The `_requestSession.status` is checked to ensure that this error is happening within - // the bounds of a request, and if so the status is updated - if (requestSession && requestSession.status !== undefined) { - requestSession.status = 'crashed'; - } - } - } - const eventId = captureException(error, { mechanism: { type: 'middleware', handled: false } }); (res as { sentry?: string }).sentry = eventId; - next(error); - - return; } next(error); diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 2ce75908168d..291e8704f468 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -4,7 +4,6 @@ import { dropUndefinedKeys, endSession, functionToStringIntegration, - getClient, getCurrentScope, getIntegrationsToSetup, getIsolationScope, @@ -156,11 +155,7 @@ function _init( logger.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`); - // TODO(V9): Remove this code since all of the logic should be in an integration - // eslint-disable-next-line deprecation/deprecation - if (options.autoSessionTracking) { - startSessionTracking(); - } + trackSessionForProcess(); client.startClientReportTracking(); @@ -314,15 +309,11 @@ function updateScopeFromEnvVariables(): void { } /** - * Enable automatic Session Tracking for the node process. + * Start a session for this process. */ -function startSessionTracking(): void { - const client = getClient(); - // eslint-disable-next-line deprecation/deprecation - if (client && client.getOptions().autoSessionTracking) { - client.initSessionFlusher(); - } - +// TODO(v9): This is still extremely funky because it's a session on the scope and therefore weirdly mutable by the user. +// Strawman proposal for v9: Either create a processSessionIntegration() or add functionality to the onunhandledexception/rejection integrations. +function trackSessionForProcess(): void { startSession(); // Emitted in the case of healthy sessions, error of `mechanism.handled: true` and unhandledrejections because diff --git a/packages/node/src/utils/prepareEvent.ts b/packages/node/src/utils/prepareEvent.ts index b15b698550bc..11e3a9aff044 100644 --- a/packages/node/src/utils/prepareEvent.ts +++ b/packages/node/src/utils/prepareEvent.ts @@ -49,7 +49,6 @@ const captureContextKeys: readonly ScopeContextProperty[] = [ 'contexts', 'tags', 'fingerprint', - 'requestSession', 'propagationContext', ] as const; diff --git a/packages/node/test/integrations/express.test.ts b/packages/node/test/integrations/express.test.ts deleted file mode 100644 index 1213e7bb38cf..000000000000 --- a/packages/node/test/integrations/express.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as http from 'http'; -import { getCurrentScope, getIsolationScope, setAsyncContextStrategy, setCurrentClient, withScope } from '@sentry/core'; -import type { Scope } from '@sentry/core'; -import { expressErrorHandler } from '../../src/integrations/tracing/express'; -import { NodeClient } from '../../src/sdk/client'; -import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions'; - -describe('expressErrorHandler()', () => { - beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - - setAsyncContextStrategy(undefined); - }); - - const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; - const method = 'wagging'; - const protocol = 'mutualsniffing'; - const hostname = 'the.dog.park'; - const path = '/by/the/trees/'; - const queryString = 'chase=me&please=thankyou'; - - const sentryErrorMiddleware = expressErrorHandler(); - - let req: http.IncomingMessage, res: http.ServerResponse, next: () => undefined; - let client: NodeClient; - - function createNoOpSpy() { - const noop = { noop: () => undefined }; // this is wrapped in an object so jest can spy on it - return jest.spyOn(noop, 'noop') as any; - } - - beforeEach(() => { - req = { - headers, - method, - protocol, - hostname, - originalUrl: `${path}?${queryString}`, - } as unknown as http.IncomingMessage; - res = new http.ServerResponse(req); - next = createNoOpSpy(); - }); - - afterEach(() => { - if (client['_sessionFlusher']) { - clearInterval(client['_sessionFlusher']['_intervalId']); - } - jest.restoreAllMocks(); - }); - it('when autoSessionTracking is disabled, does not set requestSession status on Crash', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '3.3' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - setCurrentClient(client); - - jest.spyOn(client, '_captureRequestSession'); - - // eslint-disable-next-line deprecation/deprecation - getIsolationScope().setRequestSession({ status: 'ok' }); - - let isolationScope: Scope; - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - setImmediate(() => { - // eslint-disable-next-line deprecation/deprecation - expect(isolationScope.getRequestSession()).toEqual({ status: 'ok' }); - done(); - }); - }); - - it('autoSessionTracking is enabled + requestHandler is not used -> does not set requestSession status on Crash', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '3.3' }); - client = new NodeClient(options); - setCurrentClient(client); - - jest.spyOn(client, '_captureRequestSession'); - - // eslint-disable-next-line deprecation/deprecation - getIsolationScope().setRequestSession({ status: 'ok' }); - - let isolationScope: Scope; - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - setImmediate(() => { - // eslint-disable-next-line deprecation/deprecation - expect(isolationScope.getRequestSession()).toEqual({ status: 'ok' }); - done(); - }); - }); - - it('when autoSessionTracking is enabled, should set requestSession status to Crashed when an unhandled error occurs within the bounds of a request', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.1' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - setCurrentClient(client); - - jest.spyOn(client, '_captureRequestSession'); - - withScope(() => { - // eslint-disable-next-line deprecation/deprecation - getIsolationScope().setRequestSession({ status: 'ok' }); - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { - // eslint-disable-next-line deprecation/deprecation - expect(getIsolationScope().getRequestSession()).toEqual({ status: 'crashed' }); - }); - }); - }); - - it('when autoSessionTracking is enabled, should not set requestSession status on Crash when it occurs outside the bounds of a request', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '2.2' }); - client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - setCurrentClient(client); - - jest.spyOn(client, '_captureRequestSession'); - - let isolationScope: Scope; - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { - isolationScope = getIsolationScope(); - return next(); - }); - - setImmediate(() => { - // eslint-disable-next-line deprecation/deprecation - expect(isolationScope.getRequestSession()).toEqual(undefined); - done(); - }); - }); -}); diff --git a/packages/node/test/integrations/request-session-tracking.test.ts b/packages/node/test/integrations/request-session-tracking.test.ts new file mode 100644 index 000000000000..27f9ffd336a0 --- /dev/null +++ b/packages/node/test/integrations/request-session-tracking.test.ts @@ -0,0 +1,152 @@ +import { EventEmitter } from 'stream'; +import { Scope, ServerRuntimeClient, createTransport, withScope } from '@sentry/core'; +import type { Client } from '@sentry/core'; +import { recordRequestSession } from '../../src/integrations/http/SentryHttpInstrumentation'; + +jest.useFakeTimers(); + +describe('recordRequestSession()', () => { + it('should send an "exited" session for an ok ended request', () => { + const client = createTestClient(); + const sendSessionSpy = jest.spyOn(client, 'sendSession'); + + jest.setSystemTime(new Date('March 19, 1999 06:12:34')); + + simulateRequest(client, 'ok'); + + jest.runAllTimers(); + + expect(sendSessionSpy).toBeCalledWith({ + aggregates: [{ crashed: 0, errored: 0, exited: 1, started: '1999-03-19T06:12:00.000Z' }], + }); + }); + + it('should send an "crashed" session when the session on the requestProcessingMetadata was overridden with crashed', () => { + const client = createTestClient(); + const sendSessionSpy = jest.spyOn(client, 'sendSession'); + + jest.setSystemTime(new Date('March 19, 1999 06:12:34')); + + simulateRequest(client, 'crashed'); + + jest.runAllTimers(); + + expect(sendSessionSpy).toBeCalledWith({ + aggregates: [{ crashed: 1, errored: 0, exited: 0, started: expect.stringMatching(/....-..-..T..:..:00.000Z/) }], + }); + }); + + it('should send an "errored" session when the session on the requestProcessingMetadata was overridden with errored', () => { + const client = createTestClient(); + const sendSessionSpy = jest.spyOn(client, 'sendSession'); + + jest.setSystemTime(new Date('March 19, 1999 06:12:34')); + + simulateRequest(client, 'errored'); + + jest.runAllTimers(); + + expect(sendSessionSpy).toBeCalledWith({ + aggregates: [{ crashed: 0, errored: 1, exited: 0, started: expect.stringMatching(/....-..-..T..:..:00.000Z/) }], + }); + }); + + it('should aggregate request sessions within a time frame', async () => { + const client = createTestClient(); + + const sendSessionSpy = jest.spyOn(client, 'sendSession'); + + jest.setSystemTime(new Date('March 19, 1999 06:00:00')); + + simulateRequest(client, 'ok'); + simulateRequest(client, 'ok'); + simulateRequest(client, 'crashed'); + simulateRequest(client, 'errored'); + + // "Wait" 1+ second to get into new bucket + jest.setSystemTime(new Date('March 19, 1999 06:01:01')); + + simulateRequest(client, 'ok'); + simulateRequest(client, 'errored'); + + jest.runAllTimers(); + + expect(sendSessionSpy).toBeCalledWith({ + aggregates: [ + { + crashed: 1, + errored: 1, + exited: 2, + started: '1999-03-19T06:00:00.000Z', + }, + { crashed: 0, errored: 1, exited: 1, started: '1999-03-19T06:01:00.000Z' }, + ], + }); + }); + + it('should flush pending sessions when the client emits a "flush" hook', async () => { + const client = createTestClient(); + + const sendSessionSpy = jest.spyOn(client, 'sendSession'); + + jest.setSystemTime(new Date('March 19, 1999 06:00:00')); + + simulateRequest(client, 'ok'); + + // "Wait" 1+ second to get into new bucket + jest.setSystemTime(new Date('March 19, 1999 06:01:01')); + + simulateRequest(client, 'ok'); + + client.emit('flush'); + + expect(sendSessionSpy).toBeCalledWith({ + aggregates: [ + { + crashed: 0, + errored: 0, + exited: 1, + started: '1999-03-19T06:00:00.000Z', + }, + { + crashed: 0, + errored: 0, + exited: 1, + started: '1999-03-19T06:01:00.000Z', + }, + ], + }); + }); +}); + +function simulateRequest(client: Client, status: 'ok' | 'errored' | 'crashed') { + const requestIsolationScope = new Scope(); + const response = new EventEmitter(); + + recordRequestSession({ + requestIsolationScope, + response, + }); + + requestIsolationScope.getScopeData().sdkProcessingMetadata.requestSession!.status = status; + + withScope(scope => { + scope.setClient(client); + // "end" request + response.emit('close'); + }); +} + +function createTestClient() { + return new ServerRuntimeClient({ + integrations: [], + transport: () => + createTransport( + { + recordDroppedEvent: () => undefined, + }, + () => Promise.resolve({}), + ), + stackParser: () => [], + }); +} diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index 5add93731c5e..8b3842a95661 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -1,20 +1,11 @@ import * as os from 'os'; import { ProxyTracer } from '@opentelemetry/api'; import * as opentelemetryInstrumentationPackage from '@opentelemetry/instrumentation'; -import { - SDK_VERSION, - SessionFlusher, - getCurrentScope, - getGlobalScope, - getIsolationScope, - setCurrentClient, - withIsolationScope, -} from '@sentry/core'; import type { Event, EventHint } from '@sentry/core'; -import type { Scope } from '@sentry/core'; +import { SDK_VERSION, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; -import { NodeClient, initOpenTelemetry } from '../../src'; +import { NodeClient } from '../../src'; import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions'; import { cleanupOtel } from '../helpers/mockSdkInit'; @@ -73,251 +64,6 @@ describe('NodeClient', () => { expect(tracer2).toBe(tracer); }); - describe('captureException', () => { - test('when autoSessionTracking is enabled, and requestHandler is not used -> requestStatus should not be set', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.4' }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - initOpenTelemetry(client); - - withIsolationScope(isolationScope => { - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureException(new Error('test exception')); - - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - - test('when autoSessionTracking is disabled -> requestStatus should not be set', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.4' }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - initOpenTelemetry(client); - - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureException(new Error('test exception')); - - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - - test('when autoSessionTracking is enabled + requestSession status is Crashed -> requestStatus should not be overridden', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.4' }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - initOpenTelemetry(client); - - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession({ status: 'crashed' }); - - client.captureException(new Error('test exception')); - - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('crashed'); - }); - }); - - test('when autoSessionTracking is enabled + error occurs within request bounds -> requestStatus should be set to Errored', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.4' }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - initOpenTelemetry(client); - - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureException(new Error('test exception')); - - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('errored'); - }); - }); - - test('when autoSessionTracking is enabled + error occurs outside of request bounds -> requestStatus should not be set to Errored', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.4' }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - initOpenTelemetry(client); - - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - let isolationScope: Scope; - withIsolationScope(_isolationScope => { - // eslint-disable-next-line deprecation/deprecation - _isolationScope.setRequestSession({ status: 'ok' }); - isolationScope = _isolationScope; - }); - - client.captureException(new Error('test exception')); - - setImmediate(() => { - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - expect(requestSession).toEqual({ status: 'ok' }); - done(); - }); - }); - }); - - describe('captureEvent()', () => { - test('If autoSessionTracking is disabled, requestSession status should not be set', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.4' }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - initOpenTelemetry(client); - - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession({ status: 'ok' }); - client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - - test('When captureEvent is called with an exception, requestSession status should be set to Errored', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '2.2' }); - const client = new NodeClient(options); - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); - - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('errored'); - }); - }); - - test('When captureEvent is called without an exception, requestSession status should not be set to Errored', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '2.2' }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - initOpenTelemetry(client); - - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureEvent({ message: 'message' }); - - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - - test('When captureEvent is called with an exception but outside of a request, then requestStatus should not be set', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '2.2' }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - initOpenTelemetry(client); - - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - isolationScope.clear(); - client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); - - // eslint-disable-next-line deprecation/deprecation - expect(isolationScope.getRequestSession()).toEqual(undefined); - }); - }); - - test('When captureEvent is called with a transaction, then requestSession status should not be set', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.3' }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - initOpenTelemetry(client); - - // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised - // by the`requestHandler`) - client.initSessionFlusher(); - - withIsolationScope(isolationScope => { - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureEvent({ message: 'message', type: 'transaction' }); - - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - - test('When captureEvent is called with an exception but requestHandler is not used, then requestSession status should not be set', () => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.3' }); - const client = new NodeClient(options); - setCurrentClient(client); - client.init(); - initOpenTelemetry(client); - - withIsolationScope(isolationScope => { - // eslint-disable-next-line deprecation/deprecation - isolationScope.setRequestSession({ status: 'ok' }); - - client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); - - // eslint-disable-next-line deprecation/deprecation - const requestSession = isolationScope.getRequestSession(); - expect(requestSession!.status).toEqual('ok'); - }); - }); - }); - describe('_prepareEvent', () => { test('adds platform to event', () => { const options = getDefaultNodeClientOptions({}); @@ -533,28 +279,3 @@ describe('NodeClient', () => { ); }); }); - -describe('flush/close', () => { - test('client close function disables _sessionFlusher', async () => { - jest.useRealTimers(); - - const options = getDefaultNodeClientOptions({ - autoSessionTracking: true, - release: '1.1', - }); - const client = new NodeClient(options); - client.initSessionFlusher(); - // Clearing interval is important here to ensure that the flush function later on is called by the `client.close()` - // not due to the interval running every 60s - clearInterval(client['_sessionFlusher']!['_intervalId']); - - // eslint-disable-next-line deprecation/deprecation - const sessionFlusherFlushFunc = jest.spyOn(SessionFlusher.prototype, 'flush'); - - const delay = 1; - await client.close(delay); - - expect(client['_sessionFlusher']!['_isEnabled']).toBeFalsy(); - expect(sessionFlusherFlushFunc).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index 074de8296cd9..55536a972d77 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -2,7 +2,7 @@ import { logger } from '@sentry/core'; import type { Integration } from '@sentry/core'; import * as SentryOpentelemetry from '@sentry/opentelemetry'; -import { getClient, getIsolationScope } from '../../src/'; +import { getClient } from '../../src/'; import * as auto from '../../src/integrations/tracing'; import { init, validateOpenTelemetrySetup } from '../../src/sdk'; import { NodeClient } from '../../src/sdk/client'; @@ -148,52 +148,6 @@ describe('init()', () => { expect(client).toBeInstanceOf(NodeClient); }); - - describe('autoSessionTracking', () => { - it('does not track session by default if no release is set', () => { - // On CI, we always infer the release, so this does not work - if (process.env.CI) { - return; - } - init({ dsn: PUBLIC_DSN }); - - const session = getIsolationScope().getSession(); - expect(session).toBeUndefined(); - }); - - it('tracks session by default if release is set', () => { - init({ dsn: PUBLIC_DSN, release: '1.2.3' }); - - const session = getIsolationScope().getSession(); - expect(session).toBeDefined(); - }); - - it('does not track session if no release is set even if autoSessionTracking=true', () => { - // On CI, we always infer the release, so this does not work - if (process.env.CI) { - return; - } - - init({ dsn: PUBLIC_DSN, autoSessionTracking: true }); - - const session = getIsolationScope().getSession(); - expect(session).toBeUndefined(); - }); - - it('does not track session if autoSessionTracking=false', () => { - init({ dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.2.3' }); - - const session = getIsolationScope().getSession(); - expect(session).toBeUndefined(); - }); - - it('tracks session by default if autoSessionTracking=true & release is set', () => { - init({ dsn: PUBLIC_DSN, release: '1.2.3', autoSessionTracking: true }); - - const session = getIsolationScope().getSession(); - expect(session).toBeDefined(); - }); - }); }); describe('validateOpenTelemetrySetup', () => { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 5dc7a916c9d7..1470e508bbba 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -109,8 +109,6 @@ import type { ReplayRecordingMode as ReplayRecordingMode_imported, Request as Request_imported, RequestEventData as RequestEventData_imported, - RequestSession as RequestSession_imported, - RequestSessionStatus as RequestSessionStatus_imported, Runtime as Runtime_imported, SamplingContext as SamplingContext_imported, SanitizedRequestData as SanitizedRequestData_imported, @@ -131,7 +129,6 @@ import type { SessionAggregates as SessionAggregates_imported, SessionContext as SessionContext_imported, SessionEnvelope as SessionEnvelope_imported, - SessionFlusherLike as SessionFlusherLike_imported, SessionItem as SessionItem_imported, SessionStatus as SessionStatus_imported, SeverityLevel as SeverityLevel_imported, @@ -411,15 +408,6 @@ export type SessionContext = SessionContext_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type SessionStatus = SessionStatus_imported; /** @deprecated This type has been moved to `@sentry/core`. */ -// eslint-disable-next-line deprecation/deprecation -export type RequestSession = RequestSession_imported; -/** @deprecated This type has been moved to `@sentry/core`. */ -// eslint-disable-next-line deprecation/deprecation -export type RequestSessionStatus = RequestSessionStatus_imported; -/** @deprecated This type has been moved to `@sentry/core`. */ -// eslint-disable-next-line deprecation/deprecation -export type SessionFlusherLike = SessionFlusherLike_imported; -/** @deprecated This type has been moved to `@sentry/core`. */ export type SerializedSession = SerializedSession_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type SeverityLevel = SeverityLevel_imported;