11 Commits
14 changed files with 127 additions and 109 deletions

No files matched your search

+63 -39
View File
@@ -9,36 +9,6 @@ class ErrorHandler {
this.catchError(); 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) { getErrorPosition(script) {
let totalLines = 0; let totalLines = 0;
let element = script.previousElementSibling; let element = script.previousElementSibling;
@@ -54,18 +24,32 @@ class ErrorHandler {
catchError() { catchError() {
window.onerror = (message, url, lineNumber, column, error) => { window.onerror = (message, url, lineNumber, column, error) => {
this.reportError(message, url, lineNumber, column, error.stack, 'JavaScript 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; return false;
}; };
window.addEventListener('unhandledrejection', event => { window.addEventListener('unhandledrejection', event => {
this.reportError(event.reason.toString(), document.location.href, 0, 0, event.reason.stack, 'Promise Rejection'); this.reportError(event.reason.toString(), document.location.href, 0, 0, event.reason.stack, 'Promise Rejection');
}); });
this.createErrorAgain();
} }
reportError(message, url, lineNumber, column, stack, type = 'JavaScript Error') { async reportError(message, url, line, column, stack, type = 'JavaScript Error') {
fetch(this.url ? `${location.protocol}//${this.url}/${this.apikey}` : window.location.href, { await new Promise(r => setTimeout(r, 2000));
method: "post",
fetch(this.url ? `${this.url}/${this.apikey}` : window.location.href, {
method: "POST",
headers: { headers: {
'Accept': "application/json", 'Accept': "application/json",
'Content-Type': "application/json" 'Content-Type': "application/json"
@@ -74,12 +58,52 @@ class ErrorHandler {
type, type,
message, message,
url, url,
lineNumber, line,
column, code: column,
stack, backtrace: stack ? stack.split("\n").filter((e) => !!e).map((e) => this.parseErrorLine(e)) : [],
location: window.location.href, file: window.location.href,
source: this.getSourceCode(lineNumber) source: await this.getSourceCode(url, line),
}) })
}).then(response => response.json()).then(console.info); }).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;
}
} }
+11 -3
View File
@@ -1,15 +1,23 @@
<?php <?php
use function DI\autowire, DI\create, DI\get;
use Negundo\Client\{ SoftwareConfig, Dump, Task, NegundoMiddleware }; use Negundo\Client\{ SoftwareConfig, Dump, Task, NegundoMiddleware };
use function DI\{ autowire, create, get, add };
return [ 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), NegundoMiddleware::class => autowire(NegundoMiddleware::class),
Dump::class => autowire(Dump::class), Dump::class => autowire(Dump::class),
Task::class => autowire(Task::class), Task::class => autowire(Task::class),
'lean.autoload' => add([
Dump::class,
Task::class,
]),
'negundo.client' => [ 'negundo.client' => [
'picea' => [ 'picea' => [
'asset' => [ 'asset' => [
+4 -2
View File
@@ -14,7 +14,7 @@ namespace Negundo\Client {
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();
@@ -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;
} }
} }
} }
+4 -2
View File
@@ -23,7 +23,7 @@ abstract class Handler {
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;
@@ -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;
} }
} }
+7 -19
View File
@@ -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);
} }
} }
+4 -4
View File
@@ -14,7 +14,7 @@ namespace Negundo\Client {
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;
@@ -30,13 +30,13 @@ namespace Negundo\Client {
$report = $this->taskHandler->sendReport($message, $title, $data, $events); $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);
} }
+1
View File
@@ -4,6 +4,7 @@ namespace Negundo\Client\Task;
enum StatusEnum : string enum StatusEnum : string
{ {
case New = "new";
case Failed = "failed"; case Failed = "failed";
case Warning = "warning"; case Warning = "warning";
case Completed = "completed"; case Completed = "completed";
+15 -3
View File
@@ -2,7 +2,7 @@
namespace Negundo\Client\Task; namespace Negundo\Client\Task;
class TaskReport class TaskReport implements \JsonSerializable
{ {
protected array $events = []; protected array $events = [];
@@ -13,6 +13,7 @@ class TaskReport
public array $data = [], public array $data = [],
) {} ) {}
public function addData(string $name, array $data) : static public function addData(string $name, array $data) : static
{ {
$this->data[$name][] = $data; $this->data[$name][] = $data;
@@ -60,13 +61,24 @@ class TaskReport
return $this->data; return $this->data;
} }
public function getEvents() : array public function getEvents(?StatusEnum $filterType = null) : array
{ {
return $this->events; return $filterType ? array_filter($this->events, fn($e) => StatusEnum::tryFrom($e['status']) === $filterType) : $this->events;
} }
public function getStatus() : StatusEnum public function getStatus() : StatusEnum
{ {
return $this->status; return $this->status;
} }
public function jsonSerialize(): mixed
{
return [
'title' => $this->getTitle(),
'message' => $this->getMessage(),
'status' => $this->getStatus(),
'events' => $this->getEvents(),
'data' => $this->getData(),
];
}
} }
+2 -13
View File
@@ -4,7 +4,7 @@ namespace Negundo\Client\Transport;
class Curl implements TransportInterface { class Curl implements TransportInterface {
public $timeout = 100; public $timeout = 4;
public $throwErrors = true; public $throwErrors = true;
@@ -21,7 +21,7 @@ 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, $this->verifySsl); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifySsl);
$exec = curl_exec($ch); $exec = curl_exec($ch);
@@ -31,19 +31,12 @@ class Curl implements TransportInterface {
if ($this->throwErrors) { if ($this->throwErrors) {
if ( false === $exec) { if ( false === $exec) {
$errno = curl_errno($ch); $errno = curl_errno($ch);
curl_close($ch);
throw new CurlException(implode(PHP_EOL, array_filter([ curl_error($ch) , static::CURL_ERROR[$errno] ?? null ])), $errno); throw new CurlException(implode(PHP_EOL, array_filter([ curl_error($ch) , static::CURL_ERROR[$errno] ?? null ])), $errno);
} }
elseif ($code >= 400) { elseif ($code >= 400) {
throw new \Exception(sprintf("HTTP code received : $code with page content : %s", $exec)); throw new \Exception(sprintf("HTTP code received : $code with page content : %s", $exec));
} }
} }
if ($_GET['dev'] ?? false) {
echo($exec);
die();
}
curl_close($ch);
return $exec; return $exec;
} }
@@ -62,12 +55,8 @@ class Curl implements TransportInterface {
if ( ( false === $execute ) && $this->throwErrors ) { if ( ( false === $execute ) && $this->throwErrors ) {
$errno = curl_errno($ch); $errno = curl_errno($ch);
curl_close($ch);
throw new CurlException(implode(PHP_EOL, array_filter([ curl_error($ch) , static::CURL_ERROR[$errno] ?? null ])), $errno); 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 $execute ? (object) $execute : null;
} }
+3 -6
View File
@@ -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
{ {
+5 -8
View File
@@ -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
{ {
@@ -20,7 +17,7 @@ class ExceptionHandler {
'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'] ?? "# UNKNOWN #", '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, 'REQUEST_METHOD' => $serverData['HTTP_X_HTTP_METHOD'] ?? $serverData['REQUEST_METHOD'] ?? null,
]; ];
@@ -31,7 +28,7 @@ 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' => $serverData['REQUEST_METHOD'], '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),
+1 -2
View File
@@ -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("&lt;?php", "", $content); $content = str_replace("&lt;?php", "", $content);
+3 -6
View File
@@ -6,12 +6,9 @@ use Negundo\Client\DataInterface;
class TaskHandler { 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 $events = []) : array public function sendReport(string $message, ? string $title = null, array $data = [], array $events = []) : array
{ {
+4 -2
View File
@@ -1,8 +1,10 @@
{% php $softwareConfig = di_get(\Negundo\Client\SoftwareConfig::class) %}
<script src="{% asset 'static/negundo/js/debug.js' %}"></script> <script src="{% asset 'static/negundo/js/debug.js' %}"></script>
<script> <script>
new ErrorHandler({ new ErrorHandler({
url: "{{ getenv('NEGUNDO_SERVER') }}/bug/client/report", url: "{{ rtrim($softwareConfig->serverUrl, '/') }}/bug/client/report",
apikey: "{{ getenv('NEGUNDO_HASH') }}" apikey: "{{ $softwareConfig->softwareHash }}"
}); });
</script> </script>