You are here

raven.module in Raven: Sentry Integration 7.2

Allows to track errors to Sentry server.

File

raven.module
View source
<?php

/**
 * @file
 * Allows to track errors to Sentry server.
 */

/**
 * 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 array(
    'send javascript errors to sentry' => array(
      'title' => t('Send JavaScript errors to Sentry'),
      'description' => t("For users with this permission, JavaScript errors will be captured and submitted to the Sentry server's public DSN."),
    ),
  );
}

/**
 * Implements hook_libraries_info().
 */
function raven_libraries_info() {
  $libraries = array();
  if (module_exists('xautoload')) {
    $libraries['sentry-php'] = array(
      'name' => 'Sentry PHP',
      'vendor url' => 'https://github.com/getsentry/sentry-php',
      'download url' => 'https://github.com/getsentry/sentry-php/releases',
      'version arguments' => array(
        'file' => 'lib/Raven/Client.php',
        'pattern' => '#const\\s+VERSION\\s*=\\s*\'([0-9a-z._-]+)\';#',
        'lines' => 25,
      ),
      'xautoload' => function ($adapter) {
        $adapter
          ->composerJson('composer.json');
      },
    );
  }
  return $libraries;
}

/**
 * Implements hook_init().
 */
function raven_init() {
  global $user;
  if (!variable_get('raven_enabled', FALSE)) {
    return;
  }
  $client = raven_get_client();
  if (!$client) {
    return;
  }

  // Bind the logged in user.
  $context['id'] = $user->uid;
  $context['ip_address'] = ip_address();
  $context['roles'] = implode(', ', $user->roles);
  if (user_is_logged_in()) {
    $context['name'] = $user->name;
    $context['email'] = $user->mail;
  }
  drupal_alter('raven_user', $context);
  $client
    ->user_context($context);

  // Tag the request with something interesting.
  $context = array();
  drupal_alter('raven_tags', $context);
  $client
    ->tags_context($context);

  // Provide a bit of additional context.
  $context = array();
  drupal_alter('raven_extra', $context);
  $client
    ->extra_context($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'] = '3.27.2';
  $path = drupal_get_path('module', 'raven');
  if (variable_get('raven_js_source', 'library') == 'cdn') {
    $libraries['raven']['js'][variable_get('raven_js_cdn_url')] = array(
      'type' => 'external',
    );
  }
  else {
    $libraries['raven']['js']["{$path}/js/raven-js/raven.min.js"] = array();
  }
  $options = new stdClass();
  if (!empty($_SERVER['SENTRY_RELEASE'])) {
    $options->release = $_SERVER['SENTRY_RELEASE'];
  }
  elseif ($release = variable_get('raven_release')) {
    $options->release = $release;
  }
  if (!empty($_SERVER['SENTRY_ENVIRONMENT'])) {
    $options->environment = $_SERVER['SENTRY_ENVIRONMENT'];
  }
  elseif ($environment = variable_get('raven_environment')) {
    $options->environment = $environment;
  }
  $libraries['raven']['js'][] = array(
    'data' => array(
      'raven' => array(
        'dsn' => empty($_SERVER['SENTRY_DSN']) ? variable_get('raven_public_dsn', '') : $_SERVER['SENTRY_DSN'],
        // Other modules can alter the Raven.js options.
        'options' => $options,
        'user' => array(
          'id' => $user->uid,
        ),
      ),
    ),
    'type' => 'setting',
  );
  $libraries['raven']['js']["{$path}/js/raven.js"] = array(
    // Load in the footer to ensure settings are available.
    'scope' => 'footer',
  );
  return $libraries;
}

/**
 * Implements hook_watchdog().
 */
function raven_watchdog($log_entry) {
  if (!variable_get('raven_enabled', FALSE)) {
    return;
  }
  $client = raven_get_client();
  if (!$client) {
    return;
  }
  $watchdog_levels = variable_get('raven_watchdog_levels', array());
  $levels_map = array(
    WATCHDOG_EMERGENCY => Raven_Client::FATAL,
    WATCHDOG_ALERT => Raven_Client::FATAL,
    WATCHDOG_CRITICAL => Raven_Client::FATAL,
    WATCHDOG_ERROR => Raven_Client::ERROR,
    WATCHDOG_WARNING => Raven_Client::WARNING,
    WATCHDOG_NOTICE => Raven_Client::INFO,
    WATCHDOG_INFO => Raven_Client::INFO,
    WATCHDOG_DEBUG => Raven_Client::DEBUG,
  );
  $variables = $log_entry['variables'];
  if (!$variables) {
    $variables = array();
  }
  if (!function_exists('truncate_utf8')) {
    require_once DRUPAL_ROOT . '/includes/unicode.inc';
    unicode_check();
  }
  $message = truncate_utf8(html_entity_decode(strip_tags(strtr($log_entry['message'], $variables)), ENT_QUOTES, 'UTF-8'), variable_get('raven_message_limit', 2048), FALSE, TRUE);
  $data = array(
    'level' => $levels_map[$log_entry['severity']],
    'sentry.interfaces.Message' => array(
      'message' => $log_entry['message'],
      'params' => $log_entry['variables'],
      'formatted' => $message,
    ),
    'extra' => array(
      'link' => $log_entry['link'],
      'request_uri' => $log_entry['request_uri'],
      'referer' => $log_entry['referer'],
    ),
    'logger' => $log_entry['type'],
    'user' => array(
      'id' => $log_entry['uid'],
      'ip_address' => $log_entry['ip'],
    ),
  );
  if ($log_entry['user']) {
    $data['user']['roles'] = implode(', ', $log_entry['user']->roles);
    if ($log_entry['uid']) {
      $data['user']['name'] = $log_entry['user']->name;
      $data['user']['email'] = $log_entry['user']->mail;
    }
  }
  $filter = array(
    'process' => !empty($watchdog_levels[$log_entry['severity'] + 1]),
    'log_entry' => $log_entry,
    'data' => &$data,
  );
  $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, array(
        'line' => $exception
          ->getLine(),
        'file' => $exception
          ->getFile(),
      ));
      if ($exception instanceof PDOException) {
        $db_functions = array(
          '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);
        }
      }
    }

    // By default, disable reflection tracing for user watchdog entries.
    if ($data['logger'] === 'user' && $client->trace && !variable_get('raven_trace_user', FALSE)) {
      $client->trace = FALSE;
      $client
        ->capture($data, $client->auto_log_stacks ? $stack : FALSE);
      $client->trace = TRUE;
    }
    else {
      $client
        ->capture($data, $client->auto_log_stacks ? $stack : FALSE);
    }
  }

  // Record a breadcrumb.
  $breadcrumb = [
    'log_entry' => $log_entry,
    'process' => TRUE,
    'breadcrumb' => [
      'category' => $log_entry['type'],
      'message' => $message,
      'level' => $data['level'],
    ],
  ];
  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'])) {
    $client->breadcrumbs
      ->record($breadcrumb['breadcrumb']);
  }
}

