Bugtracker

Tracking bugs like real men do

This article describes the basic ideas behind a php error/exception handler that sends errors from multiple pages to a single database.

There's nothing as unprofessional looking as a big SQL error all over your page, destroying your markup and possibly sending sensitive information straight to the intarweb. Luckily, there's an easy way to stop this: turn PHP's error reporting off altogeher and check the the error logs regularly to make sure nobody has had any errors. This is okay if you're the dedicated administrator of a single site (though it leaves your clients mystified), but what if you have ten? What if you have a hundred?

At work, we have a cooler solution. The idea is due to Bram (who has a lot of good ideas) and goes something like this:

This has a number of advantages:

Because this is PHP, sending the error report is gonna happen the script kiddie way: using a HTTP request with POST data attached (and proud of it!). This means you need some sort of script hosted somewhere online that enters this POST data into the database. Basically, the back end of a form without the actual form. And just like forms it'll have some security issues and be sensitive to spam. However, not many people will go out of their way to send random postdata to random url's, and without the form that's just what they'll need to do. If all else fails, add a password.

The main parts of the class are shown below:

<?php

class BugReporter
{
    private 
$projectId;

    private 
$successMsg "Error succesfully reported.";
    private 
$failMsg    "Automatic error reporting failed, please contact youremail@address.here";

    
/*
     * Constructs a new object of the type BugReporter using the given project ID
     */
    
public function __construct($projectId)
    {
        
set_error_handler(array($this"errorHandler"));
        
set_exception_handler(array($this"exceptionHandler"));
        
$this->projectId $projectId;
    }
    
    
/*
     * A message sending exception handler
     */    
    
public function exceptionHandler($exception)
    {
        
$post = array();
        
$post["type"]        = "auto";
        
$post["projectId"]   = $this->projectId;
        
$post["file"]        = $exception->getFile();
        
$post["line"]        = $exception->getLine();
        
$post["description"] = "Uncaught Exception (" $exception->getCode() . ") " $exception->getMessage();
        
$post["trace"]       = $exception->getTraceAsString();
        
        echo(
$post["description"]);        
        echo(
"<p>Sending error report...<br />");
        if (
$this->sendReport($post) === true)
            echo(
$this->successMsg);    
        else
            echo(
$this->failMsg);
        echo(
"</p>");
            
        exit();
    }
    
    
/*
     * A message sending error handler
     */
    
public function errorHandler($errno$errstr$errfile$errline)
    {
        if (
error_reporting() == 0) return true;
        
        
$post = array();
        
$post["type"]      = "auto";
        
$post["projectId"] = $this->projectId;
        
$post["file"]      = $errfile;
        
$post["line"]      = $errline;
        
        switch (
$errno) {
            case 
E_STRICT:
                return 
true
                break;                        
            case 
E_USER_ERROR:
                
$post["description"] = "Triggered error (FATAL)";
                break;    
            case 
E_USER_WARNING:
                 
$post["description"] = "Triggered error (WARNING)";
                break;    
            case 
E_USER_NOTICE:
                 
$post["description"] = "Triggered error (NOTICE)";
                break;    
            default:
                
$post["description"] = "Untriggered error";
                break;
        }
        
$post["description"] .= " (" $errno ") " $errstr;        
 
        echo 
$post["description"];
        echo(
"<p>Sending error report...<br />");
        if (
$this->sendReport($post) === true)
            echo(
$this->successMsg);    
        else
            echo(
$this->failMsg);
        echo(
"</p>");

        exit();
    }
    
    private function 
rawurlencode_callback($value$key)
    {
        return 
"$key=" rawurlencode($value);
    }
    
    private function 
sendReport($postData)
    {
        
$postvars array_map (array($this"rawurlencode_callback"), $postDataarray_keys($postData));
        
$postvars join("&"$postvars);
    
        do {
            @
$socket fsockopen("www.yoursitehere.com",80);
        } while (!
$socket);
        
        
// this should link to wherever you placed the bugtracking script
        
$out "POST http://www.yoursitehere.com/bugtracker.php HTTP/1.1\r\n";
        
        
// this is required (I think), but never looked at
        
$out .= "Host: www.example.com\r\n";
        
        
// Because we can
        
$out .= "User-Agent: BugTracker\r\n";

        
// And this is all copied from firefox.
        
$out .= "Accept: text/html;q=0.9,text/plain;q=0.8\r\n";
        
$out .= "Connection: close\r\n";
        
$out .= "Keep-Alive: 300\r\n";
        
$out .= "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n";
        
$out .= "Accept-Language: en-us,en;q=0.5\r\n";
        
$out .= "Content-Type: application/x-www-form-urlencoded\r\n";
        
        
// Except for this bit
        
$out .= "Content-length: " strlen($postvars) . "\r\n";
        
$out .= "\r\n";
        
$out .= $postvars;
        
fwrite($socket,$out);
        
        
$data "";
        while (!
feof($socket)) $data .= fread($socket1024);
        
        
fclose($socket);
        
        
// Parse retrieved data
        
$message "<p>Error reported</p>";
        return (
strrpos($data$message) !== false);
    }    
}
?>

