Compare commits

..

No commits in common. "master" and "v1.0.0" have entirely different histories.

19 changed files with 59 additions and 377 deletions

View File

@ -1,85 +0,0 @@
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);
}
}

View File

@ -2,7 +2,6 @@
"name": "mcnd/negundo-client",
"description": "Negundo client which allow sending dump(), error and tasks reports",
"keywords": ["negundo","dev","debug","psr15","middleware"],
"type": "library",
"license": "MIT",
"authors": [
{
@ -14,22 +13,5 @@
"psr-4": {
"Negundo\\Client\\": "src/"
}
},
"require": {
"php": "^8.2",
"ext-curl": "*",
"ext-json": "*"
},
"extra" : {
"lean" : {
"autoload": {
"definitions" : [
"meta/negundo.php"
],
"config": [
"meta/config.php"
]
}
}
}
}

View File

@ -1,9 +0,0 @@
<?php
return [
'lean' => [
'autoload' => [
'negundo.client'
]
],
];

View File

@ -1,29 +0,0 @@
<?php
use function DI\autowire, DI\create, DI\get;
use Negundo\Client\{ SoftwareConfig, Dump, Task, NegundoMiddleware };
return [
SoftwareConfig::class => create(SoftwareConfig::class)->constructor(getenv('NEGUNDO_HASH'), getenv('NEGUNDO_SERVER')),
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,
],
],
],
],
];

View File