/**
 * Returns the Sentry PHP client instance, or NULL if it could not be created.
 *
 * @return Raven_Client|null
 *   Raven PHP client library instance.
 */
function raven_get_client() {
  global $user;
  static $client;
  if (!isset($client)) {
    if (!raven_libraries_load()) {
      return;
    }

    // Prepare config.
    $dsn = empty($_SERVER['SENTRY_DSN']) ? variable_get('raven_dsn', NULL) : $_SERVER['SENTRY_DSN'];
    $timeout = variable_get('raven_timeout', 2);
    $message_limit = variable_get('raven_message_limit', 2048);
    $stack = variable_get('raven_stack', TRUE);
    $trace = variable_get('raven_trace', FALSE);

    // Build the field sanitization regular expression.
    $fields = array(
      'SESS',
      'key',
      'token',
      'pass',
      'authorization',
      'password',
      'passwd',
      'secret',
      'password_confirmation',
      'card_number',
      'auth_pw',
    );
    drupal_alter('raven_sanitize_fields', $fields);
    $fields_re = '/(' . implode('|', $fields) . ')/i';
    $options = array(
      'timeout' => $timeout,
      'message_limit' => $message_limit,
      'auto_log_stacks' => $stack,
      'trace' => $trace,
      'processorOptions' => array(
        'Raven_SanitizeDataProcessor' => array(
          'fields_re' => $fields_re,
        ),
        'Raven_Processor_SanitizeDataProcessor' => array(
          'fields_re' => $fields_re,
        ),
      ),
      'curl_method' => 'async',
      'verify_ssl' => TRUE,
    );
    if (!empty($_SERVER['SENTRY_ENVIRONMENT'])) {
      $options['environment'] = $_SERVER['SENTRY_ENVIRONMENT'];
    }
    elseif ($environment = variable_get('raven_environment')) {
      $options['environment'] = $environment;
    }
    if (!empty($_SERVER['SENTRY_RELEASE'])) {
      $options['release'] = $_SERVER['SENTRY_RELEASE'];
    }
    elseif ($release = variable_get('raven_release')) {
      $options['release'] = $release;
    }
    $raven_ssl = variable_get('raven_ssl', 'verify_ssl');

    // Verify against a CA certificate.
    if ($raven_ssl == 'ca_cert') {
      $options['ca_cert'] = drupal_realpath(variable_get('raven_ca_cert', ''));
    }
    elseif ($raven_ssl == 'no_verify_ssl') {
      $options['verify_ssl'] = FALSE;
    }

    // Breadcrumbs confuse Drupal as to which line of code is throwing an error.
    $options['install_default_breadcrumb_handlers'] = FALSE;

    // Allow other modules to alter $options before passing into Raven client.
    drupal_alter('raven_options', $options);
    try {

      // Instantiate a new client with a compatible DSN.
      $client = new Raven_Client($dsn, $options);
    } catch (InvalidArgumentException $e) {

      // Raven is incorrectly configured.
      return;
    }

    // Bind user context to prevent session ID from being sent as user ID.
    $client
      ->user_context([
      'id' => $user ? $user->uid : 0,
      'ip_address' => ip_address(),
    ]);

    // Register fatal error handler.
    if (variable_get('raven_fatal_error_handler', TRUE)) {
      $handler = new Raven_ErrorHandler($client);
      $reserved_memory = variable_get('raven_fatal_error_handler_memory', 2.5 * 1024);
      $handler
        ->registerShutdownFunction($reserved_memory);

      // Register shutdown function again for fatal error via async.
      register_shutdown_function(array(
        $client,
        'onShutdown',
      ));
    }
  }
  return $client;
}

/**
 * Loads Sentry PHP library.
 *
 * @return bool
 *   Returns TRUE if libraries loaded or FALSE otherwise.
 */
function raven_libraries_load() {
  $library['loaded'] = FALSE;
  if (class_exists('Raven_Client')) {
    $library['loaded'] = TRUE;
  }
  elseif (function_exists('composer_autoloader') && composer_autoloader() && class_exists('Raven_Client')) {
    $library['loaded'] = TRUE;
  }
  elseif (module_exists('libraries')) {
    $library = libraries_load('sentry-php');
  }
  if (!$library['loaded']) {

    // This function can be called multiple times, so prevent multiple messages.
    drupal_set_message(t('Sentry PHP library cannot be loaded. Check status report for more details.'), 'warning', FALSE);
  }
  return (bool) $library['loaded'];
}

/**
 * Appends additional context.
 *
 * @param array $data
 *   Associative array of extra data.
 */
function raven_extra_context(array $data = array()) {
  $client = raven_get_client();
  if (!$client) {
    return;
  }
  $client
    ->extra_context($data);
}

/**
 * Appends tags context.
 *
 * @param array $data
 *   Associative array of tags.
 */
function raven_tags_context(array $data = array()) {
  $client = raven_get_client();
  if (!$client) {
    return;
  }
  $client
    ->tags_context($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() {
  $client = raven_get_client();
  if (!$client) {
    return;
  }
  $client
    ->onShutdown();
}

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_libraries_info Implements hook_libraries_info().
raven_libraries_load Loads Sentry PHP library.
raven_library Implements hook_library().
raven_page_build Implements hook_page_build().
raven_permission Implements hook_permission().
raven_tags_context Appends tags context.
raven_watchdog Implements hook_watchdog().