diff --git a/.env.dist b/.env.dist index 91e63d7..e1a5956 100755 --- a/.env.dist +++ b/.env.dist @@ -121,12 +121,12 @@ INSIGHTS_MAX_STORE_TIME=172800 ### # Error reporting ### -REPORTING=true +REPORTING=false REPORTING_DRIVER=sentry SENTRY_LARAVEL_DSN="https://examplePublicKey@o0.ingest.sentry.io/0" -SENTRY_TRACES_SAMPLE_RATE=1 +SENTRY_TRACES_SAMPLE_RATE=0.5 ### # Endpoints ### -DISABLE_USER_LISTS=false \ No newline at end of file +DISABLE_USER_LISTS=false diff --git a/app/Exceptions/GithubReport.php b/app/Exceptions/GithubReport.php index e13af7e..6a190d8 100755 --- a/app/Exceptions/GithubReport.php +++ b/app/Exceptions/GithubReport.php @@ -15,54 +15,54 @@ class GithubReport /** * @var string */ - private $name; + private string $name; /** * @var string */ - private $code; + private string $code; /** * @var string */ - private $error; + private string $error; /** * @var string */ - private $requestUri; + private string $requestUri; /** * @var string */ - private $requestMethod; + private string $requestMethod; /** * @var string */ - private $repo; + private string $repo; /** * @var string */ - private $trace; + private string $trace; /** * @var string */ - private $jikanVersion; + private string $jikanVersion; /** * @var string */ - private $redisRunning; + private string $redisRunning; /** * @var string */ - private $instanceType; + private string $instanceType; /** - * @var + * @var string */ - private $phpVersion; + private string $phpVersion; /** * @param \Exception $exception diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index da86c21..72dbe24 100755 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -3,10 +3,7 @@ namespace App\Exceptions; use App\Events\SourceHeartbeatEvent; -use App\Http\HttpHelper; use Exception; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ConnectException; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\JsonResponse; @@ -16,11 +13,9 @@ use Jikan\Exception\BadResponseException; use Jikan\Exception\ParserException; use Laravel\Lumen\Exceptions\Handler as ExceptionHandler; use Predis\Connection\ConnectionException; -use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\HttpClient\Exception\TimeoutException; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; -use Illuminate\Support\Facades\Cache; /** * Class Handler @@ -41,6 +36,13 @@ class Handler extends ExceptionHandler BadResponseException::class ]; + /** + * @var string[] + */ + protected array $acceptableForReportingDriver = [ + ParserException::class + ]; + /** * @param \Throwable $e * @throws Exception @@ -55,13 +57,10 @@ class Handler extends ExceptionHandler * @param \Throwable $e * @return JsonResponse|Response */ - public function render($request, \Throwable $e) + public function render($request, \Throwable $e): JsonResponse|Response { $githubReport = GithubReport::make($e, $request); - - if (app()->bound('sentry') && $this->shouldReport($e)) { - app('sentry')->captureException($e); - } + $this->reportToSentry($e); // ConnectionException from Redis server if ($e instanceof ConnectionException) { @@ -83,18 +82,6 @@ class Handler extends ExceptionHandler ], 500); } - if ($e instanceof ConnectException) { - event(new SourceHeartbeatEvent(SourceHeartbeatEvent::BAD_HEALTH, $e->getCode())); - - return response() - ->json([ - 'status' => $e->getCode(), - 'type' => 'BadResponseException', - 'message' => 'Jikan failed to connect to MyAnimeList.net. MyAnimeList.net may be down/unavailable, refuses to connect or took too long to respond.', - 'error' => $e->getMessage() - ], 503); - } - // ParserException from Jikan PHP API if ($e instanceof ParserException) { $githubReport->setRepo(env('GITHUB_API', 'jikan-me/jikan')); @@ -108,9 +95,9 @@ class Handler extends ExceptionHandler ], 500); } - // BadResponseException from Guzzle dep via Jikan PHP API + // BadResponseException from Jikan PHP API // This is basically the response MyAnimeList returns to Jikan - if ($e instanceof BadResponseException || $e instanceof ClientException) { + if ($e instanceof BadResponseException) { switch ($e->getCode()) { case 404: // $this->set404Cache($request, $e); @@ -157,6 +144,28 @@ class Handler extends ExceptionHandler } } + if ($e instanceof TimeoutException) { + return response() + ->json([ + 'status' => 408, + 'type' => 'TimeoutException', + 'message' => 'Request to MyAnimeList.net timed out (' .env('SOURCE_TIMEOUT', 5) . ' seconds)', + 'error' => $e->getMessage() + ], 408); + } + + if ($e instanceof Exception && $e->getMessage() === "Undefined index: url") { + event(new SourceHeartbeatEvent(SourceHeartbeatEvent::BAD_HEALTH, $e->getCode())); + + return response() + ->json([ + 'status' => $e->getCode(), + 'type' => 'BadResponseException', + 'message' => 'Jikan failed to connect to MyAnimeList.net. MyAnimeList.net may be down/unavailable, refuses to connect or took too long to respond. Retry the request!', + 'error' => $e->getMessage() + ], 503); + } + // Bad REST API requests if ($e instanceof HttpException) { return response() @@ -168,71 +177,32 @@ class Handler extends ExceptionHandler ], $e->getStatusCode()); } - if ($e instanceof TimeoutException) { - return response() - ->json([ - 'status' => 408, - 'type' => 'TimeoutException', - 'message' => 'Request to MyAnimeList.net timed out (' .env('SOURCE_TIMEOUT', 5) . ' seconds)', - 'error' => $e->getMessage() - ], 408); - } - - if ($e instanceof Exception) { - if ($e->getMessage() === "Undefined index: url") { - event(new SourceHeartbeatEvent(SourceHeartbeatEvent::BAD_HEALTH, $e->getCode())); - - return response() - ->json([ - 'status' => $e->getCode(), - 'type' => 'BadResponseException', - 'message' => 'Jikan failed to connect to MyAnimeList.net. MyAnimeList.net may be down/unavailable, refuses to connect or took too long to respond. Retry the request!', - 'error' => $e->getMessage() - ], 503); - } - - - return response() - ->json([ - 'status' => 500, - 'type' => "Exception", - 'message' => 'Unhandled Exception. Please follow report_url to generate an issue on GitHub', - 'trace' => "{$e->getFile()} at line {$e->getLine()}", - 'error' => $e->getMessage(), - 'report_url' => env('GITHUB_REPORTING', true) ? (string) $githubReport : null - ], 500); - } + return response() + ->json([ + 'status' => 500, + 'type' => "Exception", + 'message' => 'Unhandled Exception. Please follow report_url to generate an issue on GitHub', + 'trace' => "{$e->getFile()} at line {$e->getLine()}", + 'error' => $e->getMessage(), + 'report_url' => env('GITHUB_REPORTING', true) ? (string) $githubReport : null + ], 500); } + /** - * @param Request $request - * @param BadResponseException $e + * @param Exception|\Throwable $e + * @return void */ - private function set404Cache(Request $request, BadResponseException $e) + private function reportToSentry(\Exception|\Throwable $e): void { - if (!env('CACHING') || env('MICROCACHING')) { + if (!app()->bound('sentry')) { return; } - $fingerprint = "request:404:".sha1(env('APP_URL') . $request->getRequestUri()); - - if (Cache::has($fingerprint)) { - return; + foreach ($this->acceptableForReportingDriver as $type) { + if ($e instanceof $type) { + app('sentry')->captureException($e); + } } - - $routeController = HttpHelper::requestControllerName($request); - $cacheTtl = env('CACHE_DEFAULT_EXPIRE', 86400); - - if (\in_array($routeController, [ - 'AnimeController', - 'MangaController', - 'CharacterController', - 'PersonController' - ])) { - $cacheTtl = env('CACHE_404_EXPIRE', 604800); - } - - - Cache::put($fingerprint, $e->getMessage(), $cacheTtl); } } diff --git a/app/Http/QueryBuilder/TopQueryBuilderManga.php b/app/Http/QueryBuilder/TopQueryBuilderManga.php index 45c6cf4..738b33c 100644 --- a/app/Http/QueryBuilder/TopQueryBuilderManga.php +++ b/app/Http/QueryBuilder/TopQueryBuilderManga.php @@ -1,38 +1,39 @@ 'Manga', 'novels' => 'Novel', + 'lightnovels' => 'Light Novel', 'oneshots' => 'One-shot', 'doujin' => 'Doujinshi', 'manhwa' => 'Manhwa', 'manhua' => 'Manhua' ]; - + /** * */ const MAP_FILTER = [ 'publishing', 'upcoming', 'bypopularity', 'favorite' ]; - + /** * @param Request $request * @param Builder $builder @@ -42,15 +43,15 @@ { $mangaType = self::mapType($request->get('type')); $filterType = self::mapFilter($request->get('filter')); - + $results = $results ->where('type', '!=', 'Doujinshi'); - + if (!is_null($mangaType)) { $results = $results ->where('type', $mangaType); } - + if (!is_null($filterType) && $filterType === 'publishing') { $results = $results ->where('publishing', true) @@ -60,14 +61,14 @@ return $results; } - + if (!is_null($filterType) && $filterType === 'bypopularity') { $results = $results ->orderBy('members', 'desc'); return $results; } - + if (!is_null($filterType) && $filterType === 'favorite') { $results = $results ->orderBy('favorites', 'desc'); @@ -82,7 +83,7 @@ return $results; } - + /** * @param string|null $type * @return string|null @@ -92,12 +93,12 @@ if (is_null($type)) { return null; } - + $type = strtolower($type); - + return self::MAP_TYPES[$type] ?? null; } - + /** * @param string|null $filter * @return string|null @@ -105,11 +106,11 @@ public static function mapFilter(?string $filter = null) : ?string { $filter = strtolower($filter); - + if (!\in_array($filter, self::MAP_FILTER)) { return null; } - + return $filter; } - } \ No newline at end of file + } diff --git a/app/Http/Resources/V4/MangaFullResource.php b/app/Http/Resources/V4/MangaFullResource.php index 8b4e8c1..65ab72a 100644 --- a/app/Http/Resources/V4/MangaFullResource.php +++ b/app/Http/Resources/V4/MangaFullResource.php @@ -70,7 +70,7 @@ class MangaFullResource extends JsonResource * @OA\Property( * property="type", * type="string", - * enum={"Manga", "Novel", "One-shot", "Doujinshi", "Manhua", "Manhwa", "OEL"}, + * enum={"Manga", "Novel", "Light Novel", "One-shot", "Doujinshi", "Manhua", "Manhwa", "OEL"}, * description="Manga Type", * nullable=true * ), @@ -105,12 +105,14 @@ class MangaFullResource extends JsonResource * property="score", * type="number", * format="float", - * description="Score" + * description="Score", + * nullable=true * ), * @OA\Property( * property="scored_by", * type="integer", - * description="Number of users" + * description="Number of users", + * nullable=true * ), * @OA\Property( * property="rank", diff --git a/app/Http/Resources/V4/MangaResource.php b/app/Http/Resources/V4/MangaResource.php index fd75a1c..986e5a9 100644 --- a/app/Http/Resources/V4/MangaResource.php +++ b/app/Http/Resources/V4/MangaResource.php @@ -61,7 +61,7 @@ class MangaResource extends JsonResource * @OA\Property( * property="type", * type="string", - * enum={"Manga", "Novel", "One-shot", "Doujinshi", "Manhua", "Manhwa", "OEL"}, + * enum={"Manga", "Novel", "Light Novel", "One-shot", "Doujinshi", "Manhua", "Manhwa", "OEL"}, * description="Manga Type", * nullable=true * ), @@ -96,12 +96,14 @@ class MangaResource extends JsonResource * property="score", * type="number", * format="float", - * description="Score" + * description="Score", + * nullable=true * ), * @OA\Property( * property="scored_by", * type="integer", - * description="Number of users" + * description="Number of users", + * nullable=true * ), * @OA\Property( * property="rank", diff --git a/app/Http/Resources/V4/UserUpdatesResource.php b/app/Http/Resources/V4/UserUpdatesResource.php index 2b93064..32bb40a 100644 --- a/app/Http/Resources/V4/UserUpdatesResource.php +++ b/app/Http/Resources/V4/UserUpdatesResource.php @@ -85,7 +85,8 @@ class UserUpdatesResource extends JsonResource * @OA\Property( * property="score", * type="integer", - * description="User Score" + * description="User Score", + * nullable=true * ), * @OA\Property( * property="status", @@ -127,4 +128,4 @@ class UserUpdatesResource extends JsonResource { return $this['users']; } -} \ No newline at end of file +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 7d5d8fc..c59ee1e 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -117,6 +117,11 @@ if (env('REPORTING') && env('REPORTING_DRIVER') === 'sentry') { $app->register(\Sentry\Laravel\ServiceProvider::class); // Sentry Performance Monitoring (optional) $app->register(\Sentry\Laravel\Tracing\ServiceProvider::class); + + \Sentry\configureScope(function (\Sentry\State\Scope $scope): void { + $scope->setTag('rest.jikan.version', env('APP_VERSION')); + $scope->setTag('parser.jikan.version', JIKAN_PARSER_VERSION); + }); } // Guzzle removed as of lumen 8.x diff --git a/config/sentry.php b/config/sentry.php index 0f2c2e1..5d5a8ff 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -8,7 +8,7 @@ return [ // 'release' => trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')), // When left empty or `null` the Laravel environment will be used - 'environment' => env('SENTRY_ENVIRONMENT'), + 'environment' => env('APP_ENV'), 'breadcrumbs' => [ // Capture Laravel logs in breadcrumbs @@ -54,4 +54,6 @@ return [ 'controllers_base_namespace' => env('SENTRY_CONTROLLERS_BASE_NAMESPACE', 'App\\Http\\Controllers'), + 'release' => env('APP_VERSION') + ]; diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json index 96bd547..3731e4f 100644 --- a/storage/api-docs/api-docs.json +++ b/storage/api-docs/api-docs.json @@ -6774,6 +6774,7 @@ "enum": [ "Manga", "Novel", + "Light Novel", "One-shot", "Doujinshi", "Manhua", @@ -6813,11 +6814,13 @@ "score": { "description": "Score", "type": "number", - "format": "float" + "format": "float", + "nullable": true }, "scored_by": { "description": "Number of users", - "type": "integer" + "type": "integer", + "nullable": true }, "rank": { "description": "Ranking", @@ -6968,6 +6971,7 @@ "enum": [ "Manga", "Novel", + "Light Novel", "One-shot", "Doujinshi", "Manhua", @@ -7007,11 +7011,13 @@ "score": { "description": "Score", "type": "number", - "format": "float" + "format": "float", + "nullable": true }, "scored_by": { "description": "Number of users", - "type": "integer" + "type": "integer", + "nullable": true }, "rank": { "description": "Ranking", @@ -8708,7 +8714,8 @@ }, "score": { "description": "User Score", - "type": "integer" + "type": "integer", + "nullable": true }, "status": { "description": "User list status",