From fa9c9ac65c13a133e0f3877ab9f11810f13fcbbb Mon Sep 17 00:00:00 2001
From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com>
Date: Sat, 8 Aug 2026 18:53:31 +0000
Subject: [PATCH 01/21] Correct Telescope storage and query boundaries
Persist every monitored tag through the existing bulk insert, encode updates with invalid UTF-8 substitution, and aggregate exception families before writing updates. Preserve nullable custom repository results while keeping the built-in repository concrete.
Apply explicit UUID, tag, and sequence filters without dropping valid falsey identifiers. Order chunked deletes by indexed columns and add counterfactual coverage for empty UUID sets, tag and sequence zero, deterministic aggregation, ordered deletion, invalid UTF-8, and custom repository behavior.
---
.../src/Jobs/ProcessPendingUpdates.php | 2 +-
.../src/Storage/DatabaseEntriesRepository.php | 59 +++---
src/telescope/src/Storage/EntryModel.php | 37 ++--
.../src/Storage/EntryQueryOptions.php | 14 +-
.../Jobs/ProcessPendingUpdatesTest.php | 27 ++-
.../Storage/DatabaseEntriesRepositoryTest.php | 175 +++++++++++++++---
6 files changed, 234 insertions(+), 80 deletions(-)
diff --git a/src/telescope/src/Jobs/ProcessPendingUpdates.php b/src/telescope/src/Jobs/ProcessPendingUpdates.php
index 0c54e7030..8a11e54f3 100644
--- a/src/telescope/src/Jobs/ProcessPendingUpdates.php
+++ b/src/telescope/src/Jobs/ProcessPendingUpdates.php
@@ -40,7 +40,7 @@ public function handle(EntriesRepository $repository): void
$delay = config('telescope.queue.delay');
- $repository->update($this->pendingUpdates)->whenNotEmpty(
+ $repository->update($this->pendingUpdates)?->whenNotEmpty(
fn ($pendingUpdates) => static::dispatchIf(
$this->attempt < 3,
$pendingUpdates,
diff --git a/src/telescope/src/Storage/DatabaseEntriesRepository.php b/src/telescope/src/Storage/DatabaseEntriesRepository.php
index 023f16462..03aff7716 100644
--- a/src/telescope/src/Storage/DatabaseEntriesRepository.php
+++ b/src/telescope/src/Storage/DatabaseEntriesRepository.php
@@ -6,6 +6,7 @@
use DateTimeInterface;
use Hypervel\Context\CoroutineContext;
+use Hypervel\Database\Query\Builder;
use Hypervel\Database\UniqueConstraintViolationException;
use Hypervel\Support\Collection;
use Hypervel\Support\Facades\DB;
@@ -140,19 +141,30 @@ public function store(Collection $entries): void
protected function storeExceptions(Collection $exceptions): void
{
$exceptions->chunk($this->chunkSize)->each(function ($chunked) {
- $this->table('telescope_entries')->insert($chunked->map(function ($exception) {
- $occurrences = $this->countExceptionOccurences($exception);
+ $occurrences = [];
+ $lastUuids = [];
- $this->table('telescope_entries')
- ->where('type', EntryType::EXCEPTION)
- ->where('family_hash', $exception->familyHash())
- ->where('should_display_on_index', true)
- ->update(['should_display_on_index' => false]);
+ $chunked->groupBy(fn ($exception) => $exception->familyHash())
+ ->each(function ($family, $familyHash) use (&$occurrences, &$lastUuids): void {
+ $occurrences[$familyHash] = $this->countExceptionOccurences($family->first());
+ $lastUuids[$familyHash] = $family->last()->uuid;
+
+ $this->table('telescope_entries')
+ ->where('type', EntryType::EXCEPTION)
+ ->where('family_hash', $familyHash)
+ ->where('should_display_on_index', true)
+ ->update(['should_display_on_index' => false]);
+ });
+
+ $this->table('telescope_entries')->insert($chunked->map(function ($exception) use (&$occurrences, $lastUuids) {
+ $familyHash = $exception->familyHash();
+ ++$occurrences[$familyHash];
return array_merge($exception->toArray(), [
- 'family_hash' => $exception->familyHash(),
+ 'family_hash' => $familyHash,
+ 'should_display_on_index' => $exception->uuid === $lastUuids[$familyHash],
'content' => json_encode(
- array_merge($exception->content, ['occurrences' => $occurrences + 1]),
+ array_merge($exception->content, ['occurrences' => $occurrences[$familyHash]]),
JSON_INVALID_UTF8_SUBSTITUTE
),
]);
@@ -203,7 +215,7 @@ protected function insertChunkOfTags(array $tags): void
/**
* Store the given entry updates and return the failed updates.
*/
- public function update(Collection $updates): ?Collection
+ public function update(Collection $updates): Collection
{
$failedUpdates = [];
@@ -219,10 +231,10 @@ public function update(Collection $updates): ?Collection
continue;
}
- $content = json_encode(array_merge(
- json_decode($entry->content ?? $entry['content'] ?? [], true) ?: [],
- $update->changes
- ));
+ $content = json_encode(
+ array_merge(json_decode($entry->content, true) ?: [], $update->changes),
+ JSON_INVALID_UTF8_SUBSTITUTE,
+ );
$this->table('telescope_entries')
->where('uuid', $update->uuid)
@@ -316,17 +328,15 @@ public function monitoring(): array
*/
public function monitor(array $tags): void
{
- $tags = array_diff($tags, $this->monitoring());
+ $tags = array_values(array_diff(array_unique($tags), $this->monitoring()));
if (empty($tags)) {
return;
}
- $this->table('telescope_monitoring')
- ->insert(Collection::make($tags)
- ->mapWithKeys(function ($tag) {
- return ['tag' => $tag];
- })->all());
+ $this->table('telescope_monitoring')->insert(
+ array_map(static fn (string $tag): array => ['tag' => $tag], $tags),
+ );
}
/**
@@ -345,7 +355,8 @@ public function stopMonitoring(array $tags): void
public function prune(DateTimeInterface $before, bool $keepExceptions): int
{
$query = $this->table('telescope_entries')
- ->where('created_at', '<', $before);
+ ->where('created_at', '<', $before)
+ ->orderBy('sequence');
if ($keepExceptions) {
$query->where('type', '!=', 'exception');
@@ -368,11 +379,11 @@ public function prune(DateTimeInterface $before, bool $keepExceptions): int
public function clear(): void
{
do {
- $deleted = $this->table('telescope_entries')->take($this->chunkSize)->delete();
+ $deleted = $this->table('telescope_entries')->orderBy('sequence')->take($this->chunkSize)->delete();
} while ($deleted !== 0);
do {
- $deleted = $this->table('telescope_monitoring')->take($this->chunkSize)->delete();
+ $deleted = $this->table('telescope_monitoring')->orderBy('tag')->take($this->chunkSize)->delete();
} while ($deleted !== 0);
}
@@ -387,7 +398,7 @@ public function terminate(): void
/**
* Get a query builder instance for the given table.
*/
- protected function table(string $table)
+ protected function table(string $table): Builder
{
return DB::connection($this->connection)->table($table);
}
diff --git a/src/telescope/src/Storage/EntryModel.php b/src/telescope/src/Storage/EntryModel.php
index ee6d7e64f..496c8de43 100644
--- a/src/telescope/src/Storage/EntryModel.php
+++ b/src/telescope/src/Storage/EntryModel.php
@@ -55,6 +55,7 @@ public function scopeWithTelescopeOptions(Builder $query, ?string $type, EntryQu
{
$this->whereType($query, $type)
->whereBatchId($query, $options)
+ ->whereUuids($query, $options)
->whereTag($query, $options)
->whereFamilyHash($query, $options)
->whereBeforeSequence($query, $options)
@@ -88,30 +89,38 @@ protected function whereBatchId(Builder $query, EntryQueryOptions $options): sta
}
/**
- * Scope the query for the given type.
+ * Scope the query for the given entry UUIDs.
*/
- protected function whereTag(Builder $query, EntryQueryOptions $options): static
+ protected function whereUuids(Builder $query, EntryQueryOptions $options): static
{
- $query->when($options->tag, function ($query, $tag) {
- $tags = Collection::make(explode(',', $tag))->map(fn ($tag) => trim($tag));
+ if ($options->uuids !== null) {
+ $query->whereIn('uuid', $options->uuids);
+ }
- if ($tags->isEmpty()) {
- return $query;
- }
+ return $this;
+ }
- return $query->whereIn('uuid', function ($query) use ($tags) {
+ /**
+ * Scope the query for the given tag.
+ */
+ protected function whereTag(Builder $query, EntryQueryOptions $options): static
+ {
+ if ($options->tag !== null) {
+ $tags = Collection::make(explode(',', $options->tag))->map(fn ($tag) => trim($tag));
+
+ $query->whereIn('uuid', function ($query) use ($tags) {
$query->select('entry_uuid')->from('telescope_entries_tags')
->whereIn('entry_uuid', function ($query) use ($tags) {
$query->select('entry_uuid')->from('telescope_entries_tags')->whereIn('tag', $tags->all());
});
});
- });
+ }
return $this;
}
/**
- * Scope the query for the given type.
+ * Scope the query for the given family hash.
*/
protected function whereFamilyHash(Builder $query, EntryQueryOptions $options): static
{
@@ -127,9 +136,9 @@ protected function whereFamilyHash(Builder $query, EntryQueryOptions $options):
*/
protected function whereBeforeSequence(Builder $query, EntryQueryOptions $options): static
{
- $query->when($options->beforeSequence, function ($query, $beforeSequence) {
- return $query->where('sequence', '<', $beforeSequence);
- });
+ if ($options->beforeSequence !== null) {
+ $query->where('sequence', '<', $options->beforeSequence);
+ }
return $this;
}
@@ -139,7 +148,7 @@ protected function whereBeforeSequence(Builder $query, EntryQueryOptions $option
*/
protected function filter(Builder $query, EntryQueryOptions $options): static
{
- if ($options->familyHash || $options->tag || $options->batchId) {
+ if ($options->familyHash || $options->tag !== null || $options->batchId) {
return $this;
}
diff --git a/src/telescope/src/Storage/EntryQueryOptions.php b/src/telescope/src/Storage/EntryQueryOptions.php
index 96d3132bb..d0cdd46f1 100644
--- a/src/telescope/src/Storage/EntryQueryOptions.php
+++ b/src/telescope/src/Storage/EntryQueryOptions.php
@@ -26,12 +26,12 @@ class EntryQueryOptions
/**
* The ID that all retrieved entries should be less than.
*/
- public mixed $beforeSequence = null;
+ public int|string|null $beforeSequence = null;
/**
- * The list of UUIDs of entries tor retrieve.
+ * The list of UUIDs of entries to retrieve.
*/
- public mixed $uuids = null;
+ public ?array $uuids = null;
/**
* The number of entries to retrieve.
@@ -71,7 +71,7 @@ public function batchId(?string $batchId): static
}
/**
- * Set the list of UUIDs of entries tor retrieve.
+ * Set the list of UUIDs of entries to retrieve.
*/
public function uuids(?array $uuids): static
{
@@ -83,9 +83,9 @@ public function uuids(?array $uuids): static
/**
* Set the ID that all retrieved entries should be less than.
*/
- public function beforeSequence(mixed $id): static
+ public function beforeSequence(int|string|null $id): static
{
- $this->beforeSequence = $id;
+ $this->beforeSequence = $id === '' ? null : $id;
return $this;
}
@@ -95,7 +95,7 @@ public function beforeSequence(mixed $id): static
*/
public function tag(?string $tag): static
{
- $this->tag = $tag;
+ $this->tag = $tag === '' ? null : $tag;
return $this;
}
diff --git a/tests/Telescope/Jobs/ProcessPendingUpdatesTest.php b/tests/Telescope/Jobs/ProcessPendingUpdatesTest.php
index b8595c8d0..fd8ba4952 100644
--- a/tests/Telescope/Jobs/ProcessPendingUpdatesTest.php
+++ b/tests/Telescope/Jobs/ProcessPendingUpdatesTest.php
@@ -12,7 +12,7 @@
class ProcessPendingUpdatesTest extends FeatureTestCase
{
- public function testPendingUpdates()
+ public function testPendingUpdates(): void
{
Bus::fake();
@@ -35,7 +35,7 @@ public function testPendingUpdates()
Bus::assertNothingDispatched();
}
- public function testPendingUpdatesMayStayPending()
+ public function testPendingUpdatesMayStayPending(): void
{
Bus::fake();
@@ -58,11 +58,11 @@ public function testPendingUpdatesMayStayPending()
(new ProcessPendingUpdates($pendingUpdates))->handle($repository);
Bus::assertDispatched(ProcessPendingUpdates::class, function ($job) {
- return $job->attempt == 1 && $job->pendingUpdates->toArray() == [['id' => 2, 'content' => 'bar']];
+ return $job->attempt === 1 && $job->pendingUpdates->toArray() === [['id' => 2, 'content' => 'bar']];
});
}
- public function testPendingUpdatesMayStayPendingOnlyThreeTimes()
+ public function testPendingUpdatesMayStayPendingOnlyThreeTimes(): void
{
Bus::fake();
@@ -86,4 +86,23 @@ public function testPendingUpdatesMayStayPendingOnlyThreeTimes()
Bus::assertNothingDispatched();
}
+
+ public function testNullableCustomRepositoryResultDoesNotDispatchAnotherUpdate(): void
+ {
+ Bus::fake();
+
+ $pendingUpdates = collect([
+ ['id' => 1, 'content' => 'foo'],
+ ]);
+
+ $repository = m::mock(EntriesRepository::class);
+ $repository->shouldReceive('update')
+ ->once()
+ ->with($pendingUpdates)
+ ->andReturnNull();
+
+ (new ProcessPendingUpdates($pendingUpdates))->handle($repository);
+
+ Bus::assertNothingDispatched();
+ }
}
diff --git a/tests/Telescope/Storage/DatabaseEntriesRepositoryTest.php b/tests/Telescope/Storage/DatabaseEntriesRepositoryTest.php
index a0953faf6..c7e017b0a 100644
--- a/tests/Telescope/Storage/DatabaseEntriesRepositoryTest.php
+++ b/tests/Telescope/Storage/DatabaseEntriesRepositoryTest.php
@@ -13,11 +13,12 @@
use Hypervel\Telescope\IncomingEntry;
use Hypervel\Telescope\IncomingExceptionEntry;
use Hypervel\Telescope\Storage\DatabaseEntriesRepository;
+use Hypervel\Telescope\Storage\EntryQueryOptions;
use Hypervel\Tests\Telescope\FeatureTestCase;
class DatabaseEntriesRepositoryTest extends FeatureTestCase
{
- public function testFindEntryByUuid()
+ public function testFindEntryByUuid(): void
{
$entry = EntryModelFactory::new()->create();
@@ -34,7 +35,7 @@ public function testFindEntryByUuid()
$this->assertNull($result['sequence']);
}
- public function testUpdate()
+ public function testUpdate(): void
{
$entry = EntryModelFactory::new()->create();
@@ -53,7 +54,123 @@ public function testUpdate()
$this->assertSame('missing-id', $failedUpdates->first()->uuid);
}
- public function testStoreBinaryContent()
+ public function testUpdateSubstitutesInvalidUtf8(): void
+ {
+ $entry = EntryModelFactory::new()->create(['content' => ['existing' => true]]);
+ $repository = $this->app->make(DatabaseEntriesRepository::class);
+
+ $repository->update(collect([
+ new EntryUpdate($entry->uuid, $entry->type, ['nested' => ['value' => "\xB1\x31"]]),
+ ]));
+
+ $content = json_decode(
+ DB::table('telescope_entries')->where('uuid', $entry->uuid)->value('content'),
+ true,
+ flags: JSON_THROW_ON_ERROR,
+ );
+
+ $this->assertTrue($content['existing']);
+ $this->assertSame("\u{FFFD}1", $content['nested']['value']);
+ }
+
+ public function testGetAppliesExplicitUuidFilters(): void
+ {
+ $requested = EntryModelFactory::new()->create();
+ EntryModelFactory::new()->create();
+
+ $repository = $this->app->make(DatabaseEntriesRepository::class);
+
+ $entries = $repository->get(null, (new EntryQueryOptions)->uuids([$requested->uuid])->limit(-1));
+
+ $this->assertSame([$requested->uuid], $entries->pluck('id')->all());
+ $this->assertTrue($repository->get(null, (new EntryQueryOptions)->uuids([])->limit(-1))->isEmpty());
+ }
+
+ public function testGetPreservesFalseyTagAndSequenceFilters(): void
+ {
+ $tagged = EntryModelFactory::new()->create();
+ EntryModelFactory::new()->create();
+
+ DB::table('telescope_entries_tags')->insert([
+ 'entry_uuid' => $tagged->uuid,
+ 'tag' => '0',
+ ]);
+
+ $repository = $this->app->make(DatabaseEntriesRepository::class);
+
+ $taggedEntries = $repository->get(null, (new EntryQueryOptions)->tag('0')->limit(-1));
+
+ $this->assertSame([$tagged->uuid], $taggedEntries->pluck('id')->all());
+ $this->assertTrue($repository->get(null, (new EntryQueryOptions)->beforeSequence(0)->limit(-1))->isEmpty());
+ $this->assertCount(2, $repository->get(null, (new EntryQueryOptions)->tag('')->limit(-1)));
+ $this->assertCount(2, $repository->get(null, (new EntryQueryOptions)->beforeSequence('')->limit(-1)));
+ }
+
+ public function testMonitorStoresEveryUniqueNewTag(): void
+ {
+ DB::table('telescope_monitoring')->insert(['tag' => 'existing']);
+
+ $this->app->make(DatabaseEntriesRepository::class)->monitor([
+ 'existing',
+ 'first',
+ 'second',
+ 'first',
+ ]);
+
+ $this->assertSame(
+ ['existing', 'first', 'second'],
+ DB::table('telescope_monitoring')->orderBy('tag')->pluck('tag')->all(),
+ );
+ }
+
+ public function testPruneOrdersDeletesToAvoidDeadlocks(): void
+ {
+ EntryModelFactory::new()->create(['created_at' => now()->subDays(2)]);
+
+ $deletes = [];
+
+ DB::listen(function ($query) use (&$deletes): void {
+ if (str_starts_with($query->sql, 'delete')) {
+ $deletes[] = $query->sql;
+ }
+ });
+
+ $this->app->make(DatabaseEntriesRepository::class)->prune(now()->subDay(), false);
+
+ $this->assertNotEmpty($deletes);
+
+ foreach ($deletes as $sql) {
+ $this->assertStringContainsString('order by', $sql);
+ }
+ }
+
+ public function testClearOrdersDeletesToAvoidDeadlocks(): void
+ {
+ EntryModelFactory::new()->create();
+
+ DB::table('telescope_monitoring')->insert([
+ ['tag' => 'one'],
+ ['tag' => 'two'],
+ ]);
+
+ $deletes = [];
+
+ DB::listen(function ($query) use (&$deletes): void {
+ if (str_starts_with($query->sql, 'delete')) {
+ $deletes[] = $query->sql;
+ }
+ });
+
+ $this->app->make(DatabaseEntriesRepository::class)->clear();
+
+ $this->assertNotEmpty($deletes);
+
+ foreach ($deletes as $sql) {
+ $this->assertStringContainsString('order by', $sql);
+ }
+ }
+
+ public function testStoreBinaryContent(): void
{
$batchId = (string) Str::uuid();
$exception = new Exception('message');
@@ -79,42 +196,40 @@ public function testStoreBinaryContent()
});
}
- public function testStoreExceptionsOnlyUpdatesVisibleRows()
+ public function testStoreExceptionsAggregatesFamiliesWithinEachChunk(): void
{
- $exception = new Exception('repeated error');
$batchId = (string) Str::uuid();
-
- $makeEntry = fn () => (new IncomingExceptionEntry($exception, [
- 'file' => $exception->getFile(),
- 'line' => $exception->getLine(),
- 'message' => $exception->getMessage(),
- ]))->batchId($batchId)->type(EntryType::EXCEPTION);
-
$repository = $this->app->make(DatabaseEntriesRepository::class);
- // Store the first occurrence.
- $repository->store(collect([$makeEntry()]));
+ $makeEntry = fn (string $file, int $line) => (new IncomingExceptionEntry(new Exception('error'), [
+ 'file' => $file,
+ 'line' => $line,
+ 'message' => 'error',
+ ]))->batchId($batchId)->type(EntryType::EXCEPTION);
- // Store a second occurrence — should hide the first.
- $repository->store(collect([$makeEntry()]));
+ $persisted = $makeEntry('first.php', 10);
+ $repository->store(collect([$persisted]));
- $entries = DB::table('telescope_entries')
- ->where('type', EntryType::EXCEPTION)
- ->get();
-
- $this->assertCount(2, $entries);
- $this->assertCount(1, $entries->where('should_display_on_index', true));
- $this->assertCount(1, $entries->where('should_display_on_index', false));
+ $first = $makeEntry('first.php', 10);
+ $other = $makeEntry('other.php', 20);
+ $last = $makeEntry('first.php', 10);
- // Store a third occurrence — should only update the one visible row, not both hidden ones.
- $repository->store(collect([$makeEntry()]));
+ $repository->store(collect([$first, $other, $last]));
$entries = DB::table('telescope_entries')
->where('type', EntryType::EXCEPTION)
- ->get();
-
- $this->assertCount(3, $entries);
- $this->assertCount(1, $entries->where('should_display_on_index', true));
- $this->assertCount(2, $entries->where('should_display_on_index', false));
+ ->get()
+ ->keyBy('uuid');
+
+ $this->assertCount(4, $entries);
+ $this->assertFalse((bool) $entries[$persisted->uuid]->should_display_on_index);
+ $this->assertFalse((bool) $entries[$first->uuid]->should_display_on_index);
+ $this->assertTrue((bool) $entries[$other->uuid]->should_display_on_index);
+ $this->assertTrue((bool) $entries[$last->uuid]->should_display_on_index);
+
+ $this->assertSame(1, json_decode($entries[$persisted->uuid]->content, true)['occurrences']);
+ $this->assertSame(2, json_decode($entries[$first->uuid]->content, true)['occurrences']);
+ $this->assertSame(1, json_decode($entries[$other->uuid]->content, true)['occurrences']);
+ $this->assertSame(3, json_decode($entries[$last->uuid]->content, true)['occurrences']);
}
}
From 0567a2f785ea89cd64835f9d2872a73d720fe657 Mon Sep 17 00:00:00 2001
From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com>
Date: Sat, 8 Aug 2026 18:53:39 +0000
Subject: [PATCH 02/21] Make Telescope recording failure-safe
Release the coroutine-local recursion guard in a finally block so failures from tag, filter, or after-recording callbacks propagate without suppressing later entries. Remove obsolete request-domain helpers now that route metadata owns host matching.
Restore the Laravel CSP nonce API with coroutine-local ownership and render the nonce on dashboard style and module-script tags. Add failure-path and concurrent-isolation regressions for both behaviors.
---
src/telescope/src/Telescope.php | 95 +++++++++++----------
tests/Telescope/Http/CspTest.php | 56 ++++++++++++
tests/Telescope/Telescope/TelescopeTest.php | 52 +++++++++++
3 files changed, 158 insertions(+), 45 deletions(-)
create mode 100644 tests/Telescope/Http/CspTest.php
diff --git a/src/telescope/src/Telescope.php b/src/telescope/src/Telescope.php
index 381cf04b7..604bdc375 100644
--- a/src/telescope/src/Telescope.php
+++ b/src/telescope/src/Telescope.php
@@ -56,6 +56,8 @@ class Telescope
public const BATCH_ID_CONTEXT_KEY = '__telescope.batch_id';
+ protected const CSP_NONCE_CONTEXT_KEY = '__telescope.csp_nonce';
+
/**
* The callbacks that filter the entries that should be recorded.
*/
@@ -165,28 +167,6 @@ protected static function commandIsApproved(?string $command): bool
);
}
- /**
- * Determine if the application is handling an approved request.
- */
- protected static function handlingApprovedRequest(Application $app): bool
- {
- if ($app->runningInConsole()) {
- return false;
- }
-
- return static::requestIsToApprovedDomain($app['request'])
- && static::requestIsToApprovedUri($app['request']);
- }
-
- /**
- * Determine if the request is to an approved domain.
- */
- protected static function requestIsToApprovedDomain(Request $request): bool
- {
- return is_null(config('telescope.domain'))
- || config('telescope.domain') !== $request->getHost();
- }
-
/**
* Determine if the request is to an approved URI.
*/
@@ -301,30 +281,32 @@ protected static function record(string $type, IncomingEntry $entry): void
CoroutineContext::set(static::IS_RECORDING_CONTEXT_KEY, true);
try {
- if (Auth::hasUser()) {
- $entry->user(Auth::user());
+ try {
+ if (Auth::hasUser()) {
+ $entry->user(Auth::user());
+ }
+ } catch (Throwable $e) {
+ // Do nothing.
}
- } catch (Throwable $e) {
- // Do nothing.
- }
- $entry->type($type)->tags(Arr::collapse(array_map(function ($tagCallback) use ($entry) {
- return $tagCallback($entry);
- }, static::$tagUsing)));
+ $entry->type($type)->tags(Arr::collapse(array_map(function ($tagCallback) use ($entry) {
+ return $tagCallback($entry);
+ }, static::$tagUsing)));
- static::withoutRecording(function () use ($entry) {
- if (Collection::make(static::$filterUsing)->every->__invoke($entry)) {
- CoroutineContext::override(static::ENTRIES_QUEUE_CONTEXT_KEY, function ($entries) use ($entry) {
- return array_merge($entries ?? [], [$entry]);
- });
- }
-
- if (static::$afterRecordingHook) {
- call_user_func(static::$afterRecordingHook, new static, $entry);
- }
- });
+ static::withoutRecording(function () use ($entry) {
+ if (Collection::make(static::$filterUsing)->every->__invoke($entry)) {
+ CoroutineContext::override(static::ENTRIES_QUEUE_CONTEXT_KEY, function ($entries) use ($entry) {
+ return array_merge($entries ?? [], [$entry]);
+ });
+ }
- CoroutineContext::set(static::IS_RECORDING_CONTEXT_KEY, false);
+ if (static::$afterRecordingHook) {
+ call_user_func(static::$afterRecordingHook, new static, $entry);
+ }
+ });
+ } finally {
+ CoroutineContext::set(static::IS_RECORDING_CONTEXT_KEY, false);
+ }
}
/**
@@ -804,9 +786,11 @@ public static function css(): HtmlString
throw new RuntimeException('Unable to load the ' . (static::$useDarkTheme ? 'dark' : 'light') . ' Telescope dashboard styles.');
}
+ $nonceAttribute = static::cspNonceAttribute();
+
return new HtmlString(<<{$app}
-
+
+
HTML);
}
@@ -820,9 +804,10 @@ public static function js(): HtmlString
}
$telescope = Js::from(static::scriptVariables());
+ $nonceAttribute = static::cspNonceAttribute();
return new HtmlString(<<
+
@@ -841,6 +826,26 @@ public static function scriptVariables(): array
];
}
+ /**
+ * Set the CSP nonce to use for style and script tags.
+ */
+ public static function cspNonce(string $nonce): static
+ {
+ CoroutineContext::set(static::CSP_NONCE_CONTEXT_KEY, $nonce);
+
+ return new static;
+ }
+
+ /**
+ * Get the current CSP nonce attribute.
+ */
+ protected static function cspNonceAttribute(): string
+ {
+ $nonce = CoroutineContext::get(static::CSP_NONCE_CONTEXT_KEY);
+
+ return $nonce === null ? '' : " nonce=\"{$nonce}\"";
+ }
+
/**
* Flush all static state.
*/
diff --git a/tests/Telescope/Http/CspTest.php b/tests/Telescope/Http/CspTest.php
new file mode 100644
index 000000000..e37c73b14
--- /dev/null
+++ b/tests/Telescope/Http/CspTest.php
@@ -0,0 +1,56 @@
+assertStringNotContainsString(' nonce=', (string) Telescope::css());
+ $this->assertStringNotContainsString(' nonce=', (string) Telescope::js());
+ }
+
+ public function testCspNonceIsAppliedToEveryDashboardAssetTag(): void
+ {
+ $this->assertInstanceOf(Telescope::class, Telescope::cspNonce('dashboard-nonce'));
+
+ $css = (string) Telescope::css();
+ $js = (string) Telescope::js();
+
+ $this->assertSame(2, substr_count($css, '