Skip to content

Commit 4164f68

Browse files
Naorayclaude
andcommitted
fix(tracing): truncate serialized job payload data
Closes #34 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7b4f091 commit 4164f68

File tree

2 files changed

+146
-1
lines changed

2 files changed

+146
-1
lines changed

src/Tracing/JobContextCollector.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ class JobContextCollector implements EventDrivenCollectorInterface
1313
use RedactsData;
1414
use ResolvesTracingConfig;
1515

16+
/**
17+
* Maximum length for serialized string values in the payload before truncation.
18+
*/
19+
protected const MAX_SERIALIZED_LENGTH = 500;
20+
1621
public function isEnabled(): bool
1722
{
1823
return $this->isTracingFeatureEnabled('jobs');
@@ -28,7 +33,47 @@ public function __invoke(JobExceptionOccurred $event): void
2833
'queue' => $job->getQueue(),
2934
'connection' => $job->getConnectionName(),
3035
'attempts' => $job->attempts(),
31-
'payload' => $this->redactPayload($job->payload()),
36+
'payload' => $this->cleanPayload($job->payload()),
3237
]);
3338
}
39+
40+
/**
41+
* Clean the job payload by redacting sensitive data and truncating serialized values.
42+
*
43+
* @param array<string, mixed> $payload
44+
* @return array<string, mixed>
45+
*/
46+
protected function cleanPayload(array $payload): array
47+
{
48+
$payload = $this->redactPayload($payload);
49+
50+
return $this->truncateSerializedValues($payload);
51+
}
52+
53+
/**
54+
* Recursively truncate long serialized string values in the payload.
55+
*
56+
* Serialized PHP objects (e.g. payload.data.command) can contain hundreds
57+
* of lines of unreadable data. This method truncates them to keep GitHub
58+
* issues clean and readable.
59+
*
60+
* @param array<string, mixed> $data
61+
* @return array<string, mixed>
62+
*/
63+
protected function truncateSerializedValues(array $data): array
64+
{
65+
$result = [];
66+
67+
foreach ($data as $key => $value) {
68+
if (is_array($value)) {
69+
$result[$key] = $this->truncateSerializedValues($value);
70+
} elseif (is_string($value) && strlen($value) > self::MAX_SERIALIZED_LENGTH) {
71+
$result[$key] = substr($value, 0, self::MAX_SERIALIZED_LENGTH).'... [truncated]';
72+
} else {
73+
$result[$key] = $value;
74+
}
75+
}
76+
77+
return $result;
78+
}
3479
}

tests/Tracing/JobContextCollectorTest.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,103 @@
3535
expect($jobContext['connection'])->toBe('redis');
3636
expect($jobContext['attempts'])->toBe(2);
3737
});
38+
39+
it('truncates long serialized command strings in payload', function () {
40+
$longSerializedCommand = str_repeat('O:32:"App\\Jobs\\TestJob":3:{s:6:"tries";', 50);
41+
42+
$job = Mockery::mock('Illuminate\Contracts\Queue\Job');
43+
$job->shouldReceive('getName')->andReturn('App\Jobs\TestJob');
44+
$job->shouldReceive('getQueue')->andReturn('default');
45+
$job->shouldReceive('getConnectionName')->andReturn('redis');
46+
$job->shouldReceive('attempts')->andReturn(1);
47+
$job->shouldReceive('payload')->andReturn([
48+
'displayName' => 'App\Jobs\TestJob',
49+
'uuid' => 'test-uuid-1234',
50+
'job' => 'Illuminate\Queue\CallQueuedHandler@call',
51+
'maxTries' => 3,
52+
'timeout' => 120,
53+
'data' => [
54+
'commandName' => 'App\Jobs\TestJob',
55+
'command' => $longSerializedCommand,
56+
],
57+
]);
58+
59+
$event = new JobExceptionOccurred('redis', $job, new \RuntimeException('Test exception'));
60+
61+
($this->collector)($event);
62+
63+
$jobContext = Context::get('job');
64+
$payload = $jobContext['payload'];
65+
66+
// The command field should be truncated
67+
expect(strlen($payload['data']['command']))->toBeLessThan(strlen($longSerializedCommand));
68+
expect($payload['data']['command'])->toEndWith('... [truncated]');
69+
// Truncated to 500 chars + the "... [truncated]" marker
70+
expect(strlen($payload['data']['command']))->toBe(500 + strlen('... [truncated]'));
71+
72+
// Other payload fields should be preserved as-is
73+
expect($payload['displayName'])->toBe('App\Jobs\TestJob');
74+
expect($payload['uuid'])->toBe('test-uuid-1234');
75+
expect($payload['job'])->toBe('Illuminate\Queue\CallQueuedHandler@call');
76+
expect($payload['maxTries'])->toBe(3);
77+
expect($payload['timeout'])->toBe(120);
78+
expect($payload['data']['commandName'])->toBe('App\Jobs\TestJob');
79+
});
80+
81+
it('does not truncate short string values in payload', function () {
82+
$job = Mockery::mock('Illuminate\Contracts\Queue\Job');
83+
$job->shouldReceive('getName')->andReturn('App\Jobs\TestJob');
84+
$job->shouldReceive('getQueue')->andReturn('default');
85+
$job->shouldReceive('getConnectionName')->andReturn('redis');
86+
$job->shouldReceive('attempts')->andReturn(1);
87+
$job->shouldReceive('payload')->andReturn([
88+
'displayName' => 'App\Jobs\TestJob',
89+
'uuid' => 'test-uuid-1234',
90+
'data' => [
91+
'commandName' => 'App\Jobs\TestJob',
92+
'command' => 'short-command-value',
93+
],
94+
]);
95+
96+
$event = new JobExceptionOccurred('redis', $job, new \RuntimeException('Test exception'));
97+
98+
($this->collector)($event);
99+
100+
$jobContext = Context::get('job');
101+
$payload = $jobContext['payload'];
102+
103+
// Short values should not be truncated
104+
expect($payload['data']['command'])->toBe('short-command-value');
105+
expect($payload['displayName'])->toBe('App\Jobs\TestJob');
106+
});
107+
108+
it('preserves non-string values in payload during truncation', function () {
109+
$job = Mockery::mock('Illuminate\Contracts\Queue\Job');
110+
$job->shouldReceive('getName')->andReturn('App\Jobs\TestJob');
111+
$job->shouldReceive('getQueue')->andReturn('default');
112+
$job->shouldReceive('getConnectionName')->andReturn('redis');
113+
$job->shouldReceive('attempts')->andReturn(1);
114+
$job->shouldReceive('payload')->andReturn([
115+
'displayName' => 'App\Jobs\TestJob',
116+
'maxTries' => 3,
117+
'timeout' => null,
118+
'attempts' => 1,
119+
'tags' => ['tag1', 'tag2'],
120+
'data' => [
121+
'commandName' => 'App\Jobs\TestJob',
122+
],
123+
]);
124+
125+
$event = new JobExceptionOccurred('redis', $job, new \RuntimeException('Test exception'));
126+
127+
($this->collector)($event);
128+
129+
$jobContext = Context::get('job');
130+
$payload = $jobContext['payload'];
131+
132+
// Integer, null, and array values should be preserved
133+
expect($payload['maxTries'])->toBe(3);
134+
expect($payload['timeout'])->toBeNull();
135+
expect($payload['attempts'])->toBe(1);
136+
expect($payload['tags'])->toBe(['tag1', 'tag2']);
137+
});

0 commit comments

Comments
 (0)