├── public ├── favicon.ico └── mix-manifest.json ├── src ├── Contracts │ ├── ClearableRepository.php │ ├── TerminableRepository.php │ ├── PrunableRepository.php │ └── EntriesRepository.php ├── FormatModel.php ├── Http │ ├── Middleware │ │ └── Authorize.php │ ├── Controllers │ │ ├── MailHtmlController.php │ │ ├── LogController.php │ │ ├── MailController.php │ │ ├── CacheController.php │ │ ├── GatesController.php │ │ ├── RedisController.php │ │ ├── ViewsController.php │ │ ├── EventsController.php │ │ ├── ModelsController.php │ │ ├── QueriesController.php │ │ ├── CommandsController.php │ │ ├── RequestsController.php │ │ ├── HomeController.php │ │ ├── ScheduleController.php │ │ ├── NotificationsController.php │ │ ├── MailEmlController.php │ │ ├── RecordingController.php │ │ ├── QueueController.php │ │ ├── MonitoredTagController.php │ │ ├── ExceptionController.php │ │ ├── EntryController.php │ │ └── DumpController.php │ └── routes.php ├── Watchers │ ├── FormatsClosure.php │ ├── Watcher.php │ ├── FetchesStackTrace.php │ ├── CommandWatcher.php │ ├── LogWatcher.php │ ├── DumpWatcher.php │ ├── ModelWatcher.php │ ├── GateWatcher.php │ ├── ScheduleWatcher.php │ ├── RedisWatcher.php │ ├── NotificationWatcher.php │ ├── ExceptionWatcher.php │ ├── MailWatcher.php │ ├── QueryWatcher.php │ ├── CacheWatcher.php │ ├── EventWatcher.php │ └── JobWatcher.php ├── EntryType.php ├── ExtractsMailableTags.php ├── Storage │ ├── factories │ │ └── EntryModelFactory.php │ ├── migrations │ │ └── 2018_08_08_100000_create_telescope_entries_table.php │ └── EntryQueryOptions.php ├── Console │ ├── ClearCommand.php │ ├── PruneCommand.php │ ├── PublishCommand.php │ └── InstallCommand.php ├── AuthorizesRequests.php ├── ExtractProperties.php ├── RegistersWatchers.php ├── Avatar.php ├── TelescopeApplicationServiceProvider.php ├── ExceptionContext.php ├── IncomingExceptionEntry.php ├── IncomingDumpEntry.php ├── EntryUpdate.php ├── ListensForStorageOpportunities.php ├── EntryResult.php └── TelescopeServiceProvider.php ├── resources ├── js │ ├── components │ │ ├── ExceptionCodePreview.vue │ │ ├── Stacktrace.vue │ │ └── Alert.vue │ ├── screens │ │ ├── redis │ │ │ ├── preview.vue │ │ │ └── index.vue │ │ ├── notifications │ │ │ ├── preview.vue │ │ │ └── index.vue │ │ ├── schedule │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ ├── commands │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ ├── models │ │ │ ├── preview.vue │ │ │ └── index.vue │ │ ├── events │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ ├── views │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ ├── cache │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ ├── gates │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ ├── queries │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ ├── logs │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ ├── jobs │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ ├── mail │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ ├── requests │ │ │ ├── index.vue │ │ │ └── preview.vue │ │ └── exceptions │ │ │ ├── index.vue │ │ │ └── preview.vue │ ├── mixins │ │ └── entriesStyles.js │ ├── app.js │ ├── base.js │ └── routes.js └── sass │ ├── syntaxhighlight.scss │ ├── app.scss │ └── app-dark.scss ├── LICENSE.md ├── webpack.mix.js ├── composer.json ├── package.json ├── stubs └── TelescopeServiceProvider.stub └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctfang/telescope/3.x/public/favicon.ico -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=b0a54caa17bc0ac7c25c", 3 | "/app.css": "/app.css?id=11fa83493c95c672325c", 4 | "/app-dark.css": "/app-dark.css?id=9f68f3c353c3417fd043" 5 | } 6 | -------------------------------------------------------------------------------- /src/Contracts/ClearableRepository.php: -------------------------------------------------------------------------------- 1 | getKey())); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Middleware/Authorize.php: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | props: ['lines', 'highlightedLine'], 4 | } 5 | 6 | 7 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/Http/Controllers/MailHtmlController.php: -------------------------------------------------------------------------------- 1 | find($id)->content['html']; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Controllers/LogController.php: -------------------------------------------------------------------------------- 1 | Telescope::$useDarkTheme ? 'app-dark.css' : 'app.css', 19 | 'telescopeScriptVariables' => Telescope::scriptVariables(), 20 | 'assetsAreCurrent' => Telescope::assetsAreCurrent(), 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Http/Controllers/ScheduleController.php: -------------------------------------------------------------------------------- 1 | getFileName(), 24 | $listener->getStartLine(), 25 | $listener->getEndLine() 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/EntryType.php: -------------------------------------------------------------------------------- 1 | options = $options; 23 | } 24 | 25 | /** 26 | * Register the watcher. 27 | * 28 | * @param \Illuminate\Contracts\Foundation\Application $app 29 | * @return void 30 | */ 31 | abstract public function register($app); 32 | } 33 | -------------------------------------------------------------------------------- /src/ExtractsMailableTags.php: -------------------------------------------------------------------------------- 1 | ExtractTags::from($mailable), 20 | '__telescope_mailable' => get_class($mailable), 21 | '__telescope_queued' => in_array(ShouldQueue::class, class_implements($mailable)), 22 | ]; 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Controllers/MailEmlController.php: -------------------------------------------------------------------------------- 1 | find($id)->content['raw'], 200, [ 20 | 'Content-Type' => 'message/rfc822', 21 | 'Content-Disposition' => 'attachment; filename="mail-'.$id.'.eml"', 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Storage/factories/EntryModelFactory.php: -------------------------------------------------------------------------------- 1 | define(EntryModel::class, function (Faker\Generator $faker) { 8 | return [ 9 | 'sequence' => random_int(1, 10000), 10 | 'uuid' => $faker->uuid, 11 | 'batch_id' => $faker->uuid, 12 | 'type' => $faker->randomElement([ 13 | EntryType::CACHE, EntryType::COMMAND, EntryType::DUMP, EntryType::EVENT, EntryType::EXCEPTION, 14 | EntryType::JOB, EntryType::LOG, EntryType::MAIL, EntryType::MODEL, EntryType::NOTIFICATION, 15 | EntryType::QUERY, EntryType::REDIS, EntryType::REQUEST, EntryType::SCHEDULED_TASK, 16 | ]), 17 | 'content' => [$faker->word => $faker->word], 18 | ]; 19 | }); 20 | -------------------------------------------------------------------------------- /src/Console/ClearCommand.php: -------------------------------------------------------------------------------- 1 | clear(); 33 | 34 | $this->info('Telescope entries cleared!'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/AuthorizesRequests.php: -------------------------------------------------------------------------------- 1 | environment('local'); 37 | })($request); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Console/PruneCommand.php: -------------------------------------------------------------------------------- 1 | info($repository->prune(now()->subHours($this->option('hours'))).' entries pruned.'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Console/PublishCommand.php: -------------------------------------------------------------------------------- 1 | call('vendor:publish', [ 31 | '--tag' => 'telescope-config', 32 | '--force' => $this->option('force'), 33 | ]); 34 | 35 | $this->call('vendor:publish', [ 36 | '--tag' => 'telescope-assets', 37 | '--force' => true, 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /resources/js/components/Stacktrace.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 38 | 39 | -------------------------------------------------------------------------------- /resources/sass/syntaxhighlight.scss: -------------------------------------------------------------------------------- 1 | .vjs-tree { 2 | // Fallback for missing fonts from vue-json-pretty on Ubuntu 3 | font-family: Monaco, Menlo, Consolas, Bitstream Vera Sans Mono, monospace !important; 4 | 5 | .vjs-tree__content { 6 | border-left: 1px dotted rgba(204, 204, 204, 0.28) !important; 7 | } 8 | .vjs-tree__node { 9 | cursor: pointer; 10 | &:hover { 11 | color: #20a0ff; 12 | } 13 | } 14 | .vjs-checkbox { 15 | position: absolute; 16 | left: -30px; 17 | } 18 | .vjs-value__null { 19 | color: #a291f5 !important; 20 | } 21 | .vjs-value__number, 22 | .vjs-value__boolean { 23 | color: #a291f5 !important; 24 | } 25 | .vjs-value__string { 26 | color: #dacb4d !important; 27 | } 28 | } 29 | 30 | .hljs-keyword, 31 | .hljs-selector-tag, 32 | .hljs-addition { 33 | color: #8bd72f; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-meta .hljs-meta-string, 38 | .hljs-doctag, 39 | .hljs-regexp { 40 | color: #dacb4d; 41 | } 42 | 43 | .hljs-number, 44 | .hljs-literal { 45 | color: #a291f5 !important; 46 | } 47 | -------------------------------------------------------------------------------- /src/Http/Controllers/RecordingController.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 26 | } 27 | 28 | /** 29 | * Toggle recording. 30 | * 31 | * @return void 32 | */ 33 | public function toggle() 34 | { 35 | if ($this->cache->get('telescope:pause-recording')) { 36 | $this->cache->forget('telescope:pause-recording'); 37 | } else { 38 | $this->cache->put('telescope:pause-recording', true, now()->addDays(30)); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Watchers/FetchesStackTrace.php: -------------------------------------------------------------------------------- 1 | forget(0); 17 | 18 | return $trace->first(function ($frame) { 19 | if (! isset($frame['file'])) { 20 | return false; 21 | } 22 | 23 | return ! Str::contains($frame['file'], 24 | base_path('vendor'.DIRECTORY_SEPARATOR.$this->ignoredVendorPath()) 25 | ); 26 | }); 27 | } 28 | 29 | /** 30 | * Choose the frame outside of either Telescope/Laravel or all packages. 31 | * 32 | * @return string|null 33 | */ 34 | protected function ignoredVendorPath() 35 | { 36 | if (! ($this->options['ignore_packages'] ?? true)) { 37 | return 'laravel'; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | $font-family-base: Nunito, sans-serif; 2 | 3 | $font-size-base: 0.95rem; 4 | $badge-font-size: 0.95rem; 5 | 6 | $primary: #4040c8; 7 | $secondary: #dae1e7; 8 | $success: #51d88a; 9 | $info: #bcdefa; 10 | $warning: #fff382; 11 | $danger: #ef5753; 12 | 13 | $body-bg: #edf1f3; 14 | 15 | $btn-focus-width: 0; 16 | 17 | $sidebar-nav-color: #2a5164; 18 | $sidebar-icon-color: #c3cbd3; 19 | 20 | $pill-link-active: $primary; 21 | 22 | $border-color: #efefef; 23 | $table-headers-color: #f3f4f6; 24 | $table-border-color: #efefef; 25 | $table-hover-bg: #f1f7fa; 26 | 27 | $header-border-color: #d5dfe9; 28 | 29 | $card-cap-bg: #fff; 30 | $card-bg-secondary: #f1f7fa; 31 | $card-shadow-color: #cdd8df; 32 | 33 | $code-bg: #120f12; 34 | 35 | $paginator-button-color: #9ea7ac; 36 | 37 | $new-entries-bg: #fffee9; 38 | 39 | $control-action-icon-color: #ccd2df; 40 | 41 | $grid-breakpoints: ( 42 | xs: 0, 43 | sm: 2px, 44 | md: 8px, 45 | lg: 9px, 46 | xl: 10px 47 | ) !default; 48 | 49 | $container-max-widths: ( 50 | sm: 1137px, 51 | md: 1138px, 52 | lg: 1139px, 53 | xl: 1140px 54 | ) !default; 55 | 56 | @import 'base'; 57 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | const webpack = require('webpack'); 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Mix Asset Management 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Mix provides a clean, fluent API for defining some Webpack build steps 10 | | for your Laravel application. By default, we are compiling the Sass 11 | | file for the application as well as bundling up all the JS files. 12 | | 13 | */ 14 | 15 | mix.options({ 16 | terser: { 17 | terserOptions: { 18 | compress: { 19 | drop_console: true, 20 | }, 21 | }, 22 | }, 23 | }) 24 | .setPublicPath('public') 25 | .js('resources/js/app.js', 'public') 26 | .sass('resources/sass/app.scss', 'public') 27 | .sass('resources/sass/app-dark.scss', 'public') 28 | .version() 29 | .copy('public', '../telescopetest/public/vendor/telescope') 30 | .webpackConfig({ 31 | resolve: { 32 | symlinks: false, 33 | alias: { 34 | '@': path.resolve(__dirname, 'resources/js/'), 35 | }, 36 | }, 37 | plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)], 38 | }); 39 | -------------------------------------------------------------------------------- /resources/js/screens/redis/preview.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | -------------------------------------------------------------------------------- /src/ExtractProperties.php: -------------------------------------------------------------------------------- 1 | getProperties()) 21 | ->mapWithKeys(function ($property) use ($target) { 22 | $property->setAccessible(true); 23 | 24 | if (($value = $property->getValue($target)) instanceof Model) { 25 | return [$property->getName() => FormatModel::given($value)]; 26 | } elseif (is_object($value)) { 27 | return [ 28 | $property->getName() => [ 29 | 'class' => get_class($value), 30 | 'properties' => json_decode(json_encode($value), true), 31 | ], 32 | ]; 33 | } else { 34 | return [$property->getName() => json_decode(json_encode($value), true)]; 35 | } 36 | })->toArray(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/RegistersWatchers.php: -------------------------------------------------------------------------------- 1 | $watcher) { 34 | if (is_string($key) && $watcher === false) { 35 | continue; 36 | } 37 | 38 | if (is_array($watcher) && ! ($watcher['enabled'] ?? true)) { 39 | continue; 40 | } 41 | 42 | $watcher = $app->make(is_string($key) ? $key : $watcher, [ 43 | 'options' => is_array($watcher) ? $watcher : [], 44 | ]); 45 | 46 | static::$watchers[] = get_class($watcher); 47 | 48 | $watcher->register($app); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /resources/js/screens/notifications/preview.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 42 | -------------------------------------------------------------------------------- /src/Http/Controllers/QueueController.php: -------------------------------------------------------------------------------- 1 | find($id); 32 | 33 | return response()->json([ 34 | 'entry' => $entry, 35 | 'batch' => isset($entry->content['updated_batch_id']) 36 | ? $storage->get(null, EntryQueryOptions::forBatchId($entry->content['updated_batch_id'])) 37 | : null, 38 | ]); 39 | } 40 | 41 | /** 42 | * The watcher class for the controller. 43 | * 44 | * @return string 45 | */ 46 | protected function watcher() 47 | { 48 | return JobWatcher::class; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/telescope", 3 | "description": "An elegant debug assistant for the Laravel framework.", 4 | "keywords": ["laravel", "monitoring", "debugging"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Taylor Otwell", 9 | "email": "taylor@laravel.com" 10 | }, 11 | { 12 | "name": "Mohamed Said", 13 | "email": "mohamed@laravel.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^7.2", 18 | "ext-json": "*", 19 | "laravel/framework": "^6.0|^7.0", 20 | "moontoast/math": "^1.1", 21 | "symfony/var-dumper": "^4.4|^5.0" 22 | }, 23 | "require-dev": { 24 | "ext-gd": "*", 25 | "orchestra/testbench": "^4.0|^5.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Laravel\\Telescope\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Laravel\\Telescope\\Tests\\": "tests/" 35 | } 36 | }, 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "3.x-dev" 40 | }, 41 | "laravel": { 42 | "providers": [ 43 | "Laravel\\Telescope\\TelescopeServiceProvider" 44 | ] 45 | } 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true 52 | } 53 | -------------------------------------------------------------------------------- /src/Avatar.php: -------------------------------------------------------------------------------- 1 | authorization(); 18 | } 19 | 20 | /** 21 | * Configure the Telescope authorization services. 22 | * 23 | * @return void 24 | */ 25 | protected function authorization() 26 | { 27 | $this->gate(); 28 | 29 | Telescope::auth(function ($request) { 30 | return app()->environment('local') || 31 | Gate::check('viewTelescope', [$request->user()]); 32 | }); 33 | } 34 | 35 | /** 36 | * Register the Telescope gate. 37 | * 38 | * This gate determines who can access Telescope in non-local environments. 39 | * 40 | * @return void 41 | */ 42 | protected function gate() 43 | { 44 | Gate::define('viewTelescope', function ($user) { 45 | return in_array($user->email, [ 46 | // 47 | ]); 48 | }); 49 | } 50 | 51 | /** 52 | * Register any application services. 53 | * 54 | * @return void 55 | */ 56 | public function register() 57 | { 58 | // 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "npm run development -- --watch", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "axios": "^0.18", 14 | "bootstrap": "^4.5.0", 15 | "cross-env": "^5.1", 16 | "highlight.js": "^9.12.0", 17 | "jquery": "^3.5", 18 | "laravel-mix": "^5.0.4", 19 | "lodash": "^4.17.19", 20 | "moment": "^2.10.6", 21 | "moment-timezone": "^0.5.21", 22 | "popper.js": "^1.12", 23 | "resolve-url-loader": "^2.3.1", 24 | "sass": "^1.15.2", 25 | "sass-loader": "^7.1.0", 26 | "sql-formatter": "^2.3.1", 27 | "vue": "^2.5.7", 28 | "vue-json-pretty": "^1.6.2", 29 | "vue-router": "^3.0.1", 30 | "vue-template-compiler": "^2.5.21" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ExceptionContext.php: -------------------------------------------------------------------------------- 1 | getFile(), "eval()'d code")) { 31 | return [ 32 | $exception->getLine() => "eval()'d code", 33 | ]; 34 | } 35 | } 36 | 37 | /** 38 | * Get the exception code context from a file. 39 | * 40 | * @param \Throwable $exception 41 | * @return array 42 | */ 43 | protected static function getFileContext(Throwable $exception) 44 | { 45 | return collect(explode("\n", file_get_contents($exception->getFile()))) 46 | ->slice($exception->getLine() - 10, 20) 47 | ->mapWithKeys(function ($value, $key) { 48 | return [$key + 1 => $value]; 49 | })->all(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /resources/js/screens/redis/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 34 | -------------------------------------------------------------------------------- /src/IncomingExceptionEntry.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 26 | 27 | parent::__construct($content); 28 | } 29 | 30 | /** 31 | * Determine if the incoming entry is a reportable exception. 32 | * 33 | * @return bool 34 | */ 35 | public function isReportableException() 36 | { 37 | $handler = app(ExceptionHandler::class); 38 | 39 | return method_exists($handler, 'shouldReport') 40 | ? $handler->shouldReport($this->exception) : true; 41 | } 42 | 43 | /** 44 | * Determine if the incoming entry is an exception. 45 | * 46 | * @return bool 47 | */ 48 | public function isException() 49 | { 50 | return true; 51 | } 52 | 53 | /** 54 | * Calculate the family look-up hash for the incoming entry. 55 | * 56 | * @return string 57 | */ 58 | public function familyHash() 59 | { 60 | return md5($this->content['file'].$this->content['line']); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /resources/sass/app-dark.scss: -------------------------------------------------------------------------------- 1 | $font-family-base: Nunito, sans-serif; 2 | 3 | $font-size-base: 0.95rem; 4 | $badge-font-size: 0.95rem; 5 | 6 | $primary: #adadff; 7 | $secondary: #494444; 8 | $success: #1f9d55; 9 | $info: #1c3d5a; 10 | $warning: #684f1d; 11 | $danger: #621b18; 12 | $gray-800: $secondary; 13 | 14 | $body-bg: #1c1c1c; 15 | $body-color: #e2edf4; 16 | 17 | $sidebar-nav-color: #6e6b6b; 18 | $sidebar-icon-color: #9f9898; 19 | 20 | $pill-link-active: $primary; 21 | 22 | $border-color: #efefef; 23 | $table-border-color: #343434; 24 | $table-headers-color: #181818; 25 | $table-hover-bg: #343434; 26 | 27 | $header-border-color: $table-border-color; 28 | 29 | $input-bg: #242424; 30 | $input-color: #e2edf4; 31 | $input-border-color: $table-border-color; 32 | 33 | $card-cap-bg: #120f12; 34 | $card-bg-secondary: #262525; 35 | $card-bg: $card-cap-bg; 36 | $card-shadow-color: $body-bg; 37 | 38 | $code-bg: $card-bg-secondary; 39 | 40 | $paginator-button-color: #9ea7ac; 41 | 42 | $modal-content-bg: $table-headers-color; 43 | $modal-backdrop-bg: #7e7e7e; 44 | $modal-footer-border-color: $input-border-color; 45 | $modal-header-border-color: $input-border-color; 46 | 47 | $new-entries-bg: #505e4a; 48 | 49 | $control-action-icon-color: #ccd2df; 50 | 51 | $dropdown-bg: $table-headers-color; 52 | $dropdown-link-color: #fff; 53 | 54 | $grid-breakpoints: ( 55 | xs: 0, 56 | sm: 2px, 57 | md: 8px, 58 | lg: 9px, 59 | xl: 10px 60 | ) !default; 61 | 62 | $container-max-widths: ( 63 | sm: 1137px, 64 | md: 1138px, 65 | lg: 1139px, 66 | xl: 1140px 67 | ) !default; 68 | 69 | @import 'base'; 70 | -------------------------------------------------------------------------------- /resources/js/screens/schedule/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /resources/js/screens/commands/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /src/Http/Controllers/MonitoredTagController.php: -------------------------------------------------------------------------------- 1 | entries = $entries; 27 | } 28 | 29 | /** 30 | * Get all of the tags being monitored. 31 | * 32 | * @return \Illuminate\Http\JsonResponse 33 | */ 34 | public function index() 35 | { 36 | return response()->json([ 37 | 'tags' => $this->entries->monitoring(), 38 | ]); 39 | } 40 | 41 | /** 42 | * Begin monitoring the given tag. 43 | * 44 | * @param \Illuminate\Http\Request $request 45 | * @return void 46 | */ 47 | public function store(Request $request) 48 | { 49 | $this->entries->monitor([$request->tag]); 50 | } 51 | 52 | /** 53 | * Stop monitoring the given tag. 54 | * 55 | * @param \Illuminate\Http\Request $request 56 | * @return void 57 | */ 58 | public function destroy(Request $request) 59 | { 60 | $this->entries->stopMonitoring([$request->tag]); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Watchers/CommandWatcher.php: -------------------------------------------------------------------------------- 1 | listen(CommandFinished::class, [$this, 'recordCommand']); 20 | } 21 | 22 | /** 23 | * Record an Artisan command was executed. 24 | * 25 | * @param \Illuminate\Console\Events\CommandFinished $event 26 | * @return void 27 | */ 28 | public function recordCommand(CommandFinished $event) 29 | { 30 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) { 31 | return; 32 | } 33 | 34 | Telescope::recordCommand(IncomingEntry::make([ 35 | 'command' => $event->command ?? $event->input->getArguments()['command'] ?? 'default', 36 | 'exit_code' => $event->exitCode, 37 | 'arguments' => $event->input->getArguments(), 38 | 'options' => $event->input->getOptions(), 39 | ])); 40 | } 41 | 42 | /** 43 | * Determine if the event should be ignored. 44 | * 45 | * @param mixed $event 46 | * @return bool 47 | */ 48 | private function shouldIgnore($event) 49 | { 50 | return in_array($event->command, array_merge($this->options['ignore'] ?? [], [ 51 | 'schedule:run', 52 | 'schedule:finish', 53 | 'package:discover', 54 | ])); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /resources/js/screens/models/preview.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 51 | -------------------------------------------------------------------------------- /src/IncomingDumpEntry.php: -------------------------------------------------------------------------------- 1 | first(function ($entry) { 26 | return in_array($entry->type, [EntryType::REQUEST, EntryType::JOB, EntryType::COMMAND]); 27 | }); 28 | 29 | if (! $entryPoint) { 30 | return; 31 | } 32 | 33 | $this->content = array_merge($this->content, [ 34 | 'entry_point_type' => $entryPoint->type, 35 | 'entry_point_uuid' => $entryPoint->uuid, 36 | 'entry_point_description' => $this->entryPointDescription($entryPoint), 37 | ]); 38 | } 39 | 40 | /** 41 | * Description for the entry point. 42 | * 43 | * @param \Laravel\Telescope\IncomingDumpEntry $entryPoint 44 | * @return string 45 | */ 46 | private function entryPointDescription($entryPoint) 47 | { 48 | switch ($entryPoint->type) { 49 | case EntryType::REQUEST: 50 | return $entryPoint->content['method'].' '.$entryPoint->content['uri']; 51 | 52 | case EntryType::JOB: 53 | return $entryPoint->content['name']; 54 | 55 | case EntryType::COMMAND: 56 | return $entryPoint->content['command']; 57 | } 58 | 59 | return ''; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /resources/js/screens/events/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 40 | -------------------------------------------------------------------------------- /resources/js/screens/views/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | -------------------------------------------------------------------------------- /src/Http/Controllers/ExceptionController.php: -------------------------------------------------------------------------------- 1 | find($id); 45 | 46 | if ($request->input('resolved_at') === 'now') { 47 | $update = new EntryUpdate($entry->id, $entry->type, [ 48 | 'resolved_at' => Carbon::now()->toDateTimeString(), 49 | ]); 50 | 51 | $storage->update(collect([$update])); 52 | 53 | // Reload entry 54 | $entry = $storage->find($id); 55 | } 56 | 57 | return response()->json([ 58 | 'entry' => $entry, 59 | 'batch' => $storage->get(null, EntryQueryOptions::forBatchId($entry->batchId)->limit(-1)), 60 | ]); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /resources/js/screens/cache/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | -------------------------------------------------------------------------------- /resources/js/screens/gates/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | -------------------------------------------------------------------------------- /resources/js/screens/models/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | -------------------------------------------------------------------------------- /resources/js/screens/queries/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 42 | -------------------------------------------------------------------------------- /resources/js/screens/logs/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 43 | -------------------------------------------------------------------------------- /src/Watchers/LogWatcher.php: -------------------------------------------------------------------------------- 1 | listen(MessageLogged::class, [$this, 'recordLog']); 22 | } 23 | 24 | /** 25 | * Record a message was logged. 26 | * 27 | * @param \Illuminate\Log\Events\MessageLogged $event 28 | * @return void 29 | */ 30 | public function recordLog(MessageLogged $event) 31 | { 32 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) { 33 | return; 34 | } 35 | 36 | Telescope::recordLog( 37 | IncomingEntry::make([ 38 | 'level' => $event->level, 39 | 'message' => (string) $event->message, 40 | 'context' => Arr::except($event->context, ['telescope']), 41 | ])->tags($this->tags($event)) 42 | ); 43 | } 44 | 45 | /** 46 | * Extract tags from the given event. 47 | * 48 | * @param \Illuminate\Log\Events\MessageLogged $event 49 | * @return array 50 | */ 51 | private function tags($event) 52 | { 53 | return $event->context['telescope'] ?? []; 54 | } 55 | 56 | /** 57 | * Determine if the event should be ignored. 58 | * 59 | * @param mixed $event 60 | * @return bool 61 | */ 62 | private function shouldIgnore($event) 63 | { 64 | return isset($event->context['exception']) && 65 | $event->context['exception'] instanceof Exception; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Watchers/DumpWatcher.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 33 | } 34 | 35 | /** 36 | * Register the watcher. 37 | * 38 | * @param \Illuminate\Contracts\Foundation\Application $app 39 | * @return void 40 | */ 41 | public function register($app) 42 | { 43 | if (! $this->cache->get('telescope:dump-watcher')) { 44 | return; 45 | } 46 | 47 | $htmlDumper = new HtmlDumper(); 48 | $htmlDumper->setDumpHeader(''); 49 | 50 | VarDumper::setHandler(function ($var) use ($htmlDumper) { 51 | $this->recordDump($htmlDumper->dump( 52 | (new VarCloner)->cloneVar($var), true 53 | )); 54 | }); 55 | } 56 | 57 | /** 58 | * Record a dumped variable. 59 | * 60 | * @param string $dump 61 | * @return void 62 | */ 63 | public function recordDump($dump) 64 | { 65 | Telescope::recordDump( 66 | IncomingDumpEntry::make(['dump' => $dump]) 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /resources/js/screens/gates/preview.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 57 | -------------------------------------------------------------------------------- /src/Watchers/ModelWatcher.php: -------------------------------------------------------------------------------- 1 | listen($this->options['events'] ?? 'eloquent.*', [$this, 'recordAction']); 21 | } 22 | 23 | /** 24 | * Record an action. 25 | * 26 | * @param string $event 27 | * @param array $data 28 | * @return void 29 | */ 30 | public function recordAction($event, $data) 31 | { 32 | if (! Telescope::isRecording() || ! $this->shouldRecord($event)) { 33 | return; 34 | } 35 | 36 | $model = FormatModel::given($data[0]); 37 | 38 | $changes = $data[0]->getChanges(); 39 | 40 | Telescope::recordModelEvent(IncomingEntry::make(array_filter([ 41 | 'action' => $this->action($event), 42 | 'model' => $model, 43 | 'changes' => empty($changes) ? null : $changes, 44 | ]))->tags([$model])); 45 | } 46 | 47 | /** 48 | * Extract the Eloquent action from the given event. 49 | * 50 | * @param string $event 51 | * @return mixed 52 | */ 53 | private function action($event) 54 | { 55 | preg_match('/\.(.*):/', $event, $matches); 56 | 57 | return $matches[1]; 58 | } 59 | 60 | /** 61 | * Determine if the Eloquent event should be recorded. 62 | * 63 | * @param string $eventName 64 | * @return bool 65 | */ 66 | private function shouldRecord($eventName) 67 | { 68 | return Str::is([ 69 | '*created*', '*updated*', '*restored*', '*deleted*', 70 | ], $eventName); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /stubs/TelescopeServiceProvider.stub: -------------------------------------------------------------------------------- 1 | hideSensitiveRequestDetails(); 22 | 23 | Telescope::filter(function (IncomingEntry $entry) { 24 | if ($this->app->environment('local')) { 25 | return true; 26 | } 27 | 28 | return $entry->isReportableException() || 29 | $entry->isFailedRequest() || 30 | $entry->isFailedJob() || 31 | $entry->isScheduledTask() || 32 | $entry->hasMonitoredTag(); 33 | }); 34 | } 35 | 36 | /** 37 | * Prevent sensitive request details from being logged by Telescope. 38 | * 39 | * @return void 40 | */ 41 | protected function hideSensitiveRequestDetails() 42 | { 43 | if ($this->app->environment('local')) { 44 | return; 45 | } 46 | 47 | Telescope::hideRequestParameters(['_token']); 48 | 49 | Telescope::hideRequestHeaders([ 50 | 'cookie', 51 | 'x-csrf-token', 52 | 'x-xsrf-token', 53 | ]); 54 | } 55 | 56 | /** 57 | * Register the Telescope gate. 58 | * 59 | * This gate determines who can access Telescope in non-local environments. 60 | * 61 | * @return void 62 | */ 63 | protected function gate() 64 | { 65 | Gate::define('viewTelescope', function ($user) { 66 | return in_array($user->email, [ 67 | // 68 | ]); 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /resources/js/screens/schedule/preview.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 60 | -------------------------------------------------------------------------------- /resources/js/screens/cache/preview.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 61 | -------------------------------------------------------------------------------- /resources/js/screens/notifications/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Build Status 5 | Total Downloads 6 | Latest Stable Version 7 | License 8 |

9 | 10 | ## Introduction 11 | 12 | Laravel Telescope is an elegant debug assistant for the Laravel framework. Telescope provides insight into the requests coming into your application, exceptions, log entries, database queries, queued jobs, mail, notifications, cache operations, scheduled tasks, variable dumps and more. Telescope makes a wonderful companion to your local Laravel development environment. 13 | 14 |

15 | 16 |

17 | 18 | ## Official Documentation 19 | 20 | Documentation for Telescope can be found on the [Laravel website](https://laravel.com/docs/telescope). 21 | 22 | ## Contributing 23 | 24 | Thank you for considering contributing to Telescope! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). 25 | 26 | ## Code of Conduct 27 | 28 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 29 | 30 | ## Security Vulnerabilities 31 | 32 | Please review [our security policy](https://github.com/laravel/telescope/security/policy) on how to report security vulnerabilities. 33 | 34 | ## License 35 | 36 | Laravel Telescope is open-sourced software licensed under the [MIT license](LICENSE.md). 37 | -------------------------------------------------------------------------------- /resources/js/screens/jobs/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 49 | -------------------------------------------------------------------------------- /src/Contracts/EntriesRepository.php: -------------------------------------------------------------------------------- 1 | = 500) return 'danger'; 48 | }, 49 | 50 | requestMethodClass(method) { 51 | if (method == 'GET') return 'secondary'; 52 | if (method == 'OPTIONS') return 'secondary'; 53 | if (method == 'POST') return 'info'; 54 | if (method == 'PATCH') return 'info'; 55 | if (method == 'PUT') return 'info'; 56 | if (method == 'DELETE') return 'danger'; 57 | }, 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /src/Watchers/GateWatcher.php: -------------------------------------------------------------------------------- 1 | shouldIgnore($ability)) { 40 | return; 41 | } 42 | 43 | $caller = $this->getCallerFromStackTrace(); 44 | 45 | Telescope::recordGate(IncomingEntry::make([ 46 | 'ability' => $ability, 47 | 'result' => $result ? 'allowed' : 'denied', 48 | 'arguments' => $this->formatArguments($arguments), 49 | 'file' => $caller['file'], 50 | 'line' => $caller['line'], 51 | ])); 52 | 53 | return $result; 54 | } 55 | 56 | /** 57 | * Determine if the ability should be ignored. 58 | * 59 | * @param string $ability 60 | * @return bool 61 | */ 62 | private function shouldIgnore($ability) 63 | { 64 | return Str::is($this->options['ignore_abilities'] ?? [], $ability); 65 | } 66 | 67 | /** 68 | * Format the given arguments. 69 | * 70 | * @param array $arguments 71 | * @return array 72 | */ 73 | private function formatArguments($arguments) 74 | { 75 | return collect($arguments)->map(function ($argument) { 76 | return $argument instanceof Model ? FormatModel::given($argument) : $argument; 77 | })->toArray(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Watchers/ScheduleWatcher.php: -------------------------------------------------------------------------------- 1 | listen(CommandStarting::class, [$this, 'recordCommand']); 23 | } 24 | 25 | /** 26 | * Record a scheduled command was executed. 27 | * 28 | * @param \Illuminate\Console\Events\CommandStarting $event 29 | * @return void 30 | */ 31 | public function recordCommand(CommandStarting $event) 32 | { 33 | if (! Telescope::isRecording() || 34 | $event->command !== 'schedule:run' && 35 | $event->command !== 'schedule:finish') { 36 | return; 37 | } 38 | 39 | collect(app(Schedule::class)->events())->each(function ($event) { 40 | $event->then(function () use ($event) { 41 | Telescope::recordScheduledCommand(IncomingEntry::make([ 42 | 'command' => $event instanceof CallbackEvent ? 'Closure' : $event->command, 43 | 'description' => $event->description, 44 | 'expression' => $event->expression, 45 | 'timezone' => $event->timezone, 46 | 'user' => $event->user, 47 | 'output' => $this->getEventOutput($event), 48 | ])); 49 | }); 50 | }); 51 | } 52 | 53 | /** 54 | * Get the output for the scheduled event. 55 | * 56 | * @param \Illuminate\Console\Scheduling\Event $event 57 | * @return string|null 58 | */ 59 | protected function getEventOutput(Event $event) 60 | { 61 | if (! $event->output || 62 | $event->output === $event->getDefaultOutput() || 63 | $event->shouldAppendOutput || 64 | ! file_exists($event->output)) { 65 | return ''; 66 | } 67 | 68 | return trim(file_get_contents($event->output)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /resources/js/screens/commands/preview.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 59 | -------------------------------------------------------------------------------- /resources/js/screens/queries/preview.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 64 | -------------------------------------------------------------------------------- /resources/js/screens/mail/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 55 | -------------------------------------------------------------------------------- /src/EntryUpdate.php: -------------------------------------------------------------------------------- 1 | [], 'added' => []]; 34 | 35 | /** 36 | * Create a new incoming entry instance. 37 | * 38 | * @param string $uuid 39 | * @param string $type 40 | * @param array $changes 41 | * @return void 42 | */ 43 | public function __construct($uuid, $type, array $changes) 44 | { 45 | $this->uuid = $uuid; 46 | $this->type = $type; 47 | $this->changes = $changes; 48 | } 49 | 50 | /** 51 | * Create a new entry update instance. 52 | * 53 | * @param mixed ...$arguments 54 | * @return static 55 | */ 56 | public static function make(...$arguments) 57 | { 58 | return new static(...$arguments); 59 | } 60 | 61 | /** 62 | * Set the properties that should be updated. 63 | * 64 | * @param array $changes 65 | * @return $this 66 | */ 67 | public function change(array $changes) 68 | { 69 | $this->changes = array_merge($this->changes, $changes); 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Add tags to the entry. 76 | * 77 | * @param array $tags 78 | * @return $this 79 | */ 80 | public function addTags(array $tags) 81 | { 82 | $this->tagsChanges['added'] = array_unique( 83 | array_merge($this->tagsChanges['added'], $tags) 84 | ); 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Remove tags from the entry. 91 | * 92 | * @param array $tags 93 | * @return $this 94 | */ 95 | public function removeTags(array $tags) 96 | { 97 | $this->tagsChanges['removed'] = array_unique( 98 | array_merge($this->tagsChanges['removed'], $tags) 99 | ); 100 | 101 | return $this; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Http/Controllers/EntryController.php: -------------------------------------------------------------------------------- 1 | json([ 36 | 'entries' => $storage->get( 37 | $this->entryType(), 38 | EntryQueryOptions::fromRequest($request) 39 | ), 40 | 'status' => $this->status(), 41 | ]); 42 | } 43 | 44 | /** 45 | * Get an entry with the given ID. 46 | * 47 | * @param \Laravel\Telescope\Contracts\EntriesRepository $storage 48 | * @param int $id 49 | * @return \Illuminate\Http\JsonResponse 50 | */ 51 | public function show(EntriesRepository $storage, $id) 52 | { 53 | $entry = $storage->find($id)->generateAvatar(); 54 | 55 | return response()->json([ 56 | 'entry' => $entry, 57 | 'batch' => $storage->get(null, EntryQueryOptions::forBatchId($entry->batchId)->limit(-1)), 58 | ]); 59 | } 60 | 61 | /** 62 | * Determine the watcher recording status. 63 | * 64 | * @return string 65 | */ 66 | protected function status() 67 | { 68 | if (! config('telescope.enabled', false)) { 69 | return 'disabled'; 70 | } 71 | 72 | if (cache('telescope:pause-recording', false)) { 73 | return 'paused'; 74 | } 75 | 76 | $watcher = config('telescope.watchers.'.$this->watcher()); 77 | 78 | if (! $watcher || (isset($watcher['enabled']) && ! $watcher['enabled'])) { 79 | return 'off'; 80 | } 81 | 82 | return 'enabled'; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /resources/js/screens/logs/preview.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 62 | 63 | 66 | -------------------------------------------------------------------------------- /resources/js/screens/requests/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 57 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Base from './base'; 3 | import axios from 'axios'; 4 | import Routes from './routes'; 5 | import VueRouter from 'vue-router'; 6 | import VueJsonPretty from 'vue-json-pretty'; 7 | import moment from 'moment-timezone'; 8 | 9 | require('bootstrap'); 10 | 11 | let token = document.head.querySelector('meta[name="csrf-token"]'); 12 | 13 | if (token) { 14 | axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 15 | } 16 | 17 | Vue.use(VueRouter); 18 | 19 | window.Popper = require('popper.js').default; 20 | 21 | moment.tz.setDefault(Telescope.timezone); 22 | 23 | window.Telescope.basePath = '/' + window.Telescope.path; 24 | 25 | let routerBasePath = window.Telescope.basePath + '/'; 26 | 27 | if (window.Telescope.path === '' || window.Telescope.path === '/') { 28 | routerBasePath = '/'; 29 | window.Telescope.basePath = ''; 30 | } 31 | 32 | const router = new VueRouter({ 33 | routes: Routes, 34 | mode: 'history', 35 | base: routerBasePath, 36 | }); 37 | 38 | Vue.component('vue-json-pretty', VueJsonPretty); 39 | Vue.component('related-entries', require('./components/RelatedEntries.vue').default); 40 | Vue.component('index-screen', require('./components/IndexScreen.vue').default); 41 | Vue.component('preview-screen', require('./components/PreviewScreen.vue').default); 42 | Vue.component('alert', require('./components/Alert.vue').default); 43 | 44 | Vue.mixin(Base); 45 | 46 | new Vue({ 47 | el: '#telescope', 48 | 49 | router, 50 | 51 | data() { 52 | return { 53 | alert: { 54 | type: null, 55 | autoClose: 0, 56 | message: '', 57 | confirmationProceed: null, 58 | confirmationCancel: null, 59 | }, 60 | 61 | autoLoadsNewEntries: localStorage.autoLoadsNewEntries === '1', 62 | 63 | recording: Telescope.recording, 64 | }; 65 | }, 66 | 67 | methods: { 68 | autoLoadNewEntries() { 69 | if (!this.autoLoadsNewEntries) { 70 | this.autoLoadsNewEntries = true; 71 | localStorage.autoLoadsNewEntries = 1; 72 | } else { 73 | this.autoLoadsNewEntries = false; 74 | localStorage.autoLoadsNewEntries = 0; 75 | } 76 | }, 77 | 78 | toggleRecording() { 79 | axios.post(Telescope.basePath + '/telescope-api/toggle-recording'); 80 | 81 | window.Telescope.recording = !Telescope.recording; 82 | this.recording = !this.recording; 83 | }, 84 | }, 85 | }); 86 | -------------------------------------------------------------------------------- /src/Http/Controllers/DumpController.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 33 | } 34 | 35 | /** 36 | * List the entries of the given type. 37 | * 38 | * @param \Illuminate\Http\Request $request 39 | * @param \Laravel\Telescope\Contracts\EntriesRepository $storage 40 | * @return \Illuminate\Http\JsonResponse 41 | */ 42 | public function index(Request $request, EntriesRepository $storage) 43 | { 44 | $this->cache->put('telescope:dump-watcher', true, now()->addSeconds(4)); 45 | 46 | return response()->json([ 47 | 'dump' => (new HtmlDumper())->dump((new VarCloner)->cloneVar(true), true), 48 | 'entries' => $storage->get( 49 | $this->entryType(), 50 | EntryQueryOptions::fromRequest($request) 51 | ), 52 | 'status' => $this->status(), 53 | ]); 54 | } 55 | 56 | /** 57 | * Determine the watcher recording status. 58 | * 59 | * @return string 60 | */ 61 | protected function status() 62 | { 63 | if ($this->cache->getStore() instanceof ArrayStore) { 64 | return 'wrong-cache'; 65 | } 66 | 67 | return parent::status(); 68 | } 69 | 70 | /** 71 | * The entry type for the controller. 72 | * 73 | * @return string 74 | */ 75 | protected function entryType() 76 | { 77 | return EntryType::DUMP; 78 | } 79 | 80 | /** 81 | * The watcher class for the controller. 82 | * 83 | * @return string 84 | */ 85 | protected function watcher() 86 | { 87 | return DumpWatcher::class; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Watchers/RedisWatcher.php: -------------------------------------------------------------------------------- 1 | listen(CommandExecuted::class, [$this, 'recordCommand']); 20 | 21 | foreach ((array) $app['redis']->connections() as $connection) { 22 | $connection->setEventDispatcher($app['events']); 23 | } 24 | 25 | $app['redis']->enableEvents(); 26 | } 27 | 28 | /** 29 | * Record a Redis command was executed. 30 | * 31 | * @param \Illuminate\Redis\Events\CommandExecuted $event 32 | * @return void 33 | */ 34 | public function recordCommand(CommandExecuted $event) 35 | { 36 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) { 37 | return; 38 | } 39 | 40 | Telescope::recordRedis(IncomingEntry::make([ 41 | 'connection' => $event->connectionName, 42 | 'command' => $this->formatCommand($event->command, $event->parameters), 43 | 'time' => number_format($event->time, 2, '.', ''), 44 | ])); 45 | } 46 | 47 | /** 48 | * Format the given Redis command. 49 | * 50 | * @param string $command 51 | * @param array $parameters 52 | * @return string 53 | */ 54 | private function formatCommand($command, $parameters) 55 | { 56 | $parameters = collect($parameters)->map(function ($parameter) { 57 | if (is_array($parameter)) { 58 | return collect($parameter)->map(function ($value, $key) { 59 | if (is_array($value)) { 60 | return json_encode($value); 61 | } 62 | 63 | return is_int($key) ? $value : "{$key} {$value}"; 64 | })->implode(' '); 65 | } 66 | 67 | return $parameter; 68 | })->implode(' '); 69 | 70 | return "{$command} {$parameters}"; 71 | } 72 | 73 | /** 74 | * Determine if the event should be ignored. 75 | * 76 | * @param mixed $event 77 | * @return bool 78 | */ 79 | private function shouldIgnore($event) 80 | { 81 | return in_array($event->command, [ 82 | 'pipeline', 'transaction', 83 | ]); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /resources/js/screens/events/preview.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 63 | -------------------------------------------------------------------------------- /src/Watchers/NotificationWatcher.php: -------------------------------------------------------------------------------- 1 | listen(NotificationSent::class, [$this, 'recordNotification']); 25 | } 26 | 27 | /** 28 | * Record a new notification message was sent. 29 | * 30 | * @param \Illuminate\Notifications\Events\NotificationSent $event 31 | * @return void 32 | */ 33 | public function recordNotification(NotificationSent $event) 34 | { 35 | if (! Telescope::isRecording()) { 36 | return; 37 | } 38 | 39 | Telescope::recordNotification(IncomingEntry::make([ 40 | 'notification' => get_class($event->notification), 41 | 'queued' => in_array(ShouldQueue::class, class_implements($event->notification)), 42 | 'notifiable' => $this->formatNotifiable($event->notifiable), 43 | 'channel' => $event->channel, 44 | 'response' => $event->response, 45 | ])->tags($this->tags($event))); 46 | } 47 | 48 | /** 49 | * Extract the tags for the given event. 50 | * 51 | * @param \Illuminate\Notifications\Events\NotificationSent $event 52 | * @return array 53 | */ 54 | private function tags($event) 55 | { 56 | return array_merge([ 57 | $this->formatNotifiable($event->notifiable), 58 | ], ExtractTags::from($event->notification)); 59 | } 60 | 61 | /** 62 | * Format the given notifiable into a tag. 63 | * 64 | * @param mixed $notifiable 65 | * @return string 66 | */ 67 | private function formatNotifiable($notifiable) 68 | { 69 | if ($notifiable instanceof Model) { 70 | return FormatModel::given($notifiable); 71 | } elseif ($notifiable instanceof AnonymousNotifiable) { 72 | $routes = array_map(function ($route) { 73 | return is_array($route) ? implode(',', $route) : $route; 74 | }, $notifiable->routes); 75 | 76 | return 'Anonymous:'.implode(',', $routes); 77 | } 78 | 79 | return get_class($notifiable); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Watchers/ExceptionWatcher.php: -------------------------------------------------------------------------------- 1 | listen(MessageLogged::class, [$this, 'recordException']); 24 | } 25 | 26 | /** 27 | * Record an exception was logged. 28 | * 29 | * @param \Illuminate\Log\Events\MessageLogged $event 30 | * @return void 31 | */ 32 | public function recordException(MessageLogged $event) 33 | { 34 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) { 35 | return; 36 | } 37 | 38 | $exception = $event->context['exception']; 39 | 40 | $trace = collect($exception->getTrace())->map(function ($item) { 41 | return Arr::only($item, ['file', 'line']); 42 | })->toArray(); 43 | 44 | Telescope::recordException( 45 | IncomingExceptionEntry::make($exception, [ 46 | 'class' => get_class($exception), 47 | 'file' => $exception->getFile(), 48 | 'line' => $exception->getLine(), 49 | 'message' => $exception->getMessage(), 50 | 'context' => transform(Arr::except($event->context, ['exception', 'telescope']), function ($context) { 51 | return ! empty($context) ? $context : null; 52 | }), 53 | 'trace' => $trace, 54 | 'line_preview' => ExceptionContext::get($exception), 55 | ])->tags($this->tags($event)) 56 | ); 57 | } 58 | 59 | /** 60 | * Extract the tags for the given event. 61 | * 62 | * @param \Illuminate\Log\Events\MessageLogged $event 63 | * @return array 64 | */ 65 | protected function tags($event) 66 | { 67 | return array_merge(ExtractTags::from($event->context['exception']), 68 | $event->context['telescope'] ?? [] 69 | ); 70 | } 71 | 72 | /** 73 | * Determine if the event should be ignored. 74 | * 75 | * @param mixed $event 76 | * @return bool 77 | */ 78 | private function shouldIgnore($event) 79 | { 80 | return ! isset($event->context['exception']) || 81 | ! $event->context['exception'] instanceof Exception; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Console/InstallCommand.php: -------------------------------------------------------------------------------- 1 | comment('Publishing Telescope Service Provider...'); 32 | $this->callSilent('vendor:publish', ['--tag' => 'telescope-provider']); 33 | 34 | $this->comment('Publishing Telescope Assets...'); 35 | $this->callSilent('vendor:publish', ['--tag' => 'telescope-assets']); 36 | 37 | $this->comment('Publishing Telescope Configuration...'); 38 | $this->callSilent('vendor:publish', ['--tag' => 'telescope-config']); 39 | 40 | $this->registerTelescopeServiceProvider(); 41 | 42 | $this->info('Telescope scaffolding installed successfully.'); 43 | } 44 | 45 | /** 46 | * Register the Telescope service provider in the application configuration file. 47 | * 48 | * @return void 49 | */ 50 | protected function registerTelescopeServiceProvider() 51 | { 52 | $namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace()); 53 | 54 | $appConfig = file_get_contents(config_path('app.php')); 55 | 56 | if (Str::contains($appConfig, $namespace.'\\Providers\\TelescopeServiceProvider::class')) { 57 | return; 58 | } 59 | 60 | $lineEndingCount = [ 61 | "\r\n" => substr_count($appConfig, "\r\n"), 62 | "\r" => substr_count($appConfig, "\r"), 63 | "\n" => substr_count($appConfig, "\n"), 64 | ]; 65 | 66 | $eol = array_keys($lineEndingCount, max($lineEndingCount))[0]; 67 | 68 | file_put_contents(config_path('app.php'), str_replace( 69 | "{$namespace}\\Providers\RouteServiceProvider::class,".$eol, 70 | "{$namespace}\\Providers\RouteServiceProvider::class,".$eol." {$namespace}\Providers\TelescopeServiceProvider::class,".$eol, 71 | $appConfig 72 | )); 73 | 74 | file_put_contents(app_path('Providers/TelescopeServiceProvider.php'), str_replace( 75 | "namespace App\Providers;", 76 | "namespace {$namespace}\Providers;", 77 | file_get_contents(app_path('Providers/TelescopeServiceProvider.php')) 78 | )); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Storage/migrations/2018_08_08_100000_create_telescope_entries_table.php: -------------------------------------------------------------------------------- 1 | schema = Schema::connection($this->getConnection()); 24 | } 25 | 26 | /** 27 | * Get the migration connection name. 28 | * 29 | * @return string|null 30 | */ 31 | public function getConnection() 32 | { 33 | return config('telescope.storage.database.connection'); 34 | } 35 | 36 | /** 37 | * Run the migrations. 38 | * 39 | * @return void 40 | */ 41 | public function up() 42 | { 43 | $this->schema->create('telescope_entries', function (Blueprint $table) { 44 | $table->bigIncrements('sequence'); 45 | $table->uuid('uuid'); 46 | $table->uuid('batch_id'); 47 | $table->string('family_hash')->nullable(); 48 | $table->boolean('should_display_on_index')->default(true); 49 | $table->string('type', 20); 50 | $table->longText('content'); 51 | $table->dateTime('created_at')->nullable(); 52 | 53 | $table->unique('uuid'); 54 | $table->index('batch_id'); 55 | $table->index('family_hash'); 56 | $table->index('created_at'); 57 | $table->index(['type', 'should_display_on_index']); 58 | }); 59 | 60 | $this->schema->create('telescope_entries_tags', function (Blueprint $table) { 61 | $table->uuid('entry_uuid'); 62 | $table->string('tag'); 63 | 64 | $table->index(['entry_uuid', 'tag']); 65 | $table->index('tag'); 66 | 67 | $table->foreign('entry_uuid') 68 | ->references('uuid') 69 | ->on('telescope_entries') 70 | ->onDelete('cascade'); 71 | }); 72 | 73 | $this->schema->create('telescope_monitoring', function (Blueprint $table) { 74 | $table->string('tag'); 75 | }); 76 | } 77 | 78 | /** 79 | * Reverse the migrations. 80 | * 81 | * @return void 82 | */ 83 | public function down() 84 | { 85 | $this->schema->dropIfExists('telescope_entries_tags'); 86 | $this->schema->dropIfExists('telescope_entries'); 87 | $this->schema->dropIfExists('telescope_monitoring'); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Watchers/MailWatcher.php: -------------------------------------------------------------------------------- 1 | listen(MessageSent::class, [$this, 'recordMail']); 20 | } 21 | 22 | /** 23 | * Record a mail message was sent. 24 | * 25 | * @param \Illuminate\Mail\Events\MessageSent $event 26 | * @return void 27 | */ 28 | public function recordMail(MessageSent $event) 29 | { 30 | if (! Telescope::isRecording()) { 31 | return; 32 | } 33 | 34 | Telescope::recordMail(IncomingEntry::make([ 35 | 'mailable' => $this->getMailable($event), 36 | 'queued' => $this->getQueuedStatus($event), 37 | 'from' => $event->message->getFrom(), 38 | 'replyTo' => $event->message->getReplyTo(), 39 | 'to' => $event->message->getTo(), 40 | 'cc' => $event->message->getCc(), 41 | 'bcc' => $event->message->getBcc(), 42 | 'subject' => $event->message->getSubject(), 43 | 'html' => $event->message->getBody(), 44 | 'raw' => $event->message->toString(), 45 | ])->tags($this->tags($event->message, $event->data))); 46 | } 47 | 48 | /** 49 | * Get the name of the mailable. 50 | * 51 | * @param \Illuminate\Mail\Events\MessageSent $event 52 | * @return string 53 | */ 54 | protected function getMailable($event) 55 | { 56 | if (isset($event->data['__laravel_notification'])) { 57 | return $event->data['__laravel_notification']; 58 | } 59 | 60 | return $event->data['__telescope_mailable'] ?? ''; 61 | } 62 | 63 | /** 64 | * Determine whether the mailable was queued. 65 | * 66 | * @param \Illuminate\Mail\Events\MessageSent $event 67 | * @return bool 68 | */ 69 | protected function getQueuedStatus($event) 70 | { 71 | if (isset($event->data['__laravel_notification_queued'])) { 72 | return $event->data['__laravel_notification_queued']; 73 | } 74 | 75 | return $event->data['__telescope_queued'] ?? false; 76 | } 77 | 78 | /** 79 | * Extract the tags from the message. 80 | * 81 | * @param \Swift_Message $message 82 | * @param array $data 83 | * @return array 84 | */ 85 | private function tags($message, $data) 86 | { 87 | return array_merge( 88 | array_keys($message->getTo() ?: []), 89 | array_keys($message->getCc() ?: []), 90 | array_keys($message->getBcc() ?: []), 91 | $data['__telescope'] ?? [] 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/ListensForStorageOpportunities.php: -------------------------------------------------------------------------------- 1 | terminating(function () use ($app) { 44 | static::store($app[EntriesRepository::class]); 45 | }); 46 | } 47 | 48 | /** 49 | * Store entries after the queue worker loops. 50 | * 51 | * @param \Illuminate\Foundation\Application $app 52 | * @return void 53 | */ 54 | protected static function storeEntriesAfterWorkerLoop($app) 55 | { 56 | $app['events']->listen(JobProcessing::class, function ($event) { 57 | if ($event->connectionName !== 'sync') { 58 | static::startRecording(); 59 | 60 | static::$processingJobs[] = true; 61 | } 62 | }); 63 | 64 | $app['events']->listen(JobProcessed::class, function ($event) use ($app) { 65 | static::storeIfDoneProcessingJob($event, $app); 66 | }); 67 | 68 | $app['events']->listen(JobFailed::class, function ($event) use ($app) { 69 | static::storeIfDoneProcessingJob($event, $app); 70 | }); 71 | 72 | $app['events']->listen(JobExceptionOccurred::class, function () { 73 | array_pop(static::$processingJobs); 74 | }); 75 | } 76 | 77 | /** 78 | * Store the recorded entries if totally done processing the current job. 79 | * 80 | * @param \Illuminate\Queue\Events\JobProcessed $event 81 | * @param \Illuminate\Foundation\Application $app 82 | * @return void 83 | */ 84 | protected static function storeIfDoneProcessingJob($event, $app) 85 | { 86 | array_pop(static::$processingJobs); 87 | 88 | if (empty(static::$processingJobs) && $event->connectionName !== 'sync') { 89 | static::store($app[EntriesRepository::class]); 90 | static::stopRecording(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /resources/js/base.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import moment from 'moment-timezone'; 3 | 4 | export default { 5 | computed: { 6 | Telescope() { 7 | return Telescope; 8 | }, 9 | }, 10 | 11 | methods: { 12 | /** 13 | * Show the time ago format for the given time. 14 | */ 15 | timeAgo(time) { 16 | moment.updateLocale('en', { 17 | relativeTime: { 18 | future: 'in %s', 19 | past: '%s ago', 20 | s: (number) => number + 's ago', 21 | ss: '%ds ago', 22 | m: '1m ago', 23 | mm: '%dm ago', 24 | h: '1h ago', 25 | hh: '%dh ago', 26 | d: '1d ago', 27 | dd: '%dd ago', 28 | M: 'a month ago', 29 | MM: '%d months ago', 30 | y: 'a year ago', 31 | yy: '%d years ago', 32 | }, 33 | }); 34 | 35 | let secondsElapsed = moment().diff(time, 'seconds'); 36 | let dayStart = moment('2018-01-01').startOf('day').seconds(secondsElapsed); 37 | 38 | if (secondsElapsed > 300) { 39 | return moment(time).fromNow(true); 40 | } else if (secondsElapsed < 60) { 41 | return dayStart.format('s') + 's ago'; 42 | } else { 43 | return dayStart.format('m:ss') + 'm ago'; 44 | } 45 | }, 46 | 47 | /** 48 | * Show the time in local time. 49 | */ 50 | localTime(time) { 51 | return moment(time).local().format('MMMM Do YYYY, h:mm:ss A'); 52 | }, 53 | 54 | /** 55 | * Truncate the given string. 56 | */ 57 | truncate(string, length = 70) { 58 | return _.truncate(string, { 59 | length: length, 60 | separator: /,? +/, 61 | }); 62 | }, 63 | 64 | /** 65 | * Creates a debounced function that delays invoking a callback. 66 | */ 67 | debouncer: _.debounce((callback) => callback(), 500), 68 | 69 | /** 70 | * Show an error message. 71 | */ 72 | alertError(message) { 73 | this.$root.alert.type = 'error'; 74 | this.$root.alert.autoClose = false; 75 | this.$root.alert.message = message; 76 | }, 77 | 78 | /** 79 | * Show a success message. 80 | */ 81 | alertSuccess(message, autoClose) { 82 | this.$root.alert.type = 'success'; 83 | this.$root.alert.autoClose = autoClose; 84 | this.$root.alert.message = message; 85 | }, 86 | 87 | /** 88 | * Show confirmation message. 89 | */ 90 | alertConfirm(message, success, failure) { 91 | this.$root.alert.type = 'confirmation'; 92 | this.$root.alert.autoClose = false; 93 | this.$root.alert.message = message; 94 | this.$root.alert.confirmationProceed = success; 95 | this.$root.alert.confirmationCancel = failure; 96 | }, 97 | }, 98 | }; 99 | -------------------------------------------------------------------------------- /resources/js/screens/views/preview.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 80 | -------------------------------------------------------------------------------- /src/EntryResult.php: -------------------------------------------------------------------------------- 1 | id = $id; 87 | $this->type = $type; 88 | $this->tags = $tags; 89 | $this->batchId = $batchId; 90 | $this->content = $content; 91 | $this->sequence = $sequence; 92 | $this->createdAt = $createdAt; 93 | $this->familyHash = $familyHash; 94 | } 95 | 96 | /** 97 | * Set the URL to the entry user's avatar. 98 | * 99 | * @return $this 100 | */ 101 | public function generateAvatar() 102 | { 103 | $this->avatar = Avatar::url($this->content['user'] ?? []); 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Get the array representation of the entry. 110 | * 111 | * @return array 112 | */ 113 | public function jsonSerialize() 114 | { 115 | return collect([ 116 | 'id' => $this->id, 117 | 'sequence' => $this->sequence, 118 | 'batch_id' => $this->batchId, 119 | 'type' => $this->type, 120 | 'content' => $this->content, 121 | 'tags' => $this->tags, 122 | 'family_hash' => $this->familyHash, 123 | 'created_at' => $this->createdAt->toDateTimeString(), 124 | ])->when($this->avatar, function ($items) { 125 | return $items->mergeRecursive([ 126 | 'content' => [ 127 | 'user' => [ 128 | 'avatar' => $this->avatar, 129 | ], 130 | ], 131 | ]); 132 | })->all(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /resources/js/screens/mail/preview.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 94 | 95 | 100 | -------------------------------------------------------------------------------- /resources/js/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 107 | 108 | 114 | -------------------------------------------------------------------------------- /src/Watchers/QueryWatcher.php: -------------------------------------------------------------------------------- 1 | listen(QueryExecuted::class, [$this, 'recordQuery']); 22 | } 23 | 24 | /** 25 | * Record a query was executed. 26 | * 27 | * @param \Illuminate\Database\Events\QueryExecuted $event 28 | * @return void 29 | */ 30 | public function recordQuery(QueryExecuted $event) 31 | { 32 | if (! Telescope::isRecording()) { 33 | return; 34 | } 35 | 36 | $time = $event->time; 37 | 38 | if ($caller = $this->getCallerFromStackTrace()) { 39 | Telescope::recordQuery(IncomingEntry::make([ 40 | 'connection' => $event->connectionName, 41 | 'bindings' => [], 42 | 'sql' => $this->replaceBindings($event), 43 | 'time' => number_format($time, 2, '.', ''), 44 | 'slow' => isset($this->options['slow']) && $time >= $this->options['slow'], 45 | 'file' => $caller['file'], 46 | 'line' => $caller['line'], 47 | 'hash' => $this->familyHash($event), 48 | ])->tags($this->tags($event))); 49 | } 50 | } 51 | 52 | /** 53 | * Get the tags for the query. 54 | * 55 | * @param \Illuminate\Database\Events\QueryExecuted $event 56 | * @return array 57 | */ 58 | protected function tags($event) 59 | { 60 | return isset($this->options['slow']) && $event->time >= $this->options['slow'] ? ['slow'] : []; 61 | } 62 | 63 | /** 64 | * Calculate the family look-up hash for the query event. 65 | * 66 | * @param \Illuminate\Database\Events\QueryExecuted $event 67 | * @return string 68 | */ 69 | public function familyHash($event) 70 | { 71 | return md5($event->sql); 72 | } 73 | 74 | /** 75 | * Format the given bindings to strings. 76 | * 77 | * @param \Illuminate\Database\Events\QueryExecuted $event 78 | * @return array 79 | */ 80 | protected function formatBindings($event) 81 | { 82 | return $event->connection->prepareBindings($event->bindings); 83 | } 84 | 85 | /** 86 | * Replace the placeholders with the actual bindings. 87 | * 88 | * @param \Illuminate\Database\Events\QueryExecuted $event 89 | * @return string 90 | */ 91 | public function replaceBindings($event) 92 | { 93 | $sql = $event->sql; 94 | 95 | foreach ($this->formatBindings($event) as $key => $binding) { 96 | $regex = is_numeric($key) 97 | ? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/" 98 | : "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"; 99 | 100 | if ($binding === null) { 101 | $binding = 'null'; 102 | } elseif (! is_int($binding) && ! is_float($binding)) { 103 | $binding = $event->connection->getPdo()->quote($binding); 104 | } 105 | 106 | $sql = preg_replace($regex, $binding, $sql, 1); 107 | } 108 | 109 | return $sql; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Http/routes.php: -------------------------------------------------------------------------------- 1 | where('view', '(.*)')->name('telescope'); 80 | -------------------------------------------------------------------------------- /resources/js/screens/exceptions/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 66 | -------------------------------------------------------------------------------- /src/Storage/EntryQueryOptions.php: -------------------------------------------------------------------------------- 1 | batchId($request->batch_id) 61 | ->uuids($request->uuids) 62 | ->beforeSequence($request->before) 63 | ->tag($request->tag) 64 | ->familyHash($request->family_hash) 65 | ->limit($request->take ?? 50); 66 | } 67 | 68 | /** 69 | * Create new entry query options for the given batch ID. 70 | * 71 | * @param string $batchId 72 | * @return static 73 | */ 74 | public static function forBatchId(?string $batchId) 75 | { 76 | return (new static)->batchId($batchId); 77 | } 78 | 79 | /** 80 | * Set the batch ID for the query. 81 | * 82 | * @param string $batchId 83 | * @return $this 84 | */ 85 | public function batchId(?string $batchId) 86 | { 87 | $this->batchId = $batchId; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Set the list of UUIDs of entries tor retrieve. 94 | * 95 | * @param array $uuids 96 | * @return $this 97 | */ 98 | public function uuids(?array $uuids) 99 | { 100 | $this->uuids = $uuids; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * Set the ID that all retrieved entries should be less than. 107 | * 108 | * @param mixed $id 109 | * @return $this 110 | */ 111 | public function beforeSequence($id) 112 | { 113 | $this->beforeSequence = $id; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Set the tag that must belong to retrieved entries. 120 | * 121 | * @param string $tag 122 | * @return $this 123 | */ 124 | public function tag(?string $tag) 125 | { 126 | $this->tag = $tag; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Set the family hash that must belong to retrieved entries. 133 | * 134 | * @param string $familyHash 135 | * @return $this 136 | */ 137 | public function familyHash(?string $familyHash) 138 | { 139 | $this->familyHash = $familyHash; 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * Set the number of entries that should be retrieved. 146 | * 147 | * @param int $limit 148 | * @return $this 149 | */ 150 | public function limit(int $limit) 151 | { 152 | $this->limit = $limit; 153 | 154 | return $this; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Watchers/CacheWatcher.php: -------------------------------------------------------------------------------- 1 | listen(CacheHit::class, [$this, 'recordCacheHit']); 24 | $app['events']->listen(CacheMissed::class, [$this, 'recordCacheMissed']); 25 | 26 | $app['events']->listen(KeyWritten::class, [$this, 'recordKeyWritten']); 27 | $app['events']->listen(KeyForgotten::class, [$this, 'recordKeyForgotten']); 28 | } 29 | 30 | /** 31 | * Record a cache key was found. 32 | * 33 | * @param \Illuminate\Cache\Events\CacheHit $event 34 | * @return void 35 | */ 36 | public function recordCacheHit(CacheHit $event) 37 | { 38 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) { 39 | return; 40 | } 41 | 42 | Telescope::recordCache(IncomingEntry::make([ 43 | 'type' => 'hit', 44 | 'key' => $event->key, 45 | 'value' => $event->value, 46 | ])); 47 | } 48 | 49 | /** 50 | * Record a missing cache key. 51 | * 52 | * @param \Illuminate\Cache\Events\CacheMissed $event 53 | * @return void 54 | */ 55 | public function recordCacheMissed(CacheMissed $event) 56 | { 57 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) { 58 | return; 59 | } 60 | 61 | Telescope::recordCache(IncomingEntry::make([ 62 | 'type' => 'missed', 63 | 'key' => $event->key, 64 | ])); 65 | } 66 | 67 | /** 68 | * Record a cache key was updated. 69 | * 70 | * @param \Illuminate\Cache\Events\KeyWritten $event 71 | * @return void 72 | */ 73 | public function recordKeyWritten(KeyWritten $event) 74 | { 75 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) { 76 | return; 77 | } 78 | 79 | Telescope::recordCache(IncomingEntry::make([ 80 | 'type' => 'set', 81 | 'key' => $event->key, 82 | 'value' => $event->value, 83 | 'expiration' => $this->formatExpiration($event), 84 | ])); 85 | } 86 | 87 | /** 88 | * Record a cache key was forgotten / removed. 89 | * 90 | * @param \Illuminate\Cache\Events\KeyForgotten $event 91 | * @return void 92 | */ 93 | public function recordKeyForgotten(KeyForgotten $event) 94 | { 95 | if (! Telescope::isRecording() || $this->shouldIgnore($event)) { 96 | return; 97 | } 98 | 99 | Telescope::recordCache(IncomingEntry::make([ 100 | 'type' => 'forget', 101 | 'key' => $event->key, 102 | ])); 103 | } 104 | 105 | /** 106 | * @param \Illuminate\Cache\Events\KeyWritten $event 107 | * @return mixed 108 | */ 109 | protected function formatExpiration(KeyWritten $event) 110 | { 111 | return property_exists($event, 'seconds') 112 | ? $event->seconds : $event->minutes * 60; 113 | } 114 | 115 | /** 116 | * Determine if the event should be ignored. 117 | * 118 | * @param mixed $event 119 | * @return bool 120 | */ 121 | private function shouldIgnore($event) 122 | { 123 | return Str::is([ 124 | 'illuminate:queue:restart', 125 | 'framework/schedule*', 126 | 'telescope:*', 127 | ], $event->key); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /resources/js/screens/requests/preview.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 105 | -------------------------------------------------------------------------------- /resources/js/screens/jobs/preview.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 114 | 115 | 118 | -------------------------------------------------------------------------------- /src/Watchers/EventWatcher.php: -------------------------------------------------------------------------------- 1 | listen('*', [$this, 'recordEvent']); 27 | } 28 | 29 | /** 30 | * Record an event was fired. 31 | * 32 | * @param string $eventName 33 | * @param array $payload 34 | * @return void 35 | */ 36 | public function recordEvent($eventName, $payload) 37 | { 38 | if (! Telescope::isRecording() || $this->shouldIgnore($eventName)) { 39 | return; 40 | } 41 | 42 | $formattedPayload = $this->extractPayload($eventName, $payload); 43 | 44 | Telescope::recordEvent(IncomingEntry::make([ 45 | 'name' => $eventName, 46 | 'payload' => empty($formattedPayload) ? null : $formattedPayload, 47 | 'listeners' => $this->formatListeners($eventName), 48 | 'broadcast' => class_exists($eventName) 49 | ? in_array(ShouldBroadcast::class, (array) class_implements($eventName)) 50 | : false, 51 | ])->tags(class_exists($eventName) && isset($payload[0]) ? ExtractTags::from($payload[0]) : [])); 52 | } 53 | 54 | /** 55 | * Extract the payload and tags from the event. 56 | * 57 | * @param string $eventName 58 | * @param array $payload 59 | * @return array 60 | */ 61 | protected function extractPayload($eventName, $payload) 62 | { 63 | if (class_exists($eventName) && isset($payload[0]) && is_object($payload[0])) { 64 | return ExtractProperties::from($payload[0]); 65 | } 66 | 67 | return collect($payload)->map(function ($value) { 68 | return is_object($value) ? [ 69 | 'class' => get_class($value), 70 | 'properties' => json_decode(json_encode($value), true), 71 | ] : $value; 72 | })->toArray(); 73 | } 74 | 75 | /** 76 | * Format list of event listeners. 77 | * 78 | * @param string $eventName 79 | * @return array 80 | */ 81 | protected function formatListeners($eventName) 82 | { 83 | return collect(app('events')->getListeners($eventName)) 84 | ->map(function ($listener) { 85 | $listener = (new ReflectionFunction($listener)) 86 | ->getStaticVariables()['listener']; 87 | 88 | if (is_string($listener)) { 89 | return Str::contains($listener, '@') ? $listener : $listener.'@handle'; 90 | } elseif (is_array($listener)) { 91 | return get_class($listener[0]).'@'.$listener[1]; 92 | } 93 | 94 | return $this->formatClosureListener($listener); 95 | })->reject(function ($listener) { 96 | return Str::contains($listener, 'Laravel\\Telescope'); 97 | })->map(function ($listener) { 98 | if (Str::contains($listener, '@')) { 99 | $queued = in_array(ShouldQueue::class, class_implements(explode('@', $listener)[0])); 100 | } 101 | 102 | return [ 103 | 'name' => $listener, 104 | 'queued' => $queued ?? false, 105 | ]; 106 | })->values()->toArray(); 107 | } 108 | 109 | /** 110 | * Determine if the event should be ignored. 111 | * 112 | * @param string $eventName 113 | * @return bool 114 | */ 115 | protected function shouldIgnore($eventName) 116 | { 117 | return $this->eventIsIgnored($eventName) || 118 | (Telescope::$ignoreFrameworkEvents && $this->eventIsFiredByTheFramework($eventName)); 119 | } 120 | 121 | /** 122 | * Determine if the event was fired internally by Laravel. 123 | * 124 | * @param string $eventName 125 | * @return bool 126 | */ 127 | protected function eventIsFiredByTheFramework($eventName) 128 | { 129 | return Str::is( 130 | ['Illuminate\*', 'eloquent*', 'bootstrapped*', 'bootstrapping*', 'creating*', 'composing*'], 131 | $eventName 132 | ); 133 | } 134 | 135 | /** 136 | * Determine if the event is ignored manually. 137 | * 138 | * @param string $eventName 139 | * @return bool 140 | */ 141 | protected function eventIsIgnored($eventName) 142 | { 143 | return Str::is($this->options['ignore'] ?? [], $eventName); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /resources/js/screens/exceptions/preview.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 117 | 118 | 121 | -------------------------------------------------------------------------------- /src/Watchers/JobWatcher.php: -------------------------------------------------------------------------------- 1 | optional($this->recordJob($connection, $queue, $payload))->uuid]; 28 | }); 29 | 30 | $app['events']->listen(JobProcessed::class, [$this, 'recordProcessedJob']); 31 | $app['events']->listen(JobFailed::class, [$this, 'recordFailedJob']); 32 | } 33 | 34 | /** 35 | * Record a job being created. 36 | * 37 | * @param string $connection 38 | * @param string $queue 39 | * @param array $payload 40 | * @return \Laravel\Telescope\IncomingEntry|null 41 | */ 42 | public function recordJob($connection, $queue, array $payload) 43 | { 44 | if (! Telescope::isRecording()) { 45 | return; 46 | } 47 | 48 | $content = array_merge([ 49 | 'status' => 'pending', 50 | ], $this->defaultJobData($connection, $queue, $payload, $this->data($payload))); 51 | 52 | Telescope::recordJob( 53 | $entry = IncomingEntry::make($content) 54 | ->tags($this->tags($payload)) 55 | ); 56 | 57 | return $entry; 58 | } 59 | 60 | /** 61 | * Record a queued job was processed. 62 | * 63 | * @param \Illuminate\Queue\Events\JobProcessed $event 64 | * @return void 65 | */ 66 | public function recordProcessedJob(JobProcessed $event) 67 | { 68 | if (! Telescope::isRecording()) { 69 | return; 70 | } 71 | 72 | $uuid = $event->job->payload()['telescope_uuid'] ?? null; 73 | 74 | if (! $uuid) { 75 | return; 76 | } 77 | 78 | Telescope::recordUpdate(EntryUpdate::make( 79 | $uuid, EntryType::JOB, ['status' => 'processed'] 80 | )); 81 | } 82 | 83 | /** 84 | * Record a queue job has failed. 85 | * 86 | * @param \Illuminate\Queue\Events\JobFailed $event 87 | * @return void 88 | */ 89 | public function recordFailedJob(JobFailed $event) 90 | { 91 | if (! Telescope::isRecording()) { 92 | return; 93 | } 94 | 95 | $uuid = $event->job->payload()['telescope_uuid'] ?? null; 96 | 97 | if (! $uuid) { 98 | return; 99 | } 100 | 101 | Telescope::recordUpdate(EntryUpdate::make( 102 | $uuid, EntryType::JOB, [ 103 | 'status' => 'failed', 104 | 'exception' => [ 105 | 'message' => $event->exception->getMessage(), 106 | 'trace' => $event->exception->getTrace(), 107 | 'line' => $event->exception->getLine(), 108 | 'line_preview' => ExceptionContext::get($event->exception), 109 | ], 110 | ] 111 | )->addTags(['failed'])); 112 | } 113 | 114 | /** 115 | * Get the default entry data for the given job. 116 | * 117 | * @param string $connection 118 | * @param string $queue 119 | * @param array $payload 120 | * @param array $data 121 | * @return array 122 | */ 123 | protected function defaultJobData($connection, $queue, array $payload, array $data) 124 | { 125 | return [ 126 | 'connection' => $connection, 127 | 'queue' => $queue, 128 | 'name' => $payload['displayName'], 129 | 'tries' => $payload['maxTries'], 130 | 'timeout' => $payload['timeout'], 131 | 'data' => $data, 132 | ]; 133 | } 134 | 135 | /** 136 | * Extract the job "data" from the job payload. 137 | * 138 | * @param array $payload 139 | * @return array 140 | */ 141 | protected function data(array $payload) 142 | { 143 | if (! isset($payload['data']['command'])) { 144 | return $payload['data']; 145 | } 146 | 147 | return ExtractProperties::from( 148 | $payload['data']['command'] 149 | ); 150 | } 151 | 152 | /** 153 | * Extract the tags from the job payload. 154 | * 155 | * @param array $payload 156 | * @return array 157 | */ 158 | protected function tags(array $payload) 159 | { 160 | if (! isset($payload['data']['command'])) { 161 | return []; 162 | } 163 | 164 | return ExtractTags::fromJob( 165 | $payload['data']['command'] 166 | ); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /resources/js/routes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { path: '/', redirect: '/requests' }, 3 | 4 | { 5 | path: '/mail/:id', 6 | name: 'mail-preview', 7 | component: require('./screens/mail/preview').default, 8 | }, 9 | 10 | { 11 | path: '/mail', 12 | name: 'mail', 13 | component: require('./screens/mail/index').default, 14 | }, 15 | 16 | { 17 | path: '/exceptions/:id', 18 | name: 'exception-preview', 19 | component: require('./screens/exceptions/preview').default, 20 | }, 21 | 22 | { 23 | path: '/exceptions', 24 | name: 'exceptions', 25 | component: require('./screens/exceptions/index').default, 26 | }, 27 | 28 | { 29 | path: '/dumps', 30 | name: 'dumps', 31 | component: require('./screens/dumps/index').default, 32 | }, 33 | 34 | { 35 | path: '/logs/:id', 36 | name: 'log-preview', 37 | component: require('./screens/logs/preview').default, 38 | }, 39 | 40 | { 41 | path: '/logs', 42 | name: 'logs', 43 | component: require('./screens/logs/index').default, 44 | }, 45 | 46 | { 47 | path: '/notifications/:id', 48 | name: 'notification-preview', 49 | component: require('./screens/notifications/preview').default, 50 | }, 51 | 52 | { 53 | path: '/notifications', 54 | name: 'notifications', 55 | component: require('./screens/notifications/index').default, 56 | }, 57 | 58 | { 59 | path: '/jobs/:id', 60 | name: 'job-preview', 61 | component: require('./screens/jobs/preview').default, 62 | }, 63 | 64 | { 65 | path: '/jobs', 66 | name: 'jobs', 67 | component: require('./screens/jobs/index').default, 68 | }, 69 | 70 | { 71 | path: '/events/:id', 72 | name: 'event-preview', 73 | component: require('./screens/events/preview').default, 74 | }, 75 | 76 | { 77 | path: '/events', 78 | name: 'events', 79 | component: require('./screens/events/index').default, 80 | }, 81 | 82 | { 83 | path: '/cache/:id', 84 | name: 'cache-preview', 85 | component: require('./screens/cache/preview').default, 86 | }, 87 | 88 | { 89 | path: '/cache', 90 | name: 'cache', 91 | component: require('./screens/cache/index').default, 92 | }, 93 | 94 | { 95 | path: '/queries/:id', 96 | name: 'query-preview', 97 | component: require('./screens/queries/preview').default, 98 | }, 99 | 100 | { 101 | path: '/queries', 102 | name: 'queries', 103 | component: require('./screens/queries/index').default, 104 | }, 105 | 106 | { 107 | path: '/models/:id', 108 | name: 'model-preview', 109 | component: require('./screens/models/preview').default, 110 | }, 111 | 112 | { 113 | path: '/models', 114 | name: 'models', 115 | component: require('./screens/models/index').default, 116 | }, 117 | 118 | { 119 | path: '/requests/:id', 120 | name: 'request-preview', 121 | component: require('./screens/requests/preview').default, 122 | }, 123 | 124 | { 125 | path: '/requests', 126 | name: 'requests', 127 | component: require('./screens/requests/index').default, 128 | }, 129 | 130 | { 131 | path: '/commands/:id', 132 | name: 'command-preview', 133 | component: require('./screens/commands/preview').default, 134 | }, 135 | 136 | { 137 | path: '/commands', 138 | name: 'commands', 139 | component: require('./screens/commands/index').default, 140 | }, 141 | 142 | { 143 | path: '/schedule/:id', 144 | name: 'schedule-preview', 145 | component: require('./screens/schedule/preview').default, 146 | }, 147 | 148 | { 149 | path: '/schedule', 150 | name: 'schedule', 151 | component: require('./screens/schedule/index').default, 152 | }, 153 | 154 | { 155 | path: '/redis/:id', 156 | name: 'redis-preview', 157 | component: require('./screens/redis/preview').default, 158 | }, 159 | 160 | { 161 | path: '/redis', 162 | name: 'redis', 163 | component: require('./screens/redis/index').default, 164 | }, 165 | 166 | { 167 | path: '/monitored-tags', 168 | name: 'monitored-tags', 169 | component: require('./screens/monitoring/index').default, 170 | }, 171 | 172 | { 173 | path: '/gates/:id', 174 | name: 'gate-preview', 175 | component: require('./screens/gates/preview').default, 176 | }, 177 | 178 | { 179 | path: '/gates', 180 | name: 'gates', 181 | component: require('./screens/gates/index').default, 182 | }, 183 | 184 | { 185 | path: '/views/:id', 186 | name: 'view-preview', 187 | component: require('./screens/views/preview').default, 188 | }, 189 | 190 | { 191 | path: '/views', 192 | name: 'views', 193 | component: require('./screens/views/index').default, 194 | }, 195 | ]; 196 | -------------------------------------------------------------------------------- /src/TelescopeServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerRoutes(); 28 | $this->registerMigrations(); 29 | $this->registerPublishing(); 30 | 31 | Telescope::start($this->app); 32 | Telescope::listenForStorageOpportunities($this->app); 33 | 34 | $this->loadViewsFrom( 35 | __DIR__.'/../resources/views', 'telescope' 36 | ); 37 | } 38 | 39 | /** 40 | * Register the package routes. 41 | * 42 | * @return void 43 | */ 44 | private function registerRoutes() 45 | { 46 | Route::group($this->routeConfiguration(), function () { 47 | $this->loadRoutesFrom(__DIR__.'/Http/routes.php'); 48 | }); 49 | } 50 | 51 | /** 52 | * Get the Telescope route group configuration array. 53 | * 54 | * @return array 55 | */ 56 | private function routeConfiguration() 57 | { 58 | return [ 59 | 'domain' => config('telescope.domain', null), 60 | 'namespace' => 'Laravel\Telescope\Http\Controllers', 61 | 'prefix' => config('telescope.path'), 62 | 'middleware' => 'telescope', 63 | ]; 64 | } 65 | 66 | /** 67 | * Register the package's migrations. 68 | * 69 | * @return void 70 | */ 71 | private function registerMigrations() 72 | { 73 | if ($this->app->runningInConsole() && $this->shouldMigrate()) { 74 | $this->loadMigrationsFrom(__DIR__.'/Storage/migrations'); 75 | } 76 | } 77 | 78 | /** 79 | * Register the package's publishable resources. 80 | * 81 | * @return void 82 | */ 83 | private function registerPublishing() 84 | { 85 | if ($this->app->runningInConsole()) { 86 | $this->publishes([ 87 | __DIR__.'/Storage/migrations' => database_path('migrations'), 88 | ], 'telescope-migrations'); 89 | 90 | $this->publishes([ 91 | __DIR__.'/../public' => public_path('vendor/telescope'), 92 | ], 'telescope-assets'); 93 | 94 | $this->publishes([ 95 | __DIR__.'/../config/telescope.php' => config_path('telescope.php'), 96 | ], 'telescope-config'); 97 | 98 | $this->publishes([ 99 | __DIR__.'/../stubs/TelescopeServiceProvider.stub' => app_path('Providers/TelescopeServiceProvider.php'), 100 | ], 'telescope-provider'); 101 | } 102 | } 103 | 104 | /** 105 | * Register any package services. 106 | * 107 | * @return void 108 | */ 109 | public function register() 110 | { 111 | $this->mergeConfigFrom( 112 | __DIR__.'/../config/telescope.php', 'telescope' 113 | ); 114 | 115 | $this->registerStorageDriver(); 116 | 117 | $this->commands([ 118 | Console\ClearCommand::class, 119 | Console\InstallCommand::class, 120 | Console\PruneCommand::class, 121 | Console\PublishCommand::class, 122 | ]); 123 | } 124 | 125 | /** 126 | * Register the package storage driver. 127 | * 128 | * @return void 129 | */ 130 | protected function registerStorageDriver() 131 | { 132 | $driver = config('telescope.driver'); 133 | 134 | if (method_exists($this, $method = 'register'.ucfirst($driver).'Driver')) { 135 | $this->$method(); 136 | } 137 | } 138 | 139 | /** 140 | * Register the package database storage driver. 141 | * 142 | * @return void 143 | */ 144 | protected function registerDatabaseDriver() 145 | { 146 | $this->app->singleton( 147 | EntriesRepository::class, DatabaseEntriesRepository::class 148 | ); 149 | 150 | $this->app->singleton( 151 | ClearableRepository::class, DatabaseEntriesRepository::class 152 | ); 153 | 154 | $this->app->singleton( 155 | PrunableRepository::class, DatabaseEntriesRepository::class 156 | ); 157 | 158 | $this->app->when(DatabaseEntriesRepository::class) 159 | ->needs('$connection') 160 | ->give(config('telescope.storage.database.connection')); 161 | 162 | $this->app->when(DatabaseEntriesRepository::class) 163 | ->needs('$chunkSize') 164 | ->give(config('telescope.storage.database.chunk')); 165 | } 166 | 167 | /** 168 | * Determine if we should register the migrations. 169 | * 170 | * @return bool 171 | */ 172 | protected function shouldMigrate() 173 | { 174 | return Telescope::$runsMigrations && config('telescope.driver') === 'database'; 175 | } 176 | } 177 | --------------------------------------------------------------------------------