Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
# Cache packages
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
Expand Down
139 changes: 139 additions & 0 deletions src/EventPayloadBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ public function create(array $data): EventPayload
$stacktrace = debug_backtrace();
}

/**
* Normalize frames to BacktraceFrame shape and wrap extra fields in additionalData.
* Also sanitize keys for MongoDB compatibility.
*/
$stacktrace = $this->normalizeBacktrace($stacktrace);

if (isset($data['type'])) {
$eventPayload->setType($data['type']);
}
Expand Down Expand Up @@ -107,4 +113,137 @@ private function resolveAddons(): array

return $result;
}

/**
* Normalize any stacktrace representation to BacktraceFrame shape
* and wrap unknown fields into additionalData with safe keys
*
* @param array $stack
*
* @return array
*/
private function normalizeBacktrace(array $stack): array
{
$normalized = [];

foreach ($stack as $frame) {
if (!is_array($frame)) {
continue;
}

$file = isset($frame['file']) ? (string) $frame['file'] : '';
$line = isset($frame['line']) ? (int) $frame['line'] : 0;
$functionName = null;

if (isset($frame['function'])) {
if (!empty($frame['class']) && !empty($frame['type'])) {
$functionName = (string) $frame['class'] . (string) $frame['type'] . (string) $frame['function'];
} else {
$functionName = (string) $frame['function'];
}
} elseif (isset($frame['functionName'])) {
$functionName = (string) $frame['functionName'];
}

$allowedKeys = ['file', 'line', 'column', 'sourceCode', 'function', 'arguments', 'additionalData'];
Comment thread
pavelzotikov marked this conversation as resolved.
Outdated

$additional = [];
foreach ($frame as $key => $value) {
if (!in_array($key, $allowedKeys, true)) {
// Drop heavy/unserializable objects from 'object' field; store class name instead
if ($key === 'object') {
$value = is_object($value) ? get_class($value) : $value;
}

$additional[$key] = $this->transformForJson($value);
}
}

$normalized[] = $this->sanitizeArrayKeys([
'file' => $file,
'line' => $line,
'column' => null,
'sourceCode' => isset($frame['sourceCode']) && is_array($frame['sourceCode']) ? $frame['sourceCode'] : null,
'function' => $functionName,
// Keep arguments only if it already looks like desired string[]; otherwise omit
'arguments' => (isset($frame['arguments']) && is_array($frame['arguments'])) ? array_values(array_map('strval', $frame['arguments'])) : [],
Comment thread
pavelzotikov marked this conversation as resolved.
Outdated
'additionalData'=> $additional,
]);
}

return $normalized;
}

/**
* Recursively sanitize array keys to be MongoDB-safe
* - replace dots with underscores
* - replace leading '$' with 'dollar_'
*
* @param mixed $value
*
* @return mixed
*/
private function sanitizeArrayKeys($value)
{
if (!is_array($value)) {
return $value;
}

$sanitized = [];

foreach ($value as $key => $subValue) {
$newKey = $key;

if (is_string($newKey)) {
if (strpos($newKey, '.') !== false) {
$newKey = str_replace('.', '_', $newKey);
}

if (isset($newKey[0]) && $newKey[0] === '$') {
$newKey = 'dollar_' . substr($newKey, 1);
}
}

$sanitized[$newKey] = $this->sanitizeArrayKeys($subValue);
}

return $sanitized;
}

/**
* Transform values to JSON-serializable representation
*
* @param mixed $value
*
* @return mixed
*/
private function transformForJson($value)
{
if (is_array($value)) {
$result = [];
foreach ($value as $k => $v) {
$result[$k] = $this->transformForJson($v);
}

return $result;
}

if (is_null($value)) {
return 'null';
Comment thread
pavelzotikov marked this conversation as resolved.
Outdated
}

if (is_callable($value)) {
return 'Closure';
}

if (is_object($value)) {
return get_class($value);
}

if (is_resource($value)) {
return 'Resource';
}

return $value;
}
}
Loading