In this scheme, every project gets its own project ID. For bigger projects you could use multiple id's for different parts of the project. The id is passed to the class's constructor, which registers itself as the default error and exception handler (two seperate tasks in PHP). The rest of code speaks for itself. Both error handlers create a type-specific message and pass it to the report-sending method, which opens a socket on the default HTTP port to send it's message. In true script-kiddie fashion the user-agent info from a firefox page request is copy-pasted to make sure any required fields are present in the HTTP request and we're off.

All that's needed now is a script that uses this post data to store the error reports: bugtracker.php. This is a fairly straightforward, but not very transparent script, that I might post sometime in the future. It's all pretty plain sailing from here.

A nice addition

Not all bugs are php. If you want to let users complain about your awful layout, your total lack of clarity or your general suckiness, add the following fields & methods to the bugreporter class:

<?php

    
private $FORM_NO_POSTDATA 0;
    private 
$FORM_NO_MESSAGE  1;
    private 
$FORM_MAIL_ERROR  2;
    private 
$FORM_SENT        3;
        
    
/*
     * Displays a user form for sending an error report, to be used with the
     *  checkPostData() function.
     */    
    
public function showUserForm($formStatus=false)
    {
        
$errorMessage "";
        if (
$formStatus !== false) {
            if (
$formStatus == $this->FORM_NO_MESSAGE)
                
$errorMessage "No message specified";
            if (
$formStatus == $this->FORM_MAIL_ERROR)
                
$errorMessage "Error sending email";
            if (
$formStatus == $this->FORM_SENT) {
                echo 
"<p>Email sent</p>";
                return;
            }
        }
        if (!empty(
$errorMessage))
            
$errorMessage "<span id=\"BugSenderErrorMessage\">$errorMessage</span><br />";
        
        
$sender  = (isset($_POST["sender"])) ? trim($_POST["sender"]) : "";
        
$message = (isset($_POST["message"])) ? trim($_POST["message"]) : "";
        
?>
        <form id="BugSenderForm" action="" method="post">
            <?=$errorMessage?>
            <label for="BugSenderEmail">Email:</label><br />
            <input name="sender" id="BugSenderEmail" value="<?=$sender?>" /><br />
            <label for="BugSenderMessage">Message:</label><br />
            <textarea name="message" id="BugSenderMessage"><?=$message?></textarea><br />
            <input type="submit" name="submitButton" id="BugSenderSubmitButton" text="Submit" />
        </form>
        <?
    
}
    
    
/*
     * Catches POST data sent by a user form and uses it to send an error report.
     * The following fields are required:
     *   sender       : the email address of the sender (optional)
     *   message      : the message to send
     *   submitButton : used to submit the data
     */    
    
public function checkPostData()
    {
        if (!isset(
$_POST["submitButton"])) return $this->FORM_NO_POSTDATA;
        
        
$post = array();
        
$post["projectId"] = $this->projectId;
        
$post["type"]      = "user";
        
        
$post["sender"] ="";
        if (isset(
$_POST["sender"])) $post["sender"] = trim($_POST["sender"]);
        if (empty(
$post["sender"])) $post["sender"] = "unknown";
        
        
$post["message"] = "";
        if (isset(
$_POST["message"])) $post["message"] = $_POST["message"];
        if (empty(
$post["message"])) return $this->FORM_NO_MESSAGE;
        
        
// Send message        
        
if ($this->sendReport($post) === true) {
            return 
$this->FORM_SENT;
        } else {
            return 
$this->FORM_MAIL_ERROR;
        }
    }

?>

And that's all. It's a simple idea, but it's working quite well for us. There's some plans to make it into a free service, where we host the bug collecting script, and users are free to use to bugreporter class to keep track of their errors. Obviously, this means some security measures will need to be taken, as well as some ways to guarantee your privacy. More on this when it happens.

Jul 9th, 2008

Comments

Michael wrote:

As a side note: there's no reason to stick to php (for the reporting script). I'm writing a Java version right now :)

Nov 28th, 2008

Post your comments here

If you wish to add code to your comment you can use code tags, like this: <code class="php">yourCodeHere</code>.
Quite a large number of languages are supported, although I can't guarantee it'll be pretty. Inside the code tags you can use any characters except for the string "</code>".