@ -2,17 +2,17 @@
namespace Negundo\Client {
class Dump {
public static array $instances = [];
public static /* array */ $instances = [];
# public string $serverUrl = "http://dev.cslsj.qc.ca/debug/dump/report/%s";
# public /*string*/ $serverUrl = "http://dev.cslsj.qc.ca/debug/dump/report/%s";
protected SoftwareConfig $config;
protected /* SoftwareConfig */ $config;
protected array $sent = [];
protected /*array*/ $sent = [];
protected Transport\TransportInterface $transport;
protected /* TransportInterface */ $transport;
protected Util\DumpHandler $dumpHandler;
protected /* Util\DumpHandler */ $dumpHandler;
public function __construct(SoftwareConfig $config, ? DataInterface $dataManipulator = null, Transport\TransportInterface $transport = null)
{
@ -22,7 +22,7 @@ namespace Negundo\Client {
static::$instances[] = $this;
}
public function pushData(...$content) : object|null|bool
public function pushData(...$content) : ? object
{
$data = $this->dumpHandler->dumpData(...$content);

View File

@ -5,21 +5,21 @@ namespace Negundo\Client;
use Closure;
abstract class Handler {
protected SoftwareConfig $config;
protected /* SoftwareConfig */ $config;
public bool $registerErrorHandler = true;
public /* bool */ $registerErrorHandler = true;
public bool $registerExceptionHandler = true;
public /* bool */ $registerExceptionHandler = true;
public bool $registerFatalErrorHandler = true;
public /* bool */ $registerFatalErrorHandler = true;
protected ? Closure $callback;
protected /*Closure*/ $callback;
protected Transport\TransportInterface $transport;
protected /*array*/ $sent = [];
protected Util\ExceptionHandler $exceptionHandler;
protected /* TransportInterface */ $transport;
protected array $sent = [];
protected /* Util\ExceptionHandler */ $exceptionHandler;
public abstract function handleException(\Throwable $ex) : array;
@ -33,7 +33,7 @@ abstract class Handler {
$this->registerHandlers();
}
public function registerHandlers() : void
public function registerHandlers()
{
$this->registerExceptionHandler && set_exception_handler(function(\Throwable $ex) {
$this->pushData($ex);
@ -55,7 +55,7 @@ abstract class Handler {
});
}
public function pushData(\Throwable $ex) : null|object|bool
public function pushData(\Throwable $ex) : ? object
{
// Make sure not to spam the server if an ErrorMessage or Exception was already sent (like inside a loop)
$exceptionHash = $this->exceptionHandler->hash($ex);

View File

@ -9,21 +9,4 @@ 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;
}
}
}
}

View File

@ -12,7 +12,7 @@ class NegundoMiddleware extends Handler implements MiddlewareInterface {
protected ServerRequestInterface $request;
public bool $registerExceptionHandler = false;
public $registerExceptionHandler = false;
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{

View File

@ -4,9 +4,9 @@ namespace Negundo\Client;
class SoftwareConfig
{
public string $serverUrl;
public /*string*/ $serverUrl;
public string $softwareHash;
public /* string */ $softwareHash;
public function __construct(string $softwareHash, string $serverUrl)
{

View File

@ -4,15 +4,15 @@ namespace Negundo\Client {
class Task {
public static array $instances = [];
public static /* array */ $instances = [];
protected array $sent = [];
protected /*array*/ $sent = [];
protected Transport\TransportInterface $transport;
protected /* TransportInterface */ $transport;
protected Util\TaskHandler $taskHandler;
protected /* Util\TaskHandler */ $taskHandler;
protected SoftwareConfig $config;
protected /* SoftwareConfig */ $config;
public function __construct(SoftwareConfig $config, ? DataInterface $dataManipulator = null, Transport\TransportInterface $transport = null)
{
@ -25,9 +25,9 @@ namespace Negundo\Client {
static::$instances[] = $this;
}
public function newReport(string $message, ? string $title = null, ? array $data = [], ? array $events = []) : object|null|bool
public function newReport(string $message, ? string $title = null, ? array $data = []) : ? object
{
$report = $this->taskHandler->sendReport($message, $title, $data, $events);
$report = $this->taskHandler->sendReport($message, $title, $data);
// 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,25 +44,10 @@ 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, ? Negundo\Client\Task\StatusEnum $status = null, array $events = []) {
function ntask(string $message, ? string $title = null, ? array $data) {
foreach (\Negundo\Client\Task::$instances as $instance) {
$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());
$instance->newReport($message, $title, $data);
}
}
}

View File

@ -1,12 +0,0 @@
<?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";
}

View File

@ -1,84 +0,0 @@
<?php
namespace Negundo\Client\Task;
class TaskReport implements \JsonSerializable
{
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(?StatusEnum $filterType = null) : array
{
return $filterType ? array_filter($this->events, fn($e) => StatusEnum::tryFrom($e['status']) === $filterType) : $this->events;
}
public function getStatus() : StatusEnum
{
return $this->status;
}
public function jsonSerialize(): mixed
{
return [
'title' => $this->getTitle(),
'message' => $this->getMessage(),
'status' => $this->getStatus(),
'events' => $this->getEvents(),
'data' => $this->getData(),
];
}
}

View File

@ -4,15 +4,13 @@ namespace Negundo\Client\Transport;
class Curl implements TransportInterface {
public $timeout = 4;
public $timeout = 1;
public $throwErrors = true;
public $verifySsl = false;
public $throwErrors = false;
public $headers = [];
public function push(string $url, array $data) : object|null|bool
public function push(string $url, array $data) : ? object
{
$ch = curl_init();
@ -21,55 +19,19 @@ class Curl implements TransportInterface {
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers );
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data, '', '&'));
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifySsl);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->timeout * 200);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$exec = curl_exec($ch);
if ( ( false === curl_exec($ch) ) && $this->throwErrors ) {
$errno = curl_errno($ch);
$errors = array_filter([ curl_error($ch) , static::CURL_ERROR[$errno] ?? null ]);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($this->throwErrors) {
if ( false === $exec) {
$errno = curl_errno($ch);
curl_close($ch);
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();
throw new CurlException(implode(PHP_EOL, $errors), $errno);
}
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;
return null;
}
const CURL_ERROR = [
@ -197,7 +159,7 @@ class Curl implements TransportInterface {
35 => "A problem occurred somewhere in the SSL/TLS handshake. You really want the error buffer and read the message there as it pinpoints the problem slightly more. Could be certificates (file formats, paths, permissions), passwords, and others.",
36 => "The download could not be resumed because the specified offset was out of the file boundary.",
37 => "A file given with FILE:// couldn't be opened. Most likely because the file path doesn't identify an existing file. Did you check file permissions? ",
38 => "LDAP cannot bind. LDAP bind operation failed.",
35 => "LDAP cannot bind. LDAP bind operation failed.",
39 => "LDAP search failed.",
41 => "Function not found. A required zlib function was not found.",
42 => "Aborted by callback. A callback returned \"abort\" to libcurl.",

View File

@ -12,7 +12,7 @@ class GuzzleClient implements TransportInterface {
public array $headers = [];
public function push(string $url, array $data) : object|null|bool
public function push(string $url, array $data) : ? object
{
return ( new Client([
'timeout' => $this->timeout,

View File

@ -3,5 +3,5 @@
namespace Negundo\Client\Transport;
interface TransportInterface {
public function push(string $url, array $data) : object|null|bool;
public function push(string $url, array $data) : ? object;
}

View File

@ -6,7 +6,7 @@ use Negundo\Client\DataInterface;
class DumpHandler {
public ? DataInterface $dataManipulator;
public /*DataInterface*/ $dataManipulator;
public function __construct(DataInterface $dataManipulator = null)
{

View File

@ -6,7 +6,7 @@ use Negundo\Client\DataInterface;
class ExceptionHandler {
public ? DataInterface $dataManipulator;
public /*DataInterface*/ $dataManipulator;
public function __construct(DataInterface $dataManipulator = null)
{
@ -19,9 +19,8 @@ class ExceptionHandler {
'HTTPS' => $serverData['HTTPS'] ?? false,
'HTTP_HOST' => $serverData['HTTP_HOST'] ?? "",
'REQUEST_URI' => $serverData['REQUEST_URI'] ?? "",
'HTTP_USER_AGENT' => $serverData['HTTP_USER_AGENT'] ?? "# UNKNOWN #",
'HTTP_USER_AGENT' => $serverData['HTTP_USER_AGENT'] ?? null,
'REMOTE_ADDR' => $serverData['REMOTE_ADDR'] ?? null,
'REQUEST_METHOD' => $serverData['HTTP_X_HTTP_METHOD'] ?? $serverData['REQUEST_METHOD'] ?? null,
];
$post = [
@ -31,12 +30,11 @@ 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'],

View File

@ -6,22 +6,22 @@ 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 $events = []) : array
public function sendReport(string $message, ? string $title = null, array $data = []) : array
{
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
array_shift($backtrace);
array_shift($backtrace);
$trace = $backtrace[0] ?? [
"file" => "unknown",
"line" => -1,
"file" => "unknown",
];
$post = [
@ -36,7 +36,6 @@ class TaskHandler {
'request_body' => json_encode($_POST),
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
],
'events' => $events,
];
if ( $this->dataManipulator ?? false ) {
@ -45,7 +44,7 @@ class TaskHandler {
$this->dataManipulator->run($post);
}
$post['data'] = json_encode($post['data'] ?? null, JSON_INVALID_UTF8_IGNORE);
$post['data'] = json_encode($post['data'] ?? null);
return $post;
}

View File

@ -1,8 +0,0 @@
<script src="{% asset 'static/negundo/js/debug.js' %}"></script>
<script>
new ErrorHandler({
url: "{{ getenv('NEGUNDO_SERVER') }}/bug/client/report",
apikey: "{{ getenv('NEGUNDO_HASH') }}"
});
</script>