diff --git a/agent/php/ElasticApm/Impl/Util/StackTraceUtil.php b/agent/php/ElasticApm/Impl/Util/StackTraceUtil.php index 815b33676..1d6ee639b 100644 --- a/agent/php/ElasticApm/Impl/Util/StackTraceUtil.php +++ b/agent/php/ElasticApm/Impl/Util/StackTraceUtil.php @@ -82,40 +82,23 @@ public function captureInApmFormat(int $offset, ?int $maxNumberOfFrames): array } /** - * @param iterable> $inputFrames + * @param iterable> $phpFormatFrames * @param ?positive-int $maxNumberOfFrames * * @return StackTraceFrame[] */ - public function convertPhpToApmFormat(iterable $inputFrames, ?int $maxNumberOfFrames): array + public function convertPhpToApmFormat(iterable $phpFormatFrames, ?int $maxNumberOfFrames): array { - /** @var StackTraceFrame[] $outputFrames */ - $outputFrames = []; - $this->excludeCodeToHide( - $inputFrames, - /** - * @param array $inputFrameWithLocationData - * @param ?array $inputFrameWithNonLocationData - */ - function (array $inputFrameWithLocationData, ?array $inputFrameWithNonLocationData) use ($maxNumberOfFrames, &$outputFrames): bool { - $outputFrameFunc = null; - if ($inputFrameWithNonLocationData !== null) { - $outputFrameFunc = StackTraceUtil::buildApmFormatFunctionForClassMethod( - $this->getNullableStringValue(StackTraceUtil::CLASS_KEY, $inputFrameWithNonLocationData), - $this->isStaticMethodInPhpFormat($inputFrameWithNonLocationData), - $this->getNullableStringValue(StackTraceUtil::FUNCTION_KEY, $inputFrameWithNonLocationData) - ); - } - - $file = $this->getNullableStringValue(StackTraceUtil::FILE_KEY, $inputFrameWithLocationData); - $line = $this->getNullableIntValue(StackTraceUtil::LINE_KEY, $inputFrameWithLocationData); - $outputFrame = new StackTraceFrame($file ?? StackTraceUtil::FILE_NAME_NOT_AVAILABLE_SUBSTITUTE, $line ?? StackTraceUtil::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE); - $outputFrame->function = $outputFrameFunc; - return self::addToOutputFrames($outputFrame, $maxNumberOfFrames, /* ref */ $outputFrames); - } + $allClassicFormatFrames = $this->convertPhpToClassicFormat( + null /* <- prevPhpFormatFrame */, + $phpFormatFrames, + $maxNumberOfFrames, + false /* keepElasticApmFrames */, + false /* $includeArgs */, + false /* $includeThisObj */ ); - return $outputFrames; + return self::convertClassicToApmFormat($allClassicFormatFrames, $maxNumberOfFrames); } /** @@ -145,213 +128,152 @@ public function captureInClassicFormatExcludeElasticApm(int $offset = 0, ?int $m public function captureInClassicFormat(int $offset = 0, ?int $maxNumberOfFrames = null, bool $keepElasticApmFrames = true, bool $includeArgs = false, bool $includeThisObj = false): array { $options = ($includeArgs ? 0 : DEBUG_BACKTRACE_IGNORE_ARGS) | ($includeThisObj ? DEBUG_BACKTRACE_PROVIDE_OBJECT : 0); - // If there is non-null $maxNumberOfFrames we need to capture one more frame in PHP format - $phpFormatFrames = debug_backtrace($options, /* limit */ $maxNumberOfFrames === null ? 0 : ($offset + $maxNumberOfFrames + 1)); - $phpFormatFrames = IterableUtil::arraySuffix($phpFormatFrames, $offset); - - /** @var ClassicFormatStackTraceFrame[] $outputFrames */ - $outputFrames = []; - $isTopFrame = true; - /** @var ?array $bufferedBeforeTopFrame */ - $bufferedBeforeTopFrame = null; - /** @var ?array $prevFrame */ - $prevFrame = null; - $hasExitedLoopEarly = false; - if ($keepElasticApmFrames) { - foreach ($phpFormatFrames as $currentFrame) { - if ($prevFrame === null) { - $prevFrame = $currentFrame; - continue; - } - if (!$this->captureInClassicFormatConsume($maxNumberOfFrames, $includeArgs, $includeThisObj, $prevFrame, $currentFrame, $bufferedBeforeTopFrame, $isTopFrame, $outputFrames)) { - $hasExitedLoopEarly = true; - break; - } - $prevFrame = $currentFrame; - } - } else { - $this->excludeCodeToHide( - $phpFormatFrames, - /** - * @param array $inputFrameWithLocationData - * @param ?array $inputFrameWithNonLocationData - * - * @return bool - */ - function ( - array $inputFrameWithLocationData, - ?array $inputFrameWithNonLocationData - ) use ( - $maxNumberOfFrames, - $includeArgs, - $includeThisObj, - &$prevFrame, - &$hasExitedLoopEarly, - &$bufferedBeforeTopFrame, - &$isTopFrame, - &$outputFrames - ): bool { - $currentFrame = $this->mergePhpFrames($inputFrameWithLocationData, $inputFrameWithNonLocationData, $includeArgs, $includeThisObj); - if ($prevFrame === null) { - $prevFrame = $currentFrame; - return true; - } - if (!$this->captureInClassicFormatConsume($maxNumberOfFrames, $includeArgs, $includeThisObj, $prevFrame, $currentFrame, $bufferedBeforeTopFrame, $isTopFrame, $outputFrames)) { - $hasExitedLoopEarly = true; - return false; - } - $prevFrame = $currentFrame; - return true; - } - ); - } - - if (!$hasExitedLoopEarly && $prevFrame !== null) { - $this->captureInClassicFormatConsume($maxNumberOfFrames, $includeArgs, $includeThisObj, $prevFrame, /* nextInputFrame */ null, $bufferedBeforeTopFrame, $isTopFrame, $outputFrames); - } - - return $outputFrames; + return $this->convertCaptureToClassicFormat( + // If there is non-null $maxNumberOfFrames we need to capture one more frame in PHP format + debug_backtrace($options, /* limit */ $maxNumberOfFrames === null ? 0 : ($offset + $maxNumberOfFrames + 1)), + // $offset + 1 to exclude the frame for the current method (captureInClassicFormat) call + $offset + 1, + $maxNumberOfFrames, + $keepElasticApmFrames, + $includeArgs, + $includeThisObj + ); } - public static function buildApmFormatFunctionForClassMethod(?string $classicName, ?bool $isStaticMethod, ?string $methodName): ?string + /** + * @param array> $phpFormatFrames + * @param int $offset + * @param ?positive-int $maxNumberOfFrames + * @param bool $keepElasticApmFrames + * @param bool $includeArgs + * @param bool $includeThisObj + * + * @return ClassicFormatStackTraceFrame[] + * + * @phpstan-param 0|positive-int $offset + */ + public function convertCaptureToClassicFormat(array $phpFormatFrames, int $offset, ?int $maxNumberOfFrames, bool $keepElasticApmFrames, bool $includeArgs, bool $includeThisObj): array { - if ($methodName === null) { - return null; - } - - if ($classicName === null) { - return $methodName; + if ($offset >= count($phpFormatFrames)) { + return []; } - $classMethodSep = ($isStaticMethod === null) ? '.' : ($isStaticMethod ? StackTraceUtil::FUNCTION_IS_STATIC_METHOD_TYPE_VALUE : StackTraceUtil::FUNCTION_IS_METHOD_TYPE_VALUE); - return $classicName . $classMethodSep . $methodName; + return $this->convertPhpToClassicFormat( + $offset === 0 ? null : $phpFormatFrames[$offset - 1] /* <- prevPhpFormatFrame */, + $offset === 0 ? $phpFormatFrames : IterableUtil::arraySuffix($phpFormatFrames, $offset), + $maxNumberOfFrames, + $keepElasticApmFrames, + $includeArgs, + $includeThisObj + ); } /** - * @param array $inputFrame + * @param ?array $prevPhpFormatFrame + * @param iterable> $phpFormatFrames + * @param ?positive-int $maxNumberOfFrames + * @param bool $keepElasticApmFrames + * @param bool $includeArgs + * @param bool $includeThisObj * - * @return bool + * @return ClassicFormatStackTraceFrame[] */ - private function isTrampolineCall(array $inputFrame): bool - { - $func = $this->getNullableStringValue(StackTraceUtil::FUNCTION_KEY, $inputFrame); - if ($func !== 'call_user_func' && $func !== 'call_user_func_array') { - return false; - } - - $class = $this->getNullableStringValue(StackTraceUtil::CLASS_KEY, $inputFrame); - if ($class !== null) { - return false; + public function convertPhpToClassicFormat( + ?array $prevPhpFormatFrame, + iterable $phpFormatFrames, + ?int $maxNumberOfFrames, + bool $keepElasticApmFrames, + bool $includeArgs, + bool $includeThisObj + ): array { + $allClassicFormatFrames = []; + $prevInFrame = $prevPhpFormatFrame; + foreach ($phpFormatFrames as $currentInFrame) { + $outFrame = new ClassicFormatStackTraceFrame(); + $isOutFrameEmpty = true; + if ($prevInFrame !== null && $this->hasLocationPropertiesInPhpFormat($prevInFrame)) { + $this->copyLocationPropertiesFromPhpToClassicFormat($prevInFrame, $outFrame); + $isOutFrameEmpty = false; + } + if ($this->hasNonLocationPropertiesInPhpFormat($currentInFrame)) { + $this->copyNonLocationPropertiesFromPhpToClassicFormat($currentInFrame, $includeArgs, $includeThisObj, $outFrame); + $isOutFrameEmpty = false; + } + if (!$isOutFrameEmpty) { + $allClassicFormatFrames[] = $outFrame; + } + $prevInFrame = $currentInFrame; } - $funcType = $this->getNullableStringValue(StackTraceUtil::TYPE_KEY, $inputFrame); - if ($funcType !== null) { - return false; + if ($prevInFrame !== null && $this->hasLocationPropertiesInPhpFormat($prevInFrame)) { + $outFrame = new ClassicFormatStackTraceFrame(); + $this->copyLocationPropertiesFromPhpToClassicFormat($prevInFrame, $outFrame); + $allClassicFormatFrames[] = $outFrame; } - return true; + return $keepElasticApmFrames + ? ($maxNumberOfFrames === null ? $allClassicFormatFrames : array_slice($allClassicFormatFrames, /* offset */ 0, $maxNumberOfFrames)) + : $this->excludeCodeToHide($allClassicFormatFrames, $maxNumberOfFrames); } + /** - * @param array $inputFrame + * @param ClassicFormatStackTraceFrame[] $inFrames + * @param ?positive-int $maxNumberOfFrames * - * @return bool + * @return ClassicFormatStackTraceFrame[] */ - private function isCallToCodeToHide(array $inputFrame): bool + private function excludeCodeToHide(array $inFrames, ?int $maxNumberOfFrames): array { - $class = $this->getNullableStringValue(StackTraceUtil::CLASS_KEY, $inputFrame); - if ($class !== null && TextUtil::isPrefixOf($this->namePrefixForFramesToHide, $class)) { - return true; - } + $outFrames = []; + /** @var ?int $bufferedFromIndex */ + $bufferedFromIndex = null; + foreach (RangeUtil::generateUpTo(count($inFrames)) as $currentInFrameIndex) { + $currentInFrame = $inFrames[$currentInFrameIndex]; + if (self::isTrampolineCall($currentInFrame)) { + if ($bufferedFromIndex === null) { + $bufferedFromIndex = $currentInFrameIndex; + } + continue; + } - $func = $this->getNullableStringValue(StackTraceUtil::FUNCTION_KEY, $inputFrame); - if ($func !== null && TextUtil::isPrefixOf($this->namePrefixForFramesToHide, $func)) { - return true; + if ($this->isCallToCodeToHide($currentInFrame)) { + $bufferedFromIndex = null; + continue; + } + + for ($index = $bufferedFromIndex ?? $currentInFrameIndex; $index <= $currentInFrameIndex; ++$index) { + self::addToOutputFrames($inFrames[$index], $maxNumberOfFrames, /* ref */ $outFrames); + } + $bufferedFromIndex = null; } - return false; + return $outFrames; } - /** - * @param array $inputFrameWithLocationData - * @param ?array $higherInputFrameWithNonLocationData - * @param callable(array, ?array): bool $consumeCallback - * - * @return bool - */ - private function excludeCodeToHideProcessBufferedFrame(array $inputFrameWithLocationData, ?array $higherInputFrameWithNonLocationData, callable $consumeCallback): bool + public static function buildApmFormatFunctionForClassMethod(?string $classicName, ?bool $isStaticMethod, ?string $methodName): ?string { - $func = null; - $frameWithNonLocationData = $this->isCallToCodeToHide($inputFrameWithLocationData) ? $higherInputFrameWithNonLocationData : $inputFrameWithLocationData; - if ($frameWithNonLocationData !== null) { - $func = $this->getNullableStringValue(StackTraceUtil::FUNCTION_KEY, $frameWithNonLocationData); + if ($methodName === null) { + return null; } - if ($this->getNullableStringValue(StackTraceUtil::FILE_KEY, $inputFrameWithLocationData) == null && $func === null) { - return true; + if ($classicName === null) { + return $methodName; } - return $consumeCallback($inputFrameWithLocationData, $frameWithNonLocationData); + $classMethodSep = ($isStaticMethod === null) ? '.' : ($isStaticMethod ? StackTraceUtil::FUNCTION_IS_STATIC_METHOD_TYPE_VALUE : StackTraceUtil::FUNCTION_IS_METHOD_TYPE_VALUE); + return $classicName . $classMethodSep . $methodName; } - /** - * @param array[] $bufferedInFrames - * @param ?array $higherInputFrameWithNonLocationData - * @param callable(array, ?array): bool $consumeCallback - * - * @return bool - */ - private function excludeCodeToHideProcessBufferedFrames(array $bufferedInFrames, ?array $higherInputFrameWithNonLocationData, callable $consumeCallback): bool + private static function isTrampolineCall(ClassicFormatStackTraceFrame $frame): bool { - if (!$this->excludeCodeToHideProcessBufferedFrame($bufferedInFrames[0], $higherInputFrameWithNonLocationData, $consumeCallback)) { - return false; - } - foreach (RangeUtil::generateFromToIncluding(1, count($bufferedInFrames) - 1) as $bufferedInFramesIndex) { - if (!$this->excludeCodeToHideProcessBufferedFrame($bufferedInFrames[$bufferedInFramesIndex], /* higherInputFrameWithNonLocationData */ null, $consumeCallback)) { - return false; - } - } - return true; + return $frame->class === null && $frame->isStaticMethod === null && ($frame->function === 'call_user_func' || $frame->function === 'call_user_func_array'); } - /** - * @param iterable> $inputFrames - * @param callable(array, ?array): bool $consumeCallback - */ - private function excludeCodeToHide(iterable $inputFrames, callable $consumeCallback): void + private function isCallToCodeToHide(ClassicFormatStackTraceFrame $frame): bool { - /** @var array[] $bufferedInFrames */ - $bufferedInFrames = []; - /** @var ?array $higherInFrameWithNonLocationData */ - $higherInFrameWithNonLocationData = null; - foreach ($inputFrames as $currentInFrame) { - if (ArrayUtil::isEmpty($bufferedInFrames)) { - $bufferedInFrames[] = $currentInFrame; - continue; - } - - if ($this->isTrampolineCall($currentInFrame)) { - $bufferedInFrames[] = $currentInFrame; - continue; - } - - if ($this->isCallToCodeToHide($currentInFrame)) { - if (!$this->isCallToCodeToHide($bufferedInFrames[0])) { - $higherInFrameWithNonLocationData = $bufferedInFrames[0]; - } - } else { - if (!$this->excludeCodeToHideProcessBufferedFrames($bufferedInFrames, $higherInFrameWithNonLocationData, $consumeCallback)) { - return; - } - $higherInFrameWithNonLocationData = null; - } - - $bufferedInFrames = [$currentInFrame]; - } - - if (!ArrayUtil::isEmpty($bufferedInFrames)) { - $this->excludeCodeToHideProcessBufferedFrames($bufferedInFrames, $higherInFrameWithNonLocationData, $consumeCallback); - } + return ($frame->class !== null && TextUtil::isPrefixOf($this->namePrefixForFramesToHide, $frame->class)) + || ($frame->function !== null && TextUtil::isPrefixOf($this->namePrefixForFramesToHide, $frame->function)); } /** @@ -474,6 +396,19 @@ private function hasNonLocationPropertiesInPhpFormat(array $frame): bool return $this->getNullableStringValue(StackTraceUtil::FUNCTION_KEY, $frame) !== null; } + /** + * @param array $frame + */ + private function hasLocationPropertiesInPhpFormat(array $frame): bool + { + return $this->getNullableStringValue(StackTraceUtil::FILE_KEY, $frame) !== null; + } + + private static function hasLocationPropertiesInClassicFormat(ClassicFormatStackTraceFrame $frame): bool + { + return $frame->file !== null; + } + /** * @param array $srcFrame * @param ClassicFormatStackTraceFrame $dstFrame @@ -503,89 +438,6 @@ private function copyNonLocationPropertiesFromPhpToClassicFormat(array $srcFrame } } - /** - * @param array $frameWithLocationData - * @param ?array $frameWithNonLocationData - * - * @return array - */ - private function mergePhpFrames(array $frameWithLocationData, ?array $frameWithNonLocationData, bool $includeArgs, bool $includeThisObj): array - { - $result = []; - foreach ([StackTraceUtil::FILE_KEY, StackTraceUtil::LINE_KEY] as $key) { - if (array_key_exists($key, $frameWithLocationData)) { - $result[$key] = $frameWithLocationData[$key]; - } - } - - if ($frameWithNonLocationData !== null) { - $keys = [StackTraceUtil::CLASS_KEY, StackTraceUtil::FUNCTION_KEY, StackTraceUtil::TYPE_KEY]; - if ($includeThisObj) { - $keys[] = StackTraceUtil::THIS_OBJECT_KEY; - } - if ($includeArgs) { - $keys[] = StackTraceUtil::ARGS_KEY; - } - foreach ($keys as $key) { - if (array_key_exists($key, $frameWithNonLocationData)) { - $result[$key] = $frameWithNonLocationData[$key]; - } else { - unset($frameWithNonLocationData[$key]); - } - } - } - return $result; - } - - /** - * @param ?int $maxNumberOfFrames - * @param bool $includeArgs - * @param bool $includeThisObj - * @param array $currentInputFrame - * @param ?array $nextInputFrame - * @param ?array &$bufferedBeforeTopFrame - * @param bool &$isTopFrame - * @param ClassicFormatStackTraceFrame[] &$outputFrames - * - * @return bool - * - * @phpstan-param null|positive-int $maxNumberOfFrames - */ - private function captureInClassicFormatConsume( - ?int $maxNumberOfFrames, - bool $includeArgs, - bool $includeThisObj, - array $currentInputFrame, - ?array $nextInputFrame, - ?array &$bufferedBeforeTopFrame, - bool &$isTopFrame, - array &$outputFrames - ): bool { - if ($isTopFrame) { - if ($bufferedBeforeTopFrame === null) { - $bufferedBeforeTopFrame = $currentInputFrame; - return true; - } - - $isTopFrame = false; - if ($this->hasNonLocationPropertiesInPhpFormat($currentInputFrame)) { - $outputFrame = new ClassicFormatStackTraceFrame(); - $this->copyNonLocationPropertiesFromPhpToClassicFormat($currentInputFrame, $includeArgs, $includeThisObj, $outputFrame); - $this->copyLocationPropertiesFromPhpToClassicFormat($bufferedBeforeTopFrame, $outputFrame); - if (!self::addToOutputFrames($outputFrame, $maxNumberOfFrames, /* ref */ $outputFrames)) { - return false; - } - } - } - - $outputFrame = new ClassicFormatStackTraceFrame(); - $this->copyLocationPropertiesFromPhpToClassicFormat($currentInputFrame, $outputFrame); - if ($nextInputFrame !== null) { - $this->copyNonLocationPropertiesFromPhpToClassicFormat($nextInputFrame, $includeArgs, $includeThisObj, $outputFrame); - } - return self::addToOutputFrames($outputFrame, $maxNumberOfFrames, /* ref */ $outputFrames); - } - /** * @template TOutputFrame * @@ -615,6 +467,41 @@ public function convertThrowableTraceToApmFormat(Throwable $throwable, ?int $max return $this->convertPhpToApmFormat(IterableUtil::prepend($frameForThrowLocation, $throwable->getTrace()), $maxNumberOfFrames); } + // TODO: Sergey Kleyman: REMOVE: + // /** + // * @param iterable $inFrames + // * @param ?positive-int $maxNumberOfFrames + // * + // * @return StackTraceFrame[] + // */ + // private static function convertClassicToApmFormat(iterable $inFrames, ?int $maxNumberOfFrames): array + // { + // /** @var StackTraceFrame[] $outFrames */ + // $outFrames = []; + // + // /** @var ?ClassicFormatStackTraceFrame $prevInFrame */ + // $prevInFrame = null; + // foreach ($inFrames as $currentInFrame) { + // if ($currentInFrame->file === null) { + // $isOutFrameEmpty = true; + // $outFrame = new StackTraceFrame(self::FILE_NAME_NOT_AVAILABLE_SUBSTITUTE, self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE); + // } else { + // $isOutFrameEmpty = false; + // $outFrame = new StackTraceFrame($currentInFrame->file, $currentInFrame->line ?? self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE); + // } + // if ($prevInFrame !== null && $prevInFrame->function !== null) { + // $isOutFrameEmpty = false; + // $outFrame->function = self::buildApmFormatFunctionForClassMethod($prevInFrame->class, $prevInFrame->isStaticMethod, $prevInFrame->function); + // } + // if (!$isOutFrameEmpty && !self::addToOutputFrames($outFrame, $maxNumberOfFrames, /* ref */ $outFrames)) { + // break; + // } + // $prevInFrame = $currentInFrame; + // } + // + // return $outFrames; + // } + /** * @param iterable $inputFrames * @param ?positive-int $maxNumberOfFrames @@ -629,8 +516,8 @@ public static function convertClassicToApmFormat(iterable $inputFrames, ?int $ma $exitedEarly = false; foreach ($inputFrames as $currentInputFrame) { if ($prevInputFrame === null) { - if ($currentInputFrame->file !== null) { - $outputFrame = new StackTraceFrame($currentInputFrame->file, $currentInputFrame->line ?? self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE); + if (self::hasLocationPropertiesInClassicFormat($currentInputFrame)) { + $outputFrame = new StackTraceFrame($currentInputFrame->file ?? self::FILE_NAME_NOT_AVAILABLE_SUBSTITUTE, $currentInputFrame->line ?? self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE); if (!self::addToOutputFrames($outputFrame, $maxNumberOfFrames, /* ref */ $outputFrames)) { $exitedEarly = true; break; diff --git a/tests/ElasticApmTests/UnitTests/UtilTests/StackTraceUtilTest.php b/tests/ElasticApmTests/UnitTests/UtilTests/StackTraceUtilTest.php index 48a4b5932..6d2ba2bd7 100644 --- a/tests/ElasticApmTests/UnitTests/UtilTests/StackTraceUtilTest.php +++ b/tests/ElasticApmTests/UnitTests/UtilTests/StackTraceUtilTest.php @@ -39,6 +39,8 @@ use ElasticApmTests\Util\StackTraceFrameExpectations; use ElasticApmTests\Util\TestCaseBase; +use function count; + class StackTraceUtilTest extends TestCaseBase { private const NUMBER_OF_STACK_FRAMES_TO_SKIP_KEY = 'number_of_stack_frames_to_skip'; @@ -66,6 +68,8 @@ private static function stackTraceUtil(): StackTraceUtil public function testClosureExpections(): void { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $closureFrameExpections = StackTraceFrameExpectations::fromLocationOnly(__FILE__, /* temp dummy lineNumber */ 0); /** * @return StackTraceFrame[] @@ -76,6 +80,7 @@ public function testClosureExpections(): void }; $thisFuncFrameExpections = StackTraceFrameExpectations::fromClosure(__FILE__, __LINE__ + 1, __NAMESPACE__, __CLASS__, /* isStatic */ false); $actualStackTrace = $closure(); + $dbgCtx->add(compact('actualStackTrace')); self::assertCountAtLeast(3, $actualStackTrace); $closureFrameExpections->assertMatches($actualStackTrace[0]); self::assertNull($actualStackTrace[0]->function); @@ -1208,4 +1213,131 @@ public function testLimitConfigToMaxNumberOfFrames(): void self::assertSame(2, StackTraceUtil::convertLimitConfigToMaxNumberOfFrames(2)); self::assertSame(5, StackTraceUtil::convertLimitConfigToMaxNumberOfFrames(5)); } + + private static function assertSameClassicFormatStackTraceFrames(ClassicFormatStackTraceFrame $expected, ClassicFormatStackTraceFrame $actual): void + { + AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); + + self::assertCount(count(get_object_vars($expected)), get_object_vars($actual)); + + $dbgCtx->pushSubScope(); + foreach (get_object_vars($expected) as $propName => $expectedPropVal) { + $dbgCtx->clearCurrentSubScope(compact('propName', 'expectedPropVal')); + self::assertSame($expectedPropVal, $actual->{$propName}); + } + $dbgCtx->popSubScope(); + } + + /** + * @param ClassicFormatStackTraceFrame[] $expected + * @param ClassicFormatStackTraceFrame[] $actual + * + * @return void + */ + private static function assertSameClassicFormatStackTraces(array $expected, array $actual): void + { + AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); + + $dbgCtx->add(compact('expected', 'actual')); + self::assertCount(count($expected), $actual); + + $dbgCtx->pushSubScope(); + foreach (RangeUtil::generateUpTo(count($expected)) as $frameIndex) { + $dbgCtx->clearCurrentSubScope(compact('frameIndex')); + self::assertSameClassicFormatStackTraceFrames($expected[$frameIndex], $actual[$frameIndex]); + } + $dbgCtx->popSubScope(); + } + + /** + * @dataProvider boolDataProvider + */ + public function testConvertCaptureToClassicFormatSleepExample(bool $addDummyLocationToSleepFunc): void + { + $input = [ + [ + 'file' => '/my_work/apm-agent-php/agent/php/ElasticApm/Impl/Util/StackTraceUtil.php', + 'line' => 131, + 'function' => 'captureInClassicFormat', + 'class' => 'Elastic\Apm\Impl\Util\StackTraceUtil', + 'type' => '->', + ], + [ + 'file' => '/my_work/apm-agent-php/agent/php/ElasticApm/Impl/InferredSpansBuilder.php', + 'line' => 98, + 'function' => 'captureInClassicFormatExcludeElasticApm', + 'class' => 'Elastic\Apm\Impl\Util\StackTraceUtil', + 'type' => '->', + ], + [ + 'file' => '/my_work/apm-agent-php/agent/php/ElasticApm/Impl/InferredSpansManager.php', + 'line' => 196, + 'function' => 'captureStackTrace', + 'class' => 'Elastic\Apm\Impl\InferredSpansBuilder', + 'type' => '->', + ], + [ + 'function' => 'handleAutomaticCapturing', + 'class' => 'Elastic\Apm\Impl\InferredSpansManager', + 'type' => '->', + ], + [ + 'file' => '/var/www/html/sleep.php', + 'line' => 6, + 'function' => 'sleep', + ], + [ + 'file' => '/var/www/html/sleep.php', + 'line' => 10, + 'function' => 'coolsleep', + ], + ]; + /** @var ClassicFormatStackTraceFrame[] $expectedOutput */ + $expectedOutput = [ + new ClassicFormatStackTraceFrame( + null /* <- file */, + null /* <- line */, + null /* <- class */, + null /* <- isStaticMethod */, + 'sleep' /* <- function */ + ), + new ClassicFormatStackTraceFrame( + '/var/www/html/sleep.php' /* <- file */, + 6 /* <- line */, + null /* <- class */, + null /* <- isStaticMethod */, + 'coolsleep' /* <- function */ + ), + new ClassicFormatStackTraceFrame( + '/var/www/html/sleep.php' /* <- file */, + 10 /* <- line */ + ), + ]; + + if ($addDummyLocationToSleepFunc) { + $inputFrame =& $input[3]; + self::assertSame('handleAutomaticCapturing', $inputFrame['function']); + self::assertArrayNotHasKey('file', $inputFrame); + self::assertArrayNotHasKey('line', $inputFrame); + $inputFrame['file'] = 'dummy_file_for_internal_func_sleep.php'; + $inputFrame['line'] = 543; + $expectedOutputFrame =& $expectedOutput[0]; + self::assertSame('sleep', $expectedOutputFrame->function); + self::assertNull($expectedOutputFrame->file); + self::assertNull($expectedOutputFrame->line); + $expectedOutputFrame->file = 'dummy_file_for_internal_func_sleep.php'; + $expectedOutputFrame->line = 543; + } + + $actualOutput = self::stackTraceUtil()->convertCaptureToClassicFormat( + $input, + 3 /* <- offset */, + null /* <- maxNumberOfFrames */, + false /* <- keepElasticApmFrames */, + false /* <- includeArgs */, + false /* <- includeThisObj */ + ); + + self::assertSameClassicFormatStackTraces($expectedOutput, $actualOutput); + } }