Compare commits
12
Commits
a552a6675e
..
1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b5a1bcf97 | ||
|
|
6227321045 | ||
|
|
38df94fc2a | ||
|
|
36d4a6971e | ||
|
|
3474cc4840 | ||
|
|
5a737c06c4 | ||
|
|
586f2344df | ||
|
|
29a780ac6a | ||
|
|
e85925ace7 | ||
|
|
ef43c7831f | ||
|
|
3590993925 | ||
|
|
d94ee83230 |
No files matched your search
@@ -0,0 +1,85 @@
|
||||
class ErrorHandler {
|
||||
constructor(options) {
|
||||
if (!options) throw new Error("Options not provided");
|
||||
if (!options.url) throw new Error("URL is required");
|
||||
if (!options.apikey) throw new Error("API key is required");
|
||||
|
||||
this.url = options.url;
|
||||
this.apikey = options.apikey;
|
||||
this.catchError();
|
||||
}
|
||||
|
||||
getSourceCode(lineNumber) {
|
||||
return this.getInlineSourceCode(lineNumber) || this.getFullSourceWithHighlight(lineNumber);
|
||||
}
|
||||
|
||||
getInlineSourceCode(lineNumber) {
|
||||
const scripts = document.querySelectorAll('script');
|
||||
for (let script of scripts) {
|
||||
if (!script.src) {
|
||||
const scriptLines = script.textContent.split("\n");
|
||||
const scriptPosition = this.getErrorPosition(script);
|
||||
if (lineNumber >= scriptPosition.startLine && lineNumber <= scriptPosition.endLine) {
|
||||
return this.highlightSource(scriptLines, lineNumber - scriptPosition.startLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getFullSourceWithHighlight(lineNumber) {
|
||||
const lines = document.documentElement.outerHTML.split("\n");
|
||||
return this.highlightSource(lines, lineNumber - 1);
|
||||
}
|
||||
|
||||
highlightSource(lines, lineNumber) {
|
||||
const start = Math.max(lineNumber - 2, 0);
|
||||
const end = Math.min(lineNumber + 2, lines.length - 1);
|
||||
lines[lineNumber] = '>> ' + lines[lineNumber] + ' <<';
|
||||
return lines.slice(start, end + 1).join("\n");
|
||||
}
|
||||
|
||||
getErrorPosition(script) {
|
||||
let totalLines = 0;
|
||||
let element = script.previousElementSibling;
|
||||
while (element) {
|
||||
totalLines += (element.outerHTML || element.textContent).split("\n").length;
|
||||
element = element.previousElementSibling;
|
||||
}
|
||||
return {
|
||||
startLine: totalLines + 1,
|
||||
endLine: totalLines + script.textContent.split("\n").length
|
||||
};
|
||||
}
|
||||
|
||||
catchError() {
|
||||
window.onerror = (message, url, lineNumber, column, error) => {
|
||||
this.reportError(message, url, lineNumber, column, error.stack, 'JavaScript Error');
|
||||
return false;
|
||||
};
|
||||
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
this.reportError(event.reason.toString(), document.location.href, 0, 0, event.reason.stack, 'Promise Rejection');
|
||||
});
|
||||
}
|
||||
|
||||
reportError(message, url, lineNumber, column, stack, type = 'JavaScript Error') {
|
||||
fetch(this.url ? `${location.protocol}//${this.url}/${this.apikey}` : window.location.href, {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Accept': "application/json",
|
||||
'Content-Type': "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type,
|
||||
message,
|
||||
url,
|
||||
lineNumber,
|
||||
column,
|
||||
stack,
|
||||
location: window.location.href,
|
||||
source: this.getSourceCode(lineNumber)
|
||||
})
|
||||
}).then(response => response.json()).then(console.info);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@
|
||||
"autoload": {
|
||||
"definitions" : [
|
||||
"meta/negundo.php"
|
||||
],
|
||||
"config": [
|
||||
"meta/config.php"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'lean' => [
|
||||
'autoload' => [
|
||||
'negundo.client'
|
||||
]
|
||||
],
|
||||
];
|
||||
@@ -9,4 +9,21 @@ return [
|
||||
NegundoMiddleware::class => autowire(NegundoMiddleware::class),
|
||||
Dump::class => autowire(Dump::class),
|
||||
Task::class => autowire(Task::class),
|
||||
|
||||
'negundo.client' => [
|
||||
'picea' => [
|
||||
'asset' => [
|
||||
[
|
||||
'path' => implode(DIRECTORY_SEPARATOR, [ dirname(__DIR__), "asset", '' ]),
|
||||
'order' => 10
|
||||
],
|
||||
],
|
||||
'view' => [
|
||||
[
|
||||
'path' => implode(DIRECTORY_SEPARATOR, [ dirname(__DIR__), "view", '' ]),
|
||||
'order' => 99,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
+1
-1
@@ -13,7 +13,7 @@ abstract class Handler {
|
||||
|
||||
public bool $registerFatalErrorHandler = true;
|
||||
|
||||
protected Closure $callback;
|
||||
protected ? Closure $callback;
|
||||
|
||||
protected Transport\TransportInterface $transport;
|
||||
|
||||
|
||||
@@ -9,4 +9,21 @@ class NativeHandler extends Handler {
|
||||
return $this->exceptionHandler->extractExceptionData($ex, $_SERVER, $_POST);
|
||||
}
|
||||
|
||||
public function process(callable $callback) : mixed
|
||||
{
|
||||
try {
|
||||
return $callback();
|
||||
}
|
||||
catch (\Throwable $ex)
|
||||
{
|
||||
$this->pushData($ex);
|
||||
|
||||
if ( $this->callback ?? false ) {
|
||||
return call_user_func_array($this->callback, [ $ex ] );
|
||||
}
|
||||
else {
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-4
@@ -25,9 +25,9 @@ namespace Negundo\Client {
|
||||
static::$instances[] = $this;
|
||||
}
|
||||
|
||||
public function newReport(string $message, ? string $title = null, ? array $data = []) : object|null|bool
|
||||
public function newReport(string $message, ? string $title = null, ? array $data = [], ? array $events = []) : object|null|bool
|
||||
{
|
||||
$report = $this->taskHandler->sendReport($message, $title, $data);
|
||||
$report = $this->taskHandler->sendReport($message, $title, $data, $events);
|
||||
|
||||
// Make sure not to spam the server if an ErrorMessage or Exception was already sent (like inside a loop)
|
||||
$dumpHash = $this->taskHandler->hash($report);
|
||||
@@ -44,15 +44,26 @@ namespace Negundo\Client {
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
use Negundo\Client\Task\StatusEnum;
|
||||
use Negundo\Client\Task\TaskReport;
|
||||
|
||||
if (! function_exists('ntask') ) {
|
||||
function ntask(string $message, ? string $title = null, ? array $data = null) {
|
||||
function ntask(string $message, ? string $title = null, ? array $data = null, ? Negundo\Client\Task\StatusEnum $status = null, array $events = []) {
|
||||
foreach (\Negundo\Client\Task::$instances as $instance) {
|
||||
$sent = $instance->newReport($message, $title, $data);
|
||||
$sent = $instance->newReport($message, $title, $data, $events);
|
||||
|
||||
if (! $sent ) {
|
||||
throw new \Exception(sprintf('Could not send report titled `%s`.', $title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function nreport(TaskReport $report)
|
||||
{
|
||||
if ($report->status !== StatusEnum::NothingToDo || $report->getEvents()) {
|
||||
ntask($report->getMessage(), $report->getTitle(), $report->getData(), $report->getStatus(), $report->getEvents());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Negundo\Client\Task;
|
||||
|
||||
enum StatusEnum : string
|
||||
{
|
||||
case Failed = "failed";
|
||||
case Warning = "warning";
|
||||
case Completed = "completed";
|
||||
case Empty = "empty";
|
||||
case NothingToDo = "nothing-to-do";
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Negundo\Client\Task;
|
||||
|
||||
class TaskReport
|
||||
{
|
||||
protected array $events = [];
|
||||
|
||||
public function __construct(
|
||||
public string|array $message,
|
||||
public string $title,
|
||||
public ? StatusEnum $status = null,
|
||||
public array $data = [],
|
||||
) {}
|
||||
|
||||
public function addData(string $name, array $data) : static
|
||||
{
|
||||
$this->data[$name][] = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addEvent(string $key, ? StatusEnum $status = null, array $data = []) : static
|
||||
{
|
||||
$this->events[] = [
|
||||
'key' => $key,
|
||||
'status' => $status ? $status->value : null,
|
||||
'data' => $data
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addMessage(string $message) : static
|
||||
{
|
||||
if ( is_string($this->message) ) {
|
||||
$this->message = [
|
||||
$this->message, $message
|
||||
];
|
||||
}
|
||||
else {
|
||||
$this->message[] = $message;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMessage() : string
|
||||
{
|
||||
return implode(PHP_EOL, (array) $this->message);
|
||||
}
|
||||
|
||||
public function getTitle() : string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getData() : array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function getEvents() : array
|
||||
{
|
||||
return $this->events;
|
||||
}
|
||||
|
||||
public function getStatus() : StatusEnum
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
}
|
||||
+43
-7
@@ -4,9 +4,11 @@ namespace Negundo\Client\Transport;
|
||||
|
||||
class Curl implements TransportInterface {
|
||||
|
||||
public $timeout = 1;
|
||||
public $timeout = 100;
|
||||
|
||||
public $throwErrors = false;
|
||||
public $throwErrors = true;
|
||||
|
||||
public $verifySsl = false;
|
||||
|
||||
public $headers = [];
|
||||
|
||||
@@ -20,22 +22,56 @@ class Curl implements TransportInterface {
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data, '', '&'));
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->timeout * 200);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifySsl);
|
||||
|
||||
$exec = curl_exec($ch);
|
||||
|
||||
if ( ( false === $exec ) && $this->throwErrors ) {
|
||||
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
|
||||
if ($this->throwErrors) {
|
||||
if ( false === $exec) {
|
||||
$errno = curl_errno($ch);
|
||||
$errors = array_filter([ curl_error($ch) , static::CURL_ERROR[$errno] ?? null ]);
|
||||
curl_close($ch);
|
||||
|
||||
throw new CurlException(implode(PHP_EOL, $errors), $errno);
|
||||
throw new CurlException(implode(PHP_EOL, array_filter([ curl_error($ch) , static::CURL_ERROR[$errno] ?? null ])), $errno);
|
||||
}
|
||||
elseif ($code >= 400) {
|
||||
throw new \Exception(sprintf("HTTP code received : $code with page content : %s", $exec));
|
||||
}
|
||||
}
|
||||
if ($_GET['dev'] ?? false) {
|
||||
echo($exec);
|
||||
die();
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return $exec;
|
||||
}
|
||||
|
||||
public function get(string $url) : ? object
|
||||
{
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers );
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->timeout * 200);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifySsl);
|
||||
$execute = curl_exec($ch);
|
||||
|
||||
if ( ( false === $execute ) && $this->throwErrors ) {
|
||||
$errno = curl_errno($ch);
|
||||
|
||||
curl_close($ch);
|
||||
throw new CurlException(implode(PHP_EOL, array_filter([ curl_error($ch) , static::CURL_ERROR[$errno] ?? null ])), $errno);
|
||||
}
|
||||
else {
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
return $execute ? (object) $execute : null;
|
||||
}
|
||||
|
||||
const CURL_ERROR = [
|
||||
0 => 'CURLE_OK',
|
||||
1 => 'CURLE_UNSUPPORTED_PROTOCOL',
|
||||
|
||||
@@ -6,7 +6,7 @@ use Negundo\Client\DataInterface;
|
||||
|
||||
class DumpHandler {
|
||||
|
||||
public DataInterface $dataManipulator;
|
||||
public ? DataInterface $dataManipulator;
|
||||
|
||||
public function __construct(DataInterface $dataManipulator = null)
|
||||
{
|
||||
|
||||
@@ -19,8 +19,9 @@ class ExceptionHandler {
|
||||
'HTTPS' => $serverData['HTTPS'] ?? false,
|
||||
'HTTP_HOST' => $serverData['HTTP_HOST'] ?? "",
|
||||
'REQUEST_URI' => $serverData['REQUEST_URI'] ?? "",
|
||||
'HTTP_USER_AGENT' => $serverData['HTTP_USER_AGENT'] ?? null,
|
||||
'HTTP_USER_AGENT' => $serverData['HTTP_USER_AGENT'] ?? "# UNKNOWN #",
|
||||
'REMOTE_ADDR' => $serverData['REMOTE_ADDR'] ?? null,
|
||||
'REQUEST_METHOD' => $serverData['HTTP_X_HTTP_METHOD'] ?? $serverData['REQUEST_METHOD'] ?? null,
|
||||
];
|
||||
|
||||
$post = [
|
||||
@@ -30,11 +31,12 @@ class ExceptionHandler {
|
||||
'type' => $ex::class,
|
||||
'message' => $ex->getMessage(),
|
||||
'url' => ( ( 'on' === $serverData['HTTPS'] ) ? 'https' : 'http' ) . '://' . $serverData['HTTP_HOST'] . $serverData["REQUEST_URI"],
|
||||
'http_method' => $serverData['REQUEST_METHOD'],
|
||||
'backtrace' => json_encode($ex->getTrace()),
|
||||
'backtrace_string' => $ex->getTraceAsString(),
|
||||
'source' => ( new SourceCodeFormatter() )->generateFromException($ex),
|
||||
'hits' => [
|
||||
'user_agent' => $serverData['HTTP_USER_AGENT'] ?? "???",
|
||||
'user_agent' => $serverData['HTTP_USER_AGENT'],
|
||||
'sent_at' => date('Y-m-d H:i:s'),
|
||||
'request_body_vars' => array_keys($postData),
|
||||
'remote_addr' => $serverData['REMOTE_ADDR'],
|
||||
|
||||
@@ -6,14 +6,14 @@ use Negundo\Client\DataInterface;
|
||||
|
||||
class TaskHandler {
|
||||
|
||||
public DataInterface $dataManipulator;
|
||||
public ? DataInterface $dataManipulator;
|
||||
|
||||
public function __construct(DataInterface $dataManipulator = null)
|
||||
{
|
||||
$this->dataManipulator = $dataManipulator;
|
||||
}
|
||||
|
||||
public function sendReport(string $message, ? string $title = null, array $data = []) : array
|
||||
public function sendReport(string $message, ? string $title = null, array $data = [], array $events = []) : array
|
||||
{
|
||||
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
array_shift($backtrace);
|
||||
@@ -36,6 +36,7 @@ class TaskHandler {
|
||||
'request_body' => json_encode($_POST),
|
||||
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
|
||||
],
|
||||
'events' => $events,
|
||||
];
|
||||
|
||||
if ( $this->dataManipulator ?? false ) {
|
||||
@@ -44,7 +45,7 @@ class TaskHandler {
|
||||
$this->dataManipulator->run($post);
|
||||
}
|
||||
|
||||
$post['data'] = json_encode($post['data'] ?? null);
|
||||
$post['data'] = json_encode($post['data'] ?? null, JSON_INVALID_UTF8_IGNORE);
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<script src="{% asset 'static/negundo/js/debug.js' %}"></script>
|
||||
|
||||
<script>
|
||||
new ErrorHandler({
|
||||
url: "{{ getenv('NEGUNDO_SERVER') }}/bug/client/report",
|
||||
apikey: "{{ getenv('NEGUNDO_HASH') }}"
|
||||
});
|
||||
</script>
|
||||
Reference in new issue
Block a user