Primo Committ

This commit is contained in:
paoloar77
2024-05-07 12:17:25 +02:00
commit e73d0e5113
7204 changed files with 884387 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
<?php
namespace Facade\FlareClient;
use Exception;
use Facade\FlareClient\Http\Client;
use Facade\FlareClient\Truncation\ReportTrimmer;
class Api
{
/** @var \Facade\FlareClient\Http\Client */
protected $client;
/** @var bool */
public static $sendInBatches = true;
/** @var array */
protected $queue = [];
public function __construct(Client $client)
{
$this->client = $client;
register_shutdown_function([$this, 'sendQueuedReports']);
}
public static function sendReportsInBatches(bool $batchSending = true)
{
static::$sendInBatches = $batchSending;
}
public function report(Report $report)
{
try {
if (static::$sendInBatches) {
$this->addReportToQueue($report);
} else {
$this->sendReportToApi($report);
}
} catch (Exception $e) {
//
}
}
public function sendTestReport(Report $report)
{
$this->sendReportToApi($report);
}
protected function addReportToQueue(Report $report)
{
$this->queue[] = $report;
}
public function sendQueuedReports()
{
try {
foreach ($this->queue as $report) {
$this->sendReportToApi($report);
}
} catch (Exception $e) {
//
} finally {
$this->queue = [];
}
}
protected function sendReportToApi(Report $report)
{
$this->client->post('reports', $this->truncateReport($report->toArray()));
}
protected function truncateReport(array $payload): array
{
return (new ReportTrimmer())->trim($payload);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Facade\FlareClient\Concerns;
trait HasContext
{
/** @var string|null */
private $messageLevel;
/** @var string|null */
private $stage;
/** @var array */
private $userProvidedContext = [];
public function stage(?string $stage)
{
$this->stage = $stage;
return $this;
}
public function messageLevel(?string $messageLevel)
{
$this->messageLevel = $messageLevel;
return $this;
}
public function getGroup(string $groupName = 'context', $default = []): array
{
return $this->userProvidedContext[$groupName] ?? $default;
}
public function context($key, $value)
{
return $this->group('context', [$key => $value]);
}
public function group(string $groupName, array $properties)
{
$group = $this->userProvidedContext[$groupName] ?? [];
$this->userProvidedContext[$groupName] = array_merge_recursive_distinct(
$group,
$properties
);
return $this;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Facade\FlareClient\Concerns;
use Facade\FlareClient\Time\SystemTime;
use Facade\FlareClient\Time\Time;
trait UsesTime
{
/** @var \Facade\FlareClient\Time\Time */
public static $time;
public static function useTime(Time $time)
{
self::$time = $time;
}
public function getCurrentTime(): int
{
$time = self::$time ?? new SystemTime();
return $time->getCurrentTime();
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Facade\FlareClient\Context;
class ConsoleContext implements ContextInterface
{
/** @var array */
private $arguments = [];
public function __construct(array $arguments = [])
{
$this->arguments = $arguments;
}
public function toArray(): array
{
return [
'arguments' => $this->arguments,
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Facade\FlareClient\Context;
class ContextContextDetector implements ContextDetectorInterface
{
public function detectCurrentContext(): ContextInterface
{
if ($this->runningInConsole()) {
return new ConsoleContext($_SERVER['argv'] ?? []);
}
return new RequestContext();
}
private function runningInConsole(): bool
{
if (isset($_ENV['APP_RUNNING_IN_CONSOLE'])) {
return $_ENV['APP_RUNNING_IN_CONSOLE'] === 'true';
}
if (isset($_ENV['FLARE_FAKE_WEB_REQUEST'])) {
return false;
}
return in_array(php_sapi_name(), ['cli', 'phpdb']);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Context;
interface ContextDetectorInterface
{
public function detectCurrentContext(): ContextInterface;
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Context;
interface ContextInterface
{
public function toArray(): array;
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Facade\FlareClient\Context;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Throwable;
class RequestContext implements ContextInterface
{
/** @var \Symfony\Component\HttpFoundation\Request|null */
protected $request;
public function __construct(Request $request = null)
{
$this->request = $request ?? Request::createFromGlobals();
}
public function getRequest(): array
{
return [
'url' => $this->request->getUri(),
'ip' => $this->request->getClientIp(),
'method' => $this->request->getMethod(),
'useragent' => $this->request->headers->get('User-Agent'),
];
}
private function getFiles(): array
{
if (is_null($this->request->files)) {
return [];
}
return $this->mapFiles($this->request->files->all());
}
protected function mapFiles(array $files)
{
return array_map(function ($file) {
if (is_array($file)) {
return $this->mapFiles($file);
}
if (! $file instanceof UploadedFile) {
return;
}
try {
$fileSize = $file->getSize();
} catch (\RuntimeException $e) {
$fileSize = 0;
}
try {
$mimeType = $file->getMimeType();
} catch (InvalidArgumentException $e) {
$mimeType = 'undefined';
}
return [
'pathname' => $file->getPathname(),
'size' => $fileSize,
'mimeType' => $mimeType,
];
}, $files);
}
public function getSession(): array
{
try {
$session = $this->request->getSession();
} catch (\Exception $exception) {
$session = [];
}
return $session ? $this->getValidSessionData($session) : [];
}
/**
* @param SessionInterface $session
* @return array
*/
protected function getValidSessionData($session): array
{
try {
json_encode($session->all());
} catch (Throwable $e) {
return [];
}
return $session->all();
}
public function getCookies(): array
{
return $this->request->cookies->all();
}
public function getHeaders(): array
{
return $this->request->headers->all();
}
public function getRequestData(): array
{
return [
'queryString' => $this->request->query->all(),
'body' => $this->request->request->all(),
'files' => $this->getFiles(),
];
}
public function toArray(): array
{
return [
'request' => $this->getRequest(),
'request_data' => $this->getRequestData(),
'headers' => $this->getHeaders(),
'cookies' => $this->getCookies(),
'session' => $this->getSession(),
];
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Contracts;
interface ProvidesFlareContext
{
public function context(): array;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Facade\FlareClient\Enums;
/** @deprecated */
class GroupingTypes
{
public const TOP_FRAME = 'topFrame';
public const EXCEPTION = 'exceptionClass';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Facade\FlareClient\Enums;
class MessageLevels
{
public const INFO = 'info';
public const DEBUG = 'debug';
public const WARNING = 'warning';
public const ERROR = 'error';
public const CRITICAL = 'critical';
}

View File

@@ -0,0 +1,319 @@
<?php
namespace Facade\FlareClient;
use Error;
use ErrorException;
use Exception;
use Facade\FlareClient\Concerns\HasContext;
use Facade\FlareClient\Context\ContextContextDetector;
use Facade\FlareClient\Context\ContextDetectorInterface;
use Facade\FlareClient\Enums\MessageLevels;
use Facade\FlareClient\Glows\Glow;
use Facade\FlareClient\Glows\Recorder;
use Facade\FlareClient\Http\Client;
use Facade\FlareClient\Middleware\AddGlows;
use Facade\FlareClient\Middleware\AnonymizeIp;
use Facade\FlareClient\Middleware\CensorRequestBodyFields;
use Illuminate\Contracts\Container\Container;
use Illuminate\Pipeline\Pipeline;
use Throwable;
class Flare
{
use HasContext;
/** @var \Facade\FlareClient\Http\Client */
protected $client;
/** @var \Facade\FlareClient\Api */
protected $api;
/** @var array */
protected $middleware = [];
/** @var \Facade\FlareClient\Glows\Recorder */
protected $recorder;
/** @var string */
protected $applicationPath;
/** @var \Illuminate\Contracts\Container\Container|null */
protected $container;
/** @var ContextDetectorInterface */
protected $contextDetector;
/** @var callable|null */
protected $previousExceptionHandler;
/** @var callable|null */
protected $previousErrorHandler;
/** @var callable|null */
protected $determineVersionCallable;
/** @var int|null */
protected $reportErrorLevels;
/** @var callable|null */
protected $filterExceptionsCallable;
public static function register(string $apiKey, string $apiSecret = null, ContextDetectorInterface $contextDetector = null, Container $container = null)
{
$client = new Client($apiKey, $apiSecret);
return new static($client, $contextDetector, $container);
}
public function determineVersionUsing($determineVersionCallable)
{
$this->determineVersionCallable = $determineVersionCallable;
}
public function reportErrorLevels(int $reportErrorLevels)
{
$this->reportErrorLevels = $reportErrorLevels;
}
public function filterExceptionsUsing(callable $filterExceptionsCallable)
{
$this->filterExceptionsCallable = $filterExceptionsCallable;
}
/**
* @return null|string
*/
public function version()
{
if (! $this->determineVersionCallable) {
return null;
}
return ($this->determineVersionCallable)();
}
public function __construct(Client $client, ContextDetectorInterface $contextDetector = null, Container $container = null, array $middleware = [])
{
$this->client = $client;
$this->recorder = new Recorder();
$this->contextDetector = $contextDetector ?? new ContextContextDetector();
$this->container = $container;
$this->middleware = $middleware;
$this->api = new Api($this->client);
$this->registerDefaultMiddleware();
}
public function getMiddleware(): array
{
return $this->middleware;
}
public function registerFlareHandlers()
{
$this->registerExceptionHandler();
$this->registerErrorHandler();
return $this;
}
public function registerExceptionHandler()
{
$this->previousExceptionHandler = set_exception_handler([$this, 'handleException']);
return $this;
}
public function registerErrorHandler()
{
$this->previousErrorHandler = set_error_handler([$this, 'handleError']);
return $this;
}
private function registerDefaultMiddleware()
{
return $this->registerMiddleware(new AddGlows($this->recorder));
}
public function registerMiddleware($callable)
{
$this->middleware[] = $callable;
return $this;
}
public function getMiddlewares(): array
{
return $this->middleware;
}
public function glow(
string $name,
string $messageLevel = MessageLevels::INFO,
array $metaData = []
) {
$this->recorder->record(new Glow($name, $messageLevel, $metaData));
}
public function handleException(Throwable $throwable)
{
$this->report($throwable);
if ($this->previousExceptionHandler) {
call_user_func($this->previousExceptionHandler, $throwable);
}
}
public function handleError($code, $message, $file = '', $line = 0)
{
$exception = new ErrorException($message, 0, $code, $file, $line);
$this->report($exception);
if ($this->previousErrorHandler) {
return call_user_func(
$this->previousErrorHandler,
$message,
$code,
$file,
$line
);
}
}
public function applicationPath(string $applicationPath)
{
$this->applicationPath = $applicationPath;
return $this;
}
public function report(Throwable $throwable, callable $callback = null): ?Report
{
if (! $this->shouldSendReport($throwable)) {
return null;
}
$report = $this->createReport($throwable);
if (! is_null($callback)) {
call_user_func($callback, $report);
}
$this->sendReportToApi($report);
return $report;
}
protected function shouldSendReport(Throwable $throwable): bool
{
if ($this->reportErrorLevels && $throwable instanceof Error) {
return $this->reportErrorLevels & $throwable->getCode();
}
if ($this->reportErrorLevels && $throwable instanceof ErrorException) {
return $this->reportErrorLevels & $throwable->getSeverity();
}
if ($this->filterExceptionsCallable && $throwable instanceof Exception) {
return call_user_func($this->filterExceptionsCallable, $throwable);
}
return true;
}
public function reportMessage(string $message, string $logLevel, callable $callback = null)
{
$report = $this->createReportFromMessage($message, $logLevel);
if (! is_null($callback)) {
call_user_func($callback, $report);
}
$this->sendReportToApi($report);
}
public function sendTestReport(Throwable $throwable)
{
$this->api->sendTestReport($this->createReport($throwable));
}
private function sendReportToApi(Report $report)
{
try {
$this->api->report($report);
} catch (Exception $exception) {
}
}
public function reset()
{
$this->api->sendQueuedReports();
$this->userProvidedContext = [];
$this->recorder->reset();
}
private function applyAdditionalParameters(Report $report)
{
$report
->stage($this->stage)
->messageLevel($this->messageLevel)
->setApplicationPath($this->applicationPath)
->userProvidedContext($this->userProvidedContext);
}
public function anonymizeIp()
{
$this->registerMiddleware(new AnonymizeIp());
return $this;
}
public function censorRequestBodyFields(array $fieldNames)
{
$this->registerMiddleware(new CensorRequestBodyFields($fieldNames));
return $this;
}
public function createReport(Throwable $throwable): Report
{
$report = Report::createForThrowable(
$throwable,
$this->contextDetector->detectCurrentContext(),
$this->applicationPath,
$this->version()
);
return $this->applyMiddlewareToReport($report);
}
public function createReportFromMessage(string $message, string $logLevel): Report
{
$report = Report::createForMessage(
$message,
$logLevel,
$this->contextDetector->detectCurrentContext(),
$this->applicationPath
);
return $this->applyMiddlewareToReport($report);
}
protected function applyMiddlewareToReport(Report $report): Report
{
$this->applyAdditionalParameters($report);
$report = (new Pipeline($this->container))
->send($report)
->through($this->middleware)
->then(function ($report) {
return $report;
});
return $report;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Facade\FlareClient;
use Facade\FlareClient\Stacktrace\Codesnippet;
class Frame
{
/** @var string */
protected $file;
/** @var int */
protected $lineNumber;
/** @var string */
protected $method;
/** @var string */
protected $class;
public function __construct(
string $file,
int $lineNumber,
string $method = null,
string $class = null
) {
$this->file = $file;
$this->lineNumber = $lineNumber;
$this->method = $method;
$this->class = $class;
}
public function toArray(): array
{
$codeSnippet = (new Codesnippet())
->snippetLineCount(9)
->surroundingLine($this->lineNumber)
->get($this->file);
return [
'line_number' => $this->lineNumber,
'method' => $this->getFullMethod(),
'code_snippet' => $codeSnippet,
'file' => $this->file,
];
}
private function getFullMethod(): string
{
$method = $this->method;
if ($class = $this->class ?? false) {
$method = "{$class}::{$method}";
}
return $method;
}
public function getFile(): string
{
return $this->file;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Facade\FlareClient\Glows;
use Facade\FlareClient\Concerns\UsesTime;
use Facade\FlareClient\Enums\MessageLevels;
class Glow
{
use UsesTime;
/** @var string */
private $name;
/** @var array */
private $metaData;
/** @var string */
private $messageLevel;
/** @var float */
private $microtime;
public function __construct(string $name, string $messageLevel = MessageLevels::INFO, array $metaData = [], ?float $microtime = null)
{
$this->name = $name;
$this->messageLevel = $messageLevel;
$this->metaData = $metaData;
$this->microtime = $microtime ?? microtime(true);
}
public function toArray()
{
return [
'time' => $this->getCurrentTime(),
'name' => $this->name,
'message_level' => $this->messageLevel,
'meta_data' => $this->metaData,
'microtime' => $this->microtime,
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Facade\FlareClient\Glows;
class Recorder
{
public const GLOW_LIMIT = 30;
private $glows = [];
public function record(Glow $glow)
{
$this->glows[] = $glow;
$this->glows = array_slice($this->glows, static::GLOW_LIMIT * -1, static::GLOW_LIMIT);
}
public function glows(): array
{
return $this->glows;
}
public function reset()
{
$this->glows = [];
}
}

View File

@@ -0,0 +1,217 @@
<?php
namespace Facade\FlareClient\Http;
use Facade\FlareClient\Http\Exceptions\BadResponseCode;
use Facade\FlareClient\Http\Exceptions\InvalidData;
use Facade\FlareClient\Http\Exceptions\MissingParameter;
use Facade\FlareClient\Http\Exceptions\NotFound;
class Client
{
/** @var null|string */
private $apiToken;
/** @var null|string */
private $apiSecret;
/** @var string */
private $baseUrl;
/** @var int */
private $timeout;
public function __construct(
?string $apiToken,
?string $apiSecret,
string $baseUrl = 'https://reporting.flareapp.io/api',
int $timeout = 10
) {
$this->apiToken = $apiToken;
$this->apiSecret = $apiSecret;
if (! $baseUrl) {
throw MissingParameter::create('baseUrl');
}
$this->baseUrl = $baseUrl;
if (! $timeout) {
throw MissingParameter::create('timeout');
}
$this->timeout = $timeout;
}
/**
* @param string $url
* @param array $arguments
*
* @return array|false
*/
public function get(string $url, array $arguments = [])
{
return $this->makeRequest('get', $url, $arguments);
}
/**
* @param string $url
* @param array $arguments
*
* @return array|false
*/
public function post(string $url, array $arguments = [])
{
return $this->makeRequest('post', $url, $arguments);
}
/**
* @param string $url
* @param array $arguments
*
* @return array|false
*/
public function patch(string $url, array $arguments = [])
{
return $this->makeRequest('patch', $url, $arguments);
}
/**
* @param string $url
* @param array $arguments
*
* @return array|false
*/
public function put(string $url, array $arguments = [])
{
return $this->makeRequest('put', $url, $arguments);
}
/**
* @param string $method
* @param array $arguments
*
* @return array|false
*/
public function delete(string $method, array $arguments = [])
{
return $this->makeRequest('delete', $method, $arguments);
}
/**
* @param string $httpVerb
* @param string $url
* @param array $arguments
*
* @return array
*/
private function makeRequest(string $httpVerb, string $url, array $arguments = [])
{
$queryString = http_build_query([
'key' => $this->apiToken,
'secret' => $this->apiSecret,
]);
$fullUrl = "{$this->baseUrl}/{$url}?{$queryString}";
$headers = [
'x-api-token: '.$this->apiToken,
];
$response = $this->makeCurlRequest($httpVerb, $fullUrl, $headers, $arguments);
if ($response->getHttpResponseCode() === 422) {
throw InvalidData::createForResponse($response);
}
if ($response->getHttpResponseCode() === 404) {
throw NotFound::createForResponse($response);
}
if ($response->getHttpResponseCode() !== 200 && $response->getHttpResponseCode() !== 204) {
throw BadResponseCode::createForResponse($response);
}
return $response->getBody();
}
public function makeCurlRequest(string $httpVerb, string $fullUrl, array $headers = [], array $arguments = []): Response
{
$curlHandle = $this->getCurlHandle($fullUrl, $headers);
switch ($httpVerb) {
case 'post':
curl_setopt($curlHandle, CURLOPT_POST, true);
$this->attachRequestPayload($curlHandle, $arguments);
break;
case 'get':
curl_setopt($curlHandle, CURLOPT_URL, $fullUrl.'&'.http_build_query($arguments));
break;
case 'delete':
curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
case 'patch':
curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PATCH');
$this->attachRequestPayload($curlHandle, $arguments);
break;
case 'put':
curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT');
$this->attachRequestPayload($curlHandle, $arguments);
break;
}
$body = json_decode(curl_exec($curlHandle), true);
$headers = curl_getinfo($curlHandle);
$error = curl_error($curlHandle);
return new Response($headers, $body, $error);
}
private function attachRequestPayload(&$curlHandle, array $data)
{
$encoded = json_encode($data);
$this->lastRequest['body'] = $encoded;
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $encoded);
}
/**
* @param string $fullUrl
* @param array $headers
*
* @return resource
*/
private function getCurlHandle(string $fullUrl, array $headers = [])
{
$curlHandle = curl_init();
curl_setopt($curlHandle, CURLOPT_URL, $fullUrl);
curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge([
'Accept: application/json',
'Content-Type: application/json',
], $headers));
curl_setopt($curlHandle, CURLOPT_USERAGENT, 'Laravel/Flare API 1.0');
curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlHandle, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
curl_setopt($curlHandle, CURLOPT_ENCODING, '');
curl_setopt($curlHandle, CURLINFO_HEADER_OUT, true);
curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curlHandle, CURLOPT_MAXREDIRS, 1);
return $curlHandle;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Facade\FlareClient\Http\Exceptions;
use Exception;
use Facade\FlareClient\Http\Response;
class BadResponse extends Exception
{
/** @var \Facade\FlareClient\Http\Response */
public $response;
public static function createForResponse(Response $response)
{
$exception = new static("Could not perform request because: {$response->getError()}");
$exception->response = $response;
return $exception;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Facade\FlareClient\Http\Exceptions;
use Exception;
use Facade\FlareClient\Http\Response;
class BadResponseCode extends Exception
{
/** @var \Facade\FlareClient\Http\Response */
public $response;
/** @var array */
public $errors;
public static function createForResponse(Response $response)
{
$exception = new static(static::getMessageForResponse($response));
$exception->response = $response;
$bodyErrors = isset($response->getBody()['errors']) ? $response->getBody()['errors'] : [];
$exception->errors = $bodyErrors;
return $exception;
}
public static function getMessageForResponse(Response $response)
{
return "Response code {$response->getHttpResponseCode()} returned";
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Facade\FlareClient\Http\Exceptions;
use Facade\FlareClient\Http\Response;
class InvalidData extends BadResponseCode
{
public static function getMessageForResponse(Response $response)
{
return 'Invalid data found';
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Facade\FlareClient\Http\Exceptions;
use Exception;
class MissingParameter extends Exception
{
public static function create(string $parameterName)
{
return new static("`$parameterName` is a required parameter");
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Facade\FlareClient\Http\Exceptions;
use Facade\FlareClient\Http\Response;
class NotFound extends BadResponseCode
{
public static function getMessageForResponse(Response $response)
{
return 'Not found';
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Facade\FlareClient\Http;
class Response
{
private $headers;
private $body;
private $error;
public function __construct($headers, $body, $error)
{
$this->headers = $headers;
$this->body = $body;
$this->error = $error;
}
/**
* @return mixed
*/
public function getHeaders()
{
return $this->headers;
}
/**
* @return mixed
*/
public function getBody()
{
return $this->body;
}
/**
* @return bool
*/
public function hasBody()
{
return $this->body != false;
}
/**
* @return mixed
*/
public function getError()
{
return $this->error;
}
/**
* @return null|int
*/
public function getHttpResponseCode()
{
if (! isset($this->headers['http_code'])) {
return;
}
return (int) $this->headers['http_code'];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Facade\FlareClient\Middleware;
use Facade\FlareClient\Glows\Recorder;
use Facade\FlareClient\Report;
class AddGlows
{
/** @var Recorder */
private $recorder;
public function __construct(Recorder $recorder)
{
$this->recorder = $recorder;
}
public function handle(Report $report, $next)
{
foreach ($this->recorder->glows() as $glow) {
$report->addGlow($glow);
}
return $next($report);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Facade\FlareClient\Middleware;
use Facade\FlareClient\Report;
class AnonymizeIp
{
public function handle(Report $report, $next)
{
$context = $report->allContext();
$context['request']['ip'] = null;
$report->userProvidedContext($context);
return $next($report);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Facade\FlareClient\Middleware;
use Facade\FlareClient\Report;
class CensorRequestBodyFields
{
protected $fieldNames = [];
public function __construct(array $fieldNames)
{
$this->fieldNames = $fieldNames;
}
public function handle(Report $report, $next)
{
$context = $report->allContext();
foreach ($this->fieldNames as $fieldName) {
if (isset($context['request_data']['body'][$fieldName])) {
$context['request_data']['body'][$fieldName] = '<CENSORED>';
}
}
$report->userProvidedContext($context);
return $next($report);
}
}

View File

@@ -0,0 +1,335 @@
<?php
namespace Facade\FlareClient;
use Facade\FlareClient\Concerns\HasContext;
use Facade\FlareClient\Concerns\UsesTime;
use Facade\FlareClient\Context\ContextInterface;
use Facade\FlareClient\Contracts\ProvidesFlareContext;
use Facade\FlareClient\Enums\GroupingTypes;
use Facade\FlareClient\Glows\Glow;
use Facade\FlareClient\Solutions\ReportSolution;
use Facade\FlareClient\Stacktrace\Stacktrace;
use Facade\IgnitionContracts\Solution;
use Throwable;
class Report
{
use UsesTime;
use HasContext;
/** @var \Facade\FlareClient\Stacktrace\Stacktrace */
private $stacktrace;
/** @var string */
private $exceptionClass;
/** @var string */
private $message;
/** @var array */
private $glows = [];
/** @var array */
private $solutions = [];
/** @var ContextInterface */
private $context;
/** @var string */
private $applicationPath;
/** @var ?string */
private $applicationVersion;
/** @var array */
private $userProvidedContext = [];
/** @var array */
private $exceptionContext = [];
/** @var Throwable */
private $throwable;
/** @var string */
private $notifierName;
/** @var string */
private $languageVersion;
/** @var string */
private $frameworkVersion;
/** @var int */
private $openFrameIndex;
/** @var string */
private $groupBy ;
/** @var string */
private $trackingUuid;
/** @var null string|null */
public static $fakeTrackingUuid = null;
public static function createForThrowable(
Throwable $throwable,
ContextInterface $context,
?string $applicationPath = null,
?string $version = null
): self {
return (new static())
->setApplicationPath($applicationPath)
->throwable($throwable)
->useContext($context)
->exceptionClass(self::getClassForThrowable($throwable))
->message($throwable->getMessage())
->stackTrace(Stacktrace::createForThrowable($throwable, $applicationPath))
->exceptionContext($throwable)
->setApplicationVersion($version);
}
protected static function getClassForThrowable(Throwable $throwable): string
{
if ($throwable instanceof \Facade\Ignition\Exceptions\ViewException) {
if ($previous = $throwable->getPrevious()) {
return get_class($previous);
}
}
return get_class($throwable);
}
public static function createForMessage(string $message, string $logLevel, ContextInterface $context, ?string $applicationPath = null): self
{
$stacktrace = Stacktrace::create($applicationPath);
return (new static())
->setApplicationPath($applicationPath)
->message($message)
->useContext($context)
->exceptionClass($logLevel)
->stacktrace($stacktrace)
->openFrameIndex($stacktrace->firstApplicationFrameIndex());
}
public function __construct()
{
$this->trackingUuid = self::$fakeTrackingUuid ?? $this->generateUuid();
}
public function trackingUuid(): string
{
return $this->trackingUuid;
}
public function exceptionClass(string $exceptionClass)
{
$this->exceptionClass = $exceptionClass;
return $this;
}
public function getExceptionClass(): string
{
return $this->exceptionClass;
}
public function throwable(Throwable $throwable)
{
$this->throwable = $throwable;
return $this;
}
public function getThrowable(): ?Throwable
{
return $this->throwable;
}
public function message(string $message)
{
$this->message = $message;
return $this;
}
public function getMessage(): string
{
return $this->message;
}
public function stacktrace(Stacktrace $stacktrace)
{
$this->stacktrace = $stacktrace;
return $this;
}
public function getStacktrace(): Stacktrace
{
return $this->stacktrace;
}
public function notifierName(string $notifierName)
{
$this->notifierName = $notifierName;
return $this;
}
public function languageVersion(string $languageVersion)
{
$this->languageVersion = $languageVersion;
return $this;
}
public function frameworkVersion(string $frameworkVersion)
{
$this->frameworkVersion = $frameworkVersion;
return $this;
}
public function useContext(ContextInterface $request)
{
$this->context = $request;
return $this;
}
public function openFrameIndex(?int $index)
{
$this->openFrameIndex = $index;
return $this;
}
public function setApplicationPath(?string $applicationPath)
{
$this->applicationPath = $applicationPath;
return $this;
}
public function getApplicationPath(): ?string
{
return $this->applicationPath;
}
public function setApplicationVersion(?string $applicationVersion)
{
$this->applicationVersion = $applicationVersion;
return $this;
}
public function getApplicationVersion(): ?string
{
return $this->applicationVersion;
}
public function view(?View $view)
{
$this->view = $view;
return $this;
}
public function addGlow(Glow $glow)
{
$this->glows[] = $glow->toArray();
return $this;
}
public function addSolution(Solution $solution)
{
$this->solutions[] = ReportSolution::fromSolution($solution)->toArray();
return $this;
}
public function userProvidedContext(array $userProvidedContext)
{
$this->userProvidedContext = $userProvidedContext;
return $this;
}
/** @deprecated */
public function groupByTopFrame()
{
$this->groupBy = GroupingTypes::TOP_FRAME;
return $this;
}
/** @deprecated */
public function groupByException()
{
$this->groupBy = GroupingTypes::EXCEPTION;
return $this;
}
public function allContext(): array
{
$context = $this->context->toArray();
$context = array_merge_recursive_distinct($context, $this->exceptionContext);
return array_merge_recursive_distinct($context, $this->userProvidedContext);
}
private function exceptionContext(Throwable $throwable)
{
if ($throwable instanceof ProvidesFlareContext) {
$this->exceptionContext = $throwable->context();
}
return $this;
}
public function toArray()
{
return [
'notifier' => $this->notifierName ?? 'Flare Client',
'language' => 'PHP',
'framework_version' => $this->frameworkVersion,
'language_version' => $this->languageVersion ?? phpversion(),
'exception_class' => $this->exceptionClass,
'seen_at' => $this->getCurrentTime(),
'message' => $this->message,
'glows' => $this->glows,
'solutions' => $this->solutions,
'stacktrace' => $this->stacktrace->toArray(),
'context' => $this->allContext(),
'stage' => $this->stage,
'message_level' => $this->messageLevel,
'open_frame_index' => $this->openFrameIndex,
'application_path' => $this->applicationPath,
'application_version' => $this->applicationVersion,
'tracking_uuid' => $this->trackingUuid,
];
}
/*
* Found on https://stackoverflow.com/questions/2040240/php-function-to-generate-v4-uuid/15875555#15875555
*/
private function generateUuid(): string
{
// Generate 16 bytes (128 bits) of random data or use the data passed into the function.
$data = $data ?? random_bytes(16);
assert(strlen($data) == 16);
// Set version to 0100
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
// Set bits 6-7 to 10
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
// Output the 36 character UUID.
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Facade\FlareClient\Solutions;
use Facade\IgnitionContracts\RunnableSolution;
use Facade\IgnitionContracts\Solution as SolutionContract;
class ReportSolution
{
/** @var SolutionContract */
protected $solution;
public function __construct(SolutionContract $solution)
{
$this->solution = $solution;
}
public static function fromSolution(SolutionContract $solution)
{
return new static($solution);
}
public function toArray(): array
{
$isRunnable = ($this->solution instanceof RunnableSolution);
return [
'class' => get_class($this->solution),
'title' => $this->solution->getSolutionTitle(),
'description' => $this->solution->getSolutionDescription(),
'links' => $this->solution->getDocumentationLinks(),
'action_description' => $isRunnable ? $this->solution->getSolutionActionDescription() : null,
'is_runnable' => $isRunnable,
];
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Facade\FlareClient\Stacktrace;
use RuntimeException;
class Codesnippet
{
/** @var int */
private $surroundingLine = 1;
/** @var int */
private $snippetLineCount = 9;
public function surroundingLine(int $surroundingLine): self
{
$this->surroundingLine = $surroundingLine;
return $this;
}
public function snippetLineCount(int $snippetLineCount): self
{
$this->snippetLineCount = $snippetLineCount;
return $this;
}
public function get(string $fileName): array
{
if (! file_exists($fileName)) {
return [];
}
try {
$file = new File($fileName);
[$startLineNumber, $endLineNumber] = $this->getBounds($file->numberOfLines());
$code = [];
$line = $file->getLine($startLineNumber);
$currentLineNumber = $startLineNumber;
while ($currentLineNumber <= $endLineNumber) {
$code[$currentLineNumber] = rtrim(substr($line, 0, 250));
$line = $file->getNextLine();
$currentLineNumber++;
}
return $code;
} catch (RuntimeException $exception) {
return [];
}
}
private function getBounds($totalNumberOfLineInFile): array
{
$startLine = max($this->surroundingLine - floor($this->snippetLineCount / 2), 1);
$endLine = $startLine + ($this->snippetLineCount - 1);
if ($endLine > $totalNumberOfLineInFile) {
$endLine = $totalNumberOfLineInFile;
$startLine = max($endLine - ($this->snippetLineCount - 1), 1);
}
return [$startLine, $endLine];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Facade\FlareClient\Stacktrace;
use SplFileObject;
class File
{
/** @var \SplFileObject */
private $file;
public function __construct(string $path)
{
$this->file = new SplFileObject($path);
}
public function numberOfLines(): int
{
$this->file->seek(PHP_INT_MAX);
return $this->file->key() + 1;
}
public function getLine(int $lineNumber = null): string
{
if (is_null($lineNumber)) {
return $this->getNextLine();
}
$this->file->seek($lineNumber - 1);
return $this->file->current();
}
public function getNextLine(): string
{
$this->file->next();
return $this->file->current();
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Facade\FlareClient\Stacktrace;
class Frame
{
/** @var string */
private $file;
/** @var int */
private $lineNumber;
/** @var string */
private $method;
/** @var string */
private $class;
/** @var bool */
private $isApplicationFrame;
public function __construct(
string $file,
int $lineNumber,
string $method = null,
string $class = null,
bool $isApplicationFrame = false
) {
$this->file = $file;
$this->lineNumber = $lineNumber;
$this->method = $method;
$this->class = $class;
$this->isApplicationFrame = $isApplicationFrame;
}
public function toArray(): array
{
$codeSnippet = (new Codesnippet())
->snippetLineCount(31)
->surroundingLine($this->lineNumber)
->get($this->file);
return [
'line_number' => $this->lineNumber,
'method' => $this->method,
'class' => $this->class,
'code_snippet' => $codeSnippet,
'file' => $this->file,
'is_application_frame' => $this->isApplicationFrame,
];
}
public function getFile(): string
{
return $this->file;
}
public function getLinenumber(): int
{
return $this->lineNumber;
}
public function isApplicationFrame()
{
return $this->isApplicationFrame;
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Facade\FlareClient\Stacktrace;
use Throwable;
class Stacktrace
{
/** @var \Facade\FlareClient\Stacktrace\Frame[] */
private $frames;
/** @var string */
private $applicationPath;
public static function createForThrowable(Throwable $throwable, ?string $applicationPath = null): self
{
return new static($throwable->getTrace(), $applicationPath, $throwable->getFile(), $throwable->getLine());
}
public static function create(?string $applicationPath = null): self
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS & ~DEBUG_BACKTRACE_PROVIDE_OBJECT);
return new static($backtrace, $applicationPath);
}
public function __construct(array $backtrace, ?string $applicationPath = null, string $topmostFile = null, string $topmostLine = null)
{
$this->applicationPath = $applicationPath;
$currentFile = $topmostFile;
$currentLine = $topmostLine;
foreach ($backtrace as $rawFrame) {
if (! $this->frameFromFlare($rawFrame) && ! $this->fileIgnored($currentFile)) {
$this->frames[] = new Frame(
$currentFile,
$currentLine,
$rawFrame['function'] ?? null,
$rawFrame['class'] ?? null,
$this->frameFileFromApplication($currentFile)
);
}
$currentFile = $rawFrame['file'] ?? 'unknown';
$currentLine = $rawFrame['line'] ?? 0;
}
$this->frames[] = new Frame(
$currentFile,
$currentLine,
'[top]'
);
}
protected function frameFromFlare(array $rawFrame): bool
{
return isset($rawFrame['class']) && strpos($rawFrame['class'], 'Facade\\FlareClient\\') === 0;
}
protected function frameFileFromApplication(string $frameFilename): bool
{
$relativeFile = str_replace('\\', DIRECTORY_SEPARATOR, $frameFilename);
if (! empty($this->applicationPath)) {
$relativeFile = array_reverse(explode($this->applicationPath ?? '', $frameFilename, 2))[0];
}
if (strpos($relativeFile, DIRECTORY_SEPARATOR . 'vendor') === 0) {
return false;
}
return true;
}
protected function fileIgnored(string $currentFile): bool
{
$currentFile = str_replace('\\', DIRECTORY_SEPARATOR, $currentFile);
$ignoredFiles = [
'/ignition/src/helpers.php',
];
foreach ($ignoredFiles as $ignoredFile) {
if (strstr($currentFile, $ignoredFile) !== false) {
return true;
}
}
return false;
}
public function firstFrame(): Frame
{
return $this->frames[0];
}
public function toArray(): array
{
return array_map(function (Frame $frame) {
return $frame->toArray();
}, $this->frames);
}
public function firstApplicationFrame(): ?Frame
{
foreach ($this->frames as $index => $frame) {
if ($frame->isApplicationFrame()) {
return $frame;
}
}
return null;
}
public function firstApplicationFrameIndex(): ?int
{
foreach ($this->frames as $index => $frame) {
if ($frame->isApplicationFrame()) {
return $index;
}
}
return null;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Facade\FlareClient\Time;
use DateTimeImmutable;
class SystemTime implements Time
{
public function getCurrentTime(): int
{
return (new DateTimeImmutable())->getTimestamp();
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Time;
interface Time
{
public function getCurrentTime(): int;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Facade\FlareClient\Truncation;
abstract class AbstractTruncationStrategy implements TruncationStrategy
{
/** @var ReportTrimmer */
protected $reportTrimmer;
public function __construct(ReportTrimmer $reportTrimmer)
{
$this->reportTrimmer = $reportTrimmer;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Facade\FlareClient\Truncation;
class ReportTrimmer
{
protected static $maxPayloadSize = 524288;
protected $strategies = [
TrimStringsStrategy::class,
TrimContextItemsStrategy::class,
];
public function trim(array $payload): array
{
foreach ($this->strategies as $strategy) {
if (! $this->needsToBeTrimmed($payload)) {
break;
}
$payload = (new $strategy($this))->execute($payload);
}
return $payload;
}
public function needsToBeTrimmed(array $payload): bool
{
return strlen(json_encode($payload)) > self::getMaxPayloadSize();
}
public static function getMaxPayloadSize(): int
{
return self::$maxPayloadSize;
}
public static function setMaxPayloadSize(int $maxPayloadSize): void
{
self::$maxPayloadSize = $maxPayloadSize;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Facade\FlareClient\Truncation;
class TrimContextItemsStrategy extends AbstractTruncationStrategy
{
public static function thresholds()
{
return [100, 50, 25, 10];
}
public function execute(array $payload): array
{
foreach (static::thresholds() as $threshold) {
if (! $this->reportTrimmer->needsToBeTrimmed($payload)) {
break;
}
$payload['context'] = $this->iterateContextItems($payload['context'], $threshold);
}
return $payload;
}
protected function iterateContextItems(array $contextItems, int $threshold): array
{
array_walk($contextItems, [$this, 'trimContextItems'], $threshold);
return $contextItems;
}
protected function trimContextItems(&$value, $key, int $threshold)
{
if (is_array($value)) {
if (count($value) > $threshold) {
$value = array_slice($value, $threshold * -1, $threshold);
}
array_walk($value, [$this, 'trimContextItems'], $threshold);
}
return $value;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Facade\FlareClient\Truncation;
class TrimStringsStrategy extends AbstractTruncationStrategy
{
public static function thresholds()
{
return [1024, 512, 256];
}
public function execute(array $payload): array
{
foreach (static::thresholds() as $threshold) {
if (! $this->reportTrimmer->needsToBeTrimmed($payload)) {
break;
}
$payload = $this->trimPayloadString($payload, $threshold);
}
return $payload;
}
protected function trimPayloadString(array $payload, int $threshold): array
{
array_walk_recursive($payload, function (&$value) use ($threshold) {
if (is_string($value) && strlen($value) > $threshold) {
$value = substr($value, 0, $threshold);
}
});
return $payload;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Truncation;
interface TruncationStrategy
{
public function execute(array $payload): array;
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Facade\FlareClient;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
class View
{
/** @var string */
private $file;
/** @var array */
private $data = [];
public function __construct(string $file, array $data = [])
{
$this->file = $file;
$this->data = $data;
}
public static function create(string $file, array $data = []): self
{
return new static($file, $data);
}
private function dumpViewData($variable): string
{
$cloner = new VarCloner();
$dumper = new HtmlDumper();
$dumper->setDumpHeader('');
$output = fopen('php://memory', 'r+b');
$dumper->dump($cloner->cloneVar($variable)->withMaxDepth(1), $output, [
'maxDepth' => 1,
'maxStringLength' => 160,
]);
return stream_get_contents($output, -1, 0);
}
public function toArray()
{
return [
'file' => $this->file,
'data' => array_map([$this, 'dumpViewData'], $this->data),
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
if (! function_exists('array_merge_recursive_distinct')) {
function array_merge_recursive_distinct(array &$array1, array &$array2)
{
$merged = $array1;
foreach ($array2 as $key => &$value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = array_merge_recursive_distinct($merged[$key], $value);
} else {
$merged[$key] = $value;
}
}
return $merged;
}
}