diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 7f3d5a6cf315..67d7055a1623 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -11,12 +11,13 @@ import type { SeverityLevel, TraceContext, } from '@sentry/types'; -import { eventFromMessage, eventFromUnknownInput, logger, uuid4 } from '@sentry/utils'; +import { eventFromMessage, eventFromUnknownInput, logger, resolvedSyncPromise, uuid4 } from '@sentry/utils'; import { BaseClient } from './baseclient'; import { createCheckInEnvelope } from './checkin'; import { getCurrentHub } from './hub'; import type { Scope } from './scope'; +import { SessionFlusher } from './sessionflusher'; import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing'; export interface ServerRuntimeClientOptions extends ClientOptions { @@ -31,6 +32,8 @@ export interface ServerRuntimeClientOptions extends ClientOptions extends BaseClient { + protected _sessionFlusher: SessionFlusher | undefined; + /** * Creates a new Edge SDK instance. * @param options Configuration options for this SDK. @@ -46,7 +49,7 @@ export class ServerRuntimeClient< * @inheritDoc */ public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { - return Promise.resolve(eventFromUnknownInput(getCurrentHub, this._options.stackParser, exception, hint)); + return resolvedSyncPromise(eventFromUnknownInput(getCurrentHub, this._options.stackParser, exception, hint)); } /** @@ -58,11 +61,83 @@ export class ServerRuntimeClient< level: Severity | SeverityLevel = 'info', hint?: EventHint, ): PromiseLike { - return Promise.resolve( + return resolvedSyncPromise( eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), ); } + /** + * @inheritDoc + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + public captureException(exception: any, hint?: EventHint, scope?: Scope): string | undefined { + // Check if the flag `autoSessionTracking` is enabled, and if `_sessionFlusher` exists because it is initialised only + // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload + // sent to the Server only when the `requestHandler` middleware is used + if (this._options.autoSessionTracking && this._sessionFlusher && scope) { + const requestSession = scope.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'; + } + } + + return super.captureException(exception, hint, scope); + } + + /** + * @inheritDoc + */ + public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string | undefined { + // Check if the flag `autoSessionTracking` is enabled, and if `_sessionFlusher` exists because it is initialised only + // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload + // sent to the Server only when the `requestHandler` middleware is used + if (this._options.autoSessionTracking && this._sessionFlusher && scope) { + 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) { + const requestSession = scope.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'; + } + } + } + + return super.captureEvent(event, hint, scope); + } + + /** + * + * @inheritdoc + */ + public close(timeout?: number): PromiseLike { + if (this._sessionFlusher) { + this._sessionFlusher.close(); + } + return super.close(timeout); + } + + /** Method that initialises an instance of SessionFlusher on Client */ + public initSessionFlusher(): void { + const { release, environment } = this._options; + if (!release) { + __DEBUG_BUILD__ && logger.warn('Cannot initialise an instance of SessionFlusher if no release is provided!'); + } else { + this._sessionFlusher = new SessionFlusher(this, { + release, + environment, + }); + } + } + /** * Create a cron monitor check in and send it to Sentry. * @@ -121,6 +196,18 @@ export class ServerRuntimeClient< return id; } + /** + * Method responsible for capturing/ending a request session by calling `incrementSessionStatusCount` to increment + * appropriate session aggregates bucket + */ + protected _captureRequestSession(): void { + if (!this._sessionFlusher) { + __DEBUG_BUILD__ && logger.warn('Discarded request mode session because autoSessionTracking option was disabled'); + } else { + this._sessionFlusher.incrementSessionStatusCount(); + } + } + /** * @inheritDoc */ diff --git a/packages/node/package.json b/packages/node/package.json index 3665f74b55b3..b2368d02ac39 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -72,5 +72,12 @@ "volta": { "extends": "../../package.json" }, + "madge":{ + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } + }, "sideEffects": false } diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index 50af36448046..8a174754d1f1 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -1,28 +1,8 @@ -import type { Scope } from '@sentry/core'; -import { - addTracingExtensions, - BaseClient, - createCheckInEnvelope, - getDynamicSamplingContextFromClient, - SDK_VERSION, - SessionFlusher, -} from '@sentry/core'; -import type { - CheckIn, - DynamicSamplingContext, - Event, - EventHint, - MonitorConfig, - SerializedCheckIn, - Severity, - SeverityLevel, - TraceContext, -} from '@sentry/types'; -import { logger, resolvedSyncPromise, uuid4 } from '@sentry/utils'; +import type { ServerRuntimeClientOptions } from '@sentry/core'; +import { SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; import * as os from 'os'; import { TextEncoder } from 'util'; -import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; import type { NodeClientOptions } from './types'; /** @@ -31,9 +11,7 @@ import type { NodeClientOptions } from './types'; * @see NodeClientOptions for documentation on configuration options. * @see SentryClient for usage documentation. */ -export class NodeClient extends BaseClient { - protected _sessionFlusher: SessionFlusher | undefined; - +export class NodeClient extends ServerRuntimeClient { /** * Creates a new Node SDK instance. * @param options Configuration options for this SDK. @@ -57,215 +35,13 @@ export class NodeClient extends BaseClient { ...options.transportOptions, }; - // The Node client always supports tracing - addTracingExtensions(); - - super(options); - } - - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - public captureException(exception: any, hint?: EventHint, scope?: Scope): string | undefined { - // Check if the flag `autoSessionTracking` is enabled, and if `_sessionFlusher` exists because it is initialised only - // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload - // sent to the Server only when the `requestHandler` middleware is used - if (this._options.autoSessionTracking && this._sessionFlusher && scope) { - const requestSession = scope.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'; - } - } - - return super.captureException(exception, hint, scope); - } - - /** - * @inheritDoc - */ - public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string | undefined { - // Check if the flag `autoSessionTracking` is enabled, and if `_sessionFlusher` exists because it is initialised only - // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload - // sent to the Server only when the `requestHandler` middleware is used - if (this._options.autoSessionTracking && this._sessionFlusher && scope) { - 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) { - const requestSession = scope.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'; - } - } - } - - return super.captureEvent(event, hint, scope); - } - - /** - * - * @inheritdoc - */ - public close(timeout?: number): PromiseLike { - this._sessionFlusher?.close(); - return super.close(timeout); - } - - /** Method that initialises an instance of SessionFlusher on Client */ - public initSessionFlusher(): void { - const { release, environment } = this._options; - if (!release) { - __DEBUG_BUILD__ && logger.warn('Cannot initialise an instance of SessionFlusher if no release is provided!'); - } else { - this._sessionFlusher = new SessionFlusher(this, { - release, - environment, - }); - } - } - - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - public eventFromException(exception: any, hint?: EventHint): PromiseLike { - return resolvedSyncPromise(eventFromUnknownInput(this._options.stackParser, exception, hint)); - } - - /** - * @inheritDoc - */ - public eventFromMessage( - message: string, - // eslint-disable-next-line deprecation/deprecation - level: Severity | SeverityLevel = 'info', - hint?: EventHint, - ): PromiseLike { - return resolvedSyncPromise( - eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), - ); - } - - /** - * Create a cron monitor check in and send it to Sentry. - * - * @param checkIn An object that describes a check in. - * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want - * to create a monitor automatically when sending a check in. - * @returns A string representing the id of the check in. - */ - public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string { - const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4(); - if (!this._isEnabled()) { - __DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.'); - return id; - } - - const options = this.getOptions(); - const { release, environment, tunnel } = options; - - const serializedCheckIn: SerializedCheckIn = { - check_in_id: id, - monitor_slug: checkIn.monitorSlug, - status: checkIn.status, - release, - environment, - }; - - if (checkIn.status !== 'in_progress') { - serializedCheckIn.duration = checkIn.duration; - } - - if (monitorConfig) { - serializedCheckIn.monitor_config = { - schedule: monitorConfig.schedule, - checkin_margin: monitorConfig.checkinMargin, - max_runtime: monitorConfig.maxRuntime, - timezone: monitorConfig.timezone, - }; - } - - const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope); - if (traceContext) { - serializedCheckIn.contexts = { - trace: traceContext, - }; - } - - const envelope = createCheckInEnvelope( - serializedCheckIn, - dynamicSamplingContext, - this.getSdkMetadata(), - tunnel, - this.getDsn(), - ); - - __DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status); - void this._sendEnvelope(envelope); - return id; - } - - /** - * @inheritDoc - */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { - event.platform = event.platform || 'node'; - event.contexts = { - ...event.contexts, - runtime: event.contexts?.runtime || { - name: 'node', - version: global.process.version, - }, - }; - event.server_name = - event.server_name || this.getOptions().serverName || global.process.env.SENTRY_NAME || os.hostname(); - return super._prepareEvent(event, hint, scope); - } - - /** - * Method responsible for capturing/ending a request session by calling `incrementSessionStatusCount` to increment - * appropriate session aggregates bucket - */ - protected _captureRequestSession(): void { - if (!this._sessionFlusher) { - __DEBUG_BUILD__ && logger.warn('Discarded request mode session because autoSessionTracking option was disabled'); - } else { - this._sessionFlusher.incrementSessionStatusCount(); - } - } - - /** Extract trace information from scope */ - private _getTraceInfoFromScope( - scope: Scope | undefined, - ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { - if (!scope) { - return [undefined, undefined]; - } - - const span = scope.getSpan(); - if (span) { - return [span?.transaction?.getDynamicSamplingContext(), span?.getTraceContext()]; - } - - const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext(); - const traceContext: TraceContext = { - trace_id: traceId, - span_id: spanId, - parent_span_id: parentSpanId, + const clientOptions: ServerRuntimeClientOptions = { + ...options, + platform: 'node', + runtime: { name: 'node', version: global.process.version }, + serverName: options.serverName || global.process.env.SENTRY_NAME || os.hostname(), }; - if (dsc) { - return [dsc, traceContext]; - } - return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext]; + super(clientOptions); } } diff --git a/packages/node/src/eventbuilder.ts b/packages/node/src/eventbuilder.ts deleted file mode 100644 index f2bb1443a40f..000000000000 --- a/packages/node/src/eventbuilder.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { getCurrentHub } from '@sentry/core'; -import type { - Event, - EventHint, - Exception, - Mechanism, - Severity, - SeverityLevel, - StackFrame, - StackParser, -} from '@sentry/types'; -import { - addExceptionMechanism, - addExceptionTypeValue, - extractExceptionKeysForMessage, - isError, - isPlainObject, - normalizeToSize, -} from '@sentry/utils'; - -/** - * Extracts stack frames from the error.stack string - */ -export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] { - return stackParser(error.stack || '', 1); -} - -/** - * Extracts stack frames from the error and builds a Sentry Exception - */ -export function exceptionFromError(stackParser: StackParser, error: Error): Exception { - const exception: Exception = { - type: error.name || error.constructor.name, - value: error.message, - }; - - const frames = parseStackFrames(stackParser, error); - if (frames.length) { - exception.stacktrace = { frames }; - } - - return exception; -} - -/** - * Builds and Event from a Exception - * @hidden - */ -export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let ex: unknown = exception; - const providedMechanism: Mechanism | undefined = - hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; - const mechanism: Mechanism = providedMechanism || { - handled: true, - type: 'generic', - }; - - if (!isError(exception)) { - if (isPlainObject(exception)) { - // This will allow us to group events based on top-level keys - // which is much better than creating new group when any key/value change - const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`; - - const hub = getCurrentHub(); - const client = hub.getClient(); - const normalizeDepth = client && client.getOptions().normalizeDepth; - hub.configureScope(scope => { - scope.setExtra('__serialized__', normalizeToSize(exception, normalizeDepth)); - }); - - ex = (hint && hint.syntheticException) || new Error(message); - (ex as Error).message = message; - } else { - // This handles when someone does: `throw "something awesome";` - // We use synthesized Error here so we can extract a (rough) stack trace. - ex = (hint && hint.syntheticException) || new Error(exception as string); - (ex as Error).message = exception as string; - } - mechanism.synthetic = true; - } - - const event = { - exception: { - values: [exceptionFromError(stackParser, ex as Error)], - }, - }; - - addExceptionTypeValue(event, undefined, undefined); - addExceptionMechanism(event, mechanism); - - return { - ...event, - event_id: hint && hint.event_id, - }; -} - -/** - * Builds and Event from a Message - * @hidden - */ -export function eventFromMessage( - stackParser: StackParser, - message: string, - // eslint-disable-next-line deprecation/deprecation - level: Severity | SeverityLevel = 'info', - hint?: EventHint, - attachStacktrace?: boolean, -): Event { - const event: Event = { - event_id: hint && hint.event_id, - level, - message, - }; - - if (attachStacktrace && hint && hint.syntheticException) { - const frames = parseStackFrames(stackParser, hint.syntheticException); - if (frames.length) { - event.exception = { - values: [ - { - value: message, - stacktrace: { frames }, - }, - ], - }; - } - } - - return event; -} diff --git a/packages/node/src/integrations/linkederrors.ts b/packages/node/src/integrations/linkederrors.ts index 78062f708d6b..de2a6e0cc1e8 100644 --- a/packages/node/src/integrations/linkederrors.ts +++ b/packages/node/src/integrations/linkederrors.ts @@ -1,7 +1,5 @@ import type { Client, Event, EventHint, Integration } from '@sentry/types'; -import { applyAggregateErrorsToEvent } from '@sentry/utils'; - -import { exceptionFromError } from '../eventbuilder'; +import { applyAggregateErrorsToEvent, exceptionFromError } from '@sentry/utils'; const DEFAULT_KEY = 'cause'; const DEFAULT_LIMIT = 5; diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts index 0ddf69105de7..ab2e69c7b33b 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -233,8 +233,8 @@ describe('NodeClient', () => { test('adds server name to event when value given in env', () => { const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN }); - client = new NodeClient(options); process.env.SENTRY_NAME = 'foo'; + client = new NodeClient(options); const event: Event = {}; const hint: EventHint = {}; diff --git a/packages/node/test/context-lines.test.ts b/packages/node/test/context-lines.test.ts index cfdd44e8b840..25b17e29ba77 100644 --- a/packages/node/test/context-lines.test.ts +++ b/packages/node/test/context-lines.test.ts @@ -1,7 +1,7 @@ import type { StackFrame } from '@sentry/types'; +import { parseStackFrames } from '@sentry/utils'; import * as fs from 'fs'; -import { parseStackFrames } from '../src/eventbuilder'; import { ContextLines, resetFileContentCache } from '../src/integrations/contextlines'; import { defaultStackParser } from '../src/sdk'; import { getError } from './helper/error'; diff --git a/packages/node/test/eventbuilders.test.ts b/packages/node/test/eventbuilders.test.ts index 46dfc02a3c33..cf612afef508 100644 --- a/packages/node/test/eventbuilders.test.ts +++ b/packages/node/test/eventbuilders.test.ts @@ -1,7 +1,7 @@ import type { Client } from '@sentry/types'; +import { eventFromUnknownInput } from '@sentry/utils'; -import { defaultStackParser, Scope } from '../src'; -import { eventFromUnknownInput } from '../src/eventbuilder'; +import { defaultStackParser, getCurrentHub, Scope } from '../src'; const testScope = new Scope(); @@ -55,7 +55,7 @@ describe('eventFromUnknownInput', () => { }, }; - eventFromUnknownInput(defaultStackParser, deepObject); + eventFromUnknownInput(getCurrentHub, defaultStackParser, deepObject); const serializedObject = (testScope as any)._extra.__serialized__; expect(serializedObject).toBeDefined(); diff --git a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js index 300870d21fac..dc6aa4485dcb 100644 --- a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js +++ b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js @@ -34,6 +34,7 @@ function makeDummyTransport() { .split('\n') .filter(l => !!l) .map(e => JSON.parse(e)); + assertSessionAggregates(sessionEnv[2], { attrs: { release: '1.1' }, aggregates: [{ crashed: 2, errored: 1, exited: 1 }], diff --git a/packages/node/test/stacktrace.test.ts b/packages/node/test/stacktrace.test.ts index f5a1b453609f..5b0f6fc52e25 100644 --- a/packages/node/test/stacktrace.test.ts +++ b/packages/node/test/stacktrace.test.ts @@ -10,7 +10,8 @@ * @license MIT */ -import { parseStackFrames } from '../src/eventbuilder'; +import { parseStackFrames } from '@sentry/utils'; + import { defaultStackParser as stackParser } from '../src/sdk'; function testBasic() { @@ -32,7 +33,7 @@ describe('Stack parsing', () => { const last = frames.length - 1; expect(frames[last].filename).toEqual(__filename); expect(frames[last].function).toEqual('testBasic'); - expect(frames[last].lineno).toEqual(17); + expect(frames[last].lineno).toEqual(18); expect(frames[last].colno).toEqual(10); });