You are here

raven.module in Raven: Sentry Integration 7.4

Allows to track errors to Sentry server.

File

raven.module
View source
<?php

/**
 * @file
 * Allows to track errors to Sentry server.
 */
use Sentry\Breadcrumb;
use Sentry\ClientInterface;
use Sentry\Dsn;
use Sentry\Event;
use Sentry\EventHint;
use Sentry\Integration\EnvironmentIntegration;
use Sentry\Integration\FatalErrorListenerIntegration;
use Sentry\Integration\FrameContextifierIntegration;
use Sentry\Integration\RequestIntegration;
use Sentry\Integration\TransactionIntegration;
use Sentry\SentrySdk;
use Sentry\Serializer\RepresentationSerializer;
use Sentry\Severity;
use Sentry\StacktraceBuilder;
use Sentry\State\Scope;
use Sentry\UserDataBag;

/**
 * Implements hook_form_system_logging_settings_alter().
 */
function raven_form_system_logging_settings_alter(array &$form, array &$form_state) {
  module_load_include('admin.inc', 'raven');
  raven_settings_form($form, $form_state);
}

/**
 * Implements hook_permission().
 */
function raven_permission() {
  return [
    'send javascript errors to sentry' => [
      'title' => t('Send JavaScript errors to Sentry'),
      'description' => t('For users with this permission, JavaScript errors will be captured and sent to Sentry.'),
    ],
  ];
}

/**
 * Implements hook_init().
 */
function raven_init() {
  if (!variable_get('raven_enabled', FALSE) || !raven_get_client()) {
    return;
  }

  // Bind the logged-in user.
  \Sentry\configureScope(function (Scope $scope) : void {
    global $user;

    // Bind the logged in user.
    $context['id'] = $user->uid;
    $context['ip_address'] = ip_address();
    $context['roles'] = implode(', ', $user->roles);
    if (user_is_logged_in() && variable_get('raven_send_user_data', FALSE)) {
      $context['username'] = $user->name;
      $context['email'] = $user->mail;
    }
    drupal_alter('raven_user', $context);
    $scope
      ->setUser($context);
  });

  // Tag the request with something interesting.
  \Sentry\configureScope(function (Scope $scope) : void {
    $context = [];
    drupal_alter('raven_tags', $context);
    $scope
      ->setTags($context);
  });

  // Provide a bit of additional context.
  \Sentry\configureScope(function (Scope $scope) : void {
    $context = [];
    drupal_alter('raven_extra', $context);
    $scope
      ->setExtras($context);
  });
}

/**
 * Implements hook_page_build().
 */
function raven_page_build(&$page) {
  if (variable_get('raven_js_enabled', FALSE) && user_access('send javascript errors to sentry')) {
    drupal_add_library('raven', 'raven', TRUE);
  }
}

/**
 * Implements hook_library().
 */
function raven_library() {
  global $user;
  $libraries['raven']['version'] = '6.12.0';
  $path = drupal_get_path('module', 'raven');
  $libraries['raven']['js']["{$path}/js/bundle.tracing.min.js"] = [];
  $options = new stdClass();
  $options->dsn = empty($_SERVER['SENTRY_DSN']) ? variable_get('raven_public_dsn', '') : $_SERVER['SENTRY_DSN'];
  if ($environment = empty($_SERVER['SENTRY_ENVIRONMENT']) ? variable_get('raven_environment') : $_SERVER['SENTRY_ENVIRONMENT']) {
    $options->environment = $environment;
  }
  if ($release = empty($_SERVER['SENTRY_RELEASE']) ? variable_get('raven_release') : $_SERVER['SENTRY_RELEASE']) {
    $options->release = $release;
  }
  $options->autoSessionTracking = variable_get('raven_auto_session_tracking', FALSE);
  $options->sendClientReports = variable_get('raven_send_client_reports', FALSE);
  if ($tracesSampleRate = variable_get('raven_js_traces_sample_rate', NULL)) {
    $options->tracesSampleRate = (double) $tracesSampleRate;
  }
  $options->integrations = [];
  $libraries['raven']['js'][] = [
    'data' => [
      'raven' => [
        // Other modules can alter the @sentry/browser options.
        'options' => $options,
        'user' => [
          'id' => $user->uid,
        ],
      ],
    ],
    'type' => 'setting',
  ];
  $libraries['raven']['js']["{$path}/js/raven.js"] = [
    // Load in the footer to ensure settings are available.
    'scope' => 'footer',
  ];
  return $libraries;
}

