Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72e50fbe50 | ||
|
|
27bfdd5902 | ||
|
|
063aba6040 | ||
|
|
956ccbba1a | ||
|
|
af0e9f291d | ||
|
|
bc8d831b6c | ||
|
|
2df4ea15e2 | ||
|
|
0694b50eb6 | ||
|
|
72f5341dff | ||
|
|
b88602a98b | ||
|
|
f1e9dddf5e | ||
|
|
8de8aafa84 | ||
|
|
7b5a1bcf97 | ||
|
|
6227321045 | ||
|
|
38df94fc2a | ||
|
|
36d4a6971e | ||
|
|
3474cc4840 | ||
|
|
5a737c06c4 | ||
|
|
586f2344df | ||
|
|
29a780ac6a | ||
|
|
e85925ace7 | ||
|
|
ef43c7831f | ||
|
|
3590993925 | ||
|
|
d94ee83230 | ||
|
|
a552a6675e | ||
|
|
5e44f36dbc | ||
|
|
4484f1c997 |
No files matched your search
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "mcnd/negundo-client",
|
"name": "mcnd/negundo-client",
|
||||||
"description": "Negundo client which allow sending dump(), error and tasks reports",
|
"description": "Negundo client which allow sending dump(), error and tasks reports",
|
||||||
"keywords": ["negundo","dev","debug","psr15","middleware"],
|
"keywords": ["negundo","dev","debug","psr15","middleware"],
|
||||||
|
"type": "library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
@@ -13,5 +14,22 @@
|
|||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Negundo\\Client\\": "src/"
|
"Negundo\\Client\\": "src/"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*"
|
||||||
|
},
|
||||||
|
"extra" : {
|
||||||
|
"lean" : {
|
||||||
|
"autoload": {
|
||||||
|
"definitions" : [
|
||||||
|
"meta/negundo.php"
|
||||||
|
],
|
||||||
|
"config": [
|
||||||
|
"meta/config.php"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'lean' => [
|
||||||
|
'autoload' => [
|
||||||
|
'negundo.client'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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(
|
||||||
|
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,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
+11
-9
@@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
namespace Negundo\Client {
|
namespace Negundo\Client {
|
||||||
class Dump {
|
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)
|
public function __construct(SoftwareConfig $config, ? DataInterface $dataManipulator = null, ? Transport\TransportInterface $transport = null)
|
||||||
{
|
{
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
$this->transport = $transport ?: new Transport\Curl();
|
$this->transport = $transport ?: new Transport\Curl();
|
||||||
@@ -22,7 +22,7 @@ namespace Negundo\Client {
|
|||||||
static::$instances[] = $this;
|
static::$instances[] = $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pushData(...$content) : ? object
|
public function pushData(...$content) : object|null|bool
|
||||||
{
|
{
|
||||||
$data = $this->dumpHandler->dumpData(...$content);
|
$data = $this->dumpHandler->dumpData(...$content);
|
||||||
|
|
||||||
@@ -35,7 +35,9 @@ namespace Negundo\Client {
|
|||||||
|
|
||||||
$this->sent[$dumpHash] = true;
|
$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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-12
@@ -5,25 +5,25 @@ namespace Negundo\Client;
|
|||||||
use Closure;
|
use Closure;
|
||||||
|
|
||||||
abstract class Handler {
|
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;
|
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;
|
$this->config = $config;
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ abstract class Handler {
|
|||||||
$this->registerHandlers();
|
$this->registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function registerHandlers()
|
public function registerHandlers() : void
|
||||||
{
|
{
|
||||||
$this->registerExceptionHandler && set_exception_handler(function(\Throwable $ex) {
|
$this->registerExceptionHandler && set_exception_handler(function(\Throwable $ex) {
|
||||||
$this->pushData($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)
|
// Make sure not to spam the server if an ErrorMessage or Exception was already sent (like inside a loop)
|
||||||
$exceptionHash = $this->exceptionHandler->hash($ex);
|
$exceptionHash = $this->exceptionHandler->hash($ex);
|
||||||
@@ -68,6 +68,8 @@ abstract class Handler {
|
|||||||
|
|
||||||
$data = $this->handleException($ex);
|
$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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,4 +9,21 @@ class NativeHandler extends Handler {
|
|||||||
return $this->exceptionHandler->extractExceptionData($ex, $_SERVER, $_POST);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ class NegundoMiddleware extends Handler implements MiddlewareInterface {
|
|||||||
|
|
||||||
protected ServerRequestInterface $request;
|
protected ServerRequestInterface $request;
|
||||||
|
|
||||||
public $registerExceptionHandler = false;
|
public bool $registerExceptionHandler = false;
|
||||||
|
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
|
||||||
{
|
{
|
||||||
|
|||||||
+7
-19
@@ -4,29 +4,17 @@ namespace Negundo\Client;
|
|||||||
|
|
||||||
class SoftwareConfig
|
class SoftwareConfig
|
||||||
{
|
{
|
||||||
public /*string*/ $serverUrl;
|
public function __construct(
|
||||||
|
public string $serverUrl,
|
||||||
|
public string $softwareHash,
|
||||||
|
) {}
|
||||||
|
|
||||||
public /* string */ $softwareHash;
|
public function url(string $path) : string|null
|
||||||
|
|
||||||
public function __construct(string $softwareHash, string $serverUrl)
|
|
||||||
{
|
{
|
||||||
if (! $softwareHash ) {
|
if (empty($this->serverUrl) || empty($this->softwareHash)) {
|
||||||
throw new \Exception("[Negundo] - You must provide a software hash to match a registered application on the plateform.");
|
return null;
|
||||||
}
|
|
||||||
else {
|
|
||||||
$this->softwareHash = $softwareHash;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
return sprintf("%s/%s/%s", rtrim($this->serverUrl, '/'), $path, $this->softwareHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+28
-13
@@ -4,17 +4,17 @@ namespace Negundo\Client {
|
|||||||
|
|
||||||
class Task {
|
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)
|
public function __construct(SoftwareConfig $config, ? DataInterface $dataManipulator = null, ? Transport\TransportInterface $transport = null)
|
||||||
{
|
{
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
|
|
||||||
@@ -25,18 +25,18 @@ namespace Negundo\Client {
|
|||||||
static::$instances[] = $this;
|
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)
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->sent[$dumpHash] = true;
|
$this->sent[$taskHash] = true;
|
||||||
|
|
||||||
return $this->transport->push($this->config->url('task/report'), $report);
|
return $this->transport->push($this->config->url('task/report'), $report);
|
||||||
}
|
}
|
||||||
@@ -44,10 +44,25 @@ namespace Negundo\Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
use Negundo\Client\Task\StatusEnum;
|
||||||
|
use Negundo\Client\Task\TaskReport;
|
||||||
|
|
||||||
if (! function_exists('ntask') ) {
|
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) {
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
-12
@@ -4,13 +4,15 @@ namespace Negundo\Client\Transport;
|
|||||||
|
|
||||||
class Curl implements TransportInterface {
|
class Curl implements TransportInterface {
|
||||||
|
|
||||||
public $timeout = 1;
|
public $timeout = 4;
|
||||||
|
|
||||||
public $throwErrors = false;
|
public $throwErrors = true;
|
||||||
|
|
||||||
|
public $verifySsl = false;
|
||||||
|
|
||||||
public $headers = [];
|
public $headers = [];
|
||||||
|
|
||||||
public function push(string $url, array $data) : ? object
|
public function push(string $url, array $data) : object|null|bool
|
||||||
{
|
{
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
|
|
||||||
@@ -19,19 +21,44 @@ class Curl implements TransportInterface {
|
|||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers );
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers );
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
curl_setopt($ch, CURLOPT_POST, 1);
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data, '', '&'));
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data, '', '&'));
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->timeout * 200);
|
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifySsl);
|
||||||
|
|
||||||
if ( ( false === curl_exec($ch) ) && $this->throwErrors ) {
|
$exec = curl_exec($ch);
|
||||||
$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;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
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 = [
|
const CURL_ERROR = [
|
||||||
@@ -159,7 +186,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.",
|
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.",
|
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? ",
|
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.",
|
39 => "LDAP search failed.",
|
||||||
41 => "Function not found. A required zlib function was not found.",
|
41 => "Function not found. A required zlib function was not found.",
|
||||||
42 => "Aborted by callback. A callback returned \"abort\" to libcurl.",
|
42 => "Aborted by callback. A callback returned \"abort\" to libcurl.",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class GuzzleClient implements TransportInterface {
|
|||||||
|
|
||||||
public array $headers = [];
|
public array $headers = [];
|
||||||
|
|
||||||
public function push(string $url, array $data) : ? object
|
public function push(string $url, array $data) : object|null|bool
|
||||||
{
|
{
|
||||||
return ( new Client([
|
return ( new Client([
|
||||||
'timeout' => $this->timeout,
|
'timeout' => $this->timeout,
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
namespace Negundo\Client\Transport;
|
namespace Negundo\Client\Transport;
|
||||||
|
|
||||||
interface TransportInterface {
|
interface TransportInterface {
|
||||||
public function push(string $url, array $data) : ? object;
|
public function push(string $url, array $data) : object|null|bool;
|
||||||
}
|
}
|
||||||
@@ -6,12 +6,9 @@ use Negundo\Client\DataInterface;
|
|||||||
|
|
||||||
class DumpHandler {
|
class DumpHandler {
|
||||||
|
|
||||||
public /*DataInterface*/ $dataManipulator;
|
public function __construct(
|
||||||
|
public ? DataInterface $dataManipulator = null
|
||||||
public function __construct(DataInterface $dataManipulator = null)
|
) {}
|
||||||
{
|
|
||||||
$this->dataManipulator = $dataManipulator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dumpData(...$content) : array
|
public function dumpData(...$content) : array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ use Negundo\Client\DataInterface;
|
|||||||
|
|
||||||
class ExceptionHandler {
|
class ExceptionHandler {
|
||||||
|
|
||||||
public /*DataInterface*/ $dataManipulator;
|
public function __construct(
|
||||||
|
public ? DataInterface $dataManipulator = null
|
||||||
public function __construct(DataInterface $dataManipulator = null)
|
) {}
|
||||||
{
|
|
||||||
$this->dataManipulator = $dataManipulator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function extractExceptionData(\Throwable $ex, array $serverData, array $postData, $dataManipulator = null) : array
|
public function extractExceptionData(\Throwable $ex, array $serverData, array $postData, $dataManipulator = null) : array
|
||||||
{
|
{
|
||||||
@@ -19,8 +16,9 @@ class ExceptionHandler {
|
|||||||
'HTTPS' => $serverData['HTTPS'] ?? false,
|
'HTTPS' => $serverData['HTTPS'] ?? false,
|
||||||
'HTTP_HOST' => $serverData['HTTP_HOST'] ?? "",
|
'HTTP_HOST' => $serverData['HTTP_HOST'] ?? "",
|
||||||
'REQUEST_URI' => $serverData['REQUEST_URI'] ?? "",
|
'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,
|
'REMOTE_ADDR' => $server['HTTP_X_FORWARDED_FOR'] ?? $serverData['REMOTE_ADDR'] ?? null,
|
||||||
|
'REQUEST_METHOD' => $serverData['HTTP_X_HTTP_METHOD'] ?? $serverData['REQUEST_METHOD'] ?? null,
|
||||||
];
|
];
|
||||||
|
|
||||||
$post = [
|
$post = [
|
||||||
@@ -30,11 +28,12 @@ class ExceptionHandler {
|
|||||||
'type' => $ex::class,
|
'type' => $ex::class,
|
||||||
'message' => $ex->getMessage(),
|
'message' => $ex->getMessage(),
|
||||||
'url' => ( ( 'on' === $serverData['HTTPS'] ) ? 'https' : 'http' ) . '://' . $serverData['HTTP_HOST'] . $serverData["REQUEST_URI"],
|
'url' => ( ( 'on' === $serverData['HTTPS'] ) ? 'https' : 'http' ) . '://' . $serverData['HTTP_HOST'] . $serverData["REQUEST_URI"],
|
||||||
|
'http_method' => strtolower($serverData['REQUEST_METHOD']),
|
||||||
'backtrace' => json_encode($ex->getTrace()),
|
'backtrace' => json_encode($ex->getTrace()),
|
||||||
'backtrace_string' => $ex->getTraceAsString(),
|
'backtrace_string' => $ex->getTraceAsString(),
|
||||||
'source' => ( new SourceCodeFormatter() )->generateFromException($ex),
|
'source' => ( new SourceCodeFormatter() )->generateFromException($ex),
|
||||||
'hits' => [
|
'hits' => [
|
||||||
'user_agent' => $serverData['HTTP_USER_AGENT'] ?? "???",
|
'user_agent' => $serverData['HTTP_USER_AGENT'],
|
||||||
'sent_at' => date('Y-m-d H:i:s'),
|
'sent_at' => date('Y-m-d H:i:s'),
|
||||||
'request_body_vars' => array_keys($postData),
|
'request_body_vars' => array_keys($postData),
|
||||||
'remote_addr' => $serverData['REMOTE_ADDR'],
|
'remote_addr' => $serverData['REMOTE_ADDR'],
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ class SourceCodeFormatter {
|
|||||||
$content = $fileSpl->current();
|
$content = $fileSpl->current();
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get code between <code></code> tags
|
$content = strip_tags($content, '<span>');
|
||||||
$content = substr($content, strpos($content, "<code>") + 6, strrpos($content, "</code>") - 6);
|
|
||||||
|
|
||||||
# Removing the added <?php tag
|
# Removing the added <?php tag
|
||||||
$content = str_replace("<?php", "", $content);
|
$content = str_replace("<?php", "", $content);
|
||||||
|
|||||||
@@ -6,22 +6,19 @@ use Negundo\Client\DataInterface;
|
|||||||
|
|
||||||
class TaskHandler {
|
class TaskHandler {
|
||||||
|
|
||||||
public /*DataInterface*/ $dataManipulator;
|
public function __construct(
|
||||||
|
public ? DataInterface $dataManipulator = null
|
||||||
|
) {}
|
||||||
|
|
||||||
public function __construct(DataInterface $dataManipulator = null)
|
public function sendReport(string $message, ? string $title = null, array $data = [], array $events = []) : array
|
||||||
{
|
|
||||||
$this->dataManipulator = $dataManipulator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sendReport(string $message, ? string $title = null, array $data = []) : array
|
|
||||||
{
|
{
|
||||||
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
|
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||||
array_shift($backtrace);
|
array_shift($backtrace);
|
||||||
array_shift($backtrace);
|
array_shift($backtrace);
|
||||||
|
|
||||||
$trace = $backtrace[0] ?? [
|
$trace = $backtrace[0] ?? [
|
||||||
"line" => -1,
|
|
||||||
"file" => "unknown",
|
"file" => "unknown",
|
||||||
|
"line" => -1,
|
||||||
];
|
];
|
||||||
|
|
||||||
$post = [
|
$post = [
|
||||||
@@ -36,6 +33,7 @@ class TaskHandler {
|
|||||||
'request_body' => json_encode($_POST),
|
'request_body' => json_encode($_POST),
|
||||||
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
|
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
|
||||||
],
|
],
|
||||||
|
'events' => $events,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ( $this->dataManipulator ?? false ) {
|
if ( $this->dataManipulator ?? false ) {
|
||||||
@@ -44,7 +42,7 @@ class TaskHandler {
|
|||||||
$this->dataManipulator->run($post);
|
$this->dataManipulator->run($post);
|
||||||
}
|
}
|
||||||
|
|
||||||
$post['data'] = json_encode($post['data'] ?? null);
|
$post['data'] = json_encode($post['data'] ?? null, JSON_INVALID_UTF8_IGNORE);
|
||||||
|
|
||||||
return $post;
|
return $post;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in new issue
Block a user