Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Context Not Propagated for Unhandled Exceptions in withScope #14839

Closed
3 tasks done
AB-291 opened this issue Dec 24, 2024 · 1 comment
Closed
3 tasks done

Context Not Propagated for Unhandled Exceptions in withScope #14839

AB-291 opened this issue Dec 24, 2024 · 1 comment
Labels
Package: nestjs Issues related to the Sentry Nestjs SDK

Comments

@AB-291
Copy link

AB-291 commented Dec 24, 2024

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/nestjs

SDK Version

8.47.0

Framework Version

Nestjs 10.4.2

Link to Sentry event

No response

Reproduction Example/SDK Setup

import { Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { SentryExceptionCaptured } from '@sentry/nestjs';
import * as Sentry from '@sentry/nestjs';

@Catch()
export class SentryFilter extends BaseExceptionFilter {
  @SentryExceptionCaptured()
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();

    Sentry.withScope((scope) => {
      // Add request details
      if (request) {
        scope.setContext('Request', {
          url: request.url,
          method: request.method,
          headers: request.headers,
          body: request.body,
          query: request.query,
          params: request.params,
        });
        scope.setUser(request.user);
      }

      if (exception instanceof HttpException) {
        const status = exception.getStatus();
        const exceptionResponse = exception.getResponse();

        scope.setLevel(status >= 500 ? 'error' : 'warning');
        scope.setContext('Error Details', {
          message: exception.message,
          status,
          response: exceptionResponse,
          stack: exception.stack,
        });

        Sentry.captureException(exception);
      } else {
        scope.setLevel('error');
        scope.setContext('Error Details', {
          message: (exception as any).message || 'Unhandled Exception',
          stack: (exception as any).stack || 'No stack trace available',
        });

        Sentry.captureException(exception);
      }
    });

    // Continue default NestJS handling
    super.catch(exception, host);
  }
}

Steps to Reproduce

  1. For handled exceptions (e.g., when throwing HttpException), the request context (URL, method, headers, etc.) and user details are captured and displayed correctly in Sentry.
  2. For unhandled exceptions (e.g., unexpected errors or rejected promises), the context does not propagate even though I explicitly set it in Sentry.withScope.

Expected Result

The request and user context attached in Sentry.withScope should propagate correctly for unhandled exceptions and appear in the Sentry logs.

Actual Result

I have debugged the issue and confirmed the following:

  • The context (e.g., request details) is successfully attached to the scope in the Sentry.withScope block.
  • However, when Sentry.captureException is called, the context does not appear in the logs on Sentry’s dashboard.

What I have tried:

  • I added middleware to attach request details globally to Sentry.configureScope, but the issue persists for unhandled exceptions.
  • I verified that my Sentry configuration is correct and that @sentry/node and @sentry/nestjs are using the latest versions.
@getsantry getsantry bot moved this to Waiting for: Product Owner in GitHub Issues with 👀 3 Dec 24, 2024
@github-actions github-actions bot added the Package: nestjs Issues related to the Sentry Nestjs SDK label Dec 24, 2024
@AB-291
Copy link
Author

AB-291 commented Dec 24, 2024

This worked for me.

import {
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import * as Sentry from '@sentry/nestjs';
import { Scope } from '@sentry/node';

@Catch()
export class SentryFilter extends BaseExceptionFilter {
  /**
   * Exception handling and Sentry integration
   */
  catch(exception: unknown, host: ArgumentsHost) {
    try {
      const ctx = host.switchToHttp();
      const request = ctx.getRequest();

      // Use Sentry's withScope for scoped error handling
      void Sentry.withScope(async (scope) => {
        this.enrichScope(scope, request, exception);
        Sentry.captureException(exception);

        // Flush events to ensure they are sent to Sentry
        try {
          await Sentry.flush(2000);
        } catch (flushError) {
          console.error('Error flushing Sentry event:', flushError);
        }
      });
    } catch (sentryError) {
      console.error('Error reporting to Sentry:', sentryError);
    }

    // Proceed with NestJS's default exception handling
    super.catch(exception, host);
  }

  /**
   * Enrich the Sentry scope with request and error details
   */
  private enrichScope(scope: Scope, request: any, exception: any): void {
    if (request) {
      // Add request details
      scope.setContext('Request Details', {
        url: request.url,
        method: request.method,
        headers: this.sanitizeHeaders(request.headers),
        body: request.body,
        query: request.query,
        params: request.params,
      });

      // Add user details if available
      if (request.user) {
        scope.setUser({
          id: request.user.id,
          sessionId: request.user.sessionId,
        });
      }

      // Add transaction tag
      scope.setTag(
        'transaction',
        `${request.method} ${request.route?.path || request.url}`,
      );
    }

    let errors: any = {};
    let title: string = '';
    let status = HttpStatus.INTERNAL_SERVER_ERROR;

    if (exception instanceof HttpException) {
      // Determine error status and set error level
      status = exception.getStatus();

      const responseObj = exception.getResponse() as any;
      errors = responseObj.errors || responseObj.error || {};
      if (
        typeof responseObj.errors === 'object' &&
        Object.keys(responseObj.errors).length > 0
      ) {
        title = Object.values(errors).join(',');
      } else {
        title = responseObj.error;
      }
    }

    scope.setLevel(status >= 500 ? 'error' : 'warning');

    // Add error details to the scope
    scope.setContext('Error Details', {
      name: exception.name || 'UnknownError',
      message: exception.message || 'An unknown error occurred',
      errors,
      stack: exception.stack || 'No stack trace available',
      status,
      timestamp: new Date().toISOString(),
    });

    // Add error-related tags
    scope.setTag(
      'error.type',
      exception instanceof HttpException ? 'handled' : 'unhandled',
    );
    scope.setTag('error.status', status.toString());

    exception.name = title;
  }

  /**
   * Sanitize headers to redact sensitive information
   */
  private sanitizeHeaders(headers: any): any {
    const sanitized = { ...headers };
    ['authorization', 'cookie'].forEach((key) => {
      if (sanitized[key]) {
        sanitized[key] = '[REDACTED]';
      }
    });
    return sanitized;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Package: nestjs Issues related to the Sentry Nestjs SDK
Projects
Archived in project
Development

No branches or pull requests

1 participant