From 9a5c0195f6d08b4533ea58c2715e47bc20b5131b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 2 Jan 2025 15:58:04 +0100 Subject: [PATCH 1/5] feat: Avoid class fields alltogether We already have an eslint rule to avoid class fields, but had exceptions for static fields as well as for arrow functions. This also leads to bundle size increases, so removing the exceptions and handling the (few) exceptions we have there should save some bytes. Additionally, this has additional challenges if we want to avoid/reduce polyfills, as class fields need to be polyfilled for ES2020, sadly. --- docs/migration/v8-to-v9.md | 3 + packages/angular/.eslintrc.cjs | 4 + packages/core/src/getCurrentHubShim.ts | 7 +- packages/core/src/types-hoist/hub.ts | 6 +- packages/core/src/types-hoist/index.ts | 2 +- packages/core/src/types-hoist/integration.ts | 10 -- packages/core/src/utils-hoist/syncpromise.ts | 117 +++++++++--------- .../src/rules/no-class-field-initializers.js | 9 +- .../sentry-nest-event-instrumentation.ts | 17 +-- .../sentry-nest-instrumentation.ts | 15 ++- packages/react/src/errorboundary.tsx | 6 +- packages/react/src/profiler.tsx | 15 +-- packages/replay-internal/src/integration.ts | 10 +- packages/replay-internal/src/replay.ts | 114 +++++++++-------- packages/types/src/index.ts | 3 - 15 files changed, 165 insertions(+), 173 deletions(-) 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..189d9f96c0a5 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'; @@ -44,10 +43,14 @@ export function rejectedSyncPromise(reason?: any): PromiseLike { * 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; + private _resolve: (value?: T | PromiseLike | null) => void; + private _reject: (reason?: any) => void; + private _executeHandlers: () => void; + private _setResult: (state: States, value?: T | PromiseLike | any) => void; public constructor( executor: (resolve: (value?: T | PromiseLike | null) => void, reject: (reason?: any) => void) => void, @@ -55,6 +58,55 @@ class SyncPromise implements PromiseLike { this._state = States.PENDING; this._handlers = []; + this._resolve = (value?: T | PromiseLike | null) => { + this._setResult(States.RESOLVED, value); + }; + + this._reject = (reason?: any) => { + this._setResult(States.REJECTED, reason); + }; + + this._executeHandlers = () => { + if (this._state === States.PENDING) { + return; + } + + const cachedHandlers = this._handlers.slice(); + this._handlers = []; + + cachedHandlers.forEach(handler => { + if (handler[0]) { + return; + } + + if (this._state === States.RESOLVED) { + handler[1](this._value as unknown as any); + } + + if (this._state === States.REJECTED) { + handler[2](this._value); + } + + handler[0] = true; + }); + }; + + this._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(); + }; + try { executor(this._resolve, this._reject); } catch (e) { @@ -62,7 +114,7 @@ class SyncPromise implements PromiseLike { } } - /** JSDoc */ + /** @inheritdoc */ public then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null, @@ -99,14 +151,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; @@ -137,59 +189,4 @@ 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 = () => { - if (this._state === States.PENDING) { - return; - } - - const cachedHandlers = this._handlers.slice(); - this._handlers = []; - - cachedHandlers.forEach(handler => { - if (handler[0]) { - return; - } - - if (this._state === States.RESOLVED) { - handler[1](this._value as unknown as any); - } - - if (this._state === States.REJECTED) { - handler[2](this._value); - } - - handler[0] = true; - }); - }; } - -export { SyncPromise }; 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..8369a7890ee4 100644 --- a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts @@ -5,6 +5,7 @@ import { InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, } from '@opentelemetry/instrumentation'; +import type { SpanAttributes } from '@sentry/core'; import { SDK_VERSION, captureException, startSpan } from '@sentry/core'; import { getEventSpanOptions } from './helpers'; import type { OnEventTarget } from './types'; @@ -17,23 +18,23 @@ 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 readonly COMPONENT: string; + public readonly COMMON_ATTRIBUTES: SpanAttributes; public constructor(config: InstrumentationConfig = {}) { super('sentry-nestjs-event', SDK_VERSION, config); + + this.COMPONENT = '@nestjs/event-emitter'; + this.COMMON_ATTRIBUTES = { + component: this.COMPONENT, + }; } /** * Initializes the instrumentation by defining the modules to be patched. */ public init(): InstrumentationNodeModuleDefinition { - const moduleDef = new InstrumentationNodeModuleDefinition( - SentryNestEventInstrumentation.COMPONENT, - supportedVersions, - ); + const moduleDef = new InstrumentationNodeModuleDefinition(this.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..dd1162585622 100644 --- a/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts @@ -5,7 +5,7 @@ import { InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, } from '@opentelemetry/instrumentation'; -import type { Span } from '@sentry/core'; +import type { Span, SpanAttributes } from '@sentry/core'; import { SDK_VERSION, addNonEnumerableProperty, @@ -29,20 +29,23 @@ 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 readonly COMPONENT: string; + public readonly COMMON_ATTRIBUTES: SpanAttributes; public constructor(config: InstrumentationConfig = {}) { super('sentry-nestjs', SDK_VERSION, config); + + this.COMPONENT = '@nestjs/common'; + this.COMMON_ATTRIBUTES = { + component: this.COMPONENT, + }; } /** * Initializes the instrumentation by defining the modules to be patched. */ public init(): InstrumentationNodeModuleDefinition { - const moduleDef = new InstrumentationNodeModuleDefinition(SentryNestInstrumentation.COMPONENT, supportedVersions); + const moduleDef = new InstrumentationNodeModuleDefinition(this.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,14 @@ class Profiler extends React.Component { } } +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..d4fc6970a81b 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,42 @@ export class ReplayContainer implements ReplayContainerInterface { traceInternals: !!experiments.traceInternals, }); } + + 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 +451,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 +964,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 +1211,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 +1220,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 +1291,7 @@ export class ReplayContainer implements ReplayContainerInterface { this._debouncedFlush(); } } - }; + } /** Save the session, if it is sticky */ private _maybeSaveSession(): void { @@ -1289,7 +1301,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 +1331,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`. */ From 39502aaead17e102cbdfab23a645f351ac2e2ccf Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 2 Jan 2025 16:27:52 +0100 Subject: [PATCH 2/5] add comments --- packages/core/src/utils-hoist/syncpromise.ts | 2 ++ packages/replay-internal/src/replay.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/core/src/utils-hoist/syncpromise.ts b/packages/core/src/utils-hoist/syncpromise.ts index 189d9f96c0a5..92b17c0c05b8 100644 --- a/packages/core/src/utils-hoist/syncpromise.ts +++ b/packages/core/src/utils-hoist/syncpromise.ts @@ -58,6 +58,8 @@ export class SyncPromise implements PromiseLike { this._state = States.PENDING; this._handlers = []; + // We set this functions here as class properties, to make binding them easier + // This way, the `this` context is easier to maintain this._resolve = (value?: T | PromiseLike | null) => { this._setResult(States.RESOLVED, value); }; diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts index d4fc6970a81b..b9f13fdff09a 100644 --- a/packages/replay-internal/src/replay.ts +++ b/packages/replay-internal/src/replay.ts @@ -235,6 +235,7 @@ export class ReplayContainer implements ReplayContainerInterface { }); } + // We set these handler properties as class properties, to make binding/unbinding them easier this._handleVisibilityChange = () => { if (WINDOW.document.visibilityState === 'visible') { this._doChangeToForegroundTasks(); From 48f4f0926ecd4d10c4fd730ca472410ce2052ef2 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 2 Jan 2025 16:36:35 +0100 Subject: [PATCH 3/5] fix nestjs --- .../sentry-nest-event-instrumentation.ts | 12 ++---------- .../src/integrations/sentry-nest-instrumentation.ts | 13 +++---------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts index 8369a7890ee4..0e3d077ddbb6 100644 --- a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts @@ -5,12 +5,12 @@ import { InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, } from '@opentelemetry/instrumentation'; -import type { SpanAttributes } from '@sentry/core'; import { SDK_VERSION, captureException, startSpan } from '@sentry/core'; 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 @@ -18,23 +18,15 @@ const supportedVersions = ['>=2.0.0']; * This hooks into the `OnEvent` decorator, which is applied on event handlers. */ export class SentryNestEventInstrumentation extends InstrumentationBase { - public readonly COMPONENT: string; - public readonly COMMON_ATTRIBUTES: SpanAttributes; - public constructor(config: InstrumentationConfig = {}) { super('sentry-nestjs-event', SDK_VERSION, config); - - this.COMPONENT = '@nestjs/event-emitter'; - this.COMMON_ATTRIBUTES = { - component: this.COMPONENT, - }; } /** * Initializes the instrumentation by defining the modules to be patched. */ public init(): InstrumentationNodeModuleDefinition { - const moduleDef = new InstrumentationNodeModuleDefinition(this.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 dd1162585622..ea7d65176aed 100644 --- a/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts @@ -5,7 +5,7 @@ import { InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, } from '@opentelemetry/instrumentation'; -import type { Span, SpanAttributes } from '@sentry/core'; +import type { Span } from '@sentry/core'; import { SDK_VERSION, addNonEnumerableProperty, @@ -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,23 +30,15 @@ const supportedVersions = ['>=8.0.0 <11']; * 2. @Catch decorator, which is applied on exception filters. */ export class SentryNestInstrumentation extends InstrumentationBase { - public readonly COMPONENT: string; - public readonly COMMON_ATTRIBUTES: SpanAttributes; - public constructor(config: InstrumentationConfig = {}) { super('sentry-nestjs', SDK_VERSION, config); - - this.COMPONENT = '@nestjs/common'; - this.COMMON_ATTRIBUTES = { - component: this.COMPONENT, - }; } /** * Initializes the instrumentation by defining the modules to be patched. */ public init(): InstrumentationNodeModuleDefinition { - const moduleDef = new InstrumentationNodeModuleDefinition(this.COMPONENT, supportedVersions); + const moduleDef = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions); moduleDef.files.push( this._getInjectableFileInstrumentation(supportedVersions), From b646f8f36daada16d51d28bf308f072e8dcbda97 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 2 Jan 2025 16:51:06 +0100 Subject: [PATCH 4/5] better sync promise? --- packages/core/src/utils-hoist/syncpromise.ts | 128 +++++++++---------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/packages/core/src/utils-hoist/syncpromise.ts b/packages/core/src/utils-hoist/syncpromise.ts index 92b17c0c05b8..95aa45598727 100644 --- a/packages/core/src/utils-hoist/syncpromise.ts +++ b/packages/core/src/utils-hoist/syncpromise.ts @@ -39,6 +39,8 @@ 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 @@ -47,73 +49,12 @@ export class SyncPromise implements PromiseLike { private _state: States; private _handlers: Array<[boolean, (value: T) => void, (reason: any) => any]>; private _value: any; - private _resolve: (value?: T | PromiseLike | null) => void; - private _reject: (reason?: any) => void; - private _executeHandlers: () => void; - private _setResult: (state: States, value?: T | PromiseLike | any) => void; - - public constructor( - executor: (resolve: (value?: T | PromiseLike | null) => void, reject: (reason?: any) => void) => void, - ) { + + public constructor(executor: Executor) { this._state = States.PENDING; this._handlers = []; - // We set this functions here as class properties, to make binding them easier - // This way, the `this` context is easier to maintain - this._resolve = (value?: T | PromiseLike | null) => { - this._setResult(States.RESOLVED, value); - }; - - this._reject = (reason?: any) => { - this._setResult(States.REJECTED, reason); - }; - - this._executeHandlers = () => { - if (this._state === States.PENDING) { - return; - } - - const cachedHandlers = this._handlers.slice(); - this._handlers = []; - - cachedHandlers.forEach(handler => { - if (handler[0]) { - return; - } - - if (this._state === States.RESOLVED) { - handler[1](this._value as unknown as any); - } - - if (this._state === States.REJECTED) { - handler[2](this._value); - } - - handler[0] = true; - }); - }; - - this._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(); - }; - - try { - executor(this._resolve, this._reject); - } catch (e) { - this._reject(e); - } + this._runExecutor(executor); } /** @inheritdoc */ @@ -191,4 +132,63 @@ export class SyncPromise implements PromiseLike { }); }); } + + /** Excute the resolve/reject handlers. */ + private _executeHandlers(): void { + if (this._state === States.PENDING) { + return; + } + + const cachedHandlers = this._handlers.slice(); + this._handlers = []; + + cachedHandlers.forEach(handler => { + if (handler[0]) { + return; + } + + if (this._state === States.RESOLVED) { + handler[1](this._value as unknown as any); + } + + if (this._state === States.REJECTED) { + handler[2](this._value); + } + + handler[0] = true; + }); + } + + /** 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); + } + } } From b2d4ca2cd97992f92b747ebcf5b8dd11038b4edc Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 3 Jan 2025 08:36:59 +0100 Subject: [PATCH 5/5] add comment --- packages/react/src/profiler.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 1a77f578c71b..d7e70d651af4 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -134,6 +134,7 @@ class Profiler extends React.Component { } } +// React.Component default props are defined as static property on the class Object.assign(Profiler, { defaultProps: { disabled: false,