<?php

/*
 * Configuration :
 *
 * // Log PHP errors in Sentry: list of errors types to logs
 * // Note: must also match with $log_php_errors_levels.
 * // See: https://www.php.net/manual/fr/errorfunc.constants.php
 * $sentry_php_error_types = array(
 *   E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR,
 *   E_RECOVERABLE_ERROR,E_DEPRECATED,
 * );
 */

// Init Sentry (if its DSN is configured)
if (isset($sentry_dsn) && $sentry_dsn) {
    \Sentry\init([
        'dsn' => $sentry_dsn,
        'traces_sample_rate' => (
            isset($sentry_traces_sample_rate) ? 
            $sentry_traces_sample_rate : 0.2
        ),
    ]);

    \Sentry\configureScope(function (\Sentry\State\Scope $scope): void {
        global $auth_user;
        $scope->setUser([
            'id' => isset($auth_user) && $auth_user?$auth_user['uid']:null,
            'email' => isset($auth_user) && $auth_user?$auth_user['mail']:null,
            'segment' => isset($auth_user) && $auth_user?$auth_user['type']:null,
            'ip_address' => $_SERVER['REMOTE_ADDR'],
        ]);
    });
}

/**
 * Log an exception or a message in Sentry
 * @param string|Exception $msg
 * @return void
 */
function log_in_sentry($msg) {
    global $sentry_dsn;
    if (!isset($sentry_dsn) || !$sentry_dsn) {
        logging('TRACE', 'Sentry DSN not configured, do not log this error');
        return;
    }
    if (is_string($msg)) {
        logging('DEBUG', 'Error logged in Sentry');
        \Sentry\captureMessage($msg);
    }
    elseif ($msg instanceof Exception) {
        logging('DEBUG', 'Exception logged in Sentry');
        \Sentry\captureException($msg);
    }
}

/**
 * Log a PHP error in Sentry
 * @param int $errno The error number
 * @param string $msg The error message
 * @return void
 */
function log_php_error_in_sentry($errno, $msg) {
    global $sentry_php_error_types;
    if (
        isset($sentry_php_error_types)
        && is_array($sentry_php_error_types)
        && in_array($errno, $sentry_php_error_types)
    )
        log_in_sentry($msg);
}

/*
 * Performance monitoring
 */

class SentryTransaction {

    /**
     * The Sentry transaction object
     * @var \Sentry\Tracing\Transaction
     */
    private $transaction;

    /**
     * The Sentry transaction context object
     * @var \Sentry\Tracing\TransactionContext
     */
    private $context;

    /**
     * Constructor: start a Sentry transaction
     * @param string|null $op The operation name
     * @param string|null $name The transaction name
     * @return void
     */
    public function __construct($op=null, $name=null) {
        // Setup context for the full transaction
        $this->context = new \Sentry\Tracing\TransactionContext();
        $this->context->setName(
            $name?$name:
            (php_sapi_name()=='cli'?'CLI execution':'HTTP request')
        );
        $this->context->setOp(
            $op?$op:
            (php_sapi_name()=='cli'?'cli.command':'http.request')
        );

        // Start the transaction
        $this->transaction = \Sentry\startTransaction($this->context);

        // Set the current transaction as the current span so we can retrieve it later
        \Sentry\SentrySdk::getCurrentHub()->setSpan($this->transaction);
    }

    /**
     * Destructor: Stop the current Sentry transaction
     * @return void
     */
    public function __destruct() {
        SentrySpan :: finishAll();
        $this->transaction->finish();
    }
}

/**
 * Internal Sentry Span object implementation
 * This internal implementation principally permit to keep trace of new span parent
 * and list of started spans.
 */
class SentrySpan {

    /**
     * Keep trace of started Sentry spans
     * @var array<int,mixed>
     */
    private static $_started_spans = array();

    /**
     * The unique ID of the Sentry span
     * Note: internal ID used as key in self::$_started_spans
     * @var int|null
     */
    private $id = null;

    /**
     * The parent of the Sentry span
     * @var mixed
     */
    private $parent = null;

    /**
     * The context of the Sentry span
     * @var null|\Sentry\Tracing\SpanContext
     */
    private $context = null;

    /**
     * The Sentry span object
     * @var mixed
     */
    private $span = null;

    /**
     * Sentry span constructor
     * @param string|null $op The operation name
     * @param string|null $name The span name
     * @return void
     */
    public function __construct($op, $name) {
        $this -> parent = \Sentry\SentrySdk::getCurrentHub()->getSpan();
        // Check if we have a parent span (this is the case if we started a transaction earlier)
        if (is_null($this -> parent)) return;

        while (is_null($this -> id)) {
            $this -> id = rand();
            if (isset(self :: $_started_spans[$this -> id]))
                $this -> id = null;
        }
        $this -> context = new \Sentry\Tracing\SpanContext();
        $this -> context->setOp($op);
        $this -> context->setDescription($name);
        $this -> span = $this->parent->startChild($this -> context);

        // Set the current span to the span we just started
        \Sentry\SentrySdk::getCurrentHub()->setSpan($this -> span);

        self :: $_started_spans[$this -> id] = $this;
    }

    /**
     * Finish the span (if started)
     * @return void
     */
    public function finish() {
        if (!$this -> span) return;
        $this -> span -> finish();
        unset(self::$_started_spans[$this -> id]);
        \Sentry\SentrySdk::getCurrentHub()->setSpan($this -> parent);
    }

    /**
     * Finish all started spans
     * @see SentryTransaction::__destruct()
     * @return void
     */
    public static function finishAll() {
        foreach (array_reverse(self :: $_started_spans) as $id => $span)
            $span -> finish();
    }
}

# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab