diff --git a/resources/js/components/RelatedEntries.vue b/resources/js/components/RelatedEntries.vue index 037f89b95..56df1a69a 100644 --- a/resources/js/components/RelatedEntries.vue +++ b/resources/js/components/RelatedEntries.vue @@ -126,6 +126,13 @@ return _.filter(this.batch, {type: 'view'}); }, + queriesSummary() { + return { + time: _.reduce(this.queries, (time, q) => { return time + parseFloat(q.content.time) }, 0.00), + duplicated: this.queries.length - _.size(_.groupBy(this.queries, 'family_hash')), + }; + }, + tabs(){ return _.filter([ {title: "Exceptions", type: "exceptions", count: this.exceptions.length}, @@ -237,12 +244,11 @@
Query | -Duration | +Query {{ queries.length }} queries, {{ queriesSummary.duplicated }} of which are duplicated. |
+ Duration {{ queriesSummary.duplicated }} ms |
|
---|---|---|---|---|
{{truncate(entry.content.sql, 110)}} | diff --git a/src/IncomingEntry.php b/src/IncomingEntry.php index 8751ca7ed..ce4c40da7 100644 --- a/src/IncomingEntry.php +++ b/src/IncomingEntry.php @@ -117,6 +117,19 @@ public function type(string $type) return $this; } + /** + * Assign the entry a family hash. + * + * @param string $familyHash + * @return $this + */ + public function withFamilyHash(string $familyHash) + { + $this->familyHash = $familyHash; + + return $this; + } + /** * Set the currently authenticated user. * @@ -249,6 +262,7 @@ public function toArray() return [ 'uuid' => $this->uuid, 'batch_id' => $this->batchId, + 'family_hash' => $this->familyHash, 'type' => $this->type, 'content' => $this->content, 'created_at' => $this->recordedAt->toDateTimeString(), diff --git a/src/Watchers/QueryWatcher.php b/src/Watchers/QueryWatcher.php index 759d91cb0..16fd39679 100644 --- a/src/Watchers/QueryWatcher.php +++ b/src/Watchers/QueryWatcher.php @@ -39,13 +39,13 @@ public function recordQuery(QueryExecuted $event) Telescope::recordQuery(IncomingEntry::make([ 'connection' => $event->connectionName, - 'bindings' => $this->formatBindings($event), - 'sql' => $event->sql, + 'bindings' => [], + 'sql' => $this->replaceBindings($event), 'time' => number_format($time, 2), 'slow' => isset($this->options['slow']) && $time >= $this->options['slow'], 'file' => $caller['file'], 'line' => $caller['line'], - ])->tags($this->tags($event))); + ])->tags($this->tags($event))->withFamilyHash($this->familyHash($event))); } /** @@ -59,6 +59,17 @@ protected function tags($event) return isset($this->options['slow']) && $event->time >= $this->options['slow'] ? ['slow'] : []; } + /** + * Calculate the family look-up hash for the query event. + * + * @param \Illuminate\Database\Events\QueryExecuted $event + * @return string + */ + public function familyHash($event) + { + return md5($event->sql); + } + /** * Format the given bindings to strings. * @@ -69,4 +80,33 @@ protected function formatBindings($event) { return $event->connection->prepareBindings($event->bindings); } + + /** + * Replace the placeholders with the actual bindings. + * + * @param \Illuminate\Database\Events\QueryExecuted $event + * @return string + */ + public function replaceBindings($event) + { + $sql = $event->sql; + + foreach ($this->formatBindings($event) as $key => $binding) { + // This regex matches placeholders only, not the question marks, + // nested in quotes, while we iterate through the bindings + // and substitute placeholders by suitable values. + $regex = is_numeric($key) + ? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/" + : "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"; + + // Quote strings + if (! is_int($binding) && ! is_float($binding)) { + $binding = $event->connection->getPdo()->quote($binding); + } + + $sql = preg_replace($regex, $binding, $sql, 1); + } + + return $sql; + } } diff --git a/tests/Watchers/QueryWatcherTest.php b/tests/Watchers/QueryWatcherTest.php index c40fd91fc..b2f4c4e25 100644 --- a/tests/Watchers/QueryWatcherTest.php +++ b/tests/Watchers/QueryWatcherTest.php @@ -2,6 +2,7 @@ namespace Laravel\Telescope\Tests\Watchers; +use Carbon\Carbon; use Illuminate\Support\Str; use Laravel\Telescope\EntryType; use Illuminate\Support\Collection; @@ -47,8 +48,24 @@ public function test_query_watcher_can_tag_slow_queries() $entry = $this->loadTelescopeEntries()->first(); $this->assertSame(EntryType::QUERY, $entry->type); - $this->assertCount(300, $entry->content['bindings']); + $this->assertGreaterThan(300 * 16, strlen($entry->content['sql'])); $this->assertSame('testbench', $entry->content['connection']); $this->assertTrue($entry->content['slow']); } + + public function test_query_watcher_can_prepare_bindings() + { + $this->app->get('db')->table('telescope_entries') + ->where('type', 'query') + ->where('should_display_on_index', true) + ->whereNull('family_hash') + ->where('created_at', '<', Carbon::parse('2019-01-01')) + ->count(); + + $entry = $this->loadTelescopeEntries()->first(); + + $this->assertSame(EntryType::QUERY, $entry->type); + $this->assertSame('select count(*) as aggregate from "telescope_entries" where "type" = \'query\' and "should_display_on_index" = 1 and "family_hash" is null and "created_at" < \'2019-01-01 00:00:00\'', $entry->content['sql']); + $this->assertSame('testbench', $entry->content['connection']); + } }