diff --git a/docs/migration/v8-to-v9.md b/docs/migration/v8-to-v9.md index 0ee920fed2c9..ded2b0a89a8f 100644 --- a/docs/migration/v8-to-v9.md +++ b/docs/migration/v8-to-v9.md @@ -78,6 +78,8 @@ Sentry.init({ In v9, an `undefined` value will be treated the same as if the value is not defined at all. You'll need to set `tracesSampleRate: 0` if you want to enable tracing without performance. +- The `getCurrentHub().getIntegration(IntegrationClass)` method will always return `null` in v9. This has already stopped working mostly in v8, because we stopped exposing integration classes. In v9, the fallback behavior has been removed. Note that this does not change the type signature and is thus not technically breaking, but still worth pointing out. + ### `@sentry/node` - When `skipOpenTelemetrySetup: true` is configured, `httpIntegration({ spans: false })` will be configured by default. This means that you no longer have to specify this yourself in this scenario. With this change, no spans are emitted once `skipOpenTelemetrySetup: true` is configured, without any further configuration being needed. @@ -208,6 +210,7 @@ This led to some duplication, where we had to keep an interface in `@sentry/type Since v9, the types have been merged into `@sentry/core`, which removed some of this duplication. This means that certain things that used to be a separate interface, will not expect an actual instance of the class/concrete implementation. This should not affect most users, unless you relied on passing things with a similar shape to internal methods. The following types are affected: - `Scope` now always expects the `Scope` class +- The `IntegrationClass` type is no longer exported - it was not used anymore. Instead, use `Integration` or `IntegrationFn`. # No Version Support Timeline diff --git a/packages/angular/.eslintrc.cjs b/packages/angular/.eslintrc.cjs index 5a263ad7adbb..f7b591f35685 100644 --- a/packages/angular/.eslintrc.cjs +++ b/packages/angular/.eslintrc.cjs @@ -4,4 +4,8 @@ module.exports = { }, extends: ['../../.eslintrc.js'], ignorePatterns: ['setup-test.ts', 'patch-vitest.ts'], + rules: { + // Angular transpiles this correctly/relies on this + '@sentry-internal/sdk/no-class-field-initializers': 'off', + }, }; diff --git a/packages/core/src/getCurrentHubShim.ts b/packages/core/src/getCurrentHubShim.ts index ceea470a727c..d0f9ab4133f4 100644 --- a/packages/core/src/getCurrentHubShim.ts +++ b/packages/core/src/getCurrentHubShim.ts @@ -11,7 +11,7 @@ import { setUser, startSession, } from './exports'; -import type { Client, EventHint, Hub, Integration, IntegrationClass, SeverityLevel } from './types-hoist'; +import type { Client, EventHint, Hub, Integration, SeverityLevel } from './types-hoist'; /** * This is for legacy reasons, and returns a proxy object instead of a hub to be used. @@ -48,9 +48,8 @@ export function getCurrentHubShim(): Hub { setExtras, setContext, - getIntegration(integration: IntegrationClass): T | null { - const client = getClient(); - return (client && client.getIntegrationByName(integration.id)) || null; + getIntegration(_integration: unknown): T | null { + return null; }, startSession, diff --git a/packages/core/src/types-hoist/hub.ts b/packages/core/src/types-hoist/hub.ts index 0e08a487fc0b..4f2bef6c5e21 100644 --- a/packages/core/src/types-hoist/hub.ts +++ b/packages/core/src/types-hoist/hub.ts @@ -3,7 +3,7 @@ import type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; import type { Client } from './client'; import type { Event, EventHint } from './event'; import type { Extra, Extras } from './extra'; -import type { Integration, IntegrationClass } from './integration'; +import type { Integration } from './integration'; import type { Primitive } from './misc'; import type { Session } from './session'; import type { SeverityLevel } from './severity'; @@ -171,9 +171,9 @@ export interface Hub { /** * Returns the integration if installed on the current client. * - * @deprecated Use `Sentry.getClient().getIntegration()` instead. + * @deprecated Use `Sentry.getClient().getIntegrationByName()` instead. */ - getIntegration(integration: IntegrationClass): T | null; + getIntegration(integration: unknown): T | null; /** * Starts a new `Session`, sets on the current scope and returns it. diff --git a/packages/core/src/types-hoist/index.ts b/packages/core/src/types-hoist/index.ts index d5973a246d81..08bec6934640 100644 --- a/packages/core/src/types-hoist/index.ts +++ b/packages/core/src/types-hoist/index.ts @@ -58,7 +58,7 @@ export type { Exception } from './exception'; export type { Extra, Extras } from './extra'; // eslint-disable-next-line deprecation/deprecation export type { Hub } from './hub'; -export type { Integration, IntegrationClass, IntegrationFn } from './integration'; +export type { Integration, IntegrationFn } from './integration'; export type { Mechanism } from './mechanism'; export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './misc'; export type { ClientOptions, Options } from './options'; diff --git a/packages/core/src/types-hoist/integration.ts b/packages/core/src/types-hoist/integration.ts index deb23baaca51..4563e2f1ba69 100644 --- a/packages/core/src/types-hoist/integration.ts +++ b/packages/core/src/types-hoist/integration.ts @@ -1,16 +1,6 @@ import type { Client } from './client'; import type { Event, EventHint } from './event'; -/** Integration Class Interface */ -export interface IntegrationClass { - /** - * Property that holds the integration name - */ - id: string; - - new (...args: any[]): T; -} - /** Integration interface */ export interface Integration { /** diff --git a/packages/core/src/utils-hoist/syncpromise.ts b/packages/core/src/utils-hoist/syncpromise.ts index 015b76b39086..95aa45598727 100644 --- a/packages/core/src/utils-hoist/syncpromise.ts +++ b/packages/core/src/utils-hoist/syncpromise.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { isThenable } from './is'; @@ -40,29 +39,25 @@ export function rejectedSyncPromise(reason?: any): PromiseLike { }); } +type Executor = (resolve: (value?: T | PromiseLike | null) => void, reject: (reason?: any) => void) => void; + /** * Thenable class that behaves like a Promise and follows it's interface * but is not async internally */ -class SyncPromise implements PromiseLike { +export class SyncPromise implements PromiseLike { private _state: States; private _handlers: Array<[boolean, (value: T) => void, (reason: any) => any]>; private _value: any; - public constructor( - executor: (resolve: (value?: T | PromiseLike | null) => void, reject: (reason?: any) => void) => void, - ) { + public constructor(executor: Executor) { this._state = States.PENDING; this._handlers = []; - try { - executor(this._resolve, this._reject); - } catch (e) { - this._reject(e); - } + this._runExecutor(executor); } - /** JSDoc */ + /** @inheritdoc */ public then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null, @@ -99,14 +94,14 @@ class SyncPromise implements PromiseLike { }); } - /** JSDoc */ + /** @inheritdoc */ public catch( onrejected?: ((reason: any) => TResult | PromiseLike) | null, ): PromiseLike { return this.then(val => val, onrejected); } - /** JSDoc */ + /** @inheritdoc */ public finally(onfinally?: (() => void) | null): PromiseLike { return new SyncPromise((resolve, reject) => { let val: TResult | any; @@ -138,35 +133,8 @@ class SyncPromise implements PromiseLike { }); } - /** JSDoc */ - private readonly _resolve = (value?: T | PromiseLike | null) => { - this._setResult(States.RESOLVED, value); - }; - - /** JSDoc */ - private readonly _reject = (reason?: any) => { - this._setResult(States.REJECTED, reason); - }; - - /** JSDoc */ - private readonly _setResult = (state: States, value?: T | PromiseLike | any) => { - if (this._state !== States.PENDING) { - return; - } - - if (isThenable(value)) { - void (value as PromiseLike).then(this._resolve, this._reject); - return; - } - - this._state = state; - this._value = value; - - this._executeHandlers(); - }; - - /** JSDoc */ - private readonly _executeHandlers = () => { + /** Excute the resolve/reject handlers. */ + private _executeHandlers(): void { if (this._state === States.PENDING) { return; } @@ -189,7 +157,38 @@ class SyncPromise implements PromiseLike { handler[0] = true; }); - }; -} + } -export { SyncPromise }; + /** Run the executor for the SyncPromise. */ + private _runExecutor(executor: Executor): void { + const setResult = (state: States, value?: T | PromiseLike | any): void => { + if (this._state !== States.PENDING) { + return; + } + + if (isThenable(value)) { + void (value as PromiseLike).then(resolve, reject); + return; + } + + this._state = state; + this._value = value; + + this._executeHandlers(); + }; + + const resolve = (value: unknown): void => { + setResult(States.RESOLVED, value); + }; + + const reject = (reason: unknown): void => { + setResult(States.REJECTED, reason); + }; + + try { + executor(resolve, reject); + } catch (e) { + reject(e); + } + } +} diff --git a/packages/eslint-plugin-sdk/src/rules/no-class-field-initializers.js b/packages/eslint-plugin-sdk/src/rules/no-class-field-initializers.js index cb7b63edb896..a3edea743bf0 100644 --- a/packages/eslint-plugin-sdk/src/rules/no-class-field-initializers.js +++ b/packages/eslint-plugin-sdk/src/rules/no-class-field-initializers.js @@ -29,14 +29,7 @@ module.exports = { create(context) { return { 'ClassProperty, PropertyDefinition'(node) { - // We do allow arrow functions being initialized directly - if ( - !node.static && - node.value !== null && - node.value.type !== 'ArrowFunctionExpression' && - node.value.type !== 'FunctionExpression' && - node.value.type !== 'CallExpression' - ) { + if (node.value !== null) { context.report({ node, message: `Avoid class field initializers. Property "${node.key.name}" should be initialized in the constructor.`, diff --git a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts index c9907945d1b5..0e3d077ddbb6 100644 --- a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts @@ -10,6 +10,7 @@ import { getEventSpanOptions } from './helpers'; import type { OnEventTarget } from './types'; const supportedVersions = ['>=2.0.0']; +const COMPONENT = '@nestjs/event-emitter'; /** * Custom instrumentation for nestjs event-emitter @@ -17,11 +18,6 @@ const supportedVersions = ['>=2.0.0']; * This hooks into the `OnEvent` decorator, which is applied on event handlers. */ export class SentryNestEventInstrumentation extends InstrumentationBase { - public static readonly COMPONENT = '@nestjs/event-emitter'; - public static readonly COMMON_ATTRIBUTES = { - component: SentryNestEventInstrumentation.COMPONENT, - }; - public constructor(config: InstrumentationConfig = {}) { super('sentry-nestjs-event', SDK_VERSION, config); } @@ -30,10 +26,7 @@ export class SentryNestEventInstrumentation extends InstrumentationBase { * Initializes the instrumentation by defining the modules to be patched. */ public init(): InstrumentationNodeModuleDefinition { - const moduleDef = new InstrumentationNodeModuleDefinition( - SentryNestEventInstrumentation.COMPONENT, - supportedVersions, - ); + const moduleDef = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions); moduleDef.files.push(this._getOnEventFileInstrumentation(supportedVersions)); return moduleDef; diff --git a/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts index f94d828bc11f..ea7d65176aed 100644 --- a/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts @@ -20,6 +20,7 @@ import { getMiddlewareSpanOptions, getNextProxy, instrumentObservable, isPatched import type { CallHandler, CatchTarget, InjectableTarget, MinimalNestJsExecutionContext, Observable } from './types'; const supportedVersions = ['>=8.0.0 <11']; +const COMPONENT = '@nestjs/common'; /** * Custom instrumentation for nestjs. @@ -29,11 +30,6 @@ const supportedVersions = ['>=8.0.0 <11']; * 2. @Catch decorator, which is applied on exception filters. */ export class SentryNestInstrumentation extends InstrumentationBase { - public static readonly COMPONENT = '@nestjs/common'; - public static readonly COMMON_ATTRIBUTES = { - component: SentryNestInstrumentation.COMPONENT, - }; - public constructor(config: InstrumentationConfig = {}) { super('sentry-nestjs', SDK_VERSION, config); } @@ -42,7 +38,7 @@ export class SentryNestInstrumentation extends InstrumentationBase { * Initializes the instrumentation by defining the modules to be patched. */ public init(): InstrumentationNodeModuleDefinition { - const moduleDef = new InstrumentationNodeModuleDefinition(SentryNestInstrumentation.COMPONENT, supportedVersions); + const moduleDef = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions); moduleDef.files.push( this._getInjectableFileInstrumentation(supportedVersions), diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 91cc0e2cdc17..fbc17f94c378 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -145,14 +145,14 @@ class ErrorBoundary extends React.Component void = () => { + public resetErrorBoundary(): void { const { onReset } = this.props; const { error, componentStack, eventId } = this.state; if (onReset) { onReset(error, componentStack, eventId); } this.setState(INITIAL_STATE); - }; + } public render(): React.ReactNode { const { fallback, children } = this.props; @@ -164,7 +164,7 @@ class ErrorBoundary extends React.Component { */ protected _updateSpan: Span | undefined; - // eslint-disable-next-line @typescript-eslint/member-ordering - public static defaultProps: Partial = { - disabled: false, - includeRender: true, - includeUpdates: true, - }; - public constructor(props: ProfilerProps) { super(props); const { name, disabled = false } = this.props; @@ -141,6 +134,15 @@ class Profiler extends React.Component { } } +// React.Component default props are defined as static property on the class +Object.assign(Profiler, { + defaultProps: { + disabled: false, + includeRender: true, + includeUpdates: true, + }, +}); + /** * withProfiler is a higher order component that wraps a * component in a {@link Profiler} component. It is recommended that diff --git a/packages/replay-internal/src/integration.ts b/packages/replay-internal/src/integration.ts index bd152f9bba48..49383d9da3b7 100644 --- a/packages/replay-internal/src/integration.ts +++ b/packages/replay-internal/src/integration.ts @@ -46,16 +46,8 @@ export const replayIntegration = ((options?: ReplayConfiguration) => { /** * Replay integration - * - * TODO: Rewrite this to be functional integration - * Exported for tests. */ export class Replay implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Replay'; - /** * @inheritDoc */ @@ -114,7 +106,7 @@ export class Replay implements Integration { beforeErrorSampling, onError, }: ReplayConfiguration = {}) { - this.name = Replay.id; + this.name = 'Replay'; const privacyOptions = getPrivacyOptions({ mask, diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts index f3169106d458..b9f13fdff09a 100644 --- a/packages/replay-internal/src/replay.ts +++ b/packages/replay-internal/src/replay.ts @@ -147,6 +147,27 @@ export class ReplayContainer implements ReplayContainerInterface { */ private _canvas: ReplayCanvasIntegrationOptions | undefined; + /** + * Handle when visibility of the page content changes. Opening a new tab will + * cause the state to change to hidden because of content of current page will + * be hidden. Likewise, moving a different window to cover the contents of the + * page will also trigger a change to a hidden state. + */ + private _handleVisibilityChange: () => void; + + /** + * Handle when page is blurred + */ + private _handleWindowBlur: () => void; + + /** + * Handle when page is focused + */ + private _handleWindowFocus: () => void; + + /** Ensure page remains active when a key is pressed. */ + private _handleKeyboardEvent: (event: KeyboardEvent) => void; + public constructor({ options, recordingOptions, @@ -213,6 +234,43 @@ export class ReplayContainer implements ReplayContainerInterface { traceInternals: !!experiments.traceInternals, }); } + + // We set these handler properties as class properties, to make binding/unbinding them easier + this._handleVisibilityChange = () => { + if (WINDOW.document.visibilityState === 'visible') { + this._doChangeToForegroundTasks(); + } else { + this._doChangeToBackgroundTasks(); + } + }; + + /** + * Handle when page is blurred + */ + this._handleWindowBlur = () => { + const breadcrumb = createBreadcrumb({ + category: 'ui.blur', + }); + + // Do not count blur as a user action -- it's part of the process of them + // leaving the page + this._doChangeToBackgroundTasks(breadcrumb); + }; + + this._handleWindowFocus = () => { + const breadcrumb = createBreadcrumb({ + category: 'ui.focus', + }); + + // Do not count focus as a user action -- instead wait until they focus and + // interactive with page + this._doChangeToForegroundTasks(breadcrumb); + }; + + /** Ensure page remains active when a key is pressed. */ + this._handleKeyboardEvent = (event: KeyboardEvent) => { + handleKeyboardEvent(this, event); + }; } /** Get the event context. */ @@ -394,7 +452,7 @@ export class ReplayContainer implements ReplayContainerInterface { checkoutEveryNms: Math.max(360_000, this._options._experiments.continuousCheckout), }), emit: getHandleRecordingEmit(this), - onMutation: this._onMutationHandler, + onMutation: this._onMutationHandler.bind(this), ...(canvasOptions ? { recordCanvas: canvasOptions.recordCanvas, @@ -907,51 +965,6 @@ export class ReplayContainer implements ReplayContainerInterface { } } - /** - * Handle when visibility of the page content changes. Opening a new tab will - * cause the state to change to hidden because of content of current page will - * be hidden. Likewise, moving a different window to cover the contents of the - * page will also trigger a change to a hidden state. - */ - private _handleVisibilityChange: () => void = () => { - if (WINDOW.document.visibilityState === 'visible') { - this._doChangeToForegroundTasks(); - } else { - this._doChangeToBackgroundTasks(); - } - }; - - /** - * Handle when page is blurred - */ - private _handleWindowBlur: () => void = () => { - const breadcrumb = createBreadcrumb({ - category: 'ui.blur', - }); - - // Do not count blur as a user action -- it's part of the process of them - // leaving the page - this._doChangeToBackgroundTasks(breadcrumb); - }; - - /** - * Handle when page is focused - */ - private _handleWindowFocus: () => void = () => { - const breadcrumb = createBreadcrumb({ - category: 'ui.focus', - }); - - // Do not count focus as a user action -- instead wait until they focus and - // interactive with page - this._doChangeToForegroundTasks(breadcrumb); - }; - - /** Ensure page remains active when a key is pressed. */ - private _handleKeyboardEvent: (event: KeyboardEvent) => void = (event: KeyboardEvent) => { - handleKeyboardEvent(this, event); - }; - /** * Tasks to run when we consider a page to be hidden (via blurring and/or visibility) */ @@ -1199,7 +1212,7 @@ export class ReplayContainer implements ReplayContainerInterface { * Flush recording data to Sentry. Creates a lock so that only a single flush * can be active at a time. Do not call this directly. */ - private _flush = async ({ + private async _flush({ force = false, }: { /** @@ -1208,7 +1221,7 @@ export class ReplayContainer implements ReplayContainerInterface { * is stopped). */ force?: boolean; - } = {}): Promise => { + } = {}): Promise { if (!this._isEnabled && !force) { // This can happen if e.g. the replay was stopped because of exceeding the retry limit return; @@ -1279,7 +1292,7 @@ export class ReplayContainer implements ReplayContainerInterface { this._debouncedFlush(); } } - }; + } /** Save the session, if it is sticky */ private _maybeSaveSession(): void { @@ -1289,7 +1302,7 @@ export class ReplayContainer implements ReplayContainerInterface { } /** Handler for rrweb.record.onMutation */ - private _onMutationHandler = (mutations: unknown[]): boolean => { + private _onMutationHandler(mutations: unknown[]): boolean { const count = mutations.length; const mutationLimit = this._options.mutationLimit; @@ -1319,5 +1332,5 @@ export class ReplayContainer implements ReplayContainerInterface { // `true` means we use the regular mutation handling by rrweb return true; - }; + } } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 36906721ad73..346ef1cda36a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -77,7 +77,6 @@ import type { InProgressCheckIn as InProgressCheckIn_imported, InformationUnit as InformationUnit_imported, Integration as Integration_imported, - IntegrationClass as IntegrationClass_imported, IntegrationFn as IntegrationFn_imported, InternalBaseTransportOptions as InternalBaseTransportOptions_imported, MeasurementUnit as MeasurementUnit_imported, @@ -305,8 +304,6 @@ export type Hub = Hub_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type Integration = Integration_imported; /** @deprecated This type has been moved to `@sentry/core`. */ -export type IntegrationClass = IntegrationClass_imported; -/** @deprecated This type has been moved to `@sentry/core`. */ // eslint-disable-next-line deprecation/deprecation export type IntegrationFn = IntegrationFn_imported; /** @deprecated This type has been moved to `@sentry/core`. */