/**
 * Implements hook_watchdog().
 */
function raven_watchdog($log_entry) {
  static $counter = 0;
  if (!variable_get('raven_enabled', FALSE)) {
    return;
  }
  $client = raven_get_client();
  if (!$client) {
    return;
  }
  $watchdog_levels = variable_get('raven_watchdog_levels', []);
  $levels_map = [
    WATCHDOG_EMERGENCY => Severity::FATAL,
    WATCHDOG_ALERT => Severity::FATAL,
    WATCHDOG_CRITICAL => Severity::FATAL,
    WATCHDOG_ERROR => Severity::ERROR,
    WATCHDOG_WARNING => Severity::WARNING,
    WATCHDOG_NOTICE => Severity::INFO,
    WATCHDOG_INFO => Severity::INFO,
    WATCHDOG_DEBUG => Severity::DEBUG,
  ];
  $variables = $log_entry['variables'];
  if (!$variables) {
    $variables = [];
  }
  $event = Event::createEvent();
  $event
    ->setLevel(new Severity($levels_map[$log_entry['severity']]));
  $message = html_entity_decode(strip_tags(strtr($log_entry['message'], $variables)), ENT_QUOTES, 'UTF-8');
  $event
    ->setMessage($log_entry['message'], $variables, $message);
  $extra = [
    'request_uri' => $log_entry['request_uri'],
  ];
  if ($log_entry['referer']) {
    $extra['referer'] = $log_entry['referer'];
  }
  if ($log_entry['link']) {
    $extra['link'] = $log_entry['link'];
  }
  $event
    ->setExtra($extra);
  $event
    ->setLogger($log_entry['type']);
  $user = UserDataBag::createFromUserIdentifier($log_entry['uid']);
  $user
    ->setIpAddress($log_entry['ip']);
  if ($log_entry['user'] && $log_entry['uid']) {
    if (variable_get('raven_send_user_data', FALSE)) {
      $user
        ->setEmail($log_entry['user']->mail);
      $user
        ->setUsername($log_entry['user']->name);
    }
    $user
      ->setMetadata('roles', implode(', ', $log_entry['user']->roles));
  }
  $event
    ->setUser($user);
  $filter = [
    'process' => !empty($watchdog_levels[$log_entry['severity'] + 1]),
    'log_entry' => $log_entry,
    'event' => $event,
  ];
  $ignored_types = array_map('trim', preg_split('/\\R/', variable_get('raven_ignored_types', ''), -1, PREG_SPLIT_NO_EMPTY));
  if (in_array($log_entry['type'], $ignored_types)) {
    $filter['process'] = FALSE;
  }
  drupal_alter('raven_watchdog_filter', $filter);
  if ($filter['process']) {

    // Save memory by not copying the object for each frame.
    $stack = debug_backtrace(0);

    // Ignore error handling and logging frames.
    if (empty($stack[0]['class']) && isset($stack[0]['function']) && $stack[0]['function'] == 'raven_watchdog') {
      array_shift($stack);
    }
    if (empty($stack[0]['class']) && isset($stack[0]['function']) && $stack[0]['function'] == 'call_user_func_array') {
      array_shift($stack);
    }
    if (empty($stack[0]['class']) && isset($stack[0]['function']) && ($stack[0]['function'] == 'module_invoke_all' || $stack[0]['function'] == 'module_invoke')) {
      array_shift($stack);
    }
    if (empty($stack[0]['class']) && isset($stack[0]['function']) && $stack[0]['function'] == 'watchdog' && empty($stack[1]['class']) && isset($stack[1]['function']) && $stack[1]['function'] == 'watchdog_exception') {
      array_shift($stack);
    }
    elseif (empty($stack[0]['class']) && isset($stack[0]['function']) && $stack[0]['function'] == 'watchdog' && empty($stack[1]['class']) && isset($stack[1]['function']) && $stack[1]['function'] == '_drupal_log_error') {
      array_shift($stack);
      array_shift($stack);
    }
    if (empty($stack[0]['class']) && isset($stack[0]['function']) && $stack[0]['function'] == '_drupal_error_handler_real') {
      array_shift($stack);
    }
    if (empty($stack[0]['class']) && isset($stack[0]['function']) && $stack[0]['function'] == '_drupal_error_handler' && empty($stack[0]['line'])) {
      array_shift($stack);
    }
    if (empty($stack[0]['class']) && isset($stack[0]['function']) && ($stack[0]['function'] == 'watchdog_exception' || $stack[0]['function'] == '_drupal_exception_handler')) {
      $arg = [
        'watchdog_exception' => 1,
        '_drupal_exception_handler' => 0,
      ];

      // Use the exception backtrace for (usually) easier debugging.
      $exception = $stack[0]['args'][$arg[$stack[0]['function']]];
      $stack = $exception
        ->getTrace();

      // Copy logic from _drupal_decode_exception().
      array_unshift($stack, [
        'line' => $exception
          ->getLine(),
        'file' => $exception
          ->getFile(),
      ]);
      if ($exception instanceof PDOException) {
        $db_functions = [
          'db_query',
          'db_query_range',
        ];
        while (!empty($stack[1]) && ($caller = $stack[1]) && (isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE) || in_array($caller['function'], $db_functions))) {
          array_shift($stack);
        }
      }
    }
    if (!variable_get('raven_trace', FALSE)) {
      foreach ($stack as &$frame) {
        unset($frame['args']);
      }
    }
    $stacktraceBuilder = new StacktraceBuilder($client
      ->getOptions(), new RepresentationSerializer($client
      ->getOptions()));
    $stacktrace = $stacktraceBuilder
      ->buildFromBacktrace($stack, '', 0);
    $stacktrace
      ->removeFrame(count($stacktrace
      ->getFrames()) - 1);
    $rateLimit = variable_get('raven_rate_limit', 0);
    if (!$rateLimit || $counter < $rateLimit) {
      \Sentry\captureEvent($event, EventHint::fromArray([
        'stacktrace' => $stacktrace,
      ]));
    }
    elseif ($counter == $rateLimit) {
      \Sentry\captureException(new RavenRateLimitException('Log event discarded due to rate limit exceeded; future log events will not be captured by Sentry.'));
    }
    $counter++;
  }

  // Record a breadcrumb.
  $breadcrumb = [
    'log_entry' => $log_entry,
    'process' => TRUE,
    'breadcrumb' => [
      'category' => $log_entry['type'],
      'message' => $message,
      'level' => $levels_map[$log_entry['severity']],
    ],
  ];
  foreach ([
    '%line',
    '%file',
    '%type',
    '%function',
  ] as $key) {
    if (isset($log_entry['variables'][$key])) {
      $breadcrumb['breadcrumb']['data'][substr($key, 1)] = $log_entry['variables'][$key];
    }
  }
  drupal_alter('raven_breadcrumb', $breadcrumb);
  if (!empty($breadcrumb['process'])) {
    \Sentry\addBreadcrumb(Breadcrumb::fromArray($breadcrumb['breadcrumb']));
  }
}

