Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
419d9d55df | ||
|
|
abd530d146 | ||
|
|
7d5272816d | ||
|
|
72e50fbe50 | ||
|
|
27bfdd5902 | ||
|
|
063aba6040 | ||
|
|
956ccbba1a | ||
|
|
af0e9f291d | ||
|
|
bc8d831b6c | ||
|
|
2df4ea15e2 | ||
|
|
0694b50eb6 | ||
|
|
72f5341dff | ||
|
|
b88602a98b | ||
|
|
f1e9dddf5e | ||
|
|
8de8aafa84 |
No files matched your search
+84
-50
@@ -1,4 +1,7 @@
|
||||
class ErrorHandler {
|
||||
|
||||
#unhandledRection = false
|
||||
|
||||
constructor(options) {
|
||||
if (!options) throw new Error("Options not provided");
|
||||
if (!options.url) throw new Error("URL is required");
|
||||
@@ -9,36 +12,6 @@ class ErrorHandler {
|
||||
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;
|
||||
@@ -54,32 +27,93 @@ class ErrorHandler {
|
||||
|
||||
catchError() {
|
||||
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;
|
||||
};
|
||||
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
this.reportError(event.reason.toString(), document.location.href, 0, 0, event.reason.stack, 'Promise Rejection');
|
||||
if (! this.#unhandledRection) {
|
||||
this.#unhandledRection = true;
|
||||
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);
|
||||
async reportError(message, url, line, column, stack, type = 'JavaScript Error') {
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
try {
|
||||
const response = await fetch(this.url ? `${this.url}/${this.apikey}` : window.location.href, {
|
||||
method: "PUT",
|
||||
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),
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.#unhandledRection = false;
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('CORS or network error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Remove undefined/empty values
|
||||
Object.keys(obj).forEach(key => {
|
||||
if (!obj[key]) delete obj[key];
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
+21
-3
@@ -1,15 +1,33 @@
|
||||
<?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' => [
|
||||
|
||||
+4
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
+7
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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;
|
||||
|
||||
@@ -30,13 +30,13 @@ namespace Negundo\Client {
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Negundo\Client\Task;
|
||||
|
||||
enum StatusEnum : string
|
||||
{
|
||||
case New = "new";
|
||||
case Failed = "failed";
|
||||
case Warning = "warning";
|
||||
case Completed = "completed";
|
||||
|
||||
+15
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Negundo\Client\Task;
|
||||
|
||||
class TaskReport
|
||||
class TaskReport implements \JsonSerializable
|
||||
{
|
||||
protected array $events = [];
|
||||
|
||||
@@ -13,6 +13,7 @@ class TaskReport
|
||||
public array $data = [],
|
||||
) {}
|
||||
|
||||
|
||||
public function addData(string $name, array $data) : static
|
||||
{
|
||||
$this->data[$name][] = $data;
|
||||
@@ -60,13 +61,24 @@ class TaskReport
|
||||
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
|
||||
{
|
||||
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
@@ -4,7 +4,7 @@ namespace Negundo\Client\Transport;
|
||||
|
||||
class Curl implements TransportInterface {
|
||||
|
||||
public $timeout = 100;
|
||||
public $timeout = 4;
|
||||
|
||||
public $throwErrors = true;
|
||||
|
||||
@@ -21,7 +21,7 @@ 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_TIMEOUT, $this->timeout);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifySsl);
|
||||
|
||||
$exec = curl_exec($ch);
|
||||
@@ -31,19 +31,12 @@ class Curl implements TransportInterface {
|
||||
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 $exec;
|
||||
}
|
||||
@@ -62,12 +55,8 @@ class Curl implements TransportInterface {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,9 @@ 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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -20,7 +17,7 @@ class ExceptionHandler {
|
||||
'HTTP_HOST' => $serverData['HTTP_HOST'] ?? "",
|
||||
'REQUEST_URI' => $serverData['REQUEST_URI'] ?? "",
|
||||
'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,
|
||||
];
|
||||
|
||||
@@ -31,7 +28,7 @@ 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'],
|
||||
'http_method' => strtolower($serverData['REQUEST_METHOD']),
|
||||
'backtrace' => json_encode($ex->getTrace()),
|
||||
'backtrace_string' => $ex->getTraceAsString(),
|
||||
'source' => ( new SourceCodeFormatter() )->generateFromException($ex),
|
||||
|
||||
@@ -42,8 +42,7 @@ class SourceCodeFormatter {
|
||||
$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("<?php", "", $content);
|
||||
|
||||
@@ -6,12 +6,9 @@ use Negundo\Client\DataInterface;
|
||||
|
||||
class TaskHandler {
|
||||
|
||||
public ? DataInterface $dataManipulator;
|
||||
|
||||
public function __construct(DataInterface $dataManipulator = null)
|
||||
{
|
||||
$this->dataManipulator = $dataManipulator;
|
||||
}
|
||||
public function __construct(
|
||||
public ? DataInterface $dataManipulator = null
|
||||
) {}
|
||||
|
||||
public function sendReport(string $message, ? string $title = null, array $data = [], array $events = []) : array
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{% php $softwareConfig = di_get(\Negundo\Client\SoftwareConfig::class) %}
|
||||
|
||||
<script src="{% asset 'static/negundo/js/debug.js' %}"></script>
|
||||
|
||||
<script>
|
||||
new ErrorHandler({
|
||||
url: "{{ getenv('NEGUNDO_SERVER') }}/bug/client/report",
|
||||
apikey: "{{ getenv('NEGUNDO_HASH') }}"
|
||||
url: "{{ rtrim($softwareConfig->serverUrl, '/') }}/bug/client/report",
|
||||
apikey: "{{ $softwareConfig->softwareHash }}"
|
||||
});
|
||||
</script>
|
||||
Reference in new issue
Block a user