Compare commits

...

16 Commits

Author SHA1 Message Date
Dave Mc Nicoll
8de8aafa84 - WIP on tasks 2025-02-04 08:36:14 -05:00
Dave Mc Nicoll
7b5a1bcf97 - WIP on TaskReport 2024-11-08 09:14:56 -05:00
Dave Mc Nicoll
6227321045 Merge branch 'master' of https://git.mcnd.ca/mcndave/negundo-client 2024-11-01 16:10:30 -04:00
Dave Mc Nicoll
38df94fc2a - WIP on a new TaksReport object 2024-11-01 16:10:18 -04:00
Dave Mc Nicoll
36d4a6971e - WIP on http method from which a request is sent 2024-10-14 12:57:58 +00:00
Dave Mc Nicoll
3474cc4840 - Added Http Method 2024-10-10 19:37:00 +00:00
5a737c06c4 - WIP on Task scheduling interface 2024-04-22 13:55:36 +00:00
Nicolas Demers
586f2344df update bug catcher on client side 2024-04-18 08:50:24 -04:00
29a780ac6a - Added a new view 2024-03-15 14:14:20 -04:00
e85925ace7 - Added a view to ease integration of JS client lib 2024-03-13 09:58:53 -04:00
ef43c7831f - Added JS debug lib 2024-03-13 09:08:54 -04:00
Dave Mc Nicoll
3590993925 - Added a process to native handler 2024-03-01 16:48:01 +00:00
Dave Mc Nicoll
d94ee83230 - Fixed nullable errors after forced typing 2023-11-07 11:12:20 -05:00
a552a6675e Merge branch 'master' of https://git.mcnd.ca/mcndave/negundo-client 2023-11-03 20:03:49 -04:00
5e44f36dbc - Forced variable typing 2023-11-03 20:03:34 -04:00
Dave Mc Nicoll
4484f1c997 - Added some more type checking 2023-11-01 11:29:08 -04:00
19 changed files with 378 additions and 60 deletions

85
asset/negundo/js/debug.js Normal file
View File

@ -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);
}
}

View File

@ -2,6 +2,7 @@
"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": [
{
@ -13,5 +14,22 @@
"psr-4": {
"Negundo\\Client\\": "src/"
}
},
"require": {
"php": "^8.2",
"ext-curl": "*",
"ext-json": "*"
},
"extra" : {
"lean" : {
"autoload": {
"definitions" : [
"meta/negundo.php"
],
"config": [
"meta/config.php"
]
}
}
}
}

9
meta/config.php Normal file
View File

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

29
meta/negundo.php Normal file
View File

@ -0,0 +1,29 @@
<?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 /* TransportInterface */ $transport;
protected Transport\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
public function pushData(...$content) : object|null|bool
{
$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 /*array*/ $sent = [];
protected Transport\TransportInterface $transport;
protected /* TransportInterface */ $transport;
protected Util\ExceptionHandler $exceptionHandler;
protected /* Util\ExceptionHandler */ $exceptionHandler;
protected array $sent = [];
public abstract function handleException(\Throwable $ex) : array;
@ -33,7 +33,7 @@ abstract class Handler {
$this->registerHandlers();
}
public function registerHandlers()
public function registerHandlers() : void
{
$this->registerExceptionHandler && set_exception_handler(function(\Throwable $ex) {
$this->pushData($ex);
@ -55,7 +55,7 @@ abstract class Handler {
});
}
public function pushData(\Throwable $ex) : ? object
public function pushData(\Throwable $ex) : null|object|bool
{
// 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,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;
}
}
}
}

View File

@ -12,7 +12,7 @@ class NegundoMiddleware extends Handler implements MiddlewareInterface {
protected ServerRequestInterface $request;
public $registerExceptionHandler = false;
public bool $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 /* TransportInterface */ $transport;
protected Transport\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 = []) : ? object
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,10 +44,25 @@ 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) {
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) {
$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());
}
}
}

12
src/Task/StatusEnum.php Normal file
View File

@ -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";
}

84
src/Task/TaskReport.php Normal file
View File

@ -0,0 +1,84 @@
<?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,13 +4,15 @@ namespace Negundo\Client\Transport;
class Curl implements TransportInterface {
public $timeout = 1;
public $timeout = 4;
public $throwErrors = false;
public $throwErrors = true;
public $verifySsl = false;
public $headers = [];
public function push(string $url, array $data) : ? object
public function push(string $url, array $data) : object|null|bool
{
$ch = curl_init();
@ -19,19 +21,55 @@ 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_MS, $this->timeout * 200);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifySsl);
if ( ( false === curl_exec($ch) ) && $this->throwErrors ) {
$errno = curl_errno($ch);
$errors = array_filter([ curl_error($ch) , static::CURL_ERROR[$errno] ?? null ]);
$exec = curl_exec($ch);
throw new CurlException(implode(PHP_EOL, $errors), $errno);
$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();
}
curl_close($ch);
return null;
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 = [
@ -159,7 +197,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? ",
35 => "LDAP cannot bind. LDAP bind operation failed.",
38 => "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
public function push(string $url, array $data) : object|null|bool
{
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;
public function push(string $url, array $data) : object|null|bool;
}

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,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'],

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
public function sendReport(string $message, ? string $title = null, array $data = [], array $events = []) : array
{
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
array_shift($backtrace);
array_shift($backtrace);
$trace = $backtrace[0] ?? [
"line" => -1,
"file" => "unknown",
"line" => -1,
];
$post = [
@ -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;
}

View File

@ -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>