/**
 * Returns the Sentry PHP client instance, or NULL if it could not be created.
 *
 * @return \Sentry\ClientInterface|null
 *   Sentry PHP client instance.
 */
function raven_get_client() : ?ClientInterface {
  if (!class_exists(SentrySdk::class)) {
    return NULL;
  }
  if ($client = SentrySdk::getCurrentHub()
    ->getClient()) {
    return $client;
  }
  $options = [
    'default_integrations' => FALSE,
    'dsn' => empty($_SERVER['SENTRY_DSN']) ? variable_get('raven_dsn', NULL) : $_SERVER['SENTRY_DSN'],
  ];
  if (variable_get('raven_stack', TRUE)) {
    $options['attach_stacktrace'] = TRUE;
  }
  if (variable_get('raven_fatal_error_handler', TRUE)) {
    $options['integrations'][] = new FatalErrorListenerIntegration();
  }
  $options['integrations'][] = new RequestIntegration();
  $options['integrations'][] = new TransactionIntegration();
  $options['integrations'][] = new FrameContextifierIntegration();
  $options['integrations'][] = new EnvironmentIntegration();
  $options['integrations'][] = new RavenSanitizeIntegration();
  if ($environment = empty($_SERVER['SENTRY_ENVIRONMENT']) ? variable_get('raven_environment') : $_SERVER['SENTRY_ENVIRONMENT']) {
    $options['environment'] = $environment;
  }
  if ($release = empty($_SERVER['SENTRY_RELEASE']) ? variable_get('raven_release') : $_SERVER['SENTRY_RELEASE']) {
    $options['release'] = $release;
  }
  if (!variable_get('raven_send_request_body', FALSE)) {
    $options['max_request_body_size'] = 'none';
  }

  // Allow other modules to alter $options before passing into Raven client.
  drupal_alter('raven_options', $options);
  try {
    \Sentry\init($options);
  } catch (InvalidArgumentException $e) {

    // Raven is incorrectly configured.
    return NULL;
  }
  \Sentry\configureScope(function (Scope $scope) : void {
    global $user;
    $context['id'] = $user ? $user->uid : 0;
    $context['ip_address'] = ip_address();
    $scope
      ->setUser($context);
  });
  return SentrySdk::getCurrentHub()
    ->getClient();
}

