24 Commits
Author SHA1 Message Date
Dave Mc Nicoll 72e50fbe50 - Added getenvonce() method which was missing 2026-07-14 20:13:20 +00:00
mcndave 27bfdd5902 - Fixed client bug in js source and also on negundo server side 2026-07-14 19:07:31 +00:00
mcndave 063aba6040 - Request method needed to be lowerscase and specified arguments in constructor) 2026-07-07 19:56:14 +00:00
mcndave 956ccbba1a - Added new Picea's Di extension for client_bugs 2026-07-06 16:29:50 +00:00
Dave Mc Nicoll af0e9f291d - Fixed some mistyped variables 2026-06-17 21:00:58 +00:00
Dev bc8d831b6c - Added noop if no software data were given 2026-06-09 19:18:42 +00:00
Dave Mc Nicoll 2df4ea15e2 - Removed curl_close() as it is deprecated 2026-06-01 15:12:34 +00:00
Dave Mc Nicoll 0694b50eb6 - Fixed null value declaration 2026-05-22 19:54:16 +00:00
Dave Mc Nicoll 72f5341dff - Fixed ndump() source code 2026-05-22 18:49:22 +00:00
Dave Mc Nicoll b88602a98b - Fixed double slash in client url 2025-07-18 13:02:54 +00:00
mcndave f1e9dddf5e - Minors fixes 2025-07-17 18:36:38 +00:00
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
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
17 changed files with 383 additions and 82 deletions

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
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();
}
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) => {
let type = 'JavaScript Error';
if (message.indexOf(':')) {
type = message.substring(0, message.indexOf(':'));
message = message.substring(message.indexOf(':') + 1).trim();
}
this.reportError(message, url, lineNumber, column, error.stack, type);
return false;
};
window.addEventListener('unhandledrejection', event => {
this.reportError(event.reason.toString(), document.location.href, 0, 0, event.reason.stack, 'Promise Rejection');
});
this.createErrorAgain();
}
async reportError(message, url, line, column, stack, type = 'JavaScript Error') {
await new Promise(r => setTimeout(r, 2000));
fetch(this.url ? `${this.url}/${this.apikey}` : window.location.href, {
method: "POST",
headers: {
'Accept': "application/json",
'Content-Type': "application/json"
},
body: JSON.stringify({
type,
message,
url,
line,
code: column,
backtrace: stack ? stack.split("\n").filter((e) => !!e).map((e) => this.parseErrorLine(e)) : [],
file: window.location.href,
source: await this.getSourceCode(url, line),
})
}).then(response => response.json()).then(console.info);
}
async getSourceCode(url, errorLineNumber) {
try {
const response = await fetch(url);
const originalHtml = await response.text();
const lines = originalHtml.split('\n');
const start = Math.max(0, errorLineNumber - 5);
const end = Math.min(lines.length, errorLineNumber + 5);
return lines.slice(start, end).join('\n');
}
catch (error) {
console.error('Failed to fetch original HTML:', error);
}
}
parseErrorLine(errorLine) {
const obj = {};
// Pattern for: functionName@url:line:column
const pattern = /^(.*)@(.+?):(\d+):(\d+)$/;
const match = errorLine.match(pattern);
if (match) {
obj.function = match[1];
obj.file = match[2];
obj.line = match[3];
// column would be match[4] if needed
console.log(match);
}
// Remove undefined/empty values
Object.keys(obj).forEach(key => {
if (!obj[key]) delete obj[key];
});
return obj;
}
}
+3
View File
@@ -25,6 +25,9 @@
"autoload": {
"definitions" : [
"meta/negundo.php"
],
"config": [
"meta/config.php"
]
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
return [
'lean' => [
'autoload' => [
'negundo.client'
]
],
];
+38 -3
View File
@@ -1,12 +1,47 @@
<?php
use function DI\autowire, DI\create, DI\get;
use Negundo\Client\{ SoftwareConfig, Dump, Task, NegundoMiddleware };
use function DI\{ autowire, create, get, add };
if (! function_exists('getenvonce')) {
function getenvonce(string $name) : array|string|false
{
$value = getenv($name);
unset($_ENV[$name], $_SERVER[$name]);
putenv($name . "=");
return $value;
}
}
return [
SoftwareConfig::class => create(SoftwareConfig::class)->constructor(getenv('NEGUNDO_HASH'), getenv('NEGUNDO_SERVER')),
SoftwareConfig::class => create(SoftwareConfig::class)->constructor(
serverUrl: getenvonce('NEGUNDO_SERVER'),
softwareHash: getenvonce('NEGUNDO_HASH'),
),
NegundoMiddleware::class => autowire(NegundoMiddleware::class),
Dump::class => autowire(Dump::class),
Task::class => autowire(Task::class),
'lean.autoload' => add([
Dump::class,
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,
],
],
],
],
];
+4 -2
View File
@@ -14,7 +14,7 @@ namespace Negundo\Client {
protected Util\DumpHandler $dumpHandler;
public function __construct(SoftwareConfig $config, ? DataInterface $dataManipulator = null, Transport\TransportInterface $transport = null)
public function __construct(SoftwareConfig $config, ? DataInterface $dataManipulator = null, ? Transport\TransportInterface $transport = null)
{
$this->config = $config;
$this->transport = $transport ?: new Transport\Curl();
@@ -35,7 +35,9 @@ namespace Negundo\Client {
$this->sent[$dumpHash] = true;
return $this->transport->push($this->config->url('dump/report'), $data);
$url = $this->config->url('dump/report');
return $url ? $this->transport->push($url, $data) : false;
}
}
}
+5 -3
View File
@@ -13,7 +13,7 @@ abstract class Handler {
public bool $registerFatalErrorHandler = true;
protected Closure $callback;
protected ? Closure $callback;
protected Transport\TransportInterface $transport;
@@ -23,7 +23,7 @@ abstract class Handler {
public abstract function handleException(\Throwable $ex) : array;
public function __construct(SoftwareConfig $config, ? DataInterface $dataManipulator = null, ? Closure $callback = null, Transport\TransportInterface $transport = null)
public function __construct(SoftwareConfig $config, ? DataInterface $dataManipulator = null, ? Closure $callback = null, ? Transport\TransportInterface $transport = null)
{
$this->config = $config;
@@ -68,6 +68,8 @@ abstract class Handler {
$data = $this->handleException($ex);
return $this->transport->push($this->config->url('bug/report'), $data);
$url = $this->config->url('bug/report');
return $url ? $this->transport->push($url, $data) : false;
}
}
+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;
}
}
}
}
+7 -19
View File
@@ -4,29 +4,17 @@ namespace Negundo\Client;
class SoftwareConfig
{
public string $serverUrl;
public function __construct(
public string $serverUrl,
public string $softwareHash,
) {}
public string $softwareHash;
public function __construct(string $softwareHash, string $serverUrl)
public function url(string $path) : string|null
{
if (! $softwareHash ) {
throw new \Exception("[Negundo] - You must provide a software hash to match a registered application on the plateform.");
}
else {
$this->softwareHash = $softwareHash;
if (empty($this->serverUrl) || empty($this->softwareHash)) {
return null;
}
if (! $serverUrl ) {
throw new \Exception("[Negundo] - You must provide a valid Negundo server url endpoint.");
}
else {
$this->serverUrl = $serverUrl;
}
}
public function url(string $path) : string
{
return sprintf("%s/%s/%s", rtrim($this->serverUrl, '/'), $path, $this->softwareHash);
}
}
+20 -9
View File
@@ -14,7 +14,7 @@ namespace Negundo\Client {
protected SoftwareConfig $config;
public function __construct(SoftwareConfig $config, ? DataInterface $dataManipulator = null, Transport\TransportInterface $transport = null)
public function __construct(SoftwareConfig $config, ? DataInterface $dataManipulator = null, ? Transport\TransportInterface $transport = null)
{
$this->config = $config;
@@ -25,18 +25,18 @@ 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);
$taskHash = $this->taskHandler->hash($report);
if ( $this->sent[$dumpHash] ?? false ) {
if ( $this->sent[$taskHash] ?? false ) {
return null;
}
$this->sent[$dumpHash] = true;
$this->sent[$taskHash] = true;
return $this->transport->push($this->config->url('task/report'), $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());
}
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace Negundo\Client\Task;
enum StatusEnum : string
{
case New = "new";
case Failed = "failed";
case Warning = "warning";
case Completed = "completed";
case Empty = "empty";
case NothingToDo = "nothing-to-do";
}
+84
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(),
];
}
}
+40 -15
View File
@@ -4,38 +4,63 @@ namespace Negundo\Client\Transport;
class Curl implements TransportInterface {
public $timeout = 1;
public $throwErrors = false;
public $timeout = 4;
public $throwErrors = true;
public $verifySsl = false;
public $headers = [];
public function push(string $url, array $data) : object|null|bool
{
$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_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);
$exec = curl_exec($ch);
if ( ( false === $exec ) && $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);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($this->throwErrors) {
if ( false === $exec) {
$errno = curl_errno($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));
}
}
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);
throw new CurlException(implode(PHP_EOL, array_filter([ curl_error($ch) , static::CURL_ERROR[$errno] ?? null ])), $errno);
}
return $execute ? (object) $execute : null;
}
const CURL_ERROR = [
0 => 'CURLE_OK',
1 => 'CURLE_UNSUPPORTED_PROTOCOL',
+4 -7
View File
@@ -5,13 +5,10 @@ namespace Negundo\Client\Util;
use Negundo\Client\DataInterface;
class DumpHandler {
public DataInterface $dataManipulator;
public function __construct(DataInterface $dataManipulator = null)
{
$this->dataManipulator = $dataManipulator;
}
public function __construct(
public ? DataInterface $dataManipulator = null
) {}
public function dumpData(...$content) : array
{
+9 -10
View File
@@ -6,12 +6,9 @@ use Negundo\Client\DataInterface;
class ExceptionHandler {
public ? DataInterface $dataManipulator;
public function __construct(DataInterface $dataManipulator = null)
{
$this->dataManipulator = $dataManipulator;
}
public function __construct(
public ? DataInterface $dataManipulator = null
) {}
public function extractExceptionData(\Throwable $ex, array $serverData, array $postData, $dataManipulator = null) : array
{
@@ -19,10 +16,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,
'REMOTE_ADDR' => $serverData['REMOTE_ADDR'] ?? null,
'HTTP_USER_AGENT' => $serverData['HTTP_USER_AGENT'] ?? "# UNKNOWN #",
'REMOTE_ADDR' => $server['HTTP_X_FORWARDED_FOR'] ?? $serverData['REMOTE_ADDR'] ?? null,
'REQUEST_METHOD' => $serverData['HTTP_X_HTTP_METHOD'] ?? $serverData['REQUEST_METHOD'] ?? null,
];
$post = [
'code' => $ex->getCode(),
'file' => $ex->getFile(),
@@ -30,11 +28,12 @@ class ExceptionHandler {
'type' => $ex::class,
'message' => $ex->getMessage(),
'url' => ( ( 'on' === $serverData['HTTPS'] ) ? 'https' : 'http' ) . '://' . $serverData['HTTP_HOST'] . $serverData["REQUEST_URI"],
'http_method' => strtolower($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'],
+3 -4
View File
@@ -41,10 +41,9 @@ class SourceCodeFormatter {
catch(\Throwable $e) {
$content = $fileSpl->current();
}
# Get code between <code></code> tags
$content = substr($content, strpos($content, "<code>") + 6, strrpos($content, "</code>") - 6);
$content = strip_tags($content, '<span>');
# Removing the added <?php tag
$content = str_replace("&lt;?php", "", $content);
+7 -9
View File
@@ -6,14 +6,11 @@ use Negundo\Client\DataInterface;
class TaskHandler {
public DataInterface $dataManipulator;
public function __construct(
public ? DataInterface $dataManipulator = null
) {}
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 +33,7 @@ class TaskHandler {
'request_body' => json_encode($_POST),
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
],
'events' => $events,
];
if ( $this->dataManipulator ?? false ) {
@@ -44,11 +42,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'] ]));
+10
View File
@@ -0,0 +1,10 @@
{% php $softwareConfig = di_get(\Negundo\Client\SoftwareConfig::class) %}
<script src="{% asset 'static/negundo/js/debug.js' %}"></script>
<script>
new ErrorHandler({
url: "{{ rtrim($softwareConfig->serverUrl, '/') }}/bug/client/report",
apikey: "{{ $softwareConfig->softwareHash }}"
});
</script>