18 Commits
Author SHA1 Message Date
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
mcndave 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
mcndave 29a780ac6a - Added a new view 2024-03-15 14:14:20 -04:00
mcndave e85925ace7 - Added a view to ease integration of JS client lib 2024-03-13 09:58:53 -04:00
mcndave 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
mcndave a552a6675e Merge branch 'master' of https://git.mcnd.ca/mcndave/negundo-client 2023-11-03 20:03:49 -04:00
mcndave 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
Dave Mc Nicoll 951c93fc63 - Task class was not properly called 2023-08-31 09:14:26 -04:00
Dave Mc Nicoll e75e7c42be Merge branch 'master' of https://git.mcnd.ca/mcndave/negundo-client 2023-04-13 15:47:18 -04:00
Dave Mc Nicoll fe37cfe246 - Fixed a missing parenthesis pair within the URL 2023-04-13 15:47:12 -04:00
19 changed files with 367 additions and 61 deletions

No files matched your search

+85
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);
}
}
+18
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
View File
@@ -0,0 +1,9 @@
<?php
return [
'lean' => [
'autoload' => [
'negundo.client'
]
],
];
+29
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,
],
],
],
],
];
+7 -7
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);
+11 -11
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 /* TransportInterface */ $transport;
protected Transport\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);
+18 -1
View File
@@ -8,5 +8,22 @@ 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;
}
}
}
}
+1 -1
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
{
+2 -2
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)
{
+26 -11
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,11 +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) {
foreach (\Negundo\Task::$instances as $instance) {
$instance->newReport($message, $title, $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) {
$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
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";
}
+72
View File
@@ -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;
}
}
+53 -15
View File
@@ -4,13 +4,15 @@ namespace Negundo\Client\Transport;
class Curl implements TransportInterface {
public $timeout = 1;
public $throwErrors = false;
public $timeout = 100;
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();
@@ -20,20 +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);
if ( ( false === curl_exec($ch) ) && $this->throwErrors ) {
$errno = curl_errno($ch);
$errors = array_filter([ curl_error($ch) , static::CURL_ERROR[$errno] ?? null ]);
throw new CurlException(implode(PHP_EOL, $errors), $errno);
$exec = curl_exec($ch);
$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 = [
0 => 'CURLE_OK',
1 => 'CURLE_UNSUPPORTED_PROTOCOL',
@@ -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.",
+1 -1
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,
+1 -1
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;
}
+2 -2
View File
@@ -6,7 +6,7 @@ use Negundo\Client\DataInterface;
class DumpHandler {
public /*DataInterface*/ $dataManipulator;
public ? DataInterface $dataManipulator;
public function __construct(DataInterface $dataManipulator = null)
{
@@ -30,7 +30,7 @@ class DumpHandler {
'file' => $trace['file'],
'line' => $trace['line'],
'title' => substr(strip_tags(trim(str_replace(['&nbsp;'], [''], $message))), 0, 255),
'url' => ( ( 'on' === $_SERVER['HTTPS'] ?? false ) ? 'https' : 'http' ) . '://' . ( $_SERVER['HTTP_HOST'] ?? "") . ( $_SERVER["REQUEST_URI"] ?? "" ),
'url' => ( ( 'on' === ($_SERVER['HTTPS'] ?? false) ) ? 'https' : 'http' ) . '://' . ( $_SERVER['HTTP_HOST'] ?? "") . ( $_SERVER["REQUEST_URI"] ?? "" ),
'backtrace' => json_encode($backtrace),
'source' => ( new SourceCodeFormatter() )->generateFromFile($trace['file'] === "unknown" ? "" : $trace['file'], $trace['line']),
'data' => [
+6 -4
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,10 +19,11 @@ 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 = [
'code' => $ex->getCode(),
'file' => $ex->getFile(),
@@ -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 -5
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,11 +45,11 @@ 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;
}
public function hash(array $content) : string
{
return md5(implode('-', [ $content['file'], $content['line'], $content['title'] ]));
+8
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>