/**
 * Appends additional context.
 *
 * @param array $data
 *   Associative array of extra data.
 */
function raven_extra_context(array $data = []) {
  if (raven_get_client()) {
    \Sentry\configureScope(function (Scope $scope) use ($data) : void {
      $scope
        ->setExtras($data);
    });
  }
}

/**
 * Appends tags context.
 *
 * @param array $data
 *   Associative array of tags.
 */
function raven_tags_context(array $data = []) {
  if (raven_get_client()) {
    \Sentry\configureScope(function (Scope $scope) use ($data) : void {
      $scope
        ->setTags($data);
    });
  }
}

/**
 * Sends all unsent events.
 *
 * Call this function periodically if you have a long-running script or
 * are processing a large set of data which may generate errors.
 */
function raven_flush() {
  if ($client = raven_get_client()) {
    $client
      ->flush();
  }
}

/**
 * Implements hook_seckit_options_alter().
 */
function raven_seckit_options_alter(&$options) {
  if (!class_exists(Dsn::class)) {
    return;
  }
  try {
    $dsn = Dsn::createFromString(empty($_SERVER['SENTRY_DSN']) ? variable_get('raven_public_dsn') : $_SERVER['SENTRY_DSN']);
  } catch (InvalidArgumentException $e) {

    // Raven is incorrectly configured.
    return;
  }
  $query = [
    'sentry_key' => $dsn
      ->getPublicKey(),
  ];
  if ($environment = empty($_SERVER['SENTRY_ENVIRONMENT']) ? variable_get('raven_environment') : $_SERVER['SENTRY_ENVIRONMENT']) {
    $query['sentry_environment'] = $environment;
  }
  if ($release = empty($_SERVER['SENTRY_RELEASE']) ? variable_get('raven_release') : $_SERVER['SENTRY_RELEASE']) {
    $query['sentry_release'] = $release;
  }
  $url = url(str_replace('/store/', '/security/', $dsn
    ->getStoreApiEndpointUrl()), [
    'query' => $query,
  ]);
  if (variable_get('raven_set_report_uri')) {
    $options['seckit_ct']['report-uri'] = $url;
    $options['seckit_xss']['csp']['report-uri'] = $url;
  }
  if (variable_get('raven_js_enabled', FALSE)) {
    $options['seckit_xss']['csp']['connect-src'] .= $options['seckit_xss']['csp']['connect-src'] ? " {$dsn->getStoreApiEndpointUrl()} {$dsn->getEnvelopeApiEndpointUrl()}" : "{$options['seckit_xss']['csp']['default-src']} {$dsn->getStoreApiEndpointUrl()} {$dsn->getEnvelopeApiEndpointUrl()}";
  }
}

Functions

Namesort descending Description
raven_extra_context Appends additional context.
raven_flush Sends all unsent events.
raven_form_system_logging_settings_alter Implements hook_form_system_logging_settings_alter().
raven_get_client Returns the Sentry PHP client instance, or NULL if it could not be created.
raven_init Implements hook_init().
raven_library Implements hook_library().
raven_page_build Implements hook_page_build().
raven_permission Implements hook_permission().
raven_seckit_options_alter Implements hook_seckit_options_alter().
raven_tags_context Appends tags context.
raven_watchdog Implements hook_watchdog().