@@ -27,6 +27,19 @@ class EventPayloadBuilder
2727 */
2828 private $ stacktraceFrameBuilder ;
2929
30+ /**
31+ * Allowed keys for stacktrace frames
32+ */
33+ private const ALLOWED_KEYS = [
34+ 'file ' ,
35+ 'line ' ,
36+ 'column ' ,
37+ 'sourceCode ' ,
38+ 'function ' ,
39+ 'arguments ' ,
40+ 'additionalData ' ,
41+ ];
42+
3043 /**
3144 * EventPayloadFactory constructor.
3245 */
@@ -76,6 +89,12 @@ public function create(array $data): EventPayload
7689 $ stacktrace = debug_backtrace ();
7790 }
7891
92+ /**
93+ * Normalize frames to BacktraceFrame shape and wrap extra fields in additionalData.
94+ * Also sanitize keys for MongoDB compatibility.
95+ */
96+ $ stacktrace = $ this ->normalizeBacktrace ($ stacktrace );
97+
7998 if (isset ($ data ['type ' ])) {
8099 $ eventPayload ->setType ($ data ['type ' ]);
81100 }
@@ -107,4 +126,136 @@ private function resolveAddons(): array
107126
108127 return $ result ;
109128 }
129+
130+ /**
131+ * Normalize any stacktrace representation to BacktraceFrame shape
132+ * and wrap unknown fields into additionalData with safe keys
133+ *
134+ * @param array $stack
135+ *
136+ * @return array
137+ */
138+ private function normalizeBacktrace (array $ stack ): array
139+ {
140+ $ normalized = [];
141+
142+ foreach ($ stack as $ frame ) {
143+ if (!is_array ($ frame )) {
144+ continue ;
145+ }
146+
147+ $ file = isset ($ frame ['file ' ]) ? (string ) $ frame ['file ' ] : '' ;
148+ $ line = isset ($ frame ['line ' ]) ? (int ) $ frame ['line ' ] : 0 ;
149+ $ functionName = null ;
150+
151+ if (isset ($ frame ['function ' ])) {
152+ if (!empty ($ frame ['class ' ]) && !empty ($ frame ['type ' ])) {
153+ $ functionName = (string ) $ frame ['class ' ] . (string ) $ frame ['type ' ] . (string ) $ frame ['function ' ];
154+ } else {
155+ $ functionName = (string ) $ frame ['function ' ];
156+ }
157+ } elseif (isset ($ frame ['functionName ' ])) {
158+ $ functionName = (string ) $ frame ['functionName ' ];
159+ }
160+
161+ $ additional = [];
162+ foreach ($ frame as $ key => $ value ) {
163+ if (!in_array ($ key , self ::ALLOWED_KEYS , true )) {
164+ // Drop heavy/unserializable objects from 'object' field; store class name instead
165+ if ($ key === 'object ' ) {
166+ $ value = is_object ($ value ) ? get_class ($ value ) : $ value ;
167+ }
168+
169+ $ additional [$ key ] = $ this ->transformForJson ($ value );
170+ }
171+ }
172+
173+ $ normalized [] = $ this ->sanitizeArrayKeys ([
174+ 'file ' => $ file ,
175+ 'line ' => $ line ,
176+ 'column ' => null ,
177+ 'sourceCode ' => isset ($ frame ['sourceCode ' ]) && is_array ($ frame ['sourceCode ' ]) ? $ frame ['sourceCode ' ] : null ,
178+ 'function ' => $ functionName ,
179+ // Keep arguments only if it already looks like desired string[]; otherwise omit
180+ // Limit argument processing to first 10 items to avoid performance issues
181+ 'arguments ' => (isset ($ frame ['arguments ' ]) && is_array ($ frame ['arguments ' ])) ? array_values (array_map ('strval ' , array_slice ($ frame ['arguments ' ], 0 , 10 ))) : [],
182+ 'additionalData ' => $ additional ,
183+ ]);
184+ }
185+
186+ return $ normalized ;
187+ }
188+
189+ /**
190+ * Recursively sanitize array keys to be MongoDB-safe
191+ * - replace dots with underscores
192+ * - replace leading '$' with 'dollar_'
193+ *
194+ * @param mixed $value
195+ *
196+ * @return mixed
197+ */
198+ private function sanitizeArrayKeys ($ value )
199+ {
200+ if (!is_array ($ value )) {
201+ return $ value ;
202+ }
203+
204+ $ sanitized = [];
205+
206+ foreach ($ value as $ key => $ subValue ) {
207+ $ newKey = $ key ;
208+
209+ if (is_string ($ newKey )) {
210+ if (strpos ($ newKey , '. ' ) !== false ) {
211+ $ newKey = str_replace ('. ' , '_ ' , $ newKey );
212+ }
213+
214+ if (isset ($ newKey [0 ]) && $ newKey [0 ] === '$ ' ) {
215+ $ newKey = 'dollar_ ' . substr ($ newKey , 1 );
216+ }
217+ }
218+
219+ $ sanitized [$ newKey ] = $ this ->sanitizeArrayKeys ($ subValue );
220+ }
221+
222+ return $ sanitized ;
223+ }
224+
225+ /**
226+ * Transform values to JSON-serializable representation
227+ *
228+ * @param mixed $value
229+ *
230+ * @return mixed
231+ */
232+ private function transformForJson ($ value )
233+ {
234+ if (is_array ($ value )) {
235+ $ result = [];
236+ foreach ($ value as $ k => $ v ) {
237+ $ result [$ k ] = $ this ->transformForJson ($ v );
238+ }
239+
240+ return $ result ;
241+ }
242+
243+ if (is_null ($ value )) {
244+ return null ;
245+ }
246+
247+ if (is_callable ($ value )) {
248+ return 'Closure ' ;
249+ }
250+
251+ if (is_object ($ value )) {
252+ return get_class ($ value );
253+ }
254+
255+ if (is_resource ($ value )) {
256+ return 'Resource ' ;
257+ }
258+
259+ return $ value ;
260+ }
110261}
0